mirror of https://github.com/nocodb/nocodb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
262 lines
8.1 KiB
262 lines
8.1 KiB
<script lang="ts" setup> |
|
import { Empty, Modal, message } from 'ant-design-vue' |
|
import type { ApiTokenType, RequestParams, UserType } from 'nocodb-sdk' |
|
import { extractSdkResponseErrorMsg, useApi, useCopy, useNuxtApp } from '#imports' |
|
|
|
const { api, isLoading } = useApi() |
|
|
|
const { $e } = useNuxtApp() |
|
|
|
const { copy } = useCopy() |
|
|
|
const { t } = useI18n() |
|
|
|
let tokens = $ref<UserType[]>([]) |
|
|
|
let currentPage = $ref(1) |
|
|
|
let showNewTokenModal = $ref(false) |
|
|
|
const currentLimit = $ref(10) |
|
|
|
let selectedTokenData = $ref<ApiTokenType>({}) |
|
|
|
const searchText = ref<string>('') |
|
|
|
const pagination = reactive({ |
|
total: 0, |
|
pageSize: 10, |
|
}) |
|
const loadTokens = async (page = currentPage, limit = currentLimit) => { |
|
currentPage = page |
|
try { |
|
const response: any = await api.orgTokens.list({ |
|
query: { |
|
limit, |
|
offset: searchText.value.length === 0 ? (page - 1) * limit : 0, |
|
}, |
|
} as RequestParams) |
|
if (!response) return |
|
|
|
pagination.total = response.pageInfo.totalRows ?? 0 |
|
pagination.pageSize = 10 |
|
|
|
tokens = response.list as UserType[] |
|
} catch (e) { |
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
} |
|
} |
|
|
|
loadTokens() |
|
|
|
const deleteToken = async (token: string) => { |
|
Modal.confirm({ |
|
title: t('msg.info.deleteTokenConfirmation'), |
|
type: 'warn', |
|
onOk: async () => { |
|
try { |
|
// todo: delete token |
|
await api.orgTokens.delete(token) |
|
message.success(t('msg.success.tokenDeleted')) |
|
await loadTokens() |
|
} catch (e) { |
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
} |
|
$e('a:account:token:delete') |
|
}, |
|
}) |
|
} |
|
|
|
const generateToken = async () => { |
|
try { |
|
await api.orgTokens.create(selectedTokenData) |
|
showNewTokenModal = false |
|
// Token generated successfully |
|
message.success(t('msg.success.tokenGenerated')) |
|
selectedTokenData = {} |
|
await loadTokens() |
|
} catch (e) { |
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
} |
|
$e('a:api-token:generate') |
|
} |
|
|
|
const copyToken = (token: string | undefined) => { |
|
if (!token) return |
|
|
|
copy(token) |
|
// Copied to clipboard |
|
message.info(t('msg.info.copiedToClipboard')) |
|
|
|
$e('c:api-token:copy') |
|
} |
|
|
|
const descriptionInput = ref((el) => { |
|
el?.focus() |
|
}) |
|
</script> |
|
|
|
<template> |
|
<div class="h-full overflow-y-scroll scrollbar-thin-dull pt-2"> |
|
<div class="text-xl mt-4 mb-8 text-center font-weight-bold">Token Management</div> |
|
<div class="max-w-[900px] mx-auto p-4" data-testid="nc-token-list"> |
|
<div class="py-2 flex gap-4 items-center"> |
|
<div class="flex-grow"></div> |
|
<MdiReload class="cursor-pointer" @click="loadTokens" /> |
|
<a-button data-testid="nc-token-create" size="small" type="primary" @click="showNewTokenModal = true"> |
|
<div class="flex items-center gap-1"> |
|
<MdiAdd /> |
|
Add new token |
|
</div> |
|
</a-button> |
|
</div> |
|
<a-table |
|
:row-key="(record) => record.id" |
|
:data-source="tokens" |
|
:pagination="{ position: ['bottomCenter'] }" |
|
:loading="isLoading" |
|
size="small" |
|
@change="loadTokens($event.current)" |
|
> |
|
<template #emptyText> |
|
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" /> |
|
</template> |
|
|
|
<!-- Created By --> |
|
<a-table-column key="created_by" :title="$t('labels.createdBy')" data-index="created_by"> |
|
<template #default="{ text }"> |
|
<div v-if="text"> |
|
{{ text }} |
|
</div> |
|
<div v-else class="text-gray-400">N/A</div> |
|
</template> |
|
</a-table-column> |
|
|
|
<!-- Description --> |
|
<a-table-column key="description" :title="$t('labels.description')" data-index="description"> |
|
<template #default="{ text }"> |
|
{{ text }} |
|
</template> |
|
</a-table-column> |
|
|
|
<!-- Token --> |
|
<a-table-column key="token" :title="$t('labels.token')" data-index="token"> |
|
<template #default="{ text, record }"> |
|
<div class="w-[320px]"> |
|
<span v-if="record.show">{{ text }}</span> |
|
<span v-else>*******************************************</span> |
|
</div> |
|
</template> |
|
</a-table-column> |
|
|
|
<!-- Actions --> |
|
|
|
<a-table-column key="actions" :title="$t('labels.actions')" data-index="token"> |
|
<template #default="{ record }"> |
|
<div class="flex items-center gap-2"> |
|
<a-tooltip placement="bottom"> |
|
<template #title> |
|
<span v-if="record.show"> {{ $t('general.hide') }} </span> |
|
<span v-else> {{ $t('general.show') }} </span> |
|
</template> |
|
|
|
<a-button type="text" class="!rounded-md" @click="record.show = !record.show"> |
|
<template #icon> |
|
<MaterialSymbolsVisibilityOff v-if="record.show" class="flex mx-auto h-[1.1rem]" /> |
|
<MaterialSymbolsVisibility v-else class="flex mx-auto h-[1rem]" /> |
|
</template> |
|
</a-button> |
|
</a-tooltip> |
|
|
|
<a-tooltip placement="bottom"> |
|
<template #title> {{ $t('general.copy') }}</template> |
|
|
|
<a-button type="text" class="!rounded-md" @click="copyToken(record.token)"> |
|
<template #icon> |
|
<MdiContentCopy class="flex mx-auto h-[1rem]" /> |
|
</template> |
|
</a-button> |
|
</a-tooltip> |
|
|
|
<a-dropdown |
|
:trigger="['click']" |
|
class="flex" |
|
placement="bottomRight" |
|
overlay-class-name="nc-dropdown-api-token-mgmt" |
|
> |
|
<div class="flex flex-row items-center"> |
|
<a-button type="text" class="!px-0"> |
|
<div class="flex flex-row items-center h-[1.2rem]"> |
|
<IcBaselineMoreVert class="nc-token-menu" /> |
|
</div> |
|
</a-button> |
|
</div> |
|
|
|
<template #overlay> |
|
<a-menu data-testid="nc-token-row-action-icon"> |
|
<a-menu-item> |
|
<div class="flex flex-row items-center py-3 h-[1rem] nc-delete-token" @click="deleteToken(record.token)"> |
|
<MdiDeleteOutline class="flex" /> |
|
<div class="text-xs pl-2">{{ $t('general.remove') }}</div> |
|
</div> |
|
</a-menu-item> |
|
</a-menu> |
|
</template> |
|
</a-dropdown> |
|
</div> |
|
</template> |
|
</a-table-column> |
|
</a-table> |
|
</div> |
|
|
|
<a-modal |
|
v-model:visible="showNewTokenModal" |
|
:closable="false" |
|
width="28rem" |
|
centered |
|
:footer="null" |
|
wrap-class-name="nc-modal-generate-token" |
|
> |
|
<div class="relative flex flex-col h-full"> |
|
<a-button type="text" class="!absolute top-0 right-0 rounded-md -mt-2 -mr-3" @click="showNewTokenModal = false"> |
|
<template #icon> |
|
<MaterialSymbolsCloseRounded class="flex mx-auto" /> |
|
</template> |
|
</a-button> |
|
|
|
<!-- Generate Token --> |
|
<div class="flex flex-row justify-center w-full -mt-1 mb-3"> |
|
<a-typography-title :level="5">{{ $t('title.generateToken') }}</a-typography-title> |
|
</div> |
|
|
|
<!-- Description --> |
|
<a-form |
|
ref="form" |
|
:model="selectedTokenData" |
|
name="basic" |
|
layout="vertical" |
|
class="flex flex-col justify-center space-y-6" |
|
no-style |
|
autocomplete="off" |
|
@finish="generateToken" |
|
> |
|
<a-input |
|
:ref="descriptionInput" |
|
v-model:value="selectedTokenData.description" |
|
data-testid="nc-token-modal-description" |
|
:placeholder="$t('labels.description')" |
|
/> |
|
|
|
<!-- Generate --> |
|
<div class="flex flex-row justify-center"> |
|
<a-button type="primary" html-type="submit" data-testid="nc-token-modal-save"> |
|
{{ $t('general.generate') }} |
|
</a-button> |
|
</div> |
|
</a-form> |
|
</div> |
|
</a-modal> |
|
</div> |
|
</template> |
|
|
|
<style scoped></style>
|
|
|