Skip to content

Commit 53c7758

Browse files
committed
Resource type replacer
1 parent 7387ac3 commit 53c7758

File tree

11 files changed

+262
-111
lines changed

11 files changed

+262
-111
lines changed

backend/windmill-api-workspaces/src/workspaces.rs

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,9 @@ struct CreateWorkspaceFork {
348348
color: Option<String>,
349349
#[serde(default)]
350350
datatable_behaviors: Option<HashMap<String, DataTableForkBehavior>>,
351+
/// For resource-type datatables: map of datatable name -> target resource path
352+
#[serde(default)]
353+
datatable_target_resources: Option<HashMap<String, String>>,
351354
}
352355

353356
#[derive(Deserialize)]
@@ -1145,24 +1148,41 @@ async fn list_ducklakes(
11451148
Ok(Json(ducklakes))
11461149
}
11471150

1151+
#[derive(Serialize)]
1152+
struct DataTableInfo {
1153+
name: String,
1154+
resource_type: String,
1155+
resource_path: String,
1156+
}
1157+
11481158
async fn list_datatables(
11491159
_authed: ApiAuthed,
11501160
Extension(db): Extension<DB>,
11511161
Path(w_id): Path<String>,
1152-
) -> JsonResult<Vec<String>> {
1153-
let datatables = sqlx::query_scalar!(
1154-
r#"
1155-
SELECT jsonb_object_keys(ws.datatable->'datatables') AS datatable_name
1156-
FROM workspace_settings ws
1157-
WHERE ws.workspace_id = $1
1158-
"#,
1162+
) -> JsonResult<Vec<DataTableInfo>> {
1163+
let datatable_config = sqlx::query_scalar!(
1164+
"SELECT datatable FROM workspace_settings WHERE workspace_id = $1",
11591165
&w_id
11601166
)
1161-
.fetch_all(&db)
1162-
.await?
1163-
.into_iter()
1164-
.filter_map(|s| s)
1165-
.collect();
1167+
.fetch_one(&db)
1168+
.await?;
1169+
1170+
let datatables: Vec<DataTableInfo> = match datatable_config {
1171+
Some(config) => {
1172+
let settings: DataTableSettings = serde_json::from_value(config)
1173+
.unwrap_or(DataTableSettings { datatables: HashMap::new() });
1174+
settings
1175+
.datatables
1176+
.into_iter()
1177+
.map(|(name, dt)| DataTableInfo {
1178+
name,
1179+
resource_type: dt.database.resource_type.as_ref().to_string(),
1180+
resource_path: dt.database.resource_path,
1181+
})
1182+
.collect()
1183+
}
1184+
None => vec![],
1185+
};
11661186

11671187
Ok(Json(datatables))
11681188
}
@@ -1643,6 +1663,7 @@ async fn fork_all_datatables(
16431663
source_workspace_id: &str,
16441664
target_workspace_id: &str,
16451665
datatable_behaviors: &Option<HashMap<String, DataTableForkBehavior>>,
1666+
datatable_target_resources: &Option<HashMap<String, String>>,
16461667
) -> Result<()> {
16471668
if !target_workspace_id.starts_with("wm-fork") {
16481669
return Err(Error::BadRequest(
@@ -1671,19 +1692,49 @@ async fn fork_all_datatables(
16711692
.and_then(|m| m.get(name).copied())
16721693
.unwrap_or(DataTableForkBehavior::SchemaOnly);
16731694

1674-
if behavior == DataTableForkBehavior::KeepOriginal {
1695+
// Resource-type datatables: only action is replacing the resource path if a target is provided
1696+
if dt.database.resource_type != DataTableCatalogResourceType::Instance {
1697+
let target_resource = datatable_target_resources
1698+
.as_ref()
1699+
.and_then(|m| m.get(name));
1700+
1701+
if let Some(target_path) = target_resource {
1702+
let new_datatable = DataTable {
1703+
database: DataTableDatabase {
1704+
resource_type: DataTableCatalogResourceType::Postgresql,
1705+
resource_path: target_path.clone(),
1706+
},
1707+
};
1708+
let datatable_value: serde_json::Value = serde_json::to_value(&new_datatable)
1709+
.map_err(|err| Error::internal_err(err.to_string()))?;
1710+
1711+
sqlx::query!(
1712+
r#"UPDATE workspace_settings
1713+
SET datatable = jsonb_set(
1714+
COALESCE(datatable, '{"datatables":{}}'::jsonb),
1715+
ARRAY['datatables', $1],
1716+
$2
1717+
)
1718+
WHERE workspace_id = $3"#,
1719+
name,
1720+
datatable_value,
1721+
target_workspace_id
1722+
)
1723+
.execute(db)
1724+
.await?;
1725+
1726+
tracing::info!(
1727+
"Forked resource datatable '{}': replaced resource with '{}'",
1728+
name,
1729+
target_path
1730+
);
1731+
}
1732+
// If no target resource provided, config already copied as-is (keep original)
16751733
continue;
16761734
}
16771735

1678-
// Resource-type datatables cannot be forked (would require creating an instance DB
1679-
// from an external resource). Skip gracefully.
1680-
if dt.database.resource_type != DataTableCatalogResourceType::Instance {
1681-
tracing::warn!(
1682-
"Skipping fork of datatable '{}' in '{}': resource type '{}' is not supported for forking",
1683-
name,
1684-
source_workspace_id,
1685-
dt.database.resource_type.as_ref()
1686-
);
1736+
// Instance-type datatables below
1737+
if behavior == DataTableForkBehavior::KeepOriginal {
16871738
continue;
16881739
}
16891740

@@ -3926,6 +3977,7 @@ async fn create_workspace_fork(
39263977
&parent_workspace_id,
39273978
&forked_id,
39283979
&nw.datatable_behaviors,
3980+
&nw.datatable_target_resources,
39293981
)
39303982
.await?;
39313983

backend/windmill-api/openapi.yaml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3333,7 +3333,21 @@ paths:
33333333
schema:
33343334
type: array
33353335
items:
3336-
type: string
3336+
type: object
3337+
properties:
3338+
name:
3339+
type: string
3340+
resource_type:
3341+
type: string
3342+
enum:
3343+
- instance
3344+
- postgres
3345+
resource_path:
3346+
type: string
3347+
required:
3348+
- name
3349+
- resource_type
3350+
- resource_path
33373351

33383352
/w/{workspace}/workspaces/list_datatable_schemas:
33393353
get:
@@ -22899,6 +22913,11 @@ components:
2289922913
- schema_only
2290022914
- schema_and_data
2290122915
- keep_original
22916+
datatable_target_resources:
22917+
type: object
22918+
description: "For resource-type datatables: map of datatable name to target resource path."
22919+
additionalProperties:
22920+
type: string
2290222921
required:
2290322922
- id
2290422923
- name

frontend/src/lib/components/DatatablePicker.svelte

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@
2929
}: Props = $props()
3030
3131
let datatables = usePromise(() =>
32-
WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' })
32+
WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' }).then((r) =>
33+
r.map((d) => d.name)
34+
)
3335
)
34-
3536
</script>
3637

3738
<div class={className}>
@@ -46,9 +47,6 @@
4647
{onClear}
4748
/>
4849
{#if showSchemaExplorer && value && assetCanBeExplored({ kind: 'datatable', path: value })}
49-
<ExploreAssetButton
50-
class="mt-1 w-fit"
51-
asset={{ kind: 'datatable', path: value }}
52-
/>
50+
<ExploreAssetButton class="mt-1 w-fit" asset={{ kind: 'datatable', path: value }} />
5351
{/if}
5452
</div>

frontend/src/lib/components/Editor.svelte

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1570,7 +1570,9 @@
15701570
15711571
let customTsTypesData = resource([() => lang], async () => {
15721572
if (lang !== 'typescript') return undefined
1573-
let datatables = await WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' })
1573+
let datatables = (
1574+
await WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' })
1575+
).map((d) => d.name)
15741576
let ducklakes = await WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? '' })
15751577
return { datatables, ducklakes }
15761578
})
@@ -1912,15 +1914,12 @@
19121914
})
19131915
})
19141916
1915-
let isTsWorkerInitialized = resource(
1916-
[() => lang, () => initialized],
1917-
async () => {
1918-
if (lang !== 'typescript' || !initialized) return false
1919-
// Use the stable model URI (computed once at mount), not filePath which changes on rename
1920-
await waitForWorkerInitialization(uri)
1921-
return true
1922-
}
1923-
)
1917+
let isTsWorkerInitialized = resource([() => lang, () => initialized], async () => {
1918+
if (lang !== 'typescript' || !initialized) return false
1919+
// Use the stable model URI (computed once at mount), not filePath which changes on rename
1920+
await waitForWorkerInitialization(uri)
1921+
return true
1922+
})
19241923
19251924
// Update SQL query type information in the TypeScript worker
19261925
// This enables TypeScript to show proper types for SQL template literals
@@ -1939,16 +1938,9 @@
19391938
updateSqlQueriesInWorker(uri, $state.snapshot(preparedAssetsSqlQueries))
19401939
}, 250)
19411940
1942-
watch(
1943-
[
1944-
() => preparedAssetsSqlQueries,
1945-
() => lang,
1946-
() => isTsWorkerInitialized.current
1947-
],
1948-
() => {
1949-
handleSqlTypingInTs()
1950-
}
1951-
)
1941+
watch([() => preparedAssetsSqlQueries, () => lang, () => isTsWorkerInitialized.current], () => {
1942+
handleSqlTypingInTs()
1943+
})
19521944
19531945
watch([() => customTsTypesData.current], setTypescriptCustomTypes)
19541946
</script>

