Skip to content

Commit

Permalink
feat(job): overview for jobs (#433)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdebon committed Dec 27, 2022
1 parent ba2379d commit ed39de4
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 40 deletions.
@@ -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 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 (
<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
89 changes: 49 additions & 40 deletions 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,
Expand All @@ -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
Expand All @@ -26,45 +28,52 @@ export function PageGeneral(props: PageGeneralProps) {
<div className="mt-2 bg-white rounded flex flex-grow min-h-0">
<div className="flex flex-col flex-grow">
<div className="py-7 px-10 flex-grow overflow-y-auto min-h-0">
<div className="flex border border-element-light-lighter-400 mb-4">
<div className="flex-1 border-r border-element-light-lighter-400 p-5">
<Skeleton height={24} width={48} show={application?.instances?.loadingStatus === 'loading'}>
<span className="text-text-600 font-bold">
{application?.instances?.items?.length || '–'}/{application?.max_running_instances || '-'}
</span>
</Skeleton>
<span className="flex text-xs text-text-400 font-medium">
Running instances{' '}
<Tooltip side="right" content="Number of running instances">
<div className="flex items-center">
<Icon
className="cursor-pointer ml-1 text-xs text-element-light-lighter-700"
name="icon-solid-circle-info"
/>
</div>
</Tooltip>
</span>
</div>
<div className="flex-1 p-5">
<div className="text-text-600 font-bold mb-1">{serviceStability}</div>
<span className="flex text-xs text-text-400 font-medium">
Service stability
<Tooltip
side="right"
content="Number of application instance restarts since the last deployment due to application errors"
>
<div className="flex items-center">
<Icon
className="cursor-pointer ml-1 text-xs text-element-light-lighter-700"
name="icon-solid-circle-info"
/>
</div>
</Tooltip>
</span>
</div>
</div>
{application?.instances?.items && application.instances.items.length > 0 && (
<InstancesTable instances={application?.instances.items} />
{!isJob(application) ? (
<>
<div className="flex border border-element-light-lighter-400 mb-4">
<div className="flex-1 border-r border-element-light-lighter-400 p-5">
<Skeleton height={24} width={48} show={application?.instances?.loadingStatus === 'loading'}>
<span className="text-text-600 font-bold">
{application?.instances?.items?.length || '–'}/
{(application as GitContainerApplicationEntity)?.max_running_instances || '-'}
</span>
</Skeleton>
<span className="flex text-xs text-text-400 font-medium">
Running instances{' '}
<Tooltip side="right" content="Number of running instances">
<div className="flex items-center">
<Icon
className="cursor-pointer ml-1 text-xs text-element-light-lighter-700"
name="icon-solid-circle-info"
/>
</div>
</Tooltip>
</span>
</div>
<div className="flex-1 p-5">
<div className="text-text-600 font-bold mb-1">{serviceStability}</div>
<span className="flex text-xs text-text-400 font-medium">
Service stability
<Tooltip
side="right"
content="Number of application instance restarts since the last deployment due to application errors"
>
<div className="flex items-center">
<Icon
className="cursor-pointer ml-1 text-xs text-element-light-lighter-700"
name="icon-solid-circle-info"
/>
</div>
</Tooltip>
</span>
</div>
</div>
{application?.instances?.items && application.instances.items.length > 0 && (
<InstancesTable instances={application?.instances.items} />
)}
</>
) : (
<JobOverview application={application as JobApplicationEntity} />
)}
</div>
<div className="bg-white rounded-b flex flex-col justify-end">
Expand Down
1 change: 1 addition & 0 deletions libs/shared/ui/src/index.ts
Expand Up @@ -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'
@@ -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()
})
})
@@ -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 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 (
<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

0 comments on commit ed39de4

Please sign in to comment.