Skip to content

Commit

Permalink
Checkpoint
Browse files Browse the repository at this point in the history
- Basics of cell types
- Persistence
- Iterate on styling
  • Loading branch information
dmfalke committed Nov 21, 2024
1 parent 34ca45e commit e162ad7
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 70 deletions.
2 changes: 1 addition & 1 deletion packages/libs/eda/src/lib/core/components/VariableLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ export const VariableLink = forwardRef(
tabIndex={0}
style={finalStyle}
onKeyDown={(event) => {
event.preventDefault();
if (disabled) {
return;
}
if (event.key === 'Enter' || event.key === ' ') {
linkConfig.onClick(value);
event.preventDefault();
}
}}
onClick={(event) => {
Expand Down
35 changes: 35 additions & 0 deletions packages/libs/eda/src/lib/notebook/EdaNotebook.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.EdaNotebook {
.Heading {
display: flex;
gap: 2em;
align-items: baseline;
}

.Paper {
max-width: 1250px;
padding: 1em;
margin: 1em auto;
background-color: #f3f3f3;
box-shadow: 0 0 2px #b5b5b5;

> * + * {
margin-block-start: 1rem;
}
h2,
h3 {
padding: 0;
}
h3 {
font-size: 1em;
font-weight: 400;
line-height: 1.5;
}
}

.Title {
fieldset {
padding: 0;
margin: 0;
}
}
}
153 changes: 84 additions & 69 deletions packages/libs/eda/src/lib/notebook/EdaNotebookAnalysis.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,101 @@
import React, { useState } from 'react';
import {
Filter,
useAnalysis,
useStudyEntities,
useStudyMetadata,
useStudyRecord,
} from '../core';
// Notes
// =====
//
// - For now, we will only support "fixed" notebooks. If we want to allow "custom" notebooks,
// we have to make some decisions.
// - Do we want a top-down data flow? E.g., subsetting is global for an analysis.
// - Do we want to separate compute config from visualization? If so, how do we
// support that in the UI?
// - Do we want text-based cells?
// - Do we want download cells? It could have a preview.
//

import React, { useCallback, useMemo } from 'react';
import { useAnalysis, useStudyRecord } from '../core';
import { safeHtml } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils';
import { SaveableTextEditor } from '@veupathdb/wdk-client/lib/Components';
import Subsetting from '../workspace/Subsetting';
import { useEntityCounts } from '../core/hooks/entityCounts';
import FilterChipList from '../core/components/FilterChipList';
import { ExpandablePanel } from '@veupathdb/coreui';
import { NotebookCell as NotebookCellType } from './Types';
import { NotebookCell } from './NotebookCell';

import './EdaNotebook.scss';

interface NotebookSettings {
/** Ordered array of notebook cells */
cells: NotebookCellType[];
}

const NOTEBOOK_UI_SETTINGS_KEY = '@@NOTEBOOK@@';

interface Props {
analysisId: string;
}

export function EdaNotebookAnalysis(props: Props) {
const { analysisId } = props;
const studyRecord = useStudyRecord();
const analysisState = useAnalysis(
analysisId === 'new' ? undefined : analysisId
);
const studyRecord = useStudyRecord();
const studyMetadata = useStudyMetadata();
const entities = useStudyEntities();
const totalCountsResult = useEntityCounts();
const filteredCountsResult = useEntityCounts(
analysisState.analysis?.descriptor.subset.descriptor
const { analysis } = analysisState;
const notebookSettings = useMemo((): NotebookSettings => {
const storedSettings =
analysis?.descriptor.subset.uiSettings[NOTEBOOK_UI_SETTINGS_KEY];
if (storedSettings == null)
return {
cells: [
{
type: 'subset',
title: 'Subset data',
},
],
};
return storedSettings as any as NotebookSettings;
}, [analysis]);
const updateCell = useCallback(
(cell: Partial<Omit<NotebookCellType, 'type'>>, cellIndex: number) => {
const oldCell = notebookSettings.cells[cellIndex];
const newCell = { ...oldCell, ...cell };
const nextCells = notebookSettings.cells.concat();
nextCells[cellIndex] = newCell;
const nextSettings = {
...notebookSettings,
cells: nextCells,
};
analysisState.setVariableUISettings({
[NOTEBOOK_UI_SETTINGS_KEY]: nextSettings,
});
},
[analysisState, notebookSettings]
);
const [entityId, setEntityId] = useState<string>();
const [variableId, setVariableId] = useState<string>();
return (
<div>
<h1>EDA Notebook</h1>
{safeHtml(studyRecord.displayName, null, 'h2')}
<h3>
<SaveableTextEditor
value={analysisState.analysis?.displayName ?? ''}
onSave={analysisState.setName}
/>
</h3>
<details>
<summary>
Subset &nbsp;&nbsp;
<FilterChipList
filters={analysisState.analysis?.descriptor.subset.descriptor}
entities={entities}
selectedEntityId={entityId}
selectedVariableId={variableId}
removeFilter={(filter) =>
analysisState.setFilters((filters) =>
filters.filter(
(f) =>
f.entityId !== filter.entityId ||
f.variableId !== filter.variableId
)
)
}
variableLinkConfig={{
type: 'button',
onClick: (value) => {
setEntityId(value?.entityId);
setVariableId(value?.variableId);
},
}}
/>
</summary>
<Subsetting
analysisState={analysisState}
entityId={entityId ?? ''}
variableId={variableId ?? ''}
totalCounts={totalCountsResult.value}
filteredCounts={filteredCountsResult.value}
variableLinkConfig={{
type: 'button',
onClick: (value) => {
setEntityId(value?.entityId);
setVariableId(value?.variableId);
},
}}
/>
</details>
<div className="EdaNotebook">
<div className="Heading">
<h1>EDA Notebook</h1>
</div>
<div className="Paper">
<div>
<h2>
<SaveableTextEditor
className="Title"
value={analysisState.analysis?.displayName ?? ''}
onSave={analysisState.setName}
/>
</h2>
<h3>Study: {safeHtml(studyRecord.displayName)}</h3>
</div>
{notebookSettings.cells.map((cell, index) => (
<ExpandablePanel title={cell.title} subTitle={{}} themeRole="primary">
<div style={{ padding: '1em' }}>
<NotebookCell
analysisState={analysisState}
cell={cell}
updateCell={(update) => updateCell(update, index)}
/>
</div>
</ExpandablePanel>
))}
</div>
</div>
);
}
28 changes: 28 additions & 0 deletions packages/libs/eda/src/lib/notebook/NotebookCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { AnalysisState } from '../core';
import { NotebookCell as NotebookCellType } from './Types';
import { SubsettingNotebookCell } from './SubsettingNotebookCell';

interface Props {
analysisState: AnalysisState;
cell: NotebookCellType;
updateCell: (cell: Partial<Omit<NotebookCellType, 'type'>>) => void;
}

/**
* Top-level component that delegates to imeplementations of NotebookCell variants.
*/
export function NotebookCell(props: Props) {
const { cell, analysisState, updateCell } = props;
switch (cell.type) {
case 'subset':
return (
<SubsettingNotebookCell
cell={cell}
analysisState={analysisState}
updateCell={updateCell}
/>
);
default:
return null;
}
}
58 changes: 58 additions & 0 deletions packages/libs/eda/src/lib/notebook/SubsettingNotebookCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useMemo } from 'react';
import { useEntityCounts } from '../core/hooks/entityCounts';
import { useStudyEntities } from '../core/hooks/workspace';
import { NotebookCellComponentProps } from './Types';
import { VariableLinkConfig } from '../core/components/VariableLink';
import FilterChipList from '../core/components/FilterChipList';
import Subsetting from '../workspace/Subsetting';

export function SubsettingNotebookCell(
props: NotebookCellComponentProps<'subset'>
) {
const { analysisState, cell, updateCell } = props;
const { selectedVariable } = cell;
const entities = useStudyEntities();
const totalCountsResult = useEntityCounts();
const filteredCountsResult = useEntityCounts(
analysisState.analysis?.descriptor.subset.descriptor
);
const variableLinkConfig = useMemo(
(): VariableLinkConfig => ({
type: 'button',
onClick: (selectedVariable) => {
updateCell({ selectedVariable });
},
}),
[updateCell]
);
return (
<div>
<div>
<FilterChipList
filters={analysisState.analysis?.descriptor.subset.descriptor}
entities={entities}
selectedEntityId={selectedVariable?.entityId}
selectedVariableId={selectedVariable?.variableId}
removeFilter={(filter) => {
analysisState.setFilters((filters) =>
filters.filter(
(f) =>
f.entityId !== filter.entityId ||
f.variableId !== filter.variableId
)
);
}}
variableLinkConfig={variableLinkConfig}
/>
</div>
<Subsetting
analysisState={analysisState}
entityId={selectedVariable?.entityId ?? ''}
variableId={selectedVariable?.variableId ?? ''}
totalCounts={totalCountsResult.value}
filteredCounts={filteredCountsResult.value}
variableLinkConfig={variableLinkConfig}
/>
</div>
);
}
44 changes: 44 additions & 0 deletions packages/libs/eda/src/lib/notebook/Types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { AnalysisState } from '../core/hooks/analysis';
import { VariableDescriptor } from '../core/types/variable';

export interface NotebookCellBase<T extends string> {
type: T;
title: string;
}

export interface SubsettingNotebookCell extends NotebookCellBase<'subset'> {
selectedVariable?: Partial<VariableDescriptor>;
}

export interface ComputeNotebookCell extends NotebookCellBase<'compute'> {
computeId: string;
}

export interface VisualizationNotebookCell
extends NotebookCellBase<'visualization'> {
visualizationId: string;
}

export interface TextNotebookCell extends NotebookCellBase<'text'> {
text: string;
}

export type NotebookCell =
| SubsettingNotebookCell
| ComputeNotebookCell
| VisualizationNotebookCell
| TextNotebookCell;

type FindByType<Union, Type> = Union extends { type: Type } ? Union : never;

export type NotebookCellOfType<T extends NotebookCell['type']> = FindByType<
NotebookCell,
T
>;

export interface NotebookCellComponentProps<T extends NotebookCell['type']> {
analysisState: AnalysisState;
cell: NotebookCellOfType<T>;
// Allow partial updates, but don't allow `type` to be changed.
updateCell: (cell: Omit<Partial<NotebookCellOfType<T>>, 'type'>) => void;
}

0 comments on commit e162ad7

Please sign in to comment.