From ed39de4ab1e0c85e14d1780f92ae1556490df42a Mon Sep 17 00:00:00 2001 From: bdebon Date: Tue, 27 Dec 2022 15:54:28 +0100 Subject: [PATCH] feat(job): overview for jobs (#433) --- .../lib/ui/job-overview/job-overview.spec.tsx | 53 +++++++++++ .../src/lib/ui/job-overview/job-overview.tsx | 91 +++++++++++++++++++ .../src/lib/ui/page-general/page-general.tsx | 89 ++++++++++-------- libs/shared/ui/src/index.ts | 1 + .../property-card/property-card.spec.tsx | 36 ++++++++ .../property-card/property-card.stories.tsx | 22 +++++ .../property-card/property-card.tsx | 49 ++++++++++ 7 files changed, 301 insertions(+), 40 deletions(-) create mode 100644 libs/pages/application/src/lib/ui/job-overview/job-overview.spec.tsx create mode 100644 libs/pages/application/src/lib/ui/job-overview/job-overview.tsx create mode 100644 libs/shared/ui/src/lib/components/property-card/property-card.spec.tsx create mode 100644 libs/shared/ui/src/lib/components/property-card/property-card.stories.tsx create mode 100644 libs/shared/ui/src/lib/components/property-card/property-card.tsx diff --git a/libs/pages/application/src/lib/ui/job-overview/job-overview.spec.tsx b/libs/pages/application/src/lib/ui/job-overview/job-overview.spec.tsx new file mode 100644 index 0000000000..85831c3494 --- /dev/null +++ b/libs/pages/application/src/lib/ui/job-overview/job-overview.spec.tsx @@ -0,0 +1,53 @@ +import { getAllByTestId, getByText } from '@testing-library/react' +import { render } from '__tests__/utils/setup-jest' +import { cronjobFactoryMock, lifecycleJobFactoryMock } from '@qovery/domains/application' +import JobOverview, { JobOverviewProps } from './job-overview' + +const props: JobOverviewProps = { + application: { + ...lifecycleJobFactoryMock(1)[0], + }, +} + +describe('JobOverview', () => { + it('should render successfully', () => { + const { baseElement } = render() + expect(baseElement).toBeTruthy() + }) + + it('should display 6 property card', () => { + const { baseElement } = render() + + getAllByTestId(baseElement, 'property-card') + expect(getAllByTestId(baseElement, 'property-card')).toHaveLength(6) + }) + + it('should display scheduling property card if cron', () => { + const cron = cronjobFactoryMock(1)[0] + const { baseElement } = render() + + getByText(baseElement, 'Scheduling') + }) + + it('should display Environment Event property card if lifecycle and display Start - Delete', () => { + const lifecycle = lifecycleJobFactoryMock(1)[0] + lifecycle.schedule = { + on_start: {}, + on_delete: {}, + } + const { baseElement } = render() + + getByText(baseElement, 'Environment Event') + getByText(baseElement, 'Start - Delete') + }) + + it('should display only Delete', () => { + const lifecycle = lifecycleJobFactoryMock(1)[0] + lifecycle.schedule = { + on_delete: {}, + } + const { baseElement } = render() + + getByText(baseElement, 'Delete') + }) +}) diff --git a/libs/pages/application/src/lib/ui/job-overview/job-overview.tsx b/libs/pages/application/src/lib/ui/job-overview/job-overview.tsx new file mode 100644 index 0000000000..93a4d479b8 --- /dev/null +++ b/libs/pages/application/src/lib/ui/job-overview/job-overview.tsx @@ -0,0 +1,91 @@ +import cronstrue from 'cronstrue' +import { useNavigate, useParams } from 'react-router-dom' +import { isCronJob, isLifeCycleJob } from '@qovery/shared/enums' +import { JobApplicationEntity } from '@qovery/shared/interfaces' +import { + APPLICATION_SETTINGS_CONFIGURE_URL, + APPLICATION_SETTINGS_RESOURCES_URL, + APPLICATION_SETTINGS_URL, + APPLICATION_URL, +} from '@qovery/shared/router' +import { PropertyCard } from '@qovery/shared/ui' + +export interface JobOverviewProps { + application: JobApplicationEntity +} + +export function JobOverview(props: JobOverviewProps) { + const { application } = props + const { organizationId = '', environmentId = '', applicationId = '', projectId = '' } = useParams() + const navigate = useNavigate() + const path = APPLICATION_URL(organizationId, projectId, environmentId, applicationId) + APPLICATION_SETTINGS_URL + + const eventsToString = (): string => { + if (!application) return '' + const events: string[] = [] + if (application.schedule?.on_start) events.push('Start') + if (application.schedule?.on_stop) events.push('Stop') + if (application.schedule?.on_delete) events.push('Delete') + + return events.length === 0 ? 'No events' : events.join(' - ') + } + + return ( +
+
+ {isCronJob(application) && ( + navigate(`${path}${APPLICATION_SETTINGS_CONFIGURE_URL}`)} + /> + )} + {isLifeCycleJob(application) && ( + navigate(`${path}${APPLICATION_SETTINGS_CONFIGURE_URL}`)} + helperText="Execute this job at some given event" + /> + )} + navigate(`${path}${APPLICATION_SETTINGS_CONFIGURE_URL}`)} + helperText="Maximum number of restarts allowed in case of job failure (0 means no failure)" + /> + navigate(`${path}${APPLICATION_SETTINGS_CONFIGURE_URL}`)} + helperText="Maximum duration allowed for the job to run before killing it and mark it as failed" + /> + navigate(`${path}${APPLICATION_SETTINGS_RESOURCES_URL}`)} + /> + navigate(`${path}${APPLICATION_SETTINGS_RESOURCES_URL}`)} + /> + navigate(`${path}${APPLICATION_SETTINGS_CONFIGURE_URL}`)} + helperText="Port where to run readiness and liveliness probes checks. The port will not be exposed externally" + /> +
+
+ ) +} + +export default JobOverview diff --git a/libs/pages/application/src/lib/ui/page-general/page-general.tsx b/libs/pages/application/src/lib/ui/page-general/page-general.tsx index d63d4e7a18..2f24f3627a 100644 --- a/libs/pages/application/src/lib/ui/page-general/page-general.tsx +++ b/libs/pages/application/src/lib/ui/page-general/page-general.tsx @@ -1,5 +1,6 @@ import { getServiceType, isApplication, isJob } from '@qovery/shared/enums' import { + ApplicationEntity, ContainerApplicationEntity, GitApplicationEntity, GitContainerApplicationEntity, @@ -11,9 +12,10 @@ import LastCommitFeature from '../../feature/last-commit-feature/last-commit-fea import AboutContentContainer from '../about-content-container/about-content-container' import About from '../about/about' import InstancesTable from '../instances-table/instances-table' +import JobOverview from '../job-overview/job-overview' export interface PageGeneralProps { - application?: GitContainerApplicationEntity + application?: ApplicationEntity listHelpfulLinks: BaseLink[] loadingStatus?: LoadingStatus serviceStability?: number @@ -26,45 +28,52 @@ export function PageGeneral(props: PageGeneralProps) {
-
-
- - - {application?.instances?.items?.length || '–'}/{application?.max_running_instances || '-'} - - - - Running instances{' '} - -
- -
-
-
-
-
-
{serviceStability}
- - Service stability - -
- -
-
-
-
-
- {application?.instances?.items && application.instances.items.length > 0 && ( - + {!isJob(application) ? ( + <> +
+
+ + + {application?.instances?.items?.length || '–'}/ + {(application as GitContainerApplicationEntity)?.max_running_instances || '-'} + + + + Running instances{' '} + +
+ +
+
+
+
+
+
{serviceStability}
+ + Service stability + +
+ +
+
+
+
+
+ {application?.instances?.items && application.instances.items.length > 0 && ( + + )} + + ) : ( + )}
diff --git a/libs/shared/ui/src/index.ts b/libs/shared/ui/src/index.ts index 1dbd531ed5..116dd05d16 100644 --- a/libs/shared/ui/src/index.ts +++ b/libs/shared/ui/src/index.ts @@ -79,3 +79,4 @@ export * from './lib/components/truncate/truncate' export * from './lib/components/inputs/input-select/input-select' export * from './lib/components/table/table-row-deployment/table-row-deployment' export * from './lib/components/enable-box/enable-box' +export * from './lib/components/property-card/property-card' diff --git a/libs/shared/ui/src/lib/components/property-card/property-card.spec.tsx b/libs/shared/ui/src/lib/components/property-card/property-card.spec.tsx new file mode 100644 index 0000000000..8413660892 --- /dev/null +++ b/libs/shared/ui/src/lib/components/property-card/property-card.spec.tsx @@ -0,0 +1,36 @@ +import { act } from '@testing-library/react' +import { render } from '__tests__/utils/setup-jest' +import PropertyCard, { PropertyCardProps } from './property-card' + +const props: PropertyCardProps = { + value: 'Every minute', + helperText: 'This is a helper text', + name: 'Frequency', + isLoading: false, + onSettingsClick: jest.fn(), +} + +describe('PropertyCard', () => { + it('should render successfully', () => { + const { baseElement } = render() + expect(baseElement).toBeTruthy() + }) + + it('should call onSettingsClick when clicking on settings', async () => { + const { getByTestId } = render() + const iconButton = getByTestId('property-card-settings-button') + + await act(() => { + iconButton.click() + }) + + expect(props.onSettingsClick).toHaveBeenCalled() + }) + + it('should have an icon when helperText is provided', () => { + const { getByTestId } = render() + const icon = getByTestId('icon-helper') + + expect(icon).toBeTruthy() + }) +}) diff --git a/libs/shared/ui/src/lib/components/property-card/property-card.stories.tsx b/libs/shared/ui/src/lib/components/property-card/property-card.stories.tsx new file mode 100644 index 0000000000..f3685d4fbf --- /dev/null +++ b/libs/shared/ui/src/lib/components/property-card/property-card.stories.tsx @@ -0,0 +1,22 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react' +import PropertyCard from './property-card' + +export default { + component: PropertyCard, + title: 'PropertyCard', + argTypes: { + value: { + control: { type: 'text' }, + }, + }, +} as ComponentMeta + +const Template: ComponentStory = (args) => + +export const Primary = Template.bind({}) +Primary.args = { + value: 'Every minute', + isLoading: false, + name: 'Frequency', + helperText: 'This is a helper text', +} diff --git a/libs/shared/ui/src/lib/components/property-card/property-card.tsx b/libs/shared/ui/src/lib/components/property-card/property-card.tsx new file mode 100644 index 0000000000..d1d3c2f2df --- /dev/null +++ b/libs/shared/ui/src/lib/components/property-card/property-card.tsx @@ -0,0 +1,49 @@ +import ButtonIcon, { ButtonIconStyle } from '../buttons/button-icon/button-icon' +import Icon from '../icon/icon' +import { IconAwesomeEnum } from '../icon/icon-awesome.enum' +import Skeleton from '../skeleton/skeleton' +import Tooltip from '../tooltip/tooltip' + +export interface PropertyCardProps { + name: string + value: string + isLoading?: boolean + helperText?: string + onSettingsClick?: () => void + dataTestId?: string +} + +export function PropertyCard(props: PropertyCardProps) { + const { name, value, isLoading, helperText, onSettingsClick, dataTestId = 'property-card' } = props + return ( +
+
+ +

{value}

+
+ +
+ {name}{' '} + {helperText && ( + +
+ +
+
+ )} +
+
+
+ +
+ ) +} + +export default PropertyCard