feat: dev/staging/prod and deploy from web (#1733)

This commit is contained in:
Ruben Fiszel
2023-06-17 13:57:04 +02:00
committed by GitHub
parent 595f87bb96
commit 183edb82a3
30 changed files with 1055 additions and 65 deletions

View File

@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE workspace_settings DROP COLUMN deploy_to;

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE workspace_settings ADD COLUMN deploy_to VARCHAR(255);

View File

@@ -677,6 +677,11 @@
"name": "webhook",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "deploy_to",
"ordinal": 10,
"type_info": "Varchar"
}
],
"nullable": [
@@ -689,6 +694,7 @@
true,
true,
true,
true,
true
],
"parameters": {
@@ -2369,6 +2375,11 @@
"name": "webhook",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "deploy_to",
"ordinal": 10,
"type_info": "Varchar"
}
],
"nullable": [
@@ -2381,6 +2392,7 @@
true,
true,
true,
true,
true
],
"parameters": {
@@ -6950,6 +6962,26 @@
},
"query": "SELECT distinct(path) FROM script WHERE workspace_id = $1"
},
"f2f42aded6f1a400c84e2575abb551dfcd9260714eef333a85b1e5d7d5c9f67e": {
"describe": {
"columns": [
{
"name": "deploy_to",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
true
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT deploy_to FROM workspace_settings WHERE workspace_id = $1"
},
"f325a1262084bd3468e12dc8bcc289a96536f172b679af54dd0fbc82d4d7c987": {
"describe": {
"columns": [],
@@ -7275,5 +7307,18 @@
}
},
"query": "UPDATE variable SET value = $1 WHERE workspace_id = $2 AND path = $3"
},
"ff89e9f0941507d5cbfbb27e7c3b1ebad6b2b0a836334a5e20f151711fc11370": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
}
},
"query": "UPDATE workspace_settings SET deploy_to = $1 WHERE workspace_id = $2"
}
}

View File

@@ -935,6 +935,28 @@ paths:
type: string
webhook:
type: string
deploy_to:
type: string
/w/{workspace}/workspaces/get_deploy_to:
get:
summary: get deploy to
operationId: getDeployTo
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: status
content:
application/json:
schema:
type: object
properties:
deploy_to:
type: string
/w/{workspace}/workspaces/premium_info:
get:
@@ -987,6 +1009,32 @@ paths:
schema:
type: string
/w/{workspace}/workspaces/edit_deploy_to:
post:
summary: edit deploy to
operationId: editDeployTo
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
deploy_to:
type: string
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/w/{workspace}/workspaces/edit_auto_invite:
post:
summary: edit auto invite

View File

