Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
301 additions
and
40 deletions.
There are no files selected for viewing
53 changes: 53 additions & 0 deletions
53
libs/pages/application/src/lib/ui/job-overview/job-overview.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(<JobOverview {...props} />) | ||
expect(baseElement).toBeTruthy() | ||
}) | ||
|
||
it('should display 6 property card', () => { | ||
const { baseElement } = render(<JobOverview {...props} />) | ||
|
||
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(<JobOverview {...props} application={cron} />) | ||
|
||
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(<JobOverview {...props} application={lifecycle} />) | ||
|
||
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(<JobOverview {...props} application={lifecycle} />) | ||
|
||
getByText(baseElement, 'Delete') | ||
}) | ||
}) |
91 changes: 91 additions & 0 deletions
91
libs/pages/application/src/lib/ui/job-overview/job-overview.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<div> | ||
<div className="grid grid-cols-2 xl:grid-cols-3 gap-3"> | ||
{isCronJob(application) && ( | ||
<PropertyCard | ||
name="Scheduling" | ||
value={cronstrue.toString(application.schedule?.cronjob?.scheduled_at?.toString() || '')} | ||
isLoading={false} | ||
onSettingsClick={() => navigate(`${path}${APPLICATION_SETTINGS_CONFIGURE_URL}`)} | ||
/> | ||
)} | ||
{isLifeCycleJob(application) && ( | ||
<PropertyCard | ||
name="Environment Event" | ||
value={eventsToString()} | ||
isLoading={false} | ||
onSettingsClick={() => navigate(`${path}${APPLICATION_SETTINGS_CONFIGURE_URL}`)} | ||
helperText="Execute this job at some given event" | ||
/> | ||
)} | ||
<PropertyCard | ||
name="Max Restarts" | ||
value={application.max_nb_restart?.toString() || ''} | ||
isLoading={false} | ||
onSettingsClick={() => navigate(`${path}${APPLICATION_SETTINGS_CONFIGURE_URL}`)} | ||
helperText="Maximum number of restarts allowed in case of job failure (0 means no failure)" | ||
/> | ||
<PropertyCard | ||
name="Max Duration" | ||
value={application.max_duration_seconds?.toString() || ''} | ||
isLoading={false} | ||
onSettingsClick={() => navigate(`${path}${APPLICATION_SETTINGS_CONFIGURE_URL}`)} | ||
helperText="Maximum duration allowed for the job to run before killing it and mark it as failed" | ||
/> | ||
<PropertyCard | ||
name="VCPU" | ||
value={application.max_nb_restart?.toString() || ''} | ||
isLoading={false} | ||
onSettingsClick={() => navigate(`${path}${APPLICATION_SETTINGS_RESOURCES_URL}`)} | ||
/> | ||
<PropertyCard | ||
name="RAM" | ||
value={application.memory.toString() + ' MB'} | ||
isLoading={false} | ||
onSettingsClick={() => navigate(`${path}${APPLICATION_SETTINGS_RESOURCES_URL}`)} | ||
/> | ||
<PropertyCard | ||
name="Port" | ||
value={application.port?.toString() || '–'} | ||
isLoading={false} | ||
onSettingsClick={() => navigate(`${path}${APPLICATION_SETTINGS_CONFIGURE_URL}`)} | ||
helperText="Port where to run readiness and liveliness probes checks. The port will not be exposed externally" | ||
/> | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
export default JobOverview |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
libs/shared/ui/src/lib/components/property-card/property-card.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(<PropertyCard {...props} />) | ||
expect(baseElement).toBeTruthy() | ||
}) | ||
|
||
it('should call onSettingsClick when clicking on settings', async () => { | ||
const { getByTestId } = render(<PropertyCard {...props} />) | ||
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(<PropertyCard {...props} />) | ||
const icon = getByTestId('icon-helper') | ||
|
||
expect(icon).toBeTruthy() | ||
}) | ||
}) |
22 changes: 22 additions & 0 deletions
22
libs/shared/ui/src/lib/components/property-card/property-card.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof PropertyCard> | ||
|
||
const Template: ComponentStory<typeof PropertyCard> = (args) => <PropertyCard {...args} /> | ||
|
||
export const Primary = Template.bind({}) | ||
Primary.args = { | ||
value: 'Every minute', | ||
isLoading: false, | ||
name: 'Frequency', | ||
helperText: 'This is a helper text', | ||
} |
49 changes: 49 additions & 0 deletions
49
libs/shared/ui/src/lib/components/property-card/property-card.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<div data-testid={dataTestId} className="px-5 py-4 rounded border border-element-light-lighter-500 bg-white flex"> | ||
<div className="flex-grow"> | ||
<Skeleton height={24} width={100} show={isLoading}> | ||
<h3 className="font-medium text-xl text-text-600 mb-1">{value}</h3> | ||
</Skeleton> | ||
<Skeleton height={24} width={100} show={isLoading}> | ||
<div className="text-xs text-text-600 flex"> | ||
{name}{' '} | ||
{helperText && ( | ||
<Tooltip side="right" content={helperText}> | ||
<div data-testid="icon-helper" className="ml-1 flex text-text-400 items-center"> | ||
<Icon name={IconAwesomeEnum.CIRCLE_INFO} /> | ||
</div> | ||
</Tooltip> | ||
)} | ||
</div> | ||
</Skeleton> | ||
</div> | ||
<ButtonIcon | ||
icon={IconAwesomeEnum.WHEEL} | ||
style={ButtonIconStyle.FLAT} | ||
dataTestId="property-card-settings-button" | ||
className=" flex-shrink-0 -mr-4 !text-text-400 hover:!text-text-600" | ||
iconClassName="cursor-pointer" | ||
onClick={onSettingsClick} | ||
/> | ||
</div> | ||
) | ||
} | ||
|
||
export default PropertyCard |