453 lines
14 KiB
Vue
453 lines
14 KiB
Vue
<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> |