@@ -59,9 +59,11 @@ pub fn workspaced_service() -> Router {
.route("/add_user", post(add_user))
.route("/delete_invite", post(delete_invite))
.route("/get_settings", get(get_settings))
.route("/get_deploy_to", get(get_deploy_to))
.route("/edit_slack_command", post(edit_slack_command))
.route("/edit_webhook", post(edit_webhook))
.route("/edit_auto_invite", post(edit_auto_invite))
.route("/edit_deploy_to", post(edit_deploy_to))
.route("/tarball", get(tarball_workspace))
.route("/premium_info", get(premium_info));
@@ -109,6 +111,7 @@ pub struct WorkspaceSettings {
pub customer_id: Option<String>,
pub plan: Option<String>,
pub webhook: Option<String>,
pub deploy_to: Option<String>,
}
#[derive(FromRow, Serialize, Debug)]
@@ -131,6 +134,11 @@ struct EditCommandScript {
slack_command_script: Option<String>,
}
#[derive(Deserialize)]
struct EditDeployTo {
deploy_to: Option<String>,
}
#[derive(Deserialize)]
struct EditAutoInvite {
operator: Option<bool>,
@@ -407,6 +415,29 @@ async fn get_settings(
Ok(Json(settings))
}
#[derive(Serialize)]
struct DeployTo {
deploy_to: Option<String>,
}
async fn get_deploy_to(
authed: Authed,
Path(w_id): Path<String>,
Extension(user_db): Extension<UserDB>,
) -> JsonResult<DeployTo> {
let mut tx = user_db.begin(&authed).await?;
let settings = sqlx::query_as!(
DeployTo,
"SELECT deploy_to FROM workspace_settings WHERE workspace_id = $1",
&w_id
)
.fetch_one(&mut tx)
.await
.map_err(|e| Error::InternalErr(format!("getting deploy_to: {e}")))?;
tx.commit().await?;
Ok(Json(settings))
}
async fn edit_slack_command(
authed: Authed,
Extension(db): Extension<DB>,
@@ -447,6 +478,51 @@ async fn edit_slack_command(
Ok(format!("Edit command script {}", &w_id))
}
async fn edit_deploy_to(
authed: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Authed { is_admin, username, .. }: Authed,
Json(es): Json<EditDeployTo>,
) -> Result<String> {
require_admin(is_admin, &username)?;
#[cfg(not(feature = "enterprise"))]
{
return Err(Error::BadRequest(
"Deploy to is only available on enterprise".to_string(),
));
}
let mut tx = db.begin().await?;
sqlx::query!(
"UPDATE workspace_settings SET deploy_to = $1 WHERE workspace_id = $2",
es.deploy_to,
&w_id
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
"workspaces.edit_deploy_to",
ActionKind::Update,
&w_id,
Some(&authed.email),
Some(
[(
"script",
es.deploy_to.unwrap_or("NO_DEPLOY_TO".to_string()).as_str(),
)]
.into(),
),
)
.await?;
tx.commit().await?;
Ok(format!("Edit deploy to for {}", &w_id))
}
const BANNED_DOMAINS: &str = include_str!("../banned_domains.txt");
async fn is_allowed_auto_domain(Authed { email, .. }: Authed) -> JsonResult<bool> {

View File

@@ -1927,7 +1927,7 @@ pub async fn get_reserved_variables(
job.script_path.clone(),
job.parent_job.map(|x| x.to_string()),
flow_path,
job.schedule_path.clone(),
job.schedule_path.clone()
);
Ok(variables
.into_iter()

View File

@@ -22,6 +22,7 @@
"chartjs-plugin-zoom": "^2.0.0",
"d3-zoom": "^3.0.0",
"date-fns": "^2.30.0",
"diff": "^5.1.0",
"esm-env": "^1.0.0",
"fast-equals": "^5.0.1",
"highlight.js": "^11.8.0",
@@ -2799,6 +2800,14 @@
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true
},
"node_modules/diff": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
"integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -10089,6 +10098,11 @@
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true
},
"diff": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
"integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw=="
},
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",

View File

@@ -82,6 +82,7 @@
"chartjs-plugin-zoom": "^2.0.0",
"d3-zoom": "^3.0.0",
"date-fns": "^2.30.0",
"diff": "^5.1.0",
"esm-env": "^1.0.0",
"fast-equals": "^5.0.1",
"highlight.js": "^11.8.0",

View File

@@ -23,7 +23,7 @@
</script>
<Drawer bind:this={jsonViewer} size="800px">
<DrawerContent title="Argument Details" on:close={jsonViewer.toggleDrawer}>
<DrawerContent title="Argument Details" on:close={jsonViewer.closeDrawer}>
<svelte:fragment slot="actions">
<Button
on:click={() => copyToClipboard(JSON.stringify(jsonViewerContent, null, 4))}

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import { WorkspaceService } from '$lib/gen'
import { usersWorkspaceStore, workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
export let workspaceToDeployTo: string | undefined
$: deployableWorkspaces = $usersWorkspaceStore?.workspaces
.map((w) => w.id)
.filter((w) => w != $workspaceStore)
</script>
<h3 class="mt-8">Workspace to link to</h3>
<div class="flex min-w-0 mt-2">
<select
bind:value={workspaceToDeployTo}
on:change={async (e) => {
await WorkspaceService.editDeployTo({
workspace: $workspaceStore ?? '',
requestBody: { deploy_to: workspaceToDeployTo == '' ? undefined : workspaceToDeployTo }
})
if (workspaceToDeployTo == '') {
workspaceToDeployTo = undefined
sendUserToast('Disabled setting deployable workspace')
} else {
sendUserToast('Set deployable workspace to ' + workspaceToDeployTo)
}
}}
>
{#if deployableWorkspaces?.length == 0}
<option disabled>No workspace deployable to</option>
{/if}
<option value="">Disable deployment</option>
{#each deployableWorkspaces ?? [] as name}
<option value={name}>{name}</option>
{/each}
</select>
</div>

View File

@@ -0,0 +1,558 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { enterpriseLicense, superadmin, workspaceStore } from '$lib/stores'
import {
AppService,
FlowService,
RawAppService,
ResourceService,
ScheduleService,
ScriptService,
UserService,
VariableService,
WorkspaceService
} from '$lib/gen'
import { getAllModules } from './flows/flowExplorer'
import Button from './common/button/Button.svelte'
import Tooltip from './Tooltip.svelte'
import Alert from './common/alert/Alert.svelte'
import Toggle from './Toggle.svelte'
import { Loader2 } from 'lucide-svelte'
import Badge from './common/badge/Badge.svelte'
import * as Diff from 'diff'
import { Drawer, DrawerContent } from './common'
const dispatch = createEventDispatcher()
type Kind = 'script' | 'resource' | 'schedule' | 'variable' | 'flow' | 'app' | 'raw_app'
export let kind: Kind
export let initialPath: string = ''
export let workspaceToDeployTo: string | undefined = undefined
export let hideButton: boolean = false
let seeTarget: boolean | undefined = undefined
let dependencies: { kind: Kind; path: string; include: boolean }[] | undefined = undefined
const allAlreadyExists: { [key: string]: boolean } = {}
let notSet: boolean | undefined = undefined
$: WorkspaceService.getDeployTo({ workspace: $workspaceStore! }).then((x) => {
workspaceToDeployTo = x.deploy_to
if (x.deploy_to == undefined) {
notSet = true
}
})
$: workspaceToDeployTo && reload(initialPath)
async function reload(path: string) {
try {
if (!$superadmin) {
await UserService.whoami({ workspace: workspaceToDeployTo! })
}
seeTarget = true
} catch {
seeTarget = false
}
dependencies = (await getDependencies(kind, path)).map((x) => ({
...x,
include: x.kind != 'variable' && x.kind != 'resource'
}))
dependencies.forEach((x) => {
checkAlreadyExists(x.kind, x.path).then(
(y) => (allAlreadyExists[computeStatusPath(x.kind, x.path)] = y)
)
})
}
async function getDependencies(
kind: Kind,
path: string
): Promise<{ kind: Kind; path: string }[]> {
async function rec(kind: Kind, path: string): Promise<{ kind: Kind; path: string }[]> {
if (kind == 'schedule') {
const schedule = await ScheduleService.getSchedule({ workspace: $workspaceStore!, path })
if (schedule.script_path && schedule.script_path != '') {
if (schedule.script_path) {
return [{ kind: 'script', path: schedule.script_path }]
} else {
return [{ kind: 'flow', path: schedule.script_path }]
}
} else {
return []
}
} else if (kind == 'flow') {
const flow = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path })
return getAllModules(flow.value.modules, flow.value.failure_module).flatMap((x) => {
let result: { kind: Kind; path: string }[] = []
if (x.value.type == 'script' || x.value.type == 'rawscript' || x.value.type == 'flow') {
Object.values(x.value.input_transforms).forEach((y) => {
if (y.type == 'static' && typeof y.value == 'string') {
if (y.value.startsWith('$res:')) {
result.push({ kind: 'resource', path: y.value.substring(5) })
} else if (y.value.startsWith('$var:')) {
result.push({ kind: 'variable', path: y.value.substring(5) })
}
}
})
}
if (x.value.type == 'script') {
if (x.value.path) {
result.push({ kind: 'script', path: x.value.path })
}
} else if (x.value.type == 'flow') {
if (x.value.path) {
result.push({ kind: 'flow', path: x.value.path })
}
}
return result
})
} else if (kind == 'resource') {
const res = await ResourceService.getResource({ workspace: $workspaceStore!, path })
function recObj(obj: any) {
if (typeof obj == 'string' && obj.startsWith('$var:')) {
return [{ kind: 'variable', path: obj.substring(5) }]
} else if (typeof obj == 'object') {
return Object.values(obj).flatMap((x) => recObj(x))
} else {
return []
}
}
return recObj(res.value)
}
return []
}
let toProcess = [{ kind, path }]
let processed: { kind: Kind; path: string }[] = []
while (toProcess.length > 0) {
const { kind, path } = toProcess.pop()!
console.log('BAR', kind, path)
toProcess.push(...(await rec(kind, path)))
processed.push({ kind, path })
}
processed.reverse()
return processed
}
async function checkAlreadyExists(kind: Kind, path: string): Promise<boolean> {
if (kind == 'flow') {
return await FlowService.existsFlowByPath({
workspace: workspaceToDeployTo!,
path: path
})
} else if (kind == 'script') {
return await ScriptService.existsScriptByPath({
workspace: workspaceToDeployTo!,
path: path
})
} else if (kind == 'app') {
return await AppService.existsApp({
workspace: workspaceToDeployTo!,
path: path
})
} else if (kind == 'raw_app') {
return await RawAppService.existsRawApp({
workspace: workspaceToDeployTo!,
path: path
})
} else if (kind == 'variable') {
return await VariableService.existsVariable({
workspace: workspaceToDeployTo!,
path: path
})
} else if (kind == 'resource') {
return await ResourceService.existsResource({
workspace: workspaceToDeployTo!,
path: path
})
} else if (kind == 'schedule') {
return await ScheduleService.existsSchedule({
workspace: workspaceToDeployTo!,
path: path
})
} else {
throw new Error(`Unknown kind ${kind}`)
}
}
const deploymentStatus: Record<
string,
{ status: 'loading' | 'deployed' | 'failed'; error?: string }
> = {}
async function deploy(kind: Kind, path: string) {
const statusPath = `${kind}:${path}`
deploymentStatus[statusPath] = { status: 'loading' }
try {
let alreadyExists = await checkAlreadyExists(kind, path)
if (kind == 'flow') {
const flow = await FlowService.getFlowByPath({
workspace: $workspaceStore!,
path: path
})
getAllModules(flow.value.modules).forEach((x) => {
if (x.value.type == 'script' && x.value.hash != undefined) {
x.value.hash = undefined
}
})
if (alreadyExists) {
await FlowService.updateFlow({
workspace: workspaceToDeployTo!,
path: path,
requestBody: {
...flow
}
})
} else {
await FlowService.createFlow({
workspace: workspaceToDeployTo!,
requestBody: {
...flow
}
})
}
} else if (kind == 'script') {
const script = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: path
})
await ScriptService.createScript({
workspace: workspaceToDeployTo!,
requestBody: {
...script,
lock: script.lock?.split('\n'),
parent_hash: alreadyExists
? (
await ScriptService.getScriptByPath({
workspace: workspaceToDeployTo!,
path: path
})
).hash
: undefined
}
})
} else if (kind == 'app') {
const app = await AppService.getAppByPath({
workspace: $workspaceStore!,
path: path
})
if (alreadyExists) {
await AppService.updateApp({
workspace: workspaceToDeployTo!,
path: path,
requestBody: {
...app
}
})
} else {
await AppService.createApp({
workspace: workspaceToDeployTo!,
requestBody: {
...app
}
})
}
} else if (kind == 'variable') {
const variable = await VariableService.getVariable({
workspace: $workspaceStore!,
path: path,
decryptSecret: true
})
if (alreadyExists) {
await VariableService.updateVariable({
workspace: workspaceToDeployTo!,
path: path,
requestBody: {
path: path,
value: variable.value ?? '',
is_secret: variable.is_secret,
description: variable.description ?? ''
},
alreadyEncrypted: false
})
} else {
await VariableService.createVariable({
workspace: workspaceToDeployTo!,
requestBody: {
path: path,
value: variable.value ?? '',
is_secret: variable.is_secret,
description: variable.description ?? ''
}
})
}
} else if (kind == 'resource') {
const resource = await ResourceService.getResource({
workspace: $workspaceStore!,
path: path
})
if (alreadyExists) {
await ResourceService.updateResource({
workspace: workspaceToDeployTo!,
path: path,
requestBody: {
path: path,
value: resource.value ?? '',
description: resource.description ?? ''
}
})
} else {
await ResourceService.createResource({
workspace: workspaceToDeployTo!,
requestBody: {
path: path,
value: resource.value ?? '',
resource_type: resource.resource_type,
description: resource.description ?? ''
}
})
}
} else if (kind == 'raw_app') {
throw new Error('Raw app deploy not implemented yet')
// const app = await RawAppService.getRawAppData({
// workspace: $workspaceStore!,
// path: path
// })
// if (alreadyExists) {
// }
// await RawAppService.updateRawApp({
// workspace: $workspaceStore!,
// path: path,
// requestBody: {
// path: path
// }
// })
}
allAlreadyExists[statusPath] = true
deploymentStatus[statusPath] = { status: 'deployed' }
} catch (e) {
deploymentStatus[statusPath] = { status: 'failed', error: e.body || e.message }
}
}
function deployAll() {
dependencies?.slice().forEach(async ({ kind, path, include }) => {
if (include) {
await deploy(kind, path)
}
})
dispatch('update', initialPath)
}
function computeStatusPath(kind: Kind, path: string) {
return `${kind}:${path}`
}
export function showDiff(local: string, remote: string) {
let finalString = ''
for (const part of Diff.diffLines(local, remote)) {
if (part.removed) {
// print red if removed without newline
finalString += `<span class="text-red-600">${part.value}</span>`
} else if (part.added) {
// print green if added
finalString += `<span class="text-green-600">${part.value}</span>`
} else {
let lines = part.value.split('\n')
if (lines.length > 12) {
lines = lines.slice(0, 6)
lines.push('...')
lines = lines.concat(part.value.split('\n').slice(-6))
}
// print white if unchanged
finalString += `${lines.join('\n')}`
}
}
return finalString
}
let diffViewer: Drawer
let diffContent: string | undefined = undefined
async function getValue(kind: Kind, path: string, workspace: string) {
try {
if (kind == 'flow') {
const flow = await FlowService.getFlowByPath({
workspace: workspace,
path: path
})
getAllModules(flow.value.modules).forEach((x) => {
if (x.value.type == 'script' && x.value.hash != undefined) {
x.value.hash = undefined
}
})
return flow.value
} else if (kind == 'script') {
const script = await ScriptService.getScriptByPath({
workspace: workspace,
path: path
})
return {
content: script.content,
lock: script.lock,
schema: script.schema,
summary: script.summary
}
} else if (kind == 'app') {
const app = await AppService.getAppByPath({
workspace: workspace,
path: path
})
return app
} else if (kind == 'variable') {
const variable = await VariableService.getVariable({
workspace: workspace,
path: path,
decryptSecret: true
})
return variable.value
} else if (kind == 'resource') {
const resource = await ResourceService.getResource({
workspace: workspace,
path: path
})
return resource.value
} else if (kind == 'raw_app') {
throw new Error('Raw app deploy not implemented yet')
// const app = await RawAppService.getRawAppData({
// workspace: workspace,
// path: path
// })
// if (alreadyExists) {
// }
// await RawAppService.updateRawApp({
// workspace: workspace,
// path: path,
// requestBody: {
// path: path
// }
// })
}
} catch {
return {}
}
}
async function computeDiff(kind: Kind, path: string) {
let values = await Promise.all([
getValue(kind, path, $workspaceStore!),
getValue(kind, path, workspaceToDeployTo!)
])
diffContent = showDiff(JSON.stringify(values[0], null, 2), JSON.stringify(values[1], null, 2))
}
</script>
<div class="mt-6" />
{#if !$enterpriseLicense}
<Alert type="error" title="Enterprise license required"
>Deploy to staging/prod from the web UI is only available with an enterprise license</Alert
>
{:else if notSet == true}
<Alert type="error" title="Staging/Prod deploy not set up"
>As an admin, go to "Workspace {'->'} Dev/Staging/Prod"</Alert
>
{:else}
<Alert type="info" title="Shareable page"
>Share this <a href="/deploy/{kind}/{initialPath}">link</a> to have another properly permissioned
user do the deployment</Alert
>
<h3 class="mb-2 mt-8"
>Destination Workspace&nbsp; <Tooltip
>Workspace to deploy to is set in the workspace settings</Tooltip
></h3
>
<input class="max-w-xs" type="text" disabled value={workspaceToDeployTo} />
{#if seeTarget == undefined}
<div class="mt-6" />
<Loader2 class="animate-spin" />
{:else if seeTarget == true}
<h3 class="mb-6 mt-16">All related deployable items</h3>
<Drawer bind:this={diffViewer} size="800px">
<DrawerContent title="Diff" on:close={diffViewer.closeDrawer}>
{#if diffContent == undefined}
<Loader2 class="animate-spin" />
{:else}
<pre class="border bg-white p-2"><code>{@html diffContent}</code></pre>
<div class="flex flex-row-reverse gap-2">
<div class="text-red-600">Removed</div>
<div class="text-green-600">Added</div></div
>
{/if}
</DrawerContent>
</Drawer>
<div class="grid grid-cols-9 justify-center max-w-3xl gap-2">
{#each dependencies ?? [] as { kind, path, include }}
{@const statusPath = computeStatusPath(kind, path)}
<div class="col-span-1 truncate text-gray-700 text-sm">{kind}</div><div
class="col-span-5 truncate font-semibold">{path}</div
><div class="col-span-1"><Toggle size="xs" bind:checked={include} /></div><div
class="col-span-1"
>
{#if allAlreadyExists[statusPath] == false}
{#if include}
<Badge
>New <Tooltip
>This {kind} doesn't exist yet on the target and will be created by the deployment</Tooltip
></Badge
>
{:else}
<Badge color="red">
Missing
<Tooltip
>This {kind} doesn't exist and is not included in the deployment. Variable and Resources
are considered to be workspace specific and are never included by default.</Tooltip
>
</Badge>
{/if}
{:else if allAlreadyExists[statusPath] == true}
<button
class="text-blue-600 font-normal mt-1"
on:click={() => {
diffContent = undefined
computeDiff(kind, path)
diffViewer.openDrawer()
}}>diff</button
>
{/if}</div
>
<div class="col-span-1">
{#if deploymentStatus[statusPath]}
{#if deploymentStatus[statusPath].status == 'loading'}
<Loader2 class="animate-spin" />
{:else if deploymentStatus[statusPath].status == 'deployed'}
<Badge color="green">Deployed</Badge>
{:else if deploymentStatus[statusPath].status == 'failed'}
<Badge color="red">Failed</Badge>
<Tooltip>{deploymentStatus[statusPath].error}</Tooltip>
{/if}
{:else}
<Button color="light" size="xs" on:click={() => deploy(kind, path)}>Deploy</Button>
{/if}
</div>
{/each}
</div>
{#if !hideButton}
<div class="mt-16 flex flex-row-reverse max-w-3xl"
><Button on:click={deployAll}>Deploy All Toggled</Button></div
>
{/if}
{:else}
<div class="my-2" />
<Alert type="error" title="User not allowed to deploy to this workspace"
>Ask a permissioned user to deploy this item using the shareable link or get the proper
permissions on the target workspace</Alert
>
{/if}
{/if}

View File

@@ -0,0 +1,40 @@
<script lang="ts">
import { Button, Drawer } from './common'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import DeployWorkspace from './DeployWorkspace.svelte'
type Kind = 'script' | 'resource' | 'schedule' | 'variable' | 'flow' | 'app' | 'raw_app'
let initialPath: string | undefined = undefined
let kind: Kind | undefined = undefined
let drawer: Drawer | undefined = undefined
let workspaceToDeployTo: string | undefined = undefined
let deployWorkspace: DeployWorkspace | undefined = undefined
export async function openDrawer(initialPath_l: string, kind_l: Kind) {
initialPath = initialPath_l
kind = kind_l
drawer?.openDrawer()
}
</script>
<Drawer bind:this={drawer} size="900px">
<DrawerContent title="Deploy {initialPath} to staging or prod" on:close={drawer.closeDrawer}>
{#if kind != undefined && initialPath != undefined}
<DeployWorkspace
hideButton
{initialPath}
{kind}
bind:workspaceToDeployTo
bind:this={deployWorkspace}
/>
{/if}
<svelte:fragment slot="actions">
<Button
disabled={workspaceToDeployTo == undefined}
on:click={() => deployWorkspace?.deploy(kind, initialPath)}>Deploy All</Button
>
</svelte:fragment>
</DrawerContent>
</Drawer>

View File

@@ -111,9 +111,9 @@
<h2 class="border-b pb-1 mt-10 mb-4">Path</h2>
<div class="flex flex-col mb-2 gap-6">
<Path disabled={!own} {kind} {initialPath} bind:path />
<div class="mt-4" />
<Button disabled={!own} on:click={updatePath}>Move/Rename</Button>
<div />
</div>
<svelte:fragment slot="actions">
<Button disabled={!own} on:click={updatePath}>Move/Rename</Button>
</svelte:fragment>
</DrawerContent>
</Drawer>

View File

@@ -21,12 +21,13 @@
import { writable } from 'svelte/store'
import { Button, Drawer, DrawerContent } from './common'
import Badge from './common/badge/Badge.svelte'
import ToggleButton from './common/toggleButton/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import FolderEditor from './FolderEditor.svelte'
import { random_adj } from './random_positive_adjetive'
import Required from './Required.svelte'
import Tooltip from './Tooltip.svelte'
import { Folder, User } from 'lucide-svelte'
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app' | 'raw_app'
let meta: Meta | undefined = undefined
@@ -277,7 +278,7 @@
<div class="flex gap-4 shrink">
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="block">
<span class="text-gray-700 text-sm whitespace-nowrap">Owner</span>
<span class="text-gray-700 text-sm whitespace-nowrap">&nbsp;</span>
<ToggleButtonGroup
class="mt-0.5"
@@ -286,7 +287,8 @@
const kind = e.detail
if (meta) {
if (kind === 'folder') {
meta.owner = $userStore?.folders?.[0] ?? ''
console.log($userStore?.folders)
meta.owner = folders?.[0]?.name ?? ''
} else if (kind === 'group') {
meta.owner = 'all'
} else {
@@ -295,9 +297,25 @@
}
}}
>
<ToggleButton light size="xs" value="user" position="left">User</ToggleButton>
<ToggleButton
icon={User}
{disabled}
light
size="xs"
value="user"
position="left"
label="User"
/>
<!-- <ToggleButton light size="xs" value="group" position="center">Group</ToggleButton> -->
<ToggleButton light size="xs" value="folder" position="right">Folder</ToggleButton>
<ToggleButton
icon={Folder}
{disabled}
light
size="xs"
value="folder"
position="right"
label="Folder"
/>
</ToggleButtonGroup>
</label>
{#if meta.ownerKind === 'user'}
@@ -308,7 +326,7 @@
type="text"
bind:value={meta.owner}
placeholder={$userStore?.username ?? ''}
disabled={!($superadmin || ($userStore?.is_admin ?? false))}
disabled={disabled || !($superadmin || ($userStore?.is_admin ?? false))}
/>
</label>
{:else if meta.ownerKind === 'folder'}
@@ -334,6 +352,7 @@
title="View folder"
btnClasses="!p-1.5"
variant="border"
color="light"
size="xs"
disabled={!meta.owner || meta.owner == ''}
on:click={viewFolder.openDrawer}
@@ -344,6 +363,7 @@
title="New folder"
btnClasses="!p-1.5"
variant="border"
color="light"
size="xs"
{disabled}
on:click={newFolder.openDrawer}

View File

@@ -409,7 +409,7 @@
{#if !isCloudHosted()}
<h2 class="border-b pb-1 mt-10 mb-4"
>Custom env variables<Tooltip
documentationLink="https://docs.windmill.dev/docs/reference#custom-environment-variables"
documentationLink="https://docs.windmill.dev/docs/reference#custom-environment-variables"
>Additional static custom env variables to pass to the script.</Tooltip
></h2
>
@@ -463,7 +463,7 @@
<div class="flex flex-col h-screen">
<div class="flex flex-col w-full px-2 py-1 border-b shadow-sm">
<div class="justify-between flex gap-8 w-full items-center px-2">
<div class="justify-between flex gap-2 lg:gap-8 w-full items-center px-2">
<div class="min-w-64 w-full max-w-md">
<input
type="text"
@@ -513,7 +513,7 @@
<Awareness />
{/if}
<div class="flex flex-row gap-x-4">
<div class="flex flex-row gap-x-1 lg:gap-x-4">
<Button
color="light"
variant="border"
@@ -548,7 +548,7 @@
size="sm"
startIcon={{ icon: faSave }}
on:click={() => editScript()}
dropdownItems={initialPath != '' ? computeDropdownItems : undefined}
dropdownItems={initialPath != '' ? computeDropdownItems : []}
>
Deploy
</Button>
@@ -563,7 +563,7 @@
Script kind &nbsp;
<Tooltip>
Tag this script's purpose within flows such that it is available as the corresponding
action.
action.
</Tooltip>
</h2>
<div class="flex flex-wrap gap-2">
@@ -599,6 +599,7 @@
<ScriptSchema bind:schema={script.schema} />
</DrawerContent>
</Drawer>
<ScriptEditor
collabMode
edit={initialPath != ''}

View File

@@ -7,11 +7,13 @@
import Select from 'svelte-select'
import { getScriptByPath } from '$lib/scripts'
import RadioButton from './RadioButton.svelte'
import { Button, Drawer, DrawerContent } from './common'
import HighlightCode from './HighlightCode.svelte'
import FlowPathViewer from './flows/content/FlowPathViewer.svelte'
import { SELECT_INPUT_DEFAULT_STYLE } from '../defaults'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import { Code2, Globe } from 'lucide-svelte'
export let initialPath: string | undefined = undefined
export let scriptPath: string | undefined = undefined
@@ -27,9 +29,9 @@
let code: string = ''
let lang: 'deno' | 'python3' | 'go' | 'bash' | undefined
let options: [[string, any]] = [['Script', 'script']]
allowHub && options.unshift(['Hub', 'hub'])
allowFlow && options.push(['Flow', 'flow'])
let options: [[string, any, any]] = [['Script', 'script', Code2]]
allowHub && options.unshift(['Hub', 'hub', Globe])
allowFlow && options.push(['Flow', 'flow', undefined])
const dispatch = createEventDispatcher()
async function loadItems(): Promise<void> {
@@ -69,10 +71,14 @@
</DrawerContent>
</Drawer>
<div class="flex flex-row items-center gap-4 w-full">
<div class="flex flex-row items-center gap-4 w-full mt-2">
{#if options.length > 1}
<div class="w-80 mt-1">
<RadioButton {disabled} bind:value={itemKind} {options} />
<div>
<ToggleButtonGroup bind:selected={itemKind}>
{#each options as [label, value, icon]}
<ToggleButton {icon} {disabled} {value} {label} />
{/each}
</ToggleButtonGroup>
</div>
{/if}

View File

@@ -91,7 +91,9 @@
}
}
$: value = (result ?? []).map((x, i) => ({ ...x, __index: i.toString() }))
$: value = Array.isArray(result)
? result.map((x, i) => ({ ...x, __index: i.toString() }))
: [{ error: 'input was not an array' }]
</script>
{#each Object.keys(components['aggridcomponent'].initialData.configuration) as key (key)}

View File

@@ -23,12 +23,14 @@
import { Eye } from 'lucide-svelte'
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
export let app: ListableApp & { has_draft?: boolean; draft_only?: boolean; canWrite: boolean }
export let marked: string | undefined
export let starred: boolean
export let shareModal: ShareModal
export let moveDrawer: MoveDrawer
export let deploymentDrawer: DeployWorkspaceDrawer
export let deleteConfirmedCallback: (() => void) | undefined
let {
@@ -157,6 +159,13 @@
},
disabled: !canWrite
},
{
displayName: 'Deploy to staging/prod',
icon: faFileExport,
action: () => {
deploymentDrawer.openDrawer(path, 'app')
}
},
{
displayName: canWrite ? 'Share' : 'See Permissions',
icon: faShare,

View File

@@ -26,6 +26,7 @@
import DraftBadge from '$lib/components/DraftBadge.svelte'
import { sendUserToast } from '$lib/toast'
import { isOwner } from '$lib/utils'
import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
export let flow: Flow & { has_draft?: boolean; draft_only?: boolean; canWrite: boolean }
export let marked: string | undefined
@@ -33,6 +34,7 @@
export let shareModal: ShareModal
export let moveDrawer: MoveDrawer
export let deleteConfirmedCallback: (() => void) | undefined
export let deploymentDrawer: DeployWorkspaceDrawer
let { summary, path, extra_perms, canWrite, workspace_id, archived, draft_only, has_draft } = flow
@@ -190,6 +192,14 @@
},
disabled: !owner || archived
},
{
displayName: 'Deploy to staging/prod',
icon: faFileExport,
action: () => {
deploymentDrawer.openDrawer(path, 'flow')
},
disabled: archived
},
{
displayName: 'Schedule',
icon: faCalendarAlt,

View File

@@ -19,6 +19,7 @@
import DrawerContent from '../drawer/DrawerContent.svelte'
import FileInput from '../fileInput/FileInput.svelte'
import { goto } from '$app/navigation'
import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
export let app: ListableRawApp & { canWrite: boolean }
export let marked: string | undefined
@@ -26,6 +27,7 @@
export let shareModal: ShareModal
export let moveDrawer: MoveDrawer
export let deleteConfirmedCallback: (() => void) | undefined
export let deploymentDrawer: DeployWorkspaceDrawer
let updateAppDrawer: Drawer
@@ -112,6 +114,21 @@
},
disabled: !canWrite
},
{
displayName: 'Move/Rename',
icon: faFileExport,
action: () => {
moveDrawer.openDrawer(path, summary, 'raw_app')
},
disabled: !canWrite
},
{
displayName: 'Deploy to prod/staging',
icon: faFileExport,
action: () => {
deploymentDrawer.openDrawer(path, 'raw_app')
}
},
{
displayName: canWrite ? 'Share' : 'See Permissions',
icon: faShare,

View File

@@ -28,12 +28,14 @@
import DraftBadge from '$lib/components/DraftBadge.svelte'
import { sendUserToast } from '$lib/toast'
import { isOwner } from '$lib/utils'
import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
export let script: Script & { canWrite: boolean }
export let marked: string | undefined
export let starred: boolean
export let shareModal: ShareModal
export let moveDrawer: MoveDrawer
export let deploymentDrawer: DeployWorkspaceDrawer
export let deleteConfirmedCallback: (() => void) | undefined
let {
@@ -209,6 +211,14 @@
},
disabled: !owner || archived
},
{
displayName: 'Deploy to staging/prod',
icon: faFileExport,
action: () => {
deploymentDrawer.openDrawer(path, 'script')
},
disabled: archived
},
{
displayName: 'View runs',
icon: faList,

View File

@@ -20,7 +20,6 @@
export let wrapperClass = ''
export let style = ''
export let hashNavigation = false
export let dflt: string | undefined = undefined
export let values: string[] | undefined = undefined
$: selected && updateSelected()

View File

@@ -37,6 +37,7 @@
import { canWrite } from '$lib/utils'
import { page } from '$app/stores'
import { setQuery } from '$lib/navigation'
import DeployWorkspaceDrawer from '../DeployWorkspaceDrawer.svelte'
type TableItem<T, U extends 'script' | 'flow' | 'app' | 'raw_app'> = T & {
canWrite: boolean
@@ -62,6 +63,7 @@
let shareModal: ShareModal
let moveDrawer: MoveDrawer
let deploymentDrawer: DeployWorkspaceDrawer
let loading = true
@@ -270,6 +272,7 @@
}}
/>
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
<MoveDrawer
bind:this={moveDrawer}
on:update={() => {
@@ -376,6 +379,7 @@
script={item}
{shareModal}
{moveDrawer}
{deploymentDrawer}
/>
{:else if item.type == 'flow'}
<FlowRow
@@ -386,6 +390,7 @@
flow={item}
{shareModal}
{moveDrawer}
{deploymentDrawer}
/>
{:else if item.type == 'app'}
<AppRow
@@ -396,6 +401,7 @@
app={item}
{moveDrawer}
{shareModal}
{deploymentDrawer}
/>
{:else if item.type == 'raw_app'}
<RawAppRow
@@ -406,6 +412,7 @@
app={item}
{moveDrawer}
{shareModal}
{deploymentDrawer}
/>
{/if}
{/key}

View File

@@ -201,7 +201,7 @@
{#if !$userStore?.operator}
<div class="w-full overflow-auto scrollbar-hidden">
<Tabs values={['hub', 'workspace']} dflt="workspace" hashNavigation bind:selected={tab}>
<Tabs values={['hub', 'workspace']} hashNavigation bind:selected={tab}>
<Tab size="md" value="workspace">
<div class="flex gap-2 items-center my-1">
<Building size={18} />

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import { page } from '$app/stores'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import DeployWorkspace from '$lib/components/DeployWorkspace.svelte'
$: kind = asKind($page.params.kind)
function asKind(
kind: string
): 'script' | 'resource' | 'schedule' | 'variable' | 'flow' | 'app' | 'raw_app' {
return kind as 'script' | 'resource' | 'schedule' | 'variable' | 'flow' | 'app' | 'raw_app'
}
</script>
<CenteredPage>
<h1 class="mt-6">Deploy {$page.params.kind} {$page.params.path}</h1>
<DeployWorkspace {kind} initialPath={$page.params.path} />
</CenteredPage>

View File

@@ -17,6 +17,7 @@
faClipboard,
faCodeFork,
faEdit,
faFileExport,
faList,
faPlay,
faShare,
@@ -41,6 +42,7 @@
import { slide } from 'svelte/transition'
import { sendUserToast } from '$lib/toast'
import Urlize from '$lib/components/Urlize.svelte'
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
let userSettings: UserSettings
@@ -147,6 +149,7 @@
let webhook: HTMLHeadElement
let moveDrawer: MoveDrawer
let deploymentDrawer: DeployWorkspaceDrawer
</script>
<ScheduleEditor on:update={() => loadSchedule()} bind:this={scheduleEditor} />
@@ -159,6 +162,8 @@
layout={[0.75, [2, 0, 2], 2.25, [{ h: 1.5, w: 40 }], 0.2, [{ h: 1, w: 30 }]]}
/>
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
<MoveDrawer
bind:this={moveDrawer}
on:update={async (e) => {
@@ -286,6 +291,15 @@
>
Move/Rename
</Button>
<Button
on:click={() => deploymentDrawer.openDrawer(flow?.path ?? '', 'flow')}
variant="border"
color="light"
size="xs"
startIcon={{ icon: faFileExport }}
>
Deploy to staging/prod
</Button>
<Button
btnClasses="ml-2"
variant="border"

View File

@@ -23,7 +23,8 @@
faClipboard,
faArrowLeft,
faChevronUp,
faChevronDown
faChevronDown,
faFileExport
} from '@fortawesome/free-solid-svg-icons'
import Tooltip from '$lib/components/Tooltip.svelte'
import ShareModal from '$lib/components/ShareModal.svelte'
@@ -56,6 +57,7 @@
import { sendUserToast } from '$lib/toast'
import { scriptToHubUrl } from '$lib/hub'
import Urlize from '$lib/components/Urlize.svelte'
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
let userSettings: UserSettings
let script: Script | undefined
@@ -197,6 +199,7 @@
}/p/${script?.path}`
}
let moveDrawer: MoveDrawer
let deploymentDrawer: DeployWorkspaceDrawer
</script>
<MoveDrawer
@@ -206,6 +209,7 @@
loadScript($page.params.hash)
}}
/>
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
<ScheduleEditor bind:this={scheduleEditor} />
@@ -344,6 +348,15 @@
>
Move/Rename
</Button>
<Button
on:click={() => deploymentDrawer.openDrawer(script?.path ?? '', 'script')}
variant="border"
color="light"
size="xs"
startIcon={{ icon: faFileExport }}
>
Deploy to staging/prod
</Button>
<Button
color="dark"
variant="border"

View File

@@ -7,6 +7,7 @@
import { Alert, Badge, Button, Skeleton, Tab, Tabs } from '$lib/components/common'
import ToggleButton from '$lib/components/common/toggleButton/ToggleButton.svelte'
import ToggleButtonGroup from '$lib/components/common/toggleButton/ToggleButtonGroup.svelte'
import DeployToSetting from '$lib/components/DeployToSetting.svelte'
import InviteUser from '$lib/components/InviteUser.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
@@ -24,9 +25,15 @@
WorkspaceService,
type WorkspaceInvite
} from '$lib/gen'
import { superadmin, userStore, usersWorkspaceStore, workspaceStore } from '$lib/stores'
import {
enterpriseLicense,
superadmin,
userStore,
usersWorkspaceStore,
workspaceStore
} from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { capitalize } from '$lib/utils'
import { capitalize, setQueryWithoutLoad } from '$lib/utils'
import { faSlack } from '@fortawesome/free-brands-svg-icons'
import { faBarsStaggered, faExternalLink, faScroll } from '@fortawesome/free-solid-svg-icons'
@@ -45,13 +52,15 @@
let plan: string | undefined = undefined
let customer_id: string | undefined = undefined
let webhook: string | undefined = undefined
let workspaceToDeployTo: string | undefined = undefined
let tab =
($page.url.searchParams.get('tab') as
| 'users'
| 'slack'
| 'premium'
| 'export_delete'
| 'webhook') ?? 'users'
| 'webhook'
| 'deploy_to') ?? 'users'
// function getDropDownItems(username: string): DropdownItem[] {
// return [
@@ -112,6 +121,7 @@
plan = settings.plan
customer_id = settings.customer_id
initialPath = scriptPath
workspaceToDeployTo = settings.deploy_to
webhook = settings.webhook
}
@@ -197,10 +207,18 @@
<PageHeader title="Workspace Settings of {$workspaceStore}" />
<div class="overflow-x-auto scrollbar-hidden">
<Tabs bind:selected={tab}>
<Tabs
bind:selected={tab}
on:selected={() => {
setQueryWithoutLoad($page.url, [{ key: 'tab', value: tab }], 0)
}}
>
<Tab size="md" value="users">
<div class="flex gap-2 items-center my-1"> Users & Invites </div>
</Tab>
<Tab size="md" value="deploy_to">
<div class="flex gap-2 items-center my-1"> Dev/Staging/Prod</div>
</Tab>
{#if WORKSPACE_SHOW_SLACK_CMD}
<Tab size="md" value="slack">
<div class="flex gap-2 items-center my-1"> Slack Command </div>
@@ -448,6 +466,22 @@
{#if !allowedAutoDomain}
<div class="text-red-400 text-sm mb-2">{domain} domain not allowed for auto-invite</div>
{/if}
{:else if tab == 'deploy_to'}
<div class="my-2"
><Alert type="info" title="Link this workspace to another Staging/Prod workspace"
>Linking this workspace to another staging/prod workspace unlock the Web-based flow to
deploy to another workspace.</Alert
></div
>
{#if $enterpriseLicense}
<DeployToSetting bind:workspaceToDeployTo />
{:else}
<div class="my-2"
><Alert type="error" title="Enterprise license required"
>Deploy to staging/prod from the web UI is only available with an enterprise license</Alert
></div
>
{/if}
{:else if tab == 'premium'}
{#if isCloudHosted()}
<div class="mt-4" />
@@ -639,9 +673,11 @@
</Button>
</div>
{:else}
<Button size="sm" endIcon={{ icon: faSlack }} href="/api/oauth/connect_slack">
Connect to Slack
</Button>
<div class="flex">
<Button size="sm" endIcon={{ icon: faSlack }} href="/api/oauth/connect_slack">
Connect to Slack
</Button>
</div>
{/if}
<h3 class="mt-5 text-gray-700"
>Script or flow to run on /windmill command <Tooltip>

View File

@@ -30,16 +30,16 @@
},
"../frontend": {
"name": "windmill",
"version": "1.109.0",
"version": "1.114.2",
"license": "AGPL-3.0",
"dependencies": {
"@aws-crypto/sha256-js": "^4.0.0",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@leeoniya/ufuzzy": "^1.0.6",
"@leeoniya/ufuzzy": "^1.0.7",
"@popperjs/core": "^2.11.6",
"@redocly/json-to-json-schema": "^0.0.1",
"@tanstack/svelte-table": "^8.9.1",
"@tanstack/svelte-table": "^8.9.2",
"ag-grid-svelte": "^0.1.4",
"chart.js": "^4.3.0",
"chartjs-adapter-date-fns": "^3.0.0",
@@ -50,8 +50,9 @@
"fast-equals": "^5.0.1",
"highlight.js": "^11.8.0",
"lodash": "^4.17.21",
"lucide-svelte": "^0.224.0",
"monaco-languageclient": "~6.0.1",
"lucide-svelte": "^0.242.0",
"monaco-languageclient": "~6.0.3",
"quill": "^1.3.7",
"svelte-autosize": "^1.0.1",
"svelte-chartjs": "^3.1.0",
"svelte-dnd-action": "^0.9.22",
@@ -60,33 +61,34 @@
"svelte-timezone-picker": "^2.0.3",
"tailwind-merge": "^1.12.0",
"vscode-ws-jsonrpc": "3.0.0",
"windmill-parser-wasm": "file:../backend/parsers/windmill-parser-wasm/pkg",
"y-monaco": "^0.1.4",
"y-websocket": "^1.5.0",
"yjs": "^13.6.1"
"yjs": "^13.6.2"
},
"devDependencies": {
"@playwright/test": "^1.34.3",
"@rgossiaux/svelte-headlessui": "^1.0.2",
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.18.0",
"@sveltejs/kit": "^1.20.1",
"@sveltejs/package": "^2.0.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/d3": "^7.4.0",
"@types/d3-zoom": "^3.0.2",
"@types/d3-zoom": "^3.0.3",
"@types/lodash": "^4.14.195",
"@types/node": "^20.2.5",
"@types/vscode": "~1.78.0",
"@types/node": "^20.3.0",
"@types/vscode": "~1.78.1",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.7",
"@zerodevx/svelte-toast": "^0.9.3",
"autoprefixer": "^10.4.13",
"cssnano": "^6.0.1",
"d3-dag": "^0.11.5",
"eslint": "^8.40.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-svelte3": "^4.0.0",
"monaco-editor-workers": "~0.37.0",
"monaco-editor-workers": "~0.38.0",
"ol": "^7.2.2",
"openapi-typescript-codegen": "^0.24.0",
"path-browserify": "^1.0.1",
@@ -101,23 +103,23 @@
"svelte": "^3.59.1",
"svelte-awesome": "^3.2.0",
"svelte-awesome-color-picker": "^2.4.3",
"svelte-check": "^3.3.2",
"svelte-check": "^3.4.3",
"svelte-highlight": "^7.3.0",
"svelte-multiselect": "^8.6.2",
"svelte-overlay": "^1.4.1",
"svelte-popperjs": "^1.3.2",
"svelte-preprocess": "^5.0.1",
"svelte-range-slider-pips": "^2.1.1",
"svelte-splitpanes": "^0.7.3",
"svelte-splitpanes": "^0.7.14",
"svelte2tsx": "^0.6.14",
"tailwindcss": "^3.3.2",
"tslib": "^2.5.2",
"typescript": "^5.0.4",
"typescript": "^5.1.3",
"vite": "^4.3.3",
"yootils": "^0.3.1"
},
"peerDependencies": {
"@sveltejs/kit": "^1.18.0",
"@sveltejs/kit": "^1.20.1",
"svelte": "^3.59.1"
}
},
@@ -4122,22 +4124,22 @@
"@aws-crypto/sha256-js": "^4.0.0",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@leeoniya/ufuzzy": "^1.0.6",
"@leeoniya/ufuzzy": "^1.0.7",
"@playwright/test": "^1.34.3",
"@popperjs/core": "^2.11.6",
"@redocly/json-to-json-schema": "^0.0.1",
"@rgossiaux/svelte-headlessui": "^1.0.2",
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.18.0",
"@sveltejs/kit": "^1.20.1",
"@sveltejs/package": "^2.0.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@tanstack/svelte-table": "^8.9.1",
"@tanstack/svelte-table": "^8.9.2",
"@types/d3": "^7.4.0",
"@types/d3-zoom": "^3.0.2",
"@types/d3-zoom": "^3.0.3",
"@types/lodash": "^4.14.195",
"@types/node": "^20.2.5",
"@types/vscode": "~1.78.0",
"@types/node": "^20.3.0",
"@types/vscode": "~1.78.1",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.7",
"@zerodevx/svelte-toast": "^0.9.3",
@@ -4150,16 +4152,16 @@
"d3-dag": "^0.11.5",
"d3-zoom": "^3.0.0",
"date-fns": "^2.30.0",
"eslint": "^8.40.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-svelte3": "^4.0.0",
"esm-env": "^1.0.0",
"fast-equals": "^5.0.1",
"highlight.js": "^11.8.0",
"lodash": "^4.17.21",
"lucide-svelte": "^0.224.0",
"monaco-editor-workers": "~0.37.0",
"monaco-languageclient": "~6.0.1",
"lucide-svelte": "^0.242.0",
"monaco-editor-workers": "~0.38.0",
"monaco-languageclient": "~6.0.3",
"ol": "^7.2.2",
"openapi-typescript-codegen": "^0.24.0",
"path-browserify": "^1.0.1",
@@ -4168,6 +4170,7 @@
"postcss-load-config": "^4.0.1",
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1",
"quill": "^1.3.7",
"simple-svelte-autocomplete": "^2.5.1",
"style-to-object": "^0.4.1",
"stylelint-config-recommended": "^12.0.0",
@@ -4176,7 +4179,7 @@
"svelte-awesome": "^3.2.0",
"svelte-awesome-color-picker": "^2.4.3",
"svelte-chartjs": "^3.1.0",
"svelte-check": "^3.3.2",
"svelte-check": "^3.4.3",
"svelte-dnd-action": "^0.9.22",
"svelte-highlight": "^7.3.0",
"svelte-multiselect": "^8.6.2",
@@ -4186,18 +4189,19 @@
"svelte-preprocess": "^5.0.1",
"svelte-range-slider-pips": "^2.1.1",
"svelte-select": "^5.6.1",
"svelte-splitpanes": "^0.7.3",
"svelte-splitpanes": "^0.7.14",
"svelte-timezone-picker": "^2.0.3",
"svelte2tsx": "^0.6.14",
"tailwind-merge": "^1.12.0",
"tailwindcss": "^3.3.2",
"tslib": "^2.5.2",
"typescript": "^5.0.4",
"typescript": "^5.1.3",
"vite": "^4.3.3",
"vscode-ws-jsonrpc": "3.0.0",
"windmill-parser-wasm": "file:../backend/parsers/windmill-parser-wasm/pkg",
"y-monaco": "^0.1.4",
"y-websocket": "^1.5.0",
"yjs": "^13.6.1",
"yjs": "^13.6.2",
"yootils": "^0.3.1"
}
},