materia-web-client: repository view
This commit is contained in:
parent
b89e8f3393
commit
577f6f3ddf
13
materia-web-client/src/materia-frontend/src/api/directory.ts
Normal file
13
materia-web-client/src/materia-frontend/src/api/directory.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { api_client, type ResponseError, handle_error } from "@/client";
|
||||||
|
|
||||||
|
export interface DirectoryInfo {
|
||||||
|
id: number,
|
||||||
|
repository_id: number,
|
||||||
|
parent_id?: number,
|
||||||
|
created: number,
|
||||||
|
updated: number,
|
||||||
|
name: string,
|
||||||
|
path?: string,
|
||||||
|
is_public: boolean,
|
||||||
|
used?: number
|
||||||
|
}
|
13
materia-web-client/src/materia-frontend/src/api/file.ts
Normal file
13
materia-web-client/src/materia-frontend/src/api/file.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { api_client, type ResponseError, handle_error } from "@/client";
|
||||||
|
|
||||||
|
export interface FileInfo {
|
||||||
|
id: number,
|
||||||
|
repository_id: number,
|
||||||
|
parent_id?: number,
|
||||||
|
created: number,
|
||||||
|
updated: number,
|
||||||
|
name: string,
|
||||||
|
path?: string,
|
||||||
|
is_public: boolean,
|
||||||
|
size: number
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
export * as auth from "@/api/auth";
|
export * as auth from "@/api/auth";
|
||||||
export * as user from "@/api/user";
|
export * as user from "@/api/user";
|
||||||
export * as repository from "@/api/repository";
|
export * as repository from "@/api/repository";
|
||||||
|
export * as directory from "@/api/directory";
|
||||||
|
export * as file from "@/api/file";
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { api_client, type ResponseError, handle_error } from "@/client";
|
import { api_client, type ResponseError, handle_error } from "@/client";
|
||||||
|
import { file, directory } from "@/api"
|
||||||
|
|
||||||
export interface RepositoryInfo {
|
export interface RepositoryInfo {
|
||||||
id: number,
|
id: number,
|
||||||
@ -6,6 +7,11 @@ export interface RepositoryInfo {
|
|||||||
used?: number
|
used?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RepositoryContent {
|
||||||
|
files: file.FileInfo[],
|
||||||
|
directories: directory.DirectoryInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
export async function info(): Promise<RepositoryInfo | ResponseError> {
|
export async function info(): Promise<RepositoryInfo | ResponseError> {
|
||||||
return await api_client.get("/repository")
|
return await api_client.get("/repository")
|
||||||
.then(async response => { return Promise.resolve<RepositoryInfo>(response.data); })
|
.then(async response => { return Promise.resolve<RepositoryInfo>(response.data); })
|
||||||
@ -21,3 +27,9 @@ export async function remove(): Promise<null | ResponseError> {
|
|||||||
return await api_client.delete("/repository")
|
return await api_client.delete("/repository")
|
||||||
.catch(handle_error);
|
.catch(handle_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function content(): Promise<RepositoryContent | ResponseError> {
|
||||||
|
return await api_client.get("/repository/content")
|
||||||
|
.then(async response => { return Promise.resolve<RepositoryContent>(response.data); })
|
||||||
|
.catch(handle_error);
|
||||||
|
}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fixed h-1/3 z-50 context-menu" :style="{ top: y + 'px', left: x + 'px' }">
|
||||||
|
<div v-for="action in actions" :key="action.action" @click="emitAction(action.action)">
|
||||||
|
{{ action.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
const { actions, x, y } = defineProps(['actions', 'x', 'y']);
|
||||||
|
const emit = defineEmits(['action-clicked']);
|
||||||
|
|
||||||
|
const emitAction = (action) => {
|
||||||
|
emit('action-clicked', action);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.context-menu {
|
||||||
|
position: absolute;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu div {
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu div:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,9 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fixed z-50 cursor-pointer" :style="{ top: pos_y + 'px', left: pos_x + 'px' }">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<svg aria-hidden="true" focusable="false" role="img" class="icon-directory" viewBox="0 0 16 16" width="16"
|
||||||
|
height="16" fill="currentColor"
|
||||||
|
style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;">
|
||||||
|
<path
|
||||||
|
d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<svg aria-hidden="true" focusable="false" role="img" class="icon-directory" viewBox="0 0 16 16" width="16"
|
||||||
|
height="16" fill="currentColor"
|
||||||
|
style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;">
|
||||||
|
<path
|
||||||
|
d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
@ -1,6 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Base from "@/views/Base.vue";
|
import Base from "@/views/Base.vue";
|
||||||
import Error from "@/components/Error.vue";
|
import Error from "@/components/Error.vue";
|
||||||
|
import IconDirectory from "@/components/icons/IconDirectory.vue";
|
||||||
|
import IconFile from "@/components/icons/IconFile.vue";
|
||||||
|
import ContextMenu from "@/components/ContextMenu.vue";
|
||||||
|
|
||||||
import { ref, onMounted, watch } from "vue";
|
import { ref, onMounted, watch } from "vue";
|
||||||
import { onBeforeRouteUpdate, useRoute } from "vue-router"
|
import { onBeforeRouteUpdate, useRoute } from "vue-router"
|
||||||
@ -15,6 +18,7 @@ const error = ref<string>(null);
|
|||||||
|
|
||||||
const repository_info = ref(null);
|
const repository_info = ref(null);
|
||||||
const is_created = ref(null);
|
const is_created = ref(null);
|
||||||
|
const repository_content = ref(null);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await repository.info()
|
await repository.info()
|
||||||
@ -25,6 +29,16 @@ onMounted(async () => {
|
|||||||
.catch(err => {
|
.catch(err => {
|
||||||
is_created.value = false;
|
is_created.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (is_created.value) {
|
||||||
|
await repository.content()
|
||||||
|
.then(async _repository_content => {
|
||||||
|
repository_content.value = _repository_content;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
error.value = err;
|
||||||
|
})
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function create_repository() {
|
async function create_repository() {
|
||||||
@ -57,6 +71,42 @@ function size_procent() {
|
|||||||
return Math.round(repository_info.value.used / repository_info.value.capacity) * 100;
|
return Math.round(repository_info.value.used / repository_info.value.capacity) * 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function format_creation_time(timestamp: number): string {
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
|
||||||
|
return `${date.getDate()}-${date.getMonth()}-${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showMenu = ref(false);
|
||||||
|
const ctxMenuPosX = ref(0);
|
||||||
|
const ctxMenuPosY = ref(0);
|
||||||
|
const targetRow = ref({});
|
||||||
|
const contextMenuActions = ref([
|
||||||
|
{ label: 'Edit', action: 'edit' },
|
||||||
|
{ label: 'Delete', action: 'delete' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const showContextMenu = (event, user) => {
|
||||||
|
event.preventDefault();
|
||||||
|
showMenu.value = true;
|
||||||
|
targetRow.value = user;
|
||||||
|
ctxMenuPosX.value = event.clientX;
|
||||||
|
ctxMenuPosY.value = event.clientY;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeContextMenu = () => {
|
||||||
|
showMenu.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleActionClick(action) {
|
||||||
|
console.log(action);
|
||||||
|
console.log(targetRow.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function close_menu() {
|
||||||
|
showMenu.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -67,9 +117,39 @@ function size_procent() {
|
|||||||
<div class="w-full rounded-full h-2.5 bg-ctp-surface0">
|
<div class="w-full rounded-full h-2.5 bg-ctp-surface0">
|
||||||
<div class="bg-ctp-lavender h-2.5 rounded-full" :style="{ width: size_procent() + '%' }"></div>
|
<div class="bg-ctp-lavender h-2.5 rounded-full" :style="{ width: size_procent() + '%' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="min-w-32 text-center">{{ round_size(repository_info.used, "GB") }} / {{
|
<span class="min-w-48 text-center">{{ round_size(repository_info.used, "MB").toFixed(2) }} MB / {{
|
||||||
round_size(repository_info.capacity, "GB") }} GB</span>
|
round_size(repository_info.capacity, "GB") }} GB</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<table v-if="repository_content" class="table-auto w-full mt-8 mb-8 pl-8 pr-8 text-ctp-text">
|
||||||
|
<tbody>
|
||||||
|
<tr class="hover:bg-ctp-surface0" v-for="directory in repository_content.directories"
|
||||||
|
@contextmenu.prevent="showContextMenu($event, directory)">
|
||||||
|
<td>
|
||||||
|
<IconDirectory />
|
||||||
|
<div class="inline ml-4">{{ directory.name }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">-</td>
|
||||||
|
<td class="text-right w-48">
|
||||||
|
{{ format_creation_time(directory.created) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="hover:bg-ctp-surface0" v-for="file in repository_content.files">
|
||||||
|
<td>
|
||||||
|
<IconFile />
|
||||||
|
<div class="inline ml-4">{{ file.name }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">{{ round_size(file.size, "MB").toFixed(2) }} MB</td>
|
||||||
|
<td class="text-right w-48">
|
||||||
|
{{ format_creation_time(file.created) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<ContextMenu v-if="showMenu" :actions="contextMenuActions" @action-clicked="handleActionClick" :x="ctxMenuPosX"
|
||||||
|
:y="ctxMenuPosY" v-click-outside="close_menu" />
|
||||||
</section>
|
</section>
|
||||||
<section v-else>
|
<section v-else>
|
||||||
<p>It looks like you don't have a repository yet...</p>
|
<p>It looks like you don't have a repository yet...</p>
|
||||||
|
Loading…
Reference in New Issue
Block a user