@chisquare-tech/origine-ngx-apps-services 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HttpClient } from '@angular/common/http';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { inject, Injectable, signal, computed, effect, untracked, DestroyRef, ChangeDetectionStrategy, Component, input, output } from '@angular/core';
|
|
3
|
+
import { inject, Injectable, signal, computed, effect, untracked, DestroyRef, ChangeDetectionStrategy, Component, input, output, Directive } from '@angular/core';
|
|
4
4
|
import { ApiService } from '@thescaffold/ngx-core';
|
|
5
5
|
import { map, forkJoin, of } from 'rxjs';
|
|
6
6
|
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
|
|
@@ -86,6 +86,19 @@ class ServicesApi {
|
|
|
86
86
|
management(project, id) {
|
|
87
87
|
return this.api.post(this.i(project, id, 'management'), {}).pipe(map((r) => data(r)));
|
|
88
88
|
}
|
|
89
|
+
/** A major version upgrade (V8-01): in place or into a new instance, now or in the maintenance window. */
|
|
90
|
+
upgrade(project, id, body) {
|
|
91
|
+
return this.api.post(this.i(project, id, 'upgrade'), body).pipe(map((r) => data(r)));
|
|
92
|
+
}
|
|
93
|
+
cancelUpgrade(project, id) {
|
|
94
|
+
return this.api.delete(this.i(project, id, 'upgrade')).pipe(map((r) => data(r)));
|
|
95
|
+
}
|
|
96
|
+
addExtension(project, id, database, name) {
|
|
97
|
+
return this.api.post(this.i(project, id, 'database/' + enc(database) + '/extension'), { name }).pipe(map((r) => data(r)));
|
|
98
|
+
}
|
|
99
|
+
removeExtension(project, id, database, name) {
|
|
100
|
+
return this.api.delete(this.i(project, id, 'database/' + enc(database) + '/extension/' + enc(name))).pipe(map((r) => data(r)));
|
|
101
|
+
}
|
|
89
102
|
restore(project, id, body) {
|
|
90
103
|
return this.api.post(this.i(project, id, 'restore'), body).pipe(map((r) => data(r)));
|
|
91
104
|
}
|
|
@@ -1585,6 +1598,351 @@ function show(f) {
|
|
|
1585
1598
|
return num(f.value);
|
|
1586
1599
|
}
|
|
1587
1600
|
|
|
1601
|
+
/** Runs an action with a busy flag; errors go to failed. */
|
|
1602
|
+
class Card {
|
|
1603
|
+
constructor() {
|
|
1604
|
+
this.api = inject(ServicesApi);
|
|
1605
|
+
this.d = input.required(...(ngDevMode ? [{ debugName: "d" }] : /* istanbul ignore next */ []));
|
|
1606
|
+
this.projectKey = input.required(...(ngDevMode ? [{ debugName: "projectKey" }] : /* istanbul ignore next */ []));
|
|
1607
|
+
this.done = output();
|
|
1608
|
+
this.failed = output();
|
|
1609
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
1610
|
+
this.when = (s) => when(s);
|
|
1611
|
+
}
|
|
1612
|
+
may(id) {
|
|
1613
|
+
return (this.d().caps ?? []).includes(id);
|
|
1614
|
+
}
|
|
1615
|
+
go(obs, ok, fail) {
|
|
1616
|
+
this.busy.set(true);
|
|
1617
|
+
obs.subscribe({
|
|
1618
|
+
next: (v) => {
|
|
1619
|
+
this.busy.set(false);
|
|
1620
|
+
ok(v);
|
|
1621
|
+
},
|
|
1622
|
+
error: (e) => {
|
|
1623
|
+
this.busy.set(false);
|
|
1624
|
+
this.failed.emit(apiMessage(e, fail));
|
|
1625
|
+
},
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: Card, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1629
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.12", type: Card, isStandalone: true, inputs: { d: { classPropertyName: "d", publicName: "d", isSignal: true, isRequired: true, transformFunction: null }, projectKey: { classPropertyName: "projectKey", publicName: "projectKey", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { done: "done", failed: "failed" }, ngImport: i0 }); }
|
|
1630
|
+
}
|
|
1631
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: Card, decorators: [{
|
|
1632
|
+
type: Directive
|
|
1633
|
+
}], propDecorators: { d: [{ type: i0.Input, args: [{ isSignal: true, alias: "d", required: true }] }], projectKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "projectKey", required: true }] }], done: [{ type: i0.Output, args: ["done"] }], failed: [{ type: i0.Output, args: ["failed"] }] } });
|
|
1634
|
+
const UPGRADE_STATE = {
|
|
1635
|
+
scheduled: 'Scheduled', 'backing-up': 'Taking the pre-upgrade backup', upgrading: 'Upgrading', testing: 'Building the new instance',
|
|
1636
|
+
ready: 'Ready to swap', done: 'Done', failed: 'Failed', canceled: 'Canceled',
|
|
1637
|
+
};
|
|
1638
|
+
// Settings › Major version (V8-01, SVC-08): what the instance can be upgraded to and how (in
|
|
1639
|
+
// place, or into a new instance for testing), now or in the maintenance window with a week's
|
|
1640
|
+
// notice; the upgrade in progress, with its pre-upgrade backup (the way back); refusals with the reason.
|
|
1641
|
+
class ServiceUpgradeCard extends Card {
|
|
1642
|
+
constructor() {
|
|
1643
|
+
super(...arguments);
|
|
1644
|
+
this.router = inject(Router);
|
|
1645
|
+
this.host = inject(ServicesHost);
|
|
1646
|
+
this.method = signal('in-place', ...(ngDevMode ? [{ debugName: "method" }] : /* istanbul ignore next */ []));
|
|
1647
|
+
this.whenTo = signal('window', ...(ngDevMode ? [{ debugName: "whenTo" }] : /* istanbul ignore next */ []));
|
|
1648
|
+
}
|
|
1649
|
+
open(s) {
|
|
1650
|
+
return !['done', 'failed', 'canceled'].includes(s);
|
|
1651
|
+
}
|
|
1652
|
+
stateLabel(s) {
|
|
1653
|
+
return UPGRADE_STATE[s] ?? s;
|
|
1654
|
+
}
|
|
1655
|
+
chip(s) {
|
|
1656
|
+
return s === 'done' || s === 'ready' ? 'o-grn' : s === 'failed' ? 'o-red' : s === 'canceled' || s === 'scheduled' ? 'o-amb' : 'o-blu';
|
|
1657
|
+
}
|
|
1658
|
+
start() {
|
|
1659
|
+
const u = this.d().upgrade;
|
|
1660
|
+
const m = u?.offer.methods.includes(this.method()) ? this.method() : (u?.offer.methods[0] ?? 'in-place');
|
|
1661
|
+
this.go(this.api.upgrade(this.projectKey(), this.d().id, { version: u?.offer.to, method: m, when: this.whenTo() }), (x) => {
|
|
1662
|
+
const c = x.upgrade?.current;
|
|
1663
|
+
this.done.emit(c?.scheduled ? `Upgrade to ${c.to} scheduled for ${when(c.at)}; the people concerned are told now.` : `Upgrading to ${c?.to}: the pre-upgrade backup runs first.`);
|
|
1664
|
+
}, 'The upgrade could not be started.');
|
|
1665
|
+
}
|
|
1666
|
+
cancel() {
|
|
1667
|
+
this.go(this.api.cancelUpgrade(this.projectKey(), this.d().id), () => this.done.emit('Upgrade canceled.'), 'The upgrade could not be canceled.');
|
|
1668
|
+
}
|
|
1669
|
+
openTarget(name) {
|
|
1670
|
+
this.router.navigate(this.host.link('services/' + name));
|
|
1671
|
+
}
|
|
1672
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ServiceUpgradeCard, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1673
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: ServiceUpgradeCard, isStandalone: true, selector: "o-svc-upgrade", usesInheritance: true, ngImport: i0, template: `
|
|
1674
|
+
@let u = d().upgrade;
|
|
1675
|
+
<div class="o-card" data-testid="svc-upgrade">
|
|
1676
|
+
<div class="o-ch"><b>Major version</b><span class="o-r">{{ d().engineName }} {{ u?.running || d().version }}</span></div>
|
|
1677
|
+
<div class="o-cb o-col" style="gap:8px">
|
|
1678
|
+
@if (u?.current; as c) {
|
|
1679
|
+
<div class="o-flex o-wrap" data-testid="svc-upgrade-current">
|
|
1680
|
+
<span [class]="'o-chip ' + chip(c.state)">{{ stateLabel(c.state) }}</span>
|
|
1681
|
+
<span class="o-sm">{{ c.from }} to {{ c.to }}, {{ c.method === 'new-instance' ? 'into a new instance' : 'in place' }}</span>
|
|
1682
|
+
@if (c.state === 'scheduled') {<span class="o-faint o-sm">starts {{ when(c.at) }}</span>}
|
|
1683
|
+
@if (open(c.state) && (c.state === 'scheduled' || c.state === 'backing-up') && may('services.instance.configure')) {
|
|
1684
|
+
<button type="button" class="o-btn o-sm o-r" (click)="cancel()" [disabled]="busy()" data-testid="svc-upgrade-cancel">Cancel upgrade</button>
|
|
1685
|
+
}
|
|
1686
|
+
@if (c.state === 'ready' && c.targetName) {<button type="button" class="o-btn o-sm o-r" (click)="openTarget(c.targetName)">Open {{ c.targetName }}</button>}
|
|
1687
|
+
</div>
|
|
1688
|
+
@if (c.message) {<span class="o-hint">{{ c.message }}</span>}
|
|
1689
|
+
@if (c.backupName && c.state !== 'canceled') {<span class="o-faint o-xs">Pre-upgrade backup: {{ c.backupName }}. To go back, restore it into a new instance on {{ c.from }}.</span>}
|
|
1690
|
+
}
|
|
1691
|
+
@if (!u?.current || !open(u!.current!.state)) {
|
|
1692
|
+
@if (u?.offer?.refused) {
|
|
1693
|
+
<span class="o-hint" data-testid="svc-upgrade-refused">{{ u!.offer.refused }}</span>
|
|
1694
|
+
} @else if (u?.offer?.to) {
|
|
1695
|
+
<span class="o-sm">Upgrade to <b>{{ d().engineName }} {{ u!.offer.to }}</b></span>
|
|
1696
|
+
@for (n of u!.offer.notes; track n) {<span class="o-faint o-xs">{{ n }}</span>}
|
|
1697
|
+
@if (may('services.instance.configure')) {
|
|
1698
|
+
<div class="o-flex o-wrap">
|
|
1699
|
+
<select class="o-in o-sm" style="width:auto" aria-label="How" [ngModel]="method()" (ngModelChange)="method.set($event)" data-testid="svc-upgrade-method">
|
|
1700
|
+
@for (m of u!.offer.methods; track m) {<option [value]="m">{{ m === 'new-instance' ? 'Into a new instance, for testing' : 'In place' }}</option>}
|
|
1701
|
+
</select>
|
|
1702
|
+
<select class="o-in o-sm" style="width:auto" aria-label="When" [ngModel]="whenTo()" (ngModelChange)="whenTo.set($event)" data-testid="svc-upgrade-when">
|
|
1703
|
+
<option value="window">In the maintenance window, with a week's notice</option>
|
|
1704
|
+
<option value="now">Now</option>
|
|
1705
|
+
</select>
|
|
1706
|
+
<button type="button" class="o-btn o-sm o-dark o-r" (click)="start()" [disabled]="busy() || d().state !== 'Running'" data-testid="svc-upgrade-go">Upgrade to {{ u!.offer.to }}</button>
|
|
1707
|
+
</div>
|
|
1708
|
+
<span class="o-faint o-xs">A backup is taken first and kept until you delete it.</span>
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
</div>
|
|
1713
|
+
</div>
|
|
1714
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1715
|
+
}
|
|
1716
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ServiceUpgradeCard, decorators: [{
|
|
1717
|
+
type: Component,
|
|
1718
|
+
args: [{
|
|
1719
|
+
selector: 'o-svc-upgrade',
|
|
1720
|
+
imports: [FormsModule],
|
|
1721
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1722
|
+
template: `
|
|
1723
|
+
@let u = d().upgrade;
|
|
1724
|
+
<div class="o-card" data-testid="svc-upgrade">
|
|
1725
|
+
<div class="o-ch"><b>Major version</b><span class="o-r">{{ d().engineName }} {{ u?.running || d().version }}</span></div>
|
|
1726
|
+
<div class="o-cb o-col" style="gap:8px">
|
|
1727
|
+
@if (u?.current; as c) {
|
|
1728
|
+
<div class="o-flex o-wrap" data-testid="svc-upgrade-current">
|
|
1729
|
+
<span [class]="'o-chip ' + chip(c.state)">{{ stateLabel(c.state) }}</span>
|
|
1730
|
+
<span class="o-sm">{{ c.from }} to {{ c.to }}, {{ c.method === 'new-instance' ? 'into a new instance' : 'in place' }}</span>
|
|
1731
|
+
@if (c.state === 'scheduled') {<span class="o-faint o-sm">starts {{ when(c.at) }}</span>}
|
|
1732
|
+
@if (open(c.state) && (c.state === 'scheduled' || c.state === 'backing-up') && may('services.instance.configure')) {
|
|
1733
|
+
<button type="button" class="o-btn o-sm o-r" (click)="cancel()" [disabled]="busy()" data-testid="svc-upgrade-cancel">Cancel upgrade</button>
|
|
1734
|
+
}
|
|
1735
|
+
@if (c.state === 'ready' && c.targetName) {<button type="button" class="o-btn o-sm o-r" (click)="openTarget(c.targetName)">Open {{ c.targetName }}</button>}
|
|
1736
|
+
</div>
|
|
1737
|
+
@if (c.message) {<span class="o-hint">{{ c.message }}</span>}
|
|
1738
|
+
@if (c.backupName && c.state !== 'canceled') {<span class="o-faint o-xs">Pre-upgrade backup: {{ c.backupName }}. To go back, restore it into a new instance on {{ c.from }}.</span>}
|
|
1739
|
+
}
|
|
1740
|
+
@if (!u?.current || !open(u!.current!.state)) {
|
|
1741
|
+
@if (u?.offer?.refused) {
|
|
1742
|
+
<span class="o-hint" data-testid="svc-upgrade-refused">{{ u!.offer.refused }}</span>
|
|
1743
|
+
} @else if (u?.offer?.to) {
|
|
1744
|
+
<span class="o-sm">Upgrade to <b>{{ d().engineName }} {{ u!.offer.to }}</b></span>
|
|
1745
|
+
@for (n of u!.offer.notes; track n) {<span class="o-faint o-xs">{{ n }}</span>}
|
|
1746
|
+
@if (may('services.instance.configure')) {
|
|
1747
|
+
<div class="o-flex o-wrap">
|
|
1748
|
+
<select class="o-in o-sm" style="width:auto" aria-label="How" [ngModel]="method()" (ngModelChange)="method.set($event)" data-testid="svc-upgrade-method">
|
|
1749
|
+
@for (m of u!.offer.methods; track m) {<option [value]="m">{{ m === 'new-instance' ? 'Into a new instance, for testing' : 'In place' }}</option>}
|
|
1750
|
+
</select>
|
|
1751
|
+
<select class="o-in o-sm" style="width:auto" aria-label="When" [ngModel]="whenTo()" (ngModelChange)="whenTo.set($event)" data-testid="svc-upgrade-when">
|
|
1752
|
+
<option value="window">In the maintenance window, with a week's notice</option>
|
|
1753
|
+
<option value="now">Now</option>
|
|
1754
|
+
</select>
|
|
1755
|
+
<button type="button" class="o-btn o-sm o-dark o-r" (click)="start()" [disabled]="busy() || d().state !== 'Running'" data-testid="svc-upgrade-go">Upgrade to {{ u!.offer.to }}</button>
|
|
1756
|
+
</div>
|
|
1757
|
+
<span class="o-faint o-xs">A backup is taken first and kept until you delete it.</span>
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
</div>
|
|
1762
|
+
</div>
|
|
1763
|
+
`,
|
|
1764
|
+
}]
|
|
1765
|
+
}] });
|
|
1766
|
+
// Backups › Copies in another region (V8-02, O10): opt-in, naming where the copies go; the
|
|
1767
|
+
// last copy; a restore from the copy into a new instance in that region.
|
|
1768
|
+
class ServiceCopiesCard extends Card {
|
|
1769
|
+
constructor() {
|
|
1770
|
+
super(...arguments);
|
|
1771
|
+
this.router = inject(Router);
|
|
1772
|
+
this.host = inject(ServicesHost);
|
|
1773
|
+
this.target = signal('', ...(ngDevMode ? [{ debugName: "target" }] : /* istanbul ignore next */ []));
|
|
1774
|
+
this.backup = signal('', ...(ngDevMode ? [{ debugName: "backup" }] : /* istanbul ignore next */ []));
|
|
1775
|
+
this.size = bytes;
|
|
1776
|
+
}
|
|
1777
|
+
regionName(id) {
|
|
1778
|
+
return this.d().copy?.offered.find((r) => r.id === id)?.name ?? id;
|
|
1779
|
+
}
|
|
1780
|
+
/** Completed backups the last copy holds. */
|
|
1781
|
+
copied() {
|
|
1782
|
+
const through = this.d().copy?.last?.through;
|
|
1783
|
+
if (!through)
|
|
1784
|
+
return [];
|
|
1785
|
+
return (this.d().backups ?? []).filter((b) => b.phase === 'completed' && b.stoppedAt && b.stoppedAt <= through);
|
|
1786
|
+
}
|
|
1787
|
+
set(region) {
|
|
1788
|
+
const name = region ? this.regionName(region) : this.d().copy?.regionName;
|
|
1789
|
+
this.go(this.api.update(this.projectKey(), this.d().id, { copyRegion: region }), () => {
|
|
1790
|
+
this.target.set('');
|
|
1791
|
+
this.done.emit(region ? `Backups of ${this.d().name} are now copied to ${name}.` : `Copies stopped; the copies in ${name} are deleted.`);
|
|
1792
|
+
}, 'The copies could not be changed.');
|
|
1793
|
+
}
|
|
1794
|
+
restore() {
|
|
1795
|
+
const b = this.backup() || this.copied().at(-1)?.name || '';
|
|
1796
|
+
const c = this.d().copy;
|
|
1797
|
+
if (!b || !c)
|
|
1798
|
+
return;
|
|
1799
|
+
this.go(this.api.restore(this.projectKey(), this.d().id, { backup: b, region: c.region }), (x) => this.router.navigate(this.host.link('services/' + x.name)), 'The restore could not be started.');
|
|
1800
|
+
}
|
|
1801
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ServiceCopiesCard, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1802
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: ServiceCopiesCard, isStandalone: true, selector: "o-svc-copies", usesInheritance: true, ngImport: i0, template: `
|
|
1803
|
+
@let c = d().copy;
|
|
1804
|
+
@if (c) {
|
|
1805
|
+
<div class="o-card o-cb o-col" style="gap:8px" data-testid="svc-copies">
|
|
1806
|
+
<b>Copies in another region</b>
|
|
1807
|
+
@if (c.region) {
|
|
1808
|
+
<span class="o-sm">Backups are copied from {{ d().regionName || d().region }} to <b>{{ c.regionName }}</b>.</span>
|
|
1809
|
+
@if (c.last; as l) {
|
|
1810
|
+
<span class="o-faint o-xs" data-testid="svc-copies-last">{{ l.error ? 'Last copy failed: ' + l.error : 'Last copied ' + when(l.at) + ' · ' + l.objects + ' objects · ' + size(l.bytes) }}</span>
|
|
1811
|
+
} @else {<span class="o-faint o-xs">The first copy is on its way.</span>}
|
|
1812
|
+
<div class="o-flex o-wrap">
|
|
1813
|
+
@if (may('services.data.import') && c.last?.through) {
|
|
1814
|
+
<select class="o-in o-sm" style="width:auto" aria-label="Backup" [ngModel]="backup()" (ngModelChange)="backup.set($event)" data-testid="svc-copies-backup">
|
|
1815
|
+
@for (b of copied(); track b.name) {<option [value]="b.name">{{ when(b.startedAt) }}</option>}
|
|
1816
|
+
</select>
|
|
1817
|
+
<button type="button" class="o-btn o-sm" (click)="restore()" [disabled]="busy() || !copied().length" data-testid="svc-copies-restore">Restore in {{ c.regionName }}</button>
|
|
1818
|
+
}
|
|
1819
|
+
@if (may('services.instance.configure')) {<button type="button" class="o-btn o-sm o-r" (click)="set('')" [disabled]="busy()" data-testid="svc-copies-off">Stop copying</button>}
|
|
1820
|
+
</div>
|
|
1821
|
+
<span class="o-faint o-xs">Stopping deletes the copies in {{ c.regionName }}.</span>
|
|
1822
|
+
} @else {
|
|
1823
|
+
<span class="o-hint">Keep a copy of every backup in another region, to restore there if this region is lost. The copies leave {{ d().regionName || d().region }}.</span>
|
|
1824
|
+
@if (may('services.instance.configure') && c.offered.length) {
|
|
1825
|
+
<div class="o-flex o-wrap">
|
|
1826
|
+
<select class="o-in o-sm" style="width:auto" aria-label="Copy to" [ngModel]="target()" (ngModelChange)="target.set($event)" data-testid="svc-copies-region">
|
|
1827
|
+
<option value="">Choose a region</option>
|
|
1828
|
+
@for (r of c.offered; track r.id) {<option [value]="r.id">{{ r.name }}</option>}
|
|
1829
|
+
</select>
|
|
1830
|
+
<button type="button" class="o-btn o-sm o-r" (click)="set(target())" [disabled]="busy() || !target()" data-testid="svc-copies-on">{{ target() ? 'Copy backups to ' + regionName(target()) : 'Copy backups' }}</button>
|
|
1831
|
+
</div>
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
</div>
|
|
1835
|
+
}
|
|
1836
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1837
|
+
}
|
|
1838
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ServiceCopiesCard, decorators: [{
|
|
1839
|
+
type: Component,
|
|
1840
|
+
args: [{
|
|
1841
|
+
selector: 'o-svc-copies',
|
|
1842
|
+
imports: [FormsModule],
|
|
1843
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1844
|
+
template: `
|
|
1845
|
+
@let c = d().copy;
|
|
1846
|
+
@if (c) {
|
|
1847
|
+
<div class="o-card o-cb o-col" style="gap:8px" data-testid="svc-copies">
|
|
1848
|
+
<b>Copies in another region</b>
|
|
1849
|
+
@if (c.region) {
|
|
1850
|
+
<span class="o-sm">Backups are copied from {{ d().regionName || d().region }} to <b>{{ c.regionName }}</b>.</span>
|
|
1851
|
+
@if (c.last; as l) {
|
|
1852
|
+
<span class="o-faint o-xs" data-testid="svc-copies-last">{{ l.error ? 'Last copy failed: ' + l.error : 'Last copied ' + when(l.at) + ' · ' + l.objects + ' objects · ' + size(l.bytes) }}</span>
|
|
1853
|
+
} @else {<span class="o-faint o-xs">The first copy is on its way.</span>}
|
|
1854
|
+
<div class="o-flex o-wrap">
|
|
1855
|
+
@if (may('services.data.import') && c.last?.through) {
|
|
1856
|
+
<select class="o-in o-sm" style="width:auto" aria-label="Backup" [ngModel]="backup()" (ngModelChange)="backup.set($event)" data-testid="svc-copies-backup">
|
|
1857
|
+
@for (b of copied(); track b.name) {<option [value]="b.name">{{ when(b.startedAt) }}</option>}
|
|
1858
|
+
</select>
|
|
1859
|
+
<button type="button" class="o-btn o-sm" (click)="restore()" [disabled]="busy() || !copied().length" data-testid="svc-copies-restore">Restore in {{ c.regionName }}</button>
|
|
1860
|
+
}
|
|
1861
|
+
@if (may('services.instance.configure')) {<button type="button" class="o-btn o-sm o-r" (click)="set('')" [disabled]="busy()" data-testid="svc-copies-off">Stop copying</button>}
|
|
1862
|
+
</div>
|
|
1863
|
+
<span class="o-faint o-xs">Stopping deletes the copies in {{ c.regionName }}.</span>
|
|
1864
|
+
} @else {
|
|
1865
|
+
<span class="o-hint">Keep a copy of every backup in another region, to restore there if this region is lost. The copies leave {{ d().regionName || d().region }}.</span>
|
|
1866
|
+
@if (may('services.instance.configure') && c.offered.length) {
|
|
1867
|
+
<div class="o-flex o-wrap">
|
|
1868
|
+
<select class="o-in o-sm" style="width:auto" aria-label="Copy to" [ngModel]="target()" (ngModelChange)="target.set($event)" data-testid="svc-copies-region">
|
|
1869
|
+
<option value="">Choose a region</option>
|
|
1870
|
+
@for (r of c.offered; track r.id) {<option [value]="r.id">{{ r.name }}</option>}
|
|
1871
|
+
</select>
|
|
1872
|
+
<button type="button" class="o-btn o-sm o-r" (click)="set(target())" [disabled]="busy() || !target()" data-testid="svc-copies-on">{{ target() ? 'Copy backups to ' + regionName(target()) : 'Copy backups' }}</button>
|
|
1873
|
+
</div>
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
</div>
|
|
1877
|
+
}
|
|
1878
|
+
`,
|
|
1879
|
+
}]
|
|
1880
|
+
}] });
|
|
1881
|
+
// Settings › Storage (V8-04, D-30): the disk grows by itself near full, up to a limit, where the
|
|
1882
|
+
// volumes can grow in place.
|
|
1883
|
+
class ServiceStorageCard extends Card {
|
|
1884
|
+
save(body) {
|
|
1885
|
+
this.go(this.api.update(this.projectKey(), this.d().id, body), () => this.done.emit('Storage saved.'), 'Storage could not be saved.');
|
|
1886
|
+
}
|
|
1887
|
+
cap(ev) {
|
|
1888
|
+
const v = Math.round(+ev.target.value);
|
|
1889
|
+
if (!Number.isFinite(v))
|
|
1890
|
+
return;
|
|
1891
|
+
this.save({ autoGrowMaxGb: v === this.d().storage?.limitGb && !this.d().storage?.maxGb ? 0 : v });
|
|
1892
|
+
}
|
|
1893
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ServiceStorageCard, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1894
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: ServiceStorageCard, isStandalone: true, selector: "o-svc-storage", usesInheritance: true, ngImport: i0, template: `
|
|
1895
|
+
@let s = d().storage;
|
|
1896
|
+
@if (s) {
|
|
1897
|
+
<div class="o-card" data-testid="svc-storage">
|
|
1898
|
+
<div class="o-ch"><b>Storage</b><span class="o-r">{{ d().diskGb }} GB</span></div>
|
|
1899
|
+
<div class="o-cb o-col" style="gap:8px">
|
|
1900
|
+
<label class="o-flex o-sm"><input type="checkbox" [ngModel]="s.autoGrow" (ngModelChange)="save({ autoGrow: $event })" [disabled]="busy() || !may('services.instance.configure')" data-testid="svc-autogrow" />Grow the disk automatically when it is {{ s.atPercent }}% full</label>
|
|
1901
|
+
@if (s.autoGrow) {
|
|
1902
|
+
<div class="o-flex o-wrap">
|
|
1903
|
+
<span class="o-sm">Up to</span>
|
|
1904
|
+
<input class="o-in o-sm" style="width:90px" type="number" [min]="d().diskGb" step="5" aria-label="Up to GB" [ngModel]="s.maxGb || s.limitGb" (change)="cap($event)" [disabled]="busy() || !may('services.instance.configure')" data-testid="svc-autogrow-max" />
|
|
1905
|
+
<span class="o-sm">GB</span>
|
|
1906
|
+
@if (s.grownAt) {<span class="o-faint o-xs o-r">last grew {{ when(s.grownAt) }}</span>}
|
|
1907
|
+
</div>
|
|
1908
|
+
<span class="o-faint o-xs">It grows by a quarter, at least 10 GB, without downtime; the people concerned are told each time.</span>
|
|
1909
|
+
}
|
|
1910
|
+
@if (s.expandable === false) {<span class="o-hint" data-testid="svc-autogrow-fixed">The volumes in this region cannot grow in place: you are told when the disk is nearly full. {{ s.message }}</span>}
|
|
1911
|
+
</div>
|
|
1912
|
+
</div>
|
|
1913
|
+
}
|
|
1914
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1915
|
+
}
|
|
1916
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ServiceStorageCard, decorators: [{
|
|
1917
|
+
type: Component,
|
|
1918
|
+
args: [{
|
|
1919
|
+
selector: 'o-svc-storage',
|
|
1920
|
+
imports: [FormsModule],
|
|
1921
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1922
|
+
template: `
|
|
1923
|
+
@let s = d().storage;
|
|
1924
|
+
@if (s) {
|
|
1925
|
+
<div class="o-card" data-testid="svc-storage">
|
|
1926
|
+
<div class="o-ch"><b>Storage</b><span class="o-r">{{ d().diskGb }} GB</span></div>
|
|
1927
|
+
<div class="o-cb o-col" style="gap:8px">
|
|
1928
|
+
<label class="o-flex o-sm"><input type="checkbox" [ngModel]="s.autoGrow" (ngModelChange)="save({ autoGrow: $event })" [disabled]="busy() || !may('services.instance.configure')" data-testid="svc-autogrow" />Grow the disk automatically when it is {{ s.atPercent }}% full</label>
|
|
1929
|
+
@if (s.autoGrow) {
|
|
1930
|
+
<div class="o-flex o-wrap">
|
|
1931
|
+
<span class="o-sm">Up to</span>
|
|
1932
|
+
<input class="o-in o-sm" style="width:90px" type="number" [min]="d().diskGb" step="5" aria-label="Up to GB" [ngModel]="s.maxGb || s.limitGb" (change)="cap($event)" [disabled]="busy() || !may('services.instance.configure')" data-testid="svc-autogrow-max" />
|
|
1933
|
+
<span class="o-sm">GB</span>
|
|
1934
|
+
@if (s.grownAt) {<span class="o-faint o-xs o-r">last grew {{ when(s.grownAt) }}</span>}
|
|
1935
|
+
</div>
|
|
1936
|
+
<span class="o-faint o-xs">It grows by a quarter, at least 10 GB, without downtime; the people concerned are told each time.</span>
|
|
1937
|
+
}
|
|
1938
|
+
@if (s.expandable === false) {<span class="o-hint" data-testid="svc-autogrow-fixed">The volumes in this region cannot grow in place: you are told when the disk is nearly full. {{ s.message }}</span>}
|
|
1939
|
+
</div>
|
|
1940
|
+
</div>
|
|
1941
|
+
}
|
|
1942
|
+
`,
|
|
1943
|
+
}]
|
|
1944
|
+
}] });
|
|
1945
|
+
|
|
1588
1946
|
const TABS = [
|
|
1589
1947
|
{ id: 'overview', label: 'Overview' }, { id: 'users', label: 'Databases and users' }, { id: 'backups', label: 'Backups' }, { id: 'metrics', label: 'Metrics' },
|
|
1590
1948
|
{ id: 'logs', label: 'Logs' }, { id: 'console', label: 'Console' }, { id: 'settings', label: 'Settings' },
|
|
@@ -1955,6 +2313,15 @@ class ServicePage {
|
|
|
1955
2313
|
removeDb(d, name) {
|
|
1956
2314
|
this.run(this.api.removeDatabase(this.key(), d.id, name), () => this.notice.set(`Database ${name} removed.`), 'The database could not be removed.');
|
|
1957
2315
|
}
|
|
2316
|
+
/** A Postgres extension from the allow-list (V8-04, D-30); what it needs comes with it. */
|
|
2317
|
+
addExt(d, db, name) {
|
|
2318
|
+
if (!name)
|
|
2319
|
+
return;
|
|
2320
|
+
this.run(this.api.addExtension(this.key(), d.id, db, name), () => this.notice.set(`${name} is being created in ${db}.`), 'The extension could not be created.');
|
|
2321
|
+
}
|
|
2322
|
+
dropExt(d, db, name) {
|
|
2323
|
+
this.run(this.api.removeExtension(this.key(), d.id, db, name), () => this.notice.set(`${name} is dropped from ${db}.`), 'The extension could not be dropped.');
|
|
2324
|
+
}
|
|
1958
2325
|
addUser(d) {
|
|
1959
2326
|
const name = this.userName().trim();
|
|
1960
2327
|
if (!name)
|
|
@@ -2265,6 +2632,17 @@ class ServicePage {
|
|
|
2265
2632
|
@for (db of d.databases; track db.name; let first = $first) {
|
|
2266
2633
|
<div class="o-row"><span class="o-mono o-b5">{{ db.name }}</span><span class="o-faint o-sm">{{ first ? 'default' : '' }}</span><span class="o-r o-faint o-sm">{{ when(db.createdAt) }}</span>
|
|
2267
2634
|
@if (has(d, 'services.instance.configure') && !first) {<button type="button" class="o-lnk o-faint" (click)="removeDb(d, db.name)" [attr.aria-label]="'Remove ' + db.name" [disabled]="busy()">×</button>}</div>
|
|
2635
|
+
@if (engine()?.extensions?.length) {
|
|
2636
|
+
<div class="o-row o-wrap" style="padding-left:24px" [attr.data-testid]="'svc-ext-' + db.name">
|
|
2637
|
+
@for (x of db.extensions ?? []; track x.name) {<span class="o-chip o-mono" [class.o-grn]="x.created" [title]="x.created ? 'Created' : 'Being created'">{{ x.name }}@if (has(d, 'services.instance.configure')) { <button type="button" class="o-lnk o-faint" (click)="dropExt(d, db.name, x.name)" [attr.aria-label]="'Drop ' + x.name" [disabled]="busy()">×</button>}</span>}
|
|
2638
|
+
@if (has(d, 'services.instance.configure') && !d.deletedAt) {
|
|
2639
|
+
<select class="o-in o-xs" style="width:auto" [attr.aria-label]="'Add an extension to ' + db.name" [ngModel]="''" (ngModelChange)="addExt(d, db.name, $event)" [disabled]="busy()" [attr.data-testid]="'svc-ext-add-' + db.name">
|
|
2640
|
+
<option value="">+ Extension</option>
|
|
2641
|
+
@for (e of engine()!.extensions!; track e.name) {<option [value]="e.name" [title]="e.description">{{ e.name }}</option>}
|
|
2642
|
+
</select>
|
|
2643
|
+
}
|
|
2644
|
+
</div>
|
|
2645
|
+
}
|
|
2268
2646
|
} @empty {<div class="o-empty">{{ engine()?.databases ? 'No databases yet.' : d.engineName + ' has no logical databases: keys live in database 0.' }}</div>}
|
|
2269
2647
|
</div>
|
|
2270
2648
|
}
|
|
@@ -2330,6 +2708,7 @@ class ServicePage {
|
|
|
2330
2708
|
</div>
|
|
2331
2709
|
</div>
|
|
2332
2710
|
<o-svc-dumps [d]="d" [projectKey]="key()" [dumps]="!!engine()?.dumps" (done)="notice.set($event); refresh()" (failed)="error.set($event)" />
|
|
2711
|
+
<o-svc-copies [d]="d" [projectKey]="key()" (done)="notice.set($event); refresh()" (failed)="error.set($event)" />
|
|
2333
2712
|
</div>
|
|
2334
2713
|
</div>
|
|
2335
2714
|
}
|
|
@@ -2354,6 +2733,8 @@ class ServicePage {
|
|
|
2354
2733
|
<button type="button" class="o-btn o-dark" (click)="resize(d)" [disabled]="busy() || !has(d, 'services.instance.configure') || (newPlan() === d.plan && newDisk() === d.diskGb)" data-testid="svc-resize">Resize</button>
|
|
2355
2734
|
</div>
|
|
2356
2735
|
</div>
|
|
2736
|
+
<o-svc-storage [d]="d" [projectKey]="key()" (done)="notice.set($event); refresh()" (failed)="error.set($event)" />
|
|
2737
|
+
<o-svc-upgrade [d]="d" [projectKey]="key()" (done)="notice.set($event); refresh()" (failed)="error.set($event)" />
|
|
2357
2738
|
<div class="o-card" data-testid="svc-ha">
|
|
2358
2739
|
<div class="o-ch"><b>High availability</b><span class="o-r">{{ d.readyNodes }} of {{ d.nodes }} node{{ d.nodes === 1 ? '' : 's' }} ready</span></div>
|
|
2359
2740
|
@switch (engine()?.ha) {
|
|
@@ -2460,13 +2841,13 @@ class ServicePage {
|
|
|
2460
2841
|
} @else {
|
|
2461
2842
|
<div class="o-page">@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>} @else {<div class="o-empty">Loading…</div>}</div>
|
|
2462
2843
|
}
|
|
2463
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: ServiceMetricsTab, selector: "o-svc-metrics", inputs: ["readings", "slowLines", "engine", "projectKey", "service", "dashboard"] }, { kind: "component", type: ServiceLogsTab, selector: "o-svc-logs", inputs: ["projectKey", "id", "service", "environment", "debugLogs"] }, { kind: "component", type: ServiceConsoleTab, selector: "o-svc-console", inputs: ["d", "projectKey", "kind"] }, { kind: "component", type: ServiceDumpsCard, selector: "o-svc-dumps", inputs: ["d", "projectKey", "dumps"], outputs: ["done", "failed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2844
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: ServiceMetricsTab, selector: "o-svc-metrics", inputs: ["readings", "slowLines", "engine", "projectKey", "service", "dashboard"] }, { kind: "component", type: ServiceLogsTab, selector: "o-svc-logs", inputs: ["projectKey", "id", "service", "environment", "debugLogs"] }, { kind: "component", type: ServiceConsoleTab, selector: "o-svc-console", inputs: ["d", "projectKey", "kind"] }, { kind: "component", type: ServiceDumpsCard, selector: "o-svc-dumps", inputs: ["d", "projectKey", "dumps"], outputs: ["done", "failed"] }, { kind: "component", type: ServiceUpgradeCard, selector: "o-svc-upgrade" }, { kind: "component", type: ServiceCopiesCard, selector: "o-svc-copies" }, { kind: "component", type: ServiceStorageCard, selector: "o-svc-storage" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2464
2845
|
}
|
|
2465
2846
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ServicePage, decorators: [{
|
|
2466
2847
|
type: Component,
|
|
2467
2848
|
args: [{
|
|
2468
2849
|
selector: 'o-service',
|
|
2469
|
-
imports: [FormsModule, RouterLink, ServiceMetricsTab, ServiceLogsTab, ServiceConsoleTab, ServiceDumpsCard],
|
|
2850
|
+
imports: [FormsModule, RouterLink, ServiceMetricsTab, ServiceLogsTab, ServiceConsoleTab, ServiceDumpsCard, ServiceUpgradeCard, ServiceCopiesCard, ServiceStorageCard],
|
|
2470
2851
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2471
2852
|
template: `
|
|
2472
2853
|
@if (d(); as d) {
|
|
@@ -2603,6 +2984,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2603
2984
|
@for (db of d.databases; track db.name; let first = $first) {
|
|
2604
2985
|
<div class="o-row"><span class="o-mono o-b5">{{ db.name }}</span><span class="o-faint o-sm">{{ first ? 'default' : '' }}</span><span class="o-r o-faint o-sm">{{ when(db.createdAt) }}</span>
|
|
2605
2986
|
@if (has(d, 'services.instance.configure') && !first) {<button type="button" class="o-lnk o-faint" (click)="removeDb(d, db.name)" [attr.aria-label]="'Remove ' + db.name" [disabled]="busy()">×</button>}</div>
|
|
2987
|
+
@if (engine()?.extensions?.length) {
|
|
2988
|
+
<div class="o-row o-wrap" style="padding-left:24px" [attr.data-testid]="'svc-ext-' + db.name">
|
|
2989
|
+
@for (x of db.extensions ?? []; track x.name) {<span class="o-chip o-mono" [class.o-grn]="x.created" [title]="x.created ? 'Created' : 'Being created'">{{ x.name }}@if (has(d, 'services.instance.configure')) { <button type="button" class="o-lnk o-faint" (click)="dropExt(d, db.name, x.name)" [attr.aria-label]="'Drop ' + x.name" [disabled]="busy()">×</button>}</span>}
|
|
2990
|
+
@if (has(d, 'services.instance.configure') && !d.deletedAt) {
|
|
2991
|
+
<select class="o-in o-xs" style="width:auto" [attr.aria-label]="'Add an extension to ' + db.name" [ngModel]="''" (ngModelChange)="addExt(d, db.name, $event)" [disabled]="busy()" [attr.data-testid]="'svc-ext-add-' + db.name">
|
|
2992
|
+
<option value="">+ Extension</option>
|
|
2993
|
+
@for (e of engine()!.extensions!; track e.name) {<option [value]="e.name" [title]="e.description">{{ e.name }}</option>}
|
|
2994
|
+
</select>
|
|
2995
|
+
}
|
|
2996
|
+
</div>
|
|
2997
|
+
}
|
|
2606
2998
|
} @empty {<div class="o-empty">{{ engine()?.databases ? 'No databases yet.' : d.engineName + ' has no logical databases: keys live in database 0.' }}</div>}
|
|
2607
2999
|
</div>
|
|
2608
3000
|
}
|
|
@@ -2668,6 +3060,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2668
3060
|
</div>
|
|
2669
3061
|
</div>
|
|
2670
3062
|
<o-svc-dumps [d]="d" [projectKey]="key()" [dumps]="!!engine()?.dumps" (done)="notice.set($event); refresh()" (failed)="error.set($event)" />
|
|
3063
|
+
<o-svc-copies [d]="d" [projectKey]="key()" (done)="notice.set($event); refresh()" (failed)="error.set($event)" />
|
|
2671
3064
|
</div>
|
|
2672
3065
|
</div>
|
|
2673
3066
|
}
|
|
@@ -2692,6 +3085,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2692
3085
|
<button type="button" class="o-btn o-dark" (click)="resize(d)" [disabled]="busy() || !has(d, 'services.instance.configure') || (newPlan() === d.plan && newDisk() === d.diskGb)" data-testid="svc-resize">Resize</button>
|
|
2693
3086
|
</div>
|
|
2694
3087
|
</div>
|
|
3088
|
+
<o-svc-storage [d]="d" [projectKey]="key()" (done)="notice.set($event); refresh()" (failed)="error.set($event)" />
|
|
3089
|
+
<o-svc-upgrade [d]="d" [projectKey]="key()" (done)="notice.set($event); refresh()" (failed)="error.set($event)" />
|
|
2695
3090
|
<div class="o-card" data-testid="svc-ha">
|
|
2696
3091
|
<div class="o-ch"><b>High availability</b><span class="o-r">{{ d.readyNodes }} of {{ d.nodes }} node{{ d.nodes === 1 ? '' : 's' }} ready</span></div>
|
|
2697
3092
|
@switch (engine()?.ha) {
|