fix: machine status layout bugs

fix: reboot icon looks cropped

fix: confirm dialog layout

fix: Status chip background colors

fix: detailed machine status layout

fix: machine detailed status layout

fix: machine status article links, status chip size

fix: confirmDialog for all machine actions

fix: confirm dialog on every action. reload when success

fix: verbose input label

fix: display software version and machine model

fix: eslint fixes

fix: removed machine version and update button

fix: get machines statuses from ping

chore: removed the support articles until they're ready

fix: reset value and error states when closing the confirm dialog

fix: removed unused info from the machine table

styles: fixed styles in the machine details card

chore: moved styles to another file

fix: fixed the version gql property
This commit is contained in:
Mauricio Navarro Miranda 2020-07-13 15:25:43 -05:00 committed by Josh Harvey
parent 825a9bfe09
commit db014a3ed4
9 changed files with 278 additions and 214 deletions

View file

@ -4,6 +4,8 @@ const axios = require('axios')
const logger = require('./logger') const logger = require('./logger')
const db = require('./db') const db = require('./db')
const pairing = require('./pairing') const pairing = require('./pairing')
const notifier = require('./notifier')
const dbm = require('./postgresql_interface')
const configManager = require('./new-config-manager') const configManager = require('./new-config-manager')
const settingsLoader = require('./new-settings-loader') const settingsLoader = require('./new-settings-loader')
@ -34,15 +36,40 @@ function getConfig (defaultConfig) {
} }
function getMachineNames (config) { function getMachineNames (config) {
const fullyFunctionalStatus = {label: 'Fully functional', type: 'success'}
const unresponsiveStatus = {label: 'Unresponsive', type: 'error'}
const stuckStatus = {label: 'Stuck', type: 'error'}
return Promise.all([getMachines(), getConfig(config)]) return Promise.all([getMachines(), getConfig(config)])
.then(([machines, config]) => { .then(([machines, config]) => Promise.all(
[machines, notifier.checkPings(machines), dbm.machineEvents(), config]
))
.then(([machines, pings, events, config]) => {
const getPingStatus = (ping) => {
if (!ping) return fullyFunctionalStatus
if (ping.age) return unresponsiveStatus
return fullyFunctionalStatus
}
const getStuckStatus = (stuck) => {
if (!stuck || !stuck.age) return undefined
return stuckStatus
}
const addName = r => { const addName = r => {
const cashOutConfig = configManager.getCashOut(r.deviceId, config) const cashOutConfig = configManager.getCashOut(r.deviceId, config)
const cashOut = !!cashOutConfig.active const cashOut = !!cashOutConfig.active
// TODO new-admin actually load status based on ping. const ping = getPingStatus(_.first(pings[r.deviceId]))
const statuses = [{label: 'Unknown detailed status', type: 'warning'}] const stuck = getStuckStatus(_.first(notifier.checkStuckScreen(events, r.name)))
const statuses = [ping]
if (stuck) statuses.push(stuck)
return _.assign(r, {cashOut, statuses}) return _.assign(r, {cashOut, statuses})
} }

View file

@ -344,4 +344,8 @@ function buildAlertFingerprint (alertRec, notifications) {
return crypto.createHash('sha256').update(subject).digest('hex') return crypto.createHash('sha256').update(subject).digest('hex')
} }
module.exports = { checkNotification } module.exports = {
checkNotification,
checkPings,
checkStuckScreen
}

View file

@ -2,51 +2,50 @@ import {
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogContentText,
makeStyles makeStyles
} from '@material-ui/core' } from '@material-ui/core'
import React, { useEffect, useState, memo } from 'react' import React, { memo, useState } from 'react'
import { Button, IconButton } from 'src/components/buttons' import { Button, IconButton } from 'src/components/buttons'
import { TextInput } from 'src/components/inputs'
import { H4 } from 'src/components/typography'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
import { fontSize3 } from 'src/styling/variables' import { spacer } from 'src/styling/variables'
import { TextInput } from './inputs' import ErrorMessage from './ErrorMessage'
import { H4, P } from './typography'
const useStyles = makeStyles({ const useStyles = makeStyles({
label: { dialogContent: {
fontSize: fontSize3 width: 434,
padding: spacer * 2,
paddingRight: spacer * 3.5
}, },
spacing: { dialogTitle: {
padding: 32 padding: spacer * 2,
paddingRight: spacer * 1.5,
display: 'flex',
'justify-content': 'space-between',
'& > h4': {
margin: 0
},
'& > button': {
padding: 0,
marginTop: -(spacer / 2)
}
}, },
wrapper: { dialogActions: {
display: 'flex' padding: spacer * 4,
}, paddingTop: spacer * 2
title: {
margin: [[20, 0, 24, 16]]
},
closeButton: {
padding: 0,
margin: [[12, 12, 'auto', 'auto']]
// position: 'absolute',
// right: spacer,
// top: spacer
} }
}) })
export const DialogTitle = ({ children, onClose }) => { export const DialogTitle = ({ children, onClose }) => {
const classes = useStyles() const classes = useStyles()
return ( return (
<div className={classes.wrapper}> <div className={classes.dialogTitle}>
{children} {children}
{onClose && ( {onClose && (
<IconButton <IconButton size={16} aria-label="close" onClick={onClose}>
size={16}
aria-label="close"
className={classes.closeButton}
onClick={onClose}>
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
)} )}
@ -57,50 +56,59 @@ export const DialogTitle = ({ children, onClose }) => {
export const ConfirmDialog = memo( export const ConfirmDialog = memo(
({ ({
title = 'Confirm action', title = 'Confirm action',
subtitle = 'This action requires confirmation', errorMessage = 'This action requires confirmation',
open, open,
toBeConfirmed, toBeConfirmed,
onConfirmed, onConfirmed,
onDissmised, onDissmised,
className,
...props ...props
}) => { }) => {
const classes = useStyles() const classes = useStyles()
const [value, setValue] = useState('') const [value, setValue] = useState('')
const [error, setError] = useState(false) const [error, setError] = useState(false)
useEffect(() => setValue(''), [open]) const handleChange = event => setValue(event.target.value)
const handleChange = event => {
setValue(event.target.value) const innerOnClose = () => {
setValue('')
setError(false)
onDissmised()
} }
return ( return (
<Dialog open={open} aria-labelledby="form-dialog-title" {...props}> <Dialog open={open} aria-labelledby="form-dialog-title" {...props}>
<DialogTitle id="customized-dialog-title" onClose={onDissmised}> <DialogTitle id="customized-dialog-title" onClose={innerOnClose}>
<H4 className={classes.title}>{title}</H4> <H4>{title}</H4>
{subtitle && (
<DialogContentText>
<P>{subtitle}</P>
</DialogContentText>
)}
</DialogTitle> </DialogTitle>
<DialogContent className={className}> {errorMessage && (
<DialogTitle>
<ErrorMessage>
{errorMessage.split(':').map(error => (
<>
{error}
<br />
</>
))}
</ErrorMessage>
</DialogTitle>
)}
<DialogContent className={classes.dialogContent}>
<TextInput <TextInput
label={`Write '${toBeConfirmed}' to confirm`} label={`Write '${toBeConfirmed}' to confirm this action`}
name="confirm-input" name="confirm-input"
autoFocus autoFocus
id="confirm-input" id="confirm-input"
type="text" type="text"
size="lg" size="sm"
fullWidth fullWidth
value={value} value={value}
touched={{}} touched={{}}
error={error} error={error}
InputLabelProps={{ shrink: true, className: classes.label }} InputLabelProps={{ shrink: true }}
onChange={handleChange} onChange={handleChange}
onBlur={() => setError(toBeConfirmed !== value)} onBlur={() => setError(toBeConfirmed !== value)}
/> />
</DialogContent> </DialogContent>
<DialogActions classes={{ spacing: classes.spacing }}> <DialogActions className={classes.dialogActions}>
<Button <Button
color="green" color="green"
disabled={toBeConfirmed !== value} disabled={toBeConfirmed !== value}

View file

@ -11,7 +11,8 @@ import {
spring3, spring3,
smallestFontSize, smallestFontSize,
inputFontFamily, inputFontFamily,
spacer spacer,
linen
} from '../styling/variables' } from '../styling/variables'
const colors = { const colors = {
@ -22,7 +23,7 @@ const colors = {
const backgroundColors = { const backgroundColors = {
error: mistyRose, error: mistyRose,
warning: mistyRose, warning: linen,
success: spring3 success: spring3
} }
@ -40,21 +41,15 @@ const useStyles = makeStyles({
fontSize: smallestFontSize, fontSize: smallestFontSize,
fontWeight: inputFontWeight, fontWeight: inputFontWeight,
fontFamily: inputFontFamily, fontFamily: inputFontFamily,
padding: [[spacer / 2, spacer]], paddingRight: spacer / 2,
paddingLeft: spacer / 2,
color: ({ type }) => colors[type] color: ({ type }) => colors[type]
} }
}) })
const Status = ({ status, className }) => { const Status = ({ status }) => {
const classes = useStyles({ type: status.type }) const classes = useStyles({ type: status.type })
return ( return <Chip type={status.type} label={status.label} classes={classes} />
<Chip
type={status.type}
label={status.label}
className={className ?? null}
classes={classes}
/>
)
} }
const MainStatus = ({ statuses }) => { const MainStatus = ({ statuses }) => {
@ -65,7 +60,7 @@ const MainStatus = ({ statuses }) => {
const plus = { label: `+${statuses.length - 1}`, type: mainStatus.type } const plus = { label: `+${statuses.length - 1}`, type: mainStatus.type }
return ( return (
<div style={{ marginLeft: -3 }}> <div>
<Status status={mainStatus} /> <Status status={mainStatus} />
{statuses.length > 1 && <Status status={plus} />} {statuses.length > 1 && <Status status={plus} />}
</div> </div>

View file

@ -1,20 +1,22 @@
import { useMutation } from '@apollo/react-hooks' import { useMutation } from '@apollo/react-hooks'
import { Dialog, DialogContent } from '@material-ui/core' import { Grid, Divider } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import moment from 'moment' import moment from 'moment'
import React, { useState } from 'react' import React, { useState } from 'react'
import { DialogTitle, ConfirmDialog } from 'src/components/ConfirmDialog' import { ConfirmDialog } from 'src/components/ConfirmDialog'
import { Status } from 'src/components/Status' import { Status } from 'src/components/Status'
import ActionButton from 'src/components/buttons/ActionButton' import ActionButton from 'src/components/buttons/ActionButton'
import { Label1, H4 } from 'src/components/typography' import { ReactComponent as LinkIcon } from 'src/styling/icons/button/link/zodiac.svg'
import { ReactComponent as RebootReversedIcon } from 'src/styling/icons/button/reboot/white.svg' import { ReactComponent as RebootReversedIcon } from 'src/styling/icons/button/reboot/white.svg'
import { ReactComponent as RebootIcon } from 'src/styling/icons/button/reboot/zodiac.svg' import { ReactComponent as RebootIcon } from 'src/styling/icons/button/reboot/zodiac.svg'
import { ReactComponent as ShutdownReversedIcon } from 'src/styling/icons/button/shut down/white.svg'
import { ReactComponent as ShutdownIcon } from 'src/styling/icons/button/shut down/zodiac.svg'
import { ReactComponent as UnpairReversedIcon } from 'src/styling/icons/button/unpair/white.svg' import { ReactComponent as UnpairReversedIcon } from 'src/styling/icons/button/unpair/white.svg'
import { ReactComponent as UnpairIcon } from 'src/styling/icons/button/unpair/zodiac.svg' import { ReactComponent as UnpairIcon } from 'src/styling/icons/button/unpair/zodiac.svg'
import styles from './MachineDetailsCard.styles' import { labelStyles, machineDetailsStyles } from './MachineDetailsCard.styles'
const MACHINE_ACTION = gql` const MACHINE_ACTION = gql`
mutation MachineAction($deviceId: ID!, $action: MachineAction!) { mutation MachineAction($deviceId: ID!, $action: MachineAction!) {
@ -24,150 +26,172 @@ const MACHINE_ACTION = gql`
} }
` `
const useStyles = makeStyles(styles) const supportArtices = [
{
// Default article for non-maped statuses
code: undefined,
label: 'Troubleshooting',
article:
'https://support.lamassu.is/hc/en-us/categories/115000075249-Troubleshooting'
}
]
const article = ({ code: status }) =>
supportArtices.find(({ code: article }) => article === status)
const useLStyles = makeStyles(labelStyles)
const Label = ({ children }) => { const Label = ({ children }) => {
const classes = useStyles() const classes = useLStyles()
return <Label1 className={classes.label}>{children}</Label1>
return <div className={classes.label}>{children}</div>
} }
const MachineDetailsRow = ({ it: machine }) => { const useMDStyles = makeStyles(machineDetailsStyles)
const [errorDialog, setErrorDialog] = useState(false)
const [dialogOpen, setOpen] = useState(false)
const [actionMessage, setActionMessage] = useState(null)
const classes = useStyles()
const unpairDialog = () => setOpen(true) const Container = ({ children, ...props }) => (
<Grid container spacing={4} {...props}>
{children}
</Grid>
)
const Item = ({ children, ...props }) => (
<Grid item xs {...props}>
{children}
</Grid>
)
const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
const [action, setAction] = useState('')
const [dialogOpen, setOpen] = useState(false)
const [errorMessage, setErrorMessage] = useState(null)
const classes = useMDStyles()
const confirmDialog = action => setAction(action) || setOpen(true)
const [machineAction, { loading }] = useMutation(MACHINE_ACTION, { const [machineAction, { loading }] = useMutation(MACHINE_ACTION, {
onError: ({ graphQLErrors, message }) => { onError: ({ message }) => {
const errorMessage = graphQLErrors[0] ? graphQLErrors[0].message : message const errorMessage = message ?? 'An error ocurred'
setActionMessage(errorMessage) setErrorMessage(errorMessage)
setErrorDialog(true) },
onCompleted: () => {
// TODO: custom onActionSuccess needs to be passed down from the machinestatus table
onActionSuccess ? onActionSuccess() : window.location.reload()
setOpen(false)
} }
}) })
return ( return (
<> <>
<Dialog open={errorDialog} aria-labelledby="form-dialog-title"> <Container className={classes.wrapper}>
<DialogTitle <Item xs={5}>
id="customized-dialog-title" <Container>
onClose={() => setErrorDialog(false)}> <Item>
<H4>Error</H4>
</DialogTitle>
<DialogContent>{actionMessage}</DialogContent>
</Dialog>
<div className={classes.wrapper}>
<div className={classes.column1}>
<div className={classes.lastRow}>
<div className={classes.status}>
<Label>Statuses</Label> <Label>Statuses</Label>
<div> <ul className={classes.list}>
{machine.statuses.map((status, index) => ( {machine.statuses.map((status, index) => (
<Status <li key={index}>
className={classes.chips} <Status status={status} />
status={status} </li>
key={index}
/>
))} ))}
</div> </ul>
</div> </Item>
<div> <Item>
<Label>Lamassu Support article</Label> <Label>Lamassu Support article</Label>
<div> <ul className={classes.list}>
{machine.statuses.map((...[, index]) => ( {machine.statuses
// TODO new-admin: support articles .map(article)
<span key={index}></span> .map(({ label, article }, index) => (
))} <li key={index}>
</div> <a
</div> target="_blank"
<div className={classes.separator} /> rel="noopener noreferrer"
</div> href={article}>
</div> '{label}' <LinkIcon />
<div className={classes.column2}> </a>
<div className={classes.row}> </li>
<div className={classes.machineModel}> ))}
</ul>
</Item>
</Container>
</Item>
<Divider
orientation="vertical"
flexItem
className={classes.separator}
/>
<ConfirmDialog
open={dialogOpen}
title={`${action} this machine?`}
errorMessage={errorMessage}
toBeConfirmed={machine.name}
onConfirmed={() => {
setErrorMessage(null)
machineAction({
variables: {
deviceId: machine.deviceId,
action: `${action}`.toLowerCase()
}
})
}}
onDissmised={() => {
setOpen(false)
setErrorMessage(null)
}}
/>
<Item xs>
<Container className={classes.row}>
<Item xs={4}>
<Label>Machine Model</Label> <Label>Machine Model</Label>
<div>{machine.model ?? 'unknown'}</div> <span>{machine.model}</span>
</div> </Item>
<div> {/* <Item>
<Label>Address</Label>
<span>{machine.machineLocation}</span>
</Item> */}
<Item xs={4}>
<Label>Paired at</Label> <Label>Paired at</Label>
<div> <span>
{machine.pairedAt {moment(machine.pairedAt).format('YYYY-MM-DD HH:mm:ss')}
? moment(machine.pairedAt).format('YYYY-MM-DD HH:mm:ss') </span>
: 'N/A'} </Item>
</div> </Container>
</div> <Container>
</div> <Item>
<div className={classes.lastRow}>
<div>
<Label>Actions</Label> <Label>Actions</Label>
<div className={classes.actionRow}> <div className={classes.stack}>
<ActionButton <ActionButton
className={classes.action}
color="primary" color="primary"
className={classes.mr}
Icon={UnpairIcon} Icon={UnpairIcon}
InverseIcon={UnpairReversedIcon} InverseIcon={UnpairReversedIcon}
disabled={loading} disabled={loading}
onClick={unpairDialog}> onClick={() => confirmDialog('Unpair')}>
Unpair Unpair
</ActionButton> </ActionButton>
<ConfirmDialog
open={dialogOpen}
className={classes.dialog}
title="Unpair this machine?"
subtitle={false}
toBeConfirmed={machine.name}
onConfirmed={() => {
setOpen(false)
machineAction({
variables: {
deviceId: machine.deviceId,
action: 'unpair'
}
})
}}
onDissmised={() => {
setOpen(false)
}}
/>
<ActionButton <ActionButton
className={classes.action}
color="primary" color="primary"
className={classes.mr}
Icon={RebootIcon} Icon={RebootIcon}
InverseIcon={RebootReversedIcon} InverseIcon={RebootReversedIcon}
disabled={loading} disabled={loading}
onClick={() => { onClick={() => confirmDialog('Reboot')}>
machineAction({
variables: {
deviceId: machine.deviceId,
action: 'reboot'
}
})
}}>
Reboot Reboot
</ActionButton> </ActionButton>
<ActionButton <ActionButton
className={classes.action} className={classes.inlineChip}
disabled={loading} disabled={loading}
color="primary" color="primary"
Icon={RebootIcon} Icon={ShutdownIcon}
InverseIcon={RebootReversedIcon} InverseIcon={ShutdownReversedIcon}
onClick={() => { onClick={() => confirmDialog('Shutdown')}>
machineAction({ Shutdown
variables: {
deviceId: machine.deviceId,
action: 'restartServices'
}
})
}}>
Restart Services
</ActionButton> </ActionButton>
</div> </div>
</div> </Item>
</div> </Container>
</div> </Item>
</div> </Container>
</> </>
) )
} }

View file

@ -1,53 +1,55 @@
import { fade } from '@material-ui/core/styles/colorManipulator' import { fade } from '@material-ui/core/styles/colorManipulator'
import { fontSize4, offColor, comet } from 'src/styling/variables' import {
detailsRowStyles,
labelStyles
} from 'src/pages/Transactions/Transactions.styles'
import { spacer, comet, primaryColor, fontSize4 } from 'src/styling/variables'
export default { const machineDetailsStyles = {
...detailsRowStyles,
colDivider: {
width: 1,
margin: [[spacer * 2, spacer * 4]],
backgroundColor: comet,
border: 'none'
},
inlineChip: {
marginInlineEnd: '0.25em'
},
stack: {
display: 'flex',
flexDirection: 'row'
},
wrapper: { wrapper: {
display: 'flex', display: 'flex',
marginTop: 24, marginTop: 24,
marginBottom: 32, marginBottom: 32,
fontSize: fontSize4 fontSize: fontSize4
}, },
column1: {
width: 600
},
column2: {
flex: 1
},
lastRow: {
display: 'flex',
flexDirection: 'row'
},
row: { row: {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
marginBottom: 36 marginBottom: 36
}, },
actionRow: { list: {
display: 'flex', padding: 0,
flexDirection: 'row', margin: 0,
marginLeft: -4 listStyle: 'none',
'& > li': {
height: spacer * 3,
marginBottom: spacer * 1.5,
'& > a, & > a:visited': {
color: primaryColor,
textDecoration: 'none'
}
}
}, },
action: { divider: {
marginRight: 4, margin: '0 1rem'
marginLeft: 4
}, },
dialog: { mr: {
width: 434 marginRight: spacer
},
label: {
color: offColor,
margin: [[0, 0, 6, 0]]
},
chips: {
marginLeft: -2
},
status: {
width: 248
},
machineModel: {
width: 198
}, },
separator: { separator: {
width: 1, width: 1,
@ -58,3 +60,5 @@ export default {
background: fade(comet, 0.5) background: fade(comet, 0.5)
} }
} }
export { labelStyles, machineDetailsStyles }

View file

@ -5,13 +5,12 @@ import moment from 'moment'
import * as R from 'ramda' import * as R from 'ramda'
import React from 'react' import React from 'react'
import { MainStatus } from 'src/components/Status'
import Title from 'src/components/Title'
import DataTable from 'src/components/tables/DataTable' import DataTable from 'src/components/tables/DataTable'
import { mainStyles } from 'src/pages/Transactions/Transactions.styles'
import { MainStatus } from '../../components/Status' import { ReactComponent as WarningIcon } from 'src/styling/icons/status/pumpkin.svg'
import Title from '../../components/Title' import { ReactComponent as ErrorIcon } from 'src/styling/icons/status/tomato.svg'
import { ReactComponent as WarningIcon } from '../../styling/icons/status/pumpkin.svg'
import { ReactComponent as ErrorIcon } from '../../styling/icons/status/tomato.svg'
import { mainStyles } from '../Transactions/Transactions.styles'
import MachineDetailsRow from './MachineDetailsCard' import MachineDetailsRow from './MachineDetailsCard'
@ -22,10 +21,13 @@ const GET_MACHINES = gql`
deviceId deviceId
lastPing lastPing
pairedAt pairedAt
version
paired paired
cashbox cashbox
cassette1 cassette1
cassette2 cassette2
version
model
statuses { statuses {
label label
type type
@ -68,7 +70,7 @@ const MachineStatus = () => {
width: 200, width: 200,
size: 'sm', size: 'sm',
textAlign: 'left', textAlign: 'left',
view: m => m.softwareVersion || 'unknown' view: m => m.version || 'unknown'
} }
] ]

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="12px" height="12px" viewBox="-0.493 -0.5 12.993 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com --> <!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<desc>Created with Sketch.</desc> <desc>Created with Sketch.</desc>
<g id="icon/button/reboot/white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> <g id="icon/button/reboot/white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="12px" height="12px" viewBox="-0.493 -0.5 12.993 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com --> <!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<desc>Created with Sketch.</desc> <desc>Created with Sketch.</desc>
<g id="icon/button/reboot/zodiac" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> <g id="icon/button/reboot/zodiac" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After