Skip to content
This repository has been archived by the owner on Nov 30, 2022. It is now read-only.

Commit

Permalink
Adding a guide that is run the first time a user enters catwalk. (#142)
Browse files Browse the repository at this point in the history
Closes #16
  • Loading branch information
Helene Rignér committed Jan 23, 2019
1 parent b243fe8 commit 785ee87
Show file tree
Hide file tree
Showing 9 changed files with 682 additions and 244 deletions.
468 changes: 231 additions & 237 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@
"react-cookie-consent": "2.0.1",
"react-dom": "16.8.0-alpha.1",
"react-ga": "2.5.6",
"react-joyride": "2.0.2",
"react-outside-click-handler": "1.2.2",
"react-svg-inline": "2.1.1",
"react-use-promise": "0.0.0-alpha.1",
"react-virtualized": "9.21.0",
"react-floater": "0.6.2"
"react-floater": "0.6.2",
"react-contextmenu": "2.10.0"
},
"devDependencies": {
"@after-work.js/cli": "5.1.3",
Expand Down
9 changes: 7 additions & 2 deletions src/components/app.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo, useEffect } from 'react';
import React, { useMemo, useEffect, useRef } from 'react';
import usePromise from 'react-use-promise';
import enigma from 'enigma.js';

Expand All @@ -8,6 +8,8 @@ import TopBar from './topbar';
import Model from './model';
import Splash from './splash';
import Cubes from './cubes';
import Guide from './guide';

import { useReloadInProgress } from '../enigma/reload-in-progress-interceptor';
import './app.pcss';

