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
6 changes: 6 additions & 0 deletions src/libs/ajax/teaspoons/Teaspoons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { fetchTeaspoons } from 'src/libs/ajax/ajax-common';
import {
GetPipelineRunsResponse,
PipelineList,
PipelineRunResponse,
PipelineWithDetails,
PreparePipelineRunResponse,
StartPipelineResponse,
Expand Down Expand Up @@ -77,6 +78,11 @@ export const Teaspoons = (signal?: AbortSignal) => ({
);
return res.json();
},

getPipelineRunResult: async (jobId: string): Promise<PipelineRunResponse> => {
const res = await fetchTeaspoons(`pipelineruns/v1/result/${jobId}`, _.merge(authOpts(), { signal }));
return res.json();
},
});

export type TeaspoonsContract = ReturnType<typeof Teaspoons>;
30 changes: 30 additions & 0 deletions src/libs/ajax/teaspoons/teaspoons-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,34 @@ export interface StartPipelineResponse {
};
}

export interface PipelineRunResponse {
jobReport: PipelineJobReport;
errorReport?: PipelineRunErrorReport;
pipelineRunReport: PipelineRunReport;
}

export interface PipelineJobReport {
id: string;
description?: string;
status: PipelineRunStatus;
statusCode?: number;
submitted: string;
completed?: string;
resultURL?: string;
}

export interface PipelineRunErrorReport {
message: string;
errorCode: number;
causes: string[];
}

export interface PipelineRunReport {
pipelineName: string;
pipelineVersion: number;
toolVersion: string;
outputs?: Record<string, string>;
outputExpirationDate?: string;
}

export type PipelineRunStatus = 'PREPARING' | 'RUNNING' | 'SUCCEEDED' | 'FAILED';
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ export const ScientificServicesDescription = () => {
</div>
<div style={{ fontWeight: 'bold', marginTop: '2rem' }}>Our current offerings:</div>
<div>
<span style={{ fontStyle: 'italic' }}>All of Us</span> + AnVIL Imputation Service
<a
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an unrelated drive-by change to link to the marketing page from the landing page

href='https://allofus-anvil-imputation.terra.bio/'
target='_blank'
rel='noopener noreferrer'
style={{ textDecoration: 'underline' }}
>
<span style={{ fontStyle: 'italic' }}>All of Us</span> + AnVIL Imputation Service
</a>
</div>
<div style={{ fontWeight: 'bold', marginTop: '2rem', marginBottom: '1rem' }}>First time using this service?</div>
{buttonVisible && (
Expand Down
14 changes: 14 additions & 0 deletions src/pages/scientificServices/pipelines/utils/mock-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
Pipeline,
PipelineInput,
PipelineRun,
PipelineRunStatus,
PipelineWithDetails,
UserPipelineQuotaDetails,
} from 'src/libs/ajax/teaspoons/teaspoons-models';
Expand Down Expand Up @@ -43,3 +45,15 @@ export function mockUserPipelineQuotaDetails(name: string): UserPipelineQuotaDet
quotaUnits: 'things',
};
}

export function mockPipelineRun(status: PipelineRunStatus): PipelineRun {
return {
jobId: 'run-id-123',
pipelineName: 'array_imputation',
status,
description: 'Test pipeline run',
timeSubmitted: '2023-10-01T00:00:00Z',
timeCompleted: status === 'SUCCEEDED' || status === 'FAILED' ? '2023-10-01T01:00:00Z' : undefined,
quotaConsumed: status === 'SUCCEEDED' ? 500 : undefined,
};
}
175 changes: 162 additions & 13 deletions src/pages/scientificServices/pipelines/views/JobHistory.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { screen } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Teaspoons, TeaspoonsContract } from 'src/libs/ajax/teaspoons/Teaspoons';
import { mockPipelineRun } from 'src/pages/scientificServices/pipelines/utils/mock-utils';
import { asMockedFn, partial, renderWithAppContexts as render } from 'src/testing/test-utils';

