Browse Source

Merge pull request #2794 from nocodb/feat/nav-drawer

Feat/nav drawer
pull/2831/head
Pranav C 2 years ago committed by GitHub
parent
commit
0c7e07a737
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 307
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  2. 121
      packages/nc-gui-v2/components/dlg/TableRename.vue
  3. 21
      packages/nc-gui-v2/composables/useMetas.ts
  4. 25
      packages/nc-gui-v2/composables/useTabs.ts
  5. 8
      packages/nc-gui-v2/lib/enums.ts
  6. 40
      packages/nc-gui-v2/lib/types.ts
  7. 28371
      packages/nc-gui-v2/package-lock.json
  8. 2
      packages/nc-gui-v2/package.json
  9. 69
      packages/nc-gui-v2/pages/index/index.vue
  10. 3
      packages/nc-gui-v2/pages/nc/[projectId].vue
  11. 14
      packages/nc-gui-v2/pages/project/index/create-external.vue
  12. 4
      packages/nc-gui-v2/pages/signin.vue
  13. 6
      packages/nc-gui-v2/pages/signup.vue
  14. 118
      packages/nc-gui-v2/utils/projectCreateUtils.ts
  15. 5
      packages/nc-gui-v2/utils/validation.ts

307
packages/nc-gui-v2/components/dashboard/TreeView.vue

