Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ilab project selector modal #3809

Merged
merged 7 commits into from
Feb 28, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ export const initIntercepts = (
).as('getIlabPipeline');

cy.interceptOdh(
'GET /apis/v2beta1/pipelines/:pipelineId/versions',
'GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/pipelines/:pipelineId/versions',
{
path: { pipelineId: 'instructlab' },
path: { namespace: projectName, serviceName: 'dspa', pipelineId: 'instructlab' },
},
buildMockPipelines([initialMockPipelineVersion]),
).as('getAllPipelineVersions');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ILAB_PIPELINE_NAME } from '~/pages/pipelines/global/modelCustomization/
import { useLatestPipelineVersion } from '~/concepts/pipelines/apiHooks/useLatestPipelineVersion';
import { usePipelineByName } from '~/concepts/pipelines/apiHooks/usePipelineByName';
import { PipelineKF, PipelineVersionKF } from '~/concepts/pipelines/kfTypes';
import { usePipelinesAPI } from '~/concepts/pipelines/context';

export const useIlabPipeline = (): {
ilabPipeline: PipelineKF | null;
Expand All @@ -11,8 +12,11 @@ export const useIlabPipeline = (): {
loadError: Error | undefined;
refresh: () => Promise<PipelineVersionKF | null | undefined>;
} => {
const { pipelinesServer } = usePipelinesAPI();
const [ilabPipeline, ilabPipelineLoaded, ilabPipelineLoadError, refreshIlabPipeline] =
usePipelineByName(ILAB_PIPELINE_NAME);
usePipelineByName(
pipelinesServer.compatible && pipelinesServer.installed ? ILAB_PIPELINE_NAME : '',
);
const [
ilabPipelineVersion,
ilabPipelineVersionLoaded,
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/concepts/projects/ProjectSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type ProjectSelectorProps = {
showTitle?: boolean;
selectorLabel?: string;
isFullWidth?: boolean;
placeholder?: string;
isLoading?: boolean;
};

const ProjectSelector: React.FC<ProjectSelectorProps> = ({
Expand All @@ -28,6 +30,8 @@ const ProjectSelector: React.FC<ProjectSelectorProps> = ({
showTitle = false,
selectorLabel = 'Project',
isFullWidth = false,
placeholder = undefined,
isLoading = false,
}) => {
const { projects } = React.useContext(ProjectsContext);
const selection = projects.find(byName(namespace));
Expand All @@ -41,7 +45,7 @@ const ProjectSelector: React.FC<ProjectSelectorProps> = ({

const selectionDisplayName = selection
? getDisplayNameFromK8sResource(selection)
: invalidDropdownPlaceholder ?? namespace;
: invalidDropdownPlaceholder ?? placeholder ?? namespace;

const filteredProjects = filterLabel
? projects.filter((project) => project.metadata.labels?.[filterLabel] !== undefined)
Expand All @@ -59,6 +63,8 @@ const ProjectSelector: React.FC<ProjectSelectorProps> = ({
searchFocusOnOpen
searchPlaceholder="Project name"
searchValue={searchText}
isLoading={isLoading}
isDisabled={isLoading}
toggleText={toggleLabel}
toggleVariant={primary ? 'primary' : undefined}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as React from 'react';
import { Alert, AlertProps, Button, Stack, StackItem } from '@patternfly/react-core';
import { useNavigate } from 'react-router';
import {
useContinueState,
ContinueCondition,
} from '~/pages/pipelines/global/modelCustomization/startRunModal/useContinueState';

type MissingConditionAlertProps = {
selectedProject: string;
setIsLoadingProject: (isLoading: boolean) => void;
setCanContinue: (canContinue: boolean) => void;
};

type PickedAlertProps = Pick<AlertProps, 'variant' | 'children' | 'title'>;

const ALERT_CONFIG: Record<ContinueCondition, PickedAlertProps> = {
ilabPipelineInstalled: {
variant: 'warning',
title: 'InstructLab pipeline not installed',
children:
'This project is missing an InstructLab pipeline. You can import the InstructLab pipeline into your project.',
},
pipelineServerConfigured: {
variant: 'warning',
title: 'Pipeline server not configured',
children:
'To utilize InstructLab fine-tuning you need a pipeline server configured with an InstructLab pipeline.',
},
pipelineServerAccessible: {
variant: 'danger',
title: 'Pipeline server not accessible',
children:
'The pipeline server is not accessible. To utilize InstructLab fine-tuning you need a pipeline server configured and online with an InstructLab pipeline.',
},
pipelineServerOnline: {
variant: 'danger',
title: 'Pipeline server is offline',
children:
'The pipeline server is offline. To utilize InstructLab fine-tuning you need to start the server.',
},
};

const MissingConditionAlert: React.FC<MissingConditionAlertProps> = ({
selectedProject,
setIsLoadingProject,
setCanContinue,
}) => {
const navigate = useNavigate();
const [alertProps, setAlertProps] = React.useState<PickedAlertProps | null>(null);
const continueState = useContinueState();

React.useEffect(() => {
setAlertProps(null);
setCanContinue(continueState.canContinue);
setIsLoadingProject(continueState.isLoading);

if (continueState.isLoading) {
return;
}

if (!continueState.canContinue) {
setAlertProps(ALERT_CONFIG[continueState.unmetCondition]);
}
}, [continueState, setCanContinue, setIsLoadingProject]);

return (
alertProps && (
<Alert
isInline
variant={alertProps.variant}
title={alertProps.title}
data-testid="missing-condition-alert"
>
<Stack hasGutter>
<StackItem>{alertProps.children}</StackItem>
<StackItem>
<Button
data-testid="go-to-pipelines"
variant="link"
isInline
component="a"
onClick={() => navigate(`/pipelines/${selectedProject}`)}
>
Go to pipelines
</Button>
</StackItem>
</Stack>
</Alert>
)
);
};

export default MissingConditionAlert;
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as React from 'react';
import { Modal } from '@patternfly/react-core/deprecated';
import { Button, Form, FormGroup, Stack, StackItem } from '@patternfly/react-core';
import ProjectSelector from '~/concepts/projects/ProjectSelector';
import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter';
import { PipelineContextProvider } from '~/concepts/pipelines/context';
import MissingConditionAlert from '~/pages/pipelines/global/modelCustomization/startRunModal/MissingConditionAlert';

export type StartRunModalProps = {
onSubmit: (selectedProject: string) => void;
onCancel: () => void;
};

const StartRunModal: React.FC<StartRunModalProps> = ({ onSubmit, onCancel }) => {
const [selectedProject, setSelectedProject] = React.useState<string | null>(null);
const [isLoadingProject, setIsLoadingProject] = React.useState<boolean>(false);
const [canContinue, setCanContinue] = React.useState<boolean>(false);

Check warning on line 17 in frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx#L14-L17

Added lines #L14 - L17 were not covered by tests

return (

Check warning on line 19 in frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx#L19

Added line #L19 was not covered by tests
<Modal
title="Start an InstructLab run"
isOpen
onClose={onCancel}
footer={
<DashboardModalFooter
onSubmit={() => {
if (selectedProject) {
onSubmit(selectedProject);

Check warning on line 28 in frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx#L26-L28

Added lines #L26 - L28 were not covered by tests
}
}}
onCancel={onCancel}
submitLabel="Continue to run details"
isSubmitDisabled={!canContinue}
/>
}
variant="medium"
data-testid="start-run-modal"
>
<Form>
<Stack hasGutter>
<StackItem>
Fine-tune your models to improve their performance, accuracy, and task specialization,
using the{' '}
<Button
data-testid="lab-method"
variant="link"
isInline
component="a"
style={{ textDecoration: 'none' }}
onClick={() => {

Check warning on line 50 in frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx#L50

Added line #L50 was not covered by tests
// TODO: Link to documentation
}}
>
LAB method
</Button>
. Before creating a run, a taxonomy is needed and a teacher and judge model must be
configured.{' '}
<Button
data-testid="learn-more-prerequisites"
variant="link"
isInline
component="a"
style={{ textDecoration: 'none' }}
onClick={() => {

Check warning on line 64 in frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx#L64

Added line #L64 was not covered by tests
// TODO: Link to documentation
}}
>
Learn more about prerequisites for InstructLab fine-tuning
</Button>
.
</StackItem>
<StackItem>
<FormGroup
label="Data science project"
fieldId="start-run-modal-project-name"
isRequired
>
<Stack hasGutter>
<StackItem>
<ProjectSelector
isFullWidth
onSelection={(projectName) => {
setSelectedProject(projectName);

Check warning on line 83 in frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx#L82-L83

Added lines #L82 - L83 were not covered by tests
}}
namespace={selectedProject ?? ''}

Check warning on line 85 in frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx#L85

Added line #L85 was not covered by tests
placeholder="Select a Data science project"
isLoading={isLoadingProject}
/>
</StackItem>
<StackItem>The InstructLab pipeline will run in the selected project</StackItem>
{selectedProject && (
<StackItem>

Check warning on line 92 in frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/pipelines/global/modelCustomization/startRunModal/StartRunModal.tsx#L91-L92

Added lines #L91 - L92 were not covered by tests
<PipelineContextProvider namespace={selectedProject}>
<MissingConditionAlert
key={selectedProject}
selectedProject={selectedProject}
setIsLoadingProject={setIsLoadingProject}
setCanContinue={setCanContinue}
/>
</PipelineContextProvider>
</StackItem>
)}
</Stack>
</FormGroup>
</StackItem>
</Stack>
</Form>
</Modal>
);
};

export default StartRunModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
ContinueCondition,
useContinueState,
} from '~/pages/pipelines/global/modelCustomization/startRunModal/useContinueState';
import MissingConditionAlert from '~/pages/pipelines/global/modelCustomization/startRunModal/MissingConditionAlert';
import '@testing-library/jest-dom';

jest.mock('~/pages/pipelines/global/modelCustomization/startRunModal/useContinueState', () => ({
useContinueState: jest.fn(),
}));

jest.mock('react-router', () => ({
useNavigate: jest.fn(() => jest.fn()),
}));

describe('MissingConditionAlert', () => {
const TEST_PROJECT = 'test-project';

const setIsLoadingProject = jest.fn();
const setCanContinue = jest.fn();
const useContinueStateMock = useContinueState as jest.Mock;

const renderComponent = () =>
render(
<MissingConditionAlert
selectedProject={TEST_PROJECT}
setIsLoadingProject={setIsLoadingProject}
setCanContinue={setCanContinue}
/>,
);

afterEach(() => {
jest.clearAllMocks();
});

it('should not render the alert when there are no unmet conditions', () => {
useContinueStateMock.mockReturnValue({
canContinue: true,
isLoading: false,
unmetCondition: null,
});

renderComponent();
expect(screen.queryByTestId('missing-condition-alert')).not.toBeInTheDocument();
});

it('should not render the alert when it is still loading', () => {
useContinueStateMock.mockReturnValue({
canContinue: false,
isLoading: true,
unmetCondition: null,
});

renderComponent();
expect(screen.queryByTestId('missing-condition-alert')).not.toBeInTheDocument();
});

it.each<ContinueCondition>([
'ilabPipelineInstalled',
'pipelineServerConfigured',
'pipelineServerAccessible',
'pipelineServerOnline',
])(
"should render the alert when the '%s' condition is unmet",
async (unmetCondition: ContinueCondition) => {
useContinueStateMock.mockReturnValue({
canContinue: false,
isLoading: false,
unmetCondition,
});

renderComponent();

await waitFor(() =>
expect(screen.getByTestId('missing-condition-alert')).toBeInTheDocument(),
);
},
);

it('should navigate to pipelines when the button is clicked', async () => {
useContinueStateMock.mockReturnValue({
canContinue: false,
isLoading: false,
unmetCondition: 'pipelineServerConfigured',
});

const navigateMock = jest.fn();
jest.mocked(require('react-router').useNavigate).mockReturnValue(navigateMock);

renderComponent();

await waitFor(() => expect(screen.getByTestId('missing-condition-alert')).toBeInTheDocument());
const button = screen.getByTestId('go-to-pipelines');
await userEvent.click(button);

expect(navigateMock).toHaveBeenCalledWith(`/pipelines/${TEST_PROJECT}`);
});
});
Loading
Loading