Browse Source

Merge pull request #2841 from nocodb/feat/gui-v2-meta-data

feat(gui-v2): Project metadata
pull/2888/head
navi 2 years ago committed by GitHub
parent
commit
88d0e99933
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 112
      packages/nc-gui-v2/components/dashboard/settings/Metadata.vue
  2. 6
      packages/nc-gui-v2/components/dashboard/settings/SettingsModal.vue
  3. 140
      packages/nc-gui-v2/components/dashboard/settings/UIAcl.vue

112
packages/nc-gui-v2/components/dashboard/settings/Metadata.vue

@ -0,0 +1,112 @@
<script setup lang="ts">
import { useToast } from 'vue-toastification'
import { h, useNuxtApp, useProject } from '#imports'
import MdiReload from '~icons/mdi/reload'
import MdiDatabaseSync from '~icons/mdi/database-sync'
const { $api } = useNuxtApp()
const { project } = useProject()
const toast = useToast()
let isLoading = $ref(false)
let isDifferent = $ref(false)
let metadiff = $ref<any[]>([])
async function loadMetaDiff() {
try {
if (!project.value?.id) return
isLoading = true
isDifferent = false
metadiff = await $api.project.metaDiffGet(project.value?.id)
for (const model of metadiff) {
if (model.detectedChanges?.length > 0) {
model.syncState = model.detectedChanges.map((el: any) => el?.msg).join(', ')
isDifferent = true
}
}
} catch (e) {
console.error(e)
} finally {
isLoading = false
}
}
async function syncMetaDiff() {
try {
if (!project.value?.id || !isDifferent) return
isLoading = true
await $api.project.metaDiffSync(project.value.id)
toast.info(`Table metadata recreated successfully`)
await loadMetaDiff()
} catch (e: any) {
if (e.response?.status === 402) {
toast.info(e.message)
} else {
toast.error(e.message)
}
} finally {
isLoading = false
}
}
onMounted(async () => {
if (metadiff.length === 0) {
await loadMetaDiff()
}
})
const tableHeaderRenderer = (label: string) => () => h('div', { class: 'text-gray-500' }, label)
const columns = [
{
title: tableHeaderRenderer('Models'),
dataIndex: 'title',
key: 'title',
},
{
title: tableHeaderRenderer('Sync State'),
dataIndex: 'syncState',
key: 'syncState',
customRender: (value: { text: string }) =>
h('div', { style: { color: value.text ? 'red' : 'gray' } }, value.text || 'No change identified'),
},
]
</script>
<template>
<div class="flex flex-row w-full">
<div class="flex flex-column w-3/5">
<div class="flex flex-row justify-end items-center w-full mb-4">
<a-button class="self-start" @click="loadMetaDiff">
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" />
Reload
</div>
</a-button>
</div>
<a-table
class="w-full"
:data-source="metadiff ?? []"
:columns="columns"
:pagination="false"
:loading="isLoading"
bordered
/>
</div>
<div class="flex place-content-center w-2/5">
<div v-if="isDifferent">
<a-button v-t="['a:proj-meta:meta-data:sync']" type="primary" @click="syncMetaDiff">
<div class="flex items-center gap-2">
<MdiDatabaseSync />
Sync Now
</div>
</a-button>
</div>
<div v-else>
<span><a-alert message="Tables metadata is in sync" type="success" show-icon /></span>
</div>
</div>
</div>
</template>

6
packages/nc-gui-v2/components/dashboard/settings/SettingsModal.vue

@ -2,6 +2,8 @@
import type { FunctionalComponent, SVGAttributes } from 'vue' import type { FunctionalComponent, SVGAttributes } from 'vue'
import AuditTab from './AuditTab.vue' import AuditTab from './AuditTab.vue'
import AppStore from './AppStore.vue' import AppStore from './AppStore.vue'
import Metadata from './Metadata.vue'
import UIAcl from './UIAcl.vue'
import StoreFrontOutline from '~icons/mdi/storefront-outline' import StoreFrontOutline from '~icons/mdi/storefront-outline'
import TeamFillIcon from '~icons/ri/team-fill' import TeamFillIcon from '~icons/ri/team-fill'
import MultipleTableIcon from '~icons/mdi/table-multiple' import MultipleTableIcon from '~icons/mdi/table-multiple'
@ -61,11 +63,11 @@ const tabsInfo: TabGroup = {
subTabs: { subTabs: {
metaData: { metaData: {
title: 'Metadata', title: 'Metadata',
body: () => AuditTab, body: () => Metadata,
}, },
acl: { acl: {
title: 'UI Access Control', title: 'UI Access Control',
body: () => AuditTab, body: () => UIAcl,
}, },
}, },
}, },

