From 4cab4ca32af2cffcc0190f91a1202bd31fee844e Mon Sep 17 00:00:00 2001 From: John Smith Date: Wed, 8 May 2024 19:18:35 +0930 Subject: [PATCH] feat: Prompt demo UI and CLI command (#222) Admin UI and CLI command for issuing cluster tasks. Currently prepends functions executed as part of a task execution with a `taskId` in order to correlate them and demonstrate function calls. In the future this should be moved into a seperate property. image image --- admin/app/clusters/[clusterId]/tasks/page.tsx | 213 ++++++++++++++++++ admin/client/contract.ts | 23 ++ admin/package.json | 1 + cli/src/client/contract.ts | 21 ++ cli/src/commands/task.ts | 70 ++++++ cli/src/index.ts | 2 + cli/src/lib/upload.ts | 4 + control-plane/src/modules/agents/agent.ts | Bin 3182 -> 3245 bytes control-plane/src/modules/contract.ts | 2 + control-plane/src/modules/router.ts | 6 +- package-lock.json | 108 +++++++++ 11 files changed, 448 insertions(+), 2 deletions(-) create mode 100644 admin/app/clusters/[clusterId]/tasks/page.tsx create mode 100644 cli/src/commands/task.ts diff --git a/admin/app/clusters/[clusterId]/tasks/page.tsx b/admin/app/clusters/[clusterId]/tasks/page.tsx new file mode 100644 index 00000000..988ae7c2 --- /dev/null +++ b/admin/app/clusters/[clusterId]/tasks/page.tsx @@ -0,0 +1,213 @@ +"use client"; + +import { client } from "@/client/client"; +import { DataTable } from "@/components/ui/DataTable"; +import { useAuth } from "@clerk/nextjs"; +import { ScrollArea } from "@radix-ui/react-scroll-area"; +import { formatRelative } from "date-fns"; +import { useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import { ThreeDots } from "react-loader-spinner"; +import { functionStatusToCircle } from "../helpers"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; + +export default function Page({ params }: { params: { clusterId: string } }) { + const { getToken, isLoaded, isSignedIn } = useAuth(); + + const [data, setData] = useState<{ + loading: boolean; + taskId: string | null; + result: string | null; + jobs: { + id: string; + createdAt: Date; + targetFn: string; + status: string; + functionExecutionTime: number | null; + }[]; + }>({ + loading: false, + taskId: null, + result: null, + jobs: [], + }); + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + callPrompt(); + } + }; + + const callPrompt = async () => { + setData({ + loading: true, + taskId: null, + result: null, + jobs: [], + }); + + const taskPrompt = (document.getElementById("prompt") as HTMLInputElement) + .value; + + const result = await client.executeTask({ + headers: { + authorization: `Bearer ${getToken()}`, + }, + params: { + clusterId: params.clusterId, + }, + body: { + task: taskPrompt, + }, + }); + + if (result.status === 200) { + setData({ + loading: false, + taskId: result.body.taskId, + result: result.body.result, + jobs: data.jobs, + }); + } else { + setData({ + loading: false, + taskId: null, + result: "Failed to execute task", + jobs: data.jobs, + }); + } + }; + + useEffect(() => { + const fetchData = async () => { + if (!data.taskId) { + return; + } + + const clusterResult = await client.getClusterDetailsForUser({ + headers: { + authorization: `Bearer ${await getToken()}`, + }, + params: { + clusterId: params.clusterId, + }, + }); + + if (clusterResult.status === 401) { + window.location.reload(); + } + + if (clusterResult.status === 200) { + setData({ + loading: data.loading, + taskId: data.taskId, + result: data.result, + jobs: clusterResult.body.jobs, + }); + } else { + toast.error("Failed to fetch cluster details."); + } + }; + + const interval = setInterval(fetchData, 500); // Refresh every 500ms + + return () => { + clearInterval(interval); // Clear the interval when the component unmounts + }; + }, [params.clusterId, isLoaded, isSignedIn, getToken, data]); + + return ( +
+
+

Execute Agent Task

+

+ Prompt the cluster to execute a task. +

+
+ + +
+ {data.result && ( +
+
{data.result}
+
+ )} + {data.loading && ( +
+ +
+ )} + + (a.createdAt > b.createdAt ? -1 : 1)) + .filter((s) => s.id.startsWith(data.taskId || "")) + .map((s) => ({ + jobId: s.id, + targetFn: s.targetFn, + status: s.status, + createdAt: formatRelative(new Date(s.createdAt), new Date()), + functionExecutionTime: s.functionExecutionTime, + }))} + noDataMessage="No functions have been performed as part of the task." + columnDef={[ + { + accessorKey: "jobId", + header: "Execution ID", + cell: ({ row }) => { + const jobId: string = row.getValue("jobId"); + + return ( + + {jobId.substring(jobId.length - 6)} + + ); + }, + }, + { + accessorKey: "targetFn", + header: "Function", + }, + { + accessorKey: "createdAt", + header: "Called", + }, + { + accessorKey: "status", + header: "", + cell: ({ row }) => { + const status = row.getValue("status"); + + return functionStatusToCircle(status as string); + }, + }, + ]} + /> + +
+
+ ); +} diff --git a/admin/client/contract.ts b/admin/client/contract.ts index 09271663..73625a7b 100644 --- a/admin/client/contract.ts +++ b/admin/client/contract.ts @@ -26,6 +26,7 @@ export const definition = { .array( z.object({ name: z.string(), + schema: z.string(), }), ) .optional(), @@ -394,6 +395,7 @@ export const definition = { query: z.object({ jobId: z.string().optional(), deploymentId: z.string().optional(), + taskId: z.string().optional(), }), }, createDeployment: { @@ -712,6 +714,27 @@ export const definition = { 401: z.undefined(), }, }, + executeTask: { + method: "POST", + path: "/clusters/:clusterId/task", + headers: z.object({ + authorization: z.string(), + }), + body: z.object({ + task: z.string(), + }), + responses: { + 401: z.undefined(), + 404: z.undefined(), + 200: z.object({ + result: z.any(), + taskId: z.string(), + }), + 500: z.object({ + error: z.string(), + }), + }, + }, } as const; export const contract = c.router(definition); diff --git a/admin/package.json b/admin/package.json index d56f3faf..43e77be6 100644 --- a/admin/package.json +++ b/admin/package.json @@ -37,6 +37,7 @@ "react-hook-form": "^7.50.1", "react-hot-toast": "^2.4.1", "react-json-pretty": "^2.2.0", + "react-loader-spinner": "^6.1.6", "react-syntax-highlighter": "^15.5.0", "recharts": "^2.10.4", "tailwind-merge": "^2.2.0", diff --git a/cli/src/client/contract.ts b/cli/src/client/contract.ts index 09271663..9cd28f2e 100644 --- a/cli/src/client/contract.ts +++ b/cli/src/client/contract.ts @@ -26,6 +26,7 @@ export const definition = { .array( z.object({ name: z.string(), + schema: z.string(), }), ) .optional(), @@ -712,6 +713,26 @@ export const definition = { 401: z.undefined(), }, }, + executeTask: { + method: "POST", + path: "/clusters/:clusterId/task", + headers: z.object({ + authorization: z.string(), + }), + body: z.object({ + task: z.string(), + }), + responses: { + 401: z.undefined(), + 404: z.undefined(), + 200: z.object({ + result: z.any(), + }), + 500: z.object({ + error: z.string(), + }), + }, + }, } as const; export const contract = c.router(definition); diff --git a/cli/src/commands/task.ts b/cli/src/commands/task.ts new file mode 100644 index 00000000..74a704c2 --- /dev/null +++ b/cli/src/commands/task.ts @@ -0,0 +1,70 @@ +import { CommandModule } from "yargs"; +import { selectCluster } from "../utils"; +import { client } from "../lib/client"; +import { input } from "@inquirer/prompts"; + +interface TaskArgs { + cluster?: string; + task?: string; +} + +export const Task: CommandModule<{}, TaskArgs> = { + command: "task", + describe: "Execute a task in the cluster using a human readable prompt", + builder: (yargs) => + yargs + .option("cluster", { + describe: "Cluster ID", + demandOption: false, + type: "string", + }) + .option("task", { + describe: "Task for the cluster to perform", + demandOption: false, + type: "string", + }), + handler: async ({ cluster, task }) => { + if (!cluster) { + cluster = await selectCluster(); + if (!cluster) { + console.log("No cluster selected"); + return; + } + } + + if (!task) { + task = await input({ + message: "Human readable prompt for the cluster to perform", + validate: (value) => { + if (!value) { + return "Prompt is required"; + } + return true; + }, + }); + } + + try { + const result = await executeTask(cluster, task); + console.log(result); + } catch (e) { + console.error(e); + } + }, +}; + +const executeTask = async (clusterId: string, task: string) => { + const result = await client.executeTask({ + params: { + clusterId, + }, + body: { + task, + }, + }); + + if (result.status !== 200) { + throw new Error(`Failed to prompt cluster: ${result.status}`); + } + return result.body.result; +}; diff --git a/cli/src/index.ts b/cli/src/index.ts index 048e38f6..5cdf189e 100755 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -12,6 +12,7 @@ import { Repl } from "./commands/repl"; import { Context } from "./commands/context"; import { setCurrentContext } from "./lib/context"; import { Services } from "./commands/services"; +import { Task } from "./commands/task"; const cli = yargs(hideBin(process.argv)) .scriptName("differential") @@ -35,6 +36,7 @@ if (authenticated) { .command(ClientLibrary) .command(Repl) .command(Context) + .command(Task) .command(Services); } else { console.log( diff --git a/cli/src/lib/upload.ts b/cli/src/lib/upload.ts index 1730d8cf..234a9ef6 100644 --- a/cli/src/lib/upload.ts +++ b/cli/src/lib/upload.ts @@ -39,6 +39,8 @@ export const uploadAsset = async ({ const { presignedUrl } = upload.body; + log("Uploading asset to s3", { presignedUrl }); + const response = await fetch(presignedUrl, { method: "PUT", body: readFileSync(path), @@ -47,6 +49,8 @@ export const uploadAsset = async ({ }, }); + log("Response from S3 put", response); + if (response.status !== 200) { throw new Error( "Failed to upload asset. Please check provided options and cluster configuration.", diff --git a/control-plane/src/modules/agents/agent.ts b/control-plane/src/modules/agents/agent.ts index 2aba1238feb491ae9a436e43ad476846bdf130a1..a7f8009c02c4251726560ad00b2bffb289009e75 100644 GIT binary patch literal 3245 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 U^&d*zpkE0sy~ylgPS9;0s%)Dk82|tP diff --git a/control-plane/src/modules/contract.ts b/control-plane/src/modules/contract.ts index 9cd28f2e..73625a7b 100644 --- a/control-plane/src/modules/contract.ts +++ b/control-plane/src/modules/contract.ts @@ -395,6 +395,7 @@ export const definition = { query: z.object({ jobId: z.string().optional(), deploymentId: z.string().optional(), + taskId: z.string().optional(), }), }, createDeployment: { @@ -727,6 +728,7 @@ export const definition = { 404: z.undefined(), 200: z.object({ result: z.any(), + taskId: z.string(), }), 500: z.object({ error: z.string(), diff --git a/control-plane/src/modules/router.ts b/control-plane/src/modules/router.ts index 58eea05c..a46c2335 100644 --- a/control-plane/src/modules/router.ts +++ b/control-plane/src/modules/router.ts @@ -35,6 +35,7 @@ import { import { deploymentResultFromNotification } from "./deployment/cfn-manager"; import { env } from "../utilities/env"; import { logger } from "../utilities/logger"; +import { ulid } from "ulid"; const readFile = util.promisify(fs.readFile); @@ -958,11 +959,12 @@ export const router = s.router(contract, { const { executeTaskForCluster } = require("./agents/agent"); - const result = await executeTaskForCluster({ clusterId }, task); + const taskId = ulid(); + const result = await executeTaskForCluster({ clusterId }, taskId, task); return { status: 200, - body: { result }, + body: { result, taskId }, }; }, }); diff --git a/package-lock.json b/package-lock.json index ff6764b6..267a6575 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "react-hook-form": "^7.50.1", "react-hot-toast": "^2.4.1", "react-json-pretty": "^2.2.0", + "react-loader-spinner": "^6.1.6", "react-syntax-highlighter": "^15.5.0", "recharts": "^2.10.4", "tailwind-merge": "^2.2.0", @@ -8050,6 +8051,11 @@ "version": "2.0.3", "license": "MIT" }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "license": "MIT" @@ -9357,6 +9363,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001612", "funding": [ @@ -10532,6 +10546,14 @@ "node": "*" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, "node_modules/cssesc": { "version": "3.0.0", "license": "MIT", @@ -21145,6 +21167,87 @@ "react-dom": ">=15.0" } }, + "node_modules/react-loader-spinner": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz", + "integrity": "sha512-x5h1Jcit7Qn03MuKlrWcMG9o12cp9SNDVHVJTNRi9TgtGPKcjKiXkou4NRfLAtXaFB3+Z8yZsVzONmPzhv2ErA==", + "dependencies": { + "react-is": "^18.2.0", + "styled-components": "^6.1.2" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-loader-spinner/node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/react-loader-spinner/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/react-loader-spinner/node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/react-loader-spinner/node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/react-loader-spinner/node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/react-loader-spinner/node_modules/styled-components": { + "version": "6.1.10", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.10.tgz", + "integrity": "sha512-4K8IKcn7iOt76riGLjvBhRyNPTkUKTvmnwoRFBOtJLswVvzy2VsoE2KOrfl9FJLQUYbITLJY2wfIZ3tjbkA/Zw==", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/react-loader-spinner/node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + }, "node_modules/react-remove-scroll": { "version": "2.5.5", "license": "MIT", @@ -22135,6 +22238,11 @@ "node": ">=8" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "license": "MIT",