@ -1,31 +1,298 @@
<script setup lang="ts">
import { useProject, useTabs } from '#imports'
import { computed } from '@vue/reactivity'
import { Modal } from 'ant-design-vue'
import { UITypes } from 'nocodb-sdk'
import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs'
import { useToast } from 'vue-toastification'
import { watchEffect } from '#imports'
import { useNuxtApp, useRoute } from '#app'
import useProject from '~/composables/useProject'
import useTabs from '~/composables/useTabs'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import MdiSettingIcon from '~icons/mdi/cog'
import MdiTable from '~icons/mdi/table'
import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large'
import MdiMenuDown from '~icons/mdi/chevron-down'
import MdiPlus from '~icons/mdi/plus-circle-outline'
import MdiDrag from '~icons/mdi/drag-vertical'
import MdiMenuIcon from '~icons/mdi/dots-vertical'
import MdiAPIDocIcon from '~icons/mdi/open-in-new'
const { tables } = useProject()
const { addTab } = useTabs()
const toast = useToast()
const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const route = useRoute()
const { tables, loadTables } = useProject(route.params.projectId as string)
const { closeTab } = useTabs()
const tablesById = $computed<Record<string, TableType>>(() =>
tables?.value?.reduce((acc: Record<string, TableType>, table: TableType) => {
acc[table.id as string] = table
return acc
}, {}),
)
const settingsDlg = ref(false)
const showTableList = ref(true)
const tableCreateDlg = ref(false)
const tableDeleteDlg = ref(false)
const menuRef = $ref<HTMLLIElement>()
let key = $ref(0)
let sortable: Sortable
// todo: replace with vuedraggable
const initSortable = (el: Element) => {
if (sortable) sortable.destroy()
sortable = Sortable.create(el as HTMLLIElement, {
handle: '.nc-drag-icon',
onEnd: async (evt) => {
const { newIndex = 0, oldIndex = 0 } = evt
const itemEl = evt.item as HTMLLIElement
const item = tablesById[itemEl.dataset.id as string]
// get the html collection of all list items
const children: HTMLCollection = evt.to.children
// get items before and after the moved item
const itemBeforeEl = children[newIndex - 1] as HTMLLIElement
const itemAfterEl = children[newIndex + 1] as HTMLLIElement
// get items meta of before and after the moved item
const itemBefore = itemBeforeEl && tablesById[itemBeforeEl.dataset.id as string]
const itemAfter = itemAfterEl && tablesById[itemAfterEl.dataset.id as string]
// set new order value based on the new order of the items
if (children.length - 1 === evt.newIndex) {
item.order = (itemBefore.order as number) + 1
} else if (newIndex === 0) {
item.order = (itemAfter.order as number) / 2
} else {
item.order = ((itemBefore.order as number) + (itemAfter.order as number)) / 2
}
// update the order of the moved item
tables.value?.splice(newIndex, 0, ...tables.value?.splice(oldIndex, 1))
// force re-render the list
key++
// update the item order
await $api.dbTable.reorder(item.id as string, {
order: item.order as any,
})
},
animation: 150,
})
}
watchEffect(() => {
if (menuRef) {
initSortable(menuRef)
}
})
const icon = (table: TableType) => {
if (table.type === 'table') {
return MdiTableLarge
}
if (table.type === 'view') {
return MdiView
}
}
const apiLink = computed(
() =>
// new URL(
`/api/v1/db/meta/projects/${route.params.projectId}/swagger`,
// todo: get siteUrl
// this.$store.state.project.appInfo && this.$store.state.project.appInfo.ncSiteUrl
// ),
)
const filterQuery = $ref('')
const filteredTables = $computed(() => {
return tables?.value?.filter((table) => !filterQuery || table?.title.toLowerCase()?.includes(filterQuery.toLowerCase()))
})
const contextMenuTarget = reactive<{ type?: 'table' | 'main'; value?: any }>({})
const setMenuContext = (type: 'table' | 'main', value?: any) => {
contextMenuTarget.type = type
contextMenuTarget.value = value
$e('c:table:create:navdraw:right-click')
}
const deleteTable = (table: TableType) => {
$e('c:table:delete')
// 'Click Submit to Delete The table'
Modal.confirm({
title: `Click Yes to Delete The table : ${table.title}`,
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
async onOk() {
const { getMeta, removeMeta } = useMetas()
try {
const meta = (await getMeta(table.id as string)) as TableType
const relationColumns = meta?.columns?.filter((c) => c.uidt === UITypes.LinkToAnotherRecord)
if (relationColumns?.length) {
const refColMsgs = await Promise.all(
relationColumns.map(async (c, i) => {
const refMeta = (await getMeta(
(c?.colOptions as LinkToAnotherRecordType)?.fk_related_model_id as string,
)) as TableType
return `${i + 1}. ${c.title} is a LinkToAnotherRecord of ${(refMeta && refMeta.title) || c.title}`
}),
)
toast.info(
h('div', {
innerHTML: `<div style="padding:10px 4px">Unable to delete tables because of the following.
<br><br>${refColMsgs.join('<br>')}<br><br>
Delete them & try again</div>`,
}),
)
return
}
await $api.dbTable.delete(table?.id as string)
closeTab({
type: 'table',
id: table.id,
title: table.title,
})
await loadTables()
removeMeta(table.id as string)
toast.info(`Deleted table ${table.title} successfully`)
$e('a:table:delete')
} catch (e: any) {
toast.error(await extractSdkResponseErrorMsg(e))
}
},
})
}
const renameTableDlg = ref(false)
const renameTableMeta = ref()
const showRenameTableDlg = (table: TableType, rightClick = false) => {
$e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options')
renameTableMeta.value = table
renameTableDlg.value = true
}
const reloadTables = async () => {
$e('a:table:refresh:navdraw')
await loadTables()
}
const addTableTab = (table: TableType) => {
$e('a:table:open')
addTab({ title: table.title, id: table.id })
}
</script>
<template>
<div class="nc-treeview-container flex flex-column">
<a-menu class="flex-1 overflow-y-auto">
<a-menu-item
v-for="table in tables"
:key="table.id"
class="p-2 text-sm pointer"
@click="addTab({ type: 'table', title: table.title, id: table.id })"
>
{{ table.title }}
</a-menu-item>
</a-menu>
<div class="cursor-pointer nc-team-settings pa-4 flex align-center hover:bg-gray-200/20" @click="settingsDlg = true">
<div class="p-1">
<a-input-search
v-model:value="filterQuery"
size="small"
class="nc-filter-input"
:placeholder="$t('placeholder.searchProjectTree')"
/>
</div>
<a-dropdown :trigger="['contextmenu']">
<div class="p-1 flex-1 overflow-y-auto flex flex-column">
<div
class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer"
@click="showTableList = !showTableList"
@contextmenu="setMenuContext('main')"
>
<MdiTable class="mr-1 text-gray-500" />
<span class="flex-grow text-bold"
>{{ $t('objects.tables') }} <template v-if="tables?.length">({{ tables.length }})</template></span
>
<MdiPlus v-t="['c:table:create:navdraw']" class="text-gray-500" @click.stop="tableCreateDlg = true" />
<MdiMenuDown
class="transition-transform !duration-100 text-gray-500"
:class="{ 'transform rotate-180': showTableList }"
/>
</div>
<div class="flex-1">
<div class="transition-height duration-200 overflow-hidden" :class="{ 'h-100': showTableList, 'h-0': !showTableList }">
<div :key="key" ref="menuRef" class="border-none sortable-list">
<div
v-for="table in tables"
:key="table.id"
v-t="['a:table:open']"
:class="{ hidden: !filteredTables?.includes(table) }"
class="!pl-1 py-1 !h-[28px] !my-0 text-sm pointer group"
:data-order="table.order"
:data-id="table.id"
@click="addTableTab(table)"
>
<div class="flex align-center gap-1 h-full" @contextmenu="setMenuContext('table', table)">
<MdiDrag class="transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 nc-drag-icon cursor-move" />
<component :is="icon(table)" class="text-[10px] text-gray-500" />
<span class="text-xs flex-1 ml-2">{{ table.title }}</span>
<a-dropdown :trigger="['click']" @click.stop>
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay>
<a-menu class="cursor-pointer">
<a-menu-item class="!text-xs" @click="showRenameTableDlg(table)"> Rename </a-menu-item>
<a-menu-item class="!text-xs" @click="deleteTable(table)"> Delete</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
</div>
</div>
</div>
</div>
<template #overlay>
<a-menu class="cursor-pointer">
<template v-if="contextMenuTarget.type === 'table'">
<a-menu-item class="!text-xs" @click="showRenameTableDlg(contextMenuTarget.value)">
{{ $t('general.rename') }}
</a-menu-item>
<a-menu-item class="!text-xs" @click="deleteTable(contextMenuTarget.value)">
{{ $t('general.delete') }}
</a-menu-item>
</template>
<template v-else>
<a-menu-item class="!text-xs" @click="reloadTables">
{{ $t('general.reload') }}
</a-menu-item>
</template>
</a-menu>
</template>
</a-dropdown>
<div class="w-full h-[1px] bg-gray-200" />
<a v-if="isUIAllowed('apiDocs')" v-t="['e:api-docs']" class="nc-treeview-footer-item" :href="apiLink" target="_blank">
<MdiAPIDocIcon class="mr-2" />
<span> {{ $t('title.apiDocs') }}</span>
</a>
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
class="nc-treeview-footer-item nc-team-settings"
@click="settingsDlg = true"
>
<MdiSettingIcon class="mr-2" />
<span> {{ $t('title.teamAndSettings') }}</span>
</div>
<a-modal v-model:visible="settingsDlg" width="max(90vw, 600px)"> Team and settings </a-modal>
<a-modal v-model:visible="settingsDlg" width="max(90vw, 600px)"> Team and settings</a-modal>
<DlgTableCreate v-model="tableCreateDlg" />
<DlgTableRename v-if="renameTableMeta" v-model="renameTableDlg" :table-meta="renameTableMeta" />
</div>
</template>
@ -37,4 +304,16 @@ const settingsDlg = ref(false)
.nc-treeview-container {
height: calc(100vh - var(--header-height));
}
.nc-treeview-footer-item {
@apply cursor-pointer px-4 py-2 flex align-center hover:bg-gray-200/20 text-xs text-current;
}
:deep(.nc-filter-input input::placeholder) {
@apply !text-xs;
}
:deep(.ant-dropdown-menu-title-content) {
@apply !p-2;
}
</style>

