277 lines
9.1 KiB
TypeScript
277 lines
9.1 KiB
TypeScript
import { authClient } from "@code/auth/web";
|
|
import { api } from "@code/backend/convex/_generated/api";
|
|
import type { Id } from "@code/backend/convex/_generated/dataModel";
|
|
import { useAction, useMutation, useQuery } from "convex/react";
|
|
import { makeFunctionReference } from "convex/server";
|
|
import { useMemo, useState } from "react";
|
|
|
|
import { useOrganizationChatAgent } from "@/hooks/chat/use-chat-agent";
|
|
import { usePersonalOrganization } from "@/hooks/use-personal-organization";
|
|
import type { WorkRecord, WorkspaceState } from "@/lib/workspace/types";
|
|
|
|
const toError = (error: unknown) =>
|
|
error instanceof Error ? error : new Error(String(error));
|
|
|
|
type ExecutionScenario =
|
|
| "success"
|
|
| "transient-failure-then-success"
|
|
| "needs-input"
|
|
| "permanent-failure"
|
|
| "cancelled";
|
|
|
|
const workListRef = makeFunctionReference<
|
|
"query",
|
|
{ projectId: Id<"projects"> },
|
|
WorkRecord[]
|
|
>("workPlanning:listForProject");
|
|
const requestDefinitionRef = makeFunctionReference<
|
|
"mutation",
|
|
{ workId: Id<"works"> },
|
|
unknown
|
|
>("workPlanning:requestDefinition");
|
|
const saveDefinitionRef = makeFunctionReference<
|
|
"mutation",
|
|
{ workId: Id<"works">; payloadJson: string },
|
|
unknown
|
|
>("workPlanning:saveDefinitionProposal");
|
|
const approveDefinitionRef = makeFunctionReference<
|
|
"mutation",
|
|
{ workId: Id<"works">; version: number },
|
|
unknown
|
|
>("workPlanning:approveDefinition");
|
|
const saveDesignRef = makeFunctionReference<
|
|
"mutation",
|
|
{ workId: Id<"works">; payloadJson: string },
|
|
unknown
|
|
>("workPlanning:saveDesignProposal");
|
|
const approveDesignRef = makeFunctionReference<
|
|
"mutation",
|
|
{ workId: Id<"works">; definitionVersion: number; designVersion: number },
|
|
unknown
|
|
>("workPlanning:approveDesign");
|
|
const startSimulationRef = makeFunctionReference<
|
|
"mutation",
|
|
{ workId: Id<"works">; scenario: ExecutionScenario; sliceId?: string },
|
|
unknown
|
|
>("workExecution:startSimulatedExecution");
|
|
const cancelSimulationRef = makeFunctionReference<
|
|
"mutation",
|
|
{ runId: Id<"workRuns"> },
|
|
unknown
|
|
>("workExecution:cancelSimulatedExecution");
|
|
const retrySimulationRef = makeFunctionReference<
|
|
"mutation",
|
|
{ runId: Id<"workRuns"> },
|
|
unknown
|
|
>("workExecution:retrySimulatedExecution");
|
|
const startExecutionRef = makeFunctionReference<
|
|
"mutation",
|
|
{ workId: Id<"works">; sliceId?: string },
|
|
unknown
|
|
>("workExecutionWorkflow:startExecution");
|
|
const cancelExecutionRef = makeFunctionReference<
|
|
"mutation",
|
|
{ runId: Id<"workRuns"> },
|
|
unknown
|
|
>("workExecutionWorkflow:cancelExecution");
|
|
const listGitConnectionsRef = makeFunctionReference<
|
|
"query",
|
|
Record<string, never>,
|
|
readonly {
|
|
id: string;
|
|
provider: "github" | "gitea";
|
|
serverUrl: string;
|
|
username?: string;
|
|
}[]
|
|
>("gitConnectionData:list");
|
|
const projectGitConnectionRef = makeFunctionReference<
|
|
"query",
|
|
{ projectId: Id<"projects"> },
|
|
{
|
|
id: string;
|
|
provider: "github" | "gitea";
|
|
serverUrl: string;
|
|
username?: string;
|
|
} | null
|
|
>("gitConnectionData:getForProject");
|
|
const connectGiteaRef = makeFunctionReference<
|
|
"action",
|
|
{ serverUrl: string; token: string; username?: string },
|
|
{ connectionId: Id<"gitConnections"> }
|
|
>("gitConnections:connectGitea");
|
|
const connectGithubRef = makeFunctionReference<
|
|
"action",
|
|
Record<string, never>,
|
|
{ connectionId: Id<"gitConnections"> }
|
|
>("gitConnections:connectGithub");
|
|
const attachGitConnectionRef = makeFunctionReference<
|
|
"mutation",
|
|
{ connectionId: Id<"gitConnections">; projectId: Id<"projects"> },
|
|
unknown
|
|
>("gitConnectionData:attachToProject");
|
|
|
|
export const useProjectWorkspace = (): WorkspaceState => {
|
|
const organization = usePersonalOrganization();
|
|
const projects = useQuery(
|
|
api.projects.list,
|
|
organization.organizationId ? {} : "skip"
|
|
);
|
|
const importPublicGit = useAction(api.projects.importPublicGit);
|
|
const agent = useOrganizationChatAgent(organization);
|
|
const [selectedProjectId, setSelectedProjectId] =
|
|
useState<Id<"projects"> | null>(null);
|
|
const [repository, setRepository] = useState("");
|
|
const [pending, setPending] = useState(false);
|
|
const [error, setError] = useState<Error>();
|
|
const [operationError, setOperationError] = useState<Error>();
|
|
|
|
const selectedProjectStillExists = projects?.some(
|
|
(project) => project.id === (selectedProjectId as unknown as string)
|
|
);
|
|
const activeProjectId = selectedProjectStillExists
|
|
? selectedProjectId
|
|
: ((projects?.[0]?.id as unknown as Id<"projects"> | undefined) ?? null);
|
|
const works = useQuery(
|
|
workListRef,
|
|
activeProjectId ? { projectId: activeProjectId } : "skip"
|
|
);
|
|
const gitConnections = useQuery(
|
|
listGitConnectionsRef,
|
|
organization.organizationId ? {} : "skip"
|
|
);
|
|
const projectGitConnection = useQuery(
|
|
projectGitConnectionRef,
|
|
activeProjectId ? { projectId: activeProjectId } : "skip"
|
|
);
|
|
const requestDefinitionMutation = useMutation(requestDefinitionRef);
|
|
const saveDefinitionMutation = useMutation(saveDefinitionRef);
|
|
const approveDefinitionMutation = useMutation(approveDefinitionRef);
|
|
const saveDesignMutation = useMutation(saveDesignRef);
|
|
const approveDesignMutation = useMutation(approveDesignRef);
|
|
const startSimulationMutation = useMutation(startSimulationRef);
|
|
const cancelSimulationMutation = useMutation(cancelSimulationRef);
|
|
const retrySimulationMutation = useMutation(retrySimulationRef);
|
|
const startExecutionMutation = useMutation(startExecutionRef);
|
|
const cancelExecutionMutation = useMutation(cancelExecutionRef);
|
|
const attachGitConnectionMutation = useMutation(attachGitConnectionRef);
|
|
const connectGiteaAction = useAction(connectGiteaRef);
|
|
const connectGithubAction = useAction(connectGithubRef);
|
|
const selectedProject = useMemo(
|
|
() =>
|
|
projects?.find(
|
|
(project) => project.id === (activeProjectId as unknown as string)
|
|
) ?? null,
|
|
[activeProjectId, projects]
|
|
);
|
|
|
|
const runOperation = async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
setOperationError(undefined);
|
|
try {
|
|
return await operation();
|
|
} catch (caughtError) {
|
|
setOperationError(toError(caughtError));
|
|
throw caughtError;
|
|
}
|
|
};
|
|
|
|
const connectRepository = async () => {
|
|
const repositoryUrl = repository.trim();
|
|
if (!repositoryUrl || pending) {
|
|
return;
|
|
}
|
|
setPending(true);
|
|
setError(undefined);
|
|
try {
|
|
const project = await importPublicGit({ repositoryUrl });
|
|
setSelectedProjectId(project.id as unknown as Id<"projects">);
|
|
setRepository("");
|
|
} catch (caughtError) {
|
|
setError(toError(caughtError));
|
|
} finally {
|
|
setPending(false);
|
|
}
|
|
};
|
|
|
|
const attachConnection = async (connectionId: Id<"gitConnections">) => {
|
|
if (!activeProjectId) {
|
|
throw new Error("Select a project first");
|
|
}
|
|
await attachGitConnectionMutation({
|
|
connectionId,
|
|
projectId: activeProjectId,
|
|
});
|
|
};
|
|
|
|
const connectGitea = (input: {
|
|
serverUrl: string;
|
|
token: string;
|
|
username?: string;
|
|
}) =>
|
|
runOperation(async () => {
|
|
const result = await connectGiteaAction(input);
|
|
await attachConnection(result.connectionId);
|
|
});
|
|
|
|
const connectLinkedGithub = () =>
|
|
runOperation(async () => {
|
|
const result = await connectGithubAction({});
|
|
await attachConnection(result.connectionId);
|
|
});
|
|
|
|
return {
|
|
agent,
|
|
approveDefinition: (workId: Id<"works">, version: number) =>
|
|
approveDefinitionMutation({ version, workId }),
|
|
approveDesign: (
|
|
workId: Id<"works">,
|
|
definitionVersion: number,
|
|
designVersion: number
|
|
) => approveDesignMutation({ definitionVersion, designVersion, workId }),
|
|
authorizeGithub: () =>
|
|
authClient.signIn.social({
|
|
callbackURL: window.location.href,
|
|
provider: "github",
|
|
}),
|
|
cancelExecution: (runId: Id<"workRuns">) =>
|
|
runOperation(() => cancelExecutionMutation({ runId })),
|
|
cancelSimulation: (runId: Id<"workRuns">) =>
|
|
runOperation(() => cancelSimulationMutation({ runId })),
|
|
clearOperationError: () => setOperationError(undefined),
|
|
connectGitea,
|
|
connectLinkedGithub,
|
|
connectRepository,
|
|
error,
|
|
gitConnections,
|
|
operationError,
|
|
pending,
|
|
projectGitConnection,
|
|
projects,
|
|
repository,
|
|
requestDefinition: (workId: Id<"works">) =>
|
|
requestDefinitionMutation({ workId }),
|
|
retrySimulation: (runId: Id<"workRuns">) =>
|
|
runOperation(() => retrySimulationMutation({ runId })),
|
|
saveDefinition: (workId: Id<"works">, payload: unknown) =>
|
|
saveDefinitionMutation({ payloadJson: JSON.stringify(payload), workId }),
|
|
saveDesign: (workId: Id<"works">, payload: unknown) =>
|
|
saveDesignMutation({ payloadJson: JSON.stringify(payload), workId }),
|
|
selectProject: (projectId: string) =>
|
|
setSelectedProjectId(projectId as unknown as Id<"projects">),
|
|
selectedProject: selectedProject
|
|
? { id: selectedProject.id, name: selectedProject.name }
|
|
: null,
|
|
setRepository,
|
|
startExecution: (workId: Id<"works">, sliceId?: string) =>
|
|
runOperation(() => startExecutionMutation({ sliceId, workId })),
|
|
startSimulation: (
|
|
workId: Id<"works">,
|
|
scenario: ExecutionScenario,
|
|
sliceId?: string
|
|
) =>
|
|
runOperation(() =>
|
|
startSimulationMutation({ scenario, sliceId, workId })
|
|
),
|
|
works,
|
|
};
|
|
};
|