140
packages/nc-gui-v2/components/dashboard/settings/UIAcl.vue

@ -0,0 +1,140 @@
<script setup lang="ts">
import { useToast } from 'vue-toastification'
import { viewIcons } from '~/utils/viewUtils'
import { h, useNuxtApp, useProject } from '#imports'
import MdiReload from '~icons/mdi/reload'
import MdiContentSave from '~icons/mdi/content-save'
import MdiMagnify from '~icons/mdi/magnify'
const { $api, $e } = useNuxtApp()
const { project } = useProject()
const toast = useToast()
const roles = $ref<string[]>(['editor', 'commenter', 'viewer'])
let isLoading = $ref(false)
let tables = $ref<any[]>([])
let searchInput = $ref('')
const filteredTables = computed(() =>
tables.filter(
(el) =>
(typeof el?._ptn === 'string' && el._ptn.toLowerCase().includes(searchInput.toLowerCase())) ||
(typeof el?.title === 'string' && el.title.toLowerCase().includes(searchInput.toLowerCase())),
),
)
async function loadTableList() {
try {
if (!project.value?.id) return
isLoading = true
// TODO includeM2M
tables = await $api.project.modelVisibilityList(project.value?.id, {
includeM2M: '',
})
} catch (e) {
console.error(e)
} finally {
isLoading = false
}
}
async function saveUIAcl() {
try {
if (!project.value?.id) return
await $api.project.modelVisibilitySet(
project.value.id,
tables.filter((t) => t.edited),
)
toast.success('Updated UI ACL for tables successfully')
} catch (e: any) {
toast.error(e?.message)
}
$e('a:proj-meta:ui-acl')
}
const onRoleCheck = (record: any, role: string) => {
record.disabled[role] = !record.disabled[role]
record.edited = true
}
onMounted(async () => {
if (tables.length === 0) {
await loadTableList()
}
})
const tableHeaderRenderer = (label: string) => () => h('div', { class: 'text-gray-500' }, label)
const columns = [
{
title: tableHeaderRenderer('Table name'),
name: 'table_name',
},
{
title: tableHeaderRenderer('View name'),
name: 'view_name',
},
{
title: tableHeaderRenderer('Editor'),
name: 'editor',
width: 150,
},
{
title: tableHeaderRenderer('Commenter'),
name: 'commenter',
width: 150,
},
{
title: tableHeaderRenderer('Viewer'),
name: 'viewer',
width: 150,
},
]
</script>
<template>
<div class="flex flex-row w-full">
<div class="flex flex-column w-full">
<div class="flex flex-row items-center w-full mb-4 gap-2">
<a-input v-model:value="searchInput" placeholder="Search models">
<template #prefix>
<MdiMagnify />
</template>
</a-input>
<a-button class="self-start" @click="loadTableList">
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" />
Reload
</div>
</a-button>
<a-button class="self-start" @click="saveUIAcl">
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiContentSave />
Save
</div>
</a-button>
</div>
<a-table class="w-full" :data-source="filteredTables" :columns="columns" :pagination="false" :loading="isLoading" bordered>
<template #bodyCell="{ record, column }">
<div v-if="column.name === 'table_name'">{{ record._ptn }}</div>
<div v-if="column.name === 'view_name'">
<div class="flex align-center">
<component :is="viewIcons[record.type].icon" :class="`text-${viewIcons[record.type].color} mr-1`" />
{{ record.title }}
</div>
</div>
<div v-for="role in roles" :key="role">
<div v-if="column.name === role">
<a-tooltip>
<template #title>Click to hide '{{ record.title }}' for role:{{ role }} in UI dashboard</template>
<a-checkbox :checked="!record.disabled[role]" @change="onRoleCheck(record, role)"></a-checkbox>
</a-tooltip>
</div>
</div>
</template>
</a-table>
</div>
</div>
</template>
Loading…
Cancel
Save