diff --git a/client/.eslintrc.cjs b/client/.eslintrc.cjs index 209a8ec..c3a07e6 100644 --- a/client/.eslintrc.cjs +++ b/client/.eslintrc.cjs @@ -47,6 +47,7 @@ module.exports = { ], 'unicorn/no-null': 0, 'unicorn/prevent-abbreviations': 0, + 'unicorn/prefer-add-event-listener': 0, 'unicorn/prefer-query-selector': 0, 'import/no-unresolved': [ 2, diff --git a/client/package.json b/client/package.json index 9a58df9..52789b3 100644 --- a/client/package.json +++ b/client/package.json @@ -42,13 +42,15 @@ "react-hook-form": "^7.51.5", "react-i18next": "^14.1.2", "react-router-dom": "^6.23.1", - "reactflow": "^11.11.3" + "reactflow": "^11.11.3", + "websocket": "^1.0.35" }, "devDependencies": { "@types/lodash": "^4", "@types/node": "^18.19.34", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/websocket": "^1.0.10", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^7.12.0", "@vitejs/plugin-react": "^4.3.0", diff --git a/client/src/App.tsx b/client/src/App.tsx index 8e68e3e..6195241 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -6,7 +6,6 @@ import routerBindings, { NavigateToResource, UnsavedChangesNotifier, } from '@refinedev/react-router-v6' -import * as React from 'react' import { BrowserRouter, Outlet, Route, Routes } from 'react-router-dom' import { ThemedLayoutV2 } from './components/layout' @@ -14,6 +13,8 @@ import { ThemedHeaderV2 } from './components/layout/Header' import { ThemedSiderV2 } from './components/layout/Sider' import { ThemedTitleV2 } from './components/layout/Title' import { ActionProvider } from './context/ActionContext' +import AppProvider from './context/AppContext' +import { liveProvider } from './live-provider' import { ActionList, ActionShow } from './pages/actions' import { FlowShow } from './pages/flow' import { dataProvider as launchrDataProvider } from './rest-data-provider' @@ -23,73 +24,70 @@ const apiUrl = import.meta.env.VITE_API_URL export function App() { return ( - - - - - - - - - } + + + + + + - } - /> - - } /> - } /> - {/*} />*/} - {/*} />*/} - - + - - + + + } - /> - - } /> - - + > + } + /> + + } /> + } /> + {/*} />*/} + {/*} />*/} + + + } /> + + } /> + + - - - - - - - + + + + + + + + + ) } diff --git a/client/src/components/FormFlow.tsx b/client/src/components/FormFlow.tsx index 6a42f1e..c67d4e8 100644 --- a/client/src/components/FormFlow.tsx +++ b/client/src/components/FormFlow.tsx @@ -2,7 +2,13 @@ import Box from '@mui/material/Box' import Button from '@mui/material/Button' import Divider from '@mui/material/Divider' import Typography from '@mui/material/Typography' -import { useApiUrl, useCustomMutation, useOne } from '@refinedev/core' +import { + useApiUrl, + useCustomMutation, + useNotification, + useOne, + usePublish, +} from '@refinedev/core' import type { IChangeEvent } from '@rjsf/core' import { withTheme } from '@rjsf/core' import { Theme } from '@rjsf/mui' @@ -13,10 +19,12 @@ import { TitleFieldProps, } from '@rjsf/utils' import validator from '@rjsf/validator-ajv8' -import { type FC, useState } from 'react' +import merge from 'lodash/merge' +import { type FC, useContext, useEffect, useState } from 'react' -import { useStartAction } from '../hooks/ActionHooks' +import { AppContext } from '../context/AppContext' import type { IActionData, IFormValues } from '../types' +import { customizeUiSchema } from '../utils/helpers' const Form = withTheme(Theme) @@ -37,70 +45,97 @@ function TitleFieldTemplate< export const FormFlow: FC<{ actionId: string }> = ({ actionId }) => { const [actionRunning, setActionRunning] = useState(false) - const startAction = useStartAction() const apiUrl = useApiUrl() + const publish = usePublish() + const { addAction } = useContext(AppContext) const { mutateAsync } = useCustomMutation() + const { open } = useNotification() const queryResult = useOne({ resource: 'actions', id: actionId, }) + const { isFetching, data } = queryResult - const { isFetching } = queryResult - - const jsonschema = queryResult?.data?.data?.jsonschema || {} - - const uischema = queryResult?.data?.data?.uischema?.uiSchema || {} + // Fetch schema and customize uiSchema + const jsonschema = data?.data?.jsonschema + let uischema = { ...data?.data?.uischema?.uiSchema } if (jsonschema) { - // @todo I actually don't know for the moment how to overcome error - // "no schema with key or ref" produced when schema is defined. - // Maybe it's because the server returns "2020-12" and default is "draft-07" - // @see https://ajv.js.org/json-schema.html delete jsonschema.$schema + uischema = merge({}, uischema, customizeUiSchema(jsonschema)) } - const onSubmit = async ( - { formData }: IChangeEvent - // e: FormEvent, - ) => { - if (!formData) { - return + useEffect(() => { + if (!jsonschema && !isFetching && open) { + open({ + type: 'error', + message: 'Schema not found', + description: 'The action schema could not be retrieved.', + }) } + }, [jsonschema, isFetching, open]) - setActionRunning(true) + const onSubmit = async ({ formData }: IChangeEvent) => { + if (!formData) return - startAction(actionId) + setActionRunning(true) + publish?.({ + channel: 'processes', + type: 'get-processes', + payload: { action: actionId }, + date: new Date(), + }) - await mutateAsync( - { + try { + const result = await mutateAsync({ url: `${apiUrl}/actions/${actionId}`, method: 'post', values: formData, - }, - { - onError: () => { - console.log('error') + successNotification: { + message: 'Action successfully created.', + description: 'Success with no errors', + type: 'success', }, - onSuccess: (data) => { - console.log(data) + errorNotification: { + message: 'Error.', + description: 'Something went wrong', + type: 'error', }, + }) + + if (result && actionId) { + addAction({ + id: actionId.toString(), + title: jsonschema?.title, + description: jsonschema?.description, + }) + publish?.({ + channel: 'process', + type: 'get-process', + payload: { action: result.data.id }, + date: new Date(), + }) } - ) + } catch (error) { + console.error('Error creating action:', error) + } finally { + setActionRunning(false) + } } return ( <> {!isFetching && (
- +
)} diff --git a/client/src/types.ts b/client/src/types.ts index dc7fab4..9e77f1e 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -1,13 +1,24 @@ import type { BaseRecord } from '@refinedev/core' import type { RJSFSchema, UiSchema } from '@rjsf/utils' -type IFlowNodeType = 'node-start' | 'node-wrapper' | 'node-action' +type ActionState = 'created' | 'running' | 'finished' | 'error' +type IFlowNodeType = 'node-start' | 'node-wrapper' | 'node-action' interface IAction { id: string title?: string description?: string } + +interface IActionProcess { + id: string + status: ActionState +} + +interface IActionWithRunInfo extends IAction { + processes: IActionProcess[] +} + interface IActionData extends BaseRecord { jsonschema: RJSFSchema uischema: UiSchema @@ -17,4 +28,12 @@ interface IFormValues { id: string } -export type { IAction, IActionData, IFlowNodeType, IFormValues } +export type { + ActionState, + IAction, + IActionData, + IActionProcess, + IActionWithRunInfo, + IFlowNodeType, + IFormValues, +} diff --git a/client/src/utils/helpers.tsx b/client/src/utils/helpers.tsx index 7b90cd4..165bc37 100644 --- a/client/src/utils/helpers.tsx +++ b/client/src/utils/helpers.tsx @@ -37,3 +37,12 @@ export const customizeUiSchema = ( return uiSchema } + +export const extractDateTimeFromId = (id: string) => { + const [timestampStr] = id.split('-') + const timestamp = Number.parseInt(timestampStr, 10) + const date = new Date(timestamp * 1000) + const formattedDate = date.toLocaleString() + + return formattedDate +} diff --git a/client/yarn.lock b/client/yarn.lock index 138c6b0..fe741c1 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -3054,6 +3054,15 @@ __metadata: languageName: node linkType: hard +"@types/websocket@npm:^1.0.10": + version: 1.0.10 + resolution: "@types/websocket@npm:1.0.10" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/5950b8d01d1178c67c049f482fcab182085c59c2f98edda5980721f6eb512439ff91534e50ca7262720d75fc42ea6c8f8e5e7739442feea8f3cc0e320ebe2c74 + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:^5.62.0": version: 5.62.0 resolution: "@typescript-eslint/eslint-plugin@npm:5.62.0" @@ -3754,6 +3763,16 @@ __metadata: languageName: node linkType: hard +"bufferutil@npm:^4.0.1": + version: 4.0.8 + resolution: "bufferutil@npm:4.0.8" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10c0/36cdc5b53a38d9f61f89fdbe62029a2ebcd020599862253fefebe31566155726df9ff961f41b8c97b02b4c12b391ef97faf94e2383392654cf8f0ed68f76e47c + languageName: node + linkType: hard + "builtin-modules@npm:^3.3.0": version: 3.3.0 resolution: "builtin-modules@npm:3.3.0" @@ -4309,6 +4328,16 @@ __metadata: languageName: node linkType: hard +"d@npm:1, d@npm:^1.0.1, d@npm:^1.0.2": + version: 1.0.2 + resolution: "d@npm:1.0.2" + dependencies: + es5-ext: "npm:^0.10.64" + type: "npm:^2.7.2" + checksum: 10c0/3e6ede10cd3b77586c47da48423b62bed161bf1a48bdbcc94d87263522e22f5dfb0e678a6dba5323fdc14c5d8612b7f7eb9e7d9e37b2e2d67a7bf9f116dabe5a + languageName: node + linkType: hard + "data-view-buffer@npm:^1.0.1": version: 1.0.1 resolution: "data-view-buffer@npm:1.0.1" @@ -4365,7 +4394,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:2.6.9": +"debug@npm:2.6.9, debug@npm:^2.2.0": version: 2.6.9 resolution: "debug@npm:2.6.9" dependencies: @@ -4806,6 +4835,39 @@ __metadata: languageName: node linkType: hard +"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.62, es5-ext@npm:^0.10.63, es5-ext@npm:^0.10.64, es5-ext@npm:~0.10.14": + version: 0.10.64 + resolution: "es5-ext@npm:0.10.64" + dependencies: + es6-iterator: "npm:^2.0.3" + es6-symbol: "npm:^3.1.3" + esniff: "npm:^2.0.1" + next-tick: "npm:^1.1.0" + checksum: 10c0/4459b6ae216f3c615db086e02437bdfde851515a101577fd61b19f9b3c1ad924bab4d197981eb7f0ccb915f643f2fc10ff76b97a680e96cbb572d15a27acd9a3 + languageName: node + linkType: hard + +"es6-iterator@npm:^2.0.3": + version: 2.0.3 + resolution: "es6-iterator@npm:2.0.3" + dependencies: + d: "npm:1" + es5-ext: "npm:^0.10.35" + es6-symbol: "npm:^3.1.1" + checksum: 10c0/91f20b799dba28fb05bf623c31857fc1524a0f1c444903beccaf8929ad196c8c9ded233e5ac7214fc63a92b3f25b64b7f2737fcca8b1f92d2d96cf3ac902f5d8 + languageName: node + linkType: hard + +"es6-symbol@npm:^3.1.1, es6-symbol@npm:^3.1.3": + version: 3.1.4 + resolution: "es6-symbol@npm:3.1.4" + dependencies: + d: "npm:^1.0.2" + ext: "npm:^1.7.0" + checksum: 10c0/777bf3388db5d7919e09a0fd175aa5b8a62385b17cb2227b7a137680cba62b4d9f6193319a102642aa23d5840d38a62e4784f19cfa5be4a2210a3f0e9b23d15d + languageName: node + linkType: hard + "esbuild@npm:^0.20.1": version: 0.20.2 resolution: "esbuild@npm:0.20.2" @@ -5148,6 +5210,18 @@ __metadata: languageName: node linkType: hard +"esniff@npm:^2.0.1": + version: 2.0.1 + resolution: "esniff@npm:2.0.1" + dependencies: + d: "npm:^1.0.1" + es5-ext: "npm:^0.10.62" + event-emitter: "npm:^0.3.5" + type: "npm:^2.7.2" + checksum: 10c0/7efd8d44ac20e5db8cb0ca77eb65eca60628b2d0f3a1030bcb05e71cc40e6e2935c47b87dba3c733db12925aa5b897f8e0e7a567a2c274206f184da676ea2e65 + languageName: node + linkType: hard + "espree@npm:^9.6.0, espree@npm:^9.6.1": version: 9.6.1 resolution: "espree@npm:9.6.1" @@ -5215,6 +5289,16 @@ __metadata: languageName: node linkType: hard +"event-emitter@npm:^0.3.5": + version: 0.3.5 + resolution: "event-emitter@npm:0.3.5" + dependencies: + d: "npm:1" + es5-ext: "npm:~0.10.14" + checksum: 10c0/75082fa8ffb3929766d0f0a063bfd6046bd2a80bea2666ebaa0cfd6f4a9116be6647c15667bea77222afc12f5b4071b68d393cf39fdaa0e8e81eda006160aff0 + languageName: node + linkType: hard + "eventemitter3@npm:^4.0.0": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" @@ -5285,6 +5369,15 @@ __metadata: languageName: node linkType: hard +"ext@npm:^1.7.0": + version: 1.7.0 + resolution: "ext@npm:1.7.0" + dependencies: + type: "npm:^2.7.2" + checksum: 10c0/a8e5f34e12214e9eee3a4af3b5c9d05ba048f28996450975b369fc86e5d0ef13b6df0615f892f5396a9c65d616213c25ec5b0ad17ef42eac4a500512a19da6c7 + languageName: node + linkType: hard + "extend-shallow@npm:^2.0.1": version: 2.0.1 resolution: "extend-shallow@npm:2.0.1" @@ -6557,6 +6650,13 @@ __metadata: languageName: node linkType: hard +"is-typedarray@npm:^1.0.0": + version: 1.0.0 + resolution: "is-typedarray@npm:1.0.0" + checksum: 10c0/4c096275ba041a17a13cca33ac21c16bc4fd2d7d7eb94525e7cd2c2f2c1a3ab956e37622290642501ff4310601e413b675cf399ad6db49855527d2163b3eeeec + languageName: node + linkType: hard + "is-unicode-supported@npm:^0.1.0": version: 0.1.0 resolution: "is-unicode-supported@npm:0.1.0" @@ -6886,6 +6986,7 @@ __metadata: "@types/node": "npm:^18.19.34" "@types/react": "npm:^18.3.3" "@types/react-dom": "npm:^18.3.0" + "@types/websocket": "npm:^1.0.10" "@typescript-eslint/eslint-plugin": "npm:^5.62.0" "@typescript-eslint/parser": "npm:^7.12.0" "@vitejs/plugin-react": "npm:^4.3.0" @@ -6917,6 +7018,7 @@ __metadata: reactflow: "npm:^11.11.3" typescript: "npm:^4.9.5" vite: "npm:^5.2.12" + websocket: "npm:^1.0.35" languageName: unknown linkType: soft @@ -7631,6 +7733,13 @@ __metadata: languageName: node linkType: hard +"next-tick@npm:^1.1.0": + version: 1.1.0 + resolution: "next-tick@npm:1.1.0" + checksum: 10c0/3ba80dd805fcb336b4f52e010992f3e6175869c8d88bf4ff0a81d5d66e6049f89993463b28211613e58a6b7fe93ff5ccbba0da18d4fa574b96289e8f0b577f28 + languageName: node + linkType: hard + "node-dir@npm:^0.1.17": version: 0.1.17 resolution: "node-dir@npm:0.1.17" @@ -7673,6 +7782,17 @@ __metadata: languageName: node linkType: hard +"node-gyp-build@npm:^4.3.0": + version: 4.8.1 + resolution: "node-gyp-build@npm:4.8.1" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: 10c0/e36ca3d2adf2b9cca316695d7687207c19ac6ed326d6d7c68d7112cebe0de4f82d6733dff139132539fcc01cf5761f6c9082a21864ab9172edf84282bc849ce7 + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 10.0.1 resolution: "node-gyp@npm:10.0.1" @@ -9770,6 +9890,13 @@ __metadata: languageName: node linkType: hard +"type@npm:^2.7.2": + version: 2.7.3 + resolution: "type@npm:2.7.3" + checksum: 10c0/dec6902c2c42fcb86e3adf8cdabdf80e5ef9de280872b5fd547351e9cca2fe58dd2aa6d2547626ddff174145db272f62d95c7aa7038e27c11315657d781a688d + languageName: node + linkType: hard + "typed-array-buffer@npm:^1.0.2": version: 1.0.2 resolution: "typed-array-buffer@npm:1.0.2" @@ -9822,6 +9949,15 @@ __metadata: languageName: node linkType: hard +"typedarray-to-buffer@npm:^3.1.5": + version: 3.1.5 + resolution: "typedarray-to-buffer@npm:3.1.5" + dependencies: + is-typedarray: "npm:^1.0.0" + checksum: 10c0/4ac5b7a93d604edabf3ac58d3a2f7e07487e9f6e98195a080e81dbffdc4127817f470f219d794a843b87052cedef102b53ac9b539855380b8c2172054b7d5027 + languageName: node + linkType: hard + "typescript@npm:^4.9.5": version: 4.9.5 resolution: "typescript@npm:4.9.5" @@ -10022,6 +10158,16 @@ __metadata: languageName: node linkType: hard +"utf-8-validate@npm:^5.0.2": + version: 5.0.10 + resolution: "utf-8-validate@npm:5.0.10" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10c0/23cd6adc29e6901aa37ff97ce4b81be9238d0023c5e217515b34792f3c3edb01470c3bd6b264096dd73d0b01a1690b57468de3a24167dd83004ff71c51cc025f + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -10185,6 +10331,20 @@ __metadata: languageName: node linkType: hard +"websocket@npm:^1.0.35": + version: 1.0.35 + resolution: "websocket@npm:1.0.35" + dependencies: + bufferutil: "npm:^4.0.1" + debug: "npm:^2.2.0" + es5-ext: "npm:^0.10.63" + typedarray-to-buffer: "npm:^3.1.5" + utf-8-validate: "npm:^5.0.2" + yaeti: "npm:^0.0.6" + checksum: 10c0/8be9a68dc0228f18058c9010d1308479f05050af8f6d68b9dbc6baebd9ab484c15a24b2521a5d742a9d78e62ee19194c532992f1047a9b9adf8c3eedb0b1fcdc + languageName: node + linkType: hard + "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0" @@ -10342,6 +10502,13 @@ __metadata: languageName: node linkType: hard +"yaeti@npm:^0.0.6": + version: 0.0.6 + resolution: "yaeti@npm:0.0.6" + checksum: 10c0/4e88702d8b34d7b61c1c4ec674422b835d453b8f8a6232be41e59fc98bc4d9ab6d5abd2da55bab75dfc07ae897fdc0c541f856ce3ab3b17de1630db6161aa3f6 + languageName: node + linkType: hard + "yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1" diff --git a/files.release.go b/files.release.go index 5aa29ae..5b58e3a 100644 --- a/files.release.go +++ b/files.release.go @@ -3,8 +3,9 @@ package web import ( - "github.com/launchrctl/web/server" "io/fs" + + "github.com/launchrctl/web/server" ) func prepareRunOption(p *Plugin, opts *server.RunOptions) { diff --git a/go.mod b/go.mod index ffe08ba..de4dc8f 100644 --- a/go.mod +++ b/go.mod @@ -34,9 +34,13 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/swag v0.22.9 // indirect + github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect + github.com/gobwas/pool v0.2.0 // indirect + github.com/gobwas/ws v1.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.2.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -55,6 +59,7 @@ require ( github.com/otiai10/copy v1.14.0 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pkgz/websocket v1.2.10 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect diff --git a/go.sum b/go.sum index 5ddda2f..9b3c6af 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,7 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8= github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= +github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= @@ -57,6 +58,12 @@ github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZC github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.0 h1:1WdyfgUcImUfVBvYbsW2krIsnko+1QU2t45soaF8v1M= +github.com/gobwas/ws v1.0.0/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= @@ -67,6 +74,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -120,6 +129,8 @@ github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkgz/websocket v1.2.10 h1:rmhfFPWIzOXEH1PgkmmKTsClKQRxdoR7qRYSm4xDa00= +github.com/pkgz/websocket v1.2.10/go.mod h1:d9K3VYbh0KuCRQM8hVUORlr2nFxZrUC1DB2762tLZkk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= @@ -135,6 +146,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= @@ -172,6 +184,7 @@ golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= diff --git a/plugin.go b/plugin.go index a7b9d00..2d8e1ae 100644 --- a/plugin.go +++ b/plugin.go @@ -3,6 +3,7 @@ package web import ( "fmt" + "github.com/launchrctl/launchr" "github.com/launchrctl/web/server" "github.com/spf13/cobra" diff --git a/server/server.go b/server/server.go index d2a872b..7db919a 100644 --- a/server/server.go +++ b/server/server.go @@ -5,13 +5,16 @@ package server import ( "context" + "encoding/json" "fmt" "io/fs" + "log" "net/http" "net/http/httputil" "net/url" "os" "path" + "sort" "strings" "time" @@ -19,6 +22,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/cors" "github.com/go-chi/render" + "github.com/gorilla/websocket" middleware "github.com/oapi-codegen/nethttp-middleware" "github.com/launchrctl/launchr" @@ -39,6 +43,8 @@ type RunOptions struct { ProxyClient string } +const asyncTickerTime = 3 + const swaggerUIPath = "/swagger-ui" const swaggerJSONPath = "/swagger.json" @@ -73,6 +79,8 @@ func Run(ctx context.Context, app launchr.App, opts *RunOptions) error { serveSwaggerUI(swagger, r, opts) } + r.HandleFunc("/ws", wsHandler(store)) + // Serve frontend files. r.HandleFunc("/*", spaHandler(opts)) @@ -144,3 +152,176 @@ func serveSwaggerUI(swagger *openapi3.T, r chi.Router, opts *RunOptions) { render.JSON(w, r, &swagger) }) } + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +type Message struct { + Message string `json:"message"` + Action string `json:"action"` +} + +func wsHandler(l *launchrServer) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Fatal(err) + } + defer ws.Close() + + for { + _, message, err := ws.ReadMessage() + if err != nil { + log.Println(err) + break + } + + var msg Message + if err := json.Unmarshal(message, &msg); err != nil { + log.Printf("Error unmarshaling message: %v", err) + continue + } + + log.Printf("Received command: %s", msg.Message) + log.Printf("Received params: %v", msg.Action) + + switch msg.Message { + case "get-processes": + go getProcesses(msg, ws, l) + case "get-streams": + go getStreams(msg, ws, l) + default: + log.Printf("Unknown command: %s", msg.Message) + } + } + } +} + +func getProcesses(msg Message, ws *websocket.Conn, l *launchrServer) { + ticker := time.NewTicker(asyncTickerTime * time.Second) + defer ticker.Stop() + + // TODO: replace that code with some listener which + // will send messages when action started or finished instead of ticker + + for range ticker.C { + + anyProccessRunning := false + + runningActions := l.actionMngr.RunInfoByAction(msg.Action) + + if len(runningActions) == 0 { + break + } + + sort.Slice(runningActions, func(i, j int) bool { + return runningActions[i].Status < runningActions[j].Status + }) + + responseMessage := map[string]interface{}{ + "message": "send-processes", + "action": msg.Action, + "processes": runningActions, + } + + finalResponse, err := json.Marshal(responseMessage) + if err != nil { + log.Printf("Error marshaling final response: %v", err) + return + } + + if err := ws.WriteMessage(websocket.TextMessage, finalResponse); err != nil { + log.Println(err) + } + + for _, ri := range runningActions { + if ri.Status == "running" { + anyProccessRunning = true + } + } + + completeMessage := map[string]interface{}{ + "message": "send-processes-finished", + } + + finalCompleteResponse, err := json.Marshal(completeMessage) + if err != nil { + log.Printf("Error marshaling final response: %v", err) + return + } + + if !anyProccessRunning { + if err := ws.WriteMessage(websocket.TextMessage, finalCompleteResponse); err != nil { + log.Println(err) + } + break + } + } +} + +func getStreams(msg Message, ws *websocket.Conn, l *launchrServer) { + ticker := time.NewTicker(asyncTickerTime * time.Second) + defer ticker.Stop() + + var lastStreamData interface{} + + for range ticker.C { + ri, _ := l.actionMngr.RunInfoByID(msg.Action) + + // Get the streams data + streams := ri.Action.GetInput().IO + fStreams, _ := streams.(fileStreams) + params := GetRunningActionStreamsParams{ + Offset: new(int), + Limit: new(int), + } + *params.Offset = 1 + *params.Limit = 1 + sd, _ := fStreams.GetStreamData(params) + + lastStreamData = sd + + if ri.Status != "running" { + break + } + + // Send the process data + responseMessage := map[string]interface{}{ + "message": "send-process", + "action": msg.Action, + "data": sd, + } + + finalResponse, err := json.Marshal(responseMessage) + if err != nil { + log.Printf("Error marshaling response: %v", err) + return + } + + if err := ws.WriteMessage(websocket.TextMessage, finalResponse); err != nil { + log.Println(err) + } + } + + // Send the final message indicating streams have finished with the last stream data + finalMessage := map[string]interface{}{ + "message": "send-process-finished", + "action": msg.Action, + "data": lastStreamData, + } + + finalResponse, err := json.Marshal(finalMessage) + if err != nil { + log.Printf("Error marshaling final message: %v", err) + return + } + + if err := ws.WriteMessage(websocket.TextMessage, finalResponse); err != nil { + log.Println(err) + } +}