frontend/src/lib/components/EditorBar.svelte

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,14 @@
7979
iconOnly?: boolean
8080
validCode?: boolean
8181
kind?: 'script' | 'trigger' | 'approval'
82-
template?: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative' | 'claudesandbox'
82+
template?:
83+
| 'pgsql'
84+
| 'mysql'
85+
| 'script'
86+
| 'docker'
87+
| 'powershell'
88+
| 'bunnative'
89+
| 'claudesandbox'
8390
collabMode?: boolean
8491
collabLive?: boolean
8592
collabUsers?: { name: string }[]
@@ -773,7 +780,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
773780
itemName="data table"
774781
loadItems={async () =>
775782
(await WorkspaceService.listDataTables({ workspace: $workspaceStore ?? 'NO_W' })).map(
776-
(path) => ({ path })
783+
(d) => ({ path: d.name })
777784
)}
778785
>
779786
{#snippet submission()}

frontend/src/lib/components/ResourcePicker.svelte

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
onClear?: () => void
3030
excludedValues?: string[]
3131
datatableAsPgResource?: boolean
32+
filter?: (s: string) => boolean
3233
}
3334
3435
let {
@@ -47,7 +48,8 @@
4748
class: className = '',
4849
onClear = undefined,
4950
excludedValues = undefined,
50-
datatableAsPgResource = false
51+
datatableAsPgResource = false,
52+
filter
5153
}: Props = $props()
5254
5355
if (initialValue && value == undefined) {
@@ -85,6 +87,9 @@
8587
})
8688
8789
let collection = $state(value ? [{ value, label: value, type: valueType }] : [])
90+
let filteredCollection = $derived(
91+
filter ? collection.filter((x) => filter?.(x.value)) : collection
92+
)
8893
8994
export async function askNewResource() {
9095
appConnect?.open?.(resourceType)
@@ -126,8 +131,8 @@
126131
})
127132
for (const dt of datatables) {
128133
nc.push({
129-
value: `datatable://${dt}`,
130-
label: `datatable://${dt}`,
134+
value: `datatable://${dt.name}`,
135+
label: `datatable://${dt.name}`,
131136
type: 'postgresql'
132137
})
133138
}
@@ -222,7 +227,7 @@
222227
valueType = undefined
223228
onClear?.()
224229
}}
225-
items={collection}
230+
items={filteredCollection}
226231
clearable
227232
class="text-clip grow min-w-0"
228233
inputClass={selectInputClass}

