feat: add license settings UI (#7210)

* wip: license page

* WIP

* WIP

* wip

* wip

* wip

* wip

* wip

* wip

* Apply suggestions from code review

Co-authored-by: Ben Potter <ben@coder.com>

* wip: ui improvements

* wip: extract components

* wip: stories

* wip: stories

* fixes from PR reviews

* fix stories

* fix empty license page

* fix copy

* fix

* wip

* add golang test

---------

Co-authored-by: Ben Potter <ben@coder.com>
This commit is contained in:
Rodrigo Maia
2023-04-26 17:47:46 -03:00
committed by GitHub
co-authored by Ben Potter
parent c3fe2515a7
commit 87b7537878
17 changed files with 1082 additions and 163 deletions
+7
View File
@@ -53,6 +53,13 @@ func Entitlements(
return entitlements, xerrors.Errorf("query active user count: %w", err)
}
// always shows active user count regardless of license
entitlements.Features[codersdk.FeatureUserLimit] = codersdk.Feature{
Entitlement: codersdk.EntitlementNotEntitled,
Enabled: enablements[codersdk.FeatureUserLimit],
Actual: &activeUserCount,
}
allFeatures := false
// Here we loop through licenses to detect enabled features.
@@ -37,6 +37,15 @@ func TestEntitlements(t *testing.T) {
require.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[featureName].Entitlement)
}
})
t.Run("Always return the current user count", func(t *testing.T) {
t.Parallel()
db := dbfake.New()
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.False(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
require.Equal(t, *entitlements.Features[codersdk.FeatureUserLimit].Actual, int64(0))
})
t.Run("SingleLicenseNothing", func(t *testing.T) {
t.Parallel()
db := dbfake.New()
+2
View File
@@ -68,6 +68,7 @@
"react": "18.2.0",
"react-chartjs-2": "4.3.1",
"react-color": "2.19.3",
"react-confetti": "^6.1.0",
"react-dom": "18.2.0",
"react-headless-tabs": "6.0.3",
"react-helmet-async": "1.3.0",
@@ -75,6 +76,7 @@
"react-markdown": "8.0.3",
"react-router-dom": "6.4.1",
"react-syntax-highlighter": "15.5.0",
"react-use": "^17.4.0",
"react-virtualized-auto-sizer": "1.0.7",
"react-window": "1.8.8",
"remark-gfm": "3.0.1",
+13
View File
@@ -156,6 +156,17 @@ const TemplateSchedulePage = lazy(
),
)
const LicensesSettingsPage = lazy(
() =>
import(
"./pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage"
),
)
const AddNewLicensePage = lazy(
() =>
import("./pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage"),
)
export const AppRouter: FC = () => {
return (
<Suspense fallback={<FullScreenLoader />}>
@@ -244,6 +255,8 @@ export const AppRouter: FC = () => {
element={<DeploySettingsLayout />}
>
<Route path="general" element={<GeneralSettingsPage />} />
<Route path="licenses" element={<LicensesSettingsPage />} />
<Route path="licenses/add" element={<AddNewLicensePage />} />
<Route path="security" element={<SecuritySettingsPage />} />
<Route path="appearance" element={<AppearanceSettingsPage />} />
<Route path="network" element={<NetworkSettingsPage />} />
+31
View File
@@ -965,6 +965,37 @@ export const getWorkspaceBuildParameters = async (
)
return response.data
}
type Claims = {
license_expires?: jwt.NumericDate
account_type?: string
account_id?: string
trial: boolean
all_features: boolean
version: number
features: Record<string, number>
require_telemetry?: boolean
}
export type GetLicensesResponse = Omit<TypesGen.License, "claims"> & {
claims: Claims
expires_at: string
}
export const getLicenses = async (): Promise<GetLicensesResponse[]> => {
const response = await axios.get(`/api/v2/licenses`)
return response.data
}
export const createLicense = async (
data: TypesGen.AddLicenseRequest,
): Promise<TypesGen.AddLicenseRequest> => {
const response = await axios.post(`/api/v2/licenses`, data)
return response.data
}
export const removeLicense = async (licenseId: number): Promise<void> => {
await axios.delete(`/api/v2/licenses/${licenseId}`)
}
export class MissingBuildParameters extends Error {
parameters: TypesGen.TemplateVersionParameter[] = []
@@ -1,6 +1,7 @@
import { makeStyles } from "@material-ui/core/styles"
import Brush from "@material-ui/icons/Brush"
import LaunchOutlined from "@material-ui/icons/LaunchOutlined"
import ApprovalIcon from "@material-ui/icons/VerifiedUserOutlined"
import LockRounded from "@material-ui/icons/LockOutlined"
import Globe from "@material-ui/icons/PublicOutlined"
import VpnKeyOutlined from "@material-ui/icons/VpnKeyOutlined"
@@ -48,6 +49,12 @@ export const Sidebar: React.FC = () => {
>
General
</SidebarNavItem>
<SidebarNavItem
href="licenses"
icon={<SidebarNavItemIcon icon={ApprovalIcon} />}
>
Licenses
</SidebarNavItem>
<SidebarNavItem
href="appearance"
icon={<SidebarNavItemIcon icon={Brush} />}
@@ -0,0 +1,178 @@
import { makeStyles } from "@material-ui/core/styles"
import { Stack } from "components/Stack/Stack"
import { FC, DragEvent, useRef, ReactNode } from "react"
import UploadIcon from "@material-ui/icons/CloudUploadOutlined"
import { useClickable } from "hooks/useClickable"
import CircularProgress from "@material-ui/core/CircularProgress"
import { combineClasses } from "utils/combineClasses"
import IconButton from "@material-ui/core/IconButton"
import RemoveIcon from "@material-ui/icons/DeleteOutline"
import FileIcon from "@material-ui/icons/FolderOutlined"
const useFileDrop = (
callback: (file: File) => void,
fileTypeRequired?: string,
): {
onDragOver: (e: DragEvent<HTMLDivElement>) => void
onDrop: (e: DragEvent<HTMLDivElement>) => void
} => {
const onDragOver = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault()
}
const onDrop = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault()
const file = e.dataTransfer.files[0]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- file can be undefined
if (!file) {
return
}
if (fileTypeRequired && file.type !== fileTypeRequired) {
return
}
callback(file)
}
return {
onDragOver,
onDrop,
}
}
export interface FileUploadProps {
isUploading: boolean
onUpload: (file: File) => void
onRemove?: () => void
file?: File
removeLabel: string
title: string
description?: ReactNode
extension?: string
fileTypeRequired?: string
}
export const FileUpload: FC<FileUploadProps> = ({
isUploading,
onUpload,
onRemove,
file,
removeLabel,
title,
description,
extension,
fileTypeRequired,
}) => {
const styles = useStyles()
const inputRef = useRef<HTMLInputElement>(null)
const tarDrop = useFileDrop(onUpload, fileTypeRequired)
const clickable = useClickable(() => {
if (inputRef.current) {
inputRef.current.click()
}
})
if (!isUploading && file) {
return (
<Stack
className={styles.file}
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Stack direction="row" alignItems="center">
<FileIcon />
<span>{file.name}</span>
</Stack>
<IconButton title={removeLabel} size="small" onClick={onRemove}>
<RemoveIcon />
</IconButton>
</Stack>
)
}
return (
<>
<div
className={combineClasses({
[styles.root]: true,
[styles.disabled]: isUploading,
})}
{...clickable}
{...tarDrop}
>
<Stack alignItems="center" spacing={1}>
{isUploading ? (
<CircularProgress size={32} />
) : (
<UploadIcon className={styles.icon} />
)}
<Stack alignItems="center" spacing={0.5}>
<span className={styles.title}>{title}</span>
<span className={styles.description}>{description}</span>
</Stack>
</Stack>
</div>
<input
type="file"
ref={inputRef}
className={styles.input}
accept={extension}
onChange={(event) => {
const file = event.currentTarget.files?.[0]
if (file) {
onUpload(file)
}
}}
/>
</>
)
}
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: theme.shape.borderRadius,
border: `2px dashed ${theme.palette.divider}`,
padding: theme.spacing(6),
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.background.paper,
},
},
disabled: {
pointerEvents: "none",
opacity: 0.75,
},
icon: {
fontSize: theme.spacing(8),
},
title: {
fontSize: theme.spacing(2),
},
description: {
color: theme.palette.text.secondary,
textAlign: "center",
maxWidth: theme.spacing(50),
},
input: {
display: "none",
},
file: {
borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.palette.divider}`,
padding: theme.spacing(2),
background: theme.palette.background.paper,
},
}))
@@ -0,0 +1,156 @@
import Box from "@material-ui/core/Box"
import Button from "@material-ui/core/Button"
import Paper from "@material-ui/core/Paper"
import { makeStyles } from "@material-ui/core/styles"
import { License } from "api/typesGenerated"
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
import { Stack } from "components/Stack/Stack"
import dayjs from "dayjs"
import { useState } from "react"
type LicenseCardProps = {
license: License
userLimitActual?: number
userLimitLimit?: number
onRemove: (licenseId: number) => void
isRemoving: boolean
}
export const LicenseCard = ({
license,
userLimitActual,
userLimitLimit,
onRemove,
isRemoving,
}: LicenseCardProps) => {
const styles = useStyles()
const [licenseIDMarkedForRemoval, setLicenseIDMarkedForRemoval] = useState<
number | undefined
>(undefined)
return (
<Paper
variant="outlined"
key={license.id}
elevation={2}
className={styles.licenseCard}
>
<ConfirmDialog
type="info"
hideCancel={false}
open={licenseIDMarkedForRemoval !== undefined}
onConfirm={() => {
if (!licenseIDMarkedForRemoval) {
return
}
onRemove(licenseIDMarkedForRemoval)
setLicenseIDMarkedForRemoval(undefined)
}}
onClose={() => setLicenseIDMarkedForRemoval(undefined)}
title="Confirm license removal"
confirmLoading={isRemoving}
confirmText="Remove"
description="Are you sure you want to remove this license?"
/>
<Stack
direction="column"
className={styles.cardContent}
justifyContent="space-between"
>
<Box className={styles.licenseId}>
<span>#{license.id}</span>
</Box>
<Stack className={styles.accountType}>
<span>{license.claims.trial ? "Trial" : "Enterprise"}</span>
</Stack>
<Stack
direction="row"
justifyContent="space-between"
alignItems="self-end"
>
<div className={styles.userLimit}>
<span className={styles.userLimitActual}>{userLimitActual}</span>
<span className={styles.userLimitLimit}>
/ {userLimitLimit || "Unlimited"} users
</span>
</div>
<Stack direction="column" spacing={0} alignItems="center">
<span className={styles.expirationDate}>
{dayjs
.unix(license.claims.license_expires)
.format("MMMM D, YYYY")}
</span>
<span className={styles.expirationDateLabel}>Valid until</span>
</Stack>
<div className={styles.actions}>
<Button
className={styles.removeButton}
variant="text"
size="small"
onClick={() => setLicenseIDMarkedForRemoval(license.id)}
>
Remove
</Button>
</div>
</Stack>
</Stack>
</Paper>
)
}
const useStyles = makeStyles((theme) => ({
userLimit: {
width: "33%",
},
actions: {
width: "33%",
textAlign: "right",
},
userLimitActual: {
// fontWeight: 600,
paddingRight: "5px",
color: theme.palette.primary.main,
},
userLimitLimit: {
color: theme.palette.secondary.main,
// fontSize: theme.typography.h5.fontSize,
fontWeight: 600,
},
licenseCard: {
padding: theme.spacing(2),
},
cardContent: {
minHeight: 100,
},
licenseId: {
color: theme.palette.secondary.main,
fontWeight: 600,
// fontSize: theme.typography.h5.fontSize,
},
accountType: {
fontWeight: 600,
fontSize: theme.typography.h4.fontSize,
justifyContent: "center",
alignItems: "center",
textTransform: "capitalize",
},
expirationDate: {
// fontWeight: 600,
color: theme.palette.primary.main,
},
expirationDateLabel: {
color: theme.palette.secondary.main,
},
removeButton: {
height: "17px",
minHeight: "17px",
padding: 0,
border: "none",
color: theme.palette.error.main,
"&:hover": {
backgroundColor: "transparent",
},
},
}))
@@ -1,43 +1,9 @@
import { makeStyles } from "@material-ui/core/styles"
import { Stack } from "components/Stack/Stack"
import { FC, DragEvent, useRef } from "react"
import UploadIcon from "@material-ui/icons/CloudUploadOutlined"
import { useClickable } from "hooks/useClickable"
import CircularProgress from "@material-ui/core/CircularProgress"
import { combineClasses } from "utils/combineClasses"
import IconButton from "@material-ui/core/IconButton"
import RemoveIcon from "@material-ui/icons/DeleteOutline"
import FileIcon from "@material-ui/icons/FolderOutlined"
import { useTranslation } from "react-i18next"
import Link from "@material-ui/core/Link"
import { FileUpload } from "components/FileUpload/FileUpload"
import { FC } from "react"
import { useTranslation } from "react-i18next"
import { Link as RouterLink } from "react-router-dom"
const useTarDrop = (
callback: (file: File) => void,
): {
onDragOver: (e: DragEvent<HTMLDivElement>) => void
onDrop: (e: DragEvent<HTMLDivElement>) => void
} => {
const onDragOver = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault()
}
const onDrop = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault()
const file = e.dataTransfer.files[0]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- file can be undefined
if (!file || file.type !== "application/x-tar") {
return
}
callback(file)
}
return {
onDragOver,
onDrop,
}
}
export interface TemplateUploadProps {
isUploading: boolean
onUpload: (file: File) => void
@@ -51,135 +17,36 @@ export const TemplateUpload: FC<TemplateUploadProps> = ({
onRemove,
file,
}) => {
const styles = useStyles()
const inputRef = useRef<HTMLInputElement>(null)
const tarDrop = useTarDrop(onUpload)
const clickable = useClickable(() => {
if (inputRef.current) {
inputRef.current.click()
}
})
const { t } = useTranslation("createTemplatePage")
if (!isUploading && file) {
return (
<Stack
className={styles.file}
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Stack direction="row" alignItems="center">
<FileIcon />
<span>{file.name}</span>
</Stack>
<IconButton
title={t("form.upload.removeFile")}
size="small"
onClick={onRemove}
>
<RemoveIcon />
</IconButton>
</Stack>
)
}
return (
const description = (
<>
<div
className={combineClasses({
[styles.root]: true,
[styles.disabled]: isUploading,
})}
{...clickable}
{...tarDrop}
>
<Stack alignItems="center" spacing={1}>
{isUploading ? (
<CircularProgress size={32} />
) : (
<UploadIcon className={styles.icon} />
)}
<Stack alignItems="center" spacing={0.5}>
<span className={styles.title}>{t("form.upload.title")}</span>
<span className={styles.description}>
The template has to be a .tar file. You can also use our{" "}
<Link
component={RouterLink}
to="/starter-templates"
// Prevent trigger the upload
onClick={(e) => {
e.stopPropagation()
}}
>
starter templates
</Link>{" "}
to getting started with Coder.
</span>
</Stack>
</Stack>
</div>
<input
type="file"
ref={inputRef}
className={styles.input}
accept=".tar"
onChange={(event) => {
const file = event.currentTarget.files?.[0]
if (file) {
onUpload(file)
}
The template has to be a .tar file. You can also use our{" "}
<Link
component={RouterLink}
to="/starter-templates"
// Prevent trigger the upload
onClick={(e) => {
e.stopPropagation()
}}
/>
>
starter templates
</Link>{" "}
to getting started with Coder.
</>
)
return (
<FileUpload
isUploading={isUploading}
onUpload={onUpload}
onRemove={onRemove}
file={file}
removeLabel={t("form.upload.removeFile")}
title={t("form.upload.title")}
description={description}
extension=".tar"
fileTypeRequired="application/x-tar"
/>
)
}
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: theme.shape.borderRadius,
border: `2px dashed ${theme.palette.divider}`,
padding: theme.spacing(6),
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.background.paper,
},
},
disabled: {
pointerEvents: "none",
opacity: 0.75,
},
icon: {
fontSize: theme.spacing(8),
},
title: {
fontSize: theme.spacing(2),
},
description: {
color: theme.palette.text.secondary,
textAlign: "center",
maxWidth: theme.spacing(50),
},
input: {
display: "none",
},
file: {
borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.palette.divider}`,
padding: theme.spacing(2),
background: theme.palette.background.paper,
},
}))
@@ -0,0 +1,53 @@
import { useMutation } from "@tanstack/react-query"
import { createLicense } from "api/api"
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"
import { FC } from "react"
import { useNavigate } from "react-router-dom"
import { AddNewLicensePageView } from "./AddNewLicensePageView"
import { pageTitle } from "utils/page"
import { Helmet } from "react-helmet-async"
const AddNewLicensePage: FC = () => {
const navigate = useNavigate()
const {
mutate: saveLicenseKeyApi,
isLoading: isCreating,
error: savingLicenseError,
} = useMutation(createLicense, {
onSuccess: () => {
displaySuccess("You have successfully added a license")
navigate("/settings/deployment/licenses?success=true")
},
onError: () => displayError("Failed to save license key"),
})
function saveLicenseKey(licenseKey: string) {
saveLicenseKeyApi(
{ license: licenseKey },
{
onSuccess: () => {
displaySuccess("You have successfully added a license")
navigate("/settings/deployment/licenses?success=true")
},
onError: () => displayError("Failed to save license key"),
},
)
}
return (
<>
<Helmet>
<title>{pageTitle("License Settings")}</title>
</Helmet>
<AddNewLicensePageView
isSavingLicense={isCreating}
savingLicenseError={savingLicenseError}
onSaveLicenseKey={saveLicenseKey}
/>
</>
)
}
export default AddNewLicensePage
@@ -0,0 +1,13 @@
import { AddNewLicensePageView } from "./AddNewLicensePageView"
export default {
title: "pages/AddNewLicensePageView",
component: AddNewLicensePageView,
}
export const Default = {
args: {
isSavingLicense: false,
didSavingFailed: false,
},
}
@@ -0,0 +1,119 @@
import Button from "@material-ui/core/Button"
import TextField from "@material-ui/core/TextField"
import { makeStyles } from "@material-ui/core/styles"
import { ApiErrorResponse } from "api/errors"
import { AlertBanner } from "components/AlertBanner/AlertBanner"
import { Fieldset } from "components/DeploySettingsLayout/Fieldset"
import { Header } from "components/DeploySettingsLayout/Header"
import { FileUpload } from "components/FileUpload/FileUpload"
import { displayError } from "components/GlobalSnackbar/utils"
import { Stack } from "components/Stack/Stack"
import { DividerWithText } from "pages/DeploySettingsPage/LicensesSettingsPage/DividerWithText"
import { FC } from "react"
import { Link as RouterLink } from "react-router-dom"
type AddNewLicenseProps = {
onSaveLicenseKey: (license: string) => void
isSavingLicense: boolean
savingLicenseError?: ApiErrorResponse
}
export const AddNewLicensePageView: FC<AddNewLicenseProps> = ({
onSaveLicenseKey,
isSavingLicense,
savingLicenseError,
}) => {
const styles = useStyles()
function handleFileUploaded(files: File[]) {
const fileReader = new FileReader()
fileReader.onload = () => {
const licenseKey = fileReader.result as string
onSaveLicenseKey(licenseKey)
fileReader.onerror = () => {
displayError("Failed to read file")
}
}
fileReader.readAsText(files[0])
}
const isUploading = false
function onUpload(file: File) {
handleFileUploaded([file])
}
return (
<>
<Stack
alignItems="baseline"
direction="row"
justifyContent="space-between"
>
<Header
title="Add your license"
description="Enterprise licenses unlock more features on your deployment."
/>
<Button
component={RouterLink}
to="/settings/deployment/licenses"
variant="outlined"
>
Back to licenses
</Button>
</Stack>
{savingLicenseError && (
<AlertBanner severity="error" error={savingLicenseError}></AlertBanner>
)}
<FileUpload
isUploading={isUploading}
onUpload={onUpload}
removeLabel="Remove File"
title="Upload your license"
description="Upload a text file containing your license key"
/>
<Stack className={styles.main}>
<DividerWithText>or</DividerWithText>
<Fieldset
title="Paste your license key"
onSubmit={(e) => {
e.preventDefault()
const form = e.target
const formData = new FormData(form as HTMLFormElement)
const licenseKey = formData.get("licenseKey")
onSaveLicenseKey(licenseKey?.toString() || "")
}}
button={
<Button type="submit" disabled={isSavingLicense}>
Add license
</Button>
}
>
<TextField
name="licenseKey"
placeholder="Paste your license key here"
multiline
rows={4}
fullWidth
/>
</Fieldset>
</Stack>
</>
)
}
const useStyles = makeStyles((theme) => ({
main: {
paddingTop: theme.spacing(5),
},
}))
@@ -0,0 +1,32 @@
import { makeStyles } from "@material-ui/core/styles"
import { FC, PropsWithChildren } from "react"
export const DividerWithText: FC<PropsWithChildren> = ({ children }) => {
const classes = useStyles()
return (
<div className={classes.container}>
<div className={classes.border} />
<span className={classes.content}>{children}</span>
<div className={classes.border} />
</div>
)
}
const useStyles = makeStyles((theme) => ({
container: {
display: "flex",
alignItems: "center",
},
border: {
borderBottom: `2px solid ${theme.palette.divider}`,
width: "100%",
},
content: {
paddingTop: theme.spacing(0.5),
paddingBottom: theme.spacing(0.5),
paddingRight: theme.spacing(2),
paddingLeft: theme.spacing(2),
fontSize: theme.typography.h5.fontSize,
color: theme.palette.text.secondary,
},
}))
@@ -0,0 +1,66 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { useMachine } from "@xstate/react"
import { getLicenses, removeLicense } from "api/api"
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"
import { FC, useEffect } from "react"
import { Helmet } from "react-helmet-async"
import { useSearchParams } from "react-router-dom"
import useToggle from "react-use/lib/useToggle"
import { pageTitle } from "utils/page"
import { entitlementsMachine } from "xServices/entitlements/entitlementsXService"
import LicensesSettingsPageView from "./LicensesSettingsPageView"
const LicensesSettingsPage: FC = () => {
const queryClient = useQueryClient()
const [entitlementsState] = useMachine(entitlementsMachine)
const { entitlements } = entitlementsState.context
const [searchParams, setSearchParams] = useSearchParams()
const success = searchParams.get("success")
const [confettiOn, toggleConfettiOn] = useToggle(false)
const { mutate: removeLicenseApi, isLoading: isRemovingLicense } =
useMutation(removeLicense, {
onSuccess: () => {
displaySuccess("Successfully removed license")
void queryClient.invalidateQueries(["licenses"])
},
onError: () => {
displayError("Failed to remove license")
},
})
const { data: licenses, isLoading } = useQuery({
queryKey: ["licenses"],
queryFn: () => getLicenses(),
})
useEffect(() => {
if (success) {
toggleConfettiOn()
const timeout = setTimeout(() => {
toggleConfettiOn(false)
setSearchParams()
}, 2000)
return () => clearTimeout(timeout)
}
}, [setSearchParams, success, toggleConfettiOn])
return (
<>
<Helmet>
<title>{pageTitle("License Settings")}</title>
</Helmet>
<LicensesSettingsPageView
showConfetti={confettiOn}
isLoading={isLoading}
userLimitActual={entitlements?.features.user_limit.actual}
userLimitLimit={entitlements?.features.user_limit.limit}
licenses={licenses}
isRemovingLicense={isRemovingLicense}
removeLicense={(licenseId: number) => removeLicenseApi(licenseId)}
/>
</>
)
}
export default LicensesSettingsPage
@@ -0,0 +1,42 @@
import { GetLicensesResponse } from "api/api"
import LicensesSettingsPageView from "./LicensesSettingsPageView"
export default {
title: "pages/LicensesSettingsPage",
component: LicensesSettingsPageView,
}
const licensesTest: GetLicensesResponse[] = [
{
id: 1,
uploaded_at: "1682346425",
expires_at: "1682346425",
uuid: "1",
claims: {
trial: false,
all_features: true,
version: 1,
features: {},
license_expires: 1682346425,
},
},
]
const defaultArgs = {
showConfetti: false,
isLoading: false,
userLimitActual: 1,
userLimitLimit: 10,
licenses: licensesTest,
}
export const Default = {
args: defaultArgs,
}
export const Empty = {
args: {
...defaultArgs,
licenses: null,
},
}
@@ -0,0 +1,125 @@
import Button from "@material-ui/core/Button"
import { makeStyles, useTheme } from "@material-ui/core/styles"
import Skeleton from "@material-ui/lab/Skeleton"
import { GetLicensesResponse } from "api/api"
import { Header } from "components/DeploySettingsLayout/Header"
import { LicenseCard } from "components/LicenseCard/LicenseCard"
import { Stack } from "components/Stack/Stack"
import { FC } from "react"
import Confetti from "react-confetti"
import { Link } from "react-router-dom"
import useWindowSize from "react-use/lib/useWindowSize"
type Props = {
showConfetti: boolean
isLoading: boolean
userLimitActual?: number
userLimitLimit?: number
licenses?: GetLicensesResponse[]
isRemovingLicense: boolean
removeLicense: (licenseId: number) => void
}
const LicensesSettingsPageView: FC<Props> = ({
showConfetti,
isLoading,
userLimitActual,
userLimitLimit,
licenses,
isRemovingLicense,
removeLicense,
}) => {
const styles = useStyles()
const { width, height } = useWindowSize()
const theme = useTheme()
return (
<>
<Confetti
width={width}
height={height}
numberOfPieces={showConfetti ? 200 : 0}
colors={[theme.palette.primary.main, theme.palette.secondary.main]}
/>
<Stack
alignItems="baseline"
direction="row"
justifyContent="space-between"
>
<Header
title="Licenses"
description="Enterprise licenses unlock more features on your deployment."
/>
<Button
variant="outlined"
component={Link}
to="/settings/deployment/licenses/add"
>
Add new License
</Button>
</Stack>
{isLoading && <Skeleton variant="rect" height={200} />}
{!isLoading && licenses && licenses?.length > 0 && (
<Stack spacing={4}>
{licenses?.map((license) => (
<LicenseCard
key={license.id}
license={license}
userLimitActual={userLimitActual}
userLimitLimit={userLimitLimit}
isRemoving={isRemovingLicense}
onRemove={removeLicense}
/>
))}
</Stack>
)}
{!isLoading && licenses === null && (
<div className={styles.root}>
<Stack alignItems="center" spacing={1}>
<Stack alignItems="center" spacing={0.5}>
<span className={styles.title}>No licenses yet</span>
<span className={styles.description}>
Contact <a href="mailto:sales@coder.com">sales</a> or{" "}
<a href="https://coder.com/trial">request a trial license</a> to
learn more.
</span>
</Stack>
</Stack>
</div>
)}
</>
)
}
const useStyles = makeStyles((theme) => ({
title: {
fontSize: theme.spacing(2),
},
root: {
minHeight: theme.spacing(30),
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.palette.divider}`,
padding: theme.spacing(6),
"&:hover": {
backgroundColor: theme.palette.background.paper,
},
},
description: {
color: theme.palette.text.secondary,
textAlign: "center",
maxWidth: theme.spacing(50),
},
}))
export default LicensesSettingsPageView
+200 -1
View File
@@ -1099,7 +1099,7 @@
core-js-pure "^3.25.1"
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
@@ -3144,6 +3144,11 @@
expect "^29.0.0"
pretty-format "^29.0.0"
"@types/js-cookie@^2.2.6":
version "2.2.7"
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.7.tgz#226a9e31680835a6188e887f3988e60c04d3f6a3"
integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==
"@types/js-levenshtein@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5"
@@ -3760,6 +3765,11 @@
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.6.tgz#8a1524eb5bd5e965c1e3735476f0262469f71440"
integrity sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg==
"@xobotyi/scrollbar-width@^1.9.5":
version "1.9.5"
resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d"
integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
"@xstate/cli@0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@xstate/cli/-/cli-0.3.0.tgz#810faa6319fa11811310b1defdd021c4cda2ec26"
@@ -4938,6 +4948,13 @@ cookie@^0.4.2:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
copy-to-clipboard@^3.3.1:
version "3.3.3"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0"
integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==
dependencies:
toggle-selection "^1.0.6"
core-js-compat@^3.25.1:
version "3.30.0"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.30.0.tgz#99aa2789f6ed2debfa1df3232784126ee97f4d80"
@@ -5029,6 +5046,21 @@ crypto-random-string@^2.0.0:
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
css-in-js-utils@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz#640ae6a33646d401fc720c54fc61c42cd76ae2bb"
integrity sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==
dependencies:
hyphenate-style-name "^1.0.3"
css-tree@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
dependencies:
mdn-data "2.0.14"
source-map "^0.6.1"
css-vendor@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d"
@@ -5074,6 +5106,11 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
csstype@^3.0.6:
version "3.1.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
damerau-levenshtein@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -5479,6 +5516,13 @@ error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
error-stack-parser@^2.0.6:
version "2.1.4"
resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286"
integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==
dependencies:
stackframe "^1.3.4"
es-abstract@^1.19.0, es-abstract@^1.20.4:
version "1.21.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.1.tgz#e6105a099967c08377830a0c9cb589d570dd86c6"
@@ -6102,11 +6146,26 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
fast-loops@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/fast-loops/-/fast-loops-1.1.3.tgz#ce96adb86d07e7bf9b4822ab9c6fac9964981f75"
integrity sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==
fast-safe-stringify@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
fast-shallow-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b"
integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==
fastest-stable-stringify@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz#3757a6774f6ec8de40c4e86ec28ea02417214c76"
integrity sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==
fastq@^1.6.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
@@ -6907,6 +6966,14 @@ inline-style-parser@0.1.1:
resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1"
integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==
inline-style-prefixer@^6.0.0:
version "6.0.4"
resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-6.0.4.tgz#4290ed453ab0e4441583284ad86e41ad88384f44"
integrity sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg==
dependencies:
css-in-js-utils "^3.1.0"
fast-loops "^1.1.3"
inquirer@^8.2.0:
version "8.2.5"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.5.tgz#d8654a7542c35a9b9e069d27e2df4858784d54f8"
@@ -7872,6 +7939,11 @@ jest_workaround@0.1.14:
resolved "https://registry.yarnpkg.com/jest_workaround/-/jest_workaround-0.1.14.tgz#0c82f35d75eeebd9f5ee183887588db44ae61bb6"
integrity sha512-9FqnkYn0mihczDESOMazSIOxbKAZ2HQqE8e12F3CsVNvEJkLBebQj/CT1xqviMOTMESJDYh6buWtsw2/zYUepw==
js-cookie@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
js-levenshtein@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
@@ -8583,6 +8655,11 @@ mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0:
dependencies:
"@types/mdast" "^3.0.0"
mdn-data@2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -9090,6 +9167,20 @@ nan@^2.17.0:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
nano-css@^5.3.1:
version "5.3.5"
resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.3.5.tgz#3075ea29ffdeb0c7cb6d25edb21d8f7fa8e8fe8e"
integrity sha512-vSB9X12bbNu4ALBu7nigJgRViZ6ja3OU7CeuiV1zMIbXOdmkLahgtPmh3GBOlDxbKY0CitqlPdOReGlBLSp+yg==
dependencies:
css-tree "^1.1.2"
csstype "^3.0.6"
fastest-stable-stringify "^2.0.2"
inline-style-prefixer "^6.0.0"
rtl-css-js "^1.14.0"
sourcemap-codec "^1.4.8"
stacktrace-js "^2.0.2"
stylis "^4.0.6"
nanoclone@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"
@@ -9928,6 +10019,13 @@ react-colorful@^5.1.2:
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b"
integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==
react-confetti@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-6.1.0.tgz#03dc4340d955acd10b174dbf301f374a06e29ce6"
integrity sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==
dependencies:
tween-functions "^1.2.0"
react-docgen-typescript@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz#4611055e569edc071204aadb20e1c93e1ab1659c"
@@ -10094,6 +10192,31 @@ react-transition-group@^4.4.0:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-universal-interface@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b"
integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==
react-use@^17.4.0:
version "17.4.0"
resolved "https://registry.yarnpkg.com/react-use/-/react-use-17.4.0.tgz#cefef258b0a6c534a5c8021c2528ac6e1a4cdc6d"
integrity sha512-TgbNTCA33Wl7xzIJegn1HndB4qTS9u03QUwyNycUnXaweZkE4Kq2SB+Yoxx8qbshkZGYBDvUXbXWRUmQDcZZ/Q==
dependencies:
"@types/js-cookie" "^2.2.6"
"@xobotyi/scrollbar-width" "^1.9.5"
copy-to-clipboard "^3.3.1"
fast-deep-equal "^3.1.3"
fast-shallow-equal "^1.0.0"
js-cookie "^2.2.1"
nano-css "^5.3.1"
react-universal-interface "^0.6.2"
resize-observer-polyfill "^1.5.1"
screenfull "^5.1.0"
set-harmonic-interval "^1.0.1"
throttle-debounce "^3.0.1"
ts-easing "^0.2.0"
tslib "^2.1.0"
react-virtualized-auto-sizer@1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz#bfb8414698ad1597912473de3e2e5f82180c1195"
@@ -10348,6 +10471,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resize-observer@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/resize-observer/-/resize-observer-1.0.4.tgz#48beb64602ce408ebd1a433784d64ef76f38d321"
@@ -10472,6 +10600,13 @@ rollup@^3.20.2:
optionalDependencies:
fsevents "~2.3.2"
rtl-css-js@^1.14.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.16.1.tgz#4b48b4354b0ff917a30488d95100fbf7219a3e80"
integrity sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==
dependencies:
"@babel/runtime" "^7.1.2"
run-async@^2.4.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
@@ -10548,6 +10683,11 @@ scheduler@^0.23.0:
dependencies:
loose-envify "^1.1.0"
screenfull@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba"
integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==
"semver@2 || 3 || 4 || 5", semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
@@ -10634,6 +10774,11 @@ set-cookie-parser@^2.4.6:
resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.5.1.tgz#ddd3e9a566b0e8e0862aca974a6ac0e01349430b"
integrity sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==
set-harmonic-interval@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz#e1773705539cdfb80ce1c3d99e7f298bb3995249"
integrity sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==
setprototypeof@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
@@ -10841,6 +10986,13 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
stack-generator@^2.0.5:
version "2.0.10"
resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.10.tgz#8ae171e985ed62287d4f1ed55a1633b3fb53bb4d"
integrity sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==
dependencies:
stackframe "^1.3.4"
stack-utils@^2.0.3:
version "2.0.6"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f"
@@ -10848,6 +11000,28 @@ stack-utils@^2.0.3:
dependencies:
escape-string-regexp "^2.0.0"
stackframe@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310"
integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==
stacktrace-gps@^3.0.4:
version "3.1.2"
resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz#0c40b24a9b119b20da4525c398795338966a2fb0"
integrity sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==
dependencies:
source-map "0.5.6"
stackframe "^1.3.4"
stacktrace-js@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.2.tgz#4ca93ea9f494752d55709a081d400fdaebee897b"
integrity sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==
dependencies:
error-stack-parser "^2.0.6"
stack-generator "^2.0.5"
stacktrace-gps "^3.0.4"
state-local@^1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5"
@@ -11028,6 +11202,11 @@ style-to-object@^0.3.0:
dependencies:
inline-style-parser "0.1.1"
stylis@^4.0.6:
version "4.1.3"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7"
integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==
supports-color@^5.3.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -11173,6 +11352,11 @@ throat@^5.0.0:
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
throttle-debounce@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb"
integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==
through2@^2.0.3:
version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
@@ -11228,6 +11412,11 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
toggle-selection@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
toidentifier@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
@@ -11285,6 +11474,11 @@ ts-dedent@^2.0.0, ts-dedent@^2.2.0:
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==
ts-easing@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec"
integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==
ts-morph@^13.0.1:
version "13.0.3"
resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-13.0.3.tgz#c0c51d1273ae2edb46d76f65161eb9d763444c1d"
@@ -11337,6 +11531,11 @@ tunnel@^0.0.6:
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
tween-functions@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff"
integrity sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"