Files
floci-ui/packages/frontend/src/components/DatabaseSnapshotsPanel.tsx
T
Zakaria 91ce11274d
CI / checks (pull_request) Successful in 25m47s
chore: show TheCloud instead of Floci in console text and messages
Labels, empty states, error messages and defaults the console shows now name
TheCloud or its runtimes. Identifiers that must match the emulator (FLOCI_*
settings, the X-Floci-CSRF header, emulator-returned host names) are unchanged.
2026-10-24 14:30:44 -04:00

237 lines
8.1 KiB
TypeScript

import { useState } from "react";
import { Camera, Loader2, Plus, RefreshCw } from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createDatabaseSnapshot, listDatabaseSnapshots } from "@/api/cloudProxyClient";
import { HttpError } from "@/api/HttpClient";
import { EmptyState } from "@/components/EmptyState";
import { capabilityEnabled } from "@/lib/capabilities";
import type { CloudProvider } from "@/types/cloud";
import type { CloudResource } from "@/types/resource";
import type { CapabilitySchema, DatabaseActionName } from "@/types/schema";
export interface DatabaseSnapshotsPanelProps {
cloud: CloudProvider;
instances: CloudResource[];
listCapability?: CapabilitySchema<DatabaseActionName>;
createCapability?: CapabilitySchema<DatabaseActionName>;
runtimeReachable: boolean;
}
export function DatabaseSnapshotsPanel({
cloud,
instances,
listCapability,
createCapability,
runtimeReachable,
}: DatabaseSnapshotsPanelProps) {
const qc = useQueryClient();
const [sourceInstance, setSourceInstance] = useState("");
const [snapshotId, setSnapshotId] = useState("");
const [createOpen, setCreateOpen] = useState(false);
const canList = runtimeReachable && capabilityEnabled(listCapability);
const canCreate = runtimeReachable && capabilityEnabled(createCapability);
const snapshotsQuery = useQuery({
queryKey: ["database-snapshots", cloud],
queryFn: ({ signal }) => listDatabaseSnapshots(cloud, undefined, signal),
enabled: canList,
});
const createMut = useMutation({
mutationFn: (input: { instanceIdentifier: string; snapshotIdentifier: string }) =>
createDatabaseSnapshot(cloud, input),
onSuccess: () => {
setSourceInstance("");
setSnapshotId("");
setCreateOpen(false);
void qc.invalidateQueries({ queryKey: ["database-snapshots", cloud] });
},
});
const handleSourceChange = (newInstance: string) => {
setSourceInstance(newInstance);
if (newInstance) {
setSnapshotId(`${newInstance}-snapshot-${Date.now()}`);
} else {
setSnapshotId("");
}
};
const isFormValid = Boolean(sourceInstance.trim() && snapshotId.trim());
const handleCreateSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!isFormValid || !canCreate || createMut.isPending) return;
createMut.mutate({
instanceIdentifier: sourceInstance.trim(),
snapshotIdentifier: snapshotId.trim(),
});
};
const getErrorMessage = (error: unknown): string => {
if (
error instanceof HttpError &&
typeof error.body === "object" &&
error.body !== null &&
"code" in error.body &&
(error.body as { code?: unknown }).code === "operation_not_implemented"
) {
return "Snapshot creation is available in TheCloud, but the selected runtime does not implement CreateDBSnapshot.";
}
if (error instanceof Error) {
return error.message;
}
return "Snapshot creation failed.";
};
return (
<section className="table-panel">
<div className="input-row resource-table-bar">
<div>
<p className="eyebrow">Database Snapshots</p>
<span className="muted">
{snapshotsQuery.data ? `${snapshotsQuery.data.length} snapshots` : "Snapshots"}
</span>
</div>
<div className="resource-table-tools">
<button
className="button"
type="button"
disabled={!canCreate}
title={createCapability?.reason}
onClick={() => setCreateOpen((open) => !open)}
>
<Plus size={14} />
Create DB snapshot
</button>
<button
className="button"
type="button"
disabled={!canList || snapshotsQuery.isFetching}
onClick={() => snapshotsQuery.refetch()}
>
<RefreshCw size={14} />
{snapshotsQuery.isFetching ? "Loading" : "Refresh"}
</button>
</div>
</div>
{createOpen && (
<div className="resource-create-inline" style={{ margin: "10px 12px 16px" }}>
<form onSubmit={handleCreateSubmit} className="dynamic-form">
<div className="form-group">
<label className="form-label" htmlFor="source-instance-select">
Source Instance *
</label>
<select
id="source-instance-select"
className="input"
required
value={sourceInstance}
onChange={(e) => handleSourceChange(e.target.value)}
>
<option value="">Select a DB instance</option>
{instances.map((inst) => (
<option key={inst.id} value={inst.name}>
{inst.name}
</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label" htmlFor="snapshot-id-input">
Snapshot Identifier *
</label>
<input
id="snapshot-id-input"
className="input"
type="text"
required
value={snapshotId}
onChange={(e) => setSnapshotId(e.target.value)}
placeholder="e.g. my-db-snapshot-1"
/>
</div>
{createMut.isError && (
<div role="alert" className="error-text compact-text" style={{ marginTop: 8 }}>
{getErrorMessage(createMut.error)}
</div>
)}
<div className="form-actions" style={{ marginTop: 12 }}>
<button
className="button primary"
type="submit"
disabled={!canCreate || !isFormValid || createMut.isPending}
title={createCapability?.reason}
>
{createMut.isPending ? "Creating…" : "Create snapshot"}
</button>
<button
className="button"
type="button"
disabled={createMut.isPending}
onClick={() => {
setCreateOpen(false);
createMut.reset();
}}
>
Cancel
</button>
</div>
</form>
</div>
)}
{!canList ? (
<p className="muted compact-text" style={{ padding: "16px" }}>
{listCapability?.reason ?? "Snapshot listing is not available."}
</p>
) : snapshotsQuery.isLoading ? (
<div className="empty compact">
<Loader2 size={18} className="spin" />
<p>Loading snapshots…</p>
</div>
) : snapshotsQuery.isError ? (
<div role="alert" className="error-text compact-text" style={{ padding: "16px" }}>
{snapshotsQuery.error instanceof Error
? snapshotsQuery.error.message
: "Failed to load database snapshots."}
</div>
) : (snapshotsQuery.data?.length ?? 0) === 0 ? (
<EmptyState
icon={Camera}
title="No snapshots"
description="No snapshots returned for this account."
/>
) : (
<div className="snapshot-list" style={{ padding: "0 12px 12px" }}>
{snapshotsQuery.data?.map((snapshot) => (
<div className="snapshot-row" key={snapshot.id || snapshot.name}>
<div>
<strong>{snapshot.name}</strong>
<span>
{snapshot.instanceIdentifier
? `Instance: ${snapshot.instanceIdentifier}`
: "Unknown instance"}
{snapshot.createdAt
? ` · ${snapshot.createdAt}`
: " · No creation timestamp"}
{snapshot.engine ? ` · ${snapshot.engine}` : ""}
{snapshot.version ? ` (${snapshot.version})` : ""}
</span>
</div>
<span className="badge neutral">
{snapshot.status ?? "unknown"}
</span>
</div>
))}
</div>
)}
</section>
);
}