366 lines
9.0 KiB
Vue
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
|
|
</option>
|
|
<option :value="20">
|
|
50
|
|
</option>
|
|
<option :value="50">
|
|
100
|
|
</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>
|