1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 11:22:43 +02:00

Adding SnapSettingsCard ui component (#12655)

* parent d89e5336a6
author georgewrmarshall <george.marshall@consensys.net> 1636692862 -0800
committer hmalik88 <hassan.malik@consensys.net> 1637342043 -0500

Initial SnapSettingsCard component

Updates to styles but having specificity issues so increased specificity

Updates to styles but having specificity issues so increased specificity

added overflow fix and added tests

lockfile update

prettier fix

added stylelint ignore

yarn.lock fixed

* merge conflict fix

* package/yarn fix

* fixed package.json

* updated lockfile...

* removed comment

* removed unnecessary key/val for chip status indicator color

* bumped lattice to 0.4.0 in package json, fixed yarn lock

* removed dupe entry in yarn lock

* ran yarn setup to update lock file

* updated chip label prop

* parent d89e5336a6
author georgewrmarshall <george.marshall@consensys.net> 1636692862 -0800
committer hmalik88 <hassan.malik@consensys.net> 1637342043 -0500

Initial SnapSettingsCard component

Updates to styles but having specificity issues so increased specificity

Updates to styles but having specificity issues so increased specificity

added overflow fix and added tests

lockfile update

prettier fix

added stylelint ignore

yarn.lock fixed

* merge conflict fix

* package/yarn fix

* fixed package.json

* updated lockfile...

* removed comment

* bumped lattice to 0.4.0 in package json, fixed yarn lock

* removed dupe entry in yarn lock

* ran yarn setup to update lock file

* Using IconWithFallback instead of SiteIcon, fixing icon prop, and adding status story and docs page

* Updating to follow storybook folder convention

* Updates to styles

* Adding localization

* added todo comment

Co-authored-by: hmalik88 <hassan.malik@consensys.net>
This commit is contained in:
George Marshall 2021-12-01 13:10:51 -08:00 committed by GitHub
parent 854fc71ae7
commit eb4f051b23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 621 additions and 1 deletions

View File

@ -1073,6 +1073,18 @@
"flaskExperimentalText5": {
"message": "Using Flask gives you much greater discretion in using the power of MetaMask, and that discretion is yours. Do you accept these risks as well as extra responsibility for your wallet's safety?"
},
"flaskSnapSettingsCardButtonCta": {
"message": "See details",
"description": "Call to action a user can take to see more information about the Snap that is installed"
},
"flaskSnapSettingsCardDateAddedOn": {
"message": "Added on",
"description": "Start of the sentence describing when and where snap was added"
},
"flaskSnapSettingsCardFrom": {
"message": "from",
"description": "Part of the sentence describing when and where snap was added"
},
"followUsOnTwitter": {
"message": "Follow us on Twitter"
},

View File

@ -248,6 +248,7 @@
"@storybook/addon-knobs": "^6.3.1",
"@storybook/addons": "^6.3.12",
"@storybook/api": "^6.3.12",
"@storybook/client-api": "^6.3.12",
"@storybook/components": "^6.3.12",
"@storybook/core": "^6.3.12",
"@storybook/core-events": "^6.3.0",

View File

@ -38,6 +38,7 @@
@import 'selected-account/index';
@import 'signature-request/index';
@import 'signature-request-original/index';
@import 'flask/snap-settings-card/index';
@import 'tab-bar/index';
@import 'token-cell/token-cell';
@import 'transaction-activity-log/index';

View File

@ -0,0 +1,41 @@
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import SnapSettingsCard from '.';
# SnapSettingsCard
A card component that displays information and the status of a snap. The `SnapSettingsCard` component is made up of the `Card`, `IconBorder`, `IconWithFallback`, `ToggleButton`, `Chip`, `ColorIndicator` and `Button` components
<Canvas>
<Story id="ui-components-app-flask-snap-settings-card-snap-settings-card-stories-js--default-story" />
</Canvas>
## Component API
<ArgsTable of={SnapSettingsCard} />
## Usage
The following describes the props and example usage for this component.
### Status
There are 4 statuses the `SnapSettingsCard` can have: `'installing'`,`'running'`,`'stopped'` and `'crashed'`.
<Canvas>
<Story id="ui-components-app-flask-snap-settings-card-snap-settings-card-stories-js--status" />
</Canvas>
### isEnabled / onToggle
Use the `isEnabled` and `onToggle` to control the `ToggleButton` component inside of the `SnapSettingsCard`
```jsx
const [isEnabled, setIsEnabled] = React.useState(false);
const handleOnToggle = () => {
setIsEnabled(!isEnabled);
};
return <SnapSettingsCard isEnabled={isEnabled} onToggle={handleOnToggle} />;
```

View File

@ -0,0 +1 @@
export { default } from './snap-settings-card';

View File

@ -0,0 +1,57 @@
$version-max-width: 56px; // Increase to show more of the version number
$body-line-clamp: 4; // Number of lines in card body before truncating
.snap-settings-card {
&__title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__toggle-container {
margin-left: auto;
&__toggle-button {
margin-right: -12px; // react-toggle-button width fix
}
}
&__body {
overflow: hidden;
/* stylelint-disable */
display: -webkit-box;
-webkit-line-clamp: $body-line-clamp;
-webkit-box-orient: vertical;
/* stylelint-enable */
}
&__version.box {
margin-left: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: $version-max-width;
flex: 0 0 $version-max-width;
text-align: right;
}
&__chip.chip {
margin: 0;
margin-left: auto;
display: inline-flex;
align-items: center;
text-transform: capitalize;
}
&__button.button {
padding: 4px 16px;
display: inline-flex;
align-items: center;
}
&__date-added {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}

View File

@ -0,0 +1,279 @@
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import Card from '../../../ui/card';
import Box from '../../../ui/box';
import IconWithFallback from '../../../ui/icon-with-fallback';
import IconBorder from '../../../ui/icon-border';
import Typography from '../../../ui/typography/typography';
import ToggleButton from '../../../ui/toggle-button';
import Chip from '../../../ui/chip';
import ColorIndicator from '../../../ui/color-indicator';
import Button from '../../../ui/button';
import {
COLORS,
TYPOGRAPHY,
FONT_WEIGHT,
ALIGN_ITEMS,
DISPLAY,
TEXT_ALIGN,
} from '../../../../helpers/constants/design-system';
const STATUSES = {
INSTALLING: 'installing',
RUNNING: 'running',
STOPPED: 'stopped',
CRASHED: 'crashed',
};
const STATUS_COLORS = {
[STATUSES.INSTALLING]: COLORS.ALERT1,
[STATUSES.RUNNING]: COLORS.SUCCESS1,
[STATUSES.STOPPED]: COLORS.UI4,
[STATUSES.CRASHED]: COLORS.ERROR1,
};
const SnapSettingsCard = ({
name,
description,
icon,
dateAdded,
version,
url,
onToggle,
isEnabled = false,
onClick,
status,
className,
cardProps,
toggleButtonProps,
buttonProps,
chipProps,
}) => {
const t = useI18nContext();
const [chipStatus, setChipStatus] = useState(STATUSES.INSTALLING);
// TODO: use state directly in place of memoization
const handleStatus = useCallback(() => {
switch (status) {
case STATUSES.INSTALLING: {
setChipStatus(STATUSES.INSTALLING);
break;
}
case STATUSES.RUNNING: {
setChipStatus(STATUSES.RUNNING);
break;
}
case STATUSES.STOPPED: {
setChipStatus(STATUSES.STOPPED);
break;
}
case STATUSES.CRASHED: {
setChipStatus(STATUSES.CRASHED);
break;
}
default: {
setChipStatus(STATUSES.INSTALLING);
}
}
}, [status]);
useEffect(() => {
handleStatus(status);
}, [handleStatus, status]);
return (
<Card
className={classnames('snap-settings-card', className)}
{...cardProps}
>
<Box
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
marginBottom={4}
>
{(icon || name) && (
<Box>
<IconBorder size={32}>
<IconWithFallback icon={icon} size={32} name={name} />
</IconBorder>
</Box>
)}
<Typography
boxProps={{
marginLeft: 4,
marginTop: 0,
marginBottom: 0,
}}
color={COLORS.BLACK}
variant={TYPOGRAPHY.H4}
fontWeight={FONT_WEIGHT.BOLD}
className="snap-settings-card__title"
>
{name}
</Typography>
<Box paddingLeft={4} className="snap-settings-card__toggle-container">
<ToggleButton
value={isEnabled}
onToggle={onToggle}
className="snap-settings-card__toggle-container__toggle-button"
{...toggleButtonProps}
/>
</Box>
</Box>
<Typography
variant={TYPOGRAPHY.Paragraph}
color={COLORS.UI4}
fontWeight={FONT_WEIGHT.NORMAL}
className="snap-settings-card__body"
boxProps={{
marginBottom: 4,
marginTop: 0,
margin: [0, 0, 4],
}}
>
{description}
</Typography>
<Box>
<Box marginBottom={4}>
<Box
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
marginBottom={4}
>
<Box>
<Button
className="snap-settings-card__button"
onClick={onClick}
{...buttonProps}
>
{t('flaskSnapSettingsCardButtonCta')}
</Button>
</Box>
<Chip
leftIcon={
<Box paddingLeft={1}>
<ColorIndicator
color={STATUS_COLORS[chipStatus]}
type={ColorIndicator.TYPES.FILLED}
/>
</Box>
}
label={chipStatus}
labelProps={{
color: COLORS.UI4,
margin: [0, 1],
}}
backgroundColor={COLORS.UI1}
className="snap-settings-card__chip"
{...chipProps}
/>
</Box>
</Box>
<Box display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.CENTER}>
{(dateAdded || version) && (
<>
<Typography
boxProps={{
margin: [0, 0],
}}
color={COLORS.UI3}
variant={TYPOGRAPHY.H8}
fontWeight={FONT_WEIGHT.NORMAL}
tag="span"
className="snap-settings-card__date-added"
>
{`${
dateAdded && t('flaskSnapSettingsCardDateAddedOn')
} ${dateAdded} ${url && t('flaskSnapSettingsCardFrom')} ${url}`}
</Typography>
<Typography
boxProps={{
paddingLeft: 2,
margin: [0, 0],
}}
color={COLORS.UI4}
variant={TYPOGRAPHY.H7}
fontWeight={FONT_WEIGHT.NORMAL}
align={TEXT_ALIGN.CENTER}
tag="span"
className="snap-settings-card__version"
>
v {version}
</Typography>
</>
)}
</Box>
</Box>
</Card>
);
};
SnapSettingsCard.propTypes = {
/**
* Name of the snap used for the title of the card and fallback letter for the snap icon
*/
name: PropTypes.string,
/**
* Description of the snap. Truncates after 4 lines
*/
description: PropTypes.string,
/**
* Image source of the snap icon for the IconWithFallback component
*/
icon: PropTypes.string,
/**
* Date the snap was added. Date will need formatting
*/
dateAdded: PropTypes.string,
/**
* The version of the snap in semver. Will truncate after 4 numbers e.g. 10.5.1...
*/
version: PropTypes.string,
/**
* Url of the snap website
*/
url: PropTypes.string,
/**
* The onChange function for the ToggleButton component
*/
onToggle: PropTypes.func,
/**
* Whether the snap is enabled. `value` prop of the ToggleButton
*/
isEnabled: PropTypes.bool,
/**
* onClick function of the "See Details" Button
*/
onClick: PropTypes.func,
/**
* Status of the snap must be one
*/
status: PropTypes.oneOf(Object.values(STATUSES)).isRequired,
/**
* Additional className added to the root div of the SnapSettingsCard component
*/
className: PropTypes.string,
/**
* Optional additional props passed to the Card component
*/
cardProps: PropTypes.shape(Card.propTypes),
/**
* Optional additional props passed to the ToggleButton component
*/
toggleButtonProps: PropTypes.shape(ToggleButton.propTypes),
/**
* Optional additional props passed to the Button component
*/
buttonProps: PropTypes.shape(Button.propTypes),
/**
* Optional additional props passed to the Chip component
*/
chipProps: PropTypes.shape(Chip.propTypes),
};
export default SnapSettingsCard;

View File

@ -0,0 +1,152 @@
import React from 'react';
import { useArgs } from '@storybook/client-api';
import README from './README.mdx';
import SnapSettingsCard from '.';
export default {
title: 'Components/App/Flask/SnapSettingsCard',
id: __filename,
component: SnapSettingsCard,
parameters: {
docs: {
page: README,
},
},
argTypes: {
name: {
control: 'text',
},
description: {
control: 'text',
},
icon: {
control: 'text',
},
dateAdded: {
control: 'text',
},
version: {
control: 'text',
},
url: {
control: 'text',
},
onToggle: {
action: 'onToggle',
},
isEnabled: {
control: 'boolean',
},
onClick: {
action: 'onClick',
},
status: {
control: {
type: 'select',
},
options: ['installing', 'stopped', 'running', 'crashed'],
},
className: {
control: 'string',
},
cardProps: {
control: 'object',
},
toggleButtonProps: {
control: 'object',
},
buttonProps: {
control: 'object',
},
chipProps: {
control: 'object',
},
},
};
export const DefaultStory = (args) => {
const [{ isEnabled }, updateArgs] = useArgs();
const handleOnToggle = () => {
updateArgs({
isEnabled: !isEnabled,
status: isEnabled ? 'stopped' : 'running',
});
};
return (
<SnapSettingsCard
{...args}
isEnabled={isEnabled}
onToggle={handleOnToggle}
/>
);
};
DefaultStory.storyName = 'Default';
let d = new Date();
d = d.toDateString();
DefaultStory.args = {
name: 'Snap name',
description:
'This snap provides developers everywhere access to an entirely new data storage paradigm, even letting your programs store data autonomously.',
icon: 'AST.png',
dateAdded: d,
version: '10.5.1234',
url: 'https://metamask.io/',
status: 'stopped',
};
export const Status = () => (
<>
<SnapSettingsCard
name="Installing snap"
description="This snap is Installing"
icon="AST.png"
dateAdded={d}
version="10.5.1234"
url="https://metamask.io/"
status="installing"
cardProps={{
marginBottom: 3,
}}
/>
<SnapSettingsCard
isEnabled
name="Running snap"
description="This snap is Running"
icon="AST.png"
dateAdded={d}
version="10.5.1234"
url="https://metamask.io/"
status="running"
cardProps={{
marginBottom: 3,
}}
/>
<SnapSettingsCard
name="Stopped snap"
description="This snap is stopped"
icon="AST.png"
dateAdded={d}
version="10.5.1234"
url="https://metamask.io/"
status="stopped"
cardProps={{
marginBottom: 3,
}}
/>
<SnapSettingsCard
isEnabled
name="Crashed snap"
description="This snap is Crashed"
icon="AST.png"
dateAdded={d}
version="10.5.1234"
url="https://metamask.io/"
status="crashed"
/>
</>
);

View File

@ -0,0 +1,76 @@
import * as React from 'react';
import { render, fireEvent } from '@testing-library/react';
import SnapSettingsCard from '.';
describe('SnapSettingsCard', () => {
const args = {
name: 'Snap name',
description:
'This snap provides developers everywhere access to an entirely new data storage paradigm, even letting your programs store data autonomously.',
dateAdded: new Date().toDateString(),
version: '10.5.1234',
url: 'https://metamask.io',
status: 'stopped',
icon: './AST.png',
};
it('should render the SnapsSettingCard without crashing', () => {
const { getByText } = render(<SnapSettingsCard {...args} />);
expect(getByText('Snap name')).toBeDefined();
});
it('should render the pill as installing when given a status of installing', () => {
args.status = 'installing';
const { getByText } = render(<SnapSettingsCard {...args} />);
expect(getByText('installing')).toBeDefined();
});
it('should render the pill as running when given a status of running', () => {
args.status = 'running';
const { getByText } = render(<SnapSettingsCard {...args} />);
expect(getByText('running')).toBeDefined();
});
it('should render the pill as installing when given a status of stopped', () => {
args.status = 'stopped';
const { getByText } = render(<SnapSettingsCard {...args} />);
expect(getByText('stopped')).toBeDefined();
});
it('should render the pill as crashed when given a status of crashed', () => {
args.status = 'crashed';
const { getByText } = render(<SnapSettingsCard {...args} />);
expect(getByText('crashed')).toBeDefined();
});
it('should call onToggle prop when toggle button is clicked', () => {
const onToggle = jest.fn();
args.onToggle = onToggle;
const { container } = render(<SnapSettingsCard {...args} />);
const toggleBtn = container.querySelector('.toggle-button').firstChild;
fireEvent.click(toggleBtn);
expect(onToggle).toHaveBeenCalled();
});
it('should call onClick prop when See Details button is clicked', () => {
const onClick = jest.fn();
args.onClick = onClick;
const { container } = render(<SnapSettingsCard {...args} />);
const seeDetailsBtn = container.querySelector(
'.snap-settings-card__button',
);
fireEvent.click(seeDetailsBtn);
expect(onClick).toHaveBeenCalled();
});
it('should render an icon image', () => {
const { getByAltText } = render(<SnapSettingsCard {...args} />);
const image = getByAltText(args.name);
expect(image).toBeDefined();
expect(image).toHaveAttribute('src', args.icon);
});
it('should render the icon fallback using the first letter of the name', () => {
const { getByText } = render(<SnapSettingsCard {...args} icon="" />);
expect(getByText('S')).toBeDefined();
});
});

View File

@ -3403,7 +3403,7 @@
ts-dedent "^2.0.0"
util-deprecate "^1.0.2"
"@storybook/client-api@6.3.12":
"@storybook/client-api@6.3.12", "@storybook/client-api@^6.3.12":
version "6.3.12"
resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.3.12.tgz#a0c6d72a871d1cb02b4b98675472839061e39b5b"
integrity sha512-xnW+lKKK2T774z+rOr9Wopt1aYTStfb86PSs9p3Fpnc2Btcftln+C3NtiHZl8Ccqft8Mz/chLGgewRui6tNI8g==