Skip to content

Commit

Permalink
Austenem/CAT-898 R template updates (#3540)
Browse files Browse the repository at this point in the history
* re-sort and add r tags

* filter templates correctly

* select r template automatically

* add tooltip to r templates

* add changelog

* naming updates

* rely on r tag from api and review changes

* review changes
  • Loading branch information
austenem authored Sep 17, 2024
1 parent 0f45269 commit 8c0df6b
Show file tree
Hide file tree
Showing 11 changed files with 86 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGELOG-r-template-updates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Update Launch Workspace dialogs to account for new R template.
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import {
TemplatesResponse,
CreateTemplateNotebooksTypes,
TemplateTagsResponse,
TemplatesTypes,
TemplateExample,
TemplatesTypes,
WorkspacesEventCategories,
} from 'js/components/workspaces/types';
import { useCreateAndLaunchWorkspace, useCreateTemplates } from 'js/components/workspaces/hooks';
import { buildDatasetSymlinks } from 'js/components/workspaces/utils';
import { useWorkspaceToasts } from 'js/components/workspaces/toastHooks';
import { useJobTypes } from 'js/components/workspaces/api';
import { DEFAULT_JOB_TYPE } from 'js/components/workspaces/constants';
import { DEFAULT_JOB_TYPE, JUPYTER_LAB_R_JOB_TYPE, R_TEMPLATE_TITLE } from 'js/components/workspaces/constants';

interface UserTemplatesTypes {
templatesURL: string;
Expand Down Expand Up @@ -51,7 +51,14 @@ function useWorkspaceTemplates(tags: string[] = []) {
const templates = result?.data?.data ?? {};

const filteredTemplates = Object.fromEntries(
Object.entries(templates).filter(([, template]) => !template?.is_hidden),
Object.entries(templates)
.filter(([, template]) => !template?.is_hidden)
.map(([key, template]) => {
const newTitle = template?.job_types?.includes(JUPYTER_LAB_R_JOB_TYPE)
? `${template.title} (${R_TEMPLATE_TITLE})`
: template.title;
return [key, { ...template, title: newTitle }];
}),
) as TemplatesTypes;

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
DEFAULT_JOB_TYPE,
DEFAULT_MEMORY_MB,
DEFAULT_NUM_CPUS,
DEFAULT_TEMPLATE_KEY,
DEFAULT_PYTHON_TEMPLATE_KEY,
DEFAULT_TIME_LIMIT_MINUTES,
} from '../constants';
import { useDatasetsAutocomplete } from '../AddDatasetsTable/hooks';
Expand Down Expand Up @@ -59,7 +59,7 @@ const schema = z

function useCreateWorkspaceForm({
defaultName,
defaultTemplate = DEFAULT_TEMPLATE_KEY,
defaultTemplate = DEFAULT_PYTHON_TEMPLATE_KEY,
defaultJobType = DEFAULT_JOB_TYPE,
defaultResourceOptions = {
num_cpus: DEFAULT_NUM_CPUS,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, ChangeEvent } from 'react';
import React, { useCallback, ChangeEvent, useMemo, useEffect } from 'react';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
Expand All @@ -7,9 +7,12 @@ import { useController, Control, Path } from 'react-hook-form';
import { SpacedSectionButtonRow } from 'js/shared-styles/sections/SectionButtonRow';
import { useSelectItems } from 'js/hooks/useSelectItems';
import ErrorOrWarningMessages from 'js/shared-styles/alerts/ErrorOrWarningMessages';
import { DEFAULT_R_TEMPLATE_KEY, JUPYTER_LAB_R_JOB_TYPE } from 'js/components/workspaces/constants';

import { TemplatesTypes, WorkspacesEventCategories } from '../types';
import TemplateGrid from '../TemplateGrid';
import { FormWithTemplates } from '../NewWorkspaceDialog/useCreateWorkspaceForm';
import { sortTemplates } from '../utils';

interface TemplateGridProps {
disabledTemplates?: TemplatesTypes;
Expand Down Expand Up @@ -39,9 +42,26 @@ function SelectableTemplateGrid<FormType extends FormWithTemplates>({
control,
name: inputName as Path<FormType>,
});
const { selectedItems: selectedTemplates, setSelectedItems: setSelectedTemplates } = useSelectItems(
field.value satisfies FormType[typeof inputName],
);
const {
selectedItems: selectedTemplates,
setSelectedItems: setSelectedTemplates,
addItem,
} = useSelectItems(field.value satisfies FormType[typeof inputName]);

const { field: jobType } = useController({
name: 'workspaceJobTypeId' as Path<FormType>,
control,
rules: { required: true },
});

// If the Python + R job type is selected, select the default R template
useEffect(() => {
if (jobType.value === JUPYTER_LAB_R_JOB_TYPE) {
addItem(DEFAULT_R_TEMPLATE_KEY);
}
}, [jobType.value, addItem]);

const sortedTemplates = useMemo(() => sortTemplates(templates, disabledTemplates), [templates, disabledTemplates]);

const updateTemplates = useCallback(
(templateKeys: string[]) => {
Expand Down Expand Up @@ -87,11 +107,12 @@ function SelectableTemplateGrid<FormType extends FormWithTemplates>({
}
/>
<TemplateGrid
templates={templates}
templates={sortedTemplates}
selectItem={selectItem}
selectedTemplates={selectedTemplates}
disabledTemplates={disabledTemplates}
trackingInfo={{ category: WorkspacesEventCategories.WorkspaceDialog }}
jobType={jobType.value as string}
/>
</Box>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Grid from '@mui/material/Grid';
import SelectableCard from 'js/shared-styles/cards/SelectableCard/SelectableCard';
import { InternalLink } from 'js/shared-styles/Links';
import { sortTemplates } from 'js/components/workspaces/utils';
import { JUPYTER_LAB_R_JOB_TYPE } from 'js/components/workspaces/constants';
import { TemplatesTypes, WorkspacesEventInfo } from 'js/components/workspaces/types';
import { trackEvent } from 'js/helpers/trackers';

Expand All @@ -12,6 +13,7 @@ interface TemplateGridProps {
selectedTemplates?: Set<string>;
disabledTemplates?: TemplatesTypes;
trackingInfo: WorkspacesEventInfo;
jobType?: string;
}

function TemplateGrid({
Expand All @@ -20,12 +22,24 @@ function TemplateGrid({
selectedTemplates = new Set([]),
disabledTemplates = {},
trackingInfo,
jobType,
}: TemplateGridProps) {
const getTooltip = (templateKey: string, job_types?: string[]) => {
if (templateKey in disabledTemplates) {
return 'This template is already in your workspace.';
}
// If the template is an R template and the job type is not R
if (jobType !== JUPYTER_LAB_R_JOB_TYPE && job_types?.includes(JUPYTER_LAB_R_JOB_TYPE)) {
return 'This template is not compatible with your current environment. To avoid potential issues, please ensure that you have selected the correct environment for your workspace.';
}
return undefined;
};

const sortedTemplates = useMemo(() => sortTemplates(templates, disabledTemplates), [templates, disabledTemplates]);

return (
<Grid container alignItems="stretch" sx={{ maxHeight: '625px', overflowY: 'auto' }}>
{Object.entries(sortedTemplates).map(([templateKey, { title, description, tags }]) => (
{Object.entries(sortedTemplates).map(([templateKey, { title, description, tags, job_types }]) => (
<Grid item md={4} xs={12} key={templateKey} paddingBottom={2} paddingX={1}>
<SelectableCard
title={
Expand All @@ -50,7 +64,8 @@ function TemplateGrid({
sx={{ height: '100%', minHeight: 225 }}
key={templateKey}
disabled={templateKey in disabledTemplates}
tooltip={templateKey in disabledTemplates ? 'This template is already in your workspace.' : undefined}
tooltip={getTooltip(templateKey, job_types)}
jobTypes={job_types}
/>
</Grid>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React from 'react';
import { Control } from 'react-hook-form';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';

import { SelectedItems } from 'js/hooks/useSelectItems';
import Step, { StepDescription } from 'js/shared-styles/surfaces/Step';
import ContactUsLink from 'js/shared-styles/Links/ContactUsLink';
import Typography from '@mui/material/Typography';
import SelectableTemplateGrid from '../SelectableTemplateGrid';
import { TemplatesTypes, WorkspacesEventCategories } from '../types';
import { FormWithTemplates } from '../NewWorkspaceDialog/useCreateWorkspaceForm';
import TemplateTagsAutocomplete from '../TemplateTagsAutocomplete';
import SelectableTemplateGrid from 'js/components/workspaces/SelectableTemplateGrid';
import { TemplatesTypes, WorkspacesEventCategories } from 'js/components/workspaces/types';
import { FormWithTemplates } from 'js/components/workspaces/NewWorkspaceDialog/useCreateWorkspaceForm';
import TemplateTagsAutocomplete from 'js/components/workspaces/TemplateTagsAutocomplete';

function ContactPrompt() {
return (
Expand All @@ -24,8 +24,6 @@ const description = [
<ContactPrompt key="configure-workspace-contact" />,
];

const recommendedTags = ['visualization', 'api'];

interface TemplateSelectProps<FormType extends FormWithTemplates> {
title: string;
stepIndex?: number;
Expand Down Expand Up @@ -58,7 +56,6 @@ function TemplateSelectStep<FormType extends FormWithTemplates>({
</Typography>
<TemplateTagsAutocomplete
selectedTags={selectedTags}
recommendedTags={recommendedTags}
toggleTag={toggleTag}
setSelectedTags={setSelectedTags}
selectedRecommendedTags={selectedRecommendedTags}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import Typography from '@mui/material/Typography';
import SelectableChip from 'js/shared-styles/chips/SelectableChip';
import MultiAutocomplete from 'js/shared-styles/inputs/MultiAutocomplete';
import { SelectedItems } from 'js/hooks/useSelectItems';
import { RECOMMENDED_TAGS } from 'js/components/workspaces/constants';
import { useWorkspaceTemplateTags } from 'js/components/workspaces/NewWorkspaceDialog/hooks';
import { trackEvent } from 'js/helpers/trackers';
import { WorkspacesEventInfo } from 'js/components/workspaces/types';

interface TemplateTagsAutocompleteProps {
selectedTags: string[];
recommendedTags: string[];
toggleTag: (itemKey: string) => void;
setSelectedTags: React.Dispatch<React.SetStateAction<string[]>>;
selectedRecommendedTags: SelectedItems;
Expand All @@ -30,7 +30,6 @@ function TagComponent({ option, ...rest }: TagTypes) {

function TemplateTagsAutocomplete({
selectedTags,
recommendedTags,
toggleTag,
setSelectedTags,
selectedRecommendedTags,
Expand All @@ -43,7 +42,7 @@ function TemplateTagsAutocomplete({
<MultiAutocomplete
value={selectedTags}
options={Object.keys(tags)
.filter((tag) => !recommendedTags.includes(tag))
.filter((tag) => !RECOMMENDED_TAGS.includes(tag))
.sort((a, b) => a.localeCompare(b))}
multiple
filterSelectedOptions
Expand Down Expand Up @@ -73,7 +72,7 @@ function TemplateTagsAutocomplete({
Recommended Tags
</Typography>
<Stack spacing={2} direction="row" useFlexGap flexWrap="wrap">
{recommendedTags.map((tag) => (
{RECOMMENDED_TAGS.map((tag) => (
<SelectableChip
isSelected={selectedRecommendedTags.has(tag)}
label={tag}
Expand Down
9 changes: 8 additions & 1 deletion context/app/static/js/components/workspaces/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ export const JUPYTER_LAB_NON_GPU_JOB_TYPE = 'jupyter_lab_non_gpu_common_packages

/* Workspace defaults */
export const DEFAULT_JOB_TYPE = JUPYTER_LAB_JOB_TYPE;
export const DEFAULT_TEMPLATE_KEY = 'blank';
export const DEFAULT_PYTHON_TEMPLATE_KEY = 'blank';
export const DEFAULT_R_TEMPLATE_KEY = 'blank_r';

/* Workspace R templates */
export const R_TEMPLATE_TAG = 'R';
export const R_TEMPLATE_TITLE = 'Jupyter Lab: Python + R';

export const RECOMMENDED_TAGS = ['visualization', 'api', R_TEMPLATE_TAG];

/* Workspace resource defaults */
export const DEFAULT_NUM_CPUS = 1;
Expand Down
1 change: 1 addition & 0 deletions context/app/static/js/components/workspaces/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ interface TemplateTypes {
is_multi_dataset_template: boolean;
template_format: string;
is_hidden: boolean;
job_types?: string[];
examples: TemplateExample[];
}

Expand Down
18 changes: 13 additions & 5 deletions context/app/static/js/components/workspaces/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
DEFAULT_JOB_TYPE,
DEFAULT_MEMORY_MB,
DEFAULT_NUM_CPUS,
DEFAULT_TEMPLATE_KEY,
DEFAULT_PYTHON_TEMPLATE_KEY,
DEFAULT_R_TEMPLATE_KEY,
DEFAULT_TIME_LIMIT_MINUTES,
} from './constants';
import {
Expand Down Expand Up @@ -316,8 +317,8 @@ function getDefaultJobType({ workspace }: { workspace: Workspace }) {
}

/**
* Sort templates alphabetically by title, with all disabled templates first, then the default template.
* Ex: [disabled1, disabled2, default, templateA, templateB, ...]
* Sort templates alphabetically by title, with all disabled templates first, then the default python template, then the default R template.
* Ex: [disabled1, disabled2, defaultPython, defaultR, templateA, templateB, ...]
* @param templates The templates to sort.
* @param disabledTemplates The templates that are disabled.
* @returns The sorted templates.
Expand All @@ -328,12 +329,19 @@ function sortTemplates(templates: TemplatesTypes, disabledTemplates?: TemplatesT
const isSelectedA = disabledTemplates && keyA in disabledTemplates;
const isSelectedB = disabledTemplates && keyB in disabledTemplates;

// Disabled templates
if (isSelectedA && !isSelectedB) return -1;
if (!isSelectedA && isSelectedB) return 1;

if (keyA === DEFAULT_TEMPLATE_KEY && keyB !== DEFAULT_TEMPLATE_KEY) return -1;
if (keyB === DEFAULT_TEMPLATE_KEY && keyA !== DEFAULT_TEMPLATE_KEY) return 1;
// Default Python template
if (keyA === DEFAULT_PYTHON_TEMPLATE_KEY && keyB !== DEFAULT_PYTHON_TEMPLATE_KEY) return -1;
if (keyB === DEFAULT_PYTHON_TEMPLATE_KEY && keyA !== DEFAULT_PYTHON_TEMPLATE_KEY) return 1;

// Default R template
if (keyA === DEFAULT_R_TEMPLATE_KEY && keyB !== DEFAULT_PYTHON_TEMPLATE_KEY) return -1;
if (keyB === DEFAULT_R_TEMPLATE_KEY && keyA !== DEFAULT_PYTHON_TEMPLATE_KEY) return 1;

// Alphabetical sorting by title for remaining templates
return templateA.title.localeCompare(templateB.title);
}),
) as TemplatesTypes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,6 @@ function TemplatePreviewSection({ trackingInfo }: TemplatePreviewSectionProps) {
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const { templates } = useWorkspaceTemplates([...selectedTags, ...selectedRecommendedTags]);

const recommendedTags = ['visualization', 'api'];

return (
<Stack spacing={3}>
{!isAuthenticated && (
Expand All @@ -159,7 +157,6 @@ function TemplatePreviewSection({ trackingInfo }: TemplatePreviewSectionProps) {
<Stack spacing={2}>
<TemplateTagsAutocomplete
selectedTags={selectedTags}
recommendedTags={recommendedTags}
toggleTag={toggleTag}
setSelectedTags={setSelectedTags}
selectedRecommendedTags={selectedRecommendedTags}
Expand Down

0 comments on commit 8c0df6b

Please sign in to comment.