From c3257ef8db61d493a62d838777edcb9688de911b Mon Sep 17 00:00:00 2001 From: Omar Date: Thu, 14 Nov 2024 15:41:54 +0100 Subject: [PATCH 1/5] Generate latest GraphQL schema for frontend npm run relay:generate-schema npm run format Signed-off-by: Omar --- frontend/src/api/schema.graphql | 107 +++++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 23 deletions(-) diff --git a/frontend/src/api/schema.graphql b/frontend/src/api/schema.graphql index 84a756c4c..2ffb2d778 100644 --- a/frontend/src/api/schema.graphql +++ b/frontend/src/api/schema.graphql @@ -3,6 +3,12 @@ schema { query: RootQueryType } +input ContainerCreateWithNestedImageInput { + id: ID + reference: String! + imageCredentialsId: ID +} + input UpdateCampaignRolloutMechanismPushInput { "This boolean flag determines if the Base Image will be pushed to the Device even if it already has a greater version of the Base Image." forceDowngrade: Boolean @@ -414,6 +420,9 @@ enum NetworkInterfaceTechnology { } enum ApplicationDeploymentStatus { + "The deployment is being deleted" + DELETING + "The deployment process entered an error state." ERROR @@ -829,10 +838,22 @@ type MutationError { fields: [String!] } +input ApplicationCreateInitialReleaseInput { + version: String! + containers: [ReleaseCreateContainersInput!] +} + +input ReleaseCreateContainersInput { + env: JsonString + image: ContainerCreateWithNestedImageInput + hostname: String + privileged: Boolean + restartPolicy: String +} + enum NetworkSortField { ID DRIVER - CHECK_DUPLICATE INTERNAL ENABLE_IPV6 } @@ -880,17 +901,6 @@ input NetworkFilterInternal { greaterThanOrEqual: Boolean } -input NetworkFilterCheckDuplicate { - isNil: Boolean - eq: Boolean - notEq: Boolean - in: [Boolean!] - lessThan: Boolean - greaterThan: Boolean - lessThanOrEqual: Boolean - greaterThanOrEqual: Boolean -} - input NetworkFilterDriver { isNil: Boolean eq: String @@ -921,7 +931,6 @@ input NetworkFilterInput { not: [NetworkFilterInput!] id: NetworkFilterId driver: NetworkFilterDriver - checkDuplicate: NetworkFilterCheckDuplicate internal: NetworkFilterInternal enableIpv6: NetworkFilterEnableIpv6 } @@ -934,9 +943,24 @@ input NetworkSortInput { type Network { id: ID! driver: String! - checkDuplicate: Boolean! internal: Boolean! enableIpv6: Boolean! + options: [String!]! +} + +"The result of the :create_release mutation" +type CreateReleaseResult { + "The successful result of the mutation" + result: Release + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input CreateReleaseInput { + version: String! + applicationId: ID + containers: [ReleaseCreateContainersInput!] } enum ReleaseSortField { @@ -1232,8 +1256,8 @@ type DeployReleaseResult { } input DeployReleaseInput { - deviceId: ID releaseId: ID + deviceId: ID! } enum DeploymentSortField { @@ -1277,13 +1301,13 @@ input DeploymentFilterReleaseId { input DeploymentFilterDeviceId { isNil: Boolean - eq: ID - notEq: ID - in: [ID] - lessThan: ID - greaterThan: ID - lessThanOrEqual: ID - greaterThanOrEqual: ID + eq: Int + notEq: Int + in: [Int] + lessThan: Int + greaterThan: Int + lessThanOrEqual: Int + greaterThanOrEqual: Int } input DeploymentFilterStatus { @@ -1340,6 +1364,7 @@ enum ContainerSortField { HOSTNAME ENV PRIVILEGED + NETWORK_MODE IMAGE_ID } @@ -1375,6 +1400,19 @@ input ContainerFilterImageId { greaterThanOrEqual: ID } +input ContainerFilterNetworkMode { + isNil: Boolean + eq: String + notEq: String + in: [String!] + lessThan: String + greaterThan: String + lessThanOrEqual: String + greaterThanOrEqual: String + like: String + ilike: String +} + input ContainerFilterPrivileged { isNil: Boolean eq: Boolean @@ -1441,6 +1479,7 @@ input ContainerFilterInput { hostname: ContainerFilterHostname env: ContainerFilterEnv privileged: ContainerFilterPrivileged + networkMode: ContainerFilterNetworkMode imageId: ContainerFilterImageId image: ImageFilterInput networks: NetworkFilterInput @@ -1458,6 +1497,7 @@ type Container { hostname: String! env: JsonString privileged: Boolean + networkMode: String! imageId: ID! image: Image! networks( @@ -1481,6 +1521,21 @@ type Container { ): NetworkConnection! } +"The result of the :create_application mutation" +type CreateApplicationResult { + "The successful result of the mutation" + result: Application + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input CreateApplicationInput { + name: String! + description: String + initialRelease: ApplicationCreateInitialReleaseInput +} + enum ApplicationSortField { ID NAME @@ -3688,8 +3743,11 @@ type RootMutationType { "Deletes a system model." deleteSystemModel(id: ID!): DeleteSystemModelResult + "Create a new application." + createApplication(input: CreateApplicationInput!): CreateApplicationResult + "Deploy the application on a device" - deployRelease(input: DeployReleaseInput): DeployReleaseResult + deployRelease(input: DeployReleaseInput!): DeployReleaseResult "Sends a :start command to the release on the device." startDeployment(id: ID!): StartDeploymentResult @@ -3704,6 +3762,9 @@ type RootMutationType { deleteImageCredentials(id: ID!): DeleteImageCredentialsResult + "Create a new release." + createRelease(input: CreateReleaseInput!): CreateReleaseResult + "Create a new base image in a base image collection." createBaseImage(input: CreateBaseImageInput!): CreateBaseImageResult From 8fd8020e8a64bb8ece972337559be15f079e9c07 Mon Sep 17 00:00:00 2001 From: Omar Date: Fri, 15 Nov 2024 08:46:23 +0100 Subject: [PATCH 2/5] Expand Deployed Applications status list with deleting status expand the list of supported statuses for deployed applications with `DELETING` status Signed-off-by: Omar --- frontend/src/components/DeployedApplicationsTable.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/components/DeployedApplicationsTable.tsx b/frontend/src/components/DeployedApplicationsTable.tsx index 55397f493..30c0d4af6 100644 --- a/frontend/src/components/DeployedApplicationsTable.tsx +++ b/frontend/src/components/DeployedApplicationsTable.tsx @@ -90,6 +90,7 @@ const statusColors: Record = { STOPPING: "text-muted", STOPPED: "text-secondary", ERROR: "text-danger", + DELETING: "text-warning", }; // Define status messages for localization @@ -114,6 +115,10 @@ const statusMessages = defineMessages({ id: "components.DeployedApplicationsTable.error", defaultMessage: "Error", }, + DELETING: { + id: "components.DeployedApplicationsTable.deleting", + defaultMessage: "Deleting", + }, }); // Component to render the status with an icon and optional spin From b9c86a8a8c9486dfd86e102104f835c4b1e23c96 Mon Sep 17 00:00:00 2001 From: Omar Date: Thu, 14 Nov 2024 15:43:23 +0100 Subject: [PATCH 3/5] Add ApplicationCreate page for new application creation - Add a page with a form to create Application. - The form contains input fields for `Name` and `Description` Signed-off-by: Omar --- frontend/src/App.tsx | 2 + frontend/src/Navigation.tsx | 2 + frontend/src/components/Sidebar.tsx | 7 +- frontend/src/forms/CreateApplication.tsx | 140 ++++++++++++++++++++++ frontend/src/pages/ApplicationCreate.tsx | 144 +++++++++++++++++++++++ frontend/src/pages/Applications.tsx | 11 +- 6 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 frontend/src/forms/CreateApplication.tsx create mode 100644 frontend/src/pages/ApplicationCreate.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d82fcca72..08df9cd24 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -51,6 +51,7 @@ import Login from "pages/Login"; import Logout from "pages/Logout"; import AttemptLogin from "pages/AttemptLogin"; import Applications from "pages/Applications"; +import ApplicationCreatePage from "pages/ApplicationCreate"; import Application from "pages/Application"; import Release from "pages/Release"; @@ -94,6 +95,7 @@ const authenticatedRoutes: RouterRule[] = [ { path: Route.updateCampaignsNew, element: }, { path: Route.updateCampaignsEdit, element: }, { path: Route.applications, element: }, + { path: Route.applicationNew, element: }, { path: Route.application, element: }, { path: Route.release, element: }, { path: Route.logout, element: }, diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx index 63341ea2a..5b132b596 100644 --- a/frontend/src/Navigation.tsx +++ b/frontend/src/Navigation.tsx @@ -52,6 +52,7 @@ enum Route { updateCampaignsNew = "/update-campaigns/new", updateCampaignsEdit = "/update-campaigns/:updateCampaignId", applications = "/applications", + applicationNew = "/applications/new", application = "/applications/:applicationId", release = "/release/:releaseId", login = "/login", @@ -116,6 +117,7 @@ const generatePath = (route: ParametricRoute): string => { case Route.login: case Route.logout: case Route.applications: + case Route.applicationNew: return route.route; } }; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index ac6bef88b..dd269aaed 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -222,7 +222,12 @@ const Sidebar = () => ( /> } route={Route.applications} - activeRoutes={[Route.applications, Route.application, Route.release]} + activeRoutes={[ + Route.applications, + Route.applicationNew, + Route.application, + Route.release, + ]} /> diff --git a/frontend/src/forms/CreateApplication.tsx b/frontend/src/forms/CreateApplication.tsx new file mode 100644 index 000000000..5d99bf4a8 --- /dev/null +++ b/frontend/src/forms/CreateApplication.tsx @@ -0,0 +1,140 @@ +/* + This file is part of Edgehog. + + Copyright 2024 SECO Mind Srl + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 +*/ + +import React from "react"; +import { useForm } from "react-hook-form"; +import { FormattedMessage } from "react-intl"; +import { yupResolver } from "@hookform/resolvers/yup"; + +import Button from "components/Button"; +import Col from "components/Col"; +import Form from "components/Form"; +import Row from "components/Row"; +import Spinner from "components/Spinner"; +import Stack from "components/Stack"; +import { yup } from "forms"; + +const FormRow = ({ + id, + label, + children, +}: { + id: string; + label: React.ReactNode; + children: React.ReactNode; +}) => ( + + + {label} + + {children} + +); + +type ApplicationData = { + name: string; + description: string; +}; + +const applicationSchema = yup + .object({ + name: yup.string().required(), + description: yup.string(), + }) + .required(); + +const initialData: ApplicationData = { + name: "", + description: "", +}; + +type Props = { + isLoading?: boolean; + onSubmit: (data: ApplicationData) => void; +}; + +const CreateApplication = ({ isLoading = false, onSubmit }: Props) => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + mode: "onTouched", + defaultValues: initialData, + resolver: yupResolver(applicationSchema), + }); + + return ( +
+ + + } + > + + + {errors.name?.message && ( + + )} + + + + } + > + + + {errors.description?.message && ( + + )} + + +
+ +
+
+
+ ); +}; + +export type { ApplicationData }; + +export default CreateApplication; diff --git a/frontend/src/pages/ApplicationCreate.tsx b/frontend/src/pages/ApplicationCreate.tsx new file mode 100644 index 000000000..63c62dd0e --- /dev/null +++ b/frontend/src/pages/ApplicationCreate.tsx @@ -0,0 +1,144 @@ +/* + This file is part of Edgehog. + + Copyright 2024 SECO Mind Srl + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 +*/ + +import { useCallback, useState } from "react"; +import { FormattedMessage } from "react-intl"; +import { graphql, useMutation } from "react-relay/hooks"; + +import type { ApplicationCreate_createApplication_Mutation } from "api/__generated__/ApplicationCreate_createApplication_Mutation.graphql"; +import Alert from "components/Alert"; +import Page from "components/Page"; +import CreateApplicationForm from "forms/CreateApplication"; +import type { ApplicationData } from "forms/CreateApplication"; +import { Route, useNavigate } from "Navigation"; + +const CREATE_APPLICATION_MUTATION = graphql` + mutation ApplicationCreate_createApplication_Mutation( + $input: CreateApplicationInput! + ) { + createApplication(input: $input) { + result { + id + description + } + } + } +`; + +const ApplicationCreatePage = () => { + const [errorFeedback, setErrorFeedback] = useState(null); + const navigate = useNavigate(); + + const [createApplication, isCreatingApplication] = + useMutation( + CREATE_APPLICATION_MUTATION, + ); + + const handleCreateApplication = useCallback( + (application: ApplicationData) => { + const newApplication = { ...application }; + + createApplication({ + variables: { input: newApplication }, + onCompleted(data, errors) { + const applicationId = data.createApplication?.result?.id; + if (applicationId) { + return navigate({ + route: Route.application, + params: { applicationId }, + }); + } + if (errors) { + const errorFeedback = errors + .map(({ fields, message }) => + fields.length ? `${fields.join(" ")} ${message}` : message, + ) + .join(". \n"); + return setErrorFeedback(errorFeedback); + } + }, + onError() { + setErrorFeedback( + , + ); + }, + updater(store, data) { + if (!data?.createApplication?.result?.id) { + return; + } + + const application = store + .getRootField("createApplication") + .getLinkedRecord("result"); + const root = store.getRoot(); + + const applications = root.getLinkedRecord("applications", { + id: "root", + }); + + if (applications) { + root.setLinkedRecords( + applications + ? [ + ...(applications.getLinkedRecords("applications") || []), + application, + ] + : [application], + "applications", + ); + } + }, + }); + }, + [createApplication, navigate, errorFeedback], + ); + + return ( + + + } + /> + + setErrorFeedback(null)} + dismissible + > + {errorFeedback} + + + + + ); +}; + +export default ApplicationCreatePage; diff --git a/frontend/src/pages/Applications.tsx b/frontend/src/pages/Applications.tsx index 77a6c5f50..7e4220fbb 100644 --- a/frontend/src/pages/Applications.tsx +++ b/frontend/src/pages/Applications.tsx @@ -30,6 +30,8 @@ import Page from "components/Page"; import Center from "components/Center"; import Spinner from "components/Spinner"; import ApplicationsTable from "components/ApplicationsTable"; +import Button from "components/Button"; +import { Link, Route } from "Navigation"; const GET_APPLICATIONS_QUERY = graphql` query Applications_getApplications_Query { @@ -62,7 +64,14 @@ const ApplicationsContent = ({ defaultMessage="Applications" /> } - /> + > + + From 70f32b71cb3a944c22a20c4dde3cc495aeda84d5 Mon Sep 17 00:00:00 2001 From: Omar Date: Thu, 14 Nov 2024 15:47:03 +0100 Subject: [PATCH 4/5] Add i18n translation strings npm run i18n:extract Signed-off-by: Omar --- frontend/src/i18n/langs/en.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/frontend/src/i18n/langs/en.json b/frontend/src/i18n/langs/en.json index 74b99b3ce..febdecf6f 100644 --- a/frontend/src/i18n/langs/en.json +++ b/frontend/src/i18n/langs/en.json @@ -246,6 +246,15 @@ "defaultMessage": "Port Bindings", "description": "Title for the Port Bindings column of the container table" }, + "components.CreateApplication.descriptionLabel": { + "defaultMessage": "Description" + }, + "components.CreateApplication.nameLabel": { + "defaultMessage": "Name" + }, + "components.CreateApplication.submitButton": { + "defaultMessage": "Create" + }, "components.CreateBaseImageCollectionForm.handleLabel": { "defaultMessage": "Handle" }, @@ -329,6 +338,9 @@ "components.DeployedApplicationsTable.applicationName": { "defaultMessage": "Application Name" }, + "components.DeployedApplicationsTable.deleting": { + "defaultMessage": "Deleting" + }, "components.DeployedApplicationsTable.error": { "defaultMessage": "Error" }, @@ -1011,9 +1023,18 @@ "pages.Application.applicationNotFound.message": { "defaultMessage": "Return to the applications list." }, + "pages.Applications.createButton": { + "defaultMessage": "Create Application" + }, "pages.Applications.title": { "defaultMessage": "Applications" }, + "pages.ApplicationCreate.creationErrorFeedback": { + "defaultMessage": "Could not create the application, please try again." + }, + "pages.ApplicationCreate.title": { + "defaultMessage": "Create Application" + }, "pages.BaseImage.baseImageNotFound.message": { "defaultMessage": "Return to the Base Image Collection." }, From d56197bafaa51f20d582cdf5a0abdb97ccb31cb0 Mon Sep 17 00:00:00 2001 From: Omar Date: Thu, 14 Nov 2024 15:48:40 +0100 Subject: [PATCH 5/5] Update CHANGELOG Update CHANGELOG Signed-off-by: Omar --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb1cfa598..3027c3cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Applications page**: Displays a list of all existing applications, with navigation to individual Application pages. - **Application page**: Shows the details of a selected application, including its name, description and a list of releases, with navigation to individual Release pages. - **Release page**: Provides details of a specific release, including a list of containers and configurations, such as image reference, image credentials (label, username), networks, and port bindings. +- Added `ApplicationCreate` page to enable users to create a new application with fields for application name and description. ## [0.9.1] - 2024-10-28 ### Fixed