Skip to content

Commit

Permalink
feat(#281): allow to collapse/expand workflow version section (#282)
Browse files Browse the repository at this point in the history
Signed-off-by: Nastya Rusina <[email protected]>
  • Loading branch information
anrusina authored Feb 10, 2022
1 parent 95128b5 commit a4ac862
Show file tree
Hide file tree
Showing 12 changed files with 370 additions and 180 deletions.
24 changes: 24 additions & 0 deletions src/basics/LocalCache/defaultConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export enum LocalCacheItem {
// Test flag is created only for unit-tests
TestUndefined = 'test-undefined',
TestSettingBool = 'test-setting-bool',
TestObject = 'test-object',

// Production flags
ShowWorkflowVersions = 'flyte.show-workflow-versions'
}

type LocalCacheConfig = { [k: string]: string };

/*
* THe default value could be present as any simple type or as a valid JSON object
* with all field names wrapped in double quotes
**/
export const defaultLocalCacheConfig: LocalCacheConfig = {
// Test
'test-setting-bool': 'false',
'test-object': '{"name":"Stella","age":"125"}',

// Production
'flyte.show-workflow-versions': 'true'
};
49 changes: 49 additions & 0 deletions src/basics/LocalCache/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// More info on Local storage: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
import { useState } from 'react';
import { defaultLocalCacheConfig, LocalCacheItem } from './defaultConfig';

export { LocalCacheItem } from './defaultConfig';

export function ClearLocalCache() {
localStorage.clear();
}

const getDefault = (setting: LocalCacheItem) => {
const result = defaultLocalCacheConfig[setting];
if (!result) {
console.error(
`ERROR: LocalCacheItem ${setting} doesn't have default value provided in defaultLocalCacheConfig`
);
return null;
}
return JSON.parse(result);
};

export function useLocalCache<T>(setting: LocalCacheItem) {
const defaultValue = getDefault(setting);
const [value, setValue] = useState<T>(() => {
const data = localStorage.getItem(setting);
const value = data ? JSON.parse(data) : defaultValue;
if (typeof value === typeof defaultValue) {
return value;
}

return defaultValue;
});

const setLocalCache = (newValue: T) => {
localStorage.setItem(setting, JSON.stringify(newValue));
setValue(newValue);
};

const clearState = () => {
localStorage.removeItem(setting);
setValue(defaultValue);
};

return [value, setLocalCache, clearState];
}

export const onlyForTesting = {
getDefault
};
77 changes: 77 additions & 0 deletions src/basics/LocalCache/localCache.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as React from 'react';
import {
render,
screen,
act,
fireEvent,
getByTestId
} from '@testing-library/react';
import {
ClearLocalCache,
LocalCacheItem,
useLocalCache,
onlyForTesting
} from '.';

const SHOW_TEXT = 'SHOWED';
const HIDDEN_TEXT = 'HIDDEN';

const TestFrame = () => {
const [show, setShow, clearShow] = useLocalCache(
LocalCacheItem.TestSettingBool
);

const toShow = () => setShow(true);
const toClear = () => clearShow();

return (
<div>
<div>{show ? SHOW_TEXT : HIDDEN_TEXT}</div>
<button onClick={toShow} data-testid="show" />
<button onClick={toClear} data-testid="clear" />
</div>
);
};

describe('LocalCache', () => {
beforeAll(() => {
ClearLocalCache();
});

afterAll(() => {
ClearLocalCache();
});

it('Can be used by component as expected', () => {
const { container } = render(<TestFrame />);
const show = getByTestId(container, 'show');
const clear = getByTestId(container, 'clear');

expect(screen.getByText(HIDDEN_TEXT)).toBeTruthy();

// change value
act(() => {
fireEvent.click(show);
});
expect(screen.getByText(SHOW_TEXT)).toBeTruthy();

// reset to default
act(() => {
fireEvent.click(clear);
});
expect(screen.getByText(HIDDEN_TEXT)).toBeTruthy();
});

it('With no default value - assumes null', () => {
const { getDefault } = onlyForTesting;
expect(getDefault(LocalCacheItem.TestUndefined)).toBeNull();
});

it('Can store use default as an object', () => {
const { getDefault } = onlyForTesting;
expect(getDefault(LocalCacheItem.TestObject)).toMatchObject({
name: 'Stella',
age: '125'
});
});
});
8 changes: 6 additions & 2 deletions src/components/Entities/EntityDescription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { useNamedEntity } from 'components/hooks/useNamedEntity';
import { NamedEntityMetadata, ResourceIdentifier } from 'models/Common/types';
import * as React from 'react';
import reactLoadingSkeleton from 'react-loading-skeleton';
import { noDescriptionStrings } from './constants';
import { entityStrings } from './constants';
import t from './strings';

const Skeleton = reactLoadingSkeleton;

Expand Down Expand Up @@ -42,7 +43,10 @@ export const EntityDescription: React.FC<{
>
{hasDescription
? metadata.description
: noDescriptionStrings[id.resourceType]}
: t(
'noDescription',
entityStrings[id.resourceType]
)}
</span>
</WaitForData>
</Typography>
Expand Down
108 changes: 28 additions & 80 deletions src/components/Entities/EntityDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import { Dialog } from '@material-ui/core';
import { makeStyles, Theme } from '@material-ui/core/styles';
import { contentMarginGridUnits } from 'common/layout';
import { WaitForData } from 'components/common/WaitForData';
import { EntityDescription } from 'components/Entities/EntityDescription';
import { useProject } from 'components/hooks/useProjects';
import { useChartState } from 'components/hooks/useChartState';
import { LaunchForm } from 'components/Launch/LaunchForm/LaunchForm';
import { ResourceIdentifier, ResourceType } from 'models/Common/types';
import { ResourceIdentifier } from 'models/Common/types';
import * as React from 'react';
import { entitySections } from './constants';
import { EntityDetailsHeader } from './EntityDetailsHeader';
import { EntityExecutions } from './EntityExecutions';
import { EntitySchedules } from './EntitySchedules';
import { EntityVersions } from './EntityVersions';
import classNames from 'classnames';
import { StaticGraphContainer } from 'components/Workflow/StaticGraphContainer';
import { WorkflowId } from 'models/Workflow/types';
import { EntityExecutionsBarChart } from './EntityExecutionsBarChart';

const useStyles = makeStyles((theme: Theme) => ({
Expand All @@ -40,49 +35,26 @@ const useStyles = makeStyles((theme: Theme) => ({
display: 'flex',
flexDirection: 'column'
},
versionView: {
flex: '1 1 auto'
},
schedulesContainer: {
flex: '1 2 auto',
marginRight: theme.spacing(30)
}
}));

export interface EntityDetailsProps {
interface EntityDetailsProps {
id: ResourceIdentifier;
versionView?: boolean;
showStaticGraph?: boolean;
}

function getLaunchProps(id: ResourceIdentifier) {
if (id.resourceType === ResourceType.TASK) {
return { taskId: id };
}

return { workflowId: id };
}

/**
* A view which optionally renders description, schedules, executions, and a
* launch button/form for a given entity. Note: not all components are suitable
* for use with all entities (not all entities have schedules, for example).
* @param id
* @param versionView
* @param showStaticGraph
*/
export const EntityDetails: React.FC<EntityDetailsProps> = ({
id,
versionView = false,
showStaticGraph = false
}) => {
export const EntityDetails: React.FC<EntityDetailsProps> = ({ id }) => {
const sections = entitySections[id.resourceType];
const workflowId = id as WorkflowId;
const project = useProject(id.project);
const styles = useStyles();
const [showLaunchForm, setShowLaunchForm] = React.useState(false);
const onLaunch = () => setShowLaunchForm(true);
const onCancelLaunch = () => setShowLaunchForm(false);
const { chartIds, onToggle, clearCharts } = useChartState();

return (
Expand All @@ -91,45 +63,34 @@ export const EntityDetails: React.FC<EntityDetailsProps> = ({
project={project.value}
id={id}
launchable={!!sections.launch}
versionView={versionView}
onClickLaunch={onLaunch}
/>
{!versionView && (
<div className={styles.metadataContainer}>
{sections.description ? (
<div className={styles.descriptionContainer}>
<EntityDescription id={id} />
</div>
) : null}
{sections.schedules ? (
<div className={styles.schedulesContainer}>
<EntitySchedules id={id} />
</div>
) : null}
</div>
)}
{sections.versions ? (
<>
{showStaticGraph ? (
<StaticGraphContainer workflowId={workflowId} />
) : null}
<div
className={classNames(styles.versionsContainer, {
[styles.versionView]: versionView
})}
>
<EntityVersions id={id} versionView={versionView} />

<div className={styles.metadataContainer}>
{sections.description ? (
<div className={styles.descriptionContainer}>
<EntityDescription id={id} />
</div>
) : null}
{sections.schedules ? (
<div className={styles.schedulesContainer}>
<EntitySchedules id={id} />
</div>
</>
) : null}
</div>

{sections.versions ? (
<div className={styles.versionsContainer}>
<EntityVersions id={id} />
</div>
) : null}
{!versionView && (
<EntityExecutionsBarChart
onToggle={onToggle}
chartIds={chartIds}
id={id}
/>
)}
{sections.executions && !versionView ? (

<EntityExecutionsBarChart
onToggle={onToggle}
chartIds={chartIds}
id={id}
/>

{sections.executions ? (
<div className={styles.executionsContainer}>
<EntityExecutions
chartIds={chartIds}
Expand All @@ -138,19 +99,6 @@ export const EntityDetails: React.FC<EntityDetailsProps> = ({
/>
</div>
) : null}
{sections.launch ? (
<Dialog
scroll="paper"
maxWidth="sm"
fullWidth={true}
open={showLaunchForm}
>
<LaunchForm
onClose={onCancelLaunch}
{...getLaunchProps(id)}
/>
</Dialog>
) : null}
</WaitForData>
);
};
Loading

0 comments on commit a4ac862

Please sign in to comment.