573 lines
15 KiB
Vue
573 lines
15 KiB
Vue
<template>
|
|
<div>
|
|
<div>
|
|
<header
|
|
class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-5 py-2.5"
|
|
>
|
|
<Breadcrumbs :items="[{ label: 'Sites', route: { name: 'Sites' } }]">
|
|
<template v-if="this.$account?.team.enabled" #actions>
|
|
<Button
|
|
variant="solid"
|
|
icon-left="plus"
|
|
class="ml-2"
|
|
label="Create"
|
|
@click="validateCreateSite"
|
|
>
|
|
</Button>
|
|
</template>
|
|
</Breadcrumbs>
|
|
</header>
|
|
|
|
<div class="my-5 space-y-2 px-5">
|
|
<div v-if="!$account.team.enabled">
|
|
<Alert title="Your account is disabled">
|
|
Enable your account to start creating sites
|
|
<template #actions>
|
|
<Button variant="solid" route="/settings/profile">
|
|
Enable Account
|
|
</Button>
|
|
</template>
|
|
</Alert>
|
|
</div>
|
|
<AlertBillingInformation />
|
|
<template v-if="showUnpaidInvoiceAlert">
|
|
<Alert
|
|
v-if="latestUnpaidInvoice.payment_mode === 'Prepaid Credits'"
|
|
title="Your last invoice payment has failed."
|
|
>
|
|
Please add
|
|
<strong>
|
|
{{ latestUnpaidInvoice.currency }}
|
|
{{ latestUnpaidInvoice.amount_due }}
|
|
</strong>
|
|
more in credits.
|
|
<template #actions>
|
|
<Button
|
|
@click="
|
|
$account.team.billing_address
|
|
? (showPrepaidCreditsDialog = true)
|
|
: (showAddressDialog = true)
|
|
"
|
|
variant="solid"
|
|
>
|
|
Add Credits
|
|
</Button>
|
|
</template>
|
|
</Alert>
|
|
|
|
<Alert v-else title="Your last invoice payment has failed.">
|
|
Pay now for uninterrupted services.
|
|
<template v-if="this.$resources.latestUnpaidInvoice.data" #actions>
|
|
<router-link
|
|
:to="{ path: '/billing', query: { invoiceStatus: 'Unpaid' } }"
|
|
>
|
|
<Button variant="solid"> Go to Billing </Button>
|
|
</router-link>
|
|
</template>
|
|
</Alert>
|
|
|
|
<UpdateBillingDetails
|
|
v-if="showAddressDialog"
|
|
v-model="showAddressDialog"
|
|
@updated="
|
|
showAddressDialog = false;
|
|
showPrepaidCreditsDialog = true;
|
|
$resources.billingDetails.reload();
|
|
"
|
|
/>
|
|
|
|
<PrepaidCreditsDialog
|
|
v-if="showPrepaidCreditsDialog"
|
|
v-model:show="showPrepaidCreditsDialog"
|
|
:minimum-amount="Math.ceil(latestUnpaidInvoice.amount_due)"
|
|
@success="handleAddPrepaidCreditsSuccess"
|
|
/>
|
|
</template>
|
|
</div>
|
|
<div class="mx-5">
|
|
<div class="pb-20">
|
|
<div class="flex">
|
|
<div class="flex w-full space-x-2 pb-4">
|
|
<FormControl label="Search Sites" v-model="searchTerm">
|
|
<template #prefix>
|
|
<FeatherIcon name="search" class="w-4 text-gray-600" />
|
|
</template>
|
|
</FormControl>
|
|
<FormControl
|
|
label="Status"
|
|
class="mr-8"
|
|
type="select"
|
|
:options="siteStatusFilterOptions"
|
|
v-model="site_status"
|
|
/>
|
|
<FormControl
|
|
v-if="$resources.siteTags.data.length > 0"
|
|
label="Tag"
|
|
class="mr-8"
|
|
type="select"
|
|
:options="siteTagFilterOptions"
|
|
v-model="site_tag"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Table
|
|
:columns="[
|
|
{ label: 'Site Name', name: 'name', width: 2 },
|
|
{ label: 'Status', name: 'status', width: 1 },
|
|
{ label: 'Region', name: 'region', width: 0.5 },
|
|
{ label: 'Tags', name: 'tags', width: 1 },
|
|
{ label: 'Plan', name: 'plan', width: 1.5 },
|
|
{ label: '', name: 'actions', width: 0.5 }
|
|
]"
|
|
:rows="sites"
|
|
v-slot="{ rows, columns }"
|
|
>
|
|
<TableHeader class="mb-4 hidden lg:grid" />
|
|
<div
|
|
v-for="group in groups"
|
|
:key="group.group"
|
|
class="mb-4 rounded border"
|
|
>
|
|
<div
|
|
class="flex w-full items-center rounded-t bg-gray-50 px-3 py-2 text-base"
|
|
>
|
|
<span class="font-semibold text-gray-900">
|
|
{{ group.title }}
|
|
</span>
|
|
<span v-if="!group.public" class="ml-2 text-gray-600">{{
|
|
group.version
|
|
}}</span>
|
|
<Button
|
|
v-if="!group.public"
|
|
variant="ghost"
|
|
class="ml-auto"
|
|
:route="{ name: 'Bench', params: { benchName: group.group } }"
|
|
>
|
|
View Bench
|
|
</Button>
|
|
<div v-else class="h-7" />
|
|
</div>
|
|
|
|
<TableRow
|
|
v-for="(row, index) in sitesByGroup[group.group]"
|
|
:key="row.name"
|
|
:row="row"
|
|
:class="index === 0 ? 'rounded-b' : 'rounded'"
|
|
>
|
|
<TableCell v-for="column in columns">
|
|
<Badge
|
|
v-if="column.name === 'status'"
|
|
:label="$siteStatus(row)"
|
|
/>
|
|
<div
|
|
v-else-if="column.name === 'tags'"
|
|
class="hidden space-x-1 lg:flex"
|
|
>
|
|
<Badge
|
|
v-for="(tag, i) in row.tags.slice(0, 1)"
|
|
theme="blue"
|
|
:label="tag"
|
|
/>
|
|
<Tooltip
|
|
v-if="row.tags.length > 1"
|
|
:text="row.tags.slice(1).join(', ')"
|
|
>
|
|
<Badge
|
|
v-if="row.tags.length > 1"
|
|
:label="`+${row.tags.length - 1}`"
|
|
/>
|
|
</Tooltip>
|
|
<span v-if="row.tags.length == 0">-</span>
|
|
</div>
|
|
<span
|
|
v-else-if="column.name === 'plan'"
|
|
class="hidden md:block"
|
|
>
|
|
{{
|
|
row.plan
|
|
? `${$planTitle(row.plan)}${
|
|
row.plan.price_usd > 0 ? '/mo' : ''
|
|
}`
|
|
: ''
|
|
}}
|
|
</span>
|
|
<div
|
|
v-else-if="column.name === 'region'"
|
|
class="hidden md:block"
|
|
>
|
|
<img
|
|
v-if="row.server_region_info.image"
|
|
class="h-4"
|
|
:src="row.server_region_info.image"
|
|
:alt="`Flag of ${row.server_region_info.title}`"
|
|
:title="row.server_region_info.title"
|
|
/>
|
|
<span class="text-base text-gray-700" v-else>
|
|
{{ row.server_region_info.title }}
|
|
</span>
|
|
</div>
|
|
<div
|
|
class="w-full text-right"
|
|
v-else-if="column.name == 'actions'"
|
|
>
|
|
<Dropdown
|
|
v-if="['Active', 'Updating'].includes(row.status)"
|
|
@click.prevent
|
|
:options="dropdownItems(row)"
|
|
>
|
|
<template v-slot="{ open }">
|
|
<Button
|
|
:variant="open ? 'subtle' : 'ghost'"
|
|
icon="more-horizontal"
|
|
/>
|
|
</template>
|
|
</Dropdown>
|
|
</div>
|
|
<span v-else>{{ row[column.name] || '' }}</span>
|
|
</TableCell>
|
|
</TableRow>
|
|
</div>
|
|
<div class="mt-8 flex items-center justify-center">
|
|
<LoadingText
|
|
v-if="$resources.allSites.loading && !$resources.allSites.data"
|
|
/>
|
|
<div
|
|
v-else-if="$resources.allSites.fetched && rows.length === 0"
|
|
class="text-base text-gray-700"
|
|
>
|
|
No Sites
|
|
</div>
|
|
</div>
|
|
</Table>
|
|
|
|
<Dialog
|
|
:options="{
|
|
title: 'Login As Administrator',
|
|
actions: [
|
|
{
|
|
label: 'Proceed',
|
|
variant: 'solid',
|
|
onClick: proceedWithLoginAsAdmin
|
|
}
|
|
]
|
|
}"
|
|
v-model="showReasonForAdminLoginDialog"
|
|
>
|
|
<template #body-content>
|
|
<FormControl
|
|
label="Reason for logging in as Administrator"
|
|
type="textarea"
|
|
v-model="reasonForAdminLogin"
|
|
required
|
|
/>
|
|
<ErrorMessage class="mt-3" :message="errorMessage" />
|
|
</template>
|
|
</Dialog>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
import { defineAsyncComponent } from 'vue';
|
|
import Table from '@/components/Table/Table.vue';
|
|
import TableHeader from '@/components/Table/TableHeader.vue';
|
|
import TableRow from '@/components/Table/TableRow.vue';
|
|
import TableCell from '@/components/Table/TableCell.vue';
|
|
import { loginAsAdmin } from '@/controllers/loginAsAdmin';
|
|
import AlertBillingInformation from '@/components/AlertBillingInformation.vue';
|
|
import { notify } from '@/utils/toast';
|
|
|
|
export default {
|
|
name: 'Sites',
|
|
pageMeta() {
|
|
return {
|
|
title: 'Sites - 今果 Jingrow'
|
|
};
|
|
},
|
|
props: ['bench'],
|
|
components: {
|
|
Table,
|
|
TableHeader,
|
|
TableRow,
|
|
TableCell,
|
|
PrepaidCreditsDialog: defineAsyncComponent(() =>
|
|
import('@/components/PrepaidCreditsDialog.vue')
|
|
),
|
|
StripeCard: defineAsyncComponent(() =>
|
|
import('@/components/StripeCard.vue')
|
|
),
|
|
AlertBillingInformation,
|
|
UpdateBillingDetails: defineAsyncComponent(() =>
|
|
import('@/components/UpdateBillingDetails.vue')
|
|
)
|
|
},
|
|
data() {
|
|
return {
|
|
showPrepaidCreditsDialog: false,
|
|
showAddCardDialog: false,
|
|
searchTerm: '',
|
|
reasonForAdminLogin: '',
|
|
errorMessage: null,
|
|
showReasonForAdminLoginDialog: false,
|
|
siteForLogin: null,
|
|
site_status: 'All',
|
|
site_tag: '',
|
|
showAddressDialog: false
|
|
};
|
|
},
|
|
resources: {
|
|
allSites() {
|
|
return {
|
|
url: 'jcloud.api.site.all',
|
|
params: {
|
|
site_filter: { status: this.site_status, tag: this.site_tag }
|
|
},
|
|
auto: true,
|
|
cache: [
|
|
'SiteList',
|
|
this.site_status,
|
|
this.site_tag,
|
|
this.$account.team.name
|
|
]
|
|
};
|
|
},
|
|
siteTags: { url: 'jcloud.api.site.site_tags', auto: true, initialData: [] },
|
|
latestUnpaidInvoice: {
|
|
url: 'jcloud.api.billing.get_latest_unpaid_invoice',
|
|
auto: true
|
|
},
|
|
loginAsAdmin() {
|
|
return loginAsAdmin('placeholderSite'); // So that RM does not yell at first load
|
|
},
|
|
billingDetails: 'jcloud.api.billing.details'
|
|
},
|
|
mounted() {
|
|
this.$socket.on('agent_job_update', this.onAgentJobUpdate);
|
|
this.$socket.on('list_update', this.onSiteUpdate);
|
|
},
|
|
unmounted() {
|
|
this.$socket.off('agent_job_update', this.onAgentJobUpdate);
|
|
this.$socket.off('list_update', this.onSiteUpdate);
|
|
},
|
|
methods: {
|
|
validateCreateSite() {
|
|
if (!this.$account.hasBillingInfo) {
|
|
this.showAddCardDialog = true;
|
|
} else if (
|
|
this.$account.billing_info.has_unpaid_invoices &&
|
|
!this.$account.team.free_account
|
|
) {
|
|
notify({
|
|
title:
|
|
'Please settle your unpaid invoices from the billing tab in order to create new sites',
|
|
icon: 'info',
|
|
color: 'yellow'
|
|
});
|
|
} else {
|
|
this.$router.replace('/sites/new');
|
|
}
|
|
},
|
|
onAgentJobUpdate(data) {
|
|
if (!(data.name === 'New Site' || data.name === 'New Site from Backup'))
|
|
return;
|
|
if (data.status === 'Success' && data.user === this.$account.user.name) {
|
|
this.reload();
|
|
notify({
|
|
title: 'Site creation complete!',
|
|
message: 'Login to your site and complete the setup wizard',
|
|
icon: 'check',
|
|
color: 'green'
|
|
});
|
|
}
|
|
},
|
|
onSiteUpdate(event) {
|
|
// Refresh if the event affects any of the sites in the list view
|
|
// TODO: Listen to a more granular event than list_update
|
|
if (event.pagetype === 'Site') {
|
|
let sites = this.sites;
|
|
if (
|
|
event.user === this.$account.user.name ||
|
|
sites.includes(event.name)
|
|
) {
|
|
this.reload();
|
|
}
|
|
}
|
|
},
|
|
reload() {
|
|
// refresh if currently not loading and have not reloaded in the last 5 seconds
|
|
if (
|
|
!this.$resources.allSites.loading &&
|
|
new Date() - this.$resources.allSites.lastLoaded > 5000
|
|
) {
|
|
this.$resources.allSites.reload();
|
|
}
|
|
},
|
|
handleAddPrepaidCreditsSuccess() {
|
|
this.$resources.latestUnpaidInvoice.reload();
|
|
this.showPrepaidCreditsDialog = false;
|
|
},
|
|
dropdownItems(site) {
|
|
return [
|
|
{
|
|
label: 'Visit Site',
|
|
onClick: () => {
|
|
window.open(`https://${site.name}`, '_blank');
|
|
}
|
|
},
|
|
{
|
|
label: 'Login As Admin',
|
|
onClick: () => {
|
|
if (this.$account.team.name === site.team) {
|
|
return this.$resources.loginAsAdmin.submit({
|
|
name: site.name
|
|
});
|
|
}
|
|
|
|
this.siteForLogin = site.name;
|
|
this.showReasonForAdminLoginDialog = true;
|
|
}
|
|
}
|
|
];
|
|
},
|
|
proceedWithLoginAsAdmin() {
|
|
this.errorMessage = '';
|
|
|
|
if (!this.reasonForAdminLogin.trim()) {
|
|
this.errorMessage = 'Reason is required';
|
|
return;
|
|
}
|
|
|
|
this.$resources.loginAsAdmin.submit({
|
|
name: this.siteForLogin,
|
|
reason: this.reasonForAdminLogin
|
|
});
|
|
|
|
this.showReasonForAdminLoginDialog = false;
|
|
}
|
|
},
|
|
computed: {
|
|
sites() {
|
|
if (!this.$resources.allSites.data) {
|
|
return [];
|
|
}
|
|
let sites = this.$resources.allSites.data.filter(site =>
|
|
this.$account.hasPermission(site.name, '', true)
|
|
);
|
|
if (this.searchTerm) {
|
|
return sites.filter(site =>
|
|
site.name.toLowerCase().includes(this.searchTerm.toLowerCase())
|
|
);
|
|
}
|
|
return sites;
|
|
},
|
|
sitesByGroup() {
|
|
let sitesByGroup = {};
|
|
|
|
for (let site of this.sites) {
|
|
let group = site.group;
|
|
if (!sitesByGroup[group]) {
|
|
sitesByGroup[group] = [];
|
|
}
|
|
site.route = {
|
|
name: 'SiteOverview',
|
|
params: {
|
|
siteName: site.name
|
|
}
|
|
};
|
|
sitesByGroup[group].push(site);
|
|
}
|
|
|
|
return sitesByGroup;
|
|
},
|
|
groups() {
|
|
let seen = [];
|
|
let groups = [];
|
|
for (let site of this.sites) {
|
|
if (site.public) {
|
|
site.title = 'Shared';
|
|
site.group = 'Shared';
|
|
}
|
|
if (!seen.includes(site.group)) {
|
|
seen.push(site.group);
|
|
if (site.public)
|
|
groups.unshift({
|
|
title: site.title,
|
|
group: site.group,
|
|
public: site.public,
|
|
version: site.version
|
|
});
|
|
else
|
|
groups.push({
|
|
title: site.title,
|
|
group: site.group,
|
|
public: site.public,
|
|
version: site.version
|
|
});
|
|
}
|
|
}
|
|
return groups;
|
|
},
|
|
showUnpaidInvoiceAlert() {
|
|
if (!this.latestUnpaidInvoice) {
|
|
return;
|
|
}
|
|
return !(
|
|
this.$account.team.jerp_partner || this.$account.team.free_account
|
|
);
|
|
},
|
|
latestUnpaidInvoice() {
|
|
if (this.$resources.latestUnpaidInvoice.data) {
|
|
return this.$resources.latestUnpaidInvoice.data;
|
|
}
|
|
},
|
|
siteStatusFilterOptions() {
|
|
return [
|
|
{
|
|
label: 'All',
|
|
value: 'All'
|
|
},
|
|
{
|
|
label: 'Active',
|
|
value: 'Active'
|
|
},
|
|
{
|
|
label: 'Broken',
|
|
value: 'Broken'
|
|
},
|
|
{
|
|
label: 'Inactive',
|
|
value: 'Inactive'
|
|
},
|
|
{
|
|
label: 'Trial',
|
|
value: 'Trial'
|
|
},
|
|
{
|
|
label: 'Update Available',
|
|
value: 'Update Available'
|
|
}
|
|
];
|
|
},
|
|
siteTagFilterOptions() {
|
|
const defaultOptions = [
|
|
{
|
|
label: '',
|
|
value: ''
|
|
}
|
|
];
|
|
|
|
if (!this.$resources.siteTags.data) return defaultOptions;
|
|
|
|
return [
|
|
...defaultOptions,
|
|
...this.$resources.siteTags.data.map(tag => ({
|
|
label: tag,
|
|
value: tag
|
|
}))
|
|
];
|
|
}
|
|
}
|
|
};
|
|
</script>
|