jcloude/dashboard/src/components/server/DatabaseBinlogsDialog.vue
2025-12-23 19:23:49 +08:00

366 lines
9.0 KiB
Vue

<template>
<Dialog
:options="{
title: 'Database Binlogs',
size: '2xl',
}"
v-model="show"
>
<template #body-content>
<div
v-if="$resources.binlogs.loading"
class="flex w-full items-center justify-center gap-2 py-32 text-gray-700"
>
<Spinner class="w-4" /> Loading
</div>
<div v-else>
<ObjectList
:options="binlogsOptions"
@update:selections="handleSelection"
ref="list"
/>
<div
class="flex items-center justify-between py-3"
v-if="totalBinlogs > 0"
>
<div class="flex flex-shrink-0 items-center gap-2">
<select class="form-select block text-sm" v-model="pageSize">
<option :value="10">
10&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
</option>
<option :value="20">
50&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
</option>
<option :value="50">
100&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
</option>
</select>
<p class="text-sm text-gray-600">Per Page</p>
</div>
<div class="flex flex-shrink-0 items-center gap-2">
<p class="tnum text-sm text-gray-600">
{{ pageStart }} - {{ pageEnd }} of {{ totalBinlogs }} binlogs
</p>
<div class="flex gap-2">
<Button
variant="ghost"
@click="goToPreviousPage()"
:disabled="!hasPreviousPage"
iconLeft="arrow-left"
>
Prev
</Button>
<Button
variant="ghost"
@click="goToNextPage()"
:disabled="!hasNextPage"
iconRight="arrow-right"
>
Next
</Button>
</div>
</div>
</div>
<AlertBanner
v-if="totalBinlogs > 0"
title="To purge binlogs, select the oldest binlog you want to delete. It and all earlier binlogs will be removed."
type="info"
:showIcon="false"
>
</AlertBanner>
</div>
</template>
</Dialog>
</template>
<script>
import { toast } from 'vue-sonner';
import { confirmDialog } from '../../utils/components';
import { bytes, date } from '../../utils/format';
import ObjectList from '../ObjectList.vue';
import { getToastErrorMessage } from '../../utils/toast';
export default {
name: 'DatabaseBinlogsDialog',
props: {
databaseServer: {
type: String,
required: true,
},
},
components: { ObjectList },
emits: ['update:show'],
data() {
return {
show: true,
page: 1,
pageSize: 10,
lastPurgeFrom: null,
purgeFrom: null,
selectingOtherRows: false,
previousSelections: new Set(),
};
},
resources: {
binlogs() {
return {
url: 'press.api.client.run_pg_method',
initialData: {},
makeParams: () => {
return {
dt: 'Database Server',
dn: this.databaseServer,
method: 'get_binlogs_info',
args: {},
};
},
onSuccess: (data) => {
if (data?.message) {
// reset selections
this.page = 1;
this.purgeFrom = null;
this.lastPurgeFrom = null;
this.previousSelections = new Set();
this.selectingOtherRows = false;
}
},
initialData: [],
auto: true,
};
},
purgeBinlogs() {
if (!this.purgeFrom) return;
return {
url: 'press.api.client.run_pg_method',
initialData: {},
makeParams: () => {
return {
dt: 'Database Server',
dn: this.databaseServer,
method: 'purge_binlogs',
args: {
to_binlog: this.purgeFrom,
},
};
},
auto: false,
};
},
},
computed: {
pageStart() {
return (this.page - 1) * this.pageSize + 1;
},
pageEnd() {
return Math.min(this.page * this.pageSize, this.totalBinlogs);
},
totalBinlogs() {
return this.binlogs.length;
},
hasPreviousPage() {
return this.page > 1;
},
hasNextPage() {
return this.page * this.pageSize < this.binlogs.length;
},
paginatedBinlogs() {
const start = (this.page - 1) * this.pageSize;
const end = start + this.pageSize;
return this.binlogs.slice(start, end);
},
binlogs() {
return this.$resources?.binlogs?.data?.message || [];
},
sortedBinlogNames() {
return this.binlogs.map((b) => b.name).sort();
},
binlogsOptions() {
return {
data: () => this.paginatedBinlogs,
columns: [
{
label: 'Name',
fieldname: 'name',
},
{
label: 'Size',
fieldname: 'size',
class: 'text-gray-600',
format(value) {
return bytes(value);
},
},
{
label: 'Timestamp',
fieldname: 'modified_at',
align: 'right',
format(value) {
return value ? date(value, 'lll') : '';
},
},
],
showTooltip: false,
selectable: true,
emptyState: {
title: 'No binlogs found',
description: 'Your database server has no binlogs',
},
actions: () => [
{
iconLeft: 'trash-2',
theme: 'red',
variant: 'solid',
label: 'Remove Binlogs',
disabled: !this.purgeFrom,
onClick: () => {
this.closeDialog();
confirmDialog({
title: 'Remove Binlogs',
message: `Are you sure you want to remove binlogs from <b>${this.purgeFrom} (${this.purgeFromDate})</b> and all older ones?\nThis action cannot be undone.`,
primaryAction: {
label: 'Confirm & Remove',
variant: 'solid',
theme: 'red',
onClick: ({ hide }) => {
if (this.$resources.purgeBinlogs.loading) {
return;
}
const purgeFrom = this.purgeFrom;
const purgeFromDate = this.purgeFromDate;
return toast.promise(
this.$resources.purgeBinlogs.submit(),
{
loading: 'Purging binlogs...',
success: () => {
hide();
return `Binlogs from ${purgeFrom} (${purgeFromDate}) have been removed`;
},
error: (e) => getToastErrorMessage(e),
},
);
},
},
});
},
},
{
label: 'Refresh',
icon: 'refresh-ccw',
loading: this.$resources.binlogs.loading,
onClick: () => this.$resources.binlogs.reload(),
},
],
};
},
purgeFromDate() {
if (!this.purgeFrom) return null;
const binlog = this.binlogs.find((b) => b.name === this.purgeFrom);
return binlog ? date(binlog.modified_at, 'lll') : '---';
},
},
methods: {
goToNextPage() {
if (this.hasNextPage) {
this.page += 1;
}
},
goToPreviousPage() {
if (this.hasPreviousPage) {
this.page -= 1;
}
},
handleSelection(selections) {
if (this.selectingOtherRows) {
console.log('Currently selecting other rows, ignoring selection event');
return;
}
const sortedSelections = Array.from(selections).sort().reverse();
if (sortedSelections.length == 0) {
this.purgeFrom = null;
this.selectingOtherRows = false;
return;
}
this.selectingOtherRows = true;
const reversedSortedBinlogNames = [...this.sortedBinlogNames].reverse();
this.purgeFrom = sortedSelections[0];
if (!this.purgeFrom) {
return;
}
setTimeout(() => {
if (this.lastPurgeFrom && this.purgeFrom === this.lastPurgeFrom) {
// reverse the user action
const unselectedBinlog = this.findFirstMissingAndNextAvailable(
sortedSelections,
reversedSortedBinlogNames,
).firstMissing;
if (unselectedBinlog) {
this.$refs.list.toggleRowSelection(unselectedBinlog);
console.log('Reversing selection to', unselectedBinlog);
}
} else {
// find index of purgeFrom in sorted binlog names
const purgeIndex = reversedSortedBinlogNames.indexOf(this.purgeFrom);
for (let i = 0; i < reversedSortedBinlogNames.length; i++) {
const binlog = reversedSortedBinlogNames[i];
if (i < purgeIndex && selections.has(binlog)) {
// console.log("Trying to deselect", binlog);
this.$refs.list.toggleRowSelection(binlog);
}
if (i >= purgeIndex && !selections.has(binlog)) {
// console.log("Trying to select", binlog);
this.$refs.list.toggleRowSelection(binlog);
}
}
this.lastPurgeFrom = this.purgeFrom;
this.previousSelections = new Set(sortedSelections);
}
this.selectingOtherRows = false;
}, 150);
},
findFirstMissingAndNextAvailable(selections, fullList) {
const selectionSet = new Set(selections);
const startIdx = fullList.indexOf(selections[0]);
let firstMissing = null;
let nextAvailable = null;
for (let i = startIdx; i < fullList.length; i++) {
const binlog = fullList[i];
if (!selectionSet.has(binlog)) {
firstMissing = binlog;
// find next available selection *after* this missing one
for (let j = i + 1; j < fullList.length; j++) {
if (selectionSet.has(fullList[j])) {
nextAvailable = fullList[j];
break;
}
}
break;
}
}
if (nextAvailable === null && selections.length > 0) {
nextAvailable = selections[0];
}
return { firstMissing, nextAvailable };
},
showDialog() {
this.show = true;
this.$emit('update:show', true);
},
closeDialog() {
this.show = false;
this.$emit('update:show', false);
},
},
};
</script>