fix: new aui

This commit is contained in:
DarkPhoenix2704
2025-05-12 05:25:46 +00:00
parent 281825f97b
commit cdb30ec02a
10 changed files with 468 additions and 90 deletions

View File

@@ -0,0 +1,131 @@
<script setup lang="ts">
import { LoadingOutlined } from '@ant-design/icons-vue'
interface Props {
code: string
}
const props = defineProps<Props>()
const code = toRef(props, 'code')
const indicator = h(LoadingOutlined, {
style: {
fontSize: '2rem',
},
spin: true,
})
const { t } = useI18n()
const { copy } = useCopy()
const isCopied = ref(false)
const onCopyToClipboard = async () => {
try {
await copy(code.value)
// Copied to clipboard
message.info(t('msg.info.copiedToClipboard'))
isCopied.value = true
setTimeout(() => {
isCopied.value = false
}, 5000)
} catch (e: any) {
message.error(e.message)
}
}
</script>
<template>
<div class="nc-mcp-code-tab-wrapper h-80 flex flex-col mt-2">
<div class="flex h-9 bg-gray-50 border-b-1 border-nc-border-gray-medium rounded-t-lg items-center px-3">
<div class="flex-1 text-nc-content-gray leading-5">JSON</div>
<NcButton type="text" size="small" class="!hover:bg-gray-200" @click="onCopyToClipboard">
<div class="flex items-center gap-2 text-small leading-[18px] min-w-80px justify-center">
<GeneralIcon
:icon="isCopied ? 'circleCheck' : 'copy'"
class="h-4 w-4"
:class="{
'text-gray-700': !isCopied,
'text-green-700': isCopied,
}"
/>
{{ isCopied ? $t('general.copied') : $t('general.copy') }}
</div>
</NcButton>
</div>
<Suspense>
<MonacoEditor
class="h-72 !rounded-b-lg overflow-hidden !bg-gray-50"
:model-value="code"
:read-only="true"
lang="json"
:validate="false"
:disable-deep-compare="true"
:monaco-config="{
minimap: {
enabled: false,
},
fontSize: 13,
lineHeight: 18,
padding: {
top: 12,
bottom: 12,
},
overviewRulerBorder: false,
overviewRulerLanes: 0,
hideCursorInOverviewRuler: true,
lineDecorationsWidth: 12,
lineNumbersMinChars: 0,
roundedSelection: false,
selectOnLineNumbers: false,
scrollBeyondLastLine: false,
contextmenu: false,
glyphMargin: false,
folding: false,
bracketPairColorization: { enabled: false },
wordWrap: 'on',
scrollbar: {
horizontal: 'hidden',
verticalScrollbarSize: 6,
},
renderIndentGuides: false,
wrappingStrategy: 'advanced',
renderLineHighlight: 'none',
tabSize: 2,
detectIndentation: false,
insertSpaces: true,
lineNumbers: 'off',
}"
hide-minimap
/>
<template #fallback>
<div class="h-full w-full flex flex-col justify-center items-center mt-28">
<a-spin size="large" :indicator="indicator" />
</div>
</template>
</Suspense>
</div>
</template>
<style lang="scss">
.nc-mcp-code-tab-wrapper {
@apply !bg-nc-bg-gray-extra-light border-1 border-nc-border-gray-medium rounded-lg flex-1;
.monaco-editor {
@apply !border-0 !rounded-b-lg pr-3;
}
.overflow-guard {
@apply !border-0 !rounded-b-lg;
}
.monaco-editor,
.monaco-diff-editor,
.monaco-component {
--vscode-editor-background: #f9f9fa;
--vscode-editorGutter-background: #f9f9fa;
}
}
</style>

View File