Expand All @@ -24,6 +26,8 @@ export default function App() {
const [app, appError] = useApp(global);
const [docs, docsError] = useDocList(global, appError && global);
const appLayout = useLayout(app);
const guideRef = useRef();

useEffect(() => () => {
if (!app) return;
session.close();
Expand Down Expand Up @@ -51,7 +55,8 @@ export default function App() {
return (
<AppContext.Provider value={app}>
<div className="app">
<TopBar app={app} appLayout={appLayout} />
<Guide ref={guideRef} />
<TopBar app={app} appLayout={appLayout} startGuide={() => guideRef.current.startGuideFunc()} />
<Model app={app} appLayout={appLayout} />
<Cubes app={app} />
</div>
Expand Down
237 changes: 237 additions & 0 deletions src/components/guide-steps.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import React from 'react';

const steps = [
{
content: (
<div>
<h2>Welcome to catwalk!</h2>
<p>
catwalk lets you explore your data model to gain
insights about fields, associations and how interactions
with the data impacts the model.
</p>
<p>
Follow the guide to discover the power of catwalk.
</p>
</div>
),
placement: 'center',
target: 'body',
},
{
content: (
<div>
<h2>Welcome to catwalk!</h2>
<p>The URL to catwalk is</p>
<span className="breakword">
{window.location.href}
</span>
<p>
where
{' '}
<code>engine_url</code>
{' '}
points to the app containing the data model. Change this to explore another data model.
</p>
</div>
),
placement: 'center',
target: 'body',
},
{
content: (
<div>
<p>
The highlighted area represents a table in the data model.
</p>
<p>On the top we can see the table name, together with the number of rows in the table.</p>
</div>
),
placement: 'right-start',
target: '.column',
title: 'Table',
},
{
content: (
<div>
<p>
Fields, the data-carrying entities in the data model, are represented with a box like this.
</p>
<p>
The field name and the number of field values are visible.
</p>
<p>
The number of field values are presented in the form of X of Y(Z), where X is the number
of field values valid in the current selection, Y is the values in total and Z is the number
of values present in this table.
</p>
</div>
),
placement: 'right',
target: '.vertcell.keycell',
title: 'Field',
},
{
content: (
<div>
A field can have different border colors where the color represents different type of keys.
<div className="guide-step">
<div className="perfect" />
<p>Perfect key.</p>
<p>
Indicates that every row contains a key value, and that all of these key values are unique.
The field&apos;s subset ratio is 100 percent.
</p>
</div>
<div className="guide-step">
<div className="primary" />
<p>Primary key</p>
<br />
<p>
Indicates that all key values are unique, but not every row contains a key value or
the field&apos;s subset ratio is less than 100 percent.
</p>
</div>
<div className="guide-step">
<div className="default" />
<p>Key</p>
<br />
<p>
Indicates that the key is not unique. Usually seen in fact tables, where the same dimension
value may be associated with many different facts.
</p>
</div>
</div>
),
target: '.table-field',
title: 'Field',
placement: 'right',
},
{
content: (
<div>
<p>
The fields are clickable. When clicking on a field, it unfolds and displays the field values with the possibility to make
selections. Selections can be helpful when trying to figure out the data model, and to find errors in the data model.
</p>
<p>
Go ahead, click the field and make a selection!
</p>
</div>
),
spotlightClicks: true,
placement: 'right',
target: '.vertcell.keycell',
title: 'Field',
},
{
step: 'selections',
content: (
<div>
<p>
The selections made can be seen in the top bar.
</p>
<p>
Selections in any single field can be removed, or all selections in the app can be removed by clicking the X to the left.
</p>
</div>
),
spotlightClicks: true,
placement: 'bottom',
target: '.selection-field',
title: 'Selections',
},
{
content: 'This shows the association between two fields, with basic frequency information on each end of the association line (*, 1 or 0/1).',
target: '.association-to-right-b',
title: 'Associations',
},
{
step: 'openHypercubeBuilder',
content: (
<div>
<p>
Here you can build your own hypercube with the fields, dimensions and
measure in the app. This could be handy when you want to see how the information
in the datamodel is connected.
</p>
<p>
Click the button to open the hypercube builder.
</p>
</div>
),
spotlightClicks: true,
disableOverlayClose: true,
hideFooter: true,
target: '.add-button',
title: 'Hypercube builder',
},
{
step: 'selectEntity',
content: (
<div>
<p>Here you can see a list of all the fields, dimensions and measures defined in the app. The input field on top will filter the list.</p>
<p>Click on an entity to select it.</p>
</div>
),
disableOverlayClose: true,
hideFooter: true,
spotlightClicks: true,
target: '.cube-column-chooser',
title: 'Hypercube builder',
},
{
step: 'addAnotherColumn',
content: (
<div>
<p>
A cube is created with the selected entity as the first column. All the entity values are shown
and it is possible to spot any errande values. If selections are applied, only the selected values
are displayed.
</p>
<p>Add another column by clicking the plus button.</p>
</div>
),
disableOverlayClose: true,
hideFooter: true,
spotlightClicks: true,
target: '.card',
title: 'Hypercube builder',
},
{
step: 'selectAnotherEntity',
content: 'Click an entity to add it as a column in the cube.',
placement: 'left',
disableOverlayClose: true,
hideFooter: true,
spotlightClicks: true,
target: '.cube-column-chooser',
title: 'Hypercube builder',
},
{
step: 'cubeFinished',
content: 'More columns can be added to the cube. To close the cube, just click the button in the upper corner.',
target: '.card',
title: 'Hypercube builder',
},
{
content: (
<div>
<p>
Now you can continue to explore your data model with catwalk on your own!
</p>
<p>
To restart the guide, right click and select
{' '}
<code>Start Guide</code>
.
</p>
</div>
),
placement: 'bottom',
target: '.topbarLogo',
title: 'Guide completed!',
},
];

export default steps;
109 changes: 109 additions & 0 deletions src/components/guide.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React,
{
useState,
useCallback,
forwardRef,
useImperativeHandle,
} from 'react';
import Joyride, { ACTIONS, EVENTS, STATUS } from 'react-joyride';
import steps from './guide-steps';

import './guide.pcss';

// The component needs to be wrapped in `forwardRef` to give access to the
// ref object assigned using the `ref` prop.
const Guide = forwardRef((props, ref) => {
const [runGuide, setRunGuide] = useState(!localStorage.getItem('catwalkGuide'));
const [stepIndex, setStepIndex] = useState(0);

// Any instance of the component is extended with what is returned from the
// callback passed as the second argument.
useImperativeHandle(ref, () => ({
startGuideFunc() {
if (!runGuide) {
setRunGuide(true);
}
},
}));

const setStep = (stepName) => {
setRunGuide(false);
setStepIndex(steps.findIndex(s => s.step === stepName));
setTimeout(() => setRunGuide(true), 300);
};

const onClick = useCallback((evt) => {
let parentElemName = evt.target.parentElement.className;
if (parentElemName) {
parentElemName = parentElemName.trim();
}

if (parentElemName === 'field') {
// a click in the field (to open the filterbox).
// stop and start the guide in order to highlight the opened filterbox.
setRunGuide(false);
setRunGuide(true);
}
if (parentElemName === 'add-button') {
// a click on the big hypercube builder button.
setStep('selectEntity');
} else if (parentElemName === 'expression' || parentElemName === 'expression-list') {
// a click on an expression in the hypercube builder.
let nbrOfColumns = 0;
const table = document.getElementsByClassName('hypercube-table');
if (table.length > 0) {
const virtTable = table[0].getElementsByClassName('ReactVirtualized__Table');
if (virtTable.length > 0) {
nbrOfColumns = virtTable[0].getAttribute('aria-colcount');
}
}
if (nbrOfColumns > 0) {
setStep('cubeFinished');
} else {
setStep('addAnotherColumn');
}
} else if (parentElemName === 'column-add-button') {
// a click on the little add button in the hypercube builder.
setStep('selectAnotherEntity');
}
}, []);

const handleJoyrideCallback = (data) => {
const {
action, index, type, status,
} = data;

if ([EVENTS.TOUR_START].includes(type)) {
document.addEventListener('mouseup', onClick);
} else if ([STATUS.FINISHED, STATUS.SKIPPED].includes(status)) {
setRunGuide(false);
setStepIndex(0);
localStorage.setItem('catwalkGuide', 'catwalk');
document.removeEventListener('mouseup', onClick);
} else if ([EVENTS.STEP_AFTER, EVENTS.TARGET_NOT_FOUND].includes(type)) {
const newStepIndex = index + (action === ACTIONS.PREV ? -1 : 1);
// Update state to advance the guide
setStepIndex(newStepIndex);
}
};

return (
<Joyride
continuous
stepIndex={stepIndex}
showProgress
showSkipButton
disableBeacon
run={runGuide}
callback={handleJoyrideCallback}
styles={{
options: {
primaryColor: '#398ab5',
},
}}
steps={steps}
/>
);
});

export default Guide;
Loading

0 comments on commit 785ee87

Please sign in to comment.