dev #3

Merged
jingrow merged 96 commits from dev into main 2026-01-13 22:47:33 +08:00
5 changed files with 1240 additions and 485 deletions
Showing only changes of commit 8b617d833b - Show all commits

View File

@ -1,83 +1,175 @@
<template>
<Dialog
:options="{
title: '激活Webhook'
}"
<n-modal
v-model:show="showDialog"
preset="card"
:title="$t('Activate Webhook')"
:style="modalStyle"
:mask-closable="true"
:close-on-esc="true"
class="activate-webhook-modal"
>
<template #body-content>
<div class="space-y-4">
<FormControl label="端点" v-model="webhook.endpoint" disabled />
<div v-if="request">
<p class="text-xs text-gray-600">请求</p>
<pre
class="mt-2 whitespace-pre-wrap rounded bg-gray-50 px-2 py-1.5 text-sm text-gray-600"
>{{ request }}</pre
>
</div>
<FormControl
v-if="response_status_code"
label="响应状态码"
v-model="response_status_code"
<template #header>
<span class="text-lg font-semibold">{{ $t('Activate Webhook') }}</span>
</template>
<n-space vertical :size="20">
<n-form-item :label="$t('Endpoint')">
<n-input
:value="webhook.endpoint"
disabled
:size="inputSize"
class="w-full"
/>
<div v-if="response">
<p class="text-xs text-gray-600">响应</p>
<pre
class="mt-2 max-h-52 overflow-y-auto whitespace-pre-wrap rounded bg-gray-50 px-2 py-1.5 text-sm text-gray-600"
>{{ response }}</pre
>
</div>
<div class="flex items-center" v-if="validated">
<ILucideCheck class="h-4 text-green-600" />
<div class="ml-2 text-sm font-medium text-gray-700">
端点已验证
</div>
</div>
<ErrorMessage :message="errorMessage" />
</n-form-item>
<div v-if="request">
<p class="text-xs text-gray-600 mb-2">{{ $t('Request') }}</p>
<pre class="mt-2 whitespace-pre-wrap rounded bg-gray-50 px-3 py-2 text-sm text-gray-700 border border-gray-200">
{{ request }}
</pre>
</div>
<n-form-item v-if="response_status_code" :label="$t('Response Status Code')">
<n-input
:value="response_status_code"
disabled
:size="inputSize"
class="w-full"
/>
</n-form-item>
<div v-if="response">
<p class="text-xs text-gray-600 mb-2">{{ $t('Response') }}</p>
<pre class="mt-2 max-h-52 overflow-y-auto whitespace-pre-wrap rounded bg-gray-50 px-3 py-2 text-sm text-gray-700 border border-gray-200">
{{ response }}
</pre>
</div>
<div class="flex items-center" v-if="validated">
<n-icon class="h-5 w-5 text-green-600 mr-2">
<CheckIcon />
</n-icon>
<div class="text-sm font-medium text-gray-700">
{{ $t('Endpoint validated') }}
</div>
</div>
<n-alert v-if="errorMessage" type="error" :title="errorMessage" />
</n-space>
<template #action>
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
<n-button @click="hide" :block="isMobile" :size="buttonSize">
{{ $t('Cancel') }}
</n-button>
<n-button
v-if="!validated"
type="primary"
:loading="$resources.validateEndpoint.loading"
@click="$resources.validateEndpoint.submit()"
:block="isMobile"
:size="buttonSize"
>
{{ $t('Validate Webhook') }}
</n-button>
<n-button
v-if="validated"
type="primary"
:loading="$resources.activateWebhook.loading"
@click="$resources.activateWebhook.submit()"
:block="isMobile"
:size="buttonSize"
>
{{ $t('Activate Webhook') }}
</n-button>
</n-space>
</template>
<template v-slot:actions>
<Button
class="w-full"
theme="gray"
variant="solid"
@click="$resources.validateEndpoint.submit()"
:loading="$resources.validateEndpoint.loading"
loadingText="正在验证Webhook"
v-if="!validated"
>验证Webhook</Button
>
<Button
class="w-full"
theme="gray"
variant="solid"
@click="$resources.activateWebhook.submit()"
:loading="$resources.activateWebhook.loading"
loadingText="正在激活Webhook"
v-if="validated"
>激活Webhook</Button
>
</template>
</Dialog>
</n-modal>
</template>
<script>
import {
NModal,
NFormItem,
NInput,
NSpace,
NAlert,
NButton,
NIcon,
} from 'naive-ui';
import { toast } from 'vue-sonner';
import CheckIcon from '~icons/lucide/check';
export default {
emits: ['success'],
props: ['webhook'],
components: {
NModal,
NFormItem,
NInput,
NSpace,
NAlert,
NButton,
NIcon,
CheckIcon,
},
emits: ['success', 'update:modelValue'],
props: {
webhook: {
type: Object,
required: true
},
modelValue: {
type: Boolean,
default: false
}
},
data() {
return {
errorMessage: '',
validated: false,
request: null,
response: null,
response_status_code: null
response_status_code: null,
windowWidth: window.innerWidth,
};
},
computed: {
showDialog: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
}
},
isMobile() {
return this.windowWidth <= 768;
},
modalStyle() {
return {
width: this.isMobile ? '95vw' : '800px',
maxWidth: this.isMobile ? '95vw' : '90vw',
};
},
inputSize() {
return this.isMobile ? 'medium' : 'large';
},
buttonSize() {
return this.isMobile ? 'medium' : 'medium';
},
},
mounted() {
this.handleResize();
window.addEventListener('resize', this.handleResize);
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
},
methods: {
handleResize() {
this.windowWidth = window.innerWidth;
},
hide() {
this.showDialog = false;
this.errorMessage = '';
this.validated = false;
this.request = null;
this.response = null;
this.response_status_code = null;
}
},
resources: {
validateEndpoint() {
return {
@ -99,9 +191,7 @@ export default {
this.validated = true;
} else {
this.validated = false;
this.errorMessage =
'端点应返回200到300之间的状态\n请检查端点并重试';
this.errorMessage = this.$t('Endpoint should return a status code between 200 and 300. Please check the endpoint and try again.');
}
},
onError: e => {
@ -121,7 +211,8 @@ export default {
};
},
onSuccess(e) {
toast.success('Webhook激活成功');
toast.success(this.$t('Webhook activated successfully'));
this.hide();
this.$emit('success');
},
onError(e) {
@ -132,4 +223,55 @@ export default {
}
}
};
</script>
</script>
<style scoped>
:deep(.activate-webhook-modal .n-card) {
width: 800px;
max-width: 90vw;
}
:deep(.activate-webhook-modal .n-card-body) {
padding: 24px;
}
:deep(.activate-webhook-modal .n-form-item) {
margin-bottom: 0;
}
:deep(.activate-webhook-modal .n-input) {
width: 100%;
}
:deep(.activate-webhook-modal .n-card__action) {
padding: 16px 24px;
}
@media (max-width: 768px) {
:deep(.activate-webhook-modal .n-card) {
width: 95vw !important;
max-width: 95vw !important;
margin: 20px auto;
border-radius: 12px;
}
:deep(.activate-webhook-modal .n-card-body) {
padding: 20px 16px;
}
:deep(.activate-webhook-modal .n-card__header) {
padding: 16px;
font-size: 18px;
}
:deep(.activate-webhook-modal .n-card__action) {
padding: 12px 16px;
}
:deep(.activate-webhook-modal .n-button) {
width: 100%;
font-size: 16px;
height: 44px;
}
}
</style>

View File

@ -1,114 +1,179 @@
<template>
<Dialog
:options="{
title: $t('Add New Webhook'),
actions: [
{
label: $t('Add Webhook'),
variant: 'solid',
onClick: addWebhook,
loading: $resources?.addWebhook?.loading
}
]
}"
<n-modal
v-model:show="showDialog"
preset="card"
:title="$t('Add New Webhook')"
:style="modalStyle"
:mask-closable="true"
:close-on-esc="true"
class="add-webhook-modal"
>
<template #body-content>
<div class="space-y-4">
<FormControl :label="$t('Endpoint')" v-model="endpoint" />
<div>
<FormControl :label="$t('Secret Key')" v-model="secret">
<template #suffix>
<FeatherIcon
class="w-4 cursor-pointer"
name="refresh-ccw"
@click="generateRandomSecret"
/>
</template>
</FormControl>
<p class="mt-2 text-sm text-gray-700">
<strong>{{ $t('Note') }}:</strong> {{ $t('The secret key is optional. View') }}
<a href="https://jingrow.com/docs/webhook-introduction" class="underline" target="_blank"
>{{ $t('documentation') }}</a
<template #header>
<span class="text-lg font-semibold">{{ $t('Add New Webhook') }}</span>
</template>
<n-space vertical :size="20">
<n-form-item :label="$t('Endpoint')" :required="true">
<n-input
v-model:value="endpoint"
:placeholder="$t('Enter webhook endpoint URL')"
:size="inputSize"
class="w-full"
/>
</n-form-item>
<n-form-item :label="$t('Secret Key')">
<n-input
v-model:value="secret"
:placeholder="$t('Enter secret key (optional)')"
:size="inputSize"
class="w-full"
>
<template #suffix>
<n-icon
class="cursor-pointer text-gray-500 hover:text-gray-700"
@click="generateRandomSecret"
>
{{ $t('to learn more') }}
</p>
</div>
<p class="text-base font-medium text-gray-900">
<RefreshIcon />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-alert type="info" class="mb-2">
<template #header>
<strong>{{ $t('Note') }}:</strong>
</template>
{{ $t('The secret key is optional. View') }}
<a href="https://jingrow.com/docs/webhook-introduction" class="text-primary underline" target="_blank">
{{ $t('documentation') }}
</a>
{{ $t('to learn more') }}
</n-alert>
<div class="mt-4">
<p class="text-base font-medium text-gray-900 mb-4">
{{ $t('Select Webhook Events') }}
</p>
<div
class="text-center text-sm leading-10 text-gray-500"
v-if="$resources.events.loading"
>
{{ $t('Loading...') }}
</div>
<div class="mt-6 flex flex-col gap-4" v-else>
<Switch
v-for="event in localizedEvents"
:key="event.name"
:label="event.title || event.name"
:description="event.description"
:modelValue="isEventSelected(event.name)"
@update:modelValue="selectEvent(event.name)"
size="sm"
/>
</div>
<ErrorMessage :message="errorMessage || $resources.addWebhook.error" />
<n-spin :show="$resources.events.loading">
<n-space vertical :size="12" v-if="!$resources.events.loading">
<div
v-for="event in localizedEvents"
:key="event.name"
class="flex items-center justify-between p-3 rounded border border-gray-200 hover:bg-gray-50"
>
<div class="flex-1">
<div class="text-sm font-medium text-gray-900">
{{ event.title || event.name }}
</div>
<div v-if="event.description" class="text-xs text-gray-500 mt-1">
{{ event.description }}
</div>
</div>
<n-switch
:value="isEventSelected(event.name)"
@update:value="selectEvent(event.name)"
size="medium"
/>
</div>
</n-space>
</n-spin>
</div>
<n-alert
v-if="errorMessage || $resources.addWebhook.error"
type="error"
:title="errorMessage || $resources.addWebhook.error"
/>
</n-space>
<template #action>
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
<n-button @click="hide" :block="isMobile" :size="buttonSize">
{{ $t('Cancel') }}
</n-button>
<n-button
type="primary"
:loading="$resources.addWebhook.loading"
@click="addWebhook"
:block="isMobile"
:size="buttonSize"
>
{{ $t('Add Webhook') }}
</n-button>
</n-space>
</template>
</Dialog>
</n-modal>
</template>
<script>
import {
NModal,
NFormItem,
NInput,
NSpace,
NAlert,
NButton,
NSwitch,
NIcon,
NSpin,
} from 'naive-ui';
import { toast } from 'vue-sonner';
import RefreshIcon from '~icons/lucide/refresh-ccw';
export default {
emits: ['success'],
components: {
NModal,
NFormItem,
NInput,
NSpace,
NAlert,
NButton,
NSwitch,
NIcon,
NSpin,
RefreshIcon,
},
emits: ['success', 'update:modelValue'],
props: {
modelValue: {
type: Boolean,
default: false
}
},
data() {
return {
endpoint: '',
secret: '',
selectedEvents: [],
errorMessage: ''
errorMessage: '',
windowWidth: window.innerWidth,
};
},
mounted() {
if (this.selectedEvents.length) {
this.selectedEvents = this.selectedEvents.map(event => event.name);
}
},
resources: {
events() {
computed: {
showDialog: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
}
},
isMobile() {
return this.windowWidth <= 768;
},
modalStyle() {
return {
url: 'jcloud.api.webhook.available_events',
inititalData: [],
auto: true
width: this.isMobile ? '95vw' : '800px',
maxWidth: this.isMobile ? '95vw' : '90vw',
};
},
addWebhook() {
return {
url: 'jcloud.api.webhook.add',
params: {
endpoint: this.endpoint,
secret: this.secret,
events: this.selectedEvents
},
onSuccess() {
toast.success(this.$t('Webhook added successfully'));
this.$emit('success');
}
};
}
},
computed: {
inputSize() {
return this.isMobile ? 'medium' : 'large';
},
buttonSize() {
return this.isMobile ? 'medium' : 'medium';
},
localizedEvents() {
if (!this.$resources.events.data) return [];
return this.$resources.events.data.map(event => {
//
const localizedEvent = { ...event };
//
if (localizedEvent.name === 'Site Status Update') {
localizedEvent.title = this.$t('Site Status Update');
localizedEvent.description = this.$t('Pending, Installing, Updating, Active, Inactive, Abnormal, Archived, Paused');
@ -116,7 +181,6 @@ export default {
localizedEvent.title = this.$t('Site Plan Change');
localizedEvent.description = this.$t('Get notifications for site subscription plan changes');
} else {
// title使namedescription使
if (!localizedEvent.title) {
localizedEvent.title = localizedEvent.name;
}
@ -129,7 +193,20 @@ export default {
});
}
},
mounted() {
this.handleResize();
window.addEventListener('resize', this.handleResize);
if (this.selectedEvents.length) {
this.selectedEvents = this.selectedEvents.map(event => event.name);
}
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
},
methods: {
handleResize() {
this.windowWidth = window.innerWidth;
},
generateRandomSecret() {
this.secret = Array(30)
.fill(0)
@ -162,7 +239,89 @@ export default {
}
this.errorMessage = '';
this.$resources.addWebhook.submit();
},
hide() {
this.showDialog = false;
this.endpoint = '';
this.secret = '';
this.selectedEvents = [];
this.errorMessage = '';
}
},
resources: {
events() {
return {
url: 'jcloud.api.webhook.available_events',
inititalData: [],
auto: true
};
},
addWebhook() {
return {
url: 'jcloud.api.webhook.add',
params: {
endpoint: this.endpoint,
secret: this.secret,
events: this.selectedEvents
},
onSuccess() {
toast.success(this.$t('Webhook added successfully'));
this.hide();
this.$emit('success');
}
};
}
}
};
</script>
</script>
<style scoped>
:deep(.add-webhook-modal .n-card) {
width: 800px;
max-width: 90vw;
}
:deep(.add-webhook-modal .n-card-body) {
padding: 24px;
}
:deep(.add-webhook-modal .n-form-item) {
margin-bottom: 0;
}
:deep(.add-webhook-modal .n-input) {
width: 100%;
}
:deep(.add-webhook-modal .n-card__action) {
padding: 16px 24px;
}
@media (max-width: 768px) {
:deep(.add-webhook-modal .n-card) {
width: 95vw !important;
max-width: 95vw !important;
margin: 20px auto;
border-radius: 12px;
}
:deep(.add-webhook-modal .n-card-body) {
padding: 20px 16px;
}
:deep(.add-webhook-modal .n-card__header) {
padding: 16px;
font-size: 18px;
}
:deep(.add-webhook-modal .n-card__action) {
padding: 12px 16px;
}
:deep(.add-webhook-modal .n-button) {
width: 100%;
font-size: 16px;
height: 44px;
}
}
</style>

View File

@ -1,103 +1,220 @@
<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">{{ $t('API Access') }}</div>
<Button @click="showCreateSecretDialog = true">{{
apiKeyButtonLabel
}}</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">
{{ $t("You don't have an API key yet. Click the button above to create one.") }}
</div>
<Dialog
v-model="showCreateSecretDialog"
:options="apiKeyDialogOptions"
>
<template #body-content>
<div v-if="createSecret.data">
<p class="text-base">
{{ $t('Please copy the API key immediately. You will not be able to view it again!') }}
</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">
<div class="developer-settings-container">
<n-space vertical :size="24">
<!-- API Access 卡片 -->
<n-card :title="$t('API Access')" class="settings-card">
<n-space vertical :size="20">
<div class="flex items-center justify-between flex-wrap gap-4">
<div class="text-base text-gray-700">
{{ $t('API key and API secret can be used to access') }}
<a href="/docs/api" class="underline" target="_blank"
>{{ $t('Jingrow API') }}</a
>.
<a href="/docs/api" class="text-primary underline" target="_blank">
{{ $t('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">{{ $t('SSH Keys') }}</div>
</div>
<n-button type="primary" @click="showCreateSecretDialog = true" :size="buttonSize">
{{ apiKeyButtonLabel }}
</n-button>
</div>
<div v-if="$team.pg?.user_info?.api_key">
<ClickToCopyField :textContent="$team.pg.user_info.api_key" />
</div>
<div v-else class="text-base text-gray-600">
{{ $t("You don't have an API key yet. Click the button above to create one.") }}
</div>
</n-space>
</n-card>
<!-- SSH Keys 卡片 -->
<n-card :title="$t('SSH Keys')" class="settings-card">
<ObjectList :options="sshKeyListOptions" />
</div>
<div
</n-card>
<!-- Webhooks 卡片 -->
<n-card
v-if="$session.hasWebhookConfigurationAccess"
class="mx-auto min-w-[48rem] max-w-3xl space-y-6 rounded-md border p-4"
:title="$t('Webhooks')"
class="settings-card"
>
<div class="flex items-center justify-between">
<div class="text-xl font-semibold">{{ $t('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"
/>
</n-card>
</n-space>
<!-- API Key 创建/重新生成弹窗 -->
<n-modal
v-model:show="showCreateSecretDialog"
preset="card"
:title="$t('API Access')"
:style="modalStyle"
:mask-closable="true"
:close-on-esc="true"
class="api-key-modal"
>
<template #header>
<span class="text-lg font-semibold">{{ $t('API Access') }}</span>
</template>
<n-space vertical :size="20">
<div v-if="createSecret.data">
<n-alert type="warning" class="mb-4">
{{ $t('Please copy the API key immediately. You will not be able to view it again!') }}
</n-alert>
<n-form-item :label="$t('API Key')">
<ClickToCopyField :textContent="createSecret.data.api_key" />
</n-form-item>
<n-form-item :label="$t('API Secret')">
<ClickToCopyField :textContent="createSecret.data.api_secret" />
</n-form-item>
</div>
<div v-else class="text-base text-gray-700">
{{ $t('API key and API secret can be used to access') }}
<a href="/docs/api" class="text-primary underline" target="_blank">
{{ $t('Jingrow API') }}
</a>.
</div>
</n-space>
<template #action>
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
<n-button @click="showCreateSecretDialog = false" :block="isMobile" :size="buttonSize">
{{ $t('Cancel') }}
</n-button>
<n-button
type="primary"
:loading="createSecret.loading"
:disabled="!!createSecret.data"
@click="createSecret.submit()"
:block="isMobile"
:size="buttonSize"
>
{{ $team.pg.user_info.api_key ? $t('Regenerate API Key') : $t('Create New API Key') }}
</n-button>
</n-space>
</template>
</n-modal>
<!-- SSH Key 添加弹窗 -->
<n-modal
v-model:show="showAddSSHKeyDialog"
preset="card"
:title="$t('Add New SSH Key')"
:style="modalStyle"
:mask-closable="true"
:close-on-esc="true"
class="ssh-key-modal"
>
<template #header>
<span class="text-lg font-semibold">{{ $t('Add New SSH Key') }}</span>
</template>
<n-space vertical :size="20">
<p class="text-base text-gray-700">{{ $t('Add a new SSH key to your account') }}</p>
<n-form-item :label="$t('SSH Key')" :required="true">
<n-input
v-model:value="sshKeyValue"
type="textarea"
:placeholder="sshKeyPlaceholder"
:rows="6"
:size="inputSize"
class="w-full"
/>
</n-form-item>
<n-alert v-if="sshKeyError" type="error" :title="sshKeyError" />
</n-space>
<template #action>
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
<n-button @click="showAddSSHKeyDialog = false" :block="isMobile" :size="buttonSize">
{{ $t('Cancel') }}
</n-button>
<n-button
type="primary"
:loading="addSSHKey.loading"
@click="handleAddSSHKey"
:block="isMobile"
:size="buttonSize"
>
{{ $t('Add SSH Key') }}
</n-button>
</n-space>
</template>
</n-modal>
<!-- Webhook 禁用确认弹窗 -->
<n-modal
v-model:show="showDisableWebhookDialog"
preset="dialog"
:title="$t('Disable Webhook')"
:positive-text="$t('Disable')"
:positive-button-props="{ type: 'error' }"
:loading="disableWebhook.loading"
:mask-closable="true"
:close-on-esc="true"
@positive-click="handleDisableWebhook"
>
<div class="py-4">
<p class="text-base mb-2">
{{ $t('Endpoint') }}: <strong>{{ selectedWebhookForAction?.endpoint }}</strong>
</p>
<p class="text-base">{{ $t('Are you sure you want to disable this webhook?') }}</p>
</div>
</div>
</n-modal>
<!-- Webhook 删除确认弹窗 -->
<n-modal
v-model:show="showDeleteWebhookDialog"
preset="dialog"
:title="$t('Delete Webhook')"
:positive-text="$t('Delete')"
:positive-button-props="{ type: 'error' }"
:loading="deleteWebhook.loading"
:mask-closable="true"
:close-on-esc="true"
@positive-click="handleDeleteWebhook"
>
<div class="py-4">
<p class="text-base mb-2">
{{ $t('Endpoint') }}: <strong>{{ selectedWebhookForAction?.endpoint }}</strong>
</p>
<p class="text-base">{{ $t('Are you sure you want to delete this webhook?') }}</p>
</div>
</n-modal>
<!-- Webhook 相关弹窗 -->
<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>
</template>
<script setup>
import {
NCard,
NSpace,
NButton,
NModal,
NFormItem,
NInput,
NAlert,
} from 'naive-ui';
import { Badge, createResource } from 'jingrow-ui';
import { toast } from 'vue-sonner';
import { computed, h, onMounted, ref, getCurrentInstance } from 'vue';
import { confirmDialog, icon } from '../../utils/components';
import { icon } from '../../utils/components';
import ObjectList from '../ObjectList.vue';
import { getTeam } from '../../data/team';
import { date } from '../../utils/format';
@ -115,11 +232,29 @@ const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
const $team = getTeam();
const router = useRouter();
let showCreateSecretDialog = ref(false);
const showAddSSHKeyDialog = ref(false);
const sshKeyValue = ref('');
const sshKeyError = ref('');
const showAddWebhookDialog = ref(false);
const showActivateWebhookDialog = ref(false);
const showEditWebhookDialog = ref(false);
const showWebhookAttempts = ref(false);
const selectedWebhook = ref(null);
const selectedWebhookForAction = ref(null);
const showDisableWebhookDialog = ref(false);
const showDeleteWebhookDialog = ref(false);
const windowWidth = ref(window.innerWidth);
const isMobile = computed(() => windowWidth.value <= 768);
const modalStyle = computed(() => ({
width: isMobile.value ? '95vw' : '700px',
maxWidth: isMobile.value ? '95vw' : '90vw',
}));
const inputSize = computed(() => (isMobile.value ? 'medium' : 'large'));
const buttonSize = computed(() => (isMobile.value ? 'medium' : 'medium'));
const sshKeyPlaceholder = computed(() => {
return $t("Starts with 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', 'sk-ecdsa-sha2-nistp256@openssh.com', or 'sk-ssh-ed25519@openssh.com'");
});
const createSecret = createResource({
url: 'jcloud.api.account.create_api_secret',
@ -129,6 +264,7 @@ const createSecret = createResource({
} else {
toast.success($t('API key created successfully'));
}
$team.reload();
}
});
@ -136,6 +272,9 @@ const addSSHKey = createResource({
url: 'jcloud.api.client.insert',
onSuccess() {
toast.success($t('SSH key added successfully'));
showAddSSHKeyDialog.value = false;
sshKeyValue.value = '';
sshKeyError.value = '';
},
onError(err) {
toast.error(
@ -209,7 +348,10 @@ const sshKeyListOptions = computed(() => ({
return {
label: $t('Add SSH Key'),
slots: { prefix: icon('plus') },
onClick: () => renderAddNewKeyDialog(listResource)
onClick: () => {
showAddSSHKeyDialog.value = true;
sshKeyListResource = listResource;
}
};
},
rowActions({ row }) {
@ -236,42 +378,30 @@ const sshKeyListOptions = computed(() => ({
}
}));
function renderAddNewKeyDialog(listResource) {
confirmDialog({
title: $t('Add New SSH Key'),
message: $t('Add a new SSH key to your account'),
fields: [
{
label: $t('SSH Key'),
fieldname: 'sshKey',
type: 'textarea',
placeholder: $t("Starts with 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', 'sk-ecdsa-sha2-nistp256@openssh.com', or 'sk-ssh-ed25519@openssh.com'"),
required: true
let sshKeyListResource = null;
function handleAddSSHKey() {
if (!sshKeyValue.value) {
sshKeyError.value = $t('SSH key is required');
return;
}
sshKeyError.value = '';
addSSHKey
.submit({
pg: {
pagetype: 'User SSH Key',
ssh_public_key: sshKeyValue.value,
user: $team.pg.user_info.name
}
],
primaryAction: {
label: $t('Add SSH Key'),
variant: 'solid',
onClick({ hide, values }) {
if (!values.sshKey) throw new Error($t('SSH key is required'));
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);
});
})
.then(() => {
if (sshKeyListResource) {
sshKeyListResource.reload();
}
}
});
})
.catch(error => {
sshKeyError.value = error.message;
});
}
const webhookListResource = createResource({
@ -289,6 +419,7 @@ const deleteWebhook = createResource({
onSuccess() {
toast.success($t('Webhook deleted successfully'));
webhookListResource.reload();
showDeleteWebhookDialog.value = false;
},
onError(err) {
toast.error(
@ -341,25 +472,8 @@ const webhookListOptions = computed(() => ({
label: $t('Disable'),
condition: () => Boolean(row.enabled),
onClick: () => {
confirmDialog({
title: $t('Disable Webhook'),
message: $t('Endpoint - {endpoint}<br>Are you sure you want to disable this webhook?<br>', { endpoint: row.endpoint }),
primaryAction: {
label: $t('Disable'),
variant: 'solid',
theme: 'red',
onClick({ hide }) {
disableWebhook
.submit({
dt: 'Jcloud Webhook',
dn: row.name,
method: 'disable'
})
.then(hide);
return disableWebhook.promise;
}
}
});
selectedWebhookForAction.value = row;
showDisableWebhookDialog.value = true;
}
},
{
@ -379,24 +493,8 @@ const webhookListOptions = computed(() => ({
{
label: $t('Delete'),
onClick() {
confirmDialog({
title: $t('Delete Webhook'),
message: $t('Endpoint - {endpoint}<br>Are you sure you want to delete this webhook?<br>', { endpoint: row.endpoint }),
primaryAction: {
label: $t('Delete'),
variant: 'solid',
theme: 'red',
onClick({ hide }) {
deleteWebhook
.submit({
pagetype: 'Jcloud Webhook',
name: row.name
})
.then(hide);
return deleteWebhook.promise;
}
}
});
selectedWebhookForAction.value = row;
showDeleteWebhookDialog.value = true;
}
}
];
@ -422,6 +520,7 @@ const disableWebhook = createResource({
onSuccess() {
toast.success($t('Webhook disabled successfully'));
webhookListResource.reload();
showDisableWebhookDialog.value = false;
},
onError(err) {
toast.error(
@ -432,6 +531,23 @@ const disableWebhook = createResource({
}
});
function handleDisableWebhook() {
if (!selectedWebhookForAction.value) return;
disableWebhook.submit({
dt: 'Jcloud Webhook',
dn: selectedWebhookForAction.value.name,
method: 'disable'
});
}
function handleDeleteWebhook() {
if (!selectedWebhookForAction.value) return;
deleteWebhook.submit({
pagetype: 'Jcloud Webhook',
name: selectedWebhookForAction.value.name
});
}
const onNewWebhookSuccess = () => {
webhookListResource.reload();
showAddWebhookDialog.value = false;
@ -443,7 +559,7 @@ const onWebHookActivated = () => {
};
const onWebHookUpdated = activationRequired => {
webhookListResource.reload();
webhookListResource.reload();
showEditWebhookDialog.value = false;
if (activationRequired) {
showActivateWebhookDialog.value = true;
@ -456,27 +572,104 @@ const apiKeyButtonLabel = computed(() => {
: $t('Create New API Key');
});
const apiKeyDialogOptions = computed(() => ({
title: $t('API Access'),
size: 'xl',
actions: [
{
label: $team.pg.user_info.api_key
? $t('Regenerate API Key')
: $t('Create New API Key'),
variant: 'solid',
disabled: !!createSecret.data,
loading: createSecret.loading,
onClick() {
createSecret.submit();
}
}
]
}));
function handleResize() {
windowWidth.value = window.innerWidth;
}
onMounted(() => {
handleResize();
window.addEventListener('resize', handleResize);
if (session.hasWebhookConfigurationAccess) {
webhookListResource.fetch();
}
});
</script>
</script>
<style scoped>
.developer-settings-container {
width: 100%;
padding: 24px;
}
.settings-card {
border-radius: 12px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
transition: box-shadow 0.2s ease;
}
.settings-card:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
:deep(.api-key-modal .n-card),
:deep(.ssh-key-modal .n-card) {
width: 700px;
max-width: 90vw;
}
:deep(.api-key-modal .n-card-body),
:deep(.ssh-key-modal .n-card-body) {
padding: 24px;
}
:deep(.api-key-modal .n-form-item),
:deep(.ssh-key-modal .n-form-item) {
margin-bottom: 0;
}
:deep(.api-key-modal .n-input),
:deep(.ssh-key-modal .n-input) {
width: 100%;
}
:deep(.api-key-modal .n-card__action),
:deep(.ssh-key-modal .n-card__action) {
padding: 16px 24px;
}
@media (max-width: 768px) {
.developer-settings-container {
padding: 16px;
}
.settings-card {
border-radius: 8px;
}
:deep(.api-key-modal .n-card),
:deep(.ssh-key-modal .n-card) {
width: 95vw !important;
max-width: 95vw !important;
margin: 20px auto;
border-radius: 12px;
}
:deep(.api-key-modal .n-card-body),
:deep(.ssh-key-modal .n-card-body) {
padding: 20px 16px;
}
:deep(.api-key-modal .n-card__header),
:deep(.ssh-key-modal .n-card__header) {
padding: 16px;
font-size: 18px;
}
:deep(.api-key-modal .n-card__action),
:deep(.ssh-key-modal .n-card__action) {
padding: 12px 16px;
}
:deep(.api-key-modal .n-space),
:deep(.ssh-key-modal .n-space) {
width: 100%;
}
:deep(.api-key-modal .n-button),
:deep(.ssh-key-modal .n-button) {
width: 100%;
font-size: 16px;
height: 44px;
}
}
</style>

View File

@ -1,104 +1,243 @@
<template>
<Dialog
:options="{
title: '编辑Webhook',
actions: [
{
label: '保存更改',
variant: 'solid',
onClick: updateWebhook,
loading: this.$resources.updateWebhook.loading
}
]
}"
<n-modal
v-model:show="showDialog"
preset="card"
:title="$t('Edit Webhook')"
:style="modalStyle"
:mask-closable="true"
:close-on-esc="true"
class="edit-webhook-modal"
>
<template #body-content>
<div class="space-y-4">
<div>
<FormControl label="端点" v-model="endpoint" />
<p class="mt-1.5 text-sm text-gray-700">
如果您更改了端点请确保重新激活webhook
</p>
</div>
<div v-if="!updateSecret">
<p class="block text-xs text-gray-600">密钥</p>
<div
class="mt-1 flex items-center justify-between text-base text-gray-700"
>
<p>想要更改密钥吗</p>
<Button @click="updateSecret = true">编辑密钥</Button>
</div>
</div>
<div v-else>
<FormControl label="密钥" v-model="secret">
<template #suffix>
<FeatherIcon
class="w-4 cursor-pointer"
name="refresh-ccw"
@click="generateRandomSecret"
/>
</template>
</FormControl>
<p class="mt-1.5 text-sm text-gray-700">
<secret>注意</secret> 密钥是可选的查看
<a
href="https://jingrow.com/docs/webhook-introduction"
class="underline"
target="_blank"
>文档</a
>
了解更多
</p>
</div>
<p class="text-base font-medium text-gray-900">
选择webhook事件
</p>
<div
class="text-center text-sm leading-10 text-gray-500"
v-if="$resources.events.loading"
>
加载中...
</div>
<div class="mt-6 flex flex-col gap-3" v-else>
<Switch
v-for="event in $resources.events.data"
:key="event.name"
:label="event.name"
:description="event.description"
:modelValue="isEventSelected(event.name)"
@update:modelValue="selectEvent(event.name)"
size="sm"
/>
</div>
<ErrorMessage
:message="errorMessage || $resources.updateWebhook.error"
/>
</div>
<template #header>
<span class="text-lg font-semibold">{{ $t('Edit Webhook') }}</span>
</template>
</Dialog>
<n-space vertical :size="20">
<n-form-item :label="$t('Endpoint')">
<n-input
v-model:value="endpoint"
:placeholder="$t('Enter webhook endpoint URL')"
:size="inputSize"
class="w-full"
/>
</n-form-item>
<n-alert type="info" class="mb-2">
{{ $t('If you change the endpoint, make sure to reactivate the webhook.') }}
</n-alert>
<div v-if="!updateSecret">
<p class="block text-xs text-gray-600 mb-2">{{ $t('Secret Key') }}</p>
<div class="mt-1 flex items-center justify-between text-base text-gray-700">
<p>{{ $t('Want to change the secret key?') }}</p>
<n-button size="small" @click="updateSecret = true">
{{ $t('Edit Secret') }}
</n-button>
</div>
</div>
<div v-else>
<n-form-item :label="$t('Secret Key')">
<n-input
v-model:value="secret"
:placeholder="$t('Enter secret key (optional)')"
:size="inputSize"
class="w-full"
>
<template #suffix>
<n-icon
class="cursor-pointer text-gray-500 hover:text-gray-700"
@click="generateRandomSecret"
>
<RefreshIcon />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-alert type="info" class="mb-2">
<template #header>
<strong>{{ $t('Note') }}:</strong>
</template>
{{ $t('The secret key is optional. View') }}
<a href="https://jingrow.com/docs/webhook-introduction" class="text-primary underline" target="_blank">
{{ $t('documentation') }}
</a>
{{ $t('to learn more') }}
</n-alert>
</div>
<div class="mt-4">
<p class="text-base font-medium text-gray-900 mb-4">
{{ $t('Select Webhook Events') }}
</p>
<n-spin :show="$resources.events.loading">
<n-space vertical :size="12" v-if="!$resources.events.loading">
<div
v-for="event in $resources.events.data"
:key="event.name"
class="flex items-center justify-between p-3 rounded border border-gray-200 hover:bg-gray-50"
>
<div class="flex-1">
<div class="text-sm font-medium text-gray-900">
{{ event.name }}
</div>
<div v-if="event.description" class="text-xs text-gray-500 mt-1">
{{ event.description }}
</div>
</div>
<n-switch
:value="isEventSelected(event.name)"
@update:value="selectEvent(event.name)"
size="medium"
/>
</div>
</n-space>
</n-spin>
</div>
<n-alert
v-if="errorMessage || $resources.updateWebhook.error"
type="error"
:title="errorMessage || $resources.updateWebhook.error"
/>
</n-space>
<template #action>
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
<n-button @click="hide" :block="isMobile" :size="buttonSize">
{{ $t('Cancel') }}
</n-button>
<n-button
type="primary"
:loading="$resources.updateWebhook.loading"
@click="updateWebhook"
:block="isMobile"
:size="buttonSize"
>
{{ $t('Save Changes') }}
</n-button>
</n-space>
</template>
</n-modal>
</template>
<script>
import {
NModal,
NFormItem,
NInput,
NSpace,
NAlert,
NButton,
NSwitch,
NIcon,
NSpin,
} from 'naive-ui';
import { toast } from 'vue-sonner';
import { getToastErrorMessage } from '../../utils/toast';
import RefreshIcon from '~icons/lucide/refresh-ccw';
export default {
emits: ['success'],
props: ['webhook'],
components: {
NModal,
NFormItem,
NInput,
NSpace,
NAlert,
NButton,
NSwitch,
NIcon,
NSpin,
RefreshIcon,
},
emits: ['success', 'update:modelValue'],
props: {
webhook: {
type: Object,
required: true
},
modelValue: {
type: Boolean,
default: false
}
},
data() {
return {
endpoint: '',
secret: '',
updateSecret: false,
selectedEvents: [],
errorMessage: ''
errorMessage: '',
windowWidth: window.innerWidth,
};
},
computed: {
showDialog: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
}
},
isMobile() {
return this.windowWidth <= 768;
},
modalStyle() {
return {
width: this.isMobile ? '95vw' : '800px',
maxWidth: this.isMobile ? '95vw' : '90vw',
};
},
inputSize() {
return this.isMobile ? 'medium' : 'large';
},
buttonSize() {
return this.isMobile ? 'medium' : 'medium';
},
},
mounted() {
this.handleResize();
window.addEventListener('resize', this.handleResize);
if (this.selectedEvents.length) {
this.selectedEvents = this.selectedEvents.map(event => event.name);
}
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
},
methods: {
handleResize() {
this.windowWidth = window.innerWidth;
},
generateRandomSecret() {
this.secret = Array(30)
.fill(0)
.map(
() =>
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'[
Math.floor(Math.random() * 62)
]
)
.join('');
},
selectEvent(event) {
if (this.selectedEvents.includes(event)) {
this.selectedEvents = this.selectedEvents.filter(e => e !== event);
} else {
this.selectedEvents.push(event);
}
},
isEventSelected(event) {
return this.selectedEvents.includes(event);
},
updateWebhook() {
if (this.selectedEvents.length === 0) {
this.errorMessage = this.$t('Please enable at least one event');
return;
}
this.errorMessage = '';
this.$resources.updateWebhook.submit();
},
hide() {
this.showDialog = false;
this.updateSecret = false;
this.errorMessage = '';
}
},
resources: {
events() {
return {
@ -125,8 +264,8 @@ export default {
return {
url: 'jcloud.api.webhook.update',
validate: () => {
if (!this.selectedEvents) {
return '请至少启用一个事件';
if (!this.selectedEvents || this.selectedEvents.length === 0) {
return this.$t('Please enable at least one event');
}
},
makeParams: () => {
@ -138,52 +277,72 @@ export default {
};
},
onSuccess: () => {
toast.success('Webhook更新成功');
toast.success(this.$t('Webhook updated successfully'));
const activationRequired = this.webhook.endpoint !== this.endpoint;
this.hide();
this.$emit('success', activationRequired);
},
onError: e => {
toast.error(
getToastErrorMessage(
e,
'更新webhook失败请重试'
this.$t('Failed to update webhook, please try again')
)
);
}
};
}
},
computed: {},
methods: {
generateRandomSecret() {
this.secret = Array(30)
.fill(0)
.map(
() =>
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'[
Math.floor(Math.random() * 62)
]
)
.join('');
},
selectEvent(event) {
if (this.selectedEvents.includes(event)) {
this.selectedEvents = this.selectedEvents.filter(e => e !== event);
} else {
this.selectedEvents.push(event);
}
},
isEventSelected(event) {
return this.selectedEvents.includes(event);
},
updateWebhook() {
if (this.selectedEvents.length === 0) {
this.errorMessage = '请至少选择一个事件来添加';
return;
}
this.errorMessage = '';
this.$resources.updateWebhook.submit();
}
}
};
</script>
</script>
<style scoped>
:deep(.edit-webhook-modal .n-card) {
width: 800px;
max-width: 90vw;
}
:deep(.edit-webhook-modal .n-card-body) {
padding: 24px;
}
:deep(.edit-webhook-modal .n-form-item) {
margin-bottom: 0;
}
:deep(.edit-webhook-modal .n-input) {
width: 100%;
}
:deep(.edit-webhook-modal .n-card__action) {
padding: 16px 24px;
}
@media (max-width: 768px) {
:deep(.edit-webhook-modal .n-card) {
width: 95vw !important;
max-width: 95vw !important;
margin: 20px auto;
border-radius: 12px;
}
:deep(.edit-webhook-modal .n-card-body) {
padding: 20px 16px;
}
:deep(.edit-webhook-modal .n-card__header) {
padding: 16px;
font-size: 18px;
}
:deep(.edit-webhook-modal .n-card__action) {
padding: 12px 16px;
}
:deep(.edit-webhook-modal .n-button) {
width: 100%;
font-size: 16px;
height: 44px;
}
}
</style>

View File

@ -1,83 +1,124 @@
<template>
<Dialog
:options="{
title: selectedWebhookAttemptId
? `Webhook 尝试 - ${selectedWebhookAttemptId}`
: 'Webhook 尝试',
size: '4xl'
}"
<n-modal
v-model:show="showDialog"
preset="card"
:title="selectedWebhookAttemptId ? $t('Webhook Attempt - {id}', { id: selectedWebhookAttemptId }) : $t('Webhook Attempts')"
:style="modalStyle"
:mask-closable="true"
:close-on-esc="true"
class="webhook-attempts-modal"
>
<template #body-content>
<p class="text-sm mb-2 text-gray-700" v-if="!selectedWebhookAttemptId">
<strong>注意</strong> 您只能查看过去24小时的日志
</p>
<template #header>
<span class="text-lg font-semibold">
{{ selectedWebhookAttemptId ? $t('Webhook Attempt - {id}', { id: selectedWebhookAttemptId }) : $t('Webhook Attempts') }}
</span>
</template>
<n-space vertical :size="20">
<n-alert type="info" v-if="!selectedWebhookAttemptId">
<template #header>
<strong>{{ $t('Note') }}:</strong>
</template>
{{ $t('You can only view logs from the past 24 hours') }}
</n-alert>
<ObjectList :options="listOptions" v-if="!selectedWebhookAttemptId" />
<Button
class="mb-2"
iconLeft="arrow-left"
<n-button
v-if="selectedWebhookAttemptId"
@click="selectedWebhookAttemptId = null"
:size="buttonSize"
>
返回
</Button>
<template #icon>
<n-icon><ArrowLeftIcon /></n-icon>
</template>
{{ $t('Back') }}
</n-button>
<WebhookAttemptDetails
:id="selectedWebhookAttemptId"
v-if="selectedWebhookAttemptId"
/>
</template>
</Dialog>
</n-space>
</n-modal>
</template>
<script>
import { Breadcrumbs, Badge } from 'jingrow-ui';
import Header from '../Header.vue';
import ObjectList from '../ObjectList.vue';
import {
NModal,
NSpace,
NAlert,
NButton,
NIcon,
} from 'naive-ui';
import { Badge } from 'jingrow-ui';
import { h } from 'vue';
import ObjectList from '../ObjectList.vue';
import WebhookAttemptDetails from './WebhookAttemptDetails.vue';
import ArrowLeftIcon from '~icons/lucide/arrow-left';
export default {
name: 'WebhookAttempts',
props: ['name'],
components: {
Header,
Breadcrumbs,
NModal,
NSpace,
NAlert,
NButton,
NIcon,
ObjectList,
WebhookAttemptDetails
WebhookAttemptDetails,
ArrowLeftIcon,
},
name: 'WebhookAttempts',
props: {
name: {
type: String,
required: true
},
modelValue: {
type: Boolean,
default: false
}
},
data() {
return {
selectedWebhookAttemptId: null
selectedWebhookAttemptId: null,
windowWidth: window.innerWidth,
};
},
resources: {
attempts() {
return {
url: 'jcloud.api.webhook.attempts',
params: {
webhook: this.$props.name
},
inititalData: [],
auto: true
};
}
},
computed: {
showDialog: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
}
},
isMobile() {
return this.windowWidth <= 768;
},
modalStyle() {
return {
width: this.isMobile ? '95vw' : '1200px',
maxWidth: this.isMobile ? '95vw' : '90vw',
};
},
buttonSize() {
return this.isMobile ? 'medium' : 'medium';
},
listOptions() {
return {
data: () => this.$resources?.attempts?.data || [],
columns: [
{
label: '事件',
label: this.$t('Event'),
fieldname: 'event',
width: 0.25
},
{
label: '端点',
label: this.$t('Endpoint'),
fieldname: 'endpoint',
width: 0.5,
format: value => value.substring(0, 50)
},
{
label: '状态',
label: this.$t('Status'),
fieldname: 'status',
width: 0.1,
type: 'Component',
@ -94,7 +135,7 @@ export default {
}
},
{
label: '代码',
label: this.$t('Code'),
fieldname: 'response_status_code',
width: 0.1,
format: val => {
@ -104,7 +145,7 @@ export default {
align: 'center'
},
{
label: '时间戳',
label: this.$t('Timestamp'),
fieldname: 'timestamp',
width: 0.3,
format(value) {
@ -117,6 +158,67 @@ export default {
}
};
}
},
mounted() {
this.handleResize();
window.addEventListener('resize', this.handleResize);
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
},
methods: {
handleResize() {
this.windowWidth = window.innerWidth;
}
},
resources: {
attempts() {
return {
url: 'jcloud.api.webhook.attempts',
params: {
webhook: this.$props.name
},
inititalData: [],
auto: true
};
}
}
};
</script>
</script>
<style scoped>
:deep(.webhook-attempts-modal .n-card) {
width: 1200px;
max-width: 90vw;
}
:deep(.webhook-attempts-modal .n-card-body) {
padding: 24px;
}
:deep(.webhook-attempts-modal .n-card__action) {
padding: 16px 24px;
}
@media (max-width: 768px) {
:deep(.webhook-attempts-modal .n-card) {
width: 95vw !important;
max-width: 95vw !important;
margin: 20px auto;
border-radius: 12px;
}
:deep(.webhook-attempts-modal .n-card-body) {
padding: 20px 16px;
}
:deep(.webhook-attempts-modal .n-card__header) {
padding: 16px;
font-size: 18px;
}
:deep(.webhook-attempts-modal .n-card__action) {
padding: 12px 16px;
}
}
</style>