Multiplayer deploy collision warning (#4533)
* 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 <guilhem@mbp-de-windmill.home> * 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 <guilhemlemouel@gmail.com> Co-authored-by: Guilhem <guilhem@mbp-de-windmill.home> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com> * 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 <faton.ramadani14@gmail.com> Co-authored-by: Guilhem <guilhemlemouel@gmail.com> Co-authored-by: Guilhem <guilhem@mbp-de-windmill.home> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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),
|
||||
@@ -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>,
|
||||
|
||||
@@ -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),
|
||||
@@ -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>,
|
||||
|
||||
@@ -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'
|
||||
@@ -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)
|
||||
@@ -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 {
|
||||
@@ -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 />
|
||||
@@ -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 }
|
||||
})
|
||||
@@ -1335,7 +1409,9 @@
|
||||
loading={loadingSave}
|
||||
size="xs"
|
||||
startIcon={{ icon: Save }}
|
||||
on:click={() => saveFlow()}
|
||||
on:click={async () => {
|
||||
await handleSaveFlow()
|
||||
}}
|
||||
dropdownItems={!newFlow ? dropdownItems : undefined}
|
||||
>
|
||||
Deploy
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
emptyString,
|
||||
encodeState,
|
||||
formatCron,
|
||||
orderedJsonStringify
|
||||
orderedJsonStringify,
|
||||
|
||||
type Value
|
||||
|
||||
} from '$lib/utils'
|
||||
import Path from './Path.svelte'
|
||||
import ScriptEditor from './ScriptEditor.svelte'
|
||||
@@ -63,6 +66,7 @@
|
||||
import CustomPopover from './CustomPopover.svelte'
|
||||
import Summary from './Summary.svelte'
|
||||
import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui'
|
||||
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
|
||||
import TriggersEditor from './triggers/TriggersEditor.svelte'
|
||||
import type { ScheduleTrigger, TriggerContext } from './triggers'
|
||||
|
||||
@@ -83,6 +87,12 @@
|
||||
export let customUi: ScriptBuilderWhitelabelCustomUi = {}
|
||||
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
|
||||
|
||||
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
|
||||
|
||||
|
||||
let metadataOpen =
|
||||
!neverShowMeta &&
|
||||
(showMeta ||
|
||||
@@ -241,7 +251,61 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function editScript(stay: boolean, deploymentMsg?: string): Promise<void> {
|
||||
async function handleEditScript(stay: boolean, deployMsg?: string): Promise<void>{
|
||||
// Fetch latest version and fetch entire script after if needed
|
||||
let actual_parent_hash = (await ScriptService.getScriptLatestVersion({
|
||||
workspace: $workspaceStore!,
|
||||
path: script.path,
|
||||
})).script_hash;
|
||||
|
||||
// Usually when we create new script, we put current hash as a parent_hash
|
||||
// But if we specify parent_hash that is already used, than we get error
|
||||
// In order to fix it we make sure that client's understanding of parent_hash
|
||||
// is aligns with understanding of backend.
|
||||
if (script.parent_hash == actual_parent_hash) {
|
||||
// Handle directly
|
||||
await editScript(stay, actual_parent_hash, deployMsg);
|
||||
} else {
|
||||
|
||||
// Fetch entire script, since we need it to show Diff
|
||||
await syncWithDeployed()
|
||||
|
||||
// Handle through confirmation modal
|
||||
confirmCallback = async () => {
|
||||
open = false
|
||||
await editScript(stay, actual_parent_hash, deployMsg);
|
||||
}
|
||||
// Open confirmation modal
|
||||
open = true
|
||||
}
|
||||
}
|
||||
|
||||
async function syncWithDeployed(){
|
||||
const latestScript = await ScriptService.getScriptByPath({
|
||||
workspace: $workspaceStore!,
|
||||
path: script.path,
|
||||
withStarredInfo: true
|
||||
});
|
||||
|
||||
deployedValue = {
|
||||
...latestScript,
|
||||
starred: undefined,
|
||||
workspace_id: undefined,
|
||||
archived: undefined,
|
||||
created_at: undefined,
|
||||
created_by: undefined,
|
||||
deleted: undefined,
|
||||
extra_perms: undefined,
|
||||
is_template: undefined,
|
||||
lock: undefined,
|
||||
lock_error_logs: undefined,
|
||||
parent_hashes: undefined,
|
||||
};
|
||||
|
||||
deployedBy = latestScript.created_by;
|
||||
}
|
||||
|
||||
async function editScript(stay: boolean, parentHash: string, deploymentMsg?: string, ): Promise<void> {
|
||||
loadingSave = true
|
||||
try {
|
||||
try {
|
||||
@@ -265,7 +329,7 @@
|
||||
summary: script.summary,
|
||||
description: script.description ?? '',
|
||||
content: script.content,
|
||||
parent_hash: script.parent_hash,
|
||||
parent_hash: parentHash,
|
||||
schema: script.schema,
|
||||
is_template: script.is_template,
|
||||
language: script.language,
|
||||
@@ -472,7 +536,7 @@
|
||||
{
|
||||
label: 'Deploy & Stay here',
|
||||
onClick: () => {
|
||||
editScript(true)
|
||||
handleEditScript(true)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -530,6 +594,14 @@
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
<slot />
|
||||
|
||||
<DeployOverrideConfirmationModal
|
||||
bind:deployedBy
|
||||
bind:confirmCallback
|
||||
bind:open
|
||||
{diffDrawer}
|
||||
bind:deployedValue
|
||||
currentValue={script}
|
||||
/>
|
||||
{#if !$userStore?.operator}
|
||||
<Drawer
|
||||
placement="right"
|
||||
@@ -1154,14 +1226,16 @@
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
on:click={async () => {
|
||||
if (!savedScript) {
|
||||
return
|
||||
}
|
||||
await syncWithDeployed()
|
||||
|
||||
diffDrawer?.openDrawer()
|
||||
diffDrawer?.setDiff({
|
||||
mode: 'normal',
|
||||
deployed: savedScript,
|
||||
deployed: deployedValue ?? savedScript,
|
||||
draft: savedScript['draft'],
|
||||
current: script
|
||||
})
|
||||
@@ -1206,7 +1280,7 @@
|
||||
size="xs"
|
||||
disabled={!fullyLoaded}
|
||||
startIcon={{ icon: Save }}
|
||||
on:click={() => editScript(false)}
|
||||
on:click={() => handleEditScript(false)}
|
||||
dropdownItems={computeDropdownItems(initialPath)}
|
||||
>
|
||||
Deploy
|
||||
@@ -1220,13 +1294,13 @@
|
||||
bind:this={msgInput}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
editScript(false, deploymentMsg)
|
||||
handleEditScript(false, deploymentMsg)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={() => editScript(false, deploymentMsg)}
|
||||
on:click={() => handleEditScript(false, deploymentMsg)}
|
||||
endIcon={{ icon: CornerDownLeft }}
|
||||
loading={loadingSave}
|
||||
>
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
cleanValueProperties,
|
||||
copyToClipboard,
|
||||
truncateRev,
|
||||
orderedJsonStringify
|
||||
orderedJsonStringify,
|
||||
type Value
|
||||
} from '../../../utils'
|
||||
import type {
|
||||
AppInput,
|
||||
@@ -85,6 +86,7 @@
|
||||
import Summary from '$lib/components/Summary.svelte'
|
||||
import ToggleEnable from '$lib/components/common/toggleButton-v2/ToggleEnable.svelte'
|
||||
import HideButton from './settingsPanel/HideButton.svelte'
|
||||
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
|
||||
|
||||
async function hash(message) {
|
||||
try {
|
||||
@@ -121,6 +123,11 @@
|
||||
export let rightPanelHidden: boolean = false
|
||||
export let bottomPanelHidden: boolean = false
|
||||
|
||||
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
|
||||
|
||||
const {
|
||||
app,
|
||||
summary,
|
||||
@@ -360,6 +367,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateApp(npath: string) {
|
||||
|
||||
// We have to make sure there is no updates when we clicked the button
|
||||
await compareVersions();
|
||||
|
||||
if (onLatest) {
|
||||
// Handle directly
|
||||
await updateApp(npath)
|
||||
} else {
|
||||
// There is onLatest, but we need more information while deploying
|
||||
// We need it to show diff
|
||||
// Handle through confirmation modal
|
||||
await syncWithDeployed();
|
||||
|
||||
confirmCallback = async () => {
|
||||
open = false
|
||||
await updateApp(npath)
|
||||
}
|
||||
// Open confirmation modal
|
||||
open = true
|
||||
}
|
||||
}
|
||||
|
||||
async function syncWithDeployed() {
|
||||
const deployedApp = await AppService.getAppByPath({
|
||||
workspace: $workspaceStore!,
|
||||
path: appPath,
|
||||
withStarredInfo: true
|
||||
})
|
||||
|
||||
deployedBy = deployedApp.created_by
|
||||
|
||||
// Strip off extra information
|
||||
deployedValue = {
|
||||
...deployedApp,
|
||||
starred: undefined,
|
||||
id: undefined,
|
||||
created_at: undefined,
|
||||
created_by: undefined,
|
||||
versions: undefined,
|
||||
extra_perms: undefined //
|
||||
}
|
||||
}
|
||||
|
||||
async function updateApp(npath: string) {
|
||||
await computeTriggerables()
|
||||
await AppService.updateApp({
|
||||
@@ -577,11 +628,11 @@
|
||||
if (version === undefined) {
|
||||
return
|
||||
}
|
||||
const appHistory = await AppService.getAppHistoryByPath({
|
||||
const appVersion = await AppService.getAppLatestVersion({
|
||||
workspace: $workspaceStore!,
|
||||
path: appPath
|
||||
})
|
||||
onLatest = version === appHistory[0]?.version
|
||||
onLatest = version === appVersion.version
|
||||
}
|
||||
$: saveDrawerOpen && compareVersions()
|
||||
|
||||
@@ -706,14 +757,18 @@
|
||||
{
|
||||
displayName: 'Diff',
|
||||
icon: DiffIcon,
|
||||
action: () => {
|
||||
action: async () => {
|
||||
if (!savedApp) {
|
||||
return
|
||||
}
|
||||
|
||||
// deployedValue should be syncronized when we open Diff
|
||||
await syncWithDeployed();
|
||||
|
||||
diffDrawer?.openDrawer()
|
||||
diffDrawer?.setDiff({
|
||||
mode: 'normal',
|
||||
deployed: savedApp,
|
||||
deployed: deployedValue ?? savedApp,
|
||||
draft: savedApp.draft,
|
||||
current: {
|
||||
summary: $summary,
|
||||
@@ -766,6 +821,20 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeployOverrideConfirmationModal
|
||||
bind:deployedBy
|
||||
bind:confirmCallback
|
||||
bind:open
|
||||
{diffDrawer}
|
||||
bind:deployedValue
|
||||
currentValue={{
|
||||
summary: $summary,
|
||||
value: $app,
|
||||
path: newPath || savedApp?.draft?.path || savedApp?.path,
|
||||
policy
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if appPath == ''}
|
||||
<Drawer bind:open={draftDrawerOpen} size="800px">
|
||||
<DrawerContent title="Initial draft save" on:close={() => closeDraftDrawer()}>
|
||||
@@ -823,8 +892,8 @@
|
||||
<Drawer bind:open={saveDrawerOpen} size="800px">
|
||||
<DrawerContent title="Deploy" on:close={() => closeSaveDrawer()}>
|
||||
{#if !onLatest}
|
||||
<Alert title="You're not on the latest app version" type="warning">
|
||||
By deploying, you may overwrite changes made by other users.
|
||||
<Alert title="You're not on the latest app version. " type="warning">
|
||||
By deploying, you may overwrite changes made by other users. Press 'Deploy' to see diff.
|
||||
</Alert>
|
||||
<div class="py-2" />
|
||||
{/if}
|
||||
@@ -880,15 +949,18 @@
|
||||
variant="border"
|
||||
color="light"
|
||||
disabled={!savedApp || savedApp.draft_only}
|
||||
on:click={() => {
|
||||
on:click={async () => {
|
||||
if (!savedApp) {
|
||||
return
|
||||
}
|
||||
// deployedValue should be syncronized when we open Diff
|
||||
await syncWithDeployed();
|
||||
|
||||
saveDrawerOpen = false
|
||||
diffDrawer?.openDrawer()
|
||||
diffDrawer?.setDiff({
|
||||
mode: 'normal',
|
||||
deployed: savedApp,
|
||||
deployed: deployedValue ?? savedApp,
|
||||
draft: savedApp.draft,
|
||||
current: {
|
||||
summary: $summary,
|
||||
@@ -902,7 +974,7 @@
|
||||
if (appPath == '') {
|
||||
createApp(newPath)
|
||||
} else {
|
||||
updateApp(newPath)
|
||||
handleUpdateApp(newPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -921,7 +993,7 @@
|
||||
if (appPath == '') {
|
||||
createApp(newPath)
|
||||
} else {
|
||||
updateApp(newPath)
|
||||
handleUpdateApp(newPath)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import ConfirmationModal from './ConfirmationModal.svelte'
|
||||
import Button from '../button/Button.svelte'
|
||||
import type DiffDrawer from '$lib/components/DiffDrawer.svelte'
|
||||
import { type Value } from '$lib/utils'
|
||||
|
||||
export let deployedValue: Value | undefined = undefined
|
||||
export let currentValue: Value | undefined = undefined
|
||||
export let diffDrawer: DiffDrawer | undefined = undefined
|
||||
export let confirmCallback: () => void
|
||||
export let deployedBy : string | undefined = undefined
|
||||
export let open = false
|
||||
</script>
|
||||
|
||||
<ConfirmationModal
|
||||
{open}
|
||||
title={"New version deployed by " + deployedBy}
|
||||
confirmationText="Override"
|
||||
on:canceled={() => {
|
||||
open = false
|
||||
}}
|
||||
on:confirmed={() => confirmCallback()}
|
||||
>
|
||||
<div class="flex flex-col w-full space-y-4">
|
||||
<span>There was deployed new version while you were editing this one.</span>
|
||||
{#if diffDrawer}
|
||||
<Button
|
||||
wrapperClasses="self-start"
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
if (!deployedValue || !currentValue) {
|
||||
return
|
||||
}
|
||||
open = false
|
||||
diffDrawer?.openDrawer()
|
||||
diffDrawer?.setDiff({
|
||||
mode: 'simple',
|
||||
original: deployedValue,
|
||||
current: currentValue,
|
||||
title: 'Deployed <> Current',
|
||||
button: {
|
||||
text: 'Override anyway',
|
||||
onClick: () => confirmCallback()
|
||||
}
|
||||
})
|
||||
}}
|
||||
>Show diff
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
@@ -15,6 +15,7 @@
|
||||
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import type { ScheduleTrigger } from '$lib/components/triggers'
|
||||
|
||||
let version: undefined | number = undefined;
|
||||
let nodraft = $page.url.searchParams.get('nodraft')
|
||||
const initialState = nodraft ? undefined : localStorage.getItem(`flow-${$page.params.path}`)
|
||||
let stateLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined
|
||||
@@ -65,6 +66,13 @@
|
||||
let flow: Flow
|
||||
let statePath = stateLoadedFromUrl?.path
|
||||
if (stateLoadedFromUrl != undefined && statePath == $page.params.path) {
|
||||
// Currently there is no way to get version of flow with flow.
|
||||
// So we have to request it here
|
||||
version = (await FlowService.getFlowLatestVersion({
|
||||
workspace: $workspaceStore!,
|
||||
path: statePath
|
||||
})).id;
|
||||
|
||||
savedFlow = await FlowService.getFlowByPathWithDraft({
|
||||
workspace: $workspaceStore!,
|
||||
path: statePath
|
||||
@@ -103,6 +111,13 @@
|
||||
])
|
||||
}
|
||||
} else {
|
||||
// Currently there is no way to get version of flow with flow.
|
||||
// So we have to request it here
|
||||
version = (await FlowService.getFlowLatestVersion({
|
||||
workspace: $workspaceStore!,
|
||||
path: $page.params.path
|
||||
})).id;
|
||||
|
||||
const flowWithDraft = await FlowService.getFlowByPathWithDraft({
|
||||
workspace: $workspaceStore!,
|
||||
path: $page.params.path
|
||||
@@ -230,6 +245,7 @@
|
||||
bind:savedFlow
|
||||
{diffDrawer}
|
||||
{savedPrimarySchedule}
|
||||
bind:version
|
||||
>
|
||||
<UnsavedConfirmationModal {diffDrawer} savedValue={savedFlow} modifiedValue={$flowStore} />
|
||||
</FlowBuilder>
|
||||
|
||||
Reference in New Issue
Block a user