基于naive ui重构设置-开发者页面及弹窗
This commit is contained in:
parent
34577e321d
commit
8b617d833b
@ -1,83 +1,175 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<n-modal
|
||||||
:options="{
|
v-model:show="showDialog"
|
||||||
title: '激活Webhook'
|
preset="card"
|
||||||
}"
|
:title="$t('Activate Webhook')"
|
||||||
|
:style="modalStyle"
|
||||||
|
:mask-closable="true"
|
||||||
|
:close-on-esc="true"
|
||||||
|
class="activate-webhook-modal"
|
||||||
>
|
>
|
||||||
<template #body-content>
|
<template #header>
|
||||||
<div class="space-y-4">
|
<span class="text-lg font-semibold">{{ $t('Activate Webhook') }}</span>
|
||||||
<FormControl label="端点" v-model="webhook.endpoint" disabled />
|
</template>
|
||||||
<div v-if="request">
|
<n-space vertical :size="20">
|
||||||
<p class="text-xs text-gray-600">请求</p>
|
<n-form-item :label="$t('Endpoint')">
|
||||||
<pre
|
<n-input
|
||||||
class="mt-2 whitespace-pre-wrap rounded bg-gray-50 px-2 py-1.5 text-sm text-gray-600"
|
:value="webhook.endpoint"
|
||||||
>{{ request }}</pre
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormControl
|
|
||||||
v-if="response_status_code"
|
|
||||||
label="响应状态码"
|
|
||||||
v-model="response_status_code"
|
|
||||||
disabled
|
disabled
|
||||||
|
:size="inputSize"
|
||||||
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
<div v-if="response">
|
</n-form-item>
|
||||||
<p class="text-xs text-gray-600">响应</p>
|
<div v-if="request">
|
||||||
<pre
|
<p class="text-xs text-gray-600 mb-2">{{ $t('Request') }}</p>
|
||||||
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"
|
<pre class="mt-2 whitespace-pre-wrap rounded bg-gray-50 px-3 py-2 text-sm text-gray-700 border border-gray-200">
|
||||||
>{{ response }}</pre
|
{{ request }}
|
||||||
>
|
</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" />
|
|
||||||
</div>
|
</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>
|
||||||
<template v-slot:actions>
|
</n-modal>
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {
|
||||||
|
NModal,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
NSpace,
|
||||||
|
NAlert,
|
||||||
|
NButton,
|
||||||
|
NIcon,
|
||||||
|
} from 'naive-ui';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
|
import CheckIcon from '~icons/lucide/check';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['success'],
|
components: {
|
||||||
props: ['webhook'],
|
NModal,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
NSpace,
|
||||||
|
NAlert,
|
||||||
|
NButton,
|
||||||
|
NIcon,
|
||||||
|
CheckIcon,
|
||||||
|
},
|
||||||
|
emits: ['success', 'update:modelValue'],
|
||||||
|
props: {
|
||||||
|
webhook: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
validated: false,
|
validated: false,
|
||||||
request: null,
|
request: null,
|
||||||
response: 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: {
|
resources: {
|
||||||
validateEndpoint() {
|
validateEndpoint() {
|
||||||
return {
|
return {
|
||||||
@ -99,9 +191,7 @@ export default {
|
|||||||
this.validated = true;
|
this.validated = true;
|
||||||
} else {
|
} else {
|
||||||
this.validated = false;
|
this.validated = false;
|
||||||
|
this.errorMessage = this.$t('Endpoint should return a status code between 200 and 300. Please check the endpoint and try again.');
|
||||||
this.errorMessage =
|
|
||||||
'端点应返回200到300之间的状态\n请检查端点并重试';
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: e => {
|
onError: e => {
|
||||||
@ -121,7 +211,8 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
onSuccess(e) {
|
onSuccess(e) {
|
||||||
toast.success('Webhook激活成功');
|
toast.success(this.$t('Webhook activated successfully'));
|
||||||
|
this.hide();
|
||||||
this.$emit('success');
|
this.$emit('success');
|
||||||
},
|
},
|
||||||
onError(e) {
|
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>
|
||||||
|
|||||||
@ -1,114 +1,179 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<n-modal
|
||||||
:options="{
|
v-model:show="showDialog"
|
||||||
title: $t('Add New Webhook'),
|
preset="card"
|
||||||
actions: [
|
:title="$t('Add New Webhook')"
|
||||||
{
|
:style="modalStyle"
|
||||||
label: $t('Add Webhook'),
|
:mask-closable="true"
|
||||||
variant: 'solid',
|
:close-on-esc="true"
|
||||||
onClick: addWebhook,
|
class="add-webhook-modal"
|
||||||
loading: $resources?.addWebhook?.loading
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<template #body-content>
|
<template #header>
|
||||||
<div class="space-y-4">
|
<span class="text-lg font-semibold">{{ $t('Add New Webhook') }}</span>
|
||||||
<FormControl :label="$t('Endpoint')" v-model="endpoint" />
|
</template>
|
||||||
<div>
|
<n-space vertical :size="20">
|
||||||
<FormControl :label="$t('Secret Key')" v-model="secret">
|
<n-form-item :label="$t('Endpoint')" :required="true">
|
||||||
<template #suffix>
|
<n-input
|
||||||
<FeatherIcon
|
v-model:value="endpoint"
|
||||||
class="w-4 cursor-pointer"
|
:placeholder="$t('Enter webhook endpoint URL')"
|
||||||
name="refresh-ccw"
|
:size="inputSize"
|
||||||
@click="generateRandomSecret"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</template>
|
</n-form-item>
|
||||||
</FormControl>
|
<n-form-item :label="$t('Secret Key')">
|
||||||
<p class="mt-2 text-sm text-gray-700">
|
<n-input
|
||||||
<strong>{{ $t('Note') }}:</strong> {{ $t('The secret key is optional. View') }}
|
v-model:value="secret"
|
||||||
<a href="https://jingrow.com/docs/webhook-introduction" class="underline" target="_blank"
|
:placeholder="$t('Enter secret key (optional)')"
|
||||||
>{{ $t('documentation') }}</a
|
: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') }}
|
<RefreshIcon />
|
||||||
</p>
|
</n-icon>
|
||||||
</div>
|
</template>
|
||||||
<p class="text-base font-medium text-gray-900">
|
</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') }}
|
{{ $t('Select Webhook Events') }}
|
||||||
</p>
|
</p>
|
||||||
<div
|
<n-spin :show="$resources.events.loading">
|
||||||
class="text-center text-sm leading-10 text-gray-500"
|
<n-space vertical :size="12" v-if="!$resources.events.loading">
|
||||||
v-if="$resources.events.loading"
|
<div
|
||||||
>
|
v-for="event in localizedEvents"
|
||||||
{{ $t('Loading...') }}
|
:key="event.name"
|
||||||
</div>
|
class="flex items-center justify-between p-3 rounded border border-gray-200 hover:bg-gray-50"
|
||||||
<div class="mt-6 flex flex-col gap-4" v-else>
|
>
|
||||||
<Switch
|
<div class="flex-1">
|
||||||
v-for="event in localizedEvents"
|
<div class="text-sm font-medium text-gray-900">
|
||||||
:key="event.name"
|
{{ event.title || event.name }}
|
||||||
:label="event.title || event.name"
|
</div>
|
||||||
:description="event.description"
|
<div v-if="event.description" class="text-xs text-gray-500 mt-1">
|
||||||
:modelValue="isEventSelected(event.name)"
|
{{ event.description }}
|
||||||
@update:modelValue="selectEvent(event.name)"
|
</div>
|
||||||
size="sm"
|
</div>
|
||||||
/>
|
<n-switch
|
||||||
</div>
|
:value="isEventSelected(event.name)"
|
||||||
<ErrorMessage :message="errorMessage || $resources.addWebhook.error" />
|
@update:value="selectEvent(event.name)"
|
||||||
|
size="medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-space>
|
||||||
|
</n-spin>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
</Dialog>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {
|
||||||
|
NModal,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
NSpace,
|
||||||
|
NAlert,
|
||||||
|
NButton,
|
||||||
|
NSwitch,
|
||||||
|
NIcon,
|
||||||
|
NSpin,
|
||||||
|
} from 'naive-ui';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
|
import RefreshIcon from '~icons/lucide/refresh-ccw';
|
||||||
|
|
||||||
export default {
|
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() {
|
data() {
|
||||||
return {
|
return {
|
||||||
endpoint: '',
|
endpoint: '',
|
||||||
secret: '',
|
secret: '',
|
||||||
selectedEvents: [],
|
selectedEvents: [],
|
||||||
errorMessage: ''
|
errorMessage: '',
|
||||||
|
windowWidth: window.innerWidth,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
computed: {
|
||||||
if (this.selectedEvents.length) {
|
showDialog: {
|
||||||
this.selectedEvents = this.selectedEvents.map(event => event.name);
|
get() {
|
||||||
}
|
return this.modelValue;
|
||||||
},
|
},
|
||||||
resources: {
|
set(value) {
|
||||||
events() {
|
this.$emit('update:modelValue', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isMobile() {
|
||||||
|
return this.windowWidth <= 768;
|
||||||
|
},
|
||||||
|
modalStyle() {
|
||||||
return {
|
return {
|
||||||
url: 'jcloud.api.webhook.available_events',
|
width: this.isMobile ? '95vw' : '800px',
|
||||||
inititalData: [],
|
maxWidth: this.isMobile ? '95vw' : '90vw',
|
||||||
auto: true
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
addWebhook() {
|
inputSize() {
|
||||||
return {
|
return this.isMobile ? 'medium' : 'large';
|
||||||
url: 'jcloud.api.webhook.add',
|
},
|
||||||
params: {
|
buttonSize() {
|
||||||
endpoint: this.endpoint,
|
return this.isMobile ? 'medium' : 'medium';
|
||||||
secret: this.secret,
|
},
|
||||||
events: this.selectedEvents
|
|
||||||
},
|
|
||||||
onSuccess() {
|
|
||||||
toast.success(this.$t('Webhook added successfully'));
|
|
||||||
this.$emit('success');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
localizedEvents() {
|
localizedEvents() {
|
||||||
if (!this.$resources.events.data) return [];
|
if (!this.$resources.events.data) return [];
|
||||||
|
|
||||||
return this.$resources.events.data.map(event => {
|
return this.$resources.events.data.map(event => {
|
||||||
// 创建新对象,避免直接修改原始数据
|
|
||||||
const localizedEvent = { ...event };
|
const localizedEvent = { ...event };
|
||||||
|
|
||||||
// 根据事件名称进行本地化翻译
|
|
||||||
if (localizedEvent.name === 'Site Status Update') {
|
if (localizedEvent.name === 'Site Status Update') {
|
||||||
localizedEvent.title = this.$t('Site Status Update');
|
localizedEvent.title = this.$t('Site Status Update');
|
||||||
localizedEvent.description = this.$t('Pending, Installing, Updating, Active, Inactive, Abnormal, Archived, Paused');
|
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.title = this.$t('Site Plan Change');
|
||||||
localizedEvent.description = this.$t('Get notifications for site subscription plan changes');
|
localizedEvent.description = this.$t('Get notifications for site subscription plan changes');
|
||||||
} else {
|
} else {
|
||||||
// 对于其他事件,如果没有title则使用name,如果没有description则使用空字符串
|
|
||||||
if (!localizedEvent.title) {
|
if (!localizedEvent.title) {
|
||||||
localizedEvent.title = localizedEvent.name;
|
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: {
|
methods: {
|
||||||
|
handleResize() {
|
||||||
|
this.windowWidth = window.innerWidth;
|
||||||
|
},
|
||||||
generateRandomSecret() {
|
generateRandomSecret() {
|
||||||
this.secret = Array(30)
|
this.secret = Array(30)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
@ -162,7 +239,89 @@ export default {
|
|||||||
}
|
}
|
||||||
this.errorMessage = '';
|
this.errorMessage = '';
|
||||||
this.$resources.addWebhook.submit();
|
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>
|
||||||
|
|||||||
@ -1,103 +1,220 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-5">
|
<div class="developer-settings-container">
|
||||||
<div class="grid grid-cols-1 gap-5">
|
<n-space vertical :size="24">
|
||||||
<div
|
<!-- API Access 卡片 -->
|
||||||
class="mx-auto min-w-[48rem] max-w-3xl space-y-6 rounded-md border p-4"
|
<n-card :title="$t('API Access')" class="settings-card">
|
||||||
>
|
<n-space vertical :size="20">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between flex-wrap gap-4">
|
||||||
<div class="text-xl font-semibold">{{ $t('API Access') }}</div>
|
<div class="text-base text-gray-700">
|
||||||
<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">
|
|
||||||
{{ $t('API key and API secret can be used to access') }}
|
{{ $t('API key and API secret can be used to access') }}
|
||||||
<a href="/docs/api" class="underline" target="_blank"
|
<a href="/docs/api" class="text-primary underline" target="_blank">
|
||||||
>{{ $t('Jingrow API') }}</a
|
{{ $t('Jingrow API') }}
|
||||||
>.
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<n-button type="primary" @click="showCreateSecretDialog = true" :size="buttonSize">
|
||||||
</Dialog>
|
{{ apiKeyButtonLabel }}
|
||||||
</div>
|
</n-button>
|
||||||
<div
|
</div>
|
||||||
class="mx-auto min-w-[48rem] max-w-3xl space-y-6 rounded-md border p-4"
|
<div v-if="$team.pg?.user_info?.api_key">
|
||||||
>
|
<ClickToCopyField :textContent="$team.pg.user_info.api_key" />
|
||||||
<div class="flex items-center justify-between">
|
</div>
|
||||||
<div class="text-xl font-semibold">{{ $t('SSH Keys') }}</div>
|
<div v-else class="text-base text-gray-600">
|
||||||
</div>
|
{{ $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" />
|
<ObjectList :options="sshKeyListOptions" />
|
||||||
</div>
|
</n-card>
|
||||||
<div
|
|
||||||
|
<!-- Webhooks 卡片 -->
|
||||||
|
<n-card
|
||||||
v-if="$session.hasWebhookConfigurationAccess"
|
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" />
|
<ObjectList :options="webhookListOptions" />
|
||||||
<AddNewWebhookDialog
|
</n-card>
|
||||||
v-if="showAddWebhookDialog"
|
</n-space>
|
||||||
v-model="showAddWebhookDialog"
|
|
||||||
@success="onNewWebhookSuccess"
|
<!-- API Key 创建/重新生成弹窗 -->
|
||||||
/>
|
<n-modal
|
||||||
<ActivateWebhookDialog
|
v-model:show="showCreateSecretDialog"
|
||||||
v-if="showActivateWebhookDialog"
|
preset="card"
|
||||||
v-model="showActivateWebhookDialog"
|
:title="$t('API Access')"
|
||||||
@success="onWebHookActivated"
|
:style="modalStyle"
|
||||||
:webhook="selectedWebhook"
|
:mask-closable="true"
|
||||||
/>
|
:close-on-esc="true"
|
||||||
<EditWebhookDialog
|
class="api-key-modal"
|
||||||
v-if="showEditWebhookDialog"
|
>
|
||||||
v-model="showEditWebhookDialog"
|
<template #header>
|
||||||
@success="onWebHookUpdated"
|
<span class="text-lg font-semibold">{{ $t('API Access') }}</span>
|
||||||
:webhook="selectedWebhook"
|
</template>
|
||||||
/>
|
<n-space vertical :size="20">
|
||||||
<WebhookAttemptsDialog
|
<div v-if="createSecret.data">
|
||||||
v-if="showWebhookAttempts"
|
<n-alert type="warning" class="mb-4">
|
||||||
v-model="showWebhookAttempts"
|
{{ $t('Please copy the API key immediately. You will not be able to view it again!') }}
|
||||||
:name="selectedWebhook.name"
|
</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>
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import {
|
||||||
|
NCard,
|
||||||
|
NSpace,
|
||||||
|
NButton,
|
||||||
|
NModal,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
NAlert,
|
||||||
|
} from 'naive-ui';
|
||||||
import { Badge, createResource } from 'jingrow-ui';
|
import { Badge, createResource } from 'jingrow-ui';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { computed, h, onMounted, ref, getCurrentInstance } from 'vue';
|
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 ObjectList from '../ObjectList.vue';
|
||||||
import { getTeam } from '../../data/team';
|
import { getTeam } from '../../data/team';
|
||||||
import { date } from '../../utils/format';
|
import { date } from '../../utils/format';
|
||||||
@ -115,11 +232,29 @@ const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
|
|||||||
const $team = getTeam();
|
const $team = getTeam();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
let showCreateSecretDialog = ref(false);
|
let showCreateSecretDialog = ref(false);
|
||||||
|
const showAddSSHKeyDialog = ref(false);
|
||||||
|
const sshKeyValue = ref('');
|
||||||
|
const sshKeyError = ref('');
|
||||||
const showAddWebhookDialog = ref(false);
|
const showAddWebhookDialog = ref(false);
|
||||||
const showActivateWebhookDialog = ref(false);
|
const showActivateWebhookDialog = ref(false);
|
||||||
const showEditWebhookDialog = ref(false);
|
const showEditWebhookDialog = ref(false);
|
||||||
const showWebhookAttempts = ref(false);
|
const showWebhookAttempts = ref(false);
|
||||||
const selectedWebhook = ref(null);
|
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({
|
const createSecret = createResource({
|
||||||
url: 'jcloud.api.account.create_api_secret',
|
url: 'jcloud.api.account.create_api_secret',
|
||||||
@ -129,6 +264,7 @@ const createSecret = createResource({
|
|||||||
} else {
|
} else {
|
||||||
toast.success($t('API key created successfully'));
|
toast.success($t('API key created successfully'));
|
||||||
}
|
}
|
||||||
|
$team.reload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -136,6 +272,9 @@ const addSSHKey = createResource({
|
|||||||
url: 'jcloud.api.client.insert',
|
url: 'jcloud.api.client.insert',
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
toast.success($t('SSH key added successfully'));
|
toast.success($t('SSH key added successfully'));
|
||||||
|
showAddSSHKeyDialog.value = false;
|
||||||
|
sshKeyValue.value = '';
|
||||||
|
sshKeyError.value = '';
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
toast.error(
|
toast.error(
|
||||||
@ -209,7 +348,10 @@ const sshKeyListOptions = computed(() => ({
|
|||||||
return {
|
return {
|
||||||
label: $t('Add SSH Key'),
|
label: $t('Add SSH Key'),
|
||||||
slots: { prefix: icon('plus') },
|
slots: { prefix: icon('plus') },
|
||||||
onClick: () => renderAddNewKeyDialog(listResource)
|
onClick: () => {
|
||||||
|
showAddSSHKeyDialog.value = true;
|
||||||
|
sshKeyListResource = listResource;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
rowActions({ row }) {
|
rowActions({ row }) {
|
||||||
@ -236,42 +378,30 @@ const sshKeyListOptions = computed(() => ({
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function renderAddNewKeyDialog(listResource) {
|
let sshKeyListResource = null;
|
||||||
confirmDialog({
|
|
||||||
title: $t('Add New SSH Key'),
|
function handleAddSSHKey() {
|
||||||
message: $t('Add a new SSH key to your account'),
|
if (!sshKeyValue.value) {
|
||||||
fields: [
|
sshKeyError.value = $t('SSH key is required');
|
||||||
{
|
return;
|
||||||
label: $t('SSH Key'),
|
}
|
||||||
fieldname: 'sshKey',
|
sshKeyError.value = '';
|
||||||
type: 'textarea',
|
addSSHKey
|
||||||
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'"),
|
.submit({
|
||||||
required: true
|
pg: {
|
||||||
|
pagetype: 'User SSH Key',
|
||||||
|
ssh_public_key: sshKeyValue.value,
|
||||||
|
user: $team.pg.user_info.name
|
||||||
}
|
}
|
||||||
],
|
})
|
||||||
primaryAction: {
|
.then(() => {
|
||||||
label: $t('Add SSH Key'),
|
if (sshKeyListResource) {
|
||||||
variant: 'solid',
|
sshKeyListResource.reload();
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
});
|
.catch(error => {
|
||||||
|
sshKeyError.value = error.message;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const webhookListResource = createResource({
|
const webhookListResource = createResource({
|
||||||
@ -289,6 +419,7 @@ const deleteWebhook = createResource({
|
|||||||
onSuccess() {
|
onSuccess() {
|
||||||
toast.success($t('Webhook deleted successfully'));
|
toast.success($t('Webhook deleted successfully'));
|
||||||
webhookListResource.reload();
|
webhookListResource.reload();
|
||||||
|
showDeleteWebhookDialog.value = false;
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
toast.error(
|
toast.error(
|
||||||
@ -341,25 +472,8 @@ const webhookListOptions = computed(() => ({
|
|||||||
label: $t('Disable'),
|
label: $t('Disable'),
|
||||||
condition: () => Boolean(row.enabled),
|
condition: () => Boolean(row.enabled),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
confirmDialog({
|
selectedWebhookForAction.value = row;
|
||||||
title: $t('Disable Webhook'),
|
showDisableWebhookDialog.value = true;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -379,24 +493,8 @@ const webhookListOptions = computed(() => ({
|
|||||||
{
|
{
|
||||||
label: $t('Delete'),
|
label: $t('Delete'),
|
||||||
onClick() {
|
onClick() {
|
||||||
confirmDialog({
|
selectedWebhookForAction.value = row;
|
||||||
title: $t('Delete Webhook'),
|
showDeleteWebhookDialog.value = true;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -422,6 +520,7 @@ const disableWebhook = createResource({
|
|||||||
onSuccess() {
|
onSuccess() {
|
||||||
toast.success($t('Webhook disabled successfully'));
|
toast.success($t('Webhook disabled successfully'));
|
||||||
webhookListResource.reload();
|
webhookListResource.reload();
|
||||||
|
showDisableWebhookDialog.value = false;
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
toast.error(
|
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 = () => {
|
const onNewWebhookSuccess = () => {
|
||||||
webhookListResource.reload();
|
webhookListResource.reload();
|
||||||
showAddWebhookDialog.value = false;
|
showAddWebhookDialog.value = false;
|
||||||
@ -443,7 +559,7 @@ const onWebHookActivated = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onWebHookUpdated = activationRequired => {
|
const onWebHookUpdated = activationRequired => {
|
||||||
webhookListResource.reload();
|
webhookListResource.reload();
|
||||||
showEditWebhookDialog.value = false;
|
showEditWebhookDialog.value = false;
|
||||||
if (activationRequired) {
|
if (activationRequired) {
|
||||||
showActivateWebhookDialog.value = true;
|
showActivateWebhookDialog.value = true;
|
||||||
@ -456,27 +572,104 @@ const apiKeyButtonLabel = computed(() => {
|
|||||||
: $t('Create New API Key');
|
: $t('Create New API Key');
|
||||||
});
|
});
|
||||||
|
|
||||||
const apiKeyDialogOptions = computed(() => ({
|
function handleResize() {
|
||||||
title: $t('API Access'),
|
windowWidth.value = window.innerWidth;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
handleResize();
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
if (session.hasWebhookConfigurationAccess) {
|
if (session.hasWebhookConfigurationAccess) {
|
||||||
webhookListResource.fetch();
|
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>
|
||||||
|
|||||||
@ -1,104 +1,243 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<n-modal
|
||||||
:options="{
|
v-model:show="showDialog"
|
||||||
title: '编辑Webhook',
|
preset="card"
|
||||||
actions: [
|
:title="$t('Edit Webhook')"
|
||||||
{
|
:style="modalStyle"
|
||||||
label: '保存更改',
|
:mask-closable="true"
|
||||||
variant: 'solid',
|
:close-on-esc="true"
|
||||||
onClick: updateWebhook,
|
class="edit-webhook-modal"
|
||||||
loading: this.$resources.updateWebhook.loading
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<template #body-content>
|
<template #header>
|
||||||
<div class="space-y-4">
|
<span class="text-lg font-semibold">{{ $t('Edit Webhook') }}</span>
|
||||||
<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>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {
|
||||||
|
NModal,
|
||||||
|
NFormItem,
|
||||||
|
NInput,
|
||||||
|
NSpace,
|
||||||
|
NAlert,
|
||||||
|
NButton,
|
||||||
|
NSwitch,
|
||||||
|
NIcon,
|
||||||
|
NSpin,
|
||||||
|
} from 'naive-ui';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { getToastErrorMessage } from '../../utils/toast';
|
import { getToastErrorMessage } from '../../utils/toast';
|
||||||
|
import RefreshIcon from '~icons/lucide/refresh-ccw';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['success'],
|
components: {
|
||||||
props: ['webhook'],
|
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() {
|
data() {
|
||||||
return {
|
return {
|
||||||
endpoint: '',
|
endpoint: '',
|
||||||
secret: '',
|
secret: '',
|
||||||
updateSecret: false,
|
updateSecret: false,
|
||||||
selectedEvents: [],
|
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() {
|
mounted() {
|
||||||
|
this.handleResize();
|
||||||
|
window.addEventListener('resize', this.handleResize);
|
||||||
if (this.selectedEvents.length) {
|
if (this.selectedEvents.length) {
|
||||||
this.selectedEvents = this.selectedEvents.map(event => event.name);
|
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: {
|
resources: {
|
||||||
events() {
|
events() {
|
||||||
return {
|
return {
|
||||||
@ -125,8 +264,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
url: 'jcloud.api.webhook.update',
|
url: 'jcloud.api.webhook.update',
|
||||||
validate: () => {
|
validate: () => {
|
||||||
if (!this.selectedEvents) {
|
if (!this.selectedEvents || this.selectedEvents.length === 0) {
|
||||||
return '请至少启用一个事件';
|
return this.$t('Please enable at least one event');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
makeParams: () => {
|
makeParams: () => {
|
||||||
@ -138,52 +277,72 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Webhook更新成功');
|
toast.success(this.$t('Webhook updated successfully'));
|
||||||
const activationRequired = this.webhook.endpoint !== this.endpoint;
|
const activationRequired = this.webhook.endpoint !== this.endpoint;
|
||||||
|
this.hide();
|
||||||
this.$emit('success', activationRequired);
|
this.$emit('success', activationRequired);
|
||||||
},
|
},
|
||||||
onError: e => {
|
onError: e => {
|
||||||
toast.error(
|
toast.error(
|
||||||
getToastErrorMessage(
|
getToastErrorMessage(
|
||||||
e,
|
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>
|
||||||
|
|||||||
@ -1,83 +1,124 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<n-modal
|
||||||
:options="{
|
v-model:show="showDialog"
|
||||||
title: selectedWebhookAttemptId
|
preset="card"
|
||||||
? `Webhook 尝试 - ${selectedWebhookAttemptId}`
|
:title="selectedWebhookAttemptId ? $t('Webhook Attempt - {id}', { id: selectedWebhookAttemptId }) : $t('Webhook Attempts')"
|
||||||
: 'Webhook 尝试',
|
:style="modalStyle"
|
||||||
size: '4xl'
|
:mask-closable="true"
|
||||||
}"
|
:close-on-esc="true"
|
||||||
|
class="webhook-attempts-modal"
|
||||||
>
|
>
|
||||||
<template #body-content>
|
<template #header>
|
||||||
<p class="text-sm mb-2 text-gray-700" v-if="!selectedWebhookAttemptId">
|
<span class="text-lg font-semibold">
|
||||||
<strong>注意:</strong> 您只能查看过去24小时的日志
|
{{ selectedWebhookAttemptId ? $t('Webhook Attempt - {id}', { id: selectedWebhookAttemptId }) : $t('Webhook Attempts') }}
|
||||||
</p>
|
</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" />
|
<ObjectList :options="listOptions" v-if="!selectedWebhookAttemptId" />
|
||||||
<Button
|
<n-button
|
||||||
class="mb-2"
|
|
||||||
iconLeft="arrow-left"
|
|
||||||
v-if="selectedWebhookAttemptId"
|
v-if="selectedWebhookAttemptId"
|
||||||
@click="selectedWebhookAttemptId = null"
|
@click="selectedWebhookAttemptId = null"
|
||||||
|
:size="buttonSize"
|
||||||
>
|
>
|
||||||
返回
|
<template #icon>
|
||||||
</Button>
|
<n-icon><ArrowLeftIcon /></n-icon>
|
||||||
|
</template>
|
||||||
|
{{ $t('Back') }}
|
||||||
|
</n-button>
|
||||||
<WebhookAttemptDetails
|
<WebhookAttemptDetails
|
||||||
:id="selectedWebhookAttemptId"
|
:id="selectedWebhookAttemptId"
|
||||||
v-if="selectedWebhookAttemptId"
|
v-if="selectedWebhookAttemptId"
|
||||||
/>
|
/>
|
||||||
</template>
|
</n-space>
|
||||||
</Dialog>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Breadcrumbs, Badge } from 'jingrow-ui';
|
import {
|
||||||
import Header from '../Header.vue';
|
NModal,
|
||||||
import ObjectList from '../ObjectList.vue';
|
NSpace,
|
||||||
|
NAlert,
|
||||||
|
NButton,
|
||||||
|
NIcon,
|
||||||
|
} from 'naive-ui';
|
||||||
|
import { Badge } from 'jingrow-ui';
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
|
import ObjectList from '../ObjectList.vue';
|
||||||
import WebhookAttemptDetails from './WebhookAttemptDetails.vue';
|
import WebhookAttemptDetails from './WebhookAttemptDetails.vue';
|
||||||
|
import ArrowLeftIcon from '~icons/lucide/arrow-left';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WebhookAttempts',
|
|
||||||
props: ['name'],
|
|
||||||
components: {
|
components: {
|
||||||
Header,
|
NModal,
|
||||||
Breadcrumbs,
|
NSpace,
|
||||||
|
NAlert,
|
||||||
|
NButton,
|
||||||
|
NIcon,
|
||||||
ObjectList,
|
ObjectList,
|
||||||
WebhookAttemptDetails
|
WebhookAttemptDetails,
|
||||||
|
ArrowLeftIcon,
|
||||||
|
},
|
||||||
|
name: 'WebhookAttempts',
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
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: {
|
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() {
|
listOptions() {
|
||||||
return {
|
return {
|
||||||
data: () => this.$resources?.attempts?.data || [],
|
data: () => this.$resources?.attempts?.data || [],
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
label: '事件',
|
label: this.$t('Event'),
|
||||||
fieldname: 'event',
|
fieldname: 'event',
|
||||||
width: 0.25
|
width: 0.25
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '端点',
|
label: this.$t('Endpoint'),
|
||||||
fieldname: 'endpoint',
|
fieldname: 'endpoint',
|
||||||
width: 0.5,
|
width: 0.5,
|
||||||
format: value => value.substring(0, 50)
|
format: value => value.substring(0, 50)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '状态',
|
label: this.$t('Status'),
|
||||||
fieldname: 'status',
|
fieldname: 'status',
|
||||||
width: 0.1,
|
width: 0.1,
|
||||||
type: 'Component',
|
type: 'Component',
|
||||||
@ -94,7 +135,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '代码',
|
label: this.$t('Code'),
|
||||||
fieldname: 'response_status_code',
|
fieldname: 'response_status_code',
|
||||||
width: 0.1,
|
width: 0.1,
|
||||||
format: val => {
|
format: val => {
|
||||||
@ -104,7 +145,7 @@ export default {
|
|||||||
align: 'center'
|
align: 'center'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '时间戳',
|
label: this.$t('Timestamp'),
|
||||||
fieldname: 'timestamp',
|
fieldname: 'timestamp',
|
||||||
width: 0.3,
|
width: 0.3,
|
||||||
format(value) {
|
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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user