From c91c2a5f4a4e7921e5c838fc98e10f25e2046938 Mon Sep 17 00:00:00 2001 From: eutopian Date: Tue, 28 Feb 2023 08:49:55 -0500 Subject: [PATCH] add deleted tab in fms --- .changeset/twelve-chefs-juggle.md | 5 + src/screens/FeedsManager/ApprovedTable.tsx | 4 +- src/screens/FeedsManager/InactiveTable.tsx | 4 +- .../FeedsManager/JobProposalsCard.test.tsx | 19 ++- src/screens/FeedsManager/JobProposalsCard.tsx | 21 +++ src/screens/FeedsManager/PendingTable.tsx | 4 +- .../FeedsManager/SupportedChainsCard.tsx | 76 +++++------ src/screens/FeedsManager/UpdatesTable.tsx | 4 +- .../JobProposal/JobProposalView.test.tsx | 22 ++++ src/screens/JobProposal/JobProposalView.tsx | 2 + src/screens/JobProposal/SpecsView.test.tsx | 120 ++++++++++++++++-- src/screens/JobProposal/SpecsView.tsx | 92 ++++++++++---- .../gql/fetchFeedsManagersWithProposals.ts | 20 +++ support/factories/gql/fetchJobProposal.ts | 1 + 14 files changed, 303 insertions(+), 91 deletions(-) create mode 100644 .changeset/twelve-chefs-juggle.md diff --git a/.changeset/twelve-chefs-juggle.md b/.changeset/twelve-chefs-juggle.md new file mode 100644 index 00000000..2809b277 --- /dev/null +++ b/.changeset/twelve-chefs-juggle.md @@ -0,0 +1,5 @@ +--- +'@smartcontractkit/operator-ui': patch +--- + +Allow job deletion requests to be sent from FMS diff --git a/src/screens/FeedsManager/ApprovedTable.tsx b/src/screens/FeedsManager/ApprovedTable.tsx index f868a7e2..10a7f886 100644 --- a/src/screens/FeedsManager/ApprovedTable.tsx +++ b/src/screens/FeedsManager/ApprovedTable.tsx @@ -33,8 +33,8 @@ export const ApprovedTable = withStyles(tableStyles)( - {proposals?.map((proposal, idx) => ( - + {proposals?.map((proposal) => ( + - {proposals?.map((proposal, idx) => ( - + {proposals?.map((proposal) => ( + { buildApprovedJobProposal({ pendingUpdate: true }), buildRejectedJobProposal({ pendingUpdate: true }), buildCancelledJobProposal({ pendingUpdate: true }), + buildDeletedJobProposal({ pendingUpdate: true }), + buildDeletedJobProposal({ pendingUpdate: false }), ] renderWithRouter() - expect(getByTestId('updates-badge')).toHaveTextContent('3') + expect(getByTestId('updates-badge')).toHaveTextContent('4') expect(getByTestId('approved-badge')).toHaveTextContent('1') expect(getByTestId('rejected-badge')).toHaveTextContent('1') expect(getByTestId('cancelled-badge')).toHaveTextContent('1') + expect(getByTestId('deleted-badge')).toHaveTextContent('1') userEvent.click(getByRole('tab', { name: /updates/i })) const rows = await findAllByRole('row') - expect(rows).toHaveLength(4) + expect(rows).toHaveLength(5) }) it('renders the approved job proposals', async () => { @@ -77,4 +81,15 @@ describe('JobProposalsCard', () => { const rows = await findAllByRole('row') expect(rows).toHaveLength(2) }) + + it('renders the deleted job proposals', async () => { + const proposals = buildJobProposals() + + renderWithRouter() + + userEvent.click(getByRole('tab', { name: /deleted/i })) + + const rows = await findAllByRole('row') + expect(rows).toHaveLength(2) + }) }) diff --git a/src/screens/FeedsManager/JobProposalsCard.tsx b/src/screens/FeedsManager/JobProposalsCard.tsx index 69fade51..8dd9601c 100644 --- a/src/screens/FeedsManager/JobProposalsCard.tsx +++ b/src/screens/FeedsManager/JobProposalsCard.tsx @@ -23,6 +23,7 @@ const tabToStatus: { [key: number]: string } = { 2: 'APPROVED', 3: 'REJECTED', 4: 'CANCELLED', + 5: 'DELETED', } const styles = (theme: Theme) => { @@ -84,6 +85,7 @@ export const JobProposalsCard = withStyles(styles)( APPROVED: number REJECTED: number CANCELLED: number + DELETED: number } = React.useMemo(() => { const tabBadgeCounts = { PENDING: 0, @@ -91,6 +93,7 @@ export const JobProposalsCard = withStyles(styles)( APPROVED: 0, REJECTED: 0, CANCELLED: 0, + DELETED: 0, } proposals.forEach((p) => { @@ -111,6 +114,10 @@ export const JobProposalsCard = withStyles(styles)( case 'REJECTED': tabBadgeCounts['REJECTED']++ + break + case 'DELETED': + tabBadgeCounts['DELETED']++ + break default: break @@ -158,6 +165,8 @@ export const JobProposalsCard = withStyles(styles)( return case 'APPROVED': return + case 'DELETED': + return default: return null } @@ -239,6 +248,18 @@ export const JobProposalsCard = withStyles(styles)( } /> + + Deleted + + } + /> {renderTable(filteredProposals)} diff --git a/src/screens/FeedsManager/PendingTable.tsx b/src/screens/FeedsManager/PendingTable.tsx index 0adf867f..42c4b784 100644 --- a/src/screens/FeedsManager/PendingTable.tsx +++ b/src/screens/FeedsManager/PendingTable.tsx @@ -29,8 +29,8 @@ export const PendingTable = withStyles(tableStyles)( - {proposals?.map((proposal, idx) => ( - + {proposals?.map((proposal) => ( + - {cfgs.map((cfg, idx) => ( + {cfgs.map((cfg) => ( ( - - - - - ) - - const renderOracle = () => ( - <> - - - - - - - - - - ) - return ( <> @@ -494,7 +474,7 @@ const OCRJobTypeRow = withStyles(styles)( - {cfg.isBootstrap ? renderBootstrap() : renderOracle()} + {cfg.isBootstrap ? renderBootstrap(cfg) : renderOracle(cfg)} ) }, @@ -510,26 +490,6 @@ const OCR2JobTypeRow = withStyles(styles)( return null } - const renderBootstrap = () => ( - - - - - ) - - const renderOracle = () => ( - <> - - - - - - - - - - ) - return ( <> @@ -541,8 +501,36 @@ const OCR2JobTypeRow = withStyles(styles)( - {cfg.isBootstrap ? renderBootstrap() : renderOracle()} + {cfg.isBootstrap ? renderBootstrap(cfg) : renderOracle(cfg)} ) }, ) + +const renderBootstrap = ( + cfg: + | FeedsManager_ChainConfigFields['ocr2JobConfig'] + | FeedsManager_ChainConfigFields['ocr1JobConfig'], +) => ( + + + + +) + +const renderOracle = ( + cfg: + | FeedsManager_ChainConfigFields['ocr2JobConfig'] + | FeedsManager_ChainConfigFields['ocr1JobConfig'], +) => ( + <> + + + + + + + + + +) diff --git a/src/screens/FeedsManager/UpdatesTable.tsx b/src/screens/FeedsManager/UpdatesTable.tsx index b3bd0ae3..b6e987f0 100644 --- a/src/screens/FeedsManager/UpdatesTable.tsx +++ b/src/screens/FeedsManager/UpdatesTable.tsx @@ -31,8 +31,8 @@ export const UpdatesTable = withStyles(tableStyles)( - {proposals?.map((proposal, idx) => ( - + {proposals?.map((proposal) => ( + { expect(queryByText(/edit/i)).toBeNull() }) }) + + describe('deleted proposal', () => { + let proposal: JobProposalPayloadFields + + beforeEach(() => { + proposal = buildJobProposal({ + status: 'DELETED', + specs: [buildJobProposalSpec({ status: 'APPROVED' })], + }) + }) + + it('renders a rejected job proposal', async () => { + renderComponent(proposal) + + expect(queryByText('Deleted')).toBeInTheDocument() + + expect(getByTestId('codeblock')).toHaveTextContent( + proposal.specs[0].definition, + ) + expect(queryByText(/edit/i)).toBeNull() + }) + }) }) diff --git a/src/screens/JobProposal/JobProposalView.tsx b/src/screens/JobProposal/JobProposalView.tsx index 17dd73c9..21fda3dc 100644 --- a/src/screens/JobProposal/JobProposalView.tsx +++ b/src/screens/JobProposal/JobProposalView.tsx @@ -22,6 +22,7 @@ export const JOB_PROPOSAL_PAYLOAD_FIELDS = gql` ...JobProposal_SpecsFields } status + pendingUpdate } ` @@ -59,6 +60,7 @@ export const JobProposalView: React.FC = ({ { let handleCancel: jest.Mock let handleReject: jest.Mock - function renderComponent(specs: ReadonlyArray) { + function renderComponent( + specs: ReadonlyArray, + proposal: JobProposalPayloadFields, + ) { render( { describe('pending proposal', () => { let specs: ReadonlyArray + let proposal: JobProposalPayloadFields beforeEach(() => { + proposal = buildJobProposal({ status: 'PENDING' }) specs = [buildJobProposalSpec({ status: 'PENDING' })] }) it('renders a pending job proposal', async () => { - renderComponent(specs) + renderComponent(specs, proposal) expect(getByTestId('codeblock')).toHaveTextContent(specs[0].definition) expect(queryByText(/edit/i)).toBeInTheDocument() }) it('approves the proposal', async () => { - renderComponent(specs) + renderComponent(specs, proposal) userEvent.click(getByRole('button', { name: /approve/i })) userEvent.click(getByRole('button', { name: /confirm/i })) await waitFor(() => expect(handleApprove).toHaveBeenCalled()) }) - it('rejects the propoosal', async () => { - renderComponent(specs) + it('rejects the proposal', async () => { + renderComponent(specs, proposal) userEvent.click(getByRole('button', { name: /reject/i })) userEvent.click(getByRole('button', { name: /confirm/i })) @@ -64,7 +73,7 @@ describe('SpecsView', () => { }) it('updates the spec', async () => { - renderComponent(specs) + renderComponent(specs, proposal) userEvent.click(getByRole('button', { name: /edit/i })) @@ -78,20 +87,22 @@ describe('SpecsView', () => { describe('approved proposal', () => { let specs: ReadonlyArray + let proposal: JobProposalPayloadFields beforeEach(() => { + proposal = buildJobProposal({ status: 'APPROVED' }) specs = [buildJobProposalSpec({ status: 'APPROVED' })] }) it('renders an approved job proposal', async () => { - renderComponent(specs) + renderComponent(specs, proposal) expect(getByTestId('codeblock')).toHaveTextContent(specs[0].definition) expect(queryByText(/edit/i)).toBeNull() }) it('cancels the proposal', async () => { - renderComponent(specs) + renderComponent(specs, proposal) userEvent.click(getByRole('button', { name: /cancel/i })) userEvent.click(getByRole('button', { name: /confirm/i })) @@ -101,20 +112,22 @@ describe('SpecsView', () => { describe('cancelled proposal', () => { let specs: ReadonlyArray + let proposal: JobProposalPayloadFields beforeEach(() => { + proposal = buildJobProposal({ status: 'CANCELLED' }) specs = [buildJobProposalSpec({ status: 'CANCELLED' })] }) it('renders a cancelled job proposal', async () => { - renderComponent(specs) + renderComponent(specs, proposal) expect(getByTestId('codeblock')).toHaveTextContent(specs[0].definition) expect(queryByText(/edit/i)).toBeInTheDocument() }) it('approves the proposal', async () => { - renderComponent(specs) + renderComponent(specs, proposal) userEvent.click(getByRole('button', { name: /approve/i })) userEvent.click(getByRole('button', { name: /confirm/i })) @@ -122,7 +135,7 @@ describe('SpecsView', () => { }) it('updates the spec', async () => { - renderComponent(specs) + renderComponent(specs, proposal) userEvent.click(getByRole('button', { name: /edit/i })) @@ -136,16 +149,97 @@ describe('SpecsView', () => { describe('rejected proposal', () => { let specs: ReadonlyArray + let proposal: JobProposalPayloadFields beforeEach(() => { + proposal = buildJobProposal({ status: 'REJECTED' }) specs = [buildJobProposalSpec({ status: 'REJECTED' })] }) it('renders a rejected job proposal', async () => { - renderComponent(specs) + renderComponent(specs, proposal) expect(getByTestId('codeblock')).toHaveTextContent(specs[0].definition) expect(queryByText(/edit/i)).toBeNull() }) }) + + describe('deleted proposal with approved spec', () => { + let specs: ReadonlyArray + let proposal: JobProposalPayloadFields + + beforeEach(() => { + proposal = buildJobProposal({ status: 'DELETED' }) + specs = [buildJobProposalSpec({ status: 'APPROVED' })] + }) + + it('renders a deleted job proposal', async () => { + renderComponent(specs, proposal) + + expect(getByTestId('codeblock')).toHaveTextContent(specs[0].definition) + expect(queryByText(/edit/i)).toBeNull() + }) + + it('cancels the proposal', async () => { + renderComponent(specs, proposal) + + userEvent.click(getByRole('button', { name: /cancel/i })) + userEvent.click(getByRole('button', { name: /confirm/i })) + await waitFor(() => expect(handleCancel).toHaveBeenCalled()) + }) + }) + + describe('deleted proposal with cancelled spec', () => { + let specs: ReadonlyArray + let proposal: JobProposalPayloadFields + + beforeEach(() => { + proposal = buildJobProposal({ status: 'DELETED' }) + specs = [buildJobProposalSpec({ status: 'CANCELLED' })] + }) + + it('renders a deleted job proposal', async () => { + renderComponent(specs, proposal) + + expect(getByTestId('codeblock')).toHaveTextContent(specs[0].definition) + expect(queryByText(/edit/i)).toBeNull() + expect(queryByText('Cancel')).not.toBeInTheDocument() + }) + }) + + describe('deleted proposal with rejected spec', () => { + let specs: ReadonlyArray + let proposal: JobProposalPayloadFields + + beforeEach(() => { + proposal = buildJobProposal({ status: 'DELETED' }) + specs = [buildJobProposalSpec({ status: 'REJECTED' })] + }) + + it('renders a deleted job proposal', async () => { + renderComponent(specs, proposal) + + expect(getByTestId('codeblock')).toHaveTextContent(specs[0].definition) + expect(queryByText(/edit/i)).toBeNull() + expect(queryByText('Cancel')).not.toBeInTheDocument() + }) + }) + + describe('deleted proposal with pending spec', () => { + let specs: ReadonlyArray + let proposal: JobProposalPayloadFields + + beforeEach(() => { + proposal = buildJobProposal({ status: 'DELETED' }) + specs = [buildJobProposalSpec({ status: 'PENDING' })] + }) + + it('renders a deleted job proposal', async () => { + renderComponent(specs, proposal) + + expect(getByTestId('codeblock')).toHaveTextContent(specs[0].definition) + expect(queryByText(/edit/i)).toBeNull() + expect(queryByText('Cancel')).not.toBeInTheDocument() + }) + }) }) diff --git a/src/screens/JobProposal/SpecsView.tsx b/src/screens/JobProposal/SpecsView.tsx index 2592655f..b32bf039 100644 --- a/src/screens/JobProposal/SpecsView.tsx +++ b/src/screens/JobProposal/SpecsView.tsx @@ -55,12 +55,16 @@ const styles = (theme: Theme) => { editContainer: { flex: 1, }, - actionsContainer: {}, + actionsContainer: { + flex: 1, + textAlign: 'right', + }, }) } interface Props extends WithStyles { specs: ReadonlyArray + proposal: JobProposalPayloadFields onApprove: (specID: string) => void onReject: (specID: string) => void onCancel: (specID: string) => void @@ -88,7 +92,15 @@ const confirmationDialogText = { } export const SpecsView = withStyles(styles)( - ({ classes, onApprove, onCancel, onReject, onUpdateSpec, specs }: Props) => { + ({ + classes, + onApprove, + onCancel, + onReject, + onUpdateSpec, + specs, + proposal, + }: Props) => { const [confirmationDialog, setConfirmationDialog] = React.useState(null) const [isEditing, setIsEditing] = React.useState(false) @@ -112,7 +124,11 @@ export const SpecsView = withStyles(styles)( return sorted.sort((a, b) => b.version - a.version) }, [specs]) - const renderActions = (status: SpecStatus, specID: string) => { + const renderActions = ( + status: SpecStatus, + specID: string, + proposal: JobProposalPayloadFields, + ) => { switch (status) { case 'PENDING': return ( @@ -125,7 +141,7 @@ export const SpecsView = withStyles(styles)( Reject - {latestSpec.id === specID && ( + {latestSpec.id === specID && proposal.status !== 'DELETED' && ( + + + This proposal was deleted. Cancel the spec to delete any + running jobs + + + )} ) case 'APPROVED': return ( - + <> + + + {proposal.status === 'DELETED' && proposal.pendingUpdate && ( + + This proposal was deleted. Cancel the spec to delete any + running jobs + + )} + ) case 'CANCELLED': - if (latestSpec.id !== specID) { - return null + if (latestSpec.id === specID && proposal.status !== 'DELETED') { + return ( + + ) } - return ( - - ) - + return null default: return null } @@ -193,7 +236,8 @@ export const SpecsView = withStyles(styles)(
{idx === 0 && (spec.status === 'PENDING' || - spec.status === 'CANCELLED') && ( + spec.status === 'CANCELLED') && + proposal.status !== 'DELETED' && (
- {renderActions(spec.status, spec.id)} + {renderActions(spec.status, spec.id, proposal)}
diff --git a/support/factories/gql/fetchFeedsManagersWithProposals.ts b/support/factories/gql/fetchFeedsManagersWithProposals.ts index 69cbf685..cd48d9b9 100644 --- a/support/factories/gql/fetchFeedsManagersWithProposals.ts +++ b/support/factories/gql/fetchFeedsManagersWithProposals.ts @@ -107,6 +107,25 @@ export function buildCancelledJobProposal( } } +// buildDeletedJobProposal builds an cancelled job proposal. +export function buildDeletedJobProposal( + overrides?: Partial, +): FeedsManager_JobProposalsFields { + const minuteAgo = isoDate(Date.now() - MINUTE_MS) + + return { + id: '400', + remoteUUID: '00000000-0000-0000-0000-000000000004', + status: 'DELETED', + pendingUpdate: true, + latestSpec: { + createdAt: minuteAgo, + version: 1, + }, + ...overrides, + } +} + // buildJobProposals builds a list of job proposals each containing a different // status for a FetchFeedsManagersWithProposals query export function buildJobProposals(): FeedsManager_JobProposalsFields[] { @@ -115,5 +134,6 @@ export function buildJobProposals(): FeedsManager_JobProposalsFields[] { buildApprovedJobProposal(), buildRejectedJobProposal(), buildCancelledJobProposal(), + buildDeletedJobProposal(), ] } diff --git a/support/factories/gql/fetchJobProposal.ts b/support/factories/gql/fetchJobProposal.ts index a4cc4d4a..1fd3b67e 100644 --- a/support/factories/gql/fetchJobProposal.ts +++ b/support/factories/gql/fetchJobProposal.ts @@ -11,6 +11,7 @@ export function buildJobProposal( status: 'PENDING', externalJobID: null, specs: [buildJobProposalSpec()], + pendingUpdate: false, ...overrides, } }