import { JobHistory } from './JobHistory';
Expand Down Expand Up @@ -35,17 +37,8 @@ jest.mock('src/libs/nav', () => ({

describe('job history table', () => {
it('renders the job history table', async () => {
const pipelineRuns = [
{
id: '123',
description: 'Test Job',
status: 'RUNNING',
createdAt: '2023-10-01T00:00:00Z',
updatedAt: '2023-10-01T00:00:00Z',
pipelineVersion: 'v1.0.0',
pipelineName: 'array_imputation',
},
];
const pipelineRun = mockPipelineRun('RUNNING');
const pipelineRuns = [pipelineRun];

const mockPipelineRunResponse = {
pageToken: 'nextPageToken',
Expand All @@ -62,7 +55,163 @@ describe('job history table', () => {
render(<JobHistory />);

expect(await screen.findByText('Job History')).toBeInTheDocument();
expect(screen.queryAllByText('Test Job')).toHaveLength(2);
expect(screen.queryAllByText(pipelineRun.description!)).toHaveLength(2);
expect(screen.getByText('In Progress')).toBeInTheDocument();
});

it('shows View Outputs button for SUCCEEDED jobs', async () => {
const pipelineRun = mockPipelineRun('SUCCEEDED');
const pipelineRuns = [pipelineRun];

const mockPipelineRunResponse = {
pageToken: null,
results: pipelineRuns,
totalResults: 1,
};

asMockedFn(Teaspoons).mockReturnValue(
partial<TeaspoonsContract>({
getAllPipelineRuns: jest.fn().mockReturnValue(mockPipelineRunResponse),
})
);

render(<JobHistory />);

await waitFor(() => {
expect(screen.getAllByText(pipelineRun.jobId)).toHaveLength(2);
});

const viewOutputsButton = screen.getByText('View Outputs');
expect(viewOutputsButton).toBeInTheDocument();

expect(screen.queryByText('View Error')).not.toBeInTheDocument();
});

it('shows View Error button for FAILED jobs', async () => {
const pipelineRun = mockPipelineRun('FAILED');
const pipelineRuns = [pipelineRun];

const mockPipelineRunResponse = {
pageToken: null,
results: pipelineRuns,
totalResults: 1,
};

asMockedFn(Teaspoons).mockReturnValue(
partial<TeaspoonsContract>({
getAllPipelineRuns: jest.fn().mockReturnValue(mockPipelineRunResponse),
})
);

render(<JobHistory />);

await waitFor(() => {
expect(screen.getAllByText(pipelineRun.jobId)).toHaveLength(2);
});

const viewErrorButton = screen.getByText('View Error');
expect(viewErrorButton).toBeInTheDocument();

expect(screen.queryByText('View Outputs')).not.toBeInTheDocument();
});

it('shows neither button for RUNNING jobs', async () => {
const pipelineRun = mockPipelineRun('RUNNING');
const pipelineRuns = [pipelineRun];

const mockPipelineRunResponse = {
pageToken: null,
results: pipelineRuns,
totalResults: 1,
};

asMockedFn(Teaspoons).mockReturnValue(
partial<TeaspoonsContract>({
getAllPipelineRuns: jest.fn().mockReturnValue(mockPipelineRunResponse),
})
);

render(<JobHistory />);

await waitFor(() => {
expect(screen.getAllByText(pipelineRun.jobId)).toHaveLength(2);
});

expect(screen.queryByText('View Outputs')).not.toBeInTheDocument();
expect(screen.queryByText('View Error')).not.toBeInTheDocument();
expect(screen.getByText('In Progress')).toBeInTheDocument();
});

it('opens the outputs modal when View Outputs button is clicked', async () => {
const pipelineRun = mockPipelineRun('SUCCEEDED');
const pipelineRuns = [pipelineRun];

const mockPipelineRunResponse = {
pageToken: null,
results: pipelineRuns,
totalResults: 1,
};

const mockPipelineRunResult = {
pipelineRunReport: {
outputs: {
'output1.txt': 'https://example.com/output1.txt',
},
},
};

asMockedFn(Teaspoons).mockReturnValue(
partial<TeaspoonsContract>({
getAllPipelineRuns: jest.fn().mockReturnValue(mockPipelineRunResponse),
getPipelineRunResult: jest.fn().mockResolvedValue(mockPipelineRunResult),
})
);

render(<JobHistory />);

await waitFor(() => {
expect(screen.getAllByText(pipelineRun.jobId)).toHaveLength(2);
});

const user = userEvent.setup();
await user.click(screen.getByText('View Outputs'));

expect(await screen.findByText('Pipeline Outputs', { exact: false })).toBeInTheDocument();
});

it('opens the error modal when View Error button is clicked', async () => {
const pipelineRun = mockPipelineRun('FAILED');
const pipelineRuns = [pipelineRun];

const mockPipelineRunResponse = {
pageToken: null,
results: pipelineRuns,
totalResults: 1,
};

const mockPipelineRunResult = {
errorReport: {
message: 'Test error message',
causes: ['Test error cause'],
},
};

asMockedFn(Teaspoons).mockReturnValue(
partial<TeaspoonsContract>({
getAllPipelineRuns: jest.fn().mockReturnValue(mockPipelineRunResponse),
getPipelineRunResult: jest.fn().mockResolvedValue(mockPipelineRunResult),
})
);

render(<JobHistory />);

await waitFor(() => {
expect(screen.getAllByText(pipelineRun.jobId)).toHaveLength(2);
});

const user = userEvent.setup();
await user.click(screen.getByText('View Error'));

expect(await screen.findByText('Pipeline Error', { exact: false })).toBeInTheDocument();
});
});
Loading
Loading