Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/flat-toys-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@smartcontractkit/operator-ui': patch
---

feat: allow approval of previous versions of job specs
149 changes: 148 additions & 1 deletion src/screens/JobProposal/SpecsView.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react'
import { render, screen, waitFor } from 'support/test-utils'
import { render, screen, waitFor, within } from 'support/test-utils'
import userEvent from '@testing-library/user-event'

import {
Expand Down Expand Up @@ -260,4 +260,151 @@ describe('SpecsView', () => {
expect(queryByText('Cancel')).not.toBeInTheDocument()
})
})

describe('Approve button', () => {
let specs: ReadonlyArray<JobProposal_SpecsFields>
let proposal: JobProposalPayloadFields

beforeEach(() => {
specs = [
buildJobProposalSpec({ id: '106', version: 6, status: 'CANCELLED' }),
buildJobProposalSpec({ id: '105', version: 5, status: 'PENDING' }),
buildJobProposalSpec({ id: '104', version: 4, status: 'CANCELLED' }),
buildJobProposalSpec({ id: '103', version: 3, status: 'CANCELLED' }),
buildJobProposalSpec({ id: '102', version: 2, status: 'APPROVED' }),
buildJobProposalSpec({ id: '101', version: 1, status: 'REVOKED' }),
]
})

it('is visible in latest two cancelled specs if proposal is not deleted or revoked', async () => {
proposal = buildJobProposal({ status: 'PENDING' })
renderComponent(specs, proposal)

const panels = screen.getAllByTestId('expansion-panel')
expect(panels).toHaveLength(6)
expect(
within(panels[0]).queryByRole('button', {
name: 'Approve',
hidden: false,
}),
).toBeInTheDocument()
expect(
within(panels[1]).queryByRole('button', {
name: 'Approve',
hidden: true,
}),
).not.toBeInTheDocument()
expect(
within(panels[2]).queryByRole('button', {
name: 'Approve',
hidden: true,
}),
).toBeInTheDocument()
expect(
within(panels[3]).queryByRole('button', {
name: 'Approve',
hidden: true,
}),
).not.toBeInTheDocument()
expect(
within(panels[4]).queryByRole('button', {
name: 'Approve',
hidden: true,
}),
).not.toBeInTheDocument()
expect(
within(panels[5]).queryByRole('button', {
name: 'Approve',
hidden: true,
}),
).not.toBeInTheDocument()
})

it('is visible in latest pending spec if proposal is not deleted or revoked', async () => {
specs = [
buildJobProposalSpec({ id: '103', version: 3, status: 'PENDING' }),
buildJobProposalSpec({ id: '102', version: 2, status: 'PENDING' }),
buildJobProposalSpec({ id: '101', version: 1, status: 'CANCELLED' }),
]
proposal = buildJobProposal({ status: 'PENDING' })
renderComponent(specs, proposal)

const panels = screen.getAllByTestId('expansion-panel')
expect(panels).toHaveLength(3)
expect(
within(panels[0]).queryByRole('button', {
name: 'Approve',
hidden: false,
}),
).toBeInTheDocument()
expect(
within(panels[1]).queryByRole('button', {
name: 'Approve',
hidden: true,
}),
).not.toBeInTheDocument()
expect(
within(panels[2]).queryByRole('button', {
name: 'Approve',
hidden: true,
}),
).toBeInTheDocument()
})

it('is not visible in any specs if proposal is deleted', async () => {
proposal = buildJobProposal({ status: 'DELETED' })
renderComponent(specs, proposal)

const panels = screen.getAllByTestId('expansion-panel')
expect(panels).toHaveLength(6)
expect(
screen.queryByRole('button', { name: 'Approve', hidden: false }),
).not.toBeInTheDocument()
})

it('is not visible in any specs if proposal is revoked', async () => {
proposal = buildJobProposal({ status: 'REVOKED' })
renderComponent(specs, proposal)

const panels = screen.getAllByTestId('expansion-panel')
expect(panels).toHaveLength(6)
expect(
screen.queryByRole('button', { name: 'Approve', hidden: false }),
).not.toBeInTheDocument()
})

it('is visible with single pending job', async () => {
proposal = buildJobProposal({ status: 'PENDING' })
specs = [
buildJobProposalSpec({ id: '101', version: 1, status: 'PENDING' }),
]
renderComponent(specs, proposal)

const panels = screen.getAllByTestId('expansion-panel')
expect(panels).toHaveLength(1)
expect(
within(panels[0]).queryByRole('button', {
name: 'Approve',
hidden: false,
}),
).toBeInTheDocument()
})

it('is visible with single cancelled job', async () => {
proposal = buildJobProposal({ status: 'PENDING' })
specs = [
buildJobProposalSpec({ id: '101', version: 1, status: 'CANCELLED' }),
]
renderComponent(specs, proposal)

const panels = screen.getAllByTestId('expansion-panel')
expect(panels).toHaveLength(1)
expect(
within(panels[0]).queryByRole('button', {
name: 'Approve',
hidden: false,
}),
).toBeInTheDocument()
})
})
})
42 changes: 38 additions & 4 deletions src/screens/JobProposal/SpecsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ interface Props extends WithStyles<typeof styles> {
}

