add tags to custom instance dbs

This commit is contained in:
Diego Imbert
2025-11-26 16:13:55 +01:00
parent 14fd6c46ea
commit 77f84eccd9
9 changed files with 116 additions and 45 deletions
@@ -1,4 +1,17 @@
-- Add up migration script here
UPDATE global_settings
SET value = jsonb_build_object(
'status', (
SELECT jsonb_object_agg(
key,
value - 'tag'
)
FROM jsonb_each(value->'databases')
),
'user_pwd', value->'user_pwd'
)
WHERE name = 'custom_instance_pg_databases';
UPDATE global_settings
SET name = 'ducklake_settings',
value = jsonb_build_object(
@@ -12,3 +12,16 @@ SET name = 'custom_instance_pg_databases',
'status', value->'instance_catalog_db_status'
)
WHERE name = 'ducklake_settings';
UPDATE global_settings
SET value = jsonb_build_object(
'databases', (
SELECT jsonb_object_agg(
key,
value || jsonb_build_object('tag', 'ducklake')
)
FROM jsonb_each(value->'status')
),
'user_pwd', value->'user_pwd'
)
WHERE name = 'custom_instance_pg_databases';
+24 -8
View File
@@ -831,10 +831,10 @@ paths:
schema:
type: boolean
/settings/get_custom_instance_pg_databases_status:
/settings/get_custom_instance_pg_databases:
post:
summary: Returns the set-up statuses of custom instance pg databases
operationId: getCustomInstanceDbStatus
operationId: getCustomInstanceDbs
tags:
- setting
responses:
@@ -845,7 +845,7 @@ paths:
schema:
type: object
additionalProperties:
$ref: "#/components/schemas/CustomInstanceDbStatus"
$ref: "#/components/schemas/CustomInstanceDb"
/settings/setup_custom_instance_pg_database/{name}:
post:
@@ -853,6 +853,15 @@ paths:
operationId: setupCustomInstanceDb
tags:
- setting
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
tag:
$ref: "#/components/schemas/CustomInstanceDbTag"
parameters:
- in: path
name: name
@@ -866,7 +875,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/CustomInstanceDbStatus"
$ref: "#/components/schemas/CustomInstanceDb"
/settings/global/{key}:
get:
@@ -18181,7 +18190,7 @@ components:
- SKIP
- FAIL
CustomInstanceDbStatusLogs:
CustomInstanceDbLogs:
type: object
properties:
super_admin:
@@ -18192,20 +18201,25 @@ components:
$ref: "#/components/schemas/LoggedWizardStatus"
created_database:
$ref: "#/components/schemas/LoggedWizardStatus"
description: Created database status log
db_connect:
$ref: "#/components/schemas/LoggedWizardStatus"
grant_permissions:
$ref: "#/components/schemas/LoggedWizardStatus"
CustomInstanceDbStatus:
CustomInstanceDbTag:
type: string
enum:
- ducklake
- datatable
CustomInstanceDb:
type: object
required:
- logs
- success
properties:
logs:
$ref: "#/components/schemas/CustomInstanceDbStatusLogs"
$ref: "#/components/schemas/CustomInstanceDbLogs"
success:
type: boolean
description: Whether the operation completed successfully
@@ -18215,6 +18229,8 @@ components:
nullable: true
description: Error message if the operation failed
example: "Connection timeout"
tag:
$ref: "#/components/schemas/CustomInstanceDbTag"
NewSqsTrigger:
type: object
+21 -14
View File
@@ -70,8 +70,8 @@ pub fn global_service() -> Router {
post(acknowledge_critical_alert),
)
.route(
"/get_custom_instance_pg_databases_status",
post(get_custom_instance_pg_databases_status),
"/get_custom_instance_pg_databases",
post(get_custom_instance_pg_databases),
)
.route(
"/setup_custom_instance_pg_database/:name",
@@ -583,15 +583,16 @@ pub async fn acknowledge_all_critical_alerts() -> error::Error {
}
#[derive(Deserialize, Debug, Serialize)]
struct CustomInstanceDbStatus {
logs: CustomInstanceDbStatusLogs, // (Step, Message)[]
struct CustomInstanceDb {
logs: CustomInstanceDbLogs, // (Step, Message)[]
success: bool,
error: Option<String>,
tag: Option<String>,
}
#[derive(Deserialize, Debug, Serialize, Default)]
#[serde(default)]
struct CustomInstanceDbStatusLogs {
struct CustomInstanceDbLogs {
super_admin: String,
#[serde(skip_serializing_if = "String::is_empty")]
database_credentials: String,
@@ -605,39 +606,45 @@ struct CustomInstanceDbStatusLogs {
grant_permissions: String,
}
async fn get_custom_instance_pg_databases_status(
async fn get_custom_instance_pg_databases(
_authed: ApiAuthed,
Extension(db): Extension<DB>,
) -> JsonResult<HashMap<String, CustomInstanceDbStatus>> {
) -> JsonResult<HashMap<String, CustomInstanceDb>> {
let result = sqlx::query_scalar!(
r#"SELECT value->'status' FROM global_settings WHERE name = 'custom_instance_pg_databases'"#,
r#"SELECT value->'databases' FROM global_settings WHERE name = 'custom_instance_pg_databases'"#,
)
.fetch_one(&db)
.await?
.ok_or_else(|| error::Error::ExecutionErr("Couldn't find custom_instance_pg_databases".to_string()))?;
let result = serde_json::from_value(result).map_err(|e| {
error::Error::ExecutionErr(format!(
"couldn't parse custom_instance_pg_databases.status : {}",
"couldn't parse custom_instance_pg_databases.databases : {}",
e.to_string()
))
})?;
return Ok(Json(result));
}
#[derive(Deserialize)]
struct SetupCustomInstanceDbBody {
tag: Option<String>,
}
async fn setup_custom_instance_pg_database(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(dbname): Path<String>,
) -> JsonResult<CustomInstanceDbStatus> {
let mut logs = CustomInstanceDbStatusLogs::default();
Json(body): Json<SetupCustomInstanceDbBody>,
) -> JsonResult<CustomInstanceDb> {
let mut logs = CustomInstanceDbLogs::default();
let result = setup_custom_instance_pg_database_inner(authed, &db, &dbname, &mut logs).await;
let success = result.is_ok();
let error = result.err().map(|e| e.to_string());
let status = CustomInstanceDbStatus { logs, success, error };
let status = CustomInstanceDb { logs, success, error, tag: body.tag };
let status_json = serde_json::to_value(&status).map_err(to_anyhow)?;
// Save that the database was setup successfully
sqlx::query!(
r#"UPDATE global_settings SET value = jsonb_set(value, '{status}', (COALESCE(value->'status', '{}'::jsonb) || to_jsonb($1::json))) WHERE name = 'custom_instance_pg_databases'"#,
r#"UPDATE global_settings SET value = jsonb_set(value, '{databases}', (COALESCE(value->'databases', '{}'::jsonb) || to_jsonb($1::json))) WHERE name = 'custom_instance_pg_databases'"#,
json!({ dbname: status_json })
).execute(&db).await?;
@@ -648,7 +655,7 @@ async fn setup_custom_instance_pg_database_inner(
authed: ApiAuthed,
db: &DB,
dbname: &str,
logs: &mut CustomInstanceDbStatusLogs,
logs: &mut CustomInstanceDbLogs,
) -> Result<()> {
require_super_admin(db, &authed.email).await?;
logs.super_admin = "OK".to_string();
@@ -1,5 +1,5 @@
<script lang="ts">
import type { GetCustomInstanceDbStatusResponse } from '$lib/gen'
import type { CustomInstanceDbTag, GetCustomInstanceDbsResponse } from '$lib/gen'
import type { ResourceReturn } from 'runed'
import Select from '../select/Select.svelte'
import { safeSelectItems } from '../select/utils.svelte'
@@ -13,24 +13,26 @@
type Props = {
value: string | undefined
customInstanceDbStatuses: ResourceReturn<GetCustomInstanceDbStatusResponse>
customInstanceDbs: ResourceReturn<GetCustomInstanceDbsResponse>
confirmationModal: ConfirmationModalHandle
dbManagerDrawer: DBManagerDrawer | undefined
wizardBottomHint?: Snippet | undefined
class?: string
tag?: CustomInstanceDbTag
}
let {
value = $bindable(),
customInstanceDbStatuses,
customInstanceDbs,
confirmationModal,
dbManagerDrawer,
wizardBottomHint,
class: className
class: className,
tag
}: Props = $props()
let openedDbNameWizard = $state(false)
let status = $derived(customInstanceDbStatuses.current?.[value ?? ''])
let status = $derived(customInstanceDbs.current?.[value ?? ''])
</script>
<div class="flex relative items-center {className}">
@@ -40,7 +42,7 @@
bind:value
onCreateItem={(i) => (value = i)}
placeholder="PostgreSQL database name"
items={safeSelectItems(Object.keys(customInstanceDbStatuses.current ?? {}))}
items={safeSelectItems(Object.keys(customInstanceDbs.current ?? {}))}
disabled={!$isCustomInstanceDbEnabled}
/>
@@ -65,14 +67,15 @@
</div>
<CustomInstanceDbWizardModal
{customInstanceDbStatuses}
{customInstanceDbs}
{confirmationModal}
{dbManagerDrawer}
{tag}
bottomHint={wizardBottomHint}
bind:opened={
() =>
openedDbNameWizard
? { dbname: value ?? '', status: customInstanceDbStatuses.current?.[value ?? ''] }
? { dbname: value ?? '', status: customInstanceDbs.current?.[value ?? ''] }
: undefined,
(v) => !v && (openedDbNameWizard = false)
}
@@ -1,8 +1,9 @@
<script lang="ts">
import {
SettingService,
type CustomInstanceDbStatus,
type GetCustomInstanceDbStatusResponse
type CustomInstanceDb,
type CustomInstanceDbTag,
type GetCustomInstanceDbsResponse
} from '$lib/gen'
import { slide } from 'svelte/transition'
import Modal2 from '../common/modal/Modal2.svelte'
@@ -20,19 +21,21 @@
import type { Snippet } from 'svelte'
type Props = {
customInstanceDbStatuses: ResourceReturn<GetCustomInstanceDbStatusResponse>
customInstanceDbs: ResourceReturn<GetCustomInstanceDbsResponse>
confirmationModal: ConfirmationModalHandle
dbManagerDrawer: DBManagerDrawer | undefined
bottomHint?: Snippet | undefined
opened: { status: CustomInstanceDbStatus | undefined; dbname: string } | undefined
opened: { status: CustomInstanceDb | undefined; dbname: string } | undefined
tag?: CustomInstanceDbTag
}
let {
customInstanceDbStatuses,
customInstanceDbs,
confirmationModal,
dbManagerDrawer,
bottomHint,
opened = $bindable()
opened = $bindable(),
tag
}: Props = $props()
let customInstanceDbSetupIsRunning = $state(false)
@@ -154,8 +157,11 @@
try {
customInstanceDbSetupIsRunning = true
let result = await SettingService.setupCustomInstanceDb({ name: dbname })
await customInstanceDbStatuses.refetch()
let result = await SettingService.setupCustomInstanceDb({
name: dbname,
requestBody: { tag }
})
await customInstanceDbs.refetch()
if (result.success) {
if (!wasAlreadySuccessful) sendUserToast('Setup successful')
else sendUserToast('Check successful')
@@ -97,7 +97,8 @@
}
})
}
const instanceCatalogStatuses = resource([], SettingService.getCustomInstanceDbStatus)
const instanceDbs = resource([], SettingService.getCustomInstanceDbs)
async function onSave() {
try {
@@ -106,7 +107,7 @@
dataTableSettings.dataTables.some(
(d) =>
d.database.resource_type === 'instance' &&
!instanceCatalogStatuses.current?.[d.database.resource_path ?? '']?.success
!instanceDbs.current?.[d.database.resource_path ?? '']?.success
)
) {
let confirm = await confirmationModal.ask({
@@ -72,7 +72,7 @@
import Tooltip from '../Tooltip.svelte'
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
import { createAsyncConfirmationModal } from '../common/confirmationModal/asyncConfirmationModal.svelte'
import { clone } from '$lib/utils'
import { clone, filterObject } from '$lib/utils'
import Alert from '../common/alert/Alert.svelte'
import { deepEqual } from 'fast-equals'
import Popover from '../meltComponents/Popover.svelte'
@@ -125,7 +125,9 @@
)
)
const customInstanceDbStatuses = resource([], SettingService.getCustomInstanceDbStatus)
const customInstanceDbs = resource([], async () =>
filterObject(await SettingService.getCustomInstanceDbs(), (_, v) => v.tag === 'ducklake')
)
async function onSave() {
try {
@@ -134,7 +136,7 @@
ducklakeSettings.ducklakes.some(
(d) =>
d.catalog.resource_type === 'instance' &&
!customInstanceDbStatuses.current?.[d.catalog.resource_path ?? '']?.success
!customInstanceDbs.current?.[d.catalog.resource_path ?? '']?.success
)
) {
let confirm = await confirmationModal.ask({
@@ -276,9 +278,10 @@
<CustomInstanceDbSelect
class="flex-1"
bind:value={ducklake.catalog.resource_path}
{customInstanceDbStatuses}
{customInstanceDbs}
{confirmationModal}
{dbManagerDrawer}
tag="ducklake"
>
{#snippet wizardBottomHint()}
Note: the 'Manage' button below is different from the Manage Ducklake button.
+9
View File
@@ -1655,3 +1655,12 @@ export function getCssColor(
}
export type IconType = Component<{ size?: number }> | typeof import('lucide-svelte').Dot
export function filterObject<Key extends string, Value>(
obj: Record<Key, Value>,
f: (k: Key, v: Value) => boolean
): Record<Key, Value> {
return Object.fromEntries(
Object.entries(obj).filter(([k, v]) => f(k as Key, v as Value))
) as Record<Key, Value>
}