121
packages/nc-gui-v2/components/dlg/TableRename.vue

@ -0,0 +1,121 @@
<script setup lang="ts">
import { watchEffect } from '@vue/runtime-core'
import { Form } from 'ant-design-vue'
import type { TableType } from 'nocodb-sdk'
import { useToast } from 'vue-toastification'
import { useProject, useTableCreate, useTabs } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { validateTableName } from '~/utils/validation'
import { useNuxtApp } from '#app'
interface Props {
modelValue?: boolean
tableMeta: TableType
}
const { modelValue = false, tableMeta } = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'updated'])
const { $e, $api } = useNuxtApp()
const toast = useToast()
const dialogShow = computed({
get() {
return modelValue
},
set(v) {
emit('update:modelValue', v)
},
})
const { updateTab } = useTabs()
const { loadTables } = useProject()
const { project, tables } = useProject()
const prefix = computed(() => project?.value?.prefix || '')
const inputEl = $ref<any>()
let loading = $ref(false)
const useForm = Form.useForm
const formState = reactive({
title: '',
})
const validators = computed(() => {
return {
title: [
validateTableName,
{
validator: (rule: any, value: any, callback: (errMsg?: string) => void) => {
if (/^\s+|\s+$/.test(value)) {
callback('Leading or trailing whitespace not allowed in table name')
}
if (
!(tables?.value || []).every(
(t) => t.id === tableMeta.id || t.table_name.toLowerCase() !== (value || '').toLowerCase(),
)
) {
callback('Duplicate table alias')
}
callback()
},
},
],
}
})
const { resetFields, validate, validateInfos } = useForm(formState, validators)
watchEffect(() => {
if (tableMeta?.title) formState.title = tableMeta?.title
// todo: replace setTimeout and follow better approach
nextTick(() => {
const input = inputEl?.$el
input.setSelectionRange(0, formState.title.length)
input.focus()
})
})
const renameTable = async () => {
loading = true
try {
await $api.dbTable.update(tableMeta?.id as string, {
title: formState.title,
})
dialogShow.value = false
loadTables()
updateTab({ id: tableMeta?.id }, { title: formState.title })
toast.success('Table renamed successfully')
$e('a:table:rename')
dialogShow.value = false
} catch (e) {
toast.error(await extractSdkResponseErrorMsg(e))
}
loading = false
}
</script>
<template>
<a-modal
v-model:visible="dialogShow"
:title="$t('activity.renameTable')"
@keydown.esc="dialogShow = false"
@finish="renameTable"
>
<template #footer>
<a-button key="back" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" type="primary" :loading="loading" @click="renameTable">{{ $t('general.submit') }}</a-button>
</template>
<div class="pl-10 pr-10 pt-5">
<a-form :model="formState" name="create-new-table-form">
<!-- hint="Enter table name" -->
<div class="mb-2">{{ $t('msg.info.enterTableName') }}</div>
<a-form-item v-bind="validateInfos.title">
<a-input
ref="inputEl"
v-model:value="formState.title"
hide-details
:placeholder="$t('msg.info.enterTableName')"
@keydown.enter="renameTable"
/>
</a-form-item>
</a-form>
</div>
</a-modal>
</template>

