jcloud/dashboard/src2/components/server/ServerOverview.vue
2025-04-12 17:39:38 +08:00

453 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div
v-if="$appServer?.pg"
class="grid grid-cols-1 items-start gap-5 sm:grid-cols-2"
>
<div
v-for="server in !!$dbReplicaServer?.pg
? ['Server', 'Database Server', 'Replication Server']
: ['Server', 'Database Server']"
class="col-span-1 rounded-md border lg:col-span-2"
>
<div class="grid grid-cols-2 lg:grid-cols-4">
<template v-for="(d, i) in currentUsage(server)" :key="d.value">
<div
class="border-b p-5 lg:border-b-0"
:class="{ 'border-r': i + 1 != currentUsage(server).length }"
>
<div
v-if="d.type === 'header'"
class="m-auto flex h-full items-center justify-between"
>
<div
v-if="d.type === 'header'"
class="mt-2 flex flex-col space-y-2"
>
<div class="text-base text-gray-700">{{ d.label }}</div>
<div class="space-y-1">
<div class="flex items-center text-base text-gray-900">
{{ d.value }}
<Tooltip v-if="d.isPremium" text="高级服务器">
<!-- this icon isn't available in unplugin package yet -->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-circle-parking ml-2 h-4 w-4 text-gray-600"
>
<circle cx="12" cy="12" r="10" />
<path d="M9 17V7h4a3 3 0 0 1 0 6H9" />
</svg>
</Tooltip>
</div>
<div class="flex space-x-1">
<div class="text-sm text-gray-600" v-html="d.subValue" />
<Tooltip v-if="d.help" :text="d.help">
<i-lucide-info class="h-3.5 w-3.5 text-gray-500" />
</Tooltip>
</div>
</div>
</div>
<Button
v-if="d.type === 'header' && !$appServer.pg.is_self_hosted"
@click="showPlanChangeDialog(server)"
label="更改"
/>
</div>
<div v-else-if="d.type === 'progress'">
<div class="flex items-center justify-between space-x-2">
<div class="text-base text-gray-700">{{ d.label }}</div>
<div v-if="d.actions" class="flex space-x-2">
<Button v-for="action in d.actions || []" v-bind="action" />
</div>
<div v-else class="h-8" />
</div>
<div class="mt-2">
<Progress size="md" :value="d.progress_value || 0" />
<div class="flex space-x-2">
<div class="mt-2 flex justify-between">
<div class="text-sm text-gray-600">
{{ d.value }}
</div>
</div>
<Tooltip v-if="d.help" :text="d.help">
<i-lucide-info class="mt-2 h-4 w-4 text-gray-500" />
</Tooltip>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
<div class="rounded-md border">
<div class="h-12 border-b px-5 py-4">
<h2 class="text-lg font-medium text-gray-900">服务器信息</h2>
</div>
<div>
<div
v-for="d in serverInformation"
:key="d.label"
class="flex items-center px-5 py-3 last:pb-5 even:bg-gray-50/70"
>
<div class="w-1/3 text-base text-gray-700">{{ d.label }}</div>
<div class="w-2/3 text-base font-medium">{{ d.value }}</div>
</div>
</div>
</div>
<ServerLoadAverage :server="server" />
</div>
</template>
<script>
import { toast } from 'vue-sonner';
import { h, defineAsyncComponent } from 'vue';
import { getCachedDocumentResource } from 'jingrow-ui';
import { confirmDialog, renderDialog } from '../../utils/components';
import { getToastErrorMessage } from '../../utils/toast';
import ServerPlansDialog from './ServerPlansDialog.vue';
import ServerLoadAverage from './ServerLoadAverage.vue';
import { getDocResource } from '../../utils/resource';
export default {
props: ['server'],
components: {
ServerLoadAverage,
ServerPlansDialog,
},
methods: {
showPlanChangeDialog(serverType) {
let ServerPlansDialog = defineAsyncComponent(
() => import('./ServerPlansDialog.vue'),
);
renderDialog(
h(ServerPlansDialog, {
server:
serverType === 'Server'
? this.$appServer.name
: this.$dbServer.name,
serverType,
}),
);
},
currentUsage(serverType) {
if (!this.$appServer?.pg) return [];
if (!this.$dbServer?.pg) return [];
let formatBytes = (v) => this.$format.bytes(v, 0, 2);
let pg =
serverType === 'Server'
? this.$appServer.pg
: serverType === 'Database Server'
? this.$dbServer.pg
: serverType === 'Replication Server'
? this.$dbReplicaServer?.pg
: null;
if (!pg) return [];
let currentPlan = pg.current_plan;
let currentUsage = pg.usage;
let diskSize = pg.disk_size;
let additionalStorage = diskSize - (currentPlan?.disk || 0);
let price = 0;
// not using $format.planTitle cuz of manual calculation of add-on storage plan
let priceField =
this.$team.pg.currency === 'CNY' ? 'price_cny' : 'price_usd';
let planDescription = '';
if (!currentPlan?.name) {
planDescription = '未选择计划';
} else if (currentPlan.price_usd > 0) {
price = currentPlan[priceField];
planDescription = `${this.$format.userCurrency(price, 0)}/月`;
} else {
planDescription = currentPlan.plan_title;
}
return [
{
label:
serverType === 'Server'
? '应用服务器计划'
: serverType === 'Database Server'
? '数据库服务器计划'
: '复制服务器计划',
value: planDescription,
subValue:
additionalStorage > 0
? `${this.$format.userCurrency(
pg.storage_plan[priceField] * additionalStorage,
0,
)}/月`
: '',
type: 'header',
isPremium: !!currentPlan?.premium,
help:
additionalStorage > 0
? `服务器计划: ${this.$format.userCurrency(
currentPlan[priceField],
)}/月 & 附加存储计划: ${this.$format.userCurrency(
pg.storage_plan[priceField] * additionalStorage,
)}/月`
: '',
},
{
label: 'CPU',
type: 'progress',
progress_value: currentPlan
? (currentUsage.vcpu / currentPlan.vcpu) * 100
: 0,
value: currentPlan
? `${(((currentUsage.vcpu || 0) / currentPlan.vcpu) * 100).toFixed(
2,
)}% of ${currentPlan.vcpu} ${this.$format.plural(
currentPlan.vcpu,
'vCPU',
'vCPUs',
)}`
: '0% vCPU',
},
{
label: '内存',
type: 'progress',
progress_value: currentPlan
? (currentUsage.memory / currentPlan.memory) * 100
: 0,
value: currentPlan
? `${formatBytes(currentUsage.memory || 0)} of ${formatBytes(
currentPlan.memory,
)}`
: formatBytes(currentUsage.memory || 0),
},
{
label: '存储',
type: 'progress',
progress_value: currentPlan
? (currentUsage.disk / (diskSize ? diskSize : currentPlan.disk)) *
100
: 0,
value: currentPlan
? `${currentUsage.disk || 0} GB 共 ${
diskSize ? diskSize : currentPlan.disk
} GB`
: `${currentUsage.disk || 0} GB`,
help:
diskSize - (currentPlan?.disk || 0) > 0
? `附加存储: ${diskSize - (currentPlan?.disk || 0)} GB`
: '',
actions: [
{
label: '增加存储',
icon: 'plus',
variant: 'ghost',
onClick: () => {
confirmDialog({
title: '增加存储',
message: `输入您想要为服务器 <b>${
pg.title || pg.name
}</b> 增加的磁盘大小<div class="rounded mt-4 p-2 text-sm text-gray-700 bg-gray-100 border">您将按 <b>${this.$format.userCurrency(
pg.storage_plan[priceField],
)}/月</b> 的费率支付每增加 1GB 存储的费用。</div><p class="mt-4 text-sm text-gray-700"><strong>注意</strong>: 您只能在 6 小时内增加一次服务器的存储大小。</div>`,
fields: [
{
fieldname: 'storage',
type: 'select',
default: 50,
label: '存储 (GB)',
variant: 'outline',
// options from 5 GB to 500 GB in steps of 5 GB
options: Array.from({ length: 100 }, (_, i) => ({
label: `${(i + 1) * 5} GB`,
value: (i + 1) * 5,
})),
},
],
onSuccess: ({ hide, values }) => {
toast.promise(
this.$appServer.increaseDiskSize.submit(
{
server: pg.name,
increment: Number(values.storage),
},
{
onSuccess: () => {
hide();
this.$router.push({
name: '服务器详情页面',
params: { name: this.$appServer.name },
});
},
onError(e) {
console.error(e);
},
},
),
{
loading: '正在增加磁盘大小...',
success: '磁盘大小已计划增加',
error: (e) =>
getToastErrorMessage(
e,
'增加磁盘大小失败',
),
},
);
},
});
},
},
{
label: '配置自动增加存储',
icon: 'tool',
variant: 'ghost',
onClick: () => {
confirmDialog({
title: '配置自动增加存储',
message: `<div class="rounded my-4 p-2 text-sm text-gray-700 bg-gray-100 border">
当可用存储达到容量的 <b>90%</b> 以上时此功能会自动增加可用存储默认情况下启用此功能以避免服务器/站点停机
<br><br>
您可以通过将下面的最小值和最大值设置为 <b>0 GB</b> 来完全禁用此功能但如果您这样做<strong>我们可能无法收到来自此服务器的事件通知</strong>
<br><br>
<strong>注意</strong>: 存储只能在 6 小时内自动增加一次
</div>输入要为服务器 <b>${
pg.title || pg.name
}</b> 增加的最大和最小存储量`,
fields: [
{
fieldname: 'min',
type: 'select',
default: String(pg.auto_add_storage_min),
label: '最小存储增加 (GB)',
variant: 'outline',
// options from 5 GB to 250 GB in steps of 5 GB
options: Array.from({ length: 51 }, (_, i) => ({
label: `${i * 5} GB`,
value: i * 5,
})),
},
{
fieldname: 'max',
type: 'select',
default: String(pg.auto_add_storage_max),
label: '最大存储增加 (GB)',
variant: 'outline',
// options from 5 GB to 250 GB in steps of 5 GB
options: Array.from({ length: 51 }, (_, i) => ({
label: `${i * 5} GB`,
value: i * 5,
})),
},
],
onSuccess: ({ hide, values }) => {
toast.promise(
this.$appServer.configureAutoAddStorage.submit(
{
server: pg.name,
min: Number(values.min),
max: Number(values.max),
},
{
onSuccess: () => {
hide();
if (pg.name === this.$appServer.name)
this.$appServer.reload();
else if (pg.name === this.$dbServer.name)
this.$dbServer.reload();
else if (pg.name === this.$replicationServer.name)
this.$replicationServer.reload();
},
},
),
{
loading: '正在配置自动扩容存储...',
success: '自动扩容存储已配置',
error: (err) => {
return err.messages.length
? err.messages.join('/n')
: err.message ||
'配置自动扩容存储失败';
},
},
);
},
});
},
},
],
},
];
},
},
computed: {
serverInformation() {
return [
{
label: '应用服务器',
value: this.$appServer.pg.name,
},
{
label: '数据库服务器',
value: this.$appServer.pg.database_server,
},
{
label: '复制服务器',
value: this.$appServer.pg.replication_server,
},
{
label: '所有者',
value: this.$appServer.pg.owner_email || this.$appServer.pg.team,
},
{
label: '创建者',
value: this.$appServer.pg.owner,
},
{
label: '创建时间',
value: this.$format.date(this.$appServer.pg.creation),
},
].filter((d) => d.value);
},
$appServer() {
return getCachedDocumentResource('Server', this.server);
},
$dbServer() {
return getDocResource({
pagetype: 'Database Server',
name: this.$appServer.pg.database_server,
whitelistedMethods: {
changePlan: 'change_plan',
reboot: 'reboot',
rename: 'rename',
},
});
},
$dbReplicaServer() {
return getDocResource({
pagetype: 'Database Server',
name: this.$appServer.pg.replication_server,
whitelistedMethods: {
changePlan: 'change_plan',
reboot: 'reboot',
rename: 'rename',
},
});
},
},
};
</script>