interface ConfirmationDialogArgs {
action: 'reject' | 'approve' | 'cancel'
action: 'reject' | 'approve' | 'approvePrevious' | 'cancel'
id: string
}

Expand All @@ -81,6 +81,25 @@ const confirmationDialogText = {
title: 'Approve Job Proposal',
body: 'Approving this job proposal will start running a new job. WARNING: If a job using the same contract address already exists, it will be deleted before running the new one.',
},
approvePrevious: {
title: 'Approve Previous Version of Job Proposal',
body: (
<div>
<p>
⚠️ You have selected a job spec version that is not the most recent
one.
</p>
<p>
Approving this job proposal will start running a new job with the old
spec version.
</p>
<p>
WARNING: If a job using the same contract address already exists, it
will be deleted before running the new one.
</p>
</div>
),
},
cancel: {
title: 'Cancel Job Proposal',
body: 'Cancelling this job proposal will delete the running job. Are you sure you want to cancel this job proposal?',
Expand Down Expand Up @@ -124,6 +143,11 @@ export const SpecsView = withStyles(styles)(
return sorted.sort((a, b) => b.version - a.version)
}, [specs])

const approvableCancelledJobSpecs = sortedSpecs
.filter((spec) => spec.status === 'CANCELLED')
.slice(0, 2)
.map((spec) => spec.id)

const renderActions = (
status: SpecStatus,
specID: string,
Expand Down Expand Up @@ -193,15 +217,20 @@ export const SpecsView = withStyles(styles)(
)
case 'CANCELLED':
if (
latestSpec.id === specID &&
approvableCancelledJobSpecs.includes(specID) &&
proposal.status !== 'DELETED' &&
proposal.status !== 'REVOKED'
) {
return (
<Button
variant="contained"
color="primary"
onClick={() => openConfirmationDialog('approve', specID)}
onClick={() =>
openConfirmationDialog(
specID === latestSpec.id ? 'approve' : 'approvePrevious',
specID,
)
}
>
Approve
</Button>
Expand All @@ -217,7 +246,11 @@ export const SpecsView = withStyles(styles)(
return (
<div>
{sortedSpecs.map((spec, idx) => (
<ExpansionPanel defaultExpanded={idx === 0} key={idx}>
<ExpansionPanel
defaultExpanded={idx === 0}
key={idx}
data-testid="expansion-panel"
>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<Typography className={classes.versionText}>
Version {spec.version}
Expand Down Expand Up @@ -285,6 +318,7 @@ export const SpecsView = withStyles(styles)(
if (confirmationDialog) {
switch (confirmationDialog.action) {
case 'approve':
case 'approvePrevious':
onApprove(confirmationDialog.id)

break
Expand Down
Loading