diff --git a/enterprise/coderd/license/license.go b/enterprise/coderd/license/license.go index d29dad402e613..1d9b6f8e20945 100644 --- a/enterprise/coderd/license/license.go +++ b/enterprise/coderd/license/license.go @@ -52,6 +52,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. diff --git a/enterprise/coderd/license/license_test.go b/enterprise/coderd/license/license_test.go index 9cd56c67875a3..d21d6f1e53558 100644 --- a/enterprise/coderd/license/license_test.go +++ b/enterprise/coderd/license/license_test.go @@ -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() diff --git a/site/package.json b/site/package.json index 59bb1b5bf6452..718d8a69f552d 100644 --- a/site/package.json +++ b/site/package.json @@ -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", diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 235061621f711..b86be4352f342 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -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 ( }> @@ -244,6 +255,8 @@ export const AppRouter: FC = () => { element={} > } /> + } /> + } /> } /> } /> } /> diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 206a62c452afc..ba39249cdcc3a 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -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 + require_telemetry?: boolean +} + +export type GetLicensesResponse = Omit & { + claims: Claims + expires_at: string +} + +export const getLicenses = async (): Promise => { + const response = await axios.get(`/api/v2/licenses`) + return response.data +} + +export const createLicense = async ( + data: TypesGen.AddLicenseRequest, +): Promise => { + const response = await axios.post(`/api/v2/licenses`, data) + return response.data +} + +export const removeLicense = async (licenseId: number): Promise => { + await axios.delete(`/api/v2/licenses/${licenseId}`) +} export class MissingBuildParameters extends Error { parameters: TypesGen.TemplateVersionParameter[] = [] diff --git a/site/src/components/DeploySettingsLayout/Sidebar.tsx b/site/src/components/DeploySettingsLayout/Sidebar.tsx index b9d3735ae1c51..ea081f943eb9b 100644 --- a/site/src/components/DeploySettingsLayout/Sidebar.tsx +++ b/site/src/components/DeploySettingsLayout/Sidebar.tsx @@ -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 + } + > + Licenses + } diff --git a/site/src/components/FileUpload/FileUpload.tsx b/site/src/components/FileUpload/FileUpload.tsx new file mode 100644 index 0000000000000..229aa2af02c9d --- /dev/null +++ b/site/src/components/FileUpload/FileUpload.tsx @@ -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) => void + onDrop: (e: DragEvent) => void +} => { + const onDragOver = (e: DragEvent) => { + e.preventDefault() + } + + const onDrop = (e: DragEvent) => { + 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 = ({ + isUploading, + onUpload, + onRemove, + file, + removeLabel, + title, + description, + extension, + fileTypeRequired, +}) => { + const styles = useStyles() + const inputRef = useRef(null) + const tarDrop = useFileDrop(onUpload, fileTypeRequired) + const clickable = useClickable(() => { + if (inputRef.current) { + inputRef.current.click() + } + }) + + if (!isUploading && file) { + return ( + + + + {file.name} + + + + + + + ) + } + + return ( + <> + + + {isUploading ? ( + + ) : ( + + )} + + + {title} + {description} + + + + + { + 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, + }, +})) diff --git a/site/src/components/LicenseCard/LicenseCard.tsx b/site/src/components/LicenseCard/LicenseCard.tsx new file mode 100644 index 0000000000000..bb484121d532c --- /dev/null +++ b/site/src/components/LicenseCard/LicenseCard.tsx @@ -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 ( + + { + 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?" + /> + + + #{license.id} + + + {license.claims.trial ? "Trial" : "Enterprise"} + + + + {userLimitActual} + + / {userLimitLimit || "Unlimited"} users + + + + + + {dayjs + .unix(license.claims.license_expires) + .format("MMMM D, YYYY")} + + Valid until + + + setLicenseIDMarkedForRemoval(license.id)} + > + Remove + + + + + + ) +} + +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", + }, + }, +})) diff --git a/site/src/pages/CreateTemplatePage/TemplateUpload.tsx b/site/src/pages/CreateTemplatePage/TemplateUpload.tsx index 4212bb057881f..7d56662e4eeae 100644 --- a/site/src/pages/CreateTemplatePage/TemplateUpload.tsx +++ b/site/src/pages/CreateTemplatePage/TemplateUpload.tsx @@ -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) => void - onDrop: (e: DragEvent) => void -} => { - const onDragOver = (e: DragEvent) => { - e.preventDefault() - } - - const onDrop = (e: DragEvent) => { - 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 = ({ onRemove, file, }) => { - const styles = useStyles() - const inputRef = useRef(null) - const tarDrop = useTarDrop(onUpload) - const clickable = useClickable(() => { - if (inputRef.current) { - inputRef.current.click() - } - }) const { t } = useTranslation("createTemplatePage") - if (!isUploading && file) { - return ( - - - - {file.name} - - - - - - - ) - } - - return ( + const description = ( <> - - - {isUploading ? ( - - ) : ( - - )} - - - {t("form.upload.title")} - - The template has to be a .tar file. You can also use our{" "} - { - e.stopPropagation() - }} - > - starter templates - {" "} - to getting started with Coder. - - - - - - { - const file = event.currentTarget.files?.[0] - if (file) { - onUpload(file) - } + The template has to be a .tar file. You can also use our{" "} + { + e.stopPropagation() }} - /> + > + starter templates + {" "} + to getting started with Coder. > ) -} - -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, - }, -})) + return ( + + ) +} diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx new file mode 100644 index 0000000000000..7c75dca3713c0 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx @@ -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 ( + <> + + {pageTitle("License Settings")} + + + + > + ) +} + +export default AddNewLicensePage diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.stories.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.stories.tsx new file mode 100644 index 0000000000000..1889668fdfcd0 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.stories.tsx @@ -0,0 +1,13 @@ +import { AddNewLicensePageView } from "./AddNewLicensePageView" + +export default { + title: "pages/AddNewLicensePageView", + component: AddNewLicensePageView, +} + +export const Default = { + args: { + isSavingLicense: false, + didSavingFailed: false, + }, +} diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.tsx new file mode 100644 index 0000000000000..9d2266b816767 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.tsx @@ -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 = ({ + 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 ( + <> + + + + Back to licenses + + + + {savingLicenseError && ( + + )} + + + + + or + + { + e.preventDefault() + + const form = e.target + const formData = new FormData(form as HTMLFormElement) + + const licenseKey = formData.get("licenseKey") + + onSaveLicenseKey(licenseKey?.toString() || "") + }} + button={ + + Add license + + } + > + + + + > + ) +} + +const useStyles = makeStyles((theme) => ({ + main: { + paddingTop: theme.spacing(5), + }, +})) diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/DividerWithText.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/DividerWithText.tsx new file mode 100644 index 0000000000000..a246163f20591 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/DividerWithText.tsx @@ -0,0 +1,32 @@ +import { makeStyles } from "@material-ui/core/styles" +import { FC, PropsWithChildren } from "react" + +export const DividerWithText: FC = ({ children }) => { + const classes = useStyles() + return ( + + + {children} + + + ) +} + +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, + }, +})) diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx new file mode 100644 index 0000000000000..2a8ae72b396e4 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx @@ -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 ( + <> + + {pageTitle("License Settings")} + + removeLicenseApi(licenseId)} + /> + > + ) +} + +export default LicensesSettingsPage diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.stories.tsx new file mode 100644 index 0000000000000..1a43ab5b03613 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.stories.tsx @@ -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, + }, +} diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx new file mode 100644 index 0000000000000..70f625513443e --- /dev/null +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx @@ -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 = ({ + showConfetti, + isLoading, + userLimitActual, + userLimitLimit, + licenses, + isRemovingLicense, + removeLicense, +}) => { + const styles = useStyles() + const { width, height } = useWindowSize() + + const theme = useTheme() + + return ( + <> + + + + + + Add new License + + + + {isLoading && } + + {!isLoading && licenses && licenses?.length > 0 && ( + + {licenses?.map((license) => ( + + ))} + + )} + + {!isLoading && licenses === null && ( + + + + No licenses yet + + Contact sales or{" "} + request a trial license to + learn more. + + + + + )} + > + ) +} + +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 diff --git a/site/yarn.lock b/site/yarn.lock index 981397b6ad1ac..f580b5c1a9f68 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -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"