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 + +
+ +
+
+
+
+ ) +} + +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 ( + <> + +
+ + + + {savingLicenseError && ( + + )} + + + + + or + +
{ + e.preventDefault() + + const form = e.target + const formData = new FormData(form as HTMLFormElement) + + const licenseKey = formData.get("licenseKey") + + onSaveLicenseKey(licenseKey?.toString() || "") + }} + button={ + + } + > + +
+
+ + ) +} + +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 ( + <> + + +
+ + + + + {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" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy