mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 00:03:07 +00:00
* feat(raw-apps): add public URL and custom path support for raw apps - Enable public URL UI in raw app editor by removing hideSecretUrl prop - Add bundle_secret field to AppWithLastVersion for raw app rendering - Compute bundle_secret in get_public_app_by_secret endpoint - Update PublicApp.svelte to render RawAppPreview for raw apps - Make get_data endpoint accessible without auth for anonymous raw apps - Use /apps_u/ endpoint for bundle loading to support anonymous access This allows raw apps to use the same public URL and custom path features as regular apps, with proper support for anonymous (no login required) execution mode. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: compute bundle_secret only once in get_public_app_by_secret Move bundle_secret computation after all authorization checks to avoid duplication between anonymous and authenticated code paths. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: add explicit error state for raw apps missing workspace Show a clear error message instead of silently falling through to render AppPreview when a raw app is loaded without workspace info. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * update sqlx --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
170 lines
4.6 KiB
TypeScript
170 lines
4.6 KiB
TypeScript
import type { ScriptLang } from '../../gen/types.gen'
|
|
import type { Schema } from '../../common'
|
|
import { schemaToTsType } from '../../schema'
|
|
import { isRunnableByName, isRunnableByPath, type RunnableWithFields } from '../apps/inputType'
|
|
import type { InlineScript } from '../apps/sharedTypes'
|
|
|
|
// export type RunnableWithFields = any
|
|
|
|
type RunnableWithInlineScript = RunnableWithFields & {
|
|
inlineScript?: InlineScript & { language: ScriptLang }
|
|
}
|
|
export type Runnable = RunnableWithInlineScript | undefined
|
|
|
|
export type RawApp = {
|
|
files: string[]
|
|
}
|
|
|
|
export function htmlContent(
|
|
workspace: string,
|
|
secret: string | undefined,
|
|
ctx: any,
|
|
baseUrl: string = '',
|
|
initialHash: string = ''
|
|
) {
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>App Preview</title>
|
|
<link rel="stylesheet" href="${baseUrl}/api/w/${workspace}/apps_u/get_data/v/${secret}.css" />
|
|
<script>
|
|
window.ctx = ${ctx ? JSON.stringify(ctx) : 'undefined'};
|
|
|
|
// Sync hash with parent window for shareable URLs
|
|
(function() {
|
|
// Set initial hash from parent URL
|
|
var initialHash = ${JSON.stringify(initialHash)};
|
|
if (initialHash && initialHash !== '#' && !window.location.hash) {
|
|
history.replaceState(null, '', initialHash);
|
|
}
|
|
|
|
// Notify parent when hash changes
|
|
function notifyParent() {
|
|
var hash = window.location.hash;
|
|
console.log('[HashSync] notifyParent called, hash:', hash);
|
|
if (window.parent !== window) {
|
|
window.parent.postMessage({
|
|
type: 'windmill:hashchange',
|
|
hash: hash
|
|
}, '*');
|
|
}
|
|
}
|
|
|
|
// Listen for hash changes
|
|
window.addEventListener('hashchange', function() {
|
|
console.log('[HashSync] hashchange event');
|
|
notifyParent();
|
|
});
|
|
|
|
// Also notify on pushState/replaceState
|
|
var originalPushState = history.pushState;
|
|
var originalReplaceState = history.replaceState;
|
|
|
|
history.pushState = function() {
|
|
console.log('[HashSync] pushState called with:', arguments[2]);
|
|
originalPushState.apply(this, arguments);
|
|
notifyParent();
|
|
};
|
|
|
|
history.replaceState = function() {
|
|
console.log('[HashSync] replaceState called with:', arguments[2]);
|
|
originalReplaceState.apply(this, arguments);
|
|
notifyParent();
|
|
};
|
|
|
|
// Notify parent of initial hash after load
|
|
setTimeout(notifyParent, 0);
|
|
})();
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div id="root"></div>
|
|
<script src="${baseUrl}/api/w/${workspace}/apps_u/get_data/v/${secret}.js"></script>
|
|
</body>
|
|
</html>`
|
|
}
|
|
|
|
function removeStaticFields(schema: Schema, fields: Record<string, { type: string }>): Schema {
|
|
const staticFields = Object.keys(fields).filter((k) => fields[k].type == 'static')
|
|
return {
|
|
...schema,
|
|
properties: {
|
|
...Object.fromEntries(
|
|
Object.entries(schema.properties ?? {}).filter(([k]) => !staticFields.includes(k))
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
function hiddenRunnableToTsType(runnable: Runnable) {
|
|
if (isRunnableByName(runnable)) {
|
|
if (runnable?.inlineScript?.schema) {
|
|
return schemaToTsType(
|
|
removeStaticFields(runnable?.inlineScript?.schema, runnable?.fields ?? {})
|
|
)
|
|
} else {
|
|
return '{}'
|
|
}
|
|
} else if (isRunnableByPath(runnable)) {
|
|
return schemaToTsType(removeStaticFields(runnable?.schema, runnable?.fields ?? {}))
|
|
} else {
|
|
return '{}'
|
|
}
|
|
}
|
|
|
|
export function genWmillTs(runnables: Record<string, Runnable>) {
|
|
return `// THIS FILE IS READ-ONLY
|
|
// AND GENERATED AUTOMATICALLY FROM YOUR RUNNABLES
|
|
|
|
export declare const backend: {
|
|
${Object.entries(runnables)
|
|
.map(([k, v]) => ` ${k}: (args: ${hiddenRunnableToTsType(v)}) => Promise<any>;`)
|
|
.join('\n')}
|
|
};
|
|
|
|
export declare const backendAsync: {
|
|
${Object.entries(runnables)
|
|
.map(([k, v]) => ` ${k}: (args: ${hiddenRunnableToTsType(v)}) => Promise<string>;`)
|
|
.join('\n')}
|
|
};
|
|
|
|
export type Job = {
|
|
type: "QueuedJob" | "CompletedJob";
|
|
id: string;
|
|
created_at: number;
|
|
started_at: number | undefined;
|
|
duration_ms: number;
|
|
success: boolean;
|
|
args: any;
|
|
result: any;
|
|
};
|
|
|
|
/**
|
|
* Execute a job and wait for it to complete and return the completed job
|
|
* @param id
|
|
*/
|
|
export declare function waitJob(id: string): Promise<Job>;
|
|
|
|
/**
|
|
* Get a job by id and return immediately with the current state of the job
|
|
* @param id
|
|
*/
|
|
export declare function getJob(id: string): Promise<Job>;
|
|
|
|
export type StreamUpdate = {
|
|
new_result_stream?: string;
|
|
stream_offset?: number;
|
|
};
|
|
|
|
/**
|
|
* Stream job results using SSE. Calls onUpdate for each stream update,
|
|
* and resolves with the final result when the job completes.
|
|
* @param id - The job ID to stream
|
|
* @param onUpdate - Optional callback for stream updates with new_result_stream data
|
|
* @returns Promise that resolves with the final job result
|
|
*/
|
|
export declare function streamJob(id: string, onUpdate?: (data: StreamUpdate) => void): Promise<any>;
|
|
`
|
|
}
|