Skip to content

Commit

Permalink
Multiplayer deploy collision warning (#4533)
Browse files Browse the repository at this point in the history
* feat: Multiplayer deploy collision warning

Show warning with diff viewer if 2+ users try to deploy flow/script/app at the same time

In other words, if user A and user B started editing same flow/script/app and user A deployed it,
user B will get warning if they try to deploy as well.

* Add warning for scripts

* Add `deployedBy` to Apps

* Format

* Fix advanced deployment on scripts

* Write comments and cleanup

* feat(frontend): unify all triggers UX and simplify flow settings (#4259)

* feat(frontend): added list of triggers in the flow graph

* feat(frontend): added list of triggers in the flow graph

* feat(frontend): clean up

* feat(frontend): improve UX

* feat(frontend): triggers

* feat(frontend): triggers

* feat(frontend): done

* feat(frontend): fix trigger when position when a preprocessor is presetn

* Glm/rework flow settings v2 (#4497)

* fat(frontend): simplify flow settings menu

* improve scroll

* changing mute toggle

* Add advanced settings badge

* Add nord theme colors

* Add bage for advanced options

* fix minor issue

* fix minor issue

* Add triggers menu to flow settings

* Add quick trigger access

* remove triggers in flow settings

* fix minor issue

* Move triggers settings to flow right panel

* polishing

* fix unset store

* remove save up to for triggers

* fix padding

* reset default tag color

* remove custom select component

* revert path change

* revert section modif

* Revert unused feature

---------

Co-authored-by: Guilhem <[email protected]>

* Connect top bar cron to schedules settings

* Turn copilot into node

* fix copilot placement

* remove useless import

* fix center copilot

* fix binding

* remove copilot on top of preprocessor

* render copilot node on condition

* quickfix

* remove copilot node

* fix minor issues

* fix route count update

* fix schedule sync

* harmonize colors

* fix alignment and add edges

* recenter node summary

* fix schedules sync

* Add id title

* all

* all

* all

* iteration

* all

* all

* done

* fix

* more fixes

---------

Co-authored-by: Guilhem <[email protected]>
Co-authored-by: Guilhem <[email protected]>
Co-authored-by: Ruben Fiszel <[email protected]>
Co-authored-by: Ruben Fiszel <[email protected]>

* Update ScriptBuilder.svelte

* Remove `onMount` for flows

* Use version instead of last_updated_at in flows

* Use only versions for apps

* Fetch latest data in Diffs in Apps

* Optimize with (script/flow/app)GetLatestVersion

Create several new endpoints, that returns just latest version without rest of the history

* Sync Diffs with deployed

* Improve Diff's data

* Use `getFlowLatestVersion`

---------

Co-authored-by: Faton Ramadani <[email protected]>
Co-authored-by: Guilhem <[email protected]>
Co-authored-by: Guilhem <[email protected]>
Co-authored-by: Ruben Fiszel <[email protected]>
Co-authored-by: Ruben Fiszel <[email protected]>
  • Loading branch information
6 people authored Oct 17, 2024
1 parent 658a934 commit ce80d6b
Show file tree
Hide file tree
Showing 9 changed files with 445 additions and 27 deletions.
51 changes: 51 additions & 0 deletions backend/windmill-api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4092,6 +4092,23 @@ paths:
items:
$ref: "#/components/schemas/ScriptHistory"

/w/{workspace}/scripts/get_latest_version/{path}:
get:
summary: get scripts's latest version (hash)
operationId: getScriptLatestVersion
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
tags:
- script
responses:
"200":
description: Script version/hash
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptHistory"

/w/{workspace}/scripts/history_update/h/{hash}/p/{path}:
post:
summary: update history of a script
Expand Down Expand Up @@ -4580,6 +4597,23 @@ paths:
items:
$ref: "#/components/schemas/FlowVersion"

/w/{workspace}/flows/get_latest_version/{path}:
get:
summary: get flow's latest version
operationId: getFlowLatestVersion
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
tags:
- flow
responses:
"200":
description: Flow version
content:
application/json:
schema:
$ref: "#/components/schemas/FlowVersion"

/w/{workspace}/flows/get/v/{version}/p/{path}:
get:
summary: get flow version
Expand Down Expand Up @@ -5134,6 +5168,23 @@ paths:
items:
$ref: "#/components/schemas/AppHistory"

/w/{workspace}/apps/get_latest_version/{path}:
get:
summary: get apps's latest version
operationId: getAppLatestVersion
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
tags:
- app
responses:
"200":
description: App version
content:
application/json:
schema:
$ref: "#/components/schemas/AppHistory"

/w/{workspace}/apps/history_update/a/{id}/v/{version}:
post:
summary: update app history
Expand Down
25 changes: 25 additions & 0 deletions backend/windmill-api/src/apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub fn workspaced_service() -> Router {
.route("/delete/*path", delete(delete_app))
.route("/create", post(create_app))
.route("/history/p/*path", get(get_app_history))
.route("/get_latest_version/*path", get(get_latest_version))
.route("/history_update/a/:id/v/:version", post(update_app_history))
}

Expand Down Expand Up @@ -427,6 +428,30 @@ async fn get_app_history(
return Ok(Json(result));
}

async fn get_latest_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<AppHistory> {
let mut tx = user_db.begin(&authed).await?;
let row = sqlx::query!(
"SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg
FROM app a LEFT JOIN app_version av ON a.id = av.app_id LEFT JOIN deployment_metadata dm ON av.id = dm.app_version
WHERE a.workspace_id = $1 AND a.path = $2
ORDER BY created_at DESC",
w_id,
path.to_path(),
).fetch_one(&mut *tx).await?;
tx.commit().await?;

let result = AppHistory {
app_id: row.app_id,
version: row.version_id,
deployment_msg: row.deployment_msg,
};
return Ok(Json(result));
}

async fn update_app_history(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Expand Down
25 changes: 25 additions & 0 deletions backend/windmill-api/src/flows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub fn workspaced_service() -> Router {
.route("/exists/*path", get(exists_flow_by_path))
.route("/list_paths", get(list_paths))
.route("/history/p/*path", get(get_flow_history))
.route("/get_latest_version/*path", get(get_latest_version))
.route(
"/history_update/v/:version/p/*path",
post(update_flow_history),
Expand Down Expand Up @@ -538,6 +539,30 @@ async fn get_flow_history(
Ok(Json(flows))
}

async fn get_latest_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<FlowVersion> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;

let version = sqlx::query_as!(
FlowVersion,
"SELECT flow_version.id, flow_version.created_at, deployment_metadata.deployment_msg FROM flow_version
LEFT JOIN deployment_metadata ON flow_version.id = deployment_metadata.flow_version
WHERE flow_version.path = $1 AND flow_version.workspace_id = $2
ORDER BY flow_version.created_at DESC",
path,
w_id
)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;

Ok(Json(version))
}

async fn get_flow_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Expand Down
26 changes: 26 additions & 0 deletions backend/windmill-api/src/scripts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ pub fn workspaced_service() -> Router {
post(toggle_workspace_error_handler),
)
.route("/history/p/*path", get(get_script_history))
.route("/get_latest_version/*path", get(get_latest_version))
.route(
"/history_update/h/:hash/p/*path",
post(update_script_history),
Expand Down Expand Up @@ -948,6 +949,31 @@ async fn get_script_history(
return Ok(Json(result));
}

async fn get_latest_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<ScriptHistory> {
let mut tx = user_db.begin(&authed).await?;
let row = sqlx::query!(
"SELECT s.hash as hash, dm.deployment_msg as deployment_msg
FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash
WHERE s.workspace_id = $1 AND s.path = $2
ORDER by created_at DESC",
w_id,
path.to_path(),
)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;

let result = ScriptHistory {
script_hash: ScriptHash(row.hash),
deployment_msg: row.deployment_msg, //
};
return Ok(Json(result));
}

async fn update_script_history(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Expand Down
90 changes: 83 additions & 7 deletions frontend/src/lib/components/FlowBuilder.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
encodeState,
formatCron,
orderedJsonStringify,
sleep
sleep,
type Value
} from '$lib/utils'
import { sendUserToast } from '$lib/toast'
import { Drawer } from '$lib/components/common'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
import { setContext, tick, type ComponentType } from 'svelte'
import { writable, type Writable } from 'svelte/store'
Expand Down Expand Up @@ -102,9 +104,29 @@
export let disableAi: boolean = false
export let disabledFlowInputs = false
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
export let version: number | undefined = undefined
// Used by multiplayer deploy collision warning
let deployedValue: Value | undefined = undefined // Value to diff against
let deployedBy: string | undefined = undefined // Author
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
let open: boolean = false // Is confirmation modal open
$: setContext('customUi', customUi)
let onLatest = true
async function compareVersions() {
if (version === undefined) {
return
}
const flowVersion = await FlowService.getFlowLatestVersion({
workspace: $workspaceStore!,
path: $pathStore
})
onLatest = version === flowVersion.id
}
const dispatch = createEventDispatcher()
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule)
Expand Down Expand Up @@ -253,6 +275,46 @@
)
}
async function handleSaveFlow(deploymentMsg?: string) {
await compareVersions();
if (onLatest) {
// Handle directly
await saveFlow(deploymentMsg)
} else {
// We need it for diff
await syncWithDeployed()
// Handle through confirmation modal
confirmCallback = async () => {
await saveFlow(deploymentMsg)
}
// Open confirmation modal
open = true
}
}
async function syncWithDeployed(){
const flow = await FlowService.getFlowByPath({
workspace: $workspaceStore!,
path: $pathStore,
withStarredInfo: true
})
deployedValue = {
...flow,
starred: undefined,
id: undefined,
edited_at: undefined,
edited_by: undefined,
workspace_id: undefined,
archived: undefined,
same_worker: undefined,
visible_to_runner_only: undefined,
ws_error_handler_muted: undefined,
}
deployedBy = flow.edited_by
}
async function saveFlow(deploymentMsg?: string): Promise<void> {
loadingSave = true
try {
Expand Down Expand Up @@ -1122,6 +1184,15 @@

<slot />

<DeployOverrideConfirmationModal
bind:deployedBy
bind:confirmCallback
bind:open
{diffDrawer}
bind:deployedValue
currentValue={$flowStore}
/>

{#key renderCount}
{#if !$userStore?.operator}
<FlowCopilotDrawer {getHubCompletions} {genFlow} bind:flowCopilotMode />
Expand Down Expand Up @@ -1286,14 +1357,17 @@
color="light"
variant="border"
size="xs"
on:click={() => {
on:click={async () => {
if (!savedFlow) {
return
}

await syncWithDeployed()

diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: savedFlow,
deployed: deployedValue ?? savedFlow,
draft: savedFlow['draft'],
current: { ...$flowStore, path: $pathStore }
})
Expand Down Expand Up @@ -1335,7 +1409,9 @@
loading={loadingSave}
size="xs"
startIcon={{ icon: Save }}
on:click={() => saveFlow()}
on:click={async () => {
await handleSaveFlow()
}}
dropdownItems={!newFlow ? dropdownItems : undefined}
>
Deploy
Expand All @@ -1346,16 +1422,16 @@
type="text"
placeholder="Deployment message"
bind:value={deploymentMsg}
on:keydown={(e) => {
on:keydown={async (e) => {
if (e.key === 'Enter') {
saveFlow(deploymentMsg)
await handleSaveFlow(deploymentMsg)
}
}}
bind:this={msgInput}
/>
<Button
size="xs"
on:click={() => saveFlow(deploymentMsg)}
on:click={async () => await handleSaveFlow(deploymentMsg)}
endIcon={{ icon: CornerDownLeft }}
loading={loadingSave}
>
Expand Down
Loading

0 comments on commit ce80d6b

Please sign in to comment.