@@ -0,0 +1,252 @@
<script setup lang="ts">
interface Props {
value: boolean
token: MCPTokenExtendedType
}
const props = defineProps<Props>()
const emits = defineEmits(['close', 'update:value', 'update:token'])
const modalVisible = useVModel(props, 'value')
const { appInfo } = useGlobal()
const { openedProject } = storeToRefs(useBases())
const token = useVModel(props, 'token')
const supportedDocs = [
{
title: 'Getting with MCP Server',
href: 'https://docs.nocodb.com/automation/webhook/create-webhook/',
},
{
title: 'Setting up MCP Server with Claude',
href: 'https://docs.nocodb.com/automation/webhook/webhook-overview',
},
{
title: 'Setting up MCP Server with Cursor',
href: 'https://docs.nocodb.com/automation/webhook/create-webhook#webhook-with-custom-payload-',
},
{
title: 'Setting up MCP Server with Windsurf',
href: 'https://docs.nocodb.com/automation/webhook/create-webhook#webhook-with-conditions',
},
]
const { updateMcpToken } = useMcpSettings()
const regenerateToken = async () => {
const newToken = await updateMcpToken(token.value)
console.log(newToken)
if (newToken) {
token.value = newToken
}
}
const closeModal = () => {
emits('close')
modalVisible.value = false
}
const activeTab = ref<'claude' | 'cursor' | 'windsurf'>('claude')
const code = computed(
() => `
{
"mcpServers": {
"NocoDB Base - ${openedProject.value?.title}": {
"command": "npx",
"args": [
"mcp-remote",
"${appInfo.value.ncSiteUrl}/mcp/${token.value.id}",
"--header",
"xc-mcp-token: ${token.value?.token ?? 'xxxxxxxxxxxxxxxxxxxxxxxxxxx'}"
]
}
}
}
`,
)
</script>
<template>
<NcModal v-model:visible="modalVisible" :show-separator="true" size="large" wrap-class-name="nc-modal-mcp-token-create-edit">
<template #header>
<div class="flex w-full items-center px-4 py-2 justify-between">
<div class="flex items-center gap-3 flex-1">
<GeneralIcon class="text-gray-900 h-5 w-5" icon="mcp" />
<span class="text-gray-900 truncate font-semibold text-xl">
{{ token.title }}
</span>
</div>
<div class="flex justify-end items-center gap-3 flex-1">
<NcButton type="text" size="small" data-testid="nc-close-webhook-modal" @click.stop="closeModal">
<GeneralIcon icon="close" />
</NcButton>
</div>
</div>
</template>
<div class="flex bg-white rounded-b-2xl h-[calc(100%_-_66px)]">
<div
ref="containerElem"
class="h-full flex-1 flex flex-col overflow-y-auto scroll-smooth nc-scrollbar-thin px-12 py-6 mx-auto"
>
<div class="flex flex-col gap-3">
<div class="text-nc-content-gray font-bold leading-6">
{{ $t('labels.mcpSetup') }}
</div>
<NcTabs v-model:activeKey="activeTab">
<a-tab-pane key="claude" class="!h-full">
<template #tab>
<span
:class="{
'text-brand-500 font-medium': activeTab === 'claude',
'text-gray-700': activeTab !== 'claude',
}"
class="text-sm"
>
Claude
</span>
</template>
<div class="relative flex flex-col leading-6 text-nc-content-gray-subtle2 gap-3 my-3">
Get started with the NocoDB MCP with Claude Desktop in 3 simple steps
<ol class="list-decimal pl-5">
<li>Navigate to Claude Desktop settings from the navigation bar.</li>
<li>Go to the Develop Tab, and click on Edit Config.</li>
<li>Add the JSON configuration thats provided after creating a token in claude_desktop_config.json</li>
</ol>
<NcButton type="secondary" class="w-44" size="small" :loading="token.loading" @click="regenerateToken(token)">
{{ $t('labels.regenerateToken') }}
</NcButton>
<DashboardSettingsBaseMCPCode :key="code" :code="code" />
</div>
</a-tab-pane>
<a-tab-pane key="cursor" class="!h-full">
<template #tab>
<span
:class="{
'text-brand-500 font-medium': activeTab === 'cursor',
'text-gray-700': activeTab !== 'cursor',
}"
class="text-sm"
>
Cursor
</span>
</template>
<div class="relative flex flex-col leading-6 text-nc-content-gray-subtle2 gap-3 my-3">
Get started with the NocoDB MCP with Cursor in 3 simple steps
<ol class="list-decimal pl-5">
<li>Open Cursor Settings (press Shift+Cmd+J)</li>
<li>Select the "MCP" tab and click "Add MCP Server" .</li>
<li>Add the JSON configuration thats provided after creating a token.</li>
</ol>
<NcButton type="secondary" class="w-44" size="small" :loading="token.loading" @click="regenerateToken(token)">
{{ $t('labels.regenerateToken') }}
</NcButton>
<DashboardSettingsBaseMCPCode :key="code" :code="code" />
</div>
</a-tab-pane>
<a-tab-pane key="windsurf" class="!h-full">
<template #tab>
<span
:class="{
'text-brand-500 font-medium': activeTab === 'windsurf',
'text-gray-700': activeTab !== 'windsurf',
}"
class="text-sm"
>
Windsurf
</span>
</template>
<div class="relative flex flex-col leading-6 text-nc-content-gray-subtle2 gap-3 my-3">
Get started with the NocoDB MCP with Windsurf in 4 simple steps
<ol class="list-decimal pl-5">
<li>Access Windsurf settings and Select Cascade Tab in the left sidebar</li>
<li>Click on Add Server.</li>
<li>Now click on Add Custom Server in the modal.</li>
<li>Paste the JSON configuration thats provided after creating a token in the opened file</li>
</ol>
<NcButton type="secondary" class="w-44" size="small" :loading="token.loading" @click="regenerateToken(token)">
{{ $t('labels.regenerateToken') }}
</NcButton>
<DashboardSettingsBaseMCPCode :code="code" />
</div>
</a-tab-pane>
</NcTabs>
</div>
<NcAlert type="info" class="mt-3">
<template #message>
{{ $t('labels.mcpTokenVisibilityInfo') }}
</template>
<template #description>
{{ $t('labels.mcpTokenVisibilityInfoDescription') }} <br />
{{ $t('labels.mcpTokenVisibilityInfoDescription2') }}
</template>
</NcAlert>
</div>
<div class="h-full bg-gray-50 border-l-1 w-80 p-5 rounded-br-2xl border-gray-200">
<div class="w-full flex flex-col gap-3">
<h2 class="text-sm text-gray-700 font-semibold !my-0">{{ $t('labels.supportDocs') }}</h2>
<div>
<div v-for="(doc, idx) of supportedDocs" :key="idx" class="flex items-center gap-1">
<div class="h-7 w-7 flex items-center justify-center">
<GeneralIcon icon="bookOpen" class="flex-none w-4 h-4 text-gray-500" />
</div>
<NuxtLink
:href="doc.href"
target="_blank"
rel="noopener noreferrer"
class="!text-gray-500 text-sm !no-underline !hover:underline"
>
{{ doc.title }}
</NuxtLink>
</div>
</div>
</div>
</div>
</div>
</NcModal>
</template>
<style lang="scss">
.nc-modal-mcp-token-create-edit {
z-index: 1050;
a {
@apply !no-underline !text-gray-700 !hover:text-primary;
}
.nc-modal {
@apply !p-0;
height: min(calc(100vh - 100px), 1024px);
max-height: min(calc(100vh - 100px), 1024px) !important;
}
.nc-modal-header {
@apply !mb-0 !pb-0;
}
.ant-tabs-nav {
@apply !pl-0;
}
.ant-tabs-tab {
@apply pt-1 pb-1.5;
}
}
</style>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,265 @@
<script setup lang="ts">
import dayjs from 'dayjs'
const { t } = useI18n()
const { appInfo } = useGlobal()
const { copy } = useCopy()
const { sorts, sortDirection, loadSorts, handleGetSortedData, saveOrUpdate: saveOrUpdateSort } = useUserSorts('Webhook') // Using 'Webhook' as the sort type since 'MCPToken' isn't defined
const orderBy = computed<Record<string, SordDirectionType>>({
get: () => {
return sortDirection.value
},
set: (value: Record<string, SordDirectionType>) => {
// Check if value is an empty object
if (Object.keys(value).length === 0) {
saveOrUpdateSort({})
return
}
const entries = Object.entries(value)
if (entries.length > 0) {
const [field, direction] = entries[0]
saveOrUpdateSort({
field,
direction,
})
}
},
})
const {
mcpTokens,
createMcpToken,
listMcpTokens,
cancelNewMcpToken,
isUnsavedMCPTokenPending,
addNewMcpToken,
isCreatingMcpToken,
newMcpTokenTitle,
updateMcpToken,
} = useMcpSettings()
const sortedMcpTokens = computed(() => handleGetSortedData(mcpTokens.value, sorts.value))
const columns = [
{
key: 'name',
title: t('general.name'),
name: 'Token',
minWidth: 397,
padding: '12px 24px',
showOrderBy: true,
dataIndex: 'title',
},
{
key: 'created_at',
title: t('labels.createdOn'),
width: 150,
minWidth: 180,
showOrderBy: true,
dataIndex: 'created_at',
},
{
key: 'action',
title: t('general.action'),
width: 162,
minWidth: 162,
justify: 'justify-end',
align: 'center',
},
] as NcTableColumnProps[]
onMounted(async () => {
loadSorts()
await listMcpTokens()
})
const isTokenModalVisible = ref(false)
const activeToken = ref<MCPTokenExtendedType | null>(null)
const handleOpenTokenModal = (token: MCPTokenExtendedType) => {
activeToken.value = token
isTokenModalVisible.value = true
}
const createTokenWithExpiry = async (token: Partial<MCPTokenExtendedType>) => {
const res = await createMcpToken(token)
if (res) {
handleOpenTokenModal(res)
}
}
const regenerateToken = async (token: MCPTokenExtendedType) => {
const newToken = await updateMcpToken(token)
if (newToken) {
handleOpenTokenModal(newToken)
}
}
const closeModal = async () => {
activeToken.value = null
isTokenModalVisible.value = false
await listMcpTokens()
}
const confirmDeleteToken = (token: MCPTokenExtendedType) => {
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgMCPDelete'), {
'modelValue': isOpen,
'mcpToken': token,
'onUpdate:modelValue': closeDialog,
'onDeleted': async () => {
closeDialog()
await listMcpTokens()
},
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
</script>
<template>
<div v-if="isCreatingMcpToken" class="absolute w-full h-full inset-0 flex items-center justify-center z-90 bg-black/12">
<div
v-if="isCreatingMcpToken"
style="box-shadow: 0px 8px 8px -4px rgba(0, 0, 0, 0.04), 0px 20px 24px -4px rgba(0, 0, 0, 0.1)"
class="bg-white p-6 flex flex-col w-[488px] rounded-2xl"
>
<div class="text-nc-content-gray-emphasis text-lg font-bold">{{ $t('labels.creatingToken') }}</div>
<div class="text-nc-gray-subtle2 mt-2">
{{ $t('labels.creatingTokenDescription') }}
</div>
<div class="w-full flex justify-between items-center gap-3 mt-5">
<GeneralLoader size="xlarge" />
</div>
</div>
</div>
<div class="flex flex-col w-full">
<div class="text-nc-content-gray-emphasis font-semibold text-lg">
{{ $t('labels.modelContextProtocol') }}
</div>
<div class="text-nc-content-gray-subtle2 mt-2 leading-5">
{{ $t('labels.mcpSubText') }}
</div>
<div class="flex items-center mt-6 gap-5">
<NcButton
:disabled="isUnsavedMCPTokenPending"
type="ghost"
class="!text-primary"
data-testid="add-new-mcp-token"
size="small"
:class="{
'!text-nc-content-inverted-primary-disabled': isUnsavedMCPTokenPending,
}"
@click="addNewMcpToken"
>
{{ $t('labels.newMCPEndpoint') }}
</NcButton>
</div>
<NcTable
v-model:order-by="orderBy"
:columns="columns"
header-row-height="44px"
row-height="44px"
:data="sortedMcpTokens"
class="h-full mt-5"
body-row-class-name="nc-base-settings-mcp-token-item"
>
<template #bodyCell="{ column, record: token }">
<template v-if="column.key === 'name'">
<NcTooltip
v-if="!token.isNew"
class="truncate text-gray-800 font-semibold text-sm"
@click="handleOpenTokenModal(token)"
>
{{ token.title }}
<template #title>
<div class="text-[10px] leading-[14px] uppercase font-semibold pt-1 text-gray-300">
{{ $t('labels.createdOn') }}
</div>
<div class="mt-1 text-[13px]">
{{ dayjs(token.created_at).format('D MMMM YYYY, hh:mm A') }}
</div>
<div class="text-[10px] leading-[14px] uppercase font-semibold mt-2 text-gray-300">
{{ $t('labels.createdBy') }}
</div>
<div class="mt-1 pb-1 text-[13px]">
{{ token.created_display_name }}
</div>
</template>
</NcTooltip>
<a-input v-else v-model:value="newMcpTokenTitle" class="new-token-title" placeholder="Token name" />
</template>
<template v-if="column.key === 'created_at'">
<div v-if="!token.isNew && token.created_at" class="text-nc-content-gray-subtle" @click="handleOpenTokenModal(token)">
{{ dayjs(token.created_at).format('D MMM YYYY') }}
</div>
</template>
<template v-if="column.key === 'action'">
<NcDropdown v-if="!token.isNew">
<NcButton type="secondary" size="small">
<GeneralIcon icon="threeDotVertical" />
</NcButton>
<template #overlay>
<NcMenu variant="small">
<NcMenuItem @click="regenerateToken(token)">
<GeneralIcon icon="refresh" />
{{ $t('labels.regenerateToken') }}
</NcMenuItem>
<NcDivider />
<NcMenuItem class="!text-nc-content-red-dark !hover:bg-nc-bg-red-light" @click="confirmDeleteToken(token)">
<GeneralIcon icon="ncTrash2" />
{{ $t('labels.deleteToken') }}
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
<div v-else>
<div class="flex gap-2">
<NcButton data-testid="cancel-token-btn" type="secondary" size="small" @click="cancelNewMcpToken()">
{{ $t('general.cancel') }}
</NcButton>
<NcButton data-testid="create-token-btn" type="primary" size="small" @click="createTokenWithExpiry(token)">
{{ $t('general.save') }}
</NcButton>
</div>
</div>
</template>
</template>
</NcTable>
<DashboardSettingsBaseMCPModal
v-if="isTokenModalVisible"
v-model:visible="isTokenModalVisible"
v-model:token="activeToken"
@close="closeModal"
/>
</div>
</template>
<style scoped lang="scss">
.ant-input {
@apply rounded-lg py-1 px-3 w-398 h-8 border-1 focus:border-brand-500 border-gray-200;
}
</style>