Skip to content

Commit 263eda3

Browse files
committed
Implement notebook side navigation
1 parent f7207b4 commit 263eda3

File tree

8 files changed

+248
-13
lines changed

8 files changed

+248
-13
lines changed

src/AnonymizationStep/AnonymizationStep.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { FunctionComponent, useEffect, useState } from 'react';
22
import { Button, Descriptions, Divider, message, Result, Typography } from 'antd';
33
import { DownloadOutlined } from '@ant-design/icons';
44

5+
import { NotebookNavAnchor, NotebookNavStep } from '../Notebook';
56
import { anonymizer, computeAnonymizationStats, formatPercentage, useCachedData } from '../shared';
67
import { AnonymizationStats, AnonymizedQueryResult, BucketColumn, TableSchema } from '../types';
78
import { AnonymizedResultsTable } from './AnonymizedResultsTable';
@@ -34,6 +35,7 @@ function AnonymizationSummary({ result, loading }: CommonProps) {
3435

3536
return (
3637
<div className="AnonymizationSummary loading notebook-step">
38+
<NotebookNavAnchor step={NotebookNavStep.AnonymizationSummary} status={loading ? 'loading' : 'done'} />
3739
<Title level={3}>Anonymization summary</Title>
3840
{result.rows.length === MAX_ROWS && (
3941
<div className="mb-1">
@@ -78,21 +80,34 @@ async function exportResult(schema: TableSchema, bucketColumns: BucketColumn[])
7880
}
7981

8082
function AnonymizationResults({ schema, bucketColumns, result, loading }: CommonProps) {
83+
const [exported, setExported] = useState(false);
84+
useEffect(() => {
85+
setExported(false);
86+
}, [bucketColumns]);
87+
8188
return (
8289
<div className="AnonymizationResults notebook-step">
90+
<NotebookNavAnchor step={NotebookNavStep.AnonymizedResults} status={loading ? 'loading' : 'done'} />
8391
<Title level={3}>Anonymized data</Title>
8492
<div className="mb-1">
8593
<Text>Here is what the result looks like:</Text>
8694
{result.rows.length === MAX_ROWS && <Text type="secondary"> (only the first {MAX_ROWS} rows are shown)</Text>}
8795
</div>
8896
<AnonymizedResultsTable loading={loading} result={result} />
97+
<NotebookNavAnchor
98+
step={NotebookNavStep.CsvExport}
99+
status={loading ? 'inactive' : exported ? 'done' : 'active'}
100+
/>
89101
<Button
90102
icon={<DownloadOutlined />}
91103
className="AnonymizationResults-export-button"
92104
type="primary"
93105
size="large"
94106
disabled={loading || !result.rows.length}
95-
onClick={() => exportResult(schema, bucketColumns)}
107+
onClick={() => {
108+
exportResult(schema, bucketColumns);
109+
setExported(true);
110+
}}
96111
>
97112
Export anonymized data to CSV
98113
</Button>
@@ -125,6 +140,8 @@ export const AnonymizationStep: FunctionComponent<AnonymizationStepProps> = ({ b
125140
case 'failed':
126141
return (
127142
<div className="AnonymizationStep notebook-step failed">
143+
<NotebookNavAnchor step={NotebookNavStep.AnonymizationSummary} status="failed" />
144+
<NotebookNavAnchor step={NotebookNavStep.AnonymizedResults} status="failed" />
128145
<Result status="error" title="Anonymization failed" subTitle="Something went wrong." />
129146
</div>
130147
);

src/ColumnSelectionStep/ColumnSelectionStep.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CloseCircleOutlined } from '@ant-design/icons';
44
import { useImmer } from 'use-immer';
55
import { assign } from 'lodash';
66

7+
import { NotebookNavAnchor, NotebookNavStep } from '../Notebook';
78
import { useMemoStable } from '../shared';
89
import { BucketColumn, ColumnType, NumericGeneralization, StringGeneralization, TableSchema } from '../types';
910

@@ -140,6 +141,7 @@ export const ColumnSelectionStep: FunctionComponent<ColumnSelectionStepProps> =
140141
return (
141142
<>
142143
<div className="ColumnSelectionStep notebook-step">
144+
<NotebookNavAnchor step={NotebookNavStep.ColumnSelection} status={anySelected ? 'done' : 'active'} />
143145
<Title level={3}>Select columns for anonymization</Title>
144146
<table className="ColumnSelectionStep-table">
145147
<thead>

src/FileLoadStep/FileLoadStep.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { FunctionComponent, useCallback, useState } from 'react';
22
import { Divider, Typography, Upload } from 'antd';
33
import { FileOutlined } from '@ant-design/icons';
44

5+
import { NotebookNavAnchor, NotebookNavStep } from '../Notebook';
56
import { File } from '../types';
67

78
const { Dragger } = Upload;
@@ -24,6 +25,7 @@ export const FileLoadStep: FunctionComponent<FileLoadStepProps> = ({ children, o
2425
return (
2526
<>
2627
<div className="FileLoadStep notebook-step">
28+
<NotebookNavAnchor step={NotebookNavStep.CsvImport} status={file ? 'done' : 'active'} />
2729
<Title level={3}>Import data to anonymize</Title>
2830
<Dragger
2931
accept=".csv"

src/Notebook/Notebook.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,14 @@
55
.Notebook .notebook-step {
66
margin: 32px 0;
77
}
8+
9+
.Notebook-nav {
10+
position: fixed;
11+
top: 60px;
12+
left: 24px;
13+
max-width: 300px;
14+
}
15+
16+
.Notebook-content {
17+
margin-left: 316px;
18+
}

src/Notebook/Notebook.tsx

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import React, { FunctionComponent } from 'react';
2+
23
import { FileLoadStep } from '../FileLoadStep';
34
import { SchemaLoadStep } from '../SchemaLoadStep';
45
import { ColumnSelectionStep } from '../ColumnSelectionStep';
56
import { AnonymizationStep } from '../AnonymizationStep';
7+
import { NotebookNavProvider, NotebookNav } from './notebook-nav';
68

79
import './Notebook.css';
810

@@ -12,18 +14,25 @@ export type NotebookProps = {
1214

1315
export const Notebook: FunctionComponent<NotebookProps> = ({ onTitleChange }) => {
1416
return (
15-
<div className="Notebook">
16-
<FileLoadStep onLoad={(file) => onTitleChange(file.name)}>
17-
{({ file }) => (
18-
<SchemaLoadStep file={file}>
19-
{({ schema }) => (
20-
<ColumnSelectionStep schema={schema}>
21-
{({ bucketColumns }) => <AnonymizationStep bucketColumns={bucketColumns} schema={schema} />}
22-
</ColumnSelectionStep>
17+
<NotebookNavProvider>
18+
<div className="Notebook">
19+
<div className="Notebook-nav">
20+
<NotebookNav />
21+
</div>
22+
<div className="Notebook-content">
23+
<FileLoadStep onLoad={(file) => onTitleChange(file.name)}>
24+
{({ file }) => (
25+
<SchemaLoadStep file={file}>
26+
{({ schema }) => (
27+
<ColumnSelectionStep schema={schema}>
28+
{({ bucketColumns }) => <AnonymizationStep bucketColumns={bucketColumns} schema={schema} />}
29+
</ColumnSelectionStep>
30+
)}
31+
</SchemaLoadStep>
2332
)}
24-
</SchemaLoadStep>
25-
)}
26-
</FileLoadStep>
27-
</div>
33+
</FileLoadStep>
34+
</div>
35+
</div>
36+
</NotebookNavProvider>
2837
);
2938
};

src/Notebook/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './Notebook';
2+
export * from './notebook-nav';

src/Notebook/notebook-nav.tsx

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import React, { useCallback, useContext, useEffect, useRef } from 'react';
2+
import { Steps } from 'antd';
3+
import { LoadingOutlined } from '@ant-design/icons';
4+
import { useImmer } from 'use-immer';
5+
import { noop } from 'lodash';
6+
7+
const { Step } = Steps;
8+
9+
// Nav state
10+
11+
export enum NotebookNavStep {
12+
CsvImport,
13+
DataPreview,
14+
ColumnSelection,
15+
AnonymizationSummary,
16+
AnonymizedResults,
17+
CsvExport,
18+
}
19+
20+
export type NotebookNavStepStatus = 'inactive' | 'active' | 'loading' | 'done' | 'failed';
21+
22+
type NotebookNavStepState = {
23+
htmlElement: HTMLElement | null;
24+
status: NotebookNavStepStatus;
25+
};
26+
27+
type NotebookNavState = {
28+
steps: NotebookNavStepState[];
29+
};
30+
31+
const defaultNavState: NotebookNavState = {
32+
steps: Array(NotebookNavStep.CsvExport + 1)
33+
.fill(null)
34+
.map(() => ({ status: 'inactive', htmlElement: null })),
35+
};
36+
37+
const NotebookNavStateContext = React.createContext<NotebookNavState>(defaultNavState);
38+
39+
function useNavState(): NotebookNavState {
40+
return useContext(NotebookNavStateContext);
41+
}
42+
43+
// Nav functions
44+
45+
type NotebookNavStepPatch =
46+
| { htmlElement: null; status?: never }
47+
| { htmlElement: HTMLElement; status: 'active' | 'loading' | 'done' | 'failed' }
48+
| { htmlElement?: never; status: NotebookNavStepStatus };
49+
50+
type NotebookNavFunctions = {
51+
updateStep(step: NotebookNavStep, patch: NotebookNavStepPatch): void;
52+
};
53+
54+
const NotebookNavFunctionsContext = React.createContext<NotebookNavFunctions>({
55+
updateStep: noop,
56+
});
57+
58+
function useNavFunctions(): NotebookNavFunctions {
59+
return useContext(NotebookNavFunctionsContext);
60+
}
61+
62+
// Context provider
63+
64+
export const NotebookNavProvider: React.FunctionComponent = ({ children }) => {
65+
const [navState, updateNavState] = useImmer(defaultNavState);
66+
const navFunctions = useRef<NotebookNavFunctions>({
67+
updateStep(step, patch) {
68+
updateNavState((draft) => {
69+
const { steps } = draft as NotebookNavState;
70+
if (patch.htmlElement === null) {
71+
steps[step] = {
72+
htmlElement: null,
73+
status: 'inactive',
74+
};
75+
} else if (patch.htmlElement) {
76+
steps[step] = patch;
77+
} else if (steps[step].htmlElement) {
78+
steps[step].status = patch.status;
79+
}
80+
});
81+
},
82+
});
83+
84+
return (
85+
<NotebookNavStateContext.Provider value={navState}>
86+
<NotebookNavFunctionsContext.Provider value={navFunctions.current}>
87+
{children}
88+
</NotebookNavFunctionsContext.Provider>
89+
</NotebookNavStateContext.Provider>
90+
);
91+
};
92+
93+
// Context consumers
94+
95+
export type NotebookNavAnchorProps = {
96+
step: NotebookNavStep;
97+
status?: NotebookNavStepStatus;
98+
};
99+
100+
export const NotebookNavAnchor: React.FunctionComponent<NotebookNavAnchorProps> = ({ step, status = 'active' }) => {
101+
const navFunctions = useNavFunctions();
102+
103+
const ref = useCallback(
104+
(htmlElement: HTMLElement | null) => {
105+
navFunctions.updateStep(step, {
106+
htmlElement,
107+
status,
108+
} as NotebookNavStepPatch);
109+
},
110+
[step, status, navFunctions],
111+
);
112+
113+
return (
114+
<div style={{ position: 'relative' }}>
115+
<div ref={ref} style={{ position: 'absolute', top: -60, left: 0 }}></div>
116+
</div>
117+
);
118+
};
119+
120+
function mapStatus(status: NotebookNavStepStatus): 'error' | 'process' | 'finish' | 'wait' {
121+
switch (status) {
122+
case 'inactive':
123+
return 'wait';
124+
case 'active':
125+
case 'loading':
126+
return 'process';
127+
case 'done':
128+
return 'finish';
129+
case 'failed':
130+
return 'error';
131+
}
132+
}
133+
134+
export const NotebookNav: React.FunctionComponent = () => {
135+
const { steps } = useNavState();
136+
const status = (step: NotebookNavStep) => mapStatus(steps[step].status);
137+
138+
return (
139+
<Steps
140+
progressDot={(dot, { index }) =>
141+
steps[index].status === 'loading' ? (
142+
<span
143+
key="loading"
144+
className="ant-steps-icon-dot"
145+
style={{
146+
backgroundColor: 'transparent',
147+
color: '#1890ff',
148+
left: -5,
149+
}}
150+
>
151+
<LoadingOutlined />
152+
</span>
153+
) : (
154+
dot
155+
)
156+
}
157+
direction="vertical"
158+
current={-1}
159+
onChange={(step) => {
160+
const { htmlElement } = steps[step];
161+
if (htmlElement) {
162+
htmlElement.scrollIntoView({
163+
behavior: 'smooth',
164+
block: 'start',
165+
});
166+
}
167+
}}
168+
>
169+
<Step status={status(NotebookNavStep.CsvImport)} title="CSV Import" description="Load data from CSV" />
170+
<Step status={status(NotebookNavStep.DataPreview)} title="Data Preview" description="Preview contents of file" />
171+
<Step
172+
status={status(NotebookNavStep.ColumnSelection)}
173+
title="Column Selection"
174+
description="Select columns for anonymization"
175+
/>
176+
<Step
177+
status={status(NotebookNavStep.AnonymizationSummary)}
178+
title="Anonymization Summary"
179+
description="Review distortion statistics"
180+
/>
181+
<Step
182+
status={status(NotebookNavStep.AnonymizedResults)}
183+
title="Anonymized Results"
184+
description="Preview anonymized results"
185+
/>
186+
<Step status={status(NotebookNavStep.CsvExport)} title="CSV Export" description="Export anonymized data to CSV" />
187+
</Steps>
188+
);
189+
};

src/SchemaLoadStep/SchemaLoadStep.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { FunctionComponent, useEffect } from 'react';
22
import { Divider, message, Result, Space, Spin, Typography } from 'antd';
33

4+
import { NotebookNavAnchor, NotebookNavStep } from '../Notebook';
45
import { File, TableSchema } from '../types';
56
import { DataPreviewTable } from './DataPreviewTable';
67
import { useSchema } from './use-schema';
@@ -31,6 +32,7 @@ export const SchemaLoadStep: FunctionComponent<SchemaLoadStepProps> = ({ childre
3132
return (
3233
<>
3334
<div className="SchemaLoadStep notebook-step completed">
35+
<NotebookNavAnchor step={NotebookNavStep.DataPreview} status="done" />
3436
<Title level={3}>Successfully imported {schema.value.file.name}</Title>
3537
<div className="mb-1">
3638
<Text>Here is what the data looks like:</Text>
@@ -49,6 +51,7 @@ export const SchemaLoadStep: FunctionComponent<SchemaLoadStepProps> = ({ childre
4951
case 'failed':
5052
return (
5153
<div className="SchemaLoadStep notebook-step failed">
54+
<NotebookNavAnchor step={NotebookNavStep.DataPreview} status="failed" />
5255
<Result
5356
status="error"
5457
title="Schema discovery failed"
@@ -60,6 +63,7 @@ export const SchemaLoadStep: FunctionComponent<SchemaLoadStepProps> = ({ childre
6063
case 'in_progress':
6164
return (
6265
<div className="SchemaLoadStep notebook-step loading">
66+
<NotebookNavAnchor step={NotebookNavStep.DataPreview} status="loading" />
6367
<Space direction="vertical">
6468
<Spin size="large" />
6569
<Text type="secondary">Loading schema</Text>

0 commit comments

Comments
 (0)