frontend/src/lib/components/assets/AssetsDropdownButton.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@
6060
}
6161
})
6262
63-
let datatables = resource([], () =>
64-
WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' })
63+
let datatables = resource([], async () =>
64+
WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' }).then((r) =>
65+
r.map((d) => d.name)
66+
)
6567
)
6668
let ducklakes = resource([], () =>
6769
WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? '' })

frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@
3939
let selectedTableKey = $state<string | undefined>(undefined)
4040
4141
// Load available datatables from workspace
42-
const datatables = resource<string[]>([], async () => {
42+
const datatables = resource([], async () => {
4343
if (!$workspaceStore) return []
4444
try {
45-
return await WorkspaceService.listDataTables({ workspace: $workspaceStore })
45+
return (await WorkspaceService.listDataTables({ workspace: $workspaceStore })).map(
46+
(d) => d.name
47+
)
4648
} catch (e) {
4749
console.error('Failed to load datatables:', e)
4850
return []
@@ -51,9 +53,9 @@
5153
5254
export function openDrawer() {
5355
// Auto-select first datatable if only one exists
54-
if (datatables.current.length === 1) {
55-
selectedDatatable = datatables.current[0]
56-
} else if (datatables.current.length > 1 && datatables.current.includes('main')) {
56+
if (datatables.current?.length === 1) {
57+
selectedDatatable = datatables.current?.[0]
58+
} else if ((datatables.current?.length ?? 0) > 1 && datatables.current?.includes('main')) {
5759
selectedDatatable = 'main'
5860
} else {
5961
selectedDatatable = undefined
@@ -111,7 +113,7 @@
111113
}
112114
113115
const datatableItems = $derived(
114-
datatables.current.map((dt) => ({
116+
datatables.current?.map((dt) => ({
115117
value: dt,
116118
label: dt
117119
}))
@@ -179,7 +181,7 @@
179181
<LoaderCircle size={14} class="animate-spin" />
180182
<span class="text-sm">Loading...</span>
181183
</div>
182-
{:else if datatables.current.length >= 1}
184+
{:else if datatables.current?.length ?? 0 >= 1}
183185
<Select
184186
transformInputSelectedText={(s) => `Datatable: ${s}`}
185187
items={datatableItems}

frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import { get } from 'svelte/store'
99
* Pass a getter function that returns the workspace to create a reactive dependency.
1010
*/
1111
export function createDatatablesResource(getWorkspace: () => string | undefined) {
12-
return resource.pre<string[]>([() => getWorkspace() ?? ''], async () => {
12+
return resource.pre([() => getWorkspace() ?? ''], async () => {
1313
const workspace = getWorkspace()
1414
if (!workspace) return []
1515
try {
16-
return await WorkspaceService.listDataTables({ workspace })
16+
return (await WorkspaceService.listDataTables({ workspace })).map((d) => d.name)
1717
} catch (e) {
1818
console.error('Failed to load datatables:', e)
1919
return []

0 commit comments

Comments
 (0)