From 320506af6831bbf58863ac757b7ce1b6abf7f880 Mon Sep 17 00:00:00 2001 From: Ivan Li Date: Mon, 4 Oct 2021 09:54:10 +0800 Subject: [PATCH] feat(pipelines): runtime configuration. --- .vscode/settings.json | 3 +- package-lock.json | 57 ++- package.json | 2 + src/App.tsx | 3 + src/commons/form/yaml-editor/yaml-editor.tsx | 28 ++ src/generated/graphql.schema.json | 343 ++++++++++++++++++- src/generated/graphql.tsx | 38 ++ src/pipelines/index.ts | 1 + src/pipelines/mutations.ts | 9 + src/pipelines/pipeline-list.tsx | 28 +- src/pipelines/queries.ts | 20 +- src/pipelines/runtime-config-editor.tsx | 49 +++ src/routes.tsx | 38 +- 13 files changed, 595 insertions(+), 24 deletions(-) create mode 100644 src/commons/form/yaml-editor/yaml-editor.tsx create mode 100644 src/pipelines/runtime-config-editor.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index e89a70f..b7ada13 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,9 @@ { "cSpell.words": [ - "Formik", "clsx", "fontsource", + "Formik", + "noconflict", "notistack", "unmount", "vditor" diff --git a/package-lock.json b/package-lock.json index a90a829..c687e6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@types/node": "^12.20.10", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.3", + "ace-builds": "^1.4.13", "apollo-link-scalars": "^2.1.3", "configuration": "file:../configuration", "date-fns": "^2.21.1", @@ -42,6 +43,7 @@ "notistack": "^1.0.6", "ramda": "^0.27.1", "react": "^17.0.2", + "react-ace": "^9.4.4", "react-dom": "^17.0.2", "react-scripts": "4.0.3", "typescript": "^4.2.4", @@ -6711,6 +6713,12 @@ "node": ">= 0.6" } }, + "node_modules/ace-builds": { + "version": "1.4.13", + "resolved": "https://npm.ivanli.cc/ace-builds/-/ace-builds-1.4.13.tgz", + "integrity": "sha512-SOLzdaQkY6ecPKYRDDg+MY1WoGgXA34cIvYJNNoBMGGUswHmlauU2Hy0UL96vW0Fs/LgFbMUjD+6vqzWTldIYQ==", + "license": "BSD-3-Clause" + }, "node_modules/acorn": { "version": "7.4.1", "resolved": "https://npm.ivanli.cc/acorn/-/acorn-7.4.1.tgz", @@ -18170,7 +18178,6 @@ "version": "4.4.2", "resolved": "https://npm.ivanli.cc/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true, "license": "MIT" }, "node_modules/lodash.has": { @@ -18193,6 +18200,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://npm.ivanli.cc/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "license": "MIT" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://npm.ivanli.cc/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -22177,6 +22190,23 @@ "node": ">=0.10.0" } }, + "node_modules/react-ace": { + "version": "9.4.4", + "resolved": "https://npm.ivanli.cc/react-ace/-/react-ace-9.4.4.tgz", + "integrity": "sha512-gHSH4Wm+jE28pcr4hXKeSwG2DHPZhVlfAfR+VI/dgStnUFWJMPClpRI8emLEYEK/g4cZ4cHY80HTfFwfc4020w==", + "license": "MIT", + "dependencies": { + "ace-builds": "^1.4.12", + "diff-match-patch": "^1.0.5", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0", + "react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/react-app-polyfill": { "version": "2.0.0", "resolved": "https://npm.ivanli.cc/react-app-polyfill/-/react-app-polyfill-2.0.0.tgz", @@ -33853,6 +33883,11 @@ "negotiator": "0.6.2" } }, + "ace-builds": { + "version": "1.4.13", + "resolved": "https://npm.ivanli.cc/ace-builds/-/ace-builds-1.4.13.tgz", + "integrity": "sha512-SOLzdaQkY6ecPKYRDDg+MY1WoGgXA34cIvYJNNoBMGGUswHmlauU2Hy0UL96vW0Fs/LgFbMUjD+6vqzWTldIYQ==" + }, "acorn": { "version": "7.4.1", "resolved": "https://npm.ivanli.cc/acorn/-/acorn-7.4.1.tgz", @@ -42172,8 +42207,7 @@ "lodash.get": { "version": "4.4.2", "resolved": "https://npm.ivanli.cc/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, "lodash.has": { "version": "4.5.2", @@ -42192,6 +42226,11 @@ "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=", "dev": true }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://npm.ivanli.cc/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "lodash.isinteger": { "version": "4.0.4", "resolved": "https://npm.ivanli.cc/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -45078,6 +45117,18 @@ "object-assign": "^4.1.1" } }, + "react-ace": { + "version": "9.4.4", + "resolved": "https://npm.ivanli.cc/react-ace/-/react-ace-9.4.4.tgz", + "integrity": "sha512-gHSH4Wm+jE28pcr4hXKeSwG2DHPZhVlfAfR+VI/dgStnUFWJMPClpRI8emLEYEK/g4cZ4cHY80HTfFwfc4020w==", + "requires": { + "ace-builds": "^1.4.12", + "diff-match-patch": "^1.0.5", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "prop-types": "^15.7.2" + } + }, "react-app-polyfill": { "version": "2.0.0", "resolved": "https://npm.ivanli.cc/react-app-polyfill/-/react-app-polyfill-2.0.0.tgz", diff --git a/package.json b/package.json index 9dc96ed..92e8bc6 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@types/node": "^12.20.10", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.3", + "ace-builds": "^1.4.13", "apollo-link-scalars": "^2.1.3", "configuration": "file:../configuration", "date-fns": "^2.21.1", @@ -37,6 +38,7 @@ "notistack": "^1.0.6", "ramda": "^0.27.1", "react": "^17.0.2", + "react-ace": "^9.4.4", "react-dom": "^17.0.2", "react-scripts": "4.0.3", "typescript": "^4.2.4", diff --git a/src/App.tsx b/src/App.tsx index 165b373..cc60781 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,9 @@ function App() { const { response } = useResponse(); const { body: Body } = response; + if (!Body) { + return Client has some wrong!; + } return ( diff --git a/src/commons/form/yaml-editor/yaml-editor.tsx b/src/commons/form/yaml-editor/yaml-editor.tsx new file mode 100644 index 0000000..4d8d04b --- /dev/null +++ b/src/commons/form/yaml-editor/yaml-editor.tsx @@ -0,0 +1,28 @@ +import { FC } from "@curi/react-universal/node_modules/@types/react"; +import { FieldConfig, useField } from "formik"; +import AceEditor from "react-ace"; +import "ace-builds/src-noconflict/mode-yaml"; +import "ace-builds/src-noconflict/theme-github"; +import { styled } from "@mui/system"; + +const Editor: FC & { label?: string; className?: string }> = + ({ label, className, ...props }) => { + const [field, , helper] = useField(props); + return ( + helper.setValue(value)} + {...props} + mode="yaml" + theme="github" + editorProps={{ $blockScrolling: true }} + /> + ); + }; + +export const YamlEditor = Editor; +styled(Editor)(({ theme }) => ({ + marginBottom: theme.spacing(2), +})); diff --git a/src/generated/graphql.schema.json b/src/generated/graphql.schema.json index 13deebf..3a3ea36 100644 --- a/src/generated/graphql.schema.json +++ b/src/generated/graphql.schema.json @@ -167,6 +167,178 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "Configuration", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pipeline", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Pipeline", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pipelineId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "project", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Project", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "projectId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "content", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "language", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "ConfigurationLanguage", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "ConfigurationLanguage", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "JavaScript", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "YAML", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "CreatePipelineInput", @@ -844,6 +1016,39 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "setConfiguration", + "description": null, + "args": [ + { + "name": "setConfigurationInput", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "SetConfigurationInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Configuration", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -978,16 +1183,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "SCALAR", - "name": "ID", - "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "PipelineTask", @@ -1783,6 +1978,59 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "getConfiguration", + "description": null, + "args": [ + { + "name": "pipelineId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "projectId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Configuration", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -1790,6 +2038,81 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "SetConfigurationInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "pipelineId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "projectId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "content", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "language", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "ConfigurationLanguage", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "Subscription", diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx index 736d229..589e84d 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -26,6 +26,23 @@ export type Commit = { tasks: Array; }; +export type Configuration = { + __typename?: 'Configuration'; + id: Scalars['ID']; + pipeline: Pipeline; + pipelineId: Scalars['String']; + project: Project; + projectId: Scalars['String']; + content: Scalars['String']; + name: Scalars['String']; + language: ConfigurationLanguage; +}; + +export enum ConfigurationLanguage { + JavaScript = 'JavaScript', + Yaml = 'YAML' +} + export type CreatePipelineInput = { projectId: Scalars['String']; branch: Scalars['String']; @@ -75,6 +92,7 @@ export type Mutation = { deletePipeline: Scalars['Float']; createPipelineTask: PipelineTask; stopPipelineTask: Scalars['Boolean']; + setConfiguration: Configuration; }; @@ -117,6 +135,11 @@ export type MutationStopPipelineTaskArgs = { id: Scalars['String']; }; + +export type MutationSetConfigurationArgs = { + setConfigurationInput: SetConfigurationInput; +}; + export type Pipeline = { __typename?: 'Pipeline'; id: Scalars['ID']; @@ -191,6 +214,7 @@ export type Query = { commits?: Maybe>; listPipelineTaskByPipelineId: Array; pipelineTask: PipelineTask; + getConfiguration: Configuration; }; @@ -223,6 +247,20 @@ export type QueryPipelineTaskArgs = { id: Scalars['String']; }; + +export type QueryGetConfigurationArgs = { + pipelineId?: Maybe; + projectId?: Maybe; + id?: Maybe; +}; + +export type SetConfigurationInput = { + pipelineId: Scalars['String']; + projectId: Scalars['String']; + content: Scalars['String']; + language: ConfigurationLanguage; +}; + export type Subscription = { __typename?: 'Subscription'; syncCommits?: Maybe; diff --git a/src/pipelines/index.ts b/src/pipelines/index.ts index 91c55d3..6199c89 100644 --- a/src/pipelines/index.ts +++ b/src/pipelines/index.ts @@ -1,3 +1,4 @@ export * from './pipeline-detail'; export * from './pipeline-list'; export * from './queries'; +export * from "./runtime-config-editor"; diff --git a/src/pipelines/mutations.ts b/src/pipelines/mutations.ts index bc3795b..d012b8c 100644 --- a/src/pipelines/mutations.ts +++ b/src/pipelines/mutations.ts @@ -41,3 +41,12 @@ export const DELETE_PIPELINE = gql` deletePipeline(id: $id) } `; + + +export const SET_CONFIGURATION = gql` + mutation SetConfiguration($configuration: SetConfigurationInput!) { + setConfiguration(setConfigurationInput: $configuration) { + id + } + } +`; \ No newline at end of file diff --git a/src/pipelines/pipeline-list.tsx b/src/pipelines/pipeline-list.tsx index a2a983a..7d34d17 100644 --- a/src/pipelines/pipeline-list.tsx +++ b/src/pipelines/pipeline-list.tsx @@ -5,17 +5,13 @@ import { ListItem, Typography, ListItemText, - ListItemSecondaryAction, - IconButton, Menu, MenuItem, PopoverPosition, - styled, } from "@mui/material"; -import { FC, MouseEventHandler, useMemo, useState, MouseEvent } from "react"; +import { FC, useMemo, useState, MouseEvent } from "react"; import { Pipeline, Project } from "../generated/graphql"; -import { CallMerge, Edit } from "@mui/icons-material"; -import { any, values } from "ramda"; +import { CallMerge } from "@mui/icons-material"; import { Divider } from "@material-ui/core"; interface Props { @@ -79,6 +75,17 @@ export const PipelineList: FC = ({ projectId }) => { }), }); }; + const modifyRuntimeConfiguration = () => { + navigate({ + url: url({ + name: "edit-runtime-configuration", + params: { + pipelineId: contextMenu![1].id, + projectId: contextMenu![1].project.id, + }, + }), + }); + }; return ( <> @@ -102,7 +109,14 @@ export const PipelineList: FC = ({ projectId }) => { anchorReference="anchorPosition" anchorPosition={contextMenu?.[0]} > - Runtime Config + { + modifyRuntimeConfiguration(); + handleClose(); + }} + > + Runtime Config + { modify(); diff --git a/src/pipelines/queries.ts b/src/pipelines/queries.ts index e6116a5..d1d1ca7 100644 --- a/src/pipelines/queries.ts +++ b/src/pipelines/queries.ts @@ -1,4 +1,4 @@ -import { gql } from '@apollo/client'; +import { gql } from "@apollo/client"; export const PIPELINE = gql` query Pipeline($id: String!) { @@ -16,4 +16,20 @@ export const PIPELINE = gql` } } } -`; \ No newline at end of file +`; + +export const CONFIGURATION = gql` + query QueryGetConfigurationArgs($pipelineId: String!, $projectId: String!) { + configuration: getConfiguration( + pipelineId: $pipelineId + projectId: $projectId + ) { + id + content + language + pipelineId + projectId + name + } + } +`; diff --git a/src/pipelines/runtime-config-editor.tsx b/src/pipelines/runtime-config-editor.tsx new file mode 100644 index 0000000..ed37ce4 --- /dev/null +++ b/src/pipelines/runtime-config-editor.tsx @@ -0,0 +1,49 @@ +import { useMutation } from "@apollo/client"; +import { useRouter } from "@curi/react-universal"; +import { Button, Paper } from "@mui/material"; +import { Form, Formik } from "formik"; +import { FC, useCallback } from "react"; +import { YamlEditor } from "../commons/form/yaml-editor/yaml-editor"; +import { Configuration } from "../generated/graphql"; +import { SET_CONFIGURATION } from "./mutations"; + +type Values = Configuration; + +interface Props { + configuration: Configuration; +} +export const RuntimeConfigEditor: FC = ({ configuration }) => { + const { history } = useRouter(); + const [setConfiguration] = + useMutation<{ configuration: Configuration }>(SET_CONFIGURATION); + const submitForm = useCallback( + async (values: Values) => { + await setConfiguration({ variables: { configuration: values } }); + history.go(-1); + }, + [setConfiguration, history] + ); + return ( + + initialValues={configuration ?? {}} onSubmit={submitForm}> + {({ submitForm, isSubmitting, values }) => { + return ( +
+ + + + ); + }} + +
+ ); +}; diff --git a/src/routes.tsx b/src/routes.tsx index b439aed..b786038 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -7,13 +7,15 @@ import { CommitList } from "./commits/commit-list"; import { PipelineTaskDetail } from "./pipeline-tasks/pipeline-task-detail"; import { PipelineEditor } from "./pipelines/pipeline-editor"; import { + Configuration, + ConfigurationLanguage, CreatePipelineInput, CreateProjectInput, Pipeline, PipelineUnits, Project, } from "./generated/graphql"; -import { PIPELINE } from "./pipelines"; +import { CONFIGURATION, PIPELINE, RuntimeConfigEditor } from "./pipelines"; export default prepareRoutes([ { @@ -108,6 +110,40 @@ export default prepareRoutes([ return resolved; }, }, // edit-pipeline + { + name: "edit-runtime-configuration", + path: "projects/:projectId/pipelines/:pipelineId/runtime-configuration", + async resolve( + matched, + { client }: { client: ApolloClient } + ) { + const { data } = await client.query<{ configuration: Configuration }>({ + query: CONFIGURATION, + variables: { + pipelineId: matched?.params.pipelineId, + projectId: matched?.params.projectId, + }, + }); + return { + body: () => ( + + ), + }; + }, + respond({ resolved }) { + return resolved; + }, + }, // edit-pipeline { name: "project-detail", path: "projects/:projectId",