474 lines
11 KiB
Vue
474 lines
11 KiB
Vue
<template>
|
||
<div class="p-5">
|
||
<div class="grid grid-cols-1 gap-5">
|
||
<div
|
||
class="mx-auto min-w-[48rem] max-w-3xl space-y-6 rounded-md border p-4"
|
||
>
|
||
<div class="flex items-center justify-between">
|
||
<div class="text-xl font-semibold">API 访问</div>
|
||
<Button @click="showCreateSecretDialog = true">{{
|
||
$team.pg?.user_info?.api_key
|
||
? '重新生成 API 密钥'
|
||
: '创建新的 API 密钥'
|
||
}}</Button>
|
||
</div>
|
||
<div v-if="$team.pg?.user_info?.api_key">
|
||
<ClickToCopyField
|
||
v-if="$team.pg?.user_info?.api_key"
|
||
:textContent="$team.pg.user_info.api_key"
|
||
/>
|
||
</div>
|
||
<div v-else class="pb-2 text-base text-gray-700">
|
||
您还没有 API 密钥。点击上面的按钮创建一个。
|
||
</div>
|
||
<Dialog
|
||
v-model="showCreateSecretDialog"
|
||
:options="{
|
||
title: 'API 访问',
|
||
size: 'xl',
|
||
actions: [
|
||
{
|
||
label: $team.pg.user_info.api_key
|
||
? '重新生成 API 密钥'
|
||
: '创建新的 API 密钥',
|
||
variant: 'solid',
|
||
disabled: createSecret.data,
|
||
loading: createSecret.loading,
|
||
onClick() {
|
||
createSecret.submit();
|
||
}
|
||
}
|
||
]
|
||
}"
|
||
>
|
||
<template #body-content>
|
||
<div v-if="createSecret.data">
|
||
<p class="text-base">
|
||
请立即复制 API 密钥。您将无法再次查看它!
|
||
</p>
|
||
<label class="block pt-2">
|
||
<span class="mb-2 block text-sm leading-4 text-gray-700"
|
||
>API key</span
|
||
>
|
||
<ClickToCopyField :textContent="createSecret.data.api_key" />
|
||
</label>
|
||
<label class="block pt-2">
|
||
<span class="mb-2 block text-sm leading-4 text-gray-700"
|
||
>API secret</span
|
||
>
|
||
<ClickToCopyField :textContent="createSecret.data.api_secret" />
|
||
</label>
|
||
</div>
|
||
<div v-else class="text-base text-gray-700">
|
||
API key 和 API secret 可用于访问
|
||
<a href="/docs/api" class="underline" target="_blank"
|
||
>今果 Jingrow API</a
|
||
>.
|
||
</div>
|
||
</template>
|
||
</Dialog>
|
||
</div>
|
||
<div
|
||
class="mx-auto min-w-[48rem] max-w-3xl space-y-6 rounded-md border p-4"
|
||
>
|
||
<div class="flex items-center justify-between">
|
||
<div class="text-xl font-semibold">SSH 密钥</div>
|
||
</div>
|
||
<ObjectList :options="sshKeyListOptions" />
|
||
</div>
|
||
<div
|
||
v-if="$session.hasWebhookConfigurationAccess"
|
||
class="mx-auto min-w-[48rem] max-w-3xl space-y-6 rounded-md border p-4"
|
||
>
|
||
<div class="flex items-center justify-between">
|
||
<div class="text-xl font-semibold">Webhooks</div>
|
||
</div>
|
||
<ObjectList :options="webhookListOptions" />
|
||
<AddNewWebhookDialog
|
||
v-if="showAddWebhookDialog"
|
||
v-model="showAddWebhookDialog"
|
||
@success="onNewWebhookSuccess"
|
||
/>
|
||
<ActivateWebhookDialog
|
||
v-if="showActivateWebhookDialog"
|
||
v-model="showActivateWebhookDialog"
|
||
@success="onWebHookActivated"
|
||
:webhook="selectedWebhook"
|
||
/>
|
||
<EditWebhookDialog
|
||
v-if="showEditWebhookDialog"
|
||
v-model="showEditWebhookDialog"
|
||
@success="onWebHookUpdated"
|
||
:webhook="selectedWebhook"
|
||
/>
|
||
<WebhookAttemptsDialog
|
||
v-if="showWebhookAttempts"
|
||
v-model="showWebhookAttempts"
|
||
:name="selectedWebhook.name"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { Badge, createResource } from 'jingrow-ui';
|
||
import { toast } from 'vue-sonner';
|
||
import { computed, h, onMounted, ref } from 'vue';
|
||
import { confirmDialog, icon } from '../../utils/components';
|
||
import ObjectList from '../ObjectList.vue';
|
||
import { getTeam } from '../../data/team';
|
||
import { date } from '../../utils/format';
|
||
import ClickToCopyField from '../ClickToCopyField.vue';
|
||
import AddNewWebhookDialog from './AddNewWebhookDialog.vue';
|
||
import ActivateWebhookDialog from './ActivateWebhookDialog.vue';
|
||
import EditWebhookDialog from './EditWebhookDialog.vue';
|
||
import { useRouter } from 'vue-router';
|
||
import WebhookAttemptsDialog from './WebhookAttemptsDialog.vue';
|
||
import { session } from '../../data/session';
|
||
|
||
const $team = getTeam();
|
||
const router = useRouter();
|
||
let showCreateSecretDialog = ref(false);
|
||
const showAddWebhookDialog = ref(false);
|
||
const showActivateWebhookDialog = ref(false);
|
||
const showEditWebhookDialog = ref(false);
|
||
const showWebhookAttempts = ref(false);
|
||
const selectedWebhook = ref(null);
|
||
|
||
const createSecret = createResource({
|
||
url: 'jcloud.api.account.create_api_secret',
|
||
onSuccess() {
|
||
if ($team.pg.user_info.api_key) {
|
||
toast.success('API 密钥已成功重新生成');
|
||
} else {
|
||
toast.success('API 密钥已成功创建');
|
||
}
|
||
}
|
||
});
|
||
|
||
const addSSHKey = createResource({
|
||
url: 'jcloud.api.client.insert',
|
||
onSuccess() {
|
||
toast.success('SSH 密钥已成功添加');
|
||
},
|
||
onError(err) {
|
||
toast.error(
|
||
err.messages.length
|
||
? err.messages.join('\n')
|
||
: 'SSH 密钥无法添加'
|
||
);
|
||
}
|
||
});
|
||
|
||
const makeKeyDefault = createResource({
|
||
url: 'jcloud.api.account.mark_key_as_default',
|
||
onSuccess() {
|
||
toast.success('SSH 密钥已成功更新');
|
||
},
|
||
onError(err) {
|
||
toast.error(
|
||
err.messages.length
|
||
? err.messages.join('\n')
|
||
: 'SSH 密钥无法设置为默认'
|
||
);
|
||
}
|
||
});
|
||
|
||
const deleteSSHKey = createResource({
|
||
url: 'jcloud.api.client.delete',
|
||
onSuccess() {
|
||
toast.success('SSH 密钥已成功删除');
|
||
},
|
||
onError(err) {
|
||
toast.error(
|
||
err.messages.length
|
||
? err.messages.join('\n')
|
||
: 'SSH 密钥无法删除'
|
||
);
|
||
}
|
||
});
|
||
|
||
const sshKeyListOptions = computed(() => ({
|
||
resource() {
|
||
return {
|
||
url: 'jcloud.api.account.get_user_ssh_keys',
|
||
initialData: [],
|
||
auto: true
|
||
};
|
||
},
|
||
columns: [
|
||
{
|
||
label: 'SSH 指纹',
|
||
fieldname: 'ssh_fingerprint',
|
||
class: 'font-mono',
|
||
format: value => `SHA256:${value}`,
|
||
suffix(row) {
|
||
return row.is_default
|
||
? h(Badge, {
|
||
label: '默认',
|
||
theme: 'green',
|
||
class: 'ml-2'
|
||
})
|
||
: null;
|
||
}
|
||
},
|
||
{
|
||
label: '添加时间',
|
||
fieldname: 'creation',
|
||
width: 0.1,
|
||
format: value => date(value, 'll')
|
||
}
|
||
],
|
||
primaryAction({ listResource }) {
|
||
return {
|
||
label: '添加 SSH 密钥',
|
||
slots: { prefix: icon('plus') },
|
||
onClick: () => renderAddNewKeyDialog(listResource)
|
||
};
|
||
},
|
||
rowActions({ row }) {
|
||
return [
|
||
{
|
||
label: '设为默认',
|
||
condition: () => !row.is_default,
|
||
onClick() {
|
||
makeKeyDefault.submit({
|
||
key_name: row.name
|
||
});
|
||
}
|
||
},
|
||
{
|
||
label: '删除',
|
||
onClick() {
|
||
deleteSSHKey.submit({
|
||
pagetype: 'User SSH Key',
|
||
name: row.name
|
||
});
|
||
}
|
||
}
|
||
];
|
||
}
|
||
}));
|
||
|
||
function renderAddNewKeyDialog(listResource) {
|
||
confirmDialog({
|
||
title: '添加新 SSH 密钥',
|
||
message: '向您的账户添加新的 SSH 密钥',
|
||
fields: [
|
||
{
|
||
label: 'SSH 密钥',
|
||
fieldname: 'sshKey',
|
||
type: 'textarea',
|
||
placeholder:
|
||
"以 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', 'sk-ecdsa-sha2-nistp256@openssh.com', 或 'sk-ssh-ed25519@openssh.com' 开头",
|
||
required: true
|
||
}
|
||
],
|
||
primaryAction: {
|
||
label: '添加 SSH 密钥',
|
||
variant: 'solid',
|
||
onClick({ hide, values }) {
|
||
if (!values.sshKey) throw new Error('SSH 密钥是必填项');
|
||
addSSHKey
|
||
.submit({
|
||
pg: {
|
||
pagetype: 'User SSH Key',
|
||
ssh_public_key: values.sshKey,
|
||
user: $team.pg.user_info.name
|
||
}
|
||
})
|
||
.then(() => {
|
||
listResource.reload();
|
||
hide();
|
||
})
|
||
.catch(error => {
|
||
toast.error(error.message);
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
const webhookListResource = createResource({
|
||
url: 'jcloud.api.client.get_list',
|
||
params: {
|
||
pagetype: 'Jcloud Webhook',
|
||
fields: ['name', 'enabled', 'endpoint']
|
||
},
|
||
initialData: [],
|
||
auto: false
|
||
});
|
||
|
||
const deleteWebhook = createResource({
|
||
url: 'jcloud.api.client.delete',
|
||
onSuccess() {
|
||
toast.success('Webhook 删除成功');
|
||
webhookListResource.reload();
|
||
},
|
||
onError(err) {
|
||
toast.error(
|
||
err.messages.length
|
||
? err.messages.join('\n')
|
||
: 'Webhook 无法删除'
|
||
);
|
||
}
|
||
});
|
||
|
||
const webhookListOptions = computed(() => ({
|
||
data: () => webhookListResource.data,
|
||
columns: [
|
||
{
|
||
label: '端点',
|
||
fieldname: 'endpoint',
|
||
width: 1,
|
||
format: value => value.substring(0, 50)
|
||
},
|
||
{
|
||
label: '状态',
|
||
fieldname: 'enabled',
|
||
width: 0.1,
|
||
type: 'Component',
|
||
align: 'right',
|
||
component({ row }) {
|
||
return row.enabled
|
||
? h(Badge, {
|
||
label: '启用',
|
||
theme: 'green'
|
||
})
|
||
: h(Badge, {
|
||
label: '禁用',
|
||
theme: 'red'
|
||
});
|
||
}
|
||
}
|
||
],
|
||
rowActions({ row }) {
|
||
return [
|
||
{
|
||
label: '激活',
|
||
condition: () => !Boolean(row.enabled),
|
||
onClick() {
|
||
selectedWebhook.value = row;
|
||
showActivateWebhookDialog.value = true;
|
||
}
|
||
},
|
||
{
|
||
label: '禁用',
|
||
condition: () => Boolean(row.enabled),
|
||
onClick: () => {
|
||
confirmDialog({
|
||
title: '禁用 Webhook',
|
||
message: `端点 - ${row.endpoint}<br>确定要禁用该 webhook 吗?<br>`,
|
||
primaryAction: {
|
||
label: '禁用',
|
||
variant: 'solid',
|
||
theme: 'red',
|
||
onClick({ hide }) {
|
||
disableWebhook
|
||
.submit({
|
||
dt: 'Jcloud Webhook',
|
||
dn: row.name,
|
||
method: 'disable'
|
||
})
|
||
.then(hide);
|
||
return disableWebhook.promise;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
},
|
||
{
|
||
label: '尝试',
|
||
onClick: () => {
|
||
selectedWebhook.value = row;
|
||
showWebhookAttempts.value = true;
|
||
}
|
||
},
|
||
{
|
||
label: '编辑',
|
||
onClick() {
|
||
selectedWebhook.value = row;
|
||
showEditWebhookDialog.value = true;
|
||
}
|
||
},
|
||
{
|
||
label: '删除',
|
||
onClick() {
|
||
confirmDialog({
|
||
title: '删除 Webhook',
|
||
message: `端点 - ${row.endpoint}<br>确定要删除该 webhook 吗?<br>`,
|
||
primaryAction: {
|
||
label: '删除',
|
||
variant: 'solid',
|
||
theme: 'red',
|
||
onClick({ hide }) {
|
||
deleteWebhook
|
||
.submit({
|
||
pagetype: 'Jcloud Webhook',
|
||
name: row.name
|
||
})
|
||
.then(hide);
|
||
return deleteWebhook.promise;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
];
|
||
},
|
||
primaryAction() {
|
||
return {
|
||
label: '添加 Webhook',
|
||
slots: { prefix: icon('plus') },
|
||
onClick: () => (showAddWebhookDialog.value = true)
|
||
};
|
||
},
|
||
secondaryAction() {
|
||
return {
|
||
label: '刷新',
|
||
icon: 'refresh-ccw',
|
||
onClick: () => webhookListResource.reload()
|
||
};
|
||
}
|
||
}));
|
||
|
||
const disableWebhook = createResource({
|
||
url: 'jcloud.api.client.run_pg_method',
|
||
onSuccess() {
|
||
toast.success('Webhook 禁用成功');
|
||
webhookListResource.reload();
|
||
},
|
||
onError(err) {
|
||
toast.error(
|
||
err.messages.length
|
||
? err.messages.join('\n')
|
||
: 'Webhook 无法禁用'
|
||
);
|
||
}
|
||
});
|
||
|
||
const onNewWebhookSuccess = () => {
|
||
webhookListResource.reload();
|
||
showAddWebhookDialog.value = false;
|
||
};
|
||
|
||
const onWebHookActivated = () => {
|
||
webhookListResource.reload();
|
||
showActivateWebhookDialog.value = false;
|
||
};
|
||
|
||
const onWebHookUpdated = activationRequired => {
|
||
webhookListResource.reload();
|
||
showEditWebhookDialog.value = false;
|
||
if (activationRequired) {
|
||
showActivateWebhookDialog.value = true;
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
if (session.hasWebhookConfigurationAccess) {
|
||
webhookListResource.fetch();
|
||
}
|
||
});
|
||
</script> |