feat: initial database ui
This commit is contained in:
162
client/components/DatabaseDetail.vue
Normal file
162
client/components/DatabaseDetail.vue
Normal file
@ -0,0 +1,162 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const documents = computedAsync(async () => {
|
||||
return await rpc.listDocuments(props.collection)
|
||||
})
|
||||
|
||||
const fields = computed(() => {
|
||||
if (documents.value && documents.value.length > 0)
|
||||
return Object.keys(documents.value[0])
|
||||
})
|
||||
|
||||
const editing = ref(false)
|
||||
const selectedDocument = ref()
|
||||
|
||||
function editDocument(document: any) {
|
||||
if (editing.value)
|
||||
return
|
||||
editing.value = true
|
||||
selectedDocument.value = { ...document }
|
||||
}
|
||||
|
||||
async function saveDocument() {
|
||||
// TODO: validate & show errors
|
||||
await rpc.updateDocument(props.collection, selectedDocument.value)
|
||||
editing.value = false
|
||||
selectedDocument.value = undefined
|
||||
documents.value = await rpc.listDocuments(props.collection)
|
||||
}
|
||||
|
||||
function discardEditing() {
|
||||
editing.value = false
|
||||
selectedDocument.value = null
|
||||
}
|
||||
|
||||
async function deleteDocument(document: any) {
|
||||
rpc.deleteDocument(props.collection, document._id)
|
||||
documents.value = await rpc.listDocuments(props.collection)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div w-full h-full p4>
|
||||
<template v-if="fields?.length">
|
||||
<div flex justify-between>
|
||||
<div flex-auto />
|
||||
<NButton icon="carbon:add" n="green" @click="drawer = !drawer">
|
||||
Add Document
|
||||
</NButton>
|
||||
</div>
|
||||
<table w-full mt4 :class="{ 'editing-mode': editing }">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="field of fields" :key="field" text-start>
|
||||
{{ field }}
|
||||
</th>
|
||||
<th text-center>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="document in documents" :key="document._id" :class="{ isEditing: editing && selectedDocument._id === document._id }">
|
||||
<td v-for="field of fields" :key="field" hover-bg-green hover-bg-opacity-5 hover-text-green cursor-pointer @dblclick="editDocument(document)">
|
||||
<template v-if="editing && selectedDocument._id === document._id">
|
||||
<input v-model="selectedDocument[field]">
|
||||
</template>
|
||||
<span v-else>
|
||||
{{ document[field] }}
|
||||
</span>
|
||||
</td>
|
||||
<td flex justify-center gap2 class="actions">
|
||||
<template v-if="editing && selectedDocument._id === document._id">
|
||||
<NIconButton icon="carbon-save" @click="saveDocument" />
|
||||
<NIconButton icon="carbon-close" @click="discardEditing" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<NIconButton icon="carbon-edit" @click="editDocument(document)" />
|
||||
<NIconButton icon="carbon-delete" @click="deleteDocument(document)" />
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<div v-else flex justify-center items-center h-full text-2xl>
|
||||
<NIcon icon="carbon-document" mr1 />
|
||||
No documents found
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.actions .n-icon {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
tr {
|
||||
width: 100%;
|
||||
td, th {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
width: 100%;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
border-right: 1px solid #272727;
|
||||
border-left: 1px solid #272727;
|
||||
border-top: 1px solid #272727;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid #272727;
|
||||
padding: 5px 10px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.editing-mode {
|
||||
tr {
|
||||
&:not(.isEditing) {
|
||||
opacity: 0.3;
|
||||
position: relative;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
&.isEditing {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
55
client/pages/index.vue
Normal file
55
client/pages/index.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<script lang="ts" setup>
|
||||
const route = useRoute()
|
||||
|
||||
const selectedCollection = ref()
|
||||
// TODO: check connection
|
||||
const connected = ref(true)
|
||||
const drawer = ref(false)
|
||||
const search = ref('')
|
||||
|
||||
const collections = computedAsync(async () => {
|
||||
return await rpc.listCollections()
|
||||
})
|
||||
|
||||
const filtered = computed(() => {
|
||||
if (!search.value)
|
||||
return collections.value
|
||||
return collections.value.filter((c: any) => c.name.toLowerCase().includes(search.value.toLowerCase()))
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.table)
|
||||
selectedCollection.value = route.query.table
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div h-full>
|
||||
<PanelLeftRight :min-left="13" :max-left="20">
|
||||
<template #left>
|
||||
<div py1 px2 border="r base">
|
||||
<div flex items-center p2>
|
||||
<!-- TODO: -->
|
||||
<NIconButton w-full mb2 icon="carbon-reset" title="Refresh" />
|
||||
<NIconButton w-full mb2 icon="carbon-data-base" title="Connection Name" :class="connected ? 'text-green-5' : 'text-orange-5'" />
|
||||
<NIconButton w-full mb2 icon="carbon-add" title="Create Table" @click="drawer = !drawer" />
|
||||
</div>
|
||||
<NTextInput v-model="search" w-full p2 mb2 :placeholder="`${collections?.length ?? '-'} collection in total`" icon="carbon-search" />
|
||||
<div grid gird-cols-1 my2 mx1>
|
||||
<NuxtLink v-for="table in filtered" :key="table.name" :to="{ name: 'index', query: { table: table.name } }" flex justify-between p2 my1 hover-bg-green hover-bg-opacity-5 hover-text-green rounded-lg :class="{ 'bg-green bg-opacity-5 text-green': selectedCollection === table.name }" @click="selectedCollection = table.name">
|
||||
<span>
|
||||
<NIcon icon="carbon-db2-database" />
|
||||
{{ table.name }}
|
||||
</span>
|
||||
<!-- TODO: -->
|
||||
<NIconButton icon="carbon-overflow-menu-horizontal" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #right>
|
||||
<DatabaseDetail v-if="selectedCollection" :collection="selectedCollection" />
|
||||
</template>
|
||||
</PanelLeftRight>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user