No files matched your search
@@ -13,7 +13,8 @@
|
||||
"cypress:run": "cypress run --headless --browser chrome --spec \"cypress/integration/**/*.spec.js\"",
|
||||
"pot": "node potfile.js",
|
||||
"po2json": "node po2json.js",
|
||||
"lint": "eslint ./src/client/"
|
||||
"lint": "eslint ./src/client/",
|
||||
"lint-fix": "eslint ./src/client/ --fix"
|
||||
},
|
||||
"author": "opennebula.io",
|
||||
"license": "ISC",
|
||||
@@ -126,4 +127,4 @@
|
||||
"yup": "^0.29.3",
|
||||
"zeromq": "^5.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 192 B |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 91 KiB |
@@ -3,13 +3,17 @@ import PropTypes from 'prop-types'
|
||||
|
||||
import { makeStyles, Chip, Box, Typography, Avatar } from '@material-ui/core'
|
||||
|
||||
import StatusBadge from 'client/components/StatusBadge'
|
||||
import SelectCard from 'client/components/Cards/SelectCard'
|
||||
import { StatusBadge } from 'client/components/Status'
|
||||
import Datastore from 'client/constants/datastore'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
title: { display: 'flex', gap: '8px' },
|
||||
avatar: { color: theme.palette.primary.contrastText },
|
||||
avatar: {
|
||||
width: '5ch',
|
||||
height: '5ch',
|
||||
color: theme.palette.primary.contrastText
|
||||
},
|
||||
card: { backgroundColor: theme.palette.primary.main }
|
||||
}))
|
||||
|
||||
|
||||
@@ -4,12 +4,16 @@ import PropTypes from 'prop-types'
|
||||
import { makeStyles, Avatar, Chip, Box, Typography } from '@material-ui/core'
|
||||
|
||||
import SelectCard from 'client/components/Cards/SelectCard'
|
||||
import StatusBadge from 'client/components/StatusBadge'
|
||||
import { StatusBadge } from 'client/components/Status'
|
||||
import Host from 'client/constants/host'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
title: { display: 'flex', gap: '8px' },
|
||||
avatar: { color: theme.palette.primary.contrastText },
|
||||
avatar: {
|
||||
width: '5ch',
|
||||
height: '5ch',
|
||||
color: theme.palette.primary.contrastText
|
||||
},
|
||||
card: { backgroundColor: theme.palette.primary.main }
|
||||
}))
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ import SelectCard from 'client/components/Cards/SelectCard'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
title: { display: 'flex', gap: '8px' },
|
||||
avatar: { color: theme.palette.primary.contrastText },
|
||||
avatar: {
|
||||
width: '5ch',
|
||||
height: '5ch',
|
||||
color: theme.palette.primary.contrastText
|
||||
},
|
||||
card: { backgroundColor: theme.palette.primary.main }
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import React, { memo, useState, useEffect } from 'react'
|
||||
import React, { memo, useState, useEffect, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Avatar } from '@material-ui/core'
|
||||
import ProviderIcon from '@material-ui/icons/Public'
|
||||
import ProvisionIcon from '@material-ui/icons/Cloud'
|
||||
|
||||
import SelectCard from 'client/components/Cards/SelectCard'
|
||||
import Action from 'client/components/Cards/SelectCard/Action'
|
||||
import { IMAGES_URL } from 'client/constants'
|
||||
import { isExternalURL } from 'client/utils'
|
||||
import { IMAGES_URL, DEFAULT_IMAGE } from 'client/constants'
|
||||
|
||||
const ProvisionCard = memo(
|
||||
({ value, isSelected, handleClick, isProvider, imgAsAvatar, actions }) => {
|
||||
const DEFAULT_IMAGE = isProvider ? 'provider.webp' : 'provision.webp'
|
||||
|
||||
({ value, isSelected, handleClick, isProvider, actions }) => {
|
||||
const [image, setImage] = useState(undefined)
|
||||
const { ID, NAME, TEMPLATE: { PLAIN = '{}' } } = value
|
||||
const { NAME, TEMPLATE: { PLAIN = '{}' } } = value
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -26,32 +24,24 @@ const ProvisionCard = memo(
|
||||
}
|
||||
}, [])
|
||||
|
||||
const isExternalURL = RegExp(/^(http|https):/g).test(image)
|
||||
const onError = evt => { evt.target.src = DEFAULT_IMAGE }
|
||||
|
||||
const onError = evt => {
|
||||
evt.target.src = `${IMAGES_URL}/${DEFAULT_IMAGE}`
|
||||
}
|
||||
const imgSource = useMemo(() =>
|
||||
isExternalURL(image) ? image : `${IMAGES_URL}/${image}`
|
||||
, [image])
|
||||
|
||||
return (
|
||||
<SelectCard
|
||||
title={`${ID} - ${NAME}`}
|
||||
title={NAME}
|
||||
isSelected={isSelected}
|
||||
handleClick={handleClick}
|
||||
action={actions?.map(action => <Action key={action?.cy} {...action} />)}
|
||||
icon={isProvider ? <ProviderIcon /> : <ProvisionIcon />}
|
||||
{...(imgAsAvatar
|
||||
? {
|
||||
icon: image && (
|
||||
<Avatar
|
||||
src={isExternalURL ? image : `${IMAGES_URL}/${image}`}
|
||||
variant="rounded"
|
||||
style={{ width: 100, height: 80 }}
|
||||
imgProps={{ onError }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
: { mediaProps: { component: 'img', image, onError } }
|
||||
)}
|
||||
mediaProps={{
|
||||
component: 'img',
|
||||
image: imgSource,
|
||||
onError
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}, (prev, next) => prev.isSelected === next.isSelected
|
||||
@@ -72,7 +62,6 @@ ProvisionCard.propTypes = {
|
||||
isSelected: PropTypes.bool,
|
||||
handleClick: PropTypes.func,
|
||||
isProvider: PropTypes.bool,
|
||||
imgAsAvatar: PropTypes.bool,
|
||||
actions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
handleClick: PropTypes.func.isRequired,
|
||||
@@ -87,7 +76,6 @@ ProvisionCard.defaultProps = {
|
||||
isSelected: undefined,
|
||||
handleClick: undefined,
|
||||
isProvider: false,
|
||||
imgAsAvatar: false,
|
||||
actions: undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,28 @@
|
||||
import React, { memo } from 'react'
|
||||
import React, { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Avatar } from '@material-ui/core'
|
||||
import SelectCard from 'client/components/Cards/SelectCard'
|
||||
import { isExternalURL } from 'client/utils'
|
||||
import { IMAGES_URL } from 'client/constants'
|
||||
|
||||
const ProvisionTemplateCard = memo(
|
||||
({ value, isProvider, isSelected, handleClick }) => {
|
||||
const DEFAULT_IMAGE = isProvider ? 'provider.webp' : 'provision.webp'
|
||||
const { name, plain: { image = DEFAULT_IMAGE } = {} } = value
|
||||
({ value, title, isSelected, handleClick }) => {
|
||||
const { plain: { image } = {} } = value
|
||||
|
||||
const isExternalURL = RegExp(/^(http|https):/g).test(image)
|
||||
|
||||
const onError = evt => {
|
||||
evt.target.src = `${IMAGES_URL}/${DEFAULT_IMAGE}`
|
||||
}
|
||||
const imgSource = useMemo(() =>
|
||||
isExternalURL(image) ? image : `${IMAGES_URL}/${image}`
|
||||
, [image])
|
||||
|
||||
return (
|
||||
<SelectCard
|
||||
stylesProps={{ minHeight: 80 }}
|
||||
title={name}
|
||||
isSelected={isSelected}
|
||||
handleClick={handleClick}
|
||||
icon={
|
||||
<Avatar
|
||||
src={isExternalURL ? image : `${IMAGES_URL}/${image}`}
|
||||
variant="rounded"
|
||||
style={{ width: 100, height: 80 }}
|
||||
imgProps={{ onError }}
|
||||
/>
|
||||
}
|
||||
title={title}
|
||||
mediaProps={image && {
|
||||
component: 'img',
|
||||
image: imgSource
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}, (prev, next) => prev.isSelected === next.isSelected
|
||||
@@ -42,6 +35,7 @@ ProvisionTemplateCard.propTypes = {
|
||||
image: PropTypes.string
|
||||
})
|
||||
}),
|
||||
title: PropTypes.string,
|
||||
isProvider: PropTypes.bool,
|
||||
isSelected: PropTypes.bool,
|
||||
handleClick: PropTypes.func
|
||||
@@ -49,6 +43,7 @@ ProvisionTemplateCard.propTypes = {
|
||||
|
||||
ProvisionTemplateCard.defaultProps = {
|
||||
value: {},
|
||||
title: undefined,
|
||||
isProvider: undefined,
|
||||
isSelected: undefined,
|
||||
handleClick: undefined
|
||||
|
||||
@@ -3,51 +3,14 @@ import PropTypes from 'prop-types'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import {
|
||||
makeStyles, Card, CardActionArea, CardHeader, CardActions, CardMedia
|
||||
Card, CardActionArea, CardHeader, CardActions, CardMedia
|
||||
} from '@material-ui/core'
|
||||
import { Skeleton } from '@material-ui/lab'
|
||||
|
||||
import useNearScreen from 'client/hooks/useNearScreen'
|
||||
import ConditionalWrap from 'client/components/HOC/ConditionalWrap'
|
||||
import Action from 'client/components/Cards/SelectCard/Action'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: { height: '100%' },
|
||||
selected: {
|
||||
color: theme.palette.secondary.contrastText,
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
'& .badge': {
|
||||
color: theme.palette.secondary.main,
|
||||
backgroundColor: theme.palette.secondary.contrastText
|
||||
},
|
||||
'& $subheader': {
|
||||
color: 'inherit'
|
||||
}
|
||||
},
|
||||
actionArea: {
|
||||
height: '100%',
|
||||
minHeight: ({ minHeight = 140 }) => minHeight
|
||||
},
|
||||
mediaImage: {
|
||||
height: ({ mediaHeight = 140 }) => mediaHeight
|
||||
},
|
||||
headerAvatar: {
|
||||
display: 'flex',
|
||||
color: theme.palette.primary.contrastText
|
||||
},
|
||||
headerContent: { overflowX: 'hidden' },
|
||||
header: {
|
||||
color: theme.palette.primary.contrastText
|
||||
},
|
||||
subheader: {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'initial',
|
||||
display: '-webkit-box',
|
||||
lineClamp: 2,
|
||||
boxOrient: 'vertical'
|
||||
}
|
||||
}))
|
||||
import selectCardStyles from 'client/components/Cards/SelectCard/styles'
|
||||
|
||||
const SelectCard = memo(({
|
||||
stylesProps,
|
||||
@@ -65,7 +28,7 @@ const SelectCard = memo(({
|
||||
observerOff,
|
||||
children
|
||||
}) => {
|
||||
const classes = useStyles(stylesProps)
|
||||
const classes = selectCardStyles(stylesProps)
|
||||
const { isNearScreen, fromRef } = useNearScreen({
|
||||
distance: '100px'
|
||||
})
|
||||
@@ -96,7 +59,7 @@ const SelectCard = memo(({
|
||||
}
|
||||
>
|
||||
{/* CARD HEADER */}
|
||||
<CardHeader
|
||||
{(title || subheader || icon || action) && <CardHeader
|
||||
action={action}
|
||||
avatar={icon}
|
||||
classes={{
|
||||
@@ -118,7 +81,7 @@ const SelectCard = memo(({
|
||||
title: subheader
|
||||
}}
|
||||
{...cardHeaderProps}
|
||||
/>
|
||||
/>}
|
||||
|
||||
{/* CARD CONTENT */}
|
||||
{children}
|
||||
@@ -128,12 +91,15 @@ const SelectCard = memo(({
|
||||
<ConditionalWrap
|
||||
condition={handleClick && action}
|
||||
wrap={children =>
|
||||
<CardActionArea onClick={handleClick}>
|
||||
<CardActionArea
|
||||
className={classes.mediaActionArea}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{children}
|
||||
</CardActionArea>
|
||||
}
|
||||
>
|
||||
<CardMedia className={classes.mediaImage} {...mediaProps} />
|
||||
<CardMedia className={classes.media} {...mediaProps} />
|
||||
</ConditionalWrap>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
|
||||
export default makeStyles(theme => ({
|
||||
root: {
|
||||
height: '100%',
|
||||
'& $actionArea, & $mediaActionArea, & $media': {
|
||||
transition: theme.transitions.create(
|
||||
['filter', 'background-color'],
|
||||
{ duration: '0.2s' }
|
||||
)
|
||||
}
|
||||
},
|
||||
selected: {
|
||||
color: theme.palette.secondary.contrastText,
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
'& .badge': {
|
||||
color: theme.palette.secondary.main,
|
||||
backgroundColor: theme.palette.secondary.contrastText
|
||||
},
|
||||
'& $subheader': { color: 'inherit' }
|
||||
},
|
||||
actionArea: {
|
||||
height: '100%',
|
||||
minHeight: ({ minHeight = 140 }) => minHeight,
|
||||
'& $media': { filter: 'contrast(0) brightness(2)' }
|
||||
},
|
||||
mediaActionArea: {
|
||||
display: 'flex',
|
||||
height: ({ mediaHeight = 140 }) => mediaHeight,
|
||||
'& $media': { filter: 'contrast(0) brightness(2)' },
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.contrastText,
|
||||
'& $media': { filter: 'none' }
|
||||
}
|
||||
},
|
||||
media: {},
|
||||
headerAvatar: {
|
||||
display: 'flex',
|
||||
color: theme.palette.primary.contrastText
|
||||
},
|
||||
headerContent: { overflowX: 'hidden' },
|
||||
header: {
|
||||
color: theme.palette.primary.contrastText
|
||||
},
|
||||
subheader: {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'initial',
|
||||
display: '-webkit-box',
|
||||
lineClamp: 2,
|
||||
boxOrient: 'vertical'
|
||||
}
|
||||
}))
|
||||
@@ -46,7 +46,7 @@ const StatusBadge = memo(({ stateColor, children }) => {
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
(prev, next) => prev.isSelected === next.isSelected
|
||||
(prev, next) => prev.stateColor === next.stateColor
|
||||
)
|
||||
|
||||
StatusBadge.propTypes = {
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { makeStyles, Typography } from '@material-ui/core'
|
||||
import { addOpacityToColor } from 'client/utils'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: ({ stateColor }) => ({
|
||||
color: stateColor,
|
||||
backgroundColor: addOpacityToColor(stateColor, 0.08),
|
||||
cursor: 'default',
|
||||
padding: theme.spacing('0.25rem', '0.5rem'),
|
||||
minWidth: 20,
|
||||
borderRadius: 2,
|
||||
textTransform: 'uppercase',
|
||||
fontSize: theme.typography.overline.fontSize,
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
lineHeight: 'normal'
|
||||
})
|
||||
}))
|
||||
|
||||
const StatusChip = memo(({ stateColor, children }) => {
|
||||
const classes = useStyles({ stateColor })
|
||||
|
||||
return (
|
||||
<Typography component="span" className={classes.root}>
|
||||
{children}
|
||||
</Typography>
|
||||
)
|
||||
},
|
||||
(prev, next) => prev.stateColor === next.stateColor
|
||||
)
|
||||
|
||||
StatusChip.propTypes = {
|
||||
stateColor: PropTypes.string,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node
|
||||
])
|
||||
}
|
||||
|
||||
StatusChip.defaultProps = {
|
||||
stateColor: undefined,
|
||||
children: ''
|
||||
}
|
||||
|
||||
StatusChip.displayName = 'StatusChip'
|
||||
|
||||
export default StatusChip
|
||||
@@ -0,0 +1,7 @@
|
||||
import StatusBadge from 'client/components/Status/Badge'
|
||||
import StatusChip from 'client/components/Status/Chip'
|
||||
|
||||
export {
|
||||
StatusBadge,
|
||||
StatusChip
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export const APP_URL = defaultAppName ? `/${defaultAppName}` : ''
|
||||
export const WEBSOCKET_URL = `${APP_URL}/websocket`
|
||||
export const STATIC_FILES_URL = `${APP_URL}/client/assets`
|
||||
export const IMAGES_URL = `${STATIC_FILES_URL}/images`
|
||||
export const DEFAULT_IMAGE = `${STATIC_FILES_URL}/images/default.webp`
|
||||
export const FONTS_URL = `${STATIC_FILES_URL}/fonts`
|
||||
export const LANGUAGES_URL = `${STATIC_FILES_URL}/languages`
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { List, ListItem, Typography, Chip, Grid, Paper, Divider } from '@material-ui/core'
|
||||
import { List, ListItem, Typography, Grid, Paper, Divider } from '@material-ui/core'
|
||||
import { CheckBox, CheckBoxOutlineBlank } from '@material-ui/icons'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import useStyles from 'client/containers/ApplicationsInstances/DialogInfo/styles'
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
import { APPLICATION_STATES } from 'client/constants/flow'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
@@ -53,11 +54,9 @@ const InfoTab = memo(({ info }) => {
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Typography>{Tr(T.State)}</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={stateInfo?.name}
|
||||
style={{ backgroundColor: stateInfo?.color }}
|
||||
/>
|
||||
<StatusChip stateColor={stateInfo?.color}>
|
||||
{stateInfo?.name}
|
||||
</StatusChip>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Typography>{Tr(T.ReadyStatusGate)}</Typography>
|
||||
|
||||
@@ -57,7 +57,6 @@ const Provider = () => ({
|
||||
handleClick: () => handleClick(name, isSelected)
|
||||
}
|
||||
}}
|
||||
breakpoints={{ xs: 12, sm: 6, md: 4 }}
|
||||
/>
|
||||
)
|
||||
}, [])
|
||||
|
||||
@@ -47,7 +47,7 @@ function ProviderCreateForm () {
|
||||
const providerTemplate = providersTemplates
|
||||
.find(({ name }) => name === providerSelected) ?? {}
|
||||
|
||||
if (!providerSelected) {
|
||||
if (!providerTemplate) {
|
||||
showError({
|
||||
message: `
|
||||
Cannot found provider template (${providerSelected}),
|
||||
@@ -56,15 +56,15 @@ function ProviderCreateForm () {
|
||||
history.push(PATH.PROVISIONS.LIST)
|
||||
}
|
||||
|
||||
const { plain, location_key: locationKey } = providerTemplate
|
||||
|
||||
const formatData = {
|
||||
...(!isUpdate && { name: `${providerSelected}_${locationSelected}` }),
|
||||
...(providerTemplate?.image && {
|
||||
plain: { image: providerTemplate.image }
|
||||
}),
|
||||
...(plain && { plain }),
|
||||
provider: providerSelected,
|
||||
connection: {
|
||||
...connection,
|
||||
[providerTemplate.location_key]: locationSelected
|
||||
[locationKey]: locationSelected
|
||||
},
|
||||
registration_time: time
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { List, ListItem, Typography, Grid, Paper, Divider, Chip } from '@material-ui/core'
|
||||
import { List, ListItem, Typography, Grid, Paper, Divider } from '@material-ui/core'
|
||||
import { CheckBox, CheckBoxOutlineBlank } from '@material-ui/icons'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import useStyles from 'client/containers/Provisions/DialogInfo/styles'
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T, PROVISIONS_STATES } from 'client/constants'
|
||||
|
||||
@@ -64,11 +65,9 @@ const Info = memo(({ data }) => {
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Typography>{Tr(T.State)}</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={stateInfo?.name}
|
||||
style={{ backgroundColor: stateInfo?.color }}
|
||||
/>
|
||||
<StatusChip stateColor={stateInfo?.color}>
|
||||
{stateInfo?.name}
|
||||
</StatusChip>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
|
||||
@@ -50,7 +50,6 @@ const Provider = () => ({
|
||||
const isSelected = data?.some(selected => selected === NAME)
|
||||
|
||||
return {
|
||||
imgAsAvatar: true,
|
||||
isProvider: true,
|
||||
isSelected,
|
||||
handleClick: () =>
|
||||
|
||||
@@ -54,6 +54,7 @@ const Provision = () => ({
|
||||
|
||||
return {
|
||||
isSelected,
|
||||
title: name,
|
||||
handleClick: () => handleClick(name, provider, isSelected)
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
palette: {
|
||||
type: 'dark',
|
||||
common: { black: '#000', white: '#ffffff' },
|
||||
common: { black: '#000', white: '#fff' },
|
||||
background: {
|
||||
paper: '#2a2d3d',
|
||||
default: '#222431'
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
export const fakeDelay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
export const isExternalURL = url => RegExp(/^(http|https):/g).test(url)
|
||||
|
||||
export const addOpacityToColor = (color, opacity) => {
|
||||
const opacityHex = Math.round(opacity * 255).toString(16)
|
||||
return `${color}${opacityHex}`
|
||||
}
|
||||
|
||||
export const capitalize = ([firstLetter, ...restOfWord]) =>
|
||||
firstLetter.toUpperCase() + restOfWord.join('')
|
||||
|
||||
|
||||