diff --git a/backend/windmill-worker/src/pg_executor.rs b/backend/windmill-worker/src/pg_executor.rs index ba71f5424c..bee6887515 100644 --- a/backend/windmill-worker/src/pg_executor.rs +++ b/backend/windmill-worker/src/pg_executor.rs @@ -322,7 +322,13 @@ fn convert_val(value: &Value, arg_t: &String) -> windmill_common::error::Result< Value::Number(n) if n.is_i64() && (arg_t == "smallint" || arg_t == "smallserial") => { Ok(PgType::I16(n.as_i64().unwrap() as i16)) } - Value::Number(n) if n.is_i64() && (arg_t == "int" || arg_t == "serial") => { + Value::Number(n) + if n.is_i64() + && (arg_t == "int" + || arg_t == "integer" + || arg_t == "int4" + || arg_t == "serial") => + { Ok(PgType::I32(n.as_i64().unwrap() as i32)) } Value::Number(n) if n.is_i64() && (arg_t == "numeric" || arg_t == "decimal") => Ok( @@ -429,7 +435,6 @@ pub fn pg_cell_to_json_value( Type::TS_VECTOR => get_basic(row, column, column_i, |a: StringCollector| { Ok(JSONValue::String(a.0)) })?, - // array types Type::BOOL_ARRAY => get_array(row, column, column_i, |a: bool| Ok(JSONValue::Bool(a)))?, Type::INT2_ARRAY => get_array(row, column, column_i, |a: i16| { diff --git a/cli/script.ts b/cli/script.ts index d12623ec8a..bf1dcadbda 100644 --- a/cli/script.ts +++ b/cli/script.ts @@ -14,6 +14,7 @@ import { writeAllSync, yamlParse, } from "./deps.ts"; +import { deepEqual } from "./utils.ts"; export interface ScriptFile { parent_hash?: string; @@ -66,7 +67,7 @@ export async function handleFile( path: string, workspace: string, alreadySynced: string[], - message?: string, + message?: string ): Promise { if ( !path.includes(".inline_script.") && @@ -125,8 +126,9 @@ export async function handleFile( typed.is_template === remote.is_template && typed.kind == remote.kind && !remote.archived && - (remote?.lock ?? "") == (typed.lock?.join("\n") ?? "") && - JSON.stringify(typed.schema) == JSON.stringify(remote.schema) && + (remote?.lock ?? "").trim() == + (typed.lock?.join("\n") ?? "").trim() && + deepEqual(typed.schema, remote.schema) && typed.tag == remote.tag && (typed.ws_error_handler_muted ?? false) == remote.ws_error_handler_muted && @@ -140,6 +142,7 @@ export async function handleFile( return true; } } + log.info( colors.yellow.bold(`Creating script with a parent ${remotePath}`) ); diff --git a/cli/sync.ts b/cli/sync.ts index a1fa8a139b..6e69635221 100644 --- a/cli/sync.ts +++ b/cli/sync.ts @@ -478,7 +478,7 @@ async function pull( !opts.json ); const local = opts.raw - ? undefined + ? await FSFSElement(Deno.cwd()) : await FSFSElement(path.join(Deno.cwd(), ".wmill")); const changes = await compareDynFSElement( remote, @@ -685,19 +685,18 @@ async function push( "Computing the files to update on the remote to match local (taking .wmillignore into account)" ) ); - const remote = opts.raw - ? undefined - : ZipFSElement( - (await downloadZip( - workspace, - opts.plainSecrets, - opts.skipVariables, - opts.skipResources, - opts.skipSecrets, - opts.includeSchedules - ))!, - !opts.json - ); + const remote = ZipFSElement( + (await downloadZip( + workspace, + opts.plainSecrets, + opts.skipVariables, + opts.skipResources, + opts.skipSecrets, + opts.includeSchedules + ))!, + !opts.json + ); + const local = await FSFSElement(path.join(Deno.cwd(), "")); const changes = await compareDynFSElement( local, @@ -850,7 +849,7 @@ async function push( case "flow": await FlowService.deleteFlowByPath({ workspace: workspaceId, - path: removeSuffix(change.path, ".flow.json"), + path: removeSuffix(change.path, ".flow/flow.json"), }); break; case "app": diff --git a/frontend/src/lib/components/AppConnect.svelte b/frontend/src/lib/components/AppConnect.svelte index 315b9334c2..b024358c46 100644 --- a/frontend/src/lib/components/AppConnect.svelte +++ b/frontend/src/lib/components/AppConnect.svelte @@ -460,7 +460,7 @@ {/if} -

+
{#if filteredConnectsManual} {#each filteredConnectsManual as [key, _]} diff --git a/frontend/src/lib/components/DBSchemaExplorer.svelte b/frontend/src/lib/components/DBSchemaExplorer.svelte index dc49ac8fdb..6c95786a5d 100644 --- a/frontend/src/lib/components/DBSchemaExplorer.svelte +++ b/frontend/src/lib/components/DBSchemaExplorer.svelte @@ -1,22 +1,21 @@ +{#if loading} + +{/if} + {#if dbSchema} +
+ {#if resolvedConfig.type.configuration?.postgresql?.resource && resolvedConfig.type.configuration?.postgresql?.table} + + + {#key renderCount} + + + {/key} + {/if} +
+ + + + + + + + + + + + diff --git a/frontend/src/lib/components/apps/components/display/dbtable/DbExplorerCount.svelte b/frontend/src/lib/components/apps/components/display/dbtable/DbExplorerCount.svelte new file mode 100644 index 0000000000..81d4347db2 --- /dev/null +++ b/frontend/src/lib/components/apps/components/display/dbtable/DbExplorerCount.svelte @@ -0,0 +1,71 @@ + + + diff --git a/frontend/src/lib/components/apps/components/display/dbtable/DeleteRow.svelte b/frontend/src/lib/components/apps/components/display/dbtable/DeleteRow.svelte new file mode 100644 index 0000000000..9edaf4e906 --- /dev/null +++ b/frontend/src/lib/components/apps/components/display/dbtable/DeleteRow.svelte @@ -0,0 +1,76 @@ + + + diff --git a/frontend/src/lib/components/apps/components/display/dbtable/InsertRow.svelte b/frontend/src/lib/components/apps/components/display/dbtable/InsertRow.svelte new file mode 100644 index 0000000000..2e9e7c9cfe --- /dev/null +++ b/frontend/src/lib/components/apps/components/display/dbtable/InsertRow.svelte @@ -0,0 +1,130 @@ + + + diff --git a/frontend/src/lib/components/apps/components/display/dbtable/InsertRowRunnable.svelte b/frontend/src/lib/components/apps/components/display/dbtable/InsertRowRunnable.svelte new file mode 100644 index 0000000000..e0fb9043d4 --- /dev/null +++ b/frontend/src/lib/components/apps/components/display/dbtable/InsertRowRunnable.svelte @@ -0,0 +1,70 @@ + + + diff --git a/frontend/src/lib/components/apps/components/display/dbtable/UpdateCell.svelte b/frontend/src/lib/components/apps/components/display/dbtable/UpdateCell.svelte new file mode 100644 index 0000000000..c13939a7e9 --- /dev/null +++ b/frontend/src/lib/components/apps/components/display/dbtable/UpdateCell.svelte @@ -0,0 +1,80 @@ + + + diff --git a/frontend/src/lib/components/apps/components/display/dbtable/utils.ts b/frontend/src/lib/components/apps/components/display/dbtable/utils.ts new file mode 100644 index 0000000000..88231322b5 --- /dev/null +++ b/frontend/src/lib/components/apps/components/display/dbtable/utils.ts @@ -0,0 +1,783 @@ +import type { AppInput, RunnableByName } from '../../../inputType' +import { JobService, Preview } from '$lib/gen' +import type { DBSchema, DBSchemas, GraphqlSchema, SQLSchema } from '$lib/stores' +import { buildClientSchema, getIntrospectionQuery, printSchema } from 'graphql' +import { tryEvery } from '$lib/utils' + +export function makeQuery( + table: string, + tableMetadata: TableMetadata, + whereClause: string | undefined +) { + if (!table) throw new Error('Table name is required') + + const filteredColumns = tableMetadata + .filter((x) => x != undefined) + .map((column) => `${column?.field}::text`) + + let selectClause = filteredColumns.join(', ') + + let orderBy = ` + ${tableMetadata + .map( + (column) => + ` +(CASE WHEN $4 = '${column.field}' AND $5 IS false THEN ${column.field}::text END), +(CASE WHEN $4 = '${column.field}' AND $5 IS true THEN ${column.field}::text END) DESC` + ) + .join(',\n')}` + + let query = ` +-- $1 limit +-- $2 offset +-- $3 quicksearch +-- $4 orderBy +-- $5 is_desc + +SELECT ${selectClause} FROM ${table} WHERE ` + if (whereClause) { + query += ` ${whereClause} AND ` + } + query += ` ($3 = '' OR ${table}::text ILIKE '%' || $3 || '%')` + + query += ` ORDER BY ${orderBy}` + query += ` LIMIT $1::INT OFFSET $2::INT` + + return query +} + +export function createPostgresInsert( + table: string, + columns: ColumnDef[], + resource: string +): AppInput { + return { + runnable: { + name: 'AppDbExplorer', + type: 'runnableByName', + inlineScript: { + content: makeInsertQuery(table, columns), + language: Preview.language.POSTGRESQL, + schema: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + properties: {}, + required: ['database'], + type: 'object' + } + } + }, + fields: { + database: { + type: 'static', + value: resource, + fieldType: 'object', + format: 'resource-postgresql' + } + }, + type: 'runnable', + fieldType: 'object' + } +} + +export function makeInsertQuery(table: string, columns: ColumnDef[]) { + if (!table) throw new Error('Table name is required') + + const columnsInsert = columns.filter((x) => !x.hideInsert) + const columnsDefault = columns.filter( + (x) => x.hideInsert && (x.overrideDefaultValue || x.defaultvalue === null) + ) + + const allInsertColumns = columnsInsert.concat(columnsDefault) + // Constructing the query + const query = ` +${columnsInsert.map((column, i) => `-- $${i + 1} ${column.field}`).join('\n')} + +INSERT INTO ${table} (${allInsertColumns.map((c) => c.field).join(', ')}) +VALUES (${columnsInsert.map((c, i) => `$${i + 1}::${c.datatype}`).join(', ')}${ + columnsDefault.length > 0 ? ',' : '' + } ${columnsDefault + .map((c) => (c.defaultValueNull ? 'NULL' : `${c.defaultUserValue}::${c.datatype}`)) + .join(', ')})` + + return query +} + +export function getPrimaryKeys(tableMetadata?: TableMetadata): string[] { + let r = tableMetadata?.filter((x) => x.isprimarykey)?.map((x) => x.field) ?? [] + if (r?.length === 0) { + r = tableMetadata?.map((x) => x.field) ?? [] + } + return r ?? [] +} + +export function createPostgresInput( + resource: string, + table: string | undefined, + columns: TableMetadata, + whereClause: string | undefined +): AppInput | undefined { + if (!resource || !table || !columns) { + // Return undefined if resource or table is not defined + return undefined + } + + const getRunnable: RunnableByName = { + name: 'AppDbExplorer', + type: 'runnableByName', + inlineScript: { + content: makeQuery(table, columns, whereClause), + language: Preview.language.POSTGRESQL + } + } + + const getQuery: AppInput = { + runnable: getRunnable, + fields: { + database: { + type: 'static', + value: resource, + fieldType: 'object', + format: 'resource-postgresql' + } + }, + type: 'runnable', + fieldType: 'object' + } + + return getQuery +} + +export function createUpdatePostgresInput( + resource: string, + table: string, + column: ColumnMetadata, + columns: ColumnMetadata[] +): AppInput | undefined { + if (!resource || !table) { + return undefined + } + + const query = updateWithAllValues() + + function updateWithAllValues() { + let query = ` +-- $1 valueToUpdate +${columns.map((c, i) => `-- $${i + 2} ${c.field}`).join('\n')} + +UPDATE ${table} SET ${column.field} = $1::text::${column.datatype} WHERE +${columns.map((c, i) => `${c.field} = $${i + 2}::text::${c.datatype} `).join(' AND ')} +RETURNING 1` + + return query + } + + const updateRunnable: RunnableByName = { + name: 'AppDbExplorer', + type: 'runnableByName', + inlineScript: { + content: query, + language: Preview.language.POSTGRESQL, + schema: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + properties: {}, + required: ['database'], + type: 'object' + } + } + } + + const updateQuery: AppInput = { + runnable: updateRunnable, + fields: { + database: { + type: 'static', + value: resource, + fieldType: 'object', + format: 'resource-postgresql' + } + }, + type: 'runnable', + fieldType: 'object' + } + + return updateQuery +} + +export function createDeletePostgresInput( + resource: string, + table: string, + columns: ColumnMetadata[] +): AppInput | undefined { + if (!resource || !table) { + return undefined + } + + const query = updateWithAllValues() + + function updateWithAllValues() { + let query = ` +${columns.map((c, i) => `-- $${i + 1} ${c.field}`).join('\n')} + +DELETE FROM ${table} WHERE ${columns + .map((c, i) => `${c.field} = $${i + 1}::text::${c.datatype}`) + .join(' AND ')} RETURNING 1;` + + return query + } + + const updateRunnable: RunnableByName = { + name: 'AppDbExplorer', + type: 'runnableByName', + inlineScript: { + content: query, + language: Preview.language.POSTGRESQL, + schema: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + properties: {}, + required: ['database'], + type: 'object' + } + } + } + + const updateQuery: AppInput = { + runnable: updateRunnable, + fields: { + database: { + type: 'static', + value: resource, + fieldType: 'object', + format: 'resource-postgresql' + } + }, + type: 'runnable', + fieldType: 'object' + } + + return updateQuery +} + +export function getCountPostgresql(resource: string, table: string): AppInput | undefined { + if (!resource || !table) { + return undefined + } + + const query = ` +-- $1 quicksearch +SELECT COUNT(*) FROM ${table} WHERE ($1 = '' OR ${table}::text ILIKE '%' || $1 || '%')` + + const updateRunnable: RunnableByName = { + name: 'AppDbExplorer', + type: 'runnableByName', + inlineScript: { + content: query, + language: Preview.language.POSTGRESQL, + schema: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + properties: { + database: { + description: 'Database name', + type: 'object', + format: 'resource-postgresql' + } + }, + required: ['database'], + type: 'object' + } + } + } + + const updateQuery: AppInput = { + runnable: updateRunnable, + fields: { + database: { + type: 'static', + value: resource, + fieldType: 'object', + format: 'resource-postgresql' + } + }, + type: 'runnable', + fieldType: 'object' + } + + return updateQuery +} + +export enum ColumnIdentity { + ByDefault = 'By Default', + Always = 'Always', + No = 'No' +} + +export type ColumnMetadata = { + field: string + datatype: string + defaultvalue: string + isprimarykey: boolean + isidentity: ColumnIdentity + isnullable: 'YES' | 'NO' + isenum: boolean +} +export type TableMetadata = ColumnMetadata[] + +export type ColumnDef = { + minWidth: number + hide: boolean + flex: number + sort: 'asc' | 'desc' + sortIndex: number + aggFunc: string + pivot: boolean + pivotIndex: number + pinned: 'left' | 'right' | boolean + rowGroup: boolean + rowGroupIndex: number + valueFormatter: string + valueParser: string + field: string + headerName: string + // DBExplorer + ignored: boolean + hideInsert: boolean + editable: boolean + overrideDefaultValue: boolean + defaultUserValue: any + defaultValueNull: boolean +} & ColumnMetadata + +export async function loadTableMetaData( + resource: string, + workspace: string | undefined, + table: string | undefined +): Promise { + if (!resource || !table || !workspace) { + return undefined + } + + const code = ` + SELECT + a.attname as field, + pg_catalog.format_type(a.atttypid, a.atttypmod) as DataType, + (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) for 128) + FROM pg_catalog.pg_attrdef d + WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) as DefaultValue, + (SELECT CASE WHEN i.indisprimary THEN true ELSE 'NO' END + FROM pg_catalog.pg_class tbl, pg_catalog.pg_class idx, pg_catalog.pg_index i, pg_catalog.pg_attribute att + WHERE tbl.oid = a.attrelid AND idx.oid = i.indexrelid AND att.attrelid = tbl.oid + AND i.indrelid = tbl.oid AND att.attnum = any(i.indkey) AND att.attname = a.attname LIMIT 1) as IsPrimaryKey, + CASE a.attidentity + WHEN 'd' THEN 'By Default' + WHEN 'a' THEN 'Always' + ELSE 'No' + END as IsIdentity, + CASE a.attnotnull + WHEN false THEN 'YES' + ELSE 'NO' + END as IsNullable, + (SELECT true + FROM pg_catalog.pg_enum e + WHERE e.enumtypid = a.atttypid FETCH FIRST ROW ONLY) as IsEnum +FROM pg_catalog.pg_attribute a +WHERE a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = '${table}') + AND a.attnum > 0 AND NOT a.attisdropped +ORDER BY a.attnum; + +` + + const maxRetries = 3 + let attempts = 0 + + while (attempts < maxRetries) { + try { + const job = await JobService.runScriptPreview({ + workspace: workspace, + requestBody: { + language: Preview.language.POSTGRESQL, + content: code, + args: { + database: resource + } + } + }) + + await new Promise((resolve) => setTimeout(resolve, 3000)) + + const testResult = await JobService.getCompletedJob({ + workspace: workspace, + id: job + }) + + if (testResult.success) { + attempts = maxRetries + + return testResult.result + } else { + attempts++ + } + } catch (error) { + attempts++ + } + // Exponential back-off + await new Promise((resolve) => setTimeout(resolve, 2000 * attempts)) + } + + console.error('Failed to load table metadata after maximum retries.') + return undefined +} + +export function resourceTypeToLang(rt: string) { + if (rt === 'ms_sql_server') { + return 'mssql' + } else { + return rt + } +} + +const scripts: Record< + string, + { + code: string + lang: string + processingFn?: (any: any) => SQLSchema['schema'] + argName: string + } +> = { + postgresql: { + code: `SELECT table_name, column_name, udt_name, column_default, is_nullable, table_schema FROM information_schema.columns WHERE table_schema != 'pg_catalog' AND table_schema != 'information_schema'`, + processingFn: (rows) => { + const schemas = rows.reduce((acc, a) => { + const table_schema = a.table_schema + delete a.table_schema + acc[table_schema] = acc[table_schema] || [] + acc[table_schema].push(a) + return acc + }, {}) + const data = {} + for (const key in schemas) { + data[key] = schemas[key].reduce((acc, a) => { + const table_name = a.table_name + delete a.table_name + acc[table_name] = acc[table_name] || {} + const p: { + type: string + required: boolean + default?: string + } = { + type: a.udt_name, + required: a.is_nullable === 'NO' + } + if (a.column_default) { + p.default = a.column_default + } + acc[table_name][a.column_name] = p + return acc + }, {}) + } + return data + }, + lang: 'postgresql', + argName: 'database' + }, + mysql: { + code: "select TABLE_SCHEMA, TABLE_NAME, DATA_TYPE, COLUMN_NAME, COLUMN_DEFAULT from information_schema.columns where table_schema != 'information_schema'", + processingFn: (rows) => { + const schemas = rows.reduce((acc, a) => { + const table_schema = a.TABLE_SCHEMA + delete a.TABLE_SCHEMA + acc[table_schema] = acc[table_schema] || [] + acc[table_schema].push(a) + return acc + }, {}) + const data = {} + for (const key in schemas) { + data[key] = schemas[key].reduce((acc, a) => { + const table_name = a.TABLE_NAME + delete a.TABLE_NAME + acc[table_name] = acc[table_name] || {} + const p: { + type: string + required: boolean + default?: string + } = { + type: a.DATA_TYPE, + required: a.is_nullable === 'NO' + } + if (a.column_default) { + p.default = a.COLUMN_DEFAULT + } + acc[table_name][a.COLUMN_NAME] = p + return acc + }, {}) + } + return data + }, + lang: 'mysql', + argName: 'database' + }, + graphql: { + code: getIntrospectionQuery(), + lang: 'graphql', + argName: 'api' + }, + bigquery: { + code: `import { BigQuery } from 'npm:@google-cloud/bigquery@7.2.0'; +export async function main(args: bigquery) { +const bq = new BigQuery({ + credentials: args +}) +const [datasets] = await bq.getDatasets(); +const schema = {} +for (const dataset of datasets) { + schema[dataset.id] = {} + const query = "SELECT table_name, ARRAY_AGG(STRUCT(if(is_nullable = 'YES', true, false) AS required, column_name AS name, data_type AS type, if(column_default = 'NULL', null, column_default) AS \`default\`) ORDER BY ordinal_position) AS schema \ +FROM \`{dataset.id}\`.INFORMATION_SCHEMA.COLUMNS \ +GROUP BY table_name".replace('{dataset.id}', dataset.id) + const [rows] = await bq.query(query) + for (const row of rows) { + schema[dataset.id][row.table_name] = {} + for (const col of row.schema) { + const colName = col.name + delete col.name + if (col.default === null) { + delete col.default + } + schema[dataset.id][row.table_name][colName] = col + } + } +} +return schema +}`, // nested template literals + lang: 'deno', + argName: 'args' + }, + snowflake: { + code: `select TABLE_SCHEMA, TABLE_NAME, DATA_TYPE, COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE from information_schema.columns where table_schema != 'INFORMATION_SCHEMA'`, + lang: 'snowflake', + processingFn: (rows) => { + const schema = {} + for (const row of rows) { + if (!(row.TABLE_SCHEMA in schema)) { + schema[row.TABLE_SCHEMA] = {} + } + if (!(row.TABLE_NAME in schema[row.TABLE_SCHEMA])) { + schema[row.TABLE_SCHEMA][row.TABLE_NAME] = {} + } + schema[row.TABLE_SCHEMA][row.TABLE_NAME][row.COLUMN_NAME] = { + type: row.DATA_TYPE, + required: row.IS_NULLABLE === 'YES' + } + if (row.COLUMN_DEFAULT !== null) { + schema[row.TABLE_SCHEMA][row.TABLE_NAME][row.COLUMN_NAME]['default'] = row.COLUMN_DEFAULT + } + } + return schema + }, + argName: 'database' + }, + ms_sql_server: { + argName: 'database', + code: `select TABLE_SCHEMA, TABLE_NAME, DATA_TYPE, COLUMN_NAME, COLUMN_DEFAULT from information_schema.columns where table_schema != 'sys'`, + lang: 'mssql', + processingFn: (rows) => { + const schemas = rows[0].reduce((acc, a) => { + const table_schema = a.TABLE_SCHEMA + delete a.TABLE_SCHEMA + acc[table_schema] = acc[table_schema] || [] + acc[table_schema].push(a) + return acc + }, {}) + const data = {} + for (const key in schemas) { + data[key] = schemas[key].reduce((acc, a) => { + const table_name = a.TABLE_NAME + delete a.TABLE_NAME + acc[table_name] = acc[table_name] || {} + const p: { + type: string + required: boolean + default?: string + } = { + type: a.DATA_TYPE, + required: a.is_nullable === 'NO' + } + if (a.column_default) { + p.default = a.COLUMN_DEFAULT + } + acc[table_name][a.COLUMN_NAME] = p + return acc + }, {}) + } + return data + } + } +} + +export { scripts } +export async function getDbSchemas( + resourceType: string, + resourcePath: string, + workspace: string | undefined, + dbSchemas: DBSchemas, + errorCallback: (message: string) => void +): Promise { + return new Promise(async (resolve, reject) => { + if (!resourceType || !resourcePath || !workspace) { + resolve() + return + } + + const job = await JobService.runScriptPreview({ + workspace: workspace, + requestBody: { + language: scripts[resourceType].lang as Preview.language, + content: scripts[resourceType].code, + args: { + [scripts[resourceType].argName]: '$res:' + resourcePath + } + } + }) + + tryEvery({ + tryCode: async () => { + if (resourcePath) { + const testResult = await JobService.getCompletedJob({ + workspace, + id: job + }) + if (!testResult.success) { + console.error(testResult.result?.['error']?.['message']) + } else { + if (resourceType !== undefined) { + if (resourceType !== 'graphql') { + const { processingFn } = scripts[resourceType] + const schema = + processingFn !== undefined ? processingFn(testResult.result) : testResult.result + dbSchemas[resourcePath] = { + lang: resourceTypeToLang(resourceType) as SQLSchema['lang'], + schema, + publicOnly: !!schema.public || !!schema.PUBLIC || !!schema.dbo + } + } else { + if (typeof testResult.result !== 'object' || !('__schema' in testResult.result)) { + console.error('Invalid GraphQL schema') + + errorCallback('Invalid GraphQL schema') + } else { + dbSchemas[resourcePath] = { + lang: 'graphql', + schema: testResult.result + } + } + } + } + } + resolve() + } + }, + timeoutCode: async () => { + console.error('Could not query schema within 5s') + errorCallback('Could not query schema within 5s') + try { + await JobService.cancelQueuedJob({ + workspace, + id: job, + requestBody: { + reason: 'Could not query schema within 5s' + } + }) + } catch (err) { + console.error(err) + } + reject() + }, + interval: 500, + timeout: 5000 + }) + }) +} + +export function formatSchema(dbSchema: DBSchema) { + if (dbSchema.lang !== 'graphql' && dbSchema.publicOnly) { + return dbSchema.schema.public || dbSchema.schema.PUBLIC || dbSchema.schema.dbo || dbSchema + } else if (dbSchema.lang === 'mysql' && Object.keys(dbSchema.schema).length === 1) { + return dbSchema.schema[Object.keys(dbSchema.schema)[0]] + } else { + return dbSchema.schema + } +} + +export function formatGraphqlSchema(dbSchema: GraphqlSchema): string { + return printSchema(buildClientSchema(dbSchema.schema)) +} + +/** + * Base class for embedding a svelte component within an AGGrid call. + * See: https://stackoverflow.com/a/72608215 + */ +import type { ICellRendererComp, ICellRendererParams } from 'ag-grid-community' + +/** + * Class for defining a cell renderer. + * If you don't need to define a separate class you could use cellRendererFactory + * to create a component with the column definitions. + */ +export abstract class AbstractCellRenderer implements ICellRendererComp { + eGui: any + protected value: any + protected params: any + constructor(parentElement = 'span') { + // create empty span (or other element) to place svelte component in + this.eGui = document.createElement(parentElement) + } + + init(params: ICellRendererParams & { onClick?: (data: any) => void }) { + this.value = params.value + this.createComponent(params) + this.eGui.addEventListener('click', () => params.onClick?.(params.data)) + this.params = params + } + + getGui() { + return this.eGui + } + + refresh(params: ICellRendererParams) { + this.value = params.value + this.eGui.innerHTML = '' + + return true + } + + /** + * Define and create the svelte component to use in the cell + * @example + * // This is all you need to do within this method: create the component with new, specify the target + * // is the class, and pass in props via the params. + * new CampusIcon({ + * target: this.eGui, + * props: { + * color: params.data?.color, + * name: params.data?.name + * } + * @param params params for rendering the call, including the value for the cell + */ + abstract createComponent(params: ICellRendererParams): void +} + +/** + * Creates a cell renderer using the given callback for how to initialise a svelte component. + * See AbstractCellRenderer.createComponent + * @param svelteComponent function for how to create the svelte component + * @returns + */ +export function cellRendererFactory( + svelteComponent: (cell: AbstractCellRenderer, params: ICellRendererParams) => void +) { + class Renderer extends AbstractCellRenderer { + createComponent(params: ICellRendererParams): void { + svelteComponent(this, params) + } + } + return Renderer +} diff --git a/frontend/src/lib/components/apps/components/display/table/AppAggridExplorerTable.svelte b/frontend/src/lib/components/apps/components/display/table/AppAggridExplorerTable.svelte new file mode 100644 index 0000000000..9c026b0bb6 --- /dev/null +++ b/frontend/src/lib/components/apps/components/display/table/AppAggridExplorerTable.svelte @@ -0,0 +1,316 @@ + + +{#each Object.keys(css ?? {}) as key (key)} + +{/each} + +{#if Array.isArray(resolvedConfig.columnDefs) && resolvedConfig.columnDefs.every(isObject)} +
+
{ + $selectedComponent = [id] + }} + style:height="{clientHeight}px" + style:width="{clientWidth}px" + class="ag-theme-alpine" + class:ag-theme-alpine-dark={$darkMode} + > +
+
+
+
{firstRow}{'->'}{lastRow + 1} of {datasource?.rowCount} rows
+{:else if resolvedConfig.columnDefs != undefined} + + The columnDefs should be an array of objects, received: +
+				{JSON.stringify(resolvedConfig.columnDefs)}
+			
+
+{:else} + The columnDefs are undefined +{/if} diff --git a/frontend/src/lib/components/apps/components/display/table/AppCell.svelte b/frontend/src/lib/components/apps/components/display/table/AppCell.svelte index 85f83f2fb2..7ee8c24ad4 100644 --- a/frontend/src/lib/components/apps/components/display/table/AppCell.svelte +++ b/frontend/src/lib/components/apps/components/display/table/AppCell.svelte @@ -1,31 +1,94 @@ -{#if type === 'badge'} - - {value} - -{:else if type === 'link'} - {#if isLinkObject(value)} - - {value.label} - + + {#if type === 'badge'} + + {value} + + {:else if type === 'link'} + {#if isLinkObject(value)} + + {value.label} + + {:else} + {value} + {/if} + {:else if $isEditable} + { + if (e.key === 'Enter') { + saveEdit() + } + }} + /> {:else} - {value} + +
+ {value} +
{/if} -{:else} - {value} -{/if} + diff --git a/frontend/src/lib/components/apps/components/display/table/AppTable.svelte b/frontend/src/lib/components/apps/components/display/table/AppTable.svelte index e9fd19c4be..bcd9c4c668 100644 --- a/frontend/src/lib/components/apps/components/display/table/AppTable.svelte +++ b/frontend/src/lib/components/apps/components/display/table/AppTable.svelte @@ -70,7 +70,7 @@ selectedRowIndex: 0, selectedRow: undefined, loading: false, - result: [], + result: [] as Record[], inputs: {}, search: '', page: 1 @@ -289,6 +289,16 @@ } $: $table && updateTable(resolvedConfig, searchValue) + + function updateCellValue(rowIndex: number, columnIndex: number, newCellValue: string) { + if (result && rowIndex < result.length) { + const updatedRow = { ...result[rowIndex] } + const columnName = Object.keys(updatedRow)[columnIndex] + updatedRow[columnName] = newCellValue + result[rowIndex] = updatedRow + outputs?.result.set([result]) + } + } {#each Object.keys(components['tablecomponent'].initialData.configuration) as key (key)} @@ -396,21 +406,20 @@ {#if cell?.column?.columnDef?.cell} {@const context = cell?.getContext()} {#if context} - toggleRow(row)} on:click={() => toggleRow(row)} - class="p-4 whitespace-pre-wrap truncate text-xs text-primary" - style={'width: ' + cell.column.getSize() + 'px'} - > - c.field === cell.column.columnDef.accessorKey - )?.type ?? 'text'} - value={cell.getValue()} - /> - + type={resolvedConfig.columnDefs?.find( + // TS types are wrong here + // @ts-ignore + (c) => c.field === cell.column.columnDef.accessorKey + )?.type ?? 'text'} + value={cell.getValue()} + width={cell.column.getSize()} + on:update={(event) => { + updateCellValue(rowIndex, index, event.detail.value) + }} + /> {/if} {/if} {/each} @@ -546,6 +555,7 @@ }} {#if actionButton.type == 'buttoncomponent'} {:else if actionButton.type == 'checkboxcomponent'} {:else if actionButton.type == 'checkboxcomponent'} ('AppViewerContext') @@ -27,7 +28,9 @@ $: result != undefined && outputs && setOutput(result) - +{#if !noInitialize} + +{/if} {#if componentInput.type !== 'runnable'} diff --git a/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte b/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte index 1347161dba..ca5b62e986 100644 --- a/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte +++ b/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte @@ -51,6 +51,7 @@ export let errorHandledByComponent: boolean = false export let hideRefreshButton: boolean = false export let hasChildrens: boolean + export let allowConcurentRequests = false const { worldStore, @@ -97,6 +98,7 @@ function setDebouncedExecute() { executeTimeout && clearTimeout(executeTimeout) executeTimeout = setTimeout(() => { + console.debug('debounce execute') executeComponent(true) }, 200) } @@ -132,7 +134,7 @@ const refreshEnabled = autoRefresh && ((recomputeOnInputChanged ?? true) || refreshOn?.length > 0) if (refreshEnabled && $initialized.initialized) { - // console.debug(`Refreshing ${id} because ${_src} (enabled)`) + console.debug(`Refreshing ${id} because ${_src} (enabled)`) setDebouncedExecute() } } @@ -216,11 +218,15 @@ return njobs }) } + async function executeComponent( noToast = false, inlineScriptOverride?: InlineScript, - setRunnableJobEditorPanel?: boolean - ) { + setRunnableJobEditorPanel?: boolean, + dynamicArgsOverride?: Record, + callbacks?: Callbacks + ): Promise { + let jobId: string | undefined console.debug(`Executing ${id}`) if (iterContext && $iterContext.disabled) { console.debug(`Skipping execution of ${id} because it is part of a disabled list`) @@ -288,8 +294,8 @@ } try { - const jobId = await resultJobLoader?.abstractRun(async () => { - const nonStaticRunnableInputs = {} + jobId = await resultJobLoader?.abstractRun(async () => { + const nonStaticRunnableInputs = dynamicArgsOverride ?? {} const staticRunnableInputs = {} for (const k of Object.keys(fields ?? {})) { let field = fields[k] @@ -338,31 +344,51 @@ addJob(uuid) } return uuid - }) + }, callbacks) if (setRunnableJobEditorPanel && editorContext) { editorContext.runnableJobEditorPanel.update((p) => { return { ...p, - jobs: { ...p.jobs, [id]: jobId } + jobs: { ...p.jobs, [id]: jobId as string } } }) } + return jobId } catch (e) { - updateResult({ error: e.body ?? e.message }) + let error = e.body ?? e.message + updateResult({ error }) + $errorByComponent[id] = { error } + loading = false } } + type Callbacks = { done: (x: any[]) => void; cancel: () => void; error: () => void } - export async function runComponent() { + export async function runComponent( + noToast = false, + inlineScriptOverride?: InlineScript, + setRunnableJobEditorPanel?: boolean, + dynamicArgsOverride?: Record, + callbacks?: Callbacks + ): Promise { try { - if (cancellableRun) { + if (cancellableRun && !dynamicArgsOverride) { + console.log('runComponent cancellable Run') await cancellableRun() } else { console.log('Run component') - executeComponent() + return await executeComponent( + noToast, + inlineScriptOverride, + setRunnableJobEditorPanel, + dynamicArgsOverride, + callbacks + ) } } catch (e) { - updateResult({ error: e.body ?? e.message }) + let error = e.body ?? e.message + updateResult({ error }) + $errorByComponent[id] = { error } } } @@ -453,7 +479,7 @@ jobId: string | undefined, setRunnableJobEditorPanel?: boolean ) { - dispatch('done') + dispatch('resultSet') const errors = getResultErrors(res) if (errors) { @@ -504,6 +530,7 @@ onMount(() => { cancellableRun = (inlineScript?: InlineScript, setRunnableJobEditorPanel?: boolean) => { + console.log('cancellableRun', inlineScript) let rejectCb: (err: Error) => void let p: Partial> = new Promise((resolve, reject) => { rejectCb = reject @@ -580,6 +607,7 @@ {/if} { console.log('started', e.detail) @@ -592,9 +620,11 @@ lastJobId = e.detail.id setResult(e.detail.result, e.detail.id) loading = false + dispatch('done', { id: e.detail.id, result: e.detail.result }) }} on:cancel={(e) => { let jobId = e.detail + console.debug('cancel', jobId) let job = $jobsById[jobId] if (job && job.created_at && !job.duration_ms) { $jobsById[jobId] = { @@ -603,6 +633,7 @@ duration_ms: Date.now() - (job.started_at ?? job.created_at) } } + dispatch('cancel', { id: e.detail }) }} on:running={(e) => { let jobId = e.detail @@ -614,6 +645,7 @@ on:doneError={(e) => { setResult({ error: e.detail.error }, e.detail.id) loading = false + dispatch('doneError', { id: e.detail.id, result: e.detail.result }) }} bind:this={resultJobLoader} /> @@ -652,19 +684,21 @@
-
+
An error occured, please contact the app author. - {#if lastJobId && $errorByComponent[id].error} + {#if $errorByComponent?.[id]?.error}
{$errorByComponent[id].error}
{/if} - - Job id: {lastJobId} - + {#if lastJobId} + + Job id: {lastJobId} + + {/if}
diff --git a/frontend/src/lib/components/apps/components/helpers/RunnableWrapper.svelte b/frontend/src/lib/components/apps/components/helpers/RunnableWrapper.svelte index a7d6ec940b..018d6848ff 100644 --- a/frontend/src/lib/components/apps/components/helpers/RunnableWrapper.svelte +++ b/frontend/src/lib/components/apps/components/helpers/RunnableWrapper.svelte @@ -10,6 +10,7 @@ import InitializeComponent from './InitializeComponent.svelte' export let componentInput: AppInput | undefined + export let noInitialize = false type SideEffectAction = | { @@ -76,6 +77,7 @@ export let refreshOnStart: boolean = false export let errorHandledByComponent: boolean = false export let hasChildrens: boolean = false + export let allowConcurentRequests = false export function setArgs(value: any) { runnableComponent?.setArgs(value) @@ -208,10 +210,13 @@ {#if componentInput === undefined} - + {#if !noInitialize} + + {/if} {:else if componentInput.type === 'runnable' && isRunnableDefined(componentInput)} (initializing = false)} + on:done + on:doneError + on:cancel + on:setResult={() => (initializing = false)} on:success={() => handleSideEffect(true)} on:handleError={(e) => handleSideEffect(false, e.detail)} {outputs} @@ -242,7 +250,7 @@ {:else} - + {/if} diff --git a/frontend/src/lib/components/apps/components/inputs/AppCheckbox.svelte b/frontend/src/lib/components/apps/components/inputs/AppCheckbox.svelte index 2124c92fe8..54c2ba97d8 100644 --- a/frontend/src/lib/components/apps/components/inputs/AppCheckbox.svelte +++ b/frontend/src/lib/components/apps/components/inputs/AppCheckbox.svelte @@ -26,6 +26,7 @@ export let render: boolean export let extraKey: string | undefined = undefined export let preclickAction: (() => Promise) | undefined = undefined + export let noInitialize = false export let controls: { left: () => boolean; right: () => boolean | string } | undefined = undefined @@ -111,7 +112,10 @@ /> {/each} - +{#if !noInitialize} + +{/if} + Promise) | undefined = undefined export let recomputeIds: string[] | undefined = undefined + export let noInitialize = false export let controls: { left: () => boolean; right: () => boolean | string } | undefined = undefined @@ -189,7 +190,9 @@ /> {/each} - +{#if !noInitialize} + +{/if}
({ input: x.componentInput, id: x.id }))) } + if (c.type === 'dbexplorercomponent') { + let nr: { id: string; input: AppInput }[] = [] + let config = c.configuration as any + let pg = config?.type?.configuration?.postgresql + if (pg) { + const { table, resource } = pg + const tableValue = table.value + const resourceValue = resource.value + const columnDefs = (c.configuration.columnDefs as any).value as ColumnDef[] + const whereClause = (c.configuration.whereClause as any).value as unknown as + | string + | undefined + console.log(columnDefs) + if (tableValue && resourceValue && columnDefs) { + r.push({ + input: createPostgresInput(resourceValue, tableValue, columnDefs, whereClause), + id: x.id + }) + r.push({ + input: getCountPostgresql(resourceValue, tableValue), + id: x.id + '_count' + }) + r.push({ + input: createPostgresInsert(tableValue, columnDefs, resourceValue), + id: x.id + '_insert' + }) + let primaryColumns = getPrimaryKeys(columnDefs) + let columns = columnDefs?.filter((x) => primaryColumns.includes(x.field)) + + columnDefs + .filter((col) => col.editable || config.allEditable.value) + .forEach((column) => { + r.push({ + input: createUpdatePostgresInput(resourceValue, tableValue, column, columns), + id: x.id + '_update' + }) + }) + } + } + r.push(...nr) + } return r .filter((x) => x.input) .map(async (o) => { @@ -213,13 +263,16 @@ ): Promise<[string, Record] | undefined> { const staticInputs = collectStaticFields(fields) if (runnable?.type == 'runnableByName') { + console.log(runnable.inlineScript?.content) let hex = await hash(runnable.inlineScript?.content) + console.log('hex', hex, id) return [`${id}:rawscript/${hex}`, staticInputs] } else if (runnable?.type == 'runnableByPath') { let prefix = runnable.runType !== 'hubscript' ? runnable.runType : 'script' return [`${id}:${prefix}/${runnable.path}`, staticInputs] } } + async function createApp(path: string) { await computeTriggerables() try { @@ -731,7 +784,6 @@
- - Timeline - Details - - {#if rightColumnSelect == 'timeline'} -
- -
- {:else if rightColumnSelect == 'detail'} -
- {#if selectedJobId} - {#if selectedJobId?.includes('Frontend')} - {@const jobResult = $jobsById[selectedJobId]} - {#if jobResult?.error !== undefined} - - - - - -
- -
-
-
- {:else if jobResult !== undefined} - - - - - -
- -
-
-
- {:else} - - {/if} - {:else} -
- {#if job?.['running']} -
- -
- {/if} - {#if job?.args} -
- -
- {/if} - - {#if job?.job_kind !== 'flow' && job?.job_kind !== 'flowpreview'} - {@const jobResult = $jobsById[selectedJobId]} +
+ + Timeline + Details + + {#if rightColumnSelect == 'timeline'} +
+ +
+ {:else if rightColumnSelect == 'detail'} +
+ {#if selectedJobId} + {#if selectedJobId?.includes('Frontend')} + {@const jobResult = $jobsById[selectedJobId]} + {#if jobResult?.error !== undefined} - + - - {#if job != undefined && 'result' in job && job.result != undefined} -
-
- {:else if testIsLoading} -
- {:else if job != undefined && 'result' in job && job?.['result'] == undefined} -
Result is undefined
- {:else} -
- -
- {/if} + +
+ +
- {#if jobResult?.transformer} - -
Transformer results
+
+ {:else if jobResult !== undefined} + + + + + +
+ +
+
+
+ {:else} + + {/if} + {:else} +
+ {#if job?.['running']} +
+ +
+ {/if} + {#if job?.args} +
+ +
+ {/if} + {#if job?.raw_code} +
+ +
+ {/if} + + {#if job?.job_kind !== 'flow' && job?.job_kind !== 'flowpreview'} + {@const jobResult = $jobsById[selectedJobId]} + + + + + {#if job != undefined && 'result' in job && job.result != undefined}
-
+ result={job.result} + />
{:else if testIsLoading}
{:else if job != undefined && 'result' in job && job?.['result'] == undefined} @@ -1055,27 +1092,49 @@
{/if} - {/if} - - {:else} -
- -
- { - job = detail - }} - /> -
- {/if} -
+ {#if jobResult?.transformer} + +
Transformer results
+ {#if job != undefined && 'result' in job && job.result != undefined} +
+ +
+ {:else if testIsLoading} +
+ {:else if job != undefined && 'result' in job && job?.['result'] == undefined} +
Result is undefined
+ {:else} +
+ +
+ {/if} +
+ {/if} + + {:else} +
+ +
+ { + job = detail + }} + /> +
+ {/if} +
+ {/if} + {:else} +
Select a job to see its details
{/if} - {:else} -
Select a job to see its details
- {/if} -
- {/if} +
+ {/if} +
@@ -1247,13 +1306,13 @@ size="xs" dropdownItems={appPath != '' ? () => [ - { - label: 'Fork', - onClick: () => { - window.open(`/apps/add?template=${appPath}`) + { + label: 'Fork', + onClick: () => { + window.open(`/apps/add?template=${appPath}`) + } } - } - ] + ] : undefined} > Deploy diff --git a/frontend/src/lib/components/apps/editor/RecomputeAllComponents.svelte b/frontend/src/lib/components/apps/editor/RecomputeAllComponents.svelte index 077f18135d..3b26396542 100644 --- a/frontend/src/lib/components/apps/editor/RecomputeAllComponents.svelte +++ b/frontend/src/lib/components/apps/editor/RecomputeAllComponents.svelte @@ -63,6 +63,7 @@ } loading = true + console.log('refresh all') const promises = Object.keys($runnableComponents) .flatMap((id) => { if ( @@ -72,6 +73,7 @@ return } + console.log('refresh start', id) return $runnableComponents?.[id]?.cb?.map((f) => f().then(() => console.log('refreshed', id)) ) @@ -106,7 +108,8 @@ ] - +
diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte index 2ec4d4db47..e01804f15f 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte @@ -373,7 +373,6 @@ {ccomponents[component.type].name} has no configuration
{/if} - {#if (`recomputeIds` in componentSettings.item.data && Array.isArray(componentSettings.item.data.recomputeIds)) || componentSettings.item.data.type === 'buttoncomponent' || componentSettings.item.data.type === 'formcomponent' || componentSettings.item.data.type === 'formbuttoncomponent' || componentSettings.item.data.type === 'checkboxcomponent'} + import PanelSection from './common/PanelSection.svelte' + import { Button } from '$lib/components/common' + import { RefreshCcw } from 'lucide-svelte' + + + +
+ +
+
diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/GridTab.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/GridTab.svelte index 9391736375..0571792c13 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/GridTab.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/GridTab.svelte @@ -182,6 +182,7 @@ +
('AppViewerContext') @@ -73,6 +74,9 @@ ? capitalize(addWhitespaceBeforeCapitals(key)) : key} + {#if loading} + + {/if} {#if tooltip} {tooltip} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte index e0a7c10f6e..7154f8d338 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecsEditor.svelte @@ -51,6 +51,7 @@ fileUpload={meta?.['fileUpload']} placeholder={meta?.['placeholder']} customTitle={meta?.['customTitle']} + loading={meta?.['loading']} {displayType} /> {#if deletable} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/OneOfInputSpecsEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/OneOfInputSpecsEditor.svelte index 0fbdf8596d..10821eb904 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/OneOfInputSpecsEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/OneOfInputSpecsEditor.svelte @@ -73,7 +73,11 @@ {/if}
{#each Object.keys(inputSpecsConfiguration?.[oneOf.selected] ?? {}) as nestedKey} - {@const config = inputSpecsConfiguration?.[oneOf.selected]?.[nestedKey]} + {@const config = { + ...inputSpecsConfiguration?.[oneOf.selected]?.[nestedKey], + ...oneOf.configuration?.[oneOf.selected]?.[nestedKey] + }} + {#if config && oneOf.configuration[oneOf.selected]} {/if} {/each} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/SynchronizeColumns.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/SynchronizeColumns.svelte new file mode 100644 index 0000000000..bf72b5e300 --- /dev/null +++ b/frontend/src/lib/components/apps/editor/settingsPanel/SynchronizeColumns.svelte @@ -0,0 +1,86 @@ + + +{#if shouldDisplaySyncButton} +
+ +
+{/if} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte index 8b2cd719ad..8fd6f70d34 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte @@ -17,13 +17,13 @@ import TableColumnWizard from '$lib/components/wizards/TableColumnWizard.svelte' import PlotlyWizard from '$lib/components/wizards/PlotlyWizard.svelte' import ChartJSWizard from '$lib/components/wizards/ChartJSWizard.svelte' + import DBExplorerWizard from '$lib/components/wizards/DBExplorerWizard.svelte' export let componentInput: StaticInput | undefined export let fieldType: InputType | undefined = undefined export let subFieldType: InputType | undefined = undefined export let selectOptions: StaticOptions['selectOptions'] | undefined = undefined export let placeholder: string | undefined = undefined - export let format: string | undefined = undefined const { onchange } = getContext('AppViewerContext') @@ -41,7 +41,7 @@ {:else if fieldType === 'boolean'} {:else if fieldType === 'select' && selectOptions} - {#each selectOptions ?? [] as option} {#if typeof option == 'string'}
+ {:else if fieldType === 'db-explorer' && componentInput.value != undefined} +
+
+ +
+ + + + + +
+
+
{:else if fieldType === 'table-column'}
diff --git a/frontend/src/lib/components/apps/inputType.ts b/frontend/src/lib/components/apps/inputType.ts index c15658cd01..7e5b4491ef 100644 --- a/frontend/src/lib/components/apps/inputType.ts +++ b/frontend/src/lib/components/apps/inputType.ts @@ -27,6 +27,9 @@ export type InputType = | 'plotly' | 'chartjs' | 'DecisionTreeNode' + | 'resource' + | 'db-explorer' + | 'db-table' // Connection to an output of another component // defined by the id of the component and the path of the output @@ -140,6 +143,7 @@ type InputConfiguration = { fieldType: T subFieldType?: V format?: string | undefined + loading?: boolean fileUpload?: { /** Use `*` to accept anything. */ accept: string @@ -173,7 +177,7 @@ export type AppInput = | AppInputSpec<'any', any> | AppInputSpec<'object', Record> | AppInputSpec<'object', string> - | (AppInputSpec<'select', string> & StaticOptions) + | (AppInputSpec<'select', string, 'db-table'> & StaticOptions) | AppInputSpec<'icon-select', string> | AppInputSpec<'color', string> | AppInputSpec<'array', string[], 'text'> @@ -192,10 +196,12 @@ export type AppInput = | AppInputSpec<'array', object[], 'tab-select'> | AppInputSpec<'schema', object> | AppInputSpec<'array', object[], 'ag-grid'> + | AppInputSpec<'array', object[], 'db-explorer'> | AppInputSpec<'array', object[], 'table-column'> | AppInputSpec<'array', object[], 'plotly'> | AppInputSpec<'array', object[], 'chartjs'> | AppInputSpec<'array', DecisionTreeNode, 'DecisionTreeNode'> + | AppInputSpec<'resource', string> export type RowAppInput = Extract export type StaticAppInput = Extract diff --git a/frontend/src/lib/components/wizards/AgGridWizard.svelte b/frontend/src/lib/components/wizards/AgGridWizard.svelte index 4709cac804..6c0063e52d 100644 --- a/frontend/src/lib/components/wizards/AgGridWizard.svelte +++ b/frontend/src/lib/components/wizards/AgGridWizard.svelte @@ -23,6 +23,7 @@ valueParser: string field: string headerName: string + editable: boolean } export let value: Column | undefined @@ -103,6 +104,17 @@ + + @@ -185,7 +197,13 @@
- + +
Use `value` in the formatter
{/key}
diff --git a/frontend/src/lib/components/wizards/DBExplorerWizard.svelte b/frontend/src/lib/components/wizards/DBExplorerWizard.svelte new file mode 100644 index 0000000000..3b4e962847 --- /dev/null +++ b/frontend/src/lib/components/wizards/DBExplorerWizard.svelte @@ -0,0 +1,381 @@ + + + + + + + {#if value} +
+
+ + + {value.field} + + + + + + + {#if warning} + + {warning.message} + + {/if} + + {#if value?.defaultvalue !== null && value?.hideInsert} + { + if (!value || !value.overrideDefaultValue) { + if (value) { + value.defaultValueNull = false + value.defaultUserValue = undefined + } + } + }} + /> + {/if} + +
+ +
+
{ + if (value?.ignored) { + e?.stopPropagation() + } + }} + > + + + + + + + + + + + +
+ {#key renderCount} +
+
+ {#if !presets.find((preset) => preset.value === value?.valueFormatter)} +
+ {/if} +
Presets
+ +
+ + +
Use `value` in the formatter
+
+ {/key} +
+ + +
+
+
+ {/if} +
diff --git a/frontend/src/lib/stores.ts b/frontend/src/lib/stores.ts index f47f3ea5df..aefcbe1a9f 100644 --- a/frontend/src/lib/stores.ts +++ b/frontend/src/lib/stores.ts @@ -102,6 +102,6 @@ export interface GraphqlSchema { export type DBSchema = SQLSchema | GraphqlSchema -type DBSchemas = Partial> +export type DBSchemas = Partial> export const dbSchemas = writable({}) diff --git a/frontend/src/routes/(root)/(logged)/resources/+page.svelte b/frontend/src/routes/(root)/(logged)/resources/+page.svelte index 0569a049fe..02b7a4fce3 100644 --- a/frontend/src/routes/(root)/(logged)/resources/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/resources/+page.svelte @@ -130,7 +130,6 @@ : true }) : preFilteredItemsOwners?.filter((x) => { - console.log(x.resource_type) return ( x.resource_type === typeFilter && (tab === 'workspace'