21
packages/nc-gui-v2/composables/useMetas.ts

@ -1,4 +1,4 @@
import type { TableType } from 'nocodb-sdk'
import type { TableInfoType, TableType } from 'nocodb-sdk'
import { useNuxtApp, useState } from '#app'
import { useProject } from '#imports'
@ -8,13 +8,13 @@ export default () => {
const metas = useState<{ [idOrTitle: string]: TableType | any }>('metas', () => ({}))
const getMeta = async (tableIdOrTitle: string, force = false) => {
if (!force && metas[tableIdOrTitle as keyof typeof metas]) return metas[tableIdOrTitle as keyof typeof metas]
const getMeta = async (tableIdOrTitle: string, force = false): Promise<TableType | TableInfoType | null> => {
if (!force && metas.value[tableIdOrTitle as string]) return metas.value[tableIdOrTitle as string]
const modelId = (tables.value.find((t) => t.title === tableIdOrTitle || t.id === tableIdOrTitle) || {}).id
if (!modelId) {
console.warn(`Table '${tableIdOrTitle}' is not found in the table list`)
return
return null
}
const model = await $api.dbTable.read(modelId)
@ -28,5 +28,16 @@ export default () => {
return model
}
return { getMeta, metas }
const clearAllMeta = () => {
metas.value = {}
}
const removeMeta = (idOrTitle: string) => {
const meta = metas.value[idOrTitle]
if (meta) {
delete metas.value[meta.id]
delete metas.value[meta.title]
}
}
return { getMeta, clearAllMeta, metas, removeMeta }
}

25
packages/nc-gui-v2/composables/useTabs.ts

@ -6,6 +6,13 @@ export interface TabItem {
id?: string
}
function getPredicate(key: Partial<TabItem>) {
return (tab: TabItem) =>
(!('id' in key) || tab.id === key.id) &&
(!('title' in key) || tab.title === key.id) &&
(!('type' in key) || tab.type === key.id)
}
export default () => {
const tabs = useState<TabItem[]>('tabs', () => [])
const activeTab = useState<number>('activeTab', () => 0)
@ -25,9 +32,21 @@ export default () => {
const clearTabs = () => {
tabs.value = []
}
const closeTab = (index: number) => {
tabs.value.splice(index, 1)
const closeTab = (key: number | Partial<TabItem>) => {
if (typeof key === 'number') tabs.value.splice(key, 1)
else {
const index = tabs.value.findIndex(getPredicate(key))
if (index > -1) tabs.value.splice(index, 1)
}
}
const updateTab = (key: number | Partial<TabItem>, newTabItemProps: Partial<TabItem>) => {
const tab = typeof key === 'number' ? tabs.value[key] : tabs.value.find(getPredicate(key))
if (tab) {
Object.assign(tab, newTabItemProps)
}
}
return { tabs, addTab, activeTab, clearTabs, closeTab }
return { tabs, addTab, activeTab, clearTabs, closeTab, updateTab }
}

8
packages/nc-gui-v2/lib/enums.ts

@ -4,7 +4,13 @@ export enum Role {
User = 'user',
}
export type Roles = Record<Role, boolean>
export enum ClientType {
MYSQL = 'mysql2',
MSSQL = 'mssql',
PG = 'pg',
SQLITE = 'sqlite3',
VITESS = 'vitess',
}
export enum Language {
de = 'Deutsch',

40
packages/nc-gui-v2/lib/types.ts

@ -1,5 +1,5 @@
import type { ComputedRef, ToRefs } from 'vue'
import type { Roles } from '~/lib/enums'
import type { Role } from './enums'
export interface User {
id: string
@ -33,40 +33,4 @@ export type ReadonlyState = Readonly<Pick<State, 'token' | 'user'>> & Omit<State
export type GlobalState = Getters & Actions & ToRefs<ReadonlyState>
export enum ClientType {
MYSQL = 'mysql2',
MSSQL = 'mssql',
PG = 'pg',
SQLITE = 'sqlite3',
VITESS = 'vitess',
}
export interface ProjectCreateForm {
title: string
dataSource: {
client: ClientType
connection:
| {
host: string
database: string
user: string
password: string
port: number | string
ssl?: Record<string, string>
searchPath?: string[]
}
| {
client?: 'sqlite3'
database: string
connection?: {
filename?: string
}
useNullAsDefault?: boolean
}
}
inflection: {
inflectionColumn?: string
inflectionTable?: string
}
sslUse?: any
}
export type Roles = Record<Role, boolean>

28371
packages/nc-gui-v2/package-lock.json generated

File diff suppressed because it is too large Load Diff

2
packages/nc-gui-v2/package.json

@ -19,6 +19,7 @@
"monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk",
"socket.io-client": "^4.5.1",
"sortablejs": "^1.15.0",
"unique-names-generator": "^4.7.1",
"vue-i18n": "^9.1.10",
"vue-toastification": "^2.0.0-rc.5",
@ -30,6 +31,7 @@
"@iconify-json/material-symbols": "^1.1.8",
"@iconify-json/mdi": "^1.1.25",
"@intlify/vite-plugin-vue-i18n": "^4.0.0",
"@types/sortablejs": "^1.13.0",
"@vitejs/plugin-vue": "^2.3.3",
"@vitest/ui": "^0.18.0",
"@vue/compiler-sfc": "^3.2.37",

69
packages/nc-gui-v2/pages/index/index.vue

@ -1,10 +1,10 @@
<script lang="ts" setup>
import {Modal} from 'ant-design-vue'
import type {ProjectType} from 'nocodb-sdk'
import {useToast} from 'vue-toastification'
import {navigateTo} from '#app'
import {computed, onMounted} from '#imports'
import {extractSdkResponseErrorMsg} from '~/utils/errorUtils'
import { Modal } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import { useToast } from 'vue-toastification'
import { navigateTo } from '#app'
import { computed, onMounted } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import MdiDeleteOutline from '~icons/mdi/delete-outline'
import MdiEditOutline from '~icons/mdi/edit-outline'
@ -13,7 +13,7 @@ import MdiMenuDown from '~icons/mdi/menu-down'
import MdiPlus from '~icons/mdi/plus'
import MdiDatabaseOutline from '~icons/mdi/database-outline'
const {$api, $state, $e} = useNuxtApp()
const { $api, $state, $e } = useNuxtApp()
const toast = useToast()
const filterQuery = ref('')
@ -29,7 +29,7 @@ const loadProjects = async () => {
const filteredProjects = computed(() => {
return projects.value.filter(
(project) => !filterQuery.value || project.title?.toLowerCase?.().includes(filterQuery.value.toLowerCase()),
(project) => !filterQuery.value || project.title?.toLowerCase?.().includes(filterQuery.value.toLowerCase()),
)
})
@ -68,17 +68,17 @@ $state.sidebarOpen.value = false
<b>{{ $t('title.myProject') }}</b>
<MdiRefresh
v-t="['a:project:refresh']"
class="text-sm text-gray-500 hover:text-primary mt-1 cursor-pointer"
@click="loadProjects"
v-t="['a:project:refresh']"
class="text-sm text-gray-500 hover:text-primary mt-1 cursor-pointer"
@click="loadProjects"
></MdiRefresh>
</h1>
<div class="flex mb-6">
<a-input-search
v-model:value="filterQuery"
class="max-w-[200px] nc-project-page-search"
:placeholder="$t('activity.searchProject')"
v-model:value="filterQuery"
class="max-w-[200px] nc-project-page-search"
:placeholder="$t('activity.searchProject')"
></a-input-search>
<div class="flex-grow"></div>
@ -86,27 +86,27 @@ $state.sidebarOpen.value = false
<a-button class="nc-new-project-menu !shadow">
<div class="flex align-center">
{{ $t('title.newProj') }}
<MdiMenuDown class="menu-icon"/>
<MdiMenuDown class="menu-icon" />
</div>
</a-button>
<template #overlay>
<a-menu>
<div
v-t="['c:project:create:xcdb']"
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2 nc-create-xc-db-project"
@click="navigateTo('/project/create')"
v-t="['c:project:create:xcdb']"
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2 nc-create-xc-db-project"
@click="navigateTo('/project/create')"
>
<MdiPlus class="col-span-2 mr-1 mt-[1px] text-primary text-lg"/>
<MdiPlus class="col-span-2 mr-1 mt-[1px] text-primary text-lg" />
<div class="col-span-10 text-sm xl:text-md">{{ $t('activity.createProject') }}</div>
</div>
<div
v-t="['c:project:create:extdb']"
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2 nc-create-external-db-project"
@click="navigateTo('/project/create-external')"
v-t="['c:project:create:extdb']"
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2 nc-create-external-db-project"
@click="navigateTo('/project/create-external')"
>
<MdiDatabaseOutline class="col-span-2 mr-1 mt-[1px] text-green-500 text-lg"/>
<div class="col-span-10 text-sm xl:text-md" v-html="$t('activity.createProjectExtended.extDB')"/>
<MdiDatabaseOutline class="col-span-2 mr-1 mt-[1px] text-green-500 text-lg" />
<div class="col-span-10 text-sm xl:text-md" v-html="$t('activity.createProjectExtended.extDB')" />
</div>
</a-menu>
</template>
@ -114,12 +114,12 @@ $state.sidebarOpen.value = false
</div>
<div v-if="loading">
<a-skeleton/>
<a-skeleton />
</div>
<a-table
v-else
:custom-row="
v-else
:custom-row="
(record) => ({
onClick: () => {
$e('a:project:open')
@ -127,14 +127,13 @@ $state.sidebarOpen.value = false
},
})
"
:data-source="filteredProjects"
:pagination="{ position: ['bottomCenter'] }"
:data-source="filteredProjects"
:pagination="{ position: ['bottomCenter'] }"
>
<!-- Title -->
<a-table-column key="title" :title="$t('general.title')" data-index="title">
<template #default="{ text }">
<div class="capitalize !w-[400px] overflow-hidden overflow-ellipsis whitespace-nowrap nc-project-row"
:title="text">
<div class="capitalize !w-[400px] overflow-hidden overflow-ellipsis whitespace-nowrap nc-project-row" :title="text">
{{ text }}
</div>
</template>
@ -144,11 +143,11 @@ $state.sidebarOpen.value = false
<template #default="{ text, record }">
<div class="flex align-center">
<MdiEditOutline
v-t="['c:project:edit:rename']"
class="nc-action-btn"
@click.stop="navigateTo(`/project/${text}`)"
v-t="['c:project:edit:rename']"
class="nc-action-btn"
@click.stop="navigateTo(`/project/${text}`)"
/>
<MdiDeleteOutline class="nc-action-btn" @click.stop="deleteProject(record)"/>
<MdiDeleteOutline class="nc-action-btn" @click.stop="deleteProject(record)" />
</div>
</template>
</a-table-column>

3
packages/nc-gui-v2/pages/nc/[projectId].vue

@ -2,6 +2,7 @@
const route = useRoute()
const { loadProject, loadTables } = useProject(route.params.projectId as string)
const { clearTabs, addTab } = useTabs()
const { $state } = useNuxtApp()
addTab({ type: 'auth', title: 'Team & Auth' })
@ -17,6 +18,8 @@ watch(
}
},
)
$state.sidebarOpen.value = true
</script>
<template>

14
packages/nc-gui-v2/pages/project/index/create-external.vue

@ -310,7 +310,7 @@ onMounted(() => {
<template #title>
<span>{{ $t('tooltip.clientCert') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" size="small" @click="certFileInput.click()" class="shadow">
<a-button :disabled="!sslFilesRequired" size="small" class="shadow" @click="certFileInput.click()">
{{ $t('labels.clientCert') }}
</a-button>
</a-tooltip>
@ -319,7 +319,7 @@ onMounted(() => {
<template #title>
<span>{{ $t('tooltip.clientKey') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" size="small" @click="keyFileInput.click()" class="shadow">
<a-button :disabled="!sslFilesRequired" size="small" class="shadow" @click="keyFileInput.click()">
{{ $t('labels.clientKey') }}
</a-button>
</a-tooltip>
@ -328,7 +328,7 @@ onMounted(() => {
<template #title>
<span>{{ $t('tooltip.clientCA') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" size="small" @click="caFileInput.click()" class="shadow">
<a-button :disabled="!sslFilesRequired" size="small" class="shadow" @click="caFileInput.click()">
{{ $t('labels.serverCA') }}
</a-button>
</a-tooltip>
@ -364,7 +364,9 @@ onMounted(() => {
<a-button type="primary" ghost class="nc-extdb-btn-test-connection" @click="testConnection">
{{ $t('activity.testDbConn') }}
</a-button>
<a-button type="primary" :disabled="!testSuccess" class="nc-extdb-btn-submit !shadow" @click="createProject"> Submit </a-button>
<a-button type="primary" :disabled="!testSuccess" class="nc-extdb-btn-submit !shadow" @click="createProject">
Submit
</a-button>
</div>
</a-form-item>
</a-form>
@ -398,7 +400,7 @@ onMounted(() => {
@apply !min-h-0;
}
:deep(.ant-card-head-title){
@apply !text-3xl
:deep(.ant-card-head-title) {
@apply !text-3xl;
}
</style>

4
packages/nc-gui-v2/pages/signin.vue

@ -111,7 +111,9 @@ const resetError = () => {
</nuxt-link>
</div>
<div class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full">
<div
class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full"
>
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MdiLogin /> {{ $t('general.signIn') }}</span>
</button>

6
packages/nc-gui-v2/pages/signup.vue

@ -19,7 +19,7 @@ let error = $ref<string | null>(null)
const form = reactive({
email: '',
password: ''
password: '',
})
const formRules = {
@ -102,7 +102,9 @@ const resetError = () => {
/>
</a-form-item>
<div class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full">
<div
class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full"
>
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MaterialSymbolsRocketLaunchOutline /> {{ $t('general.signUp') }}</span>
</button>

118
packages/nc-gui-v2/utils/projectCreateUtils.ts

@ -1,43 +1,75 @@
import { adjectives, animals, starWars, uniqueNamesGenerator } from 'unique-names-generator'
import type { ClientType, ProjectCreateForm } from '~/lib/types'
import { ClientType } from '~/lib/enums'
export interface ProjectCreateForm {
title: string
dataSource: {
client: ClientType
connection:
| {
host: string
database: string
user: string
password: string
port: number | string
ssl?: Record<string, string>
searchPath?: string[]
}
| {
client?: ClientType.SQLITE
database: string
connection?: {
filename?: string
}
useNullAsDefault?: boolean
}
}
inflection: {
inflectionColumn?: string
inflectionTable?: string
}
sslUse?: any
}
const defaultHost = 'localhost'
const testDataBaseNames = {
mysql2: null,
[ClientType.MYSQL]: null,
mysql: null,
pg: 'postgres',
[ClientType.PG]: 'postgres',
oracledb: 'xe',
mssql: undefined,
sqlite3: 'a.sqlite',
[ClientType.MSSQL]: undefined,
[ClientType.SQLITE]: 'a.sqlite',
}
export const getTestDatabaseName = (db: { client: ClientType; connection?: { database?: string } }) => {
if (db.client === 'pg') return db.connection?.database
if (db.client === ClientType.PG) return db.connection?.database
return testDataBaseNames[db.client as keyof typeof testDataBaseNames]
}
export const clientTypes = [
{
text: 'MySql',
value: 'mysql2',
value: ClientType.MYSQL,
},
{
text: 'MSSQL',
value: 'mssql',
value: ClientType.MSSQL,
},
{
text: 'PostgreSQL',
value: 'pg',
value: ClientType.PG,
},
{
text: 'SQLite',
value: 'sqlite3',
value: ClientType.SQLITE,
},
]
const homeDir = ''
const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataSource']['connection']> = {
pg: {
host: 'localhost',
[ClientType.PG]: {
host: defaultHost,
port: '5432',
user: 'postgres',
password: 'password',
@ -49,8 +81,8 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
cert: '',
},
},
mysql2: {
host: 'localhost',
[ClientType.MYSQL]: {
host: defaultHost,
port: '3306',
user: 'root',
password: 'password',
@ -61,8 +93,8 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
cert: '',
},
},
vitess: {
host: 'localhost',
[ClientType.VITESS]: {
host: defaultHost,
port: '15306',
user: 'root',
password: 'password',
@ -73,8 +105,29 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
cert: '',
},
},
[ClientType.MSSQL]: {
host: defaultHost,
port: 1433,
user: 'sa',
password: 'Password123.',
database: '_test',
searchPath: ['dbo'],
ssl: {
ca: '',
key: '',
cert: '',
},
},
[ClientType.SQLITE]: {
client: ClientType.SQLITE,
database: homeDir,
connection: {
filename: homeDir,
},
useNullAsDefault: true,
},
tidb: {
host: 'localhost',
host: defaultHost,
port: '4000',
user: 'root',
password: '',
@ -86,7 +139,7 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
},
},
yugabyte: {
host: 'localhost',
host: defaultHost,
port: '5432',
user: 'postgres',
password: '',
@ -98,7 +151,7 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
},
},
citusdb: {
host: 'localhost',
host: defaultHost,
port: '5432',
user: 'postgres',
password: '',
@ -110,7 +163,7 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
},
},
cockroachdb: {
host: 'localhost',
host: defaultHost,
port: '5432',
user: 'postgres',
password: '',
@ -122,7 +175,7 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
},
},
greenplum: {
host: 'localhost',
host: defaultHost,
port: '5432',
user: 'postgres',
password: '',
@ -133,21 +186,8 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
cert: '',
},
},
mssql: {
host: 'localhost',
port: 1433,
user: 'sa',
password: 'Password123.',
database: '_test',
searchPath: ['dbo'],
ssl: {
ca: '',
key: '',
cert: '',
},
},
oracledb: {
host: 'localhost',
host: defaultHost,
port: '1521',
user: 'system',
password: 'Oracle18',
@ -158,14 +198,6 @@ const sampleConnectionData: Record<ClientType | string, ProjectCreateForm['dataS
cert: '',
},
},
sqlite3: {
client: 'sqlite3',
database: homeDir,
connection: {
filename: homeDir,
},
useNullAsDefault: true,
},
}
export const getDefaultConnectionConfig = (client: ClientType): ProjectCreateForm['dataSource'] => {

5
packages/nc-gui-v2/utils/validation.ts

@ -78,7 +78,12 @@ export const projectTitleValidator = {
callback()
},
}
export const fieldRequiredValidator = {
required: true,
message: 'Field is required',
}
export const getRequiredValidator = (field = 'Field') => ({
required: true,
message: `${field} is required`,
})

Loading…
Cancel
Save