diff --git a/Source/DesignSystem/atoms/LoadingSpinner/LoadingSpinner.stories.tsx b/Source/DesignSystem/atoms/LoadingSpinner/LoadingSpinner.stories.tsx index 8d8e74c83..0f5c8480e 100644 --- a/Source/DesignSystem/atoms/LoadingSpinner/LoadingSpinner.stories.tsx +++ b/Source/DesignSystem/atoms/LoadingSpinner/LoadingSpinner.stories.tsx @@ -5,7 +5,7 @@ import { componentStories, LoadingSpinner } from '@dolittle/design-system'; const { metadata, createStory } = componentStories(LoadingSpinner); -//TODO: update component to include text +// TODO: Update component to improve text. metadata.parameters = { docs: { @@ -20,26 +20,3 @@ metadata.parameters = { export default metadata; export const Default = createStory(); - -// //TODO: Implement component -// export const ErrorSpinner = createStory(); -// ErrorSpinner.parameters = { -// docs: { -// description: { -// story: `When the information is not processed successfully, we provide a clear indication in the UI to the user. -// The spinner changes to an error icon with a new description indicating what happened and how the user can resolve it. -// Use the main error color.` -// }, -// }, -// }; - -// //TODO: Implement component -// export const SuccessSpinner = createStory(); -// SuccessSpinner.parameters = { -// docs: { -// description: { -// story: `When the information has been processed successfully we provide a clear indication in the UI to the user. -// The spinner changes to a checkmark icon with a new description indicating what happened. Use the main success color.` -// }, -// }, -// }; diff --git a/Source/DesignSystem/atoms/LoadingSpinner/LoadingSpinner.tsx b/Source/DesignSystem/atoms/LoadingSpinner/LoadingSpinner.tsx index 6aac1dc58..886acc60f 100644 --- a/Source/DesignSystem/atoms/LoadingSpinner/LoadingSpinner.tsx +++ b/Source/DesignSystem/atoms/LoadingSpinner/LoadingSpinner.tsx @@ -5,13 +5,12 @@ import React from 'react'; import { Box, CircularProgress, CircularProgressProps } from '@mui/material'; +/** + * The {@link LoadingSpinner} component is used to display a loading spinner. + * @param {CircularProgress} props - The {@link CircularProgressProps}. + * @returns A {@link LoadingSpinner} component. + */ export const LoadingSpinner = (props: CircularProgressProps) => - + ; - -// TODO: Move this to a separate component. -export const FullPageLoadingSpinner = (props: CircularProgressProps) => - - - ; diff --git a/Source/DesignSystem/atoms/LoadingSpinner/index.ts b/Source/DesignSystem/atoms/LoadingSpinner/index.ts index 3f630ad8a..ff5fa9336 100644 --- a/Source/DesignSystem/atoms/LoadingSpinner/index.ts +++ b/Source/DesignSystem/atoms/LoadingSpinner/index.ts @@ -1,4 +1,4 @@ // Copyright (c) Dolittle. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -export { FullPageLoadingSpinner, LoadingSpinner } from './LoadingSpinner'; +export { LoadingSpinner } from './LoadingSpinner'; diff --git a/Source/DesignSystem/atoms/LoadingSpinnerFullPage/LoadingSpinnerFullPage.stories.tsx b/Source/DesignSystem/atoms/LoadingSpinnerFullPage/LoadingSpinnerFullPage.stories.tsx new file mode 100644 index 000000000..1cdcf34ee --- /dev/null +++ b/Source/DesignSystem/atoms/LoadingSpinnerFullPage/LoadingSpinnerFullPage.stories.tsx @@ -0,0 +1,22 @@ +// Copyright (c) Dolittle. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { componentStories, LoadingSpinnerFullPage } from '../../index'; + +const { metadata, createStory } = componentStories(LoadingSpinnerFullPage); + +// TODO: Update component to improve text. + +metadata.parameters = { + docs: { + description: { + component: `A loading spinner is a progress indicator used to inform users about the status of ongoing processes, + such as loading data or submitting a form. Provide a descriptive label that indicates the status to the user. + By default, loading spinners are in an indeterminate state.` }, + }, + layout: 'centered', +}; + +export default metadata; + +export const Default = createStory(); diff --git a/Source/DesignSystem/atoms/LoadingSpinnerFullPage/LoadingSpinnerFullPage.tsx b/Source/DesignSystem/atoms/LoadingSpinnerFullPage/LoadingSpinnerFullPage.tsx new file mode 100644 index 000000000..e29395cab --- /dev/null +++ b/Source/DesignSystem/atoms/LoadingSpinnerFullPage/LoadingSpinnerFullPage.tsx @@ -0,0 +1,22 @@ +// Copyright (c) Dolittle. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import React from 'react'; +import { createPortal } from 'react-dom'; + +import { Box, CircularProgressProps } from '@mui/material'; + +import { LoadingSpinner } from '../../index'; + +/** + * The {@link LoadingSpinnerFullPage} component is used to display a loading spinner full page. + * @param {LoadingSpinnerFullPage} props - The {@link CircularProgressProps}. + * @returns A {@link LoadingSpinnerFullPage} component. + */ +export const LoadingSpinnerFullPage = (props: CircularProgressProps) => + createPortal( + + + , + document.body, + ); diff --git a/Source/DesignSystem/atoms/LoadingSpinnerFullPage/index.ts b/Source/DesignSystem/atoms/LoadingSpinnerFullPage/index.ts new file mode 100644 index 000000000..828997cc3 --- /dev/null +++ b/Source/DesignSystem/atoms/LoadingSpinnerFullPage/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Dolittle. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +export { LoadingSpinnerFullPage } from './LoadingSpinnerFullPage'; diff --git a/Source/DesignSystem/index.ts b/Source/DesignSystem/index.ts index acd278275..c169ca814 100644 --- a/Source/DesignSystem/index.ts +++ b/Source/DesignSystem/index.ts @@ -17,7 +17,8 @@ export * from './atoms/Forms'; export { Icon, IconProps } from './atoms/Icon'; export { IconButton, IconButtonProps } from './atoms/IconButton'; export { Link, LinkProps } from './atoms/Link'; -export { FullPageLoadingSpinner, LoadingSpinner } from './atoms/LoadingSpinner'; +export { LoadingSpinner } from './atoms/LoadingSpinner'; +export { LoadingSpinnerFullPage } from './atoms/LoadingSpinnerFullPage'; export { StatusIndicator, StatusIndicatorProps } from './atoms/StatusIndicator'; export { Summary } from './atoms/Metrics'; export { Tabs } from './atoms/Tabs'; diff --git a/Source/SelfService/Web/apis/solutions/api.ts b/Source/SelfService/Web/apis/solutions/api.ts index e9547eb65..23711f8fd 100644 --- a/Source/SelfService/Web/apis/solutions/api.ts +++ b/Source/SelfService/Web/apis/solutions/api.ts @@ -105,6 +105,12 @@ export type SimpleIngressPath = { pathType: string; }; +export type InputEditMicroservice = { + displayName: string; + headImage: string; + runtimeImage: string; +}; + export type InputEnvironmentVariable = { name: string; value: string; @@ -158,20 +164,18 @@ export function getRuntimes(): LatestRuntimeInfo[] { { image: 'dolittle/runtime:6.2.4', changelog: 'https://github.com/dolittle/Runtime/releases/tag/v6.2.4', - } + }, ]; }; -// getMicroservices by applicationId export async function getMicroservices(applicationId: string): Promise { - // TODO {"message":"open /tmp/dolittle-k8s/508c1745-5f2a-4b4c-b7a5-2fbb1484346d/11b6cf47-5d9f-438f-8116-0d9828654657/application.json: no such file or directory"} const url = `${getServerUrlPrefix()}/live/application/${applicationId}/microservices`; const result = await fetch( url, { method: 'GET', - mode: 'cors' + mode: 'cors', }); const jsonResult: HttpResponseMicroservices = await result.json(); @@ -179,19 +183,6 @@ export async function getMicroservices(applicationId: string): Promise { - const url = `${getServerUrlPrefix()}/application/${applicationId}/environment/${environment}/microservice/${microserviceId}`; - - const result = await fetch( - url, - { - method: 'DELETE', - mode: 'cors' - }); - - return result.status === 200; -}; - export async function saveMicroservice(input: any): Promise { const url = `${getServerUrlPrefix()}/microservice`; @@ -202,25 +193,42 @@ export async function saveMicroservice(input: any): Promise { body: JSON.stringify(input), mode: 'cors', headers: { - 'content-type': 'application/json' - } + 'content-type': 'application/json', + }, }); const text = await response.text(); if (!response.ok) { let jsonResponse; + try { jsonResponse = JSON.parse(text); } catch (error) { throw new Exception(`Couldn't parse the error message. The error was ${error}. Response Status ${response.status}. Response Body ${text}`); - } - throw new Exception(jsonResponse.message); + } throw new Exception(jsonResponse.message); }; return JSON.parse(text); }; +export async function editMicroservice(applicationId: string, environment: string, microserviceId: string, input: InputEditMicroservice): Promise { + const url = `${getServerUrlPrefix()}/application/${applicationId}/environment/${environment}/microservice/${microserviceId}`; + + const response = await fetch( + url, + { + method: 'PATCH', + body: JSON.stringify(input), + mode: 'cors', + headers: { + 'content-type': 'application/json', + }, + }); + + return response.status === 200; +}; + export async function restartMicroservice(applicationId: string, environment: string, microserviceId: string): Promise { const url = `${getServerUrlPrefix()}/live/application/${applicationId}/environment/${environment.toLowerCase()}/microservice/${microserviceId}/restart`; @@ -228,12 +236,25 @@ export async function restartMicroservice(applicationId: string, environment: st url, { method: 'DELETE', - mode: 'cors' + mode: 'cors', }); return response.status === 200; }; +export async function deleteMicroservice(applicationId: string, environment: string, microserviceId: string): Promise { + const url = `${getServerUrlPrefix()}/application/${applicationId}/environment/${environment}/microservice/${microserviceId}`; + + const result = await fetch( + url, + { + method: 'DELETE', + mode: 'cors', + }); + + return result.status === 200; +}; + export async function getPodStatus(applicationId: string, environment: string, microserviceId: string): Promise { const url = `${getServerUrlPrefix()}/live/application/${applicationId}/environment/${environment.toLowerCase()}/microservice/${microserviceId}/podstatus`; @@ -241,7 +262,7 @@ export async function getPodStatus(applicationId: string, environment: string, m url, { method: 'GET', - mode: 'cors' + mode: 'cors', }); const jsonResult: HttpResponsePodStatus = await result.json(); @@ -260,7 +281,7 @@ export async function getPodLogs(applicationId: string, podName: string, contain url, { method: 'GET', - mode: 'cors' + mode: 'cors', }); const jsonResult: HttpResponsePodLog = await result.json(); @@ -275,7 +296,7 @@ export async function getEnvironmentVariables(applicationId: string, environment url, { method: 'GET', - mode: 'cors' + mode: 'cors', }); const data = await response.json(); @@ -287,7 +308,7 @@ export async function updateEnvironmentVariables(applicationId: string, environm const url = `${getServerUrlPrefix()}/live/application/${applicationId}/environment/${environment}/microservice/${microserviceId}/environment-variables`; const data = { - data: input + data: input, } as HttpInputEnvironmentVariables; const response = await fetch( @@ -297,8 +318,8 @@ export async function updateEnvironmentVariables(applicationId: string, environm body: JSON.stringify(data), mode: 'cors', headers: { - 'content-type': 'application/json' - } + 'content-type': 'application/json', + }, }); return response.status === 200; @@ -335,12 +356,11 @@ export async function updateConfigFile(applicationId: string, environment: strin return response.status === 200 ? { success: true } : { success: false, error: parsedResponse.message }; }; - export async function deleteConfigFile(applicationId: string, environment: string, microserviceId: string, fileName: string): Promise { const url = `${getServerUrlPrefix()}/live/application/${applicationId}/environment/${environment}/microservice/${microserviceId}/config-files`; const data = { - key: fileName + key: fileName, } as HttpInputDeleteConfigFile; const response = await fetch( @@ -348,7 +368,7 @@ export async function deleteConfigFile(applicationId: string, environment: strin { method: 'DELETE', mode: 'cors', - body: JSON.stringify(data) + body: JSON.stringify(data), }); return response.status === 200; @@ -359,12 +379,12 @@ export async function parseJSONResponse(response: any): Promise { if (!response.ok) { let jsonResponse; + try { jsonResponse = JSON.parse(text); } catch (error) { throw Error(`Couldn't parse the error message. The error was ${error}. Response Status ${response.status}. Response Body ${text}`); - } - throw Error(jsonResponse.message); + } throw Error(jsonResponse.message); }; const data = JSON.parse(text); diff --git a/Source/SelfService/Web/apis/solutions/application.ts b/Source/SelfService/Web/apis/solutions/application.ts index b39186219..5e7458884 100644 --- a/Source/SelfService/Web/apis/solutions/application.ts +++ b/Source/SelfService/Web/apis/solutions/application.ts @@ -78,7 +78,6 @@ export async function getPersonalisedInfo(applicationId: string): Promise { return jsonResult; }; - export async function createApplication(input: HttpApplicationRequest): Promise { const url = `${getServerUrlPrefix()}/application`; @@ -142,7 +141,6 @@ export async function getApplications(): Promise { return jsonResult; }; - export async function getApplicationsListing(): Promise { const applicationsWithEnvironment = await getApplications(); @@ -154,7 +152,7 @@ export async function getApplicationsListing(): Promise { const url = `${getServerUrlPrefix()}/application/${applicationId}`; @@ -171,7 +169,7 @@ export async function getApplication(applicationId: string): Promise { environment.connections = environment.connections || { - m3Connector: false + m3Connector: false, }; return environment; @@ -180,7 +178,6 @@ export async function getApplication(applicationId: string): Promise { const url = `${getServerUrlPrefix()}/admin/customer/${customerId}/application/${applicationId}/access/users`; diff --git a/Source/SelfService/Web/apis/solutions/containerregistry.ts b/Source/SelfService/Web/apis/solutions/containerregistry.ts index 20969baa8..33701fdb3 100644 --- a/Source/SelfService/Web/apis/solutions/containerregistry.ts +++ b/Source/SelfService/Web/apis/solutions/containerregistry.ts @@ -4,15 +4,14 @@ import { Image } from '@fluentui/react'; import { getServerUrlPrefix } from './api'; - export type HTTPResponseImages = { url: string, - images: string[] + images: string[], }; export type ContainerRegistryImages = { url: string, - images: Image[] + images: Image[], }; export type Image = { @@ -24,12 +23,12 @@ export type Tag = { createdTime: Date, lastUpdateTime: Date, digest: string, - signed: boolean + signed: boolean, }; export type ContainerRegistryTags = { name: string, - tags: Tag[] + tags: Tag[], }; export type HTTPResponseTag = { @@ -37,12 +36,12 @@ export type HTTPResponseTag = { createdTime: string, lastUpdateTime: string, digest: string, - signed: boolean + signed: boolean, }; export type HTTPResponseTags = { name: string, - tags: HTTPResponseTag[] + tags: HTTPResponseTag[], }; export async function getTagsInContainerRegistry(applicationId: string, image: string): Promise { @@ -52,8 +51,9 @@ export async function getTagsInContainerRegistry(applicationId: string, image: s url, { method: 'GET', - mode: 'cors' + mode: 'cors', }); + const jsonResult = await result.json() as HTTPResponseTags; const items = jsonResult.tags.map(n => { @@ -62,7 +62,7 @@ export async function getTagsInContainerRegistry(applicationId: string, image: s createdTime: new Date(n.createdTime), lastUpdateTime: new Date(n.lastUpdateTime), digest: n.digest, - signed: n.signed + signed: n.signed, }; }); @@ -70,7 +70,7 @@ export async function getTagsInContainerRegistry(applicationId: string, image: s name: image, tags: items, }; -} +}; export async function getReposInContainerRegistry(applicationId: string): Promise { const url = `${getServerUrlPrefix()}/application/${applicationId}/containerregistry/images`; @@ -79,16 +79,19 @@ export async function getReposInContainerRegistry(applicationId: string): Promis url, { method: 'GET', - mode: 'cors' + mode: 'cors', }); + const jsonResult = await result.json() as HTTPResponseImages; + const theResult = jsonResult.images.map(n => { return { - name: n + name: n, }; }); + return { url: jsonResult.url, - images: theResult + images: theResult, }; -} +}; diff --git a/Source/SelfService/Web/apis/solutions/studio.ts b/Source/SelfService/Web/apis/solutions/studio.ts index d60c13823..6aa4b68dd 100644 --- a/Source/SelfService/Web/apis/solutions/studio.ts +++ b/Source/SelfService/Web/apis/solutions/studio.ts @@ -17,15 +17,15 @@ export async function getStudioConfig(customerID: string): Promise { url, { method: 'GET', - mode: 'cors' + mode: 'cors', }); return parseJSONResponse(response); -} +}; export async function saveStudioConfig(customerID: string, config: Studio): Promise { const url = `${getServerUrlPrefix()}/studio/customer/${customerID}`; - console.log(JSON.stringify(config)); + const response = await fetch( url, { @@ -34,8 +34,8 @@ export async function saveStudioConfig(customerID: string, config: Studio): Prom body: JSON.stringify(config), headers: { 'content-type': 'application/json' - } + }, }); return response.status === 200; -} +}; diff --git a/Source/SelfService/Web/applications/microservice/components/form/containerImageFields.tsx b/Source/SelfService/Web/applications/microservice/components/form/containerImageFields.tsx index c2bcfec24..95de8c654 100644 --- a/Source/SelfService/Web/applications/microservice/components/form/containerImageFields.tsx +++ b/Source/SelfService/Web/applications/microservice/components/form/containerImageFields.tsx @@ -27,10 +27,11 @@ const EntrypointDescription = () => export type ContainerImageFieldsProps = { hasDashedBorder?: boolean; + isEditMode?: boolean; isDisabled?: boolean; }; -export const ContainerImageFields = ({ hasDashedBorder, isDisabled }: ContainerImageFieldsProps) => +export const ContainerImageFields = ({ hasDashedBorder, isEditMode, isDisabled }: ContainerImageFieldsProps) => Container Image Settings @@ -39,7 +40,7 @@ export const ContainerImageFields = ({ hasDashedBorder, isDisabled }: ContainerI id='headImage' label='Image Name' dashedBorder={hasDashedBorder} - disabled={isDisabled} + disabled={isEditMode} required='Provide an image name.' sx={{ width: 1, maxWidth: 500, minWidth: 220 }} /> diff --git a/Source/SelfService/Web/applications/microservice/components/form/setupFields.tsx b/Source/SelfService/Web/applications/microservice/components/form/setupFields.tsx index a68a93d47..365f40d5f 100644 --- a/Source/SelfService/Web/applications/microservice/components/form/setupFields.tsx +++ b/Source/SelfService/Web/applications/microservice/components/form/setupFields.tsx @@ -28,11 +28,12 @@ const runtimeDescription = `By using the Dolittle runtime you'll have access to export type SetupFieldsProps = { environments: string[]; hasDashedBorder?: boolean; + isEditMode?: boolean; isDisabled?: boolean; onEnvironmentSelect?: (environment: string) => void; }; -export const SetupFields = ({ environments, hasDashedBorder, isDisabled, onEnvironmentSelect }: SetupFieldsProps) => { +export const SetupFields = ({ environments, hasDashedBorder, isEditMode, isDisabled, onEnvironmentSelect }: SetupFieldsProps) => { const [showEnvironmentInfo, setShowEnvironmentInfo] = useState(false); const [showEntrypointInfo, setShowEntrypointInfo] = useState(false); @@ -80,7 +81,7 @@ export const SetupFields = ({ environments, hasDashedBorder, isDisabled, onEnvir options={runtimeNumberOptions} onOpen={() => setShowEntrypointInfo(true)} required - disabled={isDisabled} + disabled={isEditMode} sx={hasDashedBorder ? { '& fieldset': { borderStyle: 'dashed' } } : {}} /> diff --git a/Source/SelfService/Web/applications/microservice/deployMicroservice/deployMicroservice.tsx b/Source/SelfService/Web/applications/microservice/deployMicroservice/deployMicroservice.tsx index fdf3d6b30..3a6c8fdf3 100644 --- a/Source/SelfService/Web/applications/microservice/deployMicroservice/deployMicroservice.tsx +++ b/Source/SelfService/Web/applications/microservice/deployMicroservice/deployMicroservice.tsx @@ -10,7 +10,7 @@ import { Guid } from '@dolittle/rudiments'; import { Typography } from '@mui/material'; -import { Button, Form, FullPageLoadingSpinner } from '@dolittle/design-system'; +import { Button, Form, LoadingSpinnerFullPage } from '@dolittle/design-system'; import { saveSimpleMicroservice } from '../../stores/microservice'; @@ -91,7 +91,8 @@ export const DeployMicroservice = ({ application }: DeployMicroserviceProps) => return ( <> - {isLoading && } + {isLoading && } + Deploy Base Microservice diff --git a/Source/SelfService/Web/applications/microservice/microserviceDetails/configurationFilesSection/setupSection/headerButtons.tsx b/Source/SelfService/Web/applications/microservice/microserviceDetails/configurationFilesSection/setupSection/headerButtons.tsx deleted file mode 100644 index 03ca2858f..000000000 --- a/Source/SelfService/Web/applications/microservice/microserviceDetails/configurationFilesSection/setupSection/headerButtons.tsx +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Dolittle. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -import React from 'react'; - -import { Box, Tooltip } from '@mui/material'; - -import { Button } from '@dolittle/design-system'; - -const styles = { - display: 'flex', - flexDirection: { xs: 'column', md: 'row' }, - alignItems: 'start', - mb: 2, - button: { mr: 2.5, mb: 1 }, -}; - -type HeaderButtonsProps = { - handleRestartDialog: () => void; - handleDeleteDialog: () => void; - disabled?: boolean; -}; - -export const HeaderButtons = ({ handleRestartDialog, handleDeleteDialog, disabled }: HeaderButtonsProps) => - - - -