mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
refactor(drafts): remove unused list_drafts endpoint and dev inspector
This commit is contained in:
-67
@@ -1,67 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT path,\n typ as \"typ!: UserDraftItemKind\",\n created_at as \"saved_at!\"\n FROM draft\n WHERE workspace_id = $1 AND email = $2\n ORDER BY created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "typ!: UserDraftItemKind",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "draft_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"flow",
|
||||
"app",
|
||||
"raw_app",
|
||||
"resource",
|
||||
"variable",
|
||||
"trigger_schedule",
|
||||
"trigger_webhook",
|
||||
"trigger_default_email",
|
||||
"trigger_email",
|
||||
"trigger_http",
|
||||
"trigger_websocket",
|
||||
"trigger_postgres",
|
||||
"trigger_kafka",
|
||||
"trigger_nats",
|
||||
"trigger_mqtt",
|
||||
"trigger_sqs",
|
||||
"trigger_gcp",
|
||||
"trigger_azure",
|
||||
"trigger_poll",
|
||||
"trigger_cli",
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "saved_at!",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6f857451605a0453c82b4faada248689c02edde28433eb388092d30c94922e76"
|
||||
}
|
||||
@@ -7910,33 +7910,6 @@ paths:
|
||||
format: date-time
|
||||
required: [status, current_timestamp]
|
||||
|
||||
/w/{workspace}/drafts/list_drafts:
|
||||
get:
|
||||
summary: list the current user's drafts in a workspace (metadata only)
|
||||
operationId: listDrafts
|
||||
tags:
|
||||
- draft
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: list of (path, kind, saved_at) tuples
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
typ:
|
||||
$ref: "#/components/schemas/UserDraftItemKind"
|
||||
saved_at:
|
||||
type: string
|
||||
format: date-time
|
||||
required: [path, typ, saved_at]
|
||||
|
||||
/w/{workspace}/drafts/get_draft/{kind}/{path}:
|
||||
get:
|
||||
summary: fetch the current user's draft at a path
|
||||
|
||||
@@ -24,7 +24,6 @@ pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
.route("/get/{kind}/{*path}", get(get_draft_for_user))
|
||||
.route("/save_draft/{kind}/{*path}", post(save_draft))
|
||||
.route("/list_drafts", get(list_drafts))
|
||||
.route("/get_draft/{kind}/{*path}", get(get_draft))
|
||||
}
|
||||
|
||||
@@ -165,36 +164,6 @@ async fn save_draft(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct DraftListItem {
|
||||
pub path: String,
|
||||
pub typ: UserDraftItemKind,
|
||||
pub saved_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// Metadata-only listing of the current user's drafts in a workspace.
|
||||
/// Excludes the legacy NULL-email rows. Ordered most-recently-saved first.
|
||||
async fn list_drafts(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> Result<Json<Vec<DraftListItem>>> {
|
||||
let rows = sqlx::query_as!(
|
||||
DraftListItem,
|
||||
r#"SELECT path,
|
||||
typ as "typ!: UserDraftItemKind",
|
||||
created_at as "saved_at!"
|
||||
FROM draft
|
||||
WHERE workspace_id = $1 AND email = $2
|
||||
ORDER BY created_at DESC"#,
|
||||
&w_id,
|
||||
&authed.email,
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct OwnDraft {
|
||||
pub value: sqlx::types::Json<Box<serde_json::value::RawValue>>,
|
||||
|
||||
@@ -151,13 +151,7 @@ vi.mock('$lib/gen', async () => {
|
||||
)
|
||||
}
|
||||
return { value: entry.value, saved_at: entry.saved_at }
|
||||
}),
|
||||
listDrafts: vi.fn(async () =>
|
||||
[...draftDb.store.entries()].map(([k, v]) => {
|
||||
const slash = k.indexOf('/')
|
||||
return { typ: k.slice(0, slash), path: k.slice(slash + 1), saved_at: v.saved_at }
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -179,7 +173,7 @@ import {
|
||||
setOpenPreviewHandler
|
||||
} from './core'
|
||||
import { UserDraft, __resetUserDraftForTesting } from '$lib/userDraft.svelte'
|
||||
import { clearGlobalDrafts } from './userDraftAdapter'
|
||||
import { clearEphemeralSecretVariableDraftValues } from './userDraftAdapter'
|
||||
import { bundleRawAppDraft } from './rawAppBundlerBridge'
|
||||
import {
|
||||
AppService,
|
||||
@@ -273,7 +267,7 @@ describe('global AI tools', () => {
|
||||
__resetUserDraftForTesting()
|
||||
localStorage.clear()
|
||||
draftDb.reset()
|
||||
await clearGlobalDrafts(WORKSPACE)
|
||||
clearEphemeralSecretVariableDraftValues(WORKSPACE)
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
*
|
||||
* When the mode is ready to ship to everyone, replace every call to
|
||||
* `isGlobalAiEnabled()` with `true` and delete this file. The references are
|
||||
* intentionally narrow (chat mode visibility, custom prompt settings, the
|
||||
* `change_mode` tool enum, and the `/global_drafts` dev route) so the rip-out
|
||||
* is a small grep.
|
||||
* intentionally narrow (chat mode visibility, custom prompt settings, and the
|
||||
* `change_mode` tool enum) so the rip-out is a small grep.
|
||||
*/
|
||||
const STORAGE_KEY = 'wm_dev_global_ai'
|
||||
|
||||
|
||||
@@ -37,24 +37,6 @@ const TRIGGER_KIND_BY_DRAFT_KIND = Object.fromEntries(
|
||||
])
|
||||
) as Partial<Record<UserDraftItemKind, TriggerKind>>
|
||||
|
||||
const GLOBAL_DRAFT_KINDS = [
|
||||
'script',
|
||||
'flow',
|
||||
'raw_app',
|
||||
'trigger_schedule',
|
||||
'trigger_http',
|
||||
'trigger_websocket',
|
||||
'trigger_kafka',
|
||||
'trigger_nats',
|
||||
'trigger_postgres',
|
||||
'trigger_mqtt',
|
||||
'trigger_sqs',
|
||||
'trigger_gcp',
|
||||
'trigger_azure',
|
||||
'resource',
|
||||
'variable'
|
||||
] as const satisfies UserDraftItemKind[]
|
||||
|
||||
const secretVariableDraftValues = new Map<string, Map<string, string>>()
|
||||
|
||||
function clone<T>(value: T): T {
|
||||
@@ -103,7 +85,7 @@ export function clearEphemeralSecretVariableDraftValue(workspace: string, path:
|
||||
if (workspaceValues.size === 0) secretVariableDraftValues.delete(workspace)
|
||||
}
|
||||
|
||||
function clearEphemeralSecretVariableDraftValues(workspace: string): void {
|
||||
export function clearEphemeralSecretVariableDraftValues(workspace: string): void {
|
||||
secretVariableDraftValues.delete(workspace)
|
||||
}
|
||||
|
||||
@@ -569,24 +551,3 @@ export async function deleteGlobalDraft(
|
||||
})
|
||||
if (type === 'variable') clearEphemeralSecretVariableDraftValue(workspace, storagePath)
|
||||
}
|
||||
|
||||
export async function clearGlobalDrafts(workspace: string): Promise<void> {
|
||||
// `UserDraft.list` only sees in-tab mounted entries post #9351, so
|
||||
// enumerate the user's persisted drafts from the DB instead and delete
|
||||
// each global-mode kind.
|
||||
const kinds = new Set<UserDraftItemKind>(GLOBAL_DRAFT_KINDS)
|
||||
const drafts = await DraftService.listDrafts({ workspace })
|
||||
for (const draft of drafts) {
|
||||
if (!kinds.has(draft.typ)) continue
|
||||
await UserDraftDbSyncer.save({
|
||||
workspace,
|
||||
itemKind: draft.typ,
|
||||
path: draft.path,
|
||||
value: null,
|
||||
immediate: true,
|
||||
force: true,
|
||||
throwOnError: true
|
||||
})
|
||||
}
|
||||
clearEphemeralSecretVariableDraftValues(workspace)
|
||||
}
|
||||
|
||||
@@ -480,8 +480,8 @@ export const UserDraft = {
|
||||
/**
|
||||
* List currently-mounted live entries for `workspace`. Without the
|
||||
* localStorage layer, "list" is meaningful only for in-tab entries —
|
||||
* for a workspace-wide view across sessions, call
|
||||
* `DraftService.listDrafts` instead.
|
||||
* for a workspace-wide view across sessions, use the deployed list
|
||||
* endpoints with `includeDraftOnly` (which flag the caller's drafts).
|
||||
*/
|
||||
list<V = unknown>(opts?: UserDraftListOptions): UserDraftEntry<V>[] {
|
||||
const ws = resolveWorkspace(opts)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export function load() {
|
||||
return {
|
||||
stuff: { title: 'Global AI drafts' }
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import { DraftService, type ListDraftsResponse } from '$lib/gen'
|
||||
import { isGlobalAiEnabled } from '$lib/components/copilot/chat/global/gate'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Trash2 } from 'lucide-svelte'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
type DraftRow = ListDraftsResponse[number]
|
||||
|
||||
let enabled = $state(false)
|
||||
let drafts = $state<DraftRow[]>([])
|
||||
|
||||
async function loadDrafts() {
|
||||
drafts = $workspaceStore ? await DraftService.listDrafts({ workspace: $workspaceStore }) : []
|
||||
}
|
||||
|
||||
// `value: null` is the canonical delete; `force` skips the conflict check
|
||||
// (this is a debug tool, the user just wants the row gone).
|
||||
async function removeDraft(ws: string, row: DraftRow): Promise<void> {
|
||||
await DraftService.saveDraft({
|
||||
workspace: ws,
|
||||
kind: row.typ,
|
||||
path: row.path,
|
||||
requestBody: { value: null, force: true }
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Dev-only route. Bounce to home when the global mode gate is closed.
|
||||
enabled = isGlobalAiEnabled()
|
||||
if (!enabled) {
|
||||
goto('/')
|
||||
return
|
||||
}
|
||||
|
||||
void loadDrafts()
|
||||
// Drafts live in the DB; poll so the inspector reflects writes from this
|
||||
// and other tabs (same-tab saves emit no DOM event).
|
||||
const interval = window.setInterval(() => void loadDrafts(), 1000)
|
||||
return () => window.clearInterval(interval)
|
||||
})
|
||||
|
||||
function draftKey(row: DraftRow): string {
|
||||
return `${row.typ}:${row.path}`
|
||||
}
|
||||
|
||||
async function deleteDraft(row: DraftRow) {
|
||||
if (!$workspaceStore) return
|
||||
await removeDraft($workspaceStore, row)
|
||||
await loadDrafts()
|
||||
}
|
||||
|
||||
async function clearAll() {
|
||||
const ws = $workspaceStore
|
||||
if (!ws) return
|
||||
await Promise.all(drafts.map((row) => removeDraft(ws, row)))
|
||||
await loadDrafts()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if enabled}
|
||||
<div class="p-6 max-w-5xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Global drafts</h1>
|
||||
<p class="text-sm text-tertiary">
|
||||
Dev-only inspector for the current user's DB-backed drafts.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="default"
|
||||
startIcon={{ icon: Trash2 }}
|
||||
disabled={drafts.length === 0}
|
||||
onclick={clearAll}
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if drafts.length === 0}
|
||||
<p class="text-sm text-tertiary">No drafts.</p>
|
||||
{:else}
|
||||
<ul class="space-y-2">
|
||||
{#each drafts as draft (draftKey(draft))}
|
||||
<li class="border rounded p-3 flex items-center justify-between gap-2">
|
||||
<div class="font-mono text-sm min-w-0 truncate">
|
||||
<span class="font-semibold">{draft.typ}</span>
|
||||
<span class="text-tertiary">·</span>
|
||||
<span>{draft.path}</span>
|
||||
<span class="text-tertiary text-xs ml-2">{draft.saved_at}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="default"
|
||||
startIcon={{ icon: Trash2 }}
|
||||
iconOnly
|
||||
onclick={() => deleteDraft(draft)}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user