From f23ed60ace0c98676c4e2c92ea994b0058fb2f37 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Thu, 5 Mar 2026 21:24:02 -0800 Subject: [PATCH 1/3] chore: remove legacy migration step --- .../pkg/workspace/store/store.go | 226 ------------------ 1 file changed, 226 deletions(-) diff --git a/apps/workspace-engine/pkg/workspace/store/store.go b/apps/workspace-engine/pkg/workspace/store/store.go index e824574a9..5eec24113 100644 --- a/apps/workspace-engine/pkg/workspace/store/store.go +++ b/apps/workspace-engine/pkg/workspace/store/store.go @@ -10,8 +10,6 @@ import ( hybridrepo "workspace-engine/pkg/workspace/store/repository/hybrid" "workspace-engine/pkg/workspace/store/repository/memory" - "github.com/charmbracelet/log" - "github.com/google/uuid" "go.opentelemetry.io/otel/attribute" ) @@ -462,230 +460,6 @@ func (s *Store) Restore(ctx context.Context, changes persistence.Changes, setSta // Rebuild in-memory link stores from persisted link entities. s.repo.RestoreLinks() - // Migrate legacy changelog entities into the active repos. - // After Router().Apply(), the in-memory repo may contain entities - // loaded from changelog_entry records. When the DB backend is - // active, sync them so they are available through the DB-backed repos. - if setStatus != nil { - setStatus("Migrating legacy systems") - } - for _, sys := range s.repo.Systems().Items() { - if err := s.Systems.repo.Set(sys); err != nil { - log.Warn("Failed to migrate legacy system", - "system_id", sys.Id, "name", sys.Name, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy job agents") - } - for _, ja := range s.repo.JobAgents().Items() { - if err := s.JobAgents.repo.Set(ja); err != nil { - log.Warn("Failed to migrate legacy job agent", - "job_agent_id", ja.Id, "name", ja.Name, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy deployments") - } - for _, d := range s.repo.Deployments().Items() { - if err := s.Deployments.repo.Set(d); err != nil { - log.Warn("Failed to migrate legacy deployment", - "deployment_id", d.Id, "name", d.Name, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy environments") - } - for _, env := range s.repo.Environments().Items() { - if err := s.Environments.repo.Set(env); err != nil { - log.Warn("Failed to migrate legacy environment", - "environment_id", env.Id, "name", env.Name, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy deployment versions") - } - for _, v := range s.repo.DeploymentVersions().Items() { - if err := s.DeploymentVersions.repo.Set(v); err != nil { - log.Warn("Failed to migrate legacy deployment version", - "version_id", v.Id, "name", v.Name, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy resource providers") - } - for _, rp := range s.repo.ResourceProviders().Items() { - if err := s.ResourceProviders.repo.Set(rp); err != nil { - log.Warn("Failed to migrate legacy resource provider", - "resource_provider_id", rp.Id, "name", rp.Name, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy resources") - } - for _, r := range s.repo.Resources().Items() { - if err := s.Resources.repo.Set(r); err != nil { - log.Warn("Failed to migrate legacy resource", - "resource_id", r.Id, "name", r.Name, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy policies") - } - for _, pol := range s.repo.Policies().Items() { - if err := s.Policies.repo.Set(pol); err != nil { - log.Warn("Failed to migrate legacy policy", - "policy_id", pol.Id, "name", pol.Name, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy policy skips") - } - for _, ps := range s.repo.PolicySkips().Items() { - if err := s.PolicySkips.repo.Set(ps); err != nil { - log.Warn("Failed to migrate legacy policy skip", - "policy_skip_id", ps.Id, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy user approval records") - } - for _, uar := range s.repo.UserApprovalRecords().Items() { - if err := s.UserApprovalRecords.repo.Set(uar); err != nil { - log.Warn("Failed to migrate legacy user approval record", - "key", uar.Key(), "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy deployment variables") - } - for _, dv := range s.repo.DeploymentVariables().Items() { - if err := s.DeploymentVariables.repo.Set(dv); err != nil { - log.Warn("Failed to migrate legacy deployment variable", - "id", dv.Id, "key", dv.Key, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy deployment variable values") - } - for _, dvv := range s.repo.DeploymentVariableValues().Items() { - if err := s.DeploymentVariableValues.repo.Set(dvv); err != nil { - log.Warn("Failed to migrate legacy deployment variable value", - "id", dvv.Id, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy workflows") - } - for _, wf := range s.repo.Workflows().Items() { - if err := s.Workflows.repo.Set(wf); err != nil { - log.Warn("Failed to migrate legacy workflow", - "id", wf.Id, "name", wf.Name, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy workflow job templates") - } - for _, wjt := range s.repo.WorkflowJobTemplates().Items() { - if err := s.WorkflowJobTemplates.repo.Set(wjt); err != nil { - log.Warn("Failed to migrate legacy workflow job template", - "id", wjt.Id, "name", wjt.Name, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy workflow runs") - } - for _, wr := range s.repo.WorkflowRuns().Items() { - if err := s.WorkflowRuns.repo.Set(wr); err != nil { - log.Warn("Failed to migrate legacy workflow run", - "id", wr.Id, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy workflow jobs") - } - for _, wj := range s.repo.WorkflowJobs().Items() { - if err := s.WorkflowJobs.repo.Set(wj); err != nil { - log.Warn("Failed to migrate legacy workflow job", - "id", wj.Id, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy resource variables") - } - for _, rv := range s.repo.ResourceVariables().Items() { - if err := s.ResourceVariables.repo.Set(rv); err != nil { - log.Warn("Failed to migrate legacy resource variable", - "key", rv.ID(), "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy relationship rules") - } - for _, rr := range s.repo.RelationshipRules.Items() { - if err := s.Relationships.repo.Set(rr); err != nil { - log.Warn("Failed to migrate legacy relationship rule", - "id", rr.Id, "name", rr.Name, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy releases") - } - for _, rel := range s.repo.Releases().Items() { - if rel.Id == uuid.Nil { - rel.Id = uuid.NewSHA1(uuid.NameSpaceOID, []byte(rel.ContentHash())) - } - if err := s.Releases.repo.Set(rel); err != nil { - log.Warn("Failed to migrate legacy release", - "content_hash", rel.ContentHash(), "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy jobs") - } - for _, job := range s.repo.Jobs.Items() { - if err := s.Jobs.repo.Set(job); err != nil { - log.Warn("Failed to migrate legacy job", - "job_id", job.Id, "error", err) - } - } - - if setStatus != nil { - setStatus("Migrating legacy job release IDs") - } - for _, job := range s.repo.Jobs.Items() { - if job.ReleaseId == "" { - continue - } - if _, err := uuid.Parse(job.ReleaseId); err == nil { - continue - } - job.ReleaseId = uuid.NewSHA1(uuid.NameSpaceOID, []byte(job.ReleaseId)).String() - if err := s.Jobs.repo.Set(job); err != nil { - log.Warn("Failed to migrate legacy job release ID", - "job_id", job.Id, "error", err) - } - } - if setStatus != nil { setStatus("Computing release targets") } From c9a2476937eeb354206de1806779b516eb4a4c37 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 6 Mar 2026 00:13:59 -0500 Subject: [PATCH 2/3] chore: update pnpm-lock.yaml for deprecated package and add new route for work queue in settings --- apps/web/app/components/ui/chart.tsx | 165 +++++---- apps/web/app/routes.ts | 1 + .../work-queue/CreateWorkItemDialog.tsx | 260 +++++++++++++ .../work-queue/WorkPayloadDrawer.tsx | 102 ++++++ .../work-queue/WorkQueueCharts.tsx | 341 ++++++++++++++++++ apps/web/app/routes/ws/settings/_layout.tsx | 12 +- .../web/app/routes/ws/settings/work-queue.tsx | 312 ++++++++++++++++ apps/workspace-engine/main.go | 3 +- packages/trpc/src/root.ts | 2 + packages/trpc/src/routes/reconcile.ts | 313 ++++++++++++++++ pnpm-lock.yaml | 1 + 11 files changed, 1429 insertions(+), 83 deletions(-) create mode 100644 apps/web/app/routes/ws/settings/_components/work-queue/CreateWorkItemDialog.tsx create mode 100644 apps/web/app/routes/ws/settings/_components/work-queue/WorkPayloadDrawer.tsx create mode 100644 apps/web/app/routes/ws/settings/_components/work-queue/WorkQueueCharts.tsx create mode 100644 apps/web/app/routes/ws/settings/work-queue.tsx create mode 100644 packages/trpc/src/routes/reconcile.ts diff --git a/apps/web/app/components/ui/chart.tsx b/apps/web/app/components/ui/chart.tsx index d791ce741..e0aa25224 100644 --- a/apps/web/app/components/ui/chart.tsx +++ b/apps/web/app/components/ui/chart.tsx @@ -1,37 +1,42 @@ -"use client" +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ +/* eslint-disable @typescript-eslint/consistent-indexed-object-style */ +"use client"; -import * as React from "react" -import * as RechartsPrimitive from "recharts" +import * as React from "react"; +import * as RechartsPrimitive from "recharts"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; // Format: { THEME_NAME: CSS_SELECTOR } -const THEMES = { light: "", dark: ".dark" } as const +const THEMES = { light: "", dark: ".dark" } as const; export type ChartConfig = { [k in string]: { - label?: React.ReactNode - icon?: React.ComponentType + label?: React.ReactNode; + icon?: React.ComponentType; } & ( | { color?: string; theme?: never } | { color?: never; theme: Record } - ) -} + ); +}; type ChartContextProps = { - config: ChartConfig -} + config: ChartConfig; +}; -const ChartContext = React.createContext(null) +const ChartContext = React.createContext(null); function useChart() { - const context = React.useContext(ChartContext) + const context = React.useContext(ChartContext); if (!context) { - throw new Error("useChart must be used within a ") + throw new Error("useChart must be used within a "); } - return context + return context; } function ChartContainer({ @@ -41,13 +46,13 @@ function ChartContainer({ config, ...props }: React.ComponentProps<"div"> & { - config: ChartConfig + config: ChartConfig; children: React.ComponentProps< typeof RechartsPrimitive.ResponsiveContainer - >["children"] + >["children"]; }) { - const uniqueId = React.useId() - const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + const uniqueId = React.useId(); + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; return ( @@ -55,8 +60,8 @@ function ChartContainer({ data-slot="chart" data-chart={chartId} className={cn( - "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden", - className + "[&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent", + className, )} {...props} > @@ -66,16 +71,16 @@ function ChartContainer({ - ) + ); } const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const colorConfig = Object.entries(config).filter( - ([, config]) => config.theme || config.color - ) + ([, config]) => config.theme || config.color, + ); if (!colorConfig.length) { - return null + return null; } return ( @@ -89,20 +94,20 @@ ${colorConfig .map(([key, itemConfig]) => { const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || - itemConfig.color - return color ? ` --color-${key}: ${color};` : null + itemConfig.color; + return color ? ` --color-${key}: ${color};` : null; }) .join("\n")} } -` +`, ) .join("\n"), }} /> - ) -} + ); +}; -const ChartTooltip = RechartsPrimitive.Tooltip +const ChartTooltip = RechartsPrimitive.Tooltip; function ChartTooltipContent({ active, @@ -120,40 +125,40 @@ function ChartTooltipContent({ labelKey, }: React.ComponentProps & React.ComponentProps<"div"> & { - hideLabel?: boolean - hideIndicator?: boolean - indicator?: "line" | "dot" | "dashed" - nameKey?: string - labelKey?: string + hideLabel?: boolean; + hideIndicator?: boolean; + indicator?: "line" | "dot" | "dashed"; + nameKey?: string; + labelKey?: string; }) { - const { config } = useChart() + const { config } = useChart(); const tooltipLabel = React.useMemo(() => { if (hideLabel || !payload?.length) { - return null + return null; } - const [item] = payload - const key = `${labelKey || item?.dataKey || item?.name || "value"}` - const itemConfig = getPayloadConfigFromPayload(config, item, key) + const [item] = payload; + const key = `${labelKey || item?.dataKey || item?.name || "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); const value = !labelKey && typeof label === "string" ? config[label as keyof typeof config]?.label || label - : itemConfig?.label + : itemConfig?.label; if (labelFormatter) { return (
{labelFormatter(value, payload)}
- ) + ); } if (!value) { - return null + return null; } - return
{value}
+ return
{value}
; }, [ label, labelFormatter, @@ -162,19 +167,19 @@ function ChartTooltipContent({ labelClassName, config, labelKey, - ]) + ]); if (!active || !payload?.length) { - return null + return null; } - const nestLabel = payload.length === 1 && indicator !== "dot" + const nestLabel = payload.length === 1 && indicator !== "dot"; return (
{!nestLabel ? tooltipLabel : null} @@ -182,16 +187,16 @@ function ChartTooltipContent({ {payload .filter((item) => item.type !== "none") .map((item, index) => { - const key = `${nameKey || item.name || item.dataKey || "value"}` - const itemConfig = getPayloadConfigFromPayload(config, item, key) - const indicatorColor = color || item.payload.fill || item.color + const key = `${nameKey || item.name || item.dataKey || "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); + const indicatorColor = color || item.payload.fill || item.color; return (
svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5", - indicator === "dot" && "items-center" + "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", + indicator === "dot" && "items-center", )} > {formatter && item?.value !== undefined && item.name ? ( @@ -204,14 +209,14 @@ function ChartTooltipContent({ !hideIndicator && (
@@ -235,7 +240,7 @@ function ChartTooltipContent({
{item.value && ( - + {item.value.toLocaleString()} )} @@ -243,14 +248,14 @@ function ChartTooltipContent({ )}
- ) + ); })}
- ) + ); } -const ChartLegend = RechartsPrimitive.Legend +const ChartLegend = RechartsPrimitive.Legend; function ChartLegendContent({ className, @@ -260,13 +265,13 @@ function ChartLegendContent({ nameKey, }: React.ComponentProps<"div"> & Pick & { - hideIcon?: boolean - nameKey?: string + hideIcon?: boolean; + nameKey?: string; }) { - const { config } = useChart() + const { config } = useChart(); if (!payload?.length) { - return null + return null; } return ( @@ -274,20 +279,20 @@ function ChartLegendContent({ className={cn( "flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", - className + className, )} > {payload .filter((item) => item.type !== "none") .map((item) => { - const key = `${nameKey || item.dataKey || "value"}` - const itemConfig = getPayloadConfigFromPayload(config, item, key) + const key = `${nameKey || item.dataKey || "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); return (
svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3" + "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground", )} > {itemConfig?.icon && !hideIcon ? ( @@ -302,20 +307,20 @@ function ChartLegendContent({ )} {itemConfig?.label}
- ) + ); })} - ) + ); } // Helper to extract item config from a payload. function getPayloadConfigFromPayload( config: ChartConfig, payload: unknown, - key: string + key: string, ) { if (typeof payload !== "object" || payload === null) { - return undefined + return undefined; } const payloadPayload = @@ -323,15 +328,15 @@ function getPayloadConfigFromPayload( typeof payload.payload === "object" && payload.payload !== null ? payload.payload - : undefined + : undefined; - let configLabelKey: string = key + let configLabelKey: string = key; if ( key in payload && typeof payload[key as keyof typeof payload] === "string" ) { - configLabelKey = payload[key as keyof typeof payload] as string + configLabelKey = payload[key as keyof typeof payload] as string; } else if ( payloadPayload && key in payloadPayload && @@ -339,12 +344,12 @@ function getPayloadConfigFromPayload( ) { configLabelKey = payloadPayload[ key as keyof typeof payloadPayload - ] as string + ] as string; } return configLabelKey in config ? config[configLabelKey] - : config[key as keyof typeof config] + : config[key as keyof typeof config]; } export { @@ -354,4 +359,4 @@ export { ChartLegend, ChartLegendContent, ChartStyle, -} +}; diff --git a/apps/web/app/routes.ts b/apps/web/app/routes.ts index 8b58c1e5b..c688b76f5 100644 --- a/apps/web/app/routes.ts +++ b/apps/web/app/routes.ts @@ -99,6 +99,7 @@ export default [ route("general", "routes/ws/settings/general.tsx"), route("members", "routes/ws/settings/members.tsx"), route("api-keys", "routes/ws/settings/api-keys.tsx"), + route("work-queue", "routes/ws/settings/work-queue.tsx"), route("delete-workspace", "routes/ws/settings/delete-workspace.tsx"), ]), ]), diff --git a/apps/web/app/routes/ws/settings/_components/work-queue/CreateWorkItemDialog.tsx b/apps/web/app/routes/ws/settings/_components/work-queue/CreateWorkItemDialog.tsx new file mode 100644 index 000000000..40f5c82ee --- /dev/null +++ b/apps/web/app/routes/ws/settings/_components/work-queue/CreateWorkItemDialog.tsx @@ -0,0 +1,260 @@ +import type React from "react"; +import { useState } from "react"; +import { PlusIcon } from "lucide-react"; + +import { Button } from "~/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "~/components/ui/dialog"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; +import { Textarea } from "~/components/ui/textarea"; + +import { trpc } from "~/api/trpc"; + +const WORK_ITEM_KINDS = [ + { value: "desired-release", label: "Desired Release" }, + { value: "environment-resource-selector-eval", label: "Environment Resource Selector Eval" }, + { value: "deployment-resource-selector-eval", label: "Deployment Resource Selector Eval" }, + { value: "relationship-eval", label: "Relationship Eval" }, +] as const; + +const defaultForm = { + kind: "", + scopeType: "", + scopeId: "", + priority: "100", + notBefore: "", + includePayload: false, + payloadType: "", + payloadKey: "", + payloadJson: "{}", +}; + +export const CreateWorkItemDialog: React.FC<{ + workspaceId: string; +}> = ({ workspaceId }) => { + const [open, setOpen] = useState(false); + const [form, setForm] = useState(defaultForm); + const [jsonError, setJsonError] = useState(null); + + const utils = trpc.useUtils(); + const createMutation = trpc.reconcile.create.useMutation({ + onSuccess: () => { + utils.reconcile.listWorkScopes.invalidate(); + utils.reconcile.stats.invalidate(); + utils.reconcile.chartData.invalidate(); + setOpen(false); + setForm(defaultForm); + setJsonError(null); + }, + }); + + const update = (field: keyof typeof form, value: string | boolean) => + setForm((f) => ({ ...f, [field]: value })); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + let parsedPayload: Record = {}; + if (form.includePayload) { + try { + parsedPayload = JSON.parse(form.payloadJson) as Record< + string, + unknown + >; + setJsonError(null); + } catch { + setJsonError("Invalid JSON"); + return; + } + } + + createMutation.mutate({ + workspaceId, + kind: form.kind, + scopeType: form.scopeType, + scopeId: form.scopeId, + priority: Number(form.priority), + notBefore: form.notBefore ? new Date(form.notBefore) : undefined, + payload: form.includePayload + ? { + payloadType: form.payloadType, + payloadKey: form.payloadKey, + payload: parsedPayload, + } + : undefined, + }); + }; + + return ( + + + + + + + Create Work Item + + Manually enqueue a reconcile work scope and optional payload. + + + +
+
+ + +
+ +
+
+ + update("scopeType", e.target.value)} + /> +
+
+ + update("scopeId", e.target.value)} + /> +
+
+ + update("priority", e.target.value)} + /> +
+
+ +
+ + update("notBefore", e.target.value)} + /> +

+ Leave empty for immediate processing. +

+
+ +
+ update("includePayload", e.target.checked)} + /> + +
+ + {form.includePayload && ( +
+
+
+ + update("payloadType", e.target.value)} + /> +
+
+ + update("payloadKey", e.target.value)} + /> +
+
+
+ +