Skip to content

Commit 67da0ca

Browse files
authored
Added button to run generator (#5983)
1 parent 602512c commit 67da0ca

File tree

13 files changed

+432
-18
lines changed

13 files changed

+432
-18
lines changed

frontend/app/src/app/app.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { QueryClientProvider } from "@tanstack/react-query";
55
import { Provider } from "jotai";
66
import { ErrorBoundary } from "react-error-boundary";
77
import { RouterProvider } from "react-router";
8-
import { Slide, ToastContainer } from "react-toastify";
98

109
import { TanStackQueryDevtools } from "@/app/devtools";
1110
import { router } from "@/app/router";
@@ -29,14 +28,6 @@ export function App() {
2928
<QueryClientProvider client={queryClient}>
3029
<ApolloProvider client={graphqlClient}>
3130
<ConfigProvider>
32-
<ToastContainer
33-
hideProgressBar={true}
34-
transition={Slide}
35-
autoClose={5000}
36-
closeOnClick={false}
37-
newestOnTop
38-
position="bottom-right"
39-
/>
4031
<RouterProvider router={router} />
4132
</ConfigProvider>
4233
</ApolloProvider>

frontend/app/src/app/router.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ErrorBoundaryRouter } from "@/shared/components/errors/error-boundary-r
99
import { ReactRouter7Adapter } from "@/shared/lib/use-query-params";
1010
import queryString from "query-string";
1111
import { Navigate, Outlet, UIMatch, createBrowserRouter } from "react-router";
12+
import { Slide, ToastContainer } from "react-toastify";
1213
import { QueryParamProvider } from "use-query-params";
1314

1415
export const router = createBrowserRouter([
@@ -23,6 +24,14 @@ export const router = createBrowserRouter([
2324
objectToSearchString: queryString.stringify,
2425
}}
2526
>
27+
<ToastContainer
28+
hideProgressBar={true}
29+
transition={Slide}
30+
autoClose={5000}
31+
closeOnClick={false}
32+
newestOnTop
33+
position="bottom-right"
34+
/>
2635
<Outlet />
2736
</QueryParamProvider>
2837
),
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import graphqlClient from "@/shared/api/graphql/graphqlClientApollo";
2+
import { ContextParams } from "@/shared/api/types";
3+
import { gql } from "@apollo/client";
4+
5+
const generatorRunMutation = gql`
6+
mutation CoreGeneratorDefinitionRun($generatorId: String!, $waitUntilCompletion: Boolean, $targetNodeIds: [String!]) {
7+
CoreGeneratorDefinitionRun(
8+
wait_until_completion: $waitUntilCompletion
9+
data: {id: $generatorId, nodes: $targetNodeIds}
10+
) {
11+
task {
12+
id
13+
}
14+
}
15+
}
16+
`;
17+
18+
export type RunGeneratorFromApiParams = Pick<ContextParams, "branchName"> & {
19+
generatorId: string;
20+
targetNodeIds?: string[];
21+
waitUntilCompletion?: boolean;
22+
};
23+
24+
export const runGeneratorFromApi = async ({
25+
branchName,
26+
generatorId,
27+
targetNodeIds,
28+
waitUntilCompletion = false,
29+
}: RunGeneratorFromApiParams) => {
30+
return graphqlClient.mutate({
31+
mutation: generatorRunMutation,
32+
variables: {
33+
generatorId,
34+
targetNodeIds,
35+
waitUntilCompletion,
36+
},
37+
context: {
38+
branch: branchName,
39+
},
40+
});
41+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const GENERATOR_DEFINITION_KIND = "CoreGeneratorDefinition";
2+
3+
export const GENERATOR_INSTANCE_KIND = "CoreGeneratorInstance";
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useCurrentBranch } from "@/entities/branches/ui/branches-provider";
2+
import { RunGeneratorParams, runGenerator } from "@/entities/generators/domain/run-generator";
3+
import { useMutation } from "@tanstack/react-query";
4+
5+
export const useRunGeneratorMutation = () => {
6+
const { currentBranch } = useCurrentBranch();
7+
8+
return useMutation({
9+
mutationFn: (params: Omit<RunGeneratorParams, "branchName">) => {
10+
return runGenerator({
11+
branchName: currentBranch.name,
12+
...params,
13+
});
14+
},
15+
});
16+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {
2+
RunGeneratorFromApiParams,
3+
runGeneratorFromApi,
4+
} from "@/entities/generators/api/run-generator-from-api";
5+
6+
export type RunGeneratorParams = RunGeneratorFromApiParams;
7+
8+
export type RunGenerator = (params: RunGeneratorParams) => Promise<{ taskId: string }>;
9+
10+
export const runGenerator: RunGenerator = async (params) => {
11+
const { data } = await runGeneratorFromApi(params);
12+
13+
return { taskId: data.CoreGeneratorDefinitionRun.task.id };
14+
};
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { QSP } from "@/config/qsp";
2+
import { useAuth } from "@/entities/authentication/ui/useAuth";
3+
import { useRunGeneratorMutation } from "@/entities/generators/domain/run-generator.mutation";
4+
import { getNodeLabel } from "@/entities/nodes/object/utils/get-node-label";
5+
import { RelationshipNode } from "@/entities/nodes/relationships/domain/types";
6+
import { RelationshipComboboxList } from "@/entities/nodes/relationships/ui/relationship-combobox-list";
7+
import { constructPath } from "@/shared/api/rest/fetch";
8+
import { Menu, MenuItem } from "@/shared/components/aria/menu";
9+
import { Button } from "@/shared/components/buttons/button-primitive";
10+
import { ALERT_TYPES, Alert } from "@/shared/components/ui/alert";
11+
import { Badge } from "@/shared/components/ui/badge";
12+
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/components/ui/popover";
13+
import { focusVisibleStyle } from "@/shared/components/ui/style";
14+
import { classNames } from "@/shared/utils/common";
15+
import { PlayIcon } from "lucide-react";
16+
import { useState } from "react";
17+
import { Text } from "react-aria-components";
18+
import { Link } from "react-router";
19+
import { toast } from "react-toastify";
20+
21+
export interface RunGeneratorActionProps {
22+
generatorId: string;
23+
groupId: string;
24+
}
25+
26+
export function GeneratorDefinitionRunButton({ generatorId, groupId }: RunGeneratorActionProps) {
27+
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
28+
const [showTargetForm, setShowTargetForm] = useState(false);
29+
const { isPending, mutate } = useRunGeneratorMutation();
30+
const { isAuthenticated } = useAuth();
31+
32+
const handlePopoverOpenChange = (open: boolean) => {
33+
setIsPopoverOpen(open);
34+
setShowTargetForm(false);
35+
};
36+
37+
const handleRunGenerator = (targetNodeIds?: string[]) => {
38+
mutate(
39+
{ generatorId, targetNodeIds },
40+
{
41+
onSuccess: ({ taskId }) => {
42+
const url = constructPath(window.location.pathname, [
43+
{ name: QSP.TAB, value: "tasks" },
44+
{ name: QSP.TASK_ID, value: taskId },
45+
]);
46+
47+
toast(
48+
<Alert
49+
type={ALERT_TYPES.SUCCESS}
50+
message={
51+
<>
52+
Generator started successfully.
53+
<br />
54+
<Link to={url} className="underline flex items-center gap-1">
55+
View task details
56+
</Link>
57+
</>
58+
}
59+
/>
60+
);
61+
},
62+
}
63+
);
64+
setIsPopoverOpen(false);
65+
};
66+
67+
return (
68+
<Popover open={isPopoverOpen} onOpenChange={handlePopoverOpenChange}>
69+
<PopoverTrigger asChild>
70+
<Button variant="active" isLoading={isPending} disabled={isPending || !isAuthenticated}>
71+
{!isPending && <PlayIcon className="size-4 mr-2" />}
72+
Run
73+
</Button>
74+
</PopoverTrigger>
75+
76+
<PopoverContent className="p-1 min-w-[200px] max-w-sm" align="end">
77+
{showTargetForm ? (
78+
<GeneratorTargetSelectionForm
79+
generatorId={generatorId}
80+
groupId={groupId}
81+
onSubmit={handleRunGenerator}
82+
onCancel={() => setShowTargetForm(false)}
83+
/>
84+
) : (
85+
<Menu aria-label="Run generator options">
86+
<MenuItem onAction={() => handleRunGenerator()} className="flex-col gap-0 items-start">
87+
<Text slot="label" className="font-semibold">
88+
All targets
89+
</Text>
90+
<Text slot="description" className="text-gray-600 text-xs">
91+
Generate for all members in the target group
92+
</Text>
93+
</MenuItem>
94+
<MenuItem
95+
onAction={() => setShowTargetForm(true)}
96+
className="flex-col gap-0 items-start"
97+
>
98+
<Text slot="label" className="font-semibold">
99+
Selected targets
100+
</Text>
101+
<Text slot="description" className="text-gray-600 text-xs">
102+
Choose specific members of target group
103+
</Text>
104+
</MenuItem>
105+
</Menu>
106+
)}
107+
</PopoverContent>
108+
</Popover>
109+
);
110+
}
111+
112+
interface GeneratorTargetSelectionFormProps extends RunGeneratorActionProps {
113+
onSubmit: (targetNodeIds: string[]) => void;
114+
onCancel?: () => void;
115+
}
116+
117+
export function GeneratorTargetSelectionForm({
118+
groupId,
119+
onSubmit,
120+
onCancel,
121+
}: GeneratorTargetSelectionFormProps) {
122+
const [selectedTargetNodes, setSelectedTargetNodes] = useState<RelationshipNode[]>([]);
123+
124+
const handleRemoveTarget = (nodeId: string) => {
125+
setSelectedTargetNodes((prev) => prev.filter((node) => node.id !== nodeId));
126+
};
127+
128+
const handleSelect = (selectedRelationship: RelationshipNode) => {
129+
setSelectedTargetNodes((prev) => [...prev, selectedRelationship]);
130+
};
131+
132+
const handleSubmit = () => {
133+
onSubmit(selectedTargetNodes.map((node) => node.id));
134+
};
135+
136+
return (
137+
<div className="flex flex-col gap-1">
138+
<div className="flex items-center justify-between mb-1">
139+
<h3 className="text-sm font-medium">Select target nodes</h3>
140+
<Button
141+
variant="ghost"
142+
size="xs"
143+
onClick={onCancel}
144+
className="text-gray-500 hover:text-gray-700 text-xs h-5 p-1"
145+
>
146+
Back
147+
</Button>
148+
</div>
149+
150+
<div className="rounded border p-2">
151+
{selectedTargetNodes.length > 0 ? (
152+
<div className="flex flex-wrap gap-1">
153+
{selectedTargetNodes.map((node) => {
154+
const label = getNodeLabel(node);
155+
156+
return (
157+
<Badge key={node.id} className="flex items-center gap-1">
158+
{label}
159+
<button
160+
type="button"
161+
onClick={() => handleRemoveTarget(node.id)}
162+
className={classNames(
163+
focusVisibleStyle,
164+
"text-xs hover:text-gray-900 border border-transparent rounded-full size-3.5 flex items-center justify-center"
165+
)}
166+
aria-label={`Remove ${label}`}
167+
>
168+
×
169+
</button>
170+
</Badge>
171+
);
172+
})}
173+
</div>
174+
) : (
175+
<span className="text-gray-400">No targets selected</span>
176+
)}
177+
</div>
178+
179+
<RelationshipComboboxList
180+
autoFocus
181+
className="rounded border"
182+
peer="CoreNode"
183+
onSelect={handleSelect}
184+
filterQuery={{
185+
member_of_groups__ids: groupId,
186+
}}
187+
filterItem={(node) => !selectedTargetNodes.some((v) => v.id === node.id)}
188+
/>
189+
190+
<Button disabled={selectedTargetNodes.length === 0} variant="active" onClick={handleSubmit}>
191+
Run Generator
192+
</Button>
193+
</div>
194+
);
195+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { QSP } from "@/config/qsp";
2+
import { useRunGeneratorMutation } from "@/entities/generators/domain/run-generator.mutation";
3+
import { constructPath } from "@/shared/api/rest/fetch";
4+
import { Button, ButtonProps } from "@/shared/components/buttons/button-primitive";
5+
import { ALERT_TYPES, Alert } from "@/shared/components/ui/alert";
6+
import { PlayIcon } from "lucide-react";
7+
import { Link } from "react-router";
8+
import { toast } from "react-toastify";
9+
10+
export interface GeneratorRunButtonProps extends ButtonProps {
11+
generatorId: string;
12+
targetNodeIds?: string[];
13+
}
14+
15+
export function GeneratorRunButton({
16+
generatorId,
17+
targetNodeIds,
18+
children,
19+
...props
20+
}: GeneratorRunButtonProps) {
21+
const { isPending, mutate } = useRunGeneratorMutation();
22+
23+
const handleRunGenerator = () => {
24+
mutate(
25+
{ generatorId, targetNodeIds },
26+
{
27+
onSuccess: ({ taskId }) => {
28+
const url = constructPath(window.location.pathname, [
29+
{ name: QSP.TAB, value: "tasks" },
30+
{ name: QSP.TASK_ID, value: taskId },
31+
]);
32+
33+
toast(
34+
<Alert
35+
type={ALERT_TYPES.SUCCESS}
36+
message={
37+
<>
38+
Generator started successfully.
39+
<br />
40+
<Link to={url} className="underline flex items-center gap-1">
41+
View task details
42+
</Link>
43+
</>
44+
}
45+
/>
46+
);
47+
},
48+
}
49+
);
50+
};
51+
52+
return (
53+
<Button
54+
isLoading={isPending}
55+
disabled={isPending}
56+
variant="active"
57+
onClick={handleRunGenerator}
58+
{...props}
59+
>
60+
{!isPending && <PlayIcon className="size-4 mr-2" />}
61+
{children ?? "Run"}
62+
</Button>
63+
);
64+
}

0 commit comments

Comments
 (0)