From 988c9af0153561397686c119da9d1336d2433fdd Mon Sep 17 00:00:00 2001 From: Kayla Washburn Date: Wed, 6 Sep 2023 12:59:26 -0600 Subject: [PATCH] chore: format code with semicolons when using prettier (#9555) --- .prettierrc.yaml | 3 +- docs/contributing/frontend.md | 18 +- offlinedocs/next.config.js | 4 +- offlinedocs/pages/[[...slug]].tsx | 245 +-- offlinedocs/pages/_app.tsx | 14 +- site/.prettierrc.yaml | 3 +- site/.storybook/main.js | 12 +- site/.storybook/preview.jsx | 24 +- site/e2e/constants.ts | 14 +- site/e2e/global.setup.ts | 26 +- site/e2e/helpers.ts | 422 ++-- site/e2e/hooks.ts | 40 +- site/e2e/parameters.ts | 22 +- site/e2e/playwright.config.ts | 18 +- site/e2e/pom/BasePom.ts | 16 +- site/e2e/pom/SignInPage.ts | 12 +- site/e2e/pom/WorkspacesPage.ts | 6 +- site/e2e/pom/index.ts | 4 +- site/e2e/provisionerGenerated.ts | 649 +++--- site/e2e/reporter.ts | 42 +- site/e2e/tests/app.spec.ts | 50 +- site/e2e/tests/createWorkspace.spec.ts | 62 +- site/e2e/tests/gitAuth.spec.ts | 90 +- site/e2e/tests/listTemplates.spec.ts | 12 +- site/e2e/tests/outdatedAgent.spec.ts | 44 +- site/e2e/tests/outdatedCLI.spec.ts | 44 +- site/e2e/tests/restartWorkspace.spec.ts | 28 +- site/e2e/tests/startWorkspace.spec.ts | 26 +- site/e2e/tests/updateWorkspace.spec.ts | 66 +- site/e2e/tests/webTerminal.spec.ts | 42 +- site/jest-runner-eslint.config.js | 4 +- site/jest.config.ts | 2 +- site/jest.setup.ts | 62 +- site/src/@types/emoji-mart.d.ts | 10 +- site/src/@types/eventsourcemock.d.ts | 2 +- site/src/@types/i18n.d.ts | 8 +- site/src/@types/mui.d.ts | 10 +- site/src/AppRouter.tsx | 132 +- site/src/Main.tsx | 28 +- site/src/__mocks__/js-untar.ts | 2 +- site/src/__mocks__/monaco-editor.ts | 10 +- site/src/__mocks__/react-markdown.tsx | 8 +- site/src/api/api.test.ts | 162 +- site/src/api/api.ts | 984 ++++----- site/src/api/errors.test.ts | 38 +- site/src/api/errors.ts | 50 +- site/src/api/types.ts | 48 +- site/src/api/typesGenerated.ts | 1840 +++++++++-------- site/src/app.tsx | 32 +- site/src/components/Alert/Alert.stories.tsx | 28 +- site/src/components/Alert/Alert.tsx | 34 +- .../components/Alert/ErrorAlert.stories.tsx | 34 +- site/src/components/Alert/ErrorAlert.tsx | 18 +- .../components/AuthProvider/AuthProvider.tsx | 30 +- site/src/components/Avatar/Avatar.stories.tsx | 48 +- site/src/components/Avatar/Avatar.tsx | 32 +- .../src/components/Avatar/firstLetter.test.ts | 8 +- site/src/components/Avatar/firstLetter.ts | 6 +- .../AvatarData/AvatarData.stories.tsx | 16 +- site/src/components/AvatarData/AvatarData.tsx | 26 +- .../AvatarData/AvatarDataSkeleton.tsx | 10 +- .../components/BuildAvatar/BuildAvatar.tsx | 34 +- site/src/components/BuildIcon/BuildIcon.tsx | 22 +- .../CodeExample/CodeExample.stories.tsx | 18 +- .../CodeExample/CodeExample.test.tsx | 14 +- .../components/CodeExample/CodeExample.tsx | 36 +- .../Conditionals/ChooseOne.stories.tsx | 16 +- .../src/components/Conditionals/ChooseOne.tsx | 26 +- .../components/Conditionals/Maybe.stories.tsx | 16 +- site/src/components/Conditionals/Maybe.tsx | 8 +- site/src/components/CopyButton/CopyButton.tsx | 36 +- .../CopyableValue/CopyableValue.tsx | 26 +- site/src/components/DAUChart/DAUChart.tsx | 52 +- .../Dashboard/DashboardLayout.test.tsx | 16 +- .../components/Dashboard/DashboardLayout.tsx | 58 +- .../Dashboard/DashboardProvider.tsx | 86 +- .../DeploymentBanner/DeploymentBanner.tsx | 18 +- .../DeploymentBannerView.stories.tsx | 14 +- .../DeploymentBanner/DeploymentBannerView.tsx | 130 +- .../src/components/Dashboard/HealthBanner.tsx | 26 +- .../Dashboard/LicenseBanner/LicenseBanner.tsx | 14 +- .../LicenseBannerView.stories.tsx | 20 +- .../LicenseBanner/LicenseBannerView.tsx | 36 +- .../Dashboard/Navbar/Navbar.test.tsx | 56 +- .../components/Dashboard/Navbar/Navbar.tsx | 42 +- .../Dashboard/Navbar/NavbarView.stories.tsx | 30 +- .../Dashboard/Navbar/NavbarView.test.tsx | 96 +- .../Dashboard/Navbar/NavbarView.tsx | 168 +- .../BorderedMenu/BorderedMenu.tsx | 20 +- .../BorderedMenu/BorderedMenuRow.tsx | 38 +- .../UserDropdown/UserDropdown.stories.tsx | 18 +- .../Navbar/UserDropdown/UserDropdown.test.tsx | 36 +- .../Navbar/UserDropdown/UserDropdown.tsx | 50 +- .../UserDropdownContent.stories.tsx | 22 +- .../UserDropdownContent.test.tsx | 60 +- .../UserDropdownContent.tsx | 52 +- .../Dashboard/ServiceBanner/ServiceBanner.tsx | 16 +- .../ServiceBannerView.stories.tsx | 16 +- .../ServiceBanner/ServiceBannerView.tsx | 34 +- .../DeploySettingsLayout/Badges.tsx | 72 +- .../DeploySettingsLayout.tsx | 60 +- .../DeploySettingsLayout/Fieldset.tsx | 28 +- .../DeploySettingsLayout/Header.tsx | 26 +- .../DeploySettingsLayout/Option.tsx | 46 +- .../DeploySettingsLayout/Options.test.tsx | 16 +- .../DeploySettingsLayout/OptionsTable.tsx | 50 +- .../DeploySettingsLayout/Sidebar.tsx | 52 +- .../ConfirmDialog/ConfirmDialog.stories.tsx | 30 +- .../ConfirmDialog/ConfirmDialog.test.tsx | 86 +- .../Dialogs/ConfirmDialog/ConfirmDialog.tsx | 81 +- .../DeleteDialog/DeleteDialog.stories.tsx | 14 +- .../DeleteDialog/DeleteDialog.test.tsx | 52 +- .../Dialogs/DeleteDialog/DeleteDialog.tsx | 52 +- site/src/components/Dialogs/Dialog.tsx | 50 +- site/src/components/Dialogs/types.ts | 2 +- .../DropdownArrows/DropdownArrows.tsx | 28 +- .../components/EmptyState/EmptyState.test.tsx | 32 +- site/src/components/EmptyState/EmptyState.tsx | 30 +- .../ErrorBoundary/ErrorBoundary.tsx | 18 +- .../RuntimeErrorState.stories.tsx | 16 +- .../RuntimeErrorState/RuntimeErrorState.tsx | 72 +- .../components/Expander/Expander.stories.tsx | 16 +- site/src/components/Expander/Expander.tsx | 30 +- site/src/components/FileUpload/FileUpload.tsx | 84 +- site/src/components/Filter/UserFilter.tsx | 60 +- site/src/components/Filter/filter.tsx | 282 +-- site/src/components/Filter/menu.ts | 72 +- site/src/components/Filter/options.ts | 6 +- site/src/components/Filter/storyHelpers.ts | 16 +- site/src/components/Form/Form.tsx | 70 +- .../FormFooter/FormFooter.stories.tsx | 20 +- site/src/components/FormFooter/FormFooter.tsx | 30 +- .../FullPageForm/FullPageForm.stories.tsx | 22 +- .../components/FullPageForm/FullPageForm.tsx | 20 +- .../FullPageForm/FullPageHorizontalForm.tsx | 18 +- .../EnterpriseSnackbar.stories.tsx | 20 +- .../EnterpriseSnackbar/EnterpriseSnackbar.tsx | 26 +- .../GlobalSnackbar/GlobalSnackbar.tsx | 56 +- .../components/GlobalSnackbar/utils.test.ts | 88 +- site/src/components/GlobalSnackbar/utils.ts | 42 +- .../GroupAvatar/GroupAvatar.stories.tsx | 12 +- .../components/GroupAvatar/GroupAvatar.tsx | 22 +- .../HelpTooltip/HelpTooltip.stories.tsx | 14 +- .../components/HelpTooltip/HelpTooltip.tsx | 130 +- site/src/components/HelpTooltip/index.ts | 2 +- site/src/components/IconField/IconField.tsx | 56 +- .../components/IconField/LazyIconField.tsx | 10 +- site/src/components/IconField/types.ts | 6 +- site/src/components/Icons/AzureDevOpsIcon.tsx | 4 +- site/src/components/Icons/BitbucketIcon.tsx | 4 +- site/src/components/Icons/BuildingIcon.tsx | 4 +- site/src/components/Icons/CloseIcon.tsx | 4 +- site/src/components/Icons/CoderIcon.tsx | 4 +- site/src/components/Icons/DockerIcon.tsx | 4 +- site/src/components/Icons/EditSquare.tsx | 4 +- site/src/components/Icons/ErrorIcon.tsx | 4 +- site/src/components/Icons/FileCopyIcon.tsx | 4 +- site/src/components/Icons/GitIcon.tsx | 4 +- site/src/components/Icons/GitlabIcon.tsx | 4 +- site/src/components/Icons/MarkdownIcon.tsx | 4 +- site/src/components/Icons/RocketIcon.tsx | 4 +- site/src/components/Icons/TerminalIcon.tsx | 4 +- site/src/components/Icons/TerraformIcon.tsx | 4 +- .../components/Icons/UsersOutlinedIcon.tsx | 4 +- site/src/components/Icons/VSCodeIcon.tsx | 4 +- .../components/Icons/VSCodeInsidersIcon.tsx | 4 +- .../components/Loader/FullScreenLoader.tsx | 14 +- site/src/components/Loader/Loader.tsx | 10 +- .../LoadingButton/LoadingButton.stories.tsx | 16 +- .../LoadingButton/LoadingButton.tsx | 10 +- .../components/Margins/Margins.stories.tsx | 10 +- site/src/components/Margins/Margins.tsx | 20 +- .../components/Markdown/Markdown.stories.tsx | 16 +- site/src/components/Markdown/Markdown.tsx | 64 +- .../PageHeader/FullWidthPageHeader.tsx | 32 +- .../PageHeader/PageHeader.stories.tsx | 14 +- site/src/components/PageHeader/PageHeader.tsx | 38 +- .../PaginationWidget/PageButton.tsx | 24 +- .../PaginationWidget.stories.tsx | 28 +- .../PaginationWidget.test.tsx | 46 +- .../PaginationWidget/PaginationWidget.tsx | 62 +- .../PaginationWidget/PaginationWidgetBase.tsx | 54 +- .../components/PaginationWidget/utils.test.ts | 34 +- site/src/components/PaginationWidget/utils.ts | 88 +- site/src/components/Paywall/Paywall.tsx | 28 +- site/src/components/Pill/Pill.stories.tsx | 40 +- site/src/components/Pill/Pill.tsx | 32 +- .../ProxyStatusLatency/ProxyStatusLatency.tsx | 30 +- .../RequireAuth/RequireAuth.test.tsx | 24 +- .../components/RequireAuth/RequireAuth.tsx | 46 +- .../RequirePermission/RequirePermission.tsx | 14 +- site/src/components/Resources/AgentButton.tsx | 12 +- .../src/components/Resources/AgentLatency.tsx | 44 +- .../Resources/AgentMetadata.stories.tsx | 18 +- .../components/Resources/AgentMetadata.tsx | 144 +- .../Resources/AgentOutdatedTooltip.tsx | 32 +- .../components/Resources/AgentRow.stories.tsx | 104 +- site/src/components/Resources/AgentRow.tsx | 159 +- .../Resources/AgentRowPreview.stories.tsx | 22 +- .../components/Resources/AgentRowPreview.tsx | 30 +- site/src/components/Resources/AgentStatus.tsx | 154 +- .../src/components/Resources/AgentVersion.tsx | 34 +- .../Resources/AppLink/AppLink.stories.tsx | 50 +- .../components/Resources/AppLink/AppLink.tsx | 100 +- .../Resources/AppLink/AppPreviewLink.tsx | 22 +- .../components/Resources/AppLink/BaseIcon.tsx | 10 +- .../Resources/AppLink/ShareIcon.tsx | 26 +- .../Resources/PortForwardButton.stories.tsx | 18 +- .../Resources/PortForwardButton.tsx | 85 +- .../Resources/ResourceAvatar.stories.tsx | 34 +- .../components/Resources/ResourceAvatar.tsx | 30 +- .../Resources/ResourceCard.stories.tsx | 36 +- .../src/components/Resources/ResourceCard.tsx | 44 +- site/src/components/Resources/Resources.tsx | 36 +- .../Resources/SSHButton/SSHButton.stories.tsx | 18 +- .../Resources/SSHButton/SSHButton.tsx | 44 +- .../components/Resources/SensitiveValue.tsx | 34 +- .../TerminalLink/TerminalLink.stories.tsx | 14 +- .../Resources/TerminalLink/TerminalLink.tsx | 28 +- .../VSCodeDesktopButton.stories.tsx | 14 +- .../VSCodeDesktopButton.tsx | 116 +- .../MultiTextField/MultiTextField.stories.tsx | 18 +- .../MultiTextField/MultiTextField.tsx | 54 +- .../RichParameterInput.stories.tsx | 50 +- .../RichParameterInput/RichParameterInput.tsx | 86 +- .../src/components/SettingsLayout/Section.tsx | 40 +- .../SettingsLayout/SectionAction.tsx | 12 +- .../SettingsLayout/SettingsLayout.tsx | 30 +- .../src/components/SettingsLayout/Sidebar.tsx | 42 +- site/src/components/Sidebar/Sidebar.tsx | 12 +- .../components/SignInLayout/SignInLayout.tsx | 12 +- site/src/components/Stack/Stack.stories.tsx | 14 +- site/src/components/Stack/Stack.tsx | 38 +- .../SyntaxHighlighter/SyntaxHighlighter.tsx | 36 +- .../SyntaxHighlighter/coderTheme.ts | 32 +- site/src/components/TableEmpty/TableEmpty.tsx | 14 +- .../components/TableLoader/TableLoader.tsx | 32 +- .../TableRowMenu/TableRowMenu.stories.tsx | 16 +- .../components/TableRowMenu/TableRowMenu.tsx | 38 +- .../components/TableToolbar/TableToolbar.tsx | 32 +- .../TemplateExampleCard.tsx | 22 +- .../TemplateFiles/TemplateFiles.tsx | 64 +- .../TemplateLayout/TemplateLayout.tsx | 74 +- .../TemplatePageHeader.stories.tsx | 18 +- .../TemplateLayout/TemplatePageHeader.tsx | 86 +- .../TemplateLayout/deleteTemplate.test.ts | 50 +- .../TemplateLayout/deleteTemplate.ts | 40 +- .../TemplateParameters/TemplateParameters.tsx | 26 +- .../TemplateResourcesTable.tsx | 14 +- .../TemplateVersionWarnings.stories.tsx | 12 +- .../TemplateVersionWarnings.tsx | 16 +- site/src/components/Timeline/Timeline.tsx | 36 +- .../components/Timeline/TimelineDateRow.tsx | 20 +- .../src/components/Timeline/TimelineEntry.tsx | 18 +- site/src/components/Timeline/utils.test.ts | 12 +- site/src/components/Timeline/utils.ts | 12 +- .../Typography/Typography.stories.tsx | 16 +- site/src/components/Typography/Typography.tsx | 22 +- .../UserAutocomplete.stories.tsx | 18 +- .../UserAutocomplete/UserAutocomplete.tsx | 70 +- site/src/components/UserAvatar/UserAvatar.tsx | 14 +- .../UserOrGroupAutocomplete.tsx | 80 +- .../components/UsersLayout/UsersLayout.tsx | 46 +- site/src/components/Welcome/Welcome.tsx | 18 +- .../WorkspaceBuildLogs/Logs/Logs.stories.tsx | 22 +- .../WorkspaceBuildLogs/Logs/Logs.tsx | 62 +- .../WorkspaceBuildLogs.stories.tsx | 16 +- .../WorkspaceBuildLogs.test.ts | 14 +- .../WorkspaceBuildLogs/WorkspaceBuildLogs.tsx | 76 +- .../ImpendingDeletionBanner.tsx | 48 +- .../ImpendingDeletionStat.tsx | 30 +- .../ImpendingDeletionText.tsx | 26 +- .../src/components/WorkspaceDeletion/index.ts | 6 +- .../WorkspaceDeletion/utils.test.ts | 16 +- .../src/components/WorkspaceDeletion/utils.ts | 12 +- .../WorkspaceOutdatedTooltip.tsx | 50 +- .../WorkspaceStatusBadge.stories.tsx | 54 +- .../WorkspaceStatusBadge.tsx | 48 +- site/src/contexts/ProxyContext.test.tsx | 128 +- site/src/contexts/ProxyContext.tsx | 166 +- site/src/contexts/useProxyLatency.ts | 189 +- site/src/hooks/events.test.ts | 24 +- site/src/hooks/events.ts | 18 +- site/src/hooks/index.ts | 20 +- site/src/hooks/useClickable.ts | 16 +- site/src/hooks/useClickableTableRow.ts | 18 +- site/src/hooks/useClipboard.ts | 44 +- site/src/hooks/useFeatureVisibility.ts | 12 +- site/src/hooks/useLocalStorage.ts | 16 +- site/src/hooks/useMe.ts | 16 +- site/src/hooks/useOrganizationId.ts | 14 +- site/src/hooks/usePagination.ts | 22 +- site/src/hooks/usePermissions.ts | 14 +- site/src/hooks/useTab.ts | 18 +- site/src/i18n/en/index.ts | 46 +- site/src/i18n/i18n.ts | 16 +- site/src/i18n/index.ts | 2 +- site/src/pages/404Page/404Page.tsx | 16 +- site/src/pages/AuditPage/AuditFilter.tsx | 68 +- site/src/pages/AuditPage/AuditHelpTooltip.tsx | 12 +- .../AuditLogDescription.test.tsx | 76 +- .../AuditLogDescription.tsx | 32 +- .../BuildAuditDescription.tsx | 32 +- .../AuditLogRow/AuditLogDescription/index.ts | 2 +- .../AuditLogRow/AuditLogDiff/AuditLogDiff.tsx | 34 +- .../AuditLogDiff/auditUtils.test.ts | 34 +- .../AuditLogRow/AuditLogDiff/auditUtils.ts | 10 +- .../AuditLogRow/AuditLogDiff/index.ts | 4 +- .../AuditLogRow/AuditLogRow.stories.tsx | 54 +- .../AuditPage/AuditLogRow/AuditLogRow.tsx | 72 +- site/src/pages/AuditPage/AuditPage.test.tsx | 102 +- site/src/pages/AuditPage/AuditPage.tsx | 56 +- .../pages/AuditPage/AuditPageView.stories.tsx | 37 +- site/src/pages/AuditPage/AuditPageView.tsx | 72 +- site/src/pages/AuditPage/AuditPaywall.tsx | 22 +- site/src/pages/CliAuthPage/CliAuthPage.tsx | 26 +- .../CliAuthPage/CliAuthPageView.stories.tsx | 12 +- .../src/pages/CliAuthPage/CliAuthPageView.tsx | 28 +- .../CreateTemplateForm.stories.tsx | 26 +- .../CreateTemplatePage/CreateTemplateForm.tsx | 172 +- .../CreateTemplatePage.test.tsx | 86 +- .../CreateTemplatePage/CreateTemplatePage.tsx | 72 +- .../CreateTemplatePage/TemplateUpload.tsx | 28 +- .../CreateTemplatePage/VariableInput.tsx | 52 +- .../pages/CreateTokenPage/CreateTokenForm.tsx | 66 +- .../CreateTokenPage/CreateTokenPage.test.tsx | 28 +- .../pages/CreateTokenPage/CreateTokenPage.tsx | 76 +- site/src/pages/CreateTokenPage/utils.test.tsx | 24 +- site/src/pages/CreateTokenPage/utils.ts | 34 +- .../CreateUserPage/CreateUserForm.stories.tsx | 22 +- .../pages/CreateUserPage/CreateUserForm.tsx | 74 +- .../CreateUserPage/CreateUserPage.test.tsx | 90 +- .../pages/CreateUserPage/CreateUserPage.tsx | 48 +- .../CreateWorkspacePage.test.tsx | 180 +- .../CreateWorkspacePage.tsx | 88 +- .../CreateWorkspacePageView.stories.tsx | 20 +- .../CreateWorkspacePageView.tsx | 129 +- .../CreateWorkspacePage/GitAuth.stories.tsx | 40 +- .../src/pages/CreateWorkspacePage/GitAuth.tsx | 74 +- .../SelectedTemplate.stories.tsx | 18 +- .../CreateWorkspacePage/SelectedTemplate.tsx | 20 +- .../AppearanceSettingsPage.tsx | 32 +- .../AppearanceSettingsPageView.stories.tsx | 12 +- .../AppearanceSettingsPageView.tsx | 86 +- .../GeneralSettingsPage/ChartSection.tsx | 22 +- .../GeneralSettingsPage.tsx | 18 +- .../GeneralSettingsPageView.stories.tsx | 18 +- .../GeneralSettingsPageView.tsx | 34 +- .../GitAuthSettingsPage.tsx | 18 +- .../GitAuthSettingsPageView.stories.tsx | 10 +- .../GitAuthSettingsPageView.tsx | 40 +- .../AddNewLicensePage.tsx | 36 +- .../AddNewLicensePageView.stories.tsx | 6 +- .../AddNewLicensePageView.tsx | 70 +- .../LicensesSettingsPage/DividerWithText.tsx | 12 +- .../LicensesSettingsPage/LicenseCard.test.tsx | 40 +- .../LicensesSettingsPage/LicenseCard.tsx | 50 +- .../LicensesSettingsPage.tsx | 70 +- .../LicensesSettingsPageView.stories.tsx | 12 +- .../LicensesSettingsPageView.tsx | 66 +- .../NetworkSettingsPage.tsx | 18 +- .../NetworkSettingsPageView.stories.tsx | 10 +- .../NetworkSettingsPageView.tsx | 20 +- .../SecuritySettingsPage.tsx | 22 +- .../SecuritySettingsPageView.stories.tsx | 16 +- .../SecuritySettingsPageView.tsx | 28 +- .../UserAuthSettingsPage.tsx | 18 +- .../UserAuthSettingsPageView.stories.tsx | 10 +- .../UserAuthSettingsPageView.tsx | 26 +- site/src/pages/GitAuthPage/GitAuthPage.tsx | 62 +- .../GitAuthPage/GitAuthPageView.stories.tsx | 28 +- .../src/pages/GitAuthPage/GitAuthPageView.tsx | 100 +- site/src/pages/GroupsPage/CreateGroupPage.tsx | 34 +- .../CreateGroupPageView.stories.tsx | 12 +- .../pages/GroupsPage/CreateGroupPageView.tsx | 50 +- site/src/pages/GroupsPage/GroupPage.tsx | 124 +- site/src/pages/GroupsPage/GroupsPage.tsx | 34 +- .../GroupsPage/GroupsPageView.stories.tsx | 30 +- site/src/pages/GroupsPage/GroupsPageView.tsx | 92 +- .../pages/GroupsPage/SettingsGroupPage.tsx | 36 +- .../SettingsGroupPageView.stories.tsx | 14 +- .../GroupsPage/SettingsGroupPageView.tsx | 86 +- .../pages/HealthPage/HealthPage.stories.tsx | 16 +- site/src/pages/HealthPage/HealthPage.tsx | 62 +- site/src/pages/LoginPage/LoginPage.test.tsx | 112 +- site/src/pages/LoginPage/LoginPage.tsx | 36 +- .../pages/LoginPage/LoginPageView.stories.tsx | 30 +- site/src/pages/LoginPage/LoginPageView.tsx | 42 +- .../LoginPage/SignInForm/OAuthSignInForm.tsx | 34 +- .../SignInForm/PasswordSignInForm.tsx | 36 +- .../SignInForm/SignInForm.stories.tsx | 44 +- .../pages/LoginPage/SignInForm/SignInForm.tsx | 60 +- .../LoginPage/SignInForm/SignInForm.types.ts | 4 +- site/src/pages/SetupPage/SetupPage.test.tsx | 124 +- site/src/pages/SetupPage/SetupPage.tsx | 40 +- .../pages/SetupPage/SetupPageView.stories.tsx | 24 +- site/src/pages/SetupPage/SetupPageView.tsx | 52 +- .../StarterTemplatePage.test.tsx | 20 +- .../StarterTemplatePage.tsx | 28 +- .../StarterTemplatePageView.stories.tsx | 18 +- .../StarterTemplatePageView.tsx | 48 +- .../StarterTemplatesPage.test.tsx | 23 +- .../StarterTemplatesPage.tsx | 28 +- .../StarterTemplatesPageView.stories.tsx | 20 +- .../StarterTemplatesPageView.tsx | 58 +- .../TemplateDocsPage.test.tsx | 26 +- .../TemplateDocsPage/TemplateDocsPage.tsx | 24 +- .../TemplateEmbedPage.test.tsx | 44 +- .../TemplateEmbedPage/TemplateEmbedPage.tsx | 92 +- .../TemplateEmbedPageView.stories.tsx | 16 +- .../TemplateFilesPage/TemplateFilesPage.tsx | 68 +- .../TemplateInsightsPage/DateRange.tsx | 76 +- .../TemplateInsightsPage.stories.tsx | 16 +- .../TemplateInsightsPage.tsx | 186 +- .../TemplateInsightsPage/utils.test.ts | 28 +- .../TemplateInsightsPage/utils.ts | 14 +- .../TemplateStats.stories.tsx | 34 +- .../TemplateSummaryPage/TemplateStats.tsx | 22 +- .../TemplateSummaryPage.test.tsx | 50 +- .../TemplateSummaryPage.tsx | 24 +- .../TemplateSummaryPageView.stories.tsx | 22 +- .../TemplateSummaryPageView.tsx | 40 +- .../TemplateVersionsPage.tsx | 48 +- .../TemplateVersionsPage/VersionRow.tsx | 56 +- .../VersionsTable.stories.tsx | 24 +- .../TemplateVersionsPage/VersionsTable.tsx | 42 +- site/src/pages/TemplatePage/utils.ts | 8 +- .../pages/TemplateSettingsPage/Sidebar.tsx | 42 +- .../TemplateSettingsForm.tsx | 60 +- .../TemplateSettingsPage.test.tsx | 106 +- .../TemplateSettingsPage.tsx | 46 +- .../TemplateSettingsPageView.stories.tsx | 20 +- .../TemplateSettingsPageView.tsx | 36 +- .../TemplatePermissionsPage.tsx | 60 +- .../TemplatePermissionsPageView.stories.tsx | 22 +- .../TemplatePermissionsPageView.tsx | 138 +- .../AutostopRequirementHelperText.tsx | 54 +- .../TemplateScheduleForm/TTLHelperText.tsx | 16 +- .../TemplateScheduleForm.tsx | 170 +- .../TemplateScheduleForm/formHelpers.tsx | 40 +- .../useWorkspacesToBeDeleted.ts | 36 +- .../TemplateSchedulePage.test.tsx | 258 +-- .../TemplateSchedulePage.tsx | 58 +- .../TemplateSchedulePageView.stories.tsx | 24 +- .../TemplateSchedulePageView.tsx | 38 +- .../TemplateSettingsLayout.tsx | 68 +- .../TemplateVariableField.tsx | 46 +- .../TemplateVariablesForm.tsx | 90 +- .../TemplateVariablesPage.test.tsx | 132 +- .../TemplateVariablesPage.tsx | 72 +- .../TemplateVariablesPageView.stories.tsx | 32 +- .../TemplateVariablesPageView.tsx | 52 +- .../TemplateVersionEditor/FileDialog.tsx | 150 +- .../TemplateVersionEditor/FileTreeView.tsx | 110 +- .../MissingTemplateVariablesDialog.tsx | 62 +- .../TemplateVersionEditor/MonacoEditor.tsx | 72 +- .../PublishTemplateVersionDialog.tsx | 56 +- .../TemplateVersionEditor.stories.tsx | 22 +- .../TemplateVersionEditor.tsx | 244 +-- .../TemplateVersionStatusBadge.tsx | 46 +- .../TemplateVersionEditorPage.test.tsx | 176 +- .../TemplateVersionEditorPage.tsx | 61 +- .../pages/TemplateVersionEditorPage/data.ts | 36 +- .../pages/TemplateVersionEditorPage/types.ts | 8 +- .../TemplateVersionPage.test.tsx | 40 +- .../TemplateVersionPage.tsx | 43 +- .../TemplateVersionPageView.stories.tsx | 28 +- .../TemplateVersionPageView.tsx | 50 +- .../pages/TemplatesPage/EmptyTemplates.tsx | 58 +- .../TemplatesPage/TemplatesPage.test.tsx | 60 +- .../src/pages/TemplatesPage/TemplatesPage.tsx | 28 +- .../TemplatesPageView.stories.tsx | 32 +- .../pages/TemplatesPage/TemplatesPageView.tsx | 114 +- .../pages/TerminalPage/TerminalPage.test.tsx | 134 +- site/src/pages/TerminalPage/TerminalPage.tsx | 278 +-- .../TerminalPageAlert.stories.tsx | 16 +- .../pages/TerminalPage/TerminalPageAlert.tsx | 36 +- .../AccountPage/AccountForm.stories.tsx | 24 +- .../AccountPage/AccountForm.test.tsx | 46 +- .../AccountPage/AccountForm.tsx | 48 +- .../AccountPage/AccountPage.test.tsx | 92 +- .../AccountPage/AccountPage.tsx | 30 +- .../SSHKeysPage/SSHKeysPage.test.tsx | 84 +- .../SSHKeysPage/SSHKeysPage.tsx | 36 +- .../SSHKeysPage/SSHKeysPageView.stories.tsx | 28 +- .../SSHKeysPage/SSHKeysPageView.tsx | 42 +- .../SecurityPage/SecurityPage.test.tsx | 154 +- .../SecurityPage/SecurityPage.tsx | 56 +- .../SecurityPage/SecurityPageView.stories.tsx | 28 +- .../SettingsSecurityForm.stories.tsx | 24 +- .../SecurityPage/SettingsSecurityForm.tsx | 48 +- .../SecurityPage/SingleSignOnSection.tsx | 124 +- .../ConfirmDeleteDialog.stories.tsx | 20 +- .../TokensPage/ConfirmDeleteDialog.tsx | 52 +- .../TokensPage/TokensPage.tsx | 46 +- .../TokensPage/TokensPageView.stories.tsx | 32 +- .../TokensPage/TokensPageView.tsx | 72 +- .../UserSettingsPage/TokensPage/hooks.ts | 22 +- .../WorkspaceProxyPage/WorkspaceProxyPage.tsx | 24 +- .../WorkspaceProxyPage/WorkspaceProxyRow.tsx | 86 +- .../WorkspaceProxyPage/WorkspaceProxyView.tsx | 48 +- .../WorspaceProxyView.stories.tsx | 34 +- .../UsersPage/ResetPasswordDialog.stories.tsx | 16 +- .../pages/UsersPage/ResetPasswordDialog.tsx | 34 +- site/src/pages/UsersPage/UsersFilter.tsx | 58 +- site/src/pages/UsersPage/UsersPage.test.tsx | 426 ++-- site/src/pages/UsersPage/UsersPage.tsx | 126 +- .../pages/UsersPage/UsersPageView.stories.tsx | 37 +- site/src/pages/UsersPage/UsersPageView.tsx | 60 +- .../UsersTable/EditRolesButton.stories.tsx | 22 +- .../UsersPage/UsersTable/EditRolesButton.tsx | 84 +- .../UsersTable/UserRoleHelpTooltip.tsx | 12 +- .../UsersTable/UsersTable.stories.tsx | 20 +- .../pages/UsersPage/UsersTable/UsersTable.tsx | 62 +- .../UsersPage/UsersTable/UsersTableBody.tsx | 184 +- .../WorkspaceBuildPage.test.tsx | 52 +- .../WorkspaceBuildPage/WorkspaceBuildPage.tsx | 52 +- .../WorkspaceBuildPageView.stories.tsx | 22 +- .../WorkspaceBuildPageView.tsx | 68 +- site/src/pages/WorkspacePage/BuildRow.tsx | 40 +- .../WorkspacePage/BuildsTable.stories.tsx | 18 +- site/src/pages/WorkspacePage/BuildsTable.tsx | 32 +- .../WorkspacePage/ChangeVersionDialog.tsx | 58 +- .../UpdateBuildParametersDialog.tsx | 62 +- .../pages/WorkspacePage/Workspace.stories.tsx | 76 +- site/src/pages/WorkspacePage/Workspace.tsx | 172 +- .../BuildParametersPopover.tsx | 78 +- .../WorkspaceActions/Buttons.tsx | 74 +- .../WorkspaceActions.stories.tsx | 58 +- .../WorkspaceActions/WorkspaceActions.tsx | 76 +- .../WorkspaceActions/constants.ts | 22 +- .../WorkspaceBuildLogsSection.tsx | 26 +- .../WorkspaceBuildProgress.stories.tsx | 32 +- .../WorkspacePage/WorkspaceBuildProgress.tsx | 86 +- .../WorkspaceDeletedBanner.stories.tsx | 14 +- .../WorkspacePage/WorkspaceDeletedBanner.tsx | 18 +- .../WorkspacePage/WorkspacePage.test.tsx | 372 ++-- .../src/pages/WorkspacePage/WorkspacePage.tsx | 54 +- .../WorkspacePage/WorkspaceReadyPage.tsx | 200 +- .../WorkspacePage/WorkspaceStats.stories.tsx | 22 +- .../pages/WorkspacePage/WorkspaceStats.tsx | 116 +- .../pages/WorkspaceSettingsPage/Sidebar.tsx | 40 +- .../WorkspaceParametersForm.tsx | 56 +- .../WorkspaceParametersPage.stories.tsx | 16 +- .../WorkspaceParametersPage.test.tsx | 48 +- .../WorkspaceParametersPage.tsx | 70 +- .../WorkspaceScheduleForm.stories.tsx | 58 +- .../WorkspaceScheduleForm.test.ts | 126 +- .../WorkspaceScheduleForm.tsx | 176 +- .../WorkspaceSchedulePage.test.tsx | 118 +- .../WorkspaceSchedulePage.tsx | 82 +- .../WorkspaceSchedulePage/formToRequest.ts | 38 +- .../WorkspaceSchedulePage/schedule.test.ts | 58 +- .../WorkspaceSchedulePage/schedule.ts | 72 +- .../WorkspaceSchedulePage/ttl.ts | 10 +- .../WorkspaceSchedulePage/zones.ts | 4 +- .../WorkspaceSettingsForm.tsx | 44 +- .../WorkspaceSettingsLayout.tsx | 62 +- .../WorkspaceSettingsPage.test.tsx | 38 +- .../WorkspaceSettingsPage.tsx | 44 +- .../WorkspaceSettingsPageView.stories.tsx | 16 +- .../WorkspaceSettingsPageView.tsx | 34 +- site/src/pages/WorkspacesPage/LastUsed.tsx | 58 +- .../WorkspacesPage/WorkspaceHelpTooltip.tsx | 12 +- .../WorkspacesPage/WorkspacesPage.test.tsx | 90 +- .../pages/WorkspacesPage/WorkspacesPage.tsx | 184 +- .../WorkspacesPageView.stories.tsx | 55 +- .../WorkspacesPage/WorkspacesPageView.tsx | 78 +- .../pages/WorkspacesPage/WorkspacesTable.tsx | 142 +- site/src/pages/WorkspacesPage/data.ts | 74 +- .../pages/WorkspacesPage/filter/filter.tsx | 76 +- site/src/pages/WorkspacesPage/filter/menus.ts | 42 +- .../pages/WorkspacesPage/filter/options.ts | 10 +- site/src/pages/index.tsx | 10 +- site/src/testHelpers/entities.ts | 314 +-- site/src/testHelpers/handlers.ts | 170 +- site/src/testHelpers/localstorage.ts | 18 +- site/src/testHelpers/renderHelpers.tsx | 66 +- site/src/testHelpers/server.ts | 6 +- site/src/testHelpers/styleMock.ts | 2 +- site/src/theme/colors.ts | 2 +- site/src/theme/constants.ts | 20 +- site/src/theme/globalFonts.ts | 12 +- site/src/theme/index.ts | 2 +- site/src/theme/theme.ts | 20 +- site/src/utils/colors.ts | 16 +- site/src/utils/combineClasses.test.ts | 10 +- site/src/utils/combineClasses.ts | 18 +- site/src/utils/createDayString.ts | 8 +- site/src/utils/delay.ts | 4 +- site/src/utils/deployOptions.ts | 30 +- site/src/utils/docs.ts | 26 +- site/src/utils/ellipsizeText.test.ts | 10 +- site/src/utils/ellipsizeText.ts | 10 +- site/src/utils/events.test.ts | 18 +- site/src/utils/events.ts | 14 +- site/src/utils/filetree.test.ts | 76 +- site/src/utils/filetree.ts | 84 +- site/src/utils/filters.test.ts | 10 +- site/src/utils/filters.ts | 12 +- site/src/utils/formUtils.test.ts | 156 +- site/src/utils/formUtils.ts | 70 +- site/src/utils/gitAuth.ts | 2 +- site/src/utils/groups.ts | 16 +- site/src/utils/latency.ts | 14 +- site/src/utils/nullable.ts | 2 +- site/src/utils/page.ts | 6 +- site/src/utils/portForward.ts | 8 +- site/src/utils/random.ts | 10 +- site/src/utils/redirect.test.ts | 26 +- site/src/utils/redirect.ts | 12 +- site/src/utils/richParameters.test.ts | 14 +- site/src/utils/richParameters.ts | 66 +- site/src/utils/schedule.test.ts | 66 +- site/src/utils/schedule.ts | 92 +- site/src/utils/starterTemplates.ts | 18 +- site/src/utils/tar.test.ts | 50 +- site/src/utils/tar.ts | 266 +-- site/src/utils/templateVersion.ts | 38 +- site/src/utils/templates.ts | 20 +- site/src/utils/workspace.test.ts | 58 +- site/src/utils/workspace.tsx | 172 +- .../appearance/appearanceXService.ts | 34 +- .../src/xServices/auth/authMethodsXService.ts | 16 +- site/src/xServices/auth/authXService.ts | 106 +- .../xServices/buildInfo/buildInfoXService.ts | 24 +- .../createTemplate/createTemplateXService.ts | 196 +- .../createWorkspaceXService.ts | 107 +- .../deploymentConfigMachine.ts | 28 +- .../deploymentStats/deploymentStatsMachine.ts | 16 +- .../entitlementsSelectors.test.ts | 34 +- .../entitlements/entitlementsSelectors.ts | 22 +- .../entitlements/entitlementsXService.ts | 26 +- .../experiments/experimentsMachine.ts | 26 +- .../xServices/groups/createGroupXService.ts | 20 +- .../src/xServices/groups/editGroupXService.ts | 46 +- site/src/xServices/groups/groupXService.ts | 101 +- site/src/xServices/groups/groupsXService.ts | 26 +- .../pagination/paginationXService.ts | 12 +- site/src/xServices/quotas/quotasXService.ts | 16 +- site/src/xServices/roles/siteRolesXService.ts | 22 +- site/src/xServices/setup/setupXService.ts | 22 +- site/src/xServices/sshKey/sshKeyXService.ts | 32 +- .../starterTemplateXService.ts | 28 +- .../starterTemplatesXService.ts | 20 +- .../template/searchUsersAndGroupsXService.ts | 46 +- .../xServices/template/templateACLXService.ts | 134 +- .../template/templateVariablesXService.ts | 84 +- .../templateVersionXService.ts | 70 +- .../templateVersionEditorXService.ts | 157 +- .../xServices/templates/templatesXService.ts | 32 +- .../xServices/terminal/terminalXService.ts | 148 +- .../updateCheck/updateCheckXService.test.ts | 82 +- .../updateCheck/updateCheckXService.ts | 48 +- .../userSecuritySettingsXService.ts | 22 +- .../src/xServices/users/createUserXService.ts | 22 +- .../src/xServices/users/searchUserXService.ts | 18 +- site/src/xServices/users/usersXService.ts | 178 +- .../xServices/workspace/workspaceXService.ts | 268 +-- .../workspaceAgentLogsXService.ts | 46 +- .../workspaceBuild/workspaceBuildXService.ts | 68 +- .../workspaceScheduleBannerXService.ts | 58 +- .../workspaceScheduleXService.ts | 64 +- site/vite.config.ts | 24 +- 664 files changed, 17537 insertions(+), 17407 deletions(-) diff --git a/.prettierrc.yaml b/.prettierrc.yaml index e6cef25681..189b2580f6 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -1,9 +1,8 @@ # This config file is used in conjunction with `.editorconfig` to specify # formatting for prettier-supported files. See `.editorconfig` and -# `site/.editorconfig`for whitespace formatting options. +# `site/.editorconfig` for whitespace formatting options. printWidth: 80 proseWrap: always -semi: false trailingComma: all useTabs: false tabWidth: 2 diff --git a/docs/contributing/frontend.md b/docs/contributing/frontend.md index 73dc27a1fd..c4d1f22e78 100644 --- a/docs/contributing/frontend.md +++ b/docs/contributing/frontend.md @@ -128,9 +128,9 @@ export const getAgentListeningPorts = async ( ): Promise => { const response = await axios.get( `/api/v2/workspaceagents/${agentID}/listening-ports`, - ) - return response.data -} + ); + return response.data; +}; ``` Sometimes, a FE operation can have multiple API calls so it is ok to wrap it as @@ -140,9 +140,9 @@ a single function. export const updateWorkspaceVersion = async ( workspace: TypesGen.Workspace, ): Promise => { - const template = await getTemplate(workspace.template_id) - return startWorkspace(workspace.id, template.active_version_id) -} + const template = await getTemplate(workspace.template_id); + return startWorkspace(workspace.id, template.active_version_id); +}; ``` If you need more granular errors or control, you may should consider keep them @@ -242,14 +242,14 @@ instead of using `screen.getByRole("button")` directly we could do slow. ```tsx -user.click(screen.getByRole("button")) +user.click(screen.getByRole("button")); ``` ✅ Better. We can limit the number of elements we are querying. ```tsx -const form = screen.getByTestId("form") -user.click(within(form).getByRole("button")) +const form = screen.getByTestId("form"); +user.click(within(form).getByRole("button")); ``` #### `jest.spyOn` with the API is not working diff --git a/offlinedocs/next.config.js b/offlinedocs/next.config.js index 0128f08b27..bf2eb08b5f 100644 --- a/offlinedocs/next.config.js +++ b/offlinedocs/next.config.js @@ -2,6 +2,6 @@ const nextConfig = { reactStrictMode: true, trailingSlash: true, -} +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/offlinedocs/pages/[[...slug]].tsx b/offlinedocs/pages/[[...slug]].tsx index 7f6cc7ddd4..ca40353f76 100644 --- a/offlinedocs/pages/[[...slug]].tsx +++ b/offlinedocs/pages/[[...slug]].tsx @@ -24,57 +24,57 @@ import { Tr, UnorderedList, useDisclosure, -} from "@chakra-ui/react" -import fm from "front-matter" -import { readFileSync } from "fs" -import _ from "lodash" -import { GetStaticPaths, GetStaticProps, NextPage } from "next" -import Head from "next/head" -import NextLink from "next/link" -import { useRouter } from "next/router" -import path from "path" -import { MdMenu } from "react-icons/md" -import ReactMarkdown from "react-markdown" -import rehypeRaw from "rehype-raw" -import remarkGfm from "remark-gfm" +} from "@chakra-ui/react"; +import fm from "front-matter"; +import { readFileSync } from "fs"; +import _ from "lodash"; +import { GetStaticPaths, GetStaticProps, NextPage } from "next"; +import Head from "next/head"; +import NextLink from "next/link"; +import { useRouter } from "next/router"; +import path from "path"; +import { MdMenu } from "react-icons/md"; +import ReactMarkdown from "react-markdown"; +import rehypeRaw from "rehype-raw"; +import remarkGfm from "remark-gfm"; -type FilePath = string -type UrlPath = string +type FilePath = string; +type UrlPath = string; type Route = { - path: FilePath - title: string - description?: string - children?: Route[] -} -type Manifest = { versions: string[]; routes: Route[] } -type NavItem = { title: string; path: UrlPath; children?: NavItem[] } -type Nav = NavItem[] + path: FilePath; + title: string; + description?: string; + children?: Route[]; +}; +type Manifest = { versions: string[]; routes: Route[] }; +type NavItem = { title: string; path: UrlPath; children?: NavItem[] }; +type Nav = NavItem[]; const readContentFile = (filePath: string) => { - const baseDir = process.cwd() - const docsPath = path.join(baseDir, "..", "docs") - return readFileSync(path.join(docsPath, filePath), { encoding: "utf-8" }) -} + const baseDir = process.cwd(); + const docsPath = path.join(baseDir, "..", "docs"); + return readFileSync(path.join(docsPath, filePath), { encoding: "utf-8" }); +}; -const removeTrailingSlash = (path: string) => path.replace(/\/+$/, "") +const removeTrailingSlash = (path: string) => path.replace(/\/+$/, ""); -const removeMkdExtension = (path: string) => path.replace(/\.md/g, "") +const removeMkdExtension = (path: string) => path.replace(/\.md/g, ""); const removeIndexFilename = (path: string) => { if (path.endsWith("index")) { - path = path.replace("index", "") + path = path.replace("index", ""); } - return path -} + return path; +}; const removeREADMEName = (path: string) => { if (path.startsWith("README")) { - path = path.replace("README", "") + path = path.replace("README", ""); } - return path -} + return path; +}; // transformLinkUri converts the links in the markdown file to // href html links. All index page routes are the directory name, and all @@ -87,142 +87,142 @@ const removeREADMEName = (path: string) => { // file.md -> ../file-next-to-file = ../file-next-to-file const transformLinkUriSource = (sourceFile: string) => { return (href = "") => { - const isExternal = href.startsWith("http") || href.startsWith("https") + const isExternal = href.startsWith("http") || href.startsWith("https"); if (!isExternal) { // Remove .md form the path - href = removeMkdExtension(href) + href = removeMkdExtension(href); // Add the extra '..' if not an index file. - sourceFile = removeMkdExtension(sourceFile) + sourceFile = removeMkdExtension(sourceFile); if (!sourceFile.endsWith("index")) { - href = "../" + href + href = "../" + href; } // Remove the index path - href = removeIndexFilename(href) - href = removeREADMEName(href) + href = removeIndexFilename(href); + href = removeREADMEName(href); } - return href - } -} + return href; + }; +}; const transformFilePathToUrlPath = (filePath: string) => { // Remove markdown extension - let urlPath = removeMkdExtension(filePath) + let urlPath = removeMkdExtension(filePath); // Remove relative path if (urlPath.startsWith("./")) { - urlPath = urlPath.replace("./", "") + urlPath = urlPath.replace("./", ""); } // Remove index from the root file - urlPath = removeIndexFilename(urlPath) - urlPath = removeREADMEName(urlPath) + urlPath = removeIndexFilename(urlPath); + urlPath = removeREADMEName(urlPath); // Remove trailing slash if (urlPath.endsWith("/")) { - urlPath = removeTrailingSlash(urlPath) + urlPath = removeTrailingSlash(urlPath); } - return urlPath -} + return urlPath; +}; const mapRoutes = (manifest: Manifest): Record => { - const paths: Record = {} + const paths: Record = {}; const addPaths = (routes: Route[]) => { for (const route of routes) { - paths[transformFilePathToUrlPath(route.path)] = route + paths[transformFilePathToUrlPath(route.path)] = route; if (route.children) { - addPaths(route.children) + addPaths(route.children); } } - } + }; - addPaths(manifest.routes) + addPaths(manifest.routes); - return paths -} + return paths; +}; -let manifest: Manifest | undefined +let manifest: Manifest | undefined; const getManifest = () => { if (manifest) { - return manifest + return manifest; } - const manifestContent = readContentFile("manifest.json") - manifest = JSON.parse(manifestContent) as Manifest - return manifest -} + const manifestContent = readContentFile("manifest.json"); + manifest = JSON.parse(manifestContent) as Manifest; + return manifest; +}; -let navigation: Nav | undefined +let navigation: Nav | undefined; const getNavigation = (manifest: Manifest): Nav => { if (navigation) { - return navigation + return navigation; } const getNavItem = (route: Route, parentPath?: UrlPath): NavItem => { const path = parentPath ? `${parentPath}/${transformFilePathToUrlPath(route.path)}` - : transformFilePathToUrlPath(route.path) + : transformFilePathToUrlPath(route.path); const navItem: NavItem = { title: route.title, path, - } + }; if (route.children) { - navItem.children = [] + navItem.children = []; for (const childRoute of route.children) { - navItem.children.push(getNavItem(childRoute)) + navItem.children.push(getNavItem(childRoute)); } } - return navItem - } + return navItem; + }; - navigation = [] + navigation = []; for (const route of manifest.routes) { - navigation.push(getNavItem(route)) + navigation.push(getNavItem(route)); } - return navigation -} + return navigation; +}; const removeHtmlComments = (string: string) => { - return string.replace(//g, "") -} + return string.replace(//g, ""); +}; export const getStaticPaths: GetStaticPaths = () => { - const manifest = getManifest() - const routes = mapRoutes(manifest) + const manifest = getManifest(); + const routes = mapRoutes(manifest); const paths = Object.keys(routes).map((urlPath) => ({ params: { slug: urlPath.split("/") }, - })) + })); return { paths, fallback: false, - } -} + }; +}; export const getStaticProps: GetStaticProps = (context) => { // When it is home page, the slug is undefined because there is no url path // so we make it an empty string to work good with the mapRoutes - const { slug = [""] } = context.params as { slug: string[] } - const manifest = getManifest() - const routes = mapRoutes(manifest) - const urlPath = slug.join("/") - const route = routes[urlPath] - const { body } = fm(readContentFile(route.path)) + const { slug = [""] } = context.params as { slug: string[] }; + const manifest = getManifest(); + const routes = mapRoutes(manifest); + const urlPath = slug.join("/"); + const route = routes[urlPath]; + const { body } = fm(readContentFile(route.path)); // Serialize MDX to support custom components - const content = removeHtmlComments(body) - const navigation = getNavigation(manifest) - const version = manifest.versions[0] + const content = removeHtmlComments(body); + const navigation = getNavigation(manifest); + const version = manifest.versions[0]; return { props: { @@ -231,25 +231,26 @@ export const getStaticProps: GetStaticProps = (context) => { route, version, }, - } -} + }; +}; const SidebarNavItem: React.FC<{ item: NavItem; nav: Nav }> = ({ item, nav, }) => { - const router = useRouter() - let isActive = router.asPath.startsWith(`/${item.path}`) + const router = useRouter(); + let isActive = router.asPath.startsWith(`/${item.path}`); // Special case to handle the home path if (item.path === "") { - isActive = router.asPath === "/" + isActive = router.asPath === "/"; // Special case to handle the home path children - const homeNav = nav.find((navItem) => navItem.path === "") as NavItem - const homeNavPaths = homeNav.children?.map((item) => `/${item.path}/`) ?? [] + const homeNav = nav.find((navItem) => navItem.path === "") as NavItem; + const homeNavPaths = + homeNav.children?.map((item) => `/${item.path}/`) ?? []; if (homeNavPaths.includes(router.asPath)) { - isActive = true + isActive = true; } } @@ -280,8 +281,8 @@ const SidebarNavItem: React.FC<{ item: NavItem; nav: Nav }> = ({ )} - ) -} + ); +}; const SidebarNav: React.FC<{ nav: Nav; version: string } & GridProps> = ({ nav, @@ -312,14 +313,14 @@ const SidebarNav: React.FC<{ nav: Nav; version: string } & GridProps> = ({ ))} - ) -} + ); +}; const MobileNavbar: React.FC<{ nav: Nav; version: string }> = ({ nav, version, }) => { - const { isOpen, onOpen, onClose } = useDisclosure() + const { isOpen, onOpen, onClose } = useDisclosure(); return ( <> @@ -347,26 +348,26 @@ const MobileNavbar: React.FC<{ nav: Nav; version: string }> = ({ - ) -} + ); +}; const slugifyTitle = (title: string) => { - return _.kebabCase(title.toLowerCase()) -} + return _.kebabCase(title.toLowerCase()); +}; const getImageUrl = (src: string | undefined) => { if (src === undefined) { - return "" + return ""; } - const assetPath = src.split("images/")[1] - return `/images/${assetPath}` -} + const assetPath = src.split("images/")[1]; + return `/images/${assetPath}`; +}; const DocsPage: NextPage<{ - content: string - navigation: Nav - route: Route - version: string + content: string; + navigation: Nav; + route: Route; + version: string; }> = ({ content, navigation, route, version }) => { return ( <> @@ -486,7 +487,7 @@ const DocsPage: NextPage<{ ), a: ({ children, href = "" }) => { const isExternal = - href.startsWith("http") || href.startsWith("https") + href.startsWith("http") || href.startsWith("https"); return ( {children} - ) + ); }, code: ({ node, ...props }) => ( @@ -538,7 +539,7 @@ const DocsPage: NextPage<{ - ) -} + ); +}; -export default DocsPage +export default DocsPage; diff --git a/offlinedocs/pages/_app.tsx b/offlinedocs/pages/_app.tsx index 9c79747e47..31bd99af73 100644 --- a/offlinedocs/pages/_app.tsx +++ b/offlinedocs/pages/_app.tsx @@ -1,6 +1,6 @@ -import { ChakraProvider, extendTheme } from "@chakra-ui/react" -import type { AppProps } from "next/app" -import Head from "next/head" +import { ChakraProvider, extendTheme } from "@chakra-ui/react"; +import type { AppProps } from "next/app"; +import Head from "next/head"; const theme = extendTheme({ styles: { @@ -10,7 +10,7 @@ const theme = extendTheme({ }, }, }, -}) +}); const MyApp: React.FC = ({ Component, pageProps }) => { return ( @@ -23,7 +23,7 @@ const MyApp: React.FC = ({ Component, pageProps }) => { - ) -} + ); +}; -export default MyApp +export default MyApp; diff --git a/site/.prettierrc.yaml b/site/.prettierrc.yaml index 6e914f9331..036d5d5f73 100644 --- a/site/.prettierrc.yaml +++ b/site/.prettierrc.yaml @@ -2,10 +2,9 @@ # This config file is used in conjunction with `.editorconfig` to specify # formatting for prettier-supported files. See `.editorconfig` and -# `site/.editorconfig`for whitespace formatting options. +# `site/.editorconfig` for whitespace formatting options. printWidth: 80 proseWrap: always -semi: false trailingComma: all useTabs: false tabWidth: 2 diff --git a/site/.storybook/main.js b/site/.storybook/main.js index 6fed6b2997..172d61b22a 100644 --- a/site/.storybook/main.js +++ b/site/.storybook/main.js @@ -1,5 +1,5 @@ -import turbosnap from "vite-plugin-turbosnap" -import { mergeConfig } from "vite" +import turbosnap from "vite-plugin-turbosnap"; +import { mergeConfig } from "vite"; module.exports = { stories: ["../src/**/*.stories.tsx"], @@ -15,7 +15,7 @@ module.exports = { options: {}, }, async viteFinal(config, { configType }) { - config.plugins = config.plugins || [] + config.plugins = config.plugins || []; // return the customized config if (configType === "PRODUCTION") { // ignore @ts-ignore because it's not in the vite types yet @@ -23,8 +23,8 @@ module.exports = { turbosnap({ rootDir: config.root || "", }), - ) + ); } - return config + return config; }, -} +}; diff --git a/site/.storybook/preview.jsx b/site/.storybook/preview.jsx index 371a5007ab..202da3d039 100644 --- a/site/.storybook/preview.jsx +++ b/site/.storybook/preview.jsx @@ -1,11 +1,11 @@ -import CssBaseline from "@mui/material/CssBaseline" -import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles" -import { withRouter } from "storybook-addon-react-router-v6" -import { HelmetProvider } from "react-helmet-async" -import { dark } from "../src/theme" -import "../src/theme/globalFonts" -import "../src/i18n" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import CssBaseline from "@mui/material/CssBaseline"; +import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles"; +import { withRouter } from "storybook-addon-react-router-v6"; +import { HelmetProvider } from "react-helmet-async"; +import { dark } from "../src/theme"; +import "../src/theme/globalFonts"; +import "../src/i18n"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; export const decorators = [ (Story) => ( @@ -22,16 +22,16 @@ export const decorators = [ - ) + ); }, (Story) => { return ( - ) + ); }, -] +]; export const parameters = { actions: { @@ -44,4 +44,4 @@ export const parameters = { date: /Date$/, }, }, -} +}; diff --git a/site/e2e/constants.ts b/site/e2e/constants.ts index b6e4c2d341..f75af482c7 100644 --- a/site/e2e/constants.ts +++ b/site/e2e/constants.ts @@ -1,12 +1,12 @@ // Default port from the server -export const defaultPort = 3000 -export const prometheusPort = 2114 -export const pprofPort = 6061 +export const defaultPort = 3000; +export const prometheusPort = 2114; +export const pprofPort = 6061; // Credentials for the first user -export const username = "admin" -export const password = "SomeSecurePassword!" -export const email = "admin@coder.com" +export const username = "admin"; +export const password = "SomeSecurePassword!"; +export const email = "admin@coder.com"; export const gitAuth = { deviceProvider: "device", @@ -22,4 +22,4 @@ export const gitAuth = { codePath: "/code", validatePath: "/validate", installationsPath: "/installations", -} +}; diff --git a/site/e2e/global.setup.ts b/site/e2e/global.setup.ts index db52c1057f..08a66c317d 100644 --- a/site/e2e/global.setup.ts +++ b/site/e2e/global.setup.ts @@ -1,18 +1,18 @@ -import { test, expect } from "@playwright/test" -import * as constants from "./constants" -import { STORAGE_STATE } from "./playwright.config" -import { Language } from "../src/pages/CreateUserPage/CreateUserForm" +import { test, expect } from "@playwright/test"; +import * as constants from "./constants"; +import { STORAGE_STATE } from "./playwright.config"; +import { Language } from "../src/pages/CreateUserPage/CreateUserForm"; test("create first user", async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }) + await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.getByLabel(Language.usernameLabel).fill(constants.username) - await page.getByLabel(Language.emailLabel).fill(constants.email) - await page.getByLabel(Language.passwordLabel).fill(constants.password) - await page.getByTestId("trial").click() - await page.getByTestId("create").click() + await page.getByLabel(Language.usernameLabel).fill(constants.username); + await page.getByLabel(Language.emailLabel).fill(constants.email); + await page.getByLabel(Language.passwordLabel).fill(constants.password); + await page.getByTestId("trial").click(); + await page.getByTestId("create").click(); - await expect(page).toHaveURL("/workspaces") + await expect(page).toHaveURL("/workspaces"); - await page.context().storageState({ path: STORAGE_STATE }) -}) + await page.context().storageState({ path: STORAGE_STATE }); +}); diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 354f647c28..04bb326183 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -1,9 +1,9 @@ -import { expect, Page } from "@playwright/test" -import { ChildProcess, exec, spawn } from "child_process" -import { randomUUID } from "crypto" -import path from "path" -import express from "express" -import { TarWriter } from "utils/tar" +import { expect, Page } from "@playwright/test"; +import { ChildProcess, exec, spawn } from "child_process"; +import { randomUUID } from "crypto"; +import path from "path"; +import express from "express"; +import { TarWriter } from "utils/tar"; import { Agent, App, @@ -14,13 +14,13 @@ import { ApplyComplete, Resource, RichParameter, -} from "./provisionerGenerated" -import { prometheusPort, pprofPort } from "./constants" -import { port } from "./playwright.config" -import * as ssh from "ssh2" -import { Duplex } from "stream" -import { WorkspaceBuildParameter } from "api/typesGenerated" -import axios from "axios" +} from "./provisionerGenerated"; +import { prometheusPort, pprofPort } from "./constants"; +import { port } from "./playwright.config"; +import * as ssh from "ssh2"; +import { Duplex } from "stream"; +import { WorkspaceBuildParameter } from "api/typesGenerated"; +import axios from "axios"; // createWorkspace creates a workspace for a template. // It does not wait for it to be running, but it does navigate to the page. @@ -32,25 +32,25 @@ export const createWorkspace = async ( ): Promise => { await page.goto("/templates/" + templateName + "/workspace", { waitUntil: "domcontentloaded", - }) - await expect(page).toHaveURL("/templates/" + templateName + "/workspace") + }); + await expect(page).toHaveURL("/templates/" + templateName + "/workspace"); - const name = randomName() - await page.getByLabel("name").fill(name) + const name = randomName(); + await page.getByLabel("name").fill(name); - await fillParameters(page, richParameters, buildParameters) - await page.getByTestId("form-submit").click() + await fillParameters(page, richParameters, buildParameters); + await page.getByTestId("form-submit").click(); - await expect(page).toHaveURL("/@admin/" + name) + await expect(page).toHaveURL("/@admin/" + name); await page.waitForSelector( "span[data-testid='build-status'] >> text=Running", { state: "visible", }, - ) - return name -} + ); + return name; +}; export const verifyParameters = async ( page: Page, @@ -60,56 +60,56 @@ export const verifyParameters = async ( ) => { await page.goto("/@admin/" + workspaceName + "/settings/parameters", { waitUntil: "domcontentloaded", - }) + }); await expect(page).toHaveURL( "/@admin/" + workspaceName + "/settings/parameters", - ) + ); for (const buildParameter of expectedBuildParameters) { const richParameter = richParameters.find( (richParam) => richParam.name === buildParameter.name, - ) + ); if (!richParameter) { throw new Error( "build parameter is expected to be present in rich parameter schema", - ) + ); } const parameterLabel = await page.waitForSelector( "[data-testid='parameter-field-" + richParameter.name + "']", { state: "visible" }, - ) + ); - const muiDisabled = richParameter.mutable ? "" : ".Mui-disabled" + const muiDisabled = richParameter.mutable ? "" : ".Mui-disabled"; if (richParameter.type === "bool") { const parameterField = await parameterLabel.waitForSelector( "[data-testid='parameter-field-bool'] .MuiRadio-root.Mui-checked" + muiDisabled + " input", - ) - const value = await parameterField.inputValue() - expect(value).toEqual(buildParameter.value) + ); + const value = await parameterField.inputValue(); + expect(value).toEqual(buildParameter.value); } else if (richParameter.options.length > 0) { const parameterField = await parameterLabel.waitForSelector( "[data-testid='parameter-field-options'] .MuiRadio-root.Mui-checked" + muiDisabled + " input", - ) - const value = await parameterField.inputValue() - expect(value).toEqual(buildParameter.value) + ); + const value = await parameterField.inputValue(); + expect(value).toEqual(buildParameter.value); } else if (richParameter.type === "list(string)") { - throw new Error("not implemented yet") // FIXME + throw new Error("not implemented yet"); // FIXME } else { // text or number const parameterField = await parameterLabel.waitForSelector( "[data-testid='parameter-field-text'] input" + muiDisabled, - ) - const value = await parameterField.inputValue() - expect(value).toEqual(buildParameter.value) + ); + const value = await parameterField.inputValue(); + expect(value).toEqual(buildParameter.value); } } -} +}; // createTemplate navigates to the /templates/new page and uploads a template // with the resources provided in the responses argument. @@ -120,24 +120,24 @@ export const createTemplate = async ( // Required to have templates submit their provisioner type as echo! await page.addInitScript({ content: "window.playwright = true", - }) + }); - await page.goto("/templates/new", { waitUntil: "domcontentloaded" }) - await expect(page).toHaveURL("/templates/new") + await page.goto("/templates/new", { waitUntil: "domcontentloaded" }); + await expect(page).toHaveURL("/templates/new"); await page.getByTestId("file-upload").setInputFiles({ buffer: await createTemplateVersionTar(responses), mimeType: "application/x-tar", name: "template.tar", - }) - const name = randomName() - await page.getByLabel("Name *").fill(name) - await page.getByTestId("form-submit").click() + }); + const name = randomName(); + await page.getByLabel("Name *").fill(name); + await page.getByTestId("form-submit").click(); await expect(page).toHaveURL("/templates/" + name, { timeout: 30000, - }) - return name -} + }); + return name; +}; // sshIntoWorkspace spawns a Coder SSH process and a client connected to it. export const sshIntoWorkspace = async ( @@ -147,9 +147,9 @@ export const sshIntoWorkspace = async ( binaryArgs: string[] = [], ): Promise => { if (binaryPath === "go") { - binaryArgs = ["run", coderMainPath()] + binaryArgs = ["run", coderMainPath()]; } - const sessionToken = await findSessionToken(page) + const sessionToken = await findSessionToken(page); return new Promise((resolve, reject) => { const cp = spawn(binaryPath, [...binaryArgs, "ssh", "--stdio", workspace], { env: { @@ -157,49 +157,49 @@ export const sshIntoWorkspace = async ( CODER_SESSION_TOKEN: sessionToken, CODER_URL: "http://localhost:3000", }, - }) - cp.on("error", (err) => reject(err)) + }); + cp.on("error", (err) => reject(err)); const proxyStream = new Duplex({ read: (size) => { - return cp.stdout.read(Math.min(size, cp.stdout.readableLength)) + return cp.stdout.read(Math.min(size, cp.stdout.readableLength)); }, write: cp.stdin.write.bind(cp.stdin), - }) + }); // eslint-disable-next-line no-console -- Helpful for debugging - cp.stderr.on("data", (data) => console.log(data.toString())) + cp.stderr.on("data", (data) => console.log(data.toString())); cp.stdout.on("readable", (...args) => { - proxyStream.emit("readable", ...args) + proxyStream.emit("readable", ...args); if (cp.stdout.readableLength > 0) { - proxyStream.emit("data", cp.stdout.read()) + proxyStream.emit("data", cp.stdout.read()); } - }) - const client = new ssh.Client() + }); + const client = new ssh.Client(); client.connect({ sock: proxyStream, username: "coder", - }) - client.on("error", (err) => reject(err)) + }); + client.on("error", (err) => reject(err)); client.on("ready", () => { - resolve(client) - }) - }) -} + resolve(client); + }); + }); +}; export const stopWorkspace = async (page: Page, workspaceName: string) => { await page.goto("/@admin/" + workspaceName, { waitUntil: "domcontentloaded", - }) - await expect(page).toHaveURL("/@admin/" + workspaceName) + }); + await expect(page).toHaveURL("/@admin/" + workspaceName); - await page.getByTestId("workspace-stop-button").click() + await page.getByTestId("workspace-stop-button").click(); await page.waitForSelector( "span[data-testid='build-status'] >> text=Stopped", { state: "visible", }, - ) -} + ); +}; export const buildWorkspaceWithParameters = async ( page: Page, @@ -210,15 +210,15 @@ export const buildWorkspaceWithParameters = async ( ) => { await page.goto("/@admin/" + workspaceName, { waitUntil: "domcontentloaded", - }) - await expect(page).toHaveURL("/@admin/" + workspaceName) + }); + await expect(page).toHaveURL("/@admin/" + workspaceName); - await page.getByTestId("build-parameters-button").click() + await page.getByTestId("build-parameters-button").click(); - await fillParameters(page, richParameters, buildParameters) - await page.getByTestId("build-parameters-submit").click() + await fillParameters(page, richParameters, buildParameters); + await page.getByTestId("build-parameters-submit").click(); if (confirm) { - await page.getByTestId("confirm-button").click() + await page.getByTestId("confirm-button").click(); } await page.waitForSelector( @@ -226,8 +226,8 @@ export const buildWorkspaceWithParameters = async ( { state: "visible", }, - ) -} + ); +}; // startAgent runs the coder agent with the provided token. // It awaits the agent to be ready before returning. @@ -235,8 +235,8 @@ export const startAgent = async ( page: Page, token: string, ): Promise => { - return startAgentWithCommand(page, token, "go", "run", coderMainPath()) -} + return startAgentWithCommand(page, token, "go", "run", coderMainPath()); +}; // downloadCoderVersion downloads the version provided into a temporary dir and // caches it so subsequent calls are fast. @@ -244,23 +244,23 @@ export const downloadCoderVersion = async ( version: string, ): Promise => { if (version.startsWith("v")) { - version = version.slice(1) + version = version.slice(1); } - const binaryName = "coder-e2e-" + version - const tempDir = "/tmp/coder-e2e-cache" + const binaryName = "coder-e2e-" + version; + const tempDir = "/tmp/coder-e2e-cache"; // The install script adds `./bin` automatically to the path :shrug: - const binaryPath = path.join(tempDir, "bin", binaryName) + const binaryPath = path.join(tempDir, "bin", binaryName); const exists = await new Promise((resolve) => { - const cp = spawn(binaryPath, ["version"]) + const cp = spawn(binaryPath, ["version"]); cp.on("close", (code) => { - resolve(code === 0) - }) - cp.on("error", () => resolve(false)) - }) + resolve(code === 0); + }); + cp.on("error", () => resolve(false)); + }); if (exists) { - return binaryPath + return binaryPath; } // Runs our public install script using our options to @@ -294,19 +294,19 @@ export const downloadCoderVersion = async ( XDG_CACHE_HOME: "/tmp/coder-e2e-cache", }, }, - ) + ); // eslint-disable-next-line no-console -- Needed for debugging - cp.stderr.on("data", (data) => console.log(data.toString())) + cp.stderr.on("data", (data) => console.log(data.toString())); cp.on("close", (code) => { if (code === 0) { - resolve() + resolve(); } else { - reject(new Error("curl failed with code " + code)) + reject(new Error("curl failed with code " + code)); } - }) - }) - return binaryPath -} + }); + }); + return binaryPath; +}; export const startAgentWithCommand = async ( page: Page, @@ -322,23 +322,23 @@ export const startAgentWithCommand = async ( CODER_AGENT_PPROF_ADDRESS: "127.0.0.1:" + pprofPort, CODER_AGENT_PROMETHEUS_ADDRESS: "127.0.0.1:" + prometheusPort, }, - }) + }); cp.stdout.on("data", (data: Buffer) => { // eslint-disable-next-line no-console -- Log agent activity console.log( `[agent] [stdout] [onData] ${data.toString().replace(/\n$/g, "")}`, - ) - }) + ); + }); cp.stderr.on("data", (data: Buffer) => { // eslint-disable-next-line no-console -- Log agent activity console.log( `[agent] [stderr] [onData] ${data.toString().replace(/\n$/g, "")}`, - ) - }) + ); + }); - await page.getByTestId("agent-status-ready").waitFor({ state: "visible" }) - return cp -} + await page.getByTestId("agent-status-ready").waitFor({ state: "visible" }); + return cp; +}; export const stopAgent = async (cp: ChildProcess, goRun: boolean = true) => { // When the web server is started with `go run`, it spawns a child process with coder server. @@ -346,31 +346,31 @@ export const stopAgent = async (cp: ChildProcess, goRun: boolean = true) => { // The command `kill` is used to terminate a web server started as a standalone binary. exec(goRun ? `pkill -P ${cp.pid}` : `kill ${cp.pid}`, (error) => { if (error) { - throw new Error(`exec error: ${JSON.stringify(error)}`) + throw new Error(`exec error: ${JSON.stringify(error)}`); } - }) - await waitUntilUrlIsNotResponding("http://localhost:" + prometheusPort) -} + }); + await waitUntilUrlIsNotResponding("http://localhost:" + prometheusPort); +}; const waitUntilUrlIsNotResponding = async (url: string) => { - const maxRetries = 30 - const retryIntervalMs = 1000 - let retries = 0 + const maxRetries = 30; + const retryIntervalMs = 1000; + let retries = 0; while (retries < maxRetries) { try { - await axios.get(url) + await axios.get(url); } catch (error) { - return + return; } - retries++ - await new Promise((resolve) => setTimeout(resolve, retryIntervalMs)) + retries++; + await new Promise((resolve) => setTimeout(resolve, retryIntervalMs)); } throw new Error( `URL ${url} is still responding after ${maxRetries * retryIntervalMs}ms`, - ) -} + ); +}; const coderMainPath = (): string => { return path.join( @@ -381,8 +381,8 @@ const coderMainPath = (): string => { "cmd", "coder", "main.go", - ) -} + ); +}; // Allows users to more easily define properties they want for agents and resources! type RecursivePartial = { @@ -390,16 +390,16 @@ type RecursivePartial = { ? RecursivePartial[] : T[P] extends object | undefined ? RecursivePartial - : T[P] -} + : T[P]; +}; interface EchoProvisionerResponses { // parse is for observing any Terraform variables - parse?: RecursivePartial[] + parse?: RecursivePartial[]; // plan occurs when the template is imported - plan?: RecursivePartial[] + plan?: RecursivePartial[]; // apply occurs when the workspace is built - apply?: RecursivePartial[] + apply?: RecursivePartial[]; } // createTemplateVersionTar consumes a series of echo provisioner protobufs and @@ -408,26 +408,26 @@ const createTemplateVersionTar = async ( responses?: EchoProvisionerResponses, ): Promise => { if (!responses) { - responses = {} + responses = {}; } if (!responses.parse) { responses.parse = [ { parse: {}, }, - ] + ]; } if (!responses.apply) { responses.apply = [ { apply: {}, }, - ] + ]; } if (!responses.plan) { responses.plan = responses.apply.map((response) => { if (response.log) { - return response + return response; } return { plan: { @@ -436,23 +436,23 @@ const createTemplateVersionTar = async ( parameters: response.apply?.parameters ?? [], gitAuthProviders: response.apply?.gitAuthProviders ?? [], }, - } - }) + }; + }); } - const tar = new TarWriter() + const tar = new TarWriter(); responses.parse.forEach((response, index) => { response.parse = { templateVariables: [], error: "", readme: new Uint8Array(), ...response.parse, - } as ParseComplete + } as ParseComplete; tar.addFile( `${index}.parse.protobuf`, Response.encode(response as Response).finish(), - ) - }) + ); + }); const fillResource = (resource: RecursivePartial) => { if (resource.agents) { @@ -470,8 +470,8 @@ const createTemplateVersionTar = async ( subdomain: false, url: "", ...app, - } as App - }) + } as App; + }); } return { apps: [], @@ -492,9 +492,9 @@ const createTemplateVersionTar = async ( troubleshootingUrl: "", token: randomUUID(), ...agent, - } as Agent + } as Agent; }, - ) + ); } return { agents: [], @@ -506,8 +506,8 @@ const createTemplateVersionTar = async ( name: "dev", type: "echo", ...resource, - } as Resource - } + } as Resource; + }; responses.apply.forEach((response, index) => { response.apply = { @@ -517,14 +517,14 @@ const createTemplateVersionTar = async ( parameters: [], gitAuthProviders: [], ...response.apply, - } as ApplyComplete - response.apply.resources = response.apply.resources?.map(fillResource) + } as ApplyComplete; + response.apply.resources = response.apply.resources?.map(fillResource); tar.addFile( `${index}.apply.protobuf`, Response.encode(response as Response).finish(), - ) - }) + ); + }); responses.plan.forEach((response, index) => { response.plan = { error: "", @@ -532,63 +532,63 @@ const createTemplateVersionTar = async ( parameters: [], gitAuthProviders: [], ...response.plan, - } as PlanComplete - response.plan.resources = response.plan.resources?.map(fillResource) + } as PlanComplete; + response.plan.resources = response.plan.resources?.map(fillResource); tar.addFile( `${index}.plan.protobuf`, Response.encode(response as Response).finish(), - ) - }) - const tarFile = await tar.write() + ); + }); + const tarFile = await tar.write(); return Buffer.from( tarFile instanceof Blob ? await tarFile.arrayBuffer() : tarFile, - ) -} + ); +}; const randomName = () => { - return randomUUID().slice(0, 8) -} + return randomUUID().slice(0, 8); +}; // Awaiter is a helper that allows you to wait for a callback to be called. // It is useful for waiting for events to occur. export class Awaiter { - private promise: Promise - private callback?: () => void + private promise: Promise; + private callback?: () => void; constructor() { - this.promise = new Promise((r) => (this.callback = r)) + this.promise = new Promise((r) => (this.callback = r)); } public done(): void { if (this.callback) { - this.callback() + this.callback(); } else { - this.promise = Promise.resolve() + this.promise = Promise.resolve(); } } public wait(): Promise { - return this.promise + return this.promise; } } export const createServer = async ( port: number, ): Promise> => { - const e = express() - await new Promise((r) => e.listen(port, r)) - return e -} + const e = express(); + await new Promise((r) => e.listen(port, r)); + return e; +}; const findSessionToken = async (page: Page): Promise => { - const cookies = await page.context().cookies() - const sessionCookie = cookies.find((c) => c.name === "coder_session_token") + const cookies = await page.context().cookies(); + const sessionCookie = cookies.find((c) => c.name === "coder_session_token"); if (!sessionCookie) { - throw new Error("session token not found") + throw new Error("session token not found"); } - return sessionCookie.value -} + return sessionCookie.value; +}; export const echoResponsesWithParameters = ( richParameters: RichParameter[], @@ -617,8 +617,8 @@ export const echoResponsesWithParameters = ( }, }, ], - } -} + }; +}; export const fillParameters = async ( page: Page, @@ -628,52 +628,52 @@ export const fillParameters = async ( for (const buildParameter of buildParameters) { const richParameter = richParameters.find( (richParam) => richParam.name === buildParameter.name, - ) + ); if (!richParameter) { throw new Error( "build parameter is expected to be present in rich parameter schema", - ) + ); } const parameterLabel = await page.waitForSelector( "[data-testid='parameter-field-" + richParameter.name + "']", { state: "visible" }, - ) + ); if (richParameter.type === "bool") { const parameterField = await parameterLabel.waitForSelector( "[data-testid='parameter-field-bool'] .MuiRadio-root input[value='" + buildParameter.value + "']", - ) - await parameterField.check() + ); + await parameterField.check(); } else if (richParameter.options.length > 0) { const parameterField = await parameterLabel.waitForSelector( "[data-testid='parameter-field-options'] .MuiRadio-root input[value='" + buildParameter.value + "']", - ) - await parameterField.check() + ); + await parameterField.check(); } else if (richParameter.type === "list(string)") { - throw new Error("not implemented yet") // FIXME + throw new Error("not implemented yet"); // FIXME } else { // text or number const parameterField = await parameterLabel.waitForSelector( "[data-testid='parameter-field-text'] input", - ) - await parameterField.fill(buildParameter.value) + ); + await parameterField.fill(buildParameter.value); } } -} +}; export const updateTemplate = async ( page: Page, templateName: string, responses?: EchoProvisionerResponses, ) => { - const tarball = await createTemplateVersionTar(responses) + const tarball = await createTemplateVersionTar(responses); - const sessionToken = await findSessionToken(page) + const sessionToken = await findSessionToken(page); const child = spawn( "go", [ @@ -695,23 +695,23 @@ export const updateTemplate = async ( CODER_URL: "http://localhost:3000", }, }, - ) + ); - const uploaded = new Awaiter() + const uploaded = new Awaiter(); child.on("exit", (code) => { if (code === 0) { - uploaded.done() - return + uploaded.done(); + return; } - throw new Error(`coder templates push failed with code ${code}`) - }) + throw new Error(`coder templates push failed with code ${code}`); + }); - child.stdin.write(tarball) - child.stdin.end() + child.stdin.write(tarball); + child.stdin.end(); - await uploaded.wait() -} + await uploaded.wait(); +}; export const updateWorkspace = async ( page: Page, @@ -721,22 +721,22 @@ export const updateWorkspace = async ( ) => { await page.goto("/@admin/" + workspaceName, { waitUntil: "domcontentloaded", - }) - await expect(page).toHaveURL("/@admin/" + workspaceName) + }); + await expect(page).toHaveURL("/@admin/" + workspaceName); - await page.getByTestId("workspace-update-button").click() - await page.getByTestId("confirm-button").click() + await page.getByTestId("workspace-update-button").click(); + await page.getByTestId("confirm-button").click(); - await fillParameters(page, richParameters, buildParameters) - await page.getByTestId("form-submit").click() + await fillParameters(page, richParameters, buildParameters); + await page.getByTestId("form-submit").click(); await page.waitForSelector( "span[data-testid='build-status'] >> text=Running", { state: "visible", }, - ) -} + ); +}; export const updateWorkspaceParameters = async ( page: Page, @@ -746,18 +746,18 @@ export const updateWorkspaceParameters = async ( ) => { await page.goto("/@admin/" + workspaceName + "/settings/parameters", { waitUntil: "domcontentloaded", - }) + }); await expect(page).toHaveURL( "/@admin/" + workspaceName + "/settings/parameters", - ) + ); - await fillParameters(page, richParameters, buildParameters) - await page.getByTestId("form-submit").click() + await fillParameters(page, richParameters, buildParameters); + await page.getByTestId("form-submit").click(); await page.waitForSelector( "span[data-testid='build-status'] >> text=Running", { state: "visible", }, - ) -} + ); +}; diff --git a/site/e2e/hooks.ts b/site/e2e/hooks.ts index b4f26e0ac2..cd5847ba96 100644 --- a/site/e2e/hooks.ts +++ b/site/e2e/hooks.ts @@ -1,12 +1,12 @@ -import { Page } from "@playwright/test" +import { Page } from "@playwright/test"; export const beforeCoderTest = async (page: Page) => { // eslint-disable-next-line no-console -- Show everything that was printed with console.log() - page.on("console", (msg) => console.log("[onConsole] " + msg.text())) + page.on("console", (msg) => console.log("[onConsole] " + msg.text())); page.on("request", (request) => { if (!isApiCall(request.url())) { - return + return; } // eslint-disable-next-line no-console -- Log HTTP requests for debugging purposes @@ -14,40 +14,40 @@ export const beforeCoderTest = async (page: Page) => { `[onRequest] method=${request.method()} url=${request.url()} postData=${ request.postData() ? request.postData() : "" }`, - ) - }) + ); + }); page.on("response", async (response) => { if (!isApiCall(response.url())) { - return + return; } const shouldLogResponse = !response.url().endsWith("/api/v2/deployment/config") && - !response.url().endsWith("/api/v2/debug/health") + !response.url().endsWith("/api/v2/debug/health"); - let responseText = "" + let responseText = ""; try { if (shouldLogResponse) { - const buffer = await response.body() - responseText = buffer.toString("utf-8") - responseText = responseText.replace(/\n$/g, "") + const buffer = await response.body(); + responseText = buffer.toString("utf-8"); + responseText = responseText.replace(/\n$/g, ""); } else { - responseText = "skipped..." + responseText = "skipped..."; } } catch (error) { - responseText = "not_available" + responseText = "not_available"; } // eslint-disable-next-line no-console -- Log HTTP requests for debugging purposes console.log( `[onResponse] url=${response.url()} status=${response.status()} body=${responseText}`, - ) - }) -} + ); + }); +}; const isApiCall = (urlString: string): boolean => { - const url = new URL(urlString) - const apiPath = "/api/v2" + const url = new URL(urlString); + const apiPath = "/api/v2"; - return url.pathname.startsWith(apiPath) -} + return url.pathname.startsWith(apiPath); +}; diff --git a/site/e2e/parameters.ts b/site/e2e/parameters.ts index 240eeb0f85..c1477fad4c 100644 --- a/site/e2e/parameters.ts +++ b/site/e2e/parameters.ts @@ -1,4 +1,4 @@ -import { RichParameter } from "./provisionerGenerated" +import { RichParameter } from "./provisionerGenerated"; // Rich parameters @@ -19,7 +19,7 @@ const emptyParameter: RichParameter = { displayName: "", order: 0, ephemeral: false, -} +}; // firstParameter is mutable string with a default value (parameter value not required). export const firstParameter: RichParameter = { @@ -33,7 +33,7 @@ export const firstParameter: RichParameter = { defaultValue: "123", mutable: true, order: 1, -} +}; // secondParameter is immutable string with a default value (parameter value not required). export const secondParameter: RichParameter = { @@ -45,7 +45,7 @@ export const secondParameter: RichParameter = { description: "This is second parameter.", defaultValue: "abc", order: 2, -} +}; // thirdParameter is mutable string with an empty default value (parameter value not required). export const thirdParameter: RichParameter = { @@ -57,7 +57,7 @@ export const thirdParameter: RichParameter = { defaultValue: "", mutable: true, order: 3, -} +}; // fourthParameter is immutable boolean with a default "true" value (parameter value not required). export const fourthParameter: RichParameter = { @@ -68,7 +68,7 @@ export const fourthParameter: RichParameter = { description: "This is fourth parameter.", defaultValue: "true", order: 3, -} +}; // fifthParameter is immutable "string with options", with a default option selected (parameter value not required). export const fifthParameter: RichParameter = { @@ -100,7 +100,7 @@ export const fifthParameter: RichParameter = { description: "This is fifth parameter.", defaultValue: "def", order: 3, -} +}; // sixthParameter is mutable string without a default value (parameter value is required). export const sixthParameter: RichParameter = { @@ -114,7 +114,7 @@ export const sixthParameter: RichParameter = { required: true, mutable: true, order: 1, -} +}; // seventhParameter is immutable string without a default value (parameter value is required). export const seventhParameter: RichParameter = { @@ -126,7 +126,7 @@ export const seventhParameter: RichParameter = { description: "This is seventh parameter.", required: true, order: 1, -} +}; // Build options @@ -141,7 +141,7 @@ export const firstBuildOption: RichParameter = { defaultValue: "ABCDEF", mutable: true, ephemeral: true, -} +}; export const secondBuildOption: RichParameter = { ...emptyParameter, @@ -153,4 +153,4 @@ export const secondBuildOption: RichParameter = { defaultValue: "false", mutable: true, ephemeral: true, -} +}; diff --git a/site/e2e/playwright.config.ts b/site/e2e/playwright.config.ts index 42e1ad4211..359a5bca9b 100644 --- a/site/e2e/playwright.config.ts +++ b/site/e2e/playwright.config.ts @@ -1,18 +1,18 @@ -import { defineConfig } from "@playwright/test" -import path from "path" -import { defaultPort, gitAuth } from "./constants" +import { defineConfig } from "@playwright/test"; +import path from "path"; +import { defaultPort, gitAuth } from "./constants"; export const port = process.env.CODER_E2E_PORT ? Number(process.env.CODER_E2E_PORT) - : defaultPort + : defaultPort; -const coderMain = path.join(__dirname, "../../enterprise/cmd/coder/main.go") +const coderMain = path.join(__dirname, "../../enterprise/cmd/coder/main.go"); -export const STORAGE_STATE = path.join(__dirname, ".auth.json") +export const STORAGE_STATE = path.join(__dirname, ".auth.json"); const localURL = (port: number, path: string): string => { - return `http://localhost:${port}${path}` -} + return `http://localhost:${port}${path}`; +}; export default defineConfig({ projects: [ @@ -92,4 +92,4 @@ export default defineConfig({ }, reuseExistingServer: false, }, -}) +}); diff --git a/site/e2e/pom/BasePom.ts b/site/e2e/pom/BasePom.ts index 69641c6b2e..771181ed5e 100644 --- a/site/e2e/pom/BasePom.ts +++ b/site/e2e/pom/BasePom.ts @@ -1,17 +1,17 @@ -import { Page } from "@playwright/test" +import { Page } from "@playwright/test"; export abstract class BasePom { - protected readonly baseURL: string | undefined - protected readonly path: string - protected readonly page: Page + protected readonly baseURL: string | undefined; + protected readonly path: string; + protected readonly page: Page; constructor(baseURL: string | undefined, path: string, page: Page) { - this.baseURL = baseURL - this.path = path - this.page = page + this.baseURL = baseURL; + this.path = path; + this.page = page; } get url(): string { - return this.baseURL + this.path + return this.baseURL + this.path; } } diff --git a/site/e2e/pom/SignInPage.ts b/site/e2e/pom/SignInPage.ts index 362674588f..9b24793f9a 100644 --- a/site/e2e/pom/SignInPage.ts +++ b/site/e2e/pom/SignInPage.ts @@ -1,17 +1,17 @@ -import { Page } from "@playwright/test" -import { BasePom } from "./BasePom" +import { Page } from "@playwright/test"; +import { BasePom } from "./BasePom"; export class SignInPage extends BasePom { constructor(baseURL: string | undefined, page: Page) { - super(baseURL, "/login", page) + super(baseURL, "/login", page); } async submitBuiltInAuthentication( email: string, password: string, ): Promise { - await this.page.fill("text=Email", email) - await this.page.fill("text=Password", password) - await this.page.click('button:has-text("Sign In")') + await this.page.fill("text=Email", email); + await this.page.fill("text=Password", password); + await this.page.click('button:has-text("Sign In")'); } } diff --git a/site/e2e/pom/WorkspacesPage.ts b/site/e2e/pom/WorkspacesPage.ts index 33f2dd38f1..9c2bae81d2 100644 --- a/site/e2e/pom/WorkspacesPage.ts +++ b/site/e2e/pom/WorkspacesPage.ts @@ -1,8 +1,8 @@ -import { Page } from "@playwright/test" -import { BasePom } from "./BasePom" +import { Page } from "@playwright/test"; +import { BasePom } from "./BasePom"; export class WorkspacesPage extends BasePom { constructor(baseURL: string | undefined, page: Page, params?: string) { - super(baseURL, `/workspaces${params && params}`, page) + super(baseURL, `/workspaces${params && params}`, page); } } diff --git a/site/e2e/pom/index.ts b/site/e2e/pom/index.ts index b050895f83..3fbca5e88f 100644 --- a/site/e2e/pom/index.ts +++ b/site/e2e/pom/index.ts @@ -1,2 +1,2 @@ -export * from "./SignInPage" -export * from "./WorkspacesPage" +export * from "./SignInPage"; +export * from "./WorkspacesPage"; diff --git a/site/e2e/provisionerGenerated.ts b/site/e2e/provisionerGenerated.ts index 67ac497453..cccb4405f0 100644 --- a/site/e2e/provisionerGenerated.ts +++ b/site/e2e/provisionerGenerated.ts @@ -1,8 +1,8 @@ /* eslint-disable */ -import * as _m0 from "protobufjs/minimal" -import { Observable } from "rxjs" +import * as _m0 from "protobufjs/minimal"; +import { Observable } from "rxjs"; -export const protobufPackage = "provisioner" +export const protobufPackage = "provisioner"; /** LogLevel represents severity of the log. */ export enum LogLevel { @@ -34,114 +34,114 @@ export interface Empty {} /** TemplateVariable represents a Terraform variable. */ export interface TemplateVariable { - name: string - description: string - type: string - defaultValue: string - required: boolean - sensitive: boolean + name: string; + description: string; + type: string; + defaultValue: string; + required: boolean; + sensitive: boolean; } /** RichParameterOption represents a singular option that a parameter may expose. */ export interface RichParameterOption { - name: string - description: string - value: string - icon: string + name: string; + description: string; + value: string; + icon: string; } /** RichParameter represents a variable that is exposed. */ export interface RichParameter { - name: string - description: string - type: string - mutable: boolean - defaultValue: string - icon: string - options: RichParameterOption[] - validationRegex: string - validationError: string - validationMin?: number | undefined - validationMax?: number | undefined - validationMonotonic: string - required: boolean + name: string; + description: string; + type: string; + mutable: boolean; + defaultValue: string; + icon: string; + options: RichParameterOption[]; + validationRegex: string; + validationError: string; + validationMin?: number | undefined; + validationMax?: number | undefined; + validationMonotonic: string; + required: boolean; /** legacy_variable_name was removed (= 14) */ - displayName: string - order: number - ephemeral: boolean + displayName: string; + order: number; + ephemeral: boolean; } /** RichParameterValue holds the key/value mapping of a parameter. */ export interface RichParameterValue { - name: string - value: string + name: string; + value: string; } /** VariableValue holds the key/value mapping of a Terraform variable. */ export interface VariableValue { - name: string - value: string - sensitive: boolean + name: string; + value: string; + sensitive: boolean; } /** Log represents output from a request. */ export interface Log { - level: LogLevel - output: string + level: LogLevel; + output: string; } export interface InstanceIdentityAuth { - instanceId: string + instanceId: string; } export interface GitAuthProvider { - id: string - accessToken: string + id: string; + accessToken: string; } /** Agent represents a running agent on the workspace. */ export interface Agent { - id: string - name: string - env: { [key: string]: string } - startupScript: string - operatingSystem: string - architecture: string - directory: string - apps: App[] - token?: string | undefined - instanceId?: string | undefined - connectionTimeoutSeconds: number - troubleshootingUrl: string - motdFile: string + id: string; + name: string; + env: { [key: string]: string }; + startupScript: string; + operatingSystem: string; + architecture: string; + directory: string; + apps: App[]; + token?: string | undefined; + instanceId?: string | undefined; + connectionTimeoutSeconds: number; + troubleshootingUrl: string; + motdFile: string; /** Field 14 was bool login_before_ready = 14, now removed. */ - startupScriptTimeoutSeconds: number - shutdownScript: string - shutdownScriptTimeoutSeconds: number - metadata: Agent_Metadata[] - startupScriptBehavior: string - displayApps: DisplayApps | undefined + startupScriptTimeoutSeconds: number; + shutdownScript: string; + shutdownScriptTimeoutSeconds: number; + metadata: Agent_Metadata[]; + startupScriptBehavior: string; + displayApps: DisplayApps | undefined; } export interface Agent_Metadata { - key: string - displayName: string - script: string - interval: number - timeout: number + key: string; + displayName: string; + script: string; + interval: number; + timeout: number; } export interface Agent_EnvEntry { - key: string - value: string + key: string; + value: string; } export interface DisplayApps { - vscode: boolean - vscodeInsiders: boolean - webTerminal: boolean - sshHelper: boolean - portForwardingHelper: boolean + vscode: boolean; + vscodeInsiders: boolean; + webTerminal: boolean; + sshHelper: boolean; + portForwardingHelper: boolean; } /** App represents a dev-accessible application on the workspace. */ @@ -150,65 +150,65 @@ export interface App { * slug is the unique identifier for the app, usually the name from the * template. It must be URL-safe and hostname-safe. */ - slug: string - displayName: string - command: string - url: string - icon: string - subdomain: boolean - healthcheck: Healthcheck | undefined - sharingLevel: AppSharingLevel - external: boolean + slug: string; + displayName: string; + command: string; + url: string; + icon: string; + subdomain: boolean; + healthcheck: Healthcheck | undefined; + sharingLevel: AppSharingLevel; + external: boolean; } /** Healthcheck represents configuration for checking for app readiness. */ export interface Healthcheck { - url: string - interval: number - threshold: number + url: string; + interval: number; + threshold: number; } /** Resource represents created infrastructure. */ export interface Resource { - name: string - type: string - agents: Agent[] - metadata: Resource_Metadata[] - hide: boolean - icon: string - instanceType: string - dailyCost: number + name: string; + type: string; + agents: Agent[]; + metadata: Resource_Metadata[]; + hide: boolean; + icon: string; + instanceType: string; + dailyCost: number; } export interface Resource_Metadata { - key: string - value: string - sensitive: boolean - isNull: boolean + key: string; + value: string; + sensitive: boolean; + isNull: boolean; } /** Metadata is information about a workspace used in the execution of a build */ export interface Metadata { - coderUrl: string - workspaceTransition: WorkspaceTransition - workspaceName: string - workspaceOwner: string - workspaceId: string - workspaceOwnerId: string - workspaceOwnerEmail: string - templateName: string - templateVersion: string - workspaceOwnerOidcAccessToken: string - workspaceOwnerSessionToken: string + coderUrl: string; + workspaceTransition: WorkspaceTransition; + workspaceName: string; + workspaceOwner: string; + workspaceId: string; + workspaceOwnerId: string; + workspaceOwnerEmail: string; + templateName: string; + templateVersion: string; + workspaceOwnerOidcAccessToken: string; + workspaceOwnerSessionToken: string; } /** Config represents execution configuration shared by all subsequent requests in the Session */ export interface Config { /** template_source_archive is a tar of the template source files */ - templateSourceArchive: Uint8Array + templateSourceArchive: Uint8Array; /** state is the provisioner state (if any) */ - state: Uint8Array - provisionerLogLevel: string + state: Uint8Array; + provisionerLogLevel: string; } /** ParseRequest consumes source-code to produce inputs. */ @@ -216,25 +216,25 @@ export interface ParseRequest {} /** ParseComplete indicates a request to parse completed. */ export interface ParseComplete { - error: string - templateVariables: TemplateVariable[] - readme: Uint8Array + error: string; + templateVariables: TemplateVariable[]; + readme: Uint8Array; } /** PlanRequest asks the provisioner to plan what resources & parameters it will create */ export interface PlanRequest { - metadata: Metadata | undefined - richParameterValues: RichParameterValue[] - variableValues: VariableValue[] - gitAuthProviders: GitAuthProvider[] + metadata: Metadata | undefined; + richParameterValues: RichParameterValue[]; + variableValues: VariableValue[]; + gitAuthProviders: GitAuthProvider[]; } /** PlanComplete indicates a request to plan completed. */ export interface PlanComplete { - error: string - resources: Resource[] - parameters: RichParameter[] - gitAuthProviders: string[] + error: string; + resources: Resource[]; + parameters: RichParameter[]; + gitAuthProviders: string[]; } /** @@ -242,41 +242,41 @@ export interface PlanComplete { * in the same Session. The plan data is not transmitted over the wire and is cached by the provisioner in the Session. */ export interface ApplyRequest { - metadata: Metadata | undefined + metadata: Metadata | undefined; } /** ApplyComplete indicates a request to apply completed. */ export interface ApplyComplete { - state: Uint8Array - error: string - resources: Resource[] - parameters: RichParameter[] - gitAuthProviders: string[] + state: Uint8Array; + error: string; + resources: Resource[]; + parameters: RichParameter[]; + gitAuthProviders: string[]; } /** CancelRequest requests that the previous request be canceled gracefully. */ export interface CancelRequest {} export interface Request { - config?: Config | undefined - parse?: ParseRequest | undefined - plan?: PlanRequest | undefined - apply?: ApplyRequest | undefined - cancel?: CancelRequest | undefined + config?: Config | undefined; + parse?: ParseRequest | undefined; + plan?: PlanRequest | undefined; + apply?: ApplyRequest | undefined; + cancel?: CancelRequest | undefined; } export interface Response { - log?: Log | undefined - parse?: ParseComplete | undefined - plan?: PlanComplete | undefined - apply?: ApplyComplete | undefined + log?: Log | undefined; + parse?: ParseComplete | undefined; + plan?: PlanComplete | undefined; + apply?: ApplyComplete | undefined; } export const Empty = { encode(_: Empty, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - return writer + return writer; }, -} +}; export const TemplateVariable = { encode( @@ -284,26 +284,26 @@ export const TemplateVariable = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.name !== "") { - writer.uint32(10).string(message.name) + writer.uint32(10).string(message.name); } if (message.description !== "") { - writer.uint32(18).string(message.description) + writer.uint32(18).string(message.description); } if (message.type !== "") { - writer.uint32(26).string(message.type) + writer.uint32(26).string(message.type); } if (message.defaultValue !== "") { - writer.uint32(34).string(message.defaultValue) + writer.uint32(34).string(message.defaultValue); } if (message.required === true) { - writer.uint32(40).bool(message.required) + writer.uint32(40).bool(message.required); } if (message.sensitive === true) { - writer.uint32(48).bool(message.sensitive) + writer.uint32(48).bool(message.sensitive); } - return writer + return writer; }, -} +}; export const RichParameterOption = { encode( @@ -311,20 +311,20 @@ export const RichParameterOption = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.name !== "") { - writer.uint32(10).string(message.name) + writer.uint32(10).string(message.name); } if (message.description !== "") { - writer.uint32(18).string(message.description) + writer.uint32(18).string(message.description); } if (message.value !== "") { - writer.uint32(26).string(message.value) + writer.uint32(26).string(message.value); } if (message.icon !== "") { - writer.uint32(34).string(message.icon) + writer.uint32(34).string(message.icon); } - return writer + return writer; }, -} +}; export const RichParameter = { encode( @@ -332,56 +332,56 @@ export const RichParameter = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.name !== "") { - writer.uint32(10).string(message.name) + writer.uint32(10).string(message.name); } if (message.description !== "") { - writer.uint32(18).string(message.description) + writer.uint32(18).string(message.description); } if (message.type !== "") { - writer.uint32(26).string(message.type) + writer.uint32(26).string(message.type); } if (message.mutable === true) { - writer.uint32(32).bool(message.mutable) + writer.uint32(32).bool(message.mutable); } if (message.defaultValue !== "") { - writer.uint32(42).string(message.defaultValue) + writer.uint32(42).string(message.defaultValue); } if (message.icon !== "") { - writer.uint32(50).string(message.icon) + writer.uint32(50).string(message.icon); } for (const v of message.options) { - RichParameterOption.encode(v!, writer.uint32(58).fork()).ldelim() + RichParameterOption.encode(v!, writer.uint32(58).fork()).ldelim(); } if (message.validationRegex !== "") { - writer.uint32(66).string(message.validationRegex) + writer.uint32(66).string(message.validationRegex); } if (message.validationError !== "") { - writer.uint32(74).string(message.validationError) + writer.uint32(74).string(message.validationError); } if (message.validationMin !== undefined) { - writer.uint32(80).int32(message.validationMin) + writer.uint32(80).int32(message.validationMin); } if (message.validationMax !== undefined) { - writer.uint32(88).int32(message.validationMax) + writer.uint32(88).int32(message.validationMax); } if (message.validationMonotonic !== "") { - writer.uint32(98).string(message.validationMonotonic) + writer.uint32(98).string(message.validationMonotonic); } if (message.required === true) { - writer.uint32(104).bool(message.required) + writer.uint32(104).bool(message.required); } if (message.displayName !== "") { - writer.uint32(122).string(message.displayName) + writer.uint32(122).string(message.displayName); } if (message.order !== 0) { - writer.uint32(128).int32(message.order) + writer.uint32(128).int32(message.order); } if (message.ephemeral === true) { - writer.uint32(136).bool(message.ephemeral) + writer.uint32(136).bool(message.ephemeral); } - return writer + return writer; }, -} +}; export const RichParameterValue = { encode( @@ -389,14 +389,14 @@ export const RichParameterValue = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.name !== "") { - writer.uint32(10).string(message.name) + writer.uint32(10).string(message.name); } if (message.value !== "") { - writer.uint32(18).string(message.value) + writer.uint32(18).string(message.value); } - return writer + return writer; }, -} +}; export const VariableValue = { encode( @@ -404,29 +404,29 @@ export const VariableValue = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.name !== "") { - writer.uint32(10).string(message.name) + writer.uint32(10).string(message.name); } if (message.value !== "") { - writer.uint32(18).string(message.value) + writer.uint32(18).string(message.value); } if (message.sensitive === true) { - writer.uint32(24).bool(message.sensitive) + writer.uint32(24).bool(message.sensitive); } - return writer + return writer; }, -} +}; export const Log = { encode(message: Log, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.level !== 0) { - writer.uint32(8).int32(message.level) + writer.uint32(8).int32(message.level); } if (message.output !== "") { - writer.uint32(18).string(message.output) + writer.uint32(18).string(message.output); } - return writer + return writer; }, -} +}; export const InstanceIdentityAuth = { encode( @@ -434,11 +434,11 @@ export const InstanceIdentityAuth = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.instanceId !== "") { - writer.uint32(10).string(message.instanceId) + writer.uint32(10).string(message.instanceId); } - return writer + return writer; }, -} +}; export const GitAuthProvider = { encode( @@ -446,83 +446,83 @@ export const GitAuthProvider = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.id !== "") { - writer.uint32(10).string(message.id) + writer.uint32(10).string(message.id); } if (message.accessToken !== "") { - writer.uint32(18).string(message.accessToken) + writer.uint32(18).string(message.accessToken); } - return writer + return writer; }, -} +}; export const Agent = { encode(message: Agent, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.id !== "") { - writer.uint32(10).string(message.id) + writer.uint32(10).string(message.id); } if (message.name !== "") { - writer.uint32(18).string(message.name) + writer.uint32(18).string(message.name); } Object.entries(message.env).forEach(([key, value]) => { Agent_EnvEntry.encode( { key: key as any, value }, writer.uint32(26).fork(), - ).ldelim() - }) + ).ldelim(); + }); if (message.startupScript !== "") { - writer.uint32(34).string(message.startupScript) + writer.uint32(34).string(message.startupScript); } if (message.operatingSystem !== "") { - writer.uint32(42).string(message.operatingSystem) + writer.uint32(42).string(message.operatingSystem); } if (message.architecture !== "") { - writer.uint32(50).string(message.architecture) + writer.uint32(50).string(message.architecture); } if (message.directory !== "") { - writer.uint32(58).string(message.directory) + writer.uint32(58).string(message.directory); } for (const v of message.apps) { - App.encode(v!, writer.uint32(66).fork()).ldelim() + App.encode(v!, writer.uint32(66).fork()).ldelim(); } if (message.token !== undefined) { - writer.uint32(74).string(message.token) + writer.uint32(74).string(message.token); } if (message.instanceId !== undefined) { - writer.uint32(82).string(message.instanceId) + writer.uint32(82).string(message.instanceId); } if (message.connectionTimeoutSeconds !== 0) { - writer.uint32(88).int32(message.connectionTimeoutSeconds) + writer.uint32(88).int32(message.connectionTimeoutSeconds); } if (message.troubleshootingUrl !== "") { - writer.uint32(98).string(message.troubleshootingUrl) + writer.uint32(98).string(message.troubleshootingUrl); } if (message.motdFile !== "") { - writer.uint32(106).string(message.motdFile) + writer.uint32(106).string(message.motdFile); } if (message.startupScriptTimeoutSeconds !== 0) { - writer.uint32(120).int32(message.startupScriptTimeoutSeconds) + writer.uint32(120).int32(message.startupScriptTimeoutSeconds); } if (message.shutdownScript !== "") { - writer.uint32(130).string(message.shutdownScript) + writer.uint32(130).string(message.shutdownScript); } if (message.shutdownScriptTimeoutSeconds !== 0) { - writer.uint32(136).int32(message.shutdownScriptTimeoutSeconds) + writer.uint32(136).int32(message.shutdownScriptTimeoutSeconds); } for (const v of message.metadata) { - Agent_Metadata.encode(v!, writer.uint32(146).fork()).ldelim() + Agent_Metadata.encode(v!, writer.uint32(146).fork()).ldelim(); } if (message.startupScriptBehavior !== "") { - writer.uint32(154).string(message.startupScriptBehavior) + writer.uint32(154).string(message.startupScriptBehavior); } if (message.displayApps !== undefined) { DisplayApps.encode( message.displayApps, writer.uint32(162).fork(), - ).ldelim() + ).ldelim(); } - return writer + return writer; }, -} +}; export const Agent_Metadata = { encode( @@ -530,23 +530,23 @@ export const Agent_Metadata = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.key !== "") { - writer.uint32(10).string(message.key) + writer.uint32(10).string(message.key); } if (message.displayName !== "") { - writer.uint32(18).string(message.displayName) + writer.uint32(18).string(message.displayName); } if (message.script !== "") { - writer.uint32(26).string(message.script) + writer.uint32(26).string(message.script); } if (message.interval !== 0) { - writer.uint32(32).int64(message.interval) + writer.uint32(32).int64(message.interval); } if (message.timeout !== 0) { - writer.uint32(40).int64(message.timeout) + writer.uint32(40).int64(message.timeout); } - return writer + return writer; }, -} +}; export const Agent_EnvEntry = { encode( @@ -554,14 +554,14 @@ export const Agent_EnvEntry = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.key !== "") { - writer.uint32(10).string(message.key) + writer.uint32(10).string(message.key); } if (message.value !== "") { - writer.uint32(18).string(message.value) + writer.uint32(18).string(message.value); } - return writer + return writer; }, -} +}; export const DisplayApps = { encode( @@ -569,56 +569,59 @@ export const DisplayApps = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.vscode === true) { - writer.uint32(8).bool(message.vscode) + writer.uint32(8).bool(message.vscode); } if (message.vscodeInsiders === true) { - writer.uint32(16).bool(message.vscodeInsiders) + writer.uint32(16).bool(message.vscodeInsiders); } if (message.webTerminal === true) { - writer.uint32(24).bool(message.webTerminal) + writer.uint32(24).bool(message.webTerminal); } if (message.sshHelper === true) { - writer.uint32(32).bool(message.sshHelper) + writer.uint32(32).bool(message.sshHelper); } if (message.portForwardingHelper === true) { - writer.uint32(40).bool(message.portForwardingHelper) + writer.uint32(40).bool(message.portForwardingHelper); } - return writer + return writer; }, -} +}; export const App = { encode(message: App, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.slug !== "") { - writer.uint32(10).string(message.slug) + writer.uint32(10).string(message.slug); } if (message.displayName !== "") { - writer.uint32(18).string(message.displayName) + writer.uint32(18).string(message.displayName); } if (message.command !== "") { - writer.uint32(26).string(message.command) + writer.uint32(26).string(message.command); } if (message.url !== "") { - writer.uint32(34).string(message.url) + writer.uint32(34).string(message.url); } if (message.icon !== "") { - writer.uint32(42).string(message.icon) + writer.uint32(42).string(message.icon); } if (message.subdomain === true) { - writer.uint32(48).bool(message.subdomain) + writer.uint32(48).bool(message.subdomain); } if (message.healthcheck !== undefined) { - Healthcheck.encode(message.healthcheck, writer.uint32(58).fork()).ldelim() + Healthcheck.encode( + message.healthcheck, + writer.uint32(58).fork(), + ).ldelim(); } if (message.sharingLevel !== 0) { - writer.uint32(64).int32(message.sharingLevel) + writer.uint32(64).int32(message.sharingLevel); } if (message.external === true) { - writer.uint32(72).bool(message.external) + writer.uint32(72).bool(message.external); } - return writer + return writer; }, -} +}; export const Healthcheck = { encode( @@ -626,17 +629,17 @@ export const Healthcheck = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.url !== "") { - writer.uint32(10).string(message.url) + writer.uint32(10).string(message.url); } if (message.interval !== 0) { - writer.uint32(16).int32(message.interval) + writer.uint32(16).int32(message.interval); } if (message.threshold !== 0) { - writer.uint32(24).int32(message.threshold) + writer.uint32(24).int32(message.threshold); } - return writer + return writer; }, -} +}; export const Resource = { encode( @@ -644,32 +647,32 @@ export const Resource = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.name !== "") { - writer.uint32(10).string(message.name) + writer.uint32(10).string(message.name); } if (message.type !== "") { - writer.uint32(18).string(message.type) + writer.uint32(18).string(message.type); } for (const v of message.agents) { - Agent.encode(v!, writer.uint32(26).fork()).ldelim() + Agent.encode(v!, writer.uint32(26).fork()).ldelim(); } for (const v of message.metadata) { - Resource_Metadata.encode(v!, writer.uint32(34).fork()).ldelim() + Resource_Metadata.encode(v!, writer.uint32(34).fork()).ldelim(); } if (message.hide === true) { - writer.uint32(40).bool(message.hide) + writer.uint32(40).bool(message.hide); } if (message.icon !== "") { - writer.uint32(50).string(message.icon) + writer.uint32(50).string(message.icon); } if (message.instanceType !== "") { - writer.uint32(58).string(message.instanceType) + writer.uint32(58).string(message.instanceType); } if (message.dailyCost !== 0) { - writer.uint32(64).int32(message.dailyCost) + writer.uint32(64).int32(message.dailyCost); } - return writer + return writer; }, -} +}; export const Resource_Metadata = { encode( @@ -677,20 +680,20 @@ export const Resource_Metadata = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.key !== "") { - writer.uint32(10).string(message.key) + writer.uint32(10).string(message.key); } if (message.value !== "") { - writer.uint32(18).string(message.value) + writer.uint32(18).string(message.value); } if (message.sensitive === true) { - writer.uint32(24).bool(message.sensitive) + writer.uint32(24).bool(message.sensitive); } if (message.isNull === true) { - writer.uint32(32).bool(message.isNull) + writer.uint32(32).bool(message.isNull); } - return writer + return writer; }, -} +}; export const Metadata = { encode( @@ -698,41 +701,41 @@ export const Metadata = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.coderUrl !== "") { - writer.uint32(10).string(message.coderUrl) + writer.uint32(10).string(message.coderUrl); } if (message.workspaceTransition !== 0) { - writer.uint32(16).int32(message.workspaceTransition) + writer.uint32(16).int32(message.workspaceTransition); } if (message.workspaceName !== "") { - writer.uint32(26).string(message.workspaceName) + writer.uint32(26).string(message.workspaceName); } if (message.workspaceOwner !== "") { - writer.uint32(34).string(message.workspaceOwner) + writer.uint32(34).string(message.workspaceOwner); } if (message.workspaceId !== "") { - writer.uint32(42).string(message.workspaceId) + writer.uint32(42).string(message.workspaceId); } if (message.workspaceOwnerId !== "") { - writer.uint32(50).string(message.workspaceOwnerId) + writer.uint32(50).string(message.workspaceOwnerId); } if (message.workspaceOwnerEmail !== "") { - writer.uint32(58).string(message.workspaceOwnerEmail) + writer.uint32(58).string(message.workspaceOwnerEmail); } if (message.templateName !== "") { - writer.uint32(66).string(message.templateName) + writer.uint32(66).string(message.templateName); } if (message.templateVersion !== "") { - writer.uint32(74).string(message.templateVersion) + writer.uint32(74).string(message.templateVersion); } if (message.workspaceOwnerOidcAccessToken !== "") { - writer.uint32(82).string(message.workspaceOwnerOidcAccessToken) + writer.uint32(82).string(message.workspaceOwnerOidcAccessToken); } if (message.workspaceOwnerSessionToken !== "") { - writer.uint32(90).string(message.workspaceOwnerSessionToken) + writer.uint32(90).string(message.workspaceOwnerSessionToken); } - return writer + return writer; }, -} +}; export const Config = { encode( @@ -740,26 +743,26 @@ export const Config = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.templateSourceArchive.length !== 0) { - writer.uint32(10).bytes(message.templateSourceArchive) + writer.uint32(10).bytes(message.templateSourceArchive); } if (message.state.length !== 0) { - writer.uint32(18).bytes(message.state) + writer.uint32(18).bytes(message.state); } if (message.provisionerLogLevel !== "") { - writer.uint32(26).string(message.provisionerLogLevel) + writer.uint32(26).string(message.provisionerLogLevel); } - return writer + return writer; }, -} +}; export const ParseRequest = { encode( _: ParseRequest, writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { - return writer + return writer; }, -} +}; export const ParseComplete = { encode( @@ -767,17 +770,17 @@ export const ParseComplete = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.error !== "") { - writer.uint32(10).string(message.error) + writer.uint32(10).string(message.error); } for (const v of message.templateVariables) { - TemplateVariable.encode(v!, writer.uint32(18).fork()).ldelim() + TemplateVariable.encode(v!, writer.uint32(18).fork()).ldelim(); } if (message.readme.length !== 0) { - writer.uint32(26).bytes(message.readme) + writer.uint32(26).bytes(message.readme); } - return writer + return writer; }, -} +}; export const PlanRequest = { encode( @@ -785,20 +788,20 @@ export const PlanRequest = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.metadata !== undefined) { - Metadata.encode(message.metadata, writer.uint32(10).fork()).ldelim() + Metadata.encode(message.metadata, writer.uint32(10).fork()).ldelim(); } for (const v of message.richParameterValues) { - RichParameterValue.encode(v!, writer.uint32(18).fork()).ldelim() + RichParameterValue.encode(v!, writer.uint32(18).fork()).ldelim(); } for (const v of message.variableValues) { - VariableValue.encode(v!, writer.uint32(26).fork()).ldelim() + VariableValue.encode(v!, writer.uint32(26).fork()).ldelim(); } for (const v of message.gitAuthProviders) { - GitAuthProvider.encode(v!, writer.uint32(34).fork()).ldelim() + GitAuthProvider.encode(v!, writer.uint32(34).fork()).ldelim(); } - return writer + return writer; }, -} +}; export const PlanComplete = { encode( @@ -806,20 +809,20 @@ export const PlanComplete = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.error !== "") { - writer.uint32(10).string(message.error) + writer.uint32(10).string(message.error); } for (const v of message.resources) { - Resource.encode(v!, writer.uint32(18).fork()).ldelim() + Resource.encode(v!, writer.uint32(18).fork()).ldelim(); } for (const v of message.parameters) { - RichParameter.encode(v!, writer.uint32(26).fork()).ldelim() + RichParameter.encode(v!, writer.uint32(26).fork()).ldelim(); } for (const v of message.gitAuthProviders) { - writer.uint32(34).string(v!) + writer.uint32(34).string(v!); } - return writer + return writer; }, -} +}; export const ApplyRequest = { encode( @@ -827,11 +830,11 @@ export const ApplyRequest = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.metadata !== undefined) { - Metadata.encode(message.metadata, writer.uint32(10).fork()).ldelim() + Metadata.encode(message.metadata, writer.uint32(10).fork()).ldelim(); } - return writer + return writer; }, -} +}; export const ApplyComplete = { encode( @@ -839,32 +842,32 @@ export const ApplyComplete = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.state.length !== 0) { - writer.uint32(10).bytes(message.state) + writer.uint32(10).bytes(message.state); } if (message.error !== "") { - writer.uint32(18).string(message.error) + writer.uint32(18).string(message.error); } for (const v of message.resources) { - Resource.encode(v!, writer.uint32(26).fork()).ldelim() + Resource.encode(v!, writer.uint32(26).fork()).ldelim(); } for (const v of message.parameters) { - RichParameter.encode(v!, writer.uint32(34).fork()).ldelim() + RichParameter.encode(v!, writer.uint32(34).fork()).ldelim(); } for (const v of message.gitAuthProviders) { - writer.uint32(42).string(v!) + writer.uint32(42).string(v!); } - return writer + return writer; }, -} +}; export const CancelRequest = { encode( _: CancelRequest, writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { - return writer + return writer; }, -} +}; export const Request = { encode( @@ -872,23 +875,23 @@ export const Request = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.config !== undefined) { - Config.encode(message.config, writer.uint32(10).fork()).ldelim() + Config.encode(message.config, writer.uint32(10).fork()).ldelim(); } if (message.parse !== undefined) { - ParseRequest.encode(message.parse, writer.uint32(18).fork()).ldelim() + ParseRequest.encode(message.parse, writer.uint32(18).fork()).ldelim(); } if (message.plan !== undefined) { - PlanRequest.encode(message.plan, writer.uint32(26).fork()).ldelim() + PlanRequest.encode(message.plan, writer.uint32(26).fork()).ldelim(); } if (message.apply !== undefined) { - ApplyRequest.encode(message.apply, writer.uint32(34).fork()).ldelim() + ApplyRequest.encode(message.apply, writer.uint32(34).fork()).ldelim(); } if (message.cancel !== undefined) { - CancelRequest.encode(message.cancel, writer.uint32(42).fork()).ldelim() + CancelRequest.encode(message.cancel, writer.uint32(42).fork()).ldelim(); } - return writer + return writer; }, -} +}; export const Response = { encode( @@ -896,20 +899,20 @@ export const Response = { writer: _m0.Writer = _m0.Writer.create(), ): _m0.Writer { if (message.log !== undefined) { - Log.encode(message.log, writer.uint32(10).fork()).ldelim() + Log.encode(message.log, writer.uint32(10).fork()).ldelim(); } if (message.parse !== undefined) { - ParseComplete.encode(message.parse, writer.uint32(18).fork()).ldelim() + ParseComplete.encode(message.parse, writer.uint32(18).fork()).ldelim(); } if (message.plan !== undefined) { - PlanComplete.encode(message.plan, writer.uint32(26).fork()).ldelim() + PlanComplete.encode(message.plan, writer.uint32(26).fork()).ldelim(); } if (message.apply !== undefined) { - ApplyComplete.encode(message.apply, writer.uint32(34).fork()).ldelim() + ApplyComplete.encode(message.apply, writer.uint32(34).fork()).ldelim(); } - return writer + return writer; }, -} +}; export interface Provisioner { /** @@ -924,5 +927,5 @@ export interface Provisioner { * PlanRequest, or ApplyRequest. The provisioner MUST reply with a complete message corresponding to the request * that was canceled. If the provisioner has already completed the request, it may ignore the CancelRequest. */ - Session(request: Observable): Observable + Session(request: Observable): Observable; } diff --git a/site/e2e/reporter.ts b/site/e2e/reporter.ts index 9e6cd8f822..1cb5e34c66 100644 --- a/site/e2e/reporter.ts +++ b/site/e2e/reporter.ts @@ -1,4 +1,4 @@ -import fs from "fs" +import fs from "fs"; import type { FullConfig, Suite, @@ -6,18 +6,18 @@ import type { TestResult, FullResult, Reporter, -} from "@playwright/test/reporter" -import axios from "axios" +} from "@playwright/test/reporter"; +import axios from "axios"; class CoderReporter implements Reporter { onBegin(config: FullConfig, suite: Suite) { // eslint-disable-next-line no-console -- Helpful for debugging - console.log(`Starting the run with ${suite.allTests().length} tests`) + console.log(`Starting the run with ${suite.allTests().length} tests`); } onTestBegin(test: TestCase) { // eslint-disable-next-line no-console -- Helpful for debugging - console.log(`Starting test ${test.title}`) + console.log(`Starting test ${test.title}`); } onStdOut(chunk: string, test: TestCase, _: TestResult): void { @@ -27,7 +27,7 @@ class CoderReporter implements Reporter { /\n$/g, "", )}`, - ) + ); } onStdErr(chunk: string, test: TestCase, _: TestResult): void { @@ -37,50 +37,50 @@ class CoderReporter implements Reporter { /\n$/g, "", )}`, - ) + ); } async onTestEnd(test: TestCase, result: TestResult) { // eslint-disable-next-line no-console -- Helpful for debugging - console.log(`Finished test ${test.title}: ${result.status}`) + console.log(`Finished test ${test.title}: ${result.status}`); if (result.status !== "passed") { // eslint-disable-next-line no-console -- Helpful for debugging - console.log("errors", result.errors, "attachments", result.attachments) + console.log("errors", result.errors, "attachments", result.attachments); } - await exportDebugPprof(test.title) + await exportDebugPprof(test.title); } onEnd(result: FullResult) { // eslint-disable-next-line no-console -- Helpful for debugging - console.log(`Finished the run: ${result.status}`) + console.log(`Finished the run: ${result.status}`); } } const exportDebugPprof = async (testName: string) => { - const url = "http://127.0.0.1:6060/debug/pprof/goroutine?debug=1" - const outputFile = `test-results/debug-pprof-goroutine-${testName}.txt` + const url = "http://127.0.0.1:6060/debug/pprof/goroutine?debug=1"; + const outputFile = `test-results/debug-pprof-goroutine-${testName}.txt`; await axios .get(url) .then((response) => { if (response.status !== 200) { - throw new Error(`Error: Received status code ${response.status}`) + throw new Error(`Error: Received status code ${response.status}`); } fs.writeFile(outputFile, response.data, (err) => { if (err) { - throw new Error(`Error writing to ${outputFile}: ${err.message}`) + throw new Error(`Error writing to ${outputFile}: ${err.message}`); } else { // eslint-disable-next-line no-console -- Helpful for debugging - console.log(`Data from ${url} has been saved to ${outputFile}`) + console.log(`Data from ${url} has been saved to ${outputFile}`); } - }) + }); }) .catch((error) => { - throw new Error(`Error: ${error.message}`) - }) -} + throw new Error(`Error: ${error.message}`); + }); +}; // eslint-disable-next-line no-unused-vars -- Playwright config uses it -export default CoderReporter +export default CoderReporter; diff --git a/site/e2e/tests/app.spec.ts b/site/e2e/tests/app.spec.ts index 326d113353..24f893a4e6 100644 --- a/site/e2e/tests/app.spec.ts +++ b/site/e2e/tests/app.spec.ts @@ -1,31 +1,31 @@ -import { test } from "@playwright/test" -import { randomUUID } from "crypto" -import * as http from "http" +import { test } from "@playwright/test"; +import { randomUUID } from "crypto"; +import * as http from "http"; import { createTemplate, createWorkspace, startAgent, stopAgent, stopWorkspace, -} from "../helpers" -import { beforeCoderTest } from "../hooks" +} from "../helpers"; +import { beforeCoderTest } from "../hooks"; -test.beforeEach(async ({ page }) => await beforeCoderTest(page)) +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test("app", async ({ context, page }) => { - const appContent = "Hello World" - const token = randomUUID() + const appContent = "Hello World"; + const token = randomUUID(); const srv = http .createServer((req, res) => { - res.writeHead(200, { "Content-Type": "text/plain" }) - res.end(appContent) + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end(appContent); }) - .listen(0) - const addr = srv.address() + .listen(0); + const addr = srv.address(); if (typeof addr !== "object" || !addr) { - throw new Error("Expected addr to be an object") + throw new Error("Expected addr to be an object"); } - const appName = "test-app" + const appName = "test-app"; const template = await createTemplate(page, { apply: [ { @@ -48,17 +48,17 @@ test("app", async ({ context, page }) => { }, }, ], - }) - const workspaceName = await createWorkspace(page, template) - const agent = await startAgent(page, token) + }); + const workspaceName = await createWorkspace(page, template); + const agent = await startAgent(page, token); // Wait for the web terminal to open in a new tab - const pagePromise = context.waitForEvent("page") - await page.getByText(appName).click() - const app = await pagePromise - await app.waitForLoadState("domcontentloaded") - await app.getByText(appContent).isVisible() + const pagePromise = context.waitForEvent("page"); + await page.getByText(appName).click(); + const app = await pagePromise; + await app.waitForLoadState("domcontentloaded"); + await app.getByText(appContent).isVisible(); - await stopWorkspace(page, workspaceName) - await stopAgent(agent) -}) + await stopWorkspace(page, workspaceName); + await stopAgent(agent); +}); diff --git a/site/e2e/tests/createWorkspace.spec.ts b/site/e2e/tests/createWorkspace.spec.ts index c78a088348..c2b1e7cad7 100644 --- a/site/e2e/tests/createWorkspace.spec.ts +++ b/site/e2e/tests/createWorkspace.spec.ts @@ -1,10 +1,10 @@ -import { test } from "@playwright/test" +import { test } from "@playwright/test"; import { createTemplate, createWorkspace, echoResponsesWithParameters, verifyParameters, -} from "../helpers" +} from "../helpers"; import { secondParameter, @@ -14,11 +14,11 @@ import { thirdParameter, seventhParameter, sixthParameter, -} from "../parameters" -import { RichParameter } from "../provisionerGenerated" -import { beforeCoderTest } from "../hooks" +} from "../parameters"; +import { RichParameter } from "../provisionerGenerated"; +import { beforeCoderTest } from "../hooks"; -test.beforeEach(async ({ page }) => await beforeCoderTest(page)) +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test("create workspace", async ({ page }) => { const template = await createTemplate(page, { @@ -33,40 +33,40 @@ test("create workspace", async ({ page }) => { }, }, ], - }) - await createWorkspace(page, template) -}) + }); + await createWorkspace(page, template); +}); test("create workspace with default immutable parameters", async ({ page }) => { const richParameters: RichParameter[] = [ secondParameter, fourthParameter, fifthParameter, - ] + ]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), - ) - const workspaceName = await createWorkspace(page, template) + ); + const workspaceName = await createWorkspace(page, template); await verifyParameters(page, workspaceName, richParameters, [ { name: secondParameter.name, value: secondParameter.defaultValue }, { name: fourthParameter.name, value: fourthParameter.defaultValue }, { name: fifthParameter.name, value: fifthParameter.defaultValue }, - ]) -}) + ]); +}); test("create workspace with default mutable parameters", async ({ page }) => { - const richParameters: RichParameter[] = [firstParameter, thirdParameter] + const richParameters: RichParameter[] = [firstParameter, thirdParameter]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), - ) - const workspaceName = await createWorkspace(page, template) + ); + const workspaceName = await createWorkspace(page, template); await verifyParameters(page, workspaceName, richParameters, [ { name: firstParameter.name, value: firstParameter.defaultValue }, { name: thirdParameter.name, value: thirdParameter.defaultValue }, - ]) -}) + ]); +}); test("create workspace with default and required parameters", async ({ page, @@ -76,46 +76,46 @@ test("create workspace with default and required parameters", async ({ fourthParameter, sixthParameter, seventhParameter, - ] + ]; const buildParameters = [ { name: sixthParameter.name, value: "12345" }, { name: seventhParameter.name, value: "abcdef" }, - ] + ]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), - ) + ); const workspaceName = await createWorkspace( page, template, richParameters, buildParameters, - ) + ); await verifyParameters(page, workspaceName, richParameters, [ // user values: ...buildParameters, // default values: { name: secondParameter.name, value: secondParameter.defaultValue }, { name: fourthParameter.name, value: fourthParameter.defaultValue }, - ]) -}) + ]); +}); test("create workspace and overwrite default parameters", async ({ page }) => { - const richParameters: RichParameter[] = [secondParameter, fourthParameter] + const richParameters: RichParameter[] = [secondParameter, fourthParameter]; const buildParameters = [ { name: secondParameter.name, value: "AAAAA" }, { name: fourthParameter.name, value: "false" }, - ] + ]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), - ) + ); const workspaceName = await createWorkspace( page, template, richParameters, buildParameters, - ) - await verifyParameters(page, workspaceName, richParameters, buildParameters) -}) + ); + await verifyParameters(page, workspaceName, richParameters, buildParameters); +}); diff --git a/site/e2e/tests/gitAuth.spec.ts b/site/e2e/tests/gitAuth.spec.ts index 54d98edb64..1206561538 100644 --- a/site/e2e/tests/gitAuth.spec.ts +++ b/site/e2e/tests/gitAuth.spec.ts @@ -1,11 +1,11 @@ -import { test } from "@playwright/test" -import { gitAuth } from "../constants" -import { Endpoints } from "@octokit/types" -import { GitAuthDevice } from "api/typesGenerated" -import { Awaiter, createServer } from "../helpers" -import { beforeCoderTest } from "../hooks" +import { test } from "@playwright/test"; +import { gitAuth } from "../constants"; +import { Endpoints } from "@octokit/types"; +import { GitAuthDevice } from "api/typesGenerated"; +import { Awaiter, createServer } from "../helpers"; +import { beforeCoderTest } from "../hooks"; -test.beforeEach(async ({ page }) => await beforeCoderTest(page)) +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); // Ensures that a Git auth provider with the device flow functions and completes! test("git auth device", async ({ page }) => { @@ -15,71 +15,71 @@ test("git auth device", async ({ page }) => { expires_in: 900, interval: 1, verification_uri: "", - } + }; // Start a server to mock the GitHub API. - const srv = await createServer(gitAuth.devicePort) + const srv = await createServer(gitAuth.devicePort); srv.use(gitAuth.validatePath, (req, res) => { - res.write(JSON.stringify(ghUser)) - res.end() - }) + res.write(JSON.stringify(ghUser)); + res.end(); + }); srv.use(gitAuth.codePath, (req, res) => { - res.write(JSON.stringify(device)) - res.end() - }) + res.write(JSON.stringify(device)); + res.end(); + }); srv.use(gitAuth.installationsPath, (req, res) => { - res.write(JSON.stringify(ghInstall)) - res.end() - }) + res.write(JSON.stringify(ghInstall)); + res.end(); + }); const token = { access_token: "", error: "authorization_pending", error_description: "", - } + }; // First we send a result from the API that the token hasn't been // authorized yet to ensure the UI reacts properly. - const sentPending = new Awaiter() + const sentPending = new Awaiter(); srv.use(gitAuth.tokenPath, (req, res) => { - res.write(JSON.stringify(token)) - res.end() - sentPending.done() - }) + res.write(JSON.stringify(token)); + res.end(); + sentPending.done(); + }); await page.goto(`/gitauth/${gitAuth.deviceProvider}`, { waitUntil: "domcontentloaded", - }) - await page.getByText(device.user_code).isVisible() - await sentPending.wait() + }); + await page.getByText(device.user_code).isVisible(); + await sentPending.wait(); // Update the token to be valid and ensure the UI updates! - token.error = "" - token.access_token = "hello-world" - await page.waitForSelector("text=1 organization authorized") -}) + token.error = ""; + token.access_token = "hello-world"; + await page.waitForSelector("text=1 organization authorized"); +}); test("git auth web", async ({ baseURL, page }) => { - const srv = await createServer(gitAuth.webPort) + const srv = await createServer(gitAuth.webPort); // The GitHub validate endpoint returns the currently authenticated user! srv.use(gitAuth.validatePath, (req, res) => { - res.write(JSON.stringify(ghUser)) - res.end() - }) + res.write(JSON.stringify(ghUser)); + res.end(); + }); srv.use(gitAuth.tokenPath, (req, res) => { - res.write(JSON.stringify({ access_token: "hello-world" })) - res.end() - }) + res.write(JSON.stringify({ access_token: "hello-world" })); + res.end(); + }); srv.use(gitAuth.authPath, (req, res) => { res.redirect( `${baseURL}/gitauth/${gitAuth.webProvider}/callback?code=1234&state=` + req.query.state, - ) - }) + ); + }); await page.goto(`/gitauth/${gitAuth.webProvider}`, { waitUntil: "domcontentloaded", - }) + }); // This endpoint doesn't have the installations URL set intentionally! - await page.waitForSelector("text=You've authenticated with GitHub!") -}) + await page.waitForSelector("text=You've authenticated with GitHub!"); +}); const ghUser: Endpoints["GET /user"]["response"]["data"] = { login: "kylecarbs", @@ -115,7 +115,7 @@ const ghUser: Endpoints["GET /user"]["response"]["data"] = { following: 31, created_at: "2014-04-01T02:24:41Z", updated_at: "2023-06-26T13:03:09Z", -} +}; const ghInstall: Endpoints["GET /user/installations"]["response"]["data"] = { installations: [ @@ -140,4 +140,4 @@ const ghInstall: Endpoints["GET /user/installations"]["response"]["data"] = { }, ], total_count: 1, -} +}; diff --git a/site/e2e/tests/listTemplates.spec.ts b/site/e2e/tests/listTemplates.spec.ts index 2a8714f0c0..922d7215b7 100644 --- a/site/e2e/tests/listTemplates.spec.ts +++ b/site/e2e/tests/listTemplates.spec.ts @@ -1,9 +1,9 @@ -import { test, expect } from "@playwright/test" -import { beforeCoderTest } from "../hooks" +import { test, expect } from "@playwright/test"; +import { beforeCoderTest } from "../hooks"; -test.beforeEach(async ({ page }) => await beforeCoderTest(page)) +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test("list templates", async ({ page, baseURL }) => { - await page.goto(`${baseURL}/templates`, { waitUntil: "domcontentloaded" }) - await expect(page).toHaveTitle("Templates - Coder") -}) + await page.goto(`${baseURL}/templates`, { waitUntil: "domcontentloaded" }); + await expect(page).toHaveTitle("Templates - Coder"); +}); diff --git a/site/e2e/tests/outdatedAgent.spec.ts b/site/e2e/tests/outdatedAgent.spec.ts index 854ff58e3a..835ecfd82c 100644 --- a/site/e2e/tests/outdatedAgent.spec.ts +++ b/site/e2e/tests/outdatedAgent.spec.ts @@ -1,5 +1,5 @@ -import { test } from "@playwright/test" -import { randomUUID } from "crypto" +import { test } from "@playwright/test"; +import { randomUUID } from "crypto"; import { createTemplate, createWorkspace, @@ -8,15 +8,15 @@ import { startAgentWithCommand, stopAgent, stopWorkspace, -} from "../helpers" -import { beforeCoderTest } from "../hooks" +} from "../helpers"; +import { beforeCoderTest } from "../hooks"; -const agentVersion = "v0.14.0" +const agentVersion = "v0.14.0"; -test.beforeEach(async ({ page }) => await beforeCoderTest(page)) +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test("ssh with agent " + agentVersion, async ({ page }) => { - const token = randomUUID() + const token = randomUUID(); const template = await createTemplate(page, { apply: [ { @@ -33,28 +33,28 @@ test("ssh with agent " + agentVersion, async ({ page }) => { }, }, ], - }) - const workspaceName = await createWorkspace(page, template) - const binaryPath = await downloadCoderVersion(agentVersion) - const agent = await startAgentWithCommand(page, token, binaryPath) + }); + const workspaceName = await createWorkspace(page, template); + const binaryPath = await downloadCoderVersion(agentVersion); + const agent = await startAgentWithCommand(page, token, binaryPath); - const client = await sshIntoWorkspace(page, workspaceName) + const client = await sshIntoWorkspace(page, workspaceName); await new Promise((resolve, reject) => { // We just exec a command to be certain the agent is running! client.exec("exit 0", (err, stream) => { if (err) { - return reject(err) + return reject(err); } stream.on("exit", (code) => { if (code !== 0) { - return reject(new Error(`Command exited with code ${code}`)) + return reject(new Error(`Command exited with code ${code}`)); } - client.end() - resolve() - }) - }) - }) + client.end(); + resolve(); + }); + }); + }); - await stopWorkspace(page, workspaceName) - await stopAgent(agent, false) -}) + await stopWorkspace(page, workspaceName); + await stopAgent(agent, false); +}); diff --git a/site/e2e/tests/outdatedCLI.spec.ts b/site/e2e/tests/outdatedCLI.spec.ts index b07357a2ab..ae763ca2ff 100644 --- a/site/e2e/tests/outdatedCLI.spec.ts +++ b/site/e2e/tests/outdatedCLI.spec.ts @@ -1,5 +1,5 @@ -import { test } from "@playwright/test" -import { randomUUID } from "crypto" +import { test } from "@playwright/test"; +import { randomUUID } from "crypto"; import { createTemplate, createWorkspace, @@ -8,15 +8,15 @@ import { startAgent, stopAgent, stopWorkspace, -} from "../helpers" -import { beforeCoderTest } from "../hooks" +} from "../helpers"; +import { beforeCoderTest } from "../hooks"; -const clientVersion = "v0.14.0" +const clientVersion = "v0.14.0"; -test.beforeEach(async ({ page }) => await beforeCoderTest(page)) +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test("ssh with client " + clientVersion, async ({ page }) => { - const token = randomUUID() + const token = randomUUID(); const template = await createTemplate(page, { apply: [ { @@ -33,28 +33,28 @@ test("ssh with client " + clientVersion, async ({ page }) => { }, }, ], - }) - const workspaceName = await createWorkspace(page, template) - const agent = await startAgent(page, token) - const binaryPath = await downloadCoderVersion(clientVersion) + }); + const workspaceName = await createWorkspace(page, template); + const agent = await startAgent(page, token); + const binaryPath = await downloadCoderVersion(clientVersion); - const client = await sshIntoWorkspace(page, workspaceName, binaryPath) + const client = await sshIntoWorkspace(page, workspaceName, binaryPath); await new Promise((resolve, reject) => { // We just exec a command to be certain the agent is running! client.exec("exit 0", (err, stream) => { if (err) { - return reject(err) + return reject(err); } stream.on("exit", (code) => { if (code !== 0) { - return reject(new Error(`Command exited with code ${code}`)) + return reject(new Error(`Command exited with code ${code}`)); } - client.end() - resolve() - }) - }) - }) + client.end(); + resolve(); + }); + }); + }); - await stopWorkspace(page, workspaceName) - await stopAgent(agent) -}) + await stopWorkspace(page, workspaceName); + await stopAgent(agent); +}); diff --git a/site/e2e/tests/restartWorkspace.spec.ts b/site/e2e/tests/restartWorkspace.spec.ts index f7de9c624c..3d8fb704b6 100644 --- a/site/e2e/tests/restartWorkspace.spec.ts +++ b/site/e2e/tests/restartWorkspace.spec.ts @@ -1,48 +1,48 @@ -import { test } from "@playwright/test" +import { test } from "@playwright/test"; import { buildWorkspaceWithParameters, createTemplate, createWorkspace, echoResponsesWithParameters, verifyParameters, -} from "../helpers" +} from "../helpers"; -import { firstBuildOption, secondBuildOption } from "../parameters" -import { RichParameter } from "../provisionerGenerated" -import { beforeCoderTest } from "../hooks" +import { firstBuildOption, secondBuildOption } from "../parameters"; +import { RichParameter } from "../provisionerGenerated"; +import { beforeCoderTest } from "../hooks"; -test.beforeEach(async ({ page }) => await beforeCoderTest(page)) +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test("restart workspace with ephemeral parameters", async ({ page }) => { - const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption] + const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), - ) - const workspaceName = await createWorkspace(page, template) + ); + const workspaceName = await createWorkspace(page, template); // Verify that build options are default (not selected). await verifyParameters(page, workspaceName, richParameters, [ { name: firstBuildOption.name, value: firstBuildOption.defaultValue }, { name: secondBuildOption.name, value: secondBuildOption.defaultValue }, - ]) + ]); // Now, restart the workspace with ephemeral parameters selected. const buildParameters = [ { name: firstBuildOption.name, value: "AAAAA" }, { name: secondBuildOption.name, value: "true" }, - ] + ]; await buildWorkspaceWithParameters( page, workspaceName, richParameters, buildParameters, true, - ) + ); // Verify that build options are default (not selected). await verifyParameters(page, workspaceName, richParameters, [ { name: firstBuildOption.name, value: firstBuildOption.defaultValue }, { name: secondBuildOption.name, value: secondBuildOption.defaultValue }, - ]) -}) + ]); +}); diff --git a/site/e2e/tests/startWorkspace.spec.ts b/site/e2e/tests/startWorkspace.spec.ts index 232ac27299..ec22cda01d 100644 --- a/site/e2e/tests/startWorkspace.spec.ts +++ b/site/e2e/tests/startWorkspace.spec.ts @@ -1,4 +1,4 @@ -import { test } from "@playwright/test" +import { test } from "@playwright/test"; import { buildWorkspaceWithParameters, createTemplate, @@ -6,44 +6,44 @@ import { echoResponsesWithParameters, stopWorkspace, verifyParameters, -} from "../helpers" +} from "../helpers"; -import { firstBuildOption, secondBuildOption } from "../parameters" -import { RichParameter } from "../provisionerGenerated" +import { firstBuildOption, secondBuildOption } from "../parameters"; +import { RichParameter } from "../provisionerGenerated"; test("start workspace with ephemeral parameters", async ({ page }) => { - const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption] + const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), - ) - const workspaceName = await createWorkspace(page, template) + ); + const workspaceName = await createWorkspace(page, template); // Verify that build options are default (not selected). await verifyParameters(page, workspaceName, richParameters, [ { name: firstBuildOption.name, value: firstBuildOption.defaultValue }, { name: secondBuildOption.name, value: secondBuildOption.defaultValue }, - ]) + ]); // Stop the workspace - await stopWorkspace(page, workspaceName) + await stopWorkspace(page, workspaceName); // Now, start the workspace with ephemeral parameters selected. const buildParameters = [ { name: firstBuildOption.name, value: "AAAAA" }, { name: secondBuildOption.name, value: "true" }, - ] + ]; await buildWorkspaceWithParameters( page, workspaceName, richParameters, buildParameters, - ) + ); // Verify that build options are default (not selected). await verifyParameters(page, workspaceName, richParameters, [ { name: firstBuildOption.name, value: firstBuildOption.defaultValue }, { name: secondBuildOption.name, value: secondBuildOption.defaultValue }, - ]) -}) + ]); +}); diff --git a/site/e2e/tests/updateWorkspace.spec.ts b/site/e2e/tests/updateWorkspace.spec.ts index 6f26dd3fe6..b8e3d51d45 100644 --- a/site/e2e/tests/updateWorkspace.spec.ts +++ b/site/e2e/tests/updateWorkspace.spec.ts @@ -1,4 +1,4 @@ -import { test } from "@playwright/test" +import { test } from "@playwright/test"; import { createTemplate, @@ -8,7 +8,7 @@ import { updateWorkspace, updateWorkspaceParameters, verifyParameters, -} from "../helpers" +} from "../helpers"; import { fifthParameter, @@ -16,119 +16,119 @@ import { secondParameter, sixthParameter, secondBuildOption, -} from "../parameters" -import { RichParameter } from "../provisionerGenerated" -import { beforeCoderTest } from "../hooks" +} from "../parameters"; +import { RichParameter } from "../provisionerGenerated"; +import { beforeCoderTest } from "../hooks"; -test.beforeEach(async ({ page }) => await beforeCoderTest(page)) +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test("update workspace, new optional, immutable parameter added", async ({ page, }) => { - const richParameters: RichParameter[] = [firstParameter, secondParameter] + const richParameters: RichParameter[] = [firstParameter, secondParameter]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), - ) + ); - const workspaceName = await createWorkspace(page, template) + const workspaceName = await createWorkspace(page, template); // Verify that parameter values are default. await verifyParameters(page, workspaceName, richParameters, [ { name: firstParameter.name, value: firstParameter.defaultValue }, { name: secondParameter.name, value: secondParameter.defaultValue }, - ]) + ]); // Push updated template. - const updatedRichParameters = [...richParameters, fifthParameter] + const updatedRichParameters = [...richParameters, fifthParameter]; await updateTemplate( page, template, echoResponsesWithParameters(updatedRichParameters), - ) + ); // Now, update the workspace, and select the value for immutable parameter. await updateWorkspace(page, workspaceName, updatedRichParameters, [ { name: fifthParameter.name, value: fifthParameter.options[0].value }, - ]) + ]); // Verify parameter values. await verifyParameters(page, workspaceName, updatedRichParameters, [ { name: firstParameter.name, value: firstParameter.defaultValue }, { name: secondParameter.name, value: secondParameter.defaultValue }, { name: fifthParameter.name, value: fifthParameter.options[0].value }, - ]) -}) + ]); +}); test("update workspace, new required, mutable parameter added", async ({ page, }) => { - const richParameters: RichParameter[] = [firstParameter, secondParameter] + const richParameters: RichParameter[] = [firstParameter, secondParameter]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), - ) + ); - const workspaceName = await createWorkspace(page, template) + const workspaceName = await createWorkspace(page, template); // Verify that parameter values are default. await verifyParameters(page, workspaceName, richParameters, [ { name: firstParameter.name, value: firstParameter.defaultValue }, { name: secondParameter.name, value: secondParameter.defaultValue }, - ]) + ]); // Push updated template. - const updatedRichParameters = [...richParameters, sixthParameter] + const updatedRichParameters = [...richParameters, sixthParameter]; await updateTemplate( page, template, echoResponsesWithParameters(updatedRichParameters), - ) + ); // Now, update the workspace, and provide the parameter value. - const buildParameters = [{ name: sixthParameter.name, value: "99" }] + const buildParameters = [{ name: sixthParameter.name, value: "99" }]; await updateWorkspace( page, workspaceName, updatedRichParameters, buildParameters, - ) + ); // Verify parameter values. await verifyParameters(page, workspaceName, updatedRichParameters, [ { name: firstParameter.name, value: firstParameter.defaultValue }, { name: secondParameter.name, value: secondParameter.defaultValue }, ...buildParameters, - ]) -}) + ]); +}); test("update workspace with ephemeral parameter enabled", async ({ page }) => { - const richParameters: RichParameter[] = [firstParameter, secondBuildOption] + const richParameters: RichParameter[] = [firstParameter, secondBuildOption]; const template = await createTemplate( page, echoResponsesWithParameters(richParameters), - ) + ); - const workspaceName = await createWorkspace(page, template) + const workspaceName = await createWorkspace(page, template); // Verify that parameter values are default. await verifyParameters(page, workspaceName, richParameters, [ { name: firstParameter.name, value: firstParameter.defaultValue }, { name: secondBuildOption.name, value: secondBuildOption.defaultValue }, - ]) + ]); // Now, update the workspace, and select the value for ephemeral parameter. - const buildParameters = [{ name: secondBuildOption.name, value: "true" }] + const buildParameters = [{ name: secondBuildOption.name, value: "true" }]; await updateWorkspaceParameters( page, workspaceName, richParameters, buildParameters, - ) + ); // Verify that parameter values are default. await verifyParameters(page, workspaceName, richParameters, [ { name: firstParameter.name, value: firstParameter.defaultValue }, { name: secondBuildOption.name, value: secondBuildOption.defaultValue }, - ]) -}) + ]); +}); diff --git a/site/e2e/tests/webTerminal.spec.ts b/site/e2e/tests/webTerminal.spec.ts index 8858298dc2..9c4f283b80 100644 --- a/site/e2e/tests/webTerminal.spec.ts +++ b/site/e2e/tests/webTerminal.spec.ts @@ -1,17 +1,17 @@ -import { test } from "@playwright/test" +import { test } from "@playwright/test"; import { createTemplate, createWorkspace, startAgent, stopAgent, -} from "../helpers" -import { randomUUID } from "crypto" -import { beforeCoderTest } from "../hooks" +} from "../helpers"; +import { randomUUID } from "crypto"; +import { beforeCoderTest } from "../hooks"; -test.beforeEach(async ({ page }) => await beforeCoderTest(page)) +test.beforeEach(async ({ page }) => await beforeCoderTest(page)); test("web terminal", async ({ context, page }) => { - const token = randomUUID() + const token = randomUUID(); const template = await createTemplate(page, { apply: [ { @@ -31,29 +31,29 @@ test("web terminal", async ({ context, page }) => { }, }, ], - }) - await createWorkspace(page, template) - const agent = await startAgent(page, token) + }); + await createWorkspace(page, template); + const agent = await startAgent(page, token); // Wait for the web terminal to open in a new tab - const pagePromise = context.waitForEvent("page") - await page.getByTestId("terminal").click() - const terminal = await pagePromise - await terminal.waitForLoadState("domcontentloaded") + const pagePromise = context.waitForEvent("page"); + await page.getByTestId("terminal").click(); + const terminal = await pagePromise; + await terminal.waitForLoadState("domcontentloaded"); // Ensure that we can type in it - await terminal.keyboard.type("echo hello") - await terminal.keyboard.press("Enter") + await terminal.keyboard.type("echo hello"); + await terminal.keyboard.press("Enter"); - const locator = terminal.locator("text=hello") + const locator = terminal.locator("text=hello"); for (let i = 0; i < 10; i++) { - const items = await locator.all() + const items = await locator.all(); // Make sure the text came back if (items.length === 2) { - break + break; } - await new Promise((r) => setTimeout(r, 250)) + await new Promise((r) => setTimeout(r, 250)); } - await stopAgent(agent) -}) + await stopAgent(agent); +}); diff --git a/site/jest-runner-eslint.config.js b/site/jest-runner-eslint.config.js index b009f17201..5eda6aa9bd 100644 --- a/site/jest-runner-eslint.config.js +++ b/site/jest-runner-eslint.config.js @@ -1,5 +1,5 @@ // Toggle eslint --fix by specifying the `FIX` env. -const fix = !!process.env.FIX +const fix = !!process.env.FIX; module.exports = { cliOptions: { @@ -10,4 +10,4 @@ module.exports = { resolvePluginsRelativeTo: ".", maxWarnings: 0, }, -} +}; diff --git a/site/jest.config.ts b/site/jest.config.ts index a732cb78be..4dceb389ab 100644 --- a/site/jest.config.ts +++ b/site/jest.config.ts @@ -67,4 +67,4 @@ module.exports = { "!/out/**/*.*", "!/storybook-static/**/*.*", ], -} +}; diff --git a/site/jest.setup.ts b/site/jest.setup.ts index 3ecf3711a2..2ba4542720 100644 --- a/site/jest.setup.ts +++ b/site/jest.setup.ts @@ -1,16 +1,16 @@ -import "@testing-library/jest-dom" -import { cleanup } from "@testing-library/react" -import crypto from "crypto" -import { server } from "./src/testHelpers/server" -import "jest-location-mock" -import { TextEncoder, TextDecoder } from "util" -import { Blob } from "buffer" -import jestFetchMock from "jest-fetch-mock" -import { ProxyLatencyReport } from "contexts/useProxyLatency" -import { Region } from "api/typesGenerated" -import { useMemo } from "react" +import "@testing-library/jest-dom"; +import { cleanup } from "@testing-library/react"; +import crypto from "crypto"; +import { server } from "./src/testHelpers/server"; +import "jest-location-mock"; +import { TextEncoder, TextDecoder } from "util"; +import { Blob } from "buffer"; +import jestFetchMock from "jest-fetch-mock"; +import { ProxyLatencyReport } from "contexts/useProxyLatency"; +import { Region } from "api/typesGenerated"; +import { useMemo } from "react"; -jestFetchMock.enableMocks() +jestFetchMock.enableMocks(); // useProxyLatency does some http requests to determine latency. // This would fail unit testing, or at least make it very slow with @@ -21,7 +21,7 @@ jest.mock("contexts/useProxyLatency", () => ({ // Mocking the hook with a hook. const proxyLatencies = useMemo(() => { if (!proxies) { - return {} as Record + return {} as Record; } return proxies.reduce( (acc, proxy) => { @@ -31,49 +31,49 @@ jest.mock("contexts/useProxyLatency", () => ({ // If you make this random it could break stories. latencyMS: 8, at: new Date(), - } - return acc + }; + return acc; }, {} as Record, - ) - }, [proxies]) + ); + }, [proxies]); - return { proxyLatencies, refetch: jest.fn() } + return { proxyLatencies, refetch: jest.fn() }; }, -})) +})); -global.TextEncoder = TextEncoder +global.TextEncoder = TextEncoder; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom -global.TextDecoder = TextDecoder as any +global.TextDecoder = TextDecoder as any; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom -global.Blob = Blob as any +global.Blob = Blob as any; // Polyfill the getRandomValues that is used on utils/random.ts Object.defineProperty(global.self, "crypto", { value: { getRandomValues: function (buffer: Buffer) { - return crypto.randomFillSync(buffer) + return crypto.randomFillSync(buffer); }, }, -}) +}); // Establish API mocking before all tests through MSW. beforeAll(() => server.listen({ onUnhandledRequest: "warn", }), -) +); // Reset any request handlers that we may add during the tests, // so they don't affect other tests. afterEach(() => { - cleanup() - server.resetHandlers() - jest.clearAllMocks() -}) + cleanup(); + server.resetHandlers(); + jest.clearAllMocks(); +}); // Clean up after the tests are finished. -afterAll(() => server.close()) +afterAll(() => server.close()); // This is needed because we are compiling under `--isolatedModules` -export {} +export {}; diff --git a/site/src/@types/emoji-mart.d.ts b/site/src/@types/emoji-mart.d.ts index b1f3fab41b..07508bafcc 100644 --- a/site/src/@types/emoji-mart.d.ts +++ b/site/src/@types/emoji-mart.d.ts @@ -1,9 +1,9 @@ declare module "@emoji-mart/react" { const Picker: React.FC<{ - theme: "dark" | "light" - data: Record - onEmojiSelect: (emojiData: { unified: string }) => void - }> + theme: "dark" | "light"; + data: Record; + onEmojiSelect: (emojiData: { unified: string }) => void; + }>; - export default Picker + export default Picker; } diff --git a/site/src/@types/eventsourcemock.d.ts b/site/src/@types/eventsourcemock.d.ts index 12f9cc003e..296c4f19c3 100644 --- a/site/src/@types/eventsourcemock.d.ts +++ b/site/src/@types/eventsourcemock.d.ts @@ -1 +1 @@ -declare module "eventsourcemock" +declare module "eventsourcemock"; diff --git a/site/src/@types/i18n.d.ts b/site/src/@types/i18n.d.ts index bee7fa78b1..6de407a1e5 100644 --- a/site/src/@types/i18n.d.ts +++ b/site/src/@types/i18n.d.ts @@ -1,10 +1,10 @@ -import "i18next" +import "i18next"; // https://github.com/i18next/react-i18next/issues/1543#issuecomment-1528679591 declare module "i18next" { interface TypeOptions { - returnNull: false - allowObjectInHTMLChildren: false + returnNull: false; + allowObjectInHTMLChildren: false; } - export function t(s: string): T + export function t(s: string): T; } diff --git a/site/src/@types/mui.d.ts b/site/src/@types/mui.d.ts index c8a44d5891..3783826c01 100644 --- a/site/src/@types/mui.d.ts +++ b/site/src/@types/mui.d.ts @@ -1,4 +1,4 @@ -import { PaletteColor, PaletteColorOptions, Theme } from "@mui/material/styles" +import { PaletteColor, PaletteColorOptions, Theme } from "@mui/material/styles"; declare module "@mui/styles/defaultTheme" { interface DefaultTheme extends Theme {} @@ -6,20 +6,20 @@ declare module "@mui/styles/defaultTheme" { declare module "@mui/material/styles" { interface TypeBackground { - paperLight: string + paperLight: string; } interface Palette { - neutral: PaletteColor + neutral: PaletteColor; } interface PaletteOptions { - neutral?: PaletteColorOptions + neutral?: PaletteColorOptions; } } declare module "@mui/material/Button" { interface ButtonPropsColorOverrides { - neutral: true + neutral: true; } } diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index b470b66abd..3c32ac8e42 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -1,186 +1,188 @@ -import { FullScreenLoader } from "components/Loader/FullScreenLoader" -import { TemplateLayout } from "components/TemplateLayout/TemplateLayout" -import { UsersLayout } from "components/UsersLayout/UsersLayout" -import IndexPage from "pages" -import AuditPage from "pages/AuditPage/AuditPage" -import GroupsPage from "pages/GroupsPage/GroupsPage" -import LoginPage from "pages/LoginPage/LoginPage" -import { SetupPage } from "pages/SetupPage/SetupPage" -import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage" -import TemplatesPage from "pages/TemplatesPage/TemplatesPage" -import UsersPage from "pages/UsersPage/UsersPage" -import WorkspacesPage from "pages/WorkspacesPage/WorkspacesPage" -import { FC, lazy, Suspense } from "react" -import { Route, Routes, BrowserRouter as Router } from "react-router-dom" -import { DashboardLayout } from "./components/Dashboard/DashboardLayout" -import { RequireAuth } from "./components/RequireAuth/RequireAuth" -import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout" -import { DeploySettingsLayout } from "components/DeploySettingsLayout/DeploySettingsLayout" -import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout" -import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout" +import { FullScreenLoader } from "components/Loader/FullScreenLoader"; +import { TemplateLayout } from "components/TemplateLayout/TemplateLayout"; +import { UsersLayout } from "components/UsersLayout/UsersLayout"; +import IndexPage from "pages"; +import AuditPage from "pages/AuditPage/AuditPage"; +import GroupsPage from "pages/GroupsPage/GroupsPage"; +import LoginPage from "pages/LoginPage/LoginPage"; +import { SetupPage } from "pages/SetupPage/SetupPage"; +import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage"; +import TemplatesPage from "pages/TemplatesPage/TemplatesPage"; +import UsersPage from "pages/UsersPage/UsersPage"; +import WorkspacesPage from "pages/WorkspacesPage/WorkspacesPage"; +import { FC, lazy, Suspense } from "react"; +import { Route, Routes, BrowserRouter as Router } from "react-router-dom"; +import { DashboardLayout } from "./components/Dashboard/DashboardLayout"; +import { RequireAuth } from "./components/RequireAuth/RequireAuth"; +import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout"; +import { DeploySettingsLayout } from "components/DeploySettingsLayout/DeploySettingsLayout"; +import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout"; +import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"; // Lazy load pages // - Pages that are secondary, not in the main navigation or not usually accessed // - Pages that use heavy dependencies like charts or time libraries -const NotFoundPage = lazy(() => import("./pages/404Page/404Page")) +const NotFoundPage = lazy(() => import("./pages/404Page/404Page")); const CliAuthenticationPage = lazy( () => import("./pages/CliAuthPage/CliAuthPage"), -) +); const AccountPage = lazy( () => import("./pages/UserSettingsPage/AccountPage/AccountPage"), -) +); const SecurityPage = lazy( () => import("./pages/UserSettingsPage/SecurityPage/SecurityPage"), -) +); const SSHKeysPage = lazy( () => import("./pages/UserSettingsPage/SSHKeysPage/SSHKeysPage"), -) +); const TokensPage = lazy( () => import("./pages/UserSettingsPage/TokensPage/TokensPage"), -) +); const WorkspaceProxyPage = lazy( () => import("./pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyPage"), -) +); const CreateUserPage = lazy( () => import("./pages/CreateUserPage/CreateUserPage"), -) +); const WorkspaceBuildPage = lazy( () => import("./pages/WorkspaceBuildPage/WorkspaceBuildPage"), -) -const WorkspacePage = lazy(() => import("./pages/WorkspacePage/WorkspacePage")) +); +const WorkspacePage = lazy(() => import("./pages/WorkspacePage/WorkspacePage")); const WorkspaceSchedulePage = lazy( () => import( "./pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage" ), -) +); const WorkspaceParametersPage = lazy( () => import( "./pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage" ), -) -const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage")) +); +const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage")); const TemplatePermissionsPage = lazy( () => import( "./pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage" ), -) +); const TemplateSummaryPage = lazy( () => import("./pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage"), -) +); const CreateWorkspacePage = lazy( () => import("./pages/CreateWorkspacePage/CreateWorkspacePage"), -) -const CreateGroupPage = lazy(() => import("./pages/GroupsPage/CreateGroupPage")) -const GroupPage = lazy(() => import("./pages/GroupsPage/GroupPage")) +); +const CreateGroupPage = lazy( + () => import("./pages/GroupsPage/CreateGroupPage"), +); +const GroupPage = lazy(() => import("./pages/GroupsPage/GroupPage")); const SettingsGroupPage = lazy( () => import("./pages/GroupsPage/SettingsGroupPage"), -) +); const GeneralSettingsPage = lazy( () => import( "./pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage" ), -) +); const SecuritySettingsPage = lazy( () => import( "./pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage" ), -) +); const AppearanceSettingsPage = lazy( () => import( "./pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage" ), -) +); const UserAuthSettingsPage = lazy( () => import( "./pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPage" ), -) +); const GitAuthSettingsPage = lazy( () => import( "./pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPage" ), -) +); const NetworkSettingsPage = lazy( () => import( "./pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPage" ), -) -const GitAuthPage = lazy(() => import("./pages/GitAuthPage/GitAuthPage")) +); +const GitAuthPage = lazy(() => import("./pages/GitAuthPage/GitAuthPage")); const TemplateVersionPage = lazy( () => import("./pages/TemplateVersionPage/TemplateVersionPage"), -) +); const TemplateVersionEditorPage = lazy( () => import("./pages/TemplateVersionEditorPage/TemplateVersionEditorPage"), -) +); const StarterTemplatesPage = lazy( () => import("./pages/StarterTemplatesPage/StarterTemplatesPage"), -) +); const StarterTemplatePage = lazy( () => import("pages/StarterTemplatePage/StarterTemplatePage"), -) +); const CreateTemplatePage = lazy( () => import("./pages/CreateTemplatePage/CreateTemplatePage"), -) +); const TemplateVariablesPage = lazy( () => import( "./pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage" ), -) +); const WorkspaceSettingsPage = lazy( () => import("./pages/WorkspaceSettingsPage/WorkspaceSettingsPage"), -) +); const CreateTokenPage = lazy( () => import("./pages/CreateTokenPage/CreateTokenPage"), -) +); const TemplateDocsPage = lazy( () => import("./pages/TemplatePage/TemplateDocsPage/TemplateDocsPage"), -) +); const TemplateFilesPage = lazy( () => import("./pages/TemplatePage/TemplateFilesPage/TemplateFilesPage"), -) +); const TemplateVersionsPage = lazy( () => import("./pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage"), -) +); const TemplateSchedulePage = lazy( () => import( "./pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage" ), -) +); const LicensesSettingsPage = lazy( () => import( "./pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage" ), -) +); const AddNewLicensePage = lazy( () => import("./pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage"), -) +); const TemplateEmbedPage = lazy( () => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"), -) +); const TemplateInsightsPage = lazy( () => import("./pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage"), -) -const HealthPage = lazy(() => import("./pages/HealthPage/HealthPage")) +); +const HealthPage = lazy(() => import("./pages/HealthPage/HealthPage")); export const AppRouter: FC = () => { return ( @@ -331,5 +333,5 @@ export const AppRouter: FC = () => { - ) -} + ); +}; diff --git a/site/src/Main.tsx b/site/src/Main.tsx index 8d7068caca..a606c64bef 100644 --- a/site/src/Main.tsx +++ b/site/src/Main.tsx @@ -1,8 +1,8 @@ -import { inspect } from "@xstate/inspect" -import { createRoot } from "react-dom/client" -import { Interpreter } from "xstate" -import { App } from "./app" -import "./i18n" +import { inspect } from "@xstate/inspect"; +import { createRoot } from "react-dom/client"; +import { Interpreter } from "xstate"; +import { App } from "./app"; +import "./i18n"; // if this is a development build and the developer wants to inspect // helpful to see realtime changes on the services @@ -14,9 +14,9 @@ if ( inspect({ url: "https://stately.ai/viz?inspect", iframe: false, - }) + }); // configure all XServices to use the inspector - Interpreter.defaultOptions.devTools = true + Interpreter.defaultOptions.devTools = true; } // This is the entry point for the app - where everything start. @@ -28,13 +28,13 @@ const main = () => { ▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀ █▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █ ██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀ -`) - const element = document.getElementById("root") +`); + const element = document.getElementById("root"); if (element === null) { - throw new Error("root element is null") + throw new Error("root element is null"); } - const root = createRoot(element) - root.render() -} + const root = createRoot(element); + root.render(); +}; -main() +main(); diff --git a/site/src/__mocks__/js-untar.ts b/site/src/__mocks__/js-untar.ts index bbabe6d0d1..0bb2acf508 100644 --- a/site/src/__mocks__/js-untar.ts +++ b/site/src/__mocks__/js-untar.ts @@ -1 +1 @@ -export default jest.fn() +export default jest.fn(); diff --git a/site/src/__mocks__/monaco-editor.ts b/site/src/__mocks__/monaco-editor.ts index 7a5bc456b4..bc96406f8b 100644 --- a/site/src/__mocks__/monaco-editor.ts +++ b/site/src/__mocks__/monaco-editor.ts @@ -7,14 +7,14 @@ const editor = { dispose: () => { // }, - } + }; }, -} +}; const monaco = { editor, -} +}; -module.exports = monaco +module.exports = monaco; -export {} +export {}; diff --git a/site/src/__mocks__/react-markdown.tsx b/site/src/__mocks__/react-markdown.tsx index 1c91a935a5..f94c0fbe80 100644 --- a/site/src/__mocks__/react-markdown.tsx +++ b/site/src/__mocks__/react-markdown.tsx @@ -1,7 +1,7 @@ -import { FC, PropsWithChildren } from "react" +import { FC, PropsWithChildren } from "react"; const ReactMarkdown: FC> = ({ children }) => { - return
{children}
-} + return
{children}
; +}; -export default ReactMarkdown +export default ReactMarkdown; diff --git a/site/src/api/api.test.ts b/site/src/api/api.test.ts index 70ae81b129..11d00d216f 100644 --- a/site/src/api/api.test.ts +++ b/site/src/api/api.test.ts @@ -1,4 +1,4 @@ -import axios from "axios" +import axios from "axios"; import { MockTemplate, MockTemplateVersionParameter1, @@ -6,9 +6,9 @@ import { MockWorkspace, MockWorkspaceBuild, MockWorkspaceBuildParameter1, -} from "testHelpers/entities" -import * as api from "./api" -import * as TypesGen from "./typesGenerated" +} from "testHelpers/entities"; +import * as api from "./api"; +import * as TypesGen from "./typesGenerated"; describe("api.ts", () => { describe("login", () => { @@ -16,111 +16,111 @@ describe("api.ts", () => { // given const loginResponse: TypesGen.LoginWithPasswordResponse = { session_token: "abc_123_test", - } - jest.spyOn(axios, "post").mockResolvedValueOnce({ data: loginResponse }) + }; + jest.spyOn(axios, "post").mockResolvedValueOnce({ data: loginResponse }); // when - const result = await api.login("test", "123") + const result = await api.login("test", "123"); // then - expect(axios.post).toHaveBeenCalled() - expect(result).toStrictEqual(loginResponse) - }) + expect(axios.post).toHaveBeenCalled(); + expect(result).toStrictEqual(loginResponse); + }); it("should throw an error on 401", async () => { // given // ..ensure that we await our expect assertion in async/await test - expect.assertions(1) + expect.assertions(1); const expectedError = { message: "Validation failed", errors: [{ field: "email", code: "email" }], - } + }; const axiosMockPost = jest.fn().mockImplementationOnce(() => { - return Promise.reject(expectedError) - }) - axios.post = axiosMockPost + return Promise.reject(expectedError); + }); + axios.post = axiosMockPost; try { - await api.login("test", "123") + await api.login("test", "123"); } catch (error) { - expect(error).toStrictEqual(expectedError) + expect(error).toStrictEqual(expectedError); } - }) - }) + }); + }); describe("logout", () => { it("should return without erroring", async () => { // given const axiosMockPost = jest.fn().mockImplementationOnce(() => { - return Promise.resolve() - }) - axios.post = axiosMockPost + return Promise.resolve(); + }); + axios.post = axiosMockPost; // when - await api.logout() + await api.logout(); // then - expect(axiosMockPost).toHaveBeenCalled() - }) + expect(axiosMockPost).toHaveBeenCalled(); + }); it("should throw an error on 500", async () => { // given // ..ensure that we await our expect assertion in async/await test - expect.assertions(1) + expect.assertions(1); const expectedError = { message: "Failed to logout.", - } + }; const axiosMockPost = jest.fn().mockImplementationOnce(() => { - return Promise.reject(expectedError) - }) - axios.post = axiosMockPost + return Promise.reject(expectedError); + }); + axios.post = axiosMockPost; try { - await api.logout() + await api.logout(); } catch (error) { - expect(error).toStrictEqual(expectedError) + expect(error).toStrictEqual(expectedError); } - }) - }) + }); + }); describe("getApiKey", () => { it("should return APIKeyResponse", async () => { // given const apiKeyResponse: TypesGen.GenerateAPIKeyResponse = { key: "abc_123_test", - } + }; const axiosMockPost = jest.fn().mockImplementationOnce(() => { - return Promise.resolve({ data: apiKeyResponse }) - }) - axios.post = axiosMockPost + return Promise.resolve({ data: apiKeyResponse }); + }); + axios.post = axiosMockPost; // when - const result = await api.getApiKey() + const result = await api.getApiKey(); // then - expect(axiosMockPost).toHaveBeenCalled() - expect(result).toStrictEqual(apiKeyResponse) - }) + expect(axiosMockPost).toHaveBeenCalled(); + expect(result).toStrictEqual(apiKeyResponse); + }); it("should throw an error on 401", async () => { // given // ..ensure that we await our expect assertion in async/await test - expect.assertions(1) + expect.assertions(1); const expectedError = { message: "No Cookie!", - } + }; const axiosMockPost = jest.fn().mockImplementationOnce(() => { - return Promise.reject(expectedError) - }) - axios.post = axiosMockPost + return Promise.reject(expectedError); + }); + axios.post = axiosMockPost; try { - await api.getApiKey() + await api.getApiKey(); } catch (error) { - expect(error).toStrictEqual(expectedError) + expect(error).toStrictEqual(expectedError); } - }) - }) + }); + }); describe("getURLWithSearchParams - workspaces", () => { it.each<[string, TypesGen.WorkspaceFilter | undefined, string]>([ @@ -141,10 +141,10 @@ describe("api.ts", () => { ])( `Workspaces - getURLWithSearchParams(%p, %p) returns %p`, (basePath, filter, expected) => { - expect(api.getURLWithSearchParams(basePath, filter)).toBe(expected) + expect(api.getURLWithSearchParams(basePath, filter)).toBe(expected); }, - ) - }) + ); + }); describe("getURLWithSearchParams - users", () => { it.each<[string, TypesGen.UsersRequest | undefined, string]>([ @@ -158,72 +158,72 @@ describe("api.ts", () => { ])( `Users - getURLWithSearchParams(%p, %p) returns %p`, (basePath, filter, expected) => { - expect(api.getURLWithSearchParams(basePath, filter)).toBe(expected) + expect(api.getURLWithSearchParams(basePath, filter)).toBe(expected); }, - ) - }) + ); + }); describe("update", () => { it("creates a build with start and the latest template", async () => { jest .spyOn(api, "postWorkspaceBuild") - .mockResolvedValueOnce(MockWorkspaceBuild) - jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate) - await api.updateWorkspace(MockWorkspace) + .mockResolvedValueOnce(MockWorkspaceBuild); + jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate); + await api.updateWorkspace(MockWorkspace); expect(api.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, { transition: "start", template_version_id: MockTemplate.active_version_id, rich_parameter_values: [], - }) - }) + }); + }); it("fails when having missing parameters", async () => { jest .spyOn(api, "postWorkspaceBuild") - .mockResolvedValue(MockWorkspaceBuild) - jest.spyOn(api, "getTemplate").mockResolvedValue(MockTemplate) - jest.spyOn(api, "getWorkspaceBuildParameters").mockResolvedValue([]) + .mockResolvedValue(MockWorkspaceBuild); + jest.spyOn(api, "getTemplate").mockResolvedValue(MockTemplate); + jest.spyOn(api, "getWorkspaceBuildParameters").mockResolvedValue([]); jest .spyOn(api, "getTemplateVersionRichParameters") .mockResolvedValue([ MockTemplateVersionParameter1, { ...MockTemplateVersionParameter2, mutable: false }, - ]) + ]); - let error = new Error() + let error = new Error(); try { - await api.updateWorkspace(MockWorkspace) + await api.updateWorkspace(MockWorkspace); } catch (e) { - error = e as Error + error = e as Error; } - expect(error).toBeInstanceOf(api.MissingBuildParameters) + expect(error).toBeInstanceOf(api.MissingBuildParameters); // Verify if the correct missing parameters are being passed expect((error as api.MissingBuildParameters).parameters).toEqual([ MockTemplateVersionParameter1, { ...MockTemplateVersionParameter2, mutable: false }, - ]) - }) + ]); + }); it("creates a build with the no parameters if it is already filled", async () => { jest .spyOn(api, "postWorkspaceBuild") - .mockResolvedValueOnce(MockWorkspaceBuild) - jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate) + .mockResolvedValueOnce(MockWorkspaceBuild); + jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate); jest .spyOn(api, "getWorkspaceBuildParameters") - .mockResolvedValue([MockWorkspaceBuildParameter1]) + .mockResolvedValue([MockWorkspaceBuildParameter1]); jest .spyOn(api, "getTemplateVersionRichParameters") .mockResolvedValue([ { ...MockTemplateVersionParameter1, required: true, mutable: false }, - ]) - await api.updateWorkspace(MockWorkspace) + ]); + await api.updateWorkspace(MockWorkspace); expect(api.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, { transition: "start", template_version_id: MockTemplate.active_version_id, rich_parameter_values: [], - }) - }) - }) -}) + }); + }); + }); +}); diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 3567e4f977..8bc258e072 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1,17 +1,17 @@ -import axios from "axios" -import dayjs from "dayjs" -import * as Types from "./types" -import { DeploymentConfig } from "./types" -import * as TypesGen from "./typesGenerated" -import { delay } from "utils/delay" -import userAgentParser from "ua-parser-js" +import axios from "axios"; +import dayjs from "dayjs"; +import * as Types from "./types"; +import { DeploymentConfig } from "./types"; +import * as TypesGen from "./typesGenerated"; +import { delay } from "utils/delay"; +import userAgentParser from "ua-parser-js"; // Adds 304 for the default axios validateStatus function // https://github.com/axios/axios#handling-errors Check status here // https://httpstatusdogs.com/ axios.defaults.validateStatus = (status) => { - return (status >= 200 && status < 300) || status === 304 -} + return (status >= 200 && status < 300) || status === 304; +}; export const hardCodedCSRFCookie = (): string => { // This is a hard coded CSRF token/cookie pair for local development. In prod, @@ -21,10 +21,10 @@ export const hardCodedCSRFCookie = (): string => { // remote apis. The CSRF cookie for this token is // "JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=" const csrfToken = - "KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A==" - axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken - return csrfToken -} + "KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A=="; + axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken; + return csrfToken; +}; // withDefaultFeatures sets all unspecified features to not_entitled and // disabled. @@ -34,42 +34,42 @@ export const withDefaultFeatures = ( for (const feature of TypesGen.FeatureNames) { // Skip fields that are already filled. if (fs[feature] !== undefined) { - continue + continue; } fs[feature] = { enabled: false, entitlement: "not_entitled", - } + }; } - return fs as TypesGen.Entitlements["features"] -} + return fs as TypesGen.Entitlements["features"]; +}; // Always attach CSRF token to all requests. In puppeteer the document is // undefined. In those cases, just do nothing. const token = typeof document !== "undefined" ? document.head.querySelector('meta[property="csrf-token"]') - : null + : null; if (token !== null && token.getAttribute("content") !== null) { if (process.env.NODE_ENV === "development") { // Development mode uses a hard-coded CSRF token - axios.defaults.headers.common["X-CSRF-TOKEN"] = hardCodedCSRFCookie() - token.setAttribute("content", hardCodedCSRFCookie()) + axios.defaults.headers.common["X-CSRF-TOKEN"] = hardCodedCSRFCookie(); + token.setAttribute("content", hardCodedCSRFCookie()); } else { axios.defaults.headers.common["X-CSRF-TOKEN"] = - token.getAttribute("content") ?? "" + token.getAttribute("content") ?? ""; } } else { // Do not write error logs if we are in a FE unit test. if (process.env.JEST_WORKER_ID === undefined) { - console.error("CSRF token not found") + console.error("CSRF token not found"); } } const CONTENT_TYPE_JSON = { "Content-Type": "application/json", -} +}; export const provisioners: TypesGen.ProvisionerDaemon[] = [ { @@ -86,7 +86,7 @@ export const provisioners: TypesGen.ProvisionerDaemon[] = [ provisioners: [], tags: {}, }, -] +]; export const login = async ( email: string, @@ -95,7 +95,7 @@ export const login = async ( const payload = JSON.stringify({ email, password, - }) + }); const response = await axios.post( "/api/v2/users/login", @@ -103,51 +103,51 @@ export const login = async ( { headers: { ...CONTENT_TYPE_JSON }, }, - ) + ); - return response.data -} + return response.data; +}; export const convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => { const response = await axios.post( "/api/v2/users/me/convert-login", request, - ) - return response.data -} + ); + return response.data; +}; export const logout = async (): Promise => { - await axios.post("/api/v2/users/logout") -} + await axios.post("/api/v2/users/logout"); +}; export const getAuthenticatedUser = async (): Promise< TypesGen.User | undefined > => { try { - const response = await axios.get("/api/v2/users/me") - return response.data + const response = await axios.get("/api/v2/users/me"); + return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { - return undefined + return undefined; } - throw error + throw error; } -} +}; export const getAuthMethods = async (): Promise => { const response = await axios.get( "/api/v2/users/authmethods", - ) - return response.data -} + ); + return response.data; +}; export const getUserLoginType = async (): Promise => { const response = await axios.get( "/api/v2/users/me/login-type", - ) - return response.data -} + ); + return response.data; +}; export const checkAuthorization = async ( params: TypesGen.AuthorizationRequest, @@ -155,16 +155,16 @@ export const checkAuthorization = async ( const response = await axios.post( `/api/v2/authcheck`, params, - ) - return response.data -} + ); + return response.data; +}; export const getApiKey = async (): Promise => { const response = await axios.post( "/api/v2/users/me/keys", - ) - return response.data -} + ); + return response.data; +}; export const getTokens = async ( params: TypesGen.TokensFilter, @@ -174,67 +174,67 @@ export const getTokens = async ( { params, }, - ) - return response.data -} + ); + return response.data; +}; export const deleteToken = async (keyId: string): Promise => { - await axios.delete("/api/v2/users/me/keys/" + keyId) -} + await axios.delete("/api/v2/users/me/keys/" + keyId); +}; export const createToken = async ( params: TypesGen.CreateTokenRequest, ): Promise => { - const response = await axios.post(`/api/v2/users/me/keys/tokens`, params) - return response.data -} + const response = await axios.post(`/api/v2/users/me/keys/tokens`, params); + return response.data; +}; export const getTokenConfig = async (): Promise => { - const response = await axios.get("/api/v2/users/me/keys/tokens/tokenconfig") - return response.data -} + const response = await axios.get("/api/v2/users/me/keys/tokens/tokenconfig"); + return response.data; +}; export const getUsers = async ( options: TypesGen.UsersRequest, ): Promise => { - const url = getURLWithSearchParams("/api/v2/users", options) - const response = await axios.get(url.toString()) - return response.data -} + const url = getURLWithSearchParams("/api/v2/users", options); + const response = await axios.get(url.toString()); + return response.data; +}; export const getOrganization = async ( organizationId: string, ): Promise => { const response = await axios.get( `/api/v2/organizations/${organizationId}`, - ) - return response.data -} + ); + return response.data; +}; export const getOrganizations = async (): Promise => { const response = await axios.get( "/api/v2/users/me/organizations", - ) - return response.data -} + ); + return response.data; +}; export const getTemplate = async ( templateId: string, ): Promise => { const response = await axios.get( `/api/v2/templates/${templateId}`, - ) - return response.data -} + ); + return response.data; +}; export const getTemplates = async ( organizationId: string, ): Promise => { const response = await axios.get( `/api/v2/organizations/${organizationId}/templates`, - ) - return response.data -} + ); + return response.data; +}; export const getTemplateByName = async ( organizationId: string, @@ -242,45 +242,45 @@ export const getTemplateByName = async ( ): Promise => { const response = await axios.get( `/api/v2/organizations/${organizationId}/templates/${name}`, - ) - return response.data -} + ); + return response.data; +}; export const getTemplateVersion = async ( versionId: string, ): Promise => { const response = await axios.get( `/api/v2/templateversions/${versionId}`, - ) - return response.data -} + ); + return response.data; +}; export const getTemplateVersionResources = async ( versionId: string, ): Promise => { const response = await axios.get( `/api/v2/templateversions/${versionId}/resources`, - ) - return response.data -} + ); + return response.data; +}; export const getTemplateVersionVariables = async ( versionId: string, ): Promise => { const response = await axios.get( `/api/v2/templateversions/${versionId}/variables`, - ) - return response.data -} + ); + return response.data; +}; export const getTemplateVersions = async ( templateId: string, ): Promise => { const response = await axios.get( `/api/v2/templates/${templateId}/versions`, - ) - return response.data -} + ); + return response.data; +}; export const getTemplateVersionByName = async ( organizationId: string, @@ -289,13 +289,13 @@ export const getTemplateVersionByName = async ( ): Promise => { const response = await axios.get( `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}`, - ) - return response.data -} + ); + return response.data; +}; export type GetPreviousTemplateVersionByNameResponse = | TypesGen.TemplateVersion - | undefined + | undefined; export const getPreviousTemplateVersionByName = async ( organizationId: string, @@ -305,8 +305,8 @@ export const getPreviousTemplateVersionByName = async ( try { const response = await axios.get( `/api/v2/organizations/${organizationId}/templates/${templateName}/versions/${versionName}/previous`, - ) - return response.data + ); + return response.data; } catch (error) { // When there is no previous version, like the first version of a template, // the API returns 404 so in this case we can safely return undefined @@ -315,12 +315,12 @@ export const getPreviousTemplateVersionByName = async ( error.response && error.response.status === 404 ) { - return undefined + return undefined; } - throw error + throw error; } -} +}; export const createTemplateVersion = async ( organizationId: string, @@ -329,27 +329,27 @@ export const createTemplateVersion = async ( const response = await axios.post( `/api/v2/organizations/${organizationId}/templateversions`, data, - ) - return response.data -} + ); + return response.data; +}; export const getTemplateVersionGitAuth = async ( versionId: string, ): Promise => { const response = await axios.get( `/api/v2/templateversions/${versionId}/gitauth`, - ) - return response.data -} + ); + return response.data; +}; export const getTemplateVersionRichParameters = async ( versionId: string, ): Promise => { const response = await axios.get( `/api/v2/templateversions/${versionId}/rich-parameters`, - ) - return response.data -} + ); + return response.data; +}; export const createTemplate = async ( organizationId: string, @@ -358,9 +358,9 @@ export const createTemplate = async ( const response = await axios.post( `/api/v2/organizations/${organizationId}/templates`, data, - ) - return response.data -} + ); + return response.data; +}; export const updateActiveTemplateVersion = async ( templateId: string, @@ -369,9 +369,9 @@ export const updateActiveTemplateVersion = async ( const response = await axios.patch( `/api/v2/templates/${templateId}/versions`, data, - ) - return response.data -} + ); + return response.data; +}; export const patchTemplateVersion = async ( templateVersionId: string, @@ -380,9 +380,9 @@ export const patchTemplateVersion = async ( const response = await axios.patch( `/api/v2/templateversions/${templateVersionId}`, data, - ) - return response.data -} + ); + return response.data; +}; export const updateTemplateMeta = async ( templateId: string, @@ -391,18 +391,18 @@ export const updateTemplateMeta = async ( const response = await axios.patch( `/api/v2/templates/${templateId}`, data, - ) - return response.data -} + ); + return response.data; +}; export const deleteTemplate = async ( templateId: string, ): Promise => { const response = await axios.delete( `/api/v2/templates/${templateId}`, - ) - return response.data -} + ); + return response.data; +}; export const getWorkspace = async ( workspaceId: string, @@ -413,9 +413,9 @@ export const getWorkspace = async ( { params, }, - ) - return response.data -} + ); + return response.data; +}; /** * @@ -426,11 +426,11 @@ export const watchWorkspace = (workspaceId: string): EventSource => { return new EventSource( `${location.protocol}//${location.host}/api/v2/workspaces/${workspaceId}/watch`, { withCredentials: true }, - ) -} + ); +}; interface SearchParamOptions extends TypesGen.Pagination { - q?: string + q?: string; } export const getURLWithSearchParams = ( @@ -438,28 +438,28 @@ export const getURLWithSearchParams = ( options?: SearchParamOptions, ): string => { if (options) { - const searchParams = new URLSearchParams() - const keys = Object.keys(options) as (keyof SearchParamOptions)[] + const searchParams = new URLSearchParams(); + const keys = Object.keys(options) as (keyof SearchParamOptions)[]; keys.forEach((key) => { - const value = options[key] + const value = options[key]; if (value !== undefined && value !== "") { - searchParams.append(key, value.toString()) + searchParams.append(key, value.toString()); } - }) - const searchString = searchParams.toString() - return searchString ? `${basePath}?${searchString}` : basePath + }); + const searchString = searchParams.toString(); + return searchString ? `${basePath}?${searchString}` : basePath; } else { - return basePath + return basePath; } -} +}; export const getWorkspaces = async ( options: TypesGen.WorkspacesRequest, ): Promise => { - const url = getURLWithSearchParams("/api/v2/workspaces", options) - const response = await axios.get(url) - return response.data -} + const url = getURLWithSearchParams("/api/v2/workspaces", options); + const response = await axios.get(url); + return response.data; +}; export const getWorkspaceByOwnerAndName = async ( username = "me", @@ -471,14 +471,14 @@ export const getWorkspaceByOwnerAndName = async ( { params, }, - ) - return response.data -} + ); + return response.data; +}; export function waitForBuild(build: TypesGen.WorkspaceBuild) { return new Promise((res, reject) => { void (async () => { - let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined + let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined; while ( !["succeeded", "canceled"].some( @@ -489,19 +489,19 @@ export function waitForBuild(build: TypesGen.WorkspaceBuild) { build.workspace_owner_name, build.workspace_name, build.build_number, - ) - latestJobInfo = job + ); + latestJobInfo = job; if (latestJobInfo.status === "failed") { - return reject(latestJobInfo) + return reject(latestJobInfo); } - await delay(1000) + await delay(1000); } - return res(latestJobInfo) - })() - }) + return res(latestJobInfo); + })(); + }); } export const postWorkspaceBuild = async ( @@ -511,9 +511,9 @@ export const postWorkspaceBuild = async ( const response = await axios.post( `/api/v2/workspaces/${workspaceId}/builds`, data, - ) - return response.data -} + ); + return response.data; +}; export const startWorkspace = ( workspaceId: string, @@ -526,7 +526,7 @@ export const startWorkspace = ( template_version_id: templateVersionId, log_level: logLevel, rich_parameter_values: buildParameters, - }) + }); export const stopWorkspace = ( workspaceId: string, logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"], @@ -534,7 +534,7 @@ export const stopWorkspace = ( postWorkspaceBuild(workspaceId, { transition: "stop", log_level: logLevel, - }) + }); export const deleteWorkspace = ( workspaceId: string, @@ -543,16 +543,16 @@ export const deleteWorkspace = ( postWorkspaceBuild(workspaceId, { transition: "delete", log_level: logLevel, - }) + }); export const cancelWorkspaceBuild = async ( workspaceBuildId: TypesGen.WorkspaceBuild["id"], ): Promise => { const response = await axios.patch( `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`, - ) - return response.data -} + ); + return response.data; +}; export const updateWorkspaceDormancy = async ( workspaceId: string, @@ -560,28 +560,28 @@ export const updateWorkspaceDormancy = async ( ): Promise => { const data: TypesGen.UpdateWorkspaceDormancy = { dormant: dormant, - } + }; const response = await axios.put( `/api/v2/workspaces/${workspaceId}/dormant`, data, - ) - return response.data -} + ); + return response.data; +}; export const restartWorkspace = async ({ workspace, buildParameters, }: { - workspace: TypesGen.Workspace - buildParameters?: TypesGen.WorkspaceBuildParameter[] + workspace: TypesGen.Workspace; + buildParameters?: TypesGen.WorkspaceBuildParameter[]; }) => { - const stopBuild = await stopWorkspace(workspace.id) - const awaitedStopBuild = await waitForBuild(stopBuild) + const stopBuild = await stopWorkspace(workspace.id); + const awaitedStopBuild = await waitForBuild(stopBuild); // If the restart is canceled halfway through, make sure we bail if (awaitedStopBuild?.status === "canceled") { - return + return; } const startBuild = await startWorkspace( @@ -589,25 +589,25 @@ export const restartWorkspace = async ({ workspace.latest_build.template_version_id, undefined, buildParameters, - ) - await waitForBuild(startBuild) -} + ); + await waitForBuild(startBuild); +}; export const cancelTemplateVersionBuild = async ( templateVersionId: TypesGen.TemplateVersion["id"], ): Promise => { const response = await axios.patch( `/api/v2/templateversions/${templateVersionId}/cancel`, - ) - return response.data -} + ); + return response.data; +}; export const createUser = async ( user: TypesGen.CreateUserRequest, ): Promise => { - const response = await axios.post("/api/v2/users", user) - return response.data -} + const response = await axios.post("/api/v2/users", user); + return response.data; +}; export const createWorkspace = async ( organizationId: string, @@ -617,118 +617,118 @@ export const createWorkspace = async ( const response = await axios.post( `/api/v2/organizations/${organizationId}/members/${userId}/workspaces`, workspace, - ) - return response.data -} + ); + return response.data; +}; export const patchWorkspace = async ( workspaceId: string, data: TypesGen.UpdateWorkspaceRequest, ) => { - await axios.patch(`/api/v2/workspaces/${workspaceId}`, data) -} + await axios.patch(`/api/v2/workspaces/${workspaceId}`, data); +}; export const getBuildInfo = async (): Promise => { - const response = await axios.get("/api/v2/buildinfo") - return response.data -} + const response = await axios.get("/api/v2/buildinfo"); + return response.data; +}; export const getUpdateCheck = async (): Promise => { - const response = await axios.get("/api/v2/updatecheck") - return response.data - } + const response = await axios.get("/api/v2/updatecheck"); + return response.data; + }; export const putWorkspaceAutostart = async ( workspaceID: string, autostart: TypesGen.UpdateWorkspaceAutostartRequest, ): Promise => { - const payload = JSON.stringify(autostart) + const payload = JSON.stringify(autostart); await axios.put(`/api/v2/workspaces/${workspaceID}/autostart`, payload, { headers: { ...CONTENT_TYPE_JSON }, - }) -} + }); +}; export const putWorkspaceAutostop = async ( workspaceID: string, ttl: TypesGen.UpdateWorkspaceTTLRequest, ): Promise => { - const payload = JSON.stringify(ttl) + const payload = JSON.stringify(ttl); await axios.put(`/api/v2/workspaces/${workspaceID}/ttl`, payload, { headers: { ...CONTENT_TYPE_JSON }, - }) -} + }); +}; export const updateProfile = async ( userId: string, data: TypesGen.UpdateUserProfileRequest, ): Promise => { - const response = await axios.put(`/api/v2/users/${userId}/profile`, data) - return response.data -} + const response = await axios.put(`/api/v2/users/${userId}/profile`, data); + return response.data; +}; export const activateUser = async ( userId: TypesGen.User["id"], ): Promise => { const response = await axios.put( `/api/v2/users/${userId}/status/activate`, - ) - return response.data -} + ); + return response.data; +}; export const suspendUser = async ( userId: TypesGen.User["id"], ): Promise => { const response = await axios.put( `/api/v2/users/${userId}/status/suspend`, - ) - return response.data -} + ); + return response.data; +}; export const deleteUser = async ( userId: TypesGen.User["id"], ): Promise => { - return await axios.delete(`/api/v2/users/${userId}`) -} + return await axios.delete(`/api/v2/users/${userId}`); +}; // API definition: // https://github.com/coder/coder/blob/db665e7261f3c24a272ccec48233a3e276878239/coderd/users.go#L33-L53 export const hasFirstUser = async (): Promise => { try { // If it is success, it is true - await axios.get("/api/v2/users/first") - return true + await axios.get("/api/v2/users/first"); + return true; } catch (error) { // If it returns a 404, it is false if (axios.isAxiosError(error) && error.response?.status === 404) { - return false + return false; } - throw error + throw error; } -} +}; export const createFirstUser = async ( req: TypesGen.CreateFirstUserRequest, ): Promise => { - const response = await axios.post(`/api/v2/users/first`, req) - return response.data -} + const response = await axios.post(`/api/v2/users/first`, req); + return response.data; +}; export const updateUserPassword = async ( userId: TypesGen.User["id"], updatePassword: TypesGen.UpdateUserPasswordRequest, ): Promise => - axios.put(`/api/v2/users/${userId}/password`, updatePassword) + axios.put(`/api/v2/users/${userId}/password`, updatePassword); export const getSiteRoles = async (): Promise< Array > => { const response = await axios.get>( `/api/v2/users/roles`, - ) - return response.data -} + ); + return response.data; +}; export const updateUserRoles = async ( roles: TypesGen.Role["name"][], @@ -737,27 +737,27 @@ export const updateUserRoles = async ( const response = await axios.put( `/api/v2/users/${userId}/roles`, { roles }, - ) - return response.data -} + ); + return response.data; +}; export const getUserSSHKey = async ( userId = "me", ): Promise => { const response = await axios.get( `/api/v2/users/${userId}/gitsshkey`, - ) - return response.data -} + ); + return response.data; +}; export const regenerateUserSSHKey = async ( userId = "me", ): Promise => { const response = await axios.put( `/api/v2/users/${userId}/gitsshkey`, - ) - return response.data -} + ); + return response.data; +}; export const getWorkspaceBuilds = async ( workspaceId: string, @@ -765,9 +765,9 @@ export const getWorkspaceBuilds = async ( ): Promise => { const response = await axios.get( `/api/v2/workspaces/${workspaceId}/builds?since=${since.toISOString()}`, - ) - return response.data -} + ); + return response.data; +}; export const getWorkspaceBuildByNumber = async ( username = "me", @@ -776,9 +776,9 @@ export const getWorkspaceBuildByNumber = async ( ): Promise => { const response = await axios.get( `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`, - ) - return response.data -} + ); + return response.data; +}; export const getWorkspaceBuildLogs = async ( buildname: string, @@ -786,18 +786,18 @@ export const getWorkspaceBuildLogs = async ( ): Promise => { const response = await axios.get( `/api/v2/workspacebuilds/${buildname}/logs?before=${before.getTime()}`, - ) - return response.data -} + ); + return response.data; +}; export const getWorkspaceAgentLogs = async ( agentID: string, ): Promise => { const response = await axios.get( `/api/v2/workspaceagents/${agentID}/logs`, - ) - return response.data -} + ); + return response.data; +}; export const putWorkspaceExtension = async ( workspaceId: string, @@ -805,17 +805,17 @@ export const putWorkspaceExtension = async ( ): Promise => { await axios.put(`/api/v2/workspaces/${workspaceId}/extend`, { deadline: newDeadline, - }) -} + }); +}; export const refreshEntitlements = async (): Promise => { - await axios.post("/api/v2/licenses/refresh-entitlements") -} + await axios.post("/api/v2/licenses/refresh-entitlements"); +}; export const getEntitlements = async (): Promise => { try { - const response = await axios.get("/api/v2/entitlements") - return response.data + const response = await axios.get("/api/v2/entitlements"); + return response.data; } catch (ex) { if (axios.isAxiosError(ex) && ex.response?.status === 404) { return { @@ -826,68 +826,68 @@ export const getEntitlements = async (): Promise => { trial: false, warnings: [], refreshed_at: "", - } + }; } - throw ex + throw ex; } -} +}; export const getExperiments = async (): Promise => { try { - const response = await axios.get("/api/v2/experiments") - return response.data + const response = await axios.get("/api/v2/experiments"); + return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 404) { - return [] + return []; } - throw error + throw error; } -} +}; export const getGitAuthProvider = async ( provider: string, ): Promise => { - const resp = await axios.get(`/api/v2/gitauth/${provider}`) - return resp.data -} + const resp = await axios.get(`/api/v2/gitauth/${provider}`); + return resp.data; +}; export const getGitAuthDevice = async ( provider: string, ): Promise => { - const resp = await axios.get(`/api/v2/gitauth/${provider}/device`) - return resp.data -} + const resp = await axios.get(`/api/v2/gitauth/${provider}/device`); + return resp.data; +}; export const exchangeGitAuthDevice = async ( provider: string, req: TypesGen.GitAuthDeviceExchange, ): Promise => { - const resp = await axios.post(`/api/v2/gitauth/${provider}/device`, req) - return resp.data -} + const resp = await axios.post(`/api/v2/gitauth/${provider}/device`, req); + return resp.data; +}; export const getAuditLogs = async ( options: TypesGen.AuditLogsRequest, ): Promise => { - const url = getURLWithSearchParams("/api/v2/audit", options) - const response = await axios.get(url) - return response.data -} + const url = getURLWithSearchParams("/api/v2/audit", options); + const response = await axios.get(url); + return response.data; +}; export const getTemplateDAUs = async ( templateId: string, ): Promise => { - const response = await axios.get(`/api/v2/templates/${templateId}/daus`) - return response.data -} + const response = await axios.get(`/api/v2/templates/${templateId}/daus`); + return response.data; +}; export const getDeploymentDAUs = async ( // Default to user's local timezone offset = new Date().getTimezoneOffset() / 60, ): Promise => { - const response = await axios.get(`/api/v2/insights/daus?tz_offset=${offset}`) - return response.data -} + const response = await axios.get(`/api/v2/insights/daus?tz_offset=${offset}`); + return response.data; +}; export const getTemplateACLAvailable = async ( templateId: string, @@ -896,17 +896,17 @@ export const getTemplateACLAvailable = async ( const url = getURLWithSearchParams( `/api/v2/templates/${templateId}/acl/available`, options, - ) - const response = await axios.get(url.toString()) - return response.data -} + ); + const response = await axios.get(url.toString()); + return response.data; +}; export const getTemplateACL = async ( templateId: string, ): Promise => { - const response = await axios.get(`/api/v2/templates/${templateId}/acl`) - return response.data -} + const response = await axios.get(`/api/v2/templates/${templateId}/acl`); + return response.data; +}; export const updateTemplateACL = async ( templateId: string, @@ -915,24 +915,24 @@ export const updateTemplateACL = async ( const response = await axios.patch( `/api/v2/templates/${templateId}/acl`, data, - ) - return response.data -} + ); + return response.data; +}; export const getApplicationsHost = async (): Promise => { - const response = await axios.get(`/api/v2/applications/host`) - return response.data - } + const response = await axios.get(`/api/v2/applications/host`); + return response.data; + }; export const getGroups = async ( organizationId: string, ): Promise => { const response = await axios.get( `/api/v2/organizations/${organizationId}/groups`, - ) - return response.data -} + ); + return response.data; +}; export const createGroup = async ( organizationId: string, @@ -941,95 +941,95 @@ export const createGroup = async ( const response = await axios.post( `/api/v2/organizations/${organizationId}/groups`, data, - ) - return response.data -} + ); + return response.data; +}; export const getGroup = async (groupId: string): Promise => { - const response = await axios.get(`/api/v2/groups/${groupId}`) - return response.data -} + const response = await axios.get(`/api/v2/groups/${groupId}`); + return response.data; +}; export const patchGroup = async ( groupId: string, data: TypesGen.PatchGroupRequest, ): Promise => { - const response = await axios.patch(`/api/v2/groups/${groupId}`, data) - return response.data -} + const response = await axios.patch(`/api/v2/groups/${groupId}`, data); + return response.data; +}; export const deleteGroup = async (groupId: string): Promise => { - await axios.delete(`/api/v2/groups/${groupId}`) -} + await axios.delete(`/api/v2/groups/${groupId}`); +}; export const getWorkspaceQuota = async ( userID: string, ): Promise => { - const response = await axios.get(`/api/v2/workspace-quota/${userID}`) - return response.data -} + const response = await axios.get(`/api/v2/workspace-quota/${userID}`); + return response.data; +}; export const getAgentListeningPorts = async ( agentID: string, ): Promise => { const response = await axios.get( `/api/v2/workspaceagents/${agentID}/listening-ports`, - ) - return response.data -} + ); + return response.data; +}; // getDeploymentSSHConfig is used by the VSCode-Extension. export const getDeploymentSSHConfig = async (): Promise => { - const response = await axios.get(`/api/v2/deployment/ssh`) - return response.data - } + const response = await axios.get(`/api/v2/deployment/ssh`); + return response.data; + }; export const getDeploymentValues = async (): Promise => { - const response = await axios.get(`/api/v2/deployment/config`) - return response.data -} + const response = await axios.get(`/api/v2/deployment/config`); + return response.data; +}; export const getDeploymentStats = async (): Promise => { - const response = await axios.get(`/api/v2/deployment/stats`) - return response.data - } + const response = await axios.get(`/api/v2/deployment/stats`); + return response.data; + }; export const getReplicas = async (): Promise => { - const response = await axios.get(`/api/v2/replicas`) - return response.data -} + const response = await axios.get(`/api/v2/replicas`); + return response.data; +}; export const getFile = async (fileId: string): Promise => { const response = await axios.get(`/api/v2/files/${fileId}`, { responseType: "arraybuffer", - }) - return response.data -} + }); + return response.data; +}; export const getWorkspaceProxyRegions = async (): Promise< TypesGen.RegionsResponse > => { const response = await axios.get>( `/api/v2/regions`, - ) - return response.data -} + ); + return response.data; +}; export const getWorkspaceProxies = async (): Promise< TypesGen.RegionsResponse > => { const response = await axios.get< TypesGen.RegionsResponse - >(`/api/v2/workspaceproxies`) - return response.data -} + >(`/api/v2/workspaceproxies`); + return response.data; +}; export const getAppearance = async (): Promise => { try { - const response = await axios.get(`/api/v2/appearance`) - return response.data || {} + const response = await axios.get(`/api/v2/appearance`); + return response.data || {}; } catch (ex) { if (axios.isAxiosError(ex) && ex.response?.status === 404) { return { @@ -1037,27 +1037,27 @@ export const getAppearance = async (): Promise => { service_banner: { enabled: false, }, - } + }; } - throw ex + throw ex; } -} +}; export const updateAppearance = async ( b: TypesGen.AppearanceConfig, ): Promise => { - const response = await axios.put(`/api/v2/appearance`, b) - return response.data -} + const response = await axios.put(`/api/v2/appearance`, b); + return response.data; +}; export const getTemplateExamples = async ( organizationId: string, ): Promise => { const response = await axios.get( `/api/v2/organizations/${organizationId}/templates/examples`, - ) - return response.data -} + ); + return response.data; +}; export const uploadTemplateFile = async ( file: File, @@ -1066,72 +1066,72 @@ export const uploadTemplateFile = async ( headers: { "Content-Type": "application/x-tar", }, - }) - return response.data -} + }); + return response.data; +}; export const getTemplateVersionLogs = async ( versionId: string, ): Promise => { const response = await axios.get( `/api/v2/templateversions/${versionId}/logs`, - ) - return response.data -} + ); + return response.data; +}; export const updateWorkspaceVersion = async ( workspace: TypesGen.Workspace, ): Promise => { - const template = await getTemplate(workspace.template_id) - return startWorkspace(workspace.id, template.active_version_id) -} + const template = await getTemplate(workspace.template_id); + return startWorkspace(workspace.id, template.active_version_id); +}; export const getWorkspaceBuildParameters = async ( workspaceBuildId: TypesGen.WorkspaceBuild["id"], ): Promise => { const response = await axios.get( `/api/v2/workspacebuilds/${workspaceBuildId}/parameters`, - ) - return response.data -} + ); + return response.data; +}; type Claims = { - license_expires: number - account_type?: string - account_id?: string - trial: boolean - all_features: boolean - version: number - features: Record - require_telemetry?: boolean -} + license_expires: number; + 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 -} + claims: Claims; + expires_at: string; +}; export const getLicenses = async (): Promise => { - const response = await axios.get(`/api/v2/licenses`) - return response.data -} + 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 -} + 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}`) -} + await axios.delete(`/api/v2/licenses/${licenseId}`); +}; export class MissingBuildParameters extends Error { - parameters: TypesGen.TemplateVersionParameter[] = [] + parameters: TypesGen.TemplateVersionParameter[] = []; constructor(parameters: TypesGen.TemplateVersionParameter[]) { - super("Missing build parameters.") - this.parameters = parameters + super("Missing build parameters."); + this.parameters = parameters; } } @@ -1151,24 +1151,24 @@ export const changeWorkspaceVersion = async ( const [currentBuildParameters, templateParameters] = await Promise.all([ getWorkspaceBuildParameters(workspace.latest_build.id), getTemplateVersionRichParameters(templateVersionId), - ]) + ]); const missingParameters = getMissingParameters( currentBuildParameters, newBuildParameters, templateParameters, - ) + ); if (missingParameters.length > 0) { - throw new MissingBuildParameters(missingParameters) + throw new MissingBuildParameters(missingParameters); } return postWorkspaceBuild(workspace.id, { transition: "start", template_version_id: templateVersionId, rich_parameter_values: newBuildParameters, - }) -} + }); +}; /** Steps to update the workspace * - Get the latest template to access the latest active version @@ -1186,98 +1186,100 @@ export const updateWorkspace = async ( const [template, oldBuildParameters] = await Promise.all([ getTemplate(workspace.template_id), getWorkspaceBuildParameters(workspace.latest_build.id), - ]) - const activeVersionId = template.active_version_id + ]); + const activeVersionId = template.active_version_id; const templateParameters = await getTemplateVersionRichParameters( activeVersionId, - ) + ); const missingParameters = getMissingParameters( oldBuildParameters, newBuildParameters, templateParameters, - ) + ); if (missingParameters.length > 0) { - throw new MissingBuildParameters(missingParameters) + throw new MissingBuildParameters(missingParameters); } return postWorkspaceBuild(workspace.id, { transition: "start", template_version_id: activeVersionId, rich_parameter_values: newBuildParameters, - }) -} + }); +}; const getMissingParameters = ( oldBuildParameters: TypesGen.WorkspaceBuildParameter[], newBuildParameters: TypesGen.WorkspaceBuildParameter[], templateParameters: TypesGen.TemplateVersionParameter[], ) => { - const missingParameters: TypesGen.TemplateVersionParameter[] = [] - const requiredParameters: TypesGen.TemplateVersionParameter[] = [] + const missingParameters: TypesGen.TemplateVersionParameter[] = []; + const requiredParameters: TypesGen.TemplateVersionParameter[] = []; templateParameters.forEach((p) => { // It is mutable and required. Mutable values can be changed after so we // don't need to ask them if they are not required. - const isMutableAndRequired = p.mutable && p.required + const isMutableAndRequired = p.mutable && p.required; // Is immutable, so we can check if it is its first time on the build - const isImmutable = !p.mutable + const isImmutable = !p.mutable; if (isMutableAndRequired || isImmutable) { - requiredParameters.push(p) + requiredParameters.push(p); } - }) + }); for (const parameter of requiredParameters) { // Check if there is a new value let buildParameter = newBuildParameters.find( (p) => p.name === parameter.name, - ) + ); // If not, get the old one if (!buildParameter) { - buildParameter = oldBuildParameters.find((p) => p.name === parameter.name) + buildParameter = oldBuildParameters.find( + (p) => p.name === parameter.name, + ); } // If there is a value from the new or old one, it is not missed if (buildParameter) { - continue + continue; } - missingParameters.push(parameter) + missingParameters.push(parameter); } // Check if parameter "options" changed and we can't use old build parameters. templateParameters.forEach((templateParameter) => { if (templateParameter.options.length === 0) { - return + return; } // Check if there is a new value let buildParameter = newBuildParameters.find( (p) => p.name === templateParameter.name, - ) + ); // If not, get the old one if (!buildParameter) { buildParameter = oldBuildParameters.find( (p) => p.name === templateParameter.name, - ) + ); } if (!buildParameter) { - return + return; } const matchingOption = templateParameter.options.find( (option) => option.value === buildParameter?.value, - ) + ); if (!matchingOption) { - missingParameters.push(templateParameter) + missingParameters.push(templateParameter); } - }) - return missingParameters -} + }); + return missingParameters; +}; /** * @@ -1289,15 +1291,15 @@ export const watchAgentMetadata = (agentId: string): EventSource => { return new EventSource( `${location.protocol}//${location.host}/api/v2/workspaceagents/${agentId}/watch-metadata`, { withCredentials: true }, - ) -} + ); +}; type WatchBuildLogsByTemplateVersionIdOptions = { - after?: number - onMessage: (log: TypesGen.ProvisionerJobLog) => void - onDone: () => void - onError: (error: Error) => void -} + after?: number; + onMessage: (log: TypesGen.ProvisionerJobLog) => void; + onDone: () => void; + onError: (error: Error) => void; +}; export const watchBuildLogsByTemplateVersionId = ( versionId: string, { @@ -1307,37 +1309,37 @@ export const watchBuildLogsByTemplateVersionId = ( after, }: WatchBuildLogsByTemplateVersionIdOptions, ) => { - const searchParams = new URLSearchParams({ follow: "true" }) + const searchParams = new URLSearchParams({ follow: "true" }); if (after !== undefined) { - searchParams.append("after", after.toString()) + searchParams.append("after", after.toString()); } - const proto = location.protocol === "https:" ? "wss:" : "ws:" + const proto = location.protocol === "https:" ? "wss:" : "ws:"; const socket = new WebSocket( `${proto}//${ location.host }/api/v2/templateversions/${versionId}/logs?${searchParams.toString()}`, - ) - socket.binaryType = "blob" + ); + socket.binaryType = "blob"; socket.addEventListener("message", (event) => onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog), - ) + ); socket.addEventListener("error", () => { - onError(new Error("Connection for logs failed.")) - socket.close() - }) + onError(new Error("Connection for logs failed.")); + socket.close(); + }); socket.addEventListener("close", () => { // When the socket closes, logs have finished streaming! - onDone() - }) - return socket -} + onDone(); + }); + return socket; +}; type WatchWorkspaceAgentLogsOptions = { - after: number - onMessage: (logs: TypesGen.WorkspaceAgentLog[]) => void - onDone: () => void - onError: (error: Error) => void -} + after: number; + onMessage: (logs: TypesGen.WorkspaceAgentLog[]) => void; + onDone: () => void; + onError: (error: Error) => void; +}; export const watchWorkspaceAgentLogs = ( agentId: string, @@ -1351,61 +1353,61 @@ export const watchWorkspaceAgentLogs = ( const noCompression = userAgentParser(navigator.userAgent).browser.name === "Safari" ? "&no_compression" - : "" + : ""; - const proto = location.protocol === "https:" ? "wss:" : "ws:" + const proto = location.protocol === "https:" ? "wss:" : "ws:"; const socket = new WebSocket( `${proto}//${location.host}/api/v2/workspaceagents/${agentId}/logs?follow&after=${after}${noCompression}`, - ) - socket.binaryType = "blob" + ); + socket.binaryType = "blob"; socket.addEventListener("message", (event) => { - const logs = JSON.parse(event.data) as TypesGen.WorkspaceAgentLog[] - onMessage(logs) - }) + const logs = JSON.parse(event.data) as TypesGen.WorkspaceAgentLog[]; + onMessage(logs); + }); socket.addEventListener("error", () => { - onError(new Error("socket errored")) - }) + onError(new Error("socket errored")); + }); socket.addEventListener("close", () => { - onDone() - }) + onDone(); + }); - return socket -} + return socket; +}; type WatchBuildLogsByBuildIdOptions = { - after?: number - onMessage: (log: TypesGen.ProvisionerJobLog) => void - onDone: () => void - onError: (error: Error) => void -} + after?: number; + onMessage: (log: TypesGen.ProvisionerJobLog) => void; + onDone: () => void; + onError: (error: Error) => void; +}; export const watchBuildLogsByBuildId = ( buildId: string, { onMessage, onDone, onError, after }: WatchBuildLogsByBuildIdOptions, ) => { - const searchParams = new URLSearchParams({ follow: "true" }) + const searchParams = new URLSearchParams({ follow: "true" }); if (after !== undefined) { - searchParams.append("after", after.toString()) + searchParams.append("after", after.toString()); } - const proto = location.protocol === "https:" ? "wss:" : "ws:" + const proto = location.protocol === "https:" ? "wss:" : "ws:"; const socket = new WebSocket( `${proto}//${ location.host }/api/v2/workspacebuilds/${buildId}/logs?${searchParams.toString()}`, - ) - socket.binaryType = "blob" + ); + socket.binaryType = "blob"; socket.addEventListener("message", (event) => onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog), - ) + ); socket.addEventListener("error", () => { - onError(new Error("Connection for logs failed.")) - socket.close() - }) + onError(new Error("Connection for logs failed.")); + socket.close(); + }); socket.addEventListener("close", () => { // When the socket closes, logs have finished streaming! - onDone() - }) - return socket -} + onDone(); + }); + return socket; +}; export const issueReconnectingPTYSignedToken = async ( params: TypesGen.IssueReconnectingPTYSignedTokenRequest, @@ -1413,35 +1415,35 @@ export const issueReconnectingPTYSignedToken = async ( const response = await axios.post( "/api/v2/applications/reconnecting-pty-signed-token", params, - ) - return response.data -} + ); + return response.data; +}; export const getWorkspaceParameters = async (workspace: TypesGen.Workspace) => { - const latestBuild = workspace.latest_build + const latestBuild = workspace.latest_build; const [templateVersionRichParameters, buildParameters] = await Promise.all([ getTemplateVersionRichParameters(latestBuild.template_version_id), getWorkspaceBuildParameters(latestBuild.id), - ]) + ]); return { templateVersionRichParameters, buildParameters, - } -} + }; +}; type InsightsFilter = { - start_time: string - end_time: string - template_ids: string -} + start_time: string; + end_time: string; + template_ids: string; +}; export const getInsightsUserLatency = async ( filters: InsightsFilter, ): Promise => { - const params = new URLSearchParams(filters) - const response = await axios.get(`/api/v2/insights/user-latency?${params}`) - return response.data -} + const params = new URLSearchParams(filters); + const response = await axios.get(`/api/v2/insights/user-latency?${params}`); + return response.data; +}; export const getInsightsTemplate = async ( filters: InsightsFilter, @@ -1449,19 +1451,19 @@ export const getInsightsTemplate = async ( const params = new URLSearchParams({ ...filters, interval: "day", - }) - const response = await axios.get(`/api/v2/insights/templates?${params}`) - return response.data -} + }); + const response = await axios.get(`/api/v2/insights/templates?${params}`); + return response.data; +}; export const getHealth = () => { return axios.get<{ - healthy: boolean - time: string - coder_version: string - derp: { healthy: boolean } - access_url: { healthy: boolean } - websocket: { healthy: boolean } - database: { healthy: boolean } - }>("/api/v2/debug/health") -} + healthy: boolean; + time: string; + coder_version: string; + derp: { healthy: boolean }; + access_url: { healthy: boolean }; + websocket: { healthy: boolean }; + database: { healthy: boolean }; + }>("/api/v2/debug/health"); +}; diff --git a/site/src/api/errors.test.ts b/site/src/api/errors.test.ts index 5fa94412f7..d3612b254e 100644 --- a/site/src/api/errors.test.ts +++ b/site/src/api/errors.test.ts @@ -1,9 +1,9 @@ -import { mockApiError } from "testHelpers/entities" +import { mockApiError } from "testHelpers/entities"; import { getValidationErrorMessage, isApiError, mapApiErrorToFieldErrors, -} from "./errors" +} from "./errors"; describe("isApiError", () => { it("returns true when the object is an API Error", () => { @@ -16,17 +16,17 @@ describe("isApiError", () => { ], }), ), - ).toBe(true) - }) + ).toBe(true); + }); it("returns false when the object is Error", () => { - expect(isApiError(new Error())).toBe(false) - }) + expect(isApiError(new Error())).toBe(false); + }); it("returns false when the object is undefined", () => { - expect(isApiError(undefined)).toBe(false) - }) -}) + expect(isApiError(undefined)).toBe(false); + }); +}); describe("mapApiErrorToFieldErrors", () => { it("returns correct field errors", () => { @@ -39,9 +39,9 @@ describe("mapApiErrorToFieldErrors", () => { }), ).toEqual({ username: "Username is already in use", - }) - }) -}) + }); + }); +}); describe("getValidationErrorMessage", () => { it("returns multiple validation messages", () => { @@ -63,14 +63,14 @@ describe("getValidationErrorMessage", () => { ), ).toEqual( `Query param "status" has invalid value: "inactive" is not a valid user status\nQuery element "role:a:e" can only contain 1 ':'`, - ) - }) + ); + }); it("non-API error returns empty validation message", () => { expect( getValidationErrorMessage(new Error("Invalid user search query.")), - ).toEqual("") - }) + ).toEqual(""); + }); it("no validations field returns empty validation message", () => { expect( @@ -80,6 +80,6 @@ describe("getValidationErrorMessage", () => { detail: `Query element "role:a:e" can only contain 1 ':'`, }), ), - ).toEqual("") - }) -}) + ).toEqual(""); + }); +}); diff --git a/site/src/api/errors.ts b/site/src/api/errors.ts index 33e06ae686..f8d9421631 100644 --- a/site/src/api/errors.ts +++ b/site/src/api/errors.ts @@ -1,56 +1,56 @@ -import axios, { AxiosError, AxiosResponse } from "axios" +import axios, { AxiosError, AxiosResponse } from "axios"; const Language = { errorsByCode: { defaultErrorCode: "Invalid value", }, -} +}; export interface FieldError { - field: string - detail: string + field: string; + detail: string; } -export type FieldErrors = Record +export type FieldErrors = Record; export interface ApiErrorResponse { - message: string - detail?: string - validations?: FieldError[] + message: string; + detail?: string; + validations?: FieldError[]; } export type ApiError = AxiosError & { - response: AxiosResponse -} + response: AxiosResponse; +}; export const isApiError = (err: unknown): err is ApiError => { - return axios.isAxiosError(err) && err.response !== undefined -} + return axios.isAxiosError(err) && err.response !== undefined; +}; export const hasApiFieldErrors = (error: ApiError): boolean => - Array.isArray(error.response.data.validations) + Array.isArray(error.response.data.validations); export const isApiValidationError = (error: unknown): error is ApiError => { - return isApiError(error) && hasApiFieldErrors(error) -} + return isApiError(error) && hasApiFieldErrors(error); +}; export const hasError = (error: unknown) => - error !== undefined && error !== null + error !== undefined && error !== null; export const mapApiErrorToFieldErrors = ( apiErrorResponse: ApiErrorResponse, ): FieldErrors => { - const result: FieldErrors = {} + const result: FieldErrors = {}; if (apiErrorResponse.validations) { for (const error of apiErrorResponse.validations) { result[error.field] = - error.detail || Language.errorsByCode.defaultErrorCode + error.detail || Language.errorsByCode.defaultErrorCode; } } - return result -} + return result; +}; /** * @@ -66,7 +66,7 @@ export const getErrorMessage = ( ? error.response.data.message : error instanceof Error ? error.message - : defaultMessage + : defaultMessage; /** * @@ -78,13 +78,13 @@ export const getValidationErrorMessage = (error: unknown): string => { const validationErrors = isApiError(error) && error.response.data.validations ? error.response.data.validations - : [] - return validationErrors.map((error) => error.detail).join("\n") -} + : []; + return validationErrors.map((error) => error.detail).join("\n"); +}; export const getErrorDetail = (error: unknown): string | undefined | null => isApiError(error) ? error.response.data.detail : error instanceof Error ? `Please check the developer console for more details.` - : null + : null; diff --git a/site/src/api/types.ts b/site/src/api/types.ts index d0c31fe6f8..e1be5071fc 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -1,40 +1,40 @@ -import { DeploymentValues } from "./typesGenerated" +import { DeploymentValues } from "./typesGenerated"; export interface UserAgent { - readonly browser: string - readonly device: string - readonly ip_address: string - readonly os: string + readonly browser: string; + readonly device: string; + readonly ip_address: string; + readonly os: string; } export interface ReconnectingPTYRequest { - readonly data?: string - readonly height?: number - readonly width?: number + readonly data?: string; + readonly height?: number; + readonly width?: number; } -export type WorkspaceBuildTransition = "start" | "stop" | "delete" +export type WorkspaceBuildTransition = "start" | "stop" | "delete"; -export type Message = { message: string } +export type Message = { message: string }; export interface DeploymentGroup { - readonly name: string - readonly parent?: DeploymentGroup - readonly description: string - readonly children: DeploymentGroup[] + readonly name: string; + readonly parent?: DeploymentGroup; + readonly description: string; + readonly children: DeploymentGroup[]; } export interface DeploymentOption { - readonly name: string - readonly description: string - readonly flag: string - readonly flag_shorthand: string - readonly value: unknown - readonly hidden: boolean - readonly group?: DeploymentGroup + readonly name: string; + readonly description: string; + readonly flag: string; + readonly flag_shorthand: string; + readonly value: unknown; + readonly hidden: boolean; + readonly group?: DeploymentGroup; } export type DeploymentConfig = { - readonly config: DeploymentValues - readonly options: DeploymentOption[] -} + readonly config: DeploymentValues; + readonly options: DeploymentOption[]; +}; diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 41239d8bd9..77d81a3a4a 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -4,1564 +4,1564 @@ // From codersdk/templates.go export interface ACLAvailable { - readonly users: User[] - readonly groups: Group[] + readonly users: User[]; + readonly groups: Group[]; } // From codersdk/apikey.go export interface APIKey { - readonly id: string - readonly user_id: string - readonly last_used: string - readonly expires_at: string - readonly created_at: string - readonly updated_at: string - readonly login_type: LoginType - readonly scope: APIKeyScope - readonly token_name: string - readonly lifetime_seconds: number + readonly id: string; + readonly user_id: string; + readonly last_used: string; + readonly expires_at: string; + readonly created_at: string; + readonly updated_at: string; + readonly login_type: LoginType; + readonly scope: APIKeyScope; + readonly token_name: string; + readonly lifetime_seconds: number; } // From codersdk/apikey.go export interface APIKeyWithOwner extends APIKey { - readonly username: string + readonly username: string; } // From codersdk/licenses.go export interface AddLicenseRequest { - readonly license: string + readonly license: string; } // From codersdk/templates.go export interface AgentStatsReportResponse { - readonly num_comms: number - readonly rx_bytes: number - readonly tx_bytes: number + readonly num_comms: number; + readonly rx_bytes: number; + readonly tx_bytes: number; } // From codersdk/deployment.go export interface AppHostResponse { - readonly host: string + readonly host: string; } // From codersdk/deployment.go export interface AppearanceConfig { - readonly logo_url: string - readonly service_banner: ServiceBannerConfig - readonly support_links?: LinkConfig[] + readonly logo_url: string; + readonly service_banner: ServiceBannerConfig; + readonly support_links?: LinkConfig[]; } // From codersdk/roles.go export interface AssignableRoles extends Role { - readonly assignable: boolean + readonly assignable: boolean; } // From codersdk/audit.go -export type AuditDiff = Record +export type AuditDiff = Record; // From codersdk/audit.go export interface AuditDiffField { // Empty interface{} type, cannot resolve the type. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- interface{} - readonly old?: any + readonly old?: any; // Empty interface{} type, cannot resolve the type. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- interface{} - readonly new?: any - readonly secret: boolean + readonly new?: any; + readonly secret: boolean; } // From codersdk/audit.go export interface AuditLog { - readonly id: string - readonly request_id: string - readonly time: string - readonly organization_id: string + readonly id: string; + readonly request_id: string; + readonly time: string; + readonly organization_id: string; // Named type "net/netip.Addr" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly ip: any - readonly user_agent: string - readonly resource_type: ResourceType - readonly resource_id: string - readonly resource_target: string - readonly resource_icon: string - readonly action: AuditAction - readonly diff: AuditDiff - readonly status_code: number - readonly additional_fields: Record - readonly description: string - readonly resource_link: string - readonly is_deleted: boolean - readonly user?: User + readonly ip: any; + readonly user_agent: string; + readonly resource_type: ResourceType; + readonly resource_id: string; + readonly resource_target: string; + readonly resource_icon: string; + readonly action: AuditAction; + readonly diff: AuditDiff; + readonly status_code: number; + readonly additional_fields: Record; + readonly description: string; + readonly resource_link: string; + readonly is_deleted: boolean; + readonly user?: User; } // From codersdk/audit.go export interface AuditLogResponse { - readonly audit_logs: AuditLog[] - readonly count: number + readonly audit_logs: AuditLog[]; + readonly count: number; } // From codersdk/audit.go export interface AuditLogsRequest extends Pagination { - readonly q?: string + readonly q?: string; } // From codersdk/users.go export interface AuthMethod { - readonly enabled: boolean + readonly enabled: boolean; } // From codersdk/users.go export interface AuthMethods { - readonly password: AuthMethod - readonly github: AuthMethod - readonly oidc: OIDCAuthMethod + readonly password: AuthMethod; + readonly github: AuthMethod; + readonly oidc: OIDCAuthMethod; } // From codersdk/authorization.go export interface AuthorizationCheck { - readonly object: AuthorizationObject - readonly action: string + readonly object: AuthorizationObject; + readonly action: string; } // From codersdk/authorization.go export interface AuthorizationObject { - readonly resource_type: RBACResource - readonly owner_id?: string - readonly organization_id?: string - readonly resource_id?: string + readonly resource_type: RBACResource; + readonly owner_id?: string; + readonly organization_id?: string; + readonly resource_id?: string; } // From codersdk/authorization.go export interface AuthorizationRequest { - readonly checks: Record + readonly checks: Record; } // From codersdk/authorization.go -export type AuthorizationResponse = Record +export type AuthorizationResponse = Record; // From codersdk/deployment.go export interface BuildInfoResponse { - readonly external_url: string - readonly version: string - readonly dashboard_url: string - readonly workspace_proxy: boolean + readonly external_url: string; + readonly version: string; + readonly dashboard_url: string; + readonly workspace_proxy: boolean; } // From codersdk/insights.go export interface ConnectionLatency { - readonly p50: number - readonly p95: number + readonly p50: number; + readonly p95: number; } // From codersdk/users.go export interface ConvertLoginRequest { - readonly to_type: LoginType - readonly password: string + readonly to_type: LoginType; + readonly password: string; } // From codersdk/users.go export interface CreateFirstUserRequest { - readonly email: string - readonly username: string - readonly password: string - readonly trial: boolean + readonly email: string; + readonly username: string; + readonly password: string; + readonly trial: boolean; } // From codersdk/users.go export interface CreateFirstUserResponse { - readonly user_id: string - readonly organization_id: string + readonly user_id: string; + readonly organization_id: string; } // From codersdk/groups.go export interface CreateGroupRequest { - readonly name: string - readonly display_name: string - readonly avatar_url: string - readonly quota_allowance: number + readonly name: string; + readonly display_name: string; + readonly avatar_url: string; + readonly quota_allowance: number; } // From codersdk/users.go export interface CreateOrganizationRequest { - readonly name: string + readonly name: string; } // From codersdk/organizations.go export interface CreateTemplateRequest { - readonly name: string - readonly display_name?: string - readonly description?: string - readonly icon?: string - readonly template_version_id: string - readonly default_ttl_ms?: number - readonly max_ttl_ms?: number - readonly autostop_requirement?: TemplateAutostopRequirement - readonly allow_user_cancel_workspace_jobs?: boolean - readonly allow_user_autostart?: boolean - readonly allow_user_autostop?: boolean - readonly failure_ttl_ms?: number - readonly dormant_ttl_ms?: number - readonly delete_ttl_ms?: number - readonly disable_everyone_group_access: boolean + readonly name: string; + readonly display_name?: string; + readonly description?: string; + readonly icon?: string; + readonly template_version_id: string; + readonly default_ttl_ms?: number; + readonly max_ttl_ms?: number; + readonly autostop_requirement?: TemplateAutostopRequirement; + readonly allow_user_cancel_workspace_jobs?: boolean; + readonly allow_user_autostart?: boolean; + readonly allow_user_autostop?: boolean; + readonly failure_ttl_ms?: number; + readonly dormant_ttl_ms?: number; + readonly delete_ttl_ms?: number; + readonly disable_everyone_group_access: boolean; } // From codersdk/templateversions.go export interface CreateTemplateVersionDryRunRequest { - readonly workspace_name: string - readonly rich_parameter_values: WorkspaceBuildParameter[] - readonly user_variable_values?: VariableValue[] + readonly workspace_name: string; + readonly rich_parameter_values: WorkspaceBuildParameter[]; + readonly user_variable_values?: VariableValue[]; } // From codersdk/organizations.go export interface CreateTemplateVersionRequest { - readonly name?: string - readonly message?: string - readonly template_id?: string - readonly storage_method: ProvisionerStorageMethod - readonly file_id?: string - readonly example_id?: string - readonly provisioner: ProvisionerType - readonly tags: Record - readonly user_variable_values?: VariableValue[] + readonly name?: string; + readonly message?: string; + readonly template_id?: string; + readonly storage_method: ProvisionerStorageMethod; + readonly file_id?: string; + readonly example_id?: string; + readonly provisioner: ProvisionerType; + readonly tags: Record; + readonly user_variable_values?: VariableValue[]; } // From codersdk/audit.go export interface CreateTestAuditLogRequest { - readonly action?: AuditAction - readonly resource_type?: ResourceType - readonly resource_id?: string - readonly additional_fields?: Record - readonly time?: string - readonly build_reason?: BuildReason + readonly action?: AuditAction; + readonly resource_type?: ResourceType; + readonly resource_id?: string; + readonly additional_fields?: Record; + readonly time?: string; + readonly build_reason?: BuildReason; } // From codersdk/apikey.go export interface CreateTokenRequest { - readonly lifetime: number - readonly scope: APIKeyScope - readonly token_name: string + readonly lifetime: number; + readonly scope: APIKeyScope; + readonly token_name: string; } // From codersdk/users.go export interface CreateUserRequest { - readonly email: string - readonly username: string - readonly password: string - readonly login_type: LoginType - readonly disable_login: boolean - readonly organization_id: string + readonly email: string; + readonly username: string; + readonly password: string; + readonly login_type: LoginType; + readonly disable_login: boolean; + readonly organization_id: string; } // From codersdk/workspaces.go export interface CreateWorkspaceBuildRequest { - readonly template_version_id?: string - readonly transition: WorkspaceTransition - readonly dry_run?: boolean - readonly state?: string - readonly orphan?: boolean - readonly rich_parameter_values?: WorkspaceBuildParameter[] - readonly log_level?: ProvisionerLogLevel + readonly template_version_id?: string; + readonly transition: WorkspaceTransition; + readonly dry_run?: boolean; + readonly state?: string; + readonly orphan?: boolean; + readonly rich_parameter_values?: WorkspaceBuildParameter[]; + readonly log_level?: ProvisionerLogLevel; } // From codersdk/workspaceproxy.go export interface CreateWorkspaceProxyRequest { - readonly name: string - readonly display_name: string - readonly icon: string + readonly name: string; + readonly display_name: string; + readonly icon: string; } // From codersdk/organizations.go export interface CreateWorkspaceRequest { - readonly template_id?: string - readonly template_version_id?: string - readonly name: string - readonly autostart_schedule?: string - readonly ttl_ms?: number - readonly rich_parameter_values?: WorkspaceBuildParameter[] + readonly template_id?: string; + readonly template_version_id?: string; + readonly name: string; + readonly autostart_schedule?: string; + readonly ttl_ms?: number; + readonly rich_parameter_values?: WorkspaceBuildParameter[]; } // From codersdk/deployment.go export interface DAUEntry { - readonly date: string - readonly amount: number + readonly date: string; + readonly amount: number; } // From codersdk/deployment.go export interface DAURequest { - readonly TZHourOffset: number + readonly TZHourOffset: number; } // From codersdk/deployment.go export interface DAUsResponse { - readonly entries: DAUEntry[] - readonly tz_hour_offset: number + readonly entries: DAUEntry[]; + readonly tz_hour_offset: number; } // From codersdk/deployment.go export interface DERP { - readonly server: DERPServerConfig - readonly config: DERPConfig + readonly server: DERPServerConfig; + readonly config: DERPConfig; } // From codersdk/deployment.go export interface DERPConfig { - readonly block_direct: boolean - readonly force_websockets: boolean - readonly url: string - readonly path: string + readonly block_direct: boolean; + readonly force_websockets: boolean; + readonly url: string; + readonly path: string; } // From codersdk/workspaceagents.go export interface DERPRegion { - readonly preferred: boolean - readonly latency_ms: number + readonly preferred: boolean; + readonly latency_ms: number; } // From codersdk/deployment.go export interface DERPServerConfig { - readonly enable: boolean - readonly region_id: number - readonly region_code: string - readonly region_name: string + readonly enable: boolean; + readonly region_id: number; + readonly region_code: string; + readonly region_name: string; // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray") - readonly stun_addresses: string[] - readonly relay_url: string + readonly stun_addresses: string[]; + readonly relay_url: string; } // From codersdk/deployment.go export interface DangerousConfig { - readonly allow_path_app_sharing: boolean - readonly allow_path_app_site_owner_access: boolean - readonly allow_all_cors: boolean + readonly allow_path_app_sharing: boolean; + readonly allow_path_app_site_owner_access: boolean; + readonly allow_all_cors: boolean; } // From codersdk/deployment.go export interface DeploymentStats { - readonly aggregated_from: string - readonly collected_at: string - readonly next_update_at: string - readonly workspaces: WorkspaceDeploymentStats - readonly session_count: SessionCountDeploymentStats + readonly aggregated_from: string; + readonly collected_at: string; + readonly next_update_at: string; + readonly workspaces: WorkspaceDeploymentStats; + readonly session_count: SessionCountDeploymentStats; } // From codersdk/deployment.go export interface DeploymentValues { - readonly verbose?: boolean - readonly access_url?: string - readonly wildcard_access_url?: string - readonly docs_url?: string - readonly redirect_to_access_url?: boolean - readonly http_address?: string - readonly autobuild_poll_interval?: number - readonly job_hang_detector_interval?: number - readonly derp?: DERP - readonly prometheus?: PrometheusConfig - readonly pprof?: PprofConfig + readonly verbose?: boolean; + readonly access_url?: string; + readonly wildcard_access_url?: string; + readonly docs_url?: string; + readonly redirect_to_access_url?: boolean; + readonly http_address?: string; + readonly autobuild_poll_interval?: number; + readonly job_hang_detector_interval?: number; + readonly derp?: DERP; + readonly prometheus?: PrometheusConfig; + readonly pprof?: PprofConfig; // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray") - readonly proxy_trusted_headers?: string[] + readonly proxy_trusted_headers?: string[]; // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray") - readonly proxy_trusted_origins?: string[] - readonly cache_directory?: string - readonly in_memory_database?: boolean - readonly pg_connection_url?: string - readonly oauth2?: OAuth2Config - readonly oidc?: OIDCConfig - readonly telemetry?: TelemetryConfig - readonly tls?: TLSConfig - readonly trace?: TraceConfig - readonly secure_auth_cookie?: boolean - readonly strict_transport_security?: number + readonly proxy_trusted_origins?: string[]; + readonly cache_directory?: string; + readonly in_memory_database?: boolean; + readonly pg_connection_url?: string; + readonly oauth2?: OAuth2Config; + readonly oidc?: OIDCConfig; + readonly telemetry?: TelemetryConfig; + readonly tls?: TLSConfig; + readonly trace?: TraceConfig; + readonly secure_auth_cookie?: boolean; + readonly strict_transport_security?: number; // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray") - readonly strict_transport_security_options?: string[] - readonly ssh_keygen_algorithm?: string - readonly metrics_cache_refresh_interval?: number - readonly agent_stat_refresh_interval?: number - readonly agent_fallback_troubleshooting_url?: string - readonly browser_only?: boolean - readonly scim_api_key?: string - readonly provisioner?: ProvisionerConfig - readonly rate_limit?: RateLimitConfig + readonly strict_transport_security_options?: string[]; + readonly ssh_keygen_algorithm?: string; + readonly metrics_cache_refresh_interval?: number; + readonly agent_stat_refresh_interval?: number; + readonly agent_fallback_troubleshooting_url?: string; + readonly browser_only?: boolean; + readonly scim_api_key?: string; + readonly provisioner?: ProvisionerConfig; + readonly rate_limit?: RateLimitConfig; // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray") - readonly experiments?: string[] - readonly update_check?: boolean - readonly max_token_lifetime?: number - readonly swagger?: SwaggerConfig - readonly logging?: LoggingConfig - readonly dangerous?: DangerousConfig - readonly disable_path_apps?: boolean - readonly max_session_expiry?: number - readonly disable_session_expiry_refresh?: boolean - readonly disable_password_auth?: boolean - readonly support?: SupportConfig + readonly experiments?: string[]; + readonly update_check?: boolean; + readonly max_token_lifetime?: number; + readonly swagger?: SwaggerConfig; + readonly logging?: LoggingConfig; + readonly dangerous?: DangerousConfig; + readonly disable_path_apps?: boolean; + readonly max_session_expiry?: number; + readonly disable_session_expiry_refresh?: boolean; + readonly disable_password_auth?: boolean; + readonly support?: SupportConfig; // Named type "github.com/coder/coder/v2/cli/clibase.Struct[[]github.com/coder/coder/v2/codersdk.GitAuthConfig]" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly git_auth?: any - readonly config_ssh?: SSHConfig - readonly wgtunnel_host?: string - readonly disable_owner_workspace_exec?: boolean - readonly proxy_health_status_interval?: number - readonly enable_terraform_debug_mode?: boolean - readonly user_quiet_hours_schedule?: UserQuietHoursScheduleConfig + readonly git_auth?: any; + readonly config_ssh?: SSHConfig; + readonly wgtunnel_host?: string; + readonly disable_owner_workspace_exec?: boolean; + readonly proxy_health_status_interval?: number; + readonly enable_terraform_debug_mode?: boolean; + readonly user_quiet_hours_schedule?: UserQuietHoursScheduleConfig; // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.YAMLConfigPath") - readonly config?: string - readonly write_config?: boolean + readonly config?: string; + readonly write_config?: boolean; // Named type "github.com/coder/coder/v2/cli/clibase.HostPort" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly address?: any + readonly address?: any; } // From codersdk/deployment.go export interface Entitlements { - readonly features: Record - readonly warnings: string[] - readonly errors: string[] - readonly has_license: boolean - readonly trial: boolean - readonly require_telemetry: boolean - readonly refreshed_at: string + readonly features: Record; + readonly warnings: string[]; + readonly errors: string[]; + readonly has_license: boolean; + readonly trial: boolean; + readonly require_telemetry: boolean; + readonly refreshed_at: string; } // From codersdk/deployment.go -export type Experiments = Experiment[] +export type Experiments = Experiment[]; // From codersdk/deployment.go export interface Feature { - readonly entitlement: Entitlement - readonly enabled: boolean - readonly limit?: number - readonly actual?: number + readonly entitlement: Entitlement; + readonly enabled: boolean; + readonly limit?: number; + readonly actual?: number; } // From codersdk/apikey.go export interface GenerateAPIKeyResponse { - readonly key: string + readonly key: string; } // From codersdk/users.go export interface GetUsersResponse { - readonly users: User[] - readonly count: number + readonly users: User[]; + readonly count: number; } // From codersdk/gitauth.go export interface GitAuth { - readonly authenticated: boolean - readonly device: boolean - readonly type: string - readonly user?: GitAuthUser - readonly app_installable: boolean - readonly installations: GitAuthAppInstallation[] - readonly app_install_url: string + readonly authenticated: boolean; + readonly device: boolean; + readonly type: string; + readonly user?: GitAuthUser; + readonly app_installable: boolean; + readonly installations: GitAuthAppInstallation[]; + readonly app_install_url: string; } // From codersdk/gitauth.go export interface GitAuthAppInstallation { - readonly id: number - readonly account: GitAuthUser - readonly configure_url: string + readonly id: number; + readonly account: GitAuthUser; + readonly configure_url: string; } // From codersdk/deployment.go export interface GitAuthConfig { - readonly id: string - readonly type: string - readonly client_id: string - readonly auth_url: string - readonly token_url: string - readonly validate_url: string - readonly app_install_url: string - readonly app_installations_url: string - readonly regex: string - readonly no_refresh: boolean - readonly scopes: string[] - readonly device_flow: boolean - readonly device_code_url: string + readonly id: string; + readonly type: string; + readonly client_id: string; + readonly auth_url: string; + readonly token_url: string; + readonly validate_url: string; + readonly app_install_url: string; + readonly app_installations_url: string; + readonly regex: string; + readonly no_refresh: boolean; + readonly scopes: string[]; + readonly device_flow: boolean; + readonly device_code_url: string; } // From codersdk/gitauth.go export interface GitAuthDevice { - readonly device_code: string - readonly user_code: string - readonly verification_uri: string - readonly expires_in: number - readonly interval: number + readonly device_code: string; + readonly user_code: string; + readonly verification_uri: string; + readonly expires_in: number; + readonly interval: number; } // From codersdk/gitauth.go export interface GitAuthDeviceExchange { - readonly device_code: string + readonly device_code: string; } // From codersdk/gitauth.go export interface GitAuthUser { - readonly login: string - readonly avatar_url: string - readonly profile_url: string - readonly name: string + readonly login: string; + readonly avatar_url: string; + readonly profile_url: string; + readonly name: string; } // From codersdk/gitsshkey.go export interface GitSSHKey { - readonly user_id: string - readonly created_at: string - readonly updated_at: string - readonly public_key: string + readonly user_id: string; + readonly created_at: string; + readonly updated_at: string; + readonly public_key: string; } // From codersdk/groups.go export interface Group { - readonly id: string - readonly name: string - readonly display_name: string - readonly organization_id: string - readonly members: User[] - readonly avatar_url: string - readonly quota_allowance: number - readonly source: GroupSource + readonly id: string; + readonly name: string; + readonly display_name: string; + readonly organization_id: string; + readonly members: User[]; + readonly avatar_url: string; + readonly quota_allowance: number; + readonly source: GroupSource; } // From codersdk/workspaceapps.go export interface Healthcheck { - readonly url: string - readonly interval: number - readonly threshold: number + readonly url: string; + readonly interval: number; + readonly threshold: number; } // From codersdk/workspaceagents.go export interface IssueReconnectingPTYSignedTokenRequest { - readonly url: string - readonly agentID: string + readonly url: string; + readonly agentID: string; } // From codersdk/workspaceagents.go export interface IssueReconnectingPTYSignedTokenResponse { - readonly signed_token: string + readonly signed_token: string; } // From codersdk/licenses.go export interface License { - readonly id: number - readonly uuid: string - readonly uploaded_at: string + readonly id: number; + readonly uuid: string; + readonly uploaded_at: string; // Empty interface{} type, cannot resolve the type. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- interface{} - readonly claims: Record + readonly claims: Record; } // From codersdk/deployment.go export interface LinkConfig { - readonly name: string - readonly target: string - readonly icon: string + readonly name: string; + readonly target: string; + readonly icon: string; } // From codersdk/deployment.go export interface LoggingConfig { // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray") - readonly log_filter: string[] - readonly human: string - readonly json: string - readonly stackdriver: string + readonly log_filter: string[]; + readonly human: string; + readonly json: string; + readonly stackdriver: string; } // From codersdk/users.go export interface LoginWithPasswordRequest { - readonly email: string - readonly password: string + readonly email: string; + readonly password: string; } // From codersdk/users.go export interface LoginWithPasswordResponse { - readonly session_token: string + readonly session_token: string; } // From codersdk/users.go export interface MinimalUser { - readonly id: string - readonly username: string - readonly avatar_url: string + readonly id: string; + readonly username: string; + readonly avatar_url: string; } // From codersdk/deployment.go export interface OAuth2Config { - readonly github: OAuth2GithubConfig + readonly github: OAuth2GithubConfig; } // From codersdk/deployment.go export interface OAuth2GithubConfig { - readonly client_id: string - readonly client_secret: string + readonly client_id: string; + readonly client_secret: string; // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray") - readonly allowed_orgs: string[] + readonly allowed_orgs: string[]; // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray") - readonly allowed_teams: string[] - readonly allow_signups: boolean - readonly allow_everyone: boolean - readonly enterprise_base_url: string + readonly allowed_teams: string[]; + readonly allow_signups: boolean; + readonly allow_everyone: boolean; + readonly enterprise_base_url: string; } // From codersdk/users.go export interface OAuthConversionResponse { - readonly state_string: string - readonly expires_at: string - readonly to_type: LoginType - readonly user_id: string + readonly state_string: string; + readonly expires_at: string; + readonly to_type: LoginType; + readonly user_id: string; } // From codersdk/users.go export interface OIDCAuthMethod extends AuthMethod { - readonly signInText: string - readonly iconUrl: string + readonly signInText: string; + readonly iconUrl: string; } // From codersdk/deployment.go export interface OIDCConfig { - readonly allow_signups: boolean - readonly client_id: string - readonly client_secret: string - readonly client_key_file: string - readonly client_cert_file: string + readonly allow_signups: boolean; + readonly client_id: string; + readonly client_secret: string; + readonly client_key_file: string; + readonly client_cert_file: string; // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray") - readonly email_domain: string[] - readonly issuer_url: string + readonly email_domain: string[]; + readonly issuer_url: string; // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray") - readonly scopes: string[] - readonly ignore_email_verified: boolean - readonly username_field: string - readonly email_field: string + readonly scopes: string[]; + readonly ignore_email_verified: boolean; + readonly username_field: string; + readonly email_field: string; // Named type "github.com/coder/coder/v2/cli/clibase.Struct[map[string]string]" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly auth_url_params: any - readonly ignore_user_info: boolean - readonly group_auto_create: boolean + readonly auth_url_params: any; + readonly ignore_user_info: boolean; + readonly group_auto_create: boolean; // Named type "github.com/coder/coder/v2/cli/clibase.Regexp" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly group_regex_filter: any - readonly groups_field: string + readonly group_regex_filter: any; + readonly groups_field: string; // Named type "github.com/coder/coder/v2/cli/clibase.Struct[map[string]string]" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly group_mapping: any - readonly user_role_field: string + readonly group_mapping: any; + readonly user_role_field: string; // Named type "github.com/coder/coder/v2/cli/clibase.Struct[map[string][]string]" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly user_role_mapping: any + readonly user_role_mapping: any; // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray") - readonly user_roles_default: string[] - readonly sign_in_text: string - readonly icon_url: string + readonly user_roles_default: string[]; + readonly sign_in_text: string; + readonly icon_url: string; } // From codersdk/organizations.go export interface Organization { - readonly id: string - readonly name: string - readonly created_at: string - readonly updated_at: string + readonly id: string; + readonly name: string; + readonly created_at: string; + readonly updated_at: string; } // From codersdk/organizations.go export interface OrganizationMember { - readonly user_id: string - readonly organization_id: string - readonly created_at: string - readonly updated_at: string - readonly roles: Role[] + readonly user_id: string; + readonly organization_id: string; + readonly created_at: string; + readonly updated_at: string; + readonly roles: Role[]; } // From codersdk/pagination.go export interface Pagination { - readonly after_id?: string - readonly limit?: number - readonly offset?: number + readonly after_id?: string; + readonly limit?: number; + readonly offset?: number; } // From codersdk/groups.go export interface PatchGroupRequest { - readonly add_users: string[] - readonly remove_users: string[] - readonly name: string - readonly display_name?: string - readonly avatar_url?: string - readonly quota_allowance?: number + readonly add_users: string[]; + readonly remove_users: string[]; + readonly name: string; + readonly display_name?: string; + readonly avatar_url?: string; + readonly quota_allowance?: number; } // From codersdk/templateversions.go export interface PatchTemplateVersionRequest { - readonly name: string - readonly message?: string + readonly name: string; + readonly message?: string; } // From codersdk/workspaceproxy.go export interface PatchWorkspaceProxy { - readonly id: string - readonly name: string - readonly display_name: string - readonly icon: string - readonly regenerate_token: boolean + readonly id: string; + readonly name: string; + readonly display_name: string; + readonly icon: string; + readonly regenerate_token: boolean; } // From codersdk/deployment.go export interface PprofConfig { - readonly enable: boolean + readonly enable: boolean; // Named type "github.com/coder/coder/v2/cli/clibase.HostPort" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly address: any + readonly address: any; } // From codersdk/deployment.go export interface PrometheusConfig { - readonly enable: boolean + readonly enable: boolean; // Named type "github.com/coder/coder/v2/cli/clibase.HostPort" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly address: any - readonly collect_agent_stats: boolean - readonly collect_db_metrics: boolean + readonly address: any; + readonly collect_agent_stats: boolean; + readonly collect_db_metrics: boolean; } // From codersdk/deployment.go export interface ProvisionerConfig { - readonly daemons: number - readonly daemons_echo: boolean - readonly daemon_poll_interval: number - readonly daemon_poll_jitter: number - readonly force_cancel_interval: number - readonly daemon_psk: string + readonly daemons: number; + readonly daemons_echo: boolean; + readonly daemon_poll_interval: number; + readonly daemon_poll_jitter: number; + readonly force_cancel_interval: number; + readonly daemon_psk: string; } // From codersdk/provisionerdaemons.go export interface ProvisionerDaemon { - readonly id: string - readonly created_at: string - readonly updated_at?: string - readonly name: string - readonly provisioners: ProvisionerType[] - readonly tags: Record + readonly id: string; + readonly created_at: string; + readonly updated_at?: string; + readonly name: string; + readonly provisioners: ProvisionerType[]; + readonly tags: Record; } // From codersdk/provisionerdaemons.go export interface ProvisionerJob { - readonly id: string - readonly created_at: string - readonly started_at?: string - readonly completed_at?: string - readonly canceled_at?: string - readonly error?: string - readonly error_code?: JobErrorCode - readonly status: ProvisionerJobStatus - readonly worker_id?: string - readonly file_id: string - readonly tags: Record - readonly queue_position: number - readonly queue_size: number + readonly id: string; + readonly created_at: string; + readonly started_at?: string; + readonly completed_at?: string; + readonly canceled_at?: string; + readonly error?: string; + readonly error_code?: JobErrorCode; + readonly status: ProvisionerJobStatus; + readonly worker_id?: string; + readonly file_id: string; + readonly tags: Record; + readonly queue_position: number; + readonly queue_size: number; } // From codersdk/provisionerdaemons.go export interface ProvisionerJobLog { - readonly id: number - readonly created_at: string - readonly log_source: LogSource - readonly log_level: LogLevel - readonly stage: string - readonly output: string + readonly id: number; + readonly created_at: string; + readonly log_source: LogSource; + readonly log_level: LogLevel; + readonly stage: string; + readonly output: string; } // From codersdk/workspaceproxy.go export interface ProxyHealthReport { - readonly errors: string[] - readonly warnings: string[] + readonly errors: string[]; + readonly warnings: string[]; } // From codersdk/workspaces.go export interface PutExtendWorkspaceRequest { - readonly deadline: string + readonly deadline: string; } // From codersdk/deployment.go export interface RateLimitConfig { - readonly disable_all: boolean - readonly api: number + readonly disable_all: boolean; + readonly api: number; } // From codersdk/workspaceproxy.go export interface Region { - readonly id: string - readonly name: string - readonly display_name: string - readonly icon_url: string - readonly healthy: boolean - readonly path_app_url: string - readonly wildcard_hostname: string + readonly id: string; + readonly name: string; + readonly display_name: string; + readonly icon_url: string; + readonly healthy: boolean; + readonly path_app_url: string; + readonly wildcard_hostname: string; } // From codersdk/workspaceproxy.go export interface RegionsResponse { - readonly regions: R[] + readonly regions: R[]; } // From codersdk/replicas.go export interface Replica { - readonly id: string - readonly hostname: string - readonly created_at: string - readonly relay_address: string - readonly region_id: number - readonly error: string - readonly database_latency: number + readonly id: string; + readonly hostname: string; + readonly created_at: string; + readonly relay_address: string; + readonly region_id: number; + readonly error: string; + readonly database_latency: number; } // From codersdk/client.go export interface Response { - readonly message: string - readonly detail?: string - readonly validations?: ValidationError[] + readonly message: string; + readonly detail?: string; + readonly validations?: ValidationError[]; } // From codersdk/roles.go export interface Role { - readonly name: string - readonly display_name: string + readonly name: string; + readonly display_name: string; } // From codersdk/deployment.go export interface SSHConfig { - readonly DeploymentName: string + readonly DeploymentName: string; // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray") - readonly SSHConfigOptions: string[] + readonly SSHConfigOptions: string[]; } // From codersdk/deployment.go export interface SSHConfigResponse { - readonly hostname_prefix: string - readonly ssh_config_options: Record + readonly hostname_prefix: string; + readonly ssh_config_options: Record; } // From codersdk/serversentevents.go export interface ServerSentEvent { - readonly type: ServerSentEventType + readonly type: ServerSentEventType; // Empty interface{} type, cannot resolve the type. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- interface{} - readonly data: any + readonly data: any; } // From codersdk/deployment.go export interface ServiceBannerConfig { - readonly enabled: boolean - readonly message?: string - readonly background_color?: string + readonly enabled: boolean; + readonly message?: string; + readonly background_color?: string; } // From codersdk/deployment.go export interface SessionCountDeploymentStats { - readonly vscode: number - readonly ssh: number - readonly jetbrains: number - readonly reconnecting_pty: number + readonly vscode: number; + readonly ssh: number; + readonly jetbrains: number; + readonly reconnecting_pty: number; } // From codersdk/deployment.go export interface SupportConfig { // Named type "github.com/coder/coder/v2/cli/clibase.Struct[[]github.com/coder/coder/v2/codersdk.LinkConfig]" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly links: any + readonly links: any; } // From codersdk/deployment.go export interface SwaggerConfig { - readonly enable: boolean + readonly enable: boolean; } // From codersdk/deployment.go export interface TLSConfig { - readonly enable: boolean + readonly enable: boolean; // Named type "github.com/coder/coder/v2/cli/clibase.HostPort" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly address: any - readonly redirect_http: boolean + readonly address: any; + readonly redirect_http: boolean; // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray") - readonly cert_file: string[] - readonly client_auth: string - readonly client_ca_file: string + readonly cert_file: string[]; + readonly client_auth: string; + readonly client_ca_file: string; // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray") - readonly key_file: string[] - readonly min_version: string - readonly client_cert_file: string - readonly client_key_file: string + readonly key_file: string[]; + readonly min_version: string; + readonly client_cert_file: string; + readonly client_key_file: string; } // From codersdk/deployment.go export interface TelemetryConfig { - readonly enable: boolean - readonly trace: boolean - readonly url: string + readonly enable: boolean; + readonly trace: boolean; + readonly url: string; } // From codersdk/templates.go export interface Template { - readonly id: string - readonly created_at: string - readonly updated_at: string - readonly organization_id: string - readonly name: string - readonly display_name: string - readonly provisioner: ProvisionerType - readonly active_version_id: string - readonly active_user_count: number - readonly build_time_stats: TemplateBuildTimeStats - readonly description: string - readonly icon: string - readonly default_ttl_ms: number - readonly max_ttl_ms: number - readonly autostop_requirement: TemplateAutostopRequirement - readonly created_by_id: string - readonly created_by_name: string - readonly allow_user_autostart: boolean - readonly allow_user_autostop: boolean - readonly allow_user_cancel_workspace_jobs: boolean - readonly failure_ttl_ms: number - readonly time_til_dormant_ms: number - readonly time_til_dormant_autodelete_ms: number + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly organization_id: string; + readonly name: string; + readonly display_name: string; + readonly provisioner: ProvisionerType; + readonly active_version_id: string; + readonly active_user_count: number; + readonly build_time_stats: TemplateBuildTimeStats; + readonly description: string; + readonly icon: string; + readonly default_ttl_ms: number; + readonly max_ttl_ms: number; + readonly autostop_requirement: TemplateAutostopRequirement; + readonly created_by_id: string; + readonly created_by_name: string; + readonly allow_user_autostart: boolean; + readonly allow_user_autostop: boolean; + readonly allow_user_cancel_workspace_jobs: boolean; + readonly failure_ttl_ms: number; + readonly time_til_dormant_ms: number; + readonly time_til_dormant_autodelete_ms: number; } // From codersdk/templates.go export interface TemplateACL { - readonly users: TemplateUser[] - readonly group: TemplateGroup[] + readonly users: TemplateUser[]; + readonly group: TemplateGroup[]; } // From codersdk/insights.go export interface TemplateAppUsage { - readonly template_ids: string[] - readonly type: TemplateAppsType - readonly display_name: string - readonly slug: string - readonly icon: string - readonly seconds: number + readonly template_ids: string[]; + readonly type: TemplateAppsType; + readonly display_name: string; + readonly slug: string; + readonly icon: string; + readonly seconds: number; } // From codersdk/templates.go export interface TemplateAutostopRequirement { - readonly days_of_week: string[] - readonly weeks: number + readonly days_of_week: string[]; + readonly weeks: number; } // From codersdk/templates.go export type TemplateBuildTimeStats = Record< WorkspaceTransition, TransitionStats -> +>; // From codersdk/templates.go export interface TemplateExample { - readonly id: string - readonly url: string - readonly name: string - readonly description: string - readonly icon: string - readonly tags: string[] - readonly markdown: string + readonly id: string; + readonly url: string; + readonly name: string; + readonly description: string; + readonly icon: string; + readonly tags: string[]; + readonly markdown: string; } // From codersdk/templates.go export interface TemplateGroup extends Group { - readonly role: TemplateRole + readonly role: TemplateRole; } // From codersdk/insights.go export interface TemplateInsightsIntervalReport { - readonly start_time: string - readonly end_time: string - readonly template_ids: string[] - readonly interval: InsightsReportInterval - readonly active_users: number + readonly start_time: string; + readonly end_time: string; + readonly template_ids: string[]; + readonly interval: InsightsReportInterval; + readonly active_users: number; } // From codersdk/insights.go export interface TemplateInsightsReport { - readonly start_time: string - readonly end_time: string - readonly template_ids: string[] - readonly active_users: number - readonly apps_usage: TemplateAppUsage[] - readonly parameters_usage: TemplateParameterUsage[] + readonly start_time: string; + readonly end_time: string; + readonly template_ids: string[]; + readonly active_users: number; + readonly apps_usage: TemplateAppUsage[]; + readonly parameters_usage: TemplateParameterUsage[]; } // From codersdk/insights.go export interface TemplateInsightsRequest { - readonly start_time: string - readonly end_time: string - readonly template_ids: string[] - readonly interval: InsightsReportInterval + readonly start_time: string; + readonly end_time: string; + readonly template_ids: string[]; + readonly interval: InsightsReportInterval; } // From codersdk/insights.go export interface TemplateInsightsResponse { - readonly report: TemplateInsightsReport - readonly interval_reports: TemplateInsightsIntervalReport[] + readonly report: TemplateInsightsReport; + readonly interval_reports: TemplateInsightsIntervalReport[]; } // From codersdk/insights.go export interface TemplateParameterUsage { - readonly template_ids: string[] - readonly display_name: string - readonly name: string - readonly type: string - readonly description: string - readonly options?: TemplateVersionParameterOption[] - readonly values: TemplateParameterValue[] + readonly template_ids: string[]; + readonly display_name: string; + readonly name: string; + readonly type: string; + readonly description: string; + readonly options?: TemplateVersionParameterOption[]; + readonly values: TemplateParameterValue[]; } // From codersdk/insights.go export interface TemplateParameterValue { - readonly value: string - readonly count: number + readonly value: string; + readonly count: number; } // From codersdk/templates.go export interface TemplateUser extends User { - readonly role: TemplateRole + readonly role: TemplateRole; } // From codersdk/templateversions.go export interface TemplateVersion { - readonly id: string - readonly template_id?: string - readonly organization_id?: string - readonly created_at: string - readonly updated_at: string - readonly name: string - readonly message: string - readonly job: ProvisionerJob - readonly readme: string - readonly created_by: MinimalUser - readonly warnings?: TemplateVersionWarning[] + readonly id: string; + readonly template_id?: string; + readonly organization_id?: string; + readonly created_at: string; + readonly updated_at: string; + readonly name: string; + readonly message: string; + readonly job: ProvisionerJob; + readonly readme: string; + readonly created_by: MinimalUser; + readonly warnings?: TemplateVersionWarning[]; } // From codersdk/templateversions.go export interface TemplateVersionGitAuth { - readonly id: string - readonly type: GitProvider - readonly authenticate_url: string - readonly authenticated: boolean + readonly id: string; + readonly type: GitProvider; + readonly authenticate_url: string; + readonly authenticated: boolean; } // From codersdk/templateversions.go export interface TemplateVersionParameter { - readonly name: string - readonly display_name?: string - readonly description: string - readonly description_plaintext: string - readonly type: string - readonly mutable: boolean - readonly default_value: string - readonly icon: string - readonly options: TemplateVersionParameterOption[] - readonly validation_error?: string - readonly validation_regex?: string - readonly validation_min?: number - readonly validation_max?: number - readonly validation_monotonic?: ValidationMonotonicOrder - readonly required: boolean - readonly ephemeral: boolean + readonly name: string; + readonly display_name?: string; + readonly description: string; + readonly description_plaintext: string; + readonly type: string; + readonly mutable: boolean; + readonly default_value: string; + readonly icon: string; + readonly options: TemplateVersionParameterOption[]; + readonly validation_error?: string; + readonly validation_regex?: string; + readonly validation_min?: number; + readonly validation_max?: number; + readonly validation_monotonic?: ValidationMonotonicOrder; + readonly required: boolean; + readonly ephemeral: boolean; } // From codersdk/templateversions.go export interface TemplateVersionParameterOption { - readonly name: string - readonly description: string - readonly value: string - readonly icon: string + readonly name: string; + readonly description: string; + readonly value: string; + readonly icon: string; } // From codersdk/templateversions.go export interface TemplateVersionVariable { - readonly name: string - readonly description: string - readonly type: string - readonly value: string - readonly default_value: string - readonly required: boolean - readonly sensitive: boolean + readonly name: string; + readonly description: string; + readonly type: string; + readonly value: string; + readonly default_value: string; + readonly required: boolean; + readonly sensitive: boolean; } // From codersdk/templates.go export interface TemplateVersionsByTemplateRequest extends Pagination { - readonly template_id: string + readonly template_id: string; } // From codersdk/apikey.go export interface TokenConfig { - readonly max_token_lifetime: number + readonly max_token_lifetime: number; } // From codersdk/apikey.go export interface TokensFilter { - readonly include_all: boolean + readonly include_all: boolean; } // From codersdk/deployment.go export interface TraceConfig { - readonly enable: boolean - readonly honeycomb_api_key: string - readonly capture_logs: boolean - readonly data_dog: boolean + readonly enable: boolean; + readonly honeycomb_api_key: string; + readonly capture_logs: boolean; + readonly data_dog: boolean; } // From codersdk/templates.go export interface TransitionStats { - readonly P50?: number - readonly P95?: number + readonly P50?: number; + readonly P95?: number; } // From codersdk/templates.go export interface UpdateActiveTemplateVersion { - readonly id: string + readonly id: string; } // From codersdk/deployment.go export interface UpdateAppearanceConfig { - readonly logo_url: string - readonly service_banner: ServiceBannerConfig + readonly logo_url: string; + readonly service_banner: ServiceBannerConfig; } // From codersdk/updatecheck.go export interface UpdateCheckResponse { - readonly current: boolean - readonly version: string - readonly url: string + readonly current: boolean; + readonly version: string; + readonly url: string; } // From codersdk/users.go export interface UpdateRoles { - readonly roles: string[] + readonly roles: string[]; } // From codersdk/templates.go export interface UpdateTemplateACL { - readonly user_perms?: Record - readonly group_perms?: Record + readonly user_perms?: Record; + readonly group_perms?: Record; } // From codersdk/templates.go export interface UpdateTemplateMeta { - readonly name?: string - readonly display_name?: string - readonly description?: string - readonly icon?: string - readonly default_ttl_ms?: number - readonly max_ttl_ms?: number - readonly autostop_requirement?: TemplateAutostopRequirement - readonly allow_user_autostart?: boolean - readonly allow_user_autostop?: boolean - readonly allow_user_cancel_workspace_jobs?: boolean - readonly failure_ttl_ms?: number - readonly time_til_dormant_ms?: number - readonly time_til_dormant_autodelete_ms?: number - readonly update_workspace_last_used_at: boolean - readonly update_workspace_dormant_at: boolean + readonly name?: string; + readonly display_name?: string; + readonly description?: string; + readonly icon?: string; + readonly default_ttl_ms?: number; + readonly max_ttl_ms?: number; + readonly autostop_requirement?: TemplateAutostopRequirement; + readonly allow_user_autostart?: boolean; + readonly allow_user_autostop?: boolean; + readonly allow_user_cancel_workspace_jobs?: boolean; + readonly failure_ttl_ms?: number; + readonly time_til_dormant_ms?: number; + readonly time_til_dormant_autodelete_ms?: number; + readonly update_workspace_last_used_at: boolean; + readonly update_workspace_dormant_at: boolean; } // From codersdk/users.go export interface UpdateUserPasswordRequest { - readonly old_password: string - readonly password: string + readonly old_password: string; + readonly password: string; } // From codersdk/users.go export interface UpdateUserProfileRequest { - readonly username: string + readonly username: string; } // From codersdk/users.go export interface UpdateUserQuietHoursScheduleRequest { - readonly schedule: string + readonly schedule: string; } // From codersdk/workspaces.go export interface UpdateWorkspaceAutostartRequest { - readonly schedule?: string + readonly schedule?: string; } // From codersdk/workspaces.go export interface UpdateWorkspaceDormancy { - readonly dormant: boolean + readonly dormant: boolean; } // From codersdk/workspaceproxy.go export interface UpdateWorkspaceProxyResponse { - readonly proxy: WorkspaceProxy - readonly proxy_token: string + readonly proxy: WorkspaceProxy; + readonly proxy_token: string; } // From codersdk/workspaces.go export interface UpdateWorkspaceRequest { - readonly name?: string + readonly name?: string; } // From codersdk/workspaces.go export interface UpdateWorkspaceTTLRequest { - readonly ttl_ms?: number + readonly ttl_ms?: number; } // From codersdk/files.go export interface UploadResponse { - readonly hash: string + readonly hash: string; } // From codersdk/users.go export interface User { - readonly id: string - readonly username: string - readonly email: string - readonly created_at: string - readonly last_seen_at: string - readonly status: UserStatus - readonly organization_ids: string[] - readonly roles: Role[] - readonly avatar_url: string - readonly login_type: LoginType + readonly id: string; + readonly username: string; + readonly email: string; + readonly created_at: string; + readonly last_seen_at: string; + readonly status: UserStatus; + readonly organization_ids: string[]; + readonly roles: Role[]; + readonly avatar_url: string; + readonly login_type: LoginType; } // From codersdk/insights.go export interface UserLatency { - readonly template_ids: string[] - readonly user_id: string - readonly username: string - readonly avatar_url: string - readonly latency_ms: ConnectionLatency + readonly template_ids: string[]; + readonly user_id: string; + readonly username: string; + readonly avatar_url: string; + readonly latency_ms: ConnectionLatency; } // From codersdk/insights.go export interface UserLatencyInsightsReport { - readonly start_time: string - readonly end_time: string - readonly template_ids: string[] - readonly users: UserLatency[] + readonly start_time: string; + readonly end_time: string; + readonly template_ids: string[]; + readonly users: UserLatency[]; } // From codersdk/insights.go export interface UserLatencyInsightsRequest { - readonly start_time: string - readonly end_time: string - readonly template_ids: string[] + readonly start_time: string; + readonly end_time: string; + readonly template_ids: string[]; } // From codersdk/insights.go export interface UserLatencyInsightsResponse { - readonly report: UserLatencyInsightsReport + readonly report: UserLatencyInsightsReport; } // From codersdk/users.go export interface UserLoginType { - readonly login_type: LoginType + readonly login_type: LoginType; } // From codersdk/deployment.go export interface UserQuietHoursScheduleConfig { - readonly default_schedule: string + readonly default_schedule: string; } // From codersdk/users.go export interface UserQuietHoursScheduleResponse { - readonly raw_schedule: string - readonly user_set: boolean - readonly time: string - readonly timezone: string - readonly next: string + readonly raw_schedule: string; + readonly user_set: boolean; + readonly time: string; + readonly timezone: string; + readonly next: string; } // From codersdk/users.go export interface UserRoles { - readonly roles: string[] - readonly organization_roles: Record + readonly roles: string[]; + readonly organization_roles: Record; } // From codersdk/users.go export interface UsersRequest extends Pagination { - readonly q?: string + readonly q?: string; } // From codersdk/client.go export interface ValidationError { - readonly field: string - readonly detail: string + readonly field: string; + readonly detail: string; } // From codersdk/organizations.go export interface VariableValue { - readonly name: string - readonly value: string + readonly name: string; + readonly value: string; } // From codersdk/workspaces.go export interface Workspace { - readonly id: string - readonly created_at: string - readonly updated_at: string - readonly owner_id: string - readonly owner_name: string - readonly organization_id: string - readonly template_id: string - readonly template_name: string - readonly template_display_name: string - readonly template_icon: string - readonly template_allow_user_cancel_workspace_jobs: boolean - readonly template_active_version_id: string - readonly latest_build: WorkspaceBuild - readonly outdated: boolean - readonly name: string - readonly autostart_schedule?: string - readonly ttl_ms?: number - readonly last_used_at: string - readonly deleting_at?: string - readonly dormant_at?: string - readonly health: WorkspaceHealth + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly owner_id: string; + readonly owner_name: string; + readonly organization_id: string; + readonly template_id: string; + readonly template_name: string; + readonly template_display_name: string; + readonly template_icon: string; + readonly template_allow_user_cancel_workspace_jobs: boolean; + readonly template_active_version_id: string; + readonly latest_build: WorkspaceBuild; + readonly outdated: boolean; + readonly name: string; + readonly autostart_schedule?: string; + readonly ttl_ms?: number; + readonly last_used_at: string; + readonly deleting_at?: string; + readonly dormant_at?: string; + readonly health: WorkspaceHealth; } // From codersdk/workspaceagents.go export interface WorkspaceAgent { - readonly id: string - readonly created_at: string - readonly updated_at: string - readonly first_connected_at?: string - readonly last_connected_at?: string - readonly disconnected_at?: string - readonly started_at?: string - readonly ready_at?: string - readonly status: WorkspaceAgentStatus - readonly lifecycle_state: WorkspaceAgentLifecycle - readonly name: string - readonly resource_id: string - readonly instance_id?: string - readonly architecture: string - readonly environment_variables: Record - readonly operating_system: string - readonly startup_script?: string - readonly startup_script_behavior: WorkspaceAgentStartupScriptBehavior - readonly startup_script_timeout_seconds: number - readonly logs_length: number - readonly logs_overflowed: boolean - readonly directory?: string - readonly expanded_directory?: string - readonly version: string - readonly apps: WorkspaceApp[] - readonly latency?: Record - readonly connection_timeout_seconds: number - readonly troubleshooting_url: string - readonly login_before_ready: boolean - readonly shutdown_script?: string - readonly shutdown_script_timeout_seconds: number - readonly subsystems: AgentSubsystem[] - readonly health: WorkspaceAgentHealth - readonly display_apps: DisplayApp[] + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly first_connected_at?: string; + readonly last_connected_at?: string; + readonly disconnected_at?: string; + readonly started_at?: string; + readonly ready_at?: string; + readonly status: WorkspaceAgentStatus; + readonly lifecycle_state: WorkspaceAgentLifecycle; + readonly name: string; + readonly resource_id: string; + readonly instance_id?: string; + readonly architecture: string; + readonly environment_variables: Record; + readonly operating_system: string; + readonly startup_script?: string; + readonly startup_script_behavior: WorkspaceAgentStartupScriptBehavior; + readonly startup_script_timeout_seconds: number; + readonly logs_length: number; + readonly logs_overflowed: boolean; + readonly directory?: string; + readonly expanded_directory?: string; + readonly version: string; + readonly apps: WorkspaceApp[]; + readonly latency?: Record; + readonly connection_timeout_seconds: number; + readonly troubleshooting_url: string; + readonly login_before_ready: boolean; + readonly shutdown_script?: string; + readonly shutdown_script_timeout_seconds: number; + readonly subsystems: AgentSubsystem[]; + readonly health: WorkspaceAgentHealth; + readonly display_apps: DisplayApp[]; } // From codersdk/workspaceagents.go export interface WorkspaceAgentHealth { - readonly healthy: boolean - readonly reason?: string + readonly healthy: boolean; + readonly reason?: string; } // From codersdk/workspaceagentconn.go export interface WorkspaceAgentListeningPort { - readonly process_name: string - readonly network: string - readonly port: number + readonly process_name: string; + readonly network: string; + readonly port: number; } // From codersdk/workspaceagentconn.go export interface WorkspaceAgentListeningPortsResponse { - readonly ports: WorkspaceAgentListeningPort[] + readonly ports: WorkspaceAgentListeningPort[]; } // From codersdk/workspaceagents.go export interface WorkspaceAgentLog { - readonly id: number - readonly created_at: string - readonly output: string - readonly level: LogLevel + readonly id: number; + readonly created_at: string; + readonly output: string; + readonly level: LogLevel; } // From codersdk/workspaceagents.go export interface WorkspaceAgentMetadata { - readonly result: WorkspaceAgentMetadataResult - readonly description: WorkspaceAgentMetadataDescription + readonly result: WorkspaceAgentMetadataResult; + readonly description: WorkspaceAgentMetadataDescription; } // From codersdk/workspaceagents.go export interface WorkspaceAgentMetadataDescription { - readonly display_name: string - readonly key: string - readonly script: string - readonly interval: number - readonly timeout: number + readonly display_name: string; + readonly key: string; + readonly script: string; + readonly interval: number; + readonly timeout: number; } // From codersdk/workspaceagents.go export interface WorkspaceAgentMetadataResult { - readonly collected_at: string - readonly age: number - readonly value: string - readonly error: string + readonly collected_at: string; + readonly age: number; + readonly value: string; + readonly error: string; } // From codersdk/workspaceapps.go export interface WorkspaceApp { - readonly id: string - readonly url: string - readonly external: boolean - readonly slug: string - readonly display_name: string - readonly command?: string - readonly icon?: string - readonly subdomain: boolean - readonly sharing_level: WorkspaceAppSharingLevel - readonly healthcheck: Healthcheck - readonly health: WorkspaceAppHealth + readonly id: string; + readonly url: string; + readonly external: boolean; + readonly slug: string; + readonly display_name: string; + readonly command?: string; + readonly icon?: string; + readonly subdomain: boolean; + readonly sharing_level: WorkspaceAppSharingLevel; + readonly healthcheck: Healthcheck; + readonly health: WorkspaceAppHealth; } // From codersdk/workspacebuilds.go export interface WorkspaceBuild { - readonly id: string - readonly created_at: string - readonly updated_at: string - readonly workspace_id: string - readonly workspace_name: string - readonly workspace_owner_id: string - readonly workspace_owner_name: string - readonly template_version_id: string - readonly template_version_name: string - readonly build_number: number - readonly transition: WorkspaceTransition - readonly initiator_id: string - readonly initiator_name: string - readonly job: ProvisionerJob - readonly reason: BuildReason - readonly resources: WorkspaceResource[] - readonly deadline?: string - readonly max_deadline?: string - readonly status: WorkspaceStatus - readonly daily_cost: number + readonly id: string; + readonly created_at: string; + readonly updated_at: string; + readonly workspace_id: string; + readonly workspace_name: string; + readonly workspace_owner_id: string; + readonly workspace_owner_name: string; + readonly template_version_id: string; + readonly template_version_name: string; + readonly build_number: number; + readonly transition: WorkspaceTransition; + readonly initiator_id: string; + readonly initiator_name: string; + readonly job: ProvisionerJob; + readonly reason: BuildReason; + readonly resources: WorkspaceResource[]; + readonly deadline?: string; + readonly max_deadline?: string; + readonly status: WorkspaceStatus; + readonly daily_cost: number; } // From codersdk/workspacebuilds.go export interface WorkspaceBuildParameter { - readonly name: string - readonly value: string + readonly name: string; + readonly value: string; } // From codersdk/workspaces.go export interface WorkspaceBuildsRequest extends Pagination { - readonly WorkspaceID: string - readonly Since: string + readonly WorkspaceID: string; + readonly Since: string; } // From codersdk/deployment.go export interface WorkspaceConnectionLatencyMS { - readonly P50: number - readonly P95: number + readonly P50: number; + readonly P95: number; } // From codersdk/deployment.go export interface WorkspaceDeploymentStats { - readonly pending: number - readonly building: number - readonly running: number - readonly failed: number - readonly stopped: number - readonly connection_latency_ms: WorkspaceConnectionLatencyMS - readonly rx_bytes: number - readonly tx_bytes: number + readonly pending: number; + readonly building: number; + readonly running: number; + readonly failed: number; + readonly stopped: number; + readonly connection_latency_ms: WorkspaceConnectionLatencyMS; + readonly rx_bytes: number; + readonly tx_bytes: number; } // From codersdk/workspaces.go export interface WorkspaceFilter { - readonly q?: string + readonly q?: string; } // From codersdk/workspaces.go export interface WorkspaceHealth { - readonly healthy: boolean - readonly failing_agents: string[] + readonly healthy: boolean; + readonly failing_agents: string[]; } // From codersdk/workspaces.go export interface WorkspaceOptions { - readonly include_deleted?: boolean + readonly include_deleted?: boolean; } // From codersdk/workspaceproxy.go export interface WorkspaceProxy extends Region { - readonly derp_enabled: boolean - readonly derp_only: boolean - readonly status?: WorkspaceProxyStatus - readonly created_at: string - readonly updated_at: string - readonly deleted: boolean + readonly derp_enabled: boolean; + readonly derp_only: boolean; + readonly status?: WorkspaceProxyStatus; + readonly created_at: string; + readonly updated_at: string; + readonly deleted: boolean; } // From codersdk/deployment.go export interface WorkspaceProxyBuildInfo { - readonly workspace_proxy: boolean - readonly dashboard_url: string + readonly workspace_proxy: boolean; + readonly dashboard_url: string; } // From codersdk/workspaceproxy.go export interface WorkspaceProxyStatus { - readonly status: ProxyHealthStatus - readonly report?: ProxyHealthReport - readonly checked_at: string + readonly status: ProxyHealthStatus; + readonly report?: ProxyHealthReport; + readonly checked_at: string; } // From codersdk/workspaces.go export interface WorkspaceQuota { - readonly credits_consumed: number - readonly budget: number + readonly credits_consumed: number; + readonly budget: number; } // From codersdk/workspacebuilds.go export interface WorkspaceResource { - readonly id: string - readonly created_at: string - readonly job_id: string - readonly workspace_transition: WorkspaceTransition - readonly type: string - readonly name: string - readonly hide: boolean - readonly icon: string - readonly agents?: WorkspaceAgent[] - readonly metadata?: WorkspaceResourceMetadata[] - readonly daily_cost: number + readonly id: string; + readonly created_at: string; + readonly job_id: string; + readonly workspace_transition: WorkspaceTransition; + readonly type: string; + readonly name: string; + readonly hide: boolean; + readonly icon: string; + readonly agents?: WorkspaceAgent[]; + readonly metadata?: WorkspaceResourceMetadata[]; + readonly daily_cost: number; } // From codersdk/workspacebuilds.go export interface WorkspaceResourceMetadata { - readonly key: string - readonly value: string - readonly sensitive: boolean + readonly key: string; + readonly value: string; + readonly sensitive: boolean; } // From codersdk/workspaces.go export interface WorkspacesRequest extends Pagination { - readonly q?: string + readonly q?: string; } // From codersdk/workspaces.go export interface WorkspacesResponse { - readonly workspaces: Workspace[] - readonly count: number + readonly workspaces: Workspace[]; + readonly count: number; } // From codersdk/apikey.go -export type APIKeyScope = "all" | "application_connect" -export const APIKeyScopes: APIKeyScope[] = ["all", "application_connect"] +export type APIKeyScope = "all" | "application_connect"; +export const APIKeyScopes: APIKeyScope[] = ["all", "application_connect"]; // From codersdk/workspaceagents.go -export type AgentSubsystem = "envbox" | "envbuilder" | "exectrace" +export type AgentSubsystem = "envbox" | "envbuilder" | "exectrace"; export const AgentSubsystems: AgentSubsystem[] = [ "envbox", "envbuilder", "exectrace", -] +]; // From codersdk/audit.go export type AuditAction = @@ -1572,7 +1572,7 @@ export type AuditAction = | "register" | "start" | "stop" - | "write" + | "write"; export const AuditActions: AuditAction[] = [ "create", "delete", @@ -1582,15 +1582,15 @@ export const AuditActions: AuditAction[] = [ "start", "stop", "write", -] +]; // From codersdk/workspacebuilds.go -export type BuildReason = "autostart" | "autostop" | "initiator" +export type BuildReason = "autostart" | "autostop" | "initiator"; export const BuildReasons: BuildReason[] = [ "autostart", "autostop", "initiator", -] +]; // From codersdk/workspaceagents.go export type DisplayApp = @@ -1598,22 +1598,22 @@ export type DisplayApp = | "ssh_helper" | "vscode" | "vscode_insiders" - | "web_terminal" + | "web_terminal"; export const DisplayApps: DisplayApp[] = [ "port_forwarding_helper", "ssh_helper", "vscode", "vscode_insiders", "web_terminal", -] +]; // From codersdk/deployment.go -export type Entitlement = "entitled" | "grace_period" | "not_entitled" +export type Entitlement = "entitled" | "grace_period" | "not_entitled"; export const Entitlements: Entitlement[] = [ "entitled", "grace_period", "not_entitled", -] +]; // From codersdk/deployment.go export type Experiment = @@ -1622,7 +1622,7 @@ export type Experiment = | "single_tailnet" | "tailnet_pg_coordinator" | "template_autostop_requirement" - | "workspace_actions" + | "workspace_actions"; export const Experiments: Experiment[] = [ "deployment_health_page", "moons", @@ -1630,7 +1630,7 @@ export const Experiments: Experiment[] = [ "tailnet_pg_coordinator", "template_autostop_requirement", "workspace_actions", -] +]; // From codersdk/deployment.go export type FeatureName = @@ -1647,7 +1647,7 @@ export type FeatureName = | "user_limit" | "user_role_management" | "workspace_batch_actions" - | "workspace_proxy" + | "workspace_proxy"; export const FeatureNames: FeatureName[] = [ "advanced_template_scheduling", "appearance", @@ -1663,39 +1663,45 @@ export const FeatureNames: FeatureName[] = [ "user_role_management", "workspace_batch_actions", "workspace_proxy", -] +]; // From codersdk/workspaceagents.go -export type GitProvider = "azure-devops" | "bitbucket" | "github" | "gitlab" +export type GitProvider = "azure-devops" | "bitbucket" | "github" | "gitlab"; export const GitProviders: GitProvider[] = [ "azure-devops", "bitbucket", "github", "gitlab", -] +]; // From codersdk/groups.go -export type GroupSource = "oidc" | "user" -export const GroupSources: GroupSource[] = ["oidc", "user"] +export type GroupSource = "oidc" | "user"; +export const GroupSources: GroupSource[] = ["oidc", "user"]; // From codersdk/insights.go -export type InsightsReportInterval = "day" -export const InsightsReportIntervals: InsightsReportInterval[] = ["day"] +export type InsightsReportInterval = "day"; +export const InsightsReportIntervals: InsightsReportInterval[] = ["day"]; // From codersdk/provisionerdaemons.go -export type JobErrorCode = "REQUIRED_TEMPLATE_VARIABLES" -export const JobErrorCodes: JobErrorCode[] = ["REQUIRED_TEMPLATE_VARIABLES"] +export type JobErrorCode = "REQUIRED_TEMPLATE_VARIABLES"; +export const JobErrorCodes: JobErrorCode[] = ["REQUIRED_TEMPLATE_VARIABLES"]; // From codersdk/provisionerdaemons.go -export type LogLevel = "debug" | "error" | "info" | "trace" | "warn" -export const LogLevels: LogLevel[] = ["debug", "error", "info", "trace", "warn"] +export type LogLevel = "debug" | "error" | "info" | "trace" | "warn"; +export const LogLevels: LogLevel[] = [ + "debug", + "error", + "info", + "trace", + "warn", +]; // From codersdk/provisionerdaemons.go -export type LogSource = "provisioner" | "provisioner_daemon" -export const LogSources: LogSource[] = ["provisioner", "provisioner_daemon"] +export type LogSource = "provisioner" | "provisioner_daemon"; +export const LogSources: LogSource[] = ["provisioner", "provisioner_daemon"]; // From codersdk/apikey.go -export type LoginType = "" | "github" | "none" | "oidc" | "password" | "token" +export type LoginType = "" | "github" | "none" | "oidc" | "password" | "token"; export const LoginTypes: LoginType[] = [ "", "github", @@ -1703,7 +1709,7 @@ export const LoginTypes: LoginType[] = [ "oidc", "password", "token", -] +]; // From codersdk/provisionerdaemons.go export type ProvisionerJobStatus = @@ -1712,7 +1718,7 @@ export type ProvisionerJobStatus = | "failed" | "pending" | "running" - | "succeeded" + | "succeeded"; export const ProvisionerJobStatuses: ProvisionerJobStatus[] = [ "canceled", "canceling", @@ -1720,32 +1726,32 @@ export const ProvisionerJobStatuses: ProvisionerJobStatus[] = [ "pending", "running", "succeeded", -] +]; // From codersdk/workspaces.go -export type ProvisionerLogLevel = "debug" -export const ProvisionerLogLevels: ProvisionerLogLevel[] = ["debug"] +export type ProvisionerLogLevel = "debug"; +export const ProvisionerLogLevels: ProvisionerLogLevel[] = ["debug"]; // From codersdk/organizations.go -export type ProvisionerStorageMethod = "file" -export const ProvisionerStorageMethods: ProvisionerStorageMethod[] = ["file"] +export type ProvisionerStorageMethod = "file"; +export const ProvisionerStorageMethods: ProvisionerStorageMethod[] = ["file"]; // From codersdk/organizations.go -export type ProvisionerType = "echo" | "terraform" -export const ProvisionerTypes: ProvisionerType[] = ["echo", "terraform"] +export type ProvisionerType = "echo" | "terraform"; +export const ProvisionerTypes: ProvisionerType[] = ["echo", "terraform"]; // From codersdk/workspaceproxy.go export type ProxyHealthStatus = | "ok" | "unhealthy" | "unreachable" - | "unregistered" + | "unregistered"; export const ProxyHealthStatuses: ProxyHealthStatus[] = [ "ok", "unhealthy", "unreachable", "unregistered", -] +]; // From codersdk/rbacresources.go export type RBACResource = @@ -1770,7 +1776,7 @@ export type RBACResource = | "user_data" | "workspace" | "workspace_execution" - | "workspace_proxy" + | "workspace_proxy"; export const RBACResources: RBACResource[] = [ "api_key", "application_connect", @@ -1794,7 +1800,7 @@ export const RBACResources: RBACResource[] = [ "workspace", "workspace_execution", "workspace_proxy", -] +]; // From codersdk/audit.go export type ResourceType = @@ -1809,7 +1815,7 @@ export type ResourceType = | "user" | "workspace" | "workspace_build" - | "workspace_proxy" + | "workspace_proxy"; export const ResourceTypes: ResourceType[] = [ "api_key", "convert_login", @@ -1823,40 +1829,40 @@ export const ResourceTypes: ResourceType[] = [ "workspace", "workspace_build", "workspace_proxy", -] +]; // From codersdk/serversentevents.go -export type ServerSentEventType = "data" | "error" | "ping" +export type ServerSentEventType = "data" | "error" | "ping"; export const ServerSentEventTypes: ServerSentEventType[] = [ "data", "error", "ping", -] +]; // From codersdk/insights.go -export type TemplateAppsType = "app" | "builtin" -export const TemplateAppsTypes: TemplateAppsType[] = ["app", "builtin"] +export type TemplateAppsType = "app" | "builtin"; +export const TemplateAppsTypes: TemplateAppsType[] = ["app", "builtin"]; // From codersdk/templates.go -export type TemplateRole = "" | "admin" | "use" -export const TemplateRoles: TemplateRole[] = ["", "admin", "use"] +export type TemplateRole = "" | "admin" | "use"; +export const TemplateRoles: TemplateRole[] = ["", "admin", "use"]; // From codersdk/templateversions.go -export type TemplateVersionWarning = "UNSUPPORTED_WORKSPACES" +export type TemplateVersionWarning = "UNSUPPORTED_WORKSPACES"; export const TemplateVersionWarnings: TemplateVersionWarning[] = [ "UNSUPPORTED_WORKSPACES", -] +]; // From codersdk/users.go -export type UserStatus = "active" | "dormant" | "suspended" -export const UserStatuses: UserStatus[] = ["active", "dormant", "suspended"] +export type UserStatus = "active" | "dormant" | "suspended"; +export const UserStatuses: UserStatus[] = ["active", "dormant", "suspended"]; // From codersdk/templateversions.go -export type ValidationMonotonicOrder = "decreasing" | "increasing" +export type ValidationMonotonicOrder = "decreasing" | "increasing"; export const ValidationMonotonicOrders: ValidationMonotonicOrder[] = [ "decreasing", "increasing", -] +]; // From codersdk/workspaceagents.go export type WorkspaceAgentLifecycle = @@ -1868,7 +1874,7 @@ export type WorkspaceAgentLifecycle = | "shutting_down" | "start_error" | "start_timeout" - | "starting" + | "starting"; export const WorkspaceAgentLifecycles: WorkspaceAgentLifecycle[] = [ "created", "off", @@ -1879,7 +1885,7 @@ export const WorkspaceAgentLifecycles: WorkspaceAgentLifecycle[] = [ "start_error", "start_timeout", "starting", -] +]; // From codersdk/workspaceagents.go export type WorkspaceAgentLogSource = @@ -1888,7 +1894,7 @@ export type WorkspaceAgentLogSource = | "external" | "kubernetes" | "shutdown_script" - | "startup_script" + | "startup_script"; export const WorkspaceAgentLogSources: WorkspaceAgentLogSource[] = [ "envbox", "envbuilder", @@ -1896,46 +1902,46 @@ export const WorkspaceAgentLogSources: WorkspaceAgentLogSource[] = [ "kubernetes", "shutdown_script", "startup_script", -] +]; // From codersdk/workspaceagents.go -export type WorkspaceAgentStartupScriptBehavior = "blocking" | "non-blocking" +export type WorkspaceAgentStartupScriptBehavior = "blocking" | "non-blocking"; export const WorkspaceAgentStartupScriptBehaviors: WorkspaceAgentStartupScriptBehavior[] = - ["blocking", "non-blocking"] + ["blocking", "non-blocking"]; // From codersdk/workspaceagents.go export type WorkspaceAgentStatus = | "connected" | "connecting" | "disconnected" - | "timeout" + | "timeout"; export const WorkspaceAgentStatuses: WorkspaceAgentStatus[] = [ "connected", "connecting", "disconnected", "timeout", -] +]; // From codersdk/workspaceapps.go export type WorkspaceAppHealth = | "disabled" | "healthy" | "initializing" - | "unhealthy" + | "unhealthy"; export const WorkspaceAppHealths: WorkspaceAppHealth[] = [ "disabled", "healthy", "initializing", "unhealthy", -] +]; // From codersdk/workspaceapps.go -export type WorkspaceAppSharingLevel = "authenticated" | "owner" | "public" +export type WorkspaceAppSharingLevel = "authenticated" | "owner" | "public"; export const WorkspaceAppSharingLevels: WorkspaceAppSharingLevel[] = [ "authenticated", "owner", "public", -] +]; // From codersdk/workspacebuilds.go export type WorkspaceStatus = @@ -1948,7 +1954,7 @@ export type WorkspaceStatus = | "running" | "starting" | "stopped" - | "stopping" + | "stopping"; export const WorkspaceStatuses: WorkspaceStatus[] = [ "canceled", "canceling", @@ -1960,108 +1966,108 @@ export const WorkspaceStatuses: WorkspaceStatus[] = [ "starting", "stopped", "stopping", -] +]; // From codersdk/workspacebuilds.go -export type WorkspaceTransition = "delete" | "start" | "stop" +export type WorkspaceTransition = "delete" | "start" | "stop"; export const WorkspaceTransitions: WorkspaceTransition[] = [ "delete", "start", "stop", -] +]; // From codersdk/workspaceproxy.go -export type RegionTypes = Region | WorkspaceProxy +export type RegionTypes = Region | WorkspaceProxy; // The code below is generated from coderd/healthcheck. // From healthcheck/accessurl.go export interface HealthcheckAccessURLReport { - readonly access_url: string - readonly healthy: boolean - readonly reachable: boolean - readonly status_code: number - readonly healthz_response: string - readonly error?: string + readonly access_url: string; + readonly healthy: boolean; + readonly reachable: boolean; + readonly status_code: number; + readonly healthz_response: string; + readonly error?: string; } // From healthcheck/database.go export interface HealthcheckDatabaseReport { - readonly healthy: boolean - readonly reachable: boolean - readonly latency: string - readonly latency_ms: number - readonly error?: string + readonly healthy: boolean; + readonly reachable: boolean; + readonly latency: string; + readonly latency_ms: number; + readonly error?: string; } // From healthcheck/healthcheck.go export interface HealthcheckReport { - readonly time: string - readonly healthy: boolean - readonly failing_sections: string[] + readonly time: string; + readonly healthy: boolean; + readonly failing_sections: string[]; // Named type "github.com/coder/coder/v2/coderd/healthcheck/derphealth.Report" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly derp: any - readonly access_url: HealthcheckAccessURLReport - readonly websocket: HealthcheckWebsocketReport - readonly database: HealthcheckDatabaseReport - readonly coder_version: string + readonly derp: any; + readonly access_url: HealthcheckAccessURLReport; + readonly websocket: HealthcheckWebsocketReport; + readonly database: HealthcheckDatabaseReport; + readonly coder_version: string; } // From healthcheck/websocket.go export interface HealthcheckWebsocketReport { - readonly healthy: boolean - readonly body: string - readonly code: number - readonly error?: string + readonly healthy: boolean; + readonly body: string; + readonly code: number; + readonly error?: string; } // The code below is generated from coderd/healthcheck/derphealth. // From derphealth/derp.go export interface DerphealthNodeReport { - readonly healthy: boolean + readonly healthy: boolean; // Named type "tailscale.com/tailcfg.DERPNode" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly node?: any + readonly node?: any; // Named type "tailscale.com/derp.ServerInfoMessage" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly node_info: any - readonly can_exchange_messages: boolean - readonly round_trip_ping: string - readonly round_trip_ping_ms: number - readonly uses_websocket: boolean - readonly client_logs: string[][] - readonly client_errs: string[][] - readonly error?: string - readonly stun: DerphealthStunReport + readonly node_info: any; + readonly can_exchange_messages: boolean; + readonly round_trip_ping: string; + readonly round_trip_ping_ms: number; + readonly uses_websocket: boolean; + readonly client_logs: string[][]; + readonly client_errs: string[][]; + readonly error?: string; + readonly stun: DerphealthStunReport; } // From derphealth/derp.go export interface DerphealthRegionReport { - readonly healthy: boolean + readonly healthy: boolean; // Named type "tailscale.com/tailcfg.DERPRegion" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly region?: any - readonly node_reports: DerphealthNodeReport[] - readonly error?: string + readonly region?: any; + readonly node_reports: DerphealthNodeReport[]; + readonly error?: string; } // From derphealth/derp.go export interface DerphealthReport { - readonly healthy: boolean - readonly regions: Record + readonly healthy: boolean; + readonly regions: Record; // Named type "tailscale.com/net/netcheck.Report" unknown, using "any" // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type - readonly netcheck?: any - readonly netcheck_err?: string - readonly netcheck_logs: string[] - readonly error?: string + readonly netcheck?: any; + readonly netcheck_err?: string; + readonly netcheck_logs: string[]; + readonly error?: string; } // From derphealth/derp.go export interface DerphealthStunReport { - readonly Enabled: boolean - readonly CanSTUN: boolean - readonly Error?: string + readonly Enabled: boolean; + readonly CanSTUN: boolean; + readonly Error?: string; } diff --git a/site/src/app.tsx b/site/src/app.tsx index af58e5641b..ce4f4e1c61 100644 --- a/site/src/app.tsx +++ b/site/src/app.tsx @@ -1,14 +1,14 @@ -import CssBaseline from "@mui/material/CssBaseline" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import { AuthProvider } from "components/AuthProvider/AuthProvider" -import { FC, PropsWithChildren } from "react" -import { HelmetProvider } from "react-helmet-async" -import { AppRouter } from "./AppRouter" -import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary" -import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar" -import { dark } from "./theme" -import "./theme/globalFonts" -import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles" +import CssBaseline from "@mui/material/CssBaseline"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { AuthProvider } from "components/AuthProvider/AuthProvider"; +import { FC, PropsWithChildren } from "react"; +import { HelmetProvider } from "react-helmet-async"; +import { AppRouter } from "./AppRouter"; +import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary"; +import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar"; +import { dark } from "./theme"; +import "./theme/globalFonts"; +import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles"; const queryClient = new QueryClient({ defaultOptions: { @@ -19,7 +19,7 @@ const queryClient = new QueryClient({ networkMode: "offlineFirst", }, }, -}) +}); export const AppProviders: FC = ({ children }) => { return ( @@ -38,13 +38,13 @@ export const AppProviders: FC = ({ children }) => { - ) -} + ); +}; export const App: FC = () => { return ( - ) -} + ); +}; diff --git a/site/src/components/Alert/Alert.stories.tsx b/site/src/components/Alert/Alert.stories.tsx index c5139f7c1c..42142a77c9 100644 --- a/site/src/components/Alert/Alert.stories.tsx +++ b/site/src/components/Alert/Alert.stories.tsx @@ -1,21 +1,21 @@ -import { Alert } from "./Alert" -import Button from "@mui/material/Button" -import Link from "@mui/material/Link" -import type { Meta, StoryObj } from "@storybook/react" +import { Alert } from "./Alert"; +import Button from "@mui/material/Button"; +import Link from "@mui/material/Link"; +import type { Meta, StoryObj } from "@storybook/react"; const meta: Meta = { title: "components/Alert", component: Alert, -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; const ExampleAction = ( -) +); export const Success: Story = { args: { @@ -23,14 +23,14 @@ export const Success: Story = { severity: "success", onRetry: undefined, }, -} +}; export const Warning: Story = { args: { children: "This is a warning", severity: "warning", }, -} +}; export const WarningWithDismiss: Story = { args: { @@ -38,7 +38,7 @@ export const WarningWithDismiss: Story = { dismissible: true, severity: "warning", }, -} +}; export const WarningWithAction: Story = { args: { @@ -46,7 +46,7 @@ export const WarningWithAction: Story = { actions: [ExampleAction], severity: "warning", }, -} +}; export const WarningWithActionAndDismiss: Story = { args: { @@ -55,7 +55,7 @@ export const WarningWithActionAndDismiss: Story = { dismissible: true, severity: "warning", }, -} +}; export const WithChildren: Story = { args: { @@ -66,4 +66,4 @@ export const WithChildren: Story = { ), }, -} +}; diff --git a/site/src/components/Alert/Alert.tsx b/site/src/components/Alert/Alert.tsx index 6fb919436c..b0cba398c7 100644 --- a/site/src/components/Alert/Alert.tsx +++ b/site/src/components/Alert/Alert.tsx @@ -1,16 +1,16 @@ -import { useState, FC, ReactNode } from "react" -import Collapse from "@mui/material/Collapse" +import { useState, FC, ReactNode } from "react"; +import Collapse from "@mui/material/Collapse"; // eslint-disable-next-line no-restricted-imports -- It is the base component -import MuiAlert, { AlertProps as MuiAlertProps } from "@mui/material/Alert" -import Button from "@mui/material/Button" -import Box from "@mui/material/Box" +import MuiAlert, { AlertProps as MuiAlertProps } from "@mui/material/Alert"; +import Button from "@mui/material/Button"; +import Box from "@mui/material/Box"; export type AlertProps = MuiAlertProps & { - actions?: ReactNode - dismissible?: boolean - onRetry?: () => void - onDismiss?: () => void -} + actions?: ReactNode; + dismissible?: boolean; + onRetry?: () => void; + onDismiss?: () => void; +}; export const Alert: FC = ({ children, @@ -21,7 +21,7 @@ export const Alert: FC = ({ onDismiss, ...alertProps }) => { - const [open, setOpen] = useState(true) + const [open, setOpen] = useState(true); return ( @@ -47,8 +47,8 @@ export const Alert: FC = ({ variant="text" size="small" onClick={() => { - setOpen(false) - onDismiss && onDismiss() + setOpen(false); + onDismiss && onDismiss(); }} data-testid="dismiss-banner-btn" > @@ -61,8 +61,8 @@ export const Alert: FC = ({ {children} - ) -} + ); +}; export const AlertDetail = ({ children }: { children: ReactNode }) => { return ( @@ -74,5 +74,5 @@ export const AlertDetail = ({ children }: { children: ReactNode }) => { > {children} - ) -} + ); +}; diff --git a/site/src/components/Alert/ErrorAlert.stories.tsx b/site/src/components/Alert/ErrorAlert.stories.tsx index dafaddac85..723f5d84ae 100644 --- a/site/src/components/Alert/ErrorAlert.stories.tsx +++ b/site/src/components/Alert/ErrorAlert.stories.tsx @@ -1,13 +1,13 @@ -import Button from "@mui/material/Button" -import { mockApiError } from "testHelpers/entities" -import type { Meta, StoryObj } from "@storybook/react" -import { action } from "@storybook/addon-actions" -import { ErrorAlert } from "./ErrorAlert" +import Button from "@mui/material/Button"; +import { mockApiError } from "testHelpers/entities"; +import type { Meta, StoryObj } from "@storybook/react"; +import { action } from "@storybook/addon-actions"; +import { ErrorAlert } from "./ErrorAlert"; const mockError = mockApiError({ message: "Email or password was invalid", detail: "Password is invalid", -}) +}); const meta: Meta = { title: "components/ErrorAlert", @@ -17,16 +17,16 @@ const meta: Meta = { dismissible: false, onRetry: undefined, }, -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; const ExampleAction = ( -) +); export const WithOnlyMessage: Story = { args: { @@ -34,33 +34,33 @@ export const WithOnlyMessage: Story = { message: "Email or password was invalid", }), }, -} +}; export const WithDismiss: Story = { args: { dismissible: true, }, -} +}; export const WithAction: Story = { args: { actions: [ExampleAction], }, -} +}; export const WithActionAndDismiss: Story = { args: { actions: [ExampleAction], dismissible: true, }, -} +}; export const WithRetry: Story = { args: { onRetry: action("retry"), dismissible: true, }, -} +}; export const WithActionRetryAndDismiss: Story = { args: { @@ -68,10 +68,10 @@ export const WithActionRetryAndDismiss: Story = { onRetry: action("retry"), dismissible: true, }, -} +}; export const WithNonApiError: Story = { args: { error: new Error("Non API error here"), }, -} +}; diff --git a/site/src/components/Alert/ErrorAlert.tsx b/site/src/components/Alert/ErrorAlert.tsx index 5b46633471..40b125fdf4 100644 --- a/site/src/components/Alert/ErrorAlert.tsx +++ b/site/src/components/Alert/ErrorAlert.tsx @@ -1,17 +1,17 @@ -import { AlertProps, Alert, AlertDetail } from "./Alert" -import AlertTitle from "@mui/material/AlertTitle" -import { getErrorMessage, getErrorDetail } from "api/errors" -import { FC } from "react" +import { AlertProps, Alert, AlertDetail } from "./Alert"; +import AlertTitle from "@mui/material/AlertTitle"; +import { getErrorMessage, getErrorDetail } from "api/errors"; +import { FC } from "react"; export const ErrorAlert: FC< Omit & { error: unknown } > = ({ error, ...alertProps }) => { - const message = getErrorMessage(error, "Something went wrong.") - const detail = getErrorDetail(error) + const message = getErrorMessage(error, "Something went wrong."); + const detail = getErrorDetail(error); // For some reason, the message and detail can be the same on the BE, but does // not make sense in the FE to showing them duplicated - const shouldDisplayDetail = message !== detail + const shouldDisplayDetail = message !== detail; return ( @@ -24,5 +24,5 @@ export const ErrorAlert: FC< message )} - ) -} + ); +}; diff --git a/site/src/components/AuthProvider/AuthProvider.tsx b/site/src/components/AuthProvider/AuthProvider.tsx index 1b113e80fc..d35c117b41 100644 --- a/site/src/components/AuthProvider/AuthProvider.tsx +++ b/site/src/components/AuthProvider/AuthProvider.tsx @@ -1,36 +1,36 @@ -import { useActor, useInterpret } from "@xstate/react" -import { createContext, FC, PropsWithChildren, useContext } from "react" -import { authMachine } from "xServices/auth/authXService" -import { ActorRefFrom } from "xstate" +import { useActor, useInterpret } from "@xstate/react"; +import { createContext, FC, PropsWithChildren, useContext } from "react"; +import { authMachine } from "xServices/auth/authXService"; +import { ActorRefFrom } from "xstate"; interface AuthContextValue { - authService: ActorRefFrom + authService: ActorRefFrom; } -const AuthContext = createContext(undefined) +const AuthContext = createContext(undefined); export const AuthProvider: FC = ({ children }) => { - const authService = useInterpret(authMachine) + const authService = useInterpret(authMachine); return ( {children} - ) -} + ); +}; type UseAuthReturnType = ReturnType< typeof useActor -> +>; export const useAuth = (): UseAuthReturnType => { - const context = useContext(AuthContext) + const context = useContext(AuthContext); if (!context) { - throw new Error("useAuth should be used inside of ") + throw new Error("useAuth should be used inside of "); } - const auth = useActor(context.authService) + const auth = useActor(context.authService); - return auth -} + return auth; +}; diff --git a/site/src/components/Avatar/Avatar.stories.tsx b/site/src/components/Avatar/Avatar.stories.tsx index c7b2fc1513..b29026d300 100644 --- a/site/src/components/Avatar/Avatar.stories.tsx +++ b/site/src/components/Avatar/Avatar.stories.tsx @@ -1,61 +1,63 @@ -import { Story } from "@storybook/react" -import { Avatar, AvatarIcon, AvatarProps } from "./Avatar" -import PauseIcon from "@mui/icons-material/PauseOutlined" +import { Story } from "@storybook/react"; +import { Avatar, AvatarIcon, AvatarProps } from "./Avatar"; +import PauseIcon from "@mui/icons-material/PauseOutlined"; export default { title: "components/Avatar", component: Avatar, -} +}; -const Template: Story = (args: AvatarProps) => +const Template: Story = (args: AvatarProps) => ( + +); -export const Letter = Template.bind({}) +export const Letter = Template.bind({}); Letter.args = { children: "Coder", -} +}; -export const LetterXL = Template.bind({}) +export const LetterXL = Template.bind({}); LetterXL.args = { children: "Coder", size: "xl", -} +}; -export const LetterDarken = Template.bind({}) +export const LetterDarken = Template.bind({}); LetterDarken.args = { children: "Coder", colorScheme: "darken", -} +}; -export const Image = Template.bind({}) +export const Image = Template.bind({}); Image.args = { src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4", -} +}; -export const ImageXL = Template.bind({}) +export const ImageXL = Template.bind({}); ImageXL.args = { src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4", size: "xl", -} +}; -export const MuiIcon = Template.bind({}) +export const MuiIcon = Template.bind({}); MuiIcon.args = { children: , -} +}; -export const MuiIconDarken = Template.bind({}) +export const MuiIconDarken = Template.bind({}); MuiIconDarken.args = { children: , colorScheme: "darken", -} +}; -export const MuiIconXL = Template.bind({}) +export const MuiIconXL = Template.bind({}); MuiIconXL.args = { children: , size: "xl", -} +}; -export const AvatarIconDarken = Template.bind({}) +export const AvatarIconDarken = Template.bind({}); AvatarIconDarken.args = { children: , colorScheme: "darken", -} +}; diff --git a/site/src/components/Avatar/Avatar.tsx b/site/src/components/Avatar/Avatar.tsx index 20eb1f1ab2..9eee57f6db 100644 --- a/site/src/components/Avatar/Avatar.tsx +++ b/site/src/components/Avatar/Avatar.tsx @@ -1,16 +1,16 @@ // This is the only place MuiAvatar can be used // eslint-disable-next-line no-restricted-imports -- Read above -import MuiAvatar, { AvatarProps as MuiAvatarProps } from "@mui/material/Avatar" -import { makeStyles } from "@mui/styles" -import { FC } from "react" -import { combineClasses } from "utils/combineClasses" -import { firstLetter } from "./firstLetter" +import MuiAvatar, { AvatarProps as MuiAvatarProps } from "@mui/material/Avatar"; +import { makeStyles } from "@mui/styles"; +import { FC } from "react"; +import { combineClasses } from "utils/combineClasses"; +import { firstLetter } from "./firstLetter"; export type AvatarProps = MuiAvatarProps & { - size?: "sm" | "md" | "xl" - colorScheme?: "light" | "darken" - fitImage?: boolean -} + size?: "sm" | "md" | "xl"; + colorScheme?: "light" | "darken"; + fitImage?: boolean; +}; export const Avatar: FC = ({ size = "md", @@ -20,7 +20,7 @@ export const Avatar: FC = ({ children, ...muiProps }) => { - const styles = useStyles() + const styles = useStyles(); return ( = ({ {/* If the children is a string, we always want to render the first letter */} {typeof children === "string" ? firstLetter(children) : children} - ) -} + ); +}; /** * Use it to make an img element behaves like a MaterialUI Icon component */ export const AvatarIcon: FC<{ src: string }> = ({ src }) => { - const styles = useStyles() - return -} + const styles = useStyles(); + return ; +}; const useStyles = makeStyles((theme) => ({ // Size styles @@ -77,4 +77,4 @@ const useStyles = makeStyles((theme) => ({ objectFit: "contain", }, }, -})) +})); diff --git a/site/src/components/Avatar/firstLetter.test.ts b/site/src/components/Avatar/firstLetter.test.ts index 047026af9d..b7ba5dae35 100644 --- a/site/src/components/Avatar/firstLetter.test.ts +++ b/site/src/components/Avatar/firstLetter.test.ts @@ -1,4 +1,4 @@ -import { firstLetter } from "./firstLetter" +import { firstLetter } from "./firstLetter"; describe("first-letter", () => { it.each<[string, string]>([ @@ -6,6 +6,6 @@ describe("first-letter", () => { ["User", "U"], ["test", "T"], ])(`firstLetter(%p) returns %p`, (input, expected) => { - expect(firstLetter(input)).toBe(expected) - }) -}) + expect(firstLetter(input)).toBe(expected); + }); +}); diff --git a/site/src/components/Avatar/firstLetter.ts b/site/src/components/Avatar/firstLetter.ts index 7402615915..f6fa7a90ac 100644 --- a/site/src/components/Avatar/firstLetter.ts +++ b/site/src/components/Avatar/firstLetter.ts @@ -3,8 +3,8 @@ */ export const firstLetter = (str: string): string => { if (str.length > 0) { - return str[0].toLocaleUpperCase() + return str[0].toLocaleUpperCase(); } - return "" -} + return ""; +}; diff --git a/site/src/components/AvatarData/AvatarData.stories.tsx b/site/src/components/AvatarData/AvatarData.stories.tsx index bd4fa14310..05c1762524 100644 --- a/site/src/components/AvatarData/AvatarData.stories.tsx +++ b/site/src/components/AvatarData/AvatarData.stories.tsx @@ -1,24 +1,24 @@ -import { Story } from "@storybook/react" -import { AvatarData, AvatarDataProps } from "./AvatarData" +import { Story } from "@storybook/react"; +import { AvatarData, AvatarDataProps } from "./AvatarData"; export default { title: "components/AvatarData", component: AvatarData, -} +}; const Template: Story = (args: AvatarDataProps) => ( -) +); -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { title: "coder", subtitle: "coder@coder.com", -} +}; -export const WithImage = Template.bind({}) +export const WithImage = Template.bind({}); WithImage.args = { title: "coder", subtitle: "coder@coder.com", src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4", -} +}; diff --git a/site/src/components/AvatarData/AvatarData.tsx b/site/src/components/AvatarData/AvatarData.tsx index 135dba4de6..f7e4264af3 100644 --- a/site/src/components/AvatarData/AvatarData.tsx +++ b/site/src/components/AvatarData/AvatarData.tsx @@ -1,13 +1,13 @@ -import { Avatar } from "components/Avatar/Avatar" -import { FC, PropsWithChildren } from "react" -import { Stack } from "components/Stack/Stack" -import { makeStyles } from "@mui/styles" +import { Avatar } from "components/Avatar/Avatar"; +import { FC, PropsWithChildren } from "react"; +import { Stack } from "components/Stack/Stack"; +import { makeStyles } from "@mui/styles"; export interface AvatarDataProps { - title: string | JSX.Element - subtitle?: string - src?: string - avatar?: React.ReactNode + title: string | JSX.Element; + subtitle?: string; + src?: string; + avatar?: React.ReactNode; } export const AvatarData: FC> = ({ @@ -16,10 +16,10 @@ export const AvatarData: FC> = ({ src, avatar, }) => { - const styles = useStyles() + const styles = useStyles(); if (!avatar) { - avatar = {title} + avatar = {title}; } return ( @@ -36,8 +36,8 @@ export const AvatarData: FC> = ({ {subtitle && {subtitle}} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ root: { @@ -61,4 +61,4 @@ const useStyles = makeStyles((theme) => ({ lineHeight: "150%", maxWidth: 540, }, -})) +})); diff --git a/site/src/components/AvatarData/AvatarDataSkeleton.tsx b/site/src/components/AvatarData/AvatarDataSkeleton.tsx index a519dd8ca4..275bbff156 100644 --- a/site/src/components/AvatarData/AvatarDataSkeleton.tsx +++ b/site/src/components/AvatarData/AvatarDataSkeleton.tsx @@ -1,6 +1,6 @@ -import { FC } from "react" -import { Stack } from "components/Stack/Stack" -import Skeleton from "@mui/material/Skeleton" +import { FC } from "react"; +import { Stack } from "components/Stack/Stack"; +import Skeleton from "@mui/material/Skeleton"; export const AvatarDataSkeleton: FC = () => { return ( @@ -12,5 +12,5 @@ export const AvatarDataSkeleton: FC = () => { - ) -} + ); +}; diff --git a/site/src/components/BuildAvatar/BuildAvatar.tsx b/site/src/components/BuildAvatar/BuildAvatar.tsx index 3733db4650..810df5d83a 100644 --- a/site/src/components/BuildAvatar/BuildAvatar.tsx +++ b/site/src/components/BuildAvatar/BuildAvatar.tsx @@ -1,15 +1,15 @@ -import Badge from "@mui/material/Badge" -import { useTheme, withStyles } from "@mui/styles" -import { FC } from "react" -import { WorkspaceBuild } from "api/typesGenerated" -import { getDisplayWorkspaceBuildStatus } from "utils/workspace" -import { Avatar, AvatarProps } from "components/Avatar/Avatar" -import { PaletteIndex } from "theme/theme" -import { Theme } from "@mui/material/styles" -import { BuildIcon } from "components/BuildIcon/BuildIcon" +import Badge from "@mui/material/Badge"; +import { useTheme, withStyles } from "@mui/styles"; +import { FC } from "react"; +import { WorkspaceBuild } from "api/typesGenerated"; +import { getDisplayWorkspaceBuildStatus } from "utils/workspace"; +import { Avatar, AvatarProps } from "components/Avatar/Avatar"; +import { PaletteIndex } from "theme/theme"; +import { Theme } from "@mui/material/styles"; +import { BuildIcon } from "components/BuildIcon/BuildIcon"; interface StylesBadgeProps { - type: PaletteIndex + type: PaletteIndex; } const StyledBadge = withStyles((theme) => ({ @@ -22,16 +22,16 @@ const StyledBadge = withStyles((theme) => ({ display: "block", padding: 0, }, -}))(Badge) +}))(Badge); export interface BuildAvatarProps { - build: WorkspaceBuild - size?: AvatarProps["size"] + build: WorkspaceBuild; + size?: AvatarProps["size"]; } export const BuildAvatar: FC = ({ build, size }) => { - const theme = useTheme() - const displayBuildStatus = getDisplayWorkspaceBuildStatus(theme, build) + const theme = useTheme(); + const displayBuildStatus = getDisplayWorkspaceBuildStatus(theme, build); return ( = ({ build, size }) => { - ) -} + ); +}; diff --git a/site/src/components/BuildIcon/BuildIcon.tsx b/site/src/components/BuildIcon/BuildIcon.tsx index ccdc68da1c..41b87f66b6 100644 --- a/site/src/components/BuildIcon/BuildIcon.tsx +++ b/site/src/components/BuildIcon/BuildIcon.tsx @@ -1,22 +1,22 @@ -import PlayArrowOutlined from "@mui/icons-material/PlayArrowOutlined" -import StopOutlined from "@mui/icons-material/StopOutlined" -import DeleteOutlined from "@mui/icons-material/DeleteOutlined" -import { WorkspaceTransition } from "api/typesGenerated" -import { ComponentProps } from "react" +import PlayArrowOutlined from "@mui/icons-material/PlayArrowOutlined"; +import StopOutlined from "@mui/icons-material/StopOutlined"; +import DeleteOutlined from "@mui/icons-material/DeleteOutlined"; +import { WorkspaceTransition } from "api/typesGenerated"; +import { ComponentProps } from "react"; -type SVGIcon = typeof PlayArrowOutlined +type SVGIcon = typeof PlayArrowOutlined; -type SVGIconProps = ComponentProps +type SVGIconProps = ComponentProps; const iconByTransition: Record = { start: PlayArrowOutlined, stop: StopOutlined, delete: DeleteOutlined, -} +}; export const BuildIcon = ( props: SVGIconProps & { transition: WorkspaceTransition }, ) => { - const Icon = iconByTransition[props.transition] - return -} + const Icon = iconByTransition[props.transition]; + return ; +}; diff --git a/site/src/components/CodeExample/CodeExample.stories.tsx b/site/src/components/CodeExample/CodeExample.stories.tsx index aabd7e9fe2..6c80a6c77e 100644 --- a/site/src/components/CodeExample/CodeExample.stories.tsx +++ b/site/src/components/CodeExample/CodeExample.stories.tsx @@ -1,7 +1,7 @@ -import { Story } from "@storybook/react" -import { CodeExample, CodeExampleProps } from "./CodeExample" +import { Story } from "@storybook/react"; +import { CodeExample, CodeExampleProps } from "./CodeExample"; -const sampleCode = `echo "Hello, world"` +const sampleCode = `echo "Hello, world"`; export default { title: "components/CodeExample", @@ -9,18 +9,18 @@ export default { argTypes: { code: { control: "string", defaultValue: sampleCode }, }, -} +}; const Template: Story = (args: CodeExampleProps) => ( -) +); -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { code: sampleCode, -} +}; -export const LongCode = Template.bind({}) +export const LongCode = Template.bind({}); LongCode.args = { code: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICnKzATuWwmmt5+CKTPuRGN0R1PBemA+6/SStpLiyX+L", -} +}; diff --git a/site/src/components/CodeExample/CodeExample.test.tsx b/site/src/components/CodeExample/CodeExample.test.tsx index 32d992f44e..8079051ea0 100644 --- a/site/src/components/CodeExample/CodeExample.test.tsx +++ b/site/src/components/CodeExample/CodeExample.test.tsx @@ -1,14 +1,14 @@ -import { screen } from "@testing-library/react" -import { render } from "../../testHelpers/renderHelpers" -import { CodeExample } from "./CodeExample" +import { screen } from "@testing-library/react"; +import { render } from "../../testHelpers/renderHelpers"; +import { CodeExample } from "./CodeExample"; describe("CodeExample", () => { it("renders code", async () => { // When - render() + render(); // Then // Both lines should be rendered - await screen.findByText("echo hello") - }) -}) + await screen.findByText("echo hello"); + }); +}); diff --git a/site/src/components/CodeExample/CodeExample.tsx b/site/src/components/CodeExample/CodeExample.tsx index 7d18b286b4..cbf9c6a176 100644 --- a/site/src/components/CodeExample/CodeExample.tsx +++ b/site/src/components/CodeExample/CodeExample.tsx @@ -1,17 +1,17 @@ -import { makeStyles } from "@mui/styles" -import { FC } from "react" -import { MONOSPACE_FONT_FAMILY } from "../../theme/constants" -import { combineClasses } from "../../utils/combineClasses" -import { CopyButton } from "../CopyButton/CopyButton" -import { Theme } from "@mui/material/styles" +import { makeStyles } from "@mui/styles"; +import { FC } from "react"; +import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"; +import { combineClasses } from "../../utils/combineClasses"; +import { CopyButton } from "../CopyButton/CopyButton"; +import { Theme } from "@mui/material/styles"; export interface CodeExampleProps { - code: string - className?: string - buttonClassName?: string - tooltipTitle?: string - inline?: boolean - password?: boolean + code: string; + className?: string; + buttonClassName?: string; + tooltipTitle?: string; + inline?: boolean; + password?: boolean; } /** @@ -24,7 +24,7 @@ export const CodeExample: FC> = ({ tooltipTitle, inline, }) => { - const styles = useStyles({ inline: inline }) + const styles = useStyles({ inline: inline }); return (
@@ -35,12 +35,12 @@ export const CodeExample: FC> = ({ buttonClassName={buttonClassName} />
- ) -} + ); +}; interface styleProps { - inline?: boolean - password?: boolean + inline?: boolean; + password?: boolean; } const useStyles = makeStyles((theme) => ({ @@ -65,4 +65,4 @@ const useStyles = makeStyles((theme) => ({ wordBreak: "break-all", "-webkit-text-security": (props) => (props.password ? "disc" : undefined), }, -})) +})); diff --git a/site/src/components/Conditionals/ChooseOne.stories.tsx b/site/src/components/Conditionals/ChooseOne.stories.tsx index a6c1bf8483..a4663b8819 100644 --- a/site/src/components/Conditionals/ChooseOne.stories.tsx +++ b/site/src/components/Conditionals/ChooseOne.stories.tsx @@ -1,11 +1,11 @@ -import { Story } from "@storybook/react" -import { ChooseOne, Cond } from "./ChooseOne" +import { Story } from "@storybook/react"; +import { ChooseOne, Cond } from "./ChooseOne"; export default { title: "components/Conditionals/ChooseOne", component: ChooseOne, subcomponents: { Cond }, -} +}; export const FirstIsTrue: Story = () => ( @@ -13,7 +13,7 @@ export const FirstIsTrue: Story = () => ( The second one does not show. The default does not show. -) +); export const SecondIsTrue: Story = () => ( @@ -21,7 +21,7 @@ export const SecondIsTrue: Story = () => ( The second one shows. The default does not show. -) +); export const AllAreTrue: Story = () => ( @@ -29,7 +29,7 @@ export const AllAreTrue: Story = () => ( The second one does not show. The default does not show. -) +); export const NoneAreTrue: Story = () => ( @@ -37,10 +37,10 @@ export const NoneAreTrue: Story = () => ( The second one does not show. The default shows. -) +); export const OneCond: Story = () => ( An only child renders. -) +); diff --git a/site/src/components/Conditionals/ChooseOne.tsx b/site/src/components/Conditionals/ChooseOne.tsx index 0f9087a94b..36faac837d 100644 --- a/site/src/components/Conditionals/ChooseOne.tsx +++ b/site/src/components/Conditionals/ChooseOne.tsx @@ -1,7 +1,7 @@ -import { Children, PropsWithChildren } from "react" +import { Children, PropsWithChildren } from "react"; export interface CondProps { - condition?: boolean + condition?: boolean; } /** @@ -14,8 +14,8 @@ export interface CondProps { export const Cond = ({ children, }: PropsWithChildren): JSX.Element => { - return <>{children} -} + return <>{children}; +}; /** * Wrapper component for rendering exactly one of its children. Wrap each child in Cond to associate it @@ -27,22 +27,22 @@ export const Cond = ({ export const ChooseOne = ({ children, }: PropsWithChildren): JSX.Element | null => { - const childArray = Children.toArray(children) as JSX.Element[] + const childArray = Children.toArray(children) as JSX.Element[]; if (childArray.length === 0) { - return null + return null; } - const conditionedOptions = childArray.slice(0, childArray.length - 1) - const defaultCase = childArray[childArray.length - 1] + const conditionedOptions = childArray.slice(0, childArray.length - 1); + const defaultCase = childArray[childArray.length - 1]; if (defaultCase.props.condition !== undefined) { throw new Error( "The last Cond in a ChooseOne was given a condition prop, but it is the default case.", - ) + ); } if (conditionedOptions.some((cond) => cond.props.condition === undefined)) { throw new Error( "A non-final Cond in a ChooseOne does not have a condition prop or the prop is undefined.", - ) + ); } - const chosen = conditionedOptions.find((child) => child.props.condition) - return chosen ?? defaultCase -} + const chosen = conditionedOptions.find((child) => child.props.condition); + return chosen ?? defaultCase; +}; diff --git a/site/src/components/Conditionals/Maybe.stories.tsx b/site/src/components/Conditionals/Maybe.stories.tsx index 1d94a192a4..bc94f3f810 100644 --- a/site/src/components/Conditionals/Maybe.stories.tsx +++ b/site/src/components/Conditionals/Maybe.stories.tsx @@ -1,21 +1,21 @@ -import { Story } from "@storybook/react" -import { Maybe, MaybeProps } from "./Maybe" +import { Story } from "@storybook/react"; +import { Maybe, MaybeProps } from "./Maybe"; export default { title: "components/Conditionals/Maybe", component: Maybe, -} +}; const Template: Story = (args: MaybeProps) => ( Now you see me -) +); -export const ConditionIsTrue = Template.bind({}) +export const ConditionIsTrue = Template.bind({}); ConditionIsTrue.args = { condition: true, -} +}; -export const ConditionIsFalse = Template.bind({}) +export const ConditionIsFalse = Template.bind({}); ConditionIsFalse.args = { condition: false, -} +}; diff --git a/site/src/components/Conditionals/Maybe.tsx b/site/src/components/Conditionals/Maybe.tsx index 63af6ddb19..7975101c17 100644 --- a/site/src/components/Conditionals/Maybe.tsx +++ b/site/src/components/Conditionals/Maybe.tsx @@ -1,7 +1,7 @@ -import { PropsWithChildren } from "react" +import { PropsWithChildren } from "react"; export interface MaybeProps { - condition: boolean + condition: boolean; } /** @@ -13,5 +13,5 @@ export const Maybe = ({ children, condition, }: PropsWithChildren): JSX.Element | null => { - return condition ? <>{children} : null -} + return condition ? <>{children} : null; +}; diff --git a/site/src/components/CopyButton/CopyButton.tsx b/site/src/components/CopyButton/CopyButton.tsx index e7aa3228e5..4f0760bcf1 100644 --- a/site/src/components/CopyButton/CopyButton.tsx +++ b/site/src/components/CopyButton/CopyButton.tsx @@ -1,23 +1,23 @@ -import IconButton from "@mui/material/Button" -import { makeStyles } from "@mui/styles" -import Tooltip from "@mui/material/Tooltip" -import Check from "@mui/icons-material/Check" -import { useClipboard } from "hooks/useClipboard" -import { combineClasses } from "../../utils/combineClasses" -import { FileCopyIcon } from "../Icons/FileCopyIcon" +import IconButton from "@mui/material/Button"; +import { makeStyles } from "@mui/styles"; +import Tooltip from "@mui/material/Tooltip"; +import Check from "@mui/icons-material/Check"; +import { useClipboard } from "hooks/useClipboard"; +import { combineClasses } from "../../utils/combineClasses"; +import { FileCopyIcon } from "../Icons/FileCopyIcon"; interface CopyButtonProps { - text: string - ctaCopy?: string - wrapperClassName?: string - buttonClassName?: string - tooltipTitle?: string + text: string; + ctaCopy?: string; + wrapperClassName?: string; + buttonClassName?: string; + tooltipTitle?: string; } export const Language = { tooltipTitle: "Copy to clipboard", ariaLabel: "Copy to clipboard", -} +}; /** * Copy button used inside the CodeBlock component internally @@ -29,8 +29,8 @@ export const CopyButton: React.FC> = ({ buttonClassName = "", tooltipTitle = Language.tooltipTitle, }) => { - const styles = useStyles() - const { isCopied, copy: copyToClipboard } = useClipboard(text) + const styles = useStyles(); + const { isCopied, copy: copyToClipboard } = useClipboard(text); return ( @@ -53,8 +53,8 @@ export const CopyButton: React.FC> = ({ - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ copyButtonWrapper: { @@ -76,4 +76,4 @@ const useStyles = makeStyles((theme) => ({ buttonCopy: { marginLeft: theme.spacing(1), }, -})) +})); diff --git a/site/src/components/CopyableValue/CopyableValue.tsx b/site/src/components/CopyableValue/CopyableValue.tsx index 8f3b279d1f..c099632a10 100644 --- a/site/src/components/CopyableValue/CopyableValue.tsx +++ b/site/src/components/CopyableValue/CopyableValue.tsx @@ -1,12 +1,12 @@ -import { makeStyles } from "@mui/styles" -import Tooltip from "@mui/material/Tooltip" -import { useClickable } from "hooks/useClickable" -import { useClipboard } from "hooks/useClipboard" -import { FC, HTMLProps } from "react" -import { combineClasses } from "utils/combineClasses" +import { makeStyles } from "@mui/styles"; +import Tooltip from "@mui/material/Tooltip"; +import { useClickable } from "hooks/useClickable"; +import { useClipboard } from "hooks/useClipboard"; +import { FC, HTMLProps } from "react"; +import { combineClasses } from "utils/combineClasses"; interface CopyableValueProps extends HTMLProps { - value: string + value: string; } export const CopyableValue: FC = ({ @@ -14,9 +14,9 @@ export const CopyableValue: FC = ({ className, ...props }) => { - const { isCopied, copy } = useClipboard(value) - const clickableProps = useClickable(copy) - const styles = useStyles() + const { isCopied, copy } = useClipboard(value); + const clickableProps = useClickable(copy); + const styles = useStyles(); return ( = ({ className={combineClasses([styles.value, className])} /> - ) -} + ); +}; const useStyles = makeStyles(() => ({ value: { cursor: "pointer", }, -})) +})); diff --git a/site/src/components/DAUChart/DAUChart.tsx b/site/src/components/DAUChart/DAUChart.tsx index 53c32c6ead..f41d6f04bc 100644 --- a/site/src/components/DAUChart/DAUChart.tsx +++ b/site/src/components/DAUChart/DAUChart.tsx @@ -1,7 +1,7 @@ -import Box from "@mui/material/Box" -import { Theme } from "@mui/material/styles" -import useTheme from "@mui/styles/useTheme" -import * as TypesGen from "api/typesGenerated" +import Box from "@mui/material/Box"; +import { Theme } from "@mui/material/styles"; +import useTheme from "@mui/styles/useTheme"; +import * as TypesGen from "api/typesGenerated"; import { CategoryScale, Chart as ChartJS, @@ -13,16 +13,16 @@ import { TimeScale, Title, Tooltip, -} from "chart.js" -import "chartjs-adapter-date-fns" +} from "chart.js"; +import "chartjs-adapter-date-fns"; import { HelpTooltip, HelpTooltipTitle, HelpTooltipText, -} from "components/HelpTooltip/HelpTooltip" -import dayjs from "dayjs" -import { FC } from "react" -import { Bar } from "react-chartjs-2" +} from "components/HelpTooltip/HelpTooltip"; +import dayjs from "dayjs"; +import { FC } from "react"; +import { Bar } from "react-chartjs-2"; ChartJS.register( CategoryScale, @@ -32,25 +32,25 @@ ChartJS.register( Title, Tooltip, Legend, -) +); export interface DAUChartProps { - daus: TypesGen.DAUsResponse + daus: TypesGen.DAUsResponse; } export const DAUChart: FC = ({ daus }) => { - const theme: Theme = useTheme() + const theme: Theme = useTheme(); const labels = daus.entries.map((val) => { - return dayjs(val.date).format("YYYY-MM-DD") - }) + return dayjs(val.date).format("YYYY-MM-DD"); + }); const data = daus.entries.map((val) => { - return val.amount - }) + return val.amount; + }); - defaults.font.family = theme.typography.fontFamily as string - defaults.color = theme.palette.text.secondary + defaults.font.family = theme.typography.fontFamily as string; + defaults.color = theme.palette.text.secondary; const options: ChartOptions<"bar"> = { responsive: true, @@ -62,8 +62,8 @@ export const DAUChart: FC = ({ daus }) => { displayColors: false, callbacks: { title: (context) => { - const date = new Date(context[0].parsed.x) - return date.toLocaleDateString() + const date = new Date(context[0].parsed.x); + return date.toLocaleDateString(); }, }, }, @@ -86,7 +86,7 @@ export const DAUChart: FC = ({ daus }) => { }, }, maintainAspectRatio: false, - } + }; return ( = ({ daus }) => { }} options={options} /> - ) -} + ); +}; export const DAUTitle = () => { return ( @@ -125,5 +125,5 @@ export const DAUTitle = () => { - ) -} + ); +}; diff --git a/site/src/components/Dashboard/DashboardLayout.test.tsx b/site/src/components/Dashboard/DashboardLayout.test.tsx index 8897aaca8a..0020a014fd 100644 --- a/site/src/components/Dashboard/DashboardLayout.test.tsx +++ b/site/src/components/Dashboard/DashboardLayout.test.tsx @@ -1,16 +1,16 @@ -import { renderWithAuth } from "testHelpers/renderHelpers" -import { DashboardLayout } from "./DashboardLayout" -import * as API from "api/api" -import { screen } from "@testing-library/react" +import { renderWithAuth } from "testHelpers/renderHelpers"; +import { DashboardLayout } from "./DashboardLayout"; +import * as API from "api/api"; +import { screen } from "@testing-library/react"; test("Show the new Coder version notification", async () => { jest.spyOn(API, "getUpdateCheck").mockResolvedValue({ current: false, version: "v0.12.9", url: "https://github.com/coder/coder/releases/tag/v0.12.9", - }) + }); renderWithAuth(, { children: [{ element:

Test page

}], - }) - await screen.findByTestId("update-check-snackbar") -}) + }); + await screen.findByTestId("update-check-snackbar"); +}); diff --git a/site/src/components/Dashboard/DashboardLayout.tsx b/site/src/components/Dashboard/DashboardLayout.tsx index 124630ce0a..c7412f0f9a 100644 --- a/site/src/components/Dashboard/DashboardLayout.tsx +++ b/site/src/components/Dashboard/DashboardLayout.tsx @@ -1,33 +1,33 @@ -import { makeStyles } from "@mui/styles" -import { useMachine } from "@xstate/react" -import { DeploymentBanner } from "./DeploymentBanner/DeploymentBanner" -import { LicenseBanner } from "components/Dashboard/LicenseBanner/LicenseBanner" -import { Loader } from "components/Loader/Loader" -import { ServiceBanner } from "components/Dashboard/ServiceBanner/ServiceBanner" -import { usePermissions } from "hooks/usePermissions" -import { FC, Suspense } from "react" -import { Outlet } from "react-router-dom" -import { dashboardContentBottomPadding } from "theme/constants" -import { updateCheckMachine } from "xServices/updateCheck/updateCheckXService" -import { Navbar } from "./Navbar/Navbar" -import Snackbar from "@mui/material/Snackbar" -import Link from "@mui/material/Link" -import Box, { BoxProps } from "@mui/material/Box" -import InfoOutlined from "@mui/icons-material/InfoOutlined" -import Button from "@mui/material/Button" -import { docs } from "utils/docs" -import { HealthBanner } from "./HealthBanner" +import { makeStyles } from "@mui/styles"; +import { useMachine } from "@xstate/react"; +import { DeploymentBanner } from "./DeploymentBanner/DeploymentBanner"; +import { LicenseBanner } from "components/Dashboard/LicenseBanner/LicenseBanner"; +import { Loader } from "components/Loader/Loader"; +import { ServiceBanner } from "components/Dashboard/ServiceBanner/ServiceBanner"; +import { usePermissions } from "hooks/usePermissions"; +import { FC, Suspense } from "react"; +import { Outlet } from "react-router-dom"; +import { dashboardContentBottomPadding } from "theme/constants"; +import { updateCheckMachine } from "xServices/updateCheck/updateCheckXService"; +import { Navbar } from "./Navbar/Navbar"; +import Snackbar from "@mui/material/Snackbar"; +import Link from "@mui/material/Link"; +import Box, { BoxProps } from "@mui/material/Box"; +import InfoOutlined from "@mui/icons-material/InfoOutlined"; +import Button from "@mui/material/Button"; +import { docs } from "utils/docs"; +import { HealthBanner } from "./HealthBanner"; export const DashboardLayout: FC = () => { - const styles = useStyles() - const permissions = usePermissions() + const styles = useStyles(); + const permissions = usePermissions(); const [updateCheckState, updateCheckSend] = useMachine(updateCheckMachine, { context: { permissions, }, - }) - const { updateCheck } = updateCheckState.context - const canViewDeployment = Boolean(permissions.viewDeploymentValues) + }); + const { updateCheck } = updateCheckState.context; + const canViewDeployment = Boolean(permissions.viewDeploymentValues); return ( <> @@ -99,8 +99,8 @@ export const DashboardLayout: FC = () => { /> - ) -} + ); +}; export const DashboardFullPage = (props: BoxProps) => { return ( @@ -116,8 +116,8 @@ export const DashboardFullPage = (props: BoxProps) => { minHeight: "100%", }} /> - ) -} + ); +}; const useStyles = makeStyles({ site: { @@ -131,4 +131,4 @@ const useStyles = makeStyles({ display: "flex", flexDirection: "column", }, -}) +}); diff --git a/site/src/components/Dashboard/DashboardProvider.tsx b/site/src/components/Dashboard/DashboardProvider.tsx index ed26b64ffc..bfe632d755 100644 --- a/site/src/components/Dashboard/DashboardProvider.tsx +++ b/site/src/components/Dashboard/DashboardProvider.tsx @@ -1,62 +1,62 @@ -import { useMachine } from "@xstate/react" +import { useMachine } from "@xstate/react"; import { AppearanceConfig, BuildInfoResponse, Entitlements, Experiments, -} from "api/typesGenerated" -import { FullScreenLoader } from "components/Loader/FullScreenLoader" -import { createContext, FC, PropsWithChildren, useContext } from "react" -import { appearanceMachine } from "xServices/appearance/appearanceXService" -import { buildInfoMachine } from "xServices/buildInfo/buildInfoXService" -import { entitlementsMachine } from "xServices/entitlements/entitlementsXService" -import { experimentsMachine } from "xServices/experiments/experimentsMachine" +} from "api/typesGenerated"; +import { FullScreenLoader } from "components/Loader/FullScreenLoader"; +import { createContext, FC, PropsWithChildren, useContext } from "react"; +import { appearanceMachine } from "xServices/appearance/appearanceXService"; +import { buildInfoMachine } from "xServices/buildInfo/buildInfoXService"; +import { entitlementsMachine } from "xServices/entitlements/entitlementsXService"; +import { experimentsMachine } from "xServices/experiments/experimentsMachine"; interface Appearance { - config: AppearanceConfig - preview: boolean - setPreview: (config: AppearanceConfig) => void - save: (config: AppearanceConfig) => void + config: AppearanceConfig; + preview: boolean; + setPreview: (config: AppearanceConfig) => void; + save: (config: AppearanceConfig) => void; } interface DashboardProviderValue { - buildInfo: BuildInfoResponse - entitlements: Entitlements - appearance: Appearance - experiments: Experiments + buildInfo: BuildInfoResponse; + entitlements: Entitlements; + appearance: Appearance; + experiments: Experiments; } export const DashboardProviderContext = createContext< DashboardProviderValue | undefined ->(undefined) +>(undefined); export const DashboardProvider: FC = ({ children }) => { - const [buildInfoState] = useMachine(buildInfoMachine) - const [entitlementsState] = useMachine(entitlementsMachine) - const [appearanceState, appearanceSend] = useMachine(appearanceMachine) - const [experimentsState] = useMachine(experimentsMachine) - const { buildInfo } = buildInfoState.context - const { entitlements } = entitlementsState.context - const { appearance, preview } = appearanceState.context - const { experiments } = experimentsState.context - const isLoading = !buildInfo || !entitlements || !appearance || !experiments + const [buildInfoState] = useMachine(buildInfoMachine); + const [entitlementsState] = useMachine(entitlementsMachine); + const [appearanceState, appearanceSend] = useMachine(appearanceMachine); + const [experimentsState] = useMachine(experimentsMachine); + const { buildInfo } = buildInfoState.context; + const { entitlements } = entitlementsState.context; + const { appearance, preview } = appearanceState.context; + const { experiments } = experimentsState.context; + const isLoading = !buildInfo || !entitlements || !appearance || !experiments; const setAppearancePreview = (config: AppearanceConfig) => { appearanceSend({ type: "SET_PREVIEW_APPEARANCE", appearance: config, - }) - } + }); + }; const saveAppearance = (config: AppearanceConfig) => { appearanceSend({ type: "SAVE_APPEARANCE", appearance: config, - }) - } + }); + }; if (isLoading) { - return + return ; } return ( @@ -75,25 +75,27 @@ export const DashboardProvider: FC = ({ children }) => { > {children} - ) -} + ); +}; export const useDashboard = (): DashboardProviderValue => { - const context = useContext(DashboardProviderContext) + const context = useContext(DashboardProviderContext); if (!context) { - throw new Error("useDashboard only can be used inside of DashboardProvider") + throw new Error( + "useDashboard only can be used inside of DashboardProvider", + ); } - return context -} + return context; +}; export const useIsWorkspaceActionsEnabled = (): boolean => { - const { entitlements, experiments } = useDashboard() + const { entitlements, experiments } = useDashboard(); const allowAdvancedScheduling = - entitlements.features["advanced_template_scheduling"].enabled + entitlements.features["advanced_template_scheduling"].enabled; // This check can be removed when https://github.com/coder/coder/milestone/19 // is merged up - const allowWorkspaceActions = experiments.includes("workspace_actions") - return allowWorkspaceActions && allowAdvancedScheduling -} + const allowWorkspaceActions = experiments.includes("workspace_actions"); + return allowWorkspaceActions && allowAdvancedScheduling; +}; diff --git a/site/src/components/Dashboard/DeploymentBanner/DeploymentBanner.tsx b/site/src/components/Dashboard/DeploymentBanner/DeploymentBanner.tsx index 561efcaf84..16b64d0615 100644 --- a/site/src/components/Dashboard/DeploymentBanner/DeploymentBanner.tsx +++ b/site/src/components/Dashboard/DeploymentBanner/DeploymentBanner.tsx @@ -1,14 +1,14 @@ -import { useMachine } from "@xstate/react" -import { usePermissions } from "hooks/usePermissions" -import { DeploymentBannerView } from "./DeploymentBannerView" -import { deploymentStatsMachine } from "xServices/deploymentStats/deploymentStatsMachine" +import { useMachine } from "@xstate/react"; +import { usePermissions } from "hooks/usePermissions"; +import { DeploymentBannerView } from "./DeploymentBannerView"; +import { deploymentStatsMachine } from "xServices/deploymentStats/deploymentStatsMachine"; export const DeploymentBanner: React.FC = () => { - const permissions = usePermissions() - const [state, sendEvent] = useMachine(deploymentStatsMachine) + const permissions = usePermissions(); + const [state, sendEvent] = useMachine(deploymentStatsMachine); if (!permissions.viewDeploymentValues || !state.context.deploymentStats) { - return null + return null; } return ( @@ -16,5 +16,5 @@ export const DeploymentBanner: React.FC = () => { stats={state.context.deploymentStats} fetchStats={() => sendEvent("RELOAD")} /> - ) -} + ); +}; diff --git a/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.stories.tsx b/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.stories.tsx index dd54d531a5..eda61fa691 100644 --- a/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.stories.tsx +++ b/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.stories.tsx @@ -1,20 +1,20 @@ -import { Story } from "@storybook/react" -import { MockDeploymentStats } from "testHelpers/entities" +import { Story } from "@storybook/react"; +import { MockDeploymentStats } from "testHelpers/entities"; import { DeploymentBannerView, DeploymentBannerViewProps, -} from "./DeploymentBannerView" +} from "./DeploymentBannerView"; export default { title: "components/DeploymentBannerView", component: DeploymentBannerView, -} +}; const Template: Story = (args) => ( -) +); -export const Preview = Template.bind({}) +export const Preview = Template.bind({}); Preview.args = { stats: MockDeploymentStats, -} +}; diff --git a/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.tsx b/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.tsx index 1a5db47748..9e15c7e869 100644 --- a/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.tsx +++ b/site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.tsx @@ -1,83 +1,83 @@ -import { DeploymentStats, WorkspaceStatus } from "api/typesGenerated" -import { FC, useMemo, useEffect, useState } from "react" -import prettyBytes from "pretty-bytes" -import BuildingIcon from "@mui/icons-material/Build" -import { makeStyles } from "@mui/styles" -import { RocketIcon } from "components/Icons/RocketIcon" -import { MONOSPACE_FONT_FAMILY } from "theme/constants" -import Tooltip from "@mui/material/Tooltip" -import { Link as RouterLink } from "react-router-dom" -import Link from "@mui/material/Link" -import { VSCodeIcon } from "components/Icons/VSCodeIcon" -import DownloadIcon from "@mui/icons-material/CloudDownload" -import UploadIcon from "@mui/icons-material/CloudUpload" -import LatencyIcon from "@mui/icons-material/SettingsEthernet" -import WebTerminalIcon from "@mui/icons-material/WebAsset" -import { TerminalIcon } from "components/Icons/TerminalIcon" -import dayjs from "dayjs" -import CollectedIcon from "@mui/icons-material/Compare" -import RefreshIcon from "@mui/icons-material/Refresh" -import Button from "@mui/material/Button" -import { getDisplayWorkspaceStatus } from "utils/workspace" +import { DeploymentStats, WorkspaceStatus } from "api/typesGenerated"; +import { FC, useMemo, useEffect, useState } from "react"; +import prettyBytes from "pretty-bytes"; +import BuildingIcon from "@mui/icons-material/Build"; +import { makeStyles } from "@mui/styles"; +import { RocketIcon } from "components/Icons/RocketIcon"; +import { MONOSPACE_FONT_FAMILY } from "theme/constants"; +import Tooltip from "@mui/material/Tooltip"; +import { Link as RouterLink } from "react-router-dom"; +import Link from "@mui/material/Link"; +import { VSCodeIcon } from "components/Icons/VSCodeIcon"; +import DownloadIcon from "@mui/icons-material/CloudDownload"; +import UploadIcon from "@mui/icons-material/CloudUpload"; +import LatencyIcon from "@mui/icons-material/SettingsEthernet"; +import WebTerminalIcon from "@mui/icons-material/WebAsset"; +import { TerminalIcon } from "components/Icons/TerminalIcon"; +import dayjs from "dayjs"; +import CollectedIcon from "@mui/icons-material/Compare"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import Button from "@mui/material/Button"; +import { getDisplayWorkspaceStatus } from "utils/workspace"; -export const bannerHeight = 36 +export const bannerHeight = 36; export interface DeploymentBannerViewProps { - fetchStats?: () => void - stats?: DeploymentStats + fetchStats?: () => void; + stats?: DeploymentStats; } export const DeploymentBannerView: FC = ({ stats, fetchStats, }) => { - const styles = useStyles() + const styles = useStyles(); const aggregatedMinutes = useMemo(() => { if (!stats) { - return + return; } - return dayjs(stats.collected_at).diff(stats.aggregated_from, "minutes") - }, [stats]) - const displayLatency = stats?.workspaces.connection_latency_ms.P50 || -1 - const [timeUntilRefresh, setTimeUntilRefresh] = useState(0) + return dayjs(stats.collected_at).diff(stats.aggregated_from, "minutes"); + }, [stats]); + const displayLatency = stats?.workspaces.connection_latency_ms.P50 || -1; + const [timeUntilRefresh, setTimeUntilRefresh] = useState(0); useEffect(() => { if (!stats || !fetchStats) { - return + return; } let timeUntilRefresh = dayjs(stats.next_update_at).diff( stats.collected_at, "seconds", - ) - setTimeUntilRefresh(timeUntilRefresh) - let canceled = false + ); + setTimeUntilRefresh(timeUntilRefresh); + let canceled = false; const loop = () => { if (canceled) { - return undefined + return undefined; } - setTimeUntilRefresh(timeUntilRefresh--) + setTimeUntilRefresh(timeUntilRefresh--); if (timeUntilRefresh > 0) { - return window.setTimeout(loop, 1000) + return window.setTimeout(loop, 1000); } - fetchStats() - } - const timeout = setTimeout(loop, 1000) + fetchStats(); + }; + const timeout = setTimeout(loop, 1000); return () => { - canceled = true - clearTimeout(timeout) - } - }, [fetchStats, stats]) + canceled = true; + clearTimeout(timeout); + }; + }, [fetchStats, stats]); const lastAggregated = useMemo(() => { if (!stats) { - return + return; } if (!fetchStats) { // Storybook! - return "just now" + return "just now"; } - return dayjs().to(dayjs(stats.collected_at)) + return dayjs().to(dayjs(stats.collected_at)); // eslint-disable-next-line react-hooks/exhaustive-deps -- We want this to periodically update! - }, [timeUntilRefresh, stats]) + }, [timeUntilRefresh, stats]); return (
@@ -194,7 +194,7 @@ export const DeploymentBannerView: FC = ({ className={`${styles.value} ${styles.refreshButton}`} onClick={() => { if (fetchStats) { - fetchStats() + fetchStats(); } }} variant="text" @@ -205,25 +205,25 @@ export const DeploymentBannerView: FC = ({
- ) -} + ); +}; const ValueSeparator: FC = () => { - const styles = useStyles() - return
/
-} + const styles = useStyles(); + return
/
; +}; const WorkspaceBuildValue: FC<{ - status: WorkspaceStatus - count?: number + status: WorkspaceStatus; + count?: number; }> = ({ status, count }) => { - const styles = useStyles() - const displayStatus = getDisplayWorkspaceStatus(status) - let statusText = displayStatus.text - let icon = displayStatus.icon + const styles = useStyles(); + const displayStatus = getDisplayWorkspaceStatus(status); + let statusText = displayStatus.text; + let icon = displayStatus.icon; if (status === "starting") { - icon = - statusText = "Building" + icon = ; + statusText = "Building"; } return ( @@ -238,8 +238,8 @@ const WorkspaceBuildValue: FC<{ - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ rocket: { @@ -324,4 +324,4 @@ const useStyles = makeStyles((theme) => ({ marginRight: theme.spacing(0.5), }, }, -})) +})); diff --git a/site/src/components/Dashboard/HealthBanner.tsx b/site/src/components/Dashboard/HealthBanner.tsx index a8f5a2d065..d5deadf58d 100644 --- a/site/src/components/Dashboard/HealthBanner.tsx +++ b/site/src/components/Dashboard/HealthBanner.tsx @@ -1,18 +1,18 @@ -import { Alert } from "components/Alert/Alert" -import { Link as RouterLink } from "react-router-dom" -import Link from "@mui/material/Link" -import { colors } from "theme/colors" -import { useQuery } from "@tanstack/react-query" -import { getHealth } from "api/api" -import { useDashboard } from "./DashboardProvider" +import { Alert } from "components/Alert/Alert"; +import { Link as RouterLink } from "react-router-dom"; +import Link from "@mui/material/Link"; +import { colors } from "theme/colors"; +import { useQuery } from "@tanstack/react-query"; +import { getHealth } from "api/api"; +import { useDashboard } from "./DashboardProvider"; export const HealthBanner = () => { const { data: healthStatus } = useQuery({ queryKey: ["health"], queryFn: () => getHealth(), - }) - const dashboard = useDashboard() - const hasHealthIssues = healthStatus && !healthStatus.data.healthy + }); + const dashboard = useDashboard(); + const hasHealthIssues = healthStatus && !healthStatus.data.healthy; if ( dashboard.experiments.includes("deployment_health_page") && @@ -38,8 +38,8 @@ export const HealthBanner = () => { . - ) + ); } - return null -} + return null; +}; diff --git a/site/src/components/Dashboard/LicenseBanner/LicenseBanner.tsx b/site/src/components/Dashboard/LicenseBanner/LicenseBanner.tsx index 8de586ff9e..4f37638c91 100644 --- a/site/src/components/Dashboard/LicenseBanner/LicenseBanner.tsx +++ b/site/src/components/Dashboard/LicenseBanner/LicenseBanner.tsx @@ -1,13 +1,13 @@ -import { useDashboard } from "components/Dashboard/DashboardProvider" -import { LicenseBannerView } from "./LicenseBannerView" +import { useDashboard } from "components/Dashboard/DashboardProvider"; +import { LicenseBannerView } from "./LicenseBannerView"; export const LicenseBanner: React.FC = () => { - const { entitlements } = useDashboard() - const { errors, warnings } = entitlements + const { entitlements } = useDashboard(); + const { errors, warnings } = entitlements; if (errors.length > 0 || warnings.length > 0) { - return + return ; } else { - return null + return null; } -} +}; diff --git a/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.stories.tsx b/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.stories.tsx index c7ee69c261..37d6335eb7 100644 --- a/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.stories.tsx +++ b/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.stories.tsx @@ -1,34 +1,34 @@ -import { Story } from "@storybook/react" -import { LicenseBannerView, LicenseBannerViewProps } from "./LicenseBannerView" +import { Story } from "@storybook/react"; +import { LicenseBannerView, LicenseBannerViewProps } from "./LicenseBannerView"; export default { title: "components/LicenseBannerView", component: LicenseBannerView, -} +}; const Template: Story = (args) => ( -) +); -export const OneWarning = Template.bind({}) +export const OneWarning = Template.bind({}); OneWarning.args = { errors: [], warnings: ["You have exceeded the number of seats in your license."], -} +}; -export const TwoWarnings = Template.bind({}) +export const TwoWarnings = Template.bind({}); TwoWarnings.args = { errors: [], warnings: [ "You have exceeded the number of seats in your license.", "You are flying too close to the sun.", ], -} +}; -export const OneError = Template.bind({}) +export const OneError = Template.bind({}); OneError.args = { errors: [ "You have multiple replicas but high availability is an Enterprise feature. You will be unable to connect to workspaces.", ], warnings: [], -} +}; diff --git a/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.tsx b/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.tsx index 9034a63a06..d849e9ec89 100644 --- a/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.tsx +++ b/site/src/components/Dashboard/LicenseBanner/LicenseBannerView.tsx @@ -1,9 +1,9 @@ -import Link from "@mui/material/Link" -import { makeStyles } from "@mui/styles" -import { Expander } from "components/Expander/Expander" -import { Pill } from "components/Pill/Pill" -import { useState } from "react" -import { colors } from "theme/colors" +import Link from "@mui/material/Link"; +import { makeStyles } from "@mui/styles"; +import { Expander } from "components/Expander/Expander"; +import { Pill } from "components/Pill/Pill"; +import { useState } from "react"; +import { colors } from "theme/colors"; export const Language = { licenseIssue: "License Issue", @@ -12,22 +12,22 @@ export const Language = { exceeded: "It looks like you've exceeded some limits of your license.", lessDetails: "Less", moreDetails: "More", -} +}; export interface LicenseBannerViewProps { - errors: string[] - warnings: string[] + errors: string[]; + warnings: string[]; } export const LicenseBannerView: React.FC = ({ errors, warnings, }) => { - const styles = useStyles() - const [showDetails, setShowDetails] = useState(false) - const isError = errors.length > 0 - const messages = [...errors, ...warnings] - const type = isError ? "error" : "warning" + const styles = useStyles(); + const [showDetails, setShowDetails] = useState(false); + const isError = errors.length > 0; + const messages = [...errors, ...warnings]; + const type = isError ? "error" : "warning"; if (messages.length === 1) { return ( @@ -41,7 +41,7 @@ export const LicenseBannerView: React.FC = ({ - ) + ); } else { return (
@@ -73,9 +73,9 @@ export const LicenseBannerView: React.FC = ({
- ) + ); } -} +}; const useStyles = makeStyles((theme) => ({ container: { @@ -103,4 +103,4 @@ const useStyles = makeStyles((theme) => ({ listItem: { margin: theme.spacing(0.5), }, -})) +})); diff --git a/site/src/components/Dashboard/Navbar/Navbar.test.tsx b/site/src/components/Dashboard/Navbar/Navbar.test.tsx index 8bdac69e67..0fc06e6168 100644 --- a/site/src/components/Dashboard/Navbar/Navbar.test.tsx +++ b/site/src/components/Dashboard/Navbar/Navbar.test.tsx @@ -1,12 +1,12 @@ -import { render, screen, waitFor } from "@testing-library/react" -import { App } from "app" -import { Language } from "./NavbarView" -import { rest } from "msw" +import { render, screen, waitFor } from "@testing-library/react"; +import { App } from "app"; +import { Language } from "./NavbarView"; +import { rest } from "msw"; import { MockEntitlementsWithAuditLog, MockMemberPermissions, -} from "testHelpers/entities" -import { server } from "testHelpers/server" +} from "testHelpers/entities"; +import { server } from "testHelpers/server"; /** * The LicenseBanner, mounted above the AppRouter, fetches entitlements. Thus, to test their @@ -17,52 +17,52 @@ describe("Navbar", () => { // set entitlements to allow audit log server.use( rest.get("/api/v2/entitlements", (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog)) + return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog)); }), - ) - render() + ); + render(); await waitFor( () => { - const link = screen.getByText(Language.audit) - expect(link).toBeDefined() + const link = screen.getByText(Language.audit); + expect(link).toBeDefined(); }, { timeout: 2000 }, - ) - }) + ); + }); it("does not show Audit Log link when not entitled", async () => { // by default, user is an Admin with permission to see the audit log, // but is unlicensed so not entitled to see the audit log - render() + render(); await waitFor( () => { - const link = screen.queryByText(Language.audit) - expect(link).toBe(null) + const link = screen.queryByText(Language.audit); + expect(link).toBe(null); }, { timeout: 2000 }, - ) - }) + ); + }); it("does not show Audit Log link when not permitted via role", async () => { // set permissions to Member (can't audit) server.use( rest.post("/api/v2/authcheck", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MockMemberPermissions)) + return res(ctx.status(200), ctx.json(MockMemberPermissions)); }), - ) + ); // set entitlements to allow audit log server.use( rest.get("/api/v2/entitlements", (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog)) + return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog)); }), - ) - render() + ); + render(); await waitFor( () => { - const link = screen.queryByText(Language.audit) - expect(link).toBe(null) + const link = screen.queryByText(Language.audit); + expect(link).toBe(null); }, { timeout: 2000 }, - ) - }) -}) + ); + }); +}); diff --git a/site/src/components/Dashboard/Navbar/Navbar.tsx b/site/src/components/Dashboard/Navbar/Navbar.tsx index d8cd46b8f2..de430e3fdf 100644 --- a/site/src/components/Dashboard/Navbar/Navbar.tsx +++ b/site/src/components/Dashboard/Navbar/Navbar.tsx @@ -1,25 +1,25 @@ -import { useAuth } from "components/AuthProvider/AuthProvider" -import { useDashboard } from "components/Dashboard/DashboardProvider" -import { useFeatureVisibility } from "hooks/useFeatureVisibility" -import { useMe } from "hooks/useMe" -import { usePermissions } from "hooks/usePermissions" -import { FC } from "react" -import { NavbarView } from "./NavbarView" -import { useProxy } from "contexts/ProxyContext" +import { useAuth } from "components/AuthProvider/AuthProvider"; +import { useDashboard } from "components/Dashboard/DashboardProvider"; +import { useFeatureVisibility } from "hooks/useFeatureVisibility"; +import { useMe } from "hooks/useMe"; +import { usePermissions } from "hooks/usePermissions"; +import { FC } from "react"; +import { NavbarView } from "./NavbarView"; +import { useProxy } from "contexts/ProxyContext"; export const Navbar: FC = () => { - const { appearance, buildInfo } = useDashboard() - const [_, authSend] = useAuth() - const me = useMe() - const permissions = usePermissions() - const featureVisibility = useFeatureVisibility() + const { appearance, buildInfo } = useDashboard(); + const [_, authSend] = useAuth(); + const me = useMe(); + const permissions = usePermissions(); + const featureVisibility = useFeatureVisibility(); const canViewAuditLog = - featureVisibility["audit_log"] && Boolean(permissions.viewAuditLog) - const canViewDeployment = Boolean(permissions.viewDeploymentValues) - const canViewAllUsers = Boolean(permissions.readAllUsers) - const onSignOut = () => authSend("SIGN_OUT") - const proxyContextValue = useProxy() - const dashboard = useDashboard() + featureVisibility["audit_log"] && Boolean(permissions.viewAuditLog); + const canViewDeployment = Boolean(permissions.viewDeploymentValues); + const canViewAllUsers = Boolean(permissions.readAllUsers); + const onSignOut = () => authSend("SIGN_OUT"); + const proxyContextValue = useProxy(); + const dashboard = useDashboard(); return ( { dashboard.experiments.includes("moons") ? proxyContextValue : undefined } /> - ) -} + ); +}; diff --git a/site/src/components/Dashboard/Navbar/NavbarView.stories.tsx b/site/src/components/Dashboard/Navbar/NavbarView.stories.tsx index d62358cb5f..d1d14e38b7 100644 --- a/site/src/components/Dashboard/Navbar/NavbarView.stories.tsx +++ b/site/src/components/Dashboard/Navbar/NavbarView.stories.tsx @@ -1,6 +1,6 @@ -import { Story } from "@storybook/react" -import { MockUser, MockUser2 } from "../../../testHelpers/entities" -import { NavbarView, NavbarViewProps } from "./NavbarView" +import { Story } from "@storybook/react"; +import { MockUser, MockUser2 } from "../../../testHelpers/entities"; +import { NavbarView, NavbarViewProps } from "./NavbarView"; export default { title: "components/NavbarView", @@ -8,38 +8,38 @@ export default { argTypes: { onSignOut: { action: "Sign Out" }, }, -} +}; const Template: Story = (args: NavbarViewProps) => ( -) +); -export const ForAdmin = Template.bind({}) +export const ForAdmin = Template.bind({}); ForAdmin.args = { user: MockUser, onSignOut: () => { - return Promise.resolve() + return Promise.resolve(); }, -} +}; -export const ForMember = Template.bind({}) +export const ForMember = Template.bind({}); ForMember.args = { user: MockUser2, onSignOut: () => { - return Promise.resolve() + return Promise.resolve(); }, -} +}; -export const SmallViewport = Template.bind({}) +export const SmallViewport = Template.bind({}); SmallViewport.args = { user: MockUser, onSignOut: () => { - return Promise.resolve() + return Promise.resolve(); }, -} +}; SmallViewport.parameters = { viewport: { defaultViewport: "tablet", }, chromatic: { viewports: [420] }, -} +}; diff --git a/site/src/components/Dashboard/Navbar/NavbarView.test.tsx b/site/src/components/Dashboard/Navbar/NavbarView.test.tsx index 8688269b78..70e76fc91f 100644 --- a/site/src/components/Dashboard/Navbar/NavbarView.test.tsx +++ b/site/src/components/Dashboard/Navbar/NavbarView.test.tsx @@ -1,13 +1,13 @@ -import { screen } from "@testing-library/react" +import { screen } from "@testing-library/react"; import { MockPrimaryWorkspaceProxy, MockUser, MockUser2, -} from "../../../testHelpers/entities" -import { renderWithAuth } from "../../../testHelpers/renderHelpers" -import { Language as navLanguage, NavbarView } from "./NavbarView" -import { ProxyContextValue } from "contexts/ProxyContext" -import { action } from "@storybook/addon-actions" +} from "../../../testHelpers/entities"; +import { renderWithAuth } from "../../../testHelpers/renderHelpers"; +import { Language as navLanguage, NavbarView } from "./NavbarView"; +import { ProxyContextValue } from "contexts/ProxyContext"; +import { action } from "@storybook/addon-actions"; const proxyContextValue: ProxyContextValue = { proxy: { @@ -21,24 +21,24 @@ const proxyContextValue: ProxyContextValue = { clearProxy: action("clearProxy"), refetchProxyLatencies: jest.fn(), proxyLatencies: {}, -} +}; describe("NavbarView", () => { const noop = () => { - return - } + return; + }; - const env = process.env + const env = process.env; // REMARK: copying process.env so we don't mutate that object or encounter conflicts between tests beforeEach(() => { - process.env = { ...env } - }) + process.env = { ...env }; + }); // REMARK: restoring process.env afterEach(() => { - process.env = env - }) + process.env = env; + }); it("workspaces nav link has the correct href", async () => { renderWithAuth( @@ -50,10 +50,10 @@ describe("NavbarView", () => { canViewDeployment canViewAllUsers />, - ) - const workspacesLink = await screen.findByText(navLanguage.workspaces) - expect((workspacesLink as HTMLAnchorElement).href).toContain("/workspaces") - }) + ); + const workspacesLink = await screen.findByText(navLanguage.workspaces); + expect((workspacesLink as HTMLAnchorElement).href).toContain("/workspaces"); + }); it("templates nav link has the correct href", async () => { renderWithAuth( @@ -65,10 +65,10 @@ describe("NavbarView", () => { canViewDeployment canViewAllUsers />, - ) - const templatesLink = await screen.findByText(navLanguage.templates) - expect((templatesLink as HTMLAnchorElement).href).toContain("/templates") - }) + ); + const templatesLink = await screen.findByText(navLanguage.templates); + expect((templatesLink as HTMLAnchorElement).href).toContain("/templates"); + }); it("users nav link has the correct href", async () => { renderWithAuth( @@ -80,10 +80,10 @@ describe("NavbarView", () => { canViewDeployment canViewAllUsers />, - ) - const userLink = await screen.findByText(navLanguage.users) - expect((userLink as HTMLAnchorElement).href).toContain("/users") - }) + ); + const userLink = await screen.findByText(navLanguage.users); + expect((userLink as HTMLAnchorElement).href).toContain("/users"); + }); it("renders profile picture for user", async () => { // Given @@ -91,7 +91,7 @@ describe("NavbarView", () => { ...MockUser, username: "bryan", avatar_url: "", - } + }; // When renderWithAuth( @@ -103,13 +103,13 @@ describe("NavbarView", () => { canViewDeployment canViewAllUsers />, - ) + ); // Then // There should be a 'B' avatar! - const element = await screen.findByText("B") - expect(element).toBeDefined() - }) + const element = await screen.findByText("B"); + expect(element).toBeDefined(); + }); it("audit nav link has the correct href", async () => { renderWithAuth( @@ -121,10 +121,10 @@ describe("NavbarView", () => { canViewDeployment canViewAllUsers />, - ) - const auditLink = await screen.findByText(navLanguage.audit) - expect((auditLink as HTMLAnchorElement).href).toContain("/audit") - }) + ); + const auditLink = await screen.findByText(navLanguage.audit); + expect((auditLink as HTMLAnchorElement).href).toContain("/audit"); + }); it("audit nav link is hidden for members", async () => { renderWithAuth( @@ -136,10 +136,10 @@ describe("NavbarView", () => { canViewDeployment canViewAllUsers />, - ) - const auditLink = screen.queryByText(navLanguage.audit) - expect(auditLink).not.toBeInTheDocument() - }) + ); + const auditLink = screen.queryByText(navLanguage.audit); + expect(auditLink).not.toBeInTheDocument(); + }); it("deployment nav link has the correct href", async () => { renderWithAuth( @@ -151,12 +151,12 @@ describe("NavbarView", () => { canViewDeployment canViewAllUsers />, - ) - const auditLink = await screen.findByText(navLanguage.deployment) + ); + const auditLink = await screen.findByText(navLanguage.deployment); expect((auditLink as HTMLAnchorElement).href).toContain( "/deployment/general", - ) - }) + ); + }); it("deployment nav link is hidden for members", async () => { renderWithAuth( @@ -168,8 +168,8 @@ describe("NavbarView", () => { canViewDeployment={false} canViewAllUsers />, - ) - const auditLink = screen.queryByText(navLanguage.deployment) - expect(auditLink).not.toBeInTheDocument() - }) -}) + ); + const auditLink = screen.queryByText(navLanguage.deployment); + expect(auditLink).not.toBeInTheDocument(); + }); +}); diff --git a/site/src/components/Dashboard/Navbar/NavbarView.tsx b/site/src/components/Dashboard/Navbar/NavbarView.tsx index 2f742fa417..3151be9cd8 100644 --- a/site/src/components/Dashboard/Navbar/NavbarView.tsx +++ b/site/src/components/Dashboard/Navbar/NavbarView.tsx @@ -1,43 +1,45 @@ -import Drawer from "@mui/material/Drawer" -import IconButton from "@mui/material/IconButton" -import List from "@mui/material/List" -import ListItem from "@mui/material/ListItem" -import { makeStyles } from "@mui/styles" -import MenuIcon from "@mui/icons-material/Menu" -import { CoderIcon } from "components/Icons/CoderIcon" -import { FC, useRef, useState } from "react" -import { NavLink, useLocation, useNavigate } from "react-router-dom" -import { colors } from "theme/colors" -import * as TypesGen from "../../../api/typesGenerated" -import { navHeight } from "../../../theme/constants" -import { combineClasses } from "../../../utils/combineClasses" -import { UserDropdown } from "./UserDropdown/UserDropdown" -import Box from "@mui/material/Box" -import Menu from "@mui/material/Menu" -import Button from "@mui/material/Button" -import MenuItem from "@mui/material/MenuItem" -import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutlined" -import { ProxyContextValue } from "contexts/ProxyContext" -import { displayError } from "components/GlobalSnackbar/utils" -import Divider from "@mui/material/Divider" -import Skeleton from "@mui/material/Skeleton" -import { BUTTON_SM_HEIGHT } from "theme/theme" -import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency" -import { usePermissions } from "hooks/usePermissions" -import Typography from "@mui/material/Typography" +import Drawer from "@mui/material/Drawer"; +import IconButton from "@mui/material/IconButton"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import { makeStyles } from "@mui/styles"; +import MenuIcon from "@mui/icons-material/Menu"; +import { CoderIcon } from "components/Icons/CoderIcon"; +import { FC, useRef, useState } from "react"; +import { NavLink, useLocation, useNavigate } from "react-router-dom"; +import { colors } from "theme/colors"; +import * as TypesGen from "../../../api/typesGenerated"; +import { navHeight } from "../../../theme/constants"; +import { combineClasses } from "../../../utils/combineClasses"; +import { UserDropdown } from "./UserDropdown/UserDropdown"; +import Box from "@mui/material/Box"; +import Menu from "@mui/material/Menu"; +import Button from "@mui/material/Button"; +import MenuItem from "@mui/material/MenuItem"; +import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutlined"; +import { ProxyContextValue } from "contexts/ProxyContext"; +import { displayError } from "components/GlobalSnackbar/utils"; +import Divider from "@mui/material/Divider"; +import Skeleton from "@mui/material/Skeleton"; +import { BUTTON_SM_HEIGHT } from "theme/theme"; +import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency"; +import { usePermissions } from "hooks/usePermissions"; +import Typography from "@mui/material/Typography"; -export const USERS_LINK = `/users?filter=${encodeURIComponent("status:active")}` +export const USERS_LINK = `/users?filter=${encodeURIComponent( + "status:active", +)}`; export interface NavbarViewProps { - logo_url?: string - user?: TypesGen.User - buildInfo?: TypesGen.BuildInfoResponse - supportLinks?: TypesGen.LinkConfig[] - onSignOut: () => void - canViewAuditLog: boolean - canViewDeployment: boolean - canViewAllUsers: boolean - proxyContextValue?: ProxyContextValue + logo_url?: string; + user?: TypesGen.User; + buildInfo?: TypesGen.BuildInfoResponse; + supportLinks?: TypesGen.LinkConfig[]; + onSignOut: () => void; + canViewAuditLog: boolean; + canViewDeployment: boolean; + canViewAllUsers: boolean; + proxyContextValue?: ProxyContextValue; } export const Language = { @@ -46,18 +48,18 @@ export const Language = { users: "Users", audit: "Audit", deployment: "Deployment", -} +}; const NavItems: React.FC< React.PropsWithChildren<{ - className?: string - canViewAuditLog: boolean - canViewDeployment: boolean - canViewAllUsers: boolean + className?: string; + canViewAuditLog: boolean; + canViewDeployment: boolean; + canViewAllUsers: boolean; }> > = ({ className, canViewAuditLog, canViewDeployment, canViewAllUsers }) => { - const styles = useStyles() - const location = useLocation() + const styles = useStyles(); + const location = useLocation(); return ( @@ -99,8 +101,8 @@ const NavItems: React.FC< )} - ) -} + ); +}; export const NavbarView: FC = ({ user, logo_url, @@ -112,8 +114,8 @@ export const NavbarView: FC = ({ canViewAllUsers, proxyContextValue, }) => { - const styles = useStyles() - const [isDrawerOpen, setIsDrawerOpen] = useState(false) + const styles = useStyles(); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); return ( - ) -} + ); +}; const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({ proxyContextValue, }) => { - const buttonRef = useRef(null) - const [isOpen, setIsOpen] = useState(false) - const [refetchDate, setRefetchDate] = useState() - const selectedProxy = proxyContextValue.proxy.proxy - const refreshLatencies = proxyContextValue.refetchProxyLatencies - const closeMenu = () => setIsOpen(false) - const navigate = useNavigate() - const latencies = proxyContextValue.proxyLatencies - const isLoadingLatencies = Object.keys(latencies).length === 0 - const isLoading = proxyContextValue.isLoading || isLoadingLatencies - const permissions = usePermissions() + const buttonRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); + const [refetchDate, setRefetchDate] = useState(); + const selectedProxy = proxyContextValue.proxy.proxy; + const refreshLatencies = proxyContextValue.refetchProxyLatencies; + const closeMenu = () => setIsOpen(false); + const navigate = useNavigate(); + const latencies = proxyContextValue.proxyLatencies; + const isLoadingLatencies = Object.keys(latencies).length === 0; + const isLoading = proxyContextValue.isLoading || isLoadingLatencies; + const permissions = usePermissions(); const proxyLatencyLoading = (proxy: TypesGen.Region): boolean => { if (!refetchDate) { // Only show loading if the user manually requested a refetch - return false + return false; } - const latency = latencies?.[proxy.id] + const latency = latencies?.[proxy.id]; // Only show a loading spinner if: // - A latency exists. This means the latency was fetched at some point, so the // loader *should* be resolved. @@ -220,11 +222,11 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({ // is stale and we should show a loading spinner until the new latency is // fetched. if (proxy.healthy && latency && latency.at < refetchDate) { - return true + return true; } - return false - } + return false; + }; if (isLoading) { return ( @@ -233,7 +235,7 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({ height={BUTTON_SM_HEIGHT} sx={{ borderRadius: "4px", transform: "none" }} /> - ) + ); } return ( @@ -315,21 +317,21 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({ theme.palette.divider }} /> {proxyContextValue.proxies ?.sort((a, b) => { - const latencyA = latencies?.[a.id]?.latencyMS ?? Infinity - const latencyB = latencies?.[b.id]?.latencyMS ?? Infinity - return latencyA - latencyB + const latencyA = latencies?.[a.id]?.latencyMS ?? Infinity; + const latencyB = latencies?.[b.id]?.latencyMS ?? Infinity; + return latencyA - latencyB; }) .map((proxy) => ( { if (!proxy.healthy) { - displayError("Please select a healthy workspace proxy.") - closeMenu() - return + displayError("Please select a healthy workspace proxy."); + closeMenu(); + return; } - proxyContextValue.setProxy(proxy) - closeMenu() + proxyContextValue.setProxy(proxy); + closeMenu(); }} key={proxy.id} selected={proxy.id === selectedProxy?.id} @@ -361,7 +363,7 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({ { - navigate("deployment/workspace-proxies") + navigate("deployment/workspace-proxies"); }} > Proxy settings @@ -371,18 +373,18 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({ sx={{ fontSize: 14 }} onClick={(e) => { // Stop the menu from closing - e.stopPropagation() + e.stopPropagation(); // Refresh the latencies. - const refetchDate = refreshLatencies() - setRefetchDate(refetchDate) + const refetchDate = refreshLatencies(); + setRefetchDate(refetchDate); }} > Refresh Latencies - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ displayInitial: { @@ -472,4 +474,4 @@ const useStyles = makeStyles((theme) => ({ padding: `0 ${theme.spacing(3)}`, }, }, -})) +})); diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/BorderedMenu/BorderedMenu.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/BorderedMenu/BorderedMenu.tsx index 9190845107..6e6931ca5c 100644 --- a/site/src/components/Dashboard/Navbar/UserDropdown/BorderedMenu/BorderedMenu.tsx +++ b/site/src/components/Dashboard/Navbar/UserDropdown/BorderedMenu/BorderedMenu.tsx @@ -1,19 +1,19 @@ -import Popover, { PopoverProps } from "@mui/material/Popover" -import { makeStyles } from "@mui/styles" -import { FC, PropsWithChildren } from "react" +import Popover, { PopoverProps } from "@mui/material/Popover"; +import { makeStyles } from "@mui/styles"; +import { FC, PropsWithChildren } from "react"; -type BorderedMenuVariant = "user-dropdown" +type BorderedMenuVariant = "user-dropdown"; export type BorderedMenuProps = Omit & { - variant?: BorderedMenuVariant -} + variant?: BorderedMenuVariant; +}; export const BorderedMenu: FC> = ({ children, variant, ...rest }) => { - const styles = useStyles() + const styles = useStyles(); return ( > = ({ > {children} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ paperRoot: { @@ -32,4 +32,4 @@ const useStyles = makeStyles((theme) => ({ borderRadius: theme.shape.borderRadius, boxShadow: theme.shadows[6], }, -})) +})); diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/BorderedMenu/BorderedMenuRow.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/BorderedMenu/BorderedMenuRow.tsx index 4974dc9539..673814c124 100644 --- a/site/src/components/Dashboard/Navbar/UserDropdown/BorderedMenu/BorderedMenuRow.tsx +++ b/site/src/components/Dashboard/Navbar/UserDropdown/BorderedMenu/BorderedMenuRow.tsx @@ -1,32 +1,32 @@ -import ListItem from "@mui/material/ListItem" -import { makeStyles } from "@mui/styles" -import CheckIcon from "@mui/icons-material/Check" -import { FC } from "react" -import { NavLink } from "react-router-dom" -import { ellipsizeText } from "../../../../../utils/ellipsizeText" -import { Typography } from "../../../../Typography/Typography" +import ListItem from "@mui/material/ListItem"; +import { makeStyles } from "@mui/styles"; +import CheckIcon from "@mui/icons-material/Check"; +import { FC } from "react"; +import { NavLink } from "react-router-dom"; +import { ellipsizeText } from "../../../../../utils/ellipsizeText"; +import { Typography } from "../../../../Typography/Typography"; -type BorderedMenuRowVariant = "narrow" | "wide" +type BorderedMenuRowVariant = "narrow" | "wide"; interface BorderedMenuRowProps { /** `true` indicates this row is currently selected */ - active?: boolean + active?: boolean; /** Optional description that appears beneath the title */ - description?: string + description?: string; /** URL path */ - path: string + path: string; /** Required title of this row */ - title: string + title: string; /** Defaults to `"wide"` */ - variant?: BorderedMenuRowVariant + variant?: BorderedMenuRowVariant; /** Callback fired when this row is clicked */ - onClick?: () => void + onClick?: () => void; } export const BorderedMenuRow: FC< React.PropsWithChildren > = ({ active, description, path, title, variant, onClick }) => { - const styles = useStyles() + const styles = useStyles(); return ( @@ -54,10 +54,10 @@ export const BorderedMenuRow: FC< - ) -} + ); +}; -const iconSize = 20 +const iconSize = 20; const useStyles = makeStyles((theme) => ({ root: { @@ -127,4 +127,4 @@ const useStyles = makeStyles((theme) => ({ marginLeft: theme.spacing(4.5), marginTop: theme.spacing(0.5), }, -})) +})); diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx index 29b1591e56..4c8b596f89 100644 --- a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx +++ b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx @@ -1,7 +1,7 @@ -import Box from "@mui/material/Box" -import { Story } from "@storybook/react" -import { MockUser } from "../../../../testHelpers/entities" -import { UserDropdown, UserDropdownProps } from "./UserDropdown" +import Box from "@mui/material/Box"; +import { Story } from "@storybook/react"; +import { MockUser } from "../../../../testHelpers/entities"; +import { UserDropdown, UserDropdownProps } from "./UserDropdown"; export default { title: "components/UserDropdown", @@ -9,18 +9,18 @@ export default { argTypes: { onSignOut: { action: "Sign Out" }, }, -} +}; const Template: Story = (args: UserDropdownProps) => ( -) +); -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { user: MockUser, onSignOut: () => { - return Promise.resolve() + return Promise.resolve(); }, -} +}; diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.test.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.test.tsx index ad44311f77..59ed9b18bb 100644 --- a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.test.tsx +++ b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.test.tsx @@ -1,8 +1,8 @@ -import { fireEvent, screen } from "@testing-library/react" -import { MockSupportLinks, MockUser } from "../../../../testHelpers/entities" -import { render } from "../../../../testHelpers/renderHelpers" -import { Language } from "./UserDropdownContent/UserDropdownContent" -import { UserDropdown, UserDropdownProps } from "./UserDropdown" +import { fireEvent, screen } from "@testing-library/react"; +import { MockSupportLinks, MockUser } from "../../../../testHelpers/entities"; +import { render } from "../../../../testHelpers/renderHelpers"; +import { Language } from "./UserDropdownContent/UserDropdownContent"; +import { UserDropdown, UserDropdownProps } from "./UserDropdown"; const renderAndClick = async (props: Partial = {}) => { render( @@ -11,20 +11,20 @@ const renderAndClick = async (props: Partial = {}) => { supportLinks={MockSupportLinks} onSignOut={props.onSignOut ?? jest.fn()} />, - ) - const trigger = await screen.findByTestId("user-dropdown-trigger") - fireEvent.click(trigger) -} + ); + const trigger = await screen.findByTestId("user-dropdown-trigger"); + fireEvent.click(trigger); +}; describe("UserDropdown", () => { describe("when the trigger is clicked", () => { it("opens the menu", async () => { - await renderAndClick() - expect(screen.getByText(Language.accountLabel)).toBeDefined() - expect(screen.getByText(MockSupportLinks[0].name)).toBeDefined() - expect(screen.getByText(MockSupportLinks[1].name)).toBeDefined() - expect(screen.getByText(MockSupportLinks[2].name)).toBeDefined() - expect(screen.getByText(Language.signOutLabel)).toBeDefined() - }) - }) -}) + await renderAndClick(); + expect(screen.getByText(Language.accountLabel)).toBeDefined(); + expect(screen.getByText(MockSupportLinks[0].name)).toBeDefined(); + expect(screen.getByText(MockSupportLinks[1].name)).toBeDefined(); + expect(screen.getByText(MockSupportLinks[2].name)).toBeDefined(); + expect(screen.getByText(Language.signOutLabel)).toBeDefined(); + }); + }); +}); diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.tsx index b74b7188cb..2ae831fbb9 100644 --- a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.tsx +++ b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdown.tsx @@ -1,24 +1,24 @@ -import Badge from "@mui/material/Badge" -import MenuItem from "@mui/material/MenuItem" -import { makeStyles } from "@mui/styles" -import { useState, FC, PropsWithChildren, MouseEvent } from "react" -import { colors } from "theme/colors" -import * as TypesGen from "../../../../api/typesGenerated" -import { navHeight } from "../../../../theme/constants" -import { BorderedMenu } from "./BorderedMenu/BorderedMenu" +import Badge from "@mui/material/Badge"; +import MenuItem from "@mui/material/MenuItem"; +import { makeStyles } from "@mui/styles"; +import { useState, FC, PropsWithChildren, MouseEvent } from "react"; +import { colors } from "theme/colors"; +import * as TypesGen from "../../../../api/typesGenerated"; +import { navHeight } from "../../../../theme/constants"; +import { BorderedMenu } from "./BorderedMenu/BorderedMenu"; import { CloseDropdown, OpenDropdown, -} from "../../../DropdownArrows/DropdownArrows" -import { UserAvatar } from "../../../UserAvatar/UserAvatar" -import { UserDropdownContent } from "./UserDropdownContent/UserDropdownContent" -import { BUTTON_SM_HEIGHT } from "theme/theme" +} from "../../../DropdownArrows/DropdownArrows"; +import { UserAvatar } from "../../../UserAvatar/UserAvatar"; +import { UserDropdownContent } from "./UserDropdownContent/UserDropdownContent"; +import { BUTTON_SM_HEIGHT } from "theme/theme"; export interface UserDropdownProps { - user: TypesGen.User - buildInfo?: TypesGen.BuildInfoResponse - supportLinks?: TypesGen.LinkConfig[] - onSignOut: () => void + user: TypesGen.User; + buildInfo?: TypesGen.BuildInfoResponse; + supportLinks?: TypesGen.LinkConfig[]; + onSignOut: () => void; } export const UserDropdown: FC> = ({ @@ -27,15 +27,15 @@ export const UserDropdown: FC> = ({ supportLinks, onSignOut, }: UserDropdownProps) => { - const styles = useStyles() - const [anchorEl, setAnchorEl] = useState() + const styles = useStyles(); + const [anchorEl, setAnchorEl] = useState(); const handleDropdownClick = (ev: MouseEvent): void => { - setAnchorEl(ev.currentTarget) - } + setAnchorEl(ev.currentTarget); + }; const onPopoverClose = () => { - setAnchorEl(undefined) - } + setAnchorEl(undefined); + }; return ( <> @@ -88,8 +88,8 @@ export const UserDropdown: FC> = ({ /> - ) -} + ); +}; export const useStyles = makeStyles((theme) => ({ divider: { @@ -110,4 +110,4 @@ export const useStyles = makeStyles((theme) => ({ backgroundColor: "transparent", }, }, -})) +})); diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent/UserDropdownContent.stories.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent/UserDropdownContent.stories.tsx index 79d42e1fd9..a5a9901dfc 100644 --- a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent/UserDropdownContent.stories.tsx +++ b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent/UserDropdownContent.stories.tsx @@ -1,36 +1,36 @@ -import { Story } from "@storybook/react" -import { MockUser } from "../../../../../testHelpers/entities" +import { Story } from "@storybook/react"; +import { MockUser } from "../../../../../testHelpers/entities"; import { UserDropdownContent, UserDropdownContentProps, -} from "./UserDropdownContent" +} from "./UserDropdownContent"; export default { title: "components/UserDropdownContent", component: UserDropdownContent, -} +}; const Template: Story = (args) => ( -) +); -export const ExampleNoRoles = Template.bind({}) +export const ExampleNoRoles = Template.bind({}); ExampleNoRoles.args = { user: { ...MockUser, roles: [], }, -} +}; -export const ExampleOneRole = Template.bind({}) +export const ExampleOneRole = Template.bind({}); ExampleOneRole.args = { user: { ...MockUser, roles: [{ name: "member", display_name: "Member" }], }, -} +}; -export const ExampleThreeRoles = Template.bind({}) +export const ExampleThreeRoles = Template.bind({}); ExampleThreeRoles.args = { user: { ...MockUser, @@ -40,4 +40,4 @@ ExampleThreeRoles.args = { { name: "auditor", display_name: "Auditor" }, ], }, -} +}; diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent/UserDropdownContent.test.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent/UserDropdownContent.test.tsx index 5826fb8f10..b94e1bf658 100644 --- a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent/UserDropdownContent.test.tsx +++ b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent/UserDropdownContent.test.tsx @@ -1,24 +1,24 @@ -import { screen } from "@testing-library/react" +import { screen } from "@testing-library/react"; import { MockBuildInfo, MockSupportLinks, MockUser, -} from "../../../../../testHelpers/entities" -import { render } from "../../../../../testHelpers/renderHelpers" -import { Language, UserDropdownContent } from "./UserDropdownContent" +} from "../../../../../testHelpers/entities"; +import { render } from "../../../../../testHelpers/renderHelpers"; +import { Language, UserDropdownContent } from "./UserDropdownContent"; describe("UserDropdownContent", () => { - const env = process.env + const env = process.env; // REMARK: copying process.env so we don't mutate that object or encounter conflicts between tests beforeEach(() => { - process.env = { ...env } - }) + process.env = { ...env }; + }); // REMARK: restoring process.env afterEach(() => { - process.env = env - }) + process.env = env; + }); it("displays the menu items", () => { render( @@ -29,21 +29,21 @@ describe("UserDropdownContent", () => { onSignOut={jest.fn()} onPopoverClose={jest.fn()} />, - ) - expect(screen.getByText(Language.accountLabel)).toBeDefined() - expect(screen.getByText(Language.signOutLabel)).toBeDefined() - expect(screen.getByText(Language.copyrightText)).toBeDefined() - expect(screen.getByText(MockSupportLinks[0].name)).toBeDefined() - expect(screen.getByText(MockSupportLinks[1].name)).toBeDefined() - expect(screen.getByText(MockSupportLinks[2].name)).toBeDefined() + ); + expect(screen.getByText(Language.accountLabel)).toBeDefined(); + expect(screen.getByText(Language.signOutLabel)).toBeDefined(); + expect(screen.getByText(Language.copyrightText)).toBeDefined(); + expect(screen.getByText(MockSupportLinks[0].name)).toBeDefined(); + expect(screen.getByText(MockSupportLinks[1].name)).toBeDefined(); + expect(screen.getByText(MockSupportLinks[2].name)).toBeDefined(); expect( screen.getByText(MockSupportLinks[2].name).closest("a"), ).toHaveAttribute( "href", "https://github.com/coder/coder/issues/new?labels=needs+grooming&body=Version%3A%20%5B%60v99.999.9999%2Bc9cdf14%60%5D(file%3A%2F%2F%2Fmock-url)", - ) - expect(screen.getByText(MockBuildInfo.version)).toBeDefined() - }) + ); + expect(screen.getByText(MockBuildInfo.version)).toBeDefined(); + }); it("has the correct link for the account item", () => { render( @@ -52,26 +52,26 @@ describe("UserDropdownContent", () => { onSignOut={jest.fn()} onPopoverClose={jest.fn()} />, - ) + ); - const link = screen.getByText(Language.accountLabel).closest("a") + const link = screen.getByText(Language.accountLabel).closest("a"); if (!link) { - throw new Error("Anchor tag not found for the account menu item") + throw new Error("Anchor tag not found for the account menu item"); } - expect(link.getAttribute("href")).toBe("/settings/account") - }) + expect(link.getAttribute("href")).toBe("/settings/account"); + }); it("calls the onSignOut function", () => { - const onSignOut = jest.fn() + const onSignOut = jest.fn(); render( , - ) - screen.getByText(Language.signOutLabel).click() - expect(onSignOut).toBeCalledTimes(1) - }) -}) + ); + screen.getByText(Language.signOutLabel).click(); + expect(onSignOut).toBeCalledTimes(1); + }); +}); diff --git a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent/UserDropdownContent.tsx b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent/UserDropdownContent.tsx index a17accfd4c..b9307a6514 100644 --- a/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent/UserDropdownContent.tsx +++ b/site/src/components/Dashboard/Navbar/UserDropdown/UserDropdownContent/UserDropdownContent.tsx @@ -1,30 +1,30 @@ -import Divider from "@mui/material/Divider" -import MenuItem from "@mui/material/MenuItem" -import { makeStyles } from "@mui/styles" -import AccountIcon from "@mui/icons-material/AccountCircleOutlined" -import BugIcon from "@mui/icons-material/BugReportOutlined" -import ChatIcon from "@mui/icons-material/ChatOutlined" -import LaunchIcon from "@mui/icons-material/LaunchOutlined" -import { Stack } from "components/Stack/Stack" -import { FC } from "react" -import { Link } from "react-router-dom" -import * as TypesGen from "../../../../../api/typesGenerated" -import DocsIcon from "@mui/icons-material/MenuBook" -import LogoutIcon from "@mui/icons-material/ExitToAppOutlined" -import { combineClasses } from "utils/combineClasses" +import Divider from "@mui/material/Divider"; +import MenuItem from "@mui/material/MenuItem"; +import { makeStyles } from "@mui/styles"; +import AccountIcon from "@mui/icons-material/AccountCircleOutlined"; +import BugIcon from "@mui/icons-material/BugReportOutlined"; +import ChatIcon from "@mui/icons-material/ChatOutlined"; +import LaunchIcon from "@mui/icons-material/LaunchOutlined"; +import { Stack } from "components/Stack/Stack"; +import { FC } from "react"; +import { Link } from "react-router-dom"; +import * as TypesGen from "../../../../../api/typesGenerated"; +import DocsIcon from "@mui/icons-material/MenuBook"; +import LogoutIcon from "@mui/icons-material/ExitToAppOutlined"; +import { combineClasses } from "utils/combineClasses"; export const Language = { accountLabel: "Account", signOutLabel: "Sign Out", copyrightText: `\u00a9 ${new Date().getFullYear()} Coder Technologies, Inc.`, -} +}; export interface UserDropdownContentProps { - user: TypesGen.User - buildInfo?: TypesGen.BuildInfoResponse - supportLinks?: TypesGen.LinkConfig[] - onPopoverClose: () => void - onSignOut: () => void + user: TypesGen.User; + buildInfo?: TypesGen.BuildInfoResponse; + supportLinks?: TypesGen.LinkConfig[]; + onPopoverClose: () => void; + onSignOut: () => void; } export const UserDropdownContent: FC = ({ @@ -34,7 +34,7 @@ export const UserDropdownContent: FC = ({ onPopoverClose, onSignOut, }) => { - const styles = useStyles() + const styles = useStyles(); return (
@@ -101,8 +101,8 @@ export const UserDropdownContent: FC = ({
{Language.copyrightText}
- ) -} + ); +}; const useStyles = makeStyles((theme) => ({ info: { @@ -166,7 +166,7 @@ const useStyles = makeStyles((theme) => ({ buildInfo: { color: theme.palette.text.primary, }, -})) +})); const includeBuildInfo = ( href: string, @@ -177,5 +177,5 @@ const includeBuildInfo = ( `${encodeURIComponent( `Version: [\`${buildInfo?.version}\`](${buildInfo?.external_url})`, )}`, - ) -} + ); +}; diff --git a/site/src/components/Dashboard/ServiceBanner/ServiceBanner.tsx b/site/src/components/Dashboard/ServiceBanner/ServiceBanner.tsx index d76e7053ea..3f5a65f0d7 100644 --- a/site/src/components/Dashboard/ServiceBanner/ServiceBanner.tsx +++ b/site/src/components/Dashboard/ServiceBanner/ServiceBanner.tsx @@ -1,13 +1,13 @@ -import { useDashboard } from "components/Dashboard/DashboardProvider" -import { ServiceBannerView } from "./ServiceBannerView" +import { useDashboard } from "components/Dashboard/DashboardProvider"; +import { ServiceBannerView } from "./ServiceBannerView"; export const ServiceBanner: React.FC = () => { - const { appearance } = useDashboard() + const { appearance } = useDashboard(); const { message, background_color, enabled } = - appearance.config.service_banner + appearance.config.service_banner; if (!enabled) { - return null + return null; } if (message !== undefined && background_color !== undefined) { @@ -17,8 +17,8 @@ export const ServiceBanner: React.FC = () => { backgroundColor={background_color} preview={appearance.preview} /> - ) + ); } else { - return null + return null; } -} +}; diff --git a/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.stories.tsx b/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.stories.tsx index 37b6c4276b..813b6a5a1c 100644 --- a/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.stories.tsx +++ b/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.stories.tsx @@ -1,24 +1,24 @@ -import { Story } from "@storybook/react" -import { ServiceBannerView, ServiceBannerViewProps } from "./ServiceBannerView" +import { Story } from "@storybook/react"; +import { ServiceBannerView, ServiceBannerViewProps } from "./ServiceBannerView"; export default { title: "components/ServiceBannerView", component: ServiceBannerView, -} +}; const Template: Story = (args) => ( -) +); -export const Production = Template.bind({}) +export const Production = Template.bind({}); Production.args = { message: "weeeee", backgroundColor: "#FFFFFF", -} +}; -export const Preview = Template.bind({}) +export const Preview = Template.bind({}); Preview.args = { message: "weeeee", backgroundColor: "#000000", preview: true, -} +}; diff --git a/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.tsx b/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.tsx index f452b796d8..71d99eac17 100644 --- a/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.tsx +++ b/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.tsx @@ -1,13 +1,13 @@ -import { makeStyles } from "@mui/styles" -import { Pill } from "components/Pill/Pill" -import ReactMarkdown from "react-markdown" -import { colors } from "theme/colors" -import { hex } from "color-convert" +import { makeStyles } from "@mui/styles"; +import { Pill } from "components/Pill/Pill"; +import ReactMarkdown from "react-markdown"; +import { colors } from "theme/colors"; +import { hex } from "color-convert"; export interface ServiceBannerViewProps { - message: string - backgroundColor: string - preview: boolean + message: string; + backgroundColor: string; + preview: boolean; } export const ServiceBannerView: React.FC = ({ @@ -15,7 +15,7 @@ export const ServiceBannerView: React.FC = ({ backgroundColor, preview, }) => { - const styles = useStyles() + const styles = useStyles(); // We don't want anything funky like an image or a heading in the service // banner. const markdownElementsAllowed = [ @@ -28,7 +28,7 @@ export const ServiceBannerView: React.FC = ({ "italic", "link", "em", - ] + ]; return (
= ({
- ) -} + ); +}; const useStyles = makeStyles((theme) => ({ container: { @@ -74,14 +74,14 @@ const useStyles = makeStyles((theme) => ({ color: "inherit", }, }, -})) +})); const readableForegroundColor = (backgroundColor: string): string => { - const rgb = hex.rgb(backgroundColor) + const rgb = hex.rgb(backgroundColor); // Logic taken from here: // https://github.com/casesandberg/react-color/blob/bc9a0e1dc5d11b06c511a8e02a95bd85c7129f4b/src/helpers/color.js#L56 // to be consistent with the color-picker label. - const yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000 - return yiq >= 128 ? "#000" : "#fff" -} + const yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000; + return yiq >= 128 ? "#000" : "#fff"; +}; diff --git a/site/src/components/DeploySettingsLayout/Badges.tsx b/site/src/components/DeploySettingsLayout/Badges.tsx index 465422ef03..5c32fcd8f1 100644 --- a/site/src/components/DeploySettingsLayout/Badges.tsx +++ b/site/src/components/DeploySettingsLayout/Badges.tsx @@ -1,92 +1,92 @@ -import { makeStyles } from "@mui/styles" -import { Stack } from "components/Stack/Stack" -import { PropsWithChildren, FC } from "react" -import { MONOSPACE_FONT_FAMILY } from "theme/constants" -import { combineClasses } from "utils/combineClasses" -import Tooltip from "@mui/material/Tooltip" +import { makeStyles } from "@mui/styles"; +import { Stack } from "components/Stack/Stack"; +import { PropsWithChildren, FC } from "react"; +import { MONOSPACE_FONT_FAMILY } from "theme/constants"; +import { combineClasses } from "utils/combineClasses"; +import Tooltip from "@mui/material/Tooltip"; export const EnabledBadge: FC = () => { - const styles = useStyles() + const styles = useStyles(); return ( Enabled - ) -} + ); +}; export const EntitledBadge: FC = () => { - const styles = useStyles() + const styles = useStyles(); return ( Entitled - ) -} + ); +}; export const HealthyBadge: FC<{ derpOnly: boolean }> = ({ derpOnly }) => { - const styles = useStyles() - let text = "Healthy" + const styles = useStyles(); + let text = "Healthy"; if (derpOnly) { - text = "Healthy (DERP Only)" + text = "Healthy (DERP Only)"; } return ( {text} - ) -} + ); +}; export const NotHealthyBadge: FC = () => { - const styles = useStyles() + const styles = useStyles(); return ( Unhealthy - ) -} + ); +}; export const NotRegisteredBadge: FC = () => { - const styles = useStyles() + const styles = useStyles(); return ( Never Seen - ) -} + ); +}; export const NotReachableBadge: FC = () => { - const styles = useStyles() + const styles = useStyles(); return ( Not Dialable - ) -} + ); +}; export const DisabledBadge: FC = () => { - const styles = useStyles() + const styles = useStyles(); return ( Disabled - ) -} + ); +}; export const EnterpriseBadge: FC = () => { - const styles = useStyles() + const styles = useStyles(); return ( Enterprise - ) -} + ); +}; export const Badges: FC = ({ children }) => { - const styles = useStyles() + const styles = useStyles(); return ( = ({ children }) => { > {children} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ badges: { @@ -152,4 +152,4 @@ const useStyles = makeStyles((theme) => ({ border: `1px solid ${theme.palette.divider}`, backgroundColor: theme.palette.background.paper, }, -})) +})); diff --git a/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx b/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx index 035748b3eb..c7b6a361c5 100644 --- a/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx +++ b/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx @@ -1,48 +1,48 @@ -import { makeStyles } from "@mui/styles" -import { Margins } from "components/Margins/Margins" -import { Stack } from "components/Stack/Stack" -import { Sidebar } from "./Sidebar" -import { createContext, Suspense, useContext, FC } from "react" -import { useMachine } from "@xstate/react" -import { Loader } from "components/Loader/Loader" -import { DAUsResponse } from "api/typesGenerated" -import { deploymentConfigMachine } from "xServices/deploymentConfig/deploymentConfigMachine" -import { RequirePermission } from "components/RequirePermission/RequirePermission" -import { usePermissions } from "hooks/usePermissions" -import { Outlet } from "react-router-dom" -import { DeploymentConfig } from "api/types" +import { makeStyles } from "@mui/styles"; +import { Margins } from "components/Margins/Margins"; +import { Stack } from "components/Stack/Stack"; +import { Sidebar } from "./Sidebar"; +import { createContext, Suspense, useContext, FC } from "react"; +import { useMachine } from "@xstate/react"; +import { Loader } from "components/Loader/Loader"; +import { DAUsResponse } from "api/typesGenerated"; +import { deploymentConfigMachine } from "xServices/deploymentConfig/deploymentConfigMachine"; +import { RequirePermission } from "components/RequirePermission/RequirePermission"; +import { usePermissions } from "hooks/usePermissions"; +import { Outlet } from "react-router-dom"; +import { DeploymentConfig } from "api/types"; type DeploySettingsContextValue = { - deploymentValues: DeploymentConfig - getDeploymentValuesError: unknown - deploymentDAUs?: DAUsResponse - getDeploymentDAUsError: unknown -} + deploymentValues: DeploymentConfig; + getDeploymentValuesError: unknown; + deploymentDAUs?: DAUsResponse; + getDeploymentDAUsError: unknown; +}; const DeploySettingsContext = createContext< DeploySettingsContextValue | undefined ->(undefined) +>(undefined); export const useDeploySettings = (): DeploySettingsContextValue => { - const context = useContext(DeploySettingsContext) + const context = useContext(DeploySettingsContext); if (!context) { throw new Error( "useDeploySettings should be used inside of DeploySettingsLayout", - ) + ); } - return context -} + return context; +}; export const DeploySettingsLayout: FC = () => { - const [state] = useMachine(deploymentConfigMachine) - const styles = useStyles() + const [state] = useMachine(deploymentConfigMachine); + const styles = useStyles(); const { deploymentValues, deploymentDAUs, getDeploymentValuesError, getDeploymentDAUsError, - } = state.context - const permissions = usePermissions() + } = state.context; + const permissions = usePermissions(); return ( @@ -70,8 +70,8 @@ export const DeploySettingsLayout: FC = () => { - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ wrapper: { @@ -82,4 +82,4 @@ const useStyles = makeStyles((theme) => ({ maxWidth: 800, width: "100%", }, -})) +})); diff --git a/site/src/components/DeploySettingsLayout/Fieldset.tsx b/site/src/components/DeploySettingsLayout/Fieldset.tsx index 1c0209979c..78c26e1793 100644 --- a/site/src/components/DeploySettingsLayout/Fieldset.tsx +++ b/site/src/components/DeploySettingsLayout/Fieldset.tsx @@ -1,15 +1,15 @@ -import { makeStyles } from "@mui/styles" -import { FC, ReactNode, FormEventHandler } from "react" -import Button from "@mui/material/Button" +import { makeStyles } from "@mui/styles"; +import { FC, ReactNode, FormEventHandler } from "react"; +import Button from "@mui/material/Button"; export const Fieldset: FC<{ - children: ReactNode - title: string | JSX.Element - subtitle?: string | JSX.Element - validation?: string | JSX.Element | false - button?: JSX.Element | false - onSubmit: FormEventHandler - isSubmitting?: boolean + children: ReactNode; + title: string | JSX.Element; + subtitle?: string | JSX.Element; + validation?: string | JSX.Element | false; + button?: JSX.Element | false; + onSubmit: FormEventHandler; + isSubmitting?: boolean; }> = ({ title, subtitle, @@ -19,7 +19,7 @@ export const Fieldset: FC<{ onSubmit, isSubmitting, }) => { - const styles = useStyles() + const styles = useStyles(); return (
@@ -37,8 +37,8 @@ export const Fieldset: FC<{ )}
- ) -} + ); +}; const useStyles = makeStyles((theme) => ({ fieldset: { @@ -75,4 +75,4 @@ const useStyles = makeStyles((theme) => ({ alignItems: "center", justifyContent: "space-between", }, -})) +})); diff --git a/site/src/components/DeploySettingsLayout/Header.tsx b/site/src/components/DeploySettingsLayout/Header.tsx index 90c9dc95f1..1952777133 100644 --- a/site/src/components/DeploySettingsLayout/Header.tsx +++ b/site/src/components/DeploySettingsLayout/Header.tsx @@ -1,16 +1,16 @@ -import Button from "@mui/material/Button" -import { makeStyles } from "@mui/styles" -import LaunchOutlined from "@mui/icons-material/LaunchOutlined" -import { Stack } from "components/Stack/Stack" -import { FC } from "react" +import Button from "@mui/material/Button"; +import { makeStyles } from "@mui/styles"; +import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; +import { Stack } from "components/Stack/Stack"; +import { FC } from "react"; export const Header: FC<{ - title: string | JSX.Element - description?: string | JSX.Element - secondary?: boolean - docsHref?: string + title: string | JSX.Element; + description?: string | JSX.Element; + secondary?: boolean; + docsHref?: string; }> = ({ title, description, docsHref, secondary }) => { - const styles = useStyles() + const styles = useStyles(); return ( @@ -34,8 +34,8 @@ export const Header: FC<{ )} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ headingGroup: { @@ -64,4 +64,4 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.secondary, lineHeight: "160%", }, -})) +})); diff --git a/site/src/components/DeploySettingsLayout/Option.tsx b/site/src/components/DeploySettingsLayout/Option.tsx index 49b680b55e..be28bf24e7 100644 --- a/site/src/components/DeploySettingsLayout/Option.tsx +++ b/site/src/components/DeploySettingsLayout/Option.tsx @@ -1,42 +1,42 @@ -import { makeStyles } from "@mui/styles" -import { PropsWithChildren, FC } from "react" -import { MONOSPACE_FONT_FAMILY } from "theme/constants" -import { DisabledBadge, EnabledBadge } from "./Badges" +import { makeStyles } from "@mui/styles"; +import { PropsWithChildren, FC } from "react"; +import { MONOSPACE_FONT_FAMILY } from "theme/constants"; +import { DisabledBadge, EnabledBadge } from "./Badges"; export const OptionName: FC = ({ children }) => { - const styles = useStyles() - return {children} -} + const styles = useStyles(); + return {children}; +}; export const OptionDescription: FC = ({ children }) => { - const styles = useStyles() - return {children} -} + const styles = useStyles(); + return {children}; +}; const NotSet: FC = () => { - const styles = useStyles() + const styles = useStyles(); - return Not set -} + return Not set; +}; export const OptionValue: FC<{ children?: unknown }> = ({ children }) => { - const styles = useStyles() + const styles = useStyles(); if (typeof children === "boolean") { - return children ? : + return children ? : ; } if (typeof children === "number") { - return {children} + return {children}; } if (typeof children === "string") { - return {children} + return {children}; } if (Array.isArray(children)) { if (children.length === 0) { - return + return ; } return ( @@ -47,15 +47,15 @@ export const OptionValue: FC<{ children?: unknown }> = ({ children }) => { ))} - ) + ); } if (children === "") { - return + return ; } - return {JSON.stringify(children)} -} + return {JSON.stringify(children)}; +}; const useStyles = makeStyles((theme) => ({ optionName: { @@ -88,4 +88,4 @@ const useStyles = makeStyles((theme) => ({ flexDirection: "column", gap: theme.spacing(0.5), }, -})) +})); diff --git a/site/src/components/DeploySettingsLayout/Options.test.tsx b/site/src/components/DeploySettingsLayout/Options.test.tsx index 57bf5da8ab..e5c1a5014c 100644 --- a/site/src/components/DeploySettingsLayout/Options.test.tsx +++ b/site/src/components/DeploySettingsLayout/Options.test.tsx @@ -1,5 +1,5 @@ -import { optionValue } from "./OptionsTable" -import { DeploymentOption } from "api/types" +import { optionValue } from "./OptionsTable"; +import { DeploymentOption } from "api/types"; const defaultOption: DeploymentOption = { name: "", @@ -8,12 +8,12 @@ const defaultOption: DeploymentOption = { flag_shorthand: "", value: "", hidden: false, -} +}; describe("optionValue", () => { it.each<{ - option: DeploymentOption - expected: unknown + option: DeploymentOption; + expected: unknown; }>([ { option: { @@ -68,6 +68,6 @@ describe("optionValue", () => { expected: [`"123"->"foo"`, `"456"->"bar"`, `"789"->"baz"`], }, ])(`[$option.name]optionValue($option.value)`, ({ option, expected }) => { - expect(optionValue(option)).toEqual(expected) - }) -}) + expect(optionValue(option)).toEqual(expected); + }); +}); diff --git a/site/src/components/DeploySettingsLayout/OptionsTable.tsx b/site/src/components/DeploySettingsLayout/OptionsTable.tsx index aef2661071..3a6fe1ea5b 100644 --- a/site/src/components/DeploySettingsLayout/OptionsTable.tsx +++ b/site/src/components/DeploySettingsLayout/OptionsTable.tsx @@ -1,26 +1,26 @@ -import { makeStyles } from "@mui/styles" -import Table from "@mui/material/Table" -import TableBody from "@mui/material/TableBody" -import TableCell from "@mui/material/TableCell" -import TableContainer from "@mui/material/TableContainer" -import TableHead from "@mui/material/TableHead" -import TableRow from "@mui/material/TableRow" -import { DeploymentOption } from "api/types" +import { makeStyles } from "@mui/styles"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import { DeploymentOption } from "api/types"; import { OptionDescription, OptionName, OptionValue, -} from "components/DeploySettingsLayout/Option" -import { FC } from "react" -import { intervalToDuration, formatDuration } from "date-fns" +} from "components/DeploySettingsLayout/Option"; +import { FC } from "react"; +import { intervalToDuration, formatDuration } from "date-fns"; const OptionsTable: FC<{ - options: DeploymentOption[] + options: DeploymentOption[]; }> = ({ options }) => { - const styles = useStyles() + const styles = useStyles(); if (options.length === 0) { - return

No options to configure

+ return

No options to configure

; } return ( @@ -39,7 +39,7 @@ const OptionsTable: FC<{ option.value === "" || option.value === undefined ) { - return null + return null; } return ( @@ -52,13 +52,13 @@ const OptionsTable: FC<{ {optionValue(option)} - ) + ); })} - ) -} + ); +}; // optionValue is a helper function to format the value of a specific deployment options export function optionValue(option: DeploymentOption) { @@ -68,18 +68,18 @@ export function optionValue(option: DeploymentOption) { // intervalToDuration takes ms, so convert nanoseconds to ms return formatDuration( intervalToDuration({ start: 0, end: (option.value as number) / 1e6 }), - ) + ); case "Strict-Transport-Security": if (option.value === 0) { - return "Disabled" + return "Disabled"; } - return (option.value as number).toString() + "s" + return (option.value as number).toString() + "s"; case "OIDC Group Mapping": return Object.entries(option.value as Record).map( ([key, value]) => `"${key}"->"${value}"`, - ) + ); default: - return option.value + return option.value; } } @@ -94,6 +94,6 @@ const useStyles = makeStyles((theme) => ({ paddingLeft: theme.spacing(4), }, }, -})) +})); -export default OptionsTable +export default OptionsTable; diff --git a/site/src/components/DeploySettingsLayout/Sidebar.tsx b/site/src/components/DeploySettingsLayout/Sidebar.tsx index e8b0445e4c..8cdb8856ad 100644 --- a/site/src/components/DeploySettingsLayout/Sidebar.tsx +++ b/site/src/components/DeploySettingsLayout/Sidebar.tsx @@ -1,23 +1,23 @@ -import { makeStyles } from "@mui/styles" -import Brush from "@mui/icons-material/Brush" -import LaunchOutlined from "@mui/icons-material/LaunchOutlined" -import ApprovalIcon from "@mui/icons-material/VerifiedUserOutlined" -import LockRounded from "@mui/icons-material/LockOutlined" -import Globe from "@mui/icons-material/PublicOutlined" -import HubOutlinedIcon from "@mui/icons-material/HubOutlined" -import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined" -import MonitorHeartOutlined from "@mui/icons-material/MonitorHeartOutlined" -import { GitIcon } from "components/Icons/GitIcon" -import { Stack } from "components/Stack/Stack" -import { ElementType, PropsWithChildren, ReactNode, FC } from "react" -import { NavLink } from "react-router-dom" -import { combineClasses } from "utils/combineClasses" -import { useDashboard } from "components/Dashboard/DashboardProvider" +import { makeStyles } from "@mui/styles"; +import Brush from "@mui/icons-material/Brush"; +import LaunchOutlined from "@mui/icons-material/LaunchOutlined"; +import ApprovalIcon from "@mui/icons-material/VerifiedUserOutlined"; +import LockRounded from "@mui/icons-material/LockOutlined"; +import Globe from "@mui/icons-material/PublicOutlined"; +import HubOutlinedIcon from "@mui/icons-material/HubOutlined"; +import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined"; +import MonitorHeartOutlined from "@mui/icons-material/MonitorHeartOutlined"; +import { GitIcon } from "components/Icons/GitIcon"; +import { Stack } from "components/Stack/Stack"; +import { ElementType, PropsWithChildren, ReactNode, FC } from "react"; +import { NavLink } from "react-router-dom"; +import { combineClasses } from "utils/combineClasses"; +import { useDashboard } from "components/Dashboard/DashboardProvider"; const SidebarNavItem: FC< PropsWithChildren<{ href: string; icon: ReactNode }> > = ({ children, href, icon }) => { - const styles = useStyles() + const styles = useStyles(); return ( - ) -} + ); +}; const SidebarNavItemIcon: FC<{ icon: ElementType }> = ({ icon: Icon }) => { - const styles = useStyles() - return -} + const styles = useStyles(); + return ; +}; export const Sidebar: React.FC = () => { - const styles = useStyles() - const dashboard = useDashboard() + const styles = useStyles(); + const dashboard = useDashboard(); return ( - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ sidebar: { @@ -148,4 +148,4 @@ const useStyles = makeStyles((theme) => ({ width: theme.spacing(2), height: theme.spacing(2), }, -})) +})); diff --git a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.stories.tsx b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.stories.tsx index aa9b65a32d..c329e117da 100644 --- a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.stories.tsx +++ b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.stories.tsx @@ -1,6 +1,6 @@ -import { action } from "@storybook/addon-actions" -import { ComponentMeta, Story } from "@storybook/react" -import { ConfirmDialog, ConfirmDialogProps } from "./ConfirmDialog" +import { action } from "@storybook/addon-actions"; +import { ComponentMeta, Story } from "@storybook/react"; +import { ConfirmDialog, ConfirmDialogProps } from "./ConfirmDialog"; export default { title: "Components/Dialogs/ConfirmDialog", @@ -16,43 +16,43 @@ export default { open: true, title: "Confirm Dialog", }, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) +); -export const DeleteDialog = Template.bind({}) +export const DeleteDialog = Template.bind({}); DeleteDialog.args = { description: "Do you really want to delete me?", hideCancel: false, type: "delete", -} +}; -export const InfoDialog = Template.bind({}) +export const InfoDialog = Template.bind({}); InfoDialog.args = { description: "Information is cool!", hideCancel: true, type: "info", -} +}; -export const InfoDialogWithCancel = Template.bind({}) +export const InfoDialogWithCancel = Template.bind({}); InfoDialogWithCancel.args = { description: "Information can be cool!", hideCancel: false, type: "info", -} +}; -export const SuccessDialog = Template.bind({}) +export const SuccessDialog = Template.bind({}); SuccessDialog.args = { description: "I am successful.", hideCancel: true, type: "success", -} +}; -export const SuccessDialogWithCancel = Template.bind({}) +export const SuccessDialogWithCancel = Template.bind({}); SuccessDialogWithCancel.args = { description: "I may be successful.", hideCancel: false, type: "success", -} +}; diff --git a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.test.tsx b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.test.tsx index 642029afdf..46bd548588 100644 --- a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.test.tsx +++ b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.test.tsx @@ -1,80 +1,80 @@ -import { fireEvent, screen } from "@testing-library/react" -import { ConfirmDialog, ConfirmDialogProps } from "./ConfirmDialog" -import { render } from "testHelpers/renderHelpers" +import { fireEvent, screen } from "@testing-library/react"; +import { ConfirmDialog, ConfirmDialogProps } from "./ConfirmDialog"; +import { render } from "testHelpers/renderHelpers"; describe("ConfirmDialog", () => { it("renders", () => { // Given - const onCloseMock = jest.fn() + const onCloseMock = jest.fn(); const props = { onClose: onCloseMock, open: true, title: "Test", - } + }; // When - render() + render(); // Then - expect(screen.getByRole("dialog")).toBeDefined() - }) + expect(screen.getByRole("dialog")).toBeDefined(); + }); it("does not display cancel for info dialogs", () => { // Given (note that info is the default) - const onCloseMock = jest.fn() + const onCloseMock = jest.fn(); const props = { cancelText: "CANCEL", onClose: onCloseMock, open: true, title: "Test", - } + }; // When - render() + render(); // Then - expect(screen.queryByText("CANCEL")).toBeNull() - }) + expect(screen.queryByText("CANCEL")).toBeNull(); + }); it("can display cancel when normally hidden", () => { // Given - const onCloseMock = jest.fn() + const onCloseMock = jest.fn(); const props = { cancelText: "CANCEL", onClose: onCloseMock, open: true, title: "Test", hideCancel: false, - } + }; // When - render() + render(); // Then - expect(screen.getByText("CANCEL")).toBeDefined() - }) + expect(screen.getByText("CANCEL")).toBeDefined(); + }); it("displays cancel for delete dialogs", () => { // Given - const onCloseMock = jest.fn() + const onCloseMock = jest.fn(); const props: ConfirmDialogProps = { cancelText: "CANCEL", onClose: onCloseMock, open: true, title: "Test", type: "delete", - } + }; // When - render() + render(); // Then - expect(screen.getByText("CANCEL")).toBeDefined() - }) + expect(screen.getByText("CANCEL")).toBeDefined(); + }); it("can hide cancel when normally visible", () => { // Given - const onCloseMock = jest.fn() + const onCloseMock = jest.fn(); const props: ConfirmDialogProps = { cancelText: "CANCEL", onClose: onCloseMock, @@ -82,38 +82,38 @@ describe("ConfirmDialog", () => { title: "Test", hideCancel: true, type: "delete", - } + }; // When - render() + render(); // Then - expect(screen.queryByText("CANCEL")).toBeNull() - }) + expect(screen.queryByText("CANCEL")).toBeNull(); + }); it("onClose is called when cancelled", () => { // Given - const onCloseMock = jest.fn() + const onCloseMock = jest.fn(); const props = { cancelText: "CANCEL", hideCancel: false, onClose: onCloseMock, open: true, title: "Test", - } + }; // When - render() - fireEvent.click(screen.getByText("CANCEL")) + render(); + fireEvent.click(screen.getByText("CANCEL")); // Then - expect(onCloseMock).toBeCalledTimes(1) - }) + expect(onCloseMock).toBeCalledTimes(1); + }); it("onConfirm is called when confirmed", () => { // Given - const onCloseMock = jest.fn() - const onConfirmMock = jest.fn() + const onCloseMock = jest.fn(); + const onConfirmMock = jest.fn(); const props = { cancelText: "CANCEL", confirmText: "CONFIRM", @@ -122,14 +122,14 @@ describe("ConfirmDialog", () => { onConfirm: onConfirmMock, open: true, title: "Test", - } + }; // When - render() - fireEvent.click(screen.getByText("CONFIRM")) + render(); + fireEvent.click(screen.getByText("CONFIRM")); // Then - expect(onCloseMock).toBeCalledTimes(0) - expect(onConfirmMock).toBeCalledTimes(1) - }) -}) + expect(onCloseMock).toBeCalledTimes(0); + expect(onConfirmMock).toBeCalledTimes(1); + }); +}); diff --git a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx index b16437ea62..053cde5b48 100644 --- a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx +++ b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx @@ -1,19 +1,19 @@ -import DialogActions from "@mui/material/DialogActions" -import { makeStyles } from "@mui/styles" -import { ReactNode, FC, PropsWithChildren } from "react" +import DialogActions from "@mui/material/DialogActions"; +import { makeStyles } from "@mui/styles"; +import { ReactNode, FC, PropsWithChildren } from "react"; import { Dialog, DialogActionButtons, DialogActionButtonsProps, -} from "../Dialog" -import { ConfirmDialogType } from "../types" -import Checkbox from "@mui/material/Checkbox" -import FormControlLabel from "@mui/material/FormControlLabel" -import { Stack } from "@mui/system" +} from "../Dialog"; +import { ConfirmDialogType } from "../types"; +import Checkbox from "@mui/material/Checkbox"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import { Stack } from "@mui/system"; interface ConfirmDialogTypeConfig { - confirmText: ReactNode - hideCancel: boolean + confirmText: ReactNode; + hideCancel: boolean; } const CONFIRM_DIALOG_DEFAULTS: Record< @@ -32,30 +32,30 @@ const CONFIRM_DIALOG_DEFAULTS: Record< confirmText: "OK", hideCancel: true, }, -} +}; export interface ConfirmDialogProps extends Omit< DialogActionButtonsProps, "color" | "confirmDialog" | "onCancel" > { - readonly description?: ReactNode + readonly description?: ReactNode; /** * hideCancel hides the cancel button when set true, and shows the cancel * button when set to false. When undefined: * - cancel is not displayed for "info" dialogs * - cancel is displayed for "delete" dialogs */ - readonly hideCancel?: boolean + readonly hideCancel?: boolean; /** * onClose is called when canceling (if cancel is showing). * * Additionally, if onConfirm is not defined onClose will be used in its place * when confirming. */ - readonly onClose: () => void - readonly open: boolean - readonly title: string + readonly onClose: () => void; + readonly open: boolean; + readonly title: string; } const useStyles = makeStyles((theme) => ({ @@ -98,7 +98,7 @@ const useStyles = makeStyles((theme) => ({ margin: theme.spacing(1, 0), }, }, -})) +})); /** * Quick-use version of the Dialog component with slightly alternative styles, @@ -117,12 +117,12 @@ export const ConfirmDialog: FC> = ({ title, type = "info", }) => { - const styles = useStyles({ type }) + const styles = useStyles({ type }); - const defaults = CONFIRM_DIALOG_DEFAULTS[type] + const defaults = CONFIRM_DIALOG_DEFAULTS[type]; if (typeof hideCancel === "undefined") { - hideCancel = defaults.hideCancel + hideCancel = defaults.hideCancel; } return ( @@ -152,18 +152,18 @@ export const ConfirmDialog: FC> = ({ /> - ) -} + ); +}; export interface ScheduleDialogProps extends ConfirmDialogProps { - readonly inactiveWorkspacesToGoDormant: number - readonly inactiveWorkspacesToGoDormantInWeek: number - readonly dormantWorkspacesToBeDeleted: number - readonly dormantWorkspacesToBeDeletedInWeek: number - readonly updateDormantWorkspaces: (confirm: boolean) => void - readonly updateInactiveWorkspaces: (confirm: boolean) => void - readonly dormantValueChanged: boolean - readonly deletionValueChanged: boolean + readonly inactiveWorkspacesToGoDormant: number; + readonly inactiveWorkspacesToGoDormantInWeek: number; + readonly dormantWorkspacesToBeDeleted: number; + readonly dormantWorkspacesToBeDeletedInWeek: number; + readonly updateDormantWorkspaces: (confirm: boolean) => void; + readonly updateInactiveWorkspaces: (confirm: boolean) => void; + readonly dormantValueChanged: boolean; + readonly deletionValueChanged: boolean; } export const ScheduleDialog: FC> = ({ @@ -185,21 +185,22 @@ export const ScheduleDialog: FC> = ({ dormantValueChanged, deletionValueChanged, }) => { - const styles = useScheduleStyles({ type }) + const styles = useScheduleStyles({ type }); - const defaults = CONFIRM_DIALOG_DEFAULTS["delete"] + const defaults = CONFIRM_DIALOG_DEFAULTS["delete"]; if (typeof hideCancel === "undefined") { - hideCancel = defaults.hideCancel + hideCancel = defaults.hideCancel; } const showDormancyWarning = dormantValueChanged && (inactiveWorkspacesToGoDormant > 0 || - inactiveWorkspacesToGoDormantInWeek > 0) + inactiveWorkspacesToGoDormantInWeek > 0); const showDeletionWarning = deletionValueChanged && - (dormantWorkspacesToBeDeleted > 0 || dormantWorkspacesToBeDeletedInWeek > 0) + (dormantWorkspacesToBeDeleted > 0 || + dormantWorkspacesToBeDeletedInWeek > 0); return ( > = ({ { - updateInactiveWorkspaces(e.target.checked) + updateInactiveWorkspaces(e.target.checked); }} /> } @@ -250,7 +251,7 @@ export const ScheduleDialog: FC> = ({ { - updateDormantWorkspaces(e.target.checked) + updateDormantWorkspaces(e.target.checked); }} /> } @@ -275,8 +276,8 @@ export const ScheduleDialog: FC> = ({ /> - ) -} + ); +}; const useScheduleStyles = makeStyles((theme) => ({ dialogWrapper: { @@ -318,4 +319,4 @@ const useScheduleStyles = makeStyles((theme) => ({ margin: theme.spacing(1, 0), }, }, -})) +})); diff --git a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.stories.tsx b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.stories.tsx index 810204222d..8e7f13c987 100644 --- a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.stories.tsx +++ b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.stories.tsx @@ -1,6 +1,6 @@ -import { action } from "@storybook/addon-actions" -import { ComponentMeta, Story } from "@storybook/react" -import { DeleteDialog, DeleteDialogProps } from "./DeleteDialog" +import { action } from "@storybook/addon-actions"; +import { ComponentMeta, Story } from "@storybook/react"; +import { DeleteDialog, DeleteDialogProps } from "./DeleteDialog"; export default { title: "Components/Dialogs/DeleteDialog", @@ -18,11 +18,11 @@ export default { name: "MyFoo", info: "Here's some info about the foo so you know you're deleting the right one.", }, -} as ComponentMeta +} as ComponentMeta; -const Template: Story = (args) => +const Template: Story = (args) => ; -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { isOpen: true, -} +}; diff --git a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx index 20a55275d0..688997c1e7 100644 --- a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx +++ b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.test.tsx @@ -1,8 +1,8 @@ -import { screen } from "@testing-library/react" -import userEvent from "@testing-library/user-event" -import i18next from "i18next" -import { render } from "testHelpers/renderHelpers" -import { DeleteDialog } from "./DeleteDialog" +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import i18next from "i18next"; +import { render } from "testHelpers/renderHelpers"; +import { DeleteDialog } from "./DeleteDialog"; describe("DeleteDialog", () => { it("disables confirm button when the text field is empty", () => { @@ -14,13 +14,13 @@ describe("DeleteDialog", () => { entity="template" name="MyTemplate" />, - ) - const confirmButton = screen.getByRole("button", { name: "Delete" }) - expect(confirmButton).toBeDisabled() - }) + ); + const confirmButton = screen.getByRole("button", { name: "Delete" }); + expect(confirmButton).toBeDisabled(); + }); it("disables confirm button when the text field is filled incorrectly", async () => { - const { t } = i18next + const { t } = i18next; render( { entity="template" name="MyTemplate" />, - ) + ); const labelText = t("deleteDialog.confirmLabel", { ns: "common", entity: "template", - }) - const textField = screen.getByLabelText(labelText) - await userEvent.type(textField, "MyTemplateWrong") - const confirmButton = screen.getByRole("button", { name: "Delete" }) - expect(confirmButton).toBeDisabled() - }) + }); + const textField = screen.getByLabelText(labelText); + await userEvent.type(textField, "MyTemplateWrong"); + const confirmButton = screen.getByRole("button", { name: "Delete" }); + expect(confirmButton).toBeDisabled(); + }); it("enables confirm button when the text field is filled correctly", async () => { - const { t } = i18next + const { t } = i18next; render( { entity="template" name="MyTemplate" />, - ) + ); const labelText = t("deleteDialog.confirmLabel", { ns: "common", entity: "template", - }) - const textField = screen.getByLabelText(labelText) - await userEvent.type(textField, "MyTemplate") - const confirmButton = screen.getByRole("button", { name: "Delete" }) - expect(confirmButton).not.toBeDisabled() - }) -}) + }); + const textField = screen.getByLabelText(labelText); + await userEvent.type(textField, "MyTemplate"); + const confirmButton = screen.getByRole("button", { name: "Delete" }); + expect(confirmButton).not.toBeDisabled(); + }); +}); diff --git a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx index f46405cbb4..ebca5f3da3 100644 --- a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx +++ b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx @@ -1,18 +1,18 @@ -import makeStyles from "@mui/styles/makeStyles" -import TextField from "@mui/material/TextField" -import { Maybe } from "components/Conditionals/Maybe" -import { ChangeEvent, useState, PropsWithChildren, FC } from "react" -import { useTranslation } from "react-i18next" -import { ConfirmDialog } from "../ConfirmDialog/ConfirmDialog" +import makeStyles from "@mui/styles/makeStyles"; +import TextField from "@mui/material/TextField"; +import { Maybe } from "components/Conditionals/Maybe"; +import { ChangeEvent, useState, PropsWithChildren, FC } from "react"; +import { useTranslation } from "react-i18next"; +import { ConfirmDialog } from "../ConfirmDialog/ConfirmDialog"; export interface DeleteDialogProps { - isOpen: boolean - onConfirm: () => void - onCancel: () => void - entity: string - name: string - info?: string - confirmLoading?: boolean + isOpen: boolean; + onConfirm: () => void; + onCancel: () => void; + entity: string; + name: string; + info?: string; + confirmLoading?: boolean; } export const DeleteDialog: FC> = ({ @@ -24,14 +24,14 @@ export const DeleteDialog: FC> = ({ name, confirmLoading, }) => { - const styles = useStyles() - const { t } = useTranslation("common") - const [nameValue, setNameValue] = useState("") - const confirmed = name === nameValue + const styles = useStyles(); + const { t } = useTranslation("common"); + const [nameValue, setNameValue] = useState(""); + const confirmed = name === nameValue; const handleChange = (event: ChangeEvent) => { - setNameValue(event.target.value) - } - const hasError = nameValue.length > 0 && !confirmed + setNameValue(event.target.value); + }; + const hasError = nameValue.length > 0 && !confirmed; const content = ( <> @@ -43,9 +43,9 @@ export const DeleteDialog: FC> = ({
{ - e.preventDefault() + e.preventDefault(); if (confirmed) { - onConfirm() + onConfirm(); } }} > @@ -65,7 +65,7 @@ export const DeleteDialog: FC> = ({ /> - ) + ); return ( > = ({ confirmLoading={confirmLoading} disabled={!confirmed} /> - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ warning: { @@ -90,4 +90,4 @@ const useStyles = makeStyles((theme) => ({ textField: { marginTop: theme.spacing(3), }, -})) +})); diff --git a/site/src/components/Dialogs/Dialog.tsx b/site/src/components/Dialogs/Dialog.tsx index cf75380792..50457be167 100644 --- a/site/src/components/Dialogs/Dialog.tsx +++ b/site/src/components/Dialogs/Dialog.tsx @@ -1,38 +1,38 @@ -import MuiDialog, { DialogProps as MuiDialogProps } from "@mui/material/Dialog" -import { makeStyles } from "@mui/styles" -import * as React from "react" -import { colors } from "theme/colors" -import { combineClasses } from "../../utils/combineClasses" +import MuiDialog, { DialogProps as MuiDialogProps } from "@mui/material/Dialog"; +import { makeStyles } from "@mui/styles"; +import * as React from "react"; +import { colors } from "theme/colors"; +import { combineClasses } from "../../utils/combineClasses"; import { LoadingButton, LoadingButtonProps, -} from "../LoadingButton/LoadingButton" -import { ConfirmDialogType } from "./types" +} from "../LoadingButton/LoadingButton"; +import { ConfirmDialogType } from "./types"; export interface DialogActionButtonsProps { /** Text to display in the cancel button */ - cancelText?: string + cancelText?: string; /** Text to display in the confirm button */ - confirmText?: React.ReactNode + confirmText?: React.ReactNode; /** Whether or not confirm is loading, also disables cancel when true */ - confirmLoading?: boolean + confirmLoading?: boolean; /** Whether or not this is a confirm dialog */ - confirmDialog?: boolean + confirmDialog?: boolean; /** Whether or not the submit button is disabled */ - disabled?: boolean + disabled?: boolean; /** Called when cancel is clicked */ - onCancel?: () => void + onCancel?: () => void; /** Called when confirm is clicked */ - onConfirm?: () => void - type?: ConfirmDialogType + onConfirm?: () => void; + type?: ConfirmDialogType; } const typeToColor = (type: ConfirmDialogType): LoadingButtonProps["color"] => { if (type === "delete") { - return "secondary" + return "secondary"; } - return "primary" -} + return "primary"; +}; /** * Quickly handles most modals actions, some combination of a cancel and confirm button @@ -46,7 +46,7 @@ export const DialogActionButtons: React.FC = ({ onConfirm, type = "info", }) => { - const styles = useButtonStyles({ type }) + const styles = useButtonStyles({ type }); return ( <> @@ -74,8 +74,8 @@ export const DialogActionButtons: React.FC = ({ )} - ) -} + ); +}; const useButtonStyles = makeStyles((theme) => ({ errorButton: { @@ -146,9 +146,9 @@ const useButtonStyles = makeStyles((theme) => ({ }, }, }, -})) +})); -export type DialogProps = MuiDialogProps +export type DialogProps = MuiDialogProps; /** * Wrapper around Material UI's Dialog component. Conveniently exports all of @@ -159,5 +159,5 @@ export type DialogProps = MuiDialogProps */ export const Dialog: React.FC = (props) => { // Wrapped so we can add custom attributes below - return -} + return ; +}; diff --git a/site/src/components/Dialogs/types.ts b/site/src/components/Dialogs/types.ts index a2909db825..4e3444811f 100644 --- a/site/src/components/Dialogs/types.ts +++ b/site/src/components/Dialogs/types.ts @@ -1 +1 @@ -export type ConfirmDialogType = "delete" | "info" | "success" +export type ConfirmDialogType = "delete" | "info" | "success"; diff --git a/site/src/components/DropdownArrows/DropdownArrows.tsx b/site/src/components/DropdownArrows/DropdownArrows.tsx index 6a26758018..af0f73ea50 100644 --- a/site/src/components/DropdownArrows/DropdownArrows.tsx +++ b/site/src/components/DropdownArrows/DropdownArrows.tsx @@ -1,8 +1,8 @@ -import { makeStyles } from "@mui/styles" -import KeyboardArrowDown from "@mui/icons-material/KeyboardArrowDown" -import KeyboardArrowUp from "@mui/icons-material/KeyboardArrowUp" -import { FC } from "react" -import { Theme } from "@mui/material/styles" +import { makeStyles } from "@mui/styles"; +import KeyboardArrowDown from "@mui/icons-material/KeyboardArrowDown"; +import KeyboardArrowUp from "@mui/icons-material/KeyboardArrowUp"; +import { FC } from "react"; +import { Theme } from "@mui/material/styles"; const useStyles = makeStyles((theme: Theme) => ({ arrowIcon: { @@ -14,29 +14,29 @@ const useStyles = makeStyles((theme: Theme) => ({ arrowIconUp: { color: ({ color }) => color ?? theme.palette.primary.contrastText, }, -})) +})); interface ArrowProps { - margin?: boolean - color?: string + margin?: boolean; + color?: string; } export const OpenDropdown: FC = ({ margin = true, color }) => { - const styles = useStyles({ margin, color }) + const styles = useStyles({ margin, color }); return ( - ) -} + ); +}; export const CloseDropdown: FC = ({ margin = true, color }) => { - const styles = useStyles({ margin, color }) + const styles = useStyles({ margin, color }); return ( - ) -} + ); +}; diff --git a/site/src/components/EmptyState/EmptyState.test.tsx b/site/src/components/EmptyState/EmptyState.test.tsx index 880a982fb0..5e5ad5089d 100644 --- a/site/src/components/EmptyState/EmptyState.test.tsx +++ b/site/src/components/EmptyState/EmptyState.test.tsx @@ -1,36 +1,36 @@ -import { screen } from "@testing-library/react" -import { render } from "../../testHelpers/renderHelpers" -import { EmptyState } from "./EmptyState" +import { screen } from "@testing-library/react"; +import { render } from "../../testHelpers/renderHelpers"; +import { EmptyState } from "./EmptyState"; describe("EmptyState", () => { it("renders (smoke test)", async () => { // When - render() + render(); // Then - await screen.findByText("Hello, world") - }) + await screen.findByText("Hello, world"); + }); it("renders description text", async () => { // When render( , - ) + ); // Then - await screen.findByText("Hello, world") - await screen.findByText("Friendly greeting") - }) + await screen.findByText("Hello, world"); + await screen.findByText("Friendly greeting"); + }); it("renders cta component", async () => { // Given - const cta = - ) -} + ); +}; const defaultStyles = makeStyles((theme) => ({ footer: { @@ -67,4 +67,4 @@ const defaultStyles = makeStyles((theme) => ({ button: { width: "100%", }, -})) +})); diff --git a/site/src/components/FullPageForm/FullPageForm.stories.tsx b/site/src/components/FullPageForm/FullPageForm.stories.tsx index bdc8a2da10..bf5fed1da1 100644 --- a/site/src/components/FullPageForm/FullPageForm.stories.tsx +++ b/site/src/components/FullPageForm/FullPageForm.stories.tsx @@ -1,20 +1,20 @@ -import TextField from "@mui/material/TextField" -import { action } from "@storybook/addon-actions" -import { ComponentMeta, Story } from "@storybook/react" -import { FormFooter } from "../FormFooter/FormFooter" -import { Stack } from "../Stack/Stack" -import { FullPageForm, FullPageFormProps } from "./FullPageForm" +import TextField from "@mui/material/TextField"; +import { action } from "@storybook/addon-actions"; +import { ComponentMeta, Story } from "@storybook/react"; +import { FormFooter } from "../FormFooter/FormFooter"; +import { Stack } from "../Stack/Stack"; +import { FullPageForm, FullPageFormProps } from "./FullPageForm"; export default { title: "components/FullPageForm", component: FullPageForm, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => (
{ - e.preventDefault() + e.preventDefault(); }} > @@ -24,10 +24,10 @@ const Template: Story = (args) => (
-) +); -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { title: "My Form", detail: "Lorem ipsum dolor", -} +}; diff --git a/site/src/components/FullPageForm/FullPageForm.tsx b/site/src/components/FullPageForm/FullPageForm.tsx index 2d27f33795..0fd124b969 100644 --- a/site/src/components/FullPageForm/FullPageForm.tsx +++ b/site/src/components/FullPageForm/FullPageForm.tsx @@ -1,15 +1,15 @@ -import { Margins } from "components/Margins/Margins" -import { FC, ReactNode } from "react" +import { Margins } from "components/Margins/Margins"; +import { FC, ReactNode } from "react"; import { PageHeader, PageHeaderTitle, PageHeaderSubtitle, -} from "components/PageHeader/PageHeader" -import { makeStyles } from "@mui/styles" +} from "components/PageHeader/PageHeader"; +import { makeStyles } from "@mui/styles"; export interface FullPageFormProps { - title: string - detail?: ReactNode + title: string; + detail?: ReactNode; } export const FullPageForm: FC> = ({ @@ -17,7 +17,7 @@ export const FullPageForm: FC> = ({ detail, children, }) => { - const styles = useStyles() + const styles = useStyles(); return ( @@ -28,11 +28,11 @@ export const FullPageForm: FC> = ({
{children}
- ) -} + ); +}; const useStyles = makeStyles((theme) => ({ pageHeader: { paddingBottom: theme.spacing(3), }, -})) +})); diff --git a/site/src/components/FullPageForm/FullPageHorizontalForm.tsx b/site/src/components/FullPageForm/FullPageHorizontalForm.tsx index 175ae224e1..aa85e14dc8 100644 --- a/site/src/components/FullPageForm/FullPageHorizontalForm.tsx +++ b/site/src/components/FullPageForm/FullPageHorizontalForm.tsx @@ -1,16 +1,16 @@ -import { Margins } from "components/Margins/Margins" -import { FC, ReactNode } from "react" +import { Margins } from "components/Margins/Margins"; +import { FC, ReactNode } from "react"; import { PageHeader, PageHeaderTitle, PageHeaderSubtitle, -} from "components/PageHeader/PageHeader" -import Button from "@mui/material/Button" +} from "components/PageHeader/PageHeader"; +import Button from "@mui/material/Button"; export interface FullPageHorizontalFormProps { - title: string - detail?: ReactNode - onCancel?: () => void + title: string; + detail?: ReactNode; + onCancel?: () => void; } export const FullPageHorizontalForm: FC< @@ -33,5 +33,5 @@ export const FullPageHorizontalForm: FC<
{children}
- ) -} + ); +}; diff --git a/site/src/components/GlobalSnackbar/EnterpriseSnackbar/EnterpriseSnackbar.stories.tsx b/site/src/components/GlobalSnackbar/EnterpriseSnackbar/EnterpriseSnackbar.stories.tsx index f8053809ba..d3145a9e27 100644 --- a/site/src/components/GlobalSnackbar/EnterpriseSnackbar/EnterpriseSnackbar.stories.tsx +++ b/site/src/components/GlobalSnackbar/EnterpriseSnackbar/EnterpriseSnackbar.stories.tsx @@ -1,35 +1,35 @@ -import { Story } from "@storybook/react" +import { Story } from "@storybook/react"; import { EnterpriseSnackbar, EnterpriseSnackbarProps, -} from "./EnterpriseSnackbar" +} from "./EnterpriseSnackbar"; export default { title: "components/EnterpriseSnackbar", component: EnterpriseSnackbar, -} +}; const Template: Story = ( args: EnterpriseSnackbarProps, -) => +) => ; -export const Error = Template.bind({}) +export const Error = Template.bind({}); Error.args = { variant: "error", open: true, message: "Oops, something wrong happened.", -} +}; -export const Info = Template.bind({}) +export const Info = Template.bind({}); Info.args = { variant: "info", open: true, message: "Hey, something happened.", -} +}; -export const Success = Template.bind({}) +export const Success = Template.bind({}); Success.args = { variant: "success", open: true, message: "Hey, something good happened.", -} +}; diff --git a/site/src/components/GlobalSnackbar/EnterpriseSnackbar/EnterpriseSnackbar.tsx b/site/src/components/GlobalSnackbar/EnterpriseSnackbar/EnterpriseSnackbar.tsx index 07e8ac7cd3..8325a4e4cd 100644 --- a/site/src/components/GlobalSnackbar/EnterpriseSnackbar/EnterpriseSnackbar.tsx +++ b/site/src/components/GlobalSnackbar/EnterpriseSnackbar/EnterpriseSnackbar.tsx @@ -1,19 +1,19 @@ -import IconButton from "@mui/material/IconButton" +import IconButton from "@mui/material/IconButton"; import Snackbar, { SnackbarProps as MuiSnackbarProps, -} from "@mui/material/Snackbar" -import { makeStyles } from "@mui/styles" -import CloseIcon from "@mui/icons-material/Close" -import { FC } from "react" -import { combineClasses } from "../../../utils/combineClasses" +} from "@mui/material/Snackbar"; +import { makeStyles } from "@mui/styles"; +import CloseIcon from "@mui/icons-material/Close"; +import { FC } from "react"; +import { combineClasses } from "../../../utils/combineClasses"; -type EnterpriseSnackbarVariant = "error" | "info" | "success" +type EnterpriseSnackbarVariant = "error" | "info" | "success"; export interface EnterpriseSnackbarProps extends MuiSnackbarProps { /** Called when the snackbar should close, either from timeout or clicking close */ - onClose: () => void + onClose: () => void; /** Variant of snackbar, for theming */ - variant?: EnterpriseSnackbarVariant + variant?: EnterpriseSnackbarVariant; } /** @@ -30,7 +30,7 @@ export interface EnterpriseSnackbarProps extends MuiSnackbarProps { export const EnterpriseSnackbar: FC< React.PropsWithChildren > = ({ onClose, variant = "info", ContentProps = {}, action, ...rest }) => { - const styles = useStyles() + const styles = useStyles(); return ( - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ actionWrapper: { @@ -103,4 +103,4 @@ const useStyles = makeStyles((theme) => ({ snackbarContentSuccess: { borderLeftColor: theme.palette.success.main, }, -})) +})); diff --git a/site/src/components/GlobalSnackbar/GlobalSnackbar.tsx b/site/src/components/GlobalSnackbar/GlobalSnackbar.tsx index c143f22341..3e011a65e5 100644 --- a/site/src/components/GlobalSnackbar/GlobalSnackbar.tsx +++ b/site/src/components/GlobalSnackbar/GlobalSnackbar.tsx @@ -1,10 +1,10 @@ -import { makeStyles } from "@mui/styles" -import { useCallback, useState, FC } from "react" -import { useCustomEvent } from "../../hooks/events" -import { CustomEventListener } from "../../utils/events" -import { EnterpriseSnackbar } from "./EnterpriseSnackbar/EnterpriseSnackbar" -import { ErrorIcon } from "../Icons/ErrorIcon" -import { Typography } from "../Typography/Typography" +import { makeStyles } from "@mui/styles"; +import { useCallback, useState, FC } from "react"; +import { useCustomEvent } from "../../hooks/events"; +import { CustomEventListener } from "../../utils/events"; +import { EnterpriseSnackbar } from "./EnterpriseSnackbar/EnterpriseSnackbar"; +import { ErrorIcon } from "../Icons/ErrorIcon"; +import { Typography } from "../Typography/Typography"; import { AdditionalMessage, isNotificationList, @@ -13,32 +13,32 @@ import { MsgType, NotificationMsg, SnackbarEventType, -} from "./utils" +} from "./utils"; const variantFromMsgType = (type: MsgType) => { if (type === MsgType.Error) { - return "error" + return "error"; } else if (type === MsgType.Success) { - return "success" + return "success"; } else { - return "info" + return "info"; } -} +}; export const GlobalSnackbar: FC = () => { - const styles = useStyles() - const [open, setOpen] = useState(false) - const [notification, setNotification] = useState() + const styles = useStyles(); + const [open, setOpen] = useState(false); + const [notification, setNotification] = useState(); const handleNotification = useCallback>( (event) => { - setNotification(event.detail) - setOpen(true) + setNotification(event.detail); + setOpen(true); }, [], - ) + ); - useCustomEvent(SnackbarEventType, handleNotification) + useCustomEvent(SnackbarEventType, handleNotification); const renderAdditionalMessage = (msg: AdditionalMessage, idx: number) => { if (isNotificationText(msg)) { @@ -51,7 +51,7 @@ export const GlobalSnackbar: FC = () => { > {msg} - ) + ); } else if (isNotificationTextPrefixed(msg)) { return ( { > {msg.prefix}: {msg.text} - ) + ); } else if (isNotificationList(msg)) { return (
    @@ -74,13 +74,13 @@ export const GlobalSnackbar: FC = () => { ))}
- ) + ); } - return null - } + return null; + }; if (!notification) { - return null + return null; } return ( @@ -109,8 +109,8 @@ export const GlobalSnackbar: FC = () => { horizontal: "right", }} /> - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ list: { @@ -133,4 +133,4 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.error.contrastText, marginRight: theme.spacing(2), }, -})) +})); diff --git a/site/src/components/GlobalSnackbar/utils.test.ts b/site/src/components/GlobalSnackbar/utils.test.ts index bd2806c2dc..5fae4407e9 100644 --- a/site/src/components/GlobalSnackbar/utils.test.ts +++ b/site/src/components/GlobalSnackbar/utils.test.ts @@ -6,52 +6,52 @@ import { NotificationMsg, NotificationTextPrefixed, SnackbarEventType, -} from "./utils" +} from "./utils"; describe("Snackbar", () => { describe("isNotificationTextPrefixed", () => { // Regression test for case found in #10436 it("does not crash on null values", () => { // Given - const msg = null + const msg = null; // When - const isTextPrefixed = isNotificationTextPrefixed(msg) + const isTextPrefixed = isNotificationTextPrefixed(msg); // Then - expect(isTextPrefixed).toBe(false) - }) + expect(isTextPrefixed).toBe(false); + }); it("returns true if prefixed", () => { // Given const msg: NotificationTextPrefixed = { prefix: "warning", text: "careful with this workspace", - } + }; // When - const isTextPrefixed = isNotificationTextPrefixed(msg) + const isTextPrefixed = isNotificationTextPrefixed(msg); // Then - expect(isTextPrefixed).toBe(true) - }) + expect(isTextPrefixed).toBe(true); + }); it("returns false if not prefixed", () => { // Given - const msg = "plain ol' message" + const msg = "plain ol' message"; // When - const isTextPrefixed = isNotificationTextPrefixed(msg) + const isTextPrefixed = isNotificationTextPrefixed(msg); // Then - expect(isTextPrefixed).toBe(false) - }) - }) + expect(isTextPrefixed).toBe(false); + }); + }); describe("displaySuccess", () => { - const originalWindowDispatchEvent = window.dispatchEvent + const originalWindowDispatchEvent = window.dispatchEvent; type TDispatchEventMock = jest.MockedFunction< (msg: CustomEvent) => boolean - > - let dispatchEventMock: TDispatchEventMock + >; + let dispatchEventMock: TDispatchEventMock; // Helper function to extract the notification event // that was sent to `dispatchEvent`. This lets us validate @@ -63,18 +63,18 @@ describe("Snackbar", () => { // calls[0][0] is the first argument of the first call // calls[0][0].detail is the 'detail' argument passed to the `CustomEvent` - // this is the `NotificationMsg` object that gets sent to `dispatchEvent` - return dispatchEventMock.mock.calls[0][0].detail - } + return dispatchEventMock.mock.calls[0][0].detail; + }; beforeEach(() => { - dispatchEventMock = jest.fn() + dispatchEventMock = jest.fn(); window.dispatchEvent = - dispatchEventMock as unknown as typeof window.dispatchEvent - }) + dispatchEventMock as unknown as typeof window.dispatchEvent; + }); afterEach(() => { - window.dispatchEvent = originalWindowDispatchEvent - }) + window.dispatchEvent = originalWindowDispatchEvent; + }); it("can be called with only a title", () => { // Given @@ -82,17 +82,17 @@ describe("Snackbar", () => { msgType: MsgType.Success, msg: "Test", additionalMsgs: undefined, - } + }; // When - displaySuccess("Test") + displaySuccess("Test"); // Then - expect(dispatchEventMock).toBeCalledTimes(1) + expect(dispatchEventMock).toBeCalledTimes(1); expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual( expected, - ) - }) + ); + }); it("can be called with a title and additional message", () => { // Given @@ -100,30 +100,30 @@ describe("Snackbar", () => { msgType: MsgType.Success, msg: "Test", additionalMsgs: ["additional message"], - } + }; // When - displaySuccess("Test", "additional message") + displaySuccess("Test", "additional message"); // Then - expect(dispatchEventMock).toBeCalledTimes(1) + expect(dispatchEventMock).toBeCalledTimes(1); expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual( expected, - ) - }) - }) + ); + }); + }); describe("displayError", () => { it("shows the title and the message", (done) => { - const message = "Some error happened" + const message = "Some error happened"; window.addEventListener(SnackbarEventType, (event) => { - const notificationEvent = event as CustomEvent - expect(notificationEvent.detail.msg).toEqual(message) - done() - }) + const notificationEvent = event as CustomEvent; + expect(notificationEvent.detail.msg).toEqual(message); + done(); + }); - displayError(message) - }) - }) -}) + displayError(message); + }); + }); +}); diff --git a/site/src/components/GlobalSnackbar/utils.ts b/site/src/components/GlobalSnackbar/utils.ts index 03daf29b88..0eab34f2a6 100644 --- a/site/src/components/GlobalSnackbar/utils.ts +++ b/site/src/components/GlobalSnackbar/utils.ts @@ -1,4 +1,4 @@ -import { dispatchCustomEvent } from "../../utils/events" +import { dispatchCustomEvent } from "../../utils/events"; /////////////////////////////////////////////////////////////////////////////// // Notification Types @@ -14,15 +14,15 @@ export enum MsgType { * Display a prefixed paragraph inside a notification. */ export type NotificationTextPrefixed = { - prefix: string - text: string -} + prefix: string; + text: string; +}; -export type AdditionalMessage = NotificationTextPrefixed | string[] | string +export type AdditionalMessage = NotificationTextPrefixed | string[] | string; export const isNotificationText = (msg: AdditionalMessage): msg is string => { - return !Array.isArray(msg) && typeof msg === "string" -} + return !Array.isArray(msg) && typeof msg === "string"; +}; export const isNotificationTextPrefixed = ( msg: AdditionalMessage | null, @@ -31,22 +31,22 @@ export const isNotificationTextPrefixed = ( return ( typeof msg !== "string" && Object.prototype.hasOwnProperty.call(msg, "prefix") - ) + ); } - return false -} + return false; +}; export const isNotificationList = (msg: AdditionalMessage): msg is string[] => { - return Array.isArray(msg) -} + return Array.isArray(msg); +}; export interface NotificationMsg { - msgType: MsgType - msg: string - additionalMsgs?: AdditionalMessage[] + msgType: MsgType; + msg: string; + additionalMsgs?: AdditionalMessage[]; } -export const SnackbarEventType = "coder:notification" +export const SnackbarEventType = "coder:notification"; /////////////////////////////////////////////////////////////////////////////// // Notification Functions @@ -61,7 +61,7 @@ function dispatchNotificationEvent( msgType, msg, additionalMsgs, - }) + }); } export const displaySuccess = (msg: string, additionalMsg?: string): void => { @@ -69,13 +69,13 @@ export const displaySuccess = (msg: string, additionalMsg?: string): void => { MsgType.Success, msg, additionalMsg ? [additionalMsg] : undefined, - ) -} + ); +}; export const displayError = (msg: string, additionalMsg?: string): void => { dispatchNotificationEvent( MsgType.Error, msg, additionalMsg ? [additionalMsg] : undefined, - ) -} + ); +}; diff --git a/site/src/components/GroupAvatar/GroupAvatar.stories.tsx b/site/src/components/GroupAvatar/GroupAvatar.stories.tsx index 8fda557362..21407a96e3 100644 --- a/site/src/components/GroupAvatar/GroupAvatar.stories.tsx +++ b/site/src/components/GroupAvatar/GroupAvatar.stories.tsx @@ -1,15 +1,15 @@ -import { Story } from "@storybook/react" -import { GroupAvatar, GroupAvatarProps } from "./GroupAvatar" +import { Story } from "@storybook/react"; +import { GroupAvatar, GroupAvatarProps } from "./GroupAvatar"; export default { title: "components/GroupAvatar", component: GroupAvatar, -} +}; -const Template: Story = (args) => +const Template: Story = (args) => ; -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { name: "My Group", avatarURL: "", -} +}; diff --git a/site/src/components/GroupAvatar/GroupAvatar.tsx b/site/src/components/GroupAvatar/GroupAvatar.tsx index 8b08481a67..1f4cbc0347 100644 --- a/site/src/components/GroupAvatar/GroupAvatar.tsx +++ b/site/src/components/GroupAvatar/GroupAvatar.tsx @@ -1,8 +1,8 @@ -import { Avatar } from "components/Avatar/Avatar" -import Badge from "@mui/material/Badge" -import { withStyles } from "@mui/styles" -import Group from "@mui/icons-material/Group" -import { FC } from "react" +import { Avatar } from "components/Avatar/Avatar"; +import Badge from "@mui/material/Badge"; +import { withStyles } from "@mui/styles"; +import Group from "@mui/icons-material/Group"; +import { FC } from "react"; const StyledBadge = withStyles((theme) => ({ badge: { @@ -20,12 +20,12 @@ const StyledBadge = withStyles((theme) => ({ height: 14, }, }, -}))(Badge) +}))(Badge); export type GroupAvatarProps = { - name: string - avatarURL?: string -} + name: string; + avatarURL?: string; +}; export const GroupAvatar: FC = ({ name, avatarURL }) => { return ( @@ -39,5 +39,5 @@ export const GroupAvatar: FC = ({ name, avatarURL }) => { > {name} - ) -} + ); +}; diff --git a/site/src/components/HelpTooltip/HelpTooltip.stories.tsx b/site/src/components/HelpTooltip/HelpTooltip.stories.tsx index 420dc73554..799abfa9f6 100644 --- a/site/src/components/HelpTooltip/HelpTooltip.stories.tsx +++ b/site/src/components/HelpTooltip/HelpTooltip.stories.tsx @@ -1,4 +1,4 @@ -import { ComponentMeta, Story } from "@storybook/react" +import { ComponentMeta, Story } from "@storybook/react"; import { HelpTooltip, HelpTooltipLink, @@ -6,12 +6,12 @@ import { HelpTooltipProps, HelpTooltipText, HelpTooltipTitle, -} from "./HelpTooltip" +} from "./HelpTooltip"; export default { title: "components/HelpTooltip", component: HelpTooltip, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( @@ -28,11 +28,11 @@ const Template: Story = (args) => ( -) +); -export const Close = Template.bind({}) +export const Close = Template.bind({}); -export const Open = Template.bind({}) +export const Open = Template.bind({}); Open.args = { open: true, -} +}; diff --git a/site/src/components/HelpTooltip/HelpTooltip.tsx b/site/src/components/HelpTooltip/HelpTooltip.tsx index bb1a9a9af3..184ca9bc93 100644 --- a/site/src/components/HelpTooltip/HelpTooltip.tsx +++ b/site/src/components/HelpTooltip/HelpTooltip.tsx @@ -1,8 +1,8 @@ -import Link from "@mui/material/Link" -import Popover, { PopoverProps } from "@mui/material/Popover" -import { makeStyles } from "@mui/styles" -import HelpIcon from "@mui/icons-material/HelpOutline" -import OpenInNewIcon from "@mui/icons-material/OpenInNew" +import Link from "@mui/material/Link"; +import Popover, { PopoverProps } from "@mui/material/Popover"; +import { makeStyles } from "@mui/styles"; +import HelpIcon from "@mui/icons-material/HelpOutline"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import { createContext, useContext, @@ -10,43 +10,43 @@ import { useState, FC, PropsWithChildren, -} from "react" -import { combineClasses } from "utils/combineClasses" -import { Stack } from "components/Stack/Stack" -import Box, { BoxProps } from "@mui/material/Box" +} from "react"; +import { combineClasses } from "utils/combineClasses"; +import { Stack } from "components/Stack/Stack"; +import Box, { BoxProps } from "@mui/material/Box"; -type Icon = typeof HelpIcon +type Icon = typeof HelpIcon; -type Size = "small" | "medium" +type Size = "small" | "medium"; export interface HelpTooltipProps { // Useful to test on storybook - open?: boolean - size?: Size - icon?: Icon - iconClassName?: string - buttonClassName?: string + open?: boolean; + size?: Size; + icon?: Icon; + iconClassName?: string; + buttonClassName?: string; } export const HelpTooltipContext = createContext< { open: boolean; onClose: () => void } | undefined ->(undefined) +>(undefined); const useHelpTooltip = () => { - const helpTooltipContext = useContext(HelpTooltipContext) + const helpTooltipContext = useContext(HelpTooltipContext); if (!helpTooltipContext) { throw new Error( "This hook should be used in side of the HelpTooltipContext.", - ) + ); } - return helpTooltipContext -} + return helpTooltipContext; +}; export const HelpPopover: FC< PopoverProps & { onOpen: () => void; onClose: () => void } > = ({ onOpen, onClose, children, ...props }) => { - const styles = useStyles({ size: "small" }) + const styles = useStyles({ size: "small" }); return ( {children} - ) -} + ); +}; export const HelpTooltip: FC> = ({ children, @@ -80,14 +80,14 @@ export const HelpTooltip: FC> = ({ iconClassName, buttonClassName, }) => { - const styles = useStyles({ size }) - const anchorRef = useRef(null) - const [isOpen, setIsOpen] = useState(Boolean(open)) - const id = isOpen ? "help-popover" : undefined + const styles = useStyles({ size }); + const anchorRef = useRef(null); + const [isOpen, setIsOpen] = useState(Boolean(open)); + const id = isOpen ? "help-popover" : undefined; const onClose = () => { - setIsOpen(false) - } + setIsOpen(false); + }; return ( <> @@ -96,14 +96,14 @@ export const HelpTooltip: FC> = ({ aria-describedby={id} className={combineClasses([styles.button, buttonClassName])} onClick={(event) => { - event.stopPropagation() - setIsOpen(true) + event.stopPropagation(); + setIsOpen(true); }} onMouseEnter={() => { - setIsOpen(true) + setIsOpen(true); }} onMouseLeave={() => { - setIsOpen(false) + setIsOpen(false); }} aria-label="More info" > @@ -121,19 +121,19 @@ export const HelpTooltip: FC> = ({ - ) -} + ); +}; export const HelpTooltipTitle: FC> = ({ children, }) => { - const styles = useStyles({}) + const styles = useStyles({}); - return

{children}

-} + return

{children}

; +}; export const HelpTooltipText = (props: BoxProps) => { - const styles = useStyles({}) + const styles = useStyles({}); return ( { {...props} className={combineClasses([styles.text, props.className])} /> - ) -} + ); +}; export const HelpTooltipLink: FC> = ({ children, href, }) => { - const styles = useStyles({}) + const styles = useStyles({}); return ( {children} - ) -} + ); +}; export const HelpTooltipAction: FC< PropsWithChildren<{ - icon: Icon - onClick: () => void - ariaLabel?: string + icon: Icon; + onClick: () => void; + ariaLabel?: string; }> > = ({ children, icon: Icon, onClick, ariaLabel }) => { - const styles = useStyles({}) - const tooltip = useHelpTooltip() + const styles = useStyles({}); + const tooltip = useHelpTooltip(); return ( - ) -} + ); +}; export const HelpTooltipLinksGroup: FC> = ({ children, }) => { - const styles = useStyles({}) + const styles = useStyles({}); return ( {children} - ) -} + ); +}; const getButtonSpacingFromSize = (size?: Size): number => { switch (size) { case "small": - return 2.5 + return 2.5; case "medium": default: - return 3 + return 3; } -} +}; const getIconSpacingFromSize = (size?: Size): number => { switch (size) { case "small": - return 1.5 + return 1.5; case "medium": default: - return 2 + return 2; } -} +}; const useStyles = makeStyles((theme) => ({ button: { @@ -306,4 +306,4 @@ const useStyles = makeStyles((theme) => ({ height: 14, marginRight: theme.spacing(1), }, -})) +})); diff --git a/site/src/components/HelpTooltip/index.ts b/site/src/components/HelpTooltip/index.ts index 588febfa26..39c77b4bf5 100644 --- a/site/src/components/HelpTooltip/index.ts +++ b/site/src/components/HelpTooltip/index.ts @@ -1 +1 @@ -export * from "./HelpTooltip" +export * from "./HelpTooltip"; diff --git a/site/src/components/IconField/IconField.tsx b/site/src/components/IconField/IconField.tsx index db572d1f25..d088731f94 100644 --- a/site/src/components/IconField/IconField.tsx +++ b/site/src/components/IconField/IconField.tsx @@ -1,30 +1,30 @@ -import Button from "@mui/material/Button" -import InputAdornment from "@mui/material/InputAdornment" -import Popover from "@mui/material/Popover" -import TextField from "@mui/material/TextField" -import { OpenDropdown } from "components/DropdownArrows/DropdownArrows" -import { useRef, FC, useState } from "react" -import Picker from "@emoji-mart/react" -import { makeStyles } from "@mui/styles" -import { colors } from "theme/colors" -import { useTranslation } from "react-i18next" -import data from "@emoji-mart/data/sets/14/twitter.json" -import { IconFieldProps } from "./types" -import { Stack } from "components/Stack/Stack" +import Button from "@mui/material/Button"; +import InputAdornment from "@mui/material/InputAdornment"; +import Popover from "@mui/material/Popover"; +import TextField from "@mui/material/TextField"; +import { OpenDropdown } from "components/DropdownArrows/DropdownArrows"; +import { useRef, FC, useState } from "react"; +import Picker from "@emoji-mart/react"; +import { makeStyles } from "@mui/styles"; +import { colors } from "theme/colors"; +import { useTranslation } from "react-i18next"; +import data from "@emoji-mart/data/sets/14/twitter.json"; +import { IconFieldProps } from "./types"; +import { Stack } from "components/Stack/Stack"; const IconField: FC = ({ onPickEmoji, ...textFieldProps }) => { if ( typeof textFieldProps.value !== "string" && typeof textFieldProps.value !== "undefined" ) { - throw new Error(`Invalid icon value "${typeof textFieldProps.value}"`) + throw new Error(`Invalid icon value "${typeof textFieldProps.value}"`); } - const styles = useStyles() - const emojiButtonRef = useRef(null) - const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false) - const { t } = useTranslation("templateSettingsPage") - const hasIcon = textFieldProps.value && textFieldProps.value !== "" + const styles = useStyles(); + const emojiButtonRef = useRef(null); + const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); + const { t } = useTranslation("templateSettingsPage"); + const hasIcon = textFieldProps.value && textFieldProps.value !== ""; return ( @@ -53,7 +53,7 @@ const IconField: FC = ({ onPickEmoji, ...textFieldProps }) => { ref={emojiButtonRef} endIcon={} onClick={() => { - setIsEmojiPickerOpen((v) => !v) + setIsEmojiPickerOpen((v) => !v); }} > {t("selectEmoji")} @@ -64,7 +64,7 @@ const IconField: FC = ({ onPickEmoji, ...textFieldProps }) => { open={isEmojiPickerOpen} anchorEl={emojiButtonRef.current} onClose={() => { - setIsEmojiPickerOpen(false) + setIsEmojiPickerOpen(false); }} > = ({ onPickEmoji, ...textFieldProps }) => { const value = `/emojis/${emojiData.unified.replace( /-fe0f$/, "", - )}.png` - onPickEmoji(value) - setIsEmojiPickerOpen(false) + )}.png`; + onPickEmoji(value); + setIsEmojiPickerOpen(false); }} /> - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ "@global": { @@ -104,6 +104,6 @@ const useStyles = makeStyles((theme) => ({ maxWidth: "100%", }, }, -})) +})); -export default IconField +export default IconField; diff --git a/site/src/components/IconField/LazyIconField.tsx b/site/src/components/IconField/LazyIconField.tsx index b9eafa3b42..c3b03055b6 100644 --- a/site/src/components/IconField/LazyIconField.tsx +++ b/site/src/components/IconField/LazyIconField.tsx @@ -1,12 +1,12 @@ -import { lazy, FC, Suspense } from "react" -import { IconFieldProps } from "./types" +import { lazy, FC, Suspense } from "react"; +import { IconFieldProps } from "./types"; -const IconField = lazy(() => import("./IconField")) +const IconField = lazy(() => import("./IconField")); export const LazyIconField: FC = (props) => { return ( }> - ) -} + ); +}; diff --git a/site/src/components/IconField/types.ts b/site/src/components/IconField/types.ts index a974854220..45c2d4117b 100644 --- a/site/src/components/IconField/types.ts +++ b/site/src/components/IconField/types.ts @@ -1,5 +1,5 @@ -import { TextFieldProps } from "@mui/material/TextField" +import { TextFieldProps } from "@mui/material/TextField"; export type IconFieldProps = TextFieldProps & { - onPickEmoji: (value: string) => void -} + onPickEmoji: (value: string) => void; +}; diff --git a/site/src/components/Icons/AzureDevOpsIcon.tsx b/site/src/components/Icons/AzureDevOpsIcon.tsx index b9aa303ed4..7693c6614b 100644 --- a/site/src/components/Icons/AzureDevOpsIcon.tsx +++ b/site/src/components/Icons/AzureDevOpsIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const AzureDevOpsIcon = (props: SvgIconProps): JSX.Element => ( @@ -22,4 +22,4 @@ export const AzureDevOpsIcon = (props: SvgIconProps): JSX.Element => ( -) +); diff --git a/site/src/components/Icons/BitbucketIcon.tsx b/site/src/components/Icons/BitbucketIcon.tsx index 97792a0ed4..8cb419ade8 100644 --- a/site/src/components/Icons/BitbucketIcon.tsx +++ b/site/src/components/Icons/BitbucketIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const BitbucketIcon = (props: SvgIconProps): JSX.Element => ( @@ -29,4 +29,4 @@ export const BitbucketIcon = (props: SvgIconProps): JSX.Element => ( -) +); diff --git a/site/src/components/Icons/BuildingIcon.tsx b/site/src/components/Icons/BuildingIcon.tsx index 589553680f..70361b6a23 100644 --- a/site/src/components/Icons/BuildingIcon.tsx +++ b/site/src/components/Icons/BuildingIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const BuildingIcon = (props: SvgIconProps): JSX.Element => ( @@ -9,4 +9,4 @@ export const BuildingIcon = (props: SvgIconProps): JSX.Element => ( fill="currentColor" /> -) +); diff --git a/site/src/components/Icons/CloseIcon.tsx b/site/src/components/Icons/CloseIcon.tsx index fbfc2f3d6e..1f78727d65 100644 --- a/site/src/components/Icons/CloseIcon.tsx +++ b/site/src/components/Icons/CloseIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const CloseIcon = (props: SvgIconProps) => ( @@ -9,4 +9,4 @@ export const CloseIcon = (props: SvgIconProps) => ( strokeLinecap="square" /> -) +); diff --git a/site/src/components/Icons/CoderIcon.tsx b/site/src/components/Icons/CoderIcon.tsx index e8bd8bddd1..65d4e1fc20 100644 --- a/site/src/components/Icons/CoderIcon.tsx +++ b/site/src/components/Icons/CoderIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; /** * CoderIcon represents the cloud with brackets Coder brand icon. It does not @@ -13,4 +13,4 @@ export const CoderIcon = (props: SvgIconProps): JSX.Element => (
-) +); diff --git a/site/src/components/Icons/DockerIcon.tsx b/site/src/components/Icons/DockerIcon.tsx index f2c4c042a2..4118804ac8 100644 --- a/site/src/components/Icons/DockerIcon.tsx +++ b/site/src/components/Icons/DockerIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const DockerIcon = (props: SvgIconProps): JSX.Element => ( @@ -19,4 +19,4 @@ export const DockerIcon = (props: SvgIconProps): JSX.Element => ( style={{ fill: "#fff" }} /> -) +); diff --git a/site/src/components/Icons/EditSquare.tsx b/site/src/components/Icons/EditSquare.tsx index bb4ac1eee3..7caf89131d 100644 --- a/site/src/components/Icons/EditSquare.tsx +++ b/site/src/components/Icons/EditSquare.tsx @@ -1,7 +1,7 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const EditSquare = (props: SvgIconProps): JSX.Element => ( -) +); diff --git a/site/src/components/Icons/ErrorIcon.tsx b/site/src/components/Icons/ErrorIcon.tsx index 0fb8748be6..4d81f5ff3d 100644 --- a/site/src/components/Icons/ErrorIcon.tsx +++ b/site/src/components/Icons/ErrorIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const ErrorIcon = (props: SvgIconProps): JSX.Element => ( @@ -9,4 +9,4 @@ export const ErrorIcon = (props: SvgIconProps): JSX.Element => ( fill="currentColor" /> -) +); diff --git a/site/src/components/Icons/FileCopyIcon.tsx b/site/src/components/Icons/FileCopyIcon.tsx index 4d430b504c..433498cebb 100644 --- a/site/src/components/Icons/FileCopyIcon.tsx +++ b/site/src/components/Icons/FileCopyIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const FileCopyIcon = (props: SvgIconProps) => ( @@ -7,4 +7,4 @@ export const FileCopyIcon = (props: SvgIconProps) => ( fill="currentColor" /> -) +); diff --git a/site/src/components/Icons/GitIcon.tsx b/site/src/components/Icons/GitIcon.tsx index d1c57c6b31..63122d3781 100644 --- a/site/src/components/Icons/GitIcon.tsx +++ b/site/src/components/Icons/GitIcon.tsx @@ -1,7 +1,7 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const GitIcon = (props: SvgIconProps) => ( -) +); diff --git a/site/src/components/Icons/GitlabIcon.tsx b/site/src/components/Icons/GitlabIcon.tsx index e0b94a11da..ee7b105ac4 100644 --- a/site/src/components/Icons/GitlabIcon.tsx +++ b/site/src/components/Icons/GitlabIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const GitlabIcon = (props: SvgIconProps): JSX.Element => ( @@ -26,4 +26,4 @@ export const GitlabIcon = (props: SvgIconProps): JSX.Element => ( -) +); diff --git a/site/src/components/Icons/MarkdownIcon.tsx b/site/src/components/Icons/MarkdownIcon.tsx index 52b0793c97..4db8c543b6 100644 --- a/site/src/components/Icons/MarkdownIcon.tsx +++ b/site/src/components/Icons/MarkdownIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const MarkdownIcon = (props: SvgIconProps): JSX.Element => ( @@ -18,4 +18,4 @@ export const MarkdownIcon = (props: SvgIconProps): JSX.Element => ( style={{ stroke: "#755838" }} /> -) +); diff --git a/site/src/components/Icons/RocketIcon.tsx b/site/src/components/Icons/RocketIcon.tsx index 36dfd11c00..796a59fd94 100644 --- a/site/src/components/Icons/RocketIcon.tsx +++ b/site/src/components/Icons/RocketIcon.tsx @@ -1,7 +1,7 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const RocketIcon = (props: SvgIconProps) => ( -) +); diff --git a/site/src/components/Icons/TerminalIcon.tsx b/site/src/components/Icons/TerminalIcon.tsx index 60090c3032..b4915050c8 100644 --- a/site/src/components/Icons/TerminalIcon.tsx +++ b/site/src/components/Icons/TerminalIcon.tsx @@ -1,7 +1,7 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const TerminalIcon = (props: SvgIconProps) => ( -) +); diff --git a/site/src/components/Icons/TerraformIcon.tsx b/site/src/components/Icons/TerraformIcon.tsx index fe98d09c7a..4fe2c82653 100644 --- a/site/src/components/Icons/TerraformIcon.tsx +++ b/site/src/components/Icons/TerraformIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const TerraformIcon = (props: SvgIconProps): JSX.Element => ( @@ -19,4 +19,4 @@ export const TerraformIcon = (props: SvgIconProps): JSX.Element => ( style={{ fill: "#813cf3" }} /> -) +); diff --git a/site/src/components/Icons/UsersOutlinedIcon.tsx b/site/src/components/Icons/UsersOutlinedIcon.tsx index aa689b4dc5..13240db495 100644 --- a/site/src/components/Icons/UsersOutlinedIcon.tsx +++ b/site/src/components/Icons/UsersOutlinedIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const UsersOutlinedIcon = (props: SvgIconProps) => ( @@ -27,4 +27,4 @@ export const UsersOutlinedIcon = (props: SvgIconProps) => ( /> -) +); diff --git a/site/src/components/Icons/VSCodeIcon.tsx b/site/src/components/Icons/VSCodeIcon.tsx index cb7200ca68..7e210c5d01 100644 --- a/site/src/components/Icons/VSCodeIcon.tsx +++ b/site/src/components/Icons/VSCodeIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const VSCodeIcon = (props: SvgIconProps) => ( @@ -131,4 +131,4 @@ export const VSCodeIcon = (props: SvgIconProps) => ( -) +); diff --git a/site/src/components/Icons/VSCodeInsidersIcon.tsx b/site/src/components/Icons/VSCodeInsidersIcon.tsx index 0ff3f95f40..969b69de03 100644 --- a/site/src/components/Icons/VSCodeInsidersIcon.tsx +++ b/site/src/components/Icons/VSCodeInsidersIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon" +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; export const VSCodeInsidersIcon = (props: SvgIconProps) => ( @@ -123,4 +123,4 @@ export const VSCodeInsidersIcon = (props: SvgIconProps) => ( -) +); diff --git a/site/src/components/Loader/FullScreenLoader.tsx b/site/src/components/Loader/FullScreenLoader.tsx index 9774f1bb30..a1c91a4507 100644 --- a/site/src/components/Loader/FullScreenLoader.tsx +++ b/site/src/components/Loader/FullScreenLoader.tsx @@ -1,6 +1,6 @@ -import CircularProgress from "@mui/material/CircularProgress" -import { makeStyles } from "@mui/styles" -import { FC } from "react" +import CircularProgress from "@mui/material/CircularProgress"; +import { makeStyles } from "@mui/styles"; +import { FC } from "react"; export const useStyles = makeStyles((theme) => ({ root: { @@ -14,14 +14,14 @@ export const useStyles = makeStyles((theme) => ({ alignItems: "center", background: theme.palette.background.default, }, -})) +})); export const FullScreenLoader: FC = () => { - const styles = useStyles() + const styles = useStyles(); return (
- ) -} + ); +}; diff --git a/site/src/components/Loader/Loader.tsx b/site/src/components/Loader/Loader.tsx index 65cc9d474d..0a5f487f33 100644 --- a/site/src/components/Loader/Loader.tsx +++ b/site/src/components/Loader/Loader.tsx @@ -1,6 +1,6 @@ -import Box, { BoxProps } from "@mui/material/Box" -import CircularProgress from "@mui/material/CircularProgress" -import { FC } from "react" +import Box, { BoxProps } from "@mui/material/Box"; +import CircularProgress from "@mui/material/CircularProgress"; +import { FC } from "react"; export const Loader: FC<{ size?: number } & BoxProps> = ({ size = 26, @@ -18,5 +18,5 @@ export const Loader: FC<{ size?: number } & BoxProps> = ({ > - ) -} + ); +}; diff --git a/site/src/components/LoadingButton/LoadingButton.stories.tsx b/site/src/components/LoadingButton/LoadingButton.stories.tsx index 30c43745c2..bb012cc71e 100644 --- a/site/src/components/LoadingButton/LoadingButton.stories.tsx +++ b/site/src/components/LoadingButton/LoadingButton.stories.tsx @@ -1,5 +1,5 @@ -import { Story } from "@storybook/react" -import { LoadingButton, LoadingButtonProps } from "./LoadingButton" +import { Story } from "@storybook/react"; +import { LoadingButton, LoadingButtonProps } from "./LoadingButton"; export default { title: "components/LoadingButton", @@ -11,18 +11,18 @@ export default { args: { children: "Create workspace", }, -} +}; const Template: Story = (args) => ( -) +); -export const Loading = Template.bind({}) +export const Loading = Template.bind({}); Loading.args = { loading: true, -} +}; -export const NotLoading = Template.bind({}) +export const NotLoading = Template.bind({}); NotLoading.args = { loading: false, -} +}; diff --git a/site/src/components/LoadingButton/LoadingButton.tsx b/site/src/components/LoadingButton/LoadingButton.tsx index 5270df37c4..080e357b14 100644 --- a/site/src/components/LoadingButton/LoadingButton.tsx +++ b/site/src/components/LoadingButton/LoadingButton.tsx @@ -1,9 +1,9 @@ -import { forwardRef } from "react" +import { forwardRef } from "react"; import MuiLoadingButton, { LoadingButtonProps as MuiLoadingButtonProps, -} from "@mui/lab/LoadingButton" +} from "@mui/lab/LoadingButton"; -export type LoadingButtonProps = MuiLoadingButtonProps +export type LoadingButtonProps = MuiLoadingButtonProps; export const LoadingButton = forwardRef< HTMLButtonElement, @@ -21,5 +21,5 @@ export const LoadingButton = forwardRef< {buttonProps.loading && loadingIndicator ? loadingIndicator : children} - ) -}) + ); +}); diff --git a/site/src/components/Margins/Margins.stories.tsx b/site/src/components/Margins/Margins.stories.tsx index be798fa22d..b7e43353f9 100644 --- a/site/src/components/Margins/Margins.stories.tsx +++ b/site/src/components/Margins/Margins.stories.tsx @@ -1,10 +1,10 @@ -import { ComponentMeta, Story } from "@storybook/react" -import { Margins } from "./Margins" +import { ComponentMeta, Story } from "@storybook/react"; +import { Margins } from "./Margins"; export default { title: "components/Margins", component: Margins, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( @@ -12,6 +12,6 @@ const Template: Story = (args) => ( Here is some content that will not get too wide! -) +); -export const Example = Template.bind({}) +export const Example = Template.bind({}); diff --git a/site/src/components/Margins/Margins.tsx b/site/src/components/Margins/Margins.tsx index 0e3af4db94..867b81a1d8 100644 --- a/site/src/components/Margins/Margins.tsx +++ b/site/src/components/Margins/Margins.tsx @@ -1,19 +1,19 @@ -import { makeStyles } from "@mui/styles" -import { FC } from "react" -import { combineClasses } from "utils/combineClasses" +import { makeStyles } from "@mui/styles"; +import { FC } from "react"; +import { combineClasses } from "utils/combineClasses"; import { containerWidth, containerWidthMedium, sidePadding, -} from "../../theme/constants" +} from "../../theme/constants"; -type Size = "regular" | "medium" | "small" +type Size = "regular" | "medium" | "small"; const widthBySize: Record = { regular: containerWidth, medium: containerWidthMedium, small: containerWidth / 3, -} +}; const useStyles = makeStyles(() => ({ margins: { @@ -22,17 +22,17 @@ const useStyles = makeStyles(() => ({ padding: `0 ${sidePadding}px`, width: "100%", }, -})) +})); export const Margins: FC = ({ size = "regular", ...divProps }) => { - const styles = useStyles({ maxWidth: widthBySize[size] }) + const styles = useStyles({ maxWidth: widthBySize[size] }); return (
- ) -} + ); +}; diff --git a/site/src/components/Markdown/Markdown.stories.tsx b/site/src/components/Markdown/Markdown.stories.tsx index 5319947c67..f0e5ccaeff 100644 --- a/site/src/components/Markdown/Markdown.stories.tsx +++ b/site/src/components/Markdown/Markdown.stories.tsx @@ -1,16 +1,16 @@ -import { ComponentMeta, Story } from "@storybook/react" -import { Markdown, MarkdownProps } from "./Markdown" +import { ComponentMeta, Story } from "@storybook/react"; +import { Markdown, MarkdownProps } from "./Markdown"; export default { title: "components/Markdown", component: Markdown, -} as ComponentMeta +} as ComponentMeta; const Template: Story = ({ children }) => ( {children} -) +); -export const WithCode = Template.bind({}) +export const WithCode = Template.bind({}); WithCode.args = { children: ` ## Required permissions / policy @@ -64,12 +64,12 @@ WithCode.args = { ] } \`\`\``, -} +}; -export const WithTable = Template.bind({}) +export const WithTable = Template.bind({}); WithTable.args = { children: ` | heading | b | c | d | | - | :- | -: | :-: | | cell 1 | cell 2 | 3 | 4 | `, -} +}; diff --git a/site/src/components/Markdown/Markdown.tsx b/site/src/components/Markdown/Markdown.tsx index 0573901802..32b16344fd 100644 --- a/site/src/components/Markdown/Markdown.tsx +++ b/site/src/components/Markdown/Markdown.tsx @@ -1,28 +1,28 @@ -import Link from "@mui/material/Link" -import { makeStyles } from "@mui/styles" -import Table from "@mui/material/Table" -import TableBody from "@mui/material/TableBody" -import TableCell from "@mui/material/TableCell" -import TableContainer from "@mui/material/TableContainer" -import TableHead from "@mui/material/TableHead" -import TableRow from "@mui/material/TableRow" -import { FC, memo } from "react" -import ReactMarkdown from "react-markdown" -import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" -import gfm from "remark-gfm" -import { colors } from "theme/colors" -import { darcula } from "react-syntax-highlighter/dist/cjs/styles/prism" -import { combineClasses } from "utils/combineClasses" +import Link from "@mui/material/Link"; +import { makeStyles } from "@mui/styles"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import { FC, memo } from "react"; +import ReactMarkdown from "react-markdown"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import gfm from "remark-gfm"; +import { colors } from "theme/colors"; +import { darcula } from "react-syntax-highlighter/dist/cjs/styles/prism"; +import { combineClasses } from "utils/combineClasses"; export interface MarkdownProps { - children: string + children: string; } export const Markdown: FC<{ children: string; className?: string }> = ({ children, className, }) => { - const styles = useStyles() + const styles = useStyles(); return ( = ({ ), pre: ({ node, children }) => { - const firstChild = node.children[0] + const firstChild = node.children[0]; // When pre is wrapping a code, the SyntaxHighlighter is already going // to wrap it with a pre so we don't need it if (firstChild.type === "element" && firstChild.tagName === "code") { - return <>{children} + return <>{children}; } - return
{children}
+ return
{children}
; }, code: ({ node, inline, className, children, style, ...props }) => { - const match = /language-(\w+)/.exec(className || "") + const match = /language-(\w+)/.exec(className || ""); return !inline && match ? ( = ({ {children} - ) + ); }, table: ({ children }) => { @@ -73,36 +73,36 @@ export const Markdown: FC<{ children: string; className?: string }> = ({ {children}
- ) + ); }, tr: ({ children }) => { - return {children} + return {children}; }, thead: ({ children }) => { - return {children} + return {children}; }, tbody: ({ children }) => { - return {children} + return {children}; }, td: ({ children }) => { - return {children} + return {children}; }, th: ({ children }) => { - return {children} + return {children}; }, }} > {children}
- ) -} + ); +}; -export const MemoizedMarkdown = memo(Markdown) +export const MemoizedMarkdown = memo(Markdown); const useStyles = makeStyles((theme) => ({ markdown: { @@ -167,4 +167,4 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.primary, fontSize: 14, }, -})) +})); diff --git a/site/src/components/PageHeader/FullWidthPageHeader.tsx b/site/src/components/PageHeader/FullWidthPageHeader.tsx index ced199ee8d..5803c05ee1 100644 --- a/site/src/components/PageHeader/FullWidthPageHeader.tsx +++ b/site/src/components/PageHeader/FullWidthPageHeader.tsx @@ -1,11 +1,11 @@ -import { makeStyles } from "@mui/styles" -import { FC, PropsWithChildren } from "react" -import { combineClasses } from "utils/combineClasses" +import { makeStyles } from "@mui/styles"; +import { FC, PropsWithChildren } from "react"; +import { combineClasses } from "utils/combineClasses"; export const FullWidthPageHeader: FC< PropsWithChildren & { sticky?: boolean } > = ({ children, sticky = true }) => { - const styles = useStyles() + const styles = useStyles(); return (
{children}
- ) -} + ); +}; export const PageHeaderActions: FC = ({ children }) => { - const styles = useStyles() - return
{children}
-} + const styles = useStyles(); + return
{children}
; +}; export const PageHeaderTitle: FC = ({ children }) => { - const styles = useStyles() - return

{children}

-} + const styles = useStyles(); + return

{children}

; +}; export const PageHeaderSubtitle: FC = ({ children }) => { - const styles = useStyles() - return {children} -} + const styles = useStyles(); + return {children}; +}; const useStyles = makeStyles((theme) => ({ header: { @@ -74,4 +74,4 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.secondary, display: "block", }, -})) +})); diff --git a/site/src/components/PageHeader/PageHeader.stories.tsx b/site/src/components/PageHeader/PageHeader.stories.tsx index a070eab543..780ff99e3b 100644 --- a/site/src/components/PageHeader/PageHeader.stories.tsx +++ b/site/src/components/PageHeader/PageHeader.stories.tsx @@ -1,18 +1,18 @@ -import { ComponentMeta, Story } from "@storybook/react" -import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "./PageHeader" +import { ComponentMeta, Story } from "@storybook/react"; +import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "./PageHeader"; export default { title: "components/PageHeader", component: PageHeader, -} as ComponentMeta +} as ComponentMeta; const WithTitleTemplate: Story = () => ( Templates -) +); -export const WithTitle = WithTitleTemplate.bind({}) +export const WithTitle = WithTitleTemplate.bind({}); const WithSubtitleTemplate: Story = () => ( @@ -21,6 +21,6 @@ const WithSubtitleTemplate: Story = () => ( Create a new workspace from a Template -) +); -export const WithSubtitle = WithSubtitleTemplate.bind({}) +export const WithSubtitle = WithSubtitleTemplate.bind({}); diff --git a/site/src/components/PageHeader/PageHeader.tsx b/site/src/components/PageHeader/PageHeader.tsx index 0944828fdf..b24ccb7b05 100644 --- a/site/src/components/PageHeader/PageHeader.tsx +++ b/site/src/components/PageHeader/PageHeader.tsx @@ -1,11 +1,11 @@ -import { makeStyles } from "@mui/styles" -import { PropsWithChildren, FC } from "react" -import { combineClasses } from "../../utils/combineClasses" -import { Stack } from "../Stack/Stack" +import { makeStyles } from "@mui/styles"; +import { PropsWithChildren, FC } from "react"; +import { combineClasses } from "../../utils/combineClasses"; +import { Stack } from "../Stack/Stack"; export interface PageHeaderProps { - actions?: JSX.Element - className?: string + actions?: JSX.Element; + className?: string; } export const PageHeader: FC> = ({ @@ -13,7 +13,7 @@ export const PageHeader: FC> = ({ actions, className, }) => { - const styles = useStyles({}) + const styles = useStyles({}); return (
> = ({ )}
- ) -} + ); +}; export const PageHeaderTitle: FC> = ({ children, }) => { - const styles = useStyles({}) + const styles = useStyles({}); - return

{children}

-} + return

{children}

; +}; export const PageHeaderSubtitle: FC< PropsWithChildren<{ condensed?: boolean }> > = ({ children, condensed }) => { const styles = useStyles({ condensed, - }) + }); - return

{children}

-} + return

{children}

; +}; export const PageHeaderCaption: FC = ({ children }) => { - const styles = useStyles({}) - return {children} -} + const styles = useStyles({}); + return {children}; +}; const useStyles = makeStyles((theme) => ({ root: { @@ -104,4 +104,4 @@ const useStyles = makeStyles((theme) => ({ textTransform: "uppercase", letterSpacing: "0.1em", }, -})) +})); diff --git a/site/src/components/PaginationWidget/PageButton.tsx b/site/src/components/PaginationWidget/PageButton.tsx index ade11c497d..b81167b136 100644 --- a/site/src/components/PaginationWidget/PageButton.tsx +++ b/site/src/components/PaginationWidget/PageButton.tsx @@ -1,13 +1,13 @@ -import Button from "@mui/material/Button" -import { makeStyles } from "@mui/styles" +import Button from "@mui/material/Button"; +import { makeStyles } from "@mui/styles"; interface PageButtonProps { - activePage?: number - page?: number - placeholder?: string - numPages?: number - onPageClick?: (page: number) => void - disabled?: boolean + activePage?: number; + page?: number; + placeholder?: string; + numPages?: number; + onPageClick?: (page: number) => void; + disabled?: boolean; } export const PageButton = ({ @@ -18,7 +18,7 @@ export const PageButton = ({ onPageClick, disabled = false, }: PageButtonProps): JSX.Element => { - const styles = useStyles() + const styles = useStyles(); return ( - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ pageButton: { @@ -49,4 +49,4 @@ const useStyles = makeStyles((theme) => ({ borderColor: `${theme.palette.info.main}`, backgroundColor: `${theme.palette.info.dark}`, }, -})) +})); diff --git a/site/src/components/PaginationWidget/PaginationWidget.stories.tsx b/site/src/components/PaginationWidget/PaginationWidget.stories.tsx index 8cdec1dc6c..4118d459e5 100644 --- a/site/src/components/PaginationWidget/PaginationWidget.stories.tsx +++ b/site/src/components/PaginationWidget/PaginationWidget.stories.tsx @@ -1,6 +1,6 @@ -import { Story } from "@storybook/react" -import { PaginationWidget, PaginationWidgetProps } from "./PaginationWidget" -import { createPaginationRef } from "./utils" +import { Story } from "@storybook/react"; +import { PaginationWidget, PaginationWidgetProps } from "./PaginationWidget"; +import { createPaginationRef } from "./utils"; export default { title: "components/PaginationWidget", @@ -11,30 +11,30 @@ export default { paginationRef: createPaginationRef({ page: 1, limit: 12 }), numRecords: 200, }, -} +}; const Template: Story = ( args: PaginationWidgetProps, -) => +) => ; -export const LessThan8Pages = Template.bind({}) +export const LessThan8Pages = Template.bind({}); LessThan8Pages.args = { numRecords: 84, -} +}; -export const MoreThan8Pages = Template.bind({}) +export const MoreThan8Pages = Template.bind({}); -export const MoreThan7PagesWithActivePageCloseToStart = Template.bind({}) +export const MoreThan7PagesWithActivePageCloseToStart = Template.bind({}); MoreThan7PagesWithActivePageCloseToStart.args = { paginationRef: createPaginationRef({ page: 2, limit: 12 }), -} +}; -export const MoreThan7PagesWithActivePageFarFromBoundaries = Template.bind({}) +export const MoreThan7PagesWithActivePageFarFromBoundaries = Template.bind({}); MoreThan7PagesWithActivePageFarFromBoundaries.args = { paginationRef: createPaginationRef({ page: 4, limit: 12 }), -} +}; -export const MoreThan7PagesWithActivePageCloseToEnd = Template.bind({}) +export const MoreThan7PagesWithActivePageCloseToEnd = Template.bind({}); MoreThan7PagesWithActivePageCloseToEnd.args = { paginationRef: createPaginationRef({ page: 17, limit: 12 }), -} +}; diff --git a/site/src/components/PaginationWidget/PaginationWidget.test.tsx b/site/src/components/PaginationWidget/PaginationWidget.test.tsx index d98a07a2b5..d9e15d9e66 100644 --- a/site/src/components/PaginationWidget/PaginationWidget.test.tsx +++ b/site/src/components/PaginationWidget/PaginationWidget.test.tsx @@ -1,7 +1,7 @@ -import { screen } from "@testing-library/react" -import { render } from "../../testHelpers/renderHelpers" -import { PaginationWidget } from "./PaginationWidget" -import { createPaginationRef } from "./utils" +import { screen } from "@testing-library/react"; +import { render } from "../../testHelpers/renderHelpers"; +import { PaginationWidget } from "./PaginationWidget"; +import { createPaginationRef } from "./utils"; describe("PaginatedList", () => { it("displays an accessible previous and next button", () => { @@ -12,11 +12,11 @@ describe("PaginatedList", () => { paginationRef={createPaginationRef({ page: 2, limit: 12 })} numRecords={200} />, - ) + ); - expect(screen.getByRole("button", { name: "Previous page" })).toBeEnabled() - expect(screen.getByRole("button", { name: "Next page" })).toBeEnabled() - }) + expect(screen.getByRole("button", { name: "Previous page" })).toBeEnabled(); + expect(screen.getByRole("button", { name: "Next page" })).toBeEnabled(); + }); it("displays the expected number of pages with one ellipsis tile", () => { const { container } = render( @@ -26,13 +26,13 @@ describe("PaginatedList", () => { numRecords={200} paginationRef={createPaginationRef({ page: 1, limit: 12 })} />, - ) + ); // 7 total spaces. 6 are page numbers, one is ellipsis expect( container.querySelectorAll(`button[name="Page button"]`), - ).toHaveLength(6) - }) + ).toHaveLength(6); + }); it("displays the expected number of pages with two ellipsis tiles", () => { const { container } = render( @@ -42,13 +42,13 @@ describe("PaginatedList", () => { numRecords={200} paginationRef={createPaginationRef({ page: 6, limit: 12 })} />, - ) + ); // 7 total spaces. 2 sets of ellipsis on either side of the active page expect( container.querySelectorAll(`button[name="Page button"]`), - ).toHaveLength(5) - }) + ).toHaveLength(5); + }); it("disables the previous button on the first page", () => { render( @@ -56,10 +56,10 @@ describe("PaginatedList", () => { numRecords={100} paginationRef={createPaginationRef({ page: 1, limit: 25 })} />, - ) - const prevButton = screen.getByLabelText("Previous page") - expect(prevButton).toBeDisabled() - }) + ); + const prevButton = screen.getByLabelText("Previous page"); + expect(prevButton).toBeDisabled(); + }); it("disables the next button on the last page", () => { render( @@ -67,8 +67,8 @@ describe("PaginatedList", () => { numRecords={100} paginationRef={createPaginationRef({ page: 4, limit: 25 })} />, - ) - const nextButton = screen.getByLabelText("Next page") - expect(nextButton).toBeDisabled() - }) -}) + ); + const nextButton = screen.getByLabelText("Next page"); + expect(nextButton).toBeDisabled(); + }); +}); diff --git a/site/src/components/PaginationWidget/PaginationWidget.tsx b/site/src/components/PaginationWidget/PaginationWidget.tsx index 221e3deb22..08a40412a2 100644 --- a/site/src/components/PaginationWidget/PaginationWidget.tsx +++ b/site/src/components/PaginationWidget/PaginationWidget.tsx @@ -1,23 +1,23 @@ -import Button from "@mui/material/Button" -import { makeStyles, useTheme } from "@mui/styles" -import useMediaQuery from "@mui/material/useMediaQuery" -import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft" -import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight" -import { useActor } from "@xstate/react" -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { Maybe } from "components/Conditionals/Maybe" -import { CSSProperties } from "react" -import { PaginationMachineRef } from "xServices/pagination/paginationXService" -import { PageButton } from "./PageButton" -import { buildPagedList } from "./utils" +import Button from "@mui/material/Button"; +import { makeStyles, useTheme } from "@mui/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"; +import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; +import { useActor } from "@xstate/react"; +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { Maybe } from "components/Conditionals/Maybe"; +import { CSSProperties } from "react"; +import { PaginationMachineRef } from "xServices/pagination/paginationXService"; +import { PageButton } from "./PageButton"; +import { buildPagedList } from "./utils"; export type PaginationWidgetProps = { - prevLabel?: string - nextLabel?: string - numRecords?: number - containerStyle?: CSSProperties - paginationRef: PaginationMachineRef -} + prevLabel?: string; + nextLabel?: string; + numRecords?: number; + containerStyle?: CSSProperties; + paginationRef: PaginationMachineRef; +}; export const PaginationWidget = ({ prevLabel = "", @@ -26,19 +26,19 @@ export const PaginationWidget = ({ containerStyle, paginationRef, }: PaginationWidgetProps): JSX.Element | null => { - const theme = useTheme() - const isMobile = useMediaQuery(theme.breakpoints.down("md")) - const styles = useStyles() - const [paginationState, send] = useActor(paginationRef) + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const styles = useStyles(); + const [paginationState, send] = useActor(paginationRef); - const currentPage = paginationState.context.page - const numRecordsPerPage = paginationState.context.limit + const currentPage = paginationState.context.page; + const numRecordsPerPage = paginationState.context.limit; - const numPages = numRecords ? Math.ceil(numRecords / numRecordsPerPage) : 0 - const firstPageActive = currentPage === 1 && numPages !== 0 - const lastPageActive = currentPage === numPages && numPages !== 0 + const numPages = numRecords ? Math.ceil(numRecords / numRecordsPerPage) : 0; + const firstPageActive = currentPage === 1 && numPages !== 0; + const lastPageActive = currentPage === numPages && numPages !== 0; // if beyond page 1, show pagination widget even if there's only one true page, so user can navigate back - const showWidget = numPages > 1 || currentPage > 1 + const showWidget = numPages > 1 || currentPage > 1; return ( @@ -91,8 +91,8 @@ export const PaginationWidget = ({
- ) -} + ); +}; const useStyles = makeStyles((theme) => ({ defaultContainerStyles: { @@ -106,4 +106,4 @@ const useStyles = makeStyles((theme) => ({ prevLabelStyles: { marginRight: theme.spacing(0.5), }, -})) +})); diff --git a/site/src/components/PaginationWidget/PaginationWidgetBase.tsx b/site/src/components/PaginationWidget/PaginationWidgetBase.tsx index 594150d811..3bf482f608 100644 --- a/site/src/components/PaginationWidget/PaginationWidgetBase.tsx +++ b/site/src/components/PaginationWidget/PaginationWidgetBase.tsx @@ -1,18 +1,18 @@ -import Button from "@mui/material/Button" -import { makeStyles, useTheme } from "@mui/styles" -import useMediaQuery from "@mui/material/useMediaQuery" -import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft" -import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight" -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { PageButton } from "./PageButton" -import { buildPagedList } from "./utils" +import Button from "@mui/material/Button"; +import { makeStyles, useTheme } from "@mui/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"; +import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { PageButton } from "./PageButton"; +import { buildPagedList } from "./utils"; export type PaginationWidgetBaseProps = { - count: number - page: number - limit: number - onChange: (page: number) => void -} + count: number; + page: number; + limit: number; + onChange: (page: number) => void; +}; export const PaginationWidgetBase = ({ count, @@ -20,15 +20,15 @@ export const PaginationWidgetBase = ({ limit, onChange, }: PaginationWidgetBaseProps): JSX.Element | null => { - const theme = useTheme() - const isMobile = useMediaQuery(theme.breakpoints.down("md")) - const styles = useStyles() - const numPages = Math.ceil(count / limit) - const isFirstPage = page === 0 - const isLastPage = page === numPages - 1 + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + const styles = useStyles(); + const numPages = Math.ceil(count / limit); + const isFirstPage = page === 0; + const isLastPage = page === numPages - 1; if (numPages < 2) { - return null + return null; } return ( @@ -39,7 +39,7 @@ export const PaginationWidgetBase = ({ disabled={isFirstPage} onClick={() => { if (!isFirstPage) { - onChange(page - 1) + onChange(page - 1); } }} > @@ -59,7 +59,7 @@ export const PaginationWidgetBase = ({ placeholder="..." disabled /> - ) + ); } return ( @@ -70,7 +70,7 @@ export const PaginationWidgetBase = ({ numPages={numPages} onPageClick={() => onChange(pageItem)} /> - ) + ); })} @@ -79,15 +79,15 @@ export const PaginationWidgetBase = ({ disabled={isLastPage} onClick={() => { if (!isLastPage) { - onChange(page + 1) + onChange(page + 1); } }} > - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ defaultContainerStyles: { @@ -101,4 +101,4 @@ const useStyles = makeStyles((theme) => ({ prevLabelStyles: { marginRight: theme.spacing(0.5), }, -})) +})); diff --git a/site/src/components/PaginationWidget/utils.test.ts b/site/src/components/PaginationWidget/utils.test.ts index e65d1b0b0c..eb3942e220 100644 --- a/site/src/components/PaginationWidget/utils.test.ts +++ b/site/src/components/PaginationWidget/utils.test.ts @@ -1,11 +1,11 @@ -import { buildPagedList, getOffset } from "./utils" +import { buildPagedList, getOffset } from "./utils"; describe("unit/PaginationWidget", () => { describe("buildPagedList", () => { it.each<{ - numPages: number - activePage: number - expected: (string | number)[] + numPages: number; + activePage: number; + expected: (string | number)[]; }>([ { numPages: 7, activePage: 1, expected: [1, 2, 3, 4, 5, 6, 7] }, { numPages: 17, activePage: 1, expected: [1, 2, 3, 4, 5, "right", 17] }, @@ -22,21 +22,21 @@ describe("unit/PaginationWidget", () => { ])( `buildPagedList($numPages, $activePage)`, ({ numPages, activePage, expected }) => { - expect(buildPagedList(numPages, activePage)).toEqual(expected) + expect(buildPagedList(numPages, activePage)).toEqual(expected); }, - ) - }) -}) + ); + }); +}); describe("getOffset", () => { it("returns 0 on page 1", () => { - const page = 1 - const limit = 10 - expect(getOffset(page, limit)).toEqual(0) - }) + const page = 1; + const limit = 10; + expect(getOffset(page, limit)).toEqual(0); + }); it("returns the limit on page 2", () => { - const page = 2 - const limit = 10 - expect(getOffset(page, limit)).toEqual(limit) - }) -}) + const page = 2; + const limit = 10; + expect(getOffset(page, limit)).toEqual(limit); + }); +}); diff --git a/site/src/components/PaginationWidget/utils.ts b/site/src/components/PaginationWidget/utils.ts index db25da9b15..1598ce17de 100644 --- a/site/src/components/PaginationWidget/utils.ts +++ b/site/src/components/PaginationWidget/utils.ts @@ -2,8 +2,8 @@ import { PaginationContext, paginationMachine, PaginationMachineRef, -} from "xServices/pagination/paginationXService" -import { spawn } from "xstate" +} from "xServices/pagination/paginationXService"; +import { spawn } from "xstate"; /** * Generates a ranged array with an option to step over values. @@ -11,17 +11,17 @@ import { spawn } from "xstate" * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#sequence_generator_range */ const range = (start: number, stop: number, step = 1) => - Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step) + Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step); -export const DEFAULT_RECORDS_PER_PAGE = 25 +export const DEFAULT_RECORDS_PER_PAGE = 25; // Number of pages to the left or right of the current page selection. -const PAGE_NEIGHBORS = 1 +const PAGE_NEIGHBORS = 1; // Number of pages displayed for cases where there are multiple ellipsis showing. This can be // thought of as the minimum number of page numbers to display when multiple ellipsis are showing. -const PAGES_TO_DISPLAY = PAGE_NEIGHBORS * 2 + 3 +const PAGES_TO_DISPLAY = PAGE_NEIGHBORS * 2 + 3; // Total page blocks(page numbers or ellipsis) displayed, including the maximum number of ellipsis (2). // This gives us maximum number of 7 page blocks to be displayed when the page neighbors value is 1. -const NUM_PAGE_BLOCKS = PAGES_TO_DISPLAY + 2 +const NUM_PAGE_BLOCKS = PAGES_TO_DISPLAY + 2; /** * Builds a list of pages based on how many pages exist and where the user is in their navigation of those pages. @@ -32,61 +32,61 @@ export const buildPagedList = ( activePage: number, ): ("left" | "right" | number)[] => { if (numPages > NUM_PAGE_BLOCKS) { - let pages = [] - const leftBound = activePage - PAGE_NEIGHBORS - const rightBound = activePage + PAGE_NEIGHBORS - const beforeLastPage = numPages - 1 - const startPage = leftBound > 2 ? leftBound : 2 - const endPage = rightBound < beforeLastPage ? rightBound : beforeLastPage + let pages = []; + const leftBound = activePage - PAGE_NEIGHBORS; + const rightBound = activePage + PAGE_NEIGHBORS; + const beforeLastPage = numPages - 1; + const startPage = leftBound > 2 ? leftBound : 2; + const endPage = rightBound < beforeLastPage ? rightBound : beforeLastPage; - pages = range(startPage, endPage) + pages = range(startPage, endPage); - const singleSpillOffset = PAGES_TO_DISPLAY - pages.length - 1 - const hasLeftOverflow = startPage > 2 - const hasRightOverflow = endPage < beforeLastPage - const leftOverflowPage = "left" as const - const rightOverflowPage = "right" as const + const singleSpillOffset = PAGES_TO_DISPLAY - pages.length - 1; + const hasLeftOverflow = startPage > 2; + const hasRightOverflow = endPage < beforeLastPage; + const leftOverflowPage = "left" as const; + const rightOverflowPage = "right" as const; if (hasLeftOverflow && !hasRightOverflow) { - const extraPages = range(startPage - singleSpillOffset, startPage - 1) - pages = [leftOverflowPage, ...extraPages, ...pages] + const extraPages = range(startPage - singleSpillOffset, startPage - 1); + pages = [leftOverflowPage, ...extraPages, ...pages]; } else if (!hasLeftOverflow && hasRightOverflow) { - const extraPages = range(endPage + 1, endPage + singleSpillOffset) - pages = [...pages, ...extraPages, rightOverflowPage] + const extraPages = range(endPage + 1, endPage + singleSpillOffset); + pages = [...pages, ...extraPages, rightOverflowPage]; } else if (hasLeftOverflow && hasRightOverflow) { - pages = [leftOverflowPage, ...pages, rightOverflowPage] + pages = [leftOverflowPage, ...pages, rightOverflowPage]; } - return [1, ...pages, numPages] + return [1, ...pages, numPages]; } - return range(1, numPages) -} + return range(1, numPages); +}; const getInitialPage = (page: string | null): number => - page ? Number(page) : 1 + page ? Number(page) : 1; // pages count from 1 export const getOffset = (page: number, limit: number): number => - (page - 1) * limit + (page - 1) * limit; interface PaginationData { - offset: number - limit: number + offset: number; + limit: number; } export const getPaginationData = ( ref: PaginationMachineRef, ): PaginationData => { - const snapshot = ref.getSnapshot() + const snapshot = ref.getSnapshot(); if (snapshot) { - const { page, limit } = snapshot.context - const offset = getOffset(page, limit) - return { offset, limit } + const { page, limit } = snapshot.context; + const offset = getOffset(page, limit); + return { offset, limit }; } else { - throw new Error("No pagination data") + throw new Error("No pagination data"); } -} +}; export const getPaginationContext = ( searchParams: URLSearchParams, @@ -94,17 +94,17 @@ export const getPaginationContext = ( ): PaginationContext => ({ page: getInitialPage(searchParams.get("page")), limit, -}) +}); // for storybook export const createPaginationRef = ( context: PaginationContext, ): PaginationMachineRef => { - return spawn(paginationMachine.withContext(context)) -} + return spawn(paginationMachine.withContext(context)); +}; export const nonInitialPage = (searchParams: URLSearchParams): boolean => { - const page = searchParams.get("page") - const numberPage = page ? Number(page) : 1 - return numberPage > 1 -} + const page = searchParams.get("page"); + const numberPage = page ? Number(page) : 1; + return numberPage > 1; +}; diff --git a/site/src/components/Paywall/Paywall.tsx b/site/src/components/Paywall/Paywall.tsx index 42aab2e20b..190aa02c10 100644 --- a/site/src/components/Paywall/Paywall.tsx +++ b/site/src/components/Paywall/Paywall.tsx @@ -1,19 +1,19 @@ -import Box from "@mui/material/Box" -import Chip from "@mui/material/Chip" -import { makeStyles } from "@mui/styles" -import Typography from "@mui/material/Typography" -import { Stack } from "components/Stack/Stack" -import { FC, ReactNode } from "react" +import Box from "@mui/material/Box"; +import Chip from "@mui/material/Chip"; +import { makeStyles } from "@mui/styles"; +import Typography from "@mui/material/Typography"; +import { Stack } from "components/Stack/Stack"; +import { FC, ReactNode } from "react"; export interface PaywallProps { - message: string - description?: string | React.ReactNode - cta?: ReactNode + message: string; + description?: string | React.ReactNode; + cta?: ReactNode; } export const Paywall: FC> = (props) => { - const { message, description, cta } = props - const styles = useStyles() + const { message, description, cta } = props; + const styles = useStyles(); return ( @@ -42,8 +42,8 @@ export const Paywall: FC> = (props) => { {cta} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ root: { @@ -77,4 +77,4 @@ const useStyles = makeStyles((theme) => ({ border: `1px solid ${theme.palette.success.light}`, fontSize: 13, }, -})) +})); diff --git a/site/src/components/Pill/Pill.stories.tsx b/site/src/components/Pill/Pill.stories.tsx index 5358cb7aa0..92c68ba17f 100644 --- a/site/src/components/Pill/Pill.stories.tsx +++ b/site/src/components/Pill/Pill.stories.tsx @@ -1,57 +1,57 @@ -import { Story } from "@storybook/react" -import { Pill, PillProps } from "./Pill" +import { Story } from "@storybook/react"; +import { Pill, PillProps } from "./Pill"; export default { title: "components/Pill", component: Pill, -} +}; -const Template: Story = (args) => +const Template: Story = (args) => ; -export const Primary = Template.bind({}) +export const Primary = Template.bind({}); Primary.args = { text: "Primary", type: "primary", -} +}; -export const Secondary = Template.bind({}) +export const Secondary = Template.bind({}); Secondary.args = { text: "Secondary", type: "secondary", -} +}; -export const Success = Template.bind({}) +export const Success = Template.bind({}); Success.args = { text: "Success", type: "success", -} +}; -export const Info = Template.bind({}) +export const Info = Template.bind({}); Info.args = { text: "Information", type: "info", -} +}; -export const Warning = Template.bind({}) +export const Warning = Template.bind({}); Warning.args = { text: "Warning", type: "warning", -} +}; -export const Error = Template.bind({}) +export const Error = Template.bind({}); Error.args = { text: "Error", type: "error", -} +}; -export const Default = Template.bind({}) +export const Default = Template.bind({}); Default.args = { text: "Default", -} +}; -export const WarningLight = Template.bind({}) +export const WarningLight = Template.bind({}); WarningLight.args = { text: "Warning", type: "warning", lightBorder: true, -} +}; diff --git a/site/src/components/Pill/Pill.tsx b/site/src/components/Pill/Pill.tsx index 9632ba988c..f9c4d9c092 100644 --- a/site/src/components/Pill/Pill.tsx +++ b/site/src/components/Pill/Pill.tsx @@ -1,21 +1,21 @@ -import { PaletteColor, Theme } from "@mui/material/styles" -import { makeStyles } from "@mui/styles" -import { FC } from "react" -import { PaletteIndex } from "theme/theme" -import { combineClasses } from "utils/combineClasses" +import { PaletteColor, Theme } from "@mui/material/styles"; +import { makeStyles } from "@mui/styles"; +import { FC } from "react"; +import { PaletteIndex } from "theme/theme"; +import { combineClasses } from "utils/combineClasses"; export interface PillProps { - className?: string - icon?: React.ReactNode - text: string - type?: PaletteIndex - lightBorder?: boolean - title?: string + className?: string; + icon?: React.ReactNode; + text: string; + type?: PaletteIndex; + lightBorder?: boolean; + title?: string; } export const Pill: FC = (props) => { - const { className, icon, text = false, title } = props - const styles = useStyles(props) + const { className, icon, text = false, title } = props; + const styles = useStyles(props); return (
= (props) => { {icon &&
{icon}
} {text}
- ) -} + ); +}; const useStyles = makeStyles((theme) => ({ wrapper: { @@ -72,4 +72,4 @@ const useStyles = makeStyles((theme) => ({ height: theme.spacing(1.75), }, }, -})) +})); diff --git a/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx b/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx index 6a6b09e3d0..8886652ac2 100644 --- a/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx +++ b/site/src/components/ProxyStatusLatency/ProxyStatusLatency.tsx @@ -1,17 +1,17 @@ -import { useTheme } from "@mui/material/styles" -import HelpOutline from "@mui/icons-material/HelpOutline" -import Box from "@mui/material/Box" -import Tooltip from "@mui/material/Tooltip" -import { FC } from "react" -import { getLatencyColor } from "utils/latency" -import CircularProgress from "@mui/material/CircularProgress" +import { useTheme } from "@mui/material/styles"; +import HelpOutline from "@mui/icons-material/HelpOutline"; +import Box from "@mui/material/Box"; +import Tooltip from "@mui/material/Tooltip"; +import { FC } from "react"; +import { getLatencyColor } from "utils/latency"; +import CircularProgress from "@mui/material/CircularProgress"; export const ProxyStatusLatency: FC<{ - latency?: number - isLoading?: boolean + latency?: number; + isLoading?: boolean; }> = ({ latency, isLoading }) => { - const theme = useTheme() - const color = getLatencyColor(theme, latency) + const theme = useTheme(); + const color = getLatencyColor(theme, latency); if (isLoading) { return ( @@ -25,7 +25,7 @@ export const ProxyStatusLatency: FC<{ }} /> - ) + ); } if (!latency) { @@ -39,12 +39,12 @@ export const ProxyStatusLatency: FC<{ }} /> - ) + ); } return ( {latency.toFixed(0)}ms - ) -} + ); +}; diff --git a/site/src/components/RequireAuth/RequireAuth.test.tsx b/site/src/components/RequireAuth/RequireAuth.test.tsx index a6abeead64..29e18a6ace 100644 --- a/site/src/components/RequireAuth/RequireAuth.test.tsx +++ b/site/src/components/RequireAuth/RequireAuth.test.tsx @@ -1,22 +1,22 @@ -import { screen } from "@testing-library/react" -import { rest } from "msw" -import { renderWithAuth } from "testHelpers/renderHelpers" -import { server } from "testHelpers/server" +import { screen } from "@testing-library/react"; +import { rest } from "msw"; +import { renderWithAuth } from "testHelpers/renderHelpers"; +import { server } from "testHelpers/server"; describe("RequireAuth", () => { it("redirects to /setup if there is no first user", async () => { // appear logged out server.use( rest.get("/api/v2/users/me", (req, res, ctx) => { - return res(ctx.status(401), ctx.json({ message: "no user here" })) + return res(ctx.status(401), ctx.json({ message: "no user here" })); }), - ) + ); // No first user server.use( rest.get("/api/v2/users/first", async (req, res, ctx) => { - return res(ctx.status(404)) + return res(ctx.status(404)); }), - ) + ); renderWithAuth(

Test

, { nonAuthenticatedRoutes: [ @@ -25,8 +25,8 @@ describe("RequireAuth", () => { element:

Setup

, }, ], - }) + }); - await screen.findByText("Setup") - }) -}) + await screen.findByText("Setup"); + }); +}); diff --git a/site/src/components/RequireAuth/RequireAuth.tsx b/site/src/components/RequireAuth/RequireAuth.tsx index 5d237be195..227ae0aa5f 100644 --- a/site/src/components/RequireAuth/RequireAuth.tsx +++ b/site/src/components/RequireAuth/RequireAuth.tsx @@ -1,19 +1,19 @@ -import axios from "axios" -import { useAuth } from "components/AuthProvider/AuthProvider" -import { FC, useEffect } from "react" -import { Outlet, Navigate, useLocation } from "react-router-dom" -import { embedRedirect } from "../../utils/redirect" -import { FullScreenLoader } from "../Loader/FullScreenLoader" -import { DashboardProvider } from "components/Dashboard/DashboardProvider" -import { ProxyProvider } from "contexts/ProxyContext" +import axios from "axios"; +import { useAuth } from "components/AuthProvider/AuthProvider"; +import { FC, useEffect } from "react"; +import { Outlet, Navigate, useLocation } from "react-router-dom"; +import { embedRedirect } from "../../utils/redirect"; +import { FullScreenLoader } from "../Loader/FullScreenLoader"; +import { DashboardProvider } from "components/Dashboard/DashboardProvider"; +import { ProxyProvider } from "contexts/ProxyContext"; export const RequireAuth: FC = () => { - const [authState, authSend] = useAuth() - const location = useLocation() - const isHomePage = location.pathname === "/" + const [authState, authSend] = useAuth(); + const location = useLocation(); + const isHomePage = location.pathname === "/"; const navigateTo = isHomePage ? "/login" - : embedRedirect(`${location.pathname}${location.search}`) + : embedRedirect(`${location.pathname}${location.search}`); useEffect(() => { const interceptorHandle = axios.interceptors.response.use( @@ -23,28 +23,28 @@ export const RequireAuth: FC = () => { // If we encountered an authentication error, then our token is probably // invalid and we should update the auth state to reflect that. if (error.response.status === 401) { - authSend("SIGN_OUT") + authSend("SIGN_OUT"); } // Otherwise, pass the response through so that it can be displayed in the UI - return Promise.reject(error) + return Promise.reject(error); }, - ) + ); return () => { - axios.interceptors.response.eject(interceptorHandle) - } - }, [authSend]) + axios.interceptors.response.eject(interceptorHandle); + }; + }, [authSend]); if (authState.matches("signedOut")) { - return + return ; } else if (authState.matches("configuringTheFirstUser")) { - return + return ; } else if ( authState.matches("loadingInitialAuthData") || authState.matches("signingOut") ) { - return + return ; } else { // Authenticated pages have access to some contexts for knowing enabled experiments // and where to route workspace connections. @@ -54,6 +54,6 @@ export const RequireAuth: FC = () => { - ) + ); } -} +}; diff --git a/site/src/components/RequirePermission/RequirePermission.tsx b/site/src/components/RequirePermission/RequirePermission.tsx index d7bc0a1560..6a2694f5fd 100644 --- a/site/src/components/RequirePermission/RequirePermission.tsx +++ b/site/src/components/RequirePermission/RequirePermission.tsx @@ -1,9 +1,9 @@ -import { FC } from "react" -import { Navigate } from "react-router-dom" +import { FC } from "react"; +import { Navigate } from "react-router-dom"; export interface RequirePermissionProps { - children: JSX.Element - isFeatureVisible: boolean + children: JSX.Element; + isFeatureVisible: boolean; } /** @@ -14,8 +14,8 @@ export const RequirePermission: FC = ({ isFeatureVisible, }) => { if (!isFeatureVisible) { - return + return ; } else { - return children + return children; } -} +}; diff --git a/site/src/components/Resources/AgentButton.tsx b/site/src/components/Resources/AgentButton.tsx index 3b66c0aed5..1c0d346300 100644 --- a/site/src/components/Resources/AgentButton.tsx +++ b/site/src/components/Resources/AgentButton.tsx @@ -1,5 +1,5 @@ -import Button, { ButtonProps } from "@mui/material/Button" -import { FC, forwardRef } from "react" +import Button, { ButtonProps } from "@mui/material/Button"; +import { FC, forwardRef } from "react"; export const PrimaryAgentButton: FC = ({ className, @@ -23,12 +23,12 @@ export const PrimaryAgentButton: FC = ({ ...props.sx, }} /> - ) -} + ); +}; // eslint-disable-next-line react/display-name -- Name is inferred from variable name export const SecondaryAgentButton = forwardRef( ({ className, ...props }, ref) => { - return - ) + ); })} @@ -79,8 +79,8 @@ export const TemplateFiles: FC<{ language={languageByExtension[getExtension(selectedFilename)]} /> - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ tabs: { display: "flex", @@ -151,4 +151,4 @@ const useStyles = makeStyles((theme) => ({ prism: { borderRadius: 0, }, -})) +})); diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index e407dc46e8..1112c63621 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -1,20 +1,20 @@ -import { makeStyles } from "@mui/styles" -import { useOrganizationId } from "hooks/useOrganizationId" -import { createContext, FC, Suspense, useContext } from "react" -import { NavLink, Outlet, useNavigate, useParams } from "react-router-dom" -import { combineClasses } from "utils/combineClasses" -import { Margins } from "components/Margins/Margins" -import { Stack } from "components/Stack/Stack" -import { Loader } from "components/Loader/Loader" -import { TemplatePageHeader } from "./TemplatePageHeader" +import { makeStyles } from "@mui/styles"; +import { useOrganizationId } from "hooks/useOrganizationId"; +import { createContext, FC, Suspense, useContext } from "react"; +import { NavLink, Outlet, useNavigate, useParams } from "react-router-dom"; +import { combineClasses } from "utils/combineClasses"; +import { Margins } from "components/Margins/Margins"; +import { Stack } from "components/Stack/Stack"; +import { Loader } from "components/Loader/Loader"; +import { TemplatePageHeader } from "./TemplatePageHeader"; import { checkAuthorization, getTemplateByName, getTemplateVersion, -} from "api/api" -import { useQuery } from "@tanstack/react-query" -import { AuthorizationRequest } from "api/typesGenerated" -import { ErrorAlert } from "components/Alert/ErrorAlert" +} from "api/api"; +import { useQuery } from "@tanstack/react-query"; +import { AuthorizationRequest } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; const templatePermissions = ( templateId: string, @@ -26,63 +26,63 @@ const templatePermissions = ( }, action: "update", }, -}) +}); const fetchTemplate = async (orgId: string, templateName: string) => { - const template = await getTemplateByName(orgId, templateName) + const template = await getTemplateByName(orgId, templateName); const [activeVersion, permissions] = await Promise.all([ getTemplateVersion(template.active_version_id), checkAuthorization({ checks: templatePermissions(template.id), }), - ]) + ]); return { template, activeVersion, permissions, - } -} + }; +}; -type TemplateLayoutContextValue = Awaited> +type TemplateLayoutContextValue = Awaited>; const TemplateLayoutContext = createContext< TemplateLayoutContextValue | undefined ->(undefined) +>(undefined); export const useTemplateLayoutContext = (): TemplateLayoutContextValue => { - const context = useContext(TemplateLayoutContext) + const context = useContext(TemplateLayoutContext); if (!context) { throw new Error( "useTemplateLayoutContext only can be used inside of TemplateLayout", - ) + ); } - return context -} + return context; +}; export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ children = , }) => { - const navigate = useNavigate() - const styles = useStyles() - const orgId = useOrganizationId() - const { template: templateName } = useParams() as { template: string } + const navigate = useNavigate(); + const styles = useStyles(); + const orgId = useOrganizationId(); + const { template: templateName } = useParams() as { template: string }; const { data, error, isLoading } = useQuery({ queryKey: ["template", templateName], queryFn: () => fetchTemplate(orgId, templateName), - }) - const shouldShowInsights = data?.permissions?.canUpdateTemplate + }); + const shouldShowInsights = data?.permissions?.canUpdateTemplate; if (error) { return (
- ) + ); } if (isLoading || !data) { - return + return ; } return ( @@ -92,7 +92,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ activeVersion={data.activeVersion} permissions={data.permissions} onDeleteTemplate={() => { - navigate("/templates") + navigate("/templates"); }} /> @@ -181,8 +181,8 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ - ) -} + ); +}; export const useStyles = makeStyles((theme) => { return { @@ -220,5 +220,5 @@ export const useStyles = makeStyles((theme) => { position: "absolute", }, }, - } -}) + }; +}); diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx index 016f0cee66..124f11e3de 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx @@ -1,9 +1,9 @@ -import { ComponentMeta, Story } from "@storybook/react" -import { MockTemplate, MockTemplateVersion } from "testHelpers/entities" +import { ComponentMeta, Story } from "@storybook/react"; +import { MockTemplate, MockTemplateVersion } from "testHelpers/entities"; import { TemplatePageHeader, TemplatePageHeaderProps, -} from "./TemplatePageHeader" +} from "./TemplatePageHeader"; export default { title: "Components/TemplatePageHeader", @@ -15,18 +15,18 @@ export default { canUpdateTemplate: true, }, }, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) +); -export const CanUpdate = Template.bind({}) -CanUpdate.args = {} +export const CanUpdate = Template.bind({}); +CanUpdate.args = {}; -export const CanNotUpdate = Template.bind({}) +export const CanNotUpdate = Template.bind({}); CanNotUpdate.args = { permissions: { canUpdateTemplate: false, }, -} +}; diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.tsx index bf737da78e..beca9aecb1 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.tsx @@ -1,47 +1,47 @@ -import Button from "@mui/material/Button" -import AddIcon from "@mui/icons-material/AddOutlined" +import Button from "@mui/material/Button"; +import AddIcon from "@mui/icons-material/AddOutlined"; import { AuthorizationResponse, Template, TemplateVersion, -} from "api/typesGenerated" -import { Avatar } from "components/Avatar/Avatar" -import { Maybe } from "components/Conditionals/Maybe" -import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog" +} from "api/typesGenerated"; +import { Avatar } from "components/Avatar/Avatar"; +import { Maybe } from "components/Conditionals/Maybe"; +import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; import { PageHeader, PageHeaderTitle, PageHeaderSubtitle, -} from "components/PageHeader/PageHeader" -import { Stack } from "components/Stack/Stack" -import { FC, useRef, useState } from "react" -import { Link as RouterLink, useNavigate } from "react-router-dom" -import { useDeleteTemplate } from "./deleteTemplate" -import { Margins } from "components/Margins/Margins" -import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined" -import Menu from "@mui/material/Menu" -import MenuItem from "@mui/material/MenuItem" -import SettingsOutlined from "@mui/icons-material/SettingsOutlined" -import DeleteOutlined from "@mui/icons-material/DeleteOutlined" -import EditOutlined from "@mui/icons-material/EditOutlined" -import FileCopyOutlined from "@mui/icons-material/FileCopyOutlined" -import IconButton from "@mui/material/IconButton" +} from "components/PageHeader/PageHeader"; +import { Stack } from "components/Stack/Stack"; +import { FC, useRef, useState } from "react"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; +import { useDeleteTemplate } from "./deleteTemplate"; +import { Margins } from "components/Margins/Margins"; +import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import SettingsOutlined from "@mui/icons-material/SettingsOutlined"; +import DeleteOutlined from "@mui/icons-material/DeleteOutlined"; +import EditOutlined from "@mui/icons-material/EditOutlined"; +import FileCopyOutlined from "@mui/icons-material/FileCopyOutlined"; +import IconButton from "@mui/material/IconButton"; const TemplateMenu: FC<{ - templateName: string - templateVersion: string - onDelete: () => void + templateName: string; + templateVersion: string; + onDelete: () => void; }> = ({ templateName, templateVersion, onDelete }) => { - const menuTriggerRef = useRef(null) - const [isMenuOpen, setIsMenuOpen] = useState(false) - const navigate = useNavigate() + const menuTriggerRef = useRef(null); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const navigate = useNavigate(); // Returns a function that will execute the action and close the menu const onMenuItemClick = (actionFn: () => void) => () => { - setIsMenuOpen(false) + setIsMenuOpen(false); - actionFn() - } + actionFn(); + }; return (
@@ -93,12 +93,12 @@ const TemplateMenu: FC<{
- ) -} + ); +}; const CreateWorkspaceButton: FC<{ - templateName: string - className?: string + templateName: string; + className?: string; }> = ({ templateName }) => ( -) +); export type TemplatePageHeaderProps = { - template: Template - activeVersion: TemplateVersion - permissions: AuthorizationResponse - onDeleteTemplate: () => void -} + template: Template; + activeVersion: TemplateVersion; + permissions: AuthorizationResponse; + onDeleteTemplate: () => void; +}; export const TemplatePageHeader: FC = ({ template, @@ -123,8 +123,8 @@ export const TemplatePageHeader: FC = ({ permissions, onDeleteTemplate, }) => { - const hasIcon = template.icon && template.icon !== "" - const deleteTemplate = useDeleteTemplate(template, onDeleteTemplate) + const hasIcon = template.icon && template.icon !== ""; + const deleteTemplate = useDeleteTemplate(template, onDeleteTemplate); return ( @@ -173,5 +173,5 @@ export const TemplatePageHeader: FC = ({ name={template.name} /> - ) -} + ); +}; diff --git a/site/src/components/TemplateLayout/deleteTemplate.test.ts b/site/src/components/TemplateLayout/deleteTemplate.test.ts index c573db4951..fc4a4076d2 100644 --- a/site/src/components/TemplateLayout/deleteTemplate.test.ts +++ b/site/src/components/TemplateLayout/deleteTemplate.test.ts @@ -1,48 +1,48 @@ -import { act, renderHook, waitFor } from "@testing-library/react" -import { MockTemplate } from "testHelpers/entities" -import { useDeleteTemplate } from "./deleteTemplate" -import * as API from "api/api" +import { act, renderHook, waitFor } from "@testing-library/react"; +import { MockTemplate } from "testHelpers/entities"; +import { useDeleteTemplate } from "./deleteTemplate"; +import * as API from "api/api"; test("delete dialog starts closed", () => { const { result } = renderHook(() => useDeleteTemplate(MockTemplate, jest.fn()), - ) - expect(result.current.isDeleteDialogOpen).toBeFalsy() -}) + ); + expect(result.current.isDeleteDialogOpen).toBeFalsy(); +}); test("confirm template deletion", async () => { - const onDeleteTemplate = jest.fn() + const onDeleteTemplate = jest.fn(); const { result } = renderHook(() => useDeleteTemplate(MockTemplate, onDeleteTemplate), - ) + ); //Open delete confirmation act(() => { - result.current.openDeleteConfirmation() - }) - expect(result.current.isDeleteDialogOpen).toBeTruthy() + result.current.openDeleteConfirmation(); + }); + expect(result.current.isDeleteDialogOpen).toBeTruthy(); // Confirm delete - jest.spyOn(API, "deleteTemplate") - await act(async () => result.current.confirmDelete()) - await waitFor(() => expect(API.deleteTemplate).toBeCalledTimes(1)) - expect(onDeleteTemplate).toBeCalledTimes(1) -}) + jest.spyOn(API, "deleteTemplate"); + await act(async () => result.current.confirmDelete()); + await waitFor(() => expect(API.deleteTemplate).toBeCalledTimes(1)); + expect(onDeleteTemplate).toBeCalledTimes(1); +}); test("cancel template deletion", () => { const { result } = renderHook(() => useDeleteTemplate(MockTemplate, jest.fn()), - ) + ); //Open delete confirmation act(() => { - result.current.openDeleteConfirmation() - }) - expect(result.current.isDeleteDialogOpen).toBeTruthy() + result.current.openDeleteConfirmation(); + }); + expect(result.current.isDeleteDialogOpen).toBeTruthy(); // Cancel deletion act(() => { - result.current.cancelDeleteConfirmation() - }) - expect(result.current.isDeleteDialogOpen).toBeFalsy() -}) + result.current.cancelDeleteConfirmation(); + }); + expect(result.current.isDeleteDialogOpen).toBeFalsy(); +}); diff --git a/site/src/components/TemplateLayout/deleteTemplate.ts b/site/src/components/TemplateLayout/deleteTemplate.ts index 9a56a7f2be..e8a4f1e44b 100644 --- a/site/src/components/TemplateLayout/deleteTemplate.ts +++ b/site/src/components/TemplateLayout/deleteTemplate.ts @@ -1,37 +1,37 @@ -import { deleteTemplate } from "api/api" -import { getErrorMessage } from "api/errors" -import { Template } from "api/typesGenerated" -import { displayError } from "components/GlobalSnackbar/utils" -import { useState } from "react" +import { deleteTemplate } from "api/api"; +import { getErrorMessage } from "api/errors"; +import { Template } from "api/typesGenerated"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { useState } from "react"; type DeleteTemplateState = | { status: "idle" } | { status: "confirming" } - | { status: "deleting" } + | { status: "deleting" }; export const useDeleteTemplate = (template: Template, onDelete: () => void) => { - const [state, setState] = useState({ status: "idle" }) + const [state, setState] = useState({ status: "idle" }); const isDeleteDialogOpen = - state.status === "confirming" || state.status === "deleting" + state.status === "confirming" || state.status === "deleting"; const openDeleteConfirmation = () => { - setState({ status: "confirming" }) - } + setState({ status: "confirming" }); + }; const cancelDeleteConfirmation = () => { - setState({ status: "idle" }) - } + setState({ status: "idle" }); + }; const confirmDelete = async () => { try { - setState({ status: "deleting" }) - await deleteTemplate(template.id) - onDelete() + setState({ status: "deleting" }); + await deleteTemplate(template.id); + onDelete(); } catch (e) { - setState({ status: "confirming" }) - displayError(getErrorMessage(e, "Failed to delete template")) + setState({ status: "confirming" }); + displayError(getErrorMessage(e, "Failed to delete template")); } - } + }; return { state, @@ -39,5 +39,5 @@ export const useDeleteTemplate = (template: Template, onDelete: () => void) => { openDeleteConfirmation, cancelDeleteConfirmation, confirmDelete, - } -} + }; +}; diff --git a/site/src/components/TemplateParameters/TemplateParameters.tsx b/site/src/components/TemplateParameters/TemplateParameters.tsx index ef849a101f..54f73f92ef 100644 --- a/site/src/components/TemplateParameters/TemplateParameters.tsx +++ b/site/src/components/TemplateParameters/TemplateParameters.tsx @@ -1,24 +1,24 @@ -import { TemplateVersionParameter } from "api/typesGenerated" -import { FormSection, FormFields } from "components/Form/Form" +import { TemplateVersionParameter } from "api/typesGenerated"; +import { FormSection, FormFields } from "components/Form/Form"; import { RichParameterInput, RichParameterInputProps, -} from "components/RichParameterInput/RichParameterInput" -import { ComponentProps, FC } from "react" +} from "components/RichParameterInput/RichParameterInput"; +import { ComponentProps, FC } from "react"; export type TemplateParametersSectionProps = { - templateParameters: TemplateVersionParameter[] + templateParameters: TemplateVersionParameter[]; getInputProps: ( parameter: TemplateVersionParameter, index: number, - ) => Omit -} & Pick, "classes"> + ) => Omit; +} & Pick, "classes">; export const MutableTemplateParametersSection: FC< TemplateParametersSectionProps > = ({ templateParameters, getInputProps, ...formSectionProps }) => { const hasMutableParameters = - templateParameters.filter((p) => p.mutable).length > 0 + templateParameters.filter((p) => p.mutable).length > 0; return ( <> @@ -43,14 +43,14 @@ export const MutableTemplateParametersSection: FC< )} - ) -} + ); +}; export const ImmutableTemplateParametersSection: FC< TemplateParametersSectionProps > = ({ templateParameters, getInputProps, ...formSectionProps }) => { const hasImmutableParameters = - templateParameters.filter((p) => !p.mutable).length > 0 + templateParameters.filter((p) => !p.mutable).length > 0; return ( <> @@ -81,5 +81,5 @@ export const ImmutableTemplateParametersSection: FC< )} - ) -} + ); +}; diff --git a/site/src/components/TemplateResourcesTable/TemplateResourcesTable.tsx b/site/src/components/TemplateResourcesTable/TemplateResourcesTable.tsx index c0efebebeb..22b1b218de 100644 --- a/site/src/components/TemplateResourcesTable/TemplateResourcesTable.tsx +++ b/site/src/components/TemplateResourcesTable/TemplateResourcesTable.tsx @@ -1,10 +1,10 @@ -import { AgentRowPreview } from "components/Resources/AgentRowPreview" -import { Resources } from "components/Resources/Resources" -import { FC } from "react" -import { WorkspaceResource } from "../../api/typesGenerated" +import { AgentRowPreview } from "components/Resources/AgentRowPreview"; +import { Resources } from "components/Resources/Resources"; +import { FC } from "react"; +import { WorkspaceResource } from "../../api/typesGenerated"; export interface TemplateResourcesProps { - resources: WorkspaceResource[] + resources: WorkspaceResource[]; } export const TemplateResourcesTable: FC< @@ -19,5 +19,5 @@ export const TemplateResourcesTable: FC< 1} /> )} /> - ) -} + ); +}; diff --git a/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.stories.tsx b/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.stories.tsx index 72fc1f72de..7962e9c3a9 100644 --- a/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.stories.tsx +++ b/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.stories.tsx @@ -1,19 +1,19 @@ -import { Story } from "@storybook/react" +import { Story } from "@storybook/react"; import { TemplateVersionWarnings, TemplateVersionWarningsProps, -} from "./TemplateVersionWarnings" +} from "./TemplateVersionWarnings"; export default { title: "components/TemplateVersionWarnings", component: TemplateVersionWarnings, -} +}; const Template: Story = (args) => ( -) +); -export const UnsupportedWorkspaces = Template.bind({}) +export const UnsupportedWorkspaces = Template.bind({}); UnsupportedWorkspaces.args = { warnings: ["UNSUPPORTED_WORKSPACES"], -} +}; diff --git a/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.tsx b/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.tsx index dc80b5c39c..dd7513e3a3 100644 --- a/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.tsx +++ b/site/src/components/TemplateVersionWarnings/TemplateVersionWarnings.tsx @@ -1,17 +1,17 @@ -import { FC } from "react" -import * as TypesGen from "api/typesGenerated" -import { Alert } from "components/Alert/Alert" -import { Maybe } from "components/Conditionals/Maybe" +import { FC } from "react"; +import * as TypesGen from "api/typesGenerated"; +import { Alert } from "components/Alert/Alert"; +import { Maybe } from "components/Conditionals/Maybe"; export interface TemplateVersionWarningsProps { - warnings?: TypesGen.TemplateVersionWarning[] + warnings?: TypesGen.TemplateVersionWarning[]; } export const TemplateVersionWarnings: FC< React.PropsWithChildren > = ({ warnings }) => { if (!warnings) { - return <> + return <>; } return ( @@ -23,5 +23,5 @@ export const TemplateVersionWarnings: FC< - ) -} + ); +}; diff --git a/site/src/components/Timeline/Timeline.tsx b/site/src/components/Timeline/Timeline.tsx index 02f2369756..ad7041d5e9 100644 --- a/site/src/components/Timeline/Timeline.tsx +++ b/site/src/components/Timeline/Timeline.tsx @@ -1,31 +1,31 @@ -import { TimelineDateRow } from "components/Timeline/TimelineDateRow" -import { Fragment } from "react" +import { TimelineDateRow } from "components/Timeline/TimelineDateRow"; +import { Fragment } from "react"; -type GetDateFn = (data: TData) => Date +type GetDateFn = (data: TData) => Date; const groupByDate = ( items: TData[], getDate: GetDateFn, ): Record => { - const itemsByDate: Record = {} + const itemsByDate: Record = {}; items.forEach((item) => { - const dateKey = getDate(item).toDateString() + const dateKey = getDate(item).toDateString(); if (dateKey in itemsByDate) { - itemsByDate[dateKey].push(item) + itemsByDate[dateKey].push(item); } else { - itemsByDate[dateKey] = [item] + itemsByDate[dateKey] = [item]; } - }) + }); - return itemsByDate -} + return itemsByDate; +}; export interface TimelineProps { - items: TData[] - getDate: GetDateFn - row: (item: TData) => JSX.Element + items: TData[]; + getDate: GetDateFn; + row: (item: TData) => JSX.Element; } export const Timeline = ({ @@ -33,20 +33,20 @@ export const Timeline = ({ getDate, row, }: TimelineProps): JSX.Element => { - const itemsByDate = groupByDate(items, getDate) + const itemsByDate = groupByDate(items, getDate); return ( <> {Object.keys(itemsByDate).map((dateStr) => { - const items = itemsByDate[dateStr] + const items = itemsByDate[dateStr]; return ( {items.map(row)} - ) + ); })} - ) -} + ); +}; diff --git a/site/src/components/Timeline/TimelineDateRow.tsx b/site/src/components/Timeline/TimelineDateRow.tsx index 45dbe09337..ebffbb18e1 100644 --- a/site/src/components/Timeline/TimelineDateRow.tsx +++ b/site/src/components/Timeline/TimelineDateRow.tsx @@ -1,15 +1,15 @@ -import { makeStyles } from "@mui/styles" -import TableCell from "@mui/material/TableCell" -import TableRow from "@mui/material/TableRow" -import { FC } from "react" -import { createDisplayDate } from "./utils" +import { makeStyles } from "@mui/styles"; +import TableCell from "@mui/material/TableCell"; +import TableRow from "@mui/material/TableRow"; +import { FC } from "react"; +import { createDisplayDate } from "./utils"; export interface TimelineDateRow { - date: Date + date: Date; } export const TimelineDateRow: FC = ({ date }) => { - const styles = useStyles() + const styles = useStyles(); return ( @@ -17,8 +17,8 @@ export const TimelineDateRow: FC = ({ date }) => { {createDisplayDate(date)} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ dateRow: { @@ -37,4 +37,4 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.secondary, textTransform: "capitalize", }, -})) +})); diff --git a/site/src/components/Timeline/TimelineEntry.tsx b/site/src/components/Timeline/TimelineEntry.tsx index a0156b40a5..2f8acf8a09 100644 --- a/site/src/components/Timeline/TimelineEntry.tsx +++ b/site/src/components/Timeline/TimelineEntry.tsx @@ -1,10 +1,10 @@ -import { makeStyles } from "@mui/styles" -import TableRow, { TableRowProps } from "@mui/material/TableRow" -import { PropsWithChildren } from "react" -import { combineClasses } from "utils/combineClasses" +import { makeStyles } from "@mui/styles"; +import TableRow, { TableRowProps } from "@mui/material/TableRow"; +import { PropsWithChildren } from "react"; +import { combineClasses } from "utils/combineClasses"; interface TimelineEntryProps { - clickable?: boolean + clickable?: boolean; } export const TimelineEntry = ({ @@ -12,7 +12,7 @@ export const TimelineEntry = ({ clickable = true, ...props }: PropsWithChildren): JSX.Element => { - const styles = useStyles() + const styles = useStyles(); return ( {children} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ clickable: { @@ -53,4 +53,4 @@ const useStyles = makeStyles((theme) => ({ background: theme.palette.divider, }, }, -})) +})); diff --git a/site/src/components/Timeline/utils.test.ts b/site/src/components/Timeline/utils.test.ts index 407157ca02..c377321258 100644 --- a/site/src/components/Timeline/utils.test.ts +++ b/site/src/components/Timeline/utils.test.ts @@ -1,14 +1,14 @@ -import { createDisplayDate } from "./utils" +import { createDisplayDate } from "./utils"; describe("createDisplayDate", () => { it("returns correctly for Saturdays", () => { - const now = new Date(2020, 1, 7) + const now = new Date(2020, 1, 7); const date = new Date( now.getFullYear(), now.getMonth(), // Previous Saturday, from now. now.getDate() - now.getDay() - 1, - ) - expect(createDisplayDate(date, now)).toEqual("last Saturday") - }) -}) + ); + expect(createDisplayDate(date, now)).toEqual("last Saturday"); + }); +}); diff --git a/site/src/components/Timeline/utils.ts b/site/src/components/Timeline/utils.ts index 26712caa42..f37a4c57c5 100644 --- a/site/src/components/Timeline/utils.ts +++ b/site/src/components/Timeline/utils.ts @@ -1,15 +1,15 @@ /* eslint-disable eslint-comments/disable-enable-pair -- Solve below */ /* eslint-disable import/no-duplicates -- https://github.com/date-fns/date-fns/issues/1677 */ -import formatRelative from "date-fns/formatRelative" -import subDays from "date-fns/subDays" +import formatRelative from "date-fns/formatRelative"; +import subDays from "date-fns/subDays"; export const createDisplayDate = ( date: Date, base: Date = new Date(), ): string => { - const lastWeek = subDays(base, 7) + const lastWeek = subDays(base, 7); if (date >= lastWeek) { - return formatRelative(date, base).split(" at ")[0] + return formatRelative(date, base).split(" at ")[0]; } - return date.toLocaleDateString() -} + return date.toLocaleDateString(); +}; diff --git a/site/src/components/Typography/Typography.stories.tsx b/site/src/components/Typography/Typography.stories.tsx index dd3f218df2..8046b863d2 100644 --- a/site/src/components/Typography/Typography.stories.tsx +++ b/site/src/components/Typography/Typography.stories.tsx @@ -1,10 +1,10 @@ -import { Story } from "@storybook/react" -import { Typography, TypographyProps } from "./Typography" +import { Story } from "@storybook/react"; +import { Typography, TypographyProps } from "./Typography"; export default { title: "components/Typography", component: Typography, -} +}; const Template: Story = (args: TypographyProps) => ( <> @@ -13,13 +13,13 @@ const Template: Story = (args: TypographyProps) => ( More people have been to France than I have -) +); -export const Short = Template.bind({}) +export const Short = Template.bind({}); Short.args = { short: true, -} -export const Tall = Template.bind({}) +}; +export const Tall = Template.bind({}); Tall.args = { short: false, -} +}; diff --git a/site/src/components/Typography/Typography.tsx b/site/src/components/Typography/Typography.tsx index e5c539aeb7..e5d54244e4 100644 --- a/site/src/components/Typography/Typography.tsx +++ b/site/src/components/Typography/Typography.tsx @@ -3,15 +3,15 @@ * verbatim port from `@coder/ui`. */ -import { makeStyles } from "@mui/styles" +import { makeStyles } from "@mui/styles"; import MuiTypography, { TypographyProps as MuiTypographyProps, -} from "@mui/material/Typography" -import * as React from "react" -import { appendCSSString, combineClasses } from "../../utils/combineClasses" +} from "@mui/material/Typography"; +import * as React from "react"; +import { appendCSSString, combineClasses } from "../../utils/combineClasses"; export interface TypographyProps extends MuiTypographyProps { - short?: boolean + short?: boolean; } /** @@ -25,15 +25,15 @@ export const Typography: React.FC = ({ short, ...rest }) => { - const styles = useStyles() + const styles = useStyles(); - let classes = combineClasses({ [styles.short]: short }) + let classes = combineClasses({ [styles.short]: short }); if (className) { - classes = appendCSSString(classes ?? "", className) + classes = appendCSSString(classes ?? "", className); } - return -} + return ; +}; const useStyles = makeStyles({ short: { @@ -45,4 +45,4 @@ const useStyles = makeStyles({ letterSpacing: 0.2, }, }, -}) +}); diff --git a/site/src/components/UserAutocomplete/UserAutocomplete.stories.tsx b/site/src/components/UserAutocomplete/UserAutocomplete.stories.tsx index 090c903a51..a7f24f2b18 100644 --- a/site/src/components/UserAutocomplete/UserAutocomplete.stories.tsx +++ b/site/src/components/UserAutocomplete/UserAutocomplete.stories.tsx @@ -1,23 +1,23 @@ -import { Story } from "@storybook/react" -import { MockUser } from "testHelpers/entities" -import { UserAutocomplete, UserAutocompleteProps } from "./UserAutocomplete" +import { Story } from "@storybook/react"; +import { MockUser } from "testHelpers/entities"; +import { UserAutocomplete, UserAutocompleteProps } from "./UserAutocomplete"; export default { title: "components/UserAutocomplete", component: UserAutocomplete, -} +}; const Template: Story = ( args: UserAutocompleteProps, -) => +) => ; -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { value: MockUser, label: "User", -} +}; -export const NoLabel = Template.bind({}) +export const NoLabel = Template.bind({}); NoLabel.args = { value: MockUser, -} +}; diff --git a/site/src/components/UserAutocomplete/UserAutocomplete.tsx b/site/src/components/UserAutocomplete/UserAutocomplete.tsx index a5f1727e93..640d86bebb 100644 --- a/site/src/components/UserAutocomplete/UserAutocomplete.tsx +++ b/site/src/components/UserAutocomplete/UserAutocomplete.tsx @@ -1,24 +1,24 @@ -import CircularProgress from "@mui/material/CircularProgress" -import { makeStyles } from "@mui/styles" -import TextField from "@mui/material/TextField" -import Autocomplete from "@mui/material/Autocomplete" -import { useMachine } from "@xstate/react" -import { User } from "api/typesGenerated" -import { Avatar } from "components/Avatar/Avatar" -import { AvatarData } from "components/AvatarData/AvatarData" -import debounce from "just-debounce-it" -import { ChangeEvent, ComponentProps, FC, useEffect, useState } from "react" -import { searchUserMachine } from "xServices/users/searchUserXService" -import { useTranslation } from "react-i18next" -import Box from "@mui/material/Box" +import CircularProgress from "@mui/material/CircularProgress"; +import { makeStyles } from "@mui/styles"; +import TextField from "@mui/material/TextField"; +import Autocomplete from "@mui/material/Autocomplete"; +import { useMachine } from "@xstate/react"; +import { User } from "api/typesGenerated"; +import { Avatar } from "components/Avatar/Avatar"; +import { AvatarData } from "components/AvatarData/AvatarData"; +import debounce from "just-debounce-it"; +import { ChangeEvent, ComponentProps, FC, useEffect, useState } from "react"; +import { searchUserMachine } from "xServices/users/searchUserXService"; +import { useTranslation } from "react-i18next"; +import Box from "@mui/material/Box"; export type UserAutocompleteProps = { - value: User | null - onChange: (user: User | null) => void - label?: string - className?: string - size?: ComponentProps["size"] -} + value: User | null; + onChange: (user: User | null) => void; + label?: string; + className?: string; + size?: ComponentProps["size"]; +}; export const UserAutocomplete: FC = ({ value, @@ -27,27 +27,27 @@ export const UserAutocomplete: FC = ({ className, size = "small", }) => { - const styles = useStyles() - const { t } = useTranslation("common") - const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false) - const [searchState, sendSearch] = useMachine(searchUserMachine) - const { searchResults } = searchState.context + const styles = useStyles(); + const { t } = useTranslation("common"); + const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false); + const [searchState, sendSearch] = useMachine(searchUserMachine); + const { searchResults } = searchState.context; // seed list of options on the first page load if a user pases in a value // since some organizations have long lists of users, we do not load all options on page load. useEffect(() => { if (value) { - sendSearch("SEARCH", { query: value.email }) + sendSearch("SEARCH", { query: value.email }); } // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO look into this - }, []) + }, []); const handleFilterChange = debounce( (event: ChangeEvent) => { - sendSearch("SEARCH", { query: event.target.value }) + sendSearch("SEARCH", { query: event.target.value }); }, 1000, - ) + ); return ( = ({ id="user-autocomplete" open={isAutocompleteOpen} onOpen={() => { - setIsAutocompleteOpen(true) + setIsAutocompleteOpen(true); }} onClose={() => { - setIsAutocompleteOpen(false) + setIsAutocompleteOpen(false); }} onChange={(_, newValue) => { if (newValue === null) { - sendSearch("CLEAR_RESULTS") + sendSearch("CLEAR_RESULTS"); } - onChange(newValue) + onChange(newValue); }} isOptionEqualToValue={(option: User, value: User) => option.username === value.username @@ -122,8 +122,8 @@ export const UserAutocomplete: FC = ({ )} /> - ) -} + ); +}; export const useStyles = makeStyles((theme) => ({ textField: { @@ -135,4 +135,4 @@ export const useStyles = makeStyles((theme) => ({ paddingLeft: `${theme.spacing(1.75)} !important`, // Same padding left as input gap: theme.spacing(0.5), }, -})) +})); diff --git a/site/src/components/UserAvatar/UserAvatar.tsx b/site/src/components/UserAvatar/UserAvatar.tsx index 49349a3289..fa453bcf6b 100644 --- a/site/src/components/UserAvatar/UserAvatar.tsx +++ b/site/src/components/UserAvatar/UserAvatar.tsx @@ -1,10 +1,10 @@ -import { Avatar, AvatarProps } from "components/Avatar/Avatar" -import { FC } from "react" +import { Avatar, AvatarProps } from "components/Avatar/Avatar"; +import { FC } from "react"; export type UserAvatarProps = { - username: string - avatarURL?: string -} & AvatarProps + username: string; + avatarURL?: string; +} & AvatarProps; export const UserAvatar: FC = ({ username, @@ -15,5 +15,5 @@ export const UserAvatar: FC = ({ {username} - ) -} + ); +}; diff --git a/site/src/components/UserOrGroupAutocomplete/UserOrGroupAutocomplete.tsx b/site/src/components/UserOrGroupAutocomplete/UserOrGroupAutocomplete.tsx index 127d970c9e..491a713258 100644 --- a/site/src/components/UserOrGroupAutocomplete/UserOrGroupAutocomplete.tsx +++ b/site/src/components/UserOrGroupAutocomplete/UserOrGroupAutocomplete.tsx @@ -1,35 +1,35 @@ -import CircularProgress from "@mui/material/CircularProgress" -import { makeStyles } from "@mui/styles" -import TextField from "@mui/material/TextField" -import Autocomplete from "@mui/material/Autocomplete" -import { useMachine } from "@xstate/react" -import { Group, User } from "api/typesGenerated" -import { AvatarData } from "components/AvatarData/AvatarData" -import debounce from "just-debounce-it" -import { ChangeEvent, useState } from "react" -import { getGroupSubtitle } from "utils/groups" -import { searchUsersAndGroupsMachine } from "xServices/template/searchUsersAndGroupsXService" -import Box from "@mui/material/Box" +import CircularProgress from "@mui/material/CircularProgress"; +import { makeStyles } from "@mui/styles"; +import TextField from "@mui/material/TextField"; +import Autocomplete from "@mui/material/Autocomplete"; +import { useMachine } from "@xstate/react"; +import { Group, User } from "api/typesGenerated"; +import { AvatarData } from "components/AvatarData/AvatarData"; +import debounce from "just-debounce-it"; +import { ChangeEvent, useState } from "react"; +import { getGroupSubtitle } from "utils/groups"; +import { searchUsersAndGroupsMachine } from "xServices/template/searchUsersAndGroupsXService"; +import Box from "@mui/material/Box"; -export type UserOrGroupAutocompleteValue = User | Group | null +export type UserOrGroupAutocompleteValue = User | Group | null; const isGroup = (value: UserOrGroupAutocompleteValue): value is Group => { - return value !== null && "members" in value -} + return value !== null && "members" in value; +}; export type UserOrGroupAutocompleteProps = { - value: UserOrGroupAutocompleteValue - onChange: (value: UserOrGroupAutocompleteValue) => void - organizationId: string - templateID?: string - exclude: UserOrGroupAutocompleteValue[] -} + value: UserOrGroupAutocompleteValue; + onChange: (value: UserOrGroupAutocompleteValue) => void; + organizationId: string; + templateID?: string; + exclude: UserOrGroupAutocompleteValue[]; +}; export const UserOrGroupAutocomplete: React.FC< UserOrGroupAutocompleteProps > = ({ value, onChange, organizationId, templateID, exclude }) => { - const styles = useStyles() - const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false) + const styles = useStyles(); + const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false); const [searchState, sendSearch] = useMachine(searchUsersAndGroupsMachine, { context: { userResults: [], @@ -37,19 +37,19 @@ export const UserOrGroupAutocomplete: React.FC< organizationId, templateID, }, - }) - const { userResults, groupResults } = searchState.context + }); + const { userResults, groupResults } = searchState.context; const options = [...groupResults, ...userResults].filter((result) => { - const excludeIds = exclude.map((optionToExclude) => optionToExclude?.id) - return !excludeIds.includes(result.id) - }) + const excludeIds = exclude.map((optionToExclude) => optionToExclude?.id); + return !excludeIds.includes(result.id); + }); const handleFilterChange = debounce( (event: ChangeEvent) => { - sendSearch("SEARCH", { query: event.target.value }) + sendSearch("SEARCH", { query: event.target.value }); }, 500, - ) + ); return ( { - setIsAutocompleteOpen(true) + setIsAutocompleteOpen(true); }} onClose={() => { - setIsAutocompleteOpen(false) + setIsAutocompleteOpen(false); }} onChange={(_, newValue) => { if (newValue === null) { - sendSearch("CLEAR_RESULTS") + sendSearch("CLEAR_RESULTS"); } - onChange(newValue) + onChange(newValue); }} isOptionEqualToValue={(option, value) => option.id === value.id} getOptionLabel={(option) => isGroup(option) ? option.display_name || option.name : option.email } renderOption={(props, option) => { - const isOptionGroup = isGroup(option) + const isOptionGroup = isGroup(option); return ( @@ -88,7 +88,7 @@ export const UserOrGroupAutocomplete: React.FC< src={option.avatar_url} /> - ) + ); }} options={options} loading={searchState.matches("searching")} @@ -118,8 +118,8 @@ export const UserOrGroupAutocomplete: React.FC< )} /> - ) -} + ); +}; export const useStyles = makeStyles(() => { return { @@ -134,5 +134,5 @@ export const useStyles = makeStyles(() => { width: "100%", }, }, - } -}) + }; +}); diff --git a/site/src/components/UsersLayout/UsersLayout.tsx b/site/src/components/UsersLayout/UsersLayout.tsx index 9429f29ed3..5d64f8a24a 100644 --- a/site/src/components/UsersLayout/UsersLayout.tsx +++ b/site/src/components/UsersLayout/UsersLayout.tsx @@ -1,29 +1,29 @@ -import Button from "@mui/material/Button" -import Link from "@mui/material/Link" -import { makeStyles } from "@mui/styles" -import GroupAdd from "@mui/icons-material/GroupAddOutlined" -import PersonAdd from "@mui/icons-material/PersonAddOutlined" -import { USERS_LINK } from "components/Dashboard/Navbar/NavbarView" -import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader" -import { useFeatureVisibility } from "hooks/useFeatureVisibility" -import { usePermissions } from "hooks/usePermissions" -import { FC } from "react" +import Button from "@mui/material/Button"; +import Link from "@mui/material/Link"; +import { makeStyles } from "@mui/styles"; +import GroupAdd from "@mui/icons-material/GroupAddOutlined"; +import PersonAdd from "@mui/icons-material/PersonAddOutlined"; +import { USERS_LINK } from "components/Dashboard/Navbar/NavbarView"; +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; +import { useFeatureVisibility } from "hooks/useFeatureVisibility"; +import { usePermissions } from "hooks/usePermissions"; +import { FC } from "react"; import { Link as RouterLink, NavLink, Outlet, useNavigate, -} from "react-router-dom" -import { combineClasses } from "utils/combineClasses" -import { Margins } from "../../components/Margins/Margins" -import { Stack } from "../../components/Stack/Stack" +} from "react-router-dom"; +import { combineClasses } from "utils/combineClasses"; +import { Margins } from "../../components/Margins/Margins"; +import { Stack } from "../../components/Stack/Stack"; export const UsersLayout: FC = () => { - const styles = useStyles() + const styles = useStyles(); const { createUser: canCreateUser, createGroup: canCreateGroup } = - usePermissions() - const navigate = useNavigate() - const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility() + usePermissions(); + const navigate = useNavigate(); + const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility(); return ( <> @@ -34,7 +34,7 @@ export const UsersLayout: FC = () => { {canCreateUser && ( - ) -} + ); +}; interface ProxyContextSelectionTest { // Regions is the list of regions to return via the "api" response. - regions: Region[] + regions: Region[]; // storageProxy should be the proxy stored in local storage before the // component is mounted and context is loaded. This simulates opening a // new window with a selection saved from before. - storageProxy: Region | undefined + storageProxy: Region | undefined; // latencies is the hard coded latencies to return. If empty, no latencies // are returned. - latencies?: Record + latencies?: Record; // afterLoad are actions to take after loading the component, but before // assertions. This is useful for simulating user actions. - afterLoad?: () => Promise + afterLoad?: () => Promise; // Assert these values. // expProxyID is the proxyID returned to be used. - expProxyID: string + expProxyID: string; // expUserProxyID is the user's stored selection. - expUserProxyID?: string + expUserProxyID?: string; } describe("ProxyContextSelection", () => { beforeEach(() => { - window.localStorage.clear() - }) + window.localStorage.clear(); + }); // A way to simulate a user clearing the proxy selection. const clearProxyAction = async (): Promise => { - const user = userEvent.setup() - const clearProxyButton = screen.getByTestId("clearProxy") - await user.click(clearProxyButton) - } + const user = userEvent.setup(); + const clearProxyButton = screen.getByTestId("clearProxy"); + await user.click(clearProxyButton); + }; const userSelectProxy = (proxy: Region): (() => Promise) => { return async (): Promise => { - const user = userEvent.setup() - const selectData = screen.getByTestId("userSelectProxyData") - selectData.innerText = JSON.stringify(proxy) + const user = userEvent.setup(); + const selectData = screen.getByTestId("userSelectProxyData"); + selectData.innerText = JSON.stringify(proxy); - const selectProxyButton = screen.getByTestId("userSelectProxy") - await user.click(selectProxyButton) - } - } + const selectProxyButton = screen.getByTestId("userSelectProxy"); + await user.click(selectProxyButton); + }; + }; it.each([ // Not latency behavior @@ -343,11 +343,11 @@ describe("ProxyContextSelection", () => { }, ) => { // Mock the latencies - hardCodedLatencies = latencies + hardCodedLatencies = latencies; // Initial selection if present if (storageProxy) { - saveUserSelectedProxy(storageProxy) + saveUserSelectedProxy(storageProxy); } // Mock the API response @@ -358,7 +358,7 @@ describe("ProxyContextSelection", () => { ctx.json({ regions: regions, }), - ) + ); }), rest.get("/api/v2/workspaceproxies", async (req, res, ctx) => { return res( @@ -366,29 +366,29 @@ describe("ProxyContextSelection", () => { ctx.json({ regions: regions, }), - ) + ); }), - ) + ); - TestingComponent() - await waitForLoaderToBeRemoved() + TestingComponent(); + await waitForLoaderToBeRemoved(); if (afterLoad) { - await afterLoad() + await afterLoad(); } await screen.findByTestId("isFetched").then((x) => { - expect(x.title).toBe("true") - }) + expect(x.title).toBe("true"); + }); await screen.findByTestId("isLoading").then((x) => { - expect(x.title).toBe("false") - }) + expect(x.title).toBe("false"); + }); await screen.findByTestId("preferredProxy").then((x) => { - expect(x.title).toBe(expSelectedProxyID) - }) + expect(x.title).toBe(expSelectedProxyID); + }); await screen.findByTestId("userProxy").then((x) => { - expect(x.title).toBe(expUserProxyID || "") - }) + expect(x.title).toBe(expUserProxyID || ""); + }); }, - ) -}) + ); +}); diff --git a/site/src/contexts/ProxyContext.tsx b/site/src/contexts/ProxyContext.tsx index 152b0fbd0c..7ca714938a 100644 --- a/site/src/contexts/ProxyContext.tsx +++ b/site/src/contexts/ProxyContext.tsx @@ -1,7 +1,7 @@ -import { useQuery } from "@tanstack/react-query" -import { getWorkspaceProxies, getWorkspaceProxyRegions } from "api/api" -import { Region, WorkspaceProxy } from "api/typesGenerated" -import { useDashboard } from "components/Dashboard/DashboardProvider" +import { useQuery } from "@tanstack/react-query"; +import { getWorkspaceProxies, getWorkspaceProxyRegions } from "api/api"; +import { Region, WorkspaceProxy } from "api/typesGenerated"; +import { useDashboard } from "components/Dashboard/DashboardProvider"; import { createContext, FC, @@ -10,9 +10,9 @@ import { useContext, useEffect, useState, -} from "react" -import { ProxyLatencyReport, useProxyLatency } from "./useProxyLatency" -import { usePermissions } from "hooks/usePermissions" +} from "react"; +import { ProxyLatencyReport, useProxyLatency } from "./useProxyLatency"; +import { usePermissions } from "hooks/usePermissions"; export interface ProxyContextValue { // proxy is **always** the workspace proxy that should be used. @@ -27,13 +27,13 @@ export interface ProxyContextValue { // The values 'proxy.preferredPathAppURL' and 'proxy.preferredWildcardHostname' can // always be used even if 'proxy.selectedProxy' is undefined. These values are sourced from // the 'selectedProxy', but default to relative paths if the 'selectedProxy' is undefined. - proxy: PreferredProxy + proxy: PreferredProxy; // userProxy is always the proxy the user has selected. This value comes from local storage. // The value `proxy` should always be used instead of `userProxy`. `userProxy` is only exposed // so the caller can determine if the proxy being used is the user's selected proxy, or if it // was auto selected based on some other criteria. - userProxy?: Region + userProxy?: Region; // proxies is the list of proxies returned by coderd. This is fetched async. // isFetched, isLoading, and error are used to track the state of the async call. @@ -42,25 +42,25 @@ export interface ProxyContextValue { // WorkspaceProxy[] is returned if the user is an admin. WorkspaceProxy extends Region with // more information about the proxy and the status. More information includes the error message if // the proxy is unhealthy. - proxies?: Region[] | WorkspaceProxy[] + proxies?: Region[] | WorkspaceProxy[]; // isFetched is true when the 'proxies' api call is complete. - isFetched: boolean - isLoading: boolean - error?: unknown + isFetched: boolean; + isLoading: boolean; + error?: unknown; // proxyLatencies is a map of proxy id to latency report. If the proxyLatencies[proxy.id] is undefined // then the latency has not been fetched yet. Calculations happen async for each proxy in the list. // Refer to the returned report for a given proxy for more information. - proxyLatencies: Record + proxyLatencies: Record; // refetchProxyLatencies will trigger refreshing of the proxy latencies. By default the latencies // are loaded once. - refetchProxyLatencies: () => Date + refetchProxyLatencies: () => Date; // setProxy is a function that sets the user's selected proxy. This function should // only be called if the user is manually selecting a proxy. This value is stored in local // storage and will persist across reloads and tabs. - setProxy: (selectedProxy: Region) => void + setProxy: (selectedProxy: Region) => void; // clearProxy is a function that clears the user's selected proxy. // If no proxy is selected, then the default proxy will be used. - clearProxy: () => void + clearProxy: () => void; } interface PreferredProxy { @@ -68,36 +68,36 @@ interface PreferredProxy { // getting the fields such as "display_name" and "id" // Do not use the fields 'path_app_url' or 'wildcard_hostname' from this // object. Use the preferred fields. - proxy: Region | undefined + proxy: Region | undefined; // PreferredPathAppURL is the URL of the proxy or it is the empty string // to indicate using relative paths. To add a path to this: // PreferredPathAppURL + "/path/to/app" - preferredPathAppURL: string + preferredPathAppURL: string; // PreferredWildcardHostname is a hostname that includes a wildcard. - preferredWildcardHostname: string + preferredWildcardHostname: string; } export const ProxyContext = createContext( undefined, -) +); /** * ProxyProvider interacts with local storage to indicate the preferred workspace proxy. */ export const ProxyProvider: FC = ({ children }) => { - const dashboard = useDashboard() - const experimentEnabled = dashboard?.experiments.includes("moons") + const dashboard = useDashboard(); + const experimentEnabled = dashboard?.experiments.includes("moons"); // Using a useState so the caller always has the latest user saved // proxy. - const [userSavedProxy, setUserSavedProxy] = useState(loadUserSelectedProxy()) + const [userSavedProxy, setUserSavedProxy] = useState(loadUserSelectedProxy()); // Load the initial state from local storage. const [proxy, setProxy] = useState( computeUsableURLS(userSavedProxy), - ) + ); - const queryKey = ["get-proxies"] + const queryKey = ["get-proxies"]; // This doesn't seem like an idiomatic way to get react-query to use the // initial data without performing an API request on mount, but it works. // @@ -105,29 +105,29 @@ export const ProxyProvider: FC = ({ children }) => { // from the `meta` tag if it exists, and not fetch the regions route. const [initialData] = useState(() => { // Build info is injected by the Coder server into the HTML document. - const regions = document.querySelector("meta[property=regions]") + const regions = document.querySelector("meta[property=regions]"); if (regions) { - const rawContent = regions.getAttribute("content") + const rawContent = regions.getAttribute("content"); try { - const obj = JSON.parse(rawContent as string) + const obj = JSON.parse(rawContent as string); if ("regions" in obj) { - return obj.regions as Region[] + return obj.regions as Region[]; } - return obj as Region[] + return obj as Region[]; } catch (ex) { // Ignore this and fetch as normal! } } - }) + }); - const permissions = usePermissions() + const permissions = usePermissions(); const query = async (): Promise => { const endpoint = permissions.editWorkspaceProxies ? getWorkspaceProxies - : getWorkspaceProxyRegions - const resp = await endpoint() - return resp.regions - } + : getWorkspaceProxyRegions; + const resp = await endpoint(); + return resp.regions; + }; const { data: proxiesResp, @@ -139,18 +139,18 @@ export const ProxyProvider: FC = ({ children }) => { queryFn: query, staleTime: initialData ? Infinity : undefined, initialData, - }) + }); // Every time we get a new proxiesResponse, update the latency check // to each workspace proxy. const { proxyLatencies, refetch: refetchProxyLatencies } = - useProxyLatency(proxiesResp) + useProxyLatency(proxiesResp); // updateProxy is a helper function that when called will // update the proxy being used. const updateProxy = useCallback(() => { // Update the saved user proxy for the caller. - setUserSavedProxy(loadUserSelectedProxy()) + setUserSavedProxy(loadUserSelectedProxy()); setProxy( getPreferredProxy( proxiesResp ?? [], @@ -160,15 +160,15 @@ export const ProxyProvider: FC = ({ children }) => { // to behave poorly. false, ), - ) - }, [proxiesResp, proxyLatencies]) + ); + }, [proxiesResp, proxyLatencies]); // This useEffect ensures the proxy to be used is updated whenever the state changes. // This includes proxies being loaded, latencies being calculated, and the user selecting a proxy. useEffect(() => { - updateProxy() + updateProxy(); // eslint-disable-next-line react-hooks/exhaustive-deps -- Only update if the source data changes - }, [proxiesResp, proxyLatencies]) + }, [proxiesResp, proxyLatencies]); return ( = ({ children }) => { // These functions are exposed to allow the user to select a proxy. setProxy: (proxy: Region) => { // Save to local storage to persist the user's preference across reloads - saveUserSelectedProxy(proxy) + saveUserSelectedProxy(proxy); // Update the selected proxy - updateProxy() + updateProxy(); }, clearProxy: () => { // Clear the user's selection from local storage. - clearUserSelectedProxy() - updateProxy() + clearUserSelectedProxy(); + updateProxy(); }, }} > {children} - ) -} + ); +}; export const useProxy = (): ProxyContextValue => { - const context = useContext(ProxyContext) + const context = useContext(ProxyContext); if (!context) { - throw new Error("useProxy should be used inside of ") + throw new Error("useProxy should be used inside of "); } - return context -} + return context; +}; /** * getPreferredProxy is a helper function to calculate the urls to use for a given proxy configuration. By default, it is @@ -237,60 +237,60 @@ export const getPreferredProxy = ( // we should default to the primary. selectedProxy = proxies.find( (proxy) => selectedProxy && proxy.id === selectedProxy.id, - ) + ); // If no proxy is selected, or the selected proxy is unhealthy default to the primary proxy. if (!selectedProxy || !selectedProxy.healthy) { // By default, use the primary proxy. - selectedProxy = proxies.find((proxy) => proxy.name === "primary") + selectedProxy = proxies.find((proxy) => proxy.name === "primary"); // If we have latencies, then attempt to use the best proxy by latency instead. - const best = selectByLatency(proxies, latencies) + const best = selectByLatency(proxies, latencies); if (autoSelectBasedOnLatency && best) { - selectedProxy = best + selectedProxy = best; } } - return computeUsableURLS(selectedProxy) -} + return computeUsableURLS(selectedProxy); +}; const selectByLatency = ( proxies: Region[], latencies?: Record, ): Region | undefined => { if (!latencies) { - return undefined + return undefined; } const proxyMap = proxies.reduce( (acc, proxy) => { - acc[proxy.id] = proxy - return acc + acc[proxy.id] = proxy; + return acc; }, {} as Record, - ) + ); const best = Object.keys(latencies) .map((proxyId) => { return { id: proxyId, ...latencies[proxyId], - } + }; }) // If the proxy is not in our list, or it is unhealthy, ignore it. .filter((latency) => proxyMap[latency.id]?.healthy) .sort((a, b) => a.latencyMS - b.latencyMS) - .at(0) + .at(0); // Found a new best, use it! if (best) { - const bestProxy = proxies.find((proxy) => proxy.id === best.id) + const bestProxy = proxies.find((proxy) => proxy.id === best.id); // Default to w/e it was before - return bestProxy + return bestProxy; } - return undefined -} + return undefined; +}; const computeUsableURLS = (proxy?: Region): PreferredProxy => { if (!proxy) { @@ -300,13 +300,13 @@ const computeUsableURLS = (proxy?: Region): PreferredProxy => { proxy: undefined, preferredPathAppURL: "", preferredWildcardHostname: "", - } + }; } - let pathAppURL = proxy?.path_app_url.replace(/\/$/, "") + let pathAppURL = proxy?.path_app_url.replace(/\/$/, ""); // Primary proxy uses relative paths. It's the only exception. if (proxy.name === "primary") { - pathAppURL = "" + pathAppURL = ""; } return { @@ -314,24 +314,24 @@ const computeUsableURLS = (proxy?: Region): PreferredProxy => { // Trim trailing slashes to be consistent preferredPathAppURL: pathAppURL, preferredWildcardHostname: proxy.wildcard_hostname, - } -} + }; +}; // Local storage functions export const clearUserSelectedProxy = (): void => { - window.localStorage.removeItem("user-selected-proxy") -} + window.localStorage.removeItem("user-selected-proxy"); +}; export const saveUserSelectedProxy = (saved: Region): void => { - window.localStorage.setItem("user-selected-proxy", JSON.stringify(saved)) -} + window.localStorage.setItem("user-selected-proxy", JSON.stringify(saved)); +}; export const loadUserSelectedProxy = (): Region | undefined => { - const str = localStorage.getItem("user-selected-proxy") + const str = localStorage.getItem("user-selected-proxy"); if (!str) { - return undefined + return undefined; } - return JSON.parse(str) -} + return JSON.parse(str); +}; diff --git a/site/src/contexts/useProxyLatency.ts b/site/src/contexts/useProxyLatency.ts index 8c6e28f235..dbf689f368 100644 --- a/site/src/contexts/useProxyLatency.ts +++ b/site/src/contexts/useProxyLatency.ts @@ -1,27 +1,27 @@ -import { Region } from "api/typesGenerated" -import { useEffect, useReducer, useState } from "react" -import PerformanceObserver from "@fastly/performance-observer-polyfill" -import axios from "axios" -import { generateRandomString } from "utils/random" +import { Region } from "api/typesGenerated"; +import { useEffect, useReducer, useState } from "react"; +import PerformanceObserver from "@fastly/performance-observer-polyfill"; +import axios from "axios"; +import { generateRandomString } from "utils/random"; -const proxyIntervalSeconds = 30 // seconds +const proxyIntervalSeconds = 30; // seconds export interface ProxyLatencyReport { // accurate identifies if the latency was calculated using the // PerformanceResourceTiming API. If this is false, then the // latency is calculated using the total duration of the request // and will be off by a good margin. - accurate: boolean - latencyMS: number + accurate: boolean; + latencyMS: number; // at is when the latency was recorded. - at: Date + at: Date; } interface ProxyLatencyAction { - proxyID: string + proxyID: string; // cached indicates if the latency was loaded from a cache (local storage) - cached: boolean - report: ProxyLatencyReport + cached: boolean; + report: ProxyLatencyReport; } const proxyLatenciesReducer = ( @@ -33,56 +33,56 @@ const proxyLatenciesReducer = ( return { ...state, [action.proxyID]: action.report, - } -} + }; +}; export const useProxyLatency = ( proxies?: Region[], ): { // Refetch can be called to refetch the proxy latencies. // Until the new values are loaded, the old values will still be used. - refetch: () => Date - proxyLatencies: Record + refetch: () => Date; + proxyLatencies: Record; } => { // maxStoredLatencies is the maximum number of latencies to store per proxy in local storage. - let maxStoredLatencies = 1 + let maxStoredLatencies = 1; // The reason we pull this from local storage is so for development purposes, a user can manually // set a larger number to collect data in their normal usage. This data can later be analyzed to come up // with some better magic numbers. const maxStoredLatenciesVar = localStorage.getItem( "workspace-proxy-latencies-max", - ) + ); if (maxStoredLatenciesVar) { - maxStoredLatencies = Number(maxStoredLatenciesVar) + maxStoredLatencies = Number(maxStoredLatenciesVar); } const [proxyLatencies, dispatchProxyLatencies] = useReducer( proxyLatenciesReducer, {}, - ) + ); // This latestFetchRequest is used to trigger a refetch of the proxy latencies. const [latestFetchRequest, setLatestFetchRequest] = useState( // The initial state is the current time minus the interval. Any proxies that have a latency after this // in the cache are still valid. new Date(new Date().getTime() - proxyIntervalSeconds * 1000).toISOString(), - ) + ); // Refetch will always set the latestFetchRequest to the current time, making all the cached latencies // stale and triggering a refetch of all proxies in the list. const refetch = () => { - const d = new Date() - setLatestFetchRequest(d.toISOString()) - return d - } + const d = new Date(); + setLatestFetchRequest(d.toISOString()); + return d; + }; // Only run latency updates when the proxies change. useEffect(() => { if (!proxies) { - return + return; } - const storedLatencies = loadStoredLatencies() + const storedLatencies = loadStoredLatencies(); // proxyMap is a map of the proxy path_app_url to the proxy object. // This is for the observer to know which requests are important to @@ -91,7 +91,7 @@ export const useProxyLatency = ( (acc, proxy) => { // Only run the latency check on healthy proxies. if (!proxy.healthy) { - return acc + return acc; } // Do not run latency checks if a cached check exists below the latestFetchRequest Date. @@ -103,10 +103,10 @@ export const useProxyLatency = ( storedLatencies[proxy.id] && storedLatencies[proxy.id].length > 0 ) { - const fetchRequestDate = new Date(latestFetchRequest) + const fetchRequestDate = new Date(latestFetchRequest); const latest = storedLatencies[proxy.id].reduce((prev, next) => prev.at > next.at ? prev : next, - ) + ); if (latest && latest.at > fetchRequestDate) { // dispatch the cached latency. This latency already went through the @@ -115,8 +115,8 @@ export const useProxyLatency = ( proxyID: proxy.id, cached: true, report: latest, - }) - return acc + }); + return acc; } } @@ -125,12 +125,12 @@ export const useProxyLatency = ( const url = new URL( `/latency-check?cache_bust=${generateRandomString(6)}`, proxy.path_app_url, - ) - acc[url.toString()] = proxy - return acc + ); + acc[url.toString()] = proxy; + return acc; }, {} as Record, - ) + ); // dispatchProxyLatenciesGuarded will assign the latency to the proxy // via the reducer. But it will only do so if the performance entry is @@ -138,36 +138,36 @@ export const useProxyLatency = ( const dispatchProxyLatenciesGuarded = (entry: PerformanceEntry) => { if (entry.entryType !== "resource") { // We should never get these, but just in case. - return + return; } // The entry.name is the url of the request. - const check = proxyChecks[entry.name] + const check = proxyChecks[entry.name]; if (!check) { // This is not a proxy request, so ignore it. - return + return; } // These docs are super useful. // https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Resource_timing - let latencyMS = 0 - let accurate = false + let latencyMS = 0; + let accurate = false; if ( "requestStart" in entry && (entry as PerformanceResourceTiming).requestStart !== 0 ) { // This is the preferred logic to get the latency. - const timingEntry = entry as PerformanceResourceTiming - latencyMS = timingEntry.responseStart - timingEntry.requestStart - accurate = true + const timingEntry = entry as PerformanceResourceTiming; + latencyMS = timingEntry.responseStart - timingEntry.requestStart; + accurate = true; } else { // This is the total duration of the request and will be off by a good margin. // This is a fallback if the better timing is not available. // eslint-disable-next-line no-console -- We can remove this when we display the "accurate" bool on the UI console.log( `Using fallback latency calculation for "${entry.name}". Latency will be incorrect and larger then actual.`, - ) - latencyMS = entry.duration + ); + latencyMS = entry.duration; } const update = { proxyID: check.id, @@ -177,25 +177,25 @@ export const useProxyLatency = ( accurate, at: new Date(), }, - } - dispatchProxyLatencies(update) + }; + dispatchProxyLatencies(update); // Also save to local storage to persist the latency across page refreshes. - updateStoredLatencies(update) + updateStoredLatencies(update); - return - } + return; + }; // Start a new performance observer to record of all the requests // to the proxies. const observer = new PerformanceObserver((list) => { // If we get entries via this callback, then dispatch the events to the latency reducer. list.getEntries().forEach((entry) => { - dispatchProxyLatenciesGuarded(entry) - }) - }) + dispatchProxyLatenciesGuarded(entry); + }); + }); // The resource requests include xmlhttp requests. - observer.observe({ entryTypes: ["resource"] }) + observer.observe({ entryTypes: ["resource"] }); const proxyRequests = Object.keys(proxyChecks).map((latencyURL) => { return axios.get(latencyURL, { @@ -204,8 +204,8 @@ export const useProxyLatency = ( // We want to force a preflight request. // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests headers: { "X-LATENCY-CHECK": "true" }, - }) - }) + }); + }); // When all the proxy requests finish void Promise.all(proxyRequests) @@ -215,22 +215,22 @@ export const useProxyLatency = ( // We want to call this before we disconnect the observer to make sure we get all the // proxy requests recorded. observer.takeRecords().forEach((entry) => { - dispatchProxyLatenciesGuarded(entry) - }) + dispatchProxyLatenciesGuarded(entry); + }); // At this point, we can be confident that all the proxy requests have been recorded // via the performance observer. So we can disconnect the observer. - observer.disconnect() + observer.disconnect(); // Local storage cleanup - garbageCollectStoredLatencies(proxies, maxStoredLatencies) - }) - }, [proxies, latestFetchRequest, maxStoredLatencies]) + garbageCollectStoredLatencies(proxies, maxStoredLatencies); + }); + }, [proxies, latestFetchRequest, maxStoredLatencies]); return { proxyLatencies, refetch, - } -} + }; +}; // Local storage functions @@ -239,28 +239,28 @@ export const useProxyLatency = ( // If a single request is slow, we want to omit that latency check, and go with // a more accurate latency check. const loadStoredLatencies = (): Record => { - const str = localStorage.getItem("workspace-proxy-latencies") + const str = localStorage.getItem("workspace-proxy-latencies"); if (!str) { - return {} + return {}; } return JSON.parse(str, (key, value) => { // By default json loads dates as strings. We want to convert them back to 'Date's. if (key === "at") { - return new Date(value) + return new Date(value); } - return value - }) -} + return value; + }); +}; const updateStoredLatencies = (action: ProxyLatencyAction): void => { - const latencies = loadStoredLatencies() - const reports = latencies[action.proxyID] || [] + const latencies = loadStoredLatencies(); + const reports = latencies[action.proxyID] || []; - reports.push(action.report) - latencies[action.proxyID] = reports - localStorage.setItem("workspace-proxy-latencies", JSON.stringify(latencies)) -} + reports.push(action.report); + latencies[action.proxyID] = reports; + localStorage.setItem("workspace-proxy-latencies", JSON.stringify(latencies)); +}; // garbageCollectStoredLatencies will remove any latencies that are older then 1 week or latencies of proxies // that no longer exist. This is intended to keep the size of local storage down. @@ -268,12 +268,17 @@ const garbageCollectStoredLatencies = ( regions: Region[], maxStored: number, ): void => { - const latencies = loadStoredLatencies() - const now = Date.now() - const cleaned = cleanupLatencies(latencies, regions, new Date(now), maxStored) + const latencies = loadStoredLatencies(); + const now = Date.now(); + const cleaned = cleanupLatencies( + latencies, + regions, + new Date(now), + maxStored, + ); - localStorage.setItem("workspace-proxy-latencies", JSON.stringify(cleaned)) -} + localStorage.setItem("workspace-proxy-latencies", JSON.stringify(cleaned)); +}; const cleanupLatencies = ( stored: Record, @@ -283,17 +288,17 @@ const cleanupLatencies = ( ): Record => { Object.keys(stored).forEach((proxyID) => { if (!regions.find((region) => region.id === proxyID)) { - delete stored[proxyID] - return + delete stored[proxyID]; + return; } - const reports = stored[proxyID] - const nowMS = now.getTime() + const reports = stored[proxyID]; + const nowMS = now.getTime(); stored[proxyID] = reports.filter((report) => { // Only keep the reports that are less then 1 week old. - return new Date(report.at).getTime() > nowMS - 1000 * 60 * 60 * 24 * 7 - }) + return new Date(report.at).getTime() > nowMS - 1000 * 60 * 60 * 24 * 7; + }); // Only keep the 5 latest - stored[proxyID] = stored[proxyID].slice(-1 * maxStored) - }) - return stored -} + stored[proxyID] = stored[proxyID].slice(-1 * maxStored); + }); + return stored; +}; diff --git a/site/src/hooks/events.test.ts b/site/src/hooks/events.test.ts index df75de7626..6187b77be7 100644 --- a/site/src/hooks/events.test.ts +++ b/site/src/hooks/events.test.ts @@ -1,16 +1,16 @@ -import { renderHook, waitFor } from "@testing-library/react" -import { dispatchCustomEvent } from "../utils/events" -import { useCustomEvent } from "./events" +import { renderHook, waitFor } from "@testing-library/react"; +import { dispatchCustomEvent } from "../utils/events"; +import { useCustomEvent } from "./events"; describe("useCustomEvent", () => { it("should listem a custom event", async () => { - const callback = jest.fn() - const detail = { title: "Test event" } - renderHook(() => useCustomEvent("testEvent", callback)) - dispatchCustomEvent("testEvent", detail) + const callback = jest.fn(); + const detail = { title: "Test event" }; + renderHook(() => useCustomEvent("testEvent", callback)); + dispatchCustomEvent("testEvent", detail); await waitFor(() => { - expect(callback).toBeCalledTimes(1) - }) - expect(callback.mock.calls[0][0].detail).toBe(detail) - }) -}) + expect(callback).toBeCalledTimes(1); + }); + expect(callback.mock.calls[0][0].detail).toBe(detail); + }); +}); diff --git a/site/src/hooks/events.ts b/site/src/hooks/events.ts index 2060e7cc03..2d43ae102f 100644 --- a/site/src/hooks/events.ts +++ b/site/src/hooks/events.ts @@ -1,5 +1,5 @@ -import { useEffect } from "react" -import { CustomEventListener } from "../utils/events" +import { useEffect } from "react"; +import { CustomEventListener } from "../utils/events"; /** * Handles a custom event with descriptive type information. @@ -13,12 +13,12 @@ export const useCustomEvent = ( ): void => { useEffect(() => { const handleEvent: CustomEventListener = (event) => { - listener(event) - } - window.addEventListener(eventType, handleEvent as EventListener) + listener(event); + }; + window.addEventListener(eventType, handleEvent as EventListener); return () => { - window.removeEventListener(eventType, handleEvent as EventListener) - } - }, [eventType, listener]) -} + window.removeEventListener(eventType, handleEvent as EventListener); + }; + }, [eventType, listener]); +}; diff --git a/site/src/hooks/index.ts b/site/src/hooks/index.ts index 613260603f..16b46cd134 100644 --- a/site/src/hooks/index.ts +++ b/site/src/hooks/index.ts @@ -1,10 +1,10 @@ -export * from "./useClickable" -export * from "./useClickableTableRow" -export * from "./useClipboard" -export * from "./useFeatureVisibility" -export * from "./useLocalStorage" -export * from "./useMe" -export * from "./useOrganizationId" -export * from "./usePagination" -export * from "./usePermissions" -export * from "./useTab" +export * from "./useClickable"; +export * from "./useClickableTableRow"; +export * from "./useClipboard"; +export * from "./useFeatureVisibility"; +export * from "./useLocalStorage"; +export * from "./useMe"; +export * from "./useOrganizationId"; +export * from "./usePagination"; +export * from "./usePermissions"; +export * from "./useTab"; diff --git a/site/src/hooks/useClickable.ts b/site/src/hooks/useClickable.ts index 0c761d7106..80644efd06 100644 --- a/site/src/hooks/useClickable.ts +++ b/site/src/hooks/useClickable.ts @@ -1,10 +1,10 @@ -import { KeyboardEvent } from "react" +import { KeyboardEvent } from "react"; export interface UseClickableResult { - tabIndex: 0 - role: "button" - onClick: () => void - onKeyDown: (event: KeyboardEvent) => void + tabIndex: 0; + role: "button"; + onClick: () => void; + onKeyDown: (event: KeyboardEvent) => void; } export const useClickable = (onClick: () => void): UseClickableResult => { @@ -14,8 +14,8 @@ export const useClickable = (onClick: () => void): UseClickableResult => { onClick, onKeyDown: (event: KeyboardEvent) => { if (event.key === "Enter") { - onClick() + onClick(); } }, - } -} + }; +}; diff --git a/site/src/hooks/useClickableTableRow.ts b/site/src/hooks/useClickableTableRow.ts index 002e76c92d..09a9366a9a 100644 --- a/site/src/hooks/useClickableTableRow.ts +++ b/site/src/hooks/useClickableTableRow.ts @@ -1,23 +1,23 @@ -import { makeStyles } from "@mui/styles" -import { useClickable, UseClickableResult } from "./useClickable" +import { makeStyles } from "@mui/styles"; +import { useClickable, UseClickableResult } from "./useClickable"; interface UseClickableTableRowResult extends UseClickableResult { - className: string - hover: true + className: string; + hover: true; } export const useClickableTableRow = ( onClick: () => void, ): UseClickableTableRowResult => { - const styles = useStyles() - const clickable = useClickable(onClick) + const styles = useStyles(); + const clickable = useClickable(onClick); return { ...clickable, className: styles.row, hover: true, - } -} + }; +}; const useStyles = makeStyles((theme) => ({ row: { @@ -33,4 +33,4 @@ const useStyles = makeStyles((theme) => ({ borderBottomRightRadius: theme.shape.borderRadius, }, }, -})) +})); diff --git a/site/src/hooks/useClipboard.ts b/site/src/hooks/useClipboard.ts index 3737ac64ec..0acad67a26 100644 --- a/site/src/hooks/useClipboard.ts +++ b/site/src/hooks/useClipboard.ts @@ -1,44 +1,44 @@ -import { useState } from "react" +import { useState } from "react"; export const useClipboard = ( text: string, ): { isCopied: boolean; copy: () => Promise } => { - const [isCopied, setIsCopied] = useState(false) + const [isCopied, setIsCopied] = useState(false); const copy = async (): Promise => { try { - await window.navigator.clipboard.writeText(text) - setIsCopied(true) + await window.navigator.clipboard.writeText(text); + setIsCopied(true); window.setTimeout(() => { - setIsCopied(false) - }, 1000) + setIsCopied(false); + }, 1000); } catch (err) { - const input = document.createElement("input") - input.value = text - document.body.appendChild(input) - input.focus() - input.select() - const result = document.execCommand("copy") - document.body.removeChild(input) + const input = document.createElement("input"); + input.value = text; + document.body.appendChild(input); + input.focus(); + input.select(); + const result = document.execCommand("copy"); + document.body.removeChild(input); if (result) { - setIsCopied(true) + setIsCopied(true); window.setTimeout(() => { - setIsCopied(false) - }, 1000) + setIsCopied(false); + }, 1000); } else { const wrappedErr = new Error( "copyToClipboard: failed to copy text to clipboard", - ) + ); if (err instanceof Error) { - wrappedErr.stack = err.stack + wrappedErr.stack = err.stack; } - console.error(wrappedErr) + console.error(wrappedErr); } } - } + }; return { isCopied, copy, - } -} + }; +}; diff --git a/site/src/hooks/useFeatureVisibility.ts b/site/src/hooks/useFeatureVisibility.ts index d12c701685..628fad562f 100644 --- a/site/src/hooks/useFeatureVisibility.ts +++ b/site/src/hooks/useFeatureVisibility.ts @@ -1,8 +1,8 @@ -import { FeatureName } from "api/typesGenerated" -import { useDashboard } from "components/Dashboard/DashboardProvider" -import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors" +import { FeatureName } from "api/typesGenerated"; +import { useDashboard } from "components/Dashboard/DashboardProvider"; +import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors"; export const useFeatureVisibility = (): Record => { - const { entitlements } = useDashboard() - return selectFeatureVisibility(entitlements) -} + const { entitlements } = useDashboard(); + return selectFeatureVisibility(entitlements); +}; diff --git a/site/src/hooks/useLocalStorage.ts b/site/src/hooks/useLocalStorage.ts index bae3bf5fa7..10ae288990 100644 --- a/site/src/hooks/useLocalStorage.ts +++ b/site/src/hooks/useLocalStorage.ts @@ -3,17 +3,17 @@ export const useLocalStorage = () => { saveLocal, getLocal, clearLocal, - } -} + }; +}; const saveLocal = (itemKey: string, itemValue: string): void => { - window.localStorage.setItem(itemKey, itemValue) -} + window.localStorage.setItem(itemKey, itemValue); +}; const getLocal = (itemKey: string): string | undefined => { - return localStorage.getItem(itemKey) ?? undefined -} + return localStorage.getItem(itemKey) ?? undefined; +}; const clearLocal = (itemKey: string): void => { - localStorage.removeItem(itemKey) -} + localStorage.removeItem(itemKey); +}; diff --git a/site/src/hooks/useMe.ts b/site/src/hooks/useMe.ts index bd0ac6bf59..2d15cad0f1 100644 --- a/site/src/hooks/useMe.ts +++ b/site/src/hooks/useMe.ts @@ -1,14 +1,14 @@ -import { User } from "api/typesGenerated" -import { useAuth } from "components/AuthProvider/AuthProvider" -import { isAuthenticated } from "xServices/auth/authXService" +import { User } from "api/typesGenerated"; +import { useAuth } from "components/AuthProvider/AuthProvider"; +import { isAuthenticated } from "xServices/auth/authXService"; export const useMe = (): User => { - const [authState] = useAuth() - const { data } = authState.context + const [authState] = useAuth(); + const { data } = authState.context; if (isAuthenticated(data)) { - return data.user + return data.user; } - throw new Error("User is not authenticated") -} + throw new Error("User is not authenticated"); +}; diff --git a/site/src/hooks/useOrganizationId.ts b/site/src/hooks/useOrganizationId.ts index 9f1c382c0e..c090d988e2 100644 --- a/site/src/hooks/useOrganizationId.ts +++ b/site/src/hooks/useOrganizationId.ts @@ -1,13 +1,13 @@ -import { useAuth } from "components/AuthProvider/AuthProvider" -import { isAuthenticated } from "xServices/auth/authXService" +import { useAuth } from "components/AuthProvider/AuthProvider"; +import { isAuthenticated } from "xServices/auth/authXService"; export const useOrganizationId = (): string => { - const [authState] = useAuth() - const { data } = authState.context + const [authState] = useAuth(); + const { data } = authState.context; if (isAuthenticated(data)) { - return data.user.organization_ids[0] + return data.user.organization_ids[0]; } - throw new Error("User is not authenticated") -} + throw new Error("User is not authenticated"); +}; diff --git a/site/src/hooks/usePagination.ts b/site/src/hooks/usePagination.ts index a610fc4ab0..d859bc4248 100644 --- a/site/src/hooks/usePagination.ts +++ b/site/src/hooks/usePagination.ts @@ -1,23 +1,23 @@ -import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils" -import { useSearchParams } from "react-router-dom" +import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils"; +import { useSearchParams } from "react-router-dom"; export const usePagination = ({ searchParamsResult, }: { - searchParamsResult: ReturnType + searchParamsResult: ReturnType; }) => { - const [searchParams, setSearchParams] = searchParamsResult - const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1 - const limit = DEFAULT_RECORDS_PER_PAGE + const [searchParams, setSearchParams] = searchParamsResult; + const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1; + const limit = DEFAULT_RECORDS_PER_PAGE; const goToPage = (page: number) => { - searchParams.set("page", page.toString()) - setSearchParams(searchParams) - } + searchParams.set("page", page.toString()); + setSearchParams(searchParams); + }; return { page, limit, goToPage, - } -} + }; +}; diff --git a/site/src/hooks/usePermissions.ts b/site/src/hooks/usePermissions.ts index 75e86c1dfc..b04bc0d189 100644 --- a/site/src/hooks/usePermissions.ts +++ b/site/src/hooks/usePermissions.ts @@ -1,13 +1,13 @@ -import { useAuth } from "components/AuthProvider/AuthProvider" -import { isAuthenticated, Permissions } from "xServices/auth/authXService" +import { useAuth } from "components/AuthProvider/AuthProvider"; +import { isAuthenticated, Permissions } from "xServices/auth/authXService"; export const usePermissions = (): Permissions => { - const [authState] = useAuth() - const { data } = authState.context + const [authState] = useAuth(); + const { data } = authState.context; if (isAuthenticated(data)) { - return data.permissions + return data.permissions; } - throw new Error("User is not authenticated.") -} + throw new Error("User is not authenticated."); +}; diff --git a/site/src/hooks/useTab.ts b/site/src/hooks/useTab.ts index 846a98c2f0..fce1679ae2 100644 --- a/site/src/hooks/useTab.ts +++ b/site/src/hooks/useTab.ts @@ -1,19 +1,19 @@ -import { useSearchParams } from "react-router-dom" +import { useSearchParams } from "react-router-dom"; export interface UseTabResult { - value: string - set: (value: string) => void + value: string; + set: (value: string) => void; } export const useTab = (tabKey: string, defaultValue: string): UseTabResult => { - const [searchParams, setSearchParams] = useSearchParams() - const value = searchParams.get(tabKey) ?? defaultValue + const [searchParams, setSearchParams] = useSearchParams(); + const value = searchParams.get(tabKey) ?? defaultValue; return { value, set: (value: string) => { - searchParams.set(tabKey, value) - setSearchParams(searchParams, { replace: true }) + searchParams.set(tabKey, value); + setSearchParams(searchParams, { replace: true }); }, - } -} + }; +}; diff --git a/site/src/i18n/en/index.ts b/site/src/i18n/en/index.ts index d1ce33a2e2..4e30de23b6 100644 --- a/site/src/i18n/en/index.ts +++ b/site/src/i18n/en/index.ts @@ -1,25 +1,25 @@ -import auditLog from "./auditLog.json" -import common from "./common.json" -import createWorkspacePage from "./createWorkspacePage.json" -import templatePage from "./templatePage.json" -import templatesPage from "./templatesPage.json" -import workspacePage from "./workspacePage.json" -import agent from "./agent.json" -import buildPage from "./buildPage.json" -import workspacesPage from "./workspacesPage.json" -import usersPage from "./usersPage.json" -import templateSettingsPage from "./templateSettingsPage.json" -import templateVariablesPage from "./templateVariablesPage.json" -import templateVersionPage from "./templateVersionPage.json" -import loginPage from "./loginPage.json" -import workspaceSchedulePage from "./workspaceSchedulePage.json" -import appearanceSettings from "./appearanceSettings.json" -import starterTemplatesPage from "./starterTemplatesPage.json" -import starterTemplatePage from "./starterTemplatePage.json" -import createTemplatePage from "./createTemplatePage.json" -import userSettingsPage from "./userSettingsPage.json" -import tokensPage from "./tokensPage.json" -import workspaceSettingsPage from "./workspaceSettingsPage.json" +import auditLog from "./auditLog.json"; +import common from "./common.json"; +import createWorkspacePage from "./createWorkspacePage.json"; +import templatePage from "./templatePage.json"; +import templatesPage from "./templatesPage.json"; +import workspacePage from "./workspacePage.json"; +import agent from "./agent.json"; +import buildPage from "./buildPage.json"; +import workspacesPage from "./workspacesPage.json"; +import usersPage from "./usersPage.json"; +import templateSettingsPage from "./templateSettingsPage.json"; +import templateVariablesPage from "./templateVariablesPage.json"; +import templateVersionPage from "./templateVersionPage.json"; +import loginPage from "./loginPage.json"; +import workspaceSchedulePage from "./workspaceSchedulePage.json"; +import appearanceSettings from "./appearanceSettings.json"; +import starterTemplatesPage from "./starterTemplatesPage.json"; +import starterTemplatePage from "./starterTemplatePage.json"; +import createTemplatePage from "./createTemplatePage.json"; +import userSettingsPage from "./userSettingsPage.json"; +import tokensPage from "./tokensPage.json"; +import workspaceSettingsPage from "./workspaceSettingsPage.json"; export const en = { common, @@ -44,4 +44,4 @@ export const en = { userSettingsPage, tokensPage, workspaceSettingsPage, -} +}; diff --git a/site/src/i18n/i18n.ts b/site/src/i18n/i18n.ts index da44a43afd..dcf297d643 100644 --- a/site/src/i18n/i18n.ts +++ b/site/src/i18n/i18n.ts @@ -1,11 +1,11 @@ -import i18next from "i18next" -import { initReactI18next } from "react-i18next" -import { en } from "./en" +import i18next from "i18next"; +import { initReactI18next } from "react-i18next"; +import { en } from "./en"; -export const defaultNS = "common" -export const resources = { en } as const +export const defaultNS = "common"; +export const resources = { en } as const; -export const i18n = i18next.use(initReactI18next) +export const i18n = i18next.use(initReactI18next); i18n .init({ @@ -17,5 +17,5 @@ i18n }) .catch((error) => { // we are catching here to avoid lint's no-floating-promises error - console.error("[Translation Service]:", error) - }) + console.error("[Translation Service]:", error); + }); diff --git a/site/src/i18n/index.ts b/site/src/i18n/index.ts index ec2c30488a..0c4205e06f 100644 --- a/site/src/i18n/index.ts +++ b/site/src/i18n/index.ts @@ -1 +1 @@ -export * from "./i18n" +export * from "./i18n"; diff --git a/site/src/pages/404Page/404Page.tsx b/site/src/pages/404Page/404Page.tsx index 77c05842ce..1d87b26f2b 100644 --- a/site/src/pages/404Page/404Page.tsx +++ b/site/src/pages/404Page/404Page.tsx @@ -1,9 +1,9 @@ -import { makeStyles } from "@mui/styles" -import Typography from "@mui/material/Typography" -import { FC } from "react" +import { makeStyles } from "@mui/styles"; +import Typography from "@mui/material/Typography"; +import { FC } from "react"; export const NotFoundPage: FC = () => { - const styles = useStyles() + const styles = useStyles(); return (
@@ -12,8 +12,8 @@ export const NotFoundPage: FC = () => {
This page could not be found. - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ root: { @@ -29,6 +29,6 @@ const useStyles = makeStyles((theme) => ({ padding: theme.spacing(1), borderRight: theme.palette.divider, }, -})) +})); -export default NotFoundPage +export default NotFoundPage; diff --git a/site/src/pages/AuditPage/AuditFilter.tsx b/site/src/pages/AuditPage/AuditFilter.tsx index 882285fe8a..e74f01aff3 100644 --- a/site/src/pages/AuditPage/AuditFilter.tsx +++ b/site/src/pages/AuditPage/AuditFilter.tsx @@ -1,5 +1,5 @@ -import { AuditActions, ResourceTypes } from "api/typesGenerated" -import { UserFilterMenu, UserMenu } from "components/Filter/UserFilter" +import { AuditActions, ResourceTypes } from "api/typesGenerated"; +import { UserFilterMenu, UserMenu } from "components/Filter/UserFilter"; import { Filter, FilterMenu, @@ -7,11 +7,11 @@ import { OptionItem, SearchFieldSkeleton, useFilter, -} from "components/Filter/filter" -import { UseFilterMenuOptions, useFilterMenu } from "components/Filter/menu" -import { BaseOption } from "components/Filter/options" -import capitalize from "lodash/capitalize" -import { docs } from "utils/docs" +} from "components/Filter/filter"; +import { UseFilterMenuOptions, useFilterMenu } from "components/Filter/menu"; +import { BaseOption } from "components/Filter/options"; +import capitalize from "lodash/capitalize"; +import { docs } from "utils/docs"; const PRESET_FILTERS = [ { @@ -28,20 +28,20 @@ const PRESET_FILTERS = [ query: "resource_type:api_key action:login", name: "User logins", }, -] +]; export const AuditFilter = ({ filter, error, menus, }: { - filter: ReturnType - error?: unknown + filter: ReturnType; + error?: unknown; menus: { - user: UserFilterMenu - action: ActionFilterMenu - resourceType: ResourceTypeFilterMenu - } + user: UserFilterMenu; + action: ActionFilterMenu; + resourceType: ResourceTypeFilterMenu; + }; }) => { return ( } /> - ) -} + ); +}; export const useActionFilterMenu = ({ value, @@ -76,7 +76,7 @@ export const useActionFilterMenu = ({ const actionOptions: BaseOption[] = AuditActions.map((action) => ({ value: action, label: capitalize(action), - })) + })); return useFilterMenu({ onChange, value, @@ -84,10 +84,10 @@ export const useActionFilterMenu = ({ getSelectedOption: async () => actionOptions.find((option) => option.value === value) ?? null, getOptions: async () => actionOptions, - }) -} + }); +}; -export type ActionFilterMenu = ReturnType +export type ActionFilterMenu = ReturnType; const ActionMenu = (menu: ActionFilterMenu) => { return ( @@ -104,37 +104,37 @@ const ActionMenu = (menu: ActionFilterMenu) => { > {(itemProps) => } - ) -} + ); +}; export const useResourceTypeFilterMenu = ({ value, onChange, }: Pick, "value" | "onChange">) => { const actionOptions: BaseOption[] = ResourceTypes.map((type) => { - let label = capitalize(type) + let label = capitalize(type); if (type === "api_key") { - label = "API Key" + label = "API Key"; } if (type === "git_ssh_key") { - label = "Git SSH Key" + label = "Git SSH Key"; } if (type === "template_version") { - label = "Template Version" + label = "Template Version"; } if (type === "workspace_build") { - label = "Workspace Build" + label = "Workspace Build"; } return { value: type, label, - } - }) + }; + }); return useFilterMenu({ onChange, value, @@ -142,12 +142,12 @@ export const useResourceTypeFilterMenu = ({ getSelectedOption: async () => actionOptions.find((option) => option.value === value) ?? null, getOptions: async () => actionOptions, - }) -} + }); +}; export type ResourceTypeFilterMenu = ReturnType< typeof useResourceTypeFilterMenu -> +>; const ResourceTypeMenu = (menu: ResourceTypeFilterMenu) => { return ( @@ -164,5 +164,5 @@ const ResourceTypeMenu = (menu: ResourceTypeFilterMenu) => { > {(itemProps) => } - ) -} + ); +}; diff --git a/site/src/pages/AuditPage/AuditHelpTooltip.tsx b/site/src/pages/AuditPage/AuditHelpTooltip.tsx index 1a6da31a79..597402adb7 100644 --- a/site/src/pages/AuditPage/AuditHelpTooltip.tsx +++ b/site/src/pages/AuditPage/AuditHelpTooltip.tsx @@ -1,18 +1,18 @@ -import { FC } from "react" +import { FC } from "react"; import { HelpTooltip, HelpTooltipLink, HelpTooltipLinksGroup, HelpTooltipText, HelpTooltipTitle, -} from "components/HelpTooltip/HelpTooltip" -import { docs } from "utils/docs" +} from "components/HelpTooltip/HelpTooltip"; +import { docs } from "utils/docs"; export const Language = { title: "What is an audit log?", body: "An audit log is a record of events and changes made throughout a system.", docs: "Events we track", -} +}; export const AuditHelpTooltip: FC = () => { return ( @@ -25,5 +25,5 @@ export const AuditHelpTooltip: FC = () => { - ) -} + ); +}; diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.test.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.test.tsx index be9be76b5a..cbe305c75e 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.test.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.test.tsx @@ -4,39 +4,39 @@ import { MockWorkspaceCreateAuditLogForDifferentOwner, MockAuditLogSuccessfulLogin, MockAuditLogUnsuccessfulLoginKnownUser, -} from "testHelpers/entities" -import { AuditLogDescription } from "./AuditLogDescription" -import { AuditLogRow } from "../AuditLogRow" -import { render } from "testHelpers/renderHelpers" -import { screen } from "@testing-library/react" -import { i18n } from "i18n" +} from "testHelpers/entities"; +import { AuditLogDescription } from "./AuditLogDescription"; +import { AuditLogRow } from "../AuditLogRow"; +import { render } from "testHelpers/renderHelpers"; +import { screen } from "@testing-library/react"; +import { i18n } from "i18n"; const t = (str: string, variables: Record) => - i18n.t(str, variables) + i18n.t(str, variables); const getByTextContent = (text: string) => { return screen.getByText((_, element) => { - const hasText = (element: Element | null) => element?.textContent === text - const elementHasText = hasText(element) + const hasText = (element: Element | null) => element?.textContent === text; + const elementHasText = hasText(element); const childrenDontHaveText = Array.from(element?.children || []).every( (child) => !hasText(child), - ) - return elementHasText && childrenDontHaveText - }) -} + ); + return elementHasText && childrenDontHaveText; + }); +}; describe("AuditLogDescription", () => { it("renders the correct string for a workspace create audit log", async () => { - render() + render(); - expect(screen.getByText("TestUser created workspace")).toBeDefined() - expect(screen.getByText("bruno-dev")).toBeDefined() - }) + expect(screen.getByText("TestUser created workspace")).toBeDefined(); + expect(screen.getByText("bruno-dev")).toBeDefined(); + }); it("renders the correct string for a workspace_build stop audit log", async () => { - render() + render(); - expect(getByTextContent("TestUser stopped workspace test2")).toBeDefined() - }) + expect(getByTextContent("TestUser stopped workspace test2")).toBeDefined(); + }); it("renders the correct string for a workspace_build audit log with a duplicate word", async () => { const AuditLogWithRepeat = { @@ -44,29 +44,29 @@ describe("AuditLogDescription", () => { additional_fields: { workspace_name: "workspace", }, - } - render() + }; + render(); expect( getByTextContent("TestUser stopped workspace workspace"), - ).toBeDefined() - }) + ).toBeDefined(); + }); it("renders the correct string for a workspace created for a different owner", async () => { render( , - ) + ); expect( screen.getByText( `on behalf of ${MockWorkspaceCreateAuditLogForDifferentOwner.additional_fields.workspace_owner}`, { exact: false }, ), - ).toBeDefined() - }) + ).toBeDefined(); + }); it("renders the correct string for successful login", async () => { - render() + render(); expect( screen.getByText( @@ -79,13 +79,13 @@ describe("AuditLogDescription", () => { .replace(/\s{2,}/g, " ") .trim(), ), - ).toBeInTheDocument() + ).toBeInTheDocument(); - const statusPill = screen.getByRole("status") - expect(statusPill).toHaveTextContent("201") - }) + const statusPill = screen.getByRole("status"); + expect(statusPill).toHaveTextContent("201"); + }); it("renders the correct string for unsuccessful login for a known user", async () => { - render() + render(); expect( screen.getByText( @@ -98,9 +98,9 @@ describe("AuditLogDescription", () => { .replace(/\s{2,}/g, " ") .trim(), ), - ).toBeInTheDocument() + ).toBeInTheDocument(); - const statusPill = screen.getByRole("status") - expect(statusPill).toHaveTextContent("401") - }) -}) + const statusPill = screen.getByRole("status"); + expect(statusPill).toHaveTextContent("401"); + }); +}); diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx index 7680a98d60..8799641e14 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx @@ -1,30 +1,30 @@ -import { FC } from "react" -import { AuditLog } from "api/typesGenerated" -import { Link as RouterLink } from "react-router-dom" -import Link from "@mui/material/Link" -import { Trans, useTranslation } from "react-i18next" -import { BuildAuditDescription } from "./BuildAuditDescription" +import { FC } from "react"; +import { AuditLog } from "api/typesGenerated"; +import { Link as RouterLink } from "react-router-dom"; +import Link from "@mui/material/Link"; +import { Trans, useTranslation } from "react-i18next"; +import { BuildAuditDescription } from "./BuildAuditDescription"; export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({ auditLog, }): JSX.Element => { - const { t } = useTranslation("auditLog") + const { t } = useTranslation("auditLog"); - let target = auditLog.resource_target.trim() - const user = auditLog.user?.username.trim() + let target = auditLog.resource_target.trim(); + const user = auditLog.user?.username.trim(); if (auditLog.resource_type === "workspace_build") { - return + return ; } // SSH key entries have no links if (auditLog.resource_type === "git_ssh_key") { - target = "" + target = ""; } const truncatedDescription = auditLog.description .replace("{user}", `${user}`) - .replace("{target}", "") + .replace("{target}", ""); // logs for workspaces created on behalf of other users indicate ownership in the description const onBehalfOf = @@ -32,7 +32,7 @@ export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({ auditLog.additional_fields.workspace_owner !== "unknown" && auditLog.additional_fields.workspace_owner.trim() !== user ? `on behalf of ${auditLog.additional_fields.workspace_owner}` - : "" + : ""; if (auditLog.resource_link) { return ( @@ -49,7 +49,7 @@ export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({ {"{{onBehalfOf}}"} - ) + ); } return ( @@ -64,5 +64,5 @@ export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({ {"{{onBehalfOf}}"} - ) -} + ); +}; diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/BuildAuditDescription.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/BuildAuditDescription.tsx index 7efad10221..36d6fa691a 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/BuildAuditDescription.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/BuildAuditDescription.tsx @@ -1,34 +1,34 @@ -import { Trans, useTranslation } from "react-i18next" -import { AuditLog } from "api/typesGenerated" -import { FC } from "react" -import { Link as RouterLink } from "react-router-dom" -import Link from "@mui/material/Link" +import { Trans, useTranslation } from "react-i18next"; +import { AuditLog } from "api/typesGenerated"; +import { FC } from "react"; +import { Link as RouterLink } from "react-router-dom"; +import Link from "@mui/material/Link"; export const BuildAuditDescription: FC<{ auditLog: AuditLog }> = ({ auditLog, }): JSX.Element => { - const { t } = useTranslation("auditLog") + const { t } = useTranslation("auditLog"); - const workspaceName = auditLog.additional_fields?.workspace_name?.trim() + const workspaceName = auditLog.additional_fields?.workspace_name?.trim(); // workspaces can be started/stopped/deleted by a user, or kicked off automatically by Coder const user = auditLog.additional_fields?.build_reason && auditLog.additional_fields?.build_reason !== "initiator" ? "Coder automatically" - : auditLog.user?.username.trim() + : auditLog.user?.username.trim(); const action: string = (() => { switch (auditLog.action) { case "start": - return "started" + return "started"; case "stop": - return "stopped" + return "stopped"; case "delete": - return "deleted" + return "deleted"; default: - return auditLog.action + return auditLog.action; } - })() + })(); if (auditLog.resource_link) { return ( @@ -45,7 +45,7 @@ export const BuildAuditDescription: FC<{ auditLog: AuditLog }> = ({ workspace{"{{workspaceName}}"} - ) + ); } return ( @@ -59,5 +59,5 @@ export const BuildAuditDescription: FC<{ auditLog: AuditLog }> = ({ {"{{action}}"}workspace{"{{workspaceName}}"} - ) -} + ); +}; diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/index.ts b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/index.ts index 590ff7c7b0..03c56cd0e8 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/index.ts +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/index.ts @@ -1 +1 @@ -export { AuditLogDescription } from "./AuditLogDescription" +export { AuditLogDescription } from "./AuditLogDescription"; diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/AuditLogDiff.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/AuditLogDiff.tsx index 6cb1c44cd5..857c01fc1b 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/AuditLogDiff.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/AuditLogDiff.tsx @@ -1,30 +1,30 @@ -import { makeStyles } from "@mui/styles" -import { AuditLog } from "api/typesGenerated" -import { colors } from "theme/colors" -import { MONOSPACE_FONT_FAMILY } from "theme/constants" -import { combineClasses } from "utils/combineClasses" -import { FC } from "react" +import { makeStyles } from "@mui/styles"; +import { AuditLog } from "api/typesGenerated"; +import { colors } from "theme/colors"; +import { MONOSPACE_FONT_FAMILY } from "theme/constants"; +import { combineClasses } from "utils/combineClasses"; +import { FC } from "react"; const getDiffValue = (value: unknown): string => { if (typeof value === "string") { - return `"${value}"` + return `"${value}"`; } if (Array.isArray(value)) { - const values = value.map((v) => getDiffValue(v)) - return `[${values.join(", ")}]` + const values = value.map((v) => getDiffValue(v)); + return `[${values.join(", ")}]`; } if (value === null || value === undefined) { - return "null" + return "null"; } - return String(value) -} + return String(value); +}; export const AuditLogDiff: FC<{ diff: AuditLog["diff"] }> = ({ diff }) => { - const styles = useStyles() - const diffEntries = Object.entries(diff) + const styles = useStyles(); + const diffEntries = Object.entries(diff); return (
@@ -67,8 +67,8 @@ export const AuditLogDiff: FC<{ diff: AuditLog["diff"] }> = ({ diff }) => { ))}
- ) -} + ); +}; const useStyles = makeStyles((theme) => ({ diff: { @@ -132,4 +132,4 @@ const useStyles = makeStyles((theme) => ({ diffValueNew: { backgroundColor: colors.green[12], }, -})) +})); diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/auditUtils.test.ts b/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/auditUtils.test.ts index 957204f9bc..94c83c0cc4 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/auditUtils.test.ts +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/auditUtils.test.ts @@ -1,4 +1,4 @@ -import { determineGroupDiff } from "./auditUtils" +import { determineGroupDiff } from "./auditUtils"; const auditDiffForNewGroup = { id: { @@ -15,7 +15,7 @@ const auditDiffForNewGroup = { new: "another-test-group", secret: false, }, -} +}; const auditDiffForAddedGroupMember = { members: { @@ -28,7 +28,7 @@ const auditDiffForAddedGroupMember = { ], secret: false, }, -} +}; const auditDiffForRemovedGroupMember = { members: { @@ -50,7 +50,7 @@ const auditDiffForRemovedGroupMember = { ], secret: false, }, -} +}; const AuditDiffForDeletedGroup = { id: { @@ -72,15 +72,15 @@ const AuditDiffForDeletedGroup = { new: "", secret: false, }, -} +}; describe("determineAuditDiff", () => { it("auditDiffForNewGroup", () => { // there should be no change as members are not added when a group is created expect(determineGroupDiff(auditDiffForNewGroup)).toEqual( auditDiffForNewGroup, - ) - }) + ); + }); it("auditDiffForAddedGroupMember", () => { const result = { @@ -88,10 +88,10 @@ describe("determineAuditDiff", () => { ...auditDiffForAddedGroupMember.members, new: ["cea4c2b0-6373-4858-b26a-df3cbfce8845"], }, - } + }; - expect(determineGroupDiff(auditDiffForAddedGroupMember)).toEqual(result) - }) + expect(determineGroupDiff(auditDiffForAddedGroupMember)).toEqual(result); + }); it("auditDiffForRemovedGroupMember", () => { const result = { @@ -103,10 +103,10 @@ describe("determineAuditDiff", () => { ], new: ["84d1cd5a-17e1-4022-898c-52e64256e737"], }, - } + }; - expect(determineGroupDiff(auditDiffForRemovedGroupMember)).toEqual(result) - }) + expect(determineGroupDiff(auditDiffForRemovedGroupMember)).toEqual(result); + }); it("AuditDiffForDeletedGroup", () => { const result = { @@ -115,8 +115,8 @@ describe("determineAuditDiff", () => { ...AuditDiffForDeletedGroup.members, old: ["84d1cd5a-17e1-4022-898c-52e64256e737"], }, - } + }; - expect(determineGroupDiff(AuditDiffForDeletedGroup)).toEqual(result) - }) -}) + expect(determineGroupDiff(AuditDiffForDeletedGroup)).toEqual(result); + }); +}); diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/auditUtils.ts b/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/auditUtils.ts index 05f53912e5..ed3cd917ca 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/auditUtils.ts +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/auditUtils.ts @@ -1,8 +1,8 @@ -import { AuditDiff } from "api/typesGenerated" +import { AuditDiff } from "api/typesGenerated"; interface GroupMember { - user_id: string - group_id: string + user_id: string; + group_id: string; } /** @@ -22,5 +22,5 @@ export const determineGroupDiff = (auditLogDiff: AuditDiff): AuditDiff => { ), secret: auditLogDiff.members?.secret, }, - } -} + }; +}; diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/index.ts b/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/index.ts index 7243291182..1e4c4c34a5 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/index.ts +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogDiff/index.ts @@ -1,2 +1,2 @@ -export { AuditLogDiff } from "./AuditLogDiff" -export { determineGroupDiff } from "./auditUtils" +export { AuditLogDiff } from "./AuditLogDiff"; +export { determineGroupDiff } from "./auditUtils"; diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.stories.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.stories.tsx index b747087827..5ded123ef1 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.stories.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.stories.tsx @@ -1,23 +1,23 @@ -import Table from "@mui/material/Table" -import TableBody from "@mui/material/TableBody" -import TableCell from "@mui/material/TableCell" -import TableContainer from "@mui/material/TableContainer" -import TableHead from "@mui/material/TableHead" -import TableRow from "@mui/material/TableRow" -import { ComponentMeta, Story } from "@storybook/react" +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import { ComponentMeta, Story } from "@storybook/react"; import { MockAuditLog, MockAuditLog2, MockAuditLogWithWorkspaceBuild, MockAuditLogWithDeletedResource, MockAuditLogGitSSH, -} from "testHelpers/entities" -import { AuditLogRow, AuditLogRowProps } from "./AuditLogRow" +} from "testHelpers/entities"; +import { AuditLogRow, AuditLogRowProps } from "./AuditLogRow"; export default { title: "components/AuditLogRow", component: AuditLogRow, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( @@ -32,23 +32,23 @@ const Template: Story = (args) => ( -) +); -export const NoDiff = Template.bind({}) +export const NoDiff = Template.bind({}); NoDiff.args = { auditLog: { ...MockAuditLog, diff: {}, }, -} +}; -export const WithDiff = Template.bind({}) +export const WithDiff = Template.bind({}); WithDiff.args = { auditLog: MockAuditLog2, defaultIsDiffOpen: true, -} +}; -export const WithLongDiffRow = Template.bind({}) +export const WithLongDiffRow = Template.bind({}); WithLongDiffRow.args = { auditLog: { ...MockAuditLog2, @@ -62,39 +62,39 @@ WithLongDiffRow.args = { }, }, defaultIsDiffOpen: true, -} +}; -export const WithStoppedWorkspaceBuild = Template.bind({}) +export const WithStoppedWorkspaceBuild = Template.bind({}); WithStoppedWorkspaceBuild.args = { auditLog: { ...MockAuditLogWithWorkspaceBuild, action: "stop", }, -} +}; -export const WithStartedWorkspaceBuild = Template.bind({}) +export const WithStartedWorkspaceBuild = Template.bind({}); WithStartedWorkspaceBuild.args = { auditLog: { ...MockAuditLogWithWorkspaceBuild, action: "start", }, -} +}; -export const WithDeletedWorkspaceBuild = Template.bind({}) +export const WithDeletedWorkspaceBuild = Template.bind({}); WithDeletedWorkspaceBuild.args = { auditLog: { ...MockAuditLogWithWorkspaceBuild, action: "delete", is_deleted: true, }, -} +}; -export const DeletedResource = Template.bind({}) +export const DeletedResource = Template.bind({}); DeletedResource.args = { auditLog: MockAuditLogWithDeletedResource, -} +}; -export const SecretDiffValue = Template.bind({}) +export const SecretDiffValue = Template.bind({}); SecretDiffValue.args = { auditLog: MockAuditLogGitSSH, -} +}; diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx index 322f6fc2a2..6f8c858f89 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx @@ -1,68 +1,68 @@ -import Collapse from "@mui/material/Collapse" -import { makeStyles } from "@mui/styles" -import TableCell from "@mui/material/TableCell" -import { AuditLog } from "api/typesGenerated" +import Collapse from "@mui/material/Collapse"; +import { makeStyles } from "@mui/styles"; +import TableCell from "@mui/material/TableCell"; +import { AuditLog } from "api/typesGenerated"; import { CloseDropdown, OpenDropdown, -} from "components/DropdownArrows/DropdownArrows" -import { Pill } from "components/Pill/Pill" -import { Stack } from "components/Stack/Stack" -import { TimelineEntry } from "components/Timeline/TimelineEntry" -import { UserAvatar } from "components/UserAvatar/UserAvatar" -import { useState } from "react" -import userAgentParser from "ua-parser-js" -import { AuditLogDiff, determineGroupDiff } from "./AuditLogDiff" -import { useTranslation } from "react-i18next" -import { AuditLogDescription } from "./AuditLogDescription" -import { PaletteIndex } from "theme/theme" +} from "components/DropdownArrows/DropdownArrows"; +import { Pill } from "components/Pill/Pill"; +import { Stack } from "components/Stack/Stack"; +import { TimelineEntry } from "components/Timeline/TimelineEntry"; +import { UserAvatar } from "components/UserAvatar/UserAvatar"; +import { useState } from "react"; +import userAgentParser from "ua-parser-js"; +import { AuditLogDiff, determineGroupDiff } from "./AuditLogDiff"; +import { useTranslation } from "react-i18next"; +import { AuditLogDescription } from "./AuditLogDescription"; +import { PaletteIndex } from "theme/theme"; const httpStatusColor = (httpStatus: number): PaletteIndex => { // redirects are successful if (httpStatus === 307) { - return "success" + return "success"; } if (httpStatus >= 300 && httpStatus < 500) { - return "warning" + return "warning"; } if (httpStatus >= 500) { - return "error" + return "error"; } - return "success" -} + return "success"; +}; export interface AuditLogRowProps { - auditLog: AuditLog + auditLog: AuditLog; // Useful for Storybook - defaultIsDiffOpen?: boolean + defaultIsDiffOpen?: boolean; } export const AuditLogRow: React.FC = ({ auditLog, defaultIsDiffOpen = false, }) => { - const styles = useStyles() - const { t } = useTranslation("auditLog") - const [isDiffOpen, setIsDiffOpen] = useState(defaultIsDiffOpen) - const diffs = Object.entries(auditLog.diff) - const shouldDisplayDiff = diffs.length > 0 - const { os, browser } = userAgentParser(auditLog.user_agent) + const styles = useStyles(); + const { t } = useTranslation("auditLog"); + const [isDiffOpen, setIsDiffOpen] = useState(defaultIsDiffOpen); + const diffs = Object.entries(auditLog.diff); + const shouldDisplayDiff = diffs.length > 0; + const { os, browser } = userAgentParser(auditLog.user_agent); - let auditDiff = auditLog.diff + let auditDiff = auditLog.diff; // groups have nested diffs (group members) if (auditLog.resource_type === "group") { - auditDiff = determineGroupDiff(auditLog.diff) + auditDiff = determineGroupDiff(auditLog.diff); } const toggle = () => { if (shouldDisplayDiff) { - setIsDiffOpen((v) => !v) + setIsDiffOpen((v) => !v); } - } + }; return ( = ({ onClick={toggle} onKeyDown={(event) => { if (event.key === "Enter") { - toggle() + toggle(); } }} > @@ -169,8 +169,8 @@ export const AuditLogRow: React.FC = ({ )} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ auditLogCell: { @@ -225,4 +225,4 @@ const useStyles = makeStyles((theme) => ({ ...theme.typography.caption, color: theme.palette.text.secondary, }, -})) +})); diff --git a/site/src/pages/AuditPage/AuditPage.test.tsx b/site/src/pages/AuditPage/AuditPage.test.tsx index 35ea1089bd..3d07123e9c 100644 --- a/site/src/pages/AuditPage/AuditPage.test.tsx +++ b/site/src/pages/AuditPage/AuditPage.test.tsx @@ -1,119 +1,119 @@ -import { screen, waitFor } from "@testing-library/react" -import userEvent from "@testing-library/user-event" -import * as API from "api/api" -import { rest } from "msw" +import { screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import * as API from "api/api"; +import { rest } from "msw"; import { MockAuditLog, MockAuditLog2, MockEntitlementsWithAuditLog, -} from "testHelpers/entities" +} from "testHelpers/entities"; import { renderWithAuth, waitForLoaderToBeRemoved, -} from "testHelpers/renderHelpers" -import { server } from "testHelpers/server" +} from "testHelpers/renderHelpers"; +import { server } from "testHelpers/server"; -import * as CreateDayString from "utils/createDayString" -import AuditPage from "./AuditPage" -import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils" +import * as CreateDayString from "utils/createDayString"; +import AuditPage from "./AuditPage"; +import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils"; interface RenderPageOptions { - filter?: string - page?: number + filter?: string; + page?: number; } const renderPage = async ({ filter, page }: RenderPageOptions = {}) => { - let route = "/audit" - const params = new URLSearchParams() + let route = "/audit"; + const params = new URLSearchParams(); if (filter) { - params.set("filter", filter) + params.set("filter", filter); } if (page) { - params.set("page", page.toString()) + params.set("page", page.toString()); } if (Array.from(params).length > 0) { - route += `?${params.toString()}` + route += `?${params.toString()}`; } renderWithAuth(, { route, path: "/audit", - }) - await waitForLoaderToBeRemoved() -} + }); + await waitForLoaderToBeRemoved(); +}; describe("AuditPage", () => { beforeEach(() => { // Mocking the dayjs module within the createDayString file - const mock = jest.spyOn(CreateDayString, "createDayString") - mock.mockImplementation(() => "a minute ago") + const mock = jest.spyOn(CreateDayString, "createDayString"); + mock.mockImplementation(() => "a minute ago"); // Mock the entitlements server.use( rest.get("/api/v2/entitlements", (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog)) + return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog)); }), - ) - }) + ); + }); it("shows the audit logs", async () => { // When - await renderPage() + await renderPage(); // Then - await screen.findByTestId(`audit-log-row-${MockAuditLog.id}`) - screen.getByTestId(`audit-log-row-${MockAuditLog2.id}`) - }) + await screen.findByTestId(`audit-log-row-${MockAuditLog.id}`); + screen.getByTestId(`audit-log-row-${MockAuditLog2.id}`); + }); it("renders page 5", async () => { // Given - const page = 5 + const page = 5; const getAuditLogsSpy = jest.spyOn(API, "getAuditLogs").mockResolvedValue({ audit_logs: [MockAuditLog, MockAuditLog2], count: 2, - }) + }); // When - await renderPage({ page: page }) + await renderPage({ page: page }); // Then expect(getAuditLogsSpy).toBeCalledWith({ limit: DEFAULT_RECORDS_PER_PAGE, offset: DEFAULT_RECORDS_PER_PAGE * (page - 1), q: "", - }) - screen.getByTestId(`audit-log-row-${MockAuditLog.id}`) - screen.getByTestId(`audit-log-row-${MockAuditLog2.id}`) - }) + }); + screen.getByTestId(`audit-log-row-${MockAuditLog.id}`); + screen.getByTestId(`audit-log-row-${MockAuditLog2.id}`); + }); describe("Filtering", () => { it("filters by URL", async () => { const getAuditLogsSpy = jest .spyOn(API, "getAuditLogs") - .mockResolvedValue({ audit_logs: [MockAuditLog], count: 1 }) + .mockResolvedValue({ audit_logs: [MockAuditLog], count: 1 }); - const query = "resource_type:workspace action:create" - await renderPage({ filter: query }) + const query = "resource_type:workspace action:create"; + await renderPage({ filter: query }); expect(getAuditLogsSpy).toBeCalledWith({ limit: DEFAULT_RECORDS_PER_PAGE, offset: 0, q: query, - }) - }) + }); + }); it("resets page to 1 when filter is changed", async () => { - await renderPage({ page: 2 }) + await renderPage({ page: 2 }); - const getAuditLogsSpy = jest.spyOn(API, "getAuditLogs") - getAuditLogsSpy.mockClear() + const getAuditLogsSpy = jest.spyOn(API, "getAuditLogs"); + getAuditLogsSpy.mockClear(); - const filterField = screen.getByLabelText("Filter") - const query = "resource_type:workspace action:create" - await userEvent.type(filterField, query) + const filterField = screen.getByLabelText("Filter"); + const query = "resource_type:workspace action:create"; + await userEvent.type(filterField, query); await waitFor(() => expect(getAuditLogsSpy).toBeCalledWith({ @@ -121,7 +121,7 @@ describe("AuditPage", () => { offset: 0, q: query, }), - ) - }) - }) -}) + ); + }); + }); +}); diff --git a/site/src/pages/AuditPage/AuditPage.tsx b/site/src/pages/AuditPage/AuditPage.tsx index dc5bf904a0..ae95397f27 100644 --- a/site/src/pages/AuditPage/AuditPage.tsx +++ b/site/src/pages/AuditPage/AuditPage.tsx @@ -1,29 +1,29 @@ import { DEFAULT_RECORDS_PER_PAGE, nonInitialPage, -} from "components/PaginationWidget/utils" -import { useFeatureVisibility } from "hooks/useFeatureVisibility" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { useSearchParams } from "react-router-dom" -import { pageTitle } from "utils/page" -import { AuditPageView } from "./AuditPageView" -import { useUserFilterMenu } from "components/Filter/UserFilter" -import { useFilter } from "components/Filter/filter" -import { usePagination } from "hooks" -import { useQuery } from "@tanstack/react-query" -import { getAuditLogs } from "api/api" -import { useActionFilterMenu, useResourceTypeFilterMenu } from "./AuditFilter" +} from "components/PaginationWidget/utils"; +import { useFeatureVisibility } from "hooks/useFeatureVisibility"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { useSearchParams } from "react-router-dom"; +import { pageTitle } from "utils/page"; +import { AuditPageView } from "./AuditPageView"; +import { useUserFilterMenu } from "components/Filter/UserFilter"; +import { useFilter } from "components/Filter/filter"; +import { usePagination } from "hooks"; +import { useQuery } from "@tanstack/react-query"; +import { getAuditLogs } from "api/api"; +import { useActionFilterMenu, useResourceTypeFilterMenu } from "./AuditFilter"; const AuditPage: FC = () => { - const searchParamsResult = useSearchParams() - const pagination = usePagination({ searchParamsResult }) + const searchParamsResult = useSearchParams(); + const pagination = usePagination({ searchParamsResult }); const filter = useFilter({ searchParamsResult, onUpdate: () => { - pagination.goToPage(1) + pagination.goToPage(1); }, - }) + }); const userMenu = useUserFilterMenu({ value: filter.values.username, onChange: (option) => @@ -31,7 +31,7 @@ const AuditPage: FC = () => { ...filter.values, username: option?.value, }), - }) + }); const actionMenu = useActionFilterMenu({ value: filter.values.action, onChange: (option) => @@ -39,7 +39,7 @@ const AuditPage: FC = () => { ...filter.values, action: option?.value, }), - }) + }); const resourceTypeMenu = useResourceTypeFilterMenu({ value: filter.values["resource_type"], onChange: (option) => @@ -47,20 +47,20 @@ const AuditPage: FC = () => { ...filter.values, resource_type: option?.value, }), - }) - const { audit_log: isAuditLogVisible } = useFeatureVisibility() + }); + const { audit_log: isAuditLogVisible } = useFeatureVisibility(); const { data, error } = useQuery({ queryKey: ["auditLogs", filter.query, pagination.page], queryFn: () => { - const limit = DEFAULT_RECORDS_PER_PAGE - const page = pagination.page + const limit = DEFAULT_RECORDS_PER_PAGE; + const page = pagination.page; return getAuditLogs({ offset: page <= 0 ? 0 : (page - 1) * limit, limit: limit, q: filter.query, - }) + }); }, - }) + }); return ( <> @@ -87,7 +87,7 @@ const AuditPage: FC = () => { }} /> - ) -} + ); +}; -export default AuditPage +export default AuditPage; diff --git a/site/src/pages/AuditPage/AuditPageView.stories.tsx b/site/src/pages/AuditPage/AuditPageView.stories.tsx index d41911ce70..ef8bc8c2d2 100644 --- a/site/src/pages/AuditPage/AuditPageView.stories.tsx +++ b/site/src/pages/AuditPage/AuditPageView.stories.tsx @@ -1,11 +1,14 @@ -import { Meta, StoryObj } from "@storybook/react" -import { MockAuditLog, MockAuditLog2, MockUser } from "testHelpers/entities" -import { AuditPageView } from "./AuditPageView" -import { WorkspacesPageView } from "pages/WorkspacesPage/WorkspacesPageView" -import { ComponentProps } from "react" -import { MockMenu, getDefaultFilterProps } from "components/Filter/storyHelpers" +import { Meta, StoryObj } from "@storybook/react"; +import { MockAuditLog, MockAuditLog2, MockUser } from "testHelpers/entities"; +import { AuditPageView } from "./AuditPageView"; +import { WorkspacesPageView } from "pages/WorkspacesPage/WorkspacesPageView"; +import { ComponentProps } from "react"; +import { + MockMenu, + getDefaultFilterProps, +} from "components/Filter/storyHelpers"; -type FilterProps = ComponentProps["filterProps"] +type FilterProps = ComponentProps["filterProps"]; const defaultFilterProps = getDefaultFilterProps({ query: `owner:me`, @@ -19,7 +22,7 @@ const defaultFilterProps = getDefaultFilterProps({ action: MockMenu, resourceType: MockMenu, }, -}) +}); const meta: Meta = { title: "pages/AuditPageView", @@ -32,12 +35,12 @@ const meta: Meta = { isAuditLogVisible: true, filterProps: defaultFilterProps, }, -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; -export const AuditPage: Story = {} +export const AuditPage: Story = {}; export const Loading = { args: { @@ -45,14 +48,14 @@ export const Loading = { count: undefined, isNonInitialPage: false, }, -} +}; export const EmptyPage = { args: { auditLogs: [], isNonInitialPage: true, }, -} +}; export const NoLogs = { args: { @@ -60,16 +63,16 @@ export const NoLogs = { count: 0, isNonInitialPage: false, }, -} +}; export const NotVisible = { args: { isAuditLogVisible: false, }, -} +}; export const AuditPageSmallViewport = { parameters: { chromatic: { viewports: [600] }, }, -} +}; diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index ec441b4eec..688eb353f5 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -1,47 +1,47 @@ -import Table from "@mui/material/Table" -import TableBody from "@mui/material/TableBody" -import TableCell from "@mui/material/TableCell" -import TableContainer from "@mui/material/TableContainer" -import TableRow from "@mui/material/TableRow" -import { AuditLog } from "api/typesGenerated" -import { AuditLogRow } from "pages/AuditPage/AuditLogRow/AuditLogRow" -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { EmptyState } from "components/EmptyState/EmptyState" -import { Margins } from "components/Margins/Margins" +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableRow from "@mui/material/TableRow"; +import { AuditLog } from "api/typesGenerated"; +import { AuditLogRow } from "pages/AuditPage/AuditLogRow/AuditLogRow"; +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { Margins } from "components/Margins/Margins"; import { PageHeader, PageHeaderSubtitle, PageHeaderTitle, -} from "components/PageHeader/PageHeader" -import { Stack } from "components/Stack/Stack" -import { TableLoader } from "components/TableLoader/TableLoader" -import { Timeline } from "components/Timeline/Timeline" -import { AuditHelpTooltip } from "./AuditHelpTooltip" -import { ComponentProps, FC } from "react" -import { useTranslation } from "react-i18next" -import { AuditPaywall } from "./AuditPaywall" -import { AuditFilter } from "./AuditFilter" +} from "components/PageHeader/PageHeader"; +import { Stack } from "components/Stack/Stack"; +import { TableLoader } from "components/TableLoader/TableLoader"; +import { Timeline } from "components/Timeline/Timeline"; +import { AuditHelpTooltip } from "./AuditHelpTooltip"; +import { ComponentProps, FC } from "react"; +import { useTranslation } from "react-i18next"; +import { AuditPaywall } from "./AuditPaywall"; +import { AuditFilter } from "./AuditFilter"; import { PaginationStatus, TableToolbar, -} from "components/TableToolbar/TableToolbar" -import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase" +} from "components/TableToolbar/TableToolbar"; +import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase"; export const Language = { title: "Audit", subtitle: "View events in your audit log.", -} +}; export interface AuditPageViewProps { - auditLogs?: AuditLog[] - count?: number - page: number - limit: number - onPageChange: (page: number) => void - isNonInitialPage: boolean - isAuditLogVisible: boolean - error?: unknown - filterProps: ComponentProps + auditLogs?: AuditLog[]; + count?: number; + page: number; + limit: number; + onPageChange: (page: number) => void; + isNonInitialPage: boolean; + isAuditLogVisible: boolean; + error?: unknown; + filterProps: ComponentProps; } export const AuditPageView: FC = ({ @@ -55,10 +55,10 @@ export const AuditPageView: FC = ({ error, filterProps, }) => { - const { t } = useTranslation("auditLog") + const { t } = useTranslation("auditLog"); - const isLoading = (auditLogs === undefined || count === undefined) && !error - const isEmpty = !isLoading && auditLogs?.length === 0 + const isLoading = (auditLogs === undefined || count === undefined) && !error; + const isEmpty = !isLoading && auditLogs?.length === 0; return ( @@ -149,5 +149,5 @@ export const AuditPageView: FC = ({ - ) -} + ); +}; diff --git a/site/src/pages/AuditPage/AuditPaywall.tsx b/site/src/pages/AuditPage/AuditPaywall.tsx index b4f4f77cd9..22f55982ec 100644 --- a/site/src/pages/AuditPage/AuditPaywall.tsx +++ b/site/src/pages/AuditPage/AuditPaywall.tsx @@ -1,14 +1,14 @@ -import Button from "@mui/material/Button" -import Link from "@mui/material/Link" -import ArrowRightAltOutlined from "@mui/icons-material/ArrowRightAltOutlined" -import { Paywall } from "components/Paywall/Paywall" -import { Stack } from "components/Stack/Stack" -import { FC } from "react" -import { useTranslation } from "react-i18next" -import { docs } from "utils/docs" +import Button from "@mui/material/Button"; +import Link from "@mui/material/Link"; +import ArrowRightAltOutlined from "@mui/icons-material/ArrowRightAltOutlined"; +import { Paywall } from "components/Paywall/Paywall"; +import { Stack } from "components/Stack/Stack"; +import { FC } from "react"; +import { useTranslation } from "react-i18next"; +import { docs } from "utils/docs"; export const AuditPaywall: FC = () => { - const { t } = useTranslation("auditLog") + const { t } = useTranslation("auditLog"); return ( { } /> - ) -} + ); +}; diff --git a/site/src/pages/CliAuthPage/CliAuthPage.tsx b/site/src/pages/CliAuthPage/CliAuthPage.tsx index 5da6d273c1..1f7fe94de3 100644 --- a/site/src/pages/CliAuthPage/CliAuthPage.tsx +++ b/site/src/pages/CliAuthPage/CliAuthPage.tsx @@ -1,21 +1,21 @@ -import { useEffect, useState, FC, PropsWithChildren } from "react" -import { Helmet } from "react-helmet-async" -import { getApiKey } from "../../api/api" -import { pageTitle } from "../../utils/page" -import { CliAuthPageView } from "./CliAuthPageView" +import { useEffect, useState, FC, PropsWithChildren } from "react"; +import { Helmet } from "react-helmet-async"; +import { getApiKey } from "../../api/api"; +import { pageTitle } from "../../utils/page"; +import { CliAuthPageView } from "./CliAuthPageView"; export const CliAuthenticationPage: FC> = () => { - const [apiKey, setApiKey] = useState(null) + const [apiKey, setApiKey] = useState(null); useEffect(() => { getApiKey() .then(({ key }) => { - setApiKey(key) + setApiKey(key); }) .catch((error) => { - console.error(error) - }) - }, []) + console.error(error); + }); + }, []); return ( <> @@ -24,7 +24,7 @@ export const CliAuthenticationPage: FC> = () => { - ) -} + ); +}; -export default CliAuthenticationPage +export default CliAuthenticationPage; diff --git a/site/src/pages/CliAuthPage/CliAuthPageView.stories.tsx b/site/src/pages/CliAuthPage/CliAuthPageView.stories.tsx index da3d20e4c0..44731ad3ef 100644 --- a/site/src/pages/CliAuthPage/CliAuthPageView.stories.tsx +++ b/site/src/pages/CliAuthPage/CliAuthPageView.stories.tsx @@ -1,5 +1,5 @@ -import { Story } from "@storybook/react" -import { CliAuthPageView, CliAuthPageViewProps } from "./CliAuthPageView" +import { Story } from "@storybook/react"; +import { CliAuthPageView, CliAuthPageViewProps } from "./CliAuthPageView"; export default { title: "pages/CliAuthPageView", @@ -10,11 +10,11 @@ export default { args: { sessionToken: "some-session-token", }, -} +}; const Template: Story = (args) => ( -) +); -export const Example = Template.bind({}) -Example.args = {} +export const Example = Template.bind({}); +Example.args = {}; diff --git a/site/src/pages/CliAuthPage/CliAuthPageView.tsx b/site/src/pages/CliAuthPage/CliAuthPageView.tsx index b3756f74b2..ba91484b69 100644 --- a/site/src/pages/CliAuthPage/CliAuthPageView.tsx +++ b/site/src/pages/CliAuthPage/CliAuthPageView.tsx @@ -1,21 +1,21 @@ -import Button from "@mui/material/Button" -import { makeStyles } from "@mui/styles" -import { CodeExample } from "components/CodeExample/CodeExample" -import { SignInLayout } from "components/SignInLayout/SignInLayout" -import { Welcome } from "components/Welcome/Welcome" -import { FC } from "react" -import { Link as RouterLink } from "react-router-dom" -import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" +import Button from "@mui/material/Button"; +import { makeStyles } from "@mui/styles"; +import { CodeExample } from "components/CodeExample/CodeExample"; +import { SignInLayout } from "components/SignInLayout/SignInLayout"; +import { Welcome } from "components/Welcome/Welcome"; +import { FC } from "react"; +import { Link as RouterLink } from "react-router-dom"; +import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"; export interface CliAuthPageViewProps { - sessionToken: string | null + sessionToken: string | null; } export const CliAuthPageView: FC = ({ sessionToken }) => { - const styles = useStyles() + const styles = useStyles(); if (!sessionToken) { - return + return ; } return ( @@ -35,8 +35,8 @@ export const CliAuthPageView: FC = ({ sessionToken }) => { - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ title: { @@ -63,4 +63,4 @@ const useStyles = makeStyles((theme) => ({ justifyContent: "flex-end", paddingTop: theme.spacing(1), }, -})) +})); diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx index 22811b15cf..7dd881434c 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx @@ -1,4 +1,4 @@ -import { ComponentMeta, Story } from "@storybook/react" +import { ComponentMeta, Story } from "@storybook/react"; import { MockTemplateExample, MockTemplateVersionVariable1, @@ -6,11 +6,11 @@ import { MockTemplateVersionVariable3, MockTemplateVersionVariable4, MockTemplateVersionVariable5, -} from "testHelpers/entities" +} from "testHelpers/entities"; import { CreateTemplateForm, CreateTemplateFormProps, -} from "./CreateTemplateForm" +} from "./CreateTemplateForm"; export default { title: "components/CreateTemplateForm", @@ -19,21 +19,21 @@ export default { isSubmitting: false, allowDisableEveryoneAccess: true, }, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) +); -export const Initial = Template.bind({}) -Initial.args = {} +export const Initial = Template.bind({}); +Initial.args = {}; -export const WithStarterTemplate = Template.bind({}) +export const WithStarterTemplate = Template.bind({}); WithStarterTemplate.args = { starterTemplate: MockTemplateExample, -} +}; -export const WithVariables = Template.bind({}) +export const WithVariables = Template.bind({}); WithVariables.args = { variables: [ MockTemplateVersionVariable1, @@ -42,9 +42,9 @@ WithVariables.args = { MockTemplateVersionVariable4, MockTemplateVersionVariable5, ], -} +}; -export const WithJobError = Template.bind({}) +export const WithJobError = Template.bind({}); WithJobError.args = { jobError: "template import provision for start: recv import provision: plan terraform: terraform plan: exit status 1", @@ -356,4 +356,4 @@ WithJobError.args = { output: "", }, ], -} +}; diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx index fe4f4e3ed2..53d171dc53 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx @@ -1,73 +1,73 @@ -import Checkbox from "@mui/material/Checkbox" -import { makeStyles } from "@mui/styles" -import TextField from "@mui/material/TextField" +import Checkbox from "@mui/material/Checkbox"; +import { makeStyles } from "@mui/styles"; +import TextField from "@mui/material/TextField"; import { ProvisionerJobLog, Template, TemplateExample, TemplateVersionVariable, -} from "api/typesGenerated" -import { Stack } from "components/Stack/Stack" +} from "api/typesGenerated"; +import { Stack } from "components/Stack/Stack"; import { TemplateUpload, TemplateUploadProps, -} from "pages/CreateTemplatePage/TemplateUpload" -import { useFormik } from "formik" -import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate" -import { FC, useEffect } from "react" -import { useTranslation } from "react-i18next" +} from "pages/CreateTemplatePage/TemplateUpload"; +import { useFormik } from "formik"; +import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate"; +import { FC, useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { nameValidator, getFormHelpers, onChangeTrimmed, templateDisplayNameValidator, -} from "utils/formUtils" -import { CreateTemplateData } from "xServices/createTemplate/createTemplateXService" -import * as Yup from "yup" -import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs" +} from "utils/formUtils"; +import { CreateTemplateData } from "xServices/createTemplate/createTemplateXService"; +import * as Yup from "yup"; +import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"; import { HelpTooltip, HelpTooltipText, -} from "components/HelpTooltip/HelpTooltip" -import { LazyIconField } from "components/IconField/LazyIconField" -import { Maybe } from "components/Conditionals/Maybe" -import i18next from "i18next" -import Link from "@mui/material/Link" +} from "components/HelpTooltip/HelpTooltip"; +import { LazyIconField } from "components/IconField/LazyIconField"; +import { Maybe } from "components/Conditionals/Maybe"; +import i18next from "i18next"; +import Link from "@mui/material/Link"; import { HorizontalForm, FormSection, FormFields, FormFooter, -} from "components/Form/Form" -import camelCase from "lodash/camelCase" -import capitalize from "lodash/capitalize" -import { VariableInput } from "./VariableInput" -import { docs } from "utils/docs" +} from "components/Form/Form"; +import camelCase from "lodash/camelCase"; +import capitalize from "lodash/capitalize"; +import { VariableInput } from "./VariableInput"; +import { docs } from "utils/docs"; import { AutostopRequirementDaysHelperText, AutostopRequirementWeeksHelperText, -} from "pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/AutostopRequirementHelperText" -import MenuItem from "@mui/material/MenuItem" +} from "pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/AutostopRequirementHelperText"; +import MenuItem from "@mui/material/MenuItem"; -const MAX_DESCRIPTION_CHAR_LIMIT = 128 -const MAX_TTL_DAYS = 30 +const MAX_DESCRIPTION_CHAR_LIMIT = 128; +const MAX_TTL_DAYS = 30; const TTLHelperText = ({ ttl, translationName, }: { - ttl?: number - translationName: string + ttl?: number; + translationName: string; }) => { - const { t } = useTranslation("createTemplatePage") - const count = typeof ttl !== "number" ? 0 : ttl + const { t } = useTranslation("createTemplatePage"); + const count = typeof ttl !== "number" ? 0 : ttl; return ( // no helper text if ttl is negative - error will show once field is considered touched = 0}> {t(translationName, { count })} - ) -} + ); +}; const validationSchema = Yup.object({ name: nameValidator( @@ -99,7 +99,7 @@ const validationSchema = Yup.object({ ), autostop_requirement_days_of_week: Yup.string().required(), autostop_requirement_weeks: Yup.number().required().min(1).max(16), -}) +}); const defaultInitialValues: CreateTemplateData = { name: "", @@ -125,14 +125,14 @@ const defaultInitialValues: CreateTemplateData = { allow_user_autostart: false, allow_user_autostop: false, allow_everyone_group_access: true, -} +}; type GetInitialValuesParams = { - fromExample?: TemplateExample - fromCopy?: Template - variables?: TemplateVersionVariable[] - allowAdvancedScheduling: boolean -} + fromExample?: TemplateExample; + fromCopy?: Template; + variables?: TemplateVersionVariable[]; + allowAdvancedScheduling: boolean; +}; const getInitialValues = ({ fromExample, @@ -140,13 +140,13 @@ const getInitialValues = ({ allowAdvancedScheduling, variables, }: GetInitialValuesParams) => { - let initialValues = defaultInitialValues + let initialValues = defaultInitialValues; if (!allowAdvancedScheduling) { initialValues = { ...initialValues, max_ttl_hours: 0, - } + }; } if (fromExample) { @@ -156,7 +156,7 @@ const getInitialValues = ({ display_name: fromExample.name, icon: fromExample.icon, description: fromExample.description, - } + }; } if (fromCopy) { @@ -167,38 +167,38 @@ const getInitialValues = ({ display_name: fromCopy.display_name ? `Copy of ${fromCopy.display_name}` : "", - } + }; } if (variables) { variables.forEach((variable) => { if (!initialValues.user_variable_values) { - initialValues.user_variable_values = [] + initialValues.user_variable_values = []; } initialValues.user_variable_values.push({ name: variable.name, value: variable.sensitive ? "" : variable.value, - }) - }) + }); + }); } - return initialValues -} + return initialValues; +}; export interface CreateTemplateFormProps { - onCancel: () => void - onSubmit: (data: CreateTemplateData) => void - isSubmitting: boolean - upload: TemplateUploadProps - starterTemplate?: TemplateExample - variables?: TemplateVersionVariable[] - error?: unknown - jobError?: string - logs?: ProvisionerJobLog[] - allowAdvancedScheduling: boolean - copiedTemplate?: Template - allowDisableEveryoneAccess: boolean - allowAutostopRequirement: boolean + onCancel: () => void; + onSubmit: (data: CreateTemplateData) => void; + isSubmitting: boolean; + upload: TemplateUploadProps; + starterTemplate?: TemplateExample; + variables?: TemplateVersionVariable[]; + error?: unknown; + jobError?: string; + logs?: ProvisionerJobLog[]; + allowAdvancedScheduling: boolean; + copiedTemplate?: Template; + allowDisableEveryoneAccess: boolean; + allowAutostopRequirement: boolean; } export const CreateTemplateForm: FC = ({ @@ -216,7 +216,7 @@ export const CreateTemplateForm: FC = ({ allowDisableEveryoneAccess, allowAutostopRequirement, }) => { - const styles = useStyles() + const styles = useStyles(); const form = useFormik({ initialValues: getInitialValues({ allowAdvancedScheduling, @@ -226,22 +226,22 @@ export const CreateTemplateForm: FC = ({ }), validationSchema, onSubmit, - }) - const getFieldHelpers = getFormHelpers(form, error) - const { t } = useTranslation("createTemplatePage") - const { t: commonT } = useTranslation("common") + }); + const getFieldHelpers = getFormHelpers(form, error); + const { t } = useTranslation("createTemplatePage"); + const { t: commonT } = useTranslation("common"); useEffect(() => { if (error) { - window.scrollTo(0, 0) + window.scrollTo(0, 0); } - }, [error]) + }, [error]); useEffect(() => { if (jobError) { - window.scrollTo(0, document.body.scrollHeight) + window.scrollTo(0, document.body.scrollHeight); } - }, [logs, jobError]) + }, [logs, jobError]); // Set autostop_requirement weeks to 1 when days_of_week is set to "off" or // "daily". Technically you can set weeks to a different value in the backend @@ -254,13 +254,13 @@ export const CreateTemplateForm: FC = ({ const { values: { autostop_requirement_days_of_week }, setFieldValue, - } = form + } = form; useEffect(() => { if (!["saturday", "sunday"].includes(autostop_requirement_days_of_week)) { // This is async but we don't really need to await the value. - void setFieldValue("autostop_requirement_weeks", 1) + void setFieldValue("autostop_requirement_weeks", 1); } - }, [autostop_requirement_days_of_week, setFieldValue]) + }, [autostop_requirement_days_of_week, setFieldValue]); return ( @@ -278,8 +278,8 @@ export const CreateTemplateForm: FC = ({ { - await fillNameAndDisplayWithFilename(file.name, form) - upload.onUpload(file) + await fillNameAndDisplayWithFilename(file.name, form); + upload.onUpload(file); }} /> )} @@ -439,7 +439,7 @@ export const CreateTemplateForm: FC = ({ await form.setFieldValue( "allow_user_autostart", !form.values.allow_user_autostart, - ) + ); }} name="allow_user_autostart" checked={form.values.allow_user_autostart} @@ -459,7 +459,7 @@ export const CreateTemplateForm: FC = ({ await form.setFieldValue( "allow_user_autostop", !form.values.allow_user_autostop, - ) + ); }} name="allow-user-autostop" checked={form.values.allow_user_autostop} @@ -582,7 +582,7 @@ export const CreateTemplateForm: FC = ({ await form.setFieldValue("user_variable_values." + index, { name: variable.name, value, - }) + }); }} /> ))} @@ -612,14 +612,14 @@ export const CreateTemplateForm: FC = ({ submitLabel={jobError ? "Retry" : "Create template"} /> - ) -} + ); +}; const fillNameAndDisplayWithFilename = async ( filename: string, form: ReturnType>, ) => { - const [name, _extension] = filename.split(".") + const [name, _extension] = filename.split("."); await Promise.all([ form.setFieldValue( "name", @@ -627,8 +627,8 @@ const fillNameAndDisplayWithFilename = async ( camelCase(name).toLowerCase(), ), form.setFieldValue("display_name", capitalize(name)), - ]) -} + ]); +}; const useStyles = makeStyles((theme) => ({ ttlFields: { @@ -669,4 +669,4 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.error.light, fontSize: theme.spacing(2), }, -})) +})); diff --git a/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx b/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx index b3bdf89029..9400e864a2 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplatePage.test.tsx @@ -1,8 +1,8 @@ -import { renderWithAuth } from "testHelpers/renderHelpers" -import CreateTemplatePage from "./CreateTemplatePage" -import { screen, waitFor, within } from "@testing-library/react" -import userEvent from "@testing-library/user-event" -import * as API from "api/api" +import { renderWithAuth } from "testHelpers/renderHelpers"; +import CreateTemplatePage from "./CreateTemplatePage"; +import { screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import * as API from "api/api"; import { MockTemplateExample, MockTemplateVersion, @@ -12,7 +12,7 @@ import { MockTemplate, MockOrganization, MockProvisionerJob, -} from "testHelpers/entities" +} from "testHelpers/entities"; const renderPage = async (searchParams: URLSearchParams) => { // Render with the example ID so we don't need to upload a file @@ -21,12 +21,12 @@ const renderPage = async (searchParams: URLSearchParams) => { path: "/templates/new", // We need this because after creation, the user will be redirected to here extraRoutes: [{ path: "templates/:template", element: <> }], - }) + }); // It is lazy loaded, so we have to wait for it to be rendered to not get an // act error - await screen.findByLabelText("Icon", undefined, { timeout: 5000 }) - return view -} + await screen.findByLabelText("Icon", undefined, { timeout: 5000 }); + return view; +}; test("Create template with variables", async () => { // Return pending when creating the first template version @@ -36,7 +36,7 @@ test("Create template with variables", async () => { ...MockTemplateVersion.job, status: "pending", }, - }) + }); // Return an error requesting for template variables jest.spyOn(API, "getTemplateVersion").mockResolvedValue({ ...MockTemplateVersion, @@ -45,7 +45,7 @@ test("Create template with variables", async () => { status: "failed", error_code: "REQUIRED_TEMPLATE_VARIABLES", }, - }) + }); // Return the template variables jest .spyOn(API, "getTemplateVersionVariables") @@ -53,46 +53,46 @@ test("Create template with variables", async () => { MockTemplateVersionVariable1, MockTemplateVersionVariable2, MockTemplateVersionVariable3, - ]) + ]); // Render page, fill the name and submit const searchParams = new URLSearchParams({ exampleId: MockTemplateExample.id, - }) - const { router, container } = await renderPage(searchParams) - const form = container.querySelector("form") as HTMLFormElement - await userEvent.type(screen.getByLabelText(/Name/), "my-template") + }); + const { router, container } = await renderPage(searchParams); + const form = container.querySelector("form") as HTMLFormElement; + await userEvent.type(screen.getByLabelText(/Name/), "my-template"); await userEvent.click( within(form).getByRole("button", { name: /create template/i }), - ) + ); // Wait for the variables form to be rendered and fill it - await screen.findByText(/Variables/) + await screen.findByText(/Variables/); // Type first variable - await userEvent.clear(screen.getByLabelText(/var.first_variable/)) + await userEvent.clear(screen.getByLabelText(/var.first_variable/)); await userEvent.type( screen.getByLabelText(/var.first_variable/), "First value", - ) + ); // Type second variable - await userEvent.clear(screen.getByLabelText(/var.second_variable/)) - await userEvent.type(screen.getByLabelText(/var.second_variable/), "2") + await userEvent.clear(screen.getByLabelText(/var.second_variable/)); + await userEvent.type(screen.getByLabelText(/var.second_variable/), "2"); // Select third variable on radio - await userEvent.click(screen.getByLabelText(/True/)) + await userEvent.click(screen.getByLabelText(/True/)); // Setup the mock for the second template version creation before submit the form - jest.clearAllMocks() + jest.clearAllMocks(); jest .spyOn(API, "createTemplateVersion") - .mockResolvedValue(MockTemplateVersion) - jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate) + .mockResolvedValue(MockTemplateVersion); + jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate); await userEvent.click( within(form).getByRole("button", { name: /create template/i }), - ) - await waitFor(() => expect(API.createTemplate).toBeCalledTimes(1)) + ); + await waitFor(() => expect(API.createTemplate).toBeCalledTimes(1)); expect(router.state.location.pathname).toEqual( `/templates/${MockTemplate.name}`, - ) + ); expect(API.createTemplateVersion).toHaveBeenCalledWith(MockOrganization.id, { file_id: MockProvisionerJob.file_id, provisioner: "terraform", @@ -103,34 +103,36 @@ test("Create template with variables", async () => { { name: "second_variable", value: "2" }, { name: "third_variable", value: "true" }, ], - }) -}) + }); +}); test("Create template from another template", async () => { const searchParams = new URLSearchParams({ fromTemplate: MockTemplate.name, - }) - const { router } = await renderPage(searchParams) + }); + const { router } = await renderPage(searchParams); // Name and display name are using copy prefixes - expect(screen.getByLabelText(/Name/)).toHaveValue(`${MockTemplate.name}-copy`) + expect(screen.getByLabelText(/Name/)).toHaveValue( + `${MockTemplate.name}-copy`, + ); expect(screen.getByLabelText(/Display name/)).toHaveValue( `Copy of ${MockTemplate.display_name}`, - ) + ); // Variables are using the same values expect( screen.getByLabelText(MockTemplateVersionVariable1.description, { exact: false, }), - ).toHaveValue(MockTemplateVersionVariable1.value) + ).toHaveValue(MockTemplateVersionVariable1.value); // Create template jest .spyOn(API, "createTemplateVersion") - .mockResolvedValue(MockTemplateVersion) - jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate) + .mockResolvedValue(MockTemplateVersion); + jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate); await userEvent.click( screen.getByRole("button", { name: /create template/i }), - ) + ); expect(router.state.location.pathname).toEqual( `/templates/${MockTemplate.name}`, - ) -}) + ); +}); diff --git a/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx b/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx index c402e8bc48..e9be6407be 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx @@ -1,25 +1,25 @@ -import { useMachine } from "@xstate/react" -import { isApiValidationError } from "api/errors" -import { Maybe } from "components/Conditionals/Maybe" -import { useDashboard } from "components/Dashboard/DashboardProvider" -import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm" -import { Loader } from "components/Loader/Loader" -import { Stack } from "components/Stack/Stack" -import { useOrganizationId } from "hooks/useOrganizationId" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { useTranslation } from "react-i18next" -import { useNavigate, useSearchParams } from "react-router-dom" -import { pageTitle } from "utils/page" -import { createTemplateMachine } from "xServices/createTemplate/createTemplateXService" -import { CreateTemplateForm } from "./CreateTemplateForm" -import { ErrorAlert } from "components/Alert/ErrorAlert" +import { useMachine } from "@xstate/react"; +import { isApiValidationError } from "api/errors"; +import { Maybe } from "components/Conditionals/Maybe"; +import { useDashboard } from "components/Dashboard/DashboardProvider"; +import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm"; +import { Loader } from "components/Loader/Loader"; +import { Stack } from "components/Stack/Stack"; +import { useOrganizationId } from "hooks/useOrganizationId"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { pageTitle } from "utils/page"; +import { createTemplateMachine } from "xServices/createTemplate/createTemplateXService"; +import { CreateTemplateForm } from "./CreateTemplateForm"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; const CreateTemplatePage: FC = () => { - const { t } = useTranslation("createTemplatePage") - const navigate = useNavigate() - const organizationId = useOrganizationId() - const [searchParams] = useSearchParams() + const { t } = useTranslation("createTemplatePage"); + const navigate = useNavigate(); + const organizationId = useOrganizationId(); + const [searchParams] = useSearchParams(); const [state, send] = useMachine(createTemplateMachine, { context: { organizationId, @@ -28,28 +28,28 @@ const CreateTemplatePage: FC = () => { }, actions: { onCreate: (_, { data }) => { - navigate(`/templates/${data.name}`) + navigate(`/templates/${data.name}`); }, }, - }) + }); const { starterTemplate, error, file, jobError, jobLogs, variables } = - state.context - const shouldDisplayForm = !state.hasTag("loading") - const { entitlements, experiments } = useDashboard() + state.context; + const shouldDisplayForm = !state.hasTag("loading"); + const { entitlements, experiments } = useDashboard(); const allowAdvancedScheduling = - entitlements.features["advanced_template_scheduling"].enabled + entitlements.features["advanced_template_scheduling"].enabled; // Requires the template RBAC feature, otherwise disabling everyone access // means no one can access. const allowDisableEveryoneAccess = - entitlements.features["template_rbac"].enabled + entitlements.features["template_rbac"].enabled; const allowAutostopRequirement = experiments.includes( "template_autostop_requirement", - ) + ); const onCancel = () => { - navigate(-1) - } + navigate(-1); + }; return ( <> @@ -82,16 +82,16 @@ const CreateTemplatePage: FC = () => { send({ type: "CREATE", data, - }) + }); }} upload={{ file, isUploading: state.matches("uploading"), onRemove: () => { - send("REMOVE_FILE") + send("REMOVE_FILE"); }, onUpload: (file) => { - send({ type: "UPLOAD_FILE", file }) + send({ type: "UPLOAD_FILE", file }); }, }} jobError={jobError} @@ -101,7 +101,7 @@ const CreateTemplatePage: FC = () => { - ) -} + ); +}; -export default CreateTemplatePage +export default CreateTemplatePage; diff --git a/site/src/pages/CreateTemplatePage/TemplateUpload.tsx b/site/src/pages/CreateTemplatePage/TemplateUpload.tsx index f7e73812f7..c60ae619da 100644 --- a/site/src/pages/CreateTemplatePage/TemplateUpload.tsx +++ b/site/src/pages/CreateTemplatePage/TemplateUpload.tsx @@ -1,14 +1,14 @@ -import Link from "@mui/material/Link" -import { FileUpload } from "components/FileUpload/FileUpload" -import { FC } from "react" -import { useTranslation } from "react-i18next" -import { Link as RouterLink } from "react-router-dom" +import Link from "@mui/material/Link"; +import { FileUpload } from "components/FileUpload/FileUpload"; +import { FC } from "react"; +import { useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; export interface TemplateUploadProps { - isUploading: boolean - onUpload: (file: File) => void - onRemove: () => void - file?: File + isUploading: boolean; + onUpload: (file: File) => void; + onRemove: () => void; + file?: File; } export const TemplateUpload: FC = ({ @@ -17,7 +17,7 @@ export const TemplateUpload: FC = ({ onRemove, file, }) => { - const { t } = useTranslation("createTemplatePage") + const { t } = useTranslation("createTemplatePage"); const description = ( <> @@ -27,14 +27,14 @@ export const TemplateUpload: FC = ({ to="/starter-templates" // Prevent trigger the upload onClick={(e) => { - e.stopPropagation() + e.stopPropagation(); }} > starter templates {" "} to getting started with Coder. - ) + ); return ( = ({ extension=".tar" fileTypeRequired="application/x-tar" /> - ) -} + ); +}; diff --git a/site/src/pages/CreateTemplatePage/VariableInput.tsx b/site/src/pages/CreateTemplatePage/VariableInput.tsx index 5b07e19346..d0bb52db80 100644 --- a/site/src/pages/CreateTemplatePage/VariableInput.tsx +++ b/site/src/pages/CreateTemplatePage/VariableInput.tsx @@ -1,20 +1,20 @@ -import FormControlLabel from "@mui/material/FormControlLabel" -import Radio from "@mui/material/Radio" -import RadioGroup from "@mui/material/RadioGroup" -import { makeStyles } from "@mui/styles" -import TextField from "@mui/material/TextField" -import { Stack } from "components/Stack/Stack" -import { FC } from "react" -import { TemplateVersionVariable } from "../../api/typesGenerated" +import FormControlLabel from "@mui/material/FormControlLabel"; +import Radio from "@mui/material/Radio"; +import RadioGroup from "@mui/material/RadioGroup"; +import { makeStyles } from "@mui/styles"; +import TextField from "@mui/material/TextField"; +import { Stack } from "components/Stack/Stack"; +import { FC } from "react"; +import { TemplateVersionVariable } from "../../api/typesGenerated"; const isBoolean = (variable: TemplateVersionVariable) => { - return variable.type === "bool" -} + return variable.type === "bool"; +}; const VariableLabel: React.FC<{ variable: TemplateVersionVariable }> = ({ variable, }) => { - const styles = useStyles() + const styles = useStyles(); return ( - ) -} + ); +}; export interface VariableInputProps { - disabled?: boolean - variable: TemplateVersionVariable - onChange: (value: string) => void - defaultValue?: string + disabled?: boolean; + variable: TemplateVersionVariable; + onChange: (value: string) => void; + defaultValue?: string; } export const VariableInput: FC = ({ @@ -40,7 +40,7 @@ export const VariableInput: FC = ({ variable, defaultValue, }) => { - const styles = useStyles() + const styles = useStyles(); return ( @@ -54,8 +54,8 @@ export const VariableInput: FC = ({ /> - ) -} + ); +}; const VariableField: React.FC = ({ disabled, @@ -69,7 +69,7 @@ const VariableField: React.FC = ({ id={variable.name} defaultValue={variable.default_value} onChange={(event) => { - onChange(event.target.value) + onChange(event.target.value); }} > = ({ label="False" /> - ) + ); } return ( @@ -100,7 +100,7 @@ const VariableField: React.FC = ({ variable.sensitive ? "" : defaultValue ?? variable.default_value } onChange={(event) => { - onChange(event.target.value) + onChange(event.target.value); }} type={ variable.type === "number" @@ -110,8 +110,8 @@ const VariableField: React.FC = ({ : "string" } /> - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ labelName: { @@ -135,4 +135,4 @@ const useStyles = makeStyles((theme) => ({ alignItems: "center", gap: theme.spacing(1), }, -})) +})); diff --git a/site/src/pages/CreateTokenPage/CreateTokenForm.tsx b/site/src/pages/CreateTokenPage/CreateTokenForm.tsx index fa104cb165..851e4717c4 100644 --- a/site/src/pages/CreateTokenPage/CreateTokenForm.tsx +++ b/site/src/pages/CreateTokenPage/CreateTokenForm.tsx @@ -1,34 +1,34 @@ -import { FC, useState, useEffect } from "react" +import { FC, useState, useEffect } from "react"; import { FormFields, FormSection, FormFooter, HorizontalForm, -} from "components/Form/Form" -import makeStyles from "@mui/styles/makeStyles" -import { useTranslation } from "react-i18next" -import { onChangeTrimmed, getFormHelpers } from "utils/formUtils" -import TextField from "@mui/material/TextField" -import MenuItem from "@mui/material/MenuItem" +} from "components/Form/Form"; +import makeStyles from "@mui/styles/makeStyles"; +import { useTranslation } from "react-i18next"; +import { onChangeTrimmed, getFormHelpers } from "utils/formUtils"; +import TextField from "@mui/material/TextField"; +import MenuItem from "@mui/material/MenuItem"; import { NANO_HOUR, CreateTokenData, determineDefaultLtValue, filterByMaxTokenLifetime, customLifetimeDay, -} from "./utils" -import { FormikContextType } from "formik" -import dayjs from "dayjs" -import { useNavigate } from "react-router-dom" -import { Stack } from "components/Stack/Stack" +} from "./utils"; +import { FormikContextType } from "formik"; +import dayjs from "dayjs"; +import { useNavigate } from "react-router-dom"; +import { Stack } from "components/Stack/Stack"; interface CreateTokenFormProps { - form: FormikContextType - maxTokenLifetime?: number - formError: unknown - setFormError: (arg0: unknown) => void - isCreating: boolean - creationFailed: boolean + form: FormikContextType; + maxTokenLifetime?: number; + formError: unknown; + setFormError: (arg0: unknown) => void; + isCreating: boolean; + creationFailed: boolean; } export const CreateTokenForm: FC = ({ @@ -39,25 +39,25 @@ export const CreateTokenForm: FC = ({ isCreating, creationFailed, }) => { - const styles = useStyles() - const { t } = useTranslation("tokensPage") - const navigate = useNavigate() + const styles = useStyles(); + const { t } = useTranslation("tokensPage"); + const navigate = useNavigate(); - const [expDays, setExpDays] = useState(1) + const [expDays, setExpDays] = useState(1); const [lifetimeDays, setLifetimeDays] = useState( determineDefaultLtValue(maxTokenLifetime), - ) + ); useEffect(() => { if (lifetimeDays !== "custom") { - void form.setFieldValue("lifetime", lifetimeDays) + void form.setFieldValue("lifetime", lifetimeDays); } else { - void form.setFieldValue("lifetime", expDays) + void form.setFieldValue("lifetime", expDays); } // eslint-disable-next-line react-hooks/exhaustive-deps -- adding form will cause an infinite loop - }, [lifetimeDays, expDays]) + }, [lifetimeDays, expDays]); - const getFieldHelpers = getFormHelpers(form, formError) + const getFieldHelpers = getFormHelpers(form, formError); return ( @@ -97,7 +97,7 @@ export const CreateTokenForm: FC = ({ required defaultValue={determineDefaultLtValue(maxTokenLifetime)} onChange={(event) => { - void setLifetimeDays(event.target.value) + void setLifetimeDays(event.target.value); }} fullWidth > @@ -122,8 +122,8 @@ export const CreateTokenForm: FC = ({ onChange={(event) => { const lt = Math.ceil( dayjs(event.target.value).diff(dayjs(), "day", true), - ) - setExpDays(lt) + ); + setExpDays(lt); }} inputProps={{ min: dayjs().add(1, "day").format("YYYY-MM-DD"), @@ -149,11 +149,11 @@ export const CreateTokenForm: FC = ({ submitLabel={creationFailed ? "Retry" : "Create token"} /> - ) -} + ); +}; const useStyles = makeStyles(() => ({ formSectionInfo: { minWidth: "300px", }, -})) +})); diff --git a/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx b/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx index 34cc8306d2..709ee7fce2 100644 --- a/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx +++ b/site/src/pages/CreateTokenPage/CreateTokenPage.test.tsx @@ -1,32 +1,32 @@ import { renderWithAuth, waitForLoaderToBeRemoved, -} from "testHelpers/renderHelpers" -import { CreateTokenPage } from "pages/CreateTokenPage/CreateTokenPage" -import * as API from "api/api" -import { screen, within } from "@testing-library/react" -import userEvent from "@testing-library/user-event" +} from "testHelpers/renderHelpers"; +import { CreateTokenPage } from "pages/CreateTokenPage/CreateTokenPage"; +import * as API from "api/api"; +import { screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; describe("TokenPage", () => { it("shows the success modal", async () => { jest.spyOn(API, "createToken").mockResolvedValueOnce({ key: "abcd", - }) + }); // When const { container } = renderWithAuth(, { route: "/settings/tokens/new", path: "/settings/tokens/new", - }) - await waitForLoaderToBeRemoved() + }); + await waitForLoaderToBeRemoved(); - const form = container.querySelector("form") as HTMLFormElement - await userEvent.type(screen.getByLabelText(/Name/), "my-token") + const form = container.querySelector("form") as HTMLFormElement; + await userEvent.type(screen.getByLabelText(/Name/), "my-token"); await userEvent.click( within(form).getByRole("button", { name: /create token/i }), - ) + ); // Then - expect(screen.getByText("abcd")).toBeInTheDocument() - }) -}) + expect(screen.getByText("abcd")).toBeInTheDocument(); + }); +}); diff --git a/site/src/pages/CreateTokenPage/CreateTokenPage.tsx b/site/src/pages/CreateTokenPage/CreateTokenPage.tsx index 91b58e41ed..dffb5a5104 100644 --- a/site/src/pages/CreateTokenPage/CreateTokenPage.tsx +++ b/site/src/pages/CreateTokenPage/CreateTokenPage.tsx @@ -1,30 +1,30 @@ -import { FC, useState } from "react" -import { useTranslation } from "react-i18next" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "utils/page" -import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm" -import { useNavigate } from "react-router-dom" -import { useFormik } from "formik" -import { Loader } from "components/Loader/Loader" -import { displaySuccess, displayError } from "components/GlobalSnackbar/utils" -import { useMutation, useQuery } from "@tanstack/react-query" -import { createToken, getTokenConfig } from "api/api" -import { CreateTokenForm } from "./CreateTokenForm" -import { NANO_HOUR, CreateTokenData } from "./utils" -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" -import { CodeExample } from "components/CodeExample/CodeExample" -import { makeStyles } from "@mui/styles" -import { ErrorAlert } from "components/Alert/ErrorAlert" +import { FC, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm"; +import { useNavigate } from "react-router-dom"; +import { useFormik } from "formik"; +import { Loader } from "components/Loader/Loader"; +import { displaySuccess, displayError } from "components/GlobalSnackbar/utils"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { createToken, getTokenConfig } from "api/api"; +import { CreateTokenForm } from "./CreateTokenForm"; +import { NANO_HOUR, CreateTokenData } from "./utils"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { CodeExample } from "components/CodeExample/CodeExample"; +import { makeStyles } from "@mui/styles"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; const initialValues: CreateTokenData = { name: "", lifetime: 30, -} +}; export const CreateTokenPage: FC = () => { - const { t } = useTranslation("tokensPage") - const styles = useStyles() - const navigate = useNavigate() + const { t } = useTranslation("tokensPage"); + const styles = useStyles(); + const navigate = useNavigate(); const { mutate: saveToken, @@ -32,7 +32,7 @@ export const CreateTokenPage: FC = () => { isError: creationFailed, isSuccess: creationSuccessful, data: newToken, - } = useMutation(createToken) + } = useMutation(createToken); const { data: tokenConfig, isLoading: fetchingTokenConfig, @@ -41,19 +41,19 @@ export const CreateTokenPage: FC = () => { } = useQuery({ queryKey: ["tokenconfig"], queryFn: getTokenConfig, - }) + }); - const [formError, setFormError] = useState(undefined) + const [formError, setFormError] = useState(undefined); const onCreateSuccess = () => { - displaySuccess(t("createToken.createSuccess")) - navigate("/settings/tokens") - } + displaySuccess(t("createToken.createSuccess")); + navigate("/settings/tokens"); + }; const onCreateError = (error: unknown) => { - setFormError(error) - displayError(t("createToken.createError")) - } + setFormError(error); + displayError(t("createToken.createError")); + }; const form = useFormik({ initialValues, @@ -67,19 +67,19 @@ export const CreateTokenPage: FC = () => { { onError: onCreateError, }, - ) + ); }, - }) + }); const tokenDescription = ( <>

{t("createToken.successModal.description")}

- ) + ); if (fetchingTokenConfig) { - return + return ; } return ( @@ -113,8 +113,8 @@ export const CreateTokenPage: FC = () => { /> - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ codeExample: { @@ -123,6 +123,6 @@ const useStyles = makeStyles((theme) => ({ width: "100%", marginTop: theme.spacing(3), }, -})) +})); -export default CreateTokenPage +export default CreateTokenPage; diff --git a/site/src/pages/CreateTokenPage/utils.test.tsx b/site/src/pages/CreateTokenPage/utils.test.tsx index 4ab7d18c79..edbde7ae95 100644 --- a/site/src/pages/CreateTokenPage/utils.test.tsx +++ b/site/src/pages/CreateTokenPage/utils.test.tsx @@ -4,13 +4,13 @@ import { lifetimeDayPresets, LifetimeDay, NANO_HOUR, -} from "./utils" +} from "./utils"; describe("unit/CreateTokenForm", () => { describe("filterByMaxTokenLifetime", () => { it.each<{ - maxTokenLifetime: number - expected: LifetimeDay[] + maxTokenLifetime: number; + expected: LifetimeDay[]; }>([ { maxTokenLifetime: 6 * 24 * NANO_HOUR, expected: [] }, { @@ -36,14 +36,14 @@ describe("unit/CreateTokenForm", () => { ])( `filterByMaxTokenLifetime($maxTokenLifetime)`, ({ maxTokenLifetime, expected }) => { - expect(filterByMaxTokenLifetime(maxTokenLifetime)).toEqual(expected) + expect(filterByMaxTokenLifetime(maxTokenLifetime)).toEqual(expected); }, - ) - }) + ); + }); describe("determineDefaultLtValue", () => { it.each<{ - maxTokenLifetime: number - expected: string | number + maxTokenLifetime: number; + expected: string | number; }>([ { maxTokenLifetime: 0, @@ -64,8 +64,8 @@ describe("unit/CreateTokenForm", () => { ])( `determineDefaultLtValue($maxTokenLifetime)`, ({ maxTokenLifetime, expected }) => { - expect(determineDefaultLtValue(maxTokenLifetime)).toEqual(expected) + expect(determineDefaultLtValue(maxTokenLifetime)).toEqual(expected); }, - ) - }) -}) + ); + }); +}); diff --git a/site/src/pages/CreateTokenPage/utils.ts b/site/src/pages/CreateTokenPage/utils.ts index 97b2874241..ac1a2cb634 100644 --- a/site/src/pages/CreateTokenPage/utils.ts +++ b/site/src/pages/CreateTokenPage/utils.ts @@ -1,15 +1,15 @@ -import i18next from "i18next" +import i18next from "i18next"; -export const NANO_HOUR = 3600000000000 +export const NANO_HOUR = 3600000000000; export interface CreateTokenData { - name: string - lifetime: number + name: string; + lifetime: number; } export interface LifetimeDay { - label: string - value: number | string + label: string; + value: number | string; } export const lifetimeDayPresets: LifetimeDay[] = [ @@ -29,43 +29,43 @@ export const lifetimeDayPresets: LifetimeDay[] = [ label: i18next.t("tokensPage:createToken.lifetimeSection.90"), value: 90, }, -] +]; export const customLifetimeDay: LifetimeDay = { label: i18next.t("tokensPage:createToken.lifetimeSection.custom"), value: "custom", -} +}; export const filterByMaxTokenLifetime = ( maxTokenLifetime?: number, ): LifetimeDay[] => { // if maxTokenLifetime hasn't been set, return the full array of options if (!maxTokenLifetime) { - return lifetimeDayPresets + return lifetimeDayPresets; } // otherwise only return options that are less than or equal to the max lifetime return lifetimeDayPresets.filter( (lifetime) => Number(lifetime.value) <= maxTokenLifetime / NANO_HOUR / 24, - ) -} + ); +}; export const determineDefaultLtValue = ( maxTokenLifetime?: number, ): string | number => { - const filteredArr = filterByMaxTokenLifetime(maxTokenLifetime) + const filteredArr = filterByMaxTokenLifetime(maxTokenLifetime); // default to a lifetime of 30 days if within the maxTokenLifetime - const thirtyDayDefault = filteredArr.find((lt) => lt.value === 30) + const thirtyDayDefault = filteredArr.find((lt) => lt.value === 30); if (thirtyDayDefault) { - return thirtyDayDefault.value + return thirtyDayDefault.value; } // otherwise default to the first preset option if (filteredArr[0]) { - return filteredArr[0].value + return filteredArr[0].value; } // if no preset options are within the maxTokenLifetime, default to "custom" - return "custom" -} + return "custom"; +}; diff --git a/site/src/pages/CreateUserPage/CreateUserForm.stories.tsx b/site/src/pages/CreateUserPage/CreateUserForm.stories.tsx index 1984fa0b75..6d623ed70a 100644 --- a/site/src/pages/CreateUserPage/CreateUserForm.stories.tsx +++ b/site/src/pages/CreateUserPage/CreateUserForm.stories.tsx @@ -1,7 +1,7 @@ -import { action } from "@storybook/addon-actions" -import { StoryObj, Meta } from "@storybook/react" -import { CreateUserForm } from "./CreateUserForm" -import { mockApiError } from "testHelpers/entities" +import { action } from "@storybook/addon-actions"; +import { StoryObj, Meta } from "@storybook/react"; +import { CreateUserForm } from "./CreateUserForm"; +import { mockApiError } from "testHelpers/entities"; const meta: Meta = { title: "components/CreateUserForm", @@ -11,12 +11,12 @@ const meta: Meta = { onSubmit: action("submit"), isLoading: false, }, -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; -export const Ready: Story = {} +export const Ready: Story = {}; export const FormError: Story = { args: { @@ -24,7 +24,7 @@ export const FormError: Story = { validations: [{ field: "username", detail: "Username taken" }], }), }, -} +}; export const GeneralError: Story = { args: { @@ -32,10 +32,10 @@ export const GeneralError: Story = { message: "User already exists", }), }, -} +}; export const Loading: Story = { args: { isLoading: true, }, -} +}; diff --git a/site/src/pages/CreateUserPage/CreateUserForm.tsx b/site/src/pages/CreateUserPage/CreateUserForm.tsx index 05d3c77aa6..43767ad921 100644 --- a/site/src/pages/CreateUserPage/CreateUserForm.tsx +++ b/site/src/pages/CreateUserPage/CreateUserForm.tsx @@ -1,18 +1,22 @@ -import TextField from "@mui/material/TextField" -import { FormikContextType, useFormik } from "formik" -import { FC } from "react" -import * as Yup from "yup" -import * as TypesGen from "api/typesGenerated" -import { getFormHelpers, nameValidator, onChangeTrimmed } from "utils/formUtils" -import { FormFooter } from "components/FormFooter/FormFooter" -import { FullPageForm } from "components/FullPageForm/FullPageForm" -import { Stack } from "components/Stack/Stack" -import { ErrorAlert } from "components/Alert/ErrorAlert" -import { hasApiFieldErrors, isApiError } from "api/errors" -import MenuItem from "@mui/material/MenuItem" -import { makeStyles } from "@mui/styles" -import { Theme } from "@mui/material/styles" -import Link from "@mui/material/Link" +import TextField from "@mui/material/TextField"; +import { FormikContextType, useFormik } from "formik"; +import { FC } from "react"; +import * as Yup from "yup"; +import * as TypesGen from "api/typesGenerated"; +import { + getFormHelpers, + nameValidator, + onChangeTrimmed, +} from "utils/formUtils"; +import { FormFooter } from "components/FormFooter/FormFooter"; +import { FullPageForm } from "components/FullPageForm/FullPageForm"; +import { Stack } from "components/Stack/Stack"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { hasApiFieldErrors, isApiError } from "api/errors"; +import MenuItem from "@mui/material/MenuItem"; +import { makeStyles } from "@mui/styles"; +import { Theme } from "@mui/material/styles"; +import Link from "@mui/material/Link"; export const Language = { emailLabel: "Email", @@ -23,7 +27,7 @@ export const Language = { passwordRequired: "Please enter a password.", createUser: "Create", cancel: "Cancel", -} +}; export const authMethodLanguage = { password: { @@ -54,15 +58,15 @@ export const authMethodLanguage = { ), }, -} +}; export interface CreateUserFormProps { - onSubmit: (user: TypesGen.CreateUserRequest) => void - onCancel: () => void - error?: unknown - isLoading: boolean - myOrgId: string - authMethods?: TypesGen.AuthMethods + onSubmit: (user: TypesGen.CreateUserRequest) => void; + onCancel: () => void; + error?: unknown; + isLoading: boolean; + myOrgId: string; + authMethods?: TypesGen.AuthMethods; } const validationSchema = Yup.object({ @@ -77,7 +81,7 @@ const validationSchema = Yup.object({ }), username: nameValidator(Language.usernameLabel), login_type: Yup.string().oneOf(Object.keys(authMethodLanguage)), -}) +}); export const CreateUserForm: FC< React.PropsWithChildren @@ -94,20 +98,20 @@ export const CreateUserForm: FC< }, validationSchema, onSubmit, - }) + }); const getFieldHelpers = getFormHelpers( form, error, - ) + ); - const styles = useStyles() + const styles = useStyles(); const methods = [ authMethods?.password.enabled && "password", authMethods?.oidc.enabled && "oidc", authMethods?.github.enabled && "github", "none", - ].filter(Boolean) as Array + ].filter(Boolean) as Array; return ( @@ -143,9 +147,9 @@ export const CreateUserForm: FC< label="Login Type" onChange={async (e) => { if (e.target.value !== "password") { - await form.setFieldValue("password", "") + await form.setFieldValue("password", ""); } - await form.setFieldValue("login_type", e.target.value) + await form.setFieldValue("login_type", e.target.value); }} SelectProps={{ renderValue: (selected: unknown) => @@ -154,7 +158,7 @@ export const CreateUserForm: FC< }} > {methods.map((value) => { - const language = authMethodLanguage[value] + const language = authMethodLanguage[value]; return ( @@ -164,7 +168,7 @@ export const CreateUserForm: FC< - ) + ); })} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ labelDescription: { @@ -196,4 +200,4 @@ const useStyles = makeStyles((theme) => ({ wordWrap: "normal", whiteSpace: "break-spaces", }, -})) +})); diff --git a/site/src/pages/CreateUserPage/CreateUserPage.test.tsx b/site/src/pages/CreateUserPage/CreateUserPage.test.tsx index 74df3c647c..7baaa57468 100644 --- a/site/src/pages/CreateUserPage/CreateUserPage.test.tsx +++ b/site/src/pages/CreateUserPage/CreateUserPage.test.tsx @@ -1,59 +1,59 @@ -import { fireEvent, screen } from "@testing-library/react" -import userEvent from "@testing-library/user-event" -import { rest } from "msw" -import { Language as FormLanguage } from "./CreateUserForm" -import { Language as FooterLanguage } from "components/FormFooter/FormFooter" +import { fireEvent, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { rest } from "msw"; +import { Language as FormLanguage } from "./CreateUserForm"; +import { Language as FooterLanguage } from "components/FormFooter/FormFooter"; import { renderWithAuth, waitForLoaderToBeRemoved, -} from "testHelpers/renderHelpers" -import { server } from "testHelpers/server" -import { Language as CreateUserLanguage } from "xServices/users/createUserXService" -import { CreateUserPage } from "./CreateUserPage" +} from "testHelpers/renderHelpers"; +import { server } from "testHelpers/server"; +import { Language as CreateUserLanguage } from "xServices/users/createUserXService"; +import { CreateUserPage } from "./CreateUserPage"; const renderCreateUserPage = async () => { renderWithAuth(, { extraRoutes: [{ path: "/users", element:
Users Page
}], - }) - await waitForLoaderToBeRemoved() -} + }); + await waitForLoaderToBeRemoved(); +}; const fillForm = async ({ username = "someuser", email = "someone@coder.com", password = "SomeSecurePassword!", }: { - username?: string - email?: string - password?: string + username?: string; + email?: string; + password?: string; }) => { - const usernameField = screen.getByLabelText(FormLanguage.usernameLabel) - const emailField = screen.getByLabelText(FormLanguage.emailLabel) + const usernameField = screen.getByLabelText(FormLanguage.usernameLabel); + const emailField = screen.getByLabelText(FormLanguage.emailLabel); const passwordField = screen .getByTestId("password-input") - .querySelector("input") + .querySelector("input"); - const loginTypeField = screen.getByTestId("login-type-input") - await userEvent.type(usernameField, username) - await userEvent.type(emailField, email) - await userEvent.type(loginTypeField, "password") - await userEvent.type(passwordField as HTMLElement, password) + const loginTypeField = screen.getByTestId("login-type-input"); + await userEvent.type(usernameField, username); + await userEvent.type(emailField, email); + await userEvent.type(loginTypeField, "password"); + await userEvent.type(passwordField as HTMLElement, password); const submitButton = await screen.findByText( FooterLanguage.defaultSubmitLabel, - ) - fireEvent.click(submitButton) -} + ); + fireEvent.click(submitButton); +}; describe("Create User Page", () => { it("shows validation error message", async () => { - await renderCreateUserPage() - await fillForm({ email: "test" }) - const errorMessage = await screen.findByText(FormLanguage.emailInvalid) - expect(errorMessage).toBeDefined() - }) + await renderCreateUserPage(); + await fillForm({ email: "test" }); + const errorMessage = await screen.findByText(FormLanguage.emailInvalid); + expect(errorMessage).toBeDefined(); + }); it("shows API error message", async () => { - const fieldErrorMessage = "username already in use" + const fieldErrorMessage = "username already in use"; server.use( rest.post("/api/v2/users", async (req, res, ctx) => { return res( @@ -67,21 +67,21 @@ describe("Create User Page", () => { }, ], }), - ) + ); }), - ) - await renderCreateUserPage() - await fillForm({}) - const errorMessage = await screen.findByText(fieldErrorMessage) - expect(errorMessage).toBeDefined() - }) + ); + await renderCreateUserPage(); + await fillForm({}); + const errorMessage = await screen.findByText(fieldErrorMessage); + expect(errorMessage).toBeDefined(); + }); it("shows success notification and redirects to users page", async () => { - await renderCreateUserPage() - await fillForm({}) + await renderCreateUserPage(); + await fillForm({}); const successMessage = await screen.findByText( CreateUserLanguage.createUserSuccess, - ) - expect(successMessage).toBeDefined() - }) -}) + ); + expect(successMessage).toBeDefined(); + }); +}); diff --git a/site/src/pages/CreateUserPage/CreateUserPage.tsx b/site/src/pages/CreateUserPage/CreateUserPage.tsx index 88c9cc47fe..c0c68f1f1e 100644 --- a/site/src/pages/CreateUserPage/CreateUserPage.tsx +++ b/site/src/pages/CreateUserPage/CreateUserPage.tsx @@ -1,38 +1,38 @@ -import { useMachine } from "@xstate/react" -import { useOrganizationId } from "hooks/useOrganizationId" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { useNavigate } from "react-router-dom" -import { createUserMachine } from "xServices/users/createUserXService" -import * as TypesGen from "api/typesGenerated" -import { CreateUserForm } from "./CreateUserForm" -import { Margins } from "components/Margins/Margins" -import { pageTitle } from "utils/page" -import { getAuthMethods } from "api/api" -import { useQuery } from "@tanstack/react-query" +import { useMachine } from "@xstate/react"; +import { useOrganizationId } from "hooks/useOrganizationId"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { useNavigate } from "react-router-dom"; +import { createUserMachine } from "xServices/users/createUserXService"; +import * as TypesGen from "api/typesGenerated"; +import { CreateUserForm } from "./CreateUserForm"; +import { Margins } from "components/Margins/Margins"; +import { pageTitle } from "utils/page"; +import { getAuthMethods } from "api/api"; +import { useQuery } from "@tanstack/react-query"; export const Language = { unknownError: "Oops, an unknown error occurred.", -} +}; export const CreateUserPage: FC = () => { - const myOrgId = useOrganizationId() - const navigate = useNavigate() + const myOrgId = useOrganizationId(); + const navigate = useNavigate(); const [createUserState, createUserSend] = useMachine(createUserMachine, { actions: { redirectToUsersPage: () => { - navigate("/users") + navigate("/users"); }, }, - }) - const { error } = createUserState.context + }); + const { error } = createUserState.context; // TODO: We should probably place this somewhere else to reduce the number of calls. // This would be called each time this page is loaded. const { data: authMethods } = useQuery({ queryKey: ["authMethods"], queryFn: getAuthMethods, - }) + }); return ( @@ -47,14 +47,14 @@ export const CreateUserPage: FC = () => { createUserSend({ type: "CREATE", user }) } onCancel={() => { - createUserSend("CANCEL_CREATE_USER") - navigate("/users") + createUserSend("CANCEL_CREATE_USER"); + navigate("/users"); }} isLoading={createUserState.hasTag("loading")} myOrgId={myOrgId} /> - ) -} + ); +}; -export default CreateUserPage +export default CreateUserPage; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index b34ff6fd97..5f47c5dcba 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -1,7 +1,7 @@ -import { fireEvent, screen, waitFor } from "@testing-library/react" -import userEvent from "@testing-library/user-event" -import * as API from "api/api" -import i18next from "i18next" +import { fireEvent, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import * as API from "api/api"; +import i18next from "i18next"; import { MockTemplate, MockUser, @@ -13,34 +13,34 @@ import { MockTemplateVersionParameter3, MockTemplateVersionGitAuth, MockOrganization, -} from "testHelpers/entities" +} from "testHelpers/entities"; import { renderWithAuth, waitForLoaderToBeRemoved, -} from "testHelpers/renderHelpers" -import CreateWorkspacePage from "./CreateWorkspacePage" +} from "testHelpers/renderHelpers"; +import CreateWorkspacePage from "./CreateWorkspacePage"; -const { t } = i18next +const { t } = i18next; -const nameLabelText = t("nameLabel", { ns: "createWorkspacePage" }) -const createWorkspaceText = t("createWorkspace", { ns: "createWorkspacePage" }) +const nameLabelText = t("nameLabel", { ns: "createWorkspacePage" }); +const createWorkspaceText = t("createWorkspace", { ns: "createWorkspacePage" }); const validationNumberNotInRangeText = t("validationNumberNotInRange", { ns: "createWorkspacePage", min: "1", max: "3", -}) +}); const validationPatternNotMatched = t("validationPatternNotMatched", { ns: "createWorkspacePage", error: MockTemplateVersionParameter3.validation_error, pattern: "^[a-z]{3}$", -}) +}); const renderCreateWorkspacePage = () => { return renderWithAuth(, { route: "/templates/" + MockTemplate.name + "/workspace", path: "/templates/:template/workspace", - }) -} + }); +}; Object.defineProperty(window, "BroadcastChannel", { value: class { @@ -51,56 +51,56 @@ Object.defineProperty(window, "BroadcastChannel", { // noop } }, -}) +}); describe("CreateWorkspacePage", () => { it("renders", async () => { jest .spyOn(API, "getTemplateVersionRichParameters") - .mockResolvedValueOnce([MockTemplateVersionParameter1]) - renderCreateWorkspacePage() + .mockResolvedValueOnce([MockTemplateVersionParameter1]); + renderCreateWorkspacePage(); - const element = await screen.findByText(createWorkspaceText) - expect(element).toBeDefined() - }) + const element = await screen.findByText(createWorkspaceText); + expect(element).toBeDefined(); + }); it("renders with rich parameter", async () => { jest .spyOn(API, "getTemplateVersionRichParameters") - .mockResolvedValueOnce([MockTemplateVersionParameter1]) - renderCreateWorkspacePage() + .mockResolvedValueOnce([MockTemplateVersionParameter1]); + renderCreateWorkspacePage(); - const element = await screen.findByText(createWorkspaceText) - expect(element).toBeDefined() + const element = await screen.findByText(createWorkspaceText); + expect(element).toBeDefined(); const firstParameter = await screen.findByText( MockTemplateVersionParameter1.description, - ) - expect(firstParameter).toBeDefined() - }) + ); + expect(firstParameter).toBeDefined(); + }); it("succeeds with default owner", async () => { jest .spyOn(API, "getUsers") - .mockResolvedValueOnce({ users: [MockUser], count: 1 }) + .mockResolvedValueOnce({ users: [MockUser], count: 1 }); jest .spyOn(API, "getWorkspaceQuota") - .mockResolvedValueOnce(MockWorkspaceQuota) - jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace) + .mockResolvedValueOnce(MockWorkspaceQuota); + jest.spyOn(API, "createWorkspace").mockResolvedValueOnce(MockWorkspace); jest .spyOn(API, "getTemplateVersionRichParameters") - .mockResolvedValueOnce([MockTemplateVersionParameter1]) + .mockResolvedValueOnce([MockTemplateVersionParameter1]); - renderCreateWorkspacePage() + renderCreateWorkspacePage(); - const nameField = await screen.findByLabelText(nameLabelText) + const nameField = await screen.findByLabelText(nameLabelText); // have to use fireEvent b/c userEvent isn't cleaning up properly between tests fireEvent.change(nameField, { target: { value: "test" }, - }) + }); - const submitButton = screen.getByText(createWorkspaceText) - await userEvent.click(submitButton) + const submitButton = screen.getByText(createWorkspaceText); + await userEvent.click(submitButton); await waitFor(() => expect(API.createWorkspace).toBeCalledWith( @@ -110,15 +110,15 @@ describe("CreateWorkspacePage", () => { ...MockWorkspaceRequest, }, ), - ) - }) + ); + }); it("uses default rich param values passed from the URL", async () => { - const param = "first_parameter" - const paramValue = "It works!" + const param = "first_parameter"; + const paramValue = "It works!"; jest .spyOn(API, "getTemplateVersionRichParameters") - .mockResolvedValueOnce([MockTemplateVersionParameter1]) + .mockResolvedValueOnce([MockTemplateVersionParameter1]); renderWithAuth(, { route: @@ -126,10 +126,10 @@ describe("CreateWorkspacePage", () => { MockTemplate.name + `/workspace?param.${param}=${paramValue}`, path: "/templates/:template/workspace", - }) + }); - await screen.findByDisplayValue(paramValue) - }) + await screen.findByDisplayValue(paramValue); + }); it("rich parameter: number validation fails", async () => { jest @@ -137,34 +137,34 @@ describe("CreateWorkspacePage", () => { .mockResolvedValueOnce([ MockTemplateVersionParameter1, MockTemplateVersionParameter2, - ]) + ]); - renderCreateWorkspacePage() - await waitForLoaderToBeRemoved() + renderCreateWorkspacePage(); + await waitForLoaderToBeRemoved(); - const element = await screen.findByText("Create Workspace") - expect(element).toBeDefined() + const element = await screen.findByText("Create Workspace"); + expect(element).toBeDefined(); const secondParameter = await screen.findByText( MockTemplateVersionParameter2.description, - ) - expect(secondParameter).toBeDefined() + ); + expect(secondParameter).toBeDefined(); const secondParameterField = await screen.findByLabelText( MockTemplateVersionParameter2.name, { exact: false }, - ) - expect(secondParameterField).toBeDefined() + ); + expect(secondParameterField).toBeDefined(); fireEvent.change(secondParameterField, { target: { value: "4" }, - }) - fireEvent.submit(secondParameter) + }); + fireEvent.submit(secondParameter); const validationError = await screen.findByText( validationNumberNotInRangeText, - ) - expect(validationError).toBeDefined() - }) + ); + expect(validationError).toBeDefined(); + }); it("rich parameter: string validation fails", async () => { jest @@ -172,57 +172,59 @@ describe("CreateWorkspacePage", () => { .mockResolvedValueOnce([ MockTemplateVersionParameter1, MockTemplateVersionParameter3, - ]) + ]); - renderCreateWorkspacePage() - await waitForLoaderToBeRemoved() + renderCreateWorkspacePage(); + await waitForLoaderToBeRemoved(); - const element = await screen.findByText(createWorkspaceText) - expect(element).toBeDefined() + const element = await screen.findByText(createWorkspaceText); + expect(element).toBeDefined(); const thirdParameter = await screen.findByText( MockTemplateVersionParameter3.description, - ) - expect(thirdParameter).toBeDefined() + ); + expect(thirdParameter).toBeDefined(); const thirdParameterField = await screen.findByLabelText( MockTemplateVersionParameter3.name, { exact: false }, - ) - expect(thirdParameterField).toBeDefined() + ); + expect(thirdParameterField).toBeDefined(); fireEvent.change(thirdParameterField, { target: { value: "1234" }, - }) - fireEvent.submit(thirdParameterField) + }); + fireEvent.submit(thirdParameterField); - const validationError = await screen.findByText(validationPatternNotMatched) - expect(validationError).toBeInTheDocument() - }) + const validationError = await screen.findByText( + validationPatternNotMatched, + ); + expect(validationError).toBeInTheDocument(); + }); it("gitauth: errors if unauthenticated and submits", async () => { jest .spyOn(API, "getTemplateVersionGitAuth") - .mockResolvedValueOnce([MockTemplateVersionGitAuth]) + .mockResolvedValueOnce([MockTemplateVersionGitAuth]); - renderCreateWorkspacePage() - await waitForLoaderToBeRemoved() + renderCreateWorkspacePage(); + await waitForLoaderToBeRemoved(); - const nameField = await screen.findByLabelText(nameLabelText) + const nameField = await screen.findByLabelText(nameLabelText); // have to use fireEvent b/c userEvent isn't cleaning up properly between tests fireEvent.change(nameField, { target: { value: "test" }, - }) + }); - const submitButton = screen.getByText(createWorkspaceText) - await userEvent.click(submitButton) + const submitButton = screen.getByText(createWorkspaceText); + await userEvent.click(submitButton); - await screen.findByText("You must authenticate to create a workspace!") - }) + await screen.findByText("You must authenticate to create a workspace!"); + }); it("auto create a workspace if uses mode=auto", async () => { - const param = "first_parameter" - const paramValue = "It works!" - const createWorkspaceSpy = jest.spyOn(API, "createWorkspace") + const param = "first_parameter"; + const paramValue = "It works!"; + const createWorkspaceSpy = jest.spyOn(API, "createWorkspace"); renderWithAuth(, { route: @@ -230,7 +232,7 @@ describe("CreateWorkspacePage", () => { MockTemplate.name + `/workspace?param.${param}=${paramValue}&mode=auto`, path: "/templates/:template/workspace", - }) + }); await waitFor(() => { expect(createWorkspaceSpy).toBeCalledWith( @@ -240,7 +242,7 @@ describe("CreateWorkspacePage", () => { template_id: MockTemplate.id, rich_parameter_values: [{ name: param, value: paramValue }], }), - ) - }) - }) -}) + ); + }); + }); +}); diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx index d49ea38894..0af38bf6b2 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx @@ -1,39 +1,39 @@ -import { useMachine } from "@xstate/react" +import { useMachine } from "@xstate/react"; import { Template, TemplateVersionGitAuth, TemplateVersionParameter, WorkspaceBuildParameter, -} from "api/typesGenerated" -import { useMe } from "hooks/useMe" -import { useOrganizationId } from "hooks/useOrganizationId" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { useNavigate, useParams, useSearchParams } from "react-router-dom" -import { pageTitle } from "utils/page" +} from "api/typesGenerated"; +import { useMe } from "hooks/useMe"; +import { useOrganizationId } from "hooks/useOrganizationId"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; +import { pageTitle } from "utils/page"; import { CreateWSPermissions, CreateWorkspaceMode, createWorkspaceMachine, -} from "xServices/createWorkspace/createWorkspaceXService" -import { CreateWorkspacePageView } from "./CreateWorkspacePageView" -import { Loader } from "components/Loader/Loader" -import { ErrorAlert } from "components/Alert/ErrorAlert" +} from "xServices/createWorkspace/createWorkspaceXService"; +import { CreateWorkspacePageView } from "./CreateWorkspacePageView"; +import { Loader } from "components/Loader/Loader"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; import { uniqueNamesGenerator, animals, colors, NumberDictionary, -} from "unique-names-generator" +} from "unique-names-generator"; const CreateWorkspacePage: FC = () => { - const organizationId = useOrganizationId() - const { template: templateName } = useParams() as { template: string } - const me = useMe() - const navigate = useNavigate() - const [searchParams] = useSearchParams() - const defaultBuildParameters = getDefaultBuildParameters(searchParams) - const mode = (searchParams.get("mode") ?? "form") as CreateWorkspaceMode + const organizationId = useOrganizationId(); + const { template: templateName } = useParams() as { template: string }; + const me = useMe(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const defaultBuildParameters = getDefaultBuildParameters(searchParams); + const mode = (searchParams.get("mode") ?? "form") as CreateWorkspaceMode; const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, { context: { organizationId, @@ -45,15 +45,15 @@ const CreateWorkspacePage: FC = () => { }, actions: { onCreateWorkspace: (_, event) => { - navigate(`/@${event.data.owner_name}/${event.data.name}`) + navigate(`/@${event.data.owner_name}/${event.data.name}`); }, }, - }) + }); const { template, error, parameters, permissions, gitAuth, defaultName } = - createWorkspaceState.context + createWorkspaceState.context; const title = createWorkspaceState.matches("autoCreating") ? "Creating workspace..." - : "Create Workspace" + : "Create Workspace"; return ( <> @@ -79,57 +79,57 @@ const CreateWorkspacePage: FC = () => { parameters={parameters as TemplateVersionParameter[]} creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")} onCancel={() => { - navigate(-1) + navigate(-1); }} onSubmit={(request, owner) => { send({ type: "CREATE_WORKSPACE", request, owner, - }) + }); }} /> )} - ) -} + ); +}; -export default CreateWorkspacePage +export default CreateWorkspacePage; const getDefaultBuildParameters = ( urlSearchParams: URLSearchParams, ): WorkspaceBuildParameter[] => { - const buildValues: WorkspaceBuildParameter[] = [] + const buildValues: WorkspaceBuildParameter[] = []; Array.from(urlSearchParams.keys()) .filter((key) => key.startsWith("param.")) .forEach((key) => { - const name = key.replace("param.", "") - const value = urlSearchParams.get(key) ?? "" - buildValues.push({ name, value }) - }) - return buildValues -} + const name = key.replace("param.", ""); + const value = urlSearchParams.get(key) ?? ""; + buildValues.push({ name, value }); + }); + return buildValues; +}; export const orderedTemplateParameters = ( templateParameters?: TemplateVersionParameter[], ): TemplateVersionParameter[] => { if (!templateParameters) { - return [] + return []; } const immutables = templateParameters.filter( (parameter) => !parameter.mutable, - ) - const mutables = templateParameters.filter((parameter) => parameter.mutable) - return [...immutables, ...mutables] -} + ); + const mutables = templateParameters.filter((parameter) => parameter.mutable); + return [...immutables, ...mutables]; +}; const generateUniqueName = () => { - const numberDictionary = NumberDictionary.generate({ min: 0, max: 99 }) + const numberDictionary = NumberDictionary.generate({ min: 0, max: 99 }); return uniqueNamesGenerator({ dictionaries: [colors, animals, numberDictionary], separator: "-", length: 3, style: "lowerCase", - }) -} + }); +}; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index 51cfc8c69b..bf3fab55aa 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -1,4 +1,4 @@ -import { Meta, StoryObj } from "@storybook/react" +import { Meta, StoryObj } from "@storybook/react"; import { mockApiError, MockTemplate, @@ -6,8 +6,8 @@ import { MockTemplateVersionParameter2, MockTemplateVersionParameter3, MockUser, -} from "../../testHelpers/entities" -import { CreateWorkspacePageView } from "./CreateWorkspacePageView" +} from "../../testHelpers/entities"; +import { CreateWorkspacePageView } from "./CreateWorkspacePageView"; const meta: Meta = { title: "components/Alert", @@ -23,12 +23,12 @@ const meta: Meta = { createWorkspaceForUser: true, }, }, -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; -export const NoParameters: Story = {} +export const NoParameters: Story = {}; export const CreateWorkspaceError: Story = { args: { @@ -43,7 +43,7 @@ export const CreateWorkspaceError: Story = { ], }), }, -} +}; export const Parameters: Story = { args: { @@ -84,7 +84,7 @@ export const Parameters: Story = { }, ], }, -} +}; export const GitAuth: Story = { args: { @@ -103,4 +103,4 @@ export const GitAuth: Story = { }, ], }, -} +}; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 8ca6f178a9..9980bf270a 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -1,44 +1,51 @@ -import TextField from "@mui/material/TextField" -import * as TypesGen from "api/typesGenerated" -import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete" -import { FormikContextType, useFormik } from "formik" -import { FC, useEffect, useState } from "react" -import { useTranslation } from "react-i18next" -import { getFormHelpers, nameValidator, onChangeTrimmed } from "utils/formUtils" -import * as Yup from "yup" -import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm" -import { SelectedTemplate } from "./SelectedTemplate" +import TextField from "@mui/material/TextField"; +import * as TypesGen from "api/typesGenerated"; +import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; +import { FormikContextType, useFormik } from "formik"; +import { FC, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + getFormHelpers, + nameValidator, + onChangeTrimmed, +} from "utils/formUtils"; +import * as Yup from "yup"; +import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm"; +import { SelectedTemplate } from "./SelectedTemplate"; import { FormFields, FormSection, FormFooter, HorizontalForm, -} from "components/Form/Form" -import { makeStyles } from "@mui/styles" +} from "components/Form/Form"; +import { makeStyles } from "@mui/styles"; import { getInitialRichParameterValues, useValidationSchemaForRichParameters, -} from "utils/richParameters" +} from "utils/richParameters"; import { ImmutableTemplateParametersSection, MutableTemplateParametersSection, -} from "components/TemplateParameters/TemplateParameters" -import { CreateWSPermissions } from "xServices/createWorkspace/createWorkspaceXService" -import { GitAuth } from "./GitAuth" -import { ErrorAlert } from "components/Alert/ErrorAlert" +} from "components/TemplateParameters/TemplateParameters"; +import { CreateWSPermissions } from "xServices/createWorkspace/createWorkspaceXService"; +import { GitAuth } from "./GitAuth"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; export interface CreateWorkspacePageViewProps { - error: unknown - defaultName: string - defaultOwner: TypesGen.User - template: TypesGen.Template - gitAuth: TypesGen.TemplateVersionGitAuth[] - parameters: TypesGen.TemplateVersionParameter[] - defaultBuildParameters: TypesGen.WorkspaceBuildParameter[] - permissions: CreateWSPermissions - creatingWorkspace: boolean - onCancel: () => void - onSubmit: (req: TypesGen.CreateWorkspaceRequest, owner: TypesGen.User) => void + error: unknown; + defaultName: string; + defaultOwner: TypesGen.User; + template: TypesGen.Template; + gitAuth: TypesGen.TemplateVersionGitAuth[]; + parameters: TypesGen.TemplateVersionParameter[]; + defaultBuildParameters: TypesGen.WorkspaceBuildParameter[]; + permissions: CreateWSPermissions; + creatingWorkspace: boolean; + onCancel: () => void; + onSubmit: ( + req: TypesGen.CreateWorkspaceRequest, + owner: TypesGen.User, + ) => void; } export const CreateWorkspacePageView: FC = ({ @@ -54,10 +61,10 @@ export const CreateWorkspacePageView: FC = ({ onSubmit, onCancel, }) => { - const { t } = useTranslation("createWorkspacePage") - const styles = useStyles() - const [owner, setOwner] = useState(defaultOwner) - const { verifyGitAuth, gitAuthErrors } = useGitAuthVerification(gitAuth) + const { t } = useTranslation("createWorkspacePage"); + const styles = useStyles(); + const [owner, setOwner] = useState(defaultOwner); + const { verifyGitAuth, gitAuthErrors } = useGitAuthVerification(gitAuth); const form: FormikContextType = useFormik({ initialValues: { @@ -78,24 +85,24 @@ export const CreateWorkspacePageView: FC = ({ enableReinitialize: true, onSubmit: (request) => { if (!verifyGitAuth()) { - form.setSubmitting(false) - return + form.setSubmitting(false); + return; } - onSubmit(request, owner) + onSubmit(request, owner); }, - }) + }); useEffect(() => { if (error) { - window.scrollTo(0, 0) + window.scrollTo(0, 0); } - }, [error]) + }, [error]); const getFieldHelpers = getFormHelpers( form, error, - ) + ); return ( @@ -128,7 +135,7 @@ export const CreateWorkspacePageView: FC = ({ { - setOwner(user ?? defaultOwner) + setOwner(user ?? defaultOwner); }} label={t("ownerLabel").toString()} size="medium" @@ -169,10 +176,10 @@ export const CreateWorkspacePageView: FC = ({ await form.setFieldValue("rich_parameter_values." + index, { name: parameter.name, value: value, - }) + }); }, disabled: form.isSubmitting, - } + }; }} /> = ({ await form.setFieldValue("rich_parameter_values." + index, { name: parameter.name, value: value, - }) + }); }, disabled: form.isSubmitting, - } + }; }} /> @@ -203,13 +210,13 @@ export const CreateWorkspacePageView: FC = ({ /> - ) -} + ); +}; -type GitAuthErrors = Record +type GitAuthErrors = Record; const useGitAuthVerification = (gitAuth: TypesGen.TemplateVersionGitAuth[]) => { - const [gitAuthErrors, setGitAuthErrors] = useState({}) + const [gitAuthErrors, setGitAuthErrors] = useState({}); useEffect(() => { // templateGitAuth is refreshed automatically using a BroadcastChannel @@ -217,32 +224,32 @@ const useGitAuthVerification = (gitAuth: TypesGen.TemplateVersionGitAuth[]) => { // // If the provider becomes authenticated, we want the error message // to disappear. - setGitAuthErrors({}) - }, [gitAuth]) + setGitAuthErrors({}); + }, [gitAuth]); const verifyGitAuth = () => { - const errors: GitAuthErrors = {} + const errors: GitAuthErrors = {}; for (let i = 0; i < gitAuth.length; i++) { - const auth = gitAuth.at(i) + const auth = gitAuth.at(i); if (!auth) { - continue + continue; } if (!auth.authenticated) { - errors[auth.id] = "You must authenticate to create a workspace!" + errors[auth.id] = "You must authenticate to create a workspace!"; } } - setGitAuthErrors(errors) - const isValid = Object.keys(errors).length === 0 - return isValid - } + setGitAuthErrors(errors); + const isValid = Object.keys(errors).length === 0; + return isValid; + }; return { gitAuthErrors, verifyGitAuth, - } -} + }; +}; const useStyles = makeStyles((theme) => ({ warningText: { @@ -256,4 +263,4 @@ const useStyles = makeStyles((theme) => ({ marginLeft: theme.spacing(-10), marginRight: theme.spacing(-10), }, -})) +})); diff --git a/site/src/pages/CreateWorkspacePage/GitAuth.stories.tsx b/site/src/pages/CreateWorkspacePage/GitAuth.stories.tsx index cda012212d..26eb8b5841 100644 --- a/site/src/pages/CreateWorkspacePage/GitAuth.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/GitAuth.stories.tsx @@ -1,57 +1,57 @@ -import { Story } from "@storybook/react" -import { GitAuth, GitAuthProps } from "./GitAuth" +import { Story } from "@storybook/react"; +import { GitAuth, GitAuthProps } from "./GitAuth"; export default { title: "components/GitAuth", component: GitAuth, -} +}; -const Template: Story = (args) => +const Template: Story = (args) => ; -export const GithubNotAuthenticated = Template.bind({}) +export const GithubNotAuthenticated = Template.bind({}); GithubNotAuthenticated.args = { type: "github", authenticated: false, -} +}; -export const GithubAuthenticated = Template.bind({}) +export const GithubAuthenticated = Template.bind({}); GithubAuthenticated.args = { type: "github", authenticated: true, -} +}; -export const GitlabNotAuthenticated = Template.bind({}) +export const GitlabNotAuthenticated = Template.bind({}); GitlabNotAuthenticated.args = { type: "gitlab", authenticated: false, -} +}; -export const GitlabAuthenticated = Template.bind({}) +export const GitlabAuthenticated = Template.bind({}); GitlabAuthenticated.args = { type: "gitlab", authenticated: true, -} +}; -export const AzureDevOpsNotAuthenticated = Template.bind({}) +export const AzureDevOpsNotAuthenticated = Template.bind({}); AzureDevOpsNotAuthenticated.args = { type: "azure-devops", authenticated: false, -} +}; -export const AzureDevOpsAuthenticated = Template.bind({}) +export const AzureDevOpsAuthenticated = Template.bind({}); AzureDevOpsAuthenticated.args = { type: "azure-devops", authenticated: true, -} +}; -export const BitbucketNotAuthenticated = Template.bind({}) +export const BitbucketNotAuthenticated = Template.bind({}); BitbucketNotAuthenticated.args = { type: "bitbucket", authenticated: false, -} +}; -export const BitbucketAuthenticated = Template.bind({}) +export const BitbucketAuthenticated = Template.bind({}); BitbucketAuthenticated.args = { type: "bitbucket", authenticated: true, -} +}; diff --git a/site/src/pages/CreateWorkspacePage/GitAuth.tsx b/site/src/pages/CreateWorkspacePage/GitAuth.tsx index b5b553242e..69315899b5 100644 --- a/site/src/pages/CreateWorkspacePage/GitAuth.tsx +++ b/site/src/pages/CreateWorkspacePage/GitAuth.tsx @@ -1,20 +1,20 @@ -import Button from "@mui/material/Button" -import FormHelperText from "@mui/material/FormHelperText" -import { SvgIconProps } from "@mui/material/SvgIcon" -import Tooltip from "@mui/material/Tooltip" -import GitHub from "@mui/icons-material/GitHub" -import * as TypesGen from "api/typesGenerated" -import { AzureDevOpsIcon } from "components/Icons/AzureDevOpsIcon" -import { BitbucketIcon } from "components/Icons/BitbucketIcon" -import { GitlabIcon } from "components/Icons/GitlabIcon" -import { FC } from "react" -import { makeStyles } from "@mui/styles" +import Button from "@mui/material/Button"; +import FormHelperText from "@mui/material/FormHelperText"; +import { SvgIconProps } from "@mui/material/SvgIcon"; +import Tooltip from "@mui/material/Tooltip"; +import GitHub from "@mui/icons-material/GitHub"; +import * as TypesGen from "api/typesGenerated"; +import { AzureDevOpsIcon } from "components/Icons/AzureDevOpsIcon"; +import { BitbucketIcon } from "components/Icons/BitbucketIcon"; +import { GitlabIcon } from "components/Icons/GitlabIcon"; +import { FC } from "react"; +import { makeStyles } from "@mui/styles"; export interface GitAuthProps { - type: TypesGen.GitProvider - authenticated: boolean - authenticateURL: string - error?: string + type: TypesGen.GitProvider; + authenticated: boolean; + authenticateURL: string; + error?: string; } export const GitAuth: FC = ({ @@ -25,29 +25,29 @@ export const GitAuth: FC = ({ }) => { const styles = useStyles({ error: typeof error !== "undefined", - }) + }); - let prettyName: string - let Icon: (props: SvgIconProps) => JSX.Element + let prettyName: string; + let Icon: (props: SvgIconProps) => JSX.Element; switch (type) { case "azure-devops": - prettyName = "Azure DevOps" - Icon = AzureDevOpsIcon - break + prettyName = "Azure DevOps"; + Icon = AzureDevOpsIcon; + break; case "bitbucket": - prettyName = "Bitbucket" - Icon = BitbucketIcon - break + prettyName = "Bitbucket"; + Icon = BitbucketIcon; + break; case "github": - prettyName = "GitHub" - Icon = GitHub as (props: SvgIconProps) => JSX.Element - break + prettyName = "GitHub"; + Icon = GitHub as (props: SvgIconProps) => JSX.Element; + break; case "gitlab": - prettyName = "GitLab" - Icon = GitlabIcon - break + prettyName = "GitLab"; + Icon = GitlabIcon; + break; default: - throw new Error("invalid git provider: " + type) + throw new Error("invalid git provider: " + type); } return ( @@ -67,12 +67,12 @@ export const GitAuth: FC = ({ color={error ? "error" : undefined} fullWidth onClick={(event) => { - event.preventDefault() + event.preventDefault(); // If the user is already authenticated, we don't want to redirect them if (authenticated || authenticateURL === "") { - return + return; } - window.open(authenticateURL, "_blank", "width=900,height=600") + window.open(authenticateURL, "_blank", "width=900,height=600"); }} > {authenticated @@ -83,11 +83,11 @@ export const GitAuth: FC = ({ {error && {error}} - ) -} + ); +}; const useStyles = makeStyles(() => ({ button: { height: 52, }, -})) +})); diff --git a/site/src/pages/CreateWorkspacePage/SelectedTemplate.stories.tsx b/site/src/pages/CreateWorkspacePage/SelectedTemplate.stories.tsx index 254ccdf791..c2aae06763 100644 --- a/site/src/pages/CreateWorkspacePage/SelectedTemplate.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/SelectedTemplate.stories.tsx @@ -1,28 +1,28 @@ -import { ComponentMeta, Story } from "@storybook/react" -import { MockTemplate } from "../../testHelpers/entities" -import { SelectedTemplate, SelectedTemplateProps } from "./SelectedTemplate" +import { ComponentMeta, Story } from "@storybook/react"; +import { MockTemplate } from "../../testHelpers/entities"; +import { SelectedTemplate, SelectedTemplateProps } from "./SelectedTemplate"; export default { title: "components/SelectedTemplate", component: SelectedTemplate, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) +); -export const WithIcon = Template.bind({}) +export const WithIcon = Template.bind({}); WithIcon.args = { template: { ...MockTemplate, icon: "/icon/docker.png", }, -} +}; -export const WithoutIcon = Template.bind({}) +export const WithoutIcon = Template.bind({}); WithoutIcon.args = { template: { ...MockTemplate, icon: "", }, -} +}; diff --git a/site/src/pages/CreateWorkspacePage/SelectedTemplate.tsx b/site/src/pages/CreateWorkspacePage/SelectedTemplate.tsx index 5f022b12ad..219851a64e 100644 --- a/site/src/pages/CreateWorkspacePage/SelectedTemplate.tsx +++ b/site/src/pages/CreateWorkspacePage/SelectedTemplate.tsx @@ -1,15 +1,15 @@ -import { makeStyles } from "@mui/styles" -import { Template, TemplateExample } from "api/typesGenerated" -import { Avatar } from "components/Avatar/Avatar" -import { Stack } from "components/Stack/Stack" -import { FC } from "react" +import { makeStyles } from "@mui/styles"; +import { Template, TemplateExample } from "api/typesGenerated"; +import { Avatar } from "components/Avatar/Avatar"; +import { Stack } from "components/Stack/Stack"; +import { FC } from "react"; export interface SelectedTemplateProps { - template: Template | TemplateExample + template: Template | TemplateExample; } export const SelectedTemplate: FC = ({ template }) => { - const styles = useStyles() + const styles = useStyles(); return ( = ({ template }) => { )} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ template: { @@ -58,4 +58,4 @@ const useStyles = makeStyles((theme) => ({ fontSize: 14, color: theme.palette.text.secondary, }, -})) +})); diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx index 7386bd3732..830fef0fb8 100644 --- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx @@ -1,18 +1,18 @@ -import { UpdateAppearanceConfig } from "api/typesGenerated" -import { useDashboard } from "components/Dashboard/DashboardProvider" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "utils/page" -import { AppearanceSettingsPageView } from "./AppearanceSettingsPageView" +import { UpdateAppearanceConfig } from "api/typesGenerated"; +import { useDashboard } from "components/Dashboard/DashboardProvider"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { AppearanceSettingsPageView } from "./AppearanceSettingsPageView"; // ServiceBanner is unlike the other Deployment Settings pages because it // implements a form, whereas the others are read-only. We make this // exception because the Service Banner is visual, and configuring it from // the command line would be a significantly worse user experience. const AppearanceSettingsPage: FC = () => { - const { appearance, entitlements } = useDashboard() + const { appearance, entitlements } = useDashboard(); const isEntitled = - entitlements.features["appearance"].entitlement !== "not_entitled" + entitlements.features["appearance"].entitlement !== "not_entitled"; const updateAppearance = ( newConfig: Partial, @@ -21,13 +21,13 @@ const AppearanceSettingsPage: FC = () => { const newAppearance = { ...appearance.config, ...newConfig, - } + }; if (preview) { - appearance.setPreview(newAppearance) - return + appearance.setPreview(newAppearance); + return; } - appearance.save(newAppearance) - } + appearance.save(newAppearance); + }; return ( <> @@ -41,7 +41,7 @@ const AppearanceSettingsPage: FC = () => { updateAppearance={updateAppearance} /> - ) -} + ); +}; -export default AppearanceSettingsPage +export default AppearanceSettingsPage; diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx index 3c9ef50c8d..1332e4c5ea 100644 --- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx @@ -1,8 +1,8 @@ -import { ComponentMeta, Story } from "@storybook/react" +import { ComponentMeta, Story } from "@storybook/react"; import { AppearanceSettingsPageView, AppearanceSettingsPageViewProps, -} from "./AppearanceSettingsPageView" +} from "./AppearanceSettingsPageView"; export default { title: "pages/AppearanceSettingsPageView", @@ -18,12 +18,12 @@ export default { }, isEntitled: false, updateAppearance: () => { - return undefined + return undefined; }, }, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) -export const Page = Template.bind({}) +); +export const Page = Template.bind({}); diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx index d14aad57ca..3a41dc8c0f 100644 --- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx @@ -1,53 +1,53 @@ -import { useState } from "react" -import { Header } from "components/DeploySettingsLayout/Header" +import { useState } from "react"; +import { Header } from "components/DeploySettingsLayout/Header"; import { Badges, DisabledBadge, EnterpriseBadge, EntitledBadge, -} from "components/DeploySettingsLayout/Badges" -import InputAdornment from "@mui/material/InputAdornment" -import { Fieldset } from "components/DeploySettingsLayout/Fieldset" -import { getFormHelpers } from "utils/formUtils" -import Button from "@mui/material/Button" -import FormControlLabel from "@mui/material/FormControlLabel" -import { BlockPicker } from "react-color" -import { useTranslation } from "react-i18next" -import makeStyles from "@mui/styles/makeStyles" -import Switch from "@mui/material/Switch" -import TextField from "@mui/material/TextField" -import { UpdateAppearanceConfig } from "api/typesGenerated" -import { Stack } from "components/Stack/Stack" -import { useFormik } from "formik" -import { useTheme } from "@mui/styles" -import Link from "@mui/material/Link" -import { colors } from "theme/colors" +} from "components/DeploySettingsLayout/Badges"; +import InputAdornment from "@mui/material/InputAdornment"; +import { Fieldset } from "components/DeploySettingsLayout/Fieldset"; +import { getFormHelpers } from "utils/formUtils"; +import Button from "@mui/material/Button"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import { BlockPicker } from "react-color"; +import { useTranslation } from "react-i18next"; +import makeStyles from "@mui/styles/makeStyles"; +import Switch from "@mui/material/Switch"; +import TextField from "@mui/material/TextField"; +import { UpdateAppearanceConfig } from "api/typesGenerated"; +import { Stack } from "components/Stack/Stack"; +import { useFormik } from "formik"; +import { useTheme } from "@mui/styles"; +import Link from "@mui/material/Link"; +import { colors } from "theme/colors"; export type AppearanceSettingsPageViewProps = { - appearance: UpdateAppearanceConfig - isEntitled: boolean + appearance: UpdateAppearanceConfig; + isEntitled: boolean; updateAppearance: ( newConfig: Partial, preview: boolean, - ) => void -} + ) => void; +}; export const AppearanceSettingsPageView = ({ appearance, isEntitled, updateAppearance, }: AppearanceSettingsPageViewProps): JSX.Element => { - const styles = useStyles() - const theme = useTheme() - const [t] = useTranslation("appearanceSettings") + const styles = useStyles(); + const theme = useTheme(); + const [t] = useTranslation("appearanceSettings"); const logoForm = useFormik<{ - logo_url: string + logo_url: string; }>({ initialValues: { logo_url: appearance.logo_url, }, onSubmit: (values) => updateAppearance(values, false), - }) - const logoFieldHelpers = getFormHelpers(logoForm) + }); + const logoFieldHelpers = getFormHelpers(logoForm); const serviceBannerForm = useFormik( { @@ -65,11 +65,11 @@ export const AppearanceSettingsPageView = ({ false, ), }, - ) - const serviceBannerFieldHelpers = getFormHelpers(serviceBannerForm) + ); + const serviceBannerFieldHelpers = getFormHelpers(serviceBannerForm); const [backgroundColor, setBackgroundColor] = useState( serviceBannerForm.values.background_color, - ) + ); return ( <>
{t("showPreviewLabel")} @@ -159,18 +159,18 @@ export const AppearanceSettingsPageView = ({ { - const newState = !serviceBannerForm.values.enabled + const newState = !serviceBannerForm.values.enabled; const newBanner = { ...serviceBannerForm.values, enabled: newState, - } + }; updateAppearance( { service_banner: newBanner, }, false, - ) - await serviceBannerForm.setFieldValue("enabled", newState) + ); + await serviceBannerForm.setFieldValue("enabled", newState); }} /> } @@ -193,11 +193,11 @@ export const AppearanceSettingsPageView = ({ { - setBackgroundColor(color.hex) + setBackgroundColor(color.hex); await serviceBannerForm.setFieldValue( "background_color", color.hex, - ) + ); updateAppearance( { service_banner: { @@ -206,7 +206,7 @@ export const AppearanceSettingsPageView = ({ }, }, true, - ) + ); }} triangle="hide" colors={["#004852", "#D65D0F", "#4CD473", "#D94A5D", "#5A00CF"]} @@ -231,8 +231,8 @@ export const AppearanceSettingsPageView = ({ )} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ form: { @@ -246,4 +246,4 @@ const useStyles = makeStyles((theme) => ({ maxWidth: "100%", }, }, -})) +})); diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/ChartSection.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/ChartSection.tsx index 3e97012f6b..45ad3fb5cd 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/ChartSection.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/ChartSection.tsx @@ -1,15 +1,15 @@ -import Paper from "@mui/material/Paper" -import { makeStyles } from "@mui/styles" -import { HTMLProps, ReactNode, FC, PropsWithChildren } from "react" -import { combineClasses } from "utils/combineClasses" +import Paper from "@mui/material/Paper"; +import { makeStyles } from "@mui/styles"; +import { HTMLProps, ReactNode, FC, PropsWithChildren } from "react"; +import { combineClasses } from "utils/combineClasses"; export interface ChartSectionProps { /** * action appears in the top right of the section card */ - action?: ReactNode - contentsProps?: HTMLProps - title?: string | JSX.Element + action?: ReactNode; + contentsProps?: HTMLProps; + title?: string | JSX.Element; } export const ChartSection: FC> = ({ @@ -18,7 +18,7 @@ export const ChartSection: FC> = ({ contentsProps, title, }) => { - const styles = useStyles() + const styles = useStyles(); return ( @@ -36,8 +36,8 @@ export const ChartSection: FC> = ({ {children} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ root: { @@ -58,4 +58,4 @@ const useStyles = makeStyles((theme) => ({ fontSize: 14, fontWeight: 600, }, -})) +})); diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx index db3f44b77e..a0283496e6 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx @@ -1,12 +1,12 @@ -import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "utils/page" -import { GeneralSettingsPageView } from "./GeneralSettingsPageView" +import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { GeneralSettingsPageView } from "./GeneralSettingsPageView"; const GeneralSettingsPage: FC = () => { const { deploymentValues, deploymentDAUs, getDeploymentDAUsError } = - useDeploySettings() + useDeploySettings(); return ( <> @@ -19,7 +19,7 @@ const GeneralSettingsPage: FC = () => { getDeploymentDAUsError={getDeploymentDAUsError} /> - ) -} + ); +}; -export default GeneralSettingsPage +export default GeneralSettingsPage; diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx index 4a11f6224c..aa4e204f8c 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx @@ -1,6 +1,6 @@ -import { Meta, StoryObj } from "@storybook/react" -import { mockApiError, MockDeploymentDAUResponse } from "testHelpers/entities" -import { GeneralSettingsPageView } from "./GeneralSettingsPageView" +import { Meta, StoryObj } from "@storybook/react"; +import { mockApiError, MockDeploymentDAUResponse } from "testHelpers/entities"; +import { GeneralSettingsPageView } from "./GeneralSettingsPageView"; const meta: Meta = { title: "pages/GeneralSettingsPageView", @@ -43,18 +43,18 @@ const meta: Meta = { ], deploymentDAUs: MockDeploymentDAUResponse, }, -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; -export const Page: Story = {} +export const Page: Story = {}; export const NoDAUs: Story = { args: { deploymentDAUs: undefined, }, -} +}; export const DAUError: Story = { args: { @@ -63,4 +63,4 @@ export const DAUError: Story = { message: "Error fetching DAUs.", }), }, -} +}; diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx index 5c450c48ca..60897525b8 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx @@ -1,20 +1,20 @@ -import Box from "@mui/material/Box" -import { DeploymentOption } from "api/types" -import { DAUsResponse } from "api/typesGenerated" -import { ErrorAlert } from "components/Alert/ErrorAlert" -import { DAUChart, DAUTitle } from "components/DAUChart/DAUChart" -import { Header } from "components/DeploySettingsLayout/Header" -import OptionsTable from "components/DeploySettingsLayout/OptionsTable" -import { Stack } from "components/Stack/Stack" -import { ChartSection } from "./ChartSection" -import { useDeploymentOptions } from "utils/deployOptions" -import { docs } from "utils/docs" +import Box from "@mui/material/Box"; +import { DeploymentOption } from "api/types"; +import { DAUsResponse } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { DAUChart, DAUTitle } from "components/DAUChart/DAUChart"; +import { Header } from "components/DeploySettingsLayout/Header"; +import OptionsTable from "components/DeploySettingsLayout/OptionsTable"; +import { Stack } from "components/Stack/Stack"; +import { ChartSection } from "./ChartSection"; +import { useDeploymentOptions } from "utils/deployOptions"; +import { docs } from "utils/docs"; export type GeneralSettingsPageViewProps = { - deploymentOptions: DeploymentOption[] - deploymentDAUs?: DAUsResponse - getDeploymentDAUsError: unknown -} + deploymentOptions: DeploymentOption[]; + deploymentDAUs?: DAUsResponse; + getDeploymentDAUsError: unknown; +}; export const GeneralSettingsPageView = ({ deploymentOptions, deploymentDAUs, @@ -48,5 +48,5 @@ export const GeneralSettingsPageView = ({ /> - ) -} + ); +}; diff --git a/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPage.tsx b/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPage.tsx index 4022602281..cc18a803ea 100644 --- a/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPage.tsx @@ -1,11 +1,11 @@ -import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "utils/page" -import { GitAuthSettingsPageView } from "./GitAuthSettingsPageView" +import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { GitAuthSettingsPageView } from "./GitAuthSettingsPageView"; const GitAuthSettingsPage: FC = () => { - const { deploymentValues: deploymentValues } = useDeploySettings() + const { deploymentValues: deploymentValues } = useDeploySettings(); return ( <> @@ -15,7 +15,7 @@ const GitAuthSettingsPage: FC = () => { - ) -} + ); +}; -export default GitAuthSettingsPage +export default GitAuthSettingsPage; diff --git a/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.stories.tsx index 2a2e6d47ed..6c4eafb91e 100644 --- a/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.stories.tsx @@ -1,8 +1,8 @@ -import { ComponentMeta, Story } from "@storybook/react" +import { ComponentMeta, Story } from "@storybook/react"; import { GitAuthSettingsPageView, GitAuthSettingsPageViewProps, -} from "./GitAuthSettingsPageView" +} from "./GitAuthSettingsPageView"; export default { title: "pages/GitAuthSettingsPageView", @@ -19,9 +19,9 @@ export default { ], }, }, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) -export const Page = Template.bind({}) +); +export const Page = Template.bind({}); diff --git a/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.tsx index 19b46ce707..5d1deb35ee 100644 --- a/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.tsx @@ -1,24 +1,24 @@ -import { makeStyles } from "@mui/styles" -import Table from "@mui/material/Table" -import TableBody from "@mui/material/TableBody" -import TableCell from "@mui/material/TableCell" -import TableContainer from "@mui/material/TableContainer" -import TableHead from "@mui/material/TableHead" -import TableRow from "@mui/material/TableRow" -import { DeploymentValues, GitAuthConfig } from "api/typesGenerated" -import { Alert } from "components/Alert/Alert" -import { EnterpriseBadge } from "components/DeploySettingsLayout/Badges" -import { Header } from "components/DeploySettingsLayout/Header" -import { docs } from "utils/docs" +import { makeStyles } from "@mui/styles"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import { DeploymentValues, GitAuthConfig } from "api/typesGenerated"; +import { Alert } from "components/Alert/Alert"; +import { EnterpriseBadge } from "components/DeploySettingsLayout/Badges"; +import { Header } from "components/DeploySettingsLayout/Header"; +import { docs } from "utils/docs"; export type GitAuthSettingsPageViewProps = { - config: DeploymentValues -} + config: DeploymentValues; +}; export const GitAuthSettingsPageView = ({ config, }: GitAuthSettingsPageViewProps): JSX.Element => { - const styles = useStyles() + const styles = useStyles(); return ( <> @@ -66,21 +66,21 @@ export const GitAuthSettingsPageView = ({ )) || config.git_auth.map((git: GitAuthConfig) => { - const name = git.id || git.type + const name = git.id || git.type; return ( {name} {git.client_id} {git.regex || "Not Set"} - ) + ); })} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ table: { @@ -100,4 +100,4 @@ const useStyles = makeStyles((theme) => ({ empty: { textAlign: "center", }, -})) +})); diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx index 3d60005a5e..9e6799bc49 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage.tsx @@ -1,14 +1,14 @@ -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" +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 navigate = useNavigate(); const { mutate: saveLicenseKeyApi, @@ -16,23 +16,23 @@ const AddNewLicensePage: FC = () => { error: savingLicenseError, } = useMutation(createLicense, { onSuccess: () => { - displaySuccess("You have successfully added a license") - navigate("/deployment/licenses?success=true") + displaySuccess("You have successfully added a license"); + navigate("/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("/deployment/licenses?success=true") + displaySuccess("You have successfully added a license"); + navigate("/deployment/licenses?success=true"); }, onError: () => displayError("Failed to save license key"), }, - ) + ); } return ( @@ -47,7 +47,7 @@ const AddNewLicensePage: FC = () => { onSaveLicenseKey={saveLicenseKey} /> - ) -} + ); +}; -export default AddNewLicensePage +export default AddNewLicensePage; diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.stories.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.stories.tsx index 1889668fdf..296a7318a0 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.stories.tsx @@ -1,13 +1,13 @@ -import { AddNewLicensePageView } from "./AddNewLicensePageView" +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 index a7a2fbc346..e2156b13c7 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePageView.tsx @@ -1,49 +1,49 @@ -import Button from "@mui/material/Button" -import TextField from "@mui/material/TextField" -import { makeStyles } from "@mui/styles" -import { Fieldset } from "components/DeploySettingsLayout/Fieldset" -import { Header } from "components/DeploySettingsLayout/Header" -import { FileUpload } from "components/FileUpload/FileUpload" -import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft" -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" -import { ErrorAlert } from "components/Alert/ErrorAlert" +import Button from "@mui/material/Button"; +import TextField from "@mui/material/TextField"; +import { makeStyles } from "@mui/styles"; +import { Fieldset } from "components/DeploySettingsLayout/Fieldset"; +import { Header } from "components/DeploySettingsLayout/Header"; +import { FileUpload } from "components/FileUpload/FileUpload"; +import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"; +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"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; type AddNewLicenseProps = { - onSaveLicenseKey: (license: string) => void - isSavingLicense: boolean - savingLicenseError?: unknown -} + onSaveLicenseKey: (license: string) => void; + isSavingLicense: boolean; + savingLicenseError?: unknown; +}; export const AddNewLicensePageView: FC = ({ onSaveLicenseKey, isSavingLicense, savingLicenseError, }) => { - const styles = useStyles() + const styles = useStyles(); function handleFileUploaded(files: File[]) { - const fileReader = new FileReader() + const fileReader = new FileReader(); fileReader.onload = () => { - const licenseKey = fileReader.result as string + const licenseKey = fileReader.result as string; - onSaveLicenseKey(licenseKey) + onSaveLicenseKey(licenseKey); fileReader.onerror = () => { - displayError("Failed to read file") - } - } + displayError("Failed to read file"); + }; + }; - fileReader.readAsText(files[0]) + fileReader.readAsText(files[0]); } - const isUploading = false + const isUploading = false; function onUpload(file: File) { - handleFileUploaded([file]) + handleFileUploaded([file]); } return ( @@ -82,14 +82,14 @@ export const AddNewLicensePageView: FC = ({
{ - e.preventDefault() + e.preventDefault(); - const form = e.target - const formData = new FormData(form as HTMLFormElement) + const form = e.target; + const formData = new FormData(form as HTMLFormElement); - const licenseKey = formData.get("licenseKey") + const licenseKey = formData.get("licenseKey"); - onSaveLicenseKey(licenseKey?.toString() || "") + 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 index 321de70f52..40bdb47139 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/DividerWithText.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/DividerWithText.tsx @@ -1,16 +1,16 @@ -import { makeStyles } from "@mui/styles" -import { FC, PropsWithChildren } from "react" +import { makeStyles } from "@mui/styles"; +import { FC, PropsWithChildren } from "react"; export const DividerWithText: FC = ({ children }) => { - const classes = useStyles() + const classes = useStyles(); return (
{children}
- ) -} + ); +}; const useStyles = makeStyles((theme) => ({ container: { @@ -29,4 +29,4 @@ const useStyles = makeStyles((theme) => ({ fontSize: theme.typography.h5.fontSize, color: theme.palette.text.secondary, }, -})) +})); diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicenseCard.test.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicenseCard.test.tsx index c18880ae13..63b840bc68 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicenseCard.test.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicenseCard.test.tsx @@ -1,7 +1,7 @@ -import { screen } from "@testing-library/react" -import { render } from "../../../testHelpers/renderHelpers" -import { LicenseCard } from "./LicenseCard" -import { MockLicenseResponse } from "testHelpers/entities" +import { screen } from "@testing-library/react"; +import { render } from "../../../testHelpers/renderHelpers"; +import { LicenseCard } from "./LicenseCard"; +import { MockLicenseResponse } from "testHelpers/entities"; describe("LicenseCard", () => { it("renders (smoke test)", async () => { @@ -14,13 +14,13 @@ describe("LicenseCard", () => { onRemove={() => null} isRemoving={false} />, - ) + ); // Then - await screen.findByText("#1") - await screen.findByText("1 / 10") - await screen.findByText("Enterprise") - }) + await screen.findByText("#1"); + await screen.findByText("1 / 10"); + await screen.findByText("Enterprise"); + }); it("renders userLimit as unlimited if there is not user limit", async () => { // When @@ -32,16 +32,16 @@ describe("LicenseCard", () => { onRemove={() => null} isRemoving={false} />, - ) + ); // Then - await screen.findByText("#1") - await screen.findByText("1 / Unlimited") - await screen.findByText("Enterprise") - }) + await screen.findByText("#1"); + await screen.findByText("1 / Unlimited"); + await screen.findByText("Enterprise"); + }); it("renders license's user_limit when it is available instead of using the default", async () => { - const licenseUserLimit = 3 + const licenseUserLimit = 3; const license = { ...MockLicenseResponse[0], claims: { @@ -51,7 +51,7 @@ describe("LicenseCard", () => { user_limit: licenseUserLimit, }, }, - } + }; // When render( @@ -62,9 +62,9 @@ describe("LicenseCard", () => { onRemove={() => null} isRemoving={false} />, - ) + ); // Then - await screen.findByText("1 / 3") - }) -}) + await screen.findByText("1 / 3"); + }); +}); diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicenseCard.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicenseCard.tsx index 4e6596b91a..7fb541189d 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicenseCard.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicenseCard.tsx @@ -1,21 +1,21 @@ -import Button from "@mui/material/Button" -import Paper from "@mui/material/Paper" -import { makeStyles } from "@mui/styles" -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" -import { Stack } from "components/Stack/Stack" -import dayjs from "dayjs" -import { useState } from "react" -import { Pill } from "components/Pill/Pill" -import { compareAsc } from "date-fns" -import { GetLicensesResponse } from "api/api" +import Button from "@mui/material/Button"; +import Paper from "@mui/material/Paper"; +import { makeStyles } from "@mui/styles"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { Stack } from "components/Stack/Stack"; +import dayjs from "dayjs"; +import { useState } from "react"; +import { Pill } from "components/Pill/Pill"; +import { compareAsc } from "date-fns"; +import { GetLicensesResponse } from "api/api"; type LicenseCardProps = { - license: GetLicensesResponse - userLimitActual?: number - userLimitLimit?: number - onRemove: (licenseId: number) => void - isRemoving: boolean -} + license: GetLicensesResponse; + userLimitActual?: number; + userLimitLimit?: number; + onRemove: (licenseId: number) => void; + isRemoving: boolean; +}; export const LicenseCard = ({ license, @@ -24,14 +24,14 @@ export const LicenseCard = ({ onRemove, isRemoving, }: LicenseCardProps) => { - const styles = useStyles() + const styles = useStyles(); const [licenseIDMarkedForRemoval, setLicenseIDMarkedForRemoval] = useState< number | undefined - >(undefined) + >(undefined); const currentUserLimit = - license.claims.features["user_limit"] || userLimitLimit + license.claims.features["user_limit"] || userLimitLimit; return ( @@ -41,10 +41,10 @@ export const LicenseCard = ({ open={licenseIDMarkedForRemoval !== undefined} onConfirm={() => { if (!licenseIDMarkedForRemoval) { - return + return; } - onRemove(licenseIDMarkedForRemoval) - setLicenseIDMarkedForRemoval(undefined) + onRemove(licenseIDMarkedForRemoval); + setLicenseIDMarkedForRemoval(undefined); }} onClose={() => setLicenseIDMarkedForRemoval(undefined)} title="Confirm License Removal" @@ -115,8 +115,8 @@ export const LicenseCard = ({ - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ userLimit: { @@ -157,4 +157,4 @@ const useStyles = makeStyles((theme) => ({ backgroundColor: "transparent", }, }, -})) +})); diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx index 01e4d8d582..9bfddcc538 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx @@ -1,55 +1,55 @@ -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" -import { getErrorMessage } from "api/errors" +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"; +import { getErrorMessage } from "api/errors"; const LicensesSettingsPage: FC = () => { - const queryClient = useQueryClient() - const [entitlementsState, sendEvent] = useMachine(entitlementsMachine) - const { entitlements, getEntitlementsError } = entitlementsState.context - const [searchParams, setSearchParams] = useSearchParams() - const success = searchParams.get("success") - const [confettiOn, toggleConfettiOn] = useToggle(false) + const queryClient = useQueryClient(); + const [entitlementsState, sendEvent] = useMachine(entitlementsMachine); + const { entitlements, getEntitlementsError } = entitlementsState.context; + const [searchParams, setSearchParams] = useSearchParams(); + const success = searchParams.get("success"); + const [confettiOn, toggleConfettiOn] = useToggle(false); if (getEntitlementsError) { displayError( getErrorMessage(getEntitlementsError, "Failed to fetch entitlements"), - ) + ); } const { mutate: removeLicenseApi, isLoading: isRemovingLicense } = useMutation(removeLicense, { onSuccess: () => { - displaySuccess("Successfully removed license") - void queryClient.invalidateQueries(["licenses"]) + displaySuccess("Successfully removed license"); + void queryClient.invalidateQueries(["licenses"]); }, onError: () => { - displayError("Failed to remove license") + displayError("Failed to remove license"); }, - }) + }); const { data: licenses, isLoading } = useQuery({ queryKey: ["licenses"], queryFn: () => getLicenses(), - }) + }); useEffect(() => { if (success) { - toggleConfettiOn() + toggleConfettiOn(); const timeout = setTimeout(() => { - toggleConfettiOn(false) - setSearchParams() - }, 2000) - return () => clearTimeout(timeout) + toggleConfettiOn(false); + setSearchParams(); + }, 2000); + return () => clearTimeout(timeout); } - }, [setSearchParams, success, toggleConfettiOn]) + }, [setSearchParams, success, toggleConfettiOn]); return ( <> @@ -65,12 +65,12 @@ const LicensesSettingsPage: FC = () => { isRemovingLicense={isRemovingLicense} removeLicense={(licenseId: number) => removeLicenseApi(licenseId)} refreshEntitlements={() => { - const x = sendEvent("REFRESH") - return !x.context.getEntitlementsError + const x = sendEvent("REFRESH"); + return !x.context.getEntitlementsError; }} /> - ) -} + ); +}; -export default LicensesSettingsPage +export default LicensesSettingsPage; diff --git a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.stories.tsx index daa0a70362..abe9ed53bd 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.stories.tsx @@ -1,10 +1,10 @@ -import LicensesSettingsPageView from "./LicensesSettingsPageView" -import { MockLicenseResponse } from "testHelpers/entities" +import LicensesSettingsPageView from "./LicensesSettingsPageView"; +import { MockLicenseResponse } from "testHelpers/entities"; export default { title: "pages/LicensesSettingsPageView", component: LicensesSettingsPageView, -} +}; const defaultArgs = { showConfetti: false, @@ -12,15 +12,15 @@ const defaultArgs = { userLimitActual: 1, userLimitLimit: 10, licenses: MockLicenseResponse, -} +}; 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 index fe57ae8d3c..1c9c6fe295 100644 --- a/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx @@ -1,30 +1,30 @@ -import Button from "@mui/material/Button" -import { makeStyles, useTheme } from "@mui/styles" -import Skeleton from "@mui/material/Skeleton" -import AddIcon from "@mui/icons-material/AddOutlined" -import RefreshIcon from "@mui/icons-material/Refresh" -import { GetLicensesResponse } from "api/api" -import { Header } from "components/DeploySettingsLayout/Header" -import { LicenseCard } from "./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" -import MuiLink from "@mui/material/Link" -import { displaySuccess } from "components/GlobalSnackbar/utils" -import Tooltip from "@mui/material/Tooltip" +import Button from "@mui/material/Button"; +import { makeStyles, useTheme } from "@mui/styles"; +import Skeleton from "@mui/material/Skeleton"; +import AddIcon from "@mui/icons-material/AddOutlined"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { GetLicensesResponse } from "api/api"; +import { Header } from "components/DeploySettingsLayout/Header"; +import { LicenseCard } from "./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"; +import MuiLink from "@mui/material/Link"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; +import Tooltip from "@mui/material/Tooltip"; type Props = { - showConfetti: boolean - isLoading: boolean - userLimitActual?: number - userLimitLimit?: number - licenses?: GetLicensesResponse[] - isRemovingLicense: boolean - removeLicense: (licenseId: number) => void - refreshEntitlements?: () => boolean -} + showConfetti: boolean; + isLoading: boolean; + userLimitActual?: number; + userLimitLimit?: number; + licenses?: GetLicensesResponse[]; + isRemovingLicense: boolean; + removeLicense: (licenseId: number) => void; + refreshEntitlements?: () => boolean; +}; const LicensesSettingsPageView: FC = ({ showConfetti, @@ -36,10 +36,10 @@ const LicensesSettingsPageView: FC = ({ removeLicense, refreshEntitlements, }) => { - const styles = useStyles() - const { width, height } = useWindowSize() + const styles = useStyles(); + const { width, height } = useWindowSize(); - const theme = useTheme() + const theme = useTheme(); return ( <> @@ -73,7 +73,7 @@ const LicensesSettingsPageView: FC = ({ onClick={() => { if (refreshEntitlements) { if (refreshEntitlements()) { - displaySuccess("Successfully refreshed licenses") + displaySuccess("Successfully refreshed licenses"); } } }} @@ -129,8 +129,8 @@ const LicensesSettingsPageView: FC = ({
)} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ title: { @@ -153,6 +153,6 @@ const useStyles = makeStyles((theme) => ({ maxWidth: theme.spacing(58), marginTop: theme.spacing(1), }, -})) +})); -export default LicensesSettingsPageView +export default LicensesSettingsPageView; diff --git a/site/src/pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPage.tsx b/site/src/pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPage.tsx index 061d8aa551..c1ae478352 100644 --- a/site/src/pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPage.tsx @@ -1,11 +1,11 @@ -import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "utils/page" -import { NetworkSettingsPageView } from "./NetworkSettingsPageView" +import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { NetworkSettingsPageView } from "./NetworkSettingsPageView"; const NetworkSettingsPage: FC = () => { - const { deploymentValues: deploymentValues } = useDeploySettings() + const { deploymentValues: deploymentValues } = useDeploySettings(); return ( <> @@ -15,7 +15,7 @@ const NetworkSettingsPage: FC = () => { - ) -} + ); +}; -export default NetworkSettingsPage +export default NetworkSettingsPage; diff --git a/site/src/pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPageView.stories.tsx index 7d5b889f0d..4749fcc04e 100644 --- a/site/src/pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPageView.stories.tsx @@ -1,8 +1,8 @@ -import { ComponentMeta, Story } from "@storybook/react" +import { ComponentMeta, Story } from "@storybook/react"; import { NetworkSettingsPageView, NetworkSettingsPageViewProps, -} from "./NetworkSettingsPageView" +} from "./NetworkSettingsPageView"; export default { title: "pages/NetworkSettingsPageView", @@ -52,9 +52,9 @@ export default { }, ], }, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) -export const Page = Template.bind({}) +); +export const Page = Template.bind({}); diff --git a/site/src/pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPageView.tsx index dafbbd6e07..61daa2e6b6 100644 --- a/site/src/pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPageView.tsx @@ -1,21 +1,21 @@ -import { DeploymentOption } from "api/types" +import { DeploymentOption } from "api/types"; import { Badges, EnabledBadge, DisabledBadge, -} from "components/DeploySettingsLayout/Badges" -import { Header } from "components/DeploySettingsLayout/Header" -import OptionsTable from "components/DeploySettingsLayout/OptionsTable" -import { Stack } from "components/Stack/Stack" +} from "components/DeploySettingsLayout/Badges"; +import { Header } from "components/DeploySettingsLayout/Header"; +import OptionsTable from "components/DeploySettingsLayout/OptionsTable"; +import { Stack } from "components/Stack/Stack"; import { deploymentGroupHasParent, useDeploymentOptions, -} from "utils/deployOptions" -import { docs } from "utils/docs" +} from "utils/deployOptions"; +import { docs } from "utils/docs"; export type NetworkSettingsPageViewProps = { - options: DeploymentOption[] -} + options: DeploymentOption[]; +}; export const NetworkSettingsPageView = ({ options: options, @@ -52,4 +52,4 @@ export const NetworkSettingsPageView = ({
-) +); diff --git a/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx b/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx index 1041c64597..ff78264dcb 100644 --- a/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx @@ -1,13 +1,13 @@ -import { useDashboard } from "components/Dashboard/DashboardProvider" -import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "utils/page" -import { SecuritySettingsPageView } from "./SecuritySettingsPageView" +import { useDashboard } from "components/Dashboard/DashboardProvider"; +import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { SecuritySettingsPageView } from "./SecuritySettingsPageView"; const SecuritySettingsPage: FC = () => { - const { deploymentValues: deploymentValues } = useDeploySettings() - const { entitlements } = useDashboard() + const { deploymentValues: deploymentValues } = useDeploySettings(); + const { entitlements } = useDashboard(); return ( <> @@ -23,7 +23,7 @@ const SecuritySettingsPage: FC = () => { } /> - ) -} + ); +}; -export default SecuritySettingsPage +export default SecuritySettingsPage; diff --git a/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPageView.stories.tsx index 8edbaa97d2..4ccbbdcba6 100644 --- a/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPageView.stories.tsx @@ -1,9 +1,9 @@ -import { ComponentMeta, Story } from "@storybook/react" -import { DeploymentOption } from "api/types" +import { ComponentMeta, Story } from "@storybook/react"; +import { DeploymentOption } from "api/types"; import { SecuritySettingsPageView, SecuritySettingsPageViewProps, -} from "./SecuritySettingsPageView" +} from "./SecuritySettingsPageView"; export default { title: "pages/SecuritySettingsPageView", @@ -37,14 +37,14 @@ export default { featureAuditLogEnabled: true, featureBrowserOnlyEnabled: true, }, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) -export const Page = Template.bind({}) +); +export const Page = Template.bind({}); -export const NoTLS = Template.bind({}) +export const NoTLS = Template.bind({}); NoTLS.args = { options: [ { @@ -60,4 +60,4 @@ NoTLS.args = { value: "1234", } as DeploymentOption, ], -} +}; diff --git a/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPageView.tsx b/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPageView.tsx index aad2743fb2..a4ae87a8a2 100644 --- a/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPageView.tsx @@ -1,24 +1,24 @@ -import { DeploymentOption } from "api/types" +import { DeploymentOption } from "api/types"; import { Badges, DisabledBadge, EnabledBadge, EnterpriseBadge, -} from "components/DeploySettingsLayout/Badges" -import { Header } from "components/DeploySettingsLayout/Header" -import OptionsTable from "components/DeploySettingsLayout/OptionsTable" -import { Stack } from "components/Stack/Stack" +} from "components/DeploySettingsLayout/Badges"; +import { Header } from "components/DeploySettingsLayout/Header"; +import OptionsTable from "components/DeploySettingsLayout/OptionsTable"; +import { Stack } from "components/Stack/Stack"; import { deploymentGroupHasParent, useDeploymentOptions, -} from "utils/deployOptions" -import { docs } from "utils/docs" +} from "utils/deployOptions"; +import { docs } from "utils/docs"; export type SecuritySettingsPageViewProps = { - options: DeploymentOption[] - featureAuditLogEnabled: boolean - featureBrowserOnlyEnabled: boolean -} + options: DeploymentOption[]; + featureAuditLogEnabled: boolean; + featureBrowserOnlyEnabled: boolean; +}; export const SecuritySettingsPageView = ({ options: options, featureAuditLogEnabled, @@ -26,7 +26,7 @@ export const SecuritySettingsPageView = ({ }: SecuritySettingsPageViewProps): JSX.Element => { const tlsOptions = options.filter((o) => deploymentGroupHasParent(o.group, "TLS"), - ) + ); return ( <> @@ -88,5 +88,5 @@ export const SecuritySettingsPageView = ({ )} - ) -} + ); +}; diff --git a/site/src/pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPage.tsx b/site/src/pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPage.tsx index 06f7f9917b..d8d83edc40 100644 --- a/site/src/pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPage.tsx @@ -1,11 +1,11 @@ -import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "utils/page" -import { UserAuthSettingsPageView } from "./UserAuthSettingsPageView" +import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { UserAuthSettingsPageView } from "./UserAuthSettingsPageView"; const UserAuthSettingsPage: FC = () => { - const { deploymentValues: deploymentValues } = useDeploySettings() + const { deploymentValues: deploymentValues } = useDeploySettings(); return ( <> @@ -15,7 +15,7 @@ const UserAuthSettingsPage: FC = () => { - ) -} + ); +}; -export default UserAuthSettingsPage +export default UserAuthSettingsPage; diff --git a/site/src/pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPageView.stories.tsx index 9f9c1f7af7..d0047d6dd3 100644 --- a/site/src/pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPageView.stories.tsx @@ -1,8 +1,8 @@ -import { ComponentMeta, Story } from "@storybook/react" +import { ComponentMeta, Story } from "@storybook/react"; import { UserAuthSettingsPageView, UserAuthSettingsPageViewProps, -} from "./UserAuthSettingsPageView" +} from "./UserAuthSettingsPageView"; export default { title: "pages/UserAuthSettingsPageView", @@ -95,9 +95,9 @@ export default { }, ], }, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) -export const Page = Template.bind({}) +); +export const Page = Template.bind({}); diff --git a/site/src/pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPageView.tsx index 1bf89edea3..635e2f3451 100644 --- a/site/src/pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPageView.tsx @@ -1,31 +1,31 @@ -import { DeploymentOption } from "api/types" +import { DeploymentOption } from "api/types"; import { Badges, DisabledBadge, EnabledBadge, -} from "components/DeploySettingsLayout/Badges" -import { Header } from "components/DeploySettingsLayout/Header" -import OptionsTable from "components/DeploySettingsLayout/OptionsTable" -import { Stack } from "components/Stack/Stack" +} from "components/DeploySettingsLayout/Badges"; +import { Header } from "components/DeploySettingsLayout/Header"; +import OptionsTable from "components/DeploySettingsLayout/OptionsTable"; +import { Stack } from "components/Stack/Stack"; import { deploymentGroupHasParent, useDeploymentOptions, -} from "utils/deployOptions" -import { docs } from "utils/docs" +} from "utils/deployOptions"; +import { docs } from "utils/docs"; export type UserAuthSettingsPageViewProps = { - options: DeploymentOption[] -} + options: DeploymentOption[]; +}; export const UserAuthSettingsPageView = ({ options, }: UserAuthSettingsPageViewProps): JSX.Element => { const oidcEnabled = Boolean( useDeploymentOptions(options, "OIDC Client ID")[0].value, - ) + ); const githubEnabled = Boolean( useDeploymentOptions(options, "OAuth2 GitHub Client ID")[0].value, - ) + ); return ( <> @@ -73,5 +73,5 @@ export const UserAuthSettingsPageView = ({ - ) -} + ); +}; diff --git a/site/src/pages/GitAuthPage/GitAuthPage.tsx b/site/src/pages/GitAuthPage/GitAuthPage.tsx index efe62ad6a9..0c64cad44d 100644 --- a/site/src/pages/GitAuthPage/GitAuthPage.tsx +++ b/site/src/pages/GitAuthPage/GitAuthPage.tsx @@ -1,29 +1,29 @@ -import { useQuery, useQueryClient } from "@tanstack/react-query" +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { exchangeGitAuthDevice, getGitAuthDevice, getGitAuthProvider, -} from "api/api" -import { usePermissions } from "hooks" -import { FC, useEffect } from "react" -import { useParams } from "react-router-dom" -import GitAuthPageView from "./GitAuthPageView" -import { ApiErrorResponse } from "api/errors" -import { isAxiosError } from "axios" -import { REFRESH_GITAUTH_BROADCAST_CHANNEL } from "utils/gitAuth" +} from "api/api"; +import { usePermissions } from "hooks"; +import { FC, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import GitAuthPageView from "./GitAuthPageView"; +import { ApiErrorResponse } from "api/errors"; +import { isAxiosError } from "axios"; +import { REFRESH_GITAUTH_BROADCAST_CHANNEL } from "utils/gitAuth"; const GitAuthPage: FC = () => { - const { provider } = useParams() + const { provider } = useParams(); if (!provider) { - throw new Error("provider must exist") + throw new Error("provider must exist"); } - const permissions = usePermissions() - const queryClient = useQueryClient() + const permissions = usePermissions(); + const queryClient = useQueryClient(); const getGitAuthProviderQuery = useQuery({ queryKey: ["gitauth", provider], queryFn: () => getGitAuthProvider(provider), refetchOnWindowFocus: true, - }) + }); const getGitAuthDeviceQuery = useQuery({ enabled: @@ -32,7 +32,7 @@ const GitAuthPage: FC = () => { queryFn: () => getGitAuthDevice(provider), queryKey: ["gitauth", provider, "device"], refetchOnMount: false, - }) + }); const exchangeGitAuthDeviceQuery = useQuery({ queryFn: () => exchangeGitAuthDevice(provider, { @@ -43,43 +43,43 @@ const GitAuthPage: FC = () => { onSuccess: () => { // Force a refresh of the Git auth status. queryClient.invalidateQueries(["gitauth", provider]).catch((ex) => { - console.error("invalidate queries", ex) - }) + console.error("invalidate queries", ex); + }); }, retry: true, retryDelay: (getGitAuthDeviceQuery.data?.interval || 5) * 1000, refetchOnWindowFocus: (query) => query.state.status === "success" ? false : "always", - }) + }); useEffect(() => { if (!getGitAuthProviderQuery.data?.authenticated) { - return + return; } // This is used to notify the parent window that the Git auth token has been refreshed. // It's critical in the create workspace flow! - const bc = new BroadcastChannel(REFRESH_GITAUTH_BROADCAST_CHANNEL) + const bc = new BroadcastChannel(REFRESH_GITAUTH_BROADCAST_CHANNEL); // The message doesn't matter, any message refreshes the page! - bc.postMessage("noop") - }, [getGitAuthProviderQuery.data?.authenticated]) + bc.postMessage("noop"); + }, [getGitAuthProviderQuery.data?.authenticated]); if (getGitAuthProviderQuery.isLoading || !getGitAuthProviderQuery.data) { - return null + return null; } - let deviceExchangeError: ApiErrorResponse | undefined + let deviceExchangeError: ApiErrorResponse | undefined; if (isAxiosError(exchangeGitAuthDeviceQuery.failureReason)) { deviceExchangeError = - exchangeGitAuthDeviceQuery.failureReason.response?.data + exchangeGitAuthDeviceQuery.failureReason.response?.data; } if ( !getGitAuthProviderQuery.data.authenticated && !getGitAuthProviderQuery.data.device ) { - window.location.href = `/gitauth/${provider}/callback` + window.location.href = `/gitauth/${provider}/callback`; - return null + return null; } return ( @@ -89,13 +89,13 @@ const GitAuthPage: FC = () => { queryClient.setQueryData(["gitauth", provider], { ...getGitAuthProviderQuery.data, authenticated: false, - }) + }); }} viewGitAuthConfig={permissions.viewGitAuthConfig} deviceExchangeError={deviceExchangeError} gitAuthDevice={getGitAuthDeviceQuery.data} /> - ) -} + ); +}; -export default GitAuthPage +export default GitAuthPage; diff --git a/site/src/pages/GitAuthPage/GitAuthPageView.stories.tsx b/site/src/pages/GitAuthPage/GitAuthPageView.stories.tsx index 4f0a9dc45b..99593c98d2 100644 --- a/site/src/pages/GitAuthPage/GitAuthPageView.stories.tsx +++ b/site/src/pages/GitAuthPage/GitAuthPageView.stories.tsx @@ -1,16 +1,16 @@ -import { Meta, StoryFn } from "@storybook/react" -import GitAuthPageView, { GitAuthPageViewProps } from "./GitAuthPageView" +import { Meta, StoryFn } from "@storybook/react"; +import GitAuthPageView, { GitAuthPageViewProps } from "./GitAuthPageView"; export default { title: "pages/GitAuthPageView", component: GitAuthPageView, -} as Meta +} as Meta; const Template: StoryFn = (args) => ( -) +); -export const WebAuthenticated = Template.bind({}) +export const WebAuthenticated = Template.bind({}); WebAuthenticated.args = { gitAuth: { type: "BitBucket", @@ -26,9 +26,9 @@ WebAuthenticated.args = { profile_url: "", }, }, -} +}; -export const DeviceUnauthenticated = Template.bind({}) +export const DeviceUnauthenticated = Template.bind({}); DeviceUnauthenticated.args = { gitAuth: { type: "GitHub", @@ -45,9 +45,9 @@ DeviceUnauthenticated.args = { user_code: "ABCD-EFGH", verification_uri: "", }, -} +}; -export const DeviceUnauthenticatedError = Template.bind({}) +export const DeviceUnauthenticatedError = Template.bind({}); DeviceUnauthenticatedError.args = { gitAuth: { type: "GitHub", @@ -68,9 +68,9 @@ DeviceUnauthenticatedError.args = { message: "Error exchanging device code.", detail: "expired_token", }, -} +}; -export const DeviceAuthenticatedNotInstalled = Template.bind({}) +export const DeviceAuthenticatedNotInstalled = Template.bind({}); DeviceAuthenticatedNotInstalled.args = { viewGitAuthConfig: true, gitAuth: { @@ -87,9 +87,9 @@ DeviceAuthenticatedNotInstalled.args = { profile_url: "", }, }, -} +}; -export const DeviceAuthenticatedInstalled = Template.bind({}) +export const DeviceAuthenticatedInstalled = Template.bind({}); DeviceAuthenticatedInstalled.args = { gitAuth: { type: "GitHub", @@ -116,4 +116,4 @@ DeviceAuthenticatedInstalled.args = { profile_url: "", }, }, -} +}; diff --git a/site/src/pages/GitAuthPage/GitAuthPageView.tsx b/site/src/pages/GitAuthPage/GitAuthPageView.tsx index ce46539ac3..5d15a677ea 100644 --- a/site/src/pages/GitAuthPage/GitAuthPageView.tsx +++ b/site/src/pages/GitAuthPage/GitAuthPageView.tsx @@ -1,27 +1,27 @@ -import OpenInNewIcon from "@mui/icons-material/OpenInNew" -import RefreshIcon from "@mui/icons-material/Refresh" -import CircularProgress from "@mui/material/CircularProgress" -import Link from "@mui/material/Link" -import Tooltip from "@mui/material/Tooltip" -import { makeStyles } from "@mui/styles" -import { ApiErrorResponse } from "api/errors" -import { GitAuth, GitAuthDevice } from "api/typesGenerated" -import { Alert } from "components/Alert/Alert" -import { Avatar } from "components/Avatar/Avatar" -import { CopyButton } from "components/CopyButton/CopyButton" -import { SignInLayout } from "components/SignInLayout/SignInLayout" -import { Welcome } from "components/Welcome/Welcome" -import { FC, useEffect } from "react" -import { REFRESH_GITAUTH_BROADCAST_CHANNEL } from "utils/gitAuth" +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import CircularProgress from "@mui/material/CircularProgress"; +import Link from "@mui/material/Link"; +import Tooltip from "@mui/material/Tooltip"; +import { makeStyles } from "@mui/styles"; +import { ApiErrorResponse } from "api/errors"; +import { GitAuth, GitAuthDevice } from "api/typesGenerated"; +import { Alert } from "components/Alert/Alert"; +import { Avatar } from "components/Avatar/Avatar"; +import { CopyButton } from "components/CopyButton/CopyButton"; +import { SignInLayout } from "components/SignInLayout/SignInLayout"; +import { Welcome } from "components/Welcome/Welcome"; +import { FC, useEffect } from "react"; +import { REFRESH_GITAUTH_BROADCAST_CHANNEL } from "utils/gitAuth"; export interface GitAuthPageViewProps { - gitAuth: GitAuth - viewGitAuthConfig: boolean + gitAuth: GitAuth; + viewGitAuthConfig: boolean; - gitAuthDevice?: GitAuthDevice - deviceExchangeError?: ApiErrorResponse + gitAuthDevice?: GitAuthDevice; + deviceExchangeError?: ApiErrorResponse; - onReauthenticate: () => void + onReauthenticate: () => void; } const GitAuthPageView: FC = ({ @@ -31,19 +31,19 @@ const GitAuthPageView: FC = ({ onReauthenticate, viewGitAuthConfig, }) => { - const styles = useStyles() + const styles = useStyles(); useEffect(() => { if (!gitAuth.authenticated) { - return + return; } // This is used to notify the parent window that the Git auth token has been refreshed. // It's critical in the create workspace flow! // eslint-disable-next-line compat/compat -- It actually is supported... not sure why it's complaining. - const bc = new BroadcastChannel(REFRESH_GITAUTH_BROADCAST_CHANNEL) + const bc = new BroadcastChannel(REFRESH_GITAUTH_BROADCAST_CHANNEL); // The message doesn't matter, any message refreshes the page! - bc.postMessage("noop") - }, [gitAuth.authenticated]) + bc.postMessage("noop"); + }, [gitAuth.authenticated]); if (!gitAuth.authenticated) { return ( @@ -57,19 +57,19 @@ const GitAuthPageView: FC = ({ /> )} - ) + ); } - const hasInstallations = gitAuth.installations.length > 0 + const hasInstallations = gitAuth.installations.length > 0; // We only want to wrap this with a link if an install URL is available! - let installTheApp: JSX.Element = <>{`install the ${gitAuth.type} App`} + let installTheApp: JSX.Element = <>{`install the ${gitAuth.type} App`}; if (gitAuth.app_install_url) { installTheApp = ( {installTheApp} - ) + ); } return ( @@ -85,7 +85,7 @@ const GitAuthPageView: FC = ({
{gitAuth.installations.map((install) => { if (!install.account) { - return + return; } return ( @@ -103,7 +103,7 @@ const GitAuthPageView: FC = ({ - ) + ); })}   {gitAuth.installations.length} organization @@ -138,58 +138,58 @@ const GitAuthPageView: FC = ({ className={styles.link} href="#" onClick={() => { - onReauthenticate() + onReauthenticate(); }} > Reauthenticate
- ) -} + ); +}; const GitDeviceAuth: FC<{ - gitAuthDevice?: GitAuthDevice - deviceExchangeError?: ApiErrorResponse + gitAuthDevice?: GitAuthDevice; + deviceExchangeError?: ApiErrorResponse; }> = ({ gitAuthDevice, deviceExchangeError }) => { - const styles = useStyles() + const styles = useStyles(); let status = (

Checking for authentication...

- ) + ); if (deviceExchangeError) { // See https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 switch (deviceExchangeError.detail) { case "authorization_pending": - break + break; case "expired_token": status = ( The one-time code has expired. Refresh to get a new one! - ) - break + ); + break; case "access_denied": status = ( Access to the Git provider was denied. - ) - break + ); + break; default: status = ( An unknown error occurred. Please try again:{" "} {deviceExchangeError.message} - ) - break + ); + break; } } if (!gitAuthDevice) { - return + return ; } return ( @@ -217,10 +217,10 @@ const GitDeviceAuth: FC<{ {status} - ) -} + ); +}; -export default GitAuthPageView +export default GitAuthPageView; const useStyles = makeStyles((theme) => ({ text: { @@ -274,4 +274,4 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.disabled, margin: theme.spacing(4), }, -})) +})); diff --git a/site/src/pages/GroupsPage/CreateGroupPage.tsx b/site/src/pages/GroupsPage/CreateGroupPage.tsx index 78ec70b68b..953f9cebb0 100644 --- a/site/src/pages/GroupsPage/CreateGroupPage.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPage.tsx @@ -1,26 +1,26 @@ -import { useMachine } from "@xstate/react" -import { useOrganizationId } from "hooks/useOrganizationId" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { useNavigate } from "react-router-dom" -import { pageTitle } from "utils/page" -import { createGroupMachine } from "xServices/groups/createGroupXService" -import CreateGroupPageView from "./CreateGroupPageView" +import { useMachine } from "@xstate/react"; +import { useOrganizationId } from "hooks/useOrganizationId"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { useNavigate } from "react-router-dom"; +import { pageTitle } from "utils/page"; +import { createGroupMachine } from "xServices/groups/createGroupXService"; +import CreateGroupPageView from "./CreateGroupPageView"; export const CreateGroupPage: FC = () => { - const navigate = useNavigate() - const organizationId = useOrganizationId() + const navigate = useNavigate(); + const organizationId = useOrganizationId(); const [createState, sendCreateEvent] = useMachine(createGroupMachine, { context: { organizationId, }, actions: { onCreate: (_, { data }) => { - navigate(`/groups/${data.id}`) + navigate(`/groups/${data.id}`); }, }, - }) - const { error } = createState.context + }); + const { error } = createState.context; return ( <> @@ -32,12 +32,12 @@ export const CreateGroupPage: FC = () => { sendCreateEvent({ type: "CREATE", data, - }) + }); }} formErrors={error} isLoading={createState.matches("creatingGroup")} /> - ) -} -export default CreateGroupPage + ); +}; +export default CreateGroupPage; diff --git a/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx b/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx index b728a2c545..eb75881df4 100644 --- a/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx @@ -1,17 +1,17 @@ -import { Story } from "@storybook/react" +import { Story } from "@storybook/react"; import { CreateGroupPageView, CreateGroupPageViewProps, -} from "./CreateGroupPageView" +} from "./CreateGroupPageView"; export default { title: "pages/CreateGroupPageView", component: CreateGroupPageView, -} +}; const Template: Story = ( args: CreateGroupPageViewProps, -) => +) => ; -export const Example = Template.bind({}) -Example.args = {} +export const Example = Template.bind({}); +Example.args = {}; diff --git a/site/src/pages/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/GroupsPage/CreateGroupPageView.tsx index a2936a5924..81c6aaeee6 100644 --- a/site/src/pages/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPageView.tsx @@ -1,31 +1,35 @@ -import TextField from "@mui/material/TextField" -import { CreateGroupRequest } from "api/typesGenerated" -import { FormFooter } from "components/FormFooter/FormFooter" -import { FullPageForm } from "components/FullPageForm/FullPageForm" -import { Margins } from "components/Margins/Margins" -import { Stack } from "components/Stack/Stack" -import { useFormik } from "formik" -import { FC } from "react" -import { useNavigate } from "react-router-dom" -import { getFormHelpers, nameValidator, onChangeTrimmed } from "utils/formUtils" -import * as Yup from "yup" +import TextField from "@mui/material/TextField"; +import { CreateGroupRequest } from "api/typesGenerated"; +import { FormFooter } from "components/FormFooter/FormFooter"; +import { FullPageForm } from "components/FullPageForm/FullPageForm"; +import { Margins } from "components/Margins/Margins"; +import { Stack } from "components/Stack/Stack"; +import { useFormik } from "formik"; +import { FC } from "react"; +import { useNavigate } from "react-router-dom"; +import { + getFormHelpers, + nameValidator, + onChangeTrimmed, +} from "utils/formUtils"; +import * as Yup from "yup"; const validationSchema = Yup.object({ name: nameValidator("Name"), -}) +}); export type CreateGroupPageViewProps = { - onSubmit: (data: CreateGroupRequest) => void - formErrors?: unknown - isLoading: boolean -} + onSubmit: (data: CreateGroupRequest) => void; + formErrors?: unknown; + isLoading: boolean; +}; export const CreateGroupPageView: FC = ({ onSubmit, formErrors, isLoading, }) => { - const navigate = useNavigate() + const navigate = useNavigate(); const form = useFormik({ initialValues: { name: "", @@ -35,9 +39,9 @@ export const CreateGroupPageView: FC = ({ }, validationSchema, onSubmit, - }) - const getFieldHelpers = getFormHelpers(form, formErrors) - const onCancel = () => navigate("/groups") + }); + const getFieldHelpers = getFormHelpers(form, formErrors); + const onCancel = () => navigate("/groups"); return ( @@ -75,6 +79,6 @@ export const CreateGroupPageView: FC = ({ - ) -} -export default CreateGroupPageView + ); +}; +export default CreateGroupPageView; diff --git a/site/src/pages/GroupsPage/GroupPage.tsx b/site/src/pages/GroupsPage/GroupPage.tsx index a1174336c4..f0dc74ce80 100644 --- a/site/src/pages/GroupsPage/GroupPage.tsx +++ b/site/src/pages/GroupsPage/GroupPage.tsx @@ -1,63 +1,63 @@ -import Button from "@mui/material/Button" -import Link from "@mui/material/Link" -import Table from "@mui/material/Table" -import TableBody from "@mui/material/TableBody" -import TableCell from "@mui/material/TableCell" -import TableContainer from "@mui/material/TableContainer" -import TableHead from "@mui/material/TableHead" -import TableRow from "@mui/material/TableRow" -import DeleteOutline from "@mui/icons-material/DeleteOutline" -import PersonAdd from "@mui/icons-material/PersonAdd" -import SettingsOutlined from "@mui/icons-material/SettingsOutlined" -import { useMachine } from "@xstate/react" -import { User } from "api/typesGenerated" -import { AvatarData } from "components/AvatarData/AvatarData" -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog" -import { EmptyState } from "components/EmptyState/EmptyState" -import { Loader } from "components/Loader/Loader" -import { LoadingButton } from "components/LoadingButton/LoadingButton" -import { Margins } from "components/Margins/Margins" +import Button from "@mui/material/Button"; +import Link from "@mui/material/Link"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import DeleteOutline from "@mui/icons-material/DeleteOutline"; +import PersonAdd from "@mui/icons-material/PersonAdd"; +import SettingsOutlined from "@mui/icons-material/SettingsOutlined"; +import { useMachine } from "@xstate/react"; +import { User } from "api/typesGenerated"; +import { AvatarData } from "components/AvatarData/AvatarData"; +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { Loader } from "components/Loader/Loader"; +import { LoadingButton } from "components/LoadingButton/LoadingButton"; +import { Margins } from "components/Margins/Margins"; import { PageHeader, PageHeaderSubtitle, PageHeaderTitle, -} from "components/PageHeader/PageHeader" -import { Stack } from "components/Stack/Stack" -import { TableRowMenu } from "components/TableRowMenu/TableRowMenu" -import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete" -import { useState } from "react" -import { Helmet } from "react-helmet-async" -import { Link as RouterLink, useNavigate, useParams } from "react-router-dom" -import { pageTitle } from "utils/page" -import { groupMachine } from "xServices/groups/groupXService" -import { Maybe } from "components/Conditionals/Maybe" -import { makeStyles } from "@mui/styles" +} from "components/PageHeader/PageHeader"; +import { Stack } from "components/Stack/Stack"; +import { TableRowMenu } from "components/TableRowMenu/TableRowMenu"; +import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; +import { useState } from "react"; +import { Helmet } from "react-helmet-async"; +import { Link as RouterLink, useNavigate, useParams } from "react-router-dom"; +import { pageTitle } from "utils/page"; +import { groupMachine } from "xServices/groups/groupXService"; +import { Maybe } from "components/Conditionals/Maybe"; +import { makeStyles } from "@mui/styles"; import { PaginationStatus, TableToolbar, -} from "components/TableToolbar/TableToolbar" -import { UserAvatar } from "components/UserAvatar/UserAvatar" -import { isEveryoneGroup } from "utils/groups" +} from "components/TableToolbar/TableToolbar"; +import { UserAvatar } from "components/UserAvatar/UserAvatar"; +import { isEveryoneGroup } from "utils/groups"; const AddGroupMember: React.FC<{ - isLoading: boolean - onSubmit: (user: User, reset: () => void) => void + isLoading: boolean; + onSubmit: (user: User, reset: () => void) => void; }> = ({ isLoading, onSubmit }) => { - const [selectedUser, setSelectedUser] = useState(null) - const styles = useStyles() + const [selectedUser, setSelectedUser] = useState(null); + const styles = useStyles(); const resetValues = () => { - setSelectedUser(null) - } + setSelectedUser(null); + }; return (
{ - e.preventDefault() + e.preventDefault(); if (selectedUser) { - onSubmit(selectedUser, resetValues) + onSubmit(selectedUser, resetValues); } }} > @@ -66,7 +66,7 @@ const AddGroupMember: React.FC<{ className={styles.autoComplete} value={selectedUser} onChange={(newValue) => { - setSelectedUser(newValue) + setSelectedUser(newValue); }} /> @@ -80,29 +80,29 @@ const AddGroupMember: React.FC<{
- ) -} + ); +}; export const GroupPage: React.FC = () => { - const { groupId } = useParams() + const { groupId } = useParams(); if (!groupId) { - throw new Error("groupId is not defined.") + throw new Error("groupId is not defined."); } - const navigate = useNavigate() + const navigate = useNavigate(); const [state, send] = useMachine(groupMachine, { context: { groupId, }, actions: { redirectToGroups: () => { - navigate("/groups") + navigate("/groups"); }, }, - }) - const { group, permissions } = state.context - const isLoading = group === undefined || permissions === undefined - const canUpdateGroup = permissions ? permissions.canUpdateGroup : false + }); + const { group, permissions } = state.context; + const isLoading = group === undefined || permissions === undefined; + const canUpdateGroup = permissions ? permissions.canUpdateGroup : false; return ( <> @@ -127,7 +127,7 @@ export const GroupPage: React.FC = () => { - ) -} + ); +}; diff --git a/site/src/pages/LoginPage/SignInForm/SignInForm.types.ts b/site/src/pages/LoginPage/SignInForm/SignInForm.types.ts index 8965458bd8..a143b46e4d 100644 --- a/site/src/pages/LoginPage/SignInForm/SignInForm.types.ts +++ b/site/src/pages/LoginPage/SignInForm/SignInForm.types.ts @@ -4,6 +4,6 @@ * auth providers available and administrative configurations */ export interface BuiltInAuthFormValues { - email: string - password: string + email: string; + password: string; } diff --git a/site/src/pages/SetupPage/SetupPage.test.tsx b/site/src/pages/SetupPage/SetupPage.test.tsx index 90272e00fe..263a99c185 100644 --- a/site/src/pages/SetupPage/SetupPage.test.tsx +++ b/site/src/pages/SetupPage/SetupPage.test.tsx @@ -1,59 +1,59 @@ -import { fireEvent, screen, waitFor } from "@testing-library/react" -import userEvent from "@testing-library/user-event" -import { rest } from "msw" -import { createMemoryRouter } from "react-router-dom" -import { render, renderWithRouter } from "testHelpers/renderHelpers" -import { server } from "testHelpers/server" -import { SetupPage } from "./SetupPage" -import { Language as PageViewLanguage } from "./SetupPageView" -import { MockUser } from "testHelpers/entities" +import { fireEvent, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { rest } from "msw"; +import { createMemoryRouter } from "react-router-dom"; +import { render, renderWithRouter } from "testHelpers/renderHelpers"; +import { server } from "testHelpers/server"; +import { SetupPage } from "./SetupPage"; +import { Language as PageViewLanguage } from "./SetupPageView"; +import { MockUser } from "testHelpers/entities"; const fillForm = async ({ username = "someuser", email = "someone@coder.com", password = "password", }: { - username?: string - email?: string - password?: string + username?: string; + email?: string; + password?: string; } = {}) => { - const usernameField = screen.getByLabelText(PageViewLanguage.usernameLabel) - const emailField = screen.getByLabelText(PageViewLanguage.emailLabel) - const passwordField = screen.getByLabelText(PageViewLanguage.passwordLabel) - await userEvent.type(usernameField, username) - await userEvent.type(emailField, email) - await userEvent.type(passwordField, password) + const usernameField = screen.getByLabelText(PageViewLanguage.usernameLabel); + const emailField = screen.getByLabelText(PageViewLanguage.emailLabel); + const passwordField = screen.getByLabelText(PageViewLanguage.passwordLabel); + await userEvent.type(usernameField, username); + await userEvent.type(emailField, email); + await userEvent.type(passwordField, password); const submitButton = screen.getByRole("button", { name: PageViewLanguage.create, - }) - fireEvent.click(submitButton) -} + }); + fireEvent.click(submitButton); +}; describe("Setup Page", () => { beforeEach(() => { // appear logged out server.use( rest.get("/api/v2/users/me", (req, res, ctx) => { - return res(ctx.status(401), ctx.json({ message: "no user here" })) + return res(ctx.status(401), ctx.json({ message: "no user here" })); }), rest.get("/api/v2/users/first", (req, res, ctx) => { return res( ctx.status(404), ctx.json({ message: "no first user has been created" }), - ) + ); }), - ) - }) + ); + }); it("shows validation error message", async () => { - render() - await fillForm({ email: "test" }) - const errorMessage = await screen.findByText(PageViewLanguage.emailInvalid) - expect(errorMessage).toBeDefined() - }) + render(); + await fillForm({ email: "test" }); + const errorMessage = await screen.findByText(PageViewLanguage.emailInvalid); + expect(errorMessage).toBeDefined(); + }); it("shows API error message", async () => { - const fieldErrorMessage = "invalid username" + const fieldErrorMessage = "invalid username"; server.use( rest.post("/api/v2/users/first", async (req, res, ctx) => { return res( @@ -67,51 +67,51 @@ describe("Setup Page", () => { }, ], }), - ) + ); }), - ) + ); - render() - await fillForm() - const errorMessage = await screen.findByText(fieldErrorMessage) - expect(errorMessage).toBeDefined() - }) + render(); + await fillForm(); + const errorMessage = await screen.findByText(fieldErrorMessage); + expect(errorMessage).toBeDefined(); + }); it("redirects to the app when setup is successful", async () => { - let userHasBeenCreated = false + let userHasBeenCreated = false; server.use( rest.get("/api/v2/users/me", (req, res, ctx) => { if (!userHasBeenCreated) { - return res(ctx.status(401), ctx.json({ message: "no user here" })) + return res(ctx.status(401), ctx.json({ message: "no user here" })); } - return res(ctx.status(200), ctx.json(MockUser)) + return res(ctx.status(200), ctx.json(MockUser)); }), rest.get("/api/v2/users/first", (req, res, ctx) => { if (!userHasBeenCreated) { return res( ctx.status(404), ctx.json({ message: "no first user has been created" }), - ) + ); } return res( ctx.status(200), ctx.json({ message: "hooray, someone exists!" }), - ) + ); }), rest.post("/api/v2/users/first", (req, res, ctx) => { - userHasBeenCreated = true + userHasBeenCreated = true; return res( ctx.status(200), ctx.json({ data: "user setup was successful!" }), - ) + ); }), - ) + ); - render() - await fillForm() - await waitFor(() => expect(window.location).toBeAt("/")) - }) + render(); + await fillForm(); + await waitFor(() => expect(window.location).toBeAt("/")); + }); it("redirects to login if setup has already completed", async () => { // simulates setup having already been completed @@ -120,9 +120,9 @@ describe("Setup Page", () => { return res( ctx.status(200), ctx.json({ message: "hooray, someone exists!" }), - ) + ); }), - ) + ); renderWithRouter( createMemoryRouter( @@ -138,24 +138,24 @@ describe("Setup Page", () => { ], { initialEntries: ["/setup"] }, ), - ) + ); - await screen.findByText("Login") - }) + await screen.findByText("Login"); + }); it("redirects to the app when already logged in", async () => { // simulates the user will be authenticated server.use( rest.get("/api/v2/users/me", (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MockUser)) + return res(ctx.status(200), ctx.json(MockUser)); }), rest.get("/api/v2/users/first", (req, res, ctx) => { return res( ctx.status(200), ctx.json({ message: "hooray, someone exists!" }), - ) + ); }), - ) + ); renderWithRouter( createMemoryRouter( @@ -171,8 +171,8 @@ describe("Setup Page", () => { ], { initialEntries: ["/setup"] }, ), - ) + ); - await screen.findByText("Workspaces") - }) -}) + await screen.findByText("Workspaces"); + }); +}); diff --git a/site/src/pages/SetupPage/SetupPage.tsx b/site/src/pages/SetupPage/SetupPage.tsx index 17aef0f318..f9e0eb9649 100644 --- a/site/src/pages/SetupPage/SetupPage.tsx +++ b/site/src/pages/SetupPage/SetupPage.tsx @@ -1,43 +1,43 @@ -import { useMachine } from "@xstate/react" -import { useAuth } from "components/AuthProvider/AuthProvider" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "utils/page" -import { setupMachine } from "xServices/setup/setupXService" -import { SetupPageView } from "./SetupPageView" -import { Navigate } from "react-router-dom" +import { useMachine } from "@xstate/react"; +import { useAuth } from "components/AuthProvider/AuthProvider"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { setupMachine } from "xServices/setup/setupXService"; +import { SetupPageView } from "./SetupPageView"; +import { Navigate } from "react-router-dom"; export const SetupPage: FC = () => { - const [authState, authSend] = useAuth() + const [authState, authSend] = useAuth(); const [setupState, setupSend] = useMachine(setupMachine, { actions: { onCreateFirstUser: ({ firstUser }) => { if (!firstUser) { - throw new Error("First user was not defined.") + throw new Error("First user was not defined."); } authSend({ type: "SIGN_IN", email: firstUser.email, password: firstUser.password, - }) + }); }, }, - }) - const { error } = setupState.context + }); + const { error } = setupState.context; - const userIsSignedIn = authState.matches("signedIn") + const userIsSignedIn = authState.matches("signedIn"); const setupIsComplete = !authState.matches("loadingInitialAuthData") && - !authState.matches("configuringTheFirstUser") + !authState.matches("configuringTheFirstUser"); // If the user is logged in, navigate to the app if (userIsSignedIn) { - return + return ; } // If we've already completed setup, navigate to the login page if (setupIsComplete) { - return + return ; } return ( @@ -49,9 +49,9 @@ export const SetupPage: FC = () => { isLoading={setupState.hasTag("loading")} error={error} onSubmit={(firstUser) => { - setupSend({ type: "CREATE_FIRST_USER", firstUser }) + setupSend({ type: "CREATE_FIRST_USER", firstUser }); }} /> - ) -} + ); +}; diff --git a/site/src/pages/SetupPage/SetupPageView.stories.tsx b/site/src/pages/SetupPage/SetupPageView.stories.tsx index d39e62a262..1cbe5e03a1 100644 --- a/site/src/pages/SetupPage/SetupPageView.stories.tsx +++ b/site/src/pages/SetupPage/SetupPageView.stories.tsx @@ -1,32 +1,32 @@ -import { action } from "@storybook/addon-actions" -import { Story } from "@storybook/react" -import { SetupPageView, SetupPageViewProps } from "./SetupPageView" -import { mockApiError } from "testHelpers/entities" +import { action } from "@storybook/addon-actions"; +import { Story } from "@storybook/react"; +import { SetupPageView, SetupPageViewProps } from "./SetupPageView"; +import { mockApiError } from "testHelpers/entities"; export default { title: "pages/SetupPageView", component: SetupPageView, -} +}; const Template: Story = (args: SetupPageViewProps) => ( -) +); -export const Ready = Template.bind({}) +export const Ready = Template.bind({}); Ready.args = { onSubmit: action("submit"), -} +}; -export const FormError = Template.bind({}) +export const FormError = Template.bind({}); FormError.args = { onSubmit: action("submit"), error: mockApiError({ validations: [{ field: "username", detail: "Username taken" }], }), -} +}; -export const Loading = Template.bind({}) +export const Loading = Template.bind({}); Loading.args = { onSubmit: action("submit"), isLoading: true, -} +}; diff --git a/site/src/pages/SetupPage/SetupPageView.tsx b/site/src/pages/SetupPage/SetupPageView.tsx index df6fedc2f0..681f367ad0 100644 --- a/site/src/pages/SetupPage/SetupPageView.tsx +++ b/site/src/pages/SetupPage/SetupPageView.tsx @@ -1,16 +1,20 @@ -import Box from "@mui/material/Box" -import Checkbox from "@mui/material/Checkbox" -import { makeStyles } from "@mui/styles" -import TextField from "@mui/material/TextField" -import Typography from "@mui/material/Typography" -import { LoadingButton } from "components/LoadingButton/LoadingButton" -import { SignInLayout } from "components/SignInLayout/SignInLayout" -import { Stack } from "components/Stack/Stack" -import { Welcome } from "components/Welcome/Welcome" -import { FormikContextType, useFormik } from "formik" -import { getFormHelpers, nameValidator, onChangeTrimmed } from "utils/formUtils" -import * as Yup from "yup" -import * as TypesGen from "../../api/typesGenerated" +import Box from "@mui/material/Box"; +import Checkbox from "@mui/material/Checkbox"; +import { makeStyles } from "@mui/styles"; +import TextField from "@mui/material/TextField"; +import Typography from "@mui/material/Typography"; +import { LoadingButton } from "components/LoadingButton/LoadingButton"; +import { SignInLayout } from "components/SignInLayout/SignInLayout"; +import { Stack } from "components/Stack/Stack"; +import { Welcome } from "components/Welcome/Welcome"; +import { FormikContextType, useFormik } from "formik"; +import { + getFormHelpers, + nameValidator, + onChangeTrimmed, +} from "utils/formUtils"; +import * as Yup from "yup"; +import * as TypesGen from "../../api/typesGenerated"; export const Language = { emailLabel: "Email", @@ -21,7 +25,7 @@ export const Language = { passwordRequired: "Please enter a password.", create: "Create account", welcomeMessage: <>Welcome to Coder, -} +}; const validationSchema = Yup.object({ email: Yup.string() @@ -30,12 +34,12 @@ const validationSchema = Yup.object({ .required(Language.emailRequired), password: Yup.string().required(Language.passwordRequired), username: nameValidator(Language.usernameLabel), -}) +}); export interface SetupPageViewProps { - onSubmit: (firstUser: TypesGen.CreateFirstUserRequest) => void - error?: unknown - isLoading?: boolean + onSubmit: (firstUser: TypesGen.CreateFirstUserRequest) => void; + error?: unknown; + isLoading?: boolean; } export const SetupPageView: React.FC = ({ @@ -53,12 +57,12 @@ export const SetupPageView: React.FC = ({ }, validationSchema, onSubmit, - }) + }); const getFieldHelpers = getFormHelpers( form, error, - ) - const styles = useStyles() + ); + const styles = useStyles(); return ( @@ -122,11 +126,11 @@ export const SetupPageView: React.FC = ({ - ) -} + ); +}; const useStyles = makeStyles(() => ({ callout: { borderRadius: 16, }, -})) +})); diff --git a/site/src/pages/StarterTemplatePage/StarterTemplatePage.test.tsx b/site/src/pages/StarterTemplatePage/StarterTemplatePage.test.tsx index 482a7d2e2c..a196be8fd2 100644 --- a/site/src/pages/StarterTemplatePage/StarterTemplatePage.test.tsx +++ b/site/src/pages/StarterTemplatePage/StarterTemplatePage.test.tsx @@ -1,20 +1,20 @@ -import { screen } from "@testing-library/react" -import { MockTemplateExample } from "testHelpers/entities" +import { screen } from "@testing-library/react"; +import { MockTemplateExample } from "testHelpers/entities"; import { renderWithAuth, waitForLoaderToBeRemoved, -} from "testHelpers/renderHelpers" -import StarterTemplatePage from "./StarterTemplatePage" +} from "testHelpers/renderHelpers"; +import StarterTemplatePage from "./StarterTemplatePage"; -jest.mock("remark-gfm", () => jest.fn()) +jest.mock("remark-gfm", () => jest.fn()); describe("StarterTemplatePage", () => { it("shows the starter template", async () => { renderWithAuth(, { route: `/starter-templates/${MockTemplateExample.id}`, path: "/starter-templates/:exampleId", - }) - await waitForLoaderToBeRemoved() - expect(screen.getByText(MockTemplateExample.name)).toBeInTheDocument() - }) -}) + }); + await waitForLoaderToBeRemoved(); + expect(screen.getByText(MockTemplateExample.name)).toBeInTheDocument(); + }); +}); diff --git a/site/src/pages/StarterTemplatePage/StarterTemplatePage.tsx b/site/src/pages/StarterTemplatePage/StarterTemplatePage.tsx index 59af54fdc7..289f7f0bb3 100644 --- a/site/src/pages/StarterTemplatePage/StarterTemplatePage.tsx +++ b/site/src/pages/StarterTemplatePage/StarterTemplatePage.tsx @@ -1,21 +1,21 @@ -import { useMachine } from "@xstate/react" -import { useOrganizationId } from "hooks/useOrganizationId" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { useParams } from "react-router-dom" -import { pageTitle } from "utils/page" -import { starterTemplateMachine } from "xServices/starterTemplates/starterTemplateXService" -import { StarterTemplatePageView } from "./StarterTemplatePageView" +import { useMachine } from "@xstate/react"; +import { useOrganizationId } from "hooks/useOrganizationId"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { useParams } from "react-router-dom"; +import { pageTitle } from "utils/page"; +import { starterTemplateMachine } from "xServices/starterTemplates/starterTemplateXService"; +import { StarterTemplatePageView } from "./StarterTemplatePageView"; const StarterTemplatePage: FC = () => { - const { exampleId } = useParams() as { exampleId: string } - const organizationId = useOrganizationId() + const { exampleId } = useParams() as { exampleId: string }; + const organizationId = useOrganizationId(); const [state] = useMachine(starterTemplateMachine, { context: { organizationId, exampleId, }, - }) + }); return ( <> @@ -27,7 +27,7 @@ const StarterTemplatePage: FC = () => { - ) -} + ); +}; -export default StarterTemplatePage +export default StarterTemplatePage; diff --git a/site/src/pages/StarterTemplatePage/StarterTemplatePageView.stories.tsx b/site/src/pages/StarterTemplatePage/StarterTemplatePageView.stories.tsx index 5f373e32ae..ef9489fe79 100644 --- a/site/src/pages/StarterTemplatePage/StarterTemplatePageView.stories.tsx +++ b/site/src/pages/StarterTemplatePage/StarterTemplatePageView.stories.tsx @@ -1,24 +1,24 @@ -import { Story } from "@storybook/react" +import { Story } from "@storybook/react"; import { mockApiError, MockOrganization, MockTemplateExample, -} from "testHelpers/entities" +} from "testHelpers/entities"; import { StarterTemplatePageView, StarterTemplatePageViewProps, -} from "./StarterTemplatePageView" +} from "./StarterTemplatePageView"; export default { title: "pages/StarterTemplatePageView", component: StarterTemplatePageView, -} +}; const Template: Story = (args) => ( -) +); -export const Default = Template.bind({}) +export const Default = Template.bind({}); Default.args = { context: { exampleId: MockTemplateExample.id, @@ -26,9 +26,9 @@ Default.args = { error: undefined, starterTemplate: MockTemplateExample, }, -} +}; -export const Error = Template.bind({}) +export const Error = Template.bind({}); Error.args = { context: { exampleId: MockTemplateExample.id, @@ -38,4 +38,4 @@ Error.args = { }), starterTemplate: undefined, }, -} +}; diff --git a/site/src/pages/StarterTemplatePage/StarterTemplatePageView.tsx b/site/src/pages/StarterTemplatePage/StarterTemplatePageView.tsx index 44fad0aa17..c38419c055 100644 --- a/site/src/pages/StarterTemplatePage/StarterTemplatePageView.tsx +++ b/site/src/pages/StarterTemplatePage/StarterTemplatePageView.tsx @@ -1,43 +1,43 @@ -import Button from "@mui/material/Button" -import { makeStyles } from "@mui/styles" -import { Loader } from "components/Loader/Loader" -import { Margins } from "components/Margins/Margins" -import { MemoizedMarkdown } from "components/Markdown/Markdown" +import Button from "@mui/material/Button"; +import { makeStyles } from "@mui/styles"; +import { Loader } from "components/Loader/Loader"; +import { Margins } from "components/Margins/Margins"; +import { MemoizedMarkdown } from "components/Markdown/Markdown"; import { PageHeader, PageHeaderSubtitle, PageHeaderTitle, -} from "components/PageHeader/PageHeader" -import { FC } from "react" -import { StarterTemplateContext } from "xServices/starterTemplates/starterTemplateXService" -import ViewCodeIcon from "@mui/icons-material/OpenInNewOutlined" -import PlusIcon from "@mui/icons-material/AddOutlined" -import { useTranslation } from "react-i18next" -import { Stack } from "components/Stack/Stack" -import { Link } from "react-router-dom" -import { ErrorAlert } from "components/Alert/ErrorAlert" +} from "components/PageHeader/PageHeader"; +import { FC } from "react"; +import { StarterTemplateContext } from "xServices/starterTemplates/starterTemplateXService"; +import ViewCodeIcon from "@mui/icons-material/OpenInNewOutlined"; +import PlusIcon from "@mui/icons-material/AddOutlined"; +import { useTranslation } from "react-i18next"; +import { Stack } from "components/Stack/Stack"; +import { Link } from "react-router-dom"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; export interface StarterTemplatePageViewProps { - context: StarterTemplateContext + context: StarterTemplateContext; } export const StarterTemplatePageView: FC = ({ context, }) => { - const styles = useStyles() - const { starterTemplate } = context - const { t } = useTranslation("starterTemplatePage") + const styles = useStyles(); + const { starterTemplate } = context; + const { t } = useTranslation("starterTemplatePage"); if (context.error) { return ( - ) + ); } if (!starterTemplate) { - return + return ; } return ( @@ -84,8 +84,8 @@ export const StarterTemplatePageView: FC = ({ - ) -} + ); +}; export const useStyles = makeStyles((theme) => { return { @@ -112,5 +112,5 @@ export const useStyles = makeStyles((theme) => { maxWidth: 800, margin: "auto", }, - } -}) + }; +}); diff --git a/site/src/pages/StarterTemplatesPage/StarterTemplatesPage.test.tsx b/site/src/pages/StarterTemplatesPage/StarterTemplatesPage.test.tsx index ba3addc537..27ce03f488 100644 --- a/site/src/pages/StarterTemplatesPage/StarterTemplatesPage.test.tsx +++ b/site/src/pages/StarterTemplatesPage/StarterTemplatesPage.test.tsx @@ -1,19 +1,22 @@ -import { screen } from "@testing-library/react" -import { MockTemplateExample, MockTemplateExample2 } from "testHelpers/entities" +import { screen } from "@testing-library/react"; +import { + MockTemplateExample, + MockTemplateExample2, +} from "testHelpers/entities"; import { renderWithAuth, waitForLoaderToBeRemoved, -} from "testHelpers/renderHelpers" -import StarterTemplatesPage from "./StarterTemplatesPage" +} from "testHelpers/renderHelpers"; +import StarterTemplatesPage from "./StarterTemplatesPage"; describe("StarterTemplatesPage", () => { it("shows the starter template", async () => { renderWithAuth(, { route: `/starter-templates`, path: "/starter-templates", - }) - await waitForLoaderToBeRemoved() - expect(screen.getByText(MockTemplateExample.name)).toBeInTheDocument() - expect(screen.getByText(MockTemplateExample2.name)).toBeInTheDocument() - }) -}) + }); + await waitForLoaderToBeRemoved(); + expect(screen.getByText(MockTemplateExample.name)).toBeInTheDocument(); + expect(screen.getByText(MockTemplateExample2.name)).toBeInTheDocument(); + }); +}); diff --git a/site/src/pages/StarterTemplatesPage/StarterTemplatesPage.tsx b/site/src/pages/StarterTemplatesPage/StarterTemplatesPage.tsx index 973c6d709c..65d917578d 100644 --- a/site/src/pages/StarterTemplatesPage/StarterTemplatesPage.tsx +++ b/site/src/pages/StarterTemplatesPage/StarterTemplatesPage.tsx @@ -1,18 +1,18 @@ -import { useMachine } from "@xstate/react" -import { useOrganizationId } from "hooks/useOrganizationId" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { useTranslation } from "react-i18next" -import { pageTitle } from "utils/page" -import { starterTemplatesMachine } from "xServices/starterTemplates/starterTemplatesXService" -import { StarterTemplatesPageView } from "./StarterTemplatesPageView" +import { useMachine } from "@xstate/react"; +import { useOrganizationId } from "hooks/useOrganizationId"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { useTranslation } from "react-i18next"; +import { pageTitle } from "utils/page"; +import { starterTemplatesMachine } from "xServices/starterTemplates/starterTemplatesXService"; +import { StarterTemplatesPageView } from "./StarterTemplatesPageView"; const StarterTemplatesPage: FC = () => { - const { t } = useTranslation("starterTemplatesPage") - const organizationId = useOrganizationId() + const { t } = useTranslation("starterTemplatesPage"); + const organizationId = useOrganizationId(); const [state] = useMachine(starterTemplatesMachine, { context: { organizationId }, - }) + }); return ( <> @@ -22,7 +22,7 @@ const StarterTemplatesPage: FC = () => { - ) -} + ); +}; -export default StarterTemplatesPage +export default StarterTemplatesPage; diff --git a/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.stories.tsx b/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.stories.tsx index b5c4f52b95..98efedfabc 100644 --- a/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.stories.tsx +++ b/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.stories.tsx @@ -1,26 +1,26 @@ -import { Story } from "@storybook/react" +import { Story } from "@storybook/react"; import { mockApiError, MockOrganization, MockTemplateExample, MockTemplateExample2, -} from "testHelpers/entities" -import { getTemplatesByTag } from "utils/starterTemplates" +} from "testHelpers/entities"; +import { getTemplatesByTag } from "utils/starterTemplates"; import { StarterTemplatesPageView, StarterTemplatesPageViewProps, -} from "./StarterTemplatesPageView" +} from "./StarterTemplatesPageView"; export default { title: "pages/StarterTemplatesPageView", component: StarterTemplatesPageView, -} +}; const Template: Story = (args) => ( -) +); -export const Default = Template.bind({}) +export const Default = Template.bind({}); Default.args = { context: { organizationId: MockOrganization.id, @@ -30,9 +30,9 @@ Default.args = { MockTemplateExample2, ]), }, -} +}; -export const Error = Template.bind({}) +export const Error = Template.bind({}); Error.args = { context: { organizationId: MockOrganization.id, @@ -41,4 +41,4 @@ Error.args = { }), starterTemplatesByTag: undefined, }, -} +}; diff --git a/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.tsx b/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.tsx index 28ac49a207..60828abe4a 100644 --- a/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.tsx +++ b/site/src/pages/StarterTemplatesPage/StarterTemplatesPageView.tsx @@ -1,20 +1,20 @@ -import { makeStyles } from "@mui/styles" -import { ErrorAlert } from "components/Alert/ErrorAlert" -import { Maybe } from "components/Conditionals/Maybe" -import { Loader } from "components/Loader/Loader" -import { Margins } from "components/Margins/Margins" +import { makeStyles } from "@mui/styles"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Maybe } from "components/Conditionals/Maybe"; +import { Loader } from "components/Loader/Loader"; +import { Margins } from "components/Margins/Margins"; import { PageHeader, PageHeaderSubtitle, PageHeaderTitle, -} from "components/PageHeader/PageHeader" -import { Stack } from "components/Stack/Stack" -import { TemplateExampleCard } from "components/TemplateExampleCard/TemplateExampleCard" -import { FC } from "react" -import { useTranslation } from "react-i18next" -import { Link, useSearchParams } from "react-router-dom" -import { combineClasses } from "utils/combineClasses" -import { StarterTemplatesContext } from "xServices/starterTemplates/starterTemplatesXService" +} from "components/PageHeader/PageHeader"; +import { Stack } from "components/Stack/Stack"; +import { TemplateExampleCard } from "components/TemplateExampleCard/TemplateExampleCard"; +import { FC } from "react"; +import { useTranslation } from "react-i18next"; +import { Link, useSearchParams } from "react-router-dom"; +import { combineClasses } from "utils/combineClasses"; +import { StarterTemplatesContext } from "xServices/starterTemplates/starterTemplatesXService"; const getTagLabel = (tag: string, t: (key: string) => string) => { const labelByTag: Record = { @@ -22,32 +22,32 @@ const getTagLabel = (tag: string, t: (key: string) => string) => { digitalocean: t("tags.digitalocean"), aws: t("tags.aws"), google: t("tags.google"), - } + }; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- this can be undefined - return labelByTag[tag] ?? tag -} + return labelByTag[tag] ?? tag; +}; const selectTags = ({ starterTemplatesByTag }: StarterTemplatesContext) => { return starterTemplatesByTag ? Object.keys(starterTemplatesByTag).sort((a, b) => a.localeCompare(b)) - : undefined -} + : undefined; +}; export interface StarterTemplatesPageViewProps { - context: StarterTemplatesContext + context: StarterTemplatesContext; } export const StarterTemplatesPageView: FC = ({ context, }) => { - const { t } = useTranslation("starterTemplatesPage") - const [urlParams] = useSearchParams() - const styles = useStyles() - const { starterTemplatesByTag } = context - const tags = selectTags(context) - const activeTag = urlParams.get("tag") ?? "all" + const { t } = useTranslation("starterTemplatesPage"); + const [urlParams] = useSearchParams(); + const styles = useStyles(); + const { starterTemplatesByTag } = context; + const tags = selectTags(context); + const activeTag = urlParams.get("tag") ?? "all"; const visibleTemplates = starterTemplatesByTag ? starterTemplatesByTag[activeTag] - : undefined + : undefined; return ( @@ -91,8 +91,8 @@ export const StarterTemplatesPageView: FC = ({ - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ filter: { @@ -131,4 +131,4 @@ const useStyles = makeStyles((theme) => ({ gap: theme.spacing(2), gridAutoRows: "min-content", }, -})) +})); diff --git a/site/src/pages/TemplatePage/TemplateDocsPage/TemplateDocsPage.test.tsx b/site/src/pages/TemplatePage/TemplateDocsPage/TemplateDocsPage.test.tsx index 793a7c01d9..4e50feac94 100644 --- a/site/src/pages/TemplatePage/TemplateDocsPage/TemplateDocsPage.test.tsx +++ b/site/src/pages/TemplatePage/TemplateDocsPage/TemplateDocsPage.test.tsx @@ -1,16 +1,16 @@ -import { screen } from "@testing-library/react" -import { TemplateLayout } from "components/TemplateLayout/TemplateLayout" -import { ResizeObserver } from "resize-observer" -import { renderWithAuth } from "testHelpers/renderHelpers" -import TemplateDocsPage from "./TemplateDocsPage" +import { screen } from "@testing-library/react"; +import { TemplateLayout } from "components/TemplateLayout/TemplateLayout"; +import { ResizeObserver } from "resize-observer"; +import { renderWithAuth } from "testHelpers/renderHelpers"; +import TemplateDocsPage from "./TemplateDocsPage"; -jest.mock("remark-gfm", () => jest.fn()) +jest.mock("remark-gfm", () => jest.fn()); -const TEMPLATE_NAME = "coder-ts" +const TEMPLATE_NAME = "coder-ts"; Object.defineProperty(window, "ResizeObserver", { value: ResizeObserver, -}) +}); const renderPage = () => renderWithAuth( @@ -21,11 +21,11 @@ const renderPage = () => route: `/templates/${TEMPLATE_NAME}/docs`, path: "/templates/:template/docs", }, - ) + ); describe("TemplateSummaryPage", () => { it("shows the template readme", async () => { - renderPage() - await screen.findByTestId("markdown") - }) -}) + renderPage(); + await screen.findByTestId("markdown"); + }); +}); diff --git a/site/src/pages/TemplatePage/TemplateDocsPage/TemplateDocsPage.tsx b/site/src/pages/TemplatePage/TemplateDocsPage/TemplateDocsPage.tsx index 602122b736..5e1827861b 100644 --- a/site/src/pages/TemplatePage/TemplateDocsPage/TemplateDocsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateDocsPage/TemplateDocsPage.tsx @@ -1,15 +1,15 @@ -import { makeStyles } from "@mui/styles" -import { MemoizedMarkdown } from "components/Markdown/Markdown" -import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout" -import frontMatter from "front-matter" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "utils/page" +import { makeStyles } from "@mui/styles"; +import { MemoizedMarkdown } from "components/Markdown/Markdown"; +import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout"; +import frontMatter from "front-matter"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; export default function TemplateDocsPage() { - const { template, activeVersion } = useTemplateLayoutContext() - const styles = useStyles() + const { template, activeVersion } = useTemplateLayoutContext(); + const styles = useStyles(); - const readme = frontMatter(activeVersion.readme) + const readme = frontMatter(activeVersion.readme); return ( <> @@ -24,7 +24,7 @@ export default function TemplateDocsPage() { - ) + ); } export const useStyles = makeStyles((theme) => { @@ -47,5 +47,5 @@ export const useStyles = makeStyles((theme) => { maxWidth: 800, margin: "auto", }, - } -}) + }; +}); diff --git a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx index 898e89efeb..2bfed5e94d 100644 --- a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx +++ b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.test.tsx @@ -1,22 +1,22 @@ import { renderWithAuth, waitForLoaderToBeRemoved, -} from "testHelpers/renderHelpers" -import TemplateEmbedPage from "./TemplateEmbedPage" -import { TemplateLayout } from "components/TemplateLayout/TemplateLayout" +} from "testHelpers/renderHelpers"; +import TemplateEmbedPage from "./TemplateEmbedPage"; +import { TemplateLayout } from "components/TemplateLayout/TemplateLayout"; import { MockTemplate, MockTemplateVersionParameter1 as parameter1, MockTemplateVersionParameter2 as parameter2, -} from "testHelpers/entities" -import * as API from "api/api" -import userEvent from "@testing-library/user-event" -import { screen } from "@testing-library/react" +} from "testHelpers/entities"; +import * as API from "api/api"; +import userEvent from "@testing-library/user-event"; +import { screen } from "@testing-library/react"; test("Users can fill the parameters and copy the open in coder url", async () => { jest .spyOn(API, "getTemplateVersionRichParameters") - .mockResolvedValue([parameter1, parameter2]) + .mockResolvedValue([parameter1, parameter2]); renderWithAuth( @@ -26,27 +26,27 @@ test("Users can fill the parameters and copy the open in coder url", async () => route: `/templates/${MockTemplate.name}/embed`, path: "/templates/:template/embed", }, - ) - await waitForLoaderToBeRemoved() + ); + await waitForLoaderToBeRemoved(); - const user = userEvent.setup() + const user = userEvent.setup(); const firstParameterField = screen.getByLabelText( parameter1.display_name ?? parameter1.name, { exact: false }, - ) - await user.clear(firstParameterField) - await user.type(firstParameterField, "firstParameterValue") + ); + await user.clear(firstParameterField); + await user.type(firstParameterField, "firstParameterValue"); const secondParameterField = screen.getByLabelText( parameter2.display_name ?? parameter2.name, { exact: false }, - ) - await user.clear(secondParameterField) - await user.type(secondParameterField, "123456") + ); + await user.clear(secondParameterField); + await user.type(secondParameterField, "123456"); - jest.spyOn(window.navigator.clipboard, "writeText") - const copyButton = screen.getByRole("button", { name: /copy/i }) - await userEvent.click(copyButton) + jest.spyOn(window.navigator.clipboard, "writeText"); + const copyButton = screen.getByRole("button", { name: /copy/i }); + await userEvent.click(copyButton); expect(window.navigator.clipboard.writeText).toBeCalledWith( `[![Open in Coder](http://localhost/open-in-coder.svg)](http://localhost/templates/test-template/workspace?mode=manual¶m.first_parameter=firstParameterValue¶m.second_parameter=123456)`, - ) -}) + ); +}); diff --git a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx index c6b4c39c63..25ed4b5158 100644 --- a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx +++ b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage.tsx @@ -1,36 +1,36 @@ -import CheckOutlined from "@mui/icons-material/CheckOutlined" -import FileCopyOutlined from "@mui/icons-material/FileCopyOutlined" -import Box from "@mui/material/Box" -import Button from "@mui/material/Button" -import FormControlLabel from "@mui/material/FormControlLabel" -import Radio from "@mui/material/Radio" -import RadioGroup from "@mui/material/RadioGroup" -import { useQuery } from "@tanstack/react-query" -import { getTemplateVersionRichParameters } from "api/api" -import { Template, TemplateVersionParameter } from "api/typesGenerated" -import { FormSection, VerticalForm } from "components/Form/Form" -import { Loader } from "components/Loader/Loader" -import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout" +import CheckOutlined from "@mui/icons-material/CheckOutlined"; +import FileCopyOutlined from "@mui/icons-material/FileCopyOutlined"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Radio from "@mui/material/Radio"; +import RadioGroup from "@mui/material/RadioGroup"; +import { useQuery } from "@tanstack/react-query"; +import { getTemplateVersionRichParameters } from "api/api"; +import { Template, TemplateVersionParameter } from "api/typesGenerated"; +import { FormSection, VerticalForm } from "components/Form/Form"; +import { Loader } from "components/Loader/Loader"; +import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout"; import { ImmutableTemplateParametersSection, MutableTemplateParametersSection, TemplateParametersSectionProps, -} from "components/TemplateParameters/TemplateParameters" -import { useClipboard } from "hooks/useClipboard" -import { FC, useEffect, useState } from "react" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "utils/page" -import { getInitialRichParameterValues } from "utils/richParameters" -import { paramsUsedToCreateWorkspace } from "utils/workspace" +} from "components/TemplateParameters/TemplateParameters"; +import { useClipboard } from "hooks/useClipboard"; +import { FC, useEffect, useState } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { getInitialRichParameterValues } from "utils/richParameters"; +import { paramsUsedToCreateWorkspace } from "utils/workspace"; -type ButtonValues = Record +type ButtonValues = Record; const TemplateEmbedPage = () => { - const { template } = useTemplateLayoutContext() + const { template } = useTemplateLayoutContext(); const { data: templateParameters } = useQuery({ queryKey: ["template", template.id, "embed"], queryFn: () => getTemplateVersionRichParameters(template.active_version_id), - }) + }); return ( <> @@ -44,27 +44,27 @@ const TemplateEmbedPage = () => { )} /> - ) -} + ); +}; export const TemplateEmbedPageView: FC<{ - template: Template - templateParameters?: TemplateVersionParameter[] + template: Template; + templateParameters?: TemplateVersionParameter[]; }> = ({ template, templateParameters }) => { const [buttonValues, setButtonValues] = useState( undefined, - ) - const deploymentUrl = `${window.location.protocol}//${window.location.host}` - const createWorkspaceUrl = `${deploymentUrl}/templates/${template.name}/workspace` - const createWorkspaceParams = new URLSearchParams(buttonValues) - const buttonUrl = `${createWorkspaceUrl}?${createWorkspaceParams.toString()}` - const buttonMkdCode = `[![Open in Coder](${deploymentUrl}/open-in-coder.svg)](${buttonUrl})` - const clipboard = useClipboard(buttonMkdCode) + ); + const deploymentUrl = `${window.location.protocol}//${window.location.host}`; + const createWorkspaceUrl = `${deploymentUrl}/templates/${template.name}/workspace`; + const createWorkspaceParams = new URLSearchParams(buttonValues); + const buttonUrl = `${createWorkspaceUrl}?${createWorkspaceParams.toString()}`; + const buttonMkdCode = `[![Open in Coder](${deploymentUrl}/open-in-coder.svg)](${buttonUrl})`; + const clipboard = useClipboard(buttonMkdCode); const getInputProps: TemplateParametersSectionProps["getInputProps"] = ( parameter, ) => { if (!buttonValues) { - throw new Error("buttonValues is undefined") + throw new Error("buttonValues is undefined"); } return { value: buttonValues[`param.${parameter.name}`] ?? "", @@ -72,10 +72,10 @@ export const TemplateEmbedPageView: FC<{ setButtonValues((buttonValues) => ({ ...buttonValues, [`param.${parameter.name}`]: value, - })) + })); }, - } - } + }; + }; // template parameters is async so we need to initialize the values after it // is loaded @@ -83,15 +83,15 @@ export const TemplateEmbedPageView: FC<{ if (templateParameters && !buttonValues) { const buttonValues: ButtonValues = { mode: "manual", - } + }; for (const parameter of getInitialRichParameterValues( templateParameters, )) { - buttonValues[`param.${parameter.name}`] = parameter.value + buttonValues[`param.${parameter.name}`] = parameter.value; } - setButtonValues(buttonValues) + setButtonValues(buttonValues); } - }, [buttonValues, templateParameters]) + }, [buttonValues, templateParameters]); return ( <> @@ -114,7 +114,7 @@ export const TemplateEmbedPageView: FC<{ setButtonValues((buttonValues) => ({ ...buttonValues, mode: v, - })) + })); }} > )} - ) -} + ); +}; -export default TemplateEmbedPage +export default TemplateEmbedPage; diff --git a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPageView.stories.tsx b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPageView.stories.tsx index 39860cccb7..75f4438e3c 100644 --- a/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPageView.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPageView.stories.tsx @@ -1,13 +1,13 @@ -import type { Meta, StoryObj } from "@storybook/react" +import type { Meta, StoryObj } from "@storybook/react"; -import { TemplateEmbedPageView } from "./TemplateEmbedPage" +import { TemplateEmbedPageView } from "./TemplateEmbedPage"; import { MockTemplate, MockTemplateVersionParameter1, MockTemplateVersionParameter2, MockTemplateVersionParameter3, MockTemplateVersionParameter4, -} from "testHelpers/entities" +} from "testHelpers/entities"; const meta: Meta = { title: "pages/TemplateEmbedPageView", @@ -15,16 +15,16 @@ const meta: Meta = { args: { template: MockTemplate, }, -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; export const NoParameters: Story = { args: { templateParameters: [], }, -} +}; export const WithParameters: Story = { args: { @@ -35,4 +35,4 @@ export const WithParameters: Story = { MockTemplateVersionParameter4, ], }, -} +}; diff --git a/site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx b/site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx index 07953cfb60..e361f923a9 100644 --- a/site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx +++ b/site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx @@ -1,18 +1,18 @@ -import { useQuery } from "@tanstack/react-query" -import { getPreviousTemplateVersionByName } from "api/api" -import { TemplateVersion } from "api/typesGenerated" -import { Loader } from "components/Loader/Loader" -import { TemplateFiles } from "components/TemplateFiles/TemplateFiles" -import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout" -import { useOrganizationId } from "hooks/useOrganizationId" -import { useTab } from "hooks/useTab" -import { FC, useEffect } from "react" -import { Helmet } from "react-helmet-async" +import { useQuery } from "@tanstack/react-query"; +import { getPreviousTemplateVersionByName } from "api/api"; +import { TemplateVersion } from "api/typesGenerated"; +import { Loader } from "components/Loader/Loader"; +import { TemplateFiles } from "components/TemplateFiles/TemplateFiles"; +import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout"; +import { useOrganizationId } from "hooks/useOrganizationId"; +import { useTab } from "hooks/useTab"; +import { FC, useEffect } from "react"; +import { Helmet } from "react-helmet-async"; import { getTemplateVersionFiles, TemplateVersionFiles, -} from "utils/templateVersion" -import { getTemplatePageTitle } from "../utils" +} from "utils/templateVersion"; +import { getTemplatePageTitle } from "../utils"; const fetchTemplateFiles = async ( organizationId: string, @@ -23,18 +23,18 @@ const fetchTemplateFiles = async ( organizationId, templateName, activeVersion.name, - ) - const loadFilesPromises: ReturnType[] = [] - loadFilesPromises.push(getTemplateVersionFiles(activeVersion)) + ); + const loadFilesPromises: ReturnType[] = []; + loadFilesPromises.push(getTemplateVersionFiles(activeVersion)); if (previousVersion) { - loadFilesPromises.push(getTemplateVersionFiles(previousVersion)) + loadFilesPromises.push(getTemplateVersionFiles(previousVersion)); } - const [currentFiles, previousFiles] = await Promise.all(loadFilesPromises) + const [currentFiles, previousFiles] = await Promise.all(loadFilesPromises); return { currentFiles, previousFiles, - } -} + }; +}; const useTemplateFiles = ( organizationId: string, @@ -45,37 +45,37 @@ const useTemplateFiles = ( queryKey: ["templateFiles", templateName], queryFn: () => fetchTemplateFiles(organizationId, templateName, activeVersion), - }) + }); const useFileTab = (templateFiles: TemplateVersionFiles | undefined) => { // Tabs The default tab is the tab that has main.tf but until we loads the // files and check if main.tf exists we don't know which tab is the default // one so we just use empty string - const tab = useTab("file", "") - const isLoaded = tab.value !== "" + const tab = useTab("file", ""); + const isLoaded = tab.value !== ""; useEffect(() => { if (templateFiles && !isLoaded) { - const terraformFileIndex = Object.keys(templateFiles).indexOf("main.tf") + const terraformFileIndex = Object.keys(templateFiles).indexOf("main.tf"); // If main.tf exists use the index if not just use the first tab - tab.set(terraformFileIndex !== -1 ? terraformFileIndex.toString() : "0") + tab.set(terraformFileIndex !== -1 ? terraformFileIndex.toString() : "0"); } - }, [isLoaded, tab, templateFiles]) + }, [isLoaded, tab, templateFiles]); return { ...tab, isLoaded, - } -} + }; +}; const TemplateFilesPage: FC = () => { - const { template, activeVersion } = useTemplateLayoutContext() - const orgId = useOrganizationId() + const { template, activeVersion } = useTemplateLayoutContext(); + const orgId = useOrganizationId(); const { data: templateFiles } = useTemplateFiles( orgId, template.name, activeVersion, - ) - const tab = useFileTab(templateFiles?.currentFiles) + ); + const tab = useFileTab(templateFiles?.currentFiles); return ( <> @@ -93,7 +93,7 @@ const TemplateFilesPage: FC = () => { )} - ) -} + ); +}; -export default TemplateFilesPage +export default TemplateFilesPage; diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx index 60774242da..89c5fdc47c 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/DateRange.tsx @@ -1,55 +1,57 @@ -import Box from "@mui/material/Box" -import { styled } from "@mui/material/styles" -import { ComponentProps, useRef, useState } from "react" -import "react-date-range/dist/styles.css" -import "react-date-range/dist/theme/default.css" -import Button from "@mui/material/Button" -import ArrowRightAltOutlined from "@mui/icons-material/ArrowRightAltOutlined" -import Popover from "@mui/material/Popover" -import { DateRangePicker, createStaticRanges } from "react-date-range" -import { format, subDays } from "date-fns" +import Box from "@mui/material/Box"; +import { styled } from "@mui/material/styles"; +import { ComponentProps, useRef, useState } from "react"; +import "react-date-range/dist/styles.css"; +import "react-date-range/dist/theme/default.css"; +import Button from "@mui/material/Button"; +import ArrowRightAltOutlined from "@mui/icons-material/ArrowRightAltOutlined"; +import Popover from "@mui/material/Popover"; +import { DateRangePicker, createStaticRanges } from "react-date-range"; +import { format, subDays } from "date-fns"; // The type definition from @types is wrong declare module "react-date-range" { export function createStaticRanges( ranges: Omit[], - ): StaticRange[] + ): StaticRange[]; } export type DateRangeValue = { - startDate: Date - endDate: Date -} + startDate: Date; + endDate: Date; +}; -type RangesState = NonNullable["ranges"]> +type RangesState = NonNullable< + ComponentProps["ranges"] +>; export const DateRange = ({ value, onChange, }: { - value: DateRangeValue - onChange: (value: DateRangeValue) => void + value: DateRangeValue; + onChange: (value: DateRangeValue) => void; }) => { - const selectionStatusRef = useRef<"idle" | "selecting">("idle") - const anchorRef = useRef(null) - const [isOpen, setIsOpen] = useState(false) + const selectionStatusRef = useRef<"idle" | "selecting">("idle"); + const anchorRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); const [ranges, setRanges] = useState([ { ...value, key: "selection", }, - ]) + ]); const currentRange = { startDate: ranges[0].startDate as Date, endDate: ranges[0].endDate as Date, - } + }; const handleClose = () => { onChange({ startDate: currentRange.startDate, endDate: currentRange.endDate, - }) - setIsOpen(false) - } + }); + setIsOpen(false); + }; return ( <> @@ -75,24 +77,24 @@ export const DateRange = ({ { - const range = item.selection - setRanges([range]) + const range = item.selection; + setRanges([range]); // When it is the first selection, we don't want to close the popover // We have to do that ourselves because the library doesn't provide a way to do it if (selectionStatusRef.current === "idle") { - selectionStatusRef.current = "selecting" - return + selectionStatusRef.current = "selecting"; + return; } - selectionStatusRef.current = "idle" - const startDate = range.startDate as Date - const endDate = range.endDate as Date + selectionStatusRef.current = "idle"; + const startDate = range.startDate as Date; + const endDate = range.endDate as Date; onChange({ startDate, endDate, - }) - setIsOpen(false) + }); + setIsOpen(false); }} moveRangeOnFirstSelection={false} months={2} @@ -139,8 +141,8 @@ export const DateRange = ({ /> - ) -} + ); +}; const DateRangePickerWrapper: typeof Box = styled(Box)(({ theme }) => ({ "& .rdrDefinedRangesWrapper": { @@ -231,4 +233,4 @@ const DateRangePickerWrapper: typeof Box = styled(Box)(({ theme }) => ({ color: theme.palette.text.disabled, }, }, -})) +})); diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx index 4fb65c0b2b..889db073a6 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx @@ -1,20 +1,20 @@ -import type { Meta, StoryObj } from "@storybook/react" -import { TemplateInsightsPageView } from "./TemplateInsightsPage" +import type { Meta, StoryObj } from "@storybook/react"; +import { TemplateInsightsPageView } from "./TemplateInsightsPage"; const meta: Meta = { title: "pages/TemplateInsightsPageView", component: TemplateInsightsPageView, -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; export const Loading: Story = { args: { templateInsights: undefined, userLatency: undefined, }, -} +}; export const Empty: Story = { args: { @@ -38,7 +38,7 @@ export const Empty: Story = { }, }, }, -} +}; export const Loaded: Story = { args: { @@ -658,4 +658,4 @@ export const Loaded: Story = { }, }, }, -} +}; diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index b79ba05f2f..ee4bccf932 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -1,49 +1,49 @@ -import LinearProgress from "@mui/material/LinearProgress" -import Box from "@mui/material/Box" -import { styled, useTheme } from "@mui/material/styles" -import { BoxProps } from "@mui/system" -import { useQuery } from "@tanstack/react-query" -import { getInsightsTemplate, getInsightsUserLatency } from "api/api" -import { DAUChart, DAUTitle } from "components/DAUChart/DAUChart" -import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout" +import LinearProgress from "@mui/material/LinearProgress"; +import Box from "@mui/material/Box"; +import { styled, useTheme } from "@mui/material/styles"; +import { BoxProps } from "@mui/system"; +import { useQuery } from "@tanstack/react-query"; +import { getInsightsTemplate, getInsightsUserLatency } from "api/api"; +import { DAUChart, DAUTitle } from "components/DAUChart/DAUChart"; +import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout"; import { HelpTooltip, HelpTooltipTitle, HelpTooltipText, -} from "components/HelpTooltip/HelpTooltip" -import { UserAvatar } from "components/UserAvatar/UserAvatar" -import { getLatencyColor } from "utils/latency" -import chroma from "chroma-js" -import { colors } from "theme/colors" -import { Helmet } from "react-helmet-async" -import { getTemplatePageTitle } from "../utils" -import { Loader } from "components/Loader/Loader" +} from "components/HelpTooltip/HelpTooltip"; +import { UserAvatar } from "components/UserAvatar/UserAvatar"; +import { getLatencyColor } from "utils/latency"; +import chroma from "chroma-js"; +import { colors } from "theme/colors"; +import { Helmet } from "react-helmet-async"; +import { getTemplatePageTitle } from "../utils"; +import { Loader } from "components/Loader/Loader"; import { DAUsResponse, TemplateInsightsResponse, TemplateParameterUsage, TemplateParameterValue, UserLatencyInsightsResponse, -} from "api/typesGenerated" -import { ComponentProps, ReactNode, useState } from "react" -import { subDays, isToday } from "date-fns" -import "react-date-range/dist/styles.css" -import "react-date-range/dist/theme/default.css" -import { DateRange, DateRangeValue } from "./DateRange" -import Link from "@mui/material/Link" -import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined" -import CancelOutlined from "@mui/icons-material/CancelOutlined" -import { getDateRangeFilter } from "./utils" -import Tooltip from "@mui/material/Tooltip" -import LinkOutlined from "@mui/icons-material/LinkOutlined" +} from "api/typesGenerated"; +import { ComponentProps, ReactNode, useState } from "react"; +import { subDays, isToday } from "date-fns"; +import "react-date-range/dist/styles.css"; +import "react-date-range/dist/theme/default.css"; +import { DateRange, DateRangeValue } from "./DateRange"; +import Link from "@mui/material/Link"; +import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined"; +import CancelOutlined from "@mui/icons-material/CancelOutlined"; +import { getDateRangeFilter } from "./utils"; +import Tooltip from "@mui/material/Tooltip"; +import LinkOutlined from "@mui/icons-material/LinkOutlined"; export default function TemplateInsightsPage() { - const now = new Date() + const now = new Date(); const [dateRangeValue, setDateRangeValue] = useState({ startDate: subDays(now, 6), endDate: now, - }) - const { template } = useTemplateLayoutContext() + }); + const { template } = useTemplateLayoutContext(); const insightsFilter = { template_ids: template.id, ...getDateRangeFilter({ @@ -52,15 +52,15 @@ export default function TemplateInsightsPage() { now, isToday, }), - } + }; const { data: templateInsights } = useQuery({ queryKey: ["templates", template.id, "usage", insightsFilter], queryFn: () => getInsightsTemplate(insightsFilter), - }) + }); const { data: userLatency } = useQuery({ queryKey: ["templates", template.id, "user-latency", insightsFilter], queryFn: () => getInsightsUserLatency(insightsFilter), - }) + }); return ( <> @@ -75,7 +75,7 @@ export default function TemplateInsightsPage() { userLatency={userLatency} /> - ) + ); } export const TemplateInsightsPageView = ({ @@ -83,9 +83,9 @@ export const TemplateInsightsPageView = ({ userLatency, dateRange, }: { - templateInsights: TemplateInsightsResponse | undefined - userLatency: UserLatencyInsightsResponse | undefined - dateRange: ReactNode + templateInsights: TemplateInsightsResponse | undefined; + userLatency: UserLatencyInsightsResponse | undefined; + dateRange: ReactNode; }) => { return ( <> @@ -113,14 +113,14 @@ export const TemplateInsightsPageView = ({ /> - ) -} + ); +}; const DailyUsersPanel = ({ data, ...panelProps }: PanelProps & { - data: TemplateInsightsResponse["interval_reports"] | undefined + data: TemplateInsightsResponse["interval_reports"] | undefined; }) => { return ( @@ -135,15 +135,15 @@ const DailyUsersPanel = ({ {data && data.length > 0 && } - ) -} + ); +}; const UserLatencyPanel = ({ data, ...panelProps }: PanelProps & { data: UserLatencyInsightsResponse | undefined }) => { - const theme = useTheme() - const users = data?.report.users + const theme = useTheme(); + const users = data?.report.users; return ( @@ -195,24 +195,24 @@ const UserLatencyPanel = ({ ))} - ) -} + ); +}; const TemplateUsagePanel = ({ data, ...panelProps }: PanelProps & { - data: TemplateInsightsResponse["report"]["apps_usage"] | undefined + data: TemplateInsightsResponse["report"]["apps_usage"] | undefined; }) => { - const validUsage = data?.filter((u) => u.seconds > 0) + const validUsage = data?.filter((u) => u.seconds > 0); const totalInSeconds = - validUsage?.reduce((total, usage) => total + usage.seconds, 0) ?? 1 + validUsage?.reduce((total, usage) => total + usage.seconds, 0) ?? 1; const usageColors = chroma .scale([colors.green[8], colors.blue[8]]) .mode("lch") - .colors(validUsage?.length ?? 0) + .colors(validUsage?.length ?? 0); // The API returns a row for each app, even if the user didn't use it. - const hasDataAvailable = validUsage && validUsage.length > 0 + const hasDataAvailable = validUsage && validUsage.length > 0; return ( @@ -226,7 +226,7 @@ const TemplateUsagePanel = ({ {validUsage .sort((a, b) => b.seconds - a.seconds) .map((usage, i) => { - const percentage = (usage.seconds / totalInSeconds) * 100 + const percentage = (usage.seconds / totalInSeconds) * 100; return ( - ) + ); })} )} - ) -} + ); +}; const TemplateParametersUsagePanel = ({ data, ...panelProps }: PanelProps & { - data: TemplateInsightsResponse["report"]["parameters_usage"] | undefined + data: TemplateInsightsResponse["report"]["parameters_usage"] | undefined; }) => { return ( @@ -309,7 +309,7 @@ const TemplateParametersUsagePanel = ({ const label = parameter.display_name !== "" ? parameter.display_name - : parameter.name + : parameter.name; return ( - ) + ); })} - ) -} + ); +}; const ParameterUsageRow = styled(Box)(({ theme }) => ({ display: "flex", @@ -384,19 +384,19 @@ const ParameterUsageRow = styled(Box)(({ theme }) => ({ justifyContent: "space-between", padding: theme.spacing(0.5, 0), gap: theme.spacing(5), -})) +})); const ParameterUsageLabel = ({ usage, parameter, }: { - usage: TemplateParameterValue - parameter: TemplateParameterUsage + usage: TemplateParameterValue; + parameter: TemplateParameterUsage; }) => { if (parameter.options) { - const option = parameter.options.find((o) => o.value === usage.value)! - const icon = option.icon - const label = option.name + const option = parameter.options.find((o) => o.value === usage.value)!; + const icon = option.icon; + const label = option.name; return ( - ) + ); } if (usage.value.startsWith("http")) { @@ -446,11 +446,11 @@ const ParameterUsageLabel = ({ }} /> - ) + ); } if (parameter.type === "list(string)") { - const values = JSON.parse(usage.value) as string[] + const values = JSON.parse(usage.value) as string[]; return ( {values.map((v, i) => { @@ -466,10 +466,10 @@ const ParameterUsageLabel = ({ > {v} - ) + ); })} - ) + ); } if (parameter.type === "bool") { @@ -505,11 +505,11 @@ const ParameterUsageLabel = ({ )} - ) + ); } - return {usage.value} -} + return {usage.value}; +}; const Panel = styled(Box)(({ theme }) => ({ borderRadius: theme.shape.borderRadius, @@ -517,23 +517,23 @@ const Panel = styled(Box)(({ theme }) => ({ backgroundColor: theme.palette.background.paper, display: "flex", flexDirection: "column", -})) +})); -type PanelProps = ComponentProps +type PanelProps = ComponentProps; const PanelHeader = styled(Box)(({ theme }) => ({ padding: theme.spacing(2.5, 3, 3), -})) +})); const PanelTitle = styled(Box)(() => ({ fontSize: 14, fontWeight: 500, -})) +})); const PanelContent = styled(Box)(({ theme }) => ({ padding: theme.spacing(0, 3, 3), flex: 1, -})) +})); const NoDataAvailable = (props: BoxProps) => { return ( @@ -552,8 +552,8 @@ const NoDataAvailable = (props: BoxProps) => { > No data available - ) -} + ); +}; const TextValue = ({ children }: { children: ReactNode }) => { return ( @@ -580,8 +580,8 @@ const TextValue = ({ children }: { children: ReactNode }) => { " - ) -} + ); +}; function mapToDAUsResponse( data: TemplateInsightsResponse["interval_reports"], @@ -592,24 +592,24 @@ function mapToDAUsResponse( return { amount: d.active_users, date: d.start_time, - } + }; }), - } + }; } function formatTime(seconds: number): string { if (seconds < 60) { - return seconds + " seconds" + return seconds + " seconds"; } else if (seconds >= 60 && seconds < 3600) { - const minutes = Math.floor(seconds / 60) - return minutes + " minutes" + const minutes = Math.floor(seconds / 60); + return minutes + " minutes"; } else { - const hours = seconds / 3600 - const minutes = Math.floor(seconds % 3600) + const hours = seconds / 3600; + const minutes = Math.floor(seconds % 3600); if (minutes === 0) { - return hours.toFixed(0) + " hours" + return hours.toFixed(0) + " hours"; } - return hours.toFixed(1) + " hours" + return hours.toFixed(1) + " hours"; } } diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/utils.test.ts b/site/src/pages/TemplatePage/TemplateInsightsPage/utils.test.ts index e029f69fb9..93761f9f48 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/utils.test.ts +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/utils.test.ts @@ -1,36 +1,36 @@ -import { getDateRangeFilter } from "./utils" +import { getDateRangeFilter } from "./utils"; describe("getDateRangeFilter", () => { it("returns the start time at the start of the day", () => { - const date = new Date("2020-01-01T12:00:00.000Z") + const date = new Date("2020-01-01T12:00:00.000Z"); const { start_time } = getDateRangeFilter({ startDate: date, endDate: date, now: date, isToday: () => false, - }) - expect(start_time).toEqual("2020-01-01T00:00:00+00:00") - }) + }); + expect(start_time).toEqual("2020-01-01T00:00:00+00:00"); + }); it("returns the end time at the start of the next day", () => { - const date = new Date("2020-01-01T12:00:00.000Z") + const date = new Date("2020-01-01T12:00:00.000Z"); const { end_time } = getDateRangeFilter({ startDate: date, endDate: date, now: date, isToday: () => false, - }) - expect(end_time).toEqual("2020-01-02T00:00:00+00:00") - }) + }); + expect(end_time).toEqual("2020-01-02T00:00:00+00:00"); + }); it("returns the end time at the start of the next hour if the end date is today", () => { - const date = new Date("2020-01-01T12:00:00.000Z") + const date = new Date("2020-01-01T12:00:00.000Z"); const { end_time } = getDateRangeFilter({ startDate: date, endDate: date, now: date, isToday: () => true, - }) - expect(end_time).toEqual("2020-01-01T13:00:00+00:00") - }) -}) + }); + expect(end_time).toEqual("2020-01-01T13:00:00+00:00"); + }); +}); diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/utils.ts b/site/src/pages/TemplatePage/TemplateInsightsPage/utils.ts index bfc751efe4..13c3991c78 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/utils.ts +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/utils.ts @@ -1,4 +1,4 @@ -import { addDays, addHours, format, startOfDay, startOfHour } from "date-fns" +import { addDays, addHours, format, startOfDay, startOfHour } from "date-fns"; export function getDateRangeFilter({ startDate, @@ -6,10 +6,10 @@ export function getDateRangeFilter({ now, isToday, }: { - startDate: Date - endDate: Date - now: Date - isToday: (date: Date) => boolean + startDate: Date; + endDate: Date; + now: Date; + isToday: (date: Date) => boolean; }) { return { start_time: toISOLocal(startOfDay(startDate)), @@ -18,9 +18,9 @@ export function getDateRangeFilter({ ? startOfHour(addHours(now, 1)) : startOfDay(addDays(endDate, 1)), ), - } + }; } function toISOLocal(d: Date) { - return format(d, "yyyy-MM-dd'T'HH:mm:ssxxx") + return format(d, "yyyy-MM-dd'T'HH:mm:ssxxx"); } diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateStats.stories.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateStats.stories.tsx index 64773e5e6d..cba94c3107 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateStats.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateStats.stories.tsx @@ -1,57 +1,57 @@ -import { Story } from "@storybook/react" -import { MockTemplate, MockTemplateVersion } from "testHelpers/entities" -import { TemplateStats, TemplateStatsProps } from "./TemplateStats" +import { Story } from "@storybook/react"; +import { MockTemplate, MockTemplateVersion } from "testHelpers/entities"; +import { TemplateStats, TemplateStatsProps } from "./TemplateStats"; export default { title: "components/TemplateStats", component: TemplateStats, -} +}; const Template: Story = (args) => ( -) +); -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { template: MockTemplate, activeVersion: MockTemplateVersion, -} +}; -export const UsedByMany = Template.bind({}) +export const UsedByMany = Template.bind({}); UsedByMany.args = { template: { ...MockTemplate, active_user_count: 15, }, activeVersion: MockTemplateVersion, -} +}; -export const ActiveUsersNotLoaded = Template.bind({}) +export const ActiveUsersNotLoaded = Template.bind({}); ActiveUsersNotLoaded.args = { template: { ...MockTemplate, active_user_count: -1, }, activeVersion: MockTemplateVersion, -} +}; -export const LongTemplateVersion = Template.bind({}) +export const LongTemplateVersion = Template.bind({}); LongTemplateVersion.args = { template: MockTemplate, activeVersion: { ...MockTemplateVersion, name: "thisisareallyreallylongnamefortesting", }, -} +}; LongTemplateVersion.parameters = { chromatic: { viewports: [960] }, -} +}; -export const SmallViewport = Template.bind({}) +export const SmallViewport = Template.bind({}); SmallViewport.args = { template: MockTemplate, activeVersion: MockTemplateVersion, -} +}; SmallViewport.parameters = { chromatic: { viewports: [600] }, -} +}; diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateStats.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateStats.tsx index c40e36c04b..a846698cdc 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateStats.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateStats.tsx @@ -1,12 +1,12 @@ -import { Stats, StatsItem } from "components/Stats/Stats" -import { FC } from "react" -import { Link } from "react-router-dom" -import { createDayString } from "utils/createDayString" +import { Stats, StatsItem } from "components/Stats/Stats"; +import { FC } from "react"; +import { Link } from "react-router-dom"; +import { createDayString } from "utils/createDayString"; import { formatTemplateBuildTime, formatTemplateActiveDevelopers, -} from "utils/templates" -import { Template, TemplateVersion } from "api/typesGenerated" +} from "utils/templates"; +import { Template, TemplateVersion } from "api/typesGenerated"; const Language = { usedByLabel: "Used by", @@ -16,11 +16,11 @@ const Language = { developerPlural: "developers", developerSingular: "developer", createdByLabel: "Created by", -} +}; export interface TemplateStatsProps { - template: Template - activeVersion: TemplateVersion + template: Template; + activeVersion: TemplateVersion; } export const TemplateStats: FC = ({ @@ -61,5 +61,5 @@ export const TemplateStats: FC = ({ value={template.created_by_name} /> - ) -} + ); +}; diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx index de2e2c9747..4ae508eda7 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx @@ -1,22 +1,22 @@ -import { screen } from "@testing-library/react" -import { TemplateLayout } from "components/TemplateLayout/TemplateLayout" -import { rest } from "msw" -import { ResizeObserver } from "resize-observer" +import { screen } from "@testing-library/react"; +import { TemplateLayout } from "components/TemplateLayout/TemplateLayout"; +import { rest } from "msw"; +import { ResizeObserver } from "resize-observer"; import { MockTemplate, MockTemplateVersion, MockMemberPermissions, -} from "testHelpers/entities" -import { renderWithAuth } from "testHelpers/renderHelpers" -import { server } from "testHelpers/server" -import * as CreateDayString from "utils/createDayString" -import { TemplateSummaryPage } from "./TemplateSummaryPage" +} from "testHelpers/entities"; +import { renderWithAuth } from "testHelpers/renderHelpers"; +import { server } from "testHelpers/server"; +import * as CreateDayString from "utils/createDayString"; +import { TemplateSummaryPage } from "./TemplateSummaryPage"; -jest.mock("remark-gfm", () => jest.fn()) +jest.mock("remark-gfm", () => jest.fn()); Object.defineProperty(window, "ResizeObserver", { value: ResizeObserver, -}) +}); const renderPage = () => renderWithAuth( @@ -27,27 +27,27 @@ const renderPage = () => route: `/templates/${MockTemplate.id}`, path: "/templates/:template", }, - ) + ); describe("TemplateSummaryPage", () => { it("shows the template name and resources", async () => { // Mocking the dayjs module within the createDayString file - const mock = jest.spyOn(CreateDayString, "createDayString") - mock.mockImplementation(() => "a minute ago") + const mock = jest.spyOn(CreateDayString, "createDayString"); + mock.mockImplementation(() => "a minute ago"); - renderPage() - await screen.findByText(MockTemplate.display_name) - screen.queryAllByText(`${MockTemplateVersion.name}`).length - }) + renderPage(); + await screen.findByText(MockTemplate.display_name); + screen.queryAllByText(`${MockTemplateVersion.name}`).length; + }); it("does not allow a member to delete a template", () => { // get member-level permissions server.use( rest.post("/api/v2/authcheck", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MockMemberPermissions)) + return res(ctx.status(200), ctx.json(MockMemberPermissions)); }), - ) - renderPage() - const dropdownButton = screen.queryByLabelText("open-dropdown") - expect(dropdownButton).toBe(null) - }) -}) + ); + renderPage(); + const dropdownButton = screen.queryByLabelText("open-dropdown"); + expect(dropdownButton).toBe(null); + }); +}); diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx index e169c42591..180fd00078 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx @@ -1,17 +1,17 @@ -import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { getTemplatePageTitle } from "../utils" -import { TemplateSummaryPageView } from "./TemplateSummaryPageView" -import { useQuery } from "@tanstack/react-query" -import { getTemplateVersionResources } from "api/api" +import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { getTemplatePageTitle } from "../utils"; +import { TemplateSummaryPageView } from "./TemplateSummaryPageView"; +import { useQuery } from "@tanstack/react-query"; +import { getTemplateVersionResources } from "api/api"; export const TemplateSummaryPage: FC = () => { - const { template, activeVersion } = useTemplateLayoutContext() + const { template, activeVersion } = useTemplateLayoutContext(); const { data: resources } = useQuery({ queryKey: ["templates", template.id, "resources"], queryFn: () => getTemplateVersionResources(activeVersion.id), - }) + }); return ( <> @@ -24,7 +24,7 @@ export const TemplateSummaryPage: FC = () => { activeVersion={activeVersion} /> - ) -} + ); +}; -export default TemplateSummaryPage +export default TemplateSummaryPage; diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx index 516d703621..46d2e8adc7 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx @@ -1,20 +1,20 @@ -import { Meta, StoryObj } from "@storybook/react" +import { Meta, StoryObj } from "@storybook/react"; import { MockTemplate, MockTemplateVersion, MockTemplateVersion3, MockWorkspaceResource, MockWorkspaceResource2, -} from "testHelpers/entities" -import { TemplateSummaryPageView } from "./TemplateSummaryPageView" +} from "testHelpers/entities"; +import { TemplateSummaryPageView } from "./TemplateSummaryPageView"; const meta: Meta = { title: "pages/TemplateSummaryPageView", component: TemplateSummaryPageView, -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; export const Example: Story = { args: { @@ -22,7 +22,7 @@ export const Example: Story = { activeVersion: MockTemplateVersion, resources: [MockWorkspaceResource, MockWorkspaceResource2], }, -} +}; export const NoIcon: Story = { args: { @@ -30,7 +30,7 @@ export const NoIcon: Story = { activeVersion: MockTemplateVersion, resources: [MockWorkspaceResource, MockWorkspaceResource2], }, -} +}; export const SmallViewport: Story = { args: { @@ -51,11 +51,11 @@ export const SmallViewport: Story = { }, resources: [MockWorkspaceResource, MockWorkspaceResource2], }, -} +}; SmallViewport.parameters = { chromatic: { viewports: [600] }, -} +}; export const WithDeprecatedParameters: Story = { args: { @@ -63,4 +63,4 @@ export const WithDeprecatedParameters: Story = { activeVersion: MockTemplateVersion3, resources: [MockWorkspaceResource, MockWorkspaceResource2], }, -} +}; diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx index 8c660d91a4..1ebed63451 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx @@ -2,19 +2,19 @@ import { Template, TemplateVersion, WorkspaceResource, -} from "api/typesGenerated" -import { Loader } from "components/Loader/Loader" -import { Stack } from "components/Stack/Stack" -import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable" -import { TemplateStats } from "./TemplateStats" -import { FC, useEffect } from "react" -import { useLocation, useNavigate } from "react-router-dom" -import { TemplateVersionWarnings } from "components/TemplateVersionWarnings/TemplateVersionWarnings" +} from "api/typesGenerated"; +import { Loader } from "components/Loader/Loader"; +import { Stack } from "components/Stack/Stack"; +import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable"; +import { TemplateStats } from "./TemplateStats"; +import { FC, useEffect } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { TemplateVersionWarnings } from "components/TemplateVersionWarnings/TemplateVersionWarnings"; export interface TemplateSummaryPageViewProps { - resources?: WorkspaceResource[] - template: Template - activeVersion: TemplateVersion + resources?: WorkspaceResource[]; + template: Template; + activeVersion: TemplateVersion; } export const TemplateSummaryPageView: FC = ({ @@ -22,26 +22,26 @@ export const TemplateSummaryPageView: FC = ({ template, activeVersion, }) => { - const navigate = useNavigate() - const location = useLocation() + const navigate = useNavigate(); + const location = useLocation(); useEffect(() => { if (location.hash === "#readme") { // We moved the readme to the docs page, but we known that some users // have bookmarked the readme or linked it elsewhere. Redirect them to the docs page. - navigate(`/templates/${template.name}/docs`, { replace: true }) + navigate(`/templates/${template.name}/docs`, { replace: true }); } - }, [template, navigate, location]) + }, [template, navigate, location]); if (!resources) { - return + return ; } const getStartedResources = (resources: WorkspaceResource[]) => { return resources.filter( (resource) => resource.workspace_transition === "start", - ) - } + ); + }; return ( @@ -49,5 +49,5 @@ export const TemplateSummaryPageView: FC = ({ - ) -} + ); +}; diff --git a/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx index e4b68b0138..b34690b720 100644 --- a/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx @@ -1,42 +1,42 @@ -import { useMutation, useQuery } from "@tanstack/react-query" -import { getTemplateVersions, updateActiveTemplateVersion } from "api/api" -import { getErrorMessage } from "api/errors" -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" -import { displayError, displaySuccess } from "components/GlobalSnackbar/utils" -import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout" -import { VersionsTable } from "./VersionsTable" -import { useState } from "react" -import { Helmet } from "react-helmet-async" -import { getTemplatePageTitle } from "../utils" +import { useMutation, useQuery } from "@tanstack/react-query"; +import { getTemplateVersions, updateActiveTemplateVersion } from "api/api"; +import { getErrorMessage } from "api/errors"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; +import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout"; +import { VersionsTable } from "./VersionsTable"; +import { useState } from "react"; +import { Helmet } from "react-helmet-async"; +import { getTemplatePageTitle } from "../utils"; const TemplateVersionsPage = () => { - const { template, permissions } = useTemplateLayoutContext() + const { template, permissions } = useTemplateLayoutContext(); const { data } = useQuery({ queryKey: ["template", "versions", template.id], queryFn: () => getTemplateVersions(template.id), - }) + }); // We use this to update the active version in the UI without having to refetch the template const [latestActiveVersion, setLatestActiveVersion] = useState( template.active_version_id, - ) + ); const { mutate: promoteVersion, isLoading: isPromoting } = useMutation({ mutationFn: (templateVersionId: string) => { return updateActiveTemplateVersion(template.id, { id: templateVersionId, - }) + }); }, onSuccess: async () => { - setLatestActiveVersion(selectedVersionIdToPromote as string) - setSelectedVersionIdToPromote(undefined) - displaySuccess("Version promoted successfully") + setLatestActiveVersion(selectedVersionIdToPromote as string); + setSelectedVersionIdToPromote(undefined); + displaySuccess("Version promoted successfully"); }, onError: (error) => { - displayError(getErrorMessage(error, "Failed to promote version")) + displayError(getErrorMessage(error, "Failed to promote version")); }, - }) + }); const [selectedVersionIdToPromote, setSelectedVersionIdToPromote] = useState< string | undefined - >() + >(); return ( <> @@ -57,7 +57,7 @@ const TemplateVersionsPage = () => { hideCancel={false} open={selectedVersionIdToPromote !== undefined} onConfirm={() => { - promoteVersion(selectedVersionIdToPromote as string) + promoteVersion(selectedVersionIdToPromote as string); }} onClose={() => setSelectedVersionIdToPromote(undefined)} title="Promote version" @@ -66,7 +66,7 @@ const TemplateVersionsPage = () => { description="Are you sure you want to promote this version? Workspaces will be prompted to “Update” to this version once promoted." /> - ) -} + ); +}; -export default TemplateVersionsPage +export default TemplateVersionsPage; diff --git a/site/src/pages/TemplatePage/TemplateVersionsPage/VersionRow.tsx b/site/src/pages/TemplatePage/TemplateVersionsPage/VersionRow.tsx index 6329f2bc0b..56be90d913 100644 --- a/site/src/pages/TemplatePage/TemplateVersionsPage/VersionRow.tsx +++ b/site/src/pages/TemplatePage/TemplateVersionsPage/VersionRow.tsx @@ -1,22 +1,22 @@ -import Button from "@mui/material/Button" -import { makeStyles } from "@mui/styles" -import TableCell from "@mui/material/TableCell" -import { TemplateVersion } from "api/typesGenerated" -import { Pill } from "components/Pill/Pill" -import { Stack } from "components/Stack/Stack" -import { TimelineEntry } from "components/Timeline/TimelineEntry" -import { UserAvatar } from "components/UserAvatar/UserAvatar" -import { useClickableTableRow } from "hooks/useClickableTableRow" -import { useTranslation } from "react-i18next" -import { useNavigate } from "react-router-dom" -import { colors } from "theme/colors" -import { combineClasses } from "utils/combineClasses" +import Button from "@mui/material/Button"; +import { makeStyles } from "@mui/styles"; +import TableCell from "@mui/material/TableCell"; +import { TemplateVersion } from "api/typesGenerated"; +import { Pill } from "components/Pill/Pill"; +import { Stack } from "components/Stack/Stack"; +import { TimelineEntry } from "components/Timeline/TimelineEntry"; +import { UserAvatar } from "components/UserAvatar/UserAvatar"; +import { useClickableTableRow } from "hooks/useClickableTableRow"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { colors } from "theme/colors"; +import { combineClasses } from "utils/combineClasses"; export interface VersionRowProps { - version: TemplateVersion - isActive: boolean - isLatest: boolean - onPromoteClick?: (templateVersionId: string) => void + version: TemplateVersion; + isActive: boolean; + isLatest: boolean; + onPromoteClick?: (templateVersionId: string) => void; } export const VersionRow: React.FC = ({ @@ -25,12 +25,12 @@ export const VersionRow: React.FC = ({ isLatest, onPromoteClick, }) => { - const styles = useStyles() - const { t } = useTranslation("templatePage") - const navigate = useNavigate() + const styles = useStyles(); + const { t } = useTranslation("templatePage"); + const navigate = useNavigate(); const clickableProps = useClickableTableRow(() => { - navigate(version.name) - }) + navigate(version.name); + }); return ( = ({ className={styles.promoteButton} disabled={isActive} onClick={(e) => { - e.preventDefault() - e.stopPropagation() - onPromoteClick(version.id) + e.preventDefault(); + e.stopPropagation(); + onPromoteClick(version.id); }} > Promote @@ -91,8 +91,8 @@ export const VersionRow: React.FC = ({ - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ row: { @@ -133,4 +133,4 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.secondary, fontSize: 12, }, -})) +})); diff --git a/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.stories.tsx b/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.stories.tsx index 7307efed28..348d4ec8e0 100644 --- a/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.stories.tsx @@ -1,18 +1,18 @@ -import { action } from "@storybook/addon-actions" -import { ComponentMeta, Story } from "@storybook/react" -import { MockTemplateVersion } from "testHelpers/entities" -import { VersionsTable, VersionsTableProps } from "./VersionsTable" +import { action } from "@storybook/addon-actions"; +import { ComponentMeta, Story } from "@storybook/react"; +import { MockTemplateVersion } from "testHelpers/entities"; +import { VersionsTable, VersionsTableProps } from "./VersionsTable"; export default { title: "components/VersionsTable", component: VersionsTable, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) +); -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { activeVersionId: MockTemplateVersion.id, versions: [ @@ -25,9 +25,9 @@ Example.args = { MockTemplateVersion, ], onPromoteClick: undefined, -} +}; -export const CanPromote = Template.bind({}) +export const CanPromote = Template.bind({}); CanPromote.args = { activeVersionId: MockTemplateVersion.id, onPromoteClick: action("onPromoteClick"), @@ -40,9 +40,9 @@ CanPromote.args = { }, MockTemplateVersion, ], -} +}; -export const Empty = Template.bind({}) +export const Empty = Template.bind({}); Empty.args = { versions: [], -} +}; diff --git a/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.tsx b/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.tsx index db525f7fdc..c48982acb1 100644 --- a/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.tsx +++ b/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.tsx @@ -1,27 +1,27 @@ -import Box from "@mui/material/Box" -import Table from "@mui/material/Table" -import TableBody from "@mui/material/TableBody" -import TableCell from "@mui/material/TableCell" -import TableContainer from "@mui/material/TableContainer" -import TableRow from "@mui/material/TableRow" -import { Timeline } from "components/Timeline/Timeline" -import { FC } from "react" -import * as TypesGen from "api/typesGenerated" -import { EmptyState } from "components/EmptyState/EmptyState" -import { TableLoader } from "components/TableLoader/TableLoader" -import { VersionRow } from "./VersionRow" +import Box from "@mui/material/Box"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableRow from "@mui/material/TableRow"; +import { Timeline } from "components/Timeline/Timeline"; +import { FC } from "react"; +import * as TypesGen from "api/typesGenerated"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { TableLoader } from "components/TableLoader/TableLoader"; +import { VersionRow } from "./VersionRow"; export const Language = { emptyMessage: "No versions found", nameLabel: "Version name", createdAtLabel: "Created at", createdByLabel: "Created by", -} +}; export interface VersionsTableProps { - activeVersionId: string - onPromoteClick?: (templateVersionId: string) => void - versions?: TypesGen.TemplateVersion[] + activeVersionId: string; + onPromoteClick?: (templateVersionId: string) => void; + versions?: TypesGen.TemplateVersion[]; } export const VersionsTable: FC> = ({ @@ -32,16 +32,16 @@ export const VersionsTable: FC> = ({ const latestVersionId = versions?.reduce( (latestSoFar, against) => { if (!latestSoFar) { - return against + return against; } return new Date(against.updated_at).getTime() > new Date(latestSoFar.updated_at).getTime() ? against - : latestSoFar + : latestSoFar; }, undefined as TypesGen.TemplateVersion | undefined, - )?.id + )?.id; return ( @@ -77,5 +77,5 @@ export const VersionsTable: FC> = ({ - ) -} + ); +}; diff --git a/site/src/pages/TemplatePage/utils.ts b/site/src/pages/TemplatePage/utils.ts index 3420593e34..f95c48e13f 100644 --- a/site/src/pages/TemplatePage/utils.ts +++ b/site/src/pages/TemplatePage/utils.ts @@ -1,10 +1,10 @@ -import { Template } from "api/typesGenerated" -import { pageTitle } from "utils/page" +import { Template } from "api/typesGenerated"; +import { pageTitle } from "utils/page"; export const getTemplatePageTitle = (title: string, template: Template) => { return pageTitle( `${ template.display_name.length > 0 ? template.display_name : template.name } · ${title}`, - ) -} + ); +}; diff --git a/site/src/pages/TemplateSettingsPage/Sidebar.tsx b/site/src/pages/TemplateSettingsPage/Sidebar.tsx index 1773186642..1947942241 100644 --- a/site/src/pages/TemplateSettingsPage/Sidebar.tsx +++ b/site/src/pages/TemplateSettingsPage/Sidebar.tsx @@ -1,19 +1,19 @@ -import { makeStyles } from "@mui/styles" -import ScheduleIcon from "@mui/icons-material/TimerOutlined" -import VariablesIcon from "@mui/icons-material/CodeOutlined" -import { Template } from "api/typesGenerated" -import { Stack } from "components/Stack/Stack" -import { FC, ElementType, PropsWithChildren, ReactNode } from "react" -import { Link, NavLink } from "react-router-dom" -import { combineClasses } from "utils/combineClasses" -import GeneralIcon from "@mui/icons-material/SettingsOutlined" -import SecurityIcon from "@mui/icons-material/LockOutlined" -import { Avatar } from "components/Avatar/Avatar" +import { makeStyles } from "@mui/styles"; +import ScheduleIcon from "@mui/icons-material/TimerOutlined"; +import VariablesIcon from "@mui/icons-material/CodeOutlined"; +import { Template } from "api/typesGenerated"; +import { Stack } from "components/Stack/Stack"; +import { FC, ElementType, PropsWithChildren, ReactNode } from "react"; +import { Link, NavLink } from "react-router-dom"; +import { combineClasses } from "utils/combineClasses"; +import GeneralIcon from "@mui/icons-material/SettingsOutlined"; +import SecurityIcon from "@mui/icons-material/LockOutlined"; +import { Avatar } from "components/Avatar/Avatar"; const SidebarNavItem: FC< PropsWithChildren<{ href: string; icon: ReactNode }> > = ({ children, href, icon }) => { - const styles = useStyles() + const styles = useStyles(); return ( - ) -} + ); +}; const SidebarNavItemIcon: React.FC<{ icon: ElementType }> = ({ icon: Icon, }) => { - const styles = useStyles() - return -} + const styles = useStyles(); + return ; +}; export const Sidebar: React.FC<{ template: Template }> = ({ template }) => { - const styles = useStyles() + const styles = useStyles(); return ( - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ sidebar: { @@ -147,4 +147,4 @@ const useStyles = makeStyles((theme) => ({ overflow: "hidden", textOverflow: "ellipsis", }, -})) +})); diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index 9e5b84257b..b6a223dde5 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -1,33 +1,33 @@ -import TextField from "@mui/material/TextField" -import { Template, UpdateTemplateMeta } from "api/typesGenerated" -import { FormikContextType, FormikTouched, useFormik } from "formik" -import { FC } from "react" +import TextField from "@mui/material/TextField"; +import { Template, UpdateTemplateMeta } from "api/typesGenerated"; +import { FormikContextType, FormikTouched, useFormik } from "formik"; +import { FC } from "react"; import { getFormHelpers, nameValidator, templateDisplayNameValidator, onChangeTrimmed, iconValidator, -} from "utils/formUtils" -import * as Yup from "yup" -import i18next from "i18next" -import { useTranslation } from "react-i18next" -import { LazyIconField } from "components/IconField/LazyIconField" +} from "utils/formUtils"; +import * as Yup from "yup"; +import i18next from "i18next"; +import { useTranslation } from "react-i18next"; +import { LazyIconField } from "components/IconField/LazyIconField"; import { FormFields, FormSection, HorizontalForm, FormFooter, -} from "components/Form/Form" -import { Stack } from "components/Stack/Stack" -import Checkbox from "@mui/material/Checkbox" +} from "components/Form/Form"; +import { Stack } from "components/Stack/Stack"; +import Checkbox from "@mui/material/Checkbox"; import { HelpTooltip, HelpTooltipText, -} from "components/HelpTooltip/HelpTooltip" -import { makeStyles } from "@mui/styles" +} from "components/HelpTooltip/HelpTooltip"; +import { makeStyles } from "@mui/styles"; -const MAX_DESCRIPTION_CHAR_LIMIT = 128 +const MAX_DESCRIPTION_CHAR_LIMIT = 128; export const getValidationSchema = (): Yup.AnyObjectSchema => Yup.object({ @@ -45,16 +45,16 @@ export const getValidationSchema = (): Yup.AnyObjectSchema => ), allow_user_cancel_workspace_jobs: Yup.boolean(), icon: iconValidator, - }) + }); export interface TemplateSettingsForm { - template: Template - onSubmit: (data: UpdateTemplateMeta) => void - onCancel: () => void - isSubmitting: boolean - error?: unknown + template: Template; + onSubmit: (data: UpdateTemplateMeta) => void; + onCancel: () => void; + isSubmitting: boolean; + error?: unknown; // Helpful to show field errors on Storybook - initialTouched?: FormikTouched + initialTouched?: FormikTouched; } export const TemplateSettingsForm: FC = ({ @@ -65,7 +65,7 @@ export const TemplateSettingsForm: FC = ({ isSubmitting, initialTouched, }) => { - const validationSchema = getValidationSchema() + const validationSchema = getValidationSchema(); const form: FormikContextType = useFormik({ initialValues: { @@ -81,10 +81,10 @@ export const TemplateSettingsForm: FC = ({ validationSchema, onSubmit, initialTouched, - }) - const getFieldHelpers = getFormHelpers(form, error) - const { t } = useTranslation("templateSettingsPage") - const styles = useStyles() + }); + const getFieldHelpers = getFormHelpers(form, error); + const { t } = useTranslation("templateSettingsPage"); + const styles = useStyles(); return ( = ({ - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ optionText: { @@ -191,4 +191,4 @@ const useStyles = makeStyles((theme) => ({ fontSize: theme.spacing(1.5), color: theme.palette.text.secondary, }, -})) +})); diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx index 8033232f52..fe46ddc69e 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx @@ -1,22 +1,22 @@ -import { screen, waitFor } from "@testing-library/react" -import userEvent from "@testing-library/user-event" -import * as API from "api/api" -import { UpdateTemplateMeta } from "api/typesGenerated" -import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter" -import { MockTemplate } from "../../../testHelpers/entities" +import { screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import * as API from "api/api"; +import { UpdateTemplateMeta } from "api/typesGenerated"; +import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"; +import { MockTemplate } from "../../../testHelpers/entities"; import { renderWithTemplateSettingsLayout, waitForLoaderToBeRemoved, -} from "../../../testHelpers/renderHelpers" -import { getValidationSchema } from "./TemplateSettingsForm" -import { TemplateSettingsPage } from "./TemplateSettingsPage" -import i18next from "i18next" +} from "../../../testHelpers/renderHelpers"; +import { getValidationSchema } from "./TemplateSettingsForm"; +import { TemplateSettingsPage } from "./TemplateSettingsPage"; +import i18next from "i18next"; -const { t } = i18next +const { t } = i18next; type FormValues = Required< Omit -> +>; const validFormValues: FormValues = { name: "Name", @@ -35,15 +35,15 @@ const validFormValues: FormValues = { time_til_dormant_autodelete_ms: 0, update_workspace_last_used_at: false, update_workspace_dormant_at: false, -} +}; const renderTemplateSettingsPage = async () => { renderWithTemplateSettingsLayout(, { route: `/templates/${MockTemplate.name}/settings`, path: `/templates/:template/settings`, - }) - await waitForLoaderToBeRemoved() -} + }); + await waitForLoaderToBeRemoved(); +}; const fillAndSubmitForm = async ({ name, @@ -52,69 +52,73 @@ const fillAndSubmitForm = async ({ icon, allow_user_cancel_workspace_jobs, }: FormValues) => { - const label = t("nameLabel", { ns: "templateSettingsPage" }) - const nameField = await screen.findByLabelText(label) - await userEvent.clear(nameField) - await userEvent.type(nameField, name) + const label = t("nameLabel", { ns: "templateSettingsPage" }); + const nameField = await screen.findByLabelText(label); + await userEvent.clear(nameField); + await userEvent.type(nameField, name); - const displayNameLabel = t("displayNameLabel", { ns: "templateSettingsPage" }) + const displayNameLabel = t("displayNameLabel", { + ns: "templateSettingsPage", + }); - const displayNameField = await screen.findByLabelText(displayNameLabel) - await userEvent.clear(displayNameField) - await userEvent.type(displayNameField, display_name) + const displayNameField = await screen.findByLabelText(displayNameLabel); + await userEvent.clear(displayNameField); + await userEvent.type(displayNameField, display_name); - const descriptionLabel = t("descriptionLabel", { ns: "templateSettingsPage" }) - const descriptionField = await screen.findByLabelText(descriptionLabel) - await userEvent.clear(descriptionField) - await userEvent.type(descriptionField, description) + const descriptionLabel = t("descriptionLabel", { + ns: "templateSettingsPage", + }); + const descriptionField = await screen.findByLabelText(descriptionLabel); + await userEvent.clear(descriptionField); + await userEvent.type(descriptionField, description); - const iconLabel = t("iconLabel", { ns: "templateSettingsPage" }) - const iconField = await screen.findByLabelText(iconLabel) - await userEvent.clear(iconField) - await userEvent.type(iconField, icon) + const iconLabel = t("iconLabel", { ns: "templateSettingsPage" }); + const iconField = await screen.findByLabelText(iconLabel); + await userEvent.clear(iconField); + await userEvent.type(iconField, icon); - const allowCancelJobsField = screen.getByRole("checkbox") + const allowCancelJobsField = screen.getByRole("checkbox"); // checkbox is checked by default, so it must be clicked to get unchecked if (!allow_user_cancel_workspace_jobs) { - await userEvent.click(allowCancelJobsField) + await userEvent.click(allowCancelJobsField); } const submitButton = await screen.findByText( FooterFormLanguage.defaultSubmitLabel, - ) - await userEvent.click(submitButton) -} + ); + await userEvent.click(submitButton); +}; describe("TemplateSettingsPage", () => { it("succeeds", async () => { - await renderTemplateSettingsPage() + await renderTemplateSettingsPage(); jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, ...validFormValues, - }) - await fillAndSubmitForm(validFormValues) - await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)) - }) + }); + await fillAndSubmitForm(validFormValues); + await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)); + }); it("allows a description of 128 chars", () => { const values: UpdateTemplateMeta = { ...validFormValues, description: "Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus molestie, port", - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) + }; + const validate = () => getValidationSchema().validateSync(values); + expect(validate).not.toThrowError(); + }); it("disallows a description of 128 + 1 chars", () => { const values: UpdateTemplateMeta = { ...validFormValues, description: "Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus molestie, port a", - } - const validate = () => getValidationSchema().validateSync(values) + }; + const validate = () => getValidationSchema().validateSync(values); expect(validate).toThrowError( t("descriptionMaxError", { ns: "templateSettingsPage" }).toString(), - ) - }) -}) + ); + }); +}); diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx index 3817785568..42e37141f6 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx @@ -1,24 +1,24 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query" -import { updateTemplateMeta } from "api/api" -import { UpdateTemplateMeta } from "api/typesGenerated" -import { displaySuccess } from "components/GlobalSnackbar/utils" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { useTranslation } from "react-i18next" -import { useNavigate, useParams } from "react-router-dom" -import { pageTitle } from "utils/page" +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { updateTemplateMeta } from "api/api"; +import { UpdateTemplateMeta } from "api/typesGenerated"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useParams } from "react-router-dom"; +import { pageTitle } from "utils/page"; import { getTemplateQuery, useTemplateSettingsContext, -} from "../TemplateSettingsLayout" -import { TemplateSettingsPageView } from "./TemplateSettingsPageView" +} from "../TemplateSettingsLayout"; +import { TemplateSettingsPageView } from "./TemplateSettingsPageView"; export const TemplateSettingsPage: FC = () => { - const { template: templateName } = useParams() as { template: string } - const { t } = useTranslation("templateSettingsPage") - const navigate = useNavigate() - const { template } = useTemplateSettingsContext() - const queryClient = useQueryClient() + const { template: templateName } = useParams() as { template: string }; + const { t } = useTranslation("templateSettingsPage"); + const navigate = useNavigate(); + const { template } = useTemplateSettingsContext(); + const queryClient = useQueryClient(); const { mutate: updateTemplate, isLoading: isSubmitting, @@ -29,11 +29,11 @@ export const TemplateSettingsPage: FC = () => { onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: getTemplateQuery(templateName), - }) - displaySuccess("Template updated successfully") + }); + displaySuccess("Template updated successfully"); }, }, - ) + ); return ( <> @@ -45,15 +45,15 @@ export const TemplateSettingsPage: FC = () => { template={template} submitError={submitError} onCancel={() => { - navigate(`/templates/${templateName}`) + navigate(`/templates/${templateName}`); }} onSubmit={(templateSettings) => { updateTemplate({ ...template, ...templateSettings, - }) + }); }} /> - ) -} + ); +}; diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx index d5f61f588a..676eb7201e 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx @@ -1,10 +1,10 @@ -import { action } from "@storybook/addon-actions" -import { Story } from "@storybook/react" -import { mockApiError, MockTemplate } from "testHelpers/entities" +import { action } from "@storybook/addon-actions"; +import { Story } from "@storybook/react"; +import { mockApiError, MockTemplate } from "testHelpers/entities"; import { TemplateSettingsPageView, TemplateSettingsPageViewProps, -} from "./TemplateSettingsPageView" +} from "./TemplateSettingsPageView"; export default { title: "pages/TemplateSettingsPageView", @@ -14,16 +14,16 @@ export default { onSubmit: action("onSubmit"), onCancel: action("cancel"), }, -} +}; const Template: Story = (args) => ( -) +); -export const Example = Template.bind({}) -Example.args = {} +export const Example = Template.bind({}); +Example.args = {}; -export const SaveTemplateSettingsError = Template.bind({}) +export const SaveTemplateSettingsError = Template.bind({}); SaveTemplateSettingsError.args = { submitError: mockApiError({ message: 'Template "test" already exists.', @@ -37,4 +37,4 @@ SaveTemplateSettingsError.args = { initialTouched: { allow_user_cancel_workspace_jobs: true, }, -} +}; diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx index e9339233ac..9f3c6aece3 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.tsx @@ -1,17 +1,19 @@ -import { Template, UpdateTemplateMeta } from "api/typesGenerated" -import { ComponentProps, FC } from "react" -import { TemplateSettingsForm } from "./TemplateSettingsForm" -import { useTranslation } from "react-i18next" -import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader" -import { makeStyles } from "@mui/styles" +import { Template, UpdateTemplateMeta } from "api/typesGenerated"; +import { ComponentProps, FC } from "react"; +import { TemplateSettingsForm } from "./TemplateSettingsForm"; +import { useTranslation } from "react-i18next"; +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; +import { makeStyles } from "@mui/styles"; export interface TemplateSettingsPageViewProps { - template: Template - onSubmit: (data: UpdateTemplateMeta) => void - onCancel: () => void - isSubmitting: boolean - submitError?: unknown - initialTouched?: ComponentProps["initialTouched"] + template: Template; + onSubmit: (data: UpdateTemplateMeta) => void; + onCancel: () => void; + isSubmitting: boolean; + submitError?: unknown; + initialTouched?: ComponentProps< + typeof TemplateSettingsForm + >["initialTouched"]; } export const TemplateSettingsPageView: FC = ({ @@ -22,8 +24,8 @@ export const TemplateSettingsPageView: FC = ({ submitError, initialTouched, }) => { - const { t } = useTranslation("templateSettingsPage") - const styles = useStyles() + const { t } = useTranslation("templateSettingsPage"); + const styles = useStyles(); return ( <> @@ -40,11 +42,11 @@ export const TemplateSettingsPageView: FC = ({ error={submitError} /> - ) -} + ); +}; const useStyles = makeStyles(() => ({ pageHeader: { paddingTop: 0, }, -})) +})); diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx index 7e29b4f147..8fd95ed13b 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage.tsx @@ -1,30 +1,30 @@ -import Button from "@mui/material/Button" -import Link from "@mui/material/Link" -import ArrowRightAltOutlined from "@mui/icons-material/ArrowRightAltOutlined" -import { useMachine } from "@xstate/react" -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { Paywall } from "components/Paywall/Paywall" -import { Stack } from "components/Stack/Stack" -import { useFeatureVisibility } from "hooks/useFeatureVisibility" -import { useOrganizationId } from "hooks/useOrganizationId" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "utils/page" -import { templateACLMachine } from "xServices/template/templateACLXService" -import { useTemplateSettingsContext } from "../TemplateSettingsLayout" -import { TemplatePermissionsPageView } from "./TemplatePermissionsPageView" -import { docs } from "utils/docs" +import Button from "@mui/material/Button"; +import Link from "@mui/material/Link"; +import ArrowRightAltOutlined from "@mui/icons-material/ArrowRightAltOutlined"; +import { useMachine } from "@xstate/react"; +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { Paywall } from "components/Paywall/Paywall"; +import { Stack } from "components/Stack/Stack"; +import { useFeatureVisibility } from "hooks/useFeatureVisibility"; +import { useOrganizationId } from "hooks/useOrganizationId"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { templateACLMachine } from "xServices/template/templateACLXService"; +import { useTemplateSettingsContext } from "../TemplateSettingsLayout"; +import { TemplatePermissionsPageView } from "./TemplatePermissionsPageView"; +import { docs } from "utils/docs"; export const TemplatePermissionsPage: FC< React.PropsWithChildren > = () => { - const organizationId = useOrganizationId() - const { template, permissions } = useTemplateSettingsContext() - const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility() + const organizationId = useOrganizationId(); + const { template, permissions } = useTemplateSettingsContext(); + const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility(); const [state, send] = useMachine(templateACLMachine, { context: { templateId: template.id }, - }) - const { templateACL, userToBeUpdated, groupToBeUpdated } = state.context + }); + const { templateACL, userToBeUpdated, groupToBeUpdated } = state.context; return ( <> @@ -68,32 +68,32 @@ export const TemplatePermissionsPage: FC< templateACL={templateACL} canUpdatePermissions={Boolean(permissions?.canUpdateTemplate)} onAddUser={(user, role, reset) => { - send("ADD_USER", { user, role, onDone: reset }) + send("ADD_USER", { user, role, onDone: reset }); }} isAddingUser={state.matches("addingUser")} onUpdateUser={(user, role) => { - send("UPDATE_USER_ROLE", { user, role }) + send("UPDATE_USER_ROLE", { user, role }); }} updatingUser={userToBeUpdated} onRemoveUser={(user) => { - send("REMOVE_USER", { user }) + send("REMOVE_USER", { user }); }} onAddGroup={(group, role, reset) => { - send("ADD_GROUP", { group, role, onDone: reset }) + send("ADD_GROUP", { group, role, onDone: reset }); }} isAddingGroup={state.matches("addingGroup")} onUpdateGroup={(group, role) => { - send("UPDATE_GROUP_ROLE", { group, role }) + send("UPDATE_GROUP_ROLE", { group, role }); }} updatingGroup={groupToBeUpdated} onRemoveGroup={(group) => { - send("REMOVE_GROUP", { group }) + send("REMOVE_GROUP", { group }); }} /> - ) -} + ); +}; -export default TemplatePermissionsPage +export default TemplatePermissionsPage; diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.stories.tsx index 9e8d8c19cd..643c09ae1f 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.stories.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.stories.tsx @@ -1,38 +1,38 @@ -import { Story } from "@storybook/react" +import { Story } from "@storybook/react"; import { MockOrganization, MockTemplateACL, MockTemplateACLEmpty, -} from "testHelpers/entities" +} from "testHelpers/entities"; import { TemplatePermissionsPageView, TemplatePermissionsPageViewProps, -} from "./TemplatePermissionsPageView" +} from "./TemplatePermissionsPageView"; export default { title: "pages/TemplatePermissionsPageView", component: TemplatePermissionsPageView, -} +}; const Template: Story = ( args: TemplatePermissionsPageViewProps, -) => +) => ; -export const Empty = Template.bind({}) +export const Empty = Template.bind({}); Empty.args = { templateACL: MockTemplateACLEmpty, canUpdatePermissions: false, -} +}; -export const WithTemplateACL = Template.bind({}) +export const WithTemplateACL = Template.bind({}); WithTemplateACL.args = { templateACL: MockTemplateACL, canUpdatePermissions: false, -} +}; -export const WithUpdatePermissions = Template.bind({}) +export const WithUpdatePermissions = Template.bind({}); WithUpdatePermissions.args = { templateACL: MockTemplateACL, canUpdatePermissions: true, organizationId: MockOrganization.id, -} +}; diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx index a434fcc930..27216eec7f 100644 --- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx @@ -1,48 +1,48 @@ -import MenuItem from "@mui/material/MenuItem" -import Select, { SelectProps } from "@mui/material/Select" -import { makeStyles } from "@mui/styles" -import Table from "@mui/material/Table" -import TableBody from "@mui/material/TableBody" -import TableCell from "@mui/material/TableCell" -import TableContainer from "@mui/material/TableContainer" -import TableHead from "@mui/material/TableHead" -import TableRow from "@mui/material/TableRow" -import PersonAdd from "@mui/icons-material/PersonAdd" +import MenuItem from "@mui/material/MenuItem"; +import Select, { SelectProps } from "@mui/material/Select"; +import { makeStyles } from "@mui/styles"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import PersonAdd from "@mui/icons-material/PersonAdd"; import { Group, TemplateACL, TemplateGroup, TemplateRole, TemplateUser, -} from "api/typesGenerated" -import { AvatarData } from "components/AvatarData/AvatarData" -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { EmptyState } from "components/EmptyState/EmptyState" -import { LoadingButton } from "components/LoadingButton/LoadingButton" -import { Stack } from "components/Stack/Stack" -import { TableLoader } from "components/TableLoader/TableLoader" -import { TableRowMenu } from "components/TableRowMenu/TableRowMenu" +} from "api/typesGenerated"; +import { AvatarData } from "components/AvatarData/AvatarData"; +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { LoadingButton } from "components/LoadingButton/LoadingButton"; +import { Stack } from "components/Stack/Stack"; +import { TableLoader } from "components/TableLoader/TableLoader"; +import { TableRowMenu } from "components/TableRowMenu/TableRowMenu"; import { UserOrGroupAutocomplete, UserOrGroupAutocompleteValue, -} from "components/UserOrGroupAutocomplete/UserOrGroupAutocomplete" -import { FC, useState } from "react" -import { Maybe } from "components/Conditionals/Maybe" -import { GroupAvatar } from "components/GroupAvatar/GroupAvatar" -import { getGroupSubtitle } from "utils/groups" -import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader" +} from "components/UserOrGroupAutocomplete/UserOrGroupAutocomplete"; +import { FC, useState } from "react"; +import { Maybe } from "components/Conditionals/Maybe"; +import { GroupAvatar } from "components/GroupAvatar/GroupAvatar"; +import { getGroupSubtitle } from "utils/groups"; +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; type AddTemplateUserOrGroupProps = { - organizationId: string - templateID: string - isLoading: boolean - templateACL: TemplateACL | undefined + organizationId: string; + templateID: string; + isLoading: boolean; + templateACL: TemplateACL | undefined; onSubmit: ( userOrGroup: TemplateUser | TemplateGroup, role: TemplateRole, reset: () => void, - ) => void -} + ) => void; +}; const AddTemplateUserOrGroup: React.FC = ({ isLoading, @@ -51,23 +51,23 @@ const AddTemplateUserOrGroup: React.FC = ({ templateID, templateACL, }) => { - const styles = useStyles() + const styles = useStyles(); const [selectedOption, setSelectedOption] = - useState(null) - const [selectedRole, setSelectedRole] = useState("use") + useState(null); + const [selectedRole, setSelectedRole] = useState("use"); const excludeFromAutocomplete = templateACL ? [...templateACL.group, ...templateACL.users] - : [] + : []; const resetValues = () => { - setSelectedOption(null) - setSelectedRole("use") - } + setSelectedOption(null); + setSelectedRole("use"); + }; return (
{ - e.preventDefault() + e.preventDefault(); if (selectedOption && selectedRole) { onSubmit( @@ -77,7 +77,7 @@ const AddTemplateUserOrGroup: React.FC = ({ }, selectedRole, resetValues, - ) + ); } }} > @@ -88,7 +88,7 @@ const AddTemplateUserOrGroup: React.FC = ({ templateID={templateID} value={selectedOption} onChange={(newValue) => { - setSelectedOption(newValue) + setSelectedOption(newValue); }} /> @@ -98,7 +98,7 @@ const AddTemplateUserOrGroup: React.FC = ({ className={styles.select} disabled={isLoading} onChange={(event) => { - setSelectedRole(event.target.value as TemplateRole) + setSelectedRole(event.target.value as TemplateRole); }} > @@ -119,11 +119,11 @@ const AddTemplateUserOrGroup: React.FC = ({ - ) -} + ); +}; const RoleSelect: FC = (props) => { - const styles = useStyles() + const styles = useStyles(); return ( - ) -} + ); +}; export interface TemplatePermissionsPageViewProps { - templateACL: TemplateACL | undefined - templateID: string - organizationId: string - canUpdatePermissions: boolean + templateACL: TemplateACL | undefined; + templateID: string; + organizationId: string; + canUpdatePermissions: boolean; // User - onAddUser: (user: TemplateUser, role: TemplateRole, reset: () => void) => void - isAddingUser: boolean - onUpdateUser: (user: TemplateUser, role: TemplateRole) => void - updatingUser: TemplateUser | undefined - onRemoveUser: (user: TemplateUser) => void + onAddUser: ( + user: TemplateUser, + role: TemplateRole, + reset: () => void, + ) => void; + isAddingUser: boolean; + onUpdateUser: (user: TemplateUser, role: TemplateRole) => void; + updatingUser: TemplateUser | undefined; + onRemoveUser: (user: TemplateUser) => void; // Group onAddGroup: ( group: TemplateGroup, role: TemplateRole, reset: () => void, - ) => void - isAddingGroup: boolean - onUpdateGroup: (group: TemplateGroup, role: TemplateRole) => void - updatingGroup: TemplateGroup | undefined - onRemoveGroup: (group: Group) => void + ) => void; + isAddingGroup: boolean; + onUpdateGroup: (group: TemplateGroup, role: TemplateRole) => void; + updatingGroup: TemplateGroup | undefined; + onRemoveGroup: (group: Group) => void; } export const TemplatePermissionsPageView: FC< @@ -195,12 +199,12 @@ export const TemplatePermissionsPageView: FC< onUpdateGroup, onRemoveGroup, }) => { - const styles = useStyles() + const styles = useStyles(); const isEmpty = Boolean( templateACL && templateACL.users.length === 0 && templateACL.group.length === 0, - ) + ); return ( <> @@ -273,7 +277,7 @@ export const TemplatePermissionsPageView: FC< onUpdateGroup( group, event.target.value as TemplateRole, - ) + ); }} /> @@ -321,7 +325,7 @@ export const TemplatePermissionsPageView: FC< onUpdateUser( user, event.target.value as TemplateRole, - ) + ); }} /> @@ -354,8 +358,8 @@ export const TemplatePermissionsPageView: FC< - ) -} + ); +}; export const useStyles = makeStyles((theme) => ({ select: { @@ -401,4 +405,4 @@ export const useStyles = makeStyles((theme) => ({ pageHeader: { paddingTop: 0, }, -})) +})); diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/AutostopRequirementHelperText.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/AutostopRequirementHelperText.tsx index 90127df6a4..780da697b8 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/AutostopRequirementHelperText.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/AutostopRequirementHelperText.tsx @@ -1,26 +1,26 @@ -import { Template } from "api/typesGenerated" -import { useTranslation } from "react-i18next" +import { Template } from "api/typesGenerated"; +import { useTranslation } from "react-i18next"; export type TemplateAutostopRequirementDaysValue = | "off" | "daily" | "saturday" - | "sunday" + | "sunday"; export const convertAutostopRequirementDaysValue = ( days: Template["autostop_requirement"]["days_of_week"], ): TemplateAutostopRequirementDaysValue => { if (days.length === 7) { - return "daily" + return "daily"; } else if (days.length === 1 && days[0] === "saturday") { - return "saturday" + return "saturday"; } else if (days.length === 1 && days[0] === "sunday") { - return "sunday" + return "sunday"; } // On unsupported values we default to "off". - return "off" -} + return "off"; +}; export const calculateAutostopRequirementDaysValue = ( value: TemplateAutostopRequirementDaysValue, @@ -35,46 +35,46 @@ export const calculateAutostopRequirementDaysValue = ( "friday", "saturday", "sunday", - ] + ]; case "saturday": - return ["saturday"] + return ["saturday"]; case "sunday": - return ["sunday"] + return ["sunday"]; } - return [] -} + return []; +}; export const AutostopRequirementDaysHelperText = ({ days, }: { - days: TemplateAutostopRequirementDaysValue + days: TemplateAutostopRequirementDaysValue; }) => { - const { t } = useTranslation("templateSettingsPage") + const { t } = useTranslation("templateSettingsPage"); - let str = "off" + let str = "off"; if (days) { - str = days + str = days; } - return {t("autostopRequirementDaysHelperText_" + str)} -} + return {t("autostopRequirementDaysHelperText_" + str)}; +}; export const AutostopRequirementWeeksHelperText = ({ days, weeks, }: { - days: TemplateAutostopRequirementDaysValue - weeks: number + days: TemplateAutostopRequirementDaysValue; + weeks: number; }) => { - const { t } = useTranslation("templateSettingsPage") + const { t } = useTranslation("templateSettingsPage"); - let str = "disabled" + let str = "disabled"; if (days === "saturday" || days === "sunday") { if (weeks === 0 || weeks === 1) { - str = "one" + str = "one"; } else { - str = "other" + str = "other"; } } @@ -82,5 +82,5 @@ export const AutostopRequirementWeeksHelperText = ({ {t("autostopRequirementWeeksHelperText_" + str, { count: weeks })} - ) -} + ); +}; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TTLHelperText.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TTLHelperText.tsx index 0f7a55637b..03e0c2fa4e 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TTLHelperText.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TTLHelperText.tsx @@ -1,19 +1,19 @@ -import { Maybe } from "components/Conditionals/Maybe" -import { useTranslation } from "react-i18next" +import { Maybe } from "components/Conditionals/Maybe"; +import { useTranslation } from "react-i18next"; export const TTLHelperText = ({ ttl, translationName, }: { - ttl?: number - translationName: string + ttl?: number; + translationName: string; }) => { - const { t } = useTranslation("templateSettingsPage") - const count = typeof ttl !== "number" ? 0 : ttl + const { t } = useTranslation("templateSettingsPage"); + const count = typeof ttl !== "number" ? 0 : ttl; return ( // no helper text if ttl is negative - error will show once field is considered touched = 0}> {t(translationName, { count })} - ) -} + ); +}; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx index bd48b3fcd7..d1d05578a3 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/TemplateScheduleForm.tsx @@ -1,54 +1,54 @@ -import TextField from "@mui/material/TextField" -import { Template, UpdateTemplateMeta } from "api/typesGenerated" -import { FormikTouched, useFormik } from "formik" -import { FC, ChangeEvent, useState, useEffect } from "react" -import { getFormHelpers } from "utils/formUtils" -import { useTranslation } from "react-i18next" +import TextField from "@mui/material/TextField"; +import { Template, UpdateTemplateMeta } from "api/typesGenerated"; +import { FormikTouched, useFormik } from "formik"; +import { FC, ChangeEvent, useState, useEffect } from "react"; +import { getFormHelpers } from "utils/formUtils"; +import { useTranslation } from "react-i18next"; import { FormSection, HorizontalForm, FormFooter, FormFields, -} from "components/Form/Form" -import { Stack } from "components/Stack/Stack" -import { makeStyles } from "@mui/styles" -import Link from "@mui/material/Link" -import Checkbox from "@mui/material/Checkbox" -import FormControlLabel from "@mui/material/FormControlLabel" -import Switch from "@mui/material/Switch" +} from "components/Form/Form"; +import { Stack } from "components/Stack/Stack"; +import { makeStyles } from "@mui/styles"; +import Link from "@mui/material/Link"; +import Checkbox from "@mui/material/Checkbox"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Switch from "@mui/material/Switch"; import { useWorkspacesToGoDormant, useWorkspacesToBeDeleted, -} from "./useWorkspacesToBeDeleted" -import { TemplateScheduleFormValues, getValidationSchema } from "./formHelpers" -import { TTLHelperText } from "./TTLHelperText" -import { docs } from "utils/docs" -import { ScheduleDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" -import MenuItem from "@mui/material/MenuItem" +} from "./useWorkspacesToBeDeleted"; +import { TemplateScheduleFormValues, getValidationSchema } from "./formHelpers"; +import { TTLHelperText } from "./TTLHelperText"; +import { docs } from "utils/docs"; +import { ScheduleDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import MenuItem from "@mui/material/MenuItem"; import { AutostopRequirementDaysHelperText, AutostopRequirementWeeksHelperText, calculateAutostopRequirementDaysValue, convertAutostopRequirementDaysValue, -} from "./AutostopRequirementHelperText" +} from "./AutostopRequirementHelperText"; -const MS_HOUR_CONVERSION = 3600000 -const MS_DAY_CONVERSION = 86400000 -const FAILURE_CLEANUP_DEFAULT = 7 -const INACTIVITY_CLEANUP_DEFAULT = 180 -const DORMANT_AUTODELETION_DEFAULT = 30 +const MS_HOUR_CONVERSION = 3600000; +const MS_DAY_CONVERSION = 86400000; +const FAILURE_CLEANUP_DEFAULT = 7; +const INACTIVITY_CLEANUP_DEFAULT = 180; +const DORMANT_AUTODELETION_DEFAULT = 30; export interface TemplateScheduleForm { - template: Template - onSubmit: (data: UpdateTemplateMeta) => void - onCancel: () => void - isSubmitting: boolean - error?: unknown - allowAdvancedScheduling: boolean - allowWorkspaceActions: boolean - allowAutostopRequirement: boolean + template: Template; + onSubmit: (data: UpdateTemplateMeta) => void; + onCancel: () => void; + isSubmitting: boolean; + error?: unknown; + allowAdvancedScheduling: boolean; + allowWorkspaceActions: boolean; + allowAutostopRequirement: boolean; // Helpful to show field errors on Storybook - initialTouched?: FormikTouched + initialTouched?: FormikTouched; } export const TemplateScheduleForm: FC = ({ @@ -62,8 +62,8 @@ export const TemplateScheduleForm: FC = ({ isSubmitting, initialTouched, }) => { - const { t: commonT } = useTranslation("common") - const validationSchema = getValidationSchema() + const { t: commonT } = useTranslation("common"); + const validationSchema = getValidationSchema(); const form = useFormik({ initialValues: { // on display, convert from ms => hours @@ -110,66 +110,66 @@ export const TemplateScheduleForm: FC = ({ onSubmit: () => { const dormancyChanged = form.initialValues.time_til_dormant_ms !== - form.values.time_til_dormant_ms + form.values.time_til_dormant_ms; const deletionChanged = form.initialValues.time_til_dormant_autodelete_ms !== - form.values.time_til_dormant_autodelete_ms + form.values.time_til_dormant_autodelete_ms; const dormancyScheduleChanged = form.values.inactivity_cleanup_enabled && dormancyChanged && workspacesToDormancyInWeek && - workspacesToDormancyInWeek.length > 0 + workspacesToDormancyInWeek.length > 0; const deletionScheduleChanged = form.values.inactivity_cleanup_enabled && deletionChanged && workspacesToBeDeletedInWeek && - workspacesToBeDeletedInWeek.length > 0 + workspacesToBeDeletedInWeek.length > 0; if (dormancyScheduleChanged || deletionScheduleChanged) { - setIsScheduleDialogOpen(true) + setIsScheduleDialogOpen(true); } else { - submitValues() + submitValues(); } }, initialTouched, - }) + }); const getFieldHelpers = getFormHelpers( form, error, - ) - const { t } = useTranslation("templateSettingsPage") - const styles = useStyles() + ); + const { t } = useTranslation("templateSettingsPage"); + const styles = useStyles(); - const now = new Date() - const weekFromNow = new Date(now) - weekFromNow.setDate(now.getDate() + 7) + const now = new Date(); + const weekFromNow = new Date(now); + weekFromNow.setDate(now.getDate() + 7); const workspacesToDormancyNow = useWorkspacesToGoDormant( template, form.values, now, - ) + ); const workspacesToDormancyInWeek = useWorkspacesToGoDormant( template, form.values, weekFromNow, - ) + ); const workspacesToBeDeletedNow = useWorkspacesToBeDeleted( template, form.values, now, - ) + ); const workspacesToBeDeletedInWeek = useWorkspacesToBeDeleted( template, form.values, weekFromNow, - ) + ); const showScheduleDialog = workspacesToDormancyNow && @@ -177,17 +177,17 @@ export const TemplateScheduleForm: FC = ({ workspacesToDormancyInWeek && workspacesToBeDeletedInWeek && (workspacesToDormancyInWeek.length > 0 || - workspacesToBeDeletedInWeek.length > 0) + workspacesToBeDeletedInWeek.length > 0); const [isScheduleDialogOpen, setIsScheduleDialogOpen] = - useState(false) + useState(false); const submitValues = () => { const autostop_requirement_weeks = ["saturday", "sunday"].includes( form.values.autostop_requirement_days_of_week, ) ? form.values.autostop_requirement_weeks - : 1 + : 1; // on submit, convert from hours => ms onSubmit({ @@ -218,8 +218,8 @@ export const TemplateScheduleForm: FC = ({ allow_user_autostop: form.values.allow_user_autostop, update_workspace_last_used_at: form.values.update_workspace_last_used_at, update_workspace_dormant_at: form.values.update_workspace_dormant_at, - }) - } + }); + }; // Set autostop_requirement weeks to 1 when days_of_week is set to "off" or // "daily". Technically you can set weeks to a different value in the backend @@ -229,7 +229,7 @@ export const TemplateScheduleForm: FC = ({ // // We want to set the value to 1 when the user selects "off" or "daily" // because the input gets disabled so they can't change it to 1 themselves. - const { values: currentValues, setValues } = form + const { values: currentValues, setValues } = form; useEffect(() => { if ( !["saturday", "sunday"].includes( @@ -241,66 +241,66 @@ export const TemplateScheduleForm: FC = ({ void setValues({ ...currentValues, autostop_requirement_weeks: 1, - }) + }); } - }, [currentValues, setValues]) + }, [currentValues, setValues]); const handleToggleFailureCleanup = async (e: ChangeEvent) => { - form.handleChange(e) + form.handleChange(e); if (!form.values.failure_cleanup_enabled) { // fill failure_ttl_ms with defaults await form.setValues({ ...form.values, failure_cleanup_enabled: true, failure_ttl_ms: FAILURE_CLEANUP_DEFAULT, - }) + }); } else { // clear failure_ttl_ms await form.setValues({ ...form.values, failure_cleanup_enabled: false, failure_ttl_ms: 0, - }) + }); } - } + }; const handleToggleInactivityCleanup = async (e: ChangeEvent) => { - form.handleChange(e) + form.handleChange(e); if (!form.values.inactivity_cleanup_enabled) { // fill time_til_dormant_ms with defaults await form.setValues({ ...form.values, inactivity_cleanup_enabled: true, time_til_dormant_ms: INACTIVITY_CLEANUP_DEFAULT, - }) + }); } else { // clear time_til_dormant_ms await form.setValues({ ...form.values, inactivity_cleanup_enabled: false, time_til_dormant_ms: 0, - }) + }); } - } + }; const handleToggleDormantAutoDeletion = async (e: ChangeEvent) => { - form.handleChange(e) + form.handleChange(e); if (!form.values.dormant_autodeletion_cleanup_enabled) { // fill failure_ttl_ms with defaults await form.setValues({ ...form.values, dormant_autodeletion_cleanup_enabled: true, time_til_dormant_autodelete_ms: DORMANT_AUTODELETION_DEFAULT, - }) + }); } else { // clear failure_ttl_ms await form.setValues({ ...form.values, dormant_autodeletion_cleanup_enabled: false, time_til_dormant_autodelete_ms: 0, - }) + }); } - } + }; return ( = ({ await form.setFieldValue( "allow_user_autostart", !form.values.allow_user_autostart, - ) + ); }} name="allow_user_autostart" checked={form.values.allow_user_autostart} @@ -446,7 +446,7 @@ export const TemplateScheduleForm: FC = ({ await form.setFieldValue( "allow_user_autostop", !form.values.allow_user_autostop, - ) + ); }} name="allow_user_autostop" checked={form.values.allow_user_autostop} @@ -569,20 +569,20 @@ export const TemplateScheduleForm: FC = ({ {showScheduleDialog && ( { - submitValues() - setIsScheduleDialogOpen(false) + submitValues(); + setIsScheduleDialogOpen(false); // These fields are request-scoped so they should be reset // after every submission. form .setFieldValue("update_workspace_dormant_at", false) .catch((error) => { - throw error - }) + throw error; + }); form .setFieldValue("update_workspace_last_used_at", false) .catch((error) => { - throw error - }) + throw error; + }); }} inactiveWorkspacesToGoDormant={workspacesToDormancyNow.length} inactiveWorkspacesToGoDormantInWeek={ @@ -594,7 +594,7 @@ export const TemplateScheduleForm: FC = ({ } open={isScheduleDialogOpen} onClose={() => { - setIsScheduleDialogOpen(false) + setIsScheduleDialogOpen(false); }} title="Workspace Scheduling" updateDormantWorkspaces={(update: boolean) => @@ -620,8 +620,8 @@ export const TemplateScheduleForm: FC = ({ submitDisabled={!form.isValid || !form.dirty} /> - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ ttlFields: { @@ -631,4 +631,4 @@ const useStyles = makeStyles((theme) => ({ fontSize: 12, color: theme.palette.text.secondary, }, -})) +})); diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/formHelpers.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/formHelpers.tsx index 8cb494da56..7ce5b3fec9 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/formHelpers.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/formHelpers.tsx @@ -1,18 +1,18 @@ -import { UpdateTemplateMeta } from "api/typesGenerated" -import * as Yup from "yup" -import i18next from "i18next" -import { TemplateAutostopRequirementDaysValue } from "./AutostopRequirementHelperText" +import { UpdateTemplateMeta } from "api/typesGenerated"; +import * as Yup from "yup"; +import i18next from "i18next"; +import { TemplateAutostopRequirementDaysValue } from "./AutostopRequirementHelperText"; export interface TemplateScheduleFormValues extends Omit { - autostop_requirement_days_of_week: TemplateAutostopRequirementDaysValue - autostop_requirement_weeks: number - failure_cleanup_enabled: boolean - inactivity_cleanup_enabled: boolean - dormant_autodeletion_cleanup_enabled: boolean + autostop_requirement_days_of_week: TemplateAutostopRequirementDaysValue; + autostop_requirement_weeks: number; + failure_cleanup_enabled: boolean; + inactivity_cleanup_enabled: boolean; + dormant_autodeletion_cleanup_enabled: boolean; } -const MAX_TTL_DAYS = 30 +const MAX_TTL_DAYS = 30; export const getValidationSchema = (): Yup.AnyObjectSchema => Yup.object({ @@ -46,11 +46,11 @@ export const getValidationSchema = (): Yup.AnyObjectSchema => "positive-if-enabled", "Failure cleanup days must be greater than zero when enabled.", function (value) { - const parent = this.parent as TemplateScheduleFormValues + const parent = this.parent as TemplateScheduleFormValues; if (parent.failure_cleanup_enabled) { - return Boolean(value) + return Boolean(value); } else { - return true + return true; } }, ), @@ -60,11 +60,11 @@ export const getValidationSchema = (): Yup.AnyObjectSchema => "positive-if-enabled", "Dormancy threshold days must be greater than zero when enabled.", function (value) { - const parent = this.parent as TemplateScheduleFormValues + const parent = this.parent as TemplateScheduleFormValues; if (parent.inactivity_cleanup_enabled) { - return Boolean(value) + return Boolean(value); } else { - return true + return true; } }, ), @@ -74,11 +74,11 @@ export const getValidationSchema = (): Yup.AnyObjectSchema => "positive-if-enabled", "Dormancy auto-deletion days must be greater than zero when enabled.", function (value) { - const parent = this.parent as TemplateScheduleFormValues + const parent = this.parent as TemplateScheduleFormValues; if (parent.dormant_autodeletion_cleanup_enabled) { - return Boolean(value) + return Boolean(value); } else { - return true + return true; } }, ), @@ -87,4 +87,4 @@ export const getValidationSchema = (): Yup.AnyObjectSchema => autostop_requirement_days_of_week: Yup.string().required(), autostop_requirement_weeks: Yup.number().required().min(1).max(16), - }) + }); diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts index b04cf02e7d..01c9ed16a2 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/useWorkspacesToBeDeleted.ts @@ -1,7 +1,7 @@ -import { compareAsc } from "date-fns" -import { Workspace, Template } from "api/typesGenerated" -import { TemplateScheduleFormValues } from "./formHelpers" -import { useWorkspacesData } from "pages/WorkspacesPage/data" +import { compareAsc } from "date-fns"; +import { Workspace, Template } from "api/typesGenerated"; +import { TemplateScheduleFormValues } from "./formHelpers"; +import { useWorkspacesData } from "pages/WorkspacesPage/data"; export const useWorkspacesToGoDormant = ( template: Template, @@ -12,29 +12,29 @@ export const useWorkspacesToGoDormant = ( page: 0, limit: 0, query: "template:" + template.name, - }) + }); return data?.workspaces?.filter((workspace: Workspace) => { if (!formValues.time_til_dormant_ms) { - return + return; } if (workspace.dormant_at) { - return + return; } const proposedLocking = new Date( new Date(workspace.last_used_at).getTime() + formValues.time_til_dormant_ms * DayInMS, - ) + ); if (compareAsc(proposedLocking, fromDate) < 1) { - return workspace + return workspace; } - }) -} + }); +}; -const DayInMS = 86400000 +const DayInMS = 86400000; export const useWorkspacesToBeDeleted = ( template: Template, @@ -45,19 +45,19 @@ export const useWorkspacesToBeDeleted = ( page: 0, limit: 0, query: "template:" + template.name + " dormant_at:1970-01-01", - }) + }); return data?.workspaces?.filter((workspace: Workspace) => { if (!workspace.dormant_at || !formValues.time_til_dormant_autodelete_ms) { - return false + return false; } const proposedLocking = new Date( new Date(workspace.dormant_at).getTime() + formValues.time_til_dormant_autodelete_ms * DayInMS, - ) + ); if (compareAsc(proposedLocking, fromDate) < 1) { - return workspace + return workspace; } - }) -} + }); +}; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx index 5c8e235400..c5be20870d 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx @@ -1,23 +1,23 @@ -import { screen, waitFor } from "@testing-library/react" -import userEvent from "@testing-library/user-event" -import * as API from "api/api" -import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter" +import { screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import * as API from "api/api"; +import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"; import { MockEntitlementsWithScheduling, MockTemplate, -} from "testHelpers/entities" +} from "testHelpers/entities"; import { renderWithTemplateSettingsLayout, waitForLoaderToBeRemoved, -} from "testHelpers/renderHelpers" +} from "testHelpers/renderHelpers"; import { TemplateScheduleFormValues, getValidationSchema, -} from "./TemplateScheduleForm/formHelpers" -import TemplateSchedulePage from "./TemplateSchedulePage" -import i18next from "i18next" +} from "./TemplateScheduleForm/formHelpers"; +import TemplateSchedulePage from "./TemplateSchedulePage"; +import i18next from "i18next"; -const { t } = i18next +const { t } = i18next; const validFormValues: TemplateScheduleFormValues = { default_ttl_ms: 1, @@ -32,15 +32,15 @@ const validFormValues: TemplateScheduleFormValues = { failure_cleanup_enabled: false, inactivity_cleanup_enabled: false, dormant_autodeletion_cleanup_enabled: false, -} +}; const renderTemplateSchedulePage = async () => { renderWithTemplateSettingsLayout(, { route: `/templates/${MockTemplate.name}/settings/schedule`, path: `/templates/:template/settings/schedule`, - }) - await waitForLoaderToBeRemoved() -} + }); + await waitForLoaderToBeRemoved(); +}; const fillAndSubmitForm = async ({ default_ttl_ms, @@ -49,92 +49,94 @@ const fillAndSubmitForm = async ({ time_til_dormant_ms, time_til_dormant_autodelete_ms, }: { - default_ttl_ms?: number - max_ttl_ms?: number - failure_ttl_ms?: number - time_til_dormant_ms?: number - time_til_dormant_autodelete_ms?: number + default_ttl_ms?: number; + max_ttl_ms?: number; + failure_ttl_ms?: number; + time_til_dormant_ms?: number; + time_til_dormant_autodelete_ms?: number; }) => { - const user = userEvent.setup() + const user = userEvent.setup(); if (default_ttl_ms) { - const defaultTtlLabel = t("defaultTtlLabel", { ns: "templateSettingsPage" }) - const defaultTtlField = await screen.findByLabelText(defaultTtlLabel) - await user.clear(defaultTtlField) - await user.type(defaultTtlField, default_ttl_ms.toString()) + const defaultTtlLabel = t("defaultTtlLabel", { + ns: "templateSettingsPage", + }); + const defaultTtlField = await screen.findByLabelText(defaultTtlLabel); + await user.clear(defaultTtlField); + await user.type(defaultTtlField, default_ttl_ms.toString()); } if (max_ttl_ms) { - const maxTtlLabel = t("maxTtlLabel", { ns: "templateSettingsPage" }) - const maxTtlField = await screen.findByLabelText(maxTtlLabel) - await user.clear(maxTtlField) - await user.type(maxTtlField, max_ttl_ms.toString()) + const maxTtlLabel = t("maxTtlLabel", { ns: "templateSettingsPage" }); + const maxTtlField = await screen.findByLabelText(maxTtlLabel); + await user.clear(maxTtlField); + await user.type(maxTtlField, max_ttl_ms.toString()); } if (failure_ttl_ms) { const failureTtlField = screen.getByRole("checkbox", { name: /Failure Cleanup/i, - }) - await user.type(failureTtlField, failure_ttl_ms.toString()) + }); + await user.type(failureTtlField, failure_ttl_ms.toString()); } if (time_til_dormant_ms) { const inactivityTtlField = screen.getByRole("checkbox", { name: /Dormancy Threshold/i, - }) - await user.type(inactivityTtlField, time_til_dormant_ms.toString()) + }); + await user.type(inactivityTtlField, time_til_dormant_ms.toString()); } if (time_til_dormant_autodelete_ms) { const dormancyAutoDeletionField = screen.getByRole("checkbox", { name: /Dormancy Auto-Deletion/i, - }) + }); await user.type( dormancyAutoDeletionField, time_til_dormant_autodelete_ms.toString(), - ) + ); } const submitButton = await screen.findByText( FooterFormLanguage.defaultSubmitLabel, - ) - await user.click(submitButton) + ); + await user.click(submitButton); // User needs to confirm dormancy and autodeletion fields. - const confirmButton = await screen.findByTestId("confirm-button") - await user.click(confirmButton) -} + const confirmButton = await screen.findByTestId("confirm-button"); + await user.click(confirmButton); +}; describe("TemplateSchedulePage", () => { beforeEach(() => { jest .spyOn(API, "getEntitlements") - .mockResolvedValue(MockEntitlementsWithScheduling) + .mockResolvedValue(MockEntitlementsWithScheduling); // remove when https://github.com/coder/coder/milestone/19 is completed. - jest.spyOn(API, "getExperiments").mockResolvedValue(["workspace_actions"]) - }) + jest.spyOn(API, "getExperiments").mockResolvedValue(["workspace_actions"]); + }); it("succeeds", async () => { - await renderTemplateSchedulePage() + await renderTemplateSchedulePage(); jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, ...validFormValues, - }) - await fillAndSubmitForm(validFormValues) - await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)) - }) + }); + await fillAndSubmitForm(validFormValues); + await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)); + }); test("default and max ttl is converted to and from hours", async () => { - await renderTemplateSchedulePage() + await renderTemplateSchedulePage(); jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, ...validFormValues, - }) + }); - await fillAndSubmitForm(validFormValues) - await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)) + await fillAndSubmitForm(validFormValues); + await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)); await waitFor(() => expect(API.updateTemplateMeta).toBeCalledWith( "test-template", @@ -143,19 +145,19 @@ describe("TemplateSchedulePage", () => { max_ttl_ms: (validFormValues.max_ttl_ms || 0) * 3600000, }), ), - ) - }) + ); + }); test("failure, dormancy, and dormancy auto-deletion converted to and from days", async () => { - await renderTemplateSchedulePage() + await renderTemplateSchedulePage(); jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({ ...MockTemplate, ...validFormValues, - }) + }); - await fillAndSubmitForm(validFormValues) - await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)) + await fillAndSubmitForm(validFormValues); + await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)); await waitFor(() => expect(API.updateTemplateMeta).toBeCalledWith( "test-template", @@ -167,168 +169,168 @@ describe("TemplateSchedulePage", () => { (validFormValues.time_til_dormant_autodelete_ms || 0) * 86400000, }), ), - ) - }) + ); + }); it("allows a default ttl of 7 days", () => { const values: TemplateScheduleFormValues = { ...validFormValues, default_ttl_ms: 24 * 7, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) + }; + const validate = () => getValidationSchema().validateSync(values); + expect(validate).not.toThrowError(); + }); it("allows default ttl of 0", () => { const values: TemplateScheduleFormValues = { ...validFormValues, default_ttl_ms: 0, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) + }; + const validate = () => getValidationSchema().validateSync(values); + expect(validate).not.toThrowError(); + }); it("allows a default ttl of 30 days", () => { const values: TemplateScheduleFormValues = { ...validFormValues, default_ttl_ms: 24 * 30, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) + }; + const validate = () => getValidationSchema().validateSync(values); + expect(validate).not.toThrowError(); + }); it("disallows a default ttl of 30 days + 1 hour", () => { const values: TemplateScheduleFormValues = { ...validFormValues, default_ttl_ms: 24 * 30 + 1, - } - const validate = () => getValidationSchema().validateSync(values) + }; + const validate = () => getValidationSchema().validateSync(values); expect(validate).toThrowError( t("defaultTTLMaxError", { ns: "templateSettingsPage" }).toString(), - ) - }) + ); + }); it("allows a failure ttl of 7 days", () => { const values: TemplateScheduleFormValues = { ...validFormValues, failure_ttl_ms: 86400000 * 7, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) + }; + const validate = () => getValidationSchema().validateSync(values); + expect(validate).not.toThrowError(); + }); it("allows failure ttl of 0", () => { const values: TemplateScheduleFormValues = { ...validFormValues, failure_ttl_ms: 0, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) + }; + const validate = () => getValidationSchema().validateSync(values); + expect(validate).not.toThrowError(); + }); it("disallows a negative failure ttl", () => { const values: TemplateScheduleFormValues = { ...validFormValues, failure_ttl_ms: -1, - } - const validate = () => getValidationSchema().validateSync(values) + }; + const validate = () => getValidationSchema().validateSync(values); expect(validate).toThrowError( "Failure cleanup days must not be less than 0.", - ) - }) + ); + }); it("allows an inactivity ttl of 7 days", () => { const values: TemplateScheduleFormValues = { ...validFormValues, time_til_dormant_ms: 86400000 * 7, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) + }; + const validate = () => getValidationSchema().validateSync(values); + expect(validate).not.toThrowError(); + }); it("allows an inactivity ttl of 0", () => { const values: TemplateScheduleFormValues = { ...validFormValues, time_til_dormant_ms: 0, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) + }; + const validate = () => getValidationSchema().validateSync(values); + expect(validate).not.toThrowError(); + }); it("disallows a negative inactivity ttl", () => { const values: TemplateScheduleFormValues = { ...validFormValues, time_til_dormant_ms: -1, - } - const validate = () => getValidationSchema().validateSync(values) + }; + const validate = () => getValidationSchema().validateSync(values); expect(validate).toThrowError( "Dormancy threshold days must not be less than 0.", - ) - }) + ); + }); it("allows a dormancy ttl of 7 days", () => { const values: TemplateScheduleFormValues = { ...validFormValues, time_til_dormant_autodelete_ms: 86400000 * 7, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) + }; + const validate = () => getValidationSchema().validateSync(values); + expect(validate).not.toThrowError(); + }); it("allows a dormancy ttl of 0", () => { const values: TemplateScheduleFormValues = { ...validFormValues, time_til_dormant_autodelete_ms: 0, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) + }; + const validate = () => getValidationSchema().validateSync(values); + expect(validate).not.toThrowError(); + }); it("disallows a negative inactivity ttl", () => { const values: TemplateScheduleFormValues = { ...validFormValues, time_til_dormant_autodelete_ms: -1, - } - const validate = () => getValidationSchema().validateSync(values) + }; + const validate = () => getValidationSchema().validateSync(values); expect(validate).toThrowError( "Dormancy auto-deletion days must not be less than 0.", - ) - }) + ); + }); it("allows an autostop requirement weeks of 1", () => { const values: TemplateScheduleFormValues = { ...validFormValues, autostop_requirement_days_of_week: "saturday", autostop_requirement_weeks: 1, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) + }; + const validate = () => getValidationSchema().validateSync(values); + expect(validate).not.toThrowError(); + }); it("allows a autostop requirement weeks of 16", () => { const values: TemplateScheduleFormValues = { ...validFormValues, autostop_requirement_weeks: 16, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).not.toThrowError() - }) + }; + const validate = () => getValidationSchema().validateSync(values); + expect(validate).not.toThrowError(); + }); it("disallows a autostop requirement weeks of 0", () => { const values: TemplateScheduleFormValues = { ...validFormValues, autostop_requirement_weeks: 0, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).toThrowError() - }) + }; + const validate = () => getValidationSchema().validateSync(values); + expect(validate).toThrowError(); + }); it("disallows a autostop requirement weeks of 17", () => { const values: TemplateScheduleFormValues = { ...validFormValues, autostop_requirement_weeks: 17, - } - const validate = () => getValidationSchema().validateSync(values) - expect(validate).toThrowError() - }) -}) + }; + const validate = () => getValidationSchema().validateSync(values); + expect(validate).toThrowError(); + }); +}); diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx index 0cf726705e..4e2b83c657 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx @@ -1,30 +1,30 @@ -import { useMutation } from "@tanstack/react-query" -import { updateTemplateMeta } from "api/api" -import { UpdateTemplateMeta } from "api/typesGenerated" -import { useDashboard } from "components/Dashboard/DashboardProvider" -import { displaySuccess } from "components/GlobalSnackbar/utils" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { useNavigate, useParams } from "react-router-dom" -import { pageTitle } from "utils/page" -import { useTemplateSettingsContext } from "../TemplateSettingsLayout" -import { TemplateSchedulePageView } from "./TemplateSchedulePageView" -import { useLocalStorage } from "hooks" +import { useMutation } from "@tanstack/react-query"; +import { updateTemplateMeta } from "api/api"; +import { UpdateTemplateMeta } from "api/typesGenerated"; +import { useDashboard } from "components/Dashboard/DashboardProvider"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { useNavigate, useParams } from "react-router-dom"; +import { pageTitle } from "utils/page"; +import { useTemplateSettingsContext } from "../TemplateSettingsLayout"; +import { TemplateSchedulePageView } from "./TemplateSchedulePageView"; +import { useLocalStorage } from "hooks"; const TemplateSchedulePage: FC = () => { - const { template: templateName } = useParams() as { template: string } - const navigate = useNavigate() - const { template } = useTemplateSettingsContext() - const { entitlements, experiments } = useDashboard() + const { template: templateName } = useParams() as { template: string }; + const navigate = useNavigate(); + const { template } = useTemplateSettingsContext(); + const { entitlements, experiments } = useDashboard(); const allowAdvancedScheduling = - entitlements.features["advanced_template_scheduling"].enabled + entitlements.features["advanced_template_scheduling"].enabled; // This check can be removed when https://github.com/coder/coder/milestone/19 // is merged up - const allowWorkspaceActions = experiments.includes("workspace_actions") + const allowWorkspaceActions = experiments.includes("workspace_actions"); const allowAutostopRequirement = experiments.includes( "template_autostop_requirement", - ) - const { clearLocal } = useLocalStorage() + ); + const { clearLocal } = useLocalStorage(); const { mutate: updateTemplate, @@ -34,13 +34,13 @@ const TemplateSchedulePage: FC = () => { (data: UpdateTemplateMeta) => updateTemplateMeta(template.id, data), { onSuccess: () => { - displaySuccess("Template updated successfully") + displaySuccess("Template updated successfully"); // clear browser storage of workspaces impending deletion - clearLocal("dismissedWorkspaceList") // workspaces page - clearLocal("dismissedWorkspace") // workspace page + clearLocal("dismissedWorkspaceList"); // workspaces page + clearLocal("dismissedWorkspace"); // workspace page }, }, - ) + ); return ( <> @@ -55,17 +55,17 @@ const TemplateSchedulePage: FC = () => { template={template} submitError={submitError} onCancel={() => { - navigate(`/templates/${templateName}`) + navigate(`/templates/${templateName}`); }} onSubmit={(templateScheduleSettings) => { updateTemplate({ ...template, ...templateScheduleSettings, - }) + }); }} /> - ) -} + ); +}; -export default TemplateSchedulePage +export default TemplateSchedulePage; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.stories.tsx index 130d32fafc..cb6e254662 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.stories.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.stories.tsx @@ -1,8 +1,8 @@ -import { action } from "@storybook/addon-actions" -import { Meta, StoryObj } from "@storybook/react" -import { MockTemplate } from "testHelpers/entities" -import { TemplateSchedulePageView } from "./TemplateSchedulePageView" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { action } from "@storybook/addon-actions"; +import { Meta, StoryObj } from "@storybook/react"; +import { MockTemplate } from "testHelpers/entities"; +import { TemplateSchedulePageView } from "./TemplateSchedulePageView"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const queryClient = new QueryClient({ defaultOptions: { @@ -13,7 +13,7 @@ const queryClient = new QueryClient({ networkMode: "offlineFirst", }, }, -}) +}); const meta: Meta = { title: "pages/TemplateSchedulePageView", @@ -25,9 +25,9 @@ const meta: Meta = { ), ], -} -export default meta -type Story = StoryObj +}; +export default meta; +type Story = StoryObj; const defaultArgs = { allowAdvancedScheduling: true, @@ -35,12 +35,12 @@ const defaultArgs = { template: MockTemplate, onSubmit: action("onSubmit"), onCancel: action("cancel"), -} +}; export const Example: Story = { args: { ...defaultArgs }, -} +}; export const CantSetMaxTTL: Story = { args: { ...defaultArgs, allowAdvancedScheduling: false }, -} +}; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.tsx index 3da92d535f..1ad43692ae 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.tsx @@ -1,19 +1,21 @@ -import { Template, UpdateTemplateMeta } from "api/typesGenerated" -import { ComponentProps, FC } from "react" -import { TemplateScheduleForm } from "./TemplateScheduleForm/TemplateScheduleForm" -import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader" -import { makeStyles } from "@mui/styles" +import { Template, UpdateTemplateMeta } from "api/typesGenerated"; +import { ComponentProps, FC } from "react"; +import { TemplateScheduleForm } from "./TemplateScheduleForm/TemplateScheduleForm"; +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; +import { makeStyles } from "@mui/styles"; export interface TemplateSchedulePageViewProps { - template: Template - onSubmit: (data: UpdateTemplateMeta) => void - onCancel: () => void - isSubmitting: boolean - submitError?: unknown - initialTouched?: ComponentProps["initialTouched"] - allowAdvancedScheduling: boolean - allowWorkspaceActions: boolean - allowAutostopRequirement: boolean + template: Template; + onSubmit: (data: UpdateTemplateMeta) => void; + onCancel: () => void; + isSubmitting: boolean; + submitError?: unknown; + initialTouched?: ComponentProps< + typeof TemplateScheduleForm + >["initialTouched"]; + allowAdvancedScheduling: boolean; + allowWorkspaceActions: boolean; + allowAutostopRequirement: boolean; } export const TemplateSchedulePageView: FC = ({ @@ -27,7 +29,7 @@ export const TemplateSchedulePageView: FC = ({ submitError, initialTouched, }) => { - const styles = useStyles() + const styles = useStyles(); return ( <> @@ -47,11 +49,11 @@ export const TemplateSchedulePageView: FC = ({ error={submitError} /> - ) -} + ); +}; const useStyles = makeStyles(() => ({ pageHeader: { paddingTop: 0, }, -})) +})); diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx index e816226142..4cec9e2c9e 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsLayout.tsx @@ -1,15 +1,15 @@ -import { makeStyles } from "@mui/styles" -import { Sidebar } from "./Sidebar" -import { Stack } from "components/Stack/Stack" -import { createContext, FC, Suspense, useContext } from "react" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "../../utils/page" -import { Loader } from "components/Loader/Loader" -import { Outlet, useParams } from "react-router-dom" -import { Margins } from "components/Margins/Margins" -import { checkAuthorization, getTemplateByName } from "api/api" -import { useQuery } from "@tanstack/react-query" -import { useOrganizationId } from "hooks/useOrganizationId" +import { makeStyles } from "@mui/styles"; +import { Sidebar } from "./Sidebar"; +import { Stack } from "components/Stack/Stack"; +import { createContext, FC, Suspense, useContext } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "../../utils/page"; +import { Loader } from "components/Loader/Loader"; +import { Outlet, useParams } from "react-router-dom"; +import { Margins } from "components/Margins/Margins"; +import { checkAuthorization, getTemplateByName } from "api/api"; +import { useQuery } from "@tanstack/react-query"; +import { useOrganizationId } from "hooks/useOrganizationId"; const templatePermissions = (templateId: string) => ({ @@ -20,51 +20,55 @@ const templatePermissions = (templateId: string) => }, action: "update", }, - }) as const + }) as const; const fetchTemplateSettings = async (orgId: string, name: string) => { - const template = await getTemplateByName(orgId, name) + const template = await getTemplateByName(orgId, name); const permissions = await checkAuthorization({ checks: templatePermissions(template.id), - }) + }); return { template, permissions, - } -} + }; +}; -export const getTemplateQuery = (name: string) => ["template", name, "settings"] +export const getTemplateQuery = (name: string) => [ + "template", + name, + "settings", +]; const useTemplate = (orgId: string, name: string) => { return useQuery({ queryKey: getTemplateQuery(name), queryFn: () => fetchTemplateSettings(orgId, name), keepPreviousData: true, - }) -} + }); +}; const TemplateSettingsContext = createContext< Awaited> | undefined ->(undefined) +>(undefined); export const useTemplateSettingsContext = () => { - const context = useContext(TemplateSettingsContext) + const context = useContext(TemplateSettingsContext); if (!context) { throw new Error( "useTemplateSettingsContext must be used within a TemplateSettingsContext.Provider", - ) + ); } - return context -} + return context; +}; export const TemplateSettingsLayout: FC = () => { - const styles = useStyles() - const orgId = useOrganizationId() - const { template: templateName } = useParams() as { template: string } - const { data: settings } = useTemplate(orgId, templateName) + const styles = useStyles(); + const orgId = useOrganizationId(); + const { template: templateName } = useParams() as { template: string }; + const { data: settings } = useTemplate(orgId, templateName); return ( <> @@ -89,8 +93,8 @@ export const TemplateSettingsLayout: FC = () => { )} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ wrapper: { @@ -100,4 +104,4 @@ const useStyles = makeStyles((theme) => ({ content: { width: "100%", }, -})) +})); diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariableField.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariableField.tsx index 0b1c56b65c..e362912553 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariableField.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariableField.tsx @@ -1,21 +1,21 @@ -import FormControlLabel from "@mui/material/FormControlLabel" -import Radio from "@mui/material/Radio" -import RadioGroup from "@mui/material/RadioGroup" -import TextField from "@mui/material/TextField" -import { TemplateVersionVariable } from "api/typesGenerated" -import { FC, useState } from "react" -import { useTranslation } from "react-i18next" +import FormControlLabel from "@mui/material/FormControlLabel"; +import Radio from "@mui/material/Radio"; +import RadioGroup from "@mui/material/RadioGroup"; +import TextField from "@mui/material/TextField"; +import { TemplateVersionVariable } from "api/typesGenerated"; +import { FC, useState } from "react"; +import { useTranslation } from "react-i18next"; export const SensitiveVariableHelperText = () => { - const { t } = useTranslation("templateVariablesPage") - return {t("sensitiveVariableHelperText")} -} + const { t } = useTranslation("templateVariablesPage"); + return {t("sensitiveVariableHelperText")}; +}; export interface TemplateVariableFieldProps { - templateVersionVariable: TemplateVersionVariable - initialValue: string - disabled: boolean - onChange: (value: string) => void + templateVersionVariable: TemplateVersionVariable; + initialValue: string; + disabled: boolean; + onChange: (value: string) => void; } export const TemplateVariableField: FC = ({ @@ -25,13 +25,13 @@ export const TemplateVariableField: FC = ({ onChange, ...props }) => { - const [variableValue, setVariableValue] = useState(initialValue) + const [variableValue, setVariableValue] = useState(initialValue); if (isBoolean(templateVersionVariable)) { return ( { - onChange(event.target.value) + onChange(event.target.value); }} > = ({ label="False" /> - ) + ); } return ( @@ -71,13 +71,13 @@ export const TemplateVariableField: FC = ({ : templateVersionVariable.default_value } onChange={(event) => { - setVariableValue(event.target.value) - onChange(event.target.value) + setVariableValue(event.target.value); + onChange(event.target.value); }} /> - ) -} + ); +}; const isBoolean = (variable: TemplateVersionVariable) => { - return variable.type === "bool" -} + return variable.type === "bool"; +}; diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesForm.tsx index 0b6a71725b..1b9815f43f 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesForm.tsx @@ -3,32 +3,32 @@ import { TemplateVersion, TemplateVersionVariable, VariableValue, -} from "api/typesGenerated" -import { FormikContextType, FormikTouched, useFormik } from "formik" -import { FC } from "react" -import { getFormHelpers } from "utils/formUtils" -import * as Yup from "yup" -import { useTranslation } from "react-i18next" +} from "api/typesGenerated"; +import { FormikContextType, FormikTouched, useFormik } from "formik"; +import { FC } from "react"; +import { getFormHelpers } from "utils/formUtils"; +import * as Yup from "yup"; +import { useTranslation } from "react-i18next"; import { FormFields, FormSection, HorizontalForm, FormFooter, -} from "components/Form/Form" +} from "components/Form/Form"; import { SensitiveVariableHelperText, TemplateVariableField, -} from "./TemplateVariableField" +} from "./TemplateVariableField"; export interface TemplateVariablesForm { - templateVersion: TemplateVersion - templateVariables: TemplateVersionVariable[] - onSubmit: (data: CreateTemplateVersionRequest) => void - onCancel: () => void - isSubmitting: boolean - error?: unknown + templateVersion: TemplateVersion; + templateVariables: TemplateVersionVariable[]; + onSubmit: (data: CreateTemplateVersionRequest) => void; + onCancel: () => void; + isSubmitting: boolean; + error?: unknown; // Helpful to show field errors on Storybook - initialTouched?: FormikTouched + initialTouched?: FormikTouched; } export const TemplateVariablesForm: FC = ({ templateVersion, @@ -40,7 +40,7 @@ export const TemplateVariablesForm: FC = ({ initialTouched, }) => { const initialUserVariableValues = - selectInitialUserVariableValues(templateVariables) + selectInitialUserVariableValues(templateVariables); const form: FormikContextType = useFormik({ initialValues: { @@ -59,12 +59,12 @@ export const TemplateVariablesForm: FC = ({ }), onSubmit, initialTouched, - }) + }); const getFieldHelpers = getFormHelpers( form, error, - ) - const { t } = useTranslation("templateVariablesPage") + ); + const { t } = useTranslation("templateVariablesPage"); return ( = ({ aria-label={t("formAriaLabel").toString()} > {templateVariables.map((templateVariable, index) => { - let fieldHelpers + let fieldHelpers; if (templateVariable.sensitive) { fieldHelpers = getFieldHelpers( "user_variable_values[" + index + "].value", , - ) + ); } else { fieldHelpers = getFieldHelpers( "user_variable_values[" + index + "].value", - ) + ); } return ( @@ -100,88 +100,88 @@ export const TemplateVariablesForm: FC = ({ await form.setFieldValue("user_variable_values." + index, { name: templateVariable.name, value: value, - }) + }); }} /> - ) + ); })} - ) -} + ); +}; export const selectInitialUserVariableValues = ( templateVariables: TemplateVersionVariable[], ): VariableValue[] => { - const defaults: VariableValue[] = [] + const defaults: VariableValue[] = []; templateVariables.forEach((templateVariable) => { // Boolean variables must be always either "true" or "false" if (templateVariable.type === "bool" && templateVariable.value === "") { defaults.push({ name: templateVariable.name, value: templateVariable.default_value, - }) - return + }); + return; } if (templateVariable.sensitive) { defaults.push({ name: templateVariable.name, value: "", - }) - return + }); + return; } if (templateVariable.required && templateVariable.value === "") { defaults.push({ name: templateVariable.name, value: templateVariable.default_value, - }) - return + }); + return; } defaults.push({ name: templateVariable.name, value: templateVariable.value, - }) - }) - return defaults -} + }); + }); + return defaults; +}; const ValidationSchemaForTemplateVariables = ( ns: string, templateVariables: TemplateVersionVariable[], ): Yup.AnySchema => { - const { t } = useTranslation(ns) + const { t } = useTranslation(ns); return Yup.array() .of( Yup.object().shape({ name: Yup.string().required(), value: Yup.string().test("verify with template", (val, ctx) => { - const name = ctx.parent.name + const name = ctx.parent.name; const templateVariable = templateVariables.find( (variable) => variable.name === name, - ) + ); if (templateVariable && templateVariable.sensitive) { // It's possible that the secret is already stored in database, // so we can't properly verify the "required" condition. - return true + return true; } if (templateVariable && templateVariable.required) { if (!val || val.length === 0) { return ctx.createError({ path: ctx.path, message: t("validationRequiredVariable").toString(), - }) + }); } } - return true + return true; }), }), ) - .required() -} + .required(); +}; diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx index 5ef8a3a2b7..e0b7fe8e5b 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.test.tsx @@ -1,13 +1,13 @@ -import { screen } from "@testing-library/react" -import userEvent from "@testing-library/user-event" +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { renderWithTemplateSettingsLayout, waitForLoaderToBeRemoved, -} from "testHelpers/renderHelpers" -import * as API from "api/api" -import i18next from "i18next" -import TemplateVariablesPage from "./TemplateVariablesPage" -import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter" +} from "testHelpers/renderHelpers"; +import * as API from "api/api"; +import i18next from "i18next"; +import TemplateVariablesPage from "./TemplateVariablesPage"; +import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"; import { MockTemplate, MockTemplateVersion, @@ -15,156 +15,156 @@ import { MockTemplateVersionVariable2, MockTemplateVersion2, MockTemplateVersionVariable5, -} from "testHelpers/entities" +} from "testHelpers/entities"; -const { t } = i18next +const { t } = i18next; const validFormValues = { first_variable: "Hello world", second_variable: "123", -} +}; -const pageTitleText = t("title", { ns: "templateVariablesPage" }) +const pageTitleText = t("title", { ns: "templateVariablesPage" }); const validationRequiredField = t("validationRequiredVariable", { ns: "templateVariablesPage", -}) +}); const renderTemplateVariablesPage = async () => { renderWithTemplateSettingsLayout(, { route: `/templates/${MockTemplate.name}/variables`, path: `/templates/:template/variables`, extraRoutes: [{ path: `/templates/${MockTemplate.name}`, element: <> }], - }) - await waitForLoaderToBeRemoved() -} + }); + await waitForLoaderToBeRemoved(); +}; describe("TemplateVariablesPage", () => { it("renders with variables", async () => { - jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate) + jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate); jest .spyOn(API, "getTemplateVersion") - .mockResolvedValueOnce(MockTemplateVersion) + .mockResolvedValueOnce(MockTemplateVersion); jest .spyOn(API, "getTemplateVersionVariables") .mockResolvedValueOnce([ MockTemplateVersionVariable1, MockTemplateVersionVariable2, - ]) + ]); - await renderTemplateVariablesPage() + await renderTemplateVariablesPage(); - const element = await screen.findByText(pageTitleText) - expect(element).toBeDefined() + const element = await screen.findByText(pageTitleText); + expect(element).toBeDefined(); const firstVariable = await screen.findByLabelText( MockTemplateVersionVariable1.name, - ) - expect(firstVariable).toBeDefined() + ); + expect(firstVariable).toBeDefined(); const secondVariable = await screen.findByLabelText( MockTemplateVersionVariable2.name, - ) - expect(secondVariable).toBeDefined() - }) + ); + expect(secondVariable).toBeDefined(); + }); it("user submits the form successfully", async () => { - jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate) + jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate); jest .spyOn(API, "getTemplateVersion") - .mockResolvedValueOnce(MockTemplateVersion) + .mockResolvedValueOnce(MockTemplateVersion); jest .spyOn(API, "getTemplateVersionVariables") .mockResolvedValueOnce([ MockTemplateVersionVariable1, MockTemplateVersionVariable2, - ]) + ]); jest .spyOn(API, "createTemplateVersion") - .mockResolvedValueOnce(MockTemplateVersion2) + .mockResolvedValueOnce(MockTemplateVersion2); jest.spyOn(API, "updateActiveTemplateVersion").mockResolvedValueOnce({ message: "done", - }) + }); - await renderTemplateVariablesPage() + await renderTemplateVariablesPage(); - const element = await screen.findByText(pageTitleText) - expect(element).toBeDefined() + const element = await screen.findByText(pageTitleText); + expect(element).toBeDefined(); const firstVariable = await screen.findByLabelText( MockTemplateVersionVariable1.name, - ) - expect(firstVariable).toBeDefined() + ); + expect(firstVariable).toBeDefined(); const secondVariable = await screen.findByLabelText( MockTemplateVersionVariable2.name, - ) - expect(secondVariable).toBeDefined() + ); + expect(secondVariable).toBeDefined(); // Fill the form const firstVariableField = await screen.findByLabelText( MockTemplateVersionVariable1.name, - ) - await userEvent.clear(firstVariableField) - await userEvent.type(firstVariableField, validFormValues.first_variable) + ); + await userEvent.clear(firstVariableField); + await userEvent.type(firstVariableField, validFormValues.first_variable); const secondVariableField = await screen.findByLabelText( MockTemplateVersionVariable2.name, - ) - await userEvent.clear(secondVariableField) - await userEvent.type(secondVariableField, validFormValues.second_variable) + ); + await userEvent.clear(secondVariableField); + await userEvent.type(secondVariableField, validFormValues.second_variable); // Submit the form const submitButton = await screen.findByText( FooterFormLanguage.defaultSubmitLabel, - ) - await userEvent.click(submitButton) + ); + await userEvent.click(submitButton); // Wait for the success message - await screen.findByText("Template updated successfully") - }) + await screen.findByText("Template updated successfully"); + }); it("user forgets to fill the required field", async () => { - jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate) + jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate); jest .spyOn(API, "getTemplateVersion") - .mockResolvedValueOnce(MockTemplateVersion) + .mockResolvedValueOnce(MockTemplateVersion); jest .spyOn(API, "getTemplateVersionVariables") .mockResolvedValueOnce([ MockTemplateVersionVariable1, MockTemplateVersionVariable5, - ]) + ]); jest .spyOn(API, "createTemplateVersion") - .mockResolvedValueOnce(MockTemplateVersion2) + .mockResolvedValueOnce(MockTemplateVersion2); jest.spyOn(API, "updateActiveTemplateVersion").mockResolvedValueOnce({ message: "done", - }) + }); - await renderTemplateVariablesPage() + await renderTemplateVariablesPage(); - const element = await screen.findByText(pageTitleText) - expect(element).toBeDefined() + const element = await screen.findByText(pageTitleText); + expect(element).toBeDefined(); const firstVariable = await screen.findByLabelText( MockTemplateVersionVariable1.name, - ) - expect(firstVariable).toBeDefined() + ); + expect(firstVariable).toBeDefined(); const fifthVariable = await screen.findByLabelText( MockTemplateVersionVariable5.name, - ) - expect(fifthVariable).toBeDefined() + ); + expect(fifthVariable).toBeDefined(); // Submit the form const submitButton = await screen.findByText( FooterFormLanguage.defaultSubmitLabel, - ) - await userEvent.click(submitButton) + ); + await userEvent.click(submitButton); // Check validation error - const validationError = await screen.findByText(validationRequiredField) - expect(validationError).toBeDefined() - }) -}) + const validationError = await screen.findByText(validationRequiredField); + expect(validationError).toBeDefined(); + }); +}); diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx index 64b0ed41b8..4ed28cc88e 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage.tsx @@ -1,28 +1,28 @@ -import { useMachine } from "@xstate/react" +import { useMachine } from "@xstate/react"; import { CreateTemplateVersionRequest, TemplateVersionVariable, VariableValue, -} from "api/typesGenerated" -import { displaySuccess } from "components/GlobalSnackbar/utils" -import { useOrganizationId } from "hooks/useOrganizationId" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { useTranslation } from "react-i18next" -import { useNavigate, useParams } from "react-router-dom" -import { templateVariablesMachine } from "xServices/template/templateVariablesXService" -import { pageTitle } from "../../../utils/page" -import { useTemplateSettingsContext } from "../TemplateSettingsLayout" -import { TemplateVariablesPageView } from "./TemplateVariablesPageView" +} from "api/typesGenerated"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; +import { useOrganizationId } from "hooks/useOrganizationId"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useParams } from "react-router-dom"; +import { templateVariablesMachine } from "xServices/template/templateVariablesXService"; +import { pageTitle } from "../../../utils/page"; +import { useTemplateSettingsContext } from "../TemplateSettingsLayout"; +import { TemplateVariablesPageView } from "./TemplateVariablesPageView"; export const TemplateVariablesPage: FC = () => { const { template: templateName } = useParams() as { - organization: string - template: string - } - const organizationId = useOrganizationId() - const { template } = useTemplateSettingsContext() - const navigate = useNavigate() + organization: string; + template: string; + }; + const organizationId = useOrganizationId(); + const { template } = useTemplateSettingsContext(); + const navigate = useNavigate(); const [state, send] = useMachine(templateVariablesMachine, { context: { organizationId, @@ -30,19 +30,19 @@ export const TemplateVariablesPage: FC = () => { }, actions: { onUpdateTemplate: () => { - displaySuccess("Template updated successfully") + displaySuccess("Template updated successfully"); }, }, - }) + }); const { activeTemplateVersion, templateVariables, getTemplateDataError, updateTemplateError, jobError, - } = state.context + } = state.context; - const { t } = useTranslation("templateVariablesPage") + const { t } = useTranslation("templateVariablesPage"); return ( <> @@ -59,50 +59,50 @@ export const TemplateVariablesPage: FC = () => { jobError, }} onCancel={() => { - navigate(`/templates/${templateName}`) + navigate(`/templates/${templateName}`); }} onSubmit={(formData) => { const request = filterEmptySensitiveVariables( formData, templateVariables, - ) - send({ type: "UPDATE_TEMPLATE_EVENT", request: request }) + ); + send({ type: "UPDATE_TEMPLATE_EVENT", request: request }); }} /> - ) -} + ); +}; const filterEmptySensitiveVariables = ( request: CreateTemplateVersionRequest, templateVariables?: TemplateVersionVariable[], ): CreateTemplateVersionRequest => { - const filtered: VariableValue[] = [] + const filtered: VariableValue[] = []; if (!templateVariables) { - return request + return request; } if (request.user_variable_values) { request.user_variable_values.forEach((variableValue) => { const templateVariable = templateVariables.find( (t) => t.name === variableValue.name, - ) + ); if ( templateVariable && templateVariable.sensitive && variableValue.value === "" ) { - return + return; } - filtered.push(variableValue) - }) + filtered.push(variableValue); + }); } return { ...request, user_variable_values: filtered, - } -} + }; +}; -export default TemplateVariablesPage +export default TemplateVariablesPage; diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx index 2156893b62..f5ef6724a0 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.stories.tsx @@ -1,5 +1,5 @@ -import { action } from "@storybook/addon-actions" -import { Story } from "@storybook/react" +import { action } from "@storybook/addon-actions"; +import { Story } from "@storybook/react"; import { mockApiError, MockTemplateVersion, @@ -8,28 +8,28 @@ import { MockTemplateVersionVariable3, MockTemplateVersionVariable4, MockTemplateVersionVariable5, -} from "testHelpers/entities" +} from "testHelpers/entities"; import { TemplateVariablesPageView, TemplateVariablesPageViewProps, -} from "./TemplateVariablesPageView" +} from "./TemplateVariablesPageView"; export default { title: "pages/TemplateVariablesPageView", component: TemplateVariablesPageView, -} +}; const TemplateVariables: Story = (args) => ( -) +); -export const Loading = TemplateVariables.bind({}) +export const Loading = TemplateVariables.bind({}); Loading.args = { onSubmit: action("onSubmit"), onCancel: action("cancel"), -} +}; -export const Basic = TemplateVariables.bind({}) +export const Basic = TemplateVariables.bind({}); Basic.args = { templateVersion: MockTemplateVersion, templateVariables: [ @@ -40,12 +40,12 @@ Basic.args = { ], onSubmit: action("onSubmit"), onCancel: action("cancel"), -} +}; // This example isn't fully supported. As "user_variable_values" is an array, // FormikTouched can't properly handle this. // See: https://github.com/jaredpalmer/formik/issues/2022 -export const RequiredVariable = TemplateVariables.bind({}) +export const RequiredVariable = TemplateVariables.bind({}); RequiredVariable.args = { templateVersion: MockTemplateVersion, templateVariables: [ @@ -57,9 +57,9 @@ RequiredVariable.args = { initialTouched: { user_variable_values: true, }, -} +}; -export const WithUpdateTemplateError = TemplateVariables.bind({}) +export const WithUpdateTemplateError = TemplateVariables.bind({}); WithUpdateTemplateError.args = { templateVersion: MockTemplateVersion, templateVariables: [ @@ -75,9 +75,9 @@ WithUpdateTemplateError.args = { }, onSubmit: action("onSubmit"), onCancel: action("cancel"), -} +}; -export const WithJobError = TemplateVariables.bind({}) +export const WithJobError = TemplateVariables.bind({}); WithJobError.args = { templateVersion: MockTemplateVersion, templateVariables: [ @@ -92,4 +92,4 @@ WithJobError.args = { }, onSubmit: action("onSubmit"), onCancel: action("cancel"), -} +}; diff --git a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.tsx index fad5b1e94b..139299a85e 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPageView.tsx @@ -2,30 +2,30 @@ import { CreateTemplateVersionRequest, TemplateVersion, TemplateVersionVariable, -} from "api/typesGenerated" -import { Alert } from "components/Alert/Alert" -import { Loader } from "components/Loader/Loader" -import { ComponentProps, FC } from "react" -import { TemplateVariablesForm } from "./TemplateVariablesForm" -import { makeStyles } from "@mui/styles" -import { useTranslation } from "react-i18next" -import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader" -import { ErrorAlert } from "components/Alert/ErrorAlert" +} from "api/typesGenerated"; +import { Alert } from "components/Alert/Alert"; +import { Loader } from "components/Loader/Loader"; +import { ComponentProps, FC } from "react"; +import { TemplateVariablesForm } from "./TemplateVariablesForm"; +import { makeStyles } from "@mui/styles"; +import { useTranslation } from "react-i18next"; +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; export interface TemplateVariablesPageViewProps { - templateVersion?: TemplateVersion - templateVariables?: TemplateVersionVariable[] - onSubmit: (data: CreateTemplateVersionRequest) => void - onCancel: () => void - isSubmitting: boolean + templateVersion?: TemplateVersion; + templateVariables?: TemplateVersionVariable[]; + onSubmit: (data: CreateTemplateVersionRequest) => void; + onCancel: () => void; + isSubmitting: boolean; errors?: { - getTemplateDataError?: unknown - updateTemplateError?: unknown - jobError?: TemplateVersion["job"]["error"] - } + getTemplateDataError?: unknown; + updateTemplateError?: unknown; + jobError?: TemplateVersion["job"]["error"]; + }; initialTouched?: ComponentProps< typeof TemplateVariablesForm - >["initialTouched"] + >["initialTouched"]; } export const TemplateVariablesPageView: FC = ({ @@ -37,14 +37,14 @@ export const TemplateVariablesPageView: FC = ({ errors = {}, initialTouched, }) => { - const classes = useStyles() + const classes = useStyles(); const isLoading = !templateVersion && !templateVariables && !errors.getTemplateDataError && - !errors.updateTemplateError - const { t } = useTranslation("templateVariablesPage") - const hasError = Object.values(errors).some((error) => Boolean(error)) + !errors.updateTemplateError; + const { t } = useTranslation("templateVariablesPage"); + const hasError = Object.values(errors).some((error) => Boolean(error)); return ( <> @@ -77,8 +77,8 @@ export const TemplateVariablesPageView: FC = ({ {t("unusedVariablesNotice")} )} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ errorContainer: { @@ -92,4 +92,4 @@ const useStyles = makeStyles((theme) => ({ pageHeader: { paddingTop: 0, }, -})) +})); diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/FileDialog.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/FileDialog.tsx index 9e99016054..4c3f9ce234 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/FileDialog.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/FileDialog.tsx @@ -1,56 +1,56 @@ -import TextField from "@mui/material/TextField" -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" -import { Stack } from "components/Stack/Stack" -import { ChangeEvent, FC, useState } from "react" -import Typography from "@mui/material/Typography" -import { allowedExtensions, isAllowedFile } from "utils/templateVersion" -import { FileTree, isFolder, validatePath } from "utils/filetree" +import TextField from "@mui/material/TextField"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { Stack } from "components/Stack/Stack"; +import { ChangeEvent, FC, useState } from "react"; +import Typography from "@mui/material/Typography"; +import { allowedExtensions, isAllowedFile } from "utils/templateVersion"; +import { FileTree, isFolder, validatePath } from "utils/filetree"; export const CreateFileDialog: FC<{ - onClose: () => void - checkExists: (path: string) => boolean - onConfirm: (path: string) => void - open: boolean - fileTree: FileTree + onClose: () => void; + checkExists: (path: string) => boolean; + onConfirm: (path: string) => void; + open: boolean; + fileTree: FileTree; }> = ({ checkExists, onClose, onConfirm, open, fileTree }) => { - const [pathValue, setPathValue] = useState("") - const [error, setError] = useState() + const [pathValue, setPathValue] = useState(""); + const [error, setError] = useState(); const handleChange = (event: ChangeEvent) => { - setPathValue(event.target.value) - } + setPathValue(event.target.value); + }; const handleConfirm = () => { if (pathValue === "") { - setError("You must enter a path!") - return + setError("You must enter a path!"); + return; } if (checkExists(pathValue)) { - setError("File already exists") - return + setError("File already exists"); + return; } if (!isAllowedFile(pathValue)) { - const extensions = allowedExtensions.join(", ") + const extensions = allowedExtensions.join(", "); setError( `This extension is not allowed. You only can create files with the following extensions: ${extensions}.`, - ) - return + ); + return; } - const pathError = validatePath(pathValue, fileTree) + const pathError = validatePath(pathValue, fileTree); if (pathError) { - setError(pathError) - return + setError(pathError); + return; } - onConfirm(pathValue) - setError(undefined) - setPathValue("") - } + onConfirm(pathValue); + setError(undefined); + setPathValue(""); + }; return ( { - onClose() - setError(undefined) - setPathValue("") + onClose(); + setError(undefined); + setPathValue(""); }} onConfirm={handleConfirm} hideCancel={false} @@ -68,7 +68,7 @@ export const CreateFileDialog: FC<{ autoFocus onKeyDown={(event) => { if (event.key === "Enter") { - handleConfirm() + handleConfirm(); } }} error={Boolean(error)} @@ -84,14 +84,14 @@ export const CreateFileDialog: FC<{ } /> - ) -} + ); +}; export const DeleteFileDialog: FC<{ - onClose: () => void - onConfirm: () => void - open: boolean - filename: string + onClose: () => void; + onConfirm: () => void; + open: boolean; + filename: string; }> = ({ onClose, onConfirm, open, filename }) => { return ( } /> - ) -} + ); +}; export const RenameFileDialog: FC<{ - onClose: () => void - onConfirm: (filename: string) => void - checkExists: (path: string) => boolean - open: boolean - filename: string - fileTree: FileTree + onClose: () => void; + onConfirm: (filename: string) => void; + checkExists: (path: string) => boolean; + open: boolean; + filename: string; + fileTree: FileTree; }> = ({ checkExists, onClose, onConfirm, open, filename, fileTree }) => { - const [pathValue, setPathValue] = useState(filename) - const [error, setError] = useState() + const [pathValue, setPathValue] = useState(filename); + const [error, setError] = useState(); const handleChange = (event: ChangeEvent) => { - setPathValue(event.target.value) - } + setPathValue(event.target.value); + }; const handleConfirm = () => { if (pathValue === "") { - setError("You must enter a path!") - return + setError("You must enter a path!"); + return; } if (checkExists(pathValue)) { - setError("File already exists") - return + setError("File already exists"); + return; } if (!isAllowedFile(pathValue)) { - const extensions = allowedExtensions.join(", ") + const extensions = allowedExtensions.join(", "); setError( `This extension is not allowed. You only can rename files with the following extensions: ${extensions}.`, - ) - return + ); + return; } //Check if a folder is renamed to a file - const [_, extension] = pathValue.split(".") + const [_, extension] = pathValue.split("."); if (isFolder(filename, fileTree) && extension) { - setError(`A folder can't be renamed to a file.`) - return + setError(`A folder can't be renamed to a file.`); + return; } - const pathError = validatePath(pathValue, fileTree) + const pathError = validatePath(pathValue, fileTree); if (pathError) { - setError(pathError) - return + setError(pathError); + return; } - onConfirm(pathValue) - setError(undefined) - setPathValue("") - } + onConfirm(pathValue); + setError(undefined); + setPathValue(""); + }; return ( { - onClose() - setError(undefined) - setPathValue("") + onClose(); + setError(undefined); + setPathValue(""); }} onConfirm={handleConfirm} hideCancel={false} @@ -179,7 +179,7 @@ export const RenameFileDialog: FC<{ autoFocus onKeyDown={(event) => { if (event.key === "Enter") { - handleConfirm() + handleConfirm(); } }} error={Boolean(error)} @@ -195,5 +195,5 @@ export const RenameFileDialog: FC<{ } /> - ) -} + ); +}; diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/FileTreeView.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/FileTreeView.tsx index b0a39aa619..aff625bb3a 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/FileTreeView.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/FileTreeView.tsx @@ -1,58 +1,58 @@ -import { makeStyles } from "@mui/styles" -import ChevronRightIcon from "@mui/icons-material/ChevronRight" -import ExpandMoreIcon from "@mui/icons-material/ExpandMore" -import TreeView from "@mui/lab/TreeView" -import TreeItem from "@mui/lab/TreeItem" -import Menu from "@mui/material/Menu" -import MenuItem from "@mui/material/MenuItem" -import { CSSProperties, FC, useState } from "react" -import { FileTree } from "utils/filetree" -import { DockerIcon } from "components/Icons/DockerIcon" -import { colors } from "theme/colors" +import { makeStyles } from "@mui/styles"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import TreeView from "@mui/lab/TreeView"; +import TreeItem from "@mui/lab/TreeItem"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import { CSSProperties, FC, useState } from "react"; +import { FileTree } from "utils/filetree"; +import { DockerIcon } from "components/Icons/DockerIcon"; +import { colors } from "theme/colors"; const sortFileTree = (fileTree: FileTree) => (a: string, b: string) => { - const contentA = fileTree[a] - const contentB = fileTree[b] + const contentA = fileTree[a]; + const contentB = fileTree[b]; if (typeof contentA === "object") { - return -1 + return -1; } if (typeof contentB === "object") { - return 1 + return 1; } - return a.localeCompare(b) -} + return a.localeCompare(b); +}; type ContextMenu = { - path: string - clientX: number - clientY: number -} + path: string; + clientX: number; + clientY: number; +}; export const FileTreeView: FC<{ - onSelect: (path: string) => void - onDelete: (path: string) => void - onRename: (path: string) => void - fileTree: FileTree - activePath?: string + onSelect: (path: string) => void; + onDelete: (path: string) => void; + onRename: (path: string) => void; + fileTree: FileTree; + activePath?: string; }> = ({ fileTree, activePath, onDelete, onRename, onSelect }) => { - const styles = useStyles() - const [contextMenu, setContextMenu] = useState() + const styles = useStyles(); + const [contextMenu, setContextMenu] = useState(); const buildTreeItems = ( filename: string, content?: FileTree | string, parentPath?: string, ): JSX.Element => { - const currentPath = parentPath ? `${parentPath}/${filename}` : filename - let icon: JSX.Element | null = null + const currentPath = parentPath ? `${parentPath}/${filename}` : filename; + let icon: JSX.Element | null = null; if (filename.endsWith(".tf")) { - icon = + icon = ; } if (filename.endsWith(".md")) { - icon = + icon = ; } if (filename.endsWith("Dockerfile")) { - icon = + icon = ; } return ( @@ -64,11 +64,11 @@ export const FileTreeView: FC<{ currentPath === activePath ? "active" : "" }`} onClick={() => { - onSelect(currentPath) + onSelect(currentPath); }} onContextMenu={(event) => { - event.preventDefault() // Avoid default browser behavior - event.stopPropagation() // Avoid trigger parent context menu + event.preventDefault(); // Avoid default browser behavior + event.stopPropagation(); // Avoid trigger parent context menu setContextMenu( contextMenu ? undefined @@ -77,7 +77,7 @@ export const FileTreeView: FC<{ clientY: event.clientY, clientX: event.clientX, }, - ) + ); }} icon={icon} style={ @@ -90,15 +90,15 @@ export const FileTreeView: FC<{ Object.keys(content) .sort(sortFileTree(content)) .map((filename) => { - const child = content[filename] - return buildTreeItems(filename, child, currentPath) + const child = content[filename]; + return buildTreeItems(filename, child, currentPath); }) ) : ( <> )} - ) - } + ); + }; return ( { - const child = fileTree[filename] - return buildTreeItems(filename, child) + const child = fileTree[filename]; + return buildTreeItems(filename, child); })} { if (!contextMenu) { - return + return; } - onRename(contextMenu.path) - setContextMenu(undefined) + onRename(contextMenu.path); + setContextMenu(undefined); }} > Rename @@ -148,18 +148,18 @@ export const FileTreeView: FC<{ { if (!contextMenu) { - return + return; } - onDelete(contextMenu.path) - setContextMenu(undefined) + onDelete(contextMenu.path); + setContextMenu(undefined); }} > Delete - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ fileTreeItem: { @@ -213,7 +213,7 @@ const useStyles = makeStyles((theme) => ({ flex: 1, }, preview: {}, -})) +})); const FileTypeTerraform = () => ( @@ -223,7 +223,7 @@ const FileTypeTerraform = () => ( -) +); const FileTypeMarkdown = () => ( @@ -240,6 +240,6 @@ const FileTypeMarkdown = () => ( -) +); -const FileTypeDockerfile = () => +const FileTypeDockerfile = () => ; diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/MissingTemplateVariablesDialog.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/MissingTemplateVariablesDialog.tsx index b61f6578bd..61a5c39fd1 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/MissingTemplateVariablesDialog.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/MissingTemplateVariablesDialog.tsx @@ -1,41 +1,41 @@ -import { makeStyles } from "@mui/styles" -import Dialog from "@mui/material/Dialog" -import DialogContent from "@mui/material/DialogContent" -import DialogContentText from "@mui/material/DialogContentText" -import DialogTitle from "@mui/material/DialogTitle" -import { DialogProps } from "components/Dialogs/Dialog" -import { FC, useEffect, useState } from "react" -import { FormFields, VerticalForm } from "components/Form/Form" -import { TemplateVersionVariable, VariableValue } from "api/typesGenerated" -import DialogActions from "@mui/material/DialogActions" -import Button from "@mui/material/Button" -import { VariableInput } from "pages/CreateTemplatePage/VariableInput" -import { Loader } from "components/Loader/Loader" +import { makeStyles } from "@mui/styles"; +import Dialog from "@mui/material/Dialog"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogTitle from "@mui/material/DialogTitle"; +import { DialogProps } from "components/Dialogs/Dialog"; +import { FC, useEffect, useState } from "react"; +import { FormFields, VerticalForm } from "components/Form/Form"; +import { TemplateVersionVariable, VariableValue } from "api/typesGenerated"; +import DialogActions from "@mui/material/DialogActions"; +import Button from "@mui/material/Button"; +import { VariableInput } from "pages/CreateTemplatePage/VariableInput"; +import { Loader } from "components/Loader/Loader"; export type MissingTemplateVariablesDialogProps = Omit< DialogProps, "onSubmit" > & { - onClose: () => void - onSubmit: (values: VariableValue[]) => void - missingVariables?: TemplateVersionVariable[] -} + onClose: () => void; + onSubmit: (values: VariableValue[]) => void; + missingVariables?: TemplateVersionVariable[]; +}; export const MissingTemplateVariablesDialog: FC< MissingTemplateVariablesDialogProps > = ({ missingVariables, onSubmit, ...dialogProps }) => { - const styles = useStyles() - const [variableValues, setVariableValues] = useState([]) + const styles = useStyles(); + const [variableValues, setVariableValues] = useState([]); // Pre-fill the form with the default values when missing variables are loaded useEffect(() => { if (!missingVariables) { - return + return; } setVariableValues( missingVariables.map((v) => ({ name: v.name, value: v.value })), - ) - }, [missingVariables]) + ); + }, [missingVariables]); return ( { - e.preventDefault() - onSubmit(variableValues) + e.preventDefault(); + onSubmit(variableValues); }} > {missingVariables ? ( @@ -76,12 +76,12 @@ export const MissingTemplateVariablesDialog: FC< prev[index] = { name: variable.name, value, - } - return [...prev] - }) + }; + return [...prev]; + }); }} /> - ) + ); })} ) : ( @@ -98,8 +98,8 @@ export const MissingTemplateVariablesDialog: FC< - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ title: { @@ -140,4 +140,4 @@ const useStyles = makeStyles((theme) => ({ flexDirection: "column", gap: theme.spacing(1), }, -})) +})); diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/MonacoEditor.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/MonacoEditor.tsx index 3219124542..e37798ac09 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/MonacoEditor.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/MonacoEditor.tsx @@ -1,53 +1,53 @@ -import { useTheme } from "@mui/styles" -import Editor, { loader } from "@monaco-editor/react" -import * as monaco from "monaco-editor" -import { FC, useLayoutEffect, useMemo, useState } from "react" -import { MONOSPACE_FONT_FAMILY } from "theme/constants" -import { hslToHex } from "utils/colors" -import type { editor } from "monaco-editor" +import { useTheme } from "@mui/styles"; +import Editor, { loader } from "@monaco-editor/react"; +import * as monaco from "monaco-editor"; +import { FC, useLayoutEffect, useMemo, useState } from "react"; +import { MONOSPACE_FONT_FAMILY } from "theme/constants"; +import { hslToHex } from "utils/colors"; +import type { editor } from "monaco-editor"; -loader.config({ monaco }) +loader.config({ monaco }); export const MonacoEditor: FC<{ - value?: string - path?: string - onChange?: (value: string) => void + value?: string; + path?: string; + onChange?: (value: string) => void; }> = ({ onChange, value, path }) => { - const theme = useTheme() - const [editor, setEditor] = useState() + const theme = useTheme(); + const [editor, setEditor] = useState(); useLayoutEffect(() => { if (!editor) { - return + return; } const resizeListener = () => { editor.layout({ height: 0, width: 0, - }) - } - window.addEventListener("resize", resizeListener) + }); + }; + window.addEventListener("resize", resizeListener); return () => { - window.removeEventListener("resize", resizeListener) - } - }, [editor]) + window.removeEventListener("resize", resizeListener); + }; + }, [editor]); const language = useMemo(() => { if (path?.endsWith(".tf")) { - return "hcl" + return "hcl"; } if (path?.endsWith(".md")) { - return "markdown" + return "markdown"; } if (path?.endsWith(".json")) { - return "json" + return "json"; } if (path?.endsWith(".yaml")) { - return "yaml" + return "yaml"; } if (path?.endsWith("Dockerfile")) { - return "dockerfile" + return "dockerfile"; } - }, [path]) + }, [path]); return ( { if (onChange && newValue) { - onChange(newValue) + onChange(newValue); } }} onMount={(editor, monaco) => { // This jank allows for Ctrl + Enter to work outside the editor. // We use this keybind to trigger a build. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Private type in Monaco! - ;(editor as any)._standaloneKeybindingService.addDynamicKeybinding( + (editor as any)._standaloneKeybindingService.addDynamicKeybinding( `-editor.action.insertLineAfter`, monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => { // }, - ) + ); - setEditor(editor) + setEditor(editor); document.fonts.ready .then(() => { // Ensures that all text is measured properly. // If this isn't done, there can be weird selection issues. - monaco.editor.remeasureFonts() + monaco.editor.remeasureFonts(); }) .catch(() => { // Not a biggie! - }) + }); monaco.editor.defineTheme("min", { base: "vs-dark", @@ -127,11 +127,11 @@ export const MonacoEditor: FC<{ "editor.foreground": hslToHex(theme.palette.text.primary), "editor.background": hslToHex(theme.palette.background.default), }, - }) + }); editor.updateOptions({ theme: "min", - }) + }); }} /> - ) -} + ); +}; diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/PublishTemplateVersionDialog.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/PublishTemplateVersionDialog.tsx index a50b7e96e2..66013982f5 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/PublishTemplateVersionDialog.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/PublishTemplateVersionDialog.tsx @@ -1,29 +1,29 @@ -import { DialogProps } from "components/Dialogs/Dialog" -import { FC } from "react" -import { getFormHelpers } from "utils/formUtils" -import { FormFields } from "components/Form/Form" -import { useFormik } from "formik" -import * as Yup from "yup" -import { PublishVersionData } from "pages/TemplateVersionEditorPage/types" -import TextField from "@mui/material/TextField" -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" -import Checkbox from "@mui/material/Checkbox" -import FormControlLabel from "@mui/material/FormControlLabel" -import { Stack } from "components/Stack/Stack" +import { DialogProps } from "components/Dialogs/Dialog"; +import { FC } from "react"; +import { getFormHelpers } from "utils/formUtils"; +import { FormFields } from "components/Form/Form"; +import { useFormik } from "formik"; +import * as Yup from "yup"; +import { PublishVersionData } from "pages/TemplateVersionEditorPage/types"; +import TextField from "@mui/material/TextField"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import Checkbox from "@mui/material/Checkbox"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import { Stack } from "components/Stack/Stack"; export const Language = { versionNameLabel: "Version name", messagePlaceholder: "Write a short message about the changes you made...", defaultCheckboxLabel: "Promote to default version", -} +}; export type PublishTemplateVersionDialogProps = DialogProps & { - defaultName: string - isPublishing: boolean - publishingError?: unknown - onClose: () => void - onConfirm: (data: PublishVersionData) => void -} + defaultName: string; + isPublishing: boolean; + publishingError?: unknown; + onClose: () => void; + onConfirm: (data: PublishVersionData) => void; +}; export const PublishTemplateVersionDialog: FC< PublishTemplateVersionDialogProps @@ -47,12 +47,12 @@ export const PublishTemplateVersionDialog: FC< isActiveVersion: Yup.boolean(), }), onSubmit: onConfirm, - }) - const getFieldHelpers = getFormHelpers(form, publishingError) + }); + const getFieldHelpers = getFormHelpers(form, publishingError); const handleClose = () => { - form.resetForm() - onClose() - } + form.resetForm(); + onClose(); + }; return ( { - await form.submitForm() + await form.submitForm(); }} hideCancel={false} type="success" @@ -98,7 +98,7 @@ export const PublishTemplateVersionDialog: FC< await form.setFieldValue( "isActiveVersion", e.target.checked, - ) + ); }} name="isActiveVersion" /> @@ -108,5 +108,5 @@ export const PublishTemplateVersionDialog: FC< } /> - ) -} + ); +}; diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/TemplateVersionEditor.stories.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/TemplateVersionEditor.stories.tsx index 7be7f950fc..0789e985fa 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/TemplateVersionEditor.stories.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/TemplateVersionEditor.stories.tsx @@ -7,9 +7,9 @@ import { MockWorkspaceResource, MockWorkspaceResource2, MockWorkspaceResource3, -} from "testHelpers/entities" -import { TemplateVersionEditor } from "./TemplateVersionEditor" -import type { Meta, StoryObj } from "@storybook/react" +} from "testHelpers/entities"; +import { TemplateVersionEditor } from "./TemplateVersionEditor"; +import type { Meta, StoryObj } from "@storybook/react"; const meta: Meta = { title: "components/TemplateVersionEditor", @@ -22,18 +22,18 @@ const meta: Meta = { parameters: { layout: "fullscreen", }, -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; -export const Example: Story = {} +export const Example: Story = {}; export const Logs = { args: { buildLogs: MockWorkspaceBuildLogs, }, -} +}; export const Resources: Story = { args: { @@ -44,7 +44,7 @@ export const Resources: Story = { MockWorkspaceResource3, ], }, -} +}; export const ManyLogs = { args: { @@ -58,10 +58,10 @@ export const ManyLogs = { }, buildLogs: MockWorkspaceExtendedBuildLogs, }, -} +}; export const Published = { args: { publishedVersion: MockTemplateVersion, }, -} +}; diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/TemplateVersionEditor.tsx index 26b7966c72..3fc49fd749 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -1,11 +1,11 @@ -import Button from "@mui/material/Button" -import IconButton from "@mui/material/IconButton" -import Link from "@mui/material/Link" -import { makeStyles } from "@mui/styles" -import Tooltip from "@mui/material/Tooltip" -import CreateIcon from "@mui/icons-material/AddOutlined" -import BuildIcon from "@mui/icons-material/BuildOutlined" -import PreviewIcon from "@mui/icons-material/VisibilityOutlined" +import Button from "@mui/material/Button"; +import IconButton from "@mui/material/IconButton"; +import Link from "@mui/material/Link"; +import { makeStyles } from "@mui/styles"; +import Tooltip from "@mui/material/Tooltip"; +import CreateIcon from "@mui/icons-material/AddOutlined"; +import BuildIcon from "@mui/icons-material/BuildOutlined"; +import PreviewIcon from "@mui/icons-material/VisibilityOutlined"; import { ProvisionerJobLog, Template, @@ -13,15 +13,15 @@ import { TemplateVersionVariable, VariableValue, WorkspaceResource, -} from "api/typesGenerated" -import { Link as RouterLink } from "react-router-dom" -import { Alert, AlertDetail } from "components/Alert/Alert" -import { Avatar } from "components/Avatar/Avatar" -import { AvatarData } from "components/AvatarData/AvatarData" -import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable" -import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs" -import { PublishVersionData } from "pages/TemplateVersionEditorPage/types" -import { FC, useCallback, useEffect, useRef, useState } from "react" +} from "api/typesGenerated"; +import { Link as RouterLink } from "react-router-dom"; +import { Alert, AlertDetail } from "components/Alert/Alert"; +import { Avatar } from "components/Avatar/Avatar"; +import { AvatarData } from "components/AvatarData/AvatarData"; +import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable"; +import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"; +import { PublishVersionData } from "pages/TemplateVersionEditorPage/types"; +import { FC, useCallback, useEffect, useRef, useState } from "react"; import { createFile, existsFile, @@ -32,62 +32,62 @@ import { removeFile, traverse, updateFile, -} from "utils/filetree" +} from "utils/filetree"; import { CreateFileDialog, DeleteFileDialog, RenameFileDialog, -} from "./FileDialog" -import { FileTreeView } from "./FileTreeView" -import { MissingTemplateVariablesDialog } from "./MissingTemplateVariablesDialog" -import { MonacoEditor } from "./MonacoEditor" -import { PublishTemplateVersionDialog } from "./PublishTemplateVersionDialog" +} from "./FileDialog"; +import { FileTreeView } from "./FileTreeView"; +import { MissingTemplateVariablesDialog } from "./MissingTemplateVariablesDialog"; +import { MonacoEditor } from "./MonacoEditor"; +import { PublishTemplateVersionDialog } from "./PublishTemplateVersionDialog"; import { getStatus, TemplateVersionStatusBadge, -} from "./TemplateVersionStatusBadge" -import { Theme } from "@mui/material/styles" -import AlertTitle from "@mui/material/AlertTitle" -import { DashboardFullPage } from "components/Dashboard/DashboardLayout" +} from "./TemplateVersionStatusBadge"; +import { Theme } from "@mui/material/styles"; +import AlertTitle from "@mui/material/AlertTitle"; +import { DashboardFullPage } from "components/Dashboard/DashboardLayout"; export interface TemplateVersionEditorProps { - template: Template - templateVersion: TemplateVersion - defaultFileTree: FileTree - buildLogs?: ProvisionerJobLog[] - resources?: WorkspaceResource[] - deploymentBannerVisible?: boolean - disablePreview: boolean - disableUpdate: boolean - onPreview: (files: FileTree) => void - onPublish: () => void - onConfirmPublish: (data: PublishVersionData) => void - onCancelPublish: () => void - publishingError: unknown - publishedVersion?: TemplateVersion - publishedVersionIsDefault?: boolean - onCreateWorkspace: () => void - isAskingPublishParameters: boolean - isPromptingMissingVariables: boolean - isPublishing: boolean - missingVariables?: TemplateVersionVariable[] - onSubmitMissingVariableValues: (values: VariableValue[]) => void - onCancelSubmitMissingVariableValues: () => void + template: Template; + templateVersion: TemplateVersion; + defaultFileTree: FileTree; + buildLogs?: ProvisionerJobLog[]; + resources?: WorkspaceResource[]; + deploymentBannerVisible?: boolean; + disablePreview: boolean; + disableUpdate: boolean; + onPreview: (files: FileTree) => void; + onPublish: () => void; + onConfirmPublish: (data: PublishVersionData) => void; + onCancelPublish: () => void; + publishingError: unknown; + publishedVersion?: TemplateVersion; + publishedVersionIsDefault?: boolean; + onCreateWorkspace: () => void; + isAskingPublishParameters: boolean; + isPromptingMissingVariables: boolean; + isPublishing: boolean; + missingVariables?: TemplateVersionVariable[]; + onSubmitMissingVariableValues: (values: VariableValue[]) => void; + onCancelSubmitMissingVariableValues: () => void; } -const topbarHeight = 80 +const topbarHeight = 80; const findInitialFile = (fileTree: FileTree): string | undefined => { - let initialFile: string | undefined + let initialFile: string | undefined; traverse(fileTree, (content, filename, path) => { if (filename.endsWith(".tf")) { - initialFile = path + initialFile = path; } - }) + }); - return initialFile -} + return initialFile; +}; export const TemplateVersionEditor: FC = ({ disablePreview, @@ -115,76 +115,76 @@ export const TemplateVersionEditor: FC = ({ }) => { // If resources are provided, show them by default! // This is for Storybook! - const [selectedTab, setSelectedTab] = useState(() => (resources ? 1 : 0)) - const [fileTree, setFileTree] = useState(defaultFileTree) - const [createFileOpen, setCreateFileOpen] = useState(false) - const [deleteFileOpen, setDeleteFileOpen] = useState() - const [renameFileOpen, setRenameFileOpen] = useState() - const [dirty, setDirty] = useState(false) + const [selectedTab, setSelectedTab] = useState(() => (resources ? 1 : 0)); + const [fileTree, setFileTree] = useState(defaultFileTree); + const [createFileOpen, setCreateFileOpen] = useState(false); + const [deleteFileOpen, setDeleteFileOpen] = useState(); + const [renameFileOpen, setRenameFileOpen] = useState(); + const [dirty, setDirty] = useState(false); const [activePath, setActivePath] = useState(() => findInitialFile(fileTree), - ) + ); const triggerPreview = useCallback(() => { - onPreview(fileTree) + onPreview(fileTree); // Switch to the build log! - setSelectedTab(0) - }, [fileTree, onPreview]) + setSelectedTab(0); + }, [fileTree, onPreview]); // Stop ctrl+s from saving files and make ctrl+enter trigger a preview. useEffect(() => { const keyListener = (event: KeyboardEvent) => { if (!(navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey)) { - return + return; } switch (event.key) { case "s": // Prevent opening the save dialog! - event.preventDefault() - break + event.preventDefault(); + break; case "Enter": - event.preventDefault() - triggerPreview() - break + event.preventDefault(); + triggerPreview(); + break; } - } - document.addEventListener("keydown", keyListener) + }; + document.addEventListener("keydown", keyListener); return () => { - document.removeEventListener("keydown", keyListener) - } - }, [triggerPreview]) + document.removeEventListener("keydown", keyListener); + }; + }, [triggerPreview]); // Automatically switch to the template preview tab when the build succeeds. - const previousVersion = useRef() + const previousVersion = useRef(); useEffect(() => { if (!previousVersion.current) { - previousVersion.current = templateVersion - return + previousVersion.current = templateVersion; + return; } if ( ["running", "pending"].includes(previousVersion.current.job.status) && templateVersion.job.status === "succeeded" ) { - setSelectedTab(1) - setDirty(false) + setSelectedTab(1); + setDirty(false); } - previousVersion.current = templateVersion - }, [templateVersion]) + previousVersion.current = templateVersion; + }, [templateVersion]); - const hasIcon = template.icon && template.icon !== "" - const templateVersionSucceeded = templateVersion.job.status === "succeeded" - const showBuildLogs = Boolean(buildLogs) - const editorValue = getFileContent(activePath ?? "", fileTree) as string - const firstTemplateVersionOnEditor = useRef(templateVersion) + const hasIcon = template.icon && template.icon !== ""; + const templateVersionSucceeded = templateVersion.job.status === "succeeded"; + const showBuildLogs = Boolean(buildLogs); + const editorValue = getFileContent(activePath ?? "", fileTree) as string; + const firstTemplateVersionOnEditor = useRef(templateVersion); useEffect(() => { - window.dispatchEvent(new Event("resize")) - }, [showBuildLogs]) + window.dispatchEvent(new Event("resize")); + }, [showBuildLogs]); const styles = useStyles({ templateVersionSucceeded, showBuildLogs, deploymentBannerVisible, - }) + }); return ( <> @@ -243,7 +243,7 @@ export const TemplateVersionEditor: FC = ({ title="Build template (Ctrl + Enter)" disabled={disablePreview} onClick={() => { - triggerPreview() + triggerPreview(); }} > Build template @@ -268,8 +268,8 @@ export const TemplateVersionEditor: FC = ({ { - setCreateFileOpen(true) - event.currentTarget.blur() + setCreateFileOpen(true); + event.currentTarget.blur(); }} > @@ -280,29 +280,29 @@ export const TemplateVersionEditor: FC = ({ fileTree={fileTree} open={createFileOpen} onClose={() => { - setCreateFileOpen(false) + setCreateFileOpen(false); }} checkExists={(path) => existsFile(path, fileTree)} onConfirm={(path) => { - setFileTree((fileTree) => createFile(path, fileTree, "")) - setActivePath(path) - setCreateFileOpen(false) - setDirty(true) + setFileTree((fileTree) => createFile(path, fileTree, "")); + setActivePath(path); + setCreateFileOpen(false); + setDirty(true); }} /> { if (!deleteFileOpen) { - throw new Error("delete file must be set") + throw new Error("delete file must be set"); } setFileTree((fileTree) => removeFile(deleteFileOpen, fileTree), - ) - setDeleteFileOpen(undefined) + ); + setDeleteFileOpen(undefined); if (activePath === deleteFileOpen) { - setActivePath(undefined) + setActivePath(undefined); } - setDirty(true) + setDirty(true); }} open={Boolean(deleteFileOpen)} onClose={() => setDeleteFileOpen(undefined)} @@ -312,20 +312,20 @@ export const TemplateVersionEditor: FC = ({ fileTree={fileTree} open={Boolean(renameFileOpen)} onClose={() => { - setRenameFileOpen(undefined) + setRenameFileOpen(undefined); }} filename={renameFileOpen || ""} checkExists={(path) => existsFile(path, fileTree)} onConfirm={(newPath) => { if (!renameFileOpen) { - return + return; } setFileTree((fileTree) => moveFile(renameFileOpen, newPath, fileTree), - ) - setActivePath(newPath) - setRenameFileOpen(undefined) - setDirty(true) + ); + setActivePath(newPath); + setRenameFileOpen(undefined); + setDirty(true); }} /> @@ -334,7 +334,7 @@ export const TemplateVersionEditor: FC = ({ onDelete={(file) => setDeleteFileOpen(file)} onSelect={(filePath) => { if (!isFolder(filePath, fileTree)) { - setActivePath(filePath) + setActivePath(filePath); } }} onRename={(file) => setRenameFileOpen(file)} @@ -350,12 +350,12 @@ export const TemplateVersionEditor: FC = ({ path={activePath} onChange={(value) => { if (!activePath) { - return + return; } setFileTree((fileTree) => updateFile(activePath, value, fileTree), - ) - setDirty(true) + ); + setDirty(true); }} /> ) : ( @@ -370,7 +370,7 @@ export const TemplateVersionEditor: FC = ({ selectedTab === 0 ? "active" : "" }`} onClick={() => { - setSelectedTab(0) + setSelectedTab(0); }} > {templateVersion.job.status !== "succeeded" ? ( @@ -387,7 +387,7 @@ export const TemplateVersionEditor: FC = ({ selectedTab === 1 ? "active" : "" }`} onClick={() => { - setSelectedTab(1) + setSelectedTab(1); }} > @@ -470,15 +470,15 @@ export const TemplateVersionEditor: FC = ({ missingVariables={missingVariables} /> - ) -} + ); +}; const useStyles = makeStyles< Theme, { - templateVersionSucceeded: boolean - showBuildLogs: boolean - deploymentBannerVisible?: boolean + templateVersionSucceeded: boolean; + showBuildLogs: boolean; + deploymentBannerVisible?: boolean; } >((theme) => ({ root: { @@ -634,4 +634,4 @@ const useStyles = makeStyles< resources: { paddingBottom: theme.spacing(2), }, -})) +})); diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/TemplateVersionStatusBadge.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/TemplateVersionStatusBadge.tsx index 397d5c68ae..776609b7a9 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/TemplateVersionStatusBadge.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor/TemplateVersionStatusBadge.tsx @@ -1,15 +1,15 @@ -import { TemplateVersion } from "api/typesGenerated" -import { FC, ReactNode } from "react" -import CircularProgress from "@mui/material/CircularProgress" -import ErrorIcon from "@mui/icons-material/ErrorOutline" -import CheckIcon from "@mui/icons-material/CheckOutlined" -import { Pill } from "components/Pill/Pill" -import { PaletteIndex } from "theme/theme" +import { TemplateVersion } from "api/typesGenerated"; +import { FC, ReactNode } from "react"; +import CircularProgress from "@mui/material/CircularProgress"; +import ErrorIcon from "@mui/icons-material/ErrorOutline"; +import CheckIcon from "@mui/icons-material/CheckOutlined"; +import { Pill } from "components/Pill/Pill"; +import { PaletteIndex } from "theme/theme"; export const TemplateVersionStatusBadge: FC<{ - version: TemplateVersion + version: TemplateVersion; }> = ({ version }) => { - const { text, icon, type } = getStatus(version) + const { text, icon, type } = getStatus(version); return ( - ) -} + ); +}; const LoadingIcon: FC = () => { - return -} + return ; +}; export const getStatus = ( version: TemplateVersion, ): { - type?: PaletteIndex - text: string - icon: ReactNode + type?: PaletteIndex; + text: string; + icon: ReactNode; } => { switch (version.job.status) { case "running": @@ -37,36 +37,36 @@ export const getStatus = ( type: "info", text: "Running", icon: , - } + }; case "pending": return { text: "Pending", icon: , type: "info", - } + }; case "canceling": return { type: "warning", text: "Canceling", icon: , - } + }; case "canceled": return { type: "warning", text: "Canceled", icon: , - } + }; case "failed": return { type: "error", text: "Failed", icon: , - } + }; case "succeeded": return { type: "success", text: "Success", icon: , - } + }; } -} +}; diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx index 88f0e9aed5..62d5a6a2ff 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx @@ -1,24 +1,24 @@ -import { renderWithAuth } from "testHelpers/renderHelpers" -import TemplateVersionEditorPage from "./TemplateVersionEditorPage" -import { screen, waitFor, within } from "@testing-library/react" -import userEvent from "@testing-library/user-event" -import * as api from "api/api" +import { renderWithAuth } from "testHelpers/renderHelpers"; +import TemplateVersionEditorPage from "./TemplateVersionEditorPage"; +import { screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import * as api from "api/api"; import { MockTemplateVersion, MockWorkspaceBuildLogs, -} from "testHelpers/entities" -import { Language } from "./TemplateVersionEditor/PublishTemplateVersionDialog" +} from "testHelpers/entities"; +import { Language } from "./TemplateVersionEditor/PublishTemplateVersionDialog"; // For some reason this component in Jest is throwing a MUI style warning so, // since we don't need it for this test, we can mock it out jest.mock("components/TemplateResourcesTable/TemplateResourcesTable", () => { return { TemplateResourcesTable: () =>
, - } -}) + }; +}); test("Use custom name, message and set it as active when publishing", async () => { - const user = userEvent.setup() + const user = userEvent.setup(); renderWithAuth(, { extraRoutes: [ { @@ -26,64 +26,64 @@ test("Use custom name, message and set it as active when publishing", async () = element:
, }, ], - }) - const topbar = await screen.findByTestId("topbar") + }); + const topbar = await screen.findByTestId("topbar"); // Build Template - jest.spyOn(api, "uploadTemplateFile").mockResolvedValueOnce({ hash: "hash" }) + jest.spyOn(api, "uploadTemplateFile").mockResolvedValueOnce({ hash: "hash" }); jest .spyOn(api, "createTemplateVersion") - .mockResolvedValueOnce(MockTemplateVersion) + .mockResolvedValueOnce(MockTemplateVersion); jest .spyOn(api, "getTemplateVersion") - .mockResolvedValue({ ...MockTemplateVersion, id: "new-version-id" }) + .mockResolvedValue({ ...MockTemplateVersion, id: "new-version-id" }); jest .spyOn(api, "watchBuildLogsByTemplateVersionId") .mockImplementation((_, options) => { - options.onMessage(MockWorkspaceBuildLogs[0]) - options.onDone() - return jest.fn() as never - }) + options.onMessage(MockWorkspaceBuildLogs[0]); + options.onDone(); + return jest.fn() as never; + }); const buildButton = within(topbar).getByRole("button", { name: "Build template", - }) - await user.click(buildButton) + }); + await user.click(buildButton); // Publish const patchTemplateVersion = jest .spyOn(api, "patchTemplateVersion") - .mockResolvedValue(MockTemplateVersion) + .mockResolvedValue(MockTemplateVersion); const updateActiveTemplateVersion = jest .spyOn(api, "updateActiveTemplateVersion") - .mockResolvedValue({ message: "" }) - await within(topbar).findByText("Success") + .mockResolvedValue({ message: "" }); + await within(topbar).findByText("Success"); const publishButton = within(topbar).getByRole("button", { name: "Publish version", - }) - await user.click(publishButton) - const publishDialog = await screen.findByTestId("dialog") - const nameField = within(publishDialog).getByLabelText("Version name") - await user.clear(nameField) - await user.type(nameField, "v1.0") - const messageField = within(publishDialog).getByLabelText("Message") - await user.clear(messageField) - await user.type(messageField, "Informative message") + }); + await user.click(publishButton); + const publishDialog = await screen.findByTestId("dialog"); + const nameField = within(publishDialog).getByLabelText("Version name"); + await user.clear(nameField); + await user.type(nameField, "v1.0"); + const messageField = within(publishDialog).getByLabelText("Message"); + await user.clear(messageField); + await user.type(messageField, "Informative message"); await user.click( within(publishDialog).getByRole("button", { name: "Publish" }), - ) + ); await waitFor(() => { expect(patchTemplateVersion).toBeCalledWith("new-version-id", { name: "v1.0", message: "Informative message", - }) - }) + }); + }); expect(updateActiveTemplateVersion).toBeCalledWith("test-template", { id: "new-version-id", - }) -}) + }); +}); test("Do not mark as active if promote is not checked", async () => { - const user = userEvent.setup() + const user = userEvent.setup(); renderWithAuth(, { extraRoutes: [ { @@ -91,66 +91,66 @@ test("Do not mark as active if promote is not checked", async () => { element:
, }, ], - }) - const topbar = await screen.findByTestId("topbar") + }); + const topbar = await screen.findByTestId("topbar"); // Build Template - jest.spyOn(api, "uploadTemplateFile").mockResolvedValueOnce({ hash: "hash" }) + jest.spyOn(api, "uploadTemplateFile").mockResolvedValueOnce({ hash: "hash" }); jest .spyOn(api, "createTemplateVersion") - .mockResolvedValueOnce(MockTemplateVersion) + .mockResolvedValueOnce(MockTemplateVersion); jest .spyOn(api, "getTemplateVersion") - .mockResolvedValue({ ...MockTemplateVersion, id: "new-version-id" }) + .mockResolvedValue({ ...MockTemplateVersion, id: "new-version-id" }); jest .spyOn(api, "watchBuildLogsByTemplateVersionId") .mockImplementation((_, options) => { - options.onMessage(MockWorkspaceBuildLogs[0]) - options.onDone() - return jest.fn() as never - }) + options.onMessage(MockWorkspaceBuildLogs[0]); + options.onDone(); + return jest.fn() as never; + }); const buildButton = within(topbar).getByRole("button", { name: "Build template", - }) - await user.click(buildButton) + }); + await user.click(buildButton); // Publish const patchTemplateVersion = jest .spyOn(api, "patchTemplateVersion") - .mockResolvedValue(MockTemplateVersion) + .mockResolvedValue(MockTemplateVersion); const updateActiveTemplateVersion = jest .spyOn(api, "updateActiveTemplateVersion") - .mockResolvedValue({ message: "" }) - await within(topbar).findByText("Success") + .mockResolvedValue({ message: "" }); + await within(topbar).findByText("Success"); const publishButton = within(topbar).getByRole("button", { name: "Publish version", - }) - await user.click(publishButton) - const publishDialog = await screen.findByTestId("dialog") - const nameField = within(publishDialog).getByLabelText("Version name") - await user.clear(nameField) - await user.type(nameField, "v1.0") + }); + await user.click(publishButton); + const publishDialog = await screen.findByTestId("dialog"); + const nameField = within(publishDialog).getByLabelText("Version name"); + await user.clear(nameField); + await user.type(nameField, "v1.0"); await user.click( within(publishDialog).getByLabelText(Language.defaultCheckboxLabel), - ) + ); await user.click( within(publishDialog).getByRole("button", { name: "Publish" }), - ) + ); await waitFor(() => { expect(patchTemplateVersion).toBeCalledWith("new-version-id", { name: "v1.0", message: "", - }) - }) - expect(updateActiveTemplateVersion).toBeCalledTimes(0) -}) + }); + }); + expect(updateActiveTemplateVersion).toBeCalledTimes(0); +}); test("Patch request is not send when there are no changes", async () => { const MockTemplateVersionWithEmptyMessage = { ...MockTemplateVersion, message: "", - } - const user = userEvent.setup() + }; + const user = userEvent.setup(); renderWithAuth(, { extraRoutes: [ { @@ -158,46 +158,46 @@ test("Patch request is not send when there are no changes", async () => { element:
, }, ], - }) - const topbar = await screen.findByTestId("topbar") + }); + const topbar = await screen.findByTestId("topbar"); // Build Template - jest.spyOn(api, "uploadTemplateFile").mockResolvedValueOnce({ hash: "hash" }) + jest.spyOn(api, "uploadTemplateFile").mockResolvedValueOnce({ hash: "hash" }); jest .spyOn(api, "createTemplateVersion") - .mockResolvedValueOnce(MockTemplateVersionWithEmptyMessage) + .mockResolvedValueOnce(MockTemplateVersionWithEmptyMessage); jest.spyOn(api, "getTemplateVersion").mockResolvedValue({ ...MockTemplateVersionWithEmptyMessage, id: "new-version-id", - }) + }); jest .spyOn(api, "watchBuildLogsByTemplateVersionId") .mockImplementation((_, options) => { - options.onMessage(MockWorkspaceBuildLogs[0]) - options.onDone() - return jest.fn() as never - }) + options.onMessage(MockWorkspaceBuildLogs[0]); + options.onDone(); + return jest.fn() as never; + }); const buildButton = within(topbar).getByRole("button", { name: "Build template", - }) - await user.click(buildButton) + }); + await user.click(buildButton); // Publish const patchTemplateVersion = jest .spyOn(api, "patchTemplateVersion") - .mockResolvedValue(MockTemplateVersionWithEmptyMessage) - await within(topbar).findByText("Success") + .mockResolvedValue(MockTemplateVersionWithEmptyMessage); + await within(topbar).findByText("Success"); const publishButton = within(topbar).getByRole("button", { name: "Publish version", - }) - await user.click(publishButton) - const publishDialog = await screen.findByTestId("dialog") + }); + await user.click(publishButton); + const publishDialog = await screen.findByTestId("dialog"); // It is using the name from the template version - const nameField = within(publishDialog).getByLabelText("Version name") - expect(nameField).toHaveValue(MockTemplateVersionWithEmptyMessage.name) + const nameField = within(publishDialog).getByLabelText("Version name"); + expect(nameField).toHaveValue(MockTemplateVersionWithEmptyMessage.name); // Publish await user.click( within(publishDialog).getByRole("button", { name: "Publish" }), - ) - expect(patchTemplateVersion).toBeCalledTimes(0) -}) + ); + expect(patchTemplateVersion).toBeCalledTimes(0); +}); diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index 2d80874f99..6020b42039 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -1,27 +1,28 @@ -import { useMachine } from "@xstate/react" -import { TemplateVersionEditor } from "pages/TemplateVersionEditorPage/TemplateVersionEditor/TemplateVersionEditor" -import { useOrganizationId } from "hooks/useOrganizationId" -import { usePermissions } from "hooks/usePermissions" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { useNavigate, useParams } from "react-router-dom" -import { pageTitle } from "utils/page" -import { templateVersionEditorMachine } from "xServices/templateVersionEditor/templateVersionEditorXService" -import { useTemplateVersionData } from "./data" +import { useMachine } from "@xstate/react"; +import { TemplateVersionEditor } from "pages/TemplateVersionEditorPage/TemplateVersionEditor/TemplateVersionEditor"; +import { useOrganizationId } from "hooks/useOrganizationId"; +import { usePermissions } from "hooks/usePermissions"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { useNavigate, useParams } from "react-router-dom"; +import { pageTitle } from "utils/page"; +import { templateVersionEditorMachine } from "xServices/templateVersionEditor/templateVersionEditorXService"; +import { useTemplateVersionData } from "./data"; type Params = { - version: string - template: string -} + version: string; + template: string; +}; export const TemplateVersionEditorPage: FC = () => { - const navigate = useNavigate() - const { version: versionName, template: templateName } = useParams() as Params - const orgId = useOrganizationId() + const navigate = useNavigate(); + const { version: versionName, template: templateName } = + useParams() as Params; + const orgId = useOrganizationId(); const [editorState, sendEvent] = useMachine(templateVersionEditorMachine, { context: { orgId }, - }) - const permissions = usePermissions() + }); + const permissions = usePermissions(); const { isSuccess, data } = useTemplateVersionData( { orgId, @@ -30,10 +31,10 @@ export const TemplateVersionEditorPage: FC = () => { }, { onSuccess(data) { - sendEvent({ type: "INITIALIZE", tarReader: data.tarReader }) + sendEvent({ type: "INITIALIZE", tarReader: data.tarReader }); }, }, - ) + ); return ( <> @@ -52,23 +53,23 @@ export const TemplateVersionEditorPage: FC = () => { type: "CREATE_VERSION", fileTree, templateId: data.template.id, - }) + }); }} onPublish={() => { sendEvent({ type: "PUBLISH", - }) + }); }} onCancelPublish={() => { sendEvent({ type: "CANCEL_PUBLISH", - }) + }); }} onConfirmPublish={(data) => { sendEvent({ type: "CONFIRM_PUBLISH", ...data, - }) + }); }} isAskingPublishParameters={editorState.matches( "askPublishParameters", @@ -80,7 +81,7 @@ export const TemplateVersionEditorPage: FC = () => { editorState.context.lastSuccessfulPublishIsDefault } onCreateWorkspace={() => { - navigate(`/templates/${templateName}/workspace`) + navigate(`/templates/${templateName}/workspace`); }} disablePreview={editorState.hasTag("loading")} disableUpdate={ @@ -95,17 +96,17 @@ export const TemplateVersionEditorPage: FC = () => { sendEvent({ type: "SET_MISSING_VARIABLE_VALUES", values, - }) + }); }} onCancelSubmitMissingVariableValues={() => { sendEvent({ type: "CANCEL_MISSING_VARIABLE_VALUES", - }) + }); }} /> )} - ) -} + ); +}; -export default TemplateVersionEditorPage +export default TemplateVersionEditorPage; diff --git a/site/src/pages/TemplateVersionEditorPage/data.ts b/site/src/pages/TemplateVersionEditorPage/data.ts index fba82b39b2..16bea849ce 100644 --- a/site/src/pages/TemplateVersionEditorPage/data.ts +++ b/site/src/pages/TemplateVersionEditorPage/data.ts @@ -1,7 +1,7 @@ -import { useQuery, UseQueryOptions } from "@tanstack/react-query" -import { getFile, getTemplateByName, getTemplateVersionByName } from "api/api" -import { TarReader } from "utils/tar" -import { createTemplateVersionFileTree } from "utils/templateVersion" +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; +import { getFile, getTemplateByName, getTemplateVersionByName } from "api/api"; +import { TarReader } from "utils/tar"; +import { createTemplateVersionFileTree } from "utils/templateVersion"; const getTemplateVersionData = async ( orgId: string, @@ -11,29 +11,29 @@ const getTemplateVersionData = async ( const [template, version] = await Promise.all([ getTemplateByName(orgId, templateName), getTemplateVersionByName(orgId, templateName, versionName), - ]) - const tarFile = await getFile(version.job.file_id) - const tarReader = new TarReader() - await tarReader.readFile(tarFile) - const fileTree = await createTemplateVersionFileTree(tarReader) + ]); + const tarFile = await getFile(version.job.file_id); + const tarReader = new TarReader(); + await tarReader.readFile(tarFile); + const fileTree = await createTemplateVersionFileTree(tarReader); return { template, version, fileTree, tarReader, - } -} + }; +}; type GetTemplateVersionResponse = Awaited< ReturnType -> +>; type UseTemplateVersionDataParams = { - orgId: string - templateName: string - versionName: string -} + orgId: string; + templateName: string; + versionName: string; +}; export const useTemplateVersionData = ( { templateName, versionName, orgId }: UseTemplateVersionDataParams, @@ -43,5 +43,5 @@ export const useTemplateVersionData = ( queryKey: ["templateVersion", templateName, versionName], queryFn: () => getTemplateVersionData(orgId, templateName, versionName), ...options, - }) -} + }); +}; diff --git a/site/src/pages/TemplateVersionEditorPage/types.ts b/site/src/pages/TemplateVersionEditorPage/types.ts index f3ab9aa537..c31c745a40 100644 --- a/site/src/pages/TemplateVersionEditorPage/types.ts +++ b/site/src/pages/TemplateVersionEditorPage/types.ts @@ -1,5 +1,5 @@ export type PublishVersionData = { - name: string - message: string - isActiveVersion: boolean -} + name: string; + message: string; + isActiveVersion: boolean; +}; diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPage.test.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPage.test.tsx index 25659bfe28..2f33cc0384 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPage.test.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPage.test.tsx @@ -1,42 +1,42 @@ import { renderWithAuth, waitForLoaderToBeRemoved, -} from "testHelpers/renderHelpers" -import TemplateVersionPage from "./TemplateVersionPage" -import * as templateVersionUtils from "utils/templateVersion" -import { screen } from "@testing-library/react" -import * as CreateDayString from "utils/createDayString" +} from "testHelpers/renderHelpers"; +import TemplateVersionPage from "./TemplateVersionPage"; +import * as templateVersionUtils from "utils/templateVersion"; +import { screen } from "@testing-library/react"; +import * as CreateDayString from "utils/createDayString"; -const TEMPLATE_NAME = "coder-ts" -const VERSION_NAME = "12345" -const TERRAFORM_FILENAME = "main.tf" -const README_FILENAME = "readme.md" +const TEMPLATE_NAME = "coder-ts"; +const VERSION_NAME = "12345"; +const TERRAFORM_FILENAME = "main.tf"; +const README_FILENAME = "readme.md"; const TEMPLATE_VERSION_FILES = { [TERRAFORM_FILENAME]: "{}", [README_FILENAME]: "Readme", -} +}; const setup = async () => { jest .spyOn(templateVersionUtils, "getTemplateVersionFiles") - .mockResolvedValue(TEMPLATE_VERSION_FILES) + .mockResolvedValue(TEMPLATE_VERSION_FILES); jest .spyOn(CreateDayString, "createDayString") - .mockImplementation(() => "a minute ago") + .mockImplementation(() => "a minute ago"); renderWithAuth(, { route: `/templates/${TEMPLATE_NAME}/versions/${VERSION_NAME}`, path: "/templates/:template/versions/:version", - }) - await waitForLoaderToBeRemoved() -} + }); + await waitForLoaderToBeRemoved(); +}; describe("TemplateVersionPage", () => { - beforeEach(setup) + beforeEach(setup); it("shows files", () => { - expect(screen.getByText(TERRAFORM_FILENAME)).toBeInTheDocument() - expect(screen.getByText(README_FILENAME)).toBeInTheDocument() - }) -}) + expect(screen.getByText(TERRAFORM_FILENAME)).toBeInTheDocument(); + expect(screen.getByText(README_FILENAME)).toBeInTheDocument(); + }); +}); diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx index 3664ae88bd..756f24d356 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx @@ -1,27 +1,28 @@ -import { useMachine } from "@xstate/react" -import { useOrganizationId } from "hooks/useOrganizationId" -import { useTab } from "hooks/useTab" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { useTranslation } from "react-i18next" -import { useParams } from "react-router-dom" -import { pageTitle } from "utils/page" -import { templateVersionMachine } from "xServices/templateVersion/templateVersionXService" -import TemplateVersionPageView from "./TemplateVersionPageView" +import { useMachine } from "@xstate/react"; +import { useOrganizationId } from "hooks/useOrganizationId"; +import { useTab } from "hooks/useTab"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { useTranslation } from "react-i18next"; +import { useParams } from "react-router-dom"; +import { pageTitle } from "utils/page"; +import { templateVersionMachine } from "xServices/templateVersion/templateVersionXService"; +import TemplateVersionPageView from "./TemplateVersionPageView"; type Params = { - version: string - template: string -} + version: string; + template: string; +}; export const TemplateVersionPage: FC = () => { - const { version: versionName, template: templateName } = useParams() as Params - const orgId = useOrganizationId() + const { version: versionName, template: templateName } = + useParams() as Params; + const orgId = useOrganizationId(); const [state] = useMachine(templateVersionMachine, { context: { templateName, versionName, orgId }, - }) - const tab = useTab("file", "0") - const { t } = useTranslation("templateVersionPage") + }); + const tab = useTab("file", "0"); + const { t } = useTranslation("templateVersionPage"); return ( <> @@ -38,7 +39,7 @@ export const TemplateVersionPage: FC = () => { tab={tab} /> - ) -} + ); +}; -export default TemplateVersionPage +export default TemplateVersionPage; diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.stories.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.stories.tsx index 0c259c2eaf..95893f803b 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.stories.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.stories.tsx @@ -1,30 +1,30 @@ -import { action } from "@storybook/addon-actions" -import { Story } from "@storybook/react" -import { UseTabResult } from "hooks/useTab" +import { action } from "@storybook/addon-actions"; +import { Story } from "@storybook/react"; +import { UseTabResult } from "hooks/useTab"; import { mockApiError, MockOrganization, MockTemplate, MockTemplateVersion, -} from "testHelpers/entities" +} from "testHelpers/entities"; import { TemplateVersionPageView, TemplateVersionPageViewProps, -} from "./TemplateVersionPageView" +} from "./TemplateVersionPageView"; export default { title: "pages/TemplateVersionPageView", component: TemplateVersionPageView, -} +}; const Template: Story = (args) => ( -) +); const tab: UseTabResult = { value: "0", set: action("changeTab"), -} +}; const readmeContent = `--- name:Template test @@ -35,7 +35,7 @@ You can add instructions here [Some link info](https://coder.com) \`\`\` # This is a really long sentence to test that the code block wraps into a new line properly. -\`\`\`` +\`\`\``; const defaultArgs: TemplateVersionPageViewProps = { tab, @@ -51,12 +51,12 @@ const defaultArgs: TemplateVersionPageViewProps = { "main.tf": `{}`, }, }, -} +}; -export const Default = Template.bind({}) -Default.args = defaultArgs +export const Default = Template.bind({}); +Default.args = defaultArgs; -export const Error = Template.bind({}) +export const Error = Template.bind({}); Error.args = { ...defaultArgs, context: { @@ -67,4 +67,4 @@ Error.args = { message: "Error on loading the template version", }), }, -} +}; diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx index 5a87a120f7..74b15d3965 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx @@ -1,33 +1,33 @@ -import Button from "@mui/material/Button" -import Link from "@mui/material/Link" -import EditIcon from "@mui/icons-material/Edit" -import { Loader } from "components/Loader/Loader" -import { Margins } from "components/Margins/Margins" +import Button from "@mui/material/Button"; +import Link from "@mui/material/Link"; +import EditIcon from "@mui/icons-material/Edit"; +import { Loader } from "components/Loader/Loader"; +import { Margins } from "components/Margins/Margins"; import { PageHeader, PageHeaderCaption, PageHeaderSubtitle, PageHeaderTitle, -} from "components/PageHeader/PageHeader" -import { Stack } from "components/Stack/Stack" -import { Stats, StatsItem } from "components/Stats/Stats" -import { TemplateFiles } from "components/TemplateFiles/TemplateFiles" -import { UseTabResult } from "hooks/useTab" -import { FC } from "react" -import { useTranslation } from "react-i18next" -import { Link as RouterLink } from "react-router-dom" -import { createDayString } from "utils/createDayString" -import { TemplateVersionMachineContext } from "xServices/templateVersion/templateVersionXService" -import { ErrorAlert } from "components/Alert/ErrorAlert" +} from "components/PageHeader/PageHeader"; +import { Stack } from "components/Stack/Stack"; +import { Stats, StatsItem } from "components/Stats/Stats"; +import { TemplateFiles } from "components/TemplateFiles/TemplateFiles"; +import { UseTabResult } from "hooks/useTab"; +import { FC } from "react"; +import { useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { createDayString } from "utils/createDayString"; +import { TemplateVersionMachineContext } from "xServices/templateVersion/templateVersionXService"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; export interface TemplateVersionPageViewProps { /** * Used to display the version name before loading the version in the API */ - versionName: string - templateName: string - tab: UseTabResult - context: TemplateVersionMachineContext + versionName: string; + templateName: string; + tab: UseTabResult; + context: TemplateVersionMachineContext; } export const TemplateVersionPageView: FC = ({ @@ -36,8 +36,8 @@ export const TemplateVersionPageView: FC = ({ versionName, templateName, }) => { - const { currentFiles, error, currentVersion, previousFiles } = context - const { t } = useTranslation("templateVersionPage") + const { currentFiles, error, currentVersion, previousFiles } = context; + const { t } = useTranslation("templateVersionPage"); return ( @@ -94,7 +94,7 @@ export const TemplateVersionPageView: FC = ({ )} - ) -} + ); +}; -export default TemplateVersionPageView +export default TemplateVersionPageView; diff --git a/site/src/pages/TemplatesPage/EmptyTemplates.tsx b/site/src/pages/TemplatesPage/EmptyTemplates.tsx index 228cc3c83e..5f564cae59 100644 --- a/site/src/pages/TemplatesPage/EmptyTemplates.tsx +++ b/site/src/pages/TemplatesPage/EmptyTemplates.tsx @@ -1,16 +1,16 @@ -import Button from "@mui/material/Button" -import Link from "@mui/material/Link" -import { makeStyles } from "@mui/styles" -import { TemplateExample } from "api/typesGenerated" -import { CodeExample } from "components/CodeExample/CodeExample" -import { Stack } from "components/Stack/Stack" -import { TableEmpty } from "components/TableEmpty/TableEmpty" -import { TemplateExampleCard } from "components/TemplateExampleCard/TemplateExampleCard" -import { FC } from "react" -import { useTranslation } from "react-i18next" -import { Link as RouterLink } from "react-router-dom" -import { docs } from "utils/docs" -import { Permissions } from "xServices/auth/authXService" +import Button from "@mui/material/Button"; +import Link from "@mui/material/Link"; +import { makeStyles } from "@mui/styles"; +import { TemplateExample } from "api/typesGenerated"; +import { CodeExample } from "components/CodeExample/CodeExample"; +import { Stack } from "components/Stack/Stack"; +import { TableEmpty } from "components/TableEmpty/TableEmpty"; +import { TemplateExampleCard } from "components/TemplateExampleCard/TemplateExampleCard"; +import { FC } from "react"; +import { useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; +import { docs } from "utils/docs"; +import { Permissions } from "xServices/auth/authXService"; // Those are from https://github.com/coder/coder/tree/main/examples/templates const featuredExampleIds = [ @@ -20,30 +20,30 @@ const featuredExampleIds = [ "aws-windows", "gcp-linux", "gcp-windows", -] +]; const findFeaturedExamples = (examples: TemplateExample[]) => { - const featuredExamples: TemplateExample[] = [] + const featuredExamples: TemplateExample[] = []; // We loop the featuredExampleIds first to keep the order featuredExampleIds.forEach((exampleId) => { examples.forEach((example) => { if (exampleId === example.id) { - featuredExamples.push(example) + featuredExamples.push(example); } - }) - }) + }); + }); - return featuredExamples -} + return featuredExamples; +}; export const EmptyTemplates: FC<{ - permissions: Permissions - examples: TemplateExample[] + permissions: Permissions; + examples: TemplateExample[]; }> = ({ permissions, examples }) => { - const styles = useStyles() - const { t } = useTranslation("templatesPage") - const featuredExamples = findFeaturedExamples(examples) + const styles = useStyles(); + const { t } = useTranslation("templatesPage"); + const featuredExamples = findFeaturedExamples(examples); if (permissions.createTemplates) { return ( @@ -87,7 +87,7 @@ export const EmptyTemplates: FC<{ } /> - ) + ); } return ( @@ -102,8 +102,8 @@ export const EmptyTemplates: FC<{
} /> - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ withImage: { @@ -140,4 +140,4 @@ const useStyles = makeStyles((theme) => ({ viewAllButton: { borderRadius: 9999, }, -})) +})); diff --git a/site/src/pages/TemplatesPage/TemplatesPage.test.tsx b/site/src/pages/TemplatesPage/TemplatesPage.test.tsx index afb5e9b7d1..2efd6259ab 100644 --- a/site/src/pages/TemplatesPage/TemplatesPage.test.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPage.test.tsx @@ -1,20 +1,20 @@ -import { screen } from "@testing-library/react" -import { rest } from "msw" -import * as CreateDayString from "utils/createDayString" -import { MockTemplate } from "../../testHelpers/entities" -import { renderWithAuth } from "../../testHelpers/renderHelpers" -import { server } from "../../testHelpers/server" -import { TemplatesPage } from "./TemplatesPage" -import i18next from "i18next" +import { screen } from "@testing-library/react"; +import { rest } from "msw"; +import * as CreateDayString from "utils/createDayString"; +import { MockTemplate } from "../../testHelpers/entities"; +import { renderWithAuth } from "../../testHelpers/renderHelpers"; +import { server } from "../../testHelpers/server"; +import { TemplatesPage } from "./TemplatesPage"; +import i18next from "i18next"; -const { t } = i18next +const { t } = i18next; describe("TemplatesPage", () => { beforeEach(() => { // Mocking the dayjs module within the createDayString file - const mock = jest.spyOn(CreateDayString, "createDayString") - mock.mockImplementation(() => "a minute ago") - }) + const mock = jest.spyOn(CreateDayString, "createDayString"); + mock.mockImplementation(() => "a minute ago"); + }); it("renders an empty templates page", async () => { // Given @@ -22,7 +22,7 @@ describe("TemplatesPage", () => { rest.get( "/api/v2/organizations/:organizationId/templates", (req, res, ctx) => { - return res(ctx.status(200), ctx.json([])) + return res(ctx.status(200), ctx.json([])); }, ), rest.post("/api/v2/authcheck", (req, res, ctx) => { @@ -31,40 +31,40 @@ describe("TemplatesPage", () => { ctx.json({ createTemplates: true, }), - ) + ); }), - ) + ); // When renderWithAuth(, { route: `/templates`, path: "/templates", - }) + }); // Then const emptyMessage = t("empty.message", { ns: "templatesPage", - }) - await screen.findByText(emptyMessage) - }) + }); + await screen.findByText(emptyMessage); + }); it("renders a filled templates page", async () => { // When renderWithAuth(, { route: `/templates`, path: "/templates", - }) + }); // Then - await screen.findByText(MockTemplate.display_name) - }) + await screen.findByText(MockTemplate.display_name); + }); it("shows empty view without permissions to create", async () => { server.use( rest.get( "/api/v2/organizations/:organizationId/templates", (req, res, ctx) => { - return res(ctx.status(200), ctx.json([])) + return res(ctx.status(200), ctx.json([])); }, ), rest.post("/api/v2/authcheck", (req, res, ctx) => { @@ -73,19 +73,19 @@ describe("TemplatesPage", () => { ctx.json({ createTemplates: false, }), - ) + ); }), - ) + ); // When renderWithAuth(, { route: `/templates`, path: "/templates", - }) + }); // Then const emptyMessage = t("empty.descriptionWithoutPermissions", { ns: "templatesPage", - }) - await screen.findByText(emptyMessage) - }) -}) + }); + await screen.findByText(emptyMessage); + }); +}); diff --git a/site/src/pages/TemplatesPage/TemplatesPage.tsx b/site/src/pages/TemplatesPage/TemplatesPage.tsx index e2b1f46e5e..4b7d58d87a 100644 --- a/site/src/pages/TemplatesPage/TemplatesPage.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPage.tsx @@ -1,21 +1,21 @@ -import { useMachine } from "@xstate/react" -import { useOrganizationId } from "hooks/useOrganizationId" -import { usePermissions } from "hooks/usePermissions" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "../../utils/page" -import { templatesMachine } from "../../xServices/templates/templatesXService" -import { TemplatesPageView } from "./TemplatesPageView" +import { useMachine } from "@xstate/react"; +import { useOrganizationId } from "hooks/useOrganizationId"; +import { usePermissions } from "hooks/usePermissions"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "../../utils/page"; +import { templatesMachine } from "../../xServices/templates/templatesXService"; +import { TemplatesPageView } from "./TemplatesPageView"; export const TemplatesPage: FC = () => { - const organizationId = useOrganizationId() - const permissions = usePermissions() + const organizationId = useOrganizationId(); + const permissions = usePermissions(); const [templatesState] = useMachine(templatesMachine, { context: { organizationId, permissions, }, - }) + }); return ( <> @@ -24,7 +24,7 @@ export const TemplatesPage: FC = () => { - ) -} + ); +}; -export default TemplatesPage +export default TemplatesPage; diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx index 2c74729dbc..9697c72387 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.stories.tsx @@ -1,4 +1,4 @@ -import { ComponentMeta, Story } from "@storybook/react" +import { ComponentMeta, Story } from "@storybook/react"; import { mockApiError, MockOrganization, @@ -6,19 +6,19 @@ import { MockTemplate, MockTemplateExample, MockTemplateExample2, -} from "../../testHelpers/entities" -import { TemplatesPageView, TemplatesPageViewProps } from "./TemplatesPageView" +} from "../../testHelpers/entities"; +import { TemplatesPageView, TemplatesPageViewProps } from "./TemplatesPageView"; export default { title: "pages/TemplatesPageView", component: TemplatesPageView, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) +); -export const WithTemplates = Template.bind({}) +export const WithTemplates = Template.bind({}); WithTemplates.args = { context: { organizationId: MockOrganization.id, @@ -46,17 +46,17 @@ WithTemplates.args = { ], examples: [], }, -} +}; -export const WithTemplatesSmallViewPort = Template.bind({}) +export const WithTemplatesSmallViewPort = Template.bind({}); WithTemplatesSmallViewPort.args = { ...WithTemplates.args, -} +}; WithTemplatesSmallViewPort.parameters = { chromatic: { viewports: [600] }, -} +}; -export const EmptyCanCreate = Template.bind({}) +export const EmptyCanCreate = Template.bind({}); EmptyCanCreate.args = { context: { organizationId: MockOrganization.id, @@ -65,9 +65,9 @@ EmptyCanCreate.args = { templates: [], examples: [MockTemplateExample, MockTemplateExample2], }, -} +}; -export const EmptyCannotCreate = Template.bind({}) +export const EmptyCannotCreate = Template.bind({}); EmptyCannotCreate.args = { context: { organizationId: MockOrganization.id, @@ -79,9 +79,9 @@ EmptyCannotCreate.args = { templates: [], examples: [MockTemplateExample, MockTemplateExample2], }, -} +}; -export const Error = Template.bind({}) +export const Error = Template.bind({}); Error.args = { context: { organizationId: MockOrganization.id, @@ -95,4 +95,4 @@ Error.args = { templates: undefined, examples: undefined, }, -} +}; diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index 71ac843ad2..9302a74cf9 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -1,59 +1,59 @@ -import Button from "@mui/material/Button" -import { makeStyles } from "@mui/styles" -import Table from "@mui/material/Table" -import TableBody from "@mui/material/TableBody" -import TableCell from "@mui/material/TableCell" -import TableContainer from "@mui/material/TableContainer" -import TableHead from "@mui/material/TableHead" -import TableRow from "@mui/material/TableRow" -import AddIcon from "@mui/icons-material/AddOutlined" -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { Maybe } from "components/Conditionals/Maybe" -import { FC } from "react" -import { useNavigate, Link as RouterLink } from "react-router-dom" -import { createDayString } from "utils/createDayString" +import Button from "@mui/material/Button"; +import { makeStyles } from "@mui/styles"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import AddIcon from "@mui/icons-material/AddOutlined"; +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { Maybe } from "components/Conditionals/Maybe"; +import { FC } from "react"; +import { useNavigate, Link as RouterLink } from "react-router-dom"; +import { createDayString } from "utils/createDayString"; import { formatTemplateBuildTime, formatTemplateActiveDevelopers, -} from "utils/templates" -import { AvatarData } from "../../components/AvatarData/AvatarData" -import { Margins } from "../../components/Margins/Margins" +} from "utils/templates"; +import { AvatarData } from "../../components/AvatarData/AvatarData"; +import { Margins } from "../../components/Margins/Margins"; import { PageHeader, PageHeaderSubtitle, PageHeaderTitle, -} from "../../components/PageHeader/PageHeader" -import { Stack } from "../../components/Stack/Stack" +} from "../../components/PageHeader/PageHeader"; +import { Stack } from "../../components/Stack/Stack"; import { TableLoaderSkeleton, TableRowSkeleton, -} from "../../components/TableLoader/TableLoader" +} from "../../components/TableLoader/TableLoader"; import { HelpTooltip, HelpTooltipLink, HelpTooltipLinksGroup, HelpTooltipText, HelpTooltipTitle, -} from "components/HelpTooltip/HelpTooltip" -import { EmptyTemplates } from "./EmptyTemplates" -import { TemplatesContext } from "xServices/templates/templatesXService" -import { useClickableTableRow } from "hooks/useClickableTableRow" -import { Template } from "api/typesGenerated" -import { combineClasses } from "utils/combineClasses" -import { colors } from "theme/colors" -import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined" -import { Avatar } from "components/Avatar/Avatar" -import { ErrorAlert } from "components/Alert/ErrorAlert" -import { docs } from "utils/docs" -import Skeleton from "@mui/material/Skeleton" -import { Box } from "@mui/system" -import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton" +} from "components/HelpTooltip/HelpTooltip"; +import { EmptyTemplates } from "./EmptyTemplates"; +import { TemplatesContext } from "xServices/templates/templatesXService"; +import { useClickableTableRow } from "hooks/useClickableTableRow"; +import { Template } from "api/typesGenerated"; +import { combineClasses } from "utils/combineClasses"; +import { colors } from "theme/colors"; +import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined"; +import { Avatar } from "components/Avatar/Avatar"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { docs } from "utils/docs"; +import Skeleton from "@mui/material/Skeleton"; +import { Box } from "@mui/system"; +import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"; export const Language = { developerCount: (activeCount: number): string => { return `${formatTemplateActiveDevelopers(activeCount)} developer${ activeCount !== 1 ? "s" : "" - }` + }`; }, nameLabel: "Name", buildTimeLabel: "Build time", @@ -63,7 +63,7 @@ export const Language = { templateTooltipText: "With templates you can create a common configuration for your workspaces using Terraform.", templateTooltipLink: "Manage templates", -} +}; const TemplateHelpTooltip: React.FC = () => { return ( @@ -76,18 +76,18 @@ const TemplateHelpTooltip: React.FC = () => { - ) -} + ); +}; const TemplateRow: FC<{ template: Template }> = ({ template }) => { - const templatePageLink = `/templates/${template.name}` - const hasIcon = template.icon && template.icon !== "" - const navigate = useNavigate() - const styles = useStyles() + const templatePageLink = `/templates/${template.name}`; + const hasIcon = template.icon && template.icon !== ""; + const navigate = useNavigate(); + const styles = useStyles(); const { className: clickableClassName, ...clickableRow } = useClickableTableRow(() => { - navigate(templatePageLink) - }) + navigate(templatePageLink); + }); return ( = ({ template }) => { startIcon={} title={`Create a workspace using the ${template.display_name} template`} onClick={(e) => { - e.stopPropagation() - navigate(`/templates/${template.name}/workspace`) + e.stopPropagation(); + navigate(`/templates/${template.name}/workspace`); }} > Create Workspace - ) -} + ); +}; export interface TemplatesPageViewProps { - context: TemplatesContext + context: TemplatesContext; } export const TemplatesPageView: FC< React.PropsWithChildren > = ({ context }) => { - const { templates, error, examples, permissions } = context - const isLoading = !templates - const isEmpty = Boolean(templates && templates.length === 0) + const { templates, error, examples, permissions } = context; + const isLoading = !templates; + const isEmpty = Boolean(templates && templates.length === 0); return ( @@ -225,8 +225,8 @@ export const TemplatesPageView: FC< - ) -} + ); +}; const TableLoader = () => { return ( @@ -251,8 +251,8 @@ const TableLoader = () => { - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ templateIconWrapper: { @@ -284,4 +284,4 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.secondary, transition: "none", }, -})) +})); diff --git a/site/src/pages/TerminalPage/TerminalPage.test.tsx b/site/src/pages/TerminalPage/TerminalPage.test.tsx index 6412efaaa5..54ae04ae02 100644 --- a/site/src/pages/TerminalPage/TerminalPage.test.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.test.tsx @@ -1,21 +1,21 @@ -import { waitFor } from "@testing-library/react" -import "jest-canvas-mock" -import WS from "jest-websocket-mock" -import { rest } from "msw" +import { waitFor } from "@testing-library/react"; +import "jest-canvas-mock"; +import WS from "jest-websocket-mock"; +import { rest } from "msw"; import { MockUser, MockWorkspace, MockWorkspaceAgent, -} from "testHelpers/entities" -import { TextDecoder, TextEncoder } from "util" -import { ReconnectingPTYRequest } from "../../api/types" +} from "testHelpers/entities"; +import { TextDecoder, TextEncoder } from "util"; +import { ReconnectingPTYRequest } from "../../api/types"; import { renderWithAuth, waitForLoaderToBeRemoved, -} from "../../testHelpers/renderHelpers" -import { server } from "../../testHelpers/server" -import TerminalPage, { Language } from "./TerminalPage" -import * as API from "api/api" +} from "../../testHelpers/renderHelpers"; +import { server } from "../../testHelpers/server"; +import TerminalPage, { Language } from "./TerminalPage"; +import * as API from "api/api"; Object.defineProperty(window, "matchMedia", { writable: true, @@ -29,11 +29,11 @@ Object.defineProperty(window, "matchMedia", { removeEventListener: jest.fn(), dispatchEvent: jest.fn(), })), -}) +}); Object.defineProperty(window, "TextEncoder", { value: TextEncoder, -}) +}); const renderTerminal = async ( route = `/${MockUser.username}/${MockWorkspace.name}/terminal`, @@ -41,42 +41,44 @@ const renderTerminal = async ( const utils = renderWithAuth(, { route, path: "/:username/:workspace/terminal", - }) - await waitForLoaderToBeRemoved() - return utils -} + }); + await waitForLoaderToBeRemoved(); + return utils; +}; const expectTerminalText = (container: HTMLElement, text: string) => { return waitFor( () => { - const elements = container.getElementsByClassName("xterm-rows") + const elements = container.getElementsByClassName("xterm-rows"); if (elements.length === 0) { - throw new Error("no xterm-rows") + throw new Error("no xterm-rows"); } - const row = elements[0] as HTMLDivElement + const row = elements[0] as HTMLDivElement; if (!row.textContent) { - throw new Error("no text content") + throw new Error("no text content"); } - expect(row.textContent).toContain(text) + expect(row.textContent).toContain(text); }, { timeout: 3_000 }, - ) -} + ); +}; describe("TerminalPage", () => { it("loads the right workspace data", async () => { const spy = jest .spyOn(API, "getWorkspaceByOwnerAndName") - .mockResolvedValue(MockWorkspace) - await renderTerminal(`/${MockUser.username}/${MockWorkspace.name}/terminal`) + .mockResolvedValue(MockWorkspace); + await renderTerminal( + `/${MockUser.username}/${MockWorkspace.name}/terminal`, + ); await waitFor(() => { expect(API.getWorkspaceByOwnerAndName).toHaveBeenCalledWith( MockUser.username, MockWorkspace.name, - ) - }) - spy.mockRestore() - }) + ); + }); + spy.mockRestore(); + }); it("shows an error if fetching workspace fails", async () => { // Given @@ -84,87 +86,87 @@ describe("TerminalPage", () => { rest.get( "/api/v2/users/:userId/workspace/:workspaceName", (req, res, ctx) => { - return res(ctx.status(500), ctx.json({ id: "workspace-id" })) + return res(ctx.status(500), ctx.json({ id: "workspace-id" })); }, ), - ) + ); // When - const { container } = await renderTerminal() + const { container } = await renderTerminal(); // Then - await expectTerminalText(container, Language.workspaceErrorMessagePrefix) - }) + await expectTerminalText(container, Language.workspaceErrorMessagePrefix); + }); it("shows an error if the websocket fails", async () => { // Given server.use( rest.get("/api/v2/workspaceagents/:agentId/pty", (req, res, ctx) => { - return res(ctx.status(500), ctx.json({})) + return res(ctx.status(500), ctx.json({})); }), - ) + ); // When - const { container } = await renderTerminal() + const { container } = await renderTerminal(); // Then - await expectTerminalText(container, Language.websocketErrorMessagePrefix) - }) + await expectTerminalText(container, Language.websocketErrorMessagePrefix); + }); it("renders data from the backend", async () => { // Given const ws = new WS( `ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`, - ) - const text = "something to render" + ); + const text = "something to render"; // When - const { container } = await renderTerminal() + const { container } = await renderTerminal(); // Then - await ws.connected - ws.send(text) - await expectTerminalText(container, text) - ws.close() - }) + await ws.connected; + ws.send(text); + await expectTerminalText(container, text); + ws.close(); + }); it("resizes on connect", async () => { // Given const ws = new WS( `ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`, - ) + ); // When - await renderTerminal() + await renderTerminal(); // Then - await ws.connected - const msg = await ws.nextMessage + await ws.connected; + const msg = await ws.nextMessage; const req: ReconnectingPTYRequest = JSON.parse( new TextDecoder().decode(msg as Uint8Array), - ) + ); - expect(req.height).toBeGreaterThan(0) - expect(req.width).toBeGreaterThan(0) - ws.close() - }) + expect(req.height).toBeGreaterThan(0); + expect(req.width).toBeGreaterThan(0); + ws.close(); + }); it("supports workspace.agent syntax", async () => { // Given const ws = new WS( `ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`, - ) - const text = "something to render" + ); + const text = "something to render"; // When const { container } = await renderTerminal( `/some-user/${MockWorkspace.name}.${MockWorkspaceAgent.name}/terminal`, - ) + ); // Then - await ws.connected - ws.send(text) - await expectTerminalText(container, text) - ws.close() - }) -}) + await ws.connected; + ws.send(text); + await expectTerminalText(container, text); + ws.close(); + }); +}); diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index 61c3501976..e8e4e4ab25 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -1,85 +1,85 @@ -import { makeStyles, useTheme } from "@mui/styles" -import { useMachine } from "@xstate/react" -import { Stack } from "components/Stack/Stack" -import { FC, useCallback, useEffect, useRef, useState } from "react" -import { Helmet } from "react-helmet-async" -import { useNavigate, useParams, useSearchParams } from "react-router-dom" -import { colors } from "theme/colors" -import { v4 as uuidv4 } from "uuid" -import * as XTerm from "xterm" -import { WebglAddon } from "xterm-addon-webgl" -import { FitAddon } from "xterm-addon-fit" -import { WebLinksAddon } from "xterm-addon-web-links" -import { Unicode11Addon } from "xterm-addon-unicode11" -import "xterm/css/xterm.css" -import { MONOSPACE_FONT_FAMILY } from "../../theme/constants" -import { pageTitle } from "../../utils/page" -import { terminalMachine } from "../../xServices/terminal/terminalXService" -import { useProxy } from "contexts/ProxyContext" -import Box from "@mui/material/Box" -import { useDashboard } from "components/Dashboard/DashboardProvider" -import { Region, WorkspaceAgent } from "api/typesGenerated" -import { getLatencyColor } from "utils/latency" -import Popover from "@mui/material/Popover" -import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency" -import TerminalPageAlert, { TerminalPageAlertType } from "./TerminalPageAlert" -import { portForwardURL } from "utils/portForward" +import { makeStyles, useTheme } from "@mui/styles"; +import { useMachine } from "@xstate/react"; +import { Stack } from "components/Stack/Stack"; +import { FC, useCallback, useEffect, useRef, useState } from "react"; +import { Helmet } from "react-helmet-async"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; +import { colors } from "theme/colors"; +import { v4 as uuidv4 } from "uuid"; +import * as XTerm from "xterm"; +import { WebglAddon } from "xterm-addon-webgl"; +import { FitAddon } from "xterm-addon-fit"; +import { WebLinksAddon } from "xterm-addon-web-links"; +import { Unicode11Addon } from "xterm-addon-unicode11"; +import "xterm/css/xterm.css"; +import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"; +import { pageTitle } from "../../utils/page"; +import { terminalMachine } from "../../xServices/terminal/terminalXService"; +import { useProxy } from "contexts/ProxyContext"; +import Box from "@mui/material/Box"; +import { useDashboard } from "components/Dashboard/DashboardProvider"; +import { Region, WorkspaceAgent } from "api/typesGenerated"; +import { getLatencyColor } from "utils/latency"; +import Popover from "@mui/material/Popover"; +import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency"; +import TerminalPageAlert, { TerminalPageAlertType } from "./TerminalPageAlert"; +import { portForwardURL } from "utils/portForward"; export const Language = { workspaceErrorMessagePrefix: "Unable to fetch workspace: ", workspaceAgentErrorMessagePrefix: "Unable to fetch workspace agent: ", websocketErrorMessagePrefix: "WebSocket failed: ", -} +}; const useTerminalWarning = ({ agent }: { agent?: WorkspaceAgent }) => { - const lifecycleState = agent?.lifecycle_state + const lifecycleState = agent?.lifecycle_state; const [startupWarning, setStartupWarning] = useState< TerminalPageAlertType | undefined - >(undefined) + >(undefined); useEffect(() => { if (lifecycleState === "start_error") { - setStartupWarning("error") + setStartupWarning("error"); } else if (lifecycleState === "starting") { - setStartupWarning("starting") + setStartupWarning("starting"); } else { setStartupWarning((prev) => { if (prev === "starting") { - return "success" + return "success"; } - return undefined - }) + return undefined; + }); } - }, [lifecycleState]) + }, [lifecycleState]); return { startupWarning, - } -} + }; +}; type TerminalPageProps = React.PropsWithChildren<{ - renderer: "webgl" | "dom" -}> + renderer: "webgl" | "dom"; +}>; const TerminalPage: FC = ({ renderer }) => { - const navigate = useNavigate() - const styles = useStyles() - const { proxy } = useProxy() - const params = useParams() as { username: string; workspace: string } - const username = params.username.replace("@", "") - const workspaceName = params.workspace - const xtermRef = useRef(null) - const [terminal, setTerminal] = useState(null) - const [fitAddon, setFitAddon] = useState(null) - const [searchParams] = useSearchParams() + const navigate = useNavigate(); + const styles = useStyles(); + const { proxy } = useProxy(); + const params = useParams() as { username: string; workspace: string }; + const username = params.username.replace("@", ""); + const workspaceName = params.workspace; + const xtermRef = useRef(null); + const [terminal, setTerminal] = useState(null); + const [fitAddon, setFitAddon] = useState(null); + const [searchParams] = useSearchParams(); // The reconnection token is a unique token that identifies // a terminal session. It's generated by the client to reduce // a round-trip, and must be a UUIDv4. - const reconnectionToken = searchParams.get("reconnect") ?? uuidv4() - const command = searchParams.get("command") || undefined + const reconnectionToken = searchParams.get("reconnect") ?? uuidv4(); + const command = searchParams.get("command") || undefined; // The workspace name is in the format: // [.] - const workspaceNameParts = workspaceName?.split(".") + const workspaceNameParts = workspaceName?.split("."); const [terminalState, sendEvent] = useMachine(terminalMachine, { context: { agentName: workspaceNameParts?.[1], @@ -94,32 +94,32 @@ const TerminalPage: FC = ({ renderer }) => { if (typeof event.data === "string") { // This exclusively occurs when testing. // "jest-websocket-mock" doesn't support ArrayBuffer. - terminal?.write(event.data) + terminal?.write(event.data); } else { - terminal?.write(new Uint8Array(event.data)) + terminal?.write(new Uint8Array(event.data)); } }, }, - }) - const isConnected = terminalState.matches("connected") - const isDisconnected = terminalState.matches("disconnected") + }); + const isConnected = terminalState.matches("connected"); + const isDisconnected = terminalState.matches("disconnected"); const { workspaceError, workspace, workspaceAgentError, workspaceAgent, websocketError, - } = terminalState.context - const reloading = useReloading(isDisconnected) - const dashboard = useDashboard() - const proxyContext = useProxy() - const selectedProxy = proxyContext.proxy.proxy + } = terminalState.context; + const reloading = useReloading(isDisconnected); + const dashboard = useDashboard(); + const proxyContext = useProxy(); + const selectedProxy = proxyContext.proxy.proxy; const latency = selectedProxy ? proxyContext.proxyLatencies[selectedProxy.id] - : undefined + : undefined; const { startupWarning } = useTerminalWarning({ agent: workspaceAgent, - }) + }); // handleWebLink handles opening of URLs in the terminal! const handleWebLink = useCallback( @@ -130,30 +130,30 @@ const TerminalPage: FC = ({ renderer }) => { !username || !proxy.preferredWildcardHostname ) { - return + return; } const open = (uri: string) => { // Copied from: https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-web-links/src/WebLinksAddon.ts#L23 - const newWindow = window.open() + const newWindow = window.open(); if (newWindow) { try { - newWindow.opener = null + newWindow.opener = null; } catch { // no-op, Electron can throw } - newWindow.location.href = uri + newWindow.location.href = uri; } else { - console.warn("Opening link blocked as opener could not be cleared") + console.warn("Opening link blocked as opener could not be cleared"); } - } + }; try { - const url = new URL(uri) - const localHosts = ["0.0.0.0", "127.0.0.1", "localhost"] + const url = new URL(uri); + const localHosts = ["0.0.0.0", "127.0.0.1", "localhost"]; if (!localHosts.includes(url.hostname)) { - open(uri) - return + open(uri); + return; } open( portForwardURL( @@ -163,18 +163,18 @@ const TerminalPage: FC = ({ renderer }) => { workspace.name, username, ) + url.pathname, - ) + ); } catch (ex) { - open(uri) + open(uri); } }, [workspaceAgent, workspace, username, proxy.preferredWildcardHostname], - ) + ); // Create the terminal! useEffect(() => { if (!xtermRef.current) { - return + return; } const terminal = new XTerm.Terminal({ allowProposedApi: true, @@ -185,29 +185,29 @@ const TerminalPage: FC = ({ renderer }) => { theme: { background: colors.gray[16], }, - }) + }); // DOM is the default renderer. if (renderer === "webgl") { - terminal.loadAddon(new WebglAddon()) + terminal.loadAddon(new WebglAddon()); } - const fitAddon = new FitAddon() - setFitAddon(fitAddon) - terminal.loadAddon(fitAddon) - terminal.loadAddon(new Unicode11Addon()) - terminal.unicode.activeVersion = "11" + const fitAddon = new FitAddon(); + setFitAddon(fitAddon); + terminal.loadAddon(fitAddon); + terminal.loadAddon(new Unicode11Addon()); + terminal.unicode.activeVersion = "11"; terminal.loadAddon( new WebLinksAddon((_, uri) => { - handleWebLink(uri) + handleWebLink(uri); }), - ) + ); terminal.onData((data) => { sendEvent({ type: "WRITE", request: { data: data, }, - }) - }) + }); + }); terminal.onResize((event) => { sendEvent({ type: "WRITE", @@ -215,29 +215,29 @@ const TerminalPage: FC = ({ renderer }) => { height: event.rows, width: event.cols, }, - }) - }) - setTerminal(terminal) - terminal.open(xtermRef.current) + }); + }); + setTerminal(terminal); + terminal.open(xtermRef.current); const listener = () => { // This will trigger a resize event on the terminal. - fitAddon.fit() - } - window.addEventListener("resize", listener) + fitAddon.fit(); + }; + window.addEventListener("resize", listener); return () => { - window.removeEventListener("resize", listener) - terminal.dispose() - } - }, [renderer, sendEvent, xtermRef, handleWebLink]) + window.removeEventListener("resize", listener); + terminal.dispose(); + }; + }, [renderer, sendEvent, xtermRef, handleWebLink]); // Triggers the initial terminal connection using // the reconnection token and workspace name found // from the router. useEffect(() => { if (searchParams.get("reconnect") === reconnectionToken) { - return + return; } - searchParams.set("reconnect", reconnectionToken) + searchParams.set("reconnect", reconnectionToken); navigate( { search: searchParams.toString(), @@ -245,56 +245,56 @@ const TerminalPage: FC = ({ renderer }) => { { replace: true, }, - ) - }, [searchParams, navigate, reconnectionToken]) + ); + }, [searchParams, navigate, reconnectionToken]); // Apply terminal options based on connection state. useEffect(() => { if (!terminal || !fitAddon) { - return + return; } // We have to fit twice here. It's unknown why, but // the first fit will overflow slightly in some // scenarios. Applying a second fit resolves this. - fitAddon.fit() - fitAddon.fit() + fitAddon.fit(); + fitAddon.fit(); if (!isConnected) { // Disable user input when not connected. terminal.options = { disableStdin: true, - } + }; if (workspaceError instanceof Error) { terminal.writeln( Language.workspaceErrorMessagePrefix + workspaceError.message, - ) + ); } if (workspaceAgentError instanceof Error) { terminal.writeln( Language.workspaceAgentErrorMessagePrefix + workspaceAgentError.message, - ) + ); } if (websocketError instanceof Error) { terminal.writeln( Language.websocketErrorMessagePrefix + websocketError.message, - ) + ); } - return + return; } // The terminal should be cleared on each reconnect // because all data is re-rendered from the backend. - terminal.clear() + terminal.clear(); // Focusing on connection allows users to reload the // page and start typing immediately. - terminal.focus() + terminal.focus(); terminal.options = { disableStdin: false, windowsMode: workspaceAgent?.operating_system === "windows", - } + }; // Update the terminal size post-fit. sendEvent({ @@ -303,7 +303,7 @@ const TerminalPage: FC = ({ renderer }) => { height: terminal.rows, width: terminal.cols, }, - }) + }); }, [ workspaceError, workspaceAgentError, @@ -313,7 +313,7 @@ const TerminalPage: FC = ({ renderer }) => { fitAddon, isConnected, sendEvent, - ]) + ]); return ( <> @@ -345,7 +345,7 @@ const TerminalPage: FC = ({ renderer }) => { { - fitAddon?.fit() + fitAddon?.fit(); }} /> )} @@ -361,14 +361,14 @@ const TerminalPage: FC = ({ renderer }) => { )} - ) -} + ); +}; const BottomBar = ({ proxy, latency }: { proxy: Region; latency?: number }) => { - const theme = useTheme() - const color = getLatencyColor(theme, latency) - const anchorRef = useRef(null) - const [isOpen, setIsOpen] = useState(false) + const theme = useTheme(); + const color = getLatencyColor(theme, latency); + const anchorRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); return ( { - ) -} + ); +}; const useReloading = (isDisconnected: boolean) => { const [status, setStatus] = useState<"reloading" | "notReloading">( "notReloading", - ) + ); // Retry connection on key press when it is disconnected useEffect(() => { if (!isDisconnected) { - return + return; } const keyDownHandler = () => { - setStatus("reloading") - window.location.reload() - } + setStatus("reloading"); + window.location.reload(); + }; - document.addEventListener("keydown", keyDownHandler) + document.addEventListener("keydown", keyDownHandler); return () => { - document.removeEventListener("keydown", keyDownHandler) - } - }, [isDisconnected]) + document.removeEventListener("keydown", keyDownHandler); + }; + }, [isDisconnected]); return { status, - } -} + }; +}; const useStyles = makeStyles((theme) => ({ overlay: { @@ -575,6 +575,6 @@ const useStyles = makeStyles((theme) => ({ alertActions: { marginLeft: "auto", }, -})) +})); -export default TerminalPage +export default TerminalPage; diff --git a/site/src/pages/TerminalPage/TerminalPageAlert.stories.tsx b/site/src/pages/TerminalPage/TerminalPageAlert.stories.tsx index d781188335..265769c5ce 100644 --- a/site/src/pages/TerminalPage/TerminalPageAlert.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPageAlert.stories.tsx @@ -1,6 +1,6 @@ -import type { Meta, StoryObj } from "@storybook/react" +import type { Meta, StoryObj } from "@storybook/react"; -import TerminalPageAlert from "./TerminalPageAlert" +import TerminalPageAlert from "./TerminalPageAlert"; const meta: Meta = { component: TerminalPageAlert, @@ -13,25 +13,25 @@ const meta: Meta = { options: ["error", "starting", "success"], }, }, -} -type Story = StoryObj +}; +type Story = StoryObj; export const Error: Story = { args: { alertType: "error", }, -} +}; export const Starting: Story = { args: { alertType: "starting", }, -} +}; export const Success: Story = { args: { alertType: "success", }, -} +}; -export default meta +export default meta; diff --git a/site/src/pages/TerminalPage/TerminalPageAlert.tsx b/site/src/pages/TerminalPage/TerminalPageAlert.tsx index f40c70c3e5..13199a0911 100644 --- a/site/src/pages/TerminalPage/TerminalPageAlert.tsx +++ b/site/src/pages/TerminalPage/TerminalPageAlert.tsx @@ -1,18 +1,18 @@ -import { AlertColor } from "@mui/material/Alert/Alert" -import Button from "@mui/material/Button" -import Link from "@mui/material/Link" -import { Alert } from "components/Alert/Alert" -import { ReactNode } from "react" -import { docs } from "utils/docs" +import { AlertColor } from "@mui/material/Alert/Alert"; +import Button from "@mui/material/Button"; +import Link from "@mui/material/Link"; +import { Alert } from "components/Alert/Alert"; +import { ReactNode } from "react"; +import { docs } from "utils/docs"; -export type TerminalPageAlertType = "error" | "starting" | "success" +export type TerminalPageAlertType = "error" | "starting" | "success"; type MapAlertTypeToComponent = { [key in TerminalPageAlertType]: { - severity: AlertColor - children: ReactNode | undefined - } -} + severity: AlertColor; + children: ReactNode | undefined; + }; +}; const mapAlertTypeToText: MapAlertTypeToComponent = { error: { @@ -86,16 +86,16 @@ const mapAlertTypeToText: MapAlertTypeToComponent = { ), }, -} +}; export default ({ alertType, onDismiss, }: { - alertType: TerminalPageAlertType - onDismiss: () => void + alertType: TerminalPageAlertType; + onDismiss: () => void; }) => { - const severity = mapAlertTypeToText[alertType].severity + const severity = mapAlertTypeToText[alertType].severity; return ( { // By redirecting the user without the session in the URL we // create a new one - window.location.href = window.location.pathname + window.location.href = window.location.pathname; }} > Refresh session @@ -127,5 +127,5 @@ export default ({ > {mapAlertTypeToText[alertType].children} - ) -} + ); +}; diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountForm.stories.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountForm.stories.tsx index 6f32a5d04f..a056aa1d8f 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountForm.stories.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountForm.stories.tsx @@ -1,6 +1,6 @@ -import { Story } from "@storybook/react" -import { AccountForm, AccountFormProps } from "./AccountForm" -import { mockApiError } from "testHelpers/entities" +import { Story } from "@storybook/react"; +import { AccountForm, AccountFormProps } from "./AccountForm"; +import { mockApiError } from "testHelpers/entities"; export default { title: "components/AccountForm", @@ -8,13 +8,13 @@ export default { argTypes: { onSubmit: { action: "Submit" }, }, -} +}; const Template: Story = (args: AccountFormProps) => ( -) +); -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { email: "test-user@org.com", isLoading: false, @@ -23,17 +23,17 @@ Example.args = { }, updateProfileError: undefined, onSubmit: () => { - return Promise.resolve() + return Promise.resolve(); }, -} +}; -export const Loading = Template.bind({}) +export const Loading = Template.bind({}); Loading.args = { ...Example.args, isLoading: true, -} +}; -export const WithError = Template.bind({}) +export const WithError = Template.bind({}); WithError.args = { ...Example.args, updateProfileError: mockApiError({ @@ -48,4 +48,4 @@ WithError.args = { initialTouched: { username: true, }, -} +}; diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountForm.test.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountForm.test.tsx index bad769ded5..dadb8f8881 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountForm.test.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountForm.test.tsx @@ -1,7 +1,7 @@ -import { screen } from "@testing-library/react" -import { MockUser2 } from "testHelpers/entities" -import { render } from "testHelpers/renderHelpers" -import { AccountForm, AccountFormValues } from "./AccountForm" +import { screen } from "@testing-library/react"; +import { MockUser2 } from "testHelpers/entities"; +import { render } from "testHelpers/renderHelpers"; +import { AccountForm, AccountFormValues } from "./AccountForm"; // NOTE: it does not matter what the role props of MockUser are set to, // only that editable is set to true or false. This is passed from @@ -12,7 +12,7 @@ describe("AccountForm", () => { // Given const mockInitialValues: AccountFormValues = { username: MockUser2.username, - } + }; // When render( @@ -22,27 +22,27 @@ describe("AccountForm", () => { initialValues={mockInitialValues} isLoading={false} onSubmit={() => { - return + return; }} />, - ) + ); // Then - const el = await screen.findByLabelText("Username") - expect(el).toBeEnabled() + const el = await screen.findByLabelText("Username"); + expect(el).toBeEnabled(); const btn = await screen.findByRole("button", { name: /Update account/i, - }) - expect(btn).toBeEnabled() - }) - }) + }); + expect(btn).toBeEnabled(); + }); + }); describe("when editable is set to false", () => { it("does not allow updating username", async () => { // Given const mockInitialValues: AccountFormValues = { username: MockUser2.username, - } + }; // When render( @@ -52,18 +52,18 @@ describe("AccountForm", () => { initialValues={mockInitialValues} isLoading={false} onSubmit={() => { - return + return; }} />, - ) + ); // Then - const el = await screen.findByLabelText("Username") - expect(el).toBeDisabled() + const el = await screen.findByLabelText("Username"); + expect(el).toBeDisabled(); const btn = await screen.findByRole("button", { name: /Update account/i, - }) - expect(btn).toBeDisabled() - }) - }) -}) + }); + expect(btn).toBeDisabled(); + }); + }); +}); diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountForm.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountForm.tsx index b60e5cdb94..9c6648200f 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountForm.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountForm.tsx @@ -1,35 +1,39 @@ -import TextField from "@mui/material/TextField" -import { FormikContextType, FormikTouched, useFormik } from "formik" -import { FC } from "react" -import * as Yup from "yup" -import { getFormHelpers, nameValidator, onChangeTrimmed } from "utils/formUtils" -import { LoadingButton } from "components/LoadingButton/LoadingButton" -import { ErrorAlert } from "components/Alert/ErrorAlert" -import { Form, FormFields } from "components/Form/Form" +import TextField from "@mui/material/TextField"; +import { FormikContextType, FormikTouched, useFormik } from "formik"; +import { FC } from "react"; +import * as Yup from "yup"; +import { + getFormHelpers, + nameValidator, + onChangeTrimmed, +} from "utils/formUtils"; +import { LoadingButton } from "components/LoadingButton/LoadingButton"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Form, FormFields } from "components/Form/Form"; export interface AccountFormValues { - username: string + username: string; } export const Language = { usernameLabel: "Username", emailLabel: "Email", updateSettings: "Update account", -} +}; const validationSchema = Yup.object({ username: nameValidator(Language.usernameLabel), -}) +}); export interface AccountFormProps { - editable: boolean - email: string - isLoading: boolean - initialValues: AccountFormValues - onSubmit: (values: AccountFormValues) => void - updateProfileError?: unknown + editable: boolean; + email: string; + isLoading: boolean; + initialValues: AccountFormValues; + onSubmit: (values: AccountFormValues) => void; + updateProfileError?: unknown; // initialTouched is only used for testing the error state of the form. - initialTouched?: FormikTouched + initialTouched?: FormikTouched; } export const AccountForm: FC> = ({ @@ -47,11 +51,11 @@ export const AccountForm: FC> = ({ validationSchema, onSubmit, initialTouched, - }) + }); const getFieldHelpers = getFormHelpers( form, updateProfileError, - ) + ); return ( <> @@ -90,5 +94,5 @@ export const AccountForm: FC> = ({ - ) -} + ); +}; diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx index 24167a1caf..aeec5d4fba 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx @@ -1,29 +1,29 @@ -import { fireEvent, screen, waitFor } from "@testing-library/react" -import * as API from "api/api" -import * as AccountForm from "./AccountForm" -import { renderWithAuth } from "testHelpers/renderHelpers" -import * as AuthXService from "xServices/auth/authXService" -import { AccountPage } from "./AccountPage" -import i18next from "i18next" -import { mockApiError } from "testHelpers/entities" +import { fireEvent, screen, waitFor } from "@testing-library/react"; +import * as API from "api/api"; +import * as AccountForm from "./AccountForm"; +import { renderWithAuth } from "testHelpers/renderHelpers"; +import * as AuthXService from "xServices/auth/authXService"; +import { AccountPage } from "./AccountPage"; +import i18next from "i18next"; +import { mockApiError } from "testHelpers/entities"; -const { t } = i18next +const { t } = i18next; const renderPage = () => { - return renderWithAuth() -} + return renderWithAuth(); +}; const newData = { username: "user", -} +}; const fillAndSubmitForm = async () => { - await waitFor(() => screen.findByLabelText("Username")) + await waitFor(() => screen.findByLabelText("Username")); fireEvent.change(screen.getByLabelText("Username"), { target: { value: newData.username }, - }) - fireEvent.click(screen.getByText(AccountForm.Language.updateSettings)) -} + }); + fireEvent.click(screen.getByText(AccountForm.Language.updateSettings)); +}; describe("AccountPage", () => { describe("when it is a success", () => { @@ -41,18 +41,18 @@ describe("AccountPage", () => { login_type: "password", ...data, }), - ) - const { user } = renderPage() - await fillAndSubmitForm() + ); + const { user } = renderPage(); + await fillAndSubmitForm(); const successMessage = await screen.findByText( AuthXService.Language.successProfileUpdate, - ) - expect(successMessage).toBeDefined() - expect(API.updateProfile).toBeCalledTimes(1) - expect(API.updateProfile).toBeCalledWith(user.id, newData) - }) - }) + ); + expect(successMessage).toBeDefined(); + expect(API.updateProfile).toBeCalledTimes(1); + expect(API.updateProfile).toBeCalledWith(user.id, newData); + }); + }); describe("when the username is already taken", () => { it("shows an error", async () => { @@ -63,34 +63,36 @@ describe("AccountPage", () => { { detail: "Username is already in use", field: "username" }, ], }), - ) + ); - const { user } = renderPage() - await fillAndSubmitForm() + const { user } = renderPage(); + await fillAndSubmitForm(); - const errorMessage = await screen.findByText("Username is already in use") - expect(errorMessage).toBeDefined() - expect(API.updateProfile).toBeCalledTimes(1) - expect(API.updateProfile).toBeCalledWith(user.id, newData) - }) - }) + const errorMessage = await screen.findByText( + "Username is already in use", + ); + expect(errorMessage).toBeDefined(); + expect(API.updateProfile).toBeCalledTimes(1); + expect(API.updateProfile).toBeCalledWith(user.id, newData); + }); + }); describe("when it is an unknown error", () => { it("shows a generic error message", async () => { jest.spyOn(API, "updateProfile").mockRejectedValueOnce({ data: "unknown error", - }) + }); - const { user } = renderPage() - await fillAndSubmitForm() + const { user } = renderPage(); + await fillAndSubmitForm(); const errorText = t("warningsAndErrors.somethingWentWrong", { ns: "common", - }) - const errorMessage = await screen.findByText(errorText) - expect(errorMessage).toBeDefined() - expect(API.updateProfile).toBeCalledTimes(1) - expect(API.updateProfile).toBeCalledWith(user.id, newData) - }) - }) -}) + }); + const errorMessage = await screen.findByText(errorText); + expect(errorMessage).toBeDefined(); + expect(API.updateProfile).toBeCalledTimes(1); + expect(API.updateProfile).toBeCalledWith(user.id, newData); + }); + }); +}); diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx index 8286e8e40a..4a60e6dacf 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx @@ -1,16 +1,16 @@ -import { FC } from "react" -import { Section } from "components/SettingsLayout/Section" -import { AccountForm } from "./AccountForm" -import { useAuth } from "components/AuthProvider/AuthProvider" -import { useMe } from "hooks/useMe" -import { usePermissions } from "hooks/usePermissions" +import { FC } from "react"; +import { Section } from "components/SettingsLayout/Section"; +import { AccountForm } from "./AccountForm"; +import { useAuth } from "components/AuthProvider/AuthProvider"; +import { useMe } from "hooks/useMe"; +import { usePermissions } from "hooks/usePermissions"; export const AccountPage: FC = () => { - const [authState, authSend] = useAuth() - const me = useMe() - const permissions = usePermissions() - const { updateProfileError } = authState.context - const canEditUsers = permissions && permissions.updateUsers + const [authState, authSend] = useAuth(); + const me = useMe(); + const permissions = usePermissions(); + const { updateProfileError } = authState.context; + const canEditUsers = permissions && permissions.updateUsers; return (
@@ -26,11 +26,11 @@ export const AccountPage: FC = () => { authSend({ type: "UPDATE_PROFILE", data, - }) + }); }} />
- ) -} + ); +}; -export default AccountPage +export default AccountPage; diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx index 1c0ea90613..fdb6c196e3 100644 --- a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx +++ b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx @@ -1,99 +1,99 @@ -import { fireEvent, screen, within } from "@testing-library/react" -import * as API from "../../../api/api" -import { renderWithAuth } from "../../../testHelpers/renderHelpers" -import { Language as SSHKeysPageLanguage, SSHKeysPage } from "./SSHKeysPage" -import { Language as SSHKeysPageViewLanguage } from "./SSHKeysPageView" -import { i18n } from "i18n" -import { MockGitSSHKey, mockApiError } from "testHelpers/entities" +import { fireEvent, screen, within } from "@testing-library/react"; +import * as API from "../../../api/api"; +import { renderWithAuth } from "../../../testHelpers/renderHelpers"; +import { Language as SSHKeysPageLanguage, SSHKeysPage } from "./SSHKeysPage"; +import { Language as SSHKeysPageViewLanguage } from "./SSHKeysPageView"; +import { i18n } from "i18n"; +import { MockGitSSHKey, mockApiError } from "testHelpers/entities"; -const { t } = i18n +const { t } = i18n; describe("SSH keys Page", () => { it("shows the SSH key", async () => { - renderWithAuth() - await screen.findByText(MockGitSSHKey.public_key) - }) + renderWithAuth(); + await screen.findByText(MockGitSSHKey.public_key); + }); describe("regenerate SSH key", () => { describe("when it is success", () => { it("shows a success message and updates the ssh key on the page", async () => { - renderWithAuth() + renderWithAuth(); // Wait to the ssh be rendered on the screen - await screen.findByText(MockGitSSHKey.public_key) + await screen.findByText(MockGitSSHKey.public_key); // Click on the "Regenerate" button to display the confirm dialog const regenerateButton = screen.getByRole("button", { name: SSHKeysPageViewLanguage.regenerateLabel, - }) - fireEvent.click(regenerateButton) - const confirmDialog = screen.getByRole("dialog") + }); + fireEvent.click(regenerateButton); + const confirmDialog = screen.getByRole("dialog"); expect(confirmDialog).toHaveTextContent( SSHKeysPageLanguage.regenerateDialogMessage, - ) + ); const newUserSSHKey = - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDSC/ouD/LqiT1Rd99vDv/MwUmqzJuinLTMTpk5kVy66" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDSC/ouD/LqiT1Rd99vDv/MwUmqzJuinLTMTpk5kVy66"; jest.spyOn(API, "regenerateUserSSHKey").mockResolvedValueOnce({ ...MockGitSSHKey, public_key: newUserSSHKey, - }) + }); // Click on the "Confirm" button const confirmButton = within(confirmDialog).getByRole("button", { name: SSHKeysPageLanguage.confirmLabel, - }) - fireEvent.click(confirmButton) + }); + fireEvent.click(confirmButton); // Check if the success message is displayed const successMessage = t("sshRegenerateSuccessMessage", { ns: "userSettingsPage", - }) - await screen.findByText(successMessage) + }); + await screen.findByText(successMessage); // Check if the API was called correctly - expect(API.regenerateUserSSHKey).toBeCalledTimes(1) + expect(API.regenerateUserSSHKey).toBeCalledTimes(1); // Check if the SSH key is updated - await screen.findByText(newUserSSHKey) - }) - }) + await screen.findByText(newUserSSHKey); + }); + }); describe("when it fails", () => { it("shows an error message", async () => { - renderWithAuth() + renderWithAuth(); // Wait to the ssh be rendered on the screen - await screen.findByText(MockGitSSHKey.public_key) + await screen.findByText(MockGitSSHKey.public_key); jest.spyOn(API, "regenerateUserSSHKey").mockRejectedValueOnce( mockApiError({ message: "Error regenerating SSH key", }), - ) + ); // Click on the "Regenerate" button to display the confirm dialog const regenerateButton = screen.getByRole("button", { name: SSHKeysPageViewLanguage.regenerateLabel, - }) - fireEvent.click(regenerateButton) - const confirmDialog = screen.getByRole("dialog") + }); + fireEvent.click(regenerateButton); + const confirmDialog = screen.getByRole("dialog"); expect(confirmDialog).toHaveTextContent( SSHKeysPageLanguage.regenerateDialogMessage, - ) + ); // Click on the "Confirm" button const confirmButton = within(confirmDialog).getByRole("button", { name: SSHKeysPageLanguage.confirmLabel, - }) - fireEvent.click(confirmButton) + }); + fireEvent.click(confirmButton); // Check if the error message is displayed - await screen.findByText("Error regenerating SSH key") + await screen.findByText("Error regenerating SSH key"); // Check if the API was called correctly - expect(API.regenerateUserSSHKey).toBeCalledTimes(1) - }) - }) - }) -}) + expect(API.regenerateUserSSHKey).toBeCalledTimes(1); + }); + }); + }); +}); diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx index 7feceb30c4..e34b807680 100644 --- a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx +++ b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx @@ -1,9 +1,9 @@ -import { useMachine } from "@xstate/react" -import { PropsWithChildren, FC } from "react" -import { sshKeyMachine } from "xServices/sshKey/sshKeyXService" -import { ConfirmDialog } from "../../../components/Dialogs/ConfirmDialog/ConfirmDialog" -import { Section } from "../../../components/SettingsLayout/Section" -import { SSHKeysPageView } from "./SSHKeysPageView" +import { useMachine } from "@xstate/react"; +import { PropsWithChildren, FC } from "react"; +import { sshKeyMachine } from "xServices/sshKey/sshKeyXService"; +import { ConfirmDialog } from "../../../components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { Section } from "../../../components/SettingsLayout/Section"; +import { SSHKeysPageView } from "./SSHKeysPageView"; export const Language = { title: "SSH keys", @@ -12,17 +12,17 @@ export const Language = { "You will need to replace the public SSH key on services you use it with, and you'll need to rebuild existing workspaces.", confirmLabel: "Confirm", cancelLabel: "Cancel", -} +}; export const SSHKeysPage: FC> = () => { - const [sshState, sshSend] = useMachine(sshKeyMachine) - const isLoading = sshState.matches("gettingSSHKey") - const hasLoaded = sshState.matches("loaded") - const { getSSHKeyError, regenerateSSHKeyError, sshKey } = sshState.context + const [sshState, sshSend] = useMachine(sshKeyMachine); + const isLoading = sshState.matches("gettingSSHKey"); + const hasLoaded = sshState.matches("loaded"); + const { getSSHKeyError, regenerateSSHKeyError, sshKey } = sshState.context; const onRegenerateClick = () => { - sshSend({ type: "REGENERATE_SSH_KEY" }) - } + sshSend({ type: "REGENERATE_SSH_KEY" }); + }; return ( <> @@ -45,15 +45,15 @@ export const SSHKeysPage: FC> = () => { title={Language.regenerateDialogTitle} confirmText={Language.confirmLabel} onConfirm={() => { - sshSend({ type: "CONFIRM_REGENERATE_SSH_KEY" }) + sshSend({ type: "CONFIRM_REGENERATE_SSH_KEY" }); }} onClose={() => { - sshSend({ type: "CANCEL_REGENERATE_SSH_KEY" }) + sshSend({ type: "CANCEL_REGENERATE_SSH_KEY" }); }} description={<>{Language.regenerateDialogMessage}} /> - ) -} + ); +}; -export default SSHKeysPage +export default SSHKeysPage; diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.stories.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.stories.tsx index 9b51e231ad..469d0f3492 100644 --- a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.stories.tsx +++ b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.stories.tsx @@ -1,6 +1,6 @@ -import { Story } from "@storybook/react" -import { mockApiError } from "testHelpers/entities" -import { SSHKeysPageView, SSHKeysPageViewProps } from "./SSHKeysPageView" +import { Story } from "@storybook/react"; +import { mockApiError } from "testHelpers/entities"; +import { SSHKeysPageView, SSHKeysPageViewProps } from "./SSHKeysPageView"; export default { title: "components/SSHKeysPageView", @@ -8,13 +8,13 @@ export default { argTypes: { onRegenerateClick: { action: "Submit" }, }, -} +}; const Template: Story = (args: SSHKeysPageViewProps) => ( -) +); -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { isLoading: false, hasLoaded: true, @@ -25,29 +25,29 @@ Example.args = { public_key: "SSH-Key", }, onRegenerateClick: () => { - return Promise.resolve() + return Promise.resolve(); }, -} +}; -export const Loading = Template.bind({}) +export const Loading = Template.bind({}); Loading.args = { ...Example.args, isLoading: true, -} +}; -export const WithGetSSHKeyError = Template.bind({}) +export const WithGetSSHKeyError = Template.bind({}); WithGetSSHKeyError.args = { ...Example.args, hasLoaded: false, getSSHKeyError: mockApiError({ message: "Failed to get SSH key", }), -} +}; -export const WithRegenerateSSHKeyError = Template.bind({}) +export const WithRegenerateSSHKeyError = Template.bind({}); WithRegenerateSSHKeyError.args = { ...Example.args, regenerateSSHKeyError: mockApiError({ message: "Failed to regenerate SSH key", }), -} +}; diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.tsx index 5e9b27f043..c14c801771 100644 --- a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.tsx +++ b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPageView.tsx @@ -1,24 +1,24 @@ -import { makeStyles } from "@mui/styles" -import Box from "@mui/material/Box" -import Button from "@mui/material/Button" -import CircularProgress from "@mui/material/CircularProgress" -import { GitSSHKey } from "api/typesGenerated" -import { CodeExample } from "components/CodeExample/CodeExample" -import { Stack } from "components/Stack/Stack" -import { FC } from "react" -import { ErrorAlert } from "components/Alert/ErrorAlert" +import { makeStyles } from "@mui/styles"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import CircularProgress from "@mui/material/CircularProgress"; +import { GitSSHKey } from "api/typesGenerated"; +import { CodeExample } from "components/CodeExample/CodeExample"; +import { Stack } from "components/Stack/Stack"; +import { FC } from "react"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; export const Language = { regenerateLabel: "Regenerate", -} +}; export interface SSHKeysPageViewProps { - isLoading: boolean - hasLoaded: boolean - getSSHKeyError?: unknown - regenerateSSHKeyError?: unknown - sshKey?: GitSSHKey - onRegenerateClick: () => void + isLoading: boolean; + hasLoaded: boolean; + getSSHKeyError?: unknown; + regenerateSSHKeyError?: unknown; + sshKey?: GitSSHKey; + onRegenerateClick: () => void; } export const SSHKeysPageView: FC< @@ -31,14 +31,14 @@ export const SSHKeysPageView: FC< sshKey, onRegenerateClick, }) => { - const styles = useStyles() + const styles = useStyles(); if (isLoading) { return ( - ) + ); } return ( @@ -66,8 +66,8 @@ export const SSHKeysPageView: FC< )} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ description: { @@ -82,4 +82,4 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.primary, borderRadius: 2, }, -})) +})); diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx index 1e97be0b67..be54ed21af 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx @@ -1,73 +1,73 @@ -import { fireEvent, screen, waitFor, within } from "@testing-library/react" -import * as API from "../../../api/api" -import * as SecurityForm from "./SettingsSecurityForm" +import { fireEvent, screen, waitFor, within } from "@testing-library/react"; +import * as API from "../../../api/api"; +import * as SecurityForm from "./SettingsSecurityForm"; import { renderWithAuth, waitForLoaderToBeRemoved, -} from "../../../testHelpers/renderHelpers" -import { SecurityPage } from "./SecurityPage" -import i18next from "i18next" +} from "../../../testHelpers/renderHelpers"; +import { SecurityPage } from "./SecurityPage"; +import i18next from "i18next"; import { MockAuthMethodsWithPasswordType, mockApiError, -} from "testHelpers/entities" -import userEvent from "@testing-library/user-event" -import * as SSO from "./SingleSignOnSection" -import { OAuthConversionResponse } from "api/typesGenerated" +} from "testHelpers/entities"; +import userEvent from "@testing-library/user-event"; +import * as SSO from "./SingleSignOnSection"; +import { OAuthConversionResponse } from "api/typesGenerated"; -const { t } = i18next +const { t } = i18next; const renderPage = async () => { - const utils = renderWithAuth() - await waitForLoaderToBeRemoved() - return utils -} + const utils = renderWithAuth(); + await waitForLoaderToBeRemoved(); + return utils; +}; const newSecurityFormValues = { old_password: "password1", password: "password2", confirm_password: "password2", -} +}; const fillAndSubmitSecurityForm = () => { fireEvent.change(screen.getByLabelText("Old Password"), { target: { value: newSecurityFormValues.old_password }, - }) + }); fireEvent.change(screen.getByLabelText("New Password"), { target: { value: newSecurityFormValues.password }, - }) + }); fireEvent.change(screen.getByLabelText("Confirm Password"), { target: { value: newSecurityFormValues.confirm_password }, - }) - fireEvent.click(screen.getByText(SecurityForm.Language.updatePassword)) -} + }); + fireEvent.click(screen.getByText(SecurityForm.Language.updatePassword)); +}; beforeEach(() => { jest .spyOn(API, "getAuthMethods") - .mockResolvedValue(MockAuthMethodsWithPasswordType) + .mockResolvedValue(MockAuthMethodsWithPasswordType); jest.spyOn(API, "getUserLoginType").mockResolvedValue({ login_type: "password", - }) -}) + }); +}); test("update password successfully", async () => { jest .spyOn(API, "updateUserPassword") - .mockImplementationOnce((_userId, _data) => Promise.resolve(undefined)) - const { user } = await renderPage() - fillAndSubmitSecurityForm() + .mockImplementationOnce((_userId, _data) => Promise.resolve(undefined)); + const { user } = await renderPage(); + fillAndSubmitSecurityForm(); const expectedMessage = t("securityUpdateSuccessMessage", { ns: "userSettingsPage", - }) - const successMessage = await screen.findByText(expectedMessage) - expect(successMessage).toBeDefined() - expect(API.updateUserPassword).toBeCalledTimes(1) - expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues) + }); + const successMessage = await screen.findByText(expectedMessage); + expect(successMessage).toBeDefined(); + expect(API.updateUserPassword).toBeCalledTimes(1); + expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues); - await waitFor(() => expect(window.location).toBeAt("/")) -}) + await waitFor(() => expect(window.location).toBeAt("/")); +}); test("update password with incorrect old password", async () => { jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce( @@ -75,17 +75,17 @@ test("update password with incorrect old password", async () => { message: "Incorrect password.", validations: [{ detail: "Incorrect password.", field: "old_password" }], }), - ) + ); - const { user } = await renderPage() - fillAndSubmitSecurityForm() + const { user } = await renderPage(); + fillAndSubmitSecurityForm(); - const errorMessage = await screen.findAllByText("Incorrect password.") - expect(errorMessage).toBeDefined() - expect(errorMessage).toHaveLength(2) - expect(API.updateUserPassword).toBeCalledTimes(1) - expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues) -}) + const errorMessage = await screen.findAllByText("Incorrect password."); + expect(errorMessage).toBeDefined(); + expect(errorMessage).toHaveLength(2); + expect(API.updateUserPassword).toBeCalledTimes(1); + expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues); +}); test("update password with invalid password", async () => { jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce( @@ -93,38 +93,38 @@ test("update password with invalid password", async () => { message: "Invalid password.", validations: [{ detail: "Invalid password.", field: "password" }], }), - ) + ); - const { user } = await renderPage() - fillAndSubmitSecurityForm() + const { user } = await renderPage(); + fillAndSubmitSecurityForm(); - const errorMessage = await screen.findAllByText("Invalid password.") - expect(errorMessage).toBeDefined() - expect(errorMessage).toHaveLength(2) - expect(API.updateUserPassword).toBeCalledTimes(1) - expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues) -}) + const errorMessage = await screen.findAllByText("Invalid password."); + expect(errorMessage).toBeDefined(); + expect(errorMessage).toHaveLength(2); + expect(API.updateUserPassword).toBeCalledTimes(1); + expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues); +}); test("update password when submit returns an unknown error", async () => { jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({ data: "unknown error", - }) + }); - const { user } = await renderPage() - fillAndSubmitSecurityForm() + const { user } = await renderPage(); + fillAndSubmitSecurityForm(); const errorText = t("warningsAndErrors.somethingWentWrong", { ns: "common", - }) - const errorMessage = await screen.findByText(errorText) - expect(errorMessage).toBeDefined() - expect(API.updateUserPassword).toBeCalledTimes(1) - expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues) -}) + }); + const errorMessage = await screen.findByText(errorText); + expect(errorMessage).toBeDefined(); + expect(API.updateUserPassword).toBeCalledTimes(1); + expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues); +}); test("change login type to OIDC", async () => { - const user = userEvent.setup() - const { user: userData } = await renderPage() + const user = userEvent.setup(); + const { user: userData } = await renderPage(); const convertToOAUTHSpy = jest .spyOn(API, "convertToOAUTH") .mockResolvedValue({ @@ -132,29 +132,29 @@ test("change login type to OIDC", async () => { expires_at: "2021-01-01T00:00:00Z", to_type: "oidc", user_id: userData.id, - } as OAuthConversionResponse) + } as OAuthConversionResponse); jest.spyOn(SSO, "redirectToOIDCAuth").mockImplementation(() => { // Does a noop - return "" - }) + return ""; + }); - const ssoSection = screen.getByTestId("sso-section") - const githubButton = within(ssoSection).getByText("GitHub", { exact: false }) - await user.click(githubButton) + const ssoSection = screen.getByTestId("sso-section"); + const githubButton = within(ssoSection).getByText("GitHub", { exact: false }); + await user.click(githubButton); - const confirmationDialog = await screen.findByTestId("dialog") + const confirmationDialog = await screen.findByTestId("dialog"); const confirmPasswordField = within(confirmationDialog).getByLabelText( "Confirm your password", - ) - await user.type(confirmPasswordField, "password123") - const updateButton = within(confirmationDialog).getByText("Update") - await user.click(updateButton) + ); + await user.type(confirmPasswordField, "password123"); + const updateButton = within(confirmationDialog).getByText("Update"); + await user.click(updateButton); await waitFor(() => { expect(convertToOAUTHSpy).toHaveBeenCalledWith({ password: "password123", to_type: "github", - }) - }) -}) + }); + }); +}); diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx index ee43726e26..4dd2faeead 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx @@ -1,20 +1,20 @@ -import { useMachine } from "@xstate/react" -import { useMe } from "hooks/useMe" -import { ComponentProps, FC } from "react" -import { userSecuritySettingsMachine } from "xServices/userSecuritySettings/userSecuritySettingsXService" -import { Section } from "../../../components/SettingsLayout/Section" -import { SecurityForm } from "./SettingsSecurityForm" -import { useQuery } from "@tanstack/react-query" -import { getAuthMethods, getUserLoginType } from "api/api" +import { useMachine } from "@xstate/react"; +import { useMe } from "hooks/useMe"; +import { ComponentProps, FC } from "react"; +import { userSecuritySettingsMachine } from "xServices/userSecuritySettings/userSecuritySettingsXService"; +import { Section } from "../../../components/SettingsLayout/Section"; +import { SecurityForm } from "./SettingsSecurityForm"; +import { useQuery } from "@tanstack/react-query"; +import { getAuthMethods, getUserLoginType } from "api/api"; import { SingleSignOnSection, useSingleSignOnSection, -} from "./SingleSignOnSection" -import { Loader } from "components/Loader/Loader" -import { Stack } from "components/Stack/Stack" +} from "./SingleSignOnSection"; +import { Loader } from "components/Loader/Loader"; +import { Stack } from "components/Stack/Stack"; export const SecurityPage: FC = () => { - const me = useMe() + const me = useMe(); const [securityState, securitySend] = useMachine( userSecuritySettingsMachine, { @@ -22,20 +22,20 @@ export const SecurityPage: FC = () => { userId: me.id, }, }, - ) - const { error } = securityState.context + ); + const { error } = securityState.context; const { data: authMethods } = useQuery({ queryKey: ["authMethods"], queryFn: getAuthMethods, - }) + }); const { data: userLoginType } = useQuery({ queryKey: ["loginType"], queryFn: getUserLoginType, - }) - const singleSignOnSection = useSingleSignOnSection() + }); + const singleSignOnSection = useSingleSignOnSection(); if (!authMethods || !userLoginType) { - return + return ; } return ( @@ -49,7 +49,7 @@ export const SecurityPage: FC = () => { securitySend({ type: "UPDATE_SECURITY", data, - }) + }); }, }, }} @@ -61,19 +61,19 @@ export const SecurityPage: FC = () => { }, }} /> - ) -} + ); +}; export const SecurityPageView = ({ security, oidc, }: { security: { - form: ComponentProps - } + form: ComponentProps; + }; oidc?: { - section: ComponentProps - } + section: ComponentProps; + }; }) => { return ( @@ -82,7 +82,7 @@ export const SecurityPageView = ({ {oidc && } - ) -} + ); +}; -export default SecurityPage +export default SecurityPage; diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPageView.stories.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPageView.stories.tsx index f0f71abf2f..e5c4e78c27 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPageView.stories.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPageView.stories.tsx @@ -1,12 +1,12 @@ -import type { Meta, StoryObj } from "@storybook/react" -import { SecurityPageView } from "./SecurityPage" -import { action } from "@storybook/addon-actions" +import type { Meta, StoryObj } from "@storybook/react"; +import { SecurityPageView } from "./SecurityPage"; +import { action } from "@storybook/addon-actions"; import { MockAuthMethods, MockAuthMethodsWithPasswordType, -} from "testHelpers/entities" -import { ComponentProps } from "react" -import set from "lodash/fp/set" +} from "testHelpers/entities"; +import { ComponentProps } from "react"; +import set from "lodash/fp/set"; const defaultArgs: ComponentProps = { security: { @@ -31,25 +31,25 @@ const defaultArgs: ComponentProps = { openConfirmation: action("openConfirmation"), }, }, -} +}; const meta: Meta = { title: "pages/SecurityPageView", component: SecurityPageView, args: defaultArgs, -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; -export const UsingOIDC: Story = {} +export const UsingOIDC: Story = {}; export const NoOIDCAvailable: Story = { args: { ...defaultArgs, oidc: undefined, }, -} +}; export const UserLoginTypeIsPassword: Story = { args: set( @@ -57,7 +57,7 @@ export const UserLoginTypeIsPassword: Story = { MockAuthMethodsWithPasswordType, defaultArgs, ), -} +}; export const ConfirmingOIDCConversion: Story = { args: set( @@ -69,4 +69,4 @@ export const ConfirmingOIDCConversion: Story = { }, defaultArgs, ), -} +}; diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SettingsSecurityForm.stories.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SettingsSecurityForm.stories.tsx index 506b81270e..a53d0c205c 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SettingsSecurityForm.stories.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SettingsSecurityForm.stories.tsx @@ -1,6 +1,6 @@ -import { Story } from "@storybook/react" -import { SecurityForm, SecurityFormProps } from "./SettingsSecurityForm" -import { mockApiError } from "testHelpers/entities" +import { Story } from "@storybook/react"; +import { SecurityForm, SecurityFormProps } from "./SettingsSecurityForm"; +import { mockApiError } from "testHelpers/entities"; export default { title: "components/SettingsSecurityForm", @@ -8,27 +8,27 @@ export default { argTypes: { onSubmit: { action: "Submit" }, }, -} +}; const Template: Story = (args: SecurityFormProps) => ( -) +); -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { isLoading: false, onSubmit: () => { - return Promise.resolve() + return Promise.resolve(); }, -} +}; -export const Loading = Template.bind({}) +export const Loading = Template.bind({}); Loading.args = { ...Example.args, isLoading: true, -} +}; -export const WithError = Template.bind({}) +export const WithError = Template.bind({}); WithError.args = { ...Example.args, error: mockApiError({ @@ -40,4 +40,4 @@ WithError.args = { }, ], }), -} +}; diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SettingsSecurityForm.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SettingsSecurityForm.tsx index 2e0b0436cf..aedeed228f 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SettingsSecurityForm.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SettingsSecurityForm.tsx @@ -1,17 +1,17 @@ -import TextField from "@mui/material/TextField" -import { FormikContextType, useFormik } from "formik" -import { FC } from "react" -import * as Yup from "yup" -import { ErrorAlert } from "components/Alert/ErrorAlert" -import { Form, FormFields } from "components/Form/Form" -import { Alert } from "components/Alert/Alert" -import { getFormHelpers } from "utils/formUtils" -import { LoadingButton } from "components/LoadingButton/LoadingButton" +import TextField from "@mui/material/TextField"; +import { FormikContextType, useFormik } from "formik"; +import { FC } from "react"; +import * as Yup from "yup"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Form, FormFields } from "components/Form/Form"; +import { Alert } from "components/Alert/Alert"; +import { getFormHelpers } from "utils/formUtils"; +import { LoadingButton } from "components/LoadingButton/LoadingButton"; interface SecurityFormValues { - old_password: string - password: string - confirm_password: string + old_password: string; + password: string; + confirm_password: string; } export const Language = { @@ -25,7 +25,7 @@ export const Language = { passwordMaxLength: "Password must be no more than 64 characters", confirmPasswordMatch: "Password and confirmation must match", updatePassword: "Update password", -} +}; const validationSchema = Yup.object({ old_password: Yup.string().trim().required(Language.oldPasswordRequired), @@ -37,15 +37,15 @@ const validationSchema = Yup.object({ confirm_password: Yup.string() .trim() .test("passwords-match", Language.confirmPasswordMatch, function (value) { - return (this.parent as SecurityFormValues).password === value + return (this.parent as SecurityFormValues).password === value; }), -}) +}); export interface SecurityFormProps { - disabled: boolean - isLoading: boolean - onSubmit: (values: SecurityFormValues) => void - error?: unknown + disabled: boolean; + isLoading: boolean; + onSubmit: (values: SecurityFormValues) => void; + error?: unknown; } export const SecurityForm: FC = ({ @@ -63,15 +63,15 @@ export const SecurityForm: FC = ({ }, validationSchema, onSubmit, - }) - const getFieldHelpers = getFormHelpers(form, error) + }); + const getFieldHelpers = getFormHelpers(form, error); if (disabled) { return ( Password changes are only allowed for password based accounts. - ) + ); } return ( @@ -113,5 +113,5 @@ export const SecurityForm: FC = ({ - ) -} + ); +}; diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx index 66830b793e..224ad1422c 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx @@ -1,29 +1,29 @@ -import { useState } from "react" -import { Section } from "../../../components/SettingsLayout/Section" -import TextField from "@mui/material/TextField" -import Box from "@mui/material/Box" -import GitHubIcon from "@mui/icons-material/GitHub" -import KeyIcon from "@mui/icons-material/VpnKey" -import Button from "@mui/material/Button" -import Typography from "@mui/material/Typography" -import { convertToOAUTH } from "api/api" -import { AuthMethods, LoginType, UserLoginType } from "api/typesGenerated" -import Skeleton from "@mui/material/Skeleton" -import { Stack } from "components/Stack/Stack" -import { useMutation } from "@tanstack/react-query" -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" -import { getErrorMessage } from "api/errors" -import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined" +import { useState } from "react"; +import { Section } from "../../../components/SettingsLayout/Section"; +import TextField from "@mui/material/TextField"; +import Box from "@mui/material/Box"; +import GitHubIcon from "@mui/icons-material/GitHub"; +import KeyIcon from "@mui/icons-material/VpnKey"; +import Button from "@mui/material/Button"; +import Typography from "@mui/material/Typography"; +import { convertToOAUTH } from "api/api"; +import { AuthMethods, LoginType, UserLoginType } from "api/typesGenerated"; +import Skeleton from "@mui/material/Skeleton"; +import { Stack } from "components/Stack/Stack"; +import { useMutation } from "@tanstack/react-query"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { getErrorMessage } from "api/errors"; +import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined"; type LoginTypeConfirmation = | { - open: false - selectedType: undefined + open: false; + selectedType: undefined; } | { - open: true - selectedType: LoginType - } + open: true; + selectedType: LoginType; + }; export const redirectToOIDCAuth = ( toType: string, @@ -32,24 +32,24 @@ export const redirectToOIDCAuth = ( ) => { switch (toType) { case "github": - window.location.href = `/api/v2/users/oauth2/github/callback?oidc_merge_state=${stateString}&redirect=${redirectTo}` - break + window.location.href = `/api/v2/users/oauth2/github/callback?oidc_merge_state=${stateString}&redirect=${redirectTo}`; + break; case "oidc": - window.location.href = `/api/v2/users/oidc/callback?oidc_merge_state=${stateString}&redirect=${redirectTo}` - break + window.location.href = `/api/v2/users/oidc/callback?oidc_merge_state=${stateString}&redirect=${redirectTo}`; + break; default: - throw new Error(`Unknown login type ${toType}`) + throw new Error(`Unknown login type ${toType}`); } -} +}; export const useSingleSignOnSection = () => { const [loginTypeConfirmation, setLoginTypeConfirmation] = - useState({ open: false, selectedType: undefined }) + useState({ open: false, selectedType: undefined }); const mutation = useMutation(convertToOAUTH, { onSuccess: (data) => { const loginTypeMsg = - data.to_type === "github" ? "Github" : "OpenID Connect" + data.to_type === "github" ? "Github" : "OpenID Connect"; redirectToOIDCAuth( data.to_type, data.state_string, @@ -58,28 +58,28 @@ export const useSingleSignOnSection = () => { encodeURIComponent( `/login?info=Login type has been changed to ${loginTypeMsg}. Log in again using the new method.`, ), - ) + ); }, - }) + }); const openConfirmation = (selectedType: LoginType) => { - setLoginTypeConfirmation({ open: true, selectedType }) - } + setLoginTypeConfirmation({ open: true, selectedType }); + }; const closeConfirmation = () => { - setLoginTypeConfirmation({ open: false, selectedType: undefined }) - mutation.reset() - } + setLoginTypeConfirmation({ open: false, selectedType: undefined }); + mutation.reset(); + }; const confirm = (password: string) => { if (!loginTypeConfirmation.selectedType) { - throw new Error("No login type selected") + throw new Error("No login type selected"); } mutation.mutate({ to_type: loginTypeConfirmation.selectedType, password, - }) - } + }); + }; return { openConfirmation, @@ -90,13 +90,13 @@ export const useSingleSignOnSection = () => { isUpdating: mutation.isLoading || mutation.isSuccess, isConfirming: loginTypeConfirmation.open, error: mutation.error, - } -} + }; +}; type SingleSignOnSectionProps = ReturnType & { - authMethods: AuthMethods - userLoginType: UserLoginType -} + authMethods: AuthMethods; + userLoginType: UserLoginType; +}; export const SingleSignOnSection = ({ authMethods, @@ -195,8 +195,8 @@ export const SingleSignOnSection = ({ onConfirm={confirm} /> - ) -} + ); +}; const OIDCIcon = ({ authMethods }: { authMethods: AuthMethods }) => { return authMethods.oidc.iconUrl ? ( @@ -208,12 +208,12 @@ const OIDCIcon = ({ authMethods }: { authMethods: AuthMethods }) => { /> ) : ( - ) -} + ); +}; const getOIDCLabel = (authMethods: AuthMethods) => { - return authMethods.oidc.signInText || "OpenID Connect" -} + return authMethods.oidc.signInText || "OpenID Connect"; +}; const ConfirmLoginTypeChangeModal = ({ open, @@ -222,23 +222,23 @@ const ConfirmLoginTypeChangeModal = ({ onClose, onConfirm, }: { - open: boolean - loading: boolean - error: unknown - onClose: () => void - onConfirm: (password: string) => void + open: boolean; + loading: boolean; + error: unknown; + onClose: () => void; + onConfirm: (password: string) => void; }) => { - const [password, setPassword] = useState("") + const [password, setPassword] = useState(""); const handleConfirm = () => { - onConfirm(password) - } + onConfirm(password); + }; return ( { - onClose() + onClose(); }} onConfirm={handleConfirm} hideCancel={false} @@ -256,7 +256,7 @@ const ConfirmLoginTypeChangeModal = ({ autoFocus onKeyDown={(event) => { if (event.key === "Enter") { - handleConfirm() + handleConfirm(); } }} error={Boolean(error)} @@ -275,5 +275,5 @@ const ConfirmLoginTypeChangeModal = ({ } /> - ) -} + ); +}; diff --git a/site/src/pages/UserSettingsPage/TokensPage/ConfirmDeleteDialog.stories.tsx b/site/src/pages/UserSettingsPage/TokensPage/ConfirmDeleteDialog.stories.tsx index 6f254680dc..b38e4deee1 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/ConfirmDeleteDialog.stories.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/ConfirmDeleteDialog.stories.tsx @@ -1,10 +1,10 @@ -import { Story } from "@storybook/react" -import { MockToken } from "testHelpers/entities" +import { Story } from "@storybook/react"; +import { MockToken } from "testHelpers/entities"; import { ConfirmDeleteDialog, ConfirmDeleteDialogProps, -} from "./ConfirmDeleteDialog" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +} from "./ConfirmDeleteDialog"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const queryClient = new QueryClient({ defaultOptions: { @@ -14,12 +14,12 @@ const queryClient = new QueryClient({ refetchOnWindowFocus: false, }, }, -}) +}); export default { title: "components/ConfirmDeleteDialog", component: ConfirmDeleteDialog, -} +}; const Template: Story = ( args: ConfirmDeleteDialogProps, @@ -27,13 +27,13 @@ const Template: Story = ( -) +); -export const DeleteDialog = Template.bind({}) +export const DeleteDialog = Template.bind({}); DeleteDialog.args = { queryKey: ["tokens"], token: MockToken, setToken: () => { - return null + return null; }, -} +}; diff --git a/site/src/pages/UserSettingsPage/TokensPage/ConfirmDeleteDialog.tsx b/site/src/pages/UserSettingsPage/TokensPage/ConfirmDeleteDialog.tsx index 6826ab9579..34ab7be219 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/ConfirmDeleteDialog.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/ConfirmDeleteDialog.tsx @@ -1,15 +1,15 @@ -import { FC } from "react" -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" -import { useTranslation, Trans } from "react-i18next" -import { useDeleteToken } from "./hooks" -import { displaySuccess, displayError } from "components/GlobalSnackbar/utils" -import { getErrorMessage } from "api/errors" -import { APIKeyWithOwner } from "api/typesGenerated" +import { FC } from "react"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { useTranslation, Trans } from "react-i18next"; +import { useDeleteToken } from "./hooks"; +import { displaySuccess, displayError } from "components/GlobalSnackbar/utils"; +import { getErrorMessage } from "api/errors"; +import { APIKeyWithOwner } from "api/typesGenerated"; export interface ConfirmDeleteDialogProps { - queryKey: (string | boolean)[] - token: APIKeyWithOwner | undefined - setToken: (arg: APIKeyWithOwner | undefined) => void + queryKey: (string | boolean)[]; + token: APIKeyWithOwner | undefined; + setToken: (arg: APIKeyWithOwner | undefined) => void; } export const ConfirmDeleteDialog: FC = ({ @@ -17,8 +17,8 @@ export const ConfirmDeleteDialog: FC = ({ token, setToken, }) => { - const { t } = useTranslation("tokensPage") - const tokenName = token?.token_name + const { t } = useTranslation("tokensPage"); + const tokenName = token?.token_name; const description = ( = ({ > Are you sure you want to permanently delete token {{ tokenName }}? - ) + ); const { mutate: deleteToken, isLoading: isDeleting } = - useDeleteToken(queryKey) + useDeleteToken(queryKey); const onDeleteSuccess = () => { - displaySuccess(t("tokenActions.deleteToken.deleteSuccess")) - setToken(undefined) - } + displaySuccess(t("tokenActions.deleteToken.deleteSuccess")); + setToken(undefined); + }; const onDeleteError = (error: unknown) => { const message = getErrorMessage( error, t("tokenActions.deleteToken.deleteFailure"), - ) - displayError(message) - setToken(undefined) - } + ); + displayError(message); + setToken(undefined); + }; return ( = ({ confirmLoading={isDeleting} onConfirm={() => { if (!token) { - return + return; } deleteToken(token.id, { onError: onDeleteError, onSuccess: onDeleteSuccess, - }) + }); }} onClose={() => { - setToken(undefined) + setToken(undefined); }} /> - ) -} + ); +}; diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx index 9d3cea9f5c..8dccf7d21a 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -1,21 +1,21 @@ -import { FC, PropsWithChildren, useState } from "react" -import { Section } from "components/SettingsLayout/Section" -import { TokensPageView } from "./TokensPageView" -import makeStyles from "@mui/styles/makeStyles" -import { useTranslation } from "react-i18next" -import { useTokensData } from "./hooks" -import { ConfirmDeleteDialog } from "./ConfirmDeleteDialog" -import { Stack } from "components/Stack/Stack" -import Button from "@mui/material/Button" -import { Link as RouterLink } from "react-router-dom" -import AddIcon from "@mui/icons-material/AddOutlined" -import { APIKeyWithOwner } from "api/typesGenerated" +import { FC, PropsWithChildren, useState } from "react"; +import { Section } from "components/SettingsLayout/Section"; +import { TokensPageView } from "./TokensPageView"; +import makeStyles from "@mui/styles/makeStyles"; +import { useTranslation } from "react-i18next"; +import { useTokensData } from "./hooks"; +import { ConfirmDeleteDialog } from "./ConfirmDeleteDialog"; +import { Stack } from "components/Stack/Stack"; +import Button from "@mui/material/Button"; +import { Link as RouterLink } from "react-router-dom"; +import AddIcon from "@mui/icons-material/AddOutlined"; +import { APIKeyWithOwner } from "api/typesGenerated"; export const TokensPage: FC> = () => { - const styles = useStyles() - const { t } = useTranslation("tokensPage") + const styles = useStyles(); + const { t } = useTranslation("tokensPage"); - const cliCreateCommand = "coder tokens create" + const cliCreateCommand = "coder tokens create"; const TokenActions = () => ( @@ -23,11 +23,11 @@ export const TokensPage: FC> = () => { {t("tokenActions.addToken")} - ) + ); const [tokenToDelete, setTokenToDelete] = useState< APIKeyWithOwner | undefined - >(undefined) + >(undefined); const { data: tokens, @@ -39,7 +39,7 @@ export const TokensPage: FC> = () => { // we currently do not show all tokens in the UI, even if // the user has read all permissions include_all: false, - }) + }); return ( <> @@ -62,7 +62,7 @@ export const TokensPage: FC> = () => { hasLoaded={isFetched} getTokensError={getTokensError} onDelete={(token) => { - setTokenToDelete(token) + setTokenToDelete(token); }} /> @@ -72,8 +72,8 @@ export const TokensPage: FC> = () => { setToken={setTokenToDelete} /> - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ section: { @@ -88,6 +88,6 @@ const useStyles = makeStyles((theme) => ({ tokenActions: { marginBottom: theme.spacing(1), }, -})) +})); -export default TokensPage +export default TokensPage; diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.stories.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.stories.tsx index 6748f53b74..5fed9d2864 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.stories.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.stories.tsx @@ -1,6 +1,6 @@ -import { Story } from "@storybook/react" -import { mockApiError, MockTokens } from "testHelpers/entities" -import { TokensPageView, TokensPageViewProps } from "./TokensPageView" +import { Story } from "@storybook/react"; +import { mockApiError, MockTokens } from "testHelpers/entities"; +import { TokensPageView, TokensPageViewProps } from "./TokensPageView"; export default { title: "components/TokensPageView", @@ -8,49 +8,49 @@ export default { args: { onRegenerateClick: { action: "Submit" }, }, -} +}; const Template: Story = (args: TokensPageViewProps) => ( -) +); -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { isLoading: false, hasLoaded: true, tokens: MockTokens, onDelete: () => { - return Promise.resolve() + return Promise.resolve(); }, -} +}; -export const Loading = Template.bind({}) +export const Loading = Template.bind({}); Loading.args = { ...Example.args, isLoading: true, hasLoaded: false, -} +}; -export const Empty = Template.bind({}) +export const Empty = Template.bind({}); Empty.args = { ...Example.args, tokens: [], -} +}; -export const WithGetTokensError = Template.bind({}) +export const WithGetTokensError = Template.bind({}); WithGetTokensError.args = { ...Example.args, hasLoaded: false, getTokensError: mockApiError({ message: "Failed to get tokens.", }), -} +}; -export const WithDeleteTokenError = Template.bind({}) +export const WithDeleteTokenError = Template.bind({}); WithDeleteTokenError.args = { ...Example.args, hasLoaded: false, deleteTokenError: mockApiError({ message: "Failed to delete token.", }), -} +}; diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx index 43150e8422..51ef70064a 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx @@ -1,38 +1,38 @@ -import { useTheme } from "@mui/styles" -import Table from "@mui/material/Table" -import TableBody from "@mui/material/TableBody" -import TableCell from "@mui/material/TableCell" -import TableContainer from "@mui/material/TableContainer" -import TableHead from "@mui/material/TableHead" -import TableRow from "@mui/material/TableRow" -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { Stack } from "components/Stack/Stack" -import { TableEmpty } from "components/TableEmpty/TableEmpty" -import { TableLoader } from "components/TableLoader/TableLoader" -import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline" -import dayjs from "dayjs" -import { FC } from "react" -import IconButton from "@mui/material/IconButton/IconButton" -import { useTranslation } from "react-i18next" -import { APIKeyWithOwner } from "api/typesGenerated" -import relativeTime from "dayjs/plugin/relativeTime" -import { ErrorAlert } from "components/Alert/ErrorAlert" +import { useTheme } from "@mui/styles"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { Stack } from "components/Stack/Stack"; +import { TableEmpty } from "components/TableEmpty/TableEmpty"; +import { TableLoader } from "components/TableLoader/TableLoader"; +import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; +import dayjs from "dayjs"; +import { FC } from "react"; +import IconButton from "@mui/material/IconButton/IconButton"; +import { useTranslation } from "react-i18next"; +import { APIKeyWithOwner } from "api/typesGenerated"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; -dayjs.extend(relativeTime) +dayjs.extend(relativeTime); const lastUsedOrNever = (lastUsed: string) => { - const t = dayjs(lastUsed) - const now = dayjs() - return now.isBefore(t.add(100, "year")) ? t.fromNow() : "Never" -} + const t = dayjs(lastUsed); + const now = dayjs(); + return now.isBefore(t.add(100, "year")) ? t.fromNow() : "Never"; +}; export interface TokensPageViewProps { - tokens?: APIKeyWithOwner[] - getTokensError?: unknown - isLoading: boolean - hasLoaded: boolean - onDelete: (token: APIKeyWithOwner) => void - deleteTokenError?: unknown + tokens?: APIKeyWithOwner[]; + getTokensError?: unknown; + isLoading: boolean; + hasLoaded: boolean; + onDelete: (token: APIKeyWithOwner) => void; + deleteTokenError?: unknown; } export const TokensPageView: FC< @@ -45,8 +45,8 @@ export const TokensPageView: FC< onDelete, deleteTokenError, }) => { - const theme = useTheme() - const { t } = useTranslation("tokensPage") + const theme = useTheme(); + const { t } = useTranslation("tokensPage"); return ( @@ -113,7 +113,7 @@ export const TokensPageView: FC< { - onDelete(token) + onDelete(token); }} size="medium" aria-label={t( @@ -125,7 +125,7 @@ export const TokensPageView: FC< - ) + ); })} @@ -133,5 +133,5 @@ export const TokensPageView: FC< - ) -} + ); +}; diff --git a/site/src/pages/UserSettingsPage/TokensPage/hooks.ts b/site/src/pages/UserSettingsPage/TokensPage/hooks.ts index 38b23c7354..39d57ec6fb 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/hooks.ts +++ b/site/src/pages/UserSettingsPage/TokensPage/hooks.ts @@ -3,36 +3,36 @@ import { useMutation, useQueryClient, QueryKey, -} from "@tanstack/react-query" -import { getTokens, deleteToken } from "api/api" -import { TokensFilter } from "api/typesGenerated" +} from "@tanstack/react-query"; +import { getTokens, deleteToken } from "api/api"; +import { TokensFilter } from "api/typesGenerated"; // Load all tokens export const useTokensData = ({ include_all }: TokensFilter) => { - const queryKey = ["tokens", include_all] + const queryKey = ["tokens", include_all]; const result = useQuery({ queryKey, queryFn: () => getTokens({ include_all, }), - }) + }); return { queryKey, ...result, - } -} + }; +}; // Delete a token export const useDeleteToken = (queryKey: QueryKey) => { - const queryClient = useQueryClient() + const queryClient = useQueryClient(); return useMutation({ mutationFn: deleteToken, onSuccess: () => { // Invalidate and refetch - void queryClient.invalidateQueries(queryKey) + void queryClient.invalidateQueries(queryKey); }, - }) -} + }); +}; diff --git a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyPage.tsx b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyPage.tsx index 88d337bf43..eabf730ff4 100644 --- a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyPage.tsx +++ b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyPage.tsx @@ -1,14 +1,14 @@ -import { FC, PropsWithChildren } from "react" -import { Section } from "components/SettingsLayout/Section" -import { WorkspaceProxyView } from "./WorkspaceProxyView" -import makeStyles from "@mui/styles/makeStyles" -import { useProxy } from "contexts/ProxyContext" +import { FC, PropsWithChildren } from "react"; +import { Section } from "components/SettingsLayout/Section"; +import { WorkspaceProxyView } from "./WorkspaceProxyView"; +import makeStyles from "@mui/styles/makeStyles"; +import { useProxy } from "contexts/ProxyContext"; export const WorkspaceProxyPage: FC> = () => { - const styles = useStyles() + const styles = useStyles(); const description = - "Workspace proxies improve terminal and web app connections to workspaces." + "Workspace proxies improve terminal and web app connections to workspaces."; const { proxyLatencies, @@ -17,7 +17,7 @@ export const WorkspaceProxyPage: FC> = () => { isFetched: proxiesFetched, isLoading: proxiesLoading, proxy, - } = useProxy() + } = useProxy(); return (
> = () => { preferredProxy={proxy.proxy} />
- ) -} + ); +}; const useStyles = makeStyles((theme) => ({ section: { @@ -48,6 +48,6 @@ const useStyles = makeStyles((theme) => ({ borderRadius: 2, }, }, -})) +})); -export default WorkspaceProxyPage +export default WorkspaceProxyPage; diff --git a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx index 760d6f5ca8..5747398cb1 100644 --- a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx +++ b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx @@ -1,37 +1,37 @@ -import { Region, WorkspaceProxy } from "api/typesGenerated" -import { AvatarData } from "components/AvatarData/AvatarData" -import { Avatar } from "components/Avatar/Avatar" -import TableCell from "@mui/material/TableCell" -import TableRow from "@mui/material/TableRow" -import { FC, ReactNode } from "react" +import { Region, WorkspaceProxy } from "api/typesGenerated"; +import { AvatarData } from "components/AvatarData/AvatarData"; +import { Avatar } from "components/Avatar/Avatar"; +import TableCell from "@mui/material/TableCell"; +import TableRow from "@mui/material/TableRow"; +import { FC, ReactNode } from "react"; import { HealthyBadge, NotHealthyBadge, NotReachableBadge, NotRegisteredBadge, -} from "components/DeploySettingsLayout/Badges" -import { ProxyLatencyReport } from "contexts/useProxyLatency" -import { getLatencyColor } from "utils/latency" -import { Maybe } from "components/Conditionals/Maybe" -import Box from "@mui/material/Box" +} from "components/DeploySettingsLayout/Badges"; +import { ProxyLatencyReport } from "contexts/useProxyLatency"; +import { getLatencyColor } from "utils/latency"; +import { Maybe } from "components/Conditionals/Maybe"; +import Box from "@mui/material/Box"; export const ProxyRow: FC<{ - latency?: ProxyLatencyReport - proxy: Region + latency?: ProxyLatencyReport; + proxy: Region; }> = ({ proxy, latency }) => { // If we have a more specific proxy status, use that. // All users can see healthy/unhealthy, some can see more. - let statusBadge = - let shouldShowMessages = false + let statusBadge = ; + let shouldShowMessages = false; if ("status" in proxy) { - const wsproxy = proxy as WorkspaceProxy - statusBadge = + const wsproxy = proxy as WorkspaceProxy; + statusBadge = ; shouldShowMessages = Boolean( (wsproxy.status?.report?.warnings && wsproxy.status?.report?.warnings.length > 0) || (wsproxy.status?.report?.errors && wsproxy.status?.report?.errors.length > 0), - ) + ); } return ( @@ -83,11 +83,11 @@ export const ProxyRow: FC<{ - ) -} + ); +}; const ProxyMessagesRow: FC<{ - proxy: WorkspaceProxy + proxy: WorkspaceProxy; }> = ({ proxy }) => { return ( <> @@ -114,15 +114,15 @@ const ProxyMessagesRow: FC<{ messages={proxy.status?.report?.warnings} /> - ) -} + ); +}; const ProxyMessagesList: FC<{ - title: ReactNode - messages?: string[] + title: ReactNode; + messages?: string[]; }> = ({ title, messages }) => { if (!messages) { - return <> + return <>; } return ( @@ -157,45 +157,45 @@ const ProxyMessagesList: FC<{ ))} - ) -} + ); +}; // DetailedProxyStatus allows a more precise status to be displayed. const DetailedProxyStatus: FC<{ - proxy: WorkspaceProxy + proxy: WorkspaceProxy; }> = ({ proxy }) => { if (!proxy.status) { // If the status is null/undefined/not provided, just go with the boolean "healthy" value. - return + return ; } - let derpOnly = false + let derpOnly = false; if ("derp_only" in proxy) { - derpOnly = proxy.derp_only + derpOnly = proxy.derp_only; } switch (proxy.status.status) { case "ok": - return + return ; case "unhealthy": - return + return ; case "unreachable": - return + return ; case "unregistered": - return + return ; default: - return + return ; } -} +}; // ProxyStatus will only show "healthy" or "not healthy" status. const ProxyStatus: FC<{ - proxy: Region + proxy: Region; }> = ({ proxy }) => { - let icon = + let icon = ; if (proxy.healthy) { - icon = + icon = ; } - return icon -} + return icon; +}; diff --git a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyView.tsx b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyView.tsx index de0fad789b..21037635de 100644 --- a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyView.tsx +++ b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyView.tsx @@ -1,27 +1,27 @@ -import Table from "@mui/material/Table" -import TableBody from "@mui/material/TableBody" -import TableCell from "@mui/material/TableCell" -import TableContainer from "@mui/material/TableContainer" -import TableHead from "@mui/material/TableHead" -import TableRow from "@mui/material/TableRow" -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { Stack } from "components/Stack/Stack" -import { TableEmpty } from "components/TableEmpty/TableEmpty" -import { TableLoader } from "components/TableLoader/TableLoader" -import { FC } from "react" -import { Region } from "api/typesGenerated" -import { ProxyRow } from "./WorkspaceProxyRow" -import { ProxyLatencyReport } from "contexts/useProxyLatency" -import { ErrorAlert } from "components/Alert/ErrorAlert" +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { Stack } from "components/Stack/Stack"; +import { TableEmpty } from "components/TableEmpty/TableEmpty"; +import { TableLoader } from "components/TableLoader/TableLoader"; +import { FC } from "react"; +import { Region } from "api/typesGenerated"; +import { ProxyRow } from "./WorkspaceProxyRow"; +import { ProxyLatencyReport } from "contexts/useProxyLatency"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; export interface WorkspaceProxyViewProps { - proxies?: Region[] - proxyLatencies?: Record - getWorkspaceProxiesError?: unknown - isLoading: boolean - hasLoaded: boolean - preferredProxy?: Region - selectProxyError?: unknown + proxies?: Region[]; + proxyLatencies?: Record; + getWorkspaceProxiesError?: unknown; + isLoading: boolean; + hasLoaded: boolean; + preferredProxy?: Region; + selectProxyError?: unknown; } export const WorkspaceProxyView: FC< @@ -74,5 +74,5 @@ export const WorkspaceProxyView: FC< - ) -} + ); +}; diff --git a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorspaceProxyView.stories.tsx b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorspaceProxyView.stories.tsx index 552aa63782..4e6d09e606 100644 --- a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorspaceProxyView.stories.tsx +++ b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorspaceProxyView.stories.tsx @@ -1,15 +1,15 @@ -import { Story } from "@storybook/react" +import { Story } from "@storybook/react"; import { mockApiError, MockWorkspaceProxies, MockPrimaryWorkspaceProxy, MockHealthyWildWorkspaceProxy, MockProxyLatencies, -} from "testHelpers/entities" +} from "testHelpers/entities"; import { WorkspaceProxyView, WorkspaceProxyViewProps, -} from "./WorkspaceProxyView" +} from "./WorkspaceProxyView"; export default { title: "components/WorkspaceProxyView", @@ -17,57 +17,57 @@ export default { args: { onRegenerateClick: { action: "Submit" }, }, -} +}; const Template: Story = ( args: WorkspaceProxyViewProps, -) => +) => ; -export const PrimarySelected = Template.bind({}) +export const PrimarySelected = Template.bind({}); PrimarySelected.args = { isLoading: false, hasLoaded: true, proxies: MockWorkspaceProxies, proxyLatencies: MockProxyLatencies, preferredProxy: MockPrimaryWorkspaceProxy, -} +}; -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { isLoading: false, hasLoaded: true, proxies: MockWorkspaceProxies, proxyLatencies: MockProxyLatencies, preferredProxy: MockHealthyWildWorkspaceProxy, -} +}; -export const Loading = Template.bind({}) +export const Loading = Template.bind({}); Loading.args = { ...Example.args, isLoading: true, hasLoaded: false, -} +}; -export const Empty = Template.bind({}) +export const Empty = Template.bind({}); Empty.args = { ...Example.args, proxies: [], -} +}; -export const WithProxiesError = Template.bind({}) +export const WithProxiesError = Template.bind({}); WithProxiesError.args = { ...Example.args, hasLoaded: false, getWorkspaceProxiesError: mockApiError({ message: "Failed to get proxies.", }), -} +}; -export const WithSelectProxyError = Template.bind({}) +export const WithSelectProxyError = Template.bind({}); WithSelectProxyError.args = { ...Example.args, hasLoaded: false, selectProxyError: mockApiError({ message: "Failed to select proxy.", }), -} +}; diff --git a/site/src/pages/UsersPage/ResetPasswordDialog.stories.tsx b/site/src/pages/UsersPage/ResetPasswordDialog.stories.tsx index 66d123bbfb..5facf41985 100644 --- a/site/src/pages/UsersPage/ResetPasswordDialog.stories.tsx +++ b/site/src/pages/UsersPage/ResetPasswordDialog.stories.tsx @@ -1,11 +1,11 @@ -import { action } from "@storybook/addon-actions" -import { Story } from "@storybook/react" -import { MockUser } from "testHelpers/entities" +import { action } from "@storybook/addon-actions"; +import { Story } from "@storybook/react"; +import { MockUser } from "testHelpers/entities"; import { ResetPasswordDialog, ResetPasswordDialogProps, -} from "./ResetPasswordDialog" +} from "./ResetPasswordDialog"; export default { title: "components/Dialogs/ResetPasswordDialog", @@ -14,15 +14,15 @@ export default { onClose: { action: "onClose", defaultValue: action("onClose") }, onConfirm: { action: "onConfirm", defaultValue: action("onConfirm") }, }, -} +}; const Template: Story = ( args: ResetPasswordDialogProps, -) => +) => ; -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { open: true, user: MockUser, newPassword: "somerandomstringhere", -} +}; diff --git a/site/src/pages/UsersPage/ResetPasswordDialog.tsx b/site/src/pages/UsersPage/ResetPasswordDialog.tsx index 0ae2fe4c9c..691131c30e 100644 --- a/site/src/pages/UsersPage/ResetPasswordDialog.tsx +++ b/site/src/pages/UsersPage/ResetPasswordDialog.tsx @@ -1,16 +1,16 @@ -import { makeStyles } from "@mui/styles" -import { FC } from "react" -import * as TypesGen from "api/typesGenerated" -import { CodeExample } from "components/CodeExample/CodeExample" -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" +import { makeStyles } from "@mui/styles"; +import { FC } from "react"; +import * as TypesGen from "api/typesGenerated"; +import { CodeExample } from "components/CodeExample/CodeExample"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; export interface ResetPasswordDialogProps { - open: boolean - onClose: () => void - onConfirm: () => void - user?: TypesGen.User - newPassword?: string - loading: boolean + open: boolean; + onClose: () => void; + onConfirm: () => void; + user?: TypesGen.User; + newPassword?: string; + loading: boolean; } export const Language = { @@ -21,19 +21,19 @@ export const Language = { ), confirmText: "Reset password", -} +}; export const ResetPasswordDialog: FC< React.PropsWithChildren > = ({ open, onClose, onConfirm, user, newPassword, loading }) => { - const styles = useStyles() + const styles = useStyles(); const description = ( <>

{Language.message(user?.username)}

- ) + ); return ( - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ codeExample: { @@ -57,4 +57,4 @@ const useStyles = makeStyles((theme) => ({ width: "100%", marginTop: theme.spacing(3), }, -})) +})); diff --git a/site/src/pages/UsersPage/UsersFilter.tsx b/site/src/pages/UsersPage/UsersFilter.tsx index e89e765cae..588925fe56 100644 --- a/site/src/pages/UsersPage/UsersFilter.tsx +++ b/site/src/pages/UsersPage/UsersFilter.tsx @@ -1,6 +1,6 @@ -import { FC } from "react" -import Box from "@mui/material/Box" -import { Palette, PaletteColor } from "@mui/material/styles" +import { FC } from "react"; +import Box from "@mui/material/Box"; +import { Palette, PaletteColor } from "@mui/material/styles"; import { Filter, FilterMenu, @@ -8,15 +8,15 @@ import { OptionItem, SearchFieldSkeleton, useFilter, -} from "components/Filter/filter" -import { BaseOption } from "components/Filter/options" -import { UseFilterMenuOptions, useFilterMenu } from "components/Filter/menu" -import { userFilterQuery } from "utils/filters" -import { docs } from "utils/docs" +} from "components/Filter/filter"; +import { BaseOption } from "components/Filter/options"; +import { UseFilterMenuOptions, useFilterMenu } from "components/Filter/menu"; +import { userFilterQuery } from "utils/filters"; +import { docs } from "utils/docs"; type StatusOption = BaseOption & { - color: string -} + color: string; +}; export const useStatusFilterMenu = ({ value, @@ -26,7 +26,7 @@ export const useStatusFilterMenu = ({ { value: "active", label: "Active", color: "success" }, { value: "dormant", label: "Dormant", color: "secondary" }, { value: "suspended", label: "Suspended", color: "warning" }, - ] + ]; return useFilterMenu({ onChange, value, @@ -34,26 +34,26 @@ export const useStatusFilterMenu = ({ getSelectedOption: async () => statusOptions.find((option) => option.value === value) ?? null, getOptions: async () => statusOptions, - }) -} + }); +}; -export type StatusFilterMenu = ReturnType +export type StatusFilterMenu = ReturnType; const PRESET_FILTERS = [ { query: userFilterQuery.active, name: "Active users" }, { query: userFilterQuery.all, name: "All users" }, -] +]; export const UsersFilter = ({ filter, error, menus, }: { - filter: ReturnType - error?: unknown + filter: ReturnType; + error?: unknown; menus: { - status: StatusFilterMenu - } + status: StatusFilterMenu; + }; }) => { return ( } /> - ) -} + ); +}; const StatusMenu = (menu: StatusFilterMenu) => { return ( @@ -90,15 +90,15 @@ const StatusMenu = (menu: StatusFilterMenu) => { > {(itemProps) => } - ) -} + ); +}; const StatusOptionItem = ({ option, isSelected, }: { - option: StatusOption - isSelected?: boolean + option: StatusOption; + isSelected?: boolean; }) => { return ( } isSelected={isSelected} /> - ) -} + ); +}; const StatusIndicator: FC<{ option: StatusOption }> = ({ option }) => { return ( @@ -120,5 +120,5 @@ const StatusIndicator: FC<{ option: StatusOption }> = ({ option }) => { (theme.palette[option.color as keyof Palette] as PaletteColor).light, }} /> - ) -} + ); +}; diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx index e90f8fb7f9..68734c7d1c 100644 --- a/site/src/pages/UsersPage/UsersPage.test.tsx +++ b/site/src/pages/UsersPage/UsersPage.test.tsx @@ -1,464 +1,468 @@ -import { fireEvent, screen, waitFor, within } from "@testing-library/react" -import userEvent from "@testing-library/user-event" -import { i18n } from "i18n" -import { rest } from "msw" +import { fireEvent, screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { i18n } from "i18n"; +import { rest } from "msw"; import { MockUser, MockUser2, SuspendedMockUser, MockAuditorRole, MockOwnerRole, -} from "testHelpers/entities" -import { Language as usersXServiceLanguage } from "xServices/users/usersXService" -import * as API from "../../api/api" -import { Role } from "../../api/typesGenerated" -import { Language as ResetPasswordDialogLanguage } from "./ResetPasswordDialog" -import { renderWithAuth } from "../../testHelpers/renderHelpers" -import { server } from "../../testHelpers/server" -import { Language as UsersPageLanguage, UsersPage } from "./UsersPage" +} from "testHelpers/entities"; +import { Language as usersXServiceLanguage } from "xServices/users/usersXService"; +import * as API from "../../api/api"; +import { Role } from "../../api/typesGenerated"; +import { Language as ResetPasswordDialogLanguage } from "./ResetPasswordDialog"; +import { renderWithAuth } from "../../testHelpers/renderHelpers"; +import { server } from "../../testHelpers/server"; +import { Language as UsersPageLanguage, UsersPage } from "./UsersPage"; -const { t } = i18n +const { t } = i18n; const renderPage = () => { - return renderWithAuth() -} + return renderWithAuth(); +}; const suspendUser = async (setupActionSpies: () => void) => { - const user = userEvent.setup() + const user = userEvent.setup(); // Get the first user in the table - const moreButtons = await screen.findAllByLabelText("more") - const firstMoreButton = moreButtons[0] + const moreButtons = await screen.findAllByLabelText("more"); + const firstMoreButton = moreButtons[0]; - await user.click(firstMoreButton) + await user.click(firstMoreButton); - const menu = await screen.findByRole("menu") - const text = t("suspendMenuItem", { ns: "usersPage" }) - const suspendButton = within(menu).getByText(text) + const menu = await screen.findByRole("menu"); + const text = t("suspendMenuItem", { ns: "usersPage" }); + const suspendButton = within(menu).getByText(text); - await user.click(suspendButton) + await user.click(suspendButton); // Check if the confirm message is displayed - const confirmDialog = await screen.findByRole("dialog") + const confirmDialog = await screen.findByRole("dialog"); expect(confirmDialog).toHaveTextContent( `${UsersPageLanguage.suspendDialogMessagePrefix} ${MockUser.username}?`, - ) + ); // Setup spies to check the actions after - setupActionSpies() + setupActionSpies(); // Click on the "Confirm" button const confirmButton = await within(confirmDialog).findByText( UsersPageLanguage.suspendDialogAction, - ) - await user.click(confirmButton) -} + ); + await user.click(confirmButton); +}; const deleteUser = async (setupActionSpies: () => void) => { - const user = userEvent.setup() + const user = userEvent.setup(); // Click on the "more" button to display the "Delete" option // Needs to await fetching users and fetching permissions, because they're needed to see the more button - const moreButtons = await screen.findAllByLabelText("more") + const moreButtons = await screen.findAllByLabelText("more"); // get MockUser2 - const selectedMoreButton = moreButtons[1] + const selectedMoreButton = moreButtons[1]; - await user.click(selectedMoreButton) + await user.click(selectedMoreButton); - const menu = await screen.findByRole("menu") - const text = t("deleteMenuItem", { ns: "usersPage" }) - const deleteButton = within(menu).getByText(text) + const menu = await screen.findByRole("menu"); + const text = t("deleteMenuItem", { ns: "usersPage" }); + const deleteButton = within(menu).getByText(text); - await user.click(deleteButton) + await user.click(deleteButton); // Check if the confirm message is displayed - const confirmDialog = await screen.findByRole("dialog") + const confirmDialog = await screen.findByRole("dialog"); expect(confirmDialog).toHaveTextContent( t("deleteDialog.confirm", { ns: "common", entity: "user", name: MockUser2.username, }).toString(), - ) + ); // Confirm with text input const labelText = t("deleteDialog.confirmLabel", { ns: "common", entity: "user", - }) - const textField = screen.getByLabelText(labelText) - const dialog = screen.getByRole("dialog") - await user.type(textField, MockUser2.username) + }); + const textField = screen.getByLabelText(labelText); + const dialog = screen.getByRole("dialog"); + await user.type(textField, MockUser2.username); // Setup spies to check the actions after - setupActionSpies() + setupActionSpies(); // Click on the "Confirm" button - const confirmButton = within(dialog).getByRole("button", { name: "Delete" }) - await user.click(confirmButton) -} + const confirmButton = within(dialog).getByRole("button", { name: "Delete" }); + await user.click(confirmButton); +}; const activateUser = async (setupActionSpies: () => void) => { - const moreButtons = await screen.findAllByLabelText("more") - const suspendedMoreButton = moreButtons[2] - fireEvent.click(suspendedMoreButton) + const moreButtons = await screen.findAllByLabelText("more"); + const suspendedMoreButton = moreButtons[2]; + fireEvent.click(suspendedMoreButton); - const menu = screen.getByRole("menu") - const text = t("activateMenuItem", { ns: "usersPage" }) - const activateButton = within(menu).getByText(text) - fireEvent.click(activateButton) + const menu = screen.getByRole("menu"); + const text = t("activateMenuItem", { ns: "usersPage" }); + const activateButton = within(menu).getByText(text); + fireEvent.click(activateButton); // Check if the confirm message is displayed - const confirmDialog = screen.getByRole("dialog") + const confirmDialog = screen.getByRole("dialog"); expect(confirmDialog).toHaveTextContent( `${UsersPageLanguage.activateDialogMessagePrefix} ${SuspendedMockUser.username}?`, - ) + ); // Setup spies to check the actions after - setupActionSpies() + setupActionSpies(); // Click on the "Confirm" button const confirmButton = within(confirmDialog).getByText( UsersPageLanguage.activateDialogAction, - ) - fireEvent.click(confirmButton) -} + ); + fireEvent.click(confirmButton); +}; const resetUserPassword = async (setupActionSpies: () => void) => { - const moreButtons = await screen.findAllByLabelText("more") - const firstMoreButton = moreButtons[0] + const moreButtons = await screen.findAllByLabelText("more"); + const firstMoreButton = moreButtons[0]; - fireEvent.click(firstMoreButton) + fireEvent.click(firstMoreButton); - const menu = screen.getByRole("menu") - const text = t("resetPasswordMenuItem", { ns: "usersPage" }) - const resetPasswordButton = within(menu).getByText(text) + const menu = screen.getByRole("menu"); + const text = t("resetPasswordMenuItem", { ns: "usersPage" }); + const resetPasswordButton = within(menu).getByText(text); - fireEvent.click(resetPasswordButton) + fireEvent.click(resetPasswordButton); // Check if the confirm message is displayed - const confirmDialog = screen.getByRole("dialog") + const confirmDialog = screen.getByRole("dialog"); expect(confirmDialog).toHaveTextContent( `You will need to send ${MockUser.username} the following password:`, - ) + ); // Setup spies to check the actions after - setupActionSpies() + setupActionSpies(); // Click on the "Confirm" button const confirmButton = within(confirmDialog).getByRole("button", { name: ResetPasswordDialogLanguage.confirmText, - }) + }); - fireEvent.click(confirmButton) -} + fireEvent.click(confirmButton); +}; const updateUserRole = async (setupActionSpies: () => void, role: Role) => { // Get the first user in the table - const users = await screen.findAllByText(/.*@coder.com/) - const userRow = users[0].closest("tr") + const users = await screen.findAllByText(/.*@coder.com/); + const userRow = users[0].closest("tr"); if (!userRow) { - throw new Error("Error on get the first user row") + throw new Error("Error on get the first user row"); } // Click on the "edit icon" to display the role options - const buttonTitle = t("editUserRolesTooltip", { ns: "usersPage" }) - const editButton = within(userRow).getByTitle(buttonTitle) - fireEvent.click(editButton) + const buttonTitle = t("editUserRolesTooltip", { ns: "usersPage" }); + const editButton = within(userRow).getByTitle(buttonTitle); + fireEvent.click(editButton); // Setup spies to check the actions after - setupActionSpies() + setupActionSpies(); // Click on the role option - const fieldsetTitle = t("fieldSetRolesTooltip", { ns: "usersPage" }) - const fieldset = await screen.findByTitle(fieldsetTitle) - const auditorOption = within(fieldset).getByText(role.display_name) - fireEvent.click(auditorOption) + const fieldsetTitle = t("fieldSetRolesTooltip", { ns: "usersPage" }); + const fieldset = await screen.findByTitle(fieldsetTitle); + const auditorOption = within(fieldset).getByText(role.display_name); + fireEvent.click(auditorOption); return { userRow, - } -} + }; +}; describe("UsersPage", () => { it("shows users", async () => { - renderPage() - const users = await screen.findAllByText(/.*@coder.com/) - expect(users.length).toEqual(3) - }) + renderPage(); + const users = await screen.findAllByText(/.*@coder.com/); + expect(users.length).toEqual(3); + }); describe("suspend user", () => { describe("when it is success", () => { it("shows a success message and refresh the page", async () => { - renderPage() + renderPage(); await suspendUser(() => { - jest.spyOn(API, "suspendUser").mockResolvedValueOnce(MockUser) + jest.spyOn(API, "suspendUser").mockResolvedValueOnce(MockUser); jest.spyOn(API, "getUsers").mockResolvedValueOnce({ users: [SuspendedMockUser, MockUser2], count: 2, - }) - }) + }); + }); // Check if the success message is displayed - await screen.findByText(usersXServiceLanguage.suspendUserSuccess) + await screen.findByText(usersXServiceLanguage.suspendUserSuccess); // Check if the API was called correctly - expect(API.suspendUser).toBeCalledTimes(1) - expect(API.suspendUser).toBeCalledWith(MockUser.id) + expect(API.suspendUser).toBeCalledTimes(1); + expect(API.suspendUser).toBeCalledWith(MockUser.id); // Check if the users list was reload - await waitFor(() => expect(API.getUsers).toBeCalledTimes(1)) - }) - }) + await waitFor(() => expect(API.getUsers).toBeCalledTimes(1)); + }); + }); describe("when it fails", () => { it("shows an error message", async () => { - renderPage() + renderPage(); await suspendUser(() => { - jest.spyOn(API, "suspendUser").mockRejectedValueOnce({}) - }) + jest.spyOn(API, "suspendUser").mockRejectedValueOnce({}); + }); // Check if the error message is displayed - await screen.findByText(usersXServiceLanguage.suspendUserError) + await screen.findByText(usersXServiceLanguage.suspendUserError); // Check if the API was called correctly - expect(API.suspendUser).toBeCalledTimes(1) - expect(API.suspendUser).toBeCalledWith(MockUser.id) - }) - }) - }) + expect(API.suspendUser).toBeCalledTimes(1); + expect(API.suspendUser).toBeCalledWith(MockUser.id); + }); + }); + }); describe("pagination", () => { it("goes to next and previous page", async () => { - const { container } = renderPage() - const user = userEvent.setup() + const { container } = renderPage(); + const user = userEvent.setup(); const mock = jest .spyOn(API, "getUsers") - .mockResolvedValueOnce({ users: [MockUser, MockUser2], count: 26 }) + .mockResolvedValueOnce({ users: [MockUser, MockUser2], count: 26 }); - const nextButton = await screen.findByLabelText("Next page") - expect(nextButton).toBeEnabled() - const previousButton = await screen.findByLabelText("Previous page") - expect(previousButton).toBeDisabled() - await user.click(nextButton) + const nextButton = await screen.findByLabelText("Next page"); + expect(nextButton).toBeEnabled(); + const previousButton = await screen.findByLabelText("Previous page"); + expect(previousButton).toBeDisabled(); + await user.click(nextButton); await waitFor(() => expect(API.getUsers).toBeCalledWith({ offset: 25, limit: 25, q: "" }), - ) + ); - mock.mockClear() - await user.click(previousButton) + mock.mockClear(); + await user.click(previousButton); await waitFor(() => expect(API.getUsers).toBeCalledWith({ offset: 0, limit: 25, q: "" }), - ) + ); const pageButtons = container.querySelectorAll( `button[name="Page button"]`, - ) + ); // count handler says there are 2 pages of results - expect(pageButtons.length).toBe(2) - }) - }) + expect(pageButtons.length).toBe(2); + }); + }); describe("delete user", () => { describe("when it is success", () => { it("shows a success message and refresh the page", async () => { - renderPage() + renderPage(); await deleteUser(() => { - jest.spyOn(API, "deleteUser").mockResolvedValueOnce(undefined) + jest.spyOn(API, "deleteUser").mockResolvedValueOnce(undefined); jest.spyOn(API, "getUsers").mockResolvedValueOnce({ users: [MockUser, SuspendedMockUser], count: 2, - }) - }) + }); + }); // Check if the success message is displayed - await screen.findByText(usersXServiceLanguage.deleteUserSuccess) + await screen.findByText(usersXServiceLanguage.deleteUserSuccess); // Check if the API was called correctly - expect(API.deleteUser).toBeCalledTimes(1) - expect(API.deleteUser).toBeCalledWith(MockUser2.id) + expect(API.deleteUser).toBeCalledTimes(1); + expect(API.deleteUser).toBeCalledWith(MockUser2.id); // Check if the users list was reloaded await waitFor(() => { - const users = screen.getAllByLabelText("more") - expect(users.length).toEqual(2) - }) - }) - }) + const users = screen.getAllByLabelText("more"); + expect(users.length).toEqual(2); + }); + }); + }); describe("when it fails", () => { it("shows an error message", async () => { - renderPage() + renderPage(); await deleteUser(() => { - jest.spyOn(API, "deleteUser").mockRejectedValueOnce({}) - }) + jest.spyOn(API, "deleteUser").mockRejectedValueOnce({}); + }); // Check if the error message is displayed - await screen.findByText(usersXServiceLanguage.deleteUserError) + await screen.findByText(usersXServiceLanguage.deleteUserError); // Check if the API was called correctly - expect(API.deleteUser).toBeCalledTimes(1) - expect(API.deleteUser).toBeCalledWith(MockUser2.id) - }) - }) - }) + expect(API.deleteUser).toBeCalledTimes(1); + expect(API.deleteUser).toBeCalledWith(MockUser2.id); + }); + }); + }); describe("activate user", () => { describe("when user is successfully activated", () => { it("shows a success message and refreshes the page", async () => { - renderPage() + renderPage(); await activateUser(() => { jest .spyOn(API, "activateUser") - .mockResolvedValueOnce(SuspendedMockUser) + .mockResolvedValueOnce(SuspendedMockUser); jest.spyOn(API, "getUsers").mockImplementationOnce(() => Promise.resolve({ users: [MockUser, MockUser2, SuspendedMockUser], count: 3, }), - ) - }) + ); + }); // Check if the success message is displayed - await screen.findByText(usersXServiceLanguage.activateUserSuccess) + await screen.findByText(usersXServiceLanguage.activateUserSuccess); // Check if the API was called correctly - expect(API.activateUser).toBeCalledTimes(1) - expect(API.activateUser).toBeCalledWith(SuspendedMockUser.id) - }) - }) + expect(API.activateUser).toBeCalledTimes(1); + expect(API.activateUser).toBeCalledWith(SuspendedMockUser.id); + }); + }); describe("when activation fails", () => { it("shows an error message", async () => { - renderPage() + renderPage(); await activateUser(() => { - jest.spyOn(API, "activateUser").mockRejectedValueOnce({}) - }) + jest.spyOn(API, "activateUser").mockRejectedValueOnce({}); + }); // Check if the error message is displayed - await screen.findByText(usersXServiceLanguage.activateUserError) + await screen.findByText(usersXServiceLanguage.activateUserError); // Check if the API was called correctly - expect(API.activateUser).toBeCalledTimes(1) - expect(API.activateUser).toBeCalledWith(SuspendedMockUser.id) - }) - }) - }) + expect(API.activateUser).toBeCalledTimes(1); + expect(API.activateUser).toBeCalledWith(SuspendedMockUser.id); + }); + }); + }); describe("reset user password", () => { describe("when it is success", () => { it("shows a success message", async () => { - renderPage() + renderPage(); await resetUserPassword(() => { - jest.spyOn(API, "updateUserPassword").mockResolvedValueOnce(undefined) - }) + jest + .spyOn(API, "updateUserPassword") + .mockResolvedValueOnce(undefined); + }); // Check if the success message is displayed - await screen.findByText(usersXServiceLanguage.resetUserPasswordSuccess) + await screen.findByText(usersXServiceLanguage.resetUserPasswordSuccess); // Check if the API was called correctly - expect(API.updateUserPassword).toBeCalledTimes(1) + expect(API.updateUserPassword).toBeCalledTimes(1); expect(API.updateUserPassword).toBeCalledWith(MockUser.id, { password: expect.any(String), old_password: "", - }) - }) - }) + }); + }); + }); describe("when it fails", () => { it("shows an error message", async () => { - renderPage() + renderPage(); await resetUserPassword(() => { - jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({}) - }) + jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({}); + }); // Check if the error message is displayed - await screen.findByText(usersXServiceLanguage.resetUserPasswordError) + await screen.findByText(usersXServiceLanguage.resetUserPasswordError); // Check if the API was called correctly - expect(API.updateUserPassword).toBeCalledTimes(1) + expect(API.updateUserPassword).toBeCalledTimes(1); expect(API.updateUserPassword).toBeCalledWith(MockUser.id, { password: expect.any(String), old_password: "", - }) - }) - }) - }) + }); + }); + }); + }); describe("Update user role", () => { describe("when it is success", () => { it("updates the roles", async () => { - renderPage() + renderPage(); const { userRow } = await updateUserRole(() => { jest.spyOn(API, "updateUserRoles").mockResolvedValueOnce({ ...MockUser, roles: [...MockUser.roles, MockAuditorRole], - }) - }, MockAuditorRole) + }); + }, MockAuditorRole); // Check if the select text was updated with the Auditor role await waitFor(() => { - expect(userRow).toHaveTextContent(MockOwnerRole.display_name) - }) - expect(userRow).toHaveTextContent(MockAuditorRole.display_name) + expect(userRow).toHaveTextContent(MockOwnerRole.display_name); + }); + expect(userRow).toHaveTextContent(MockAuditorRole.display_name); // Check if the API was called correctly - const currentRoles = MockUser.roles.map((r) => r.name) - expect(API.updateUserRoles).toBeCalledTimes(1) + const currentRoles = MockUser.roles.map((r) => r.name); + expect(API.updateUserRoles).toBeCalledTimes(1); expect(API.updateUserRoles).toBeCalledWith( [...currentRoles, MockAuditorRole.name], MockUser.id, - ) - }) - }) + ); + }); + }); describe("when it fails", () => { it("shows an error message", async () => { - renderPage() + renderPage(); await updateUserRole(() => { - jest.spyOn(API, "updateUserRoles").mockRejectedValueOnce({}) - }, MockAuditorRole) + jest.spyOn(API, "updateUserRoles").mockRejectedValueOnce({}); + }, MockAuditorRole); // Check if the error message is displayed const errorMessage = await screen.findByText( usersXServiceLanguage.updateUserRolesError, - ) - await waitFor(() => expect(errorMessage).toBeDefined()) + ); + await waitFor(() => expect(errorMessage).toBeDefined()); // Check if the API was called correctly - const currentRoles = MockUser.roles.map((r) => r.name) + const currentRoles = MockUser.roles.map((r) => r.name); - expect(API.updateUserRoles).toBeCalledTimes(1) + expect(API.updateUserRoles).toBeCalledTimes(1); expect(API.updateUserRoles).toBeCalledWith( [...currentRoles, MockAuditorRole.name], MockUser.id, - ) - }) + ); + }); it("shows an error from the backend", async () => { - renderPage() + renderPage(); server.use( rest.put(`/api/v2/users/${MockUser.id}/roles`, (req, res, ctx) => { return res( ctx.status(400), ctx.json({ message: "message from the backend" }), - ) + ); }), - ) + ); - await updateUserRole(() => null, MockAuditorRole) + await updateUserRole(() => null, MockAuditorRole); // Check if the error message is displayed - const errorMessage = await screen.findByText("message from the backend") - expect(errorMessage).toBeDefined() - }) - }) - }) -}) + const errorMessage = await screen.findByText( + "message from the backend", + ); + expect(errorMessage).toBeDefined(); + }); + }); + }); +}); diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 49b02f556c..809b14c683 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -1,27 +1,27 @@ -import { useMachine } from "@xstate/react" -import { User } from "api/typesGenerated" -import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog" +import { useMachine } from "@xstate/react"; +import { User } from "api/typesGenerated"; +import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; import { getPaginationContext, nonInitialPage, -} from "components/PaginationWidget/utils" -import { useMe } from "hooks/useMe" -import { usePermissions } from "hooks/usePermissions" -import { FC, ReactNode, useEffect } from "react" -import { Helmet } from "react-helmet-async" -import { useSearchParams, useNavigate } from "react-router-dom" -import { siteRolesMachine } from "xServices/roles/siteRolesXService" -import { usersMachine } from "xServices/users/usersXService" -import { ConfirmDialog } from "../../components/Dialogs/ConfirmDialog/ConfirmDialog" -import { ResetPasswordDialog } from "./ResetPasswordDialog" -import { pageTitle } from "../../utils/page" -import { UsersPageView } from "./UsersPageView" -import { useStatusFilterMenu } from "./UsersFilter" -import { useFilter } from "components/Filter/filter" -import { useDashboard } from "components/Dashboard/DashboardProvider" -import { deploymentConfigMachine } from "xServices/deploymentConfig/deploymentConfigMachine" -import { useQuery } from "@tanstack/react-query" -import { getAuthMethods } from "api/api" +} from "components/PaginationWidget/utils"; +import { useMe } from "hooks/useMe"; +import { usePermissions } from "hooks/usePermissions"; +import { FC, ReactNode, useEffect } from "react"; +import { Helmet } from "react-helmet-async"; +import { useSearchParams, useNavigate } from "react-router-dom"; +import { siteRolesMachine } from "xServices/roles/siteRolesXService"; +import { usersMachine } from "xServices/users/usersXService"; +import { ConfirmDialog } from "../../components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { ResetPasswordDialog } from "./ResetPasswordDialog"; +import { pageTitle } from "../../utils/page"; +import { UsersPageView } from "./UsersPageView"; +import { useStatusFilterMenu } from "./UsersFilter"; +import { useFilter } from "components/Filter/filter"; +import { useDashboard } from "components/Dashboard/DashboardProvider"; +import { deploymentConfigMachine } from "xServices/deploymentConfig/deploymentConfigMachine"; +import { useQuery } from "@tanstack/react-query"; +import { getAuthMethods } from "api/api"; export const Language = { suspendDialogTitle: "Suspend user", @@ -30,17 +30,17 @@ export const Language = { activateDialogTitle: "Activate user", activateDialogAction: "Activate", activateDialogMessagePrefix: "Do you want to activate the user", -} +}; const getSelectedUser = (id: string, users?: User[]) => - users?.find((u) => u.id === id) + users?.find((u) => u.id === id); export const UsersPage: FC<{ children?: ReactNode }> = () => { - const navigate = useNavigate() - const searchParamsResult = useSearchParams() - const { entitlements } = useDashboard() - const [searchParams, setSearchParams] = searchParamsResult - const filter = searchParams.get("filter") ?? "" + const navigate = useNavigate(); + const searchParamsResult = useSearchParams(); + const { entitlements } = useDashboard(); + const [searchParams, setSearchParams] = searchParamsResult; + const filter = searchParams.get("filter") ?? ""; const [usersState, usersSend] = useMachine(usersMachine, { context: { filter, @@ -50,7 +50,7 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { updateURL: (context, event) => setSearchParams({ page: event.page, filter: context.filter }), }, - }) + }); const { users, getUsersError, @@ -61,35 +61,35 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { newUserPassword, paginationRef, count, - } = usersState.context + } = usersState.context; - const { updateUsers: canEditUsers, viewDeploymentValues } = usePermissions() + const { updateUsers: canEditUsers, viewDeploymentValues } = usePermissions(); const [rolesState] = useMachine(siteRolesMachine, { context: { hasPermission: canEditUsers, }, - }) - const { roles } = rolesState.context + }); + const { roles } = rolesState.context; // Ideally this only runs if 'canViewDeployment' is true. // TODO: Prevent api call if the user does not have the perms. - const [state] = useMachine(deploymentConfigMachine) - const { deploymentValues } = state.context + const [state] = useMachine(deploymentConfigMachine); + const { deploymentValues } = state.context; // Indicates if oidc roles are synced from the oidc idp. // Assign 'false' if unknown. const oidcRoleSyncEnabled = viewDeploymentValues && - deploymentValues?.config.oidc?.user_role_field !== "" - const me = useMe() + deploymentValues?.config.oidc?.user_role_field !== ""; + const me = useMe(); const useFilterResult = useFilter({ searchParamsResult, onUpdate: () => { - usersSend({ type: "UPDATE_PAGE", page: "1" }) + usersSend({ type: "UPDATE_PAGE", page: "1" }); }, - }) + }); useEffect(() => { - usersSend({ type: "UPDATE_FILTER", query: useFilterResult.query }) - }, [useFilterResult.query, usersSend]) + usersSend({ type: "UPDATE_FILTER", query: useFilterResult.query }); + }, [useFilterResult.query, usersSend]); const statusMenu = useStatusFilterMenu({ value: useFilterResult.values.status, onChange: (option) => @@ -97,20 +97,20 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { ...useFilterResult.values, status: option?.value, }), - }) + }); const authMethods = useQuery({ queryKey: ["authMethods"], queryFn: () => { - return getAuthMethods() + return getAuthMethods(); }, - }) + }); // Is loading if // - users are loading or // - the user can edit the users but the roles are loading const isLoading = usersState.matches("gettingUsers") || (canEditUsers && rolesState.matches("gettingRoles")) || - authMethods.isLoading + authMethods.isLoading; return ( <> @@ -127,43 +127,43 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { navigate( "/workspaces?filter=" + encodeURIComponent(`owner:${user.username}`), - ) + ); }} onViewActivity={(user) => { navigate( "/audit?filter=" + encodeURIComponent(`username:${user.username}`), - ) + ); }} onDeleteUser={(user) => { usersSend({ type: "DELETE_USER", userId: user.id, username: user.username, - }) + }); }} onSuspendUser={(user) => { usersSend({ type: "SUSPEND_USER", userId: user.id, username: user.username, - }) + }); }} onActivateUser={(user) => { usersSend({ type: "ACTIVATE_USER", userId: user.id, username: user.username, - }) + }); }} onResetUserPassword={(user) => { - usersSend({ type: "RESET_USER_PASSWORD", userId: user.id }) + usersSend({ type: "RESET_USER_PASSWORD", userId: user.id }); }} onUpdateUserRoles={(user, roles) => { usersSend({ type: "UPDATE_USER_ROLES", userId: user.id, roles, - }) + }); }} isUpdatingUserRoles={usersState.matches("updatingUserRoles")} isLoading={isLoading} @@ -191,10 +191,10 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { name={usernameToDelete ?? ""} entity="user" onConfirm={() => { - usersSend("CONFIRM_USER_DELETE") + usersSend("CONFIRM_USER_DELETE"); }} onCancel={() => { - usersSend("CANCEL_USER_DELETE") + usersSend("CANCEL_USER_DELETE"); }} /> @@ -209,10 +209,10 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { title={Language.suspendDialogTitle} confirmText={Language.suspendDialogAction} onConfirm={() => { - usersSend("CONFIRM_USER_SUSPENSION") + usersSend("CONFIRM_USER_SUSPENSION"); }} onClose={() => { - usersSend("CANCEL_USER_SUSPENSION") + usersSend("CANCEL_USER_SUSPENSION"); }} description={ <> @@ -234,10 +234,10 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { title={Language.activateDialogTitle} confirmText={Language.activateDialogAction} onConfirm={() => { - usersSend("CONFIRM_USER_ACTIVATION") + usersSend("CONFIRM_USER_ACTIVATION"); }} onClose={() => { - usersSend("CANCEL_USER_ACTIVATION") + usersSend("CANCEL_USER_ACTIVATION"); }} description={ <> @@ -258,15 +258,15 @@ export const UsersPage: FC<{ children?: ReactNode }> = () => { user={getSelectedUser(userIdToResetPassword, users)} newPassword={newUserPassword} onClose={() => { - usersSend("CANCEL_USER_PASSWORD_RESET") + usersSend("CANCEL_USER_PASSWORD_RESET"); }} onConfirm={() => { - usersSend("CONFIRM_USER_PASSWORD_RESET") + usersSend("CONFIRM_USER_PASSWORD_RESET"); }} /> )} - ) -} + ); +}; -export default UsersPage +export default UsersPage; diff --git a/site/src/pages/UsersPage/UsersPageView.stories.tsx b/site/src/pages/UsersPage/UsersPageView.stories.tsx index a08a758ad3..f91932beed 100644 --- a/site/src/pages/UsersPage/UsersPageView.stories.tsx +++ b/site/src/pages/UsersPage/UsersPageView.stories.tsx @@ -1,17 +1,20 @@ -import { Meta, StoryObj } from "@storybook/react" -import { createPaginationRef } from "components/PaginationWidget/utils" +import { Meta, StoryObj } from "@storybook/react"; +import { createPaginationRef } from "components/PaginationWidget/utils"; import { MockUser, MockUser2, MockAssignableSiteRoles, mockApiError, MockAuthMethods, -} from "testHelpers/entities" -import { UsersPageView } from "./UsersPageView" -import { ComponentProps } from "react" -import { MockMenu, getDefaultFilterProps } from "components/Filter/storyHelpers" +} from "testHelpers/entities"; +import { UsersPageView } from "./UsersPageView"; +import { ComponentProps } from "react"; +import { + MockMenu, + getDefaultFilterProps, +} from "components/Filter/storyHelpers"; -type FilterProps = ComponentProps["filterProps"] +type FilterProps = ComponentProps["filterProps"]; const defaultFilterProps = getDefaultFilterProps({ query: "owner:me", @@ -21,7 +24,7 @@ const defaultFilterProps = getDefaultFilterProps({ values: { status: "active", }, -}) +}); const meta: Meta = { title: "pages/UsersPageView", @@ -36,26 +39,26 @@ const meta: Meta = { filterProps: defaultFilterProps, authMethods: MockAuthMethods, }, -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; -export const Admin: Story = {} +export const Admin: Story = {}; export const SmallViewport = { parameters: { chromatic: { viewports: [600] }, }, -} +}; export const Member = { args: { canEditUsers: false }, -} +}; export const Empty = { args: { users: [], count: 0 }, -} +}; export const EmptyPage = { args: { @@ -63,7 +66,7 @@ export const EmptyPage = { count: 0, isNonInitialPage: true, }, -} +}; export const Error = { args: { @@ -82,4 +85,4 @@ export const Error = { }), }, }, -} +}; diff --git a/site/src/pages/UsersPage/UsersPageView.tsx b/site/src/pages/UsersPage/UsersPageView.tsx index 40e733c707..d1baa2bcbc 100644 --- a/site/src/pages/UsersPage/UsersPageView.tsx +++ b/site/src/pages/UsersPage/UsersPageView.tsx @@ -1,42 +1,42 @@ -import { PaginationWidget } from "components/PaginationWidget/PaginationWidget" -import { ComponentProps, FC } from "react" -import { PaginationMachineRef } from "xServices/pagination/paginationXService" -import * as TypesGen from "../../api/typesGenerated" -import { UsersTable } from "./UsersTable/UsersTable" -import { UsersFilter } from "./UsersFilter" +import { PaginationWidget } from "components/PaginationWidget/PaginationWidget"; +import { ComponentProps, FC } from "react"; +import { PaginationMachineRef } from "xServices/pagination/paginationXService"; +import * as TypesGen from "../../api/typesGenerated"; +import { UsersTable } from "./UsersTable/UsersTable"; +import { UsersFilter } from "./UsersFilter"; import { PaginationStatus, TableToolbar, -} from "components/TableToolbar/TableToolbar" +} from "components/TableToolbar/TableToolbar"; export const Language = { activeUsersFilterName: "Active users", allUsersFilterName: "All users", -} +}; export interface UsersPageViewProps { - users?: TypesGen.User[] - count?: number - roles?: TypesGen.AssignableRoles[] - isUpdatingUserRoles?: boolean - canEditUsers?: boolean - oidcRoleSyncEnabled: boolean - canViewActivity?: boolean - isLoading?: boolean - authMethods?: TypesGen.AuthMethods - onSuspendUser: (user: TypesGen.User) => void - onDeleteUser: (user: TypesGen.User) => void - onListWorkspaces: (user: TypesGen.User) => void - onViewActivity: (user: TypesGen.User) => void - onActivateUser: (user: TypesGen.User) => void - onResetUserPassword: (user: TypesGen.User) => void + users?: TypesGen.User[]; + count?: number; + roles?: TypesGen.AssignableRoles[]; + isUpdatingUserRoles?: boolean; + canEditUsers?: boolean; + oidcRoleSyncEnabled: boolean; + canViewActivity?: boolean; + isLoading?: boolean; + authMethods?: TypesGen.AuthMethods; + onSuspendUser: (user: TypesGen.User) => void; + onDeleteUser: (user: TypesGen.User) => void; + onListWorkspaces: (user: TypesGen.User) => void; + onViewActivity: (user: TypesGen.User) => void; + onActivateUser: (user: TypesGen.User) => void; + onResetUserPassword: (user: TypesGen.User) => void; onUpdateUserRoles: ( user: TypesGen.User, roles: TypesGen.Role["name"][], - ) => void - filterProps: ComponentProps - paginationRef: PaginationMachineRef - isNonInitialPage: boolean - actorID: string + ) => void; + filterProps: ComponentProps; + paginationRef: PaginationMachineRef; + isNonInitialPage: boolean; + actorID: string; } export const UsersPageView: FC> = ({ @@ -96,5 +96,5 @@ export const UsersPageView: FC> = ({ - ) -} + ); +}; diff --git a/site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx b/site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx index 66b58f0741..191e761000 100644 --- a/site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx +++ b/site/src/pages/UsersPage/UsersTable/EditRolesButton.stories.tsx @@ -1,10 +1,10 @@ -import { ComponentMeta, Story } from "@storybook/react" +import { ComponentMeta, Story } from "@storybook/react"; import { MockOwnerRole, MockSiteRoles, MockUserAdminRole, -} from "testHelpers/entities" -import { EditRolesButtonProps, EditRolesButton } from "./EditRolesButton" +} from "testHelpers/entities"; +import { EditRolesButtonProps, EditRolesButton } from "./EditRolesButton"; export default { title: "components/EditRolesButton", @@ -14,29 +14,29 @@ export default { defaultValue: true, }, }, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) +); -export const Open = Template.bind({}) +export const Open = Template.bind({}); Open.args = { roles: MockSiteRoles, selectedRoles: [MockUserAdminRole, MockOwnerRole], -} +}; Open.parameters = { chromatic: { delay: 300 }, -} +}; -export const Loading = Template.bind({}) +export const Loading = Template.bind({}); Loading.args = { isLoading: true, roles: MockSiteRoles, selectedRoles: [MockUserAdminRole, MockOwnerRole], userLoginType: "password", oidcRoleSync: false, -} +}; Loading.parameters = { chromatic: { delay: 300 }, -} +}; diff --git a/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx b/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx index 1d88f2bb2d..8e0e8ef2ed 100644 --- a/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx +++ b/site/src/pages/UsersPage/UsersTable/EditRolesButton.tsx @@ -1,28 +1,28 @@ -import IconButton from "@mui/material/IconButton" -import { EditSquare } from "components/Icons/EditSquare" -import { useRef, useState, FC } from "react" -import { makeStyles } from "@mui/styles" -import { useTranslation } from "react-i18next" -import Popover from "@mui/material/Popover" -import { Stack } from "components/Stack/Stack" -import Checkbox from "@mui/material/Checkbox" -import UserIcon from "@mui/icons-material/PersonOutline" -import { Role } from "api/typesGenerated" +import IconButton from "@mui/material/IconButton"; +import { EditSquare } from "components/Icons/EditSquare"; +import { useRef, useState, FC } from "react"; +import { makeStyles } from "@mui/styles"; +import { useTranslation } from "react-i18next"; +import Popover from "@mui/material/Popover"; +import { Stack } from "components/Stack/Stack"; +import Checkbox from "@mui/material/Checkbox"; +import UserIcon from "@mui/icons-material/PersonOutline"; +import { Role } from "api/typesGenerated"; import { HelpTooltip, HelpTooltipText, HelpTooltipTitle, -} from "components/HelpTooltip/HelpTooltip" -import { Maybe } from "components/Conditionals/Maybe" +} from "components/HelpTooltip/HelpTooltip"; +import { Maybe } from "components/Conditionals/Maybe"; const Option: React.FC<{ - value: string - name: string - description: string - isChecked: boolean - onChange: (roleName: string) => void + value: string; + name: string; + description: string; + isChecked: boolean; + onChange: (roleName: string) => void; }> = ({ value, name, description, isChecked, onChange }) => { - const styles = useStyles() + const styles = useStyles(); return ( - ) -} + ); +}; export interface EditRolesButtonProps { - isLoading: boolean - roles: Role[] - selectedRoles: Role[] - onChange: (roles: Role["name"][]) => void - defaultIsOpen?: boolean - oidcRoleSync: boolean - userLoginType: string + isLoading: boolean; + roles: Role[]; + selectedRoles: Role[]; + onChange: (roles: Role["name"][]) => void; + defaultIsOpen?: boolean; + oidcRoleSync: boolean; + userLoginType: string; } export const EditRolesButton: FC = ({ @@ -65,24 +65,24 @@ export const EditRolesButton: FC = ({ userLoginType, oidcRoleSync, }) => { - const styles = useStyles() - const { t } = useTranslation("usersPage") - const anchorRef = useRef(null) - const [isOpen, setIsOpen] = useState(defaultIsOpen) - const id = isOpen ? "edit-roles-popover" : undefined - const selectedRoleNames = selectedRoles.map((role) => role.name) + const styles = useStyles(); + const { t } = useTranslation("usersPage"); + const anchorRef = useRef(null); + const [isOpen, setIsOpen] = useState(defaultIsOpen); + const id = isOpen ? "edit-roles-popover" : undefined; + const selectedRoleNames = selectedRoles.map((role) => role.name); const handleChange = (roleName: string) => { if (selectedRoleNames.includes(roleName)) { - onChange(selectedRoleNames.filter((role) => role !== roleName)) - return + onChange(selectedRoleNames.filter((role) => role !== roleName)); + return; } - onChange([...selectedRoleNames, roleName]) - } + onChange([...selectedRoleNames, roleName]); + }; const canSetRoles = - userLoginType !== "oidc" || (userLoginType === "oidc" && !oidcRoleSync) + userLoginType !== "oidc" || (userLoginType === "oidc" && !oidcRoleSync); return ( <> @@ -152,8 +152,8 @@ export const EditRolesButton: FC = ({
- ) -} + ); +}; const useStyles = makeStyles((theme) => ({ editButton: { @@ -218,4 +218,4 @@ const useStyles = makeStyles((theme) => ({ height: theme.spacing(2.5), color: theme.palette.primary.main, }, -})) +})); diff --git a/site/src/pages/UsersPage/UsersTable/UserRoleHelpTooltip.tsx b/site/src/pages/UsersPage/UsersTable/UserRoleHelpTooltip.tsx index ff7dca132f..8cbe7f744c 100644 --- a/site/src/pages/UsersPage/UsersTable/UserRoleHelpTooltip.tsx +++ b/site/src/pages/UsersPage/UsersTable/UserRoleHelpTooltip.tsx @@ -1,12 +1,12 @@ -import { FC } from "react" +import { FC } from "react"; import { HelpTooltip, HelpTooltipLink, HelpTooltipLinksGroup, HelpTooltipText, HelpTooltipTitle, -} from "components/HelpTooltip/HelpTooltip" -import { docs } from "utils/docs" +} from "components/HelpTooltip/HelpTooltip"; +import { docs } from "utils/docs"; export const Language = { title: "What is a role?", @@ -14,7 +14,7 @@ export const Language = { "Coder role-based access control (RBAC) provides fine-grained access management. " + "View our docs on how to use the available roles.", link: "User Roles", -} +}; export const UserRoleHelpTooltip: FC = () => { return ( @@ -27,5 +27,5 @@ export const UserRoleHelpTooltip: FC = () => { - ) -} + ); +}; diff --git a/site/src/pages/UsersPage/UsersTable/UsersTable.stories.tsx b/site/src/pages/UsersPage/UsersTable/UsersTable.stories.tsx index a40d44e861..33a8e0c041 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTable.stories.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTable.stories.tsx @@ -3,9 +3,9 @@ import { MockUser2, MockAssignableSiteRoles, MockAuthMethods, -} from "testHelpers/entities" -import { UsersTable } from "./UsersTable" -import type { Meta, StoryObj } from "@storybook/react" +} from "testHelpers/entities"; +import { UsersTable } from "./UsersTable"; +import type { Meta, StoryObj } from "@storybook/react"; const meta: Meta = { title: "components/UsersTable", @@ -14,10 +14,10 @@ const meta: Meta = { isNonInitialPage: false, authMethods: MockAuthMethods, }, -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; export const Example: Story = { args: { @@ -25,7 +25,7 @@ export const Example: Story = { roles: MockAssignableSiteRoles, canEditUsers: false, }, -} +}; export const Editable: Story = { args: { @@ -59,14 +59,14 @@ export const Editable: Story = { canEditUsers: true, canViewActivity: true, }, -} +}; export const Empty: Story = { args: { users: [], roles: MockAssignableSiteRoles, }, -} +}; export const Loading: Story = { args: { @@ -77,4 +77,4 @@ export const Loading: Story = { parameters: { chromatic: { pauseAnimationAtEnd: true }, }, -} +}; diff --git a/site/src/pages/UsersPage/UsersTable/UsersTable.tsx b/site/src/pages/UsersPage/UsersTable/UsersTable.tsx index 0756d2cec2..d89e1d719c 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTable.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTable.tsx @@ -1,14 +1,14 @@ -import Table from "@mui/material/Table" -import TableBody from "@mui/material/TableBody" -import TableCell from "@mui/material/TableCell" -import TableContainer from "@mui/material/TableContainer" -import TableHead from "@mui/material/TableHead" -import TableRow from "@mui/material/TableRow" -import { FC } from "react" -import * as TypesGen from "../../../api/typesGenerated" -import { Stack } from "../../../components/Stack/Stack" -import { UserRoleHelpTooltip } from "./UserRoleHelpTooltip" -import { UsersTableBody } from "./UsersTableBody" +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import { FC } from "react"; +import * as TypesGen from "../../../api/typesGenerated"; +import { Stack } from "../../../components/Stack/Stack"; +import { UserRoleHelpTooltip } from "./UserRoleHelpTooltip"; +import { UsersTableBody } from "./UsersTableBody"; export const Language = { usernameLabel: "User", @@ -16,29 +16,29 @@ export const Language = { statusLabel: "Status", lastSeenLabel: "Last Seen", loginTypeLabel: "Login Type", -} +}; export interface UsersTableProps { - users?: TypesGen.User[] - roles?: TypesGen.AssignableRoles[] - isUpdatingUserRoles?: boolean - canEditUsers?: boolean - canViewActivity?: boolean - isLoading?: boolean - onSuspendUser: (user: TypesGen.User) => void - onActivateUser: (user: TypesGen.User) => void - onDeleteUser: (user: TypesGen.User) => void - onListWorkspaces: (user: TypesGen.User) => void - onViewActivity: (user: TypesGen.User) => void - onResetUserPassword: (user: TypesGen.User) => void + users?: TypesGen.User[]; + roles?: TypesGen.AssignableRoles[]; + isUpdatingUserRoles?: boolean; + canEditUsers?: boolean; + canViewActivity?: boolean; + isLoading?: boolean; + onSuspendUser: (user: TypesGen.User) => void; + onActivateUser: (user: TypesGen.User) => void; + onDeleteUser: (user: TypesGen.User) => void; + onListWorkspaces: (user: TypesGen.User) => void; + onViewActivity: (user: TypesGen.User) => void; + onResetUserPassword: (user: TypesGen.User) => void; onUpdateUserRoles: ( user: TypesGen.User, roles: TypesGen.Role["name"][], - ) => void - isNonInitialPage: boolean - actorID: string - oidcRoleSyncEnabled: boolean - authMethods?: TypesGen.AuthMethods + ) => void; + isNonInitialPage: boolean; + actorID: string; + oidcRoleSyncEnabled: boolean; + authMethods?: TypesGen.AuthMethods; } export const UsersTable: FC> = ({ @@ -101,5 +101,5 @@ export const UsersTable: FC> = ({ - ) -} + ); +}; diff --git a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx index 9beb57be63..e3859be247 100644 --- a/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx +++ b/site/src/pages/UsersPage/UsersTable/UsersTableBody.tsx @@ -1,72 +1,72 @@ -import Box, { BoxProps } from "@mui/material/Box" -import { makeStyles, useTheme } from "@mui/styles" -import TableCell from "@mui/material/TableCell" -import TableRow from "@mui/material/TableRow" -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { Pill } from "components/Pill/Pill" -import { FC } from "react" -import { useTranslation } from "react-i18next" -import * as TypesGen from "../../../api/typesGenerated" -import { combineClasses } from "../../../utils/combineClasses" -import { AvatarData } from "../../../components/AvatarData/AvatarData" -import { EmptyState } from "../../../components/EmptyState/EmptyState" +import Box, { BoxProps } from "@mui/material/Box"; +import { makeStyles, useTheme } from "@mui/styles"; +import TableCell from "@mui/material/TableCell"; +import TableRow from "@mui/material/TableRow"; +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { Pill } from "components/Pill/Pill"; +import { FC } from "react"; +import { useTranslation } from "react-i18next"; +import * as TypesGen from "../../../api/typesGenerated"; +import { combineClasses } from "../../../utils/combineClasses"; +import { AvatarData } from "../../../components/AvatarData/AvatarData"; +import { EmptyState } from "../../../components/EmptyState/EmptyState"; import { TableLoaderSkeleton, TableRowSkeleton, -} from "../../../components/TableLoader/TableLoader" -import { TableRowMenu } from "../../../components/TableRowMenu/TableRowMenu" -import { EditRolesButton } from "./EditRolesButton" -import { Stack } from "components/Stack/Stack" -import { EnterpriseBadge } from "components/DeploySettingsLayout/Badges" -import dayjs from "dayjs" -import { SxProps, Theme } from "@mui/material/styles" -import HideSourceOutlined from "@mui/icons-material/HideSourceOutlined" -import KeyOutlined from "@mui/icons-material/KeyOutlined" -import GitHub from "@mui/icons-material/GitHub" -import PasswordOutlined from "@mui/icons-material/PasswordOutlined" -import relativeTime from "dayjs/plugin/relativeTime" -import ShieldOutlined from "@mui/icons-material/ShieldOutlined" -import Skeleton from "@mui/material/Skeleton" -import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton" +} from "../../../components/TableLoader/TableLoader"; +import { TableRowMenu } from "../../../components/TableRowMenu/TableRowMenu"; +import { EditRolesButton } from "./EditRolesButton"; +import { Stack } from "components/Stack/Stack"; +import { EnterpriseBadge } from "components/DeploySettingsLayout/Badges"; +import dayjs from "dayjs"; +import { SxProps, Theme } from "@mui/material/styles"; +import HideSourceOutlined from "@mui/icons-material/HideSourceOutlined"; +import KeyOutlined from "@mui/icons-material/KeyOutlined"; +import GitHub from "@mui/icons-material/GitHub"; +import PasswordOutlined from "@mui/icons-material/PasswordOutlined"; +import relativeTime from "dayjs/plugin/relativeTime"; +import ShieldOutlined from "@mui/icons-material/ShieldOutlined"; +import Skeleton from "@mui/material/Skeleton"; +import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"; -dayjs.extend(relativeTime) +dayjs.extend(relativeTime); const isOwnerRole = (role: TypesGen.Role): boolean => { - return role.name === "owner" -} + return role.name === "owner"; +}; -const roleOrder = ["owner", "user-admin", "template-admin", "auditor"] +const roleOrder = ["owner", "user-admin", "template-admin", "auditor"]; const sortRoles = (roles: TypesGen.Role[]) => { return roles.slice(0).sort((a, b) => { - return roleOrder.indexOf(a.name) - roleOrder.indexOf(b.name) - }) -} + return roleOrder.indexOf(a.name) - roleOrder.indexOf(b.name); + }); +}; interface UsersTableBodyProps { - users?: TypesGen.User[] - authMethods?: TypesGen.AuthMethods - roles?: TypesGen.AssignableRoles[] - isUpdatingUserRoles?: boolean - canEditUsers?: boolean - isLoading?: boolean - canViewActivity?: boolean - onSuspendUser: (user: TypesGen.User) => void - onDeleteUser: (user: TypesGen.User) => void - onListWorkspaces: (user: TypesGen.User) => void - onViewActivity: (user: TypesGen.User) => void - onActivateUser: (user: TypesGen.User) => void - onResetUserPassword: (user: TypesGen.User) => void + users?: TypesGen.User[]; + authMethods?: TypesGen.AuthMethods; + roles?: TypesGen.AssignableRoles[]; + isUpdatingUserRoles?: boolean; + canEditUsers?: boolean; + isLoading?: boolean; + canViewActivity?: boolean; + onSuspendUser: (user: TypesGen.User) => void; + onDeleteUser: (user: TypesGen.User) => void; + onListWorkspaces: (user: TypesGen.User) => void; + onViewActivity: (user: TypesGen.User) => void; + onActivateUser: (user: TypesGen.User) => void; + onResetUserPassword: (user: TypesGen.User) => void; onUpdateUserRoles: ( user: TypesGen.User, roles: TypesGen.Role["name"][], - ) => void - isNonInitialPage: boolean - actorID: string + ) => void; + isNonInitialPage: boolean; + actorID: string; // oidcRoleSyncEnabled should be set to false if unknown. // This is used to determine if the oidc roles are synced from the oidc idp and // editing via the UI should be disabled. - oidcRoleSyncEnabled: boolean + oidcRoleSyncEnabled: boolean; } export const UsersTableBody: FC< @@ -90,8 +90,8 @@ export const UsersTableBody: FC< actorID, oidcRoleSyncEnabled, }) => { - const styles = useStyles() - const { t } = useTranslation("usersPage") + const styles = useStyles(); + const { t } = useTranslation("usersPage"); return ( @@ -150,9 +150,11 @@ export const UsersTableBody: FC< const fallbackRole: TypesGen.Role = { name: "member", display_name: "Member", - } + }; const userRoles = - user.roles.length === 0 ? [fallbackRole] : sortRoles(user.roles) + user.roles.length === 0 + ? [fallbackRole] + : sortRoles(user.roles); return ( @@ -176,8 +178,8 @@ export const UsersTableBody: FC< // Remove the fallback role because it is only for the UI const rolesWithoutFallback = roles.filter( (role) => role !== fallbackRole.name, - ) - onUpdateUserRoles(user, rolesWithoutFallback) + ); + onUpdateUserRoles(user, rolesWithoutFallback); }} /> )} @@ -268,40 +270,40 @@ export const UsersTableBody: FC< )} - ) + ); })} - ) -} + ); +}; const LoginType = ({ authMethods, value, }: { - authMethods: TypesGen.AuthMethods - value: TypesGen.LoginType + authMethods: TypesGen.AuthMethods; + value: TypesGen.LoginType; }) => { - let displayName = value as string - let icon = <> - const iconStyles: SxProps = { width: 14, height: 14 } + let displayName = value as string; + let icon = <>; + const iconStyles: SxProps = { width: 14, height: 14 }; if (value === "password") { - displayName = "Password" - icon = + displayName = "Password"; + icon = ; } else if (value === "none") { - displayName = "None" - icon = + displayName = "None"; + icon = ; } else if (value === "github") { - displayName = "GitHub" - icon = + displayName = "GitHub"; + icon = ; } else if (value === "token") { - displayName = "Token" - icon = + displayName = "Token"; + icon = ; } else if (value === "oidc") { displayName = - authMethods.oidc.signInText === "" ? "OIDC" : authMethods.oidc.signInText + authMethods.oidc.signInText === "" ? "OIDC" : authMethods.oidc.signInText; icon = authMethods.oidc.iconUrl === "" ? ( @@ -312,7 +314,7 @@ const LoginType = ({ src={authMethods.oidc.iconUrl} sx={iconStyles} /> - ) + ); } return ( @@ -320,30 +322,30 @@ const LoginType = ({ {icon} {displayName} - ) -} + ); +}; const LastSeen = ({ value, ...boxProps }: { value: string } & BoxProps) => { - const theme: Theme = useTheme() - const t = dayjs(value) - const now = dayjs() + const theme: Theme = useTheme(); + const t = dayjs(value); + const now = dayjs(); - let message = t.fromNow() - let color = theme.palette.text.secondary + let message = t.fromNow(); + let color = theme.palette.text.secondary; if (t.isAfter(now.subtract(1, "hour"))) { - color = theme.palette.success.light + color = theme.palette.success.light; // Since the agent reports on a 10m interval, // the last_used_at can be inaccurate when recent. - message = "Now" + message = "Now"; } else if (t.isAfter(now.subtract(3, "day"))) { - color = theme.palette.text.secondary + color = theme.palette.text.secondary; } else if (t.isAfter(now.subtract(1, "month"))) { - color = theme.palette.warning.light + color = theme.palette.warning.light; } else if (t.isAfter(now.subtract(100, "year"))) { - color = theme.palette.error.light + color = theme.palette.error.light; } else { - message = "Never" + message = "Never"; } return ( @@ -355,8 +357,8 @@ const LastSeen = ({ value, ...boxProps }: { value: string } & BoxProps) => { > {message} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ status: { @@ -373,4 +375,4 @@ const useStyles = makeStyles((theme) => ({ backgroundColor: theme.palette.info.dark, borderColor: theme.palette.info.light, }, -})) +})); diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx index 13a3bcb4a5..832a58a434 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx @@ -1,37 +1,37 @@ -import { screen, waitFor } from "@testing-library/react" -import WS from "jest-websocket-mock" -import { renderWithAuth } from "../../testHelpers/renderHelpers" -import { WorkspaceBuildPage } from "./WorkspaceBuildPage" -import { MockWorkspace, MockWorkspaceBuild } from "testHelpers/entities" -import * as API from "api/api" +import { screen, waitFor } from "@testing-library/react"; +import WS from "jest-websocket-mock"; +import { renderWithAuth } from "../../testHelpers/renderHelpers"; +import { WorkspaceBuildPage } from "./WorkspaceBuildPage"; +import { MockWorkspace, MockWorkspaceBuild } from "testHelpers/entities"; +import * as API from "api/api"; afterEach(() => { - WS.clean() -}) + WS.clean(); +}); describe("WorkspaceBuildPage", () => { test("gets the right workspace build", async () => { const getWorkspaceBuildSpy = jest .spyOn(API, "getWorkspaceBuildByNumber") - .mockResolvedValue(MockWorkspaceBuild) + .mockResolvedValue(MockWorkspaceBuild); renderWithAuth(, { route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/builds/${MockWorkspace.latest_build.build_number}`, path: "/:username/:workspace/builds/:buildNumber", - }) + }); await waitFor(() => expect(getWorkspaceBuildSpy).toBeCalledWith( MockWorkspace.owner_name, MockWorkspace.name, MockWorkspaceBuild.build_number, ), - ) - }) + ); + }); test("the mock server seamlessly handles JSON protocols", async () => { - const server = new WS("ws://localhost:1234", { jsonProtocol: true }) - const client = new WebSocket("ws://localhost:1234") + const server = new WS("ws://localhost:1234", { jsonProtocol: true }); + const client = new WebSocket("ws://localhost:1234"); - await server.connected + await server.connected; const log = { id: "70459334-4878-4bda-a546-98eee166c4c6", created_at: "2022-05-19T16:46:02.283Z", @@ -39,21 +39,21 @@ describe("WorkspaceBuildPage", () => { log_level: "info", stage: "Another stage", output: "", - } - client.send(JSON.stringify(log)) - await expect(server).toReceiveMessage(log) - expect(server).toHaveReceivedMessages([log]) + }; + client.send(JSON.stringify(log)); + await expect(server).toReceiveMessage(log); + expect(server).toHaveReceivedMessages([log]); client.onmessage = async () => { renderWithAuth(, { route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/builds/${MockWorkspace.latest_build.build_number}`, path: "/:username/:workspace/builds/:buildNumber", - }) + }); - await screen.findByText(MockWorkspaceBuild.workspace_name) - await screen.findByText(log.stage) - } + await screen.findByText(MockWorkspaceBuild.workspace_name); + await screen.findByText(log.stage); + }; - server.close() - }) -}) + server.close(); + }); +}); diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx index 9f48e92846..7e06018c47 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx @@ -1,41 +1,41 @@ -import { useMachine } from "@xstate/react" -import { FC, useEffect } from "react" -import { Helmet } from "react-helmet-async" -import { useParams } from "react-router-dom" -import { pageTitle } from "../../utils/page" -import { workspaceBuildMachine } from "../../xServices/workspaceBuild/workspaceBuildXService" -import { WorkspaceBuildPageView } from "./WorkspaceBuildPageView" -import { useQuery } from "@tanstack/react-query" -import { getWorkspaceBuilds } from "api/api" -import dayjs from "dayjs" +import { useMachine } from "@xstate/react"; +import { FC, useEffect } from "react"; +import { Helmet } from "react-helmet-async"; +import { useParams } from "react-router-dom"; +import { pageTitle } from "../../utils/page"; +import { workspaceBuildMachine } from "../../xServices/workspaceBuild/workspaceBuildXService"; +import { WorkspaceBuildPageView } from "./WorkspaceBuildPageView"; +import { useQuery } from "@tanstack/react-query"; +import { getWorkspaceBuilds } from "api/api"; +import dayjs from "dayjs"; export const WorkspaceBuildPage: FC = () => { const params = useParams() as { - username: string - workspace: string - buildNumber: string - } - const workspaceName = params.workspace - const buildNumber = Number(params.buildNumber) - const username = params.username.replace("@", "") + username: string; + workspace: string; + buildNumber: string; + }; + const workspaceName = params.workspace; + const buildNumber = Number(params.buildNumber); + const username = params.username.replace("@", ""); const [buildState, send] = useMachine(workspaceBuildMachine, { context: { username, workspaceName, buildNumber, timeCursor: new Date() }, - }) - const { logs, build } = buildState.context + }); + const { logs, build } = buildState.context; const { data: builds } = useQuery({ queryKey: ["builds", username, build?.workspace_id], queryFn: () => { return getWorkspaceBuilds( build?.workspace_id ?? "", dayjs().add(-30, "day").toDate(), - ) + ); }, enabled: Boolean(build), - }) + }); useEffect(() => { - send("RESET", { buildNumber, timeCursor: new Date() }) - }, [buildNumber, send]) + send("RESET", { buildNumber, timeCursor: new Date() }); + }, [buildNumber, send]); return ( <> @@ -56,7 +56,7 @@ export const WorkspaceBuildPage: FC = () => { activeBuildNumber={buildNumber} /> - ) -} + ); +}; -export default WorkspaceBuildPage +export default WorkspaceBuildPage; diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.stories.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.stories.tsx index 918ebf690c..49ddbef520 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.stories.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.stories.tsx @@ -1,16 +1,16 @@ -import { Meta, StoryObj } from "@storybook/react" +import { Meta, StoryObj } from "@storybook/react"; import { MockFailedWorkspaceBuild, MockWorkspaceBuild, MockWorkspaceBuildLogs, -} from "../../testHelpers/entities" -import { WorkspaceBuildPageView } from "./WorkspaceBuildPageView" +} from "../../testHelpers/entities"; +import { WorkspaceBuildPageView } from "./WorkspaceBuildPageView"; const defaultBuilds = Array.from({ length: 15 }, (_, i) => ({ ...MockWorkspaceBuild, id: `${i}`, build_number: i, -})) +})); const meta: Meta = { title: "pages/WorkspaceBuildPageView", @@ -21,23 +21,23 @@ const meta: Meta = { builds: defaultBuilds, activeBuildNumber: defaultBuilds[0].build_number, }, -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; -export const Loaded: Story = {} +export const Loaded: Story = {}; export const LoadingBuildLogs: Story = { args: { builds: undefined, }, -} +}; const failedBuild = { ...MockFailedWorkspaceBuild("delete"), build_number: 123123123123, -} +}; export const FailedDelete: Story = { args: { @@ -45,4 +45,4 @@ export const FailedDelete: Story = { builds: [failedBuild, ...defaultBuilds], activeBuildNumber: failedBuild.build_number, }, -} +}; diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx index 7a2ccf361b..65142c781e 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx @@ -1,45 +1,45 @@ -import { BuildAvatar } from "components/BuildAvatar/BuildAvatar" -import { FC } from "react" -import { ProvisionerJobLog, WorkspaceBuild } from "../../api/typesGenerated" -import { Loader } from "../../components/Loader/Loader" -import { Stack } from "../../components/Stack/Stack" -import { WorkspaceBuildLogs } from "../../components/WorkspaceBuildLogs/WorkspaceBuildLogs" -import { makeStyles } from "@mui/styles" +import { BuildAvatar } from "components/BuildAvatar/BuildAvatar"; +import { FC } from "react"; +import { ProvisionerJobLog, WorkspaceBuild } from "../../api/typesGenerated"; +import { Loader } from "../../components/Loader/Loader"; +import { Stack } from "../../components/Stack/Stack"; +import { WorkspaceBuildLogs } from "../../components/WorkspaceBuildLogs/WorkspaceBuildLogs"; +import { makeStyles } from "@mui/styles"; import { FullWidthPageHeader, PageHeaderTitle, PageHeaderSubtitle, -} from "components/PageHeader/FullWidthPageHeader" -import { Link } from "react-router-dom" -import { Stats, StatsItem } from "components/Stats/Stats" +} from "components/PageHeader/FullWidthPageHeader"; +import { Link } from "react-router-dom"; +import { Stats, StatsItem } from "components/Stats/Stats"; import { displayWorkspaceBuildDuration, getDisplayWorkspaceBuildInitiatedBy, getDisplayWorkspaceBuildStatus, -} from "utils/workspace" -import Box from "@mui/material/Box" +} from "utils/workspace"; +import Box from "@mui/material/Box"; import { Sidebar, SidebarCaption, SidebarItem, -} from "components/Sidebar/Sidebar" -import { BuildIcon } from "components/BuildIcon/BuildIcon" -import Skeleton from "@mui/material/Skeleton" -import { Alert } from "components/Alert/Alert" -import { DashboardFullPage } from "components/Dashboard/DashboardLayout" +} from "components/Sidebar/Sidebar"; +import { BuildIcon } from "components/BuildIcon/BuildIcon"; +import Skeleton from "@mui/material/Skeleton"; +import { Alert } from "components/Alert/Alert"; +import { DashboardFullPage } from "components/Dashboard/DashboardLayout"; const sortLogsByCreatedAt = (logs: ProvisionerJobLog[]) => { return [...logs].sort( (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime(), - ) -} + ); +}; export interface WorkspaceBuildPageViewProps { - logs: ProvisionerJobLog[] | undefined - build: WorkspaceBuild | undefined - builds: WorkspaceBuild[] | undefined - activeBuildNumber: number + logs: ProvisionerJobLog[] | undefined; + build: WorkspaceBuild | undefined; + builds: WorkspaceBuild[] | undefined; + activeBuildNumber: number; } export const WorkspaceBuildPageView: FC = ({ @@ -48,10 +48,10 @@ export const WorkspaceBuildPageView: FC = ({ builds, activeBuildNumber, }) => { - const styles = useStyles() + const styles = useStyles(); if (!build) { - return + return ; } return ( @@ -170,15 +170,15 @@ export const WorkspaceBuildPageView: FC = ({ - ) -} + ); +}; const BuildSidebarItem = ({ build, active, }: { - build: WorkspaceBuild - active: boolean + build: WorkspaceBuild; + active: boolean; }) => { return ( - ) -} + ); +}; const BuildSidebarItemSkeleton = () => { return ( @@ -237,8 +237,8 @@ const BuildSidebarItemSkeleton = () => { - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ stats: { @@ -266,4 +266,4 @@ const useStyles = makeStyles((theme) => ({ fontWeight: 500, }, }, -})) +})); diff --git a/site/src/pages/WorkspacePage/BuildRow.tsx b/site/src/pages/WorkspacePage/BuildRow.tsx index bfb0f8157c..c9ae962316 100644 --- a/site/src/pages/WorkspacePage/BuildRow.tsx +++ b/site/src/pages/WorkspacePage/BuildRow.tsx @@ -1,30 +1,30 @@ -import { makeStyles } from "@mui/styles" -import TableCell from "@mui/material/TableCell" -import { WorkspaceBuild } from "api/typesGenerated" -import { Stack } from "components/Stack/Stack" -import { TimelineEntry } from "components/Timeline/TimelineEntry" -import { useClickable } from "hooks/useClickable" -import { useTranslation } from "react-i18next" -import { useNavigate } from "react-router-dom" -import { MONOSPACE_FONT_FAMILY } from "theme/constants" +import { makeStyles } from "@mui/styles"; +import TableCell from "@mui/material/TableCell"; +import { WorkspaceBuild } from "api/typesGenerated"; +import { Stack } from "components/Stack/Stack"; +import { TimelineEntry } from "components/Timeline/TimelineEntry"; +import { useClickable } from "hooks/useClickable"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { MONOSPACE_FONT_FAMILY } from "theme/constants"; import { displayWorkspaceBuildDuration, getDisplayWorkspaceBuildInitiatedBy, -} from "utils/workspace" -import { BuildAvatar } from "components/BuildAvatar/BuildAvatar" +} from "utils/workspace"; +import { BuildAvatar } from "components/BuildAvatar/BuildAvatar"; export interface BuildRowProps { - build: WorkspaceBuild + build: WorkspaceBuild; } export const BuildRow: React.FC = ({ build }) => { - const styles = useStyles() - const { t } = useTranslation("workspacePage") - const initiatedBy = getDisplayWorkspaceBuildInitiatedBy(build) - const navigate = useNavigate() + const styles = useStyles(); + const { t } = useTranslation("workspacePage"); + const initiatedBy = getDisplayWorkspaceBuildInitiatedBy(build); + const navigate = useNavigate(); const clickableProps = useClickable(() => navigate(`builds/${build.build_number}`), - ) + ); return ( @@ -86,8 +86,8 @@ export const BuildRow: React.FC = ({ build }) => { - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ buildWrapper: { @@ -132,4 +132,4 @@ const useStyles = makeStyles((theme) => ({ fullWidth: { width: "100%", }, -})) +})); diff --git a/site/src/pages/WorkspacePage/BuildsTable.stories.tsx b/site/src/pages/WorkspacePage/BuildsTable.stories.tsx index bcef5eb8eb..01f81f8a9a 100644 --- a/site/src/pages/WorkspacePage/BuildsTable.stories.tsx +++ b/site/src/pages/WorkspacePage/BuildsTable.stories.tsx @@ -1,20 +1,20 @@ -import { ComponentMeta, Story } from "@storybook/react" -import { MockBuilds } from "testHelpers/entities" -import { BuildsTable, BuildsTableProps } from "./BuildsTable" +import { ComponentMeta, Story } from "@storybook/react"; +import { MockBuilds } from "testHelpers/entities"; +import { BuildsTable, BuildsTableProps } from "./BuildsTable"; export default { title: "components/BuildsTable", component: BuildsTable, -} as ComponentMeta +} as ComponentMeta; -const Template: Story = (args) => +const Template: Story = (args) => ; -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { builds: MockBuilds, -} +}; -export const Empty = Template.bind({}) +export const Empty = Template.bind({}); Empty.args = { builds: [], -} +}; diff --git a/site/src/pages/WorkspacePage/BuildsTable.tsx b/site/src/pages/WorkspacePage/BuildsTable.tsx index 48457c9234..d74a971c64 100644 --- a/site/src/pages/WorkspacePage/BuildsTable.tsx +++ b/site/src/pages/WorkspacePage/BuildsTable.tsx @@ -1,22 +1,22 @@ -import Box from "@mui/material/Box" -import Table from "@mui/material/Table" -import TableBody from "@mui/material/TableBody" -import TableCell from "@mui/material/TableCell" -import TableContainer from "@mui/material/TableContainer" -import TableRow from "@mui/material/TableRow" -import { Timeline } from "components/Timeline/Timeline" -import { FC } from "react" -import * as TypesGen from "api/typesGenerated" -import { EmptyState } from "components/EmptyState/EmptyState" -import { TableLoader } from "components/TableLoader/TableLoader" -import { BuildRow } from "./BuildRow" +import Box from "@mui/material/Box"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableRow from "@mui/material/TableRow"; +import { Timeline } from "components/Timeline/Timeline"; +import { FC } from "react"; +import * as TypesGen from "api/typesGenerated"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { TableLoader } from "components/TableLoader/TableLoader"; +import { BuildRow } from "./BuildRow"; export const Language = { emptyMessage: "No builds found", -} +}; export interface BuildsTableProps { - builds?: TypesGen.WorkspaceBuild[] + builds?: TypesGen.WorkspaceBuild[]; } export const BuildsTable: FC> = ({ @@ -48,5 +48,5 @@ export const BuildsTable: FC> = ({ - ) -} + ); +}; diff --git a/site/src/pages/WorkspacePage/ChangeVersionDialog.tsx b/site/src/pages/WorkspacePage/ChangeVersionDialog.tsx index d4d170fe4e..d3365b7a8e 100644 --- a/site/src/pages/WorkspacePage/ChangeVersionDialog.tsx +++ b/site/src/pages/WorkspacePage/ChangeVersionDialog.tsx @@ -1,26 +1,26 @@ -import { DialogProps } from "components/Dialogs/Dialog" -import { FC, useRef, useState } from "react" -import { FormFields } from "components/Form/Form" -import TextField from "@mui/material/TextField" -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" -import { Stack } from "components/Stack/Stack" -import { Template, TemplateVersion } from "api/typesGenerated" -import { Loader } from "components/Loader/Loader" -import Autocomplete from "@mui/material/Autocomplete" -import { createDayString } from "utils/createDayString" -import { AvatarData } from "components/AvatarData/AvatarData" -import { Pill } from "components/Pill/Pill" -import { Avatar } from "components/Avatar/Avatar" -import CircularProgress from "@mui/material/CircularProgress" -import Box from "@mui/material/Box" +import { DialogProps } from "components/Dialogs/Dialog"; +import { FC, useRef, useState } from "react"; +import { FormFields } from "components/Form/Form"; +import TextField from "@mui/material/TextField"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { Stack } from "components/Stack/Stack"; +import { Template, TemplateVersion } from "api/typesGenerated"; +import { Loader } from "components/Loader/Loader"; +import Autocomplete from "@mui/material/Autocomplete"; +import { createDayString } from "utils/createDayString"; +import { AvatarData } from "components/AvatarData/AvatarData"; +import { Pill } from "components/Pill/Pill"; +import { Avatar } from "components/Avatar/Avatar"; +import CircularProgress from "@mui/material/CircularProgress"; +import Box from "@mui/material/Box"; export type ChangeVersionDialogProps = DialogProps & { - template: Template | undefined - templateVersions: TemplateVersion[] | undefined - defaultTemplateVersion: TemplateVersion | undefined - onClose: () => void - onConfirm: (templateVersion: TemplateVersion) => void -} + template: Template | undefined; + templateVersions: TemplateVersion[] | undefined; + defaultTemplateVersion: TemplateVersion | undefined; + onClose: () => void; + onConfirm: (templateVersion: TemplateVersion) => void; +}; export const ChangeVersionDialog: FC = ({ onConfirm, @@ -30,8 +30,8 @@ export const ChangeVersionDialog: FC = ({ defaultTemplateVersion, ...dialogProps }) => { - const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false) - const selectedTemplateVersion = useRef() + const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false); + const selectedTemplateVersion = useRef(); return ( = ({ onClose={onClose} onConfirm={() => { if (selectedTemplateVersion.current) { - onConfirm(selectedTemplateVersion.current) + onConfirm(selectedTemplateVersion.current); } }} hideCancel={false} @@ -60,13 +60,13 @@ export const ChangeVersionDialog: FC = ({ open={isAutocompleteOpen} onChange={(_, newTemplateVersion) => { selectedTemplateVersion.current = - newTemplateVersion ?? undefined + newTemplateVersion ?? undefined; }} onOpen={() => { - setIsAutocompleteOpen(true) + setIsAutocompleteOpen(true); }} onClose={() => { - setIsAutocompleteOpen(false) + setIsAutocompleteOpen(false); }} isOptionEqualToValue={( option: TemplateVersion, @@ -127,5 +127,5 @@ export const ChangeVersionDialog: FC = ({ } /> - ) -} + ); +}; diff --git a/site/src/pages/WorkspacePage/UpdateBuildParametersDialog.tsx b/site/src/pages/WorkspacePage/UpdateBuildParametersDialog.tsx index f71bcc94bf..0240819285 100644 --- a/site/src/pages/WorkspacePage/UpdateBuildParametersDialog.tsx +++ b/site/src/pages/WorkspacePage/UpdateBuildParametersDialog.tsx @@ -1,37 +1,37 @@ -import { makeStyles } from "@mui/styles" -import Dialog from "@mui/material/Dialog" -import DialogContent from "@mui/material/DialogContent" -import DialogContentText from "@mui/material/DialogContentText" -import DialogTitle from "@mui/material/DialogTitle" -import { DialogProps } from "components/Dialogs/Dialog" -import { FC } from "react" -import { getFormHelpers } from "utils/formUtils" -import { FormFields, VerticalForm } from "components/Form/Form" +import { makeStyles } from "@mui/styles"; +import Dialog from "@mui/material/Dialog"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogTitle from "@mui/material/DialogTitle"; +import { DialogProps } from "components/Dialogs/Dialog"; +import { FC } from "react"; +import { getFormHelpers } from "utils/formUtils"; +import { FormFields, VerticalForm } from "components/Form/Form"; import { TemplateVersionParameter, WorkspaceBuildParameter, -} from "api/typesGenerated" -import { RichParameterInput } from "components/RichParameterInput/RichParameterInput" -import { useFormik } from "formik" +} from "api/typesGenerated"; +import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"; +import { useFormik } from "formik"; import { getInitialRichParameterValues, useValidationSchemaForRichParameters, -} from "utils/richParameters" -import * as Yup from "yup" -import DialogActions from "@mui/material/DialogActions" -import Button from "@mui/material/Button" -import { useTranslation } from "react-i18next" +} from "utils/richParameters"; +import * as Yup from "yup"; +import DialogActions from "@mui/material/DialogActions"; +import Button from "@mui/material/Button"; +import { useTranslation } from "react-i18next"; export type UpdateBuildParametersDialogProps = DialogProps & { - onClose: () => void - onUpdate: (buildParameters: WorkspaceBuildParameter[]) => void - missedParameters: TemplateVersionParameter[] -} + onClose: () => void; + onUpdate: (buildParameters: WorkspaceBuildParameter[]) => void; + missedParameters: TemplateVersionParameter[]; +}; export const UpdateBuildParametersDialog: FC< UpdateBuildParametersDialogProps > = ({ missedParameters, onUpdate, ...dialogProps }) => { - const styles = useStyles() + const styles = useStyles(); const form = useFormik({ initialValues: { rich_parameter_values: getInitialRichParameterValues(missedParameters), @@ -43,12 +43,12 @@ export const UpdateBuildParametersDialog: FC< ), }), onSubmit: (values) => { - onUpdate(values.rich_parameter_values) + onUpdate(values.rich_parameter_values); }, enableReinitialize: true, - }) - const getFieldHelpers = getFormHelpers(form) - const { t } = useTranslation("workspacePage") + }); + const getFieldHelpers = getFormHelpers(form); + const { t } = useTranslation("workspacePage"); return ( - ) + ); })} )} @@ -114,8 +114,8 @@ export const UpdateBuildParametersDialog: FC< - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ title: { @@ -161,4 +161,4 @@ const useStyles = makeStyles((theme) => ({ flexDirection: "column", gap: theme.spacing(1), }, -})) +})); diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index 189eb8f879..b5819e6b6d 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -1,21 +1,21 @@ -import { action } from "@storybook/addon-actions" -import { Meta, StoryObj } from "@storybook/react" -import { WatchAgentMetadataContext } from "components/Resources/AgentMetadata" -import { ProvisionerJobLog } from "api/typesGenerated" -import * as Mocks from "testHelpers/entities" -import { Workspace, WorkspaceErrors } from "./Workspace" -import { withReactContext } from "storybook-react-context" -import EventSource from "eventsourcemock" -import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext" -import { DashboardProviderContext } from "components/Dashboard/DashboardProvider" -import { WorkspaceBuildLogsSection } from "pages/WorkspacePage/WorkspaceBuildLogsSection" +import { action } from "@storybook/addon-actions"; +import { Meta, StoryObj } from "@storybook/react"; +import { WatchAgentMetadataContext } from "components/Resources/AgentMetadata"; +import { ProvisionerJobLog } from "api/typesGenerated"; +import * as Mocks from "testHelpers/entities"; +import { Workspace, WorkspaceErrors } from "./Workspace"; +import { withReactContext } from "storybook-react-context"; +import EventSource from "eventsourcemock"; +import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; +import { DashboardProviderContext } from "components/Dashboard/DashboardProvider"; +import { WorkspaceBuildLogsSection } from "pages/WorkspacePage/WorkspaceBuildLogsSection"; const MockedAppearance = { config: Mocks.MockAppearance, preview: false, setPreview: () => null, save: () => null, -} +}; const meta: Meta = { title: "components/Workspace", @@ -38,13 +38,13 @@ const meta: Meta = { isLoading: false, isFetched: true, clearProxy: () => { - return + return; }, setProxy: () => { - return + return; }, refetchProxyLatencies: (): Date => { - return new Date() + return new Date(); }, }} > @@ -56,13 +56,13 @@ const meta: Meta = { Context: WatchAgentMetadataContext, initialState: (_: string): EventSource => { // Need Bruno's help here. - return new EventSource() + return new EventSource(); }, }), ], -} -export default meta -type Story = StoryObj +}; +export default meta; +type Story = StoryObj; export const Running: Story = { args: { @@ -90,42 +90,42 @@ export const Running: Story = { buildInfo: Mocks.MockBuildInfo, template: Mocks.MockTemplate, }, -} +}; export const WithoutUpdateAccess: Story = { args: { ...Running.args, canUpdateWorkspace: false, }, -} +}; export const PendingInQueue: Story = { args: { ...Running.args, workspace: Mocks.MockPendingWorkspace, }, -} +}; export const Starting: Story = { args: { ...Running.args, workspace: Mocks.MockStartingWorkspace, }, -} +}; export const Stopped: Story = { args: { ...Running.args, workspace: Mocks.MockStoppedWorkspace, }, -} +}; export const Stopping: Story = { args: { ...Running.args, workspace: Mocks.MockStoppingWorkspace, }, -} +}; export const Failed: Story = { args: { @@ -137,7 +137,7 @@ export const Failed: Story = { }), }, }, -} +}; export const FailedWithLogs: Story = { args: { @@ -155,7 +155,7 @@ export const FailedWithLogs: Story = { }, buildLogs: , }, -} +}; export const FailedWithRetry: Story = { args: { @@ -174,42 +174,42 @@ export const FailedWithRetry: Story = { canRetryDebugMode: true, buildLogs: , }, -} +}; export const Deleting: Story = { args: { ...Running.args, workspace: Mocks.MockDeletingWorkspace, }, -} +}; export const Deleted: Story = { args: { ...Running.args, workspace: Mocks.MockDeletedWorkspace, }, -} +}; export const Canceling: Story = { args: { ...Running.args, workspace: Mocks.MockCancelingWorkspace, }, -} +}; export const Canceled: Story = { args: { ...Running.args, workspace: Mocks.MockCanceledWorkspace, }, -} +}; export const Outdated: Story = { args: { ...Running.args, workspace: Mocks.MockOutdatedWorkspace, }, -} +}; export const GetBuildsError: Story = { args: { @@ -220,7 +220,7 @@ export const GetBuildsError: Story = { }), }, }, -} +}; export const CancellationError: Story = { args: { @@ -232,7 +232,7 @@ export const CancellationError: Story = { }, buildLogs: , }, -} +}; export const Unhealthy: Story = { args: { @@ -246,7 +246,7 @@ export const Unhealthy: Story = { }, }, }, -} +}; function makeFailedBuildLogs(): ProvisionerJobLog[] { return [ @@ -736,7 +736,7 @@ function makeFailedBuildLogs(): ProvisionerJobLog[] { stage: "Cleaning Up", output: "", }, - ] + ]; } export const UnsupportedWorkspace: Story = { @@ -744,4 +744,4 @@ export const UnsupportedWorkspace: Story = { ...Running.args, templateWarnings: ["UNSUPPORTED_WORKSPACES"], }, -} +}; diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 28ba5e3136..569f8e63b6 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -1,37 +1,37 @@ -import Button from "@mui/material/Button" -import { makeStyles } from "@mui/styles" -import { Avatar } from "components/Avatar/Avatar" -import { AgentRow } from "components/Resources/AgentRow" +import Button from "@mui/material/Button"; +import { makeStyles } from "@mui/styles"; +import { Avatar } from "components/Avatar/Avatar"; +import { AgentRow } from "components/Resources/AgentRow"; import { ActiveTransition, WorkspaceBuildProgress, -} from "./WorkspaceBuildProgress" -import { FC, useEffect, useState } from "react" -import { useTranslation } from "react-i18next" -import { useNavigate } from "react-router-dom" -import * as TypesGen from "api/typesGenerated" -import { Alert, AlertDetail } from "components/Alert/Alert" -import { BuildsTable } from "./BuildsTable" -import { Margins } from "components/Margins/Margins" -import { Resources } from "components/Resources/Resources" -import { Stack } from "components/Stack/Stack" -import { WorkspaceActions } from "pages/WorkspacePage/WorkspaceActions/WorkspaceActions" -import { WorkspaceDeletedBanner } from "./WorkspaceDeletedBanner" -import { WorkspaceStats } from "./WorkspaceStats" +} from "./WorkspaceBuildProgress"; +import { FC, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import * as TypesGen from "api/typesGenerated"; +import { Alert, AlertDetail } from "components/Alert/Alert"; +import { BuildsTable } from "./BuildsTable"; +import { Margins } from "components/Margins/Margins"; +import { Resources } from "components/Resources/Resources"; +import { Stack } from "components/Stack/Stack"; +import { WorkspaceActions } from "pages/WorkspacePage/WorkspaceActions/WorkspaceActions"; +import { WorkspaceDeletedBanner } from "./WorkspaceDeletedBanner"; +import { WorkspaceStats } from "./WorkspaceStats"; import { FullWidthPageHeader, PageHeaderActions, PageHeaderTitle, PageHeaderSubtitle, -} from "components/PageHeader/FullWidthPageHeader" -import { TemplateVersionWarnings } from "components/TemplateVersionWarnings/TemplateVersionWarnings" -import { ErrorAlert } from "components/Alert/ErrorAlert" -import { DormantWorkspaceBanner } from "components/WorkspaceDeletion" -import { useLocalStorage } from "hooks" -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import AlertTitle from "@mui/material/AlertTitle" -import { Maybe } from "components/Conditionals/Maybe" -import dayjs from "dayjs" +} from "components/PageHeader/FullWidthPageHeader"; +import { TemplateVersionWarnings } from "components/TemplateVersionWarnings/TemplateVersionWarnings"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { DormantWorkspaceBanner } from "components/WorkspaceDeletion"; +import { useLocalStorage } from "hooks"; +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import AlertTitle from "@mui/material/AlertTitle"; +import { Maybe } from "components/Conditionals/Maybe"; +import dayjs from "dayjs"; export enum WorkspaceErrors { GET_BUILDS_ERROR = "getBuildsError", @@ -40,38 +40,38 @@ export enum WorkspaceErrors { } export interface WorkspaceProps { scheduleProps: { - onDeadlinePlus: (hours: number) => void - onDeadlineMinus: (hours: number) => void - maxDeadlineIncrease: number - maxDeadlineDecrease: number - } - handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void - handleStop: () => void - handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void - handleDelete: () => void - handleUpdate: () => void - handleCancel: () => void - handleSettings: () => void - handleChangeVersion: () => void - handleDormantActivate: () => void - isUpdating: boolean - isRestarting: boolean - workspace: TypesGen.Workspace - resources?: TypesGen.WorkspaceResource[] - builds?: TypesGen.WorkspaceBuild[] - templateWarnings?: TypesGen.TemplateVersionWarning[] - canUpdateWorkspace: boolean - canRetryDebugMode: boolean - canChangeVersions: boolean - hideSSHButton?: boolean - hideVSCodeDesktopButton?: boolean - workspaceErrors: Partial> - buildInfo?: TypesGen.BuildInfoResponse - sshPrefix?: string - template?: TypesGen.Template - quota_budget?: number - handleBuildRetry: () => void - buildLogs?: React.ReactNode + onDeadlinePlus: (hours: number) => void; + onDeadlineMinus: (hours: number) => void; + maxDeadlineIncrease: number; + maxDeadlineDecrease: number; + }; + handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; + handleStop: () => void; + handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; + handleDelete: () => void; + handleUpdate: () => void; + handleCancel: () => void; + handleSettings: () => void; + handleChangeVersion: () => void; + handleDormantActivate: () => void; + isUpdating: boolean; + isRestarting: boolean; + workspace: TypesGen.Workspace; + resources?: TypesGen.WorkspaceResource[]; + builds?: TypesGen.WorkspaceBuild[]; + templateWarnings?: TypesGen.TemplateVersionWarning[]; + canUpdateWorkspace: boolean; + canRetryDebugMode: boolean; + canChangeVersions: boolean; + hideSSHButton?: boolean; + hideVSCodeDesktopButton?: boolean; + workspaceErrors: Partial>; + buildInfo?: TypesGen.BuildInfoResponse; + sshPrefix?: string; + template?: TypesGen.Template; + quota_budget?: number; + handleBuildRetry: () => void; + buildLogs?: React.ReactNode; } /** @@ -107,18 +107,18 @@ export const Workspace: FC> = ({ templateWarnings, buildLogs, }) => { - const styles = useStyles() - const navigate = useNavigate() - const serverVersion = buildInfo?.version || "" - const { t } = useTranslation("workspacePage") - const { saveLocal, getLocal } = useLocalStorage() + const styles = useStyles(); + const navigate = useNavigate(); + const serverVersion = buildInfo?.version || ""; + const { t } = useTranslation("workspacePage"); + const { saveLocal, getLocal } = useLocalStorage(); const buildError = Boolean(workspaceErrors[WorkspaceErrors.BUILD_ERROR]) && ( - ) + ); const cancellationError = Boolean( workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR], @@ -127,44 +127,44 @@ export const Workspace: FC> = ({ error={workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR]} dismissible /> - ) + ); - let transitionStats: TypesGen.TransitionStats | undefined = undefined + let transitionStats: TypesGen.TransitionStats | undefined = undefined; if (template !== undefined) { - transitionStats = ActiveTransition(template, workspace) + transitionStats = ActiveTransition(template, workspace); } - const [showAlertPendingInQueue, setShowAlertPendingInQueue] = useState(false) - const now = dayjs() + const [showAlertPendingInQueue, setShowAlertPendingInQueue] = useState(false); + const now = dayjs(); useEffect(() => { if ( workspace.latest_build.status !== "pending" || workspace.latest_build.job.queue_size === 0 ) { if (!showAlertPendingInQueue) { - return + return; } const hideTimer = setTimeout(() => { - setShowAlertPendingInQueue(false) - }, 250) + setShowAlertPendingInQueue(false); + }, 250); return () => { - clearTimeout(hideTimer) - } + clearTimeout(hideTimer); + }; } const t = Math.max( 0, 5000 - dayjs().diff(dayjs(workspace.latest_build.created_at)), - ) + ); const showTimer = setTimeout(() => { - setShowAlertPendingInQueue(true) - }, t) + setShowAlertPendingInQueue(true); + }, t); return () => { - clearTimeout(showTimer) - } - }, [workspace, now, showAlertPendingInQueue]) + clearTimeout(showTimer); + }; + }, [workspace, now, showAlertPendingInQueue]); return ( <> @@ -233,7 +233,7 @@ export const Workspace: FC> = ({ variant="text" size="small" onClick={() => { - handleRestart() + handleRestart(); }} > Restart @@ -350,10 +350,10 @@ export const Workspace: FC> = ({ - ) -} + ); +}; -const spacerWidth = 300 +const spacerWidth = 300; export const useStyles = makeStyles((theme) => { return { @@ -406,5 +406,5 @@ export const useStyles = makeStyles((theme) => { alertPendingInQueue: { marginBottom: 12, }, - } -}) + }; +}); diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx index 06431b9bc8..04893665e1 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx @@ -1,48 +1,48 @@ -import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined" -import Box from "@mui/material/Box" -import Button from "@mui/material/Button" -import Popover from "@mui/material/Popover" -import { useQuery } from "@tanstack/react-query" -import { getWorkspaceParameters } from "api/api" +import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Popover from "@mui/material/Popover"; +import { useQuery } from "@tanstack/react-query"; +import { getWorkspaceParameters } from "api/api"; import { TemplateVersionParameter, Workspace, WorkspaceBuildParameter, -} from "api/typesGenerated" -import { FormFields } from "components/Form/Form" -import { Loader } from "components/Loader/Loader" -import { RichParameterInput } from "components/RichParameterInput/RichParameterInput" +} from "api/typesGenerated"; +import { FormFields } from "components/Form/Form"; +import { Loader } from "components/Loader/Loader"; +import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"; import { HelpTooltipLink, HelpTooltipLinksGroup, HelpTooltipText, HelpTooltipTitle, -} from "components/HelpTooltip/HelpTooltip" -import { useFormik } from "formik" -import { useRef, useState } from "react" -import { docs } from "utils/docs" -import { getFormHelpers } from "utils/formUtils" -import { getInitialRichParameterValues } from "utils/richParameters" +} from "components/HelpTooltip/HelpTooltip"; +import { useFormik } from "formik"; +import { useRef, useState } from "react"; +import { docs } from "utils/docs"; +import { getFormHelpers } from "utils/formUtils"; +import { getInitialRichParameterValues } from "utils/richParameters"; export const BuildParametersPopover = ({ workspace, disabled, onSubmit, }: { - workspace: Workspace - disabled?: boolean - onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void + workspace: Workspace; + disabled?: boolean; + onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void; }) => { - const anchorRef = useRef(null) - const [isOpen, setIsOpen] = useState(false) + const anchorRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); const { data: parameters } = useQuery({ queryKey: ["workspace", workspace.id, "parameters"], queryFn: () => getWorkspaceParameters(workspace), enabled: isOpen, - }) + }); const ephemeralParameters = parameters ? parameters.templateVersionRichParameters.filter((p) => p.ephemeral) - : undefined + : undefined; return ( <> @@ -53,7 +53,7 @@ export const BuildParametersPopover = ({ sx={{ px: 0 }} ref={anchorRef} onClick={() => { - setIsOpen(true) + setIsOpen(true); }} > @@ -62,7 +62,7 @@ export const BuildParametersPopover = ({ open={isOpen} anchorEl={anchorRef.current} onClose={() => { - setIsOpen(false) + setIsOpen(false); }} anchorOrigin={{ vertical: "bottom", @@ -99,8 +99,8 @@ export const BuildParametersPopover = ({
{ - onSubmit(buildParameters) - setIsOpen(false) + onSubmit(buildParameters); + setIsOpen(false); }} ephemeralParameters={ephemeralParameters} buildParameters={parameters.buildParameters} @@ -134,17 +134,17 @@ export const BuildParametersPopover = ({ - ) -} + ); +}; const Form = ({ ephemeralParameters, buildParameters, onSubmit, }: { - ephemeralParameters: TemplateVersionParameter[] - buildParameters: WorkspaceBuildParameter[] - onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void + ephemeralParameters: TemplateVersionParameter[]; + buildParameters: WorkspaceBuildParameter[]; + onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void; }) => { const form = useFormik({ initialValues: { @@ -154,10 +154,10 @@ const Form = ({ ), }, onSubmit: (values) => { - onSubmit(values.rich_parameter_values) + onSubmit(values.rich_parameter_values); }, - }) - const getFieldHelpers = getFormHelpers(form) + }); + const getFieldHelpers = getFormHelpers(form); return ( @@ -173,10 +173,10 @@ const Form = ({ await form.setFieldValue(`rich_parameter_values[${index}]`, { name: parameter.name, value: value, - }) + }); }} /> - ) + ); })} @@ -191,5 +191,5 @@ const Form = ({
- ) -} + ); +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx index e2a005929a..9091330a14 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx @@ -1,20 +1,20 @@ -import Button from "@mui/material/Button" -import BlockIcon from "@mui/icons-material/Block" -import CloudQueueIcon from "@mui/icons-material/CloudQueue" -import CropSquareIcon from "@mui/icons-material/CropSquare" -import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline" -import ReplayIcon from "@mui/icons-material/Replay" -import { LoadingButton } from "components/LoadingButton/LoadingButton" -import { FC } from "react" -import BlockOutlined from "@mui/icons-material/BlockOutlined" -import ButtonGroup from "@mui/material/ButtonGroup" -import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated" -import { BuildParametersPopover } from "./BuildParametersPopover" -import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew" +import Button from "@mui/material/Button"; +import BlockIcon from "@mui/icons-material/Block"; +import CloudQueueIcon from "@mui/icons-material/CloudQueue"; +import CropSquareIcon from "@mui/icons-material/CropSquare"; +import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline"; +import ReplayIcon from "@mui/icons-material/Replay"; +import { LoadingButton } from "components/LoadingButton/LoadingButton"; +import { FC } from "react"; +import BlockOutlined from "@mui/icons-material/BlockOutlined"; +import ButtonGroup from "@mui/material/ButtonGroup"; +import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"; +import { BuildParametersPopover } from "./BuildParametersPopover"; +import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew"; interface WorkspaceAction { - loading?: boolean - handleAction: () => void + loading?: boolean; + handleAction: () => void; } export const UpdateButton: FC = ({ @@ -32,8 +32,8 @@ export const UpdateButton: FC = ({ > Update - ) -} + ); +}; export const ActivateButton: FC = ({ handleAction, @@ -49,13 +49,13 @@ export const ActivateButton: FC = ({ > Activate - ) -} + ); +}; export const StartButton: FC< Omit & { - workspace: Workspace - handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void + workspace: Workspace; + handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void; } > = ({ handleAction, workspace, loading }) => { return ( @@ -83,8 +83,8 @@ export const StartButton: FC< onSubmit={handleAction} /> - ) -} + ); +}; export const StopButton: FC = ({ handleAction, loading }) => { return ( @@ -98,13 +98,13 @@ export const StopButton: FC = ({ handleAction, loading }) => { > Stop - ) -} + ); +}; export const RestartButton: FC< Omit & { - workspace: Workspace - handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void + workspace: Workspace; + handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void; } > = ({ handleAction, loading, workspace }) => { return ( @@ -133,19 +133,19 @@ export const RestartButton: FC< onSubmit={handleAction} /> - ) -} + ); +}; export const CancelButton: FC = ({ handleAction }) => { return ( - ) -} + ); +}; interface DisabledProps { - label: string + label: string; } export const DisabledButton: FC = ({ label }) => { @@ -153,11 +153,11 @@ export const DisabledButton: FC = ({ label }) => { - ) -} + ); +}; interface LoadingProps { - label: string + label: string; } export const ActionLoadingButton: FC = ({ label }) => { @@ -169,5 +169,5 @@ export const ActionLoadingButton: FC = ({ label }) => { // This icon can be anything startIcon={} /> - ) -} + ); +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx index 3091bfff6c..a7c0015dd0 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx @@ -1,16 +1,16 @@ -import { action } from "@storybook/addon-actions" -import { Story } from "@storybook/react" -import * as Mocks from "../../../testHelpers/entities" -import { WorkspaceActions, WorkspaceActionsProps } from "./WorkspaceActions" +import { action } from "@storybook/addon-actions"; +import { Story } from "@storybook/react"; +import * as Mocks from "../../../testHelpers/entities"; +import { WorkspaceActions, WorkspaceActionsProps } from "./WorkspaceActions"; export default { title: "components/WorkspaceActions", component: WorkspaceActions, -} +}; const Template: Story = (args) => ( -) +); const defaultArgs = { handleStart: action("start"), @@ -21,71 +21,71 @@ const defaultArgs = { handleCancel: action("cancel"), isOutdated: false, isUpdating: false, -} +}; -export const Starting = Template.bind({}) +export const Starting = Template.bind({}); Starting.args = { ...defaultArgs, workspace: Mocks.MockStartingWorkspace, -} +}; -export const Running = Template.bind({}) +export const Running = Template.bind({}); Running.args = { ...defaultArgs, workspace: Mocks.MockWorkspace, -} +}; -export const Stopping = Template.bind({}) +export const Stopping = Template.bind({}); Stopping.args = { ...defaultArgs, workspace: Mocks.MockStoppingWorkspace, -} +}; -export const Stopped = Template.bind({}) +export const Stopped = Template.bind({}); Stopped.args = { ...defaultArgs, workspace: Mocks.MockStoppedWorkspace, -} +}; -export const Canceling = Template.bind({}) +export const Canceling = Template.bind({}); Canceling.args = { ...defaultArgs, workspace: Mocks.MockCancelingWorkspace, -} +}; -export const Canceled = Template.bind({}) +export const Canceled = Template.bind({}); Canceled.args = { ...defaultArgs, workspace: Mocks.MockCanceledWorkspace, -} +}; -export const Deleting = Template.bind({}) +export const Deleting = Template.bind({}); Deleting.args = { ...defaultArgs, workspace: Mocks.MockDeletingWorkspace, -} +}; -export const Deleted = Template.bind({}) +export const Deleted = Template.bind({}); Deleted.args = { ...defaultArgs, workspace: Mocks.MockDeletedWorkspace, -} +}; -export const Outdated = Template.bind({}) +export const Outdated = Template.bind({}); Outdated.args = { ...defaultArgs, workspace: Mocks.MockOutdatedWorkspace, -} +}; -export const Failed = Template.bind({}) +export const Failed = Template.bind({}); Failed.args = { ...defaultArgs, workspace: Mocks.MockFailedWorkspace, -} +}; -export const Updating = Template.bind({}) +export const Updating = Template.bind({}); Updating.args = { ...defaultArgs, isUpdating: true, workspace: Mocks.MockOutdatedWorkspace, -} +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index 23c2398372..e04eda6e95 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -1,9 +1,9 @@ -import MenuItem from "@mui/material/MenuItem" -import Menu from "@mui/material/Menu" -import { makeStyles } from "@mui/styles" -import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined" -import { FC, Fragment, ReactNode, useRef, useState } from "react" -import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated" +import MenuItem from "@mui/material/MenuItem"; +import Menu from "@mui/material/Menu"; +import { makeStyles } from "@mui/styles"; +import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined"; +import { FC, Fragment, ReactNode, useRef, useState } from "react"; +import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"; import { ActionLoadingButton, CancelButton, @@ -13,32 +13,32 @@ import { RestartButton, UpdateButton, ActivateButton, -} from "./Buttons" +} from "./Buttons"; import { ButtonMapping, ButtonTypesEnum, actionsByWorkspaceStatus, -} from "./constants" -import SettingsOutlined from "@mui/icons-material/SettingsOutlined" -import HistoryOutlined from "@mui/icons-material/HistoryOutlined" -import DeleteOutlined from "@mui/icons-material/DeleteOutlined" -import IconButton from "@mui/material/IconButton" +} from "./constants"; +import SettingsOutlined from "@mui/icons-material/SettingsOutlined"; +import HistoryOutlined from "@mui/icons-material/HistoryOutlined"; +import DeleteOutlined from "@mui/icons-material/DeleteOutlined"; +import IconButton from "@mui/material/IconButton"; export interface WorkspaceActionsProps { - workspace: Workspace - handleStart: (buildParameters?: WorkspaceBuildParameter[]) => void - handleStop: () => void - handleRestart: (buildParameters?: WorkspaceBuildParameter[]) => void - handleDelete: () => void - handleUpdate: () => void - handleCancel: () => void - handleSettings: () => void - handleChangeVersion: () => void - handleDormantActivate: () => void - isUpdating: boolean - isRestarting: boolean - children?: ReactNode - canChangeVersions: boolean + workspace: Workspace; + handleStart: (buildParameters?: WorkspaceBuildParameter[]) => void; + handleStop: () => void; + handleRestart: (buildParameters?: WorkspaceBuildParameter[]) => void; + handleDelete: () => void; + handleUpdate: () => void; + handleCancel: () => void; + handleSettings: () => void; + handleChangeVersion: () => void; + handleDormantActivate: () => void; + isUpdating: boolean; + isRestarting: boolean; + children?: ReactNode; + canChangeVersions: boolean; } export const WorkspaceActions: FC = ({ @@ -56,15 +56,15 @@ export const WorkspaceActions: FC = ({ isRestarting, canChangeVersions, }) => { - const styles = useStyles() + const styles = useStyles(); const { canCancel, canAcceptJobs, actions: actionsByStatus, - } = actionsByWorkspaceStatus(workspace, workspace.latest_build.status) - const canBeUpdated = workspace.outdated && canAcceptJobs - const menuTriggerRef = useRef(null) - const [isMenuOpen, setIsMenuOpen] = useState(false) + } = actionsByWorkspaceStatus(workspace, workspace.latest_build.status); + const canBeUpdated = workspace.outdated && canAcceptJobs; + const menuTriggerRef = useRef(null); + const [isMenuOpen, setIsMenuOpen] = useState(false); // A mapping of button type to the corresponding React component const buttonMapping: ButtonMapping = { @@ -102,13 +102,13 @@ export const WorkspaceActions: FC = ({ [ButtonTypesEnum.activating]: ( ), - } + }; // Returns a function that will execute the action and close the menu const onMenuItemClick = (actionFn: () => void) => () => { - setIsMenuOpen(false) - actionFn() - } + setIsMenuOpen(false); + actionFn(); + }; return (
@@ -158,8 +158,8 @@ export const WorkspaceActions: FC = ({
- ) -} + ); +}; const useStyles = makeStyles((theme) => ({ actions: { @@ -167,4 +167,4 @@ const useStyles = makeStyles((theme) => ({ alignItems: "center", gap: theme.spacing(1.5), }, -})) +})); diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts b/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts index 5e8b1345ee..d6f2704a18 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts +++ b/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts @@ -1,5 +1,5 @@ -import { Workspace, WorkspaceStatus } from "api/typesGenerated" -import { ReactNode } from "react" +import { Workspace, WorkspaceStatus } from "api/typesGenerated"; +import { ReactNode } from "react"; // the button types we have export enum ButtonTypesEnum { @@ -21,13 +21,13 @@ export enum ButtonTypesEnum { } export type ButtonMapping = { - [key in ButtonTypesEnum]: ReactNode -} + [key in ButtonTypesEnum]: ReactNode; +}; interface WorkspaceAbilities { - actions: ButtonTypesEnum[] - canCancel: boolean - canAcceptJobs: boolean + actions: ButtonTypesEnum[]; + canCancel: boolean; + canAcceptJobs: boolean; } export const actionsByWorkspaceStatus = ( @@ -39,10 +39,10 @@ export const actionsByWorkspaceStatus = ( actions: [ButtonTypesEnum.activate], canCancel: false, canAcceptJobs: false, - } + }; } - return statusToActions[status] -} + return statusToActions[status]; +}; const statusToActions: Record = { starting: { @@ -99,4 +99,4 @@ const statusToActions: Record = { canCancel: false, canAcceptJobs: false, }, -} +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceBuildLogsSection.tsx b/site/src/pages/WorkspacePage/WorkspaceBuildLogsSection.tsx index 795c77bc35..332e7aa076 100644 --- a/site/src/pages/WorkspacePage/WorkspaceBuildLogsSection.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceBuildLogsSection.tsx @@ -1,27 +1,27 @@ -import Box from "@mui/material/Box" -import { ProvisionerJobLog } from "api/typesGenerated" -import { Loader } from "components/Loader/Loader" -import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs" -import { useRef, useEffect } from "react" +import Box from "@mui/material/Box"; +import { ProvisionerJobLog } from "api/typesGenerated"; +import { Loader } from "components/Loader/Loader"; +import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"; +import { useRef, useEffect } from "react"; export const WorkspaceBuildLogsSection = ({ logs, }: { - logs: ProvisionerJobLog[] | undefined + logs: ProvisionerJobLog[] | undefined; }) => { - const scrollRef = useRef(null) + const scrollRef = useRef(null); useEffect(() => { // Auto scrolling makes hard to snapshot test using Chromatic if (process.env.STORYBOOK === "true") { - return + return; } - const scrollEl = scrollRef.current + const scrollEl = scrollRef.current; if (scrollEl) { - scrollEl.scrollTop = scrollEl.scrollHeight + scrollEl.scrollTop = scrollEl.scrollHeight; } - }, [logs]) + }, [logs]); return ( - ) -} + ); +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceBuildProgress.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceBuildProgress.stories.tsx index 7d74acb452..ae1688dd51 100644 --- a/site/src/pages/WorkspacePage/WorkspaceBuildProgress.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceBuildProgress.stories.tsx @@ -1,25 +1,25 @@ -import { ComponentMeta, Story } from "@storybook/react" -import dayjs from "dayjs" +import { ComponentMeta, Story } from "@storybook/react"; +import dayjs from "dayjs"; import { MockStartingWorkspace, MockWorkspaceBuild, MockProvisionerJob, -} from "testHelpers/entities" +} from "testHelpers/entities"; import { WorkspaceBuildProgress, WorkspaceBuildProgressProps, -} from "./WorkspaceBuildProgress" +} from "./WorkspaceBuildProgress"; export default { title: "components/WorkspaceBuildProgress", component: WorkspaceBuildProgress, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) +); -export const Starting = Template.bind({}) +export const Starting = Template.bind({}); Starting.args = { transitionStats: { P50: 10000, @@ -37,11 +37,11 @@ Starting.args = { }, }, }, -} +}; // When the transition stats are returning null, the progress bar should not be // displayed -export const StartingUnknown = Template.bind({}) +export const StartingUnknown = Template.bind({}); StartingUnknown.args = { ...Starting.args, transitionStats: { @@ -54,22 +54,22 @@ StartingUnknown.args = { // @ts-ignore-error P95: null, }, -} +}; -export const StartingPassedEstimate = Template.bind({}) +export const StartingPassedEstimate = Template.bind({}); StartingPassedEstimate.args = { ...Starting.args, transitionStats: { P50: 1000, P95: 1000 }, -} +}; -export const StartingHighVariaton = Template.bind({}) +export const StartingHighVariaton = Template.bind({}); StartingHighVariaton.args = { ...Starting.args, transitionStats: { P50: 10000, P95: 20000 }, -} +}; -export const StartingZeroEstimate = Template.bind({}) +export const StartingZeroEstimate = Template.bind({}); StartingZeroEstimate.args = { ...Starting.args, transitionStats: { P50: 0, P95: 0 }, -} +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx b/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx index 29df203e17..64fdabc67c 100644 --- a/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx @@ -1,13 +1,13 @@ -import LinearProgress from "@mui/material/LinearProgress" -import makeStyles from "@mui/styles/makeStyles" -import { TransitionStats, Template, Workspace } from "api/typesGenerated" -import dayjs, { Dayjs } from "dayjs" -import { FC, useEffect, useState } from "react" -import capitalize from "lodash/capitalize" +import LinearProgress from "@mui/material/LinearProgress"; +import makeStyles from "@mui/styles/makeStyles"; +import { TransitionStats, Template, Workspace } from "api/typesGenerated"; +import dayjs, { Dayjs } from "dayjs"; +import { FC, useEffect, useState } from "react"; +import capitalize from "lodash/capitalize"; -import duration from "dayjs/plugin/duration" +import duration from "dayjs/plugin/duration"; -dayjs.extend(duration) +dayjs.extend(duration); // ActiveTransition gets the build estimate for the workspace, // if it is in a transition state. @@ -15,65 +15,65 @@ export const ActiveTransition = ( template: Template, workspace: Workspace, ): TransitionStats | undefined => { - const status = workspace.latest_build.status + const status = workspace.latest_build.status; switch (status) { case "starting": - return template.build_time_stats.start + return template.build_time_stats.start; case "stopping": - return template.build_time_stats.stop + return template.build_time_stats.stop; case "deleting": - return template.build_time_stats.delete + return template.build_time_stats.delete; default: - return undefined + return undefined; } -} +}; const estimateFinish = ( startedAt: Dayjs, p50: number, p95: number, ): [number | undefined, string] => { - const sinceStart = dayjs().diff(startedAt) + const sinceStart = dayjs().diff(startedAt); const secondsLeft = (est: number) => { const max = Math.max( Math.ceil(dayjs.duration((1 - sinceStart / est) * est).asSeconds()), 0, - ) - return isNaN(max) ? 0 : max - } + ); + return isNaN(max) ? 0 : max; + }; // Under-promise, over-deliver with the 95th percentile estimate. - const highGuess = secondsLeft(p95) + const highGuess = secondsLeft(p95); const anyMomentNow: [number | undefined, string] = [ undefined, "Any moment now...", - ] + ]; - const p50percent = (sinceStart * 100) / p50 + const p50percent = (sinceStart * 100) / p50; if (highGuess <= 0) { - return anyMomentNow + return anyMomentNow; } - return [p50percent, `Up to ${highGuess} seconds remaining...`] -} + return [p50percent, `Up to ${highGuess} seconds remaining...`]; +}; export interface WorkspaceBuildProgressProps { - workspace: Workspace - transitionStats: TransitionStats + workspace: Workspace; + transitionStats: TransitionStats; } export const WorkspaceBuildProgress: FC = ({ workspace, transitionStats: transitionStats, }) => { - const styles = useStyles() - const job = workspace.latest_build.job - const [progressValue, setProgressValue] = useState(0) + const styles = useStyles(); + const job = workspace.latest_build.job; + const [progressValue, setProgressValue] = useState(0); const [progressText, setProgressText] = useState( "Finding ETA...", - ) + ); // By default workspace is updated every second, which can cause visual stutter // when the build estimate is a few seconds. The timer ensures no observable @@ -85,26 +85,26 @@ export const WorkspaceBuildProgress: FC = ({ transitionStats.P50 === undefined || transitionStats.P95 === undefined ) { - setProgressValue(undefined) - setProgressText(undefined) - return + setProgressValue(undefined); + setProgressText(undefined); + return; } const [est, text] = estimateFinish( dayjs(job.started_at), transitionStats.P50, transitionStats.P95, - ) - setProgressValue(est) - setProgressText(text) - } - setTimeout(updateProgress, 5) - }, [progressValue, job, transitionStats]) + ); + setProgressValue(est); + setProgressText(text); + }; + setTimeout(updateProgress, 5); + }, [progressValue, job, transitionStats]); // HACK: the codersdk type generator doesn't support null values, but this // can be null when the template is new. if ((transitionStats.P50 as number | null) === null) { - return <> + return <>; } return (
@@ -134,8 +134,8 @@ export const WorkspaceBuildProgress: FC = ({
- ) -} + ); +}; const useStyles = makeStyles((theme) => ({ stack: { @@ -156,4 +156,4 @@ const useStyles = makeStyles((theme) => ({ fontWeight: 600, color: theme.palette.text.secondary, }, -})) +})); diff --git a/site/src/pages/WorkspacePage/WorkspaceDeletedBanner.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceDeletedBanner.stories.tsx index fde4f1d309..3230bf7ccc 100644 --- a/site/src/pages/WorkspacePage/WorkspaceDeletedBanner.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceDeletedBanner.stories.tsx @@ -1,20 +1,20 @@ -import { action } from "@storybook/addon-actions" -import { Story } from "@storybook/react" +import { action } from "@storybook/addon-actions"; +import { Story } from "@storybook/react"; import { WorkspaceDeletedBanner, WorkspaceDeletedBannerProps, -} from "./WorkspaceDeletedBanner" +} from "./WorkspaceDeletedBanner"; export default { title: "components/WorkspaceDeletedBanner", component: WorkspaceDeletedBanner, -} +}; const Template: Story = (args) => ( -) +); -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { handleClick: action("extend"), -} +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceDeletedBanner.tsx b/site/src/pages/WorkspacePage/WorkspaceDeletedBanner.tsx index 1ab764b741..dcb1a4f261 100644 --- a/site/src/pages/WorkspacePage/WorkspaceDeletedBanner.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceDeletedBanner.tsx @@ -1,26 +1,26 @@ -import Button from "@mui/material/Button" -import { FC } from "react" -import { Alert } from "components/Alert/Alert" -import { useTranslation } from "react-i18next" +import Button from "@mui/material/Button"; +import { FC } from "react"; +import { Alert } from "components/Alert/Alert"; +import { useTranslation } from "react-i18next"; export interface WorkspaceDeletedBannerProps { - handleClick: () => void + handleClick: () => void; } export const WorkspaceDeletedBanner: FC< React.PropsWithChildren > = ({ handleClick }) => { - const { t } = useTranslation("workspacePage") + const { t } = useTranslation("workspacePage"); const NewWorkspaceButton = ( - ) + ); return ( {t("warningsAndErrors.workspaceDeletedWarning")} - ) -} + ); +}; diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 1fb863d0d6..d431746a9c 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -1,8 +1,8 @@ -import { screen, waitFor, within } from "@testing-library/react" -import userEvent from "@testing-library/user-event" -import EventSourceMock from "eventsourcemock" -import i18next from "i18next" -import { rest } from "msw" +import { screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import EventSourceMock from "eventsourcemock"; +import i18next from "i18next"; +import { rest } from "msw"; import { MockTemplate, MockWorkspace, @@ -24,38 +24,38 @@ import { MockUser, MockEntitlementsWithScheduling, MockDeploymentConfig, -} from "testHelpers/entities" -import * as api from "../../api/api" -import { Workspace } from "../../api/typesGenerated" +} from "testHelpers/entities"; +import * as api from "../../api/api"; +import { Workspace } from "../../api/typesGenerated"; import { renderWithAuth, waitForLoaderToBeRemoved, -} from "../../testHelpers/renderHelpers" -import { server } from "../../testHelpers/server" -import { WorkspacePage } from "./WorkspacePage" +} from "../../testHelpers/renderHelpers"; +import { server } from "../../testHelpers/server"; +import { WorkspacePage } from "./WorkspacePage"; -const { t } = i18next +const { t } = i18next; // It renders the workspace page and waits for it be loaded const renderWorkspacePage = async () => { - jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate) - jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([]) + jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate); + jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([]); jest .spyOn(api, "getDeploymentValues") - .mockResolvedValueOnce(MockDeploymentConfig) + .mockResolvedValueOnce(MockDeploymentConfig); jest .spyOn(api, "watchWorkspaceAgentLogs") .mockImplementation((_, options) => { - options.onDone() - return new WebSocket("") - }) + options.onDone(); + return new WebSocket(""); + }); renderWithAuth(, { route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`, path: "/:username/:workspace", - }) + }); - await waitForLoaderToBeRemoved() -} + await waitForLoaderToBeRemoved(); +}; /** * Requests and responses related to workspace status are unrelated, so we can't test in the usual way. @@ -64,204 +64,204 @@ const renderWorkspacePage = async () => { * workspaceStatus was calculated correctly. */ const testButton = async (label: string, actionMock: jest.SpyInstance) => { - const user = userEvent.setup() - await renderWorkspacePage() - const workspaceActions = screen.getByTestId("workspace-actions") - const button = within(workspaceActions).getByRole("button", { name: label }) - await user.click(button) - expect(actionMock).toBeCalled() -} + const user = userEvent.setup(); + await renderWorkspacePage(); + const workspaceActions = screen.getByTestId("workspace-actions"); + const button = within(workspaceActions).getByRole("button", { name: label }); + await user.click(button); + expect(actionMock).toBeCalled(); +}; const testStatus = async (ws: Workspace, label: string) => { server.use( rest.get( `/api/v2/users/:username/workspace/:workspaceName`, (req, res, ctx) => { - return res(ctx.status(200), ctx.json(ws)) + return res(ctx.status(200), ctx.json(ws)); }, ), - ) - await renderWorkspacePage() - const header = screen.getByTestId("header") - const status = within(header).getByRole("status") - expect(status).toHaveTextContent(label) -} + ); + await renderWorkspacePage(); + const header = screen.getByTestId("header"); + const status = within(header).getByRole("status"); + expect(status).toHaveTextContent(label); +}; -let originalEventSource: typeof window.EventSource +let originalEventSource: typeof window.EventSource; beforeAll(() => { - originalEventSource = window.EventSource + originalEventSource = window.EventSource; // mocking out EventSource for SSE - window.EventSource = EventSourceMock -}) + window.EventSource = EventSourceMock; +}); beforeEach(() => { - jest.resetAllMocks() -}) + jest.resetAllMocks(); +}); afterAll(() => { - window.EventSource = originalEventSource -}) + window.EventSource = originalEventSource; +}); describe("WorkspacePage", () => { it("requests a delete job when the user presses Delete and confirms", async () => { - const user = userEvent.setup({ delay: 0 }) + const user = userEvent.setup({ delay: 0 }); const deleteWorkspaceMock = jest .spyOn(api, "deleteWorkspace") - .mockResolvedValueOnce(MockWorkspaceBuild) - await renderWorkspacePage() + .mockResolvedValueOnce(MockWorkspaceBuild); + await renderWorkspacePage(); // open the workspace action popover so we have access to all available ctas - const trigger = screen.getByTestId("workspace-options-button") - await user.click(trigger) - const buttonText = t("actionButton.delete", { ns: "workspacePage" }) + const trigger = screen.getByTestId("workspace-options-button"); + await user.click(trigger); + const buttonText = t("actionButton.delete", { ns: "workspacePage" }); // Click on delete - const button = await screen.findByText(buttonText) - await user.click(button) + const button = await screen.findByText(buttonText); + await user.click(button); // Get dialog and confirm - const dialog = await screen.findByTestId("dialog") + const dialog = await screen.findByTestId("dialog"); const labelText = t("deleteDialog.confirmLabel", { ns: "common", entity: "workspace", - }) - const textField = within(dialog).getByLabelText(labelText) - await user.type(textField, MockWorkspace.name) + }); + const textField = within(dialog).getByLabelText(labelText); + await user.type(textField, MockWorkspace.name); const confirmButton = within(dialog).getByRole("button", { name: "Delete", hidden: false, - }) - await user.click(confirmButton) - expect(deleteWorkspaceMock).toBeCalled() - }) + }); + await user.click(confirmButton); + expect(deleteWorkspaceMock).toBeCalled(); + }); it("requests a start job when the user presses Start", async () => { server.use( rest.get( `/api/v2/users/:userId/workspace/:workspaceName`, (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MockStoppedWorkspace)) + return res(ctx.status(200), ctx.json(MockStoppedWorkspace)); }, ), - ) + ); const startWorkspaceMock = jest .spyOn(api, "startWorkspace") - .mockImplementation(() => Promise.resolve(MockWorkspaceBuild)) + .mockImplementation(() => Promise.resolve(MockWorkspaceBuild)); await testButton( t("actionButton.start", { ns: "workspacePage" }), startWorkspaceMock, - ) - }) + ); + }); it("requests a stop job when the user presses Stop", async () => { const stopWorkspaceMock = jest .spyOn(api, "stopWorkspace") - .mockResolvedValueOnce(MockWorkspaceBuild) + .mockResolvedValueOnce(MockWorkspaceBuild); await testButton( t("actionButton.stop", { ns: "workspacePage" }), stopWorkspaceMock, - ) - }) + ); + }); it("requests a stop when the user presses Restart", async () => { const stopWorkspaceMock = jest .spyOn(api, "stopWorkspace") - .mockResolvedValueOnce(MockWorkspaceBuild) + .mockResolvedValueOnce(MockWorkspaceBuild); // Render - await renderWorkspacePage() + await renderWorkspacePage(); // Actions - const user = userEvent.setup() - await user.click(screen.getByTestId("workspace-restart-button")) - const confirmButton = await screen.findByTestId("confirm-button") - await user.click(confirmButton) + const user = userEvent.setup(); + await user.click(screen.getByTestId("workspace-restart-button")); + const confirmButton = await screen.findByTestId("confirm-button"); + await user.click(confirmButton); // Assertions await waitFor(() => { - expect(stopWorkspaceMock).toBeCalled() - }) - }) + expect(stopWorkspaceMock).toBeCalled(); + }); + }); it("requests a stop without confirmation when the user presses Restart", async () => { const stopWorkspaceMock = jest .spyOn(api, "stopWorkspace") - .mockResolvedValueOnce(MockWorkspaceBuild) + .mockResolvedValueOnce(MockWorkspaceBuild); window.localStorage.setItem( `${MockUser.id}_ignoredWarnings`, JSON.stringify({ restart: new Date().toISOString() }), - ) + ); // Render - await renderWorkspacePage() + await renderWorkspacePage(); // Actions - const user = userEvent.setup() - await user.click(screen.getByTestId("workspace-restart-button")) + const user = userEvent.setup(); + await user.click(screen.getByTestId("workspace-restart-button")); // Assertions await waitFor(() => { - expect(stopWorkspaceMock).toBeCalled() - }) - }) + expect(stopWorkspaceMock).toBeCalled(); + }); + }); it("requests cancellation when the user presses Cancel", async () => { server.use( rest.get( `/api/v2/users/:userId/workspace/:workspaceName`, (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MockStartingWorkspace)) + return res(ctx.status(200), ctx.json(MockStartingWorkspace)); }, ), - ) + ); const cancelWorkspaceMock = jest .spyOn(api, "cancelWorkspaceBuild") - .mockImplementation(() => Promise.resolve({ message: "job canceled" })) + .mockImplementation(() => Promise.resolve({ message: "job canceled" })); - await renderWorkspacePage() + await renderWorkspacePage(); - const workspaceActions = screen.getByTestId("workspace-actions") + const workspaceActions = screen.getByTestId("workspace-actions"); const cancelButton = within(workspaceActions).getByRole("button", { name: "Cancel", - }) + }); - await userEvent.click(cancelButton) + await userEvent.click(cancelButton); - expect(cancelWorkspaceMock).toBeCalled() - }) + expect(cancelWorkspaceMock).toBeCalled(); + }); it("requests an update when the user presses Update", async () => { // Mocks jest .spyOn(api, "getWorkspaceByOwnerAndName") - .mockResolvedValueOnce(MockOutdatedWorkspace) + .mockResolvedValueOnce(MockOutdatedWorkspace); const updateWorkspaceMock = jest .spyOn(api, "updateWorkspace") - .mockResolvedValueOnce(MockWorkspaceBuild) + .mockResolvedValueOnce(MockWorkspaceBuild); // Render - await renderWorkspacePage() + await renderWorkspacePage(); // Actions - const user = userEvent.setup() - await user.click(screen.getByTestId("workspace-update-button")) - const confirmButton = await screen.findByTestId("confirm-button") - await user.click(confirmButton) + const user = userEvent.setup(); + await user.click(screen.getByTestId("workspace-update-button")); + const confirmButton = await screen.findByTestId("confirm-button"); + await user.click(confirmButton); // Assertions await waitFor(() => { - expect(updateWorkspaceMock).toBeCalled() - }) - }) + expect(updateWorkspaceMock).toBeCalled(); + }); + }); it("updates the parameters when they are missing during update", async () => { // Mocks jest .spyOn(api, "getWorkspaceByOwnerAndName") - .mockResolvedValueOnce(MockOutdatedWorkspace) + .mockResolvedValueOnce(MockOutdatedWorkspace); const updateWorkspaceSpy = jest .spyOn(api, "updateWorkspace") .mockRejectedValueOnce( @@ -269,39 +269,39 @@ describe("WorkspacePage", () => { MockTemplateVersionParameter1, MockTemplateVersionParameter2, ]), - ) + ); // Render - await renderWorkspacePage() + await renderWorkspacePage(); // Actions - const user = userEvent.setup() - await user.click(screen.getByTestId("workspace-update-button")) - const confirmButton = await screen.findByTestId("confirm-button") - await user.click(confirmButton) + const user = userEvent.setup(); + await user.click(screen.getByTestId("workspace-update-button")); + const confirmButton = await screen.findByTestId("confirm-button"); + await user.click(confirmButton); // The update was called await waitFor(() => { - expect(api.updateWorkspace).toBeCalled() - updateWorkspaceSpy.mockClear() - }) + expect(api.updateWorkspace).toBeCalled(); + updateWorkspaceSpy.mockClear(); + }); // After trying to update, a new dialog asking for missed parameters should // be displayed and filled - const dialog = await screen.findByTestId("dialog") + const dialog = await screen.findByTestId("dialog"); const firstParameterInput = within(dialog).getByLabelText( MockTemplateVersionParameter1.name, { exact: false }, - ) - await user.clear(firstParameterInput) - await user.type(firstParameterInput, "some-value") + ); + await user.clear(firstParameterInput); + await user.type(firstParameterInput, "some-value"); const secondParameterInput = within(dialog).getByLabelText( MockTemplateVersionParameter2.name, { exact: false }, - ) - await user.clear(secondParameterInput) - await user.type(secondParameterInput, "2") - await user.click(within(dialog).getByRole("button", { name: "Update" })) + ); + await user.clear(secondParameterInput); + await user.type(secondParameterInput, "2"); + await user.click(within(dialog).getByRole("button", { name: "Update" })); // Check if the update was called using the values from the form await waitFor(() => { @@ -314,108 +314,108 @@ describe("WorkspacePage", () => { name: MockTemplateVersionParameter2.name, value: "2", }, - ]) - }) - }) + ]); + }); + }); it("shows the Stopping status when the workspace is stopping", async () => { await testStatus( MockStoppingWorkspace, t("workspaceStatus.stopping", { ns: "common" }), - ) - }) + ); + }); it("shows the Stopped status when the workspace is stopped", async () => { await testStatus( MockStoppedWorkspace, t("workspaceStatus.stopped", { ns: "common" }), - ) - }) + ); + }); it("shows the Building status when the workspace is starting", async () => { await testStatus( MockStartingWorkspace, t("workspaceStatus.starting", { ns: "common" }), - ) - }) + ); + }); it("shows the Running status when the workspace is running", async () => { await testStatus( MockWorkspace, t("workspaceStatus.running", { ns: "common" }), - ) - }) + ); + }); it("shows the Failed status when the workspace is failed or canceled", async () => { await testStatus( MockFailedWorkspace, t("workspaceStatus.failed", { ns: "common" }), - ) - }) + ); + }); it("shows the Canceling status when the workspace is canceling", async () => { await testStatus( MockCancelingWorkspace, t("workspaceStatus.canceling", { ns: "common" }), - ) - }) + ); + }); it("shows the Canceled status when the workspace is canceling", async () => { await testStatus( MockCanceledWorkspace, t("workspaceStatus.canceled", { ns: "common" }), - ) - }) + ); + }); it("shows the Deleting status when the workspace is deleting", async () => { await testStatus( MockDeletingWorkspace, t("workspaceStatus.deleting", { ns: "common" }), - ) - }) + ); + }); it("shows the Deleted status when the workspace is deleted", async () => { await testStatus( MockDeletedWorkspace, t("workspaceStatus.deleted", { ns: "common" }), - ) - }) + ); + }); it("shows the Impending deletion status when the workspace is impending deletion", async () => { jest .spyOn(api, "getEntitlements") - .mockResolvedValue(MockEntitlementsWithScheduling) - await testStatus(MockWorkspaceWithDeletion, "Impending deletion") - }) + .mockResolvedValue(MockEntitlementsWithScheduling); + await testStatus(MockWorkspaceWithDeletion, "Impending deletion"); + }); it("shows the timeline build", async () => { - await renderWorkspacePage() - const table = await screen.findByTestId("builds-table") + await renderWorkspacePage(); + const table = await screen.findByTestId("builds-table"); // Wait for the results to be loaded await waitFor(async () => { - const rows = table.querySelectorAll("tbody > tr") + const rows = table.querySelectorAll("tbody > tr"); // Added +1 because of the date row - expect(rows).toHaveLength(MockBuilds.length + 1) - }) - }) + expect(rows).toHaveLength(MockBuilds.length + 1); + }); + }); it("shows the template warning", async () => { server.use( rest.get( "/api/v2/templateversions/:templateVersionId", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MockTemplateVersion3)) + return res(ctx.status(200), ctx.json(MockTemplateVersion3)); }, ), - ) + ); - await renderWorkspacePage() - await screen.findByTestId("error-unsupported-workspaces") - }) + await renderWorkspacePage(); + await screen.findByTestId("error-unsupported-workspaces"); + }); it("restart the workspace with one time parameters when having the confirmation dialog", async () => { - window.localStorage.removeItem(`${MockUser.id}_ignoredWarnings`) + window.localStorage.removeItem(`${MockUser.id}_ignoredWarnings`); jest.spyOn(api, "getWorkspaceParameters").mockResolvedValue({ templateVersionRichParameters: [ { @@ -427,28 +427,28 @@ describe("WorkspacePage", () => { }, ], buildParameters: [{ name: "rebuild", value: "false" }], - }) - const restartWorkspaceSpy = jest.spyOn(api, "restartWorkspace") - const user = userEvent.setup() - await renderWorkspacePage() - await user.click(screen.getByTestId("build-parameters-button")) + }); + const restartWorkspaceSpy = jest.spyOn(api, "restartWorkspace"); + const user = userEvent.setup(); + await renderWorkspacePage(); + await user.click(screen.getByTestId("build-parameters-button")); const buildParametersForm = await screen.findByTestId( "build-parameters-form", - ) + ); const rebuildField = within(buildParametersForm).getByLabelText("Rebuild", { exact: false, - }) - await user.clear(rebuildField) - await user.type(rebuildField, "true") - await user.click(screen.getByTestId("build-parameters-submit")) - await user.click(screen.getByTestId("confirm-button")) + }); + await user.clear(rebuildField); + await user.type(rebuildField, "true"); + await user.click(screen.getByTestId("build-parameters-submit")); + await user.click(screen.getByTestId("confirm-button")); await waitFor(() => { expect(restartWorkspaceSpy).toBeCalledWith({ workspace: MockWorkspace, buildParameters: [{ name: "rebuild", value: "true" }], - }) - }) - }) + }); + }); + }); it("restart the workspace with one time parameters without the confirmation dialog", async () => { window.localStorage.setItem( @@ -456,7 +456,7 @@ describe("WorkspacePage", () => { JSON.stringify({ restart: new Date().toISOString(), }), - ) + ); jest.spyOn(api, "getWorkspaceParameters").mockResolvedValue({ templateVersionRichParameters: [ { @@ -468,25 +468,25 @@ describe("WorkspacePage", () => { }, ], buildParameters: [{ name: "rebuild", value: "false" }], - }) - const restartWorkspaceSpy = jest.spyOn(api, "restartWorkspace") - const user = userEvent.setup() - await renderWorkspacePage() - await user.click(screen.getByTestId("build-parameters-button")) + }); + const restartWorkspaceSpy = jest.spyOn(api, "restartWorkspace"); + const user = userEvent.setup(); + await renderWorkspacePage(); + await user.click(screen.getByTestId("build-parameters-button")); const buildParametersForm = await screen.findByTestId( "build-parameters-form", - ) + ); const rebuildField = within(buildParametersForm).getByLabelText("Rebuild", { exact: false, - }) - await user.clear(rebuildField) - await user.type(rebuildField, "true") - await user.click(screen.getByTestId("build-parameters-submit")) + }); + await user.clear(rebuildField); + await user.type(rebuildField, "true"); + await user.click(screen.getByTestId("build-parameters-submit")); await waitFor(() => { expect(restartWorkspaceSpy).toBeCalledWith({ workspace: MockWorkspace, buildParameters: [{ name: "rebuild", value: "true" }], - }) - }) - }) -}) + }); + }); + }); +}); diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 43a1944207..c3accb8de1 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -1,36 +1,36 @@ -import { useMachine } from "@xstate/react" -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { Loader } from "components/Loader/Loader" -import { FC } from "react" -import { useParams } from "react-router-dom" -import { quotaMachine } from "xServices/quotas/quotasXService" -import { workspaceMachine } from "xServices/workspace/workspaceXService" -import { WorkspaceReadyPage } from "./WorkspaceReadyPage" -import { RequirePermission } from "components/RequirePermission/RequirePermission" -import { ErrorAlert } from "components/Alert/ErrorAlert" -import { useOrganizationId } from "hooks" -import { isAxiosError } from "axios" -import { Margins } from "components/Margins/Margins" +import { useMachine } from "@xstate/react"; +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { Loader } from "components/Loader/Loader"; +import { FC } from "react"; +import { useParams } from "react-router-dom"; +import { quotaMachine } from "xServices/quotas/quotasXService"; +import { workspaceMachine } from "xServices/workspace/workspaceXService"; +import { WorkspaceReadyPage } from "./WorkspaceReadyPage"; +import { RequirePermission } from "components/RequirePermission/RequirePermission"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { useOrganizationId } from "hooks"; +import { isAxiosError } from "axios"; +import { Margins } from "components/Margins/Margins"; export const WorkspacePage: FC = () => { const params = useParams() as { - username: string - workspace: string - } - const workspaceName = params.workspace - const username = params.username.replace("@", "") - const orgId = useOrganizationId() + username: string; + workspace: string; + }; + const workspaceName = params.workspace; + const username = params.username.replace("@", ""); + const orgId = useOrganizationId(); const [workspaceState, workspaceSend] = useMachine(workspaceMachine, { context: { orgId, workspaceName, username, }, - }) - const { workspace, error } = workspaceState.context - const [quotaState] = useMachine(quotaMachine, { context: { username } }) - const { getQuotaError } = quotaState.context - const pageError = error ?? getQuotaError + }); + const { workspace, error } = workspaceState.context; + const [quotaState] = useMachine(quotaMachine, { context: { username } }); + const { getQuotaError } = quotaState.context; + const pageError = error ?? getQuotaError; return ( { - ) -} + ); +}; -export default WorkspacePage +export default WorkspacePage; diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index fc86bef830..7579e11b17 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -1,46 +1,46 @@ -import { useActor, useMachine } from "@xstate/react" -import { useDashboard } from "components/Dashboard/DashboardProvider" -import dayjs from "dayjs" -import { useFeatureVisibility } from "hooks/useFeatureVisibility" -import { FC, useEffect, useState } from "react" -import { Helmet } from "react-helmet-async" -import { useTranslation } from "react-i18next" -import { useNavigate } from "react-router-dom" +import { useActor, useMachine } from "@xstate/react"; +import { useDashboard } from "components/Dashboard/DashboardProvider"; +import dayjs from "dayjs"; +import { useFeatureVisibility } from "hooks/useFeatureVisibility"; +import { FC, useEffect, useState } from "react"; +import { Helmet } from "react-helmet-async"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; import { getDeadline, getMaxDeadline, getMaxDeadlineChange, getMinDeadline, -} from "utils/schedule" -import { quotaMachine } from "xServices/quotas/quotasXService" -import { StateFrom } from "xstate" -import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog" -import { Workspace, WorkspaceErrors } from "./Workspace" -import { pageTitle } from "utils/page" -import { getFaviconByStatus, hasJobError } from "utils/workspace" +} from "utils/schedule"; +import { quotaMachine } from "xServices/quotas/quotasXService"; +import { StateFrom } from "xstate"; +import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; +import { Workspace, WorkspaceErrors } from "./Workspace"; +import { pageTitle } from "utils/page"; +import { getFaviconByStatus, hasJobError } from "utils/workspace"; import { WorkspaceEvent, workspaceMachine, -} from "xServices/workspace/workspaceXService" -import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog" -import { ChangeVersionDialog } from "./ChangeVersionDialog" -import { useMutation, useQuery } from "@tanstack/react-query" -import { getTemplateVersions, restartWorkspace } from "api/api" +} from "xServices/workspace/workspaceXService"; +import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; +import { ChangeVersionDialog } from "./ChangeVersionDialog"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { getTemplateVersions, restartWorkspace } from "api/api"; import { ConfirmDialog, ConfirmDialogProps, -} from "components/Dialogs/ConfirmDialog/ConfirmDialog" -import { useMe } from "hooks/useMe" -import Checkbox from "@mui/material/Checkbox" -import FormControlLabel from "@mui/material/FormControlLabel" -import { workspaceBuildMachine } from "xServices/workspaceBuild/workspaceBuildXService" -import * as TypesGen from "api/typesGenerated" -import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection" +} from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { useMe } from "hooks/useMe"; +import Checkbox from "@mui/material/Checkbox"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import { workspaceBuildMachine } from "xServices/workspaceBuild/workspaceBuildXService"; +import * as TypesGen from "api/typesGenerated"; +import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection"; interface WorkspaceReadyPageProps { - workspaceState: StateFrom - quotaState: StateFrom - workspaceSend: (event: WorkspaceEvent) => void + workspaceState: StateFrom; + quotaState: StateFrom; + workspaceSend: (event: WorkspaceEvent) => void; } export const WorkspaceReadyPage = ({ @@ -50,9 +50,9 @@ export const WorkspaceReadyPage = ({ }: WorkspaceReadyPageProps): JSX.Element => { const [_, bannerSend] = useActor( workspaceState.children["scheduleBannerMachine"], - ) - const { buildInfo } = useDashboard() - const featureVisibility = useFeatureVisibility() + ); + const { buildInfo } = useDashboard(); + const featureVisibility = useFeatureVisibility(); const { workspace, template, @@ -65,49 +65,49 @@ export const WorkspaceReadyPage = ({ sshPrefix, permissions, missedParameters, - } = workspaceState.context + } = workspaceState.context; if (workspace === undefined) { - throw Error("Workspace is undefined") + throw Error("Workspace is undefined"); } - const deadline = getDeadline(workspace) - const canUpdateWorkspace = Boolean(permissions?.updateWorkspace) - const canUpdateTemplate = Boolean(permissions?.updateTemplate) + const deadline = getDeadline(workspace); + const canUpdateWorkspace = Boolean(permissions?.updateWorkspace); + const canUpdateTemplate = Boolean(permissions?.updateTemplate); const canRetryDebugMode = Boolean(permissions?.viewDeploymentValues) && - Boolean(deploymentValues?.enable_terraform_debug_mode) - const { t } = useTranslation("workspacePage") - const favicon = getFaviconByStatus(workspace.latest_build) - const navigate = useNavigate() - const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false) + Boolean(deploymentValues?.enable_terraform_debug_mode); + const { t } = useTranslation("workspacePage"); + const favicon = getFaviconByStatus(workspace.latest_build); + const navigate = useNavigate(); + const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); const { data: templateVersions } = useQuery({ queryKey: ["template", "versions", workspace.template_id], queryFn: () => getTemplateVersions(workspace.template_id), enabled: changeVersionDialogOpen, - }) - const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false) + }); + const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false); const [confirmingRestart, setConfirmingRestart] = useState<{ - open: boolean - buildParameters?: TypesGen.WorkspaceBuildParameter[] - }>({ open: false }) - const user = useMe() - const { isWarningIgnored, ignoreWarning } = useIgnoreWarnings(user.id) - const buildLogs = useBuildLogs(workspace) + open: boolean; + buildParameters?: TypesGen.WorkspaceBuildParameter[]; + }>({ open: false }); + const user = useMe(); + const { isWarningIgnored, ignoreWarning } = useIgnoreWarnings(user.id); + const buildLogs = useBuildLogs(workspace); const shouldDisplayBuildLogs = hasJobError(workspace) || ["canceling", "deleting", "pending", "starting", "stopping"].includes( workspace.latest_build.status, - ) + ); const { mutate: mutateRestartWorkspace, error: restartBuildError, isLoading: isRestarting, } = useMutation({ mutationFn: restartWorkspace, - }) + }); // keep banner machine in sync with workspace useEffect(() => { - bannerSend({ type: "REFRESH_WORKSPACE", workspace }) - }, [bannerSend, workspace]) + bannerSend({ type: "REFRESH_WORKSPACE", workspace }); + }, [bannerSend, workspace]); return ( <> @@ -130,13 +130,13 @@ export const WorkspaceReadyPage = ({ bannerSend({ type: "DECREASE_DEADLINE", hours, - }) + }); }, onDeadlinePlus: (hours: number) => { bannerSend({ type: "INCREASE_DEADLINE", hours, - }) + }); }, maxDeadlineDecrease: getMaxDeadlineChange(deadline, getMinDeadline()), maxDeadlineIncrease: getMaxDeadlineChange( @@ -154,23 +154,23 @@ export const WorkspaceReadyPage = ({ handleDelete={() => workspaceSend({ type: "ASK_DELETE" })} handleRestart={(buildParameters) => { if (isWarningIgnored("restart")) { - mutateRestartWorkspace({ workspace, buildParameters }) + mutateRestartWorkspace({ workspace, buildParameters }); } else { - setConfirmingRestart({ open: true, buildParameters }) + setConfirmingRestart({ open: true, buildParameters }); } }} handleUpdate={() => { if (isWarningIgnored("update")) { - workspaceSend({ type: "UPDATE" }) + workspaceSend({ type: "UPDATE" }); } else { - setIsConfirmingUpdate(true) + setIsConfirmingUpdate(true); } }} handleCancel={() => workspaceSend({ type: "CANCEL" })} handleSettings={() => navigate("settings")} handleBuildRetry={() => workspaceSend({ type: "RETRY_BUILD" })} handleChangeVersion={() => { - setChangeVersionDialogOpen(true) + setChangeVersionDialogOpen(true); }} handleDormantActivate={() => workspaceSend({ type: "ACTIVATE" })} resources={workspace.latest_build.resources} @@ -205,7 +205,7 @@ export const WorkspaceReadyPage = ({ isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })} onCancel={() => workspaceSend({ type: "CANCEL_DELETE" })} onConfirm={() => { - workspaceSend({ type: "DELETE" }) + workspaceSend({ type: "DELETE" }); }} /> { - workspaceSend({ type: "CANCEL" }) + workspaceSend({ type: "CANCEL" }); }} onUpdate={(buildParameters) => { - workspaceSend({ type: "UPDATE", buildParameters }) + workspaceSend({ type: "UPDATE", buildParameters }); }} /> { - setChangeVersionDialogOpen(false) + setChangeVersionDialogOpen(false); }} onConfirm={(templateVersion) => { - setChangeVersionDialogOpen(false) + setChangeVersionDialogOpen(false); workspaceSend({ type: "CHANGE_VERSION", templateVersionId: templateVersion.id, - }) + }); }} /> { if (shouldIgnore) { - ignoreWarning("update") + ignoreWarning("update"); } - workspaceSend({ type: "UPDATE" }) - setIsConfirmingUpdate(false) + workspaceSend({ type: "UPDATE" }); + setIsConfirmingUpdate(false); }} onClose={() => setIsConfirmingUpdate(false)} title="Confirm update" @@ -257,13 +257,13 @@ export const WorkspaceReadyPage = ({ open={confirmingRestart.open} onConfirm={(shouldIgnore) => { if (shouldIgnore) { - ignoreWarning("restart") + ignoreWarning("restart"); } mutateRestartWorkspace({ workspace, buildParameters: confirmingRestart.buildParameters, - }) - setConfirmingRestart({ open: false }) + }); + setConfirmingRestart({ open: false }); }} onClose={() => setConfirmingRestart({ open: false })} title="Confirm restart" @@ -271,38 +271,38 @@ export const WorkspaceReadyPage = ({ description="Are you sure you want to restart your workspace? Updating your workspace will stop all running processes and delete non-persistent data." /> - ) -} + ); +}; -type IgnoredWarnings = Record +type IgnoredWarnings = Record; const useIgnoreWarnings = (prefix: string) => { - const ignoredWarningsJSON = localStorage.getItem(`${prefix}_ignoredWarnings`) - let ignoredWarnings: IgnoredWarnings | undefined + const ignoredWarningsJSON = localStorage.getItem(`${prefix}_ignoredWarnings`); + let ignoredWarnings: IgnoredWarnings | undefined; if (ignoredWarningsJSON) { - ignoredWarnings = JSON.parse(ignoredWarningsJSON) + ignoredWarnings = JSON.parse(ignoredWarningsJSON); } const isWarningIgnored = (warningId: string) => { - return Boolean(ignoredWarnings?.[warningId]) - } + return Boolean(ignoredWarnings?.[warningId]); + }; const ignoreWarning = (warningId: string) => { if (!ignoredWarnings) { - ignoredWarnings = {} + ignoredWarnings = {}; } - ignoredWarnings[warningId] = new Date().toISOString() + ignoredWarnings[warningId] = new Date().toISOString(); localStorage.setItem( `${prefix}_ignoredWarnings`, JSON.stringify(ignoredWarnings), - ) - } + ); + }; return { isWarningIgnored, ignoreWarning, - } -} + }; +}; const WarningDialog: FC< Pick< @@ -310,7 +310,7 @@ const WarningDialog: FC< "open" | "onClose" | "title" | "confirmText" | "description" > & { onConfirm: (shouldIgnore: boolean) => void } > = ({ open, onConfirm, onClose, title, confirmText, description }) => { - const [shouldIgnore, setShouldIgnore] = useState(false) + const [shouldIgnore, setShouldIgnore] = useState(false); return ( { - onConfirm(shouldIgnore) + onConfirm(shouldIgnore); }} onClose={onClose} title={title} @@ -334,7 +334,7 @@ const WarningDialog: FC< { - setShouldIgnore(e.target.checked) + setShouldIgnore(e.target.checked); }} /> } @@ -343,11 +343,11 @@ const WarningDialog: FC< } /> - ) -} + ); +}; const useBuildLogs = (workspace: TypesGen.Workspace) => { - const buildNumber = workspace.latest_build.build_number + const buildNumber = workspace.latest_build.build_number; const [buildState, buildSend] = useMachine(workspaceBuildMachine, { context: { buildNumber, @@ -355,12 +355,12 @@ const useBuildLogs = (workspace: TypesGen.Workspace) => { workspaceName: workspace.name, timeCursor: new Date(), }, - }) - const { logs } = buildState.context + }); + const { logs } = buildState.context; useEffect(() => { - buildSend({ type: "RESET", buildNumber, timeCursor: new Date() }) - }, [buildNumber, buildSend]) + buildSend({ type: "RESET", buildNumber, timeCursor: new Date() }); + }, [buildNumber, buildSend]); - return logs -} + return logs; +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceStats.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceStats.stories.tsx index b13a7787cd..714854d71c 100644 --- a/site/src/pages/WorkspacePage/WorkspaceStats.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceStats.stories.tsx @@ -1,25 +1,25 @@ -import { Story } from "@storybook/react" +import { Story } from "@storybook/react"; import { MockWorkspace, MockAppearance, MockBuildInfo, MockEntitlementsWithScheduling, MockExperiments, -} from "testHelpers/entities" -import { WorkspaceStats, WorkspaceStatsProps } from "./WorkspaceStats" -import { DashboardProviderContext } from "components/Dashboard/DashboardProvider" +} from "testHelpers/entities"; +import { WorkspaceStats, WorkspaceStatsProps } from "./WorkspaceStats"; +import { DashboardProviderContext } from "components/Dashboard/DashboardProvider"; export default { title: "components/WorkspaceStats", component: WorkspaceStats, -} +}; const MockedAppearance = { config: MockAppearance, preview: false, setPreview: () => null, save: () => null, -} +}; const Template: Story = (args) => ( = (args) => ( > -) +); -export const Example = Template.bind({}) +export const Example = Template.bind({}); Example.args = { workspace: MockWorkspace, -} +}; -export const Outdated = Template.bind({}) +export const Outdated = Template.bind({}); Outdated.args = { workspace: { ...MockWorkspace, outdated: true, }, -} +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceStats.tsx b/site/src/pages/WorkspacePage/WorkspaceStats.tsx index d75dde2695..a9995aed15 100644 --- a/site/src/pages/WorkspacePage/WorkspaceStats.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceStats.tsx @@ -1,26 +1,26 @@ -import Link from "@mui/material/Link" -import { WorkspaceOutdatedTooltip } from "components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip" -import { FC, useRef, useState } from "react" -import { Link as RouterLink } from "react-router-dom" -import { createDayString } from "utils/createDayString" +import Link from "@mui/material/Link"; +import { WorkspaceOutdatedTooltip } from "components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; +import { FC, useRef, useState } from "react"; +import { Link as RouterLink } from "react-router-dom"; +import { createDayString } from "utils/createDayString"; import { getDisplayWorkspaceBuildInitiatedBy, getDisplayWorkspaceTemplateName, isWorkspaceOn, -} from "utils/workspace" -import { Workspace } from "api/typesGenerated" -import { Stats, StatsItem } from "components/Stats/Stats" -import upperFirst from "lodash/upperFirst" -import { autostartDisplay, autostopDisplay } from "utils/schedule" -import IconButton from "@mui/material/IconButton" -import RemoveIcon from "@mui/icons-material/RemoveOutlined" -import { makeStyles } from "@mui/styles" -import AddIcon from "@mui/icons-material/AddOutlined" -import Popover from "@mui/material/Popover" -import TextField from "@mui/material/TextField" -import Button from "@mui/material/Button" -import { WorkspaceStatusText } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge" -import { ImpendingDeletionStat } from "components/WorkspaceDeletion" +} from "utils/workspace"; +import { Workspace } from "api/typesGenerated"; +import { Stats, StatsItem } from "components/Stats/Stats"; +import upperFirst from "lodash/upperFirst"; +import { autostartDisplay, autostopDisplay } from "utils/schedule"; +import IconButton from "@mui/material/IconButton"; +import RemoveIcon from "@mui/icons-material/RemoveOutlined"; +import { makeStyles } from "@mui/styles"; +import AddIcon from "@mui/icons-material/AddOutlined"; +import Popover from "@mui/material/Popover"; +import TextField from "@mui/material/TextField"; +import Button from "@mui/material/Button"; +import { WorkspaceStatusText } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"; +import { ImpendingDeletionStat } from "components/WorkspaceDeletion"; const Language = { workspaceDetails: "Workspace Details", @@ -32,17 +32,17 @@ const Language = { upToDate: "Up to date", byLabel: "Last built by", costLabel: "Daily cost", -} +}; export interface WorkspaceStatsProps { - workspace: Workspace - maxDeadlineIncrease: number - maxDeadlineDecrease: number - canUpdateWorkspace: boolean - quota_budget?: number - onDeadlinePlus: (hours: number) => void - onDeadlineMinus: (hours: number) => void - handleUpdate: () => void + workspace: Workspace; + maxDeadlineIncrease: number; + maxDeadlineDecrease: number; + canUpdateWorkspace: boolean; + quota_budget?: number; + onDeadlinePlus: (hours: number) => void; + onDeadlineMinus: (hours: number) => void; + handleUpdate: () => void; } export const WorkspaceStats: FC = ({ @@ -57,15 +57,15 @@ export const WorkspaceStats: FC = ({ }) => { const initiatedBy = getDisplayWorkspaceBuildInitiatedBy( workspace.latest_build, - ) - const displayTemplateName = getDisplayWorkspaceTemplateName(workspace) - const styles = useStyles() - const deadlinePlusEnabled = maxDeadlineIncrease >= 1 - const deadlineMinusEnabled = maxDeadlineDecrease >= 1 - const addButtonRef = useRef(null) - const subButtonRef = useRef(null) - const [isAddingTime, setIsAddingTime] = useState(false) - const [isSubTime, setIsSubTime] = useState(false) + ); + const displayTemplateName = getDisplayWorkspaceTemplateName(workspace); + const styles = useStyles(); + const deadlinePlusEnabled = maxDeadlineIncrease >= 1; + const deadlineMinusEnabled = maxDeadlineDecrease >= 1; + const addButtonRef = useRef(null); + const subButtonRef = useRef(null); + const [isAddingTime, setIsAddingTime] = useState(false); + const [isSubTime, setIsSubTime] = useState(false); return ( <> @@ -198,11 +198,11 @@ export const WorkspaceStats: FC = ({
{ - e.preventDefault() - const formData = new FormData(e.currentTarget) - const hours = Number(formData.get("hours")) - onDeadlinePlus(hours) - setIsAddingTime(false) + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const hours = Number(formData.get("hours")); + onDeadlinePlus(hours); + setIsAddingTime(false); }} > = ({ { - e.preventDefault() - const formData = new FormData(e.currentTarget) - const hours = Number(formData.get("hours")) - onDeadlineMinus(hours) - setIsSubTime(false) + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const hours = Number(formData.get("hours")); + onDeadlineMinus(hours); + setIsSubTime(false); }} > = ({ - ) -} + ); +}; export const canEditDeadline = (workspace: Workspace): boolean => { - return isWorkspaceOn(workspace) && Boolean(workspace.latest_build.deadline) -} + return isWorkspaceOn(workspace) && Boolean(workspace.latest_build.deadline); +}; export const shouldDisplayScheduleLabel = (workspace: Workspace): boolean => { if (canEditDeadline(workspace)) { - return true + return true; } if (isWorkspaceOn(workspace)) { - return false + return false; } - return Boolean(workspace.autostart_schedule) -} + return Boolean(workspace.autostart_schedule); +}; const getScheduleLabel = (workspace: Workspace) => { - return isWorkspaceOn(workspace) ? "Stops at" : "Starts at" -} + return isWorkspaceOn(workspace) ? "Stops at" : "Starts at"; +}; const useStyles = makeStyles((theme) => ({ stats: { @@ -400,4 +400,4 @@ const useStyles = makeStyles((theme) => ({ paddingRight: theme.spacing(2), flexShrink: 0, }, -})) +})); diff --git a/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx b/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx index b2b8cb30c6..cffa51c4bf 100644 --- a/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx +++ b/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx @@ -1,18 +1,18 @@ -import { makeStyles } from "@mui/styles" -import ScheduleIcon from "@mui/icons-material/TimerOutlined" -import { Workspace } from "api/typesGenerated" -import { Stack } from "components/Stack/Stack" -import { FC, ElementType, PropsWithChildren, ReactNode } from "react" -import { Link, NavLink } from "react-router-dom" -import { combineClasses } from "utils/combineClasses" -import GeneralIcon from "@mui/icons-material/SettingsOutlined" -import ParameterIcon from "@mui/icons-material/CodeOutlined" -import { Avatar } from "components/Avatar/Avatar" +import { makeStyles } from "@mui/styles"; +import ScheduleIcon from "@mui/icons-material/TimerOutlined"; +import { Workspace } from "api/typesGenerated"; +import { Stack } from "components/Stack/Stack"; +import { FC, ElementType, PropsWithChildren, ReactNode } from "react"; +import { Link, NavLink } from "react-router-dom"; +import { combineClasses } from "utils/combineClasses"; +import GeneralIcon from "@mui/icons-material/SettingsOutlined"; +import ParameterIcon from "@mui/icons-material/CodeOutlined"; +import { Avatar } from "components/Avatar/Avatar"; const SidebarNavItem: FC< PropsWithChildren<{ href: string; icon: ReactNode }> > = ({ children, href, icon }) => { - const styles = useStyles() + const styles = useStyles(); return ( - ) -} + ); +}; const SidebarNavItemIcon: React.FC<{ icon: ElementType }> = ({ icon: Icon, }) => { - const styles = useStyles() - return -} + const styles = useStyles(); + return ; +}; export const Sidebar: React.FC<{ username: string; workspace: Workspace }> = ({ username, workspace, }) => { - const styles = useStyles() + const styles = useStyles(); return ( - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ sidebar: { @@ -143,4 +143,4 @@ const useStyles = makeStyles((theme) => ({ overflow: "hidden", textOverflow: "ellipsis", }, -})) +})); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx index 08223dd948..d5633a0a43 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersForm.tsx @@ -3,33 +3,33 @@ import { FormFooter, FormSection, HorizontalForm, -} from "components/Form/Form" -import { RichParameterInput } from "components/RichParameterInput/RichParameterInput" -import { useFormik } from "formik" -import { FC } from "react" -import { useTranslation } from "react-i18next" +} from "components/Form/Form"; +import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"; +import { useFormik } from "formik"; +import { FC } from "react"; +import { useTranslation } from "react-i18next"; import { getInitialRichParameterValues, useValidationSchemaForRichParameters, -} from "utils/richParameters" -import * as Yup from "yup" -import { getFormHelpers } from "utils/formUtils" +} from "utils/richParameters"; +import * as Yup from "yup"; +import { getFormHelpers } from "utils/formUtils"; import { TemplateVersionParameter, WorkspaceBuildParameter, -} from "api/typesGenerated" +} from "api/typesGenerated"; export type WorkspaceParametersFormValues = { - rich_parameter_values: WorkspaceBuildParameter[] -} + rich_parameter_values: WorkspaceBuildParameter[]; +}; export const WorkspaceParametersForm: FC<{ - isSubmitting: boolean - templateVersionRichParameters: TemplateVersionParameter[] - buildParameters: WorkspaceBuildParameter[] - error: unknown - onCancel: () => void - onSubmit: (values: WorkspaceParametersFormValues) => void + isSubmitting: boolean; + templateVersionRichParameters: TemplateVersionParameter[]; + buildParameters: WorkspaceBuildParameter[]; + error: unknown; + onCancel: () => void; + onSubmit: (values: WorkspaceParametersFormValues) => void; }> = ({ onCancel, onSubmit, @@ -38,7 +38,7 @@ export const WorkspaceParametersForm: FC<{ error, isSubmitting, }) => { - const { t } = useTranslation("workspaceSettingsPage") + const { t } = useTranslation("workspaceSettingsPage"); const form = useFormik({ onSubmit, @@ -54,20 +54,20 @@ export const WorkspaceParametersForm: FC<{ templateVersionRichParameters, ), }), - }) + }); const getFieldHelpers = getFormHelpers( form, error, - ) + ); const hasEphemeralParameters = templateVersionRichParameters.some( (parameter) => parameter.ephemeral, - ) + ); const hasNonEphemeralParameters = templateVersionRichParameters.some( (parameter) => !parameter.ephemeral, - ) + ); const hasImmutableParameters = templateVersionRichParameters.some( (parameter) => !parameter.mutable, - ) + ); return ( @@ -91,7 +91,7 @@ export const WorkspaceParametersForm: FC<{ await form.setFieldValue("rich_parameter_values." + index, { name: parameter.name, value: value, - }) + }); }} parameter={parameter} /> @@ -120,7 +120,7 @@ export const WorkspaceParametersForm: FC<{ await form.setFieldValue("rich_parameter_values." + index, { name: parameter.name, value: value, - }) + }); }} parameter={parameter} /> @@ -152,7 +152,7 @@ export const WorkspaceParametersForm: FC<{ key={parameter.name} parameter={parameter} onChange={() => { - throw new Error("Immutable parameters cannot be changed") + throw new Error("Immutable parameters cannot be changed"); }} /> ) : null, @@ -162,5 +162,5 @@ export const WorkspaceParametersForm: FC<{ )} - ) -} + ); +}; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.stories.tsx index 6639674566..9dfd817b31 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.stories.tsx @@ -1,9 +1,9 @@ -import { ComponentMeta, Story } from "@storybook/react" +import { ComponentMeta, Story } from "@storybook/react"; import { WorkspaceParametersPageView, WorkspaceParametersPageViewProps, -} from "./WorkspaceParametersPage" -import { action } from "@storybook/addon-actions" +} from "./WorkspaceParametersPage"; +import { action } from "@storybook/addon-actions"; import { MockWorkspaceBuildParameter1, MockWorkspaceBuildParameter2, @@ -11,7 +11,7 @@ import { MockTemplateVersionParameter2, MockTemplateVersionParameter3, MockWorkspaceBuildParameter3, -} from "testHelpers/entities" +} from "testHelpers/entities"; export default { title: "pages/WorkspaceParametersPageView", @@ -36,11 +36,11 @@ export default { ], }, }, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) +); -export const Example = Template.bind({}) -Example.args = {} +export const Example = Template.bind({}); +Example.args = {}; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx index 83d5771288..18267f7a8f 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx @@ -1,11 +1,11 @@ -import userEvent from "@testing-library/user-event" +import userEvent from "@testing-library/user-event"; import { renderWithWorkspaceSettingsLayout, waitForLoaderToBeRemoved, -} from "testHelpers/renderHelpers" -import WorkspaceParametersPage from "./WorkspaceParametersPage" -import { screen, waitFor, within } from "@testing-library/react" -import * as api from "api/api" +} from "testHelpers/renderHelpers"; +import WorkspaceParametersPage from "./WorkspaceParametersPage"; +import { screen, waitFor, within } from "@testing-library/react"; +import * as api from "api/api"; import { MockWorkspace, MockTemplateVersionParameter1, @@ -15,53 +15,53 @@ import { MockWorkspaceBuild, MockTemplateVersionParameter4, MockWorkspaceBuildParameter4, -} from "testHelpers/entities" +} from "testHelpers/entities"; test("Submit the workspace settings page successfully", async () => { // Mock the API calls that loads data jest .spyOn(api, "getWorkspaceByOwnerAndName") - .mockResolvedValueOnce(MockWorkspace) + .mockResolvedValueOnce(MockWorkspace); jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([ MockTemplateVersionParameter1, MockTemplateVersionParameter2, // Immutable parameters MockTemplateVersionParameter4, - ]) + ]); jest.spyOn(api, "getWorkspaceBuildParameters").mockResolvedValueOnce([ MockWorkspaceBuildParameter1, MockWorkspaceBuildParameter2, // Immutable value MockWorkspaceBuildParameter4, - ]) + ]); // Mock the API calls that submit data const postWorkspaceBuildSpy = jest .spyOn(api, "postWorkspaceBuild") - .mockResolvedValue(MockWorkspaceBuild) + .mockResolvedValue(MockWorkspaceBuild); // Setup event and rendering - const user = userEvent.setup() + const user = userEvent.setup(); renderWithWorkspaceSettingsLayout(, { route: "/@test-user/test-workspace/settings", path: "/:username/:workspace/settings", // Need this because after submit the user is redirected extraRoutes: [{ path: "/:username/:workspace", element:
}], - }) - await waitForLoaderToBeRemoved() + }); + await waitForLoaderToBeRemoved(); // Fill the form and submit - const form = screen.getByTestId("form") + const form = screen.getByTestId("form"); const parameter1 = within(form).getByLabelText( MockWorkspaceBuildParameter1.name, { exact: false }, - ) - await user.clear(parameter1) - await user.type(parameter1, "new-value") + ); + await user.clear(parameter1); + await user.type(parameter1, "new-value"); const parameter2 = within(form).getByLabelText( MockWorkspaceBuildParameter2.name, { exact: false }, - ) - await user.clear(parameter2) - await user.type(parameter2, "1") - await user.click(within(form).getByRole("button", { name: "Submit" })) + ); + await user.clear(parameter2); + await user.type(parameter2, "1"); + await user.click(within(form).getByRole("button", { name: "Submit" })); // Assert that the API calls were made with the correct data await waitFor(() => { expect(postWorkspaceBuildSpy).toHaveBeenCalledWith(MockWorkspace.id, { @@ -70,6 +70,6 @@ test("Submit the workspace settings page successfully", async () => { { name: MockTemplateVersionParameter1.name, value: "new-value" }, { name: MockTemplateVersionParameter2.name, value: "1" }, ], - }) - }) -}) + }); + }); +}); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx index 4b8c0818c9..42f938e9e7 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx @@ -1,28 +1,28 @@ -import { getWorkspaceParameters, postWorkspaceBuild } from "api/api" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "utils/page" -import { useWorkspaceSettingsContext } from "../WorkspaceSettingsLayout" -import { useMutation, useQuery } from "@tanstack/react-query" -import { Loader } from "components/Loader/Loader" +import { getWorkspaceParameters, postWorkspaceBuild } from "api/api"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { useWorkspaceSettingsContext } from "../WorkspaceSettingsLayout"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { Loader } from "components/Loader/Loader"; import { WorkspaceParametersFormValues, WorkspaceParametersForm, -} from "./WorkspaceParametersForm" -import { useNavigate } from "react-router-dom" -import { makeStyles } from "@mui/styles" -import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader" -import { FC } from "react" -import { isApiValidationError } from "api/errors" -import { ErrorAlert } from "components/Alert/ErrorAlert" -import { WorkspaceBuildParameter } from "api/typesGenerated" +} from "./WorkspaceParametersForm"; +import { useNavigate } from "react-router-dom"; +import { makeStyles } from "@mui/styles"; +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; +import { FC } from "react"; +import { isApiValidationError } from "api/errors"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { WorkspaceBuildParameter } from "api/typesGenerated"; const WorkspaceParametersPage = () => { - const { workspace } = useWorkspaceSettingsContext() + const { workspace } = useWorkspaceSettingsContext(); const parameters = useQuery({ queryKey: ["workspace", workspace.id, "parameters"], queryFn: () => getWorkspaceParameters(workspace), - }) - const navigate = useNavigate() + }); + const navigate = useNavigate(); const updateParameters = useMutation({ mutationFn: (buildParameters: WorkspaceBuildParameter[]) => postWorkspaceBuild(workspace.id, { @@ -30,9 +30,9 @@ const WorkspaceParametersPage = () => { rich_parameter_values: buildParameters, }), onSuccess: () => { - navigate(`/${workspace.owner_name}/${workspace.name}`) + navigate(`/${workspace.owner_name}/${workspace.name}`); }, - }) + }); return ( <> @@ -52,29 +52,29 @@ const WorkspaceParametersPage = () => { .map( (p) => values.rich_parameter_values.find((v) => v.name === p.name)!, - ) - updateParameters.mutate(onlyMultableValues) + ); + updateParameters.mutate(onlyMultableValues); }} onCancel={() => { - navigate("../..") + navigate("../.."); }} /> - ) -} + ); +}; export type WorkspaceParametersPageViewProps = { - data: Awaited> | undefined - submitError: unknown - isSubmitting: boolean - onSubmit: (formValues: WorkspaceParametersFormValues) => void - onCancel: () => void -} + data: Awaited> | undefined; + submitError: unknown; + isSubmitting: boolean; + onSubmit: (formValues: WorkspaceParametersFormValues) => void; + onCancel: () => void; +}; export const WorkspaceParametersPageView: FC< WorkspaceParametersPageViewProps > = ({ data, submitError, isSubmitting, onSubmit, onCancel }) => { - const styles = useStyles() + const styles = useStyles(); return ( <> @@ -99,13 +99,13 @@ export const WorkspaceParametersPageView: FC< )} - ) -} + ); +}; const useStyles = makeStyles(() => ({ pageHeader: { paddingTop: 0, }, -})) +})); -export default WorkspaceParametersPage +export default WorkspaceParametersPage; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.stories.tsx index 0ca9d30c69..61571eee82 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.stories.tsx @@ -1,22 +1,22 @@ -import { Story } from "@storybook/react" -import dayjs from "dayjs" -import advancedFormat from "dayjs/plugin/advancedFormat" -import timezone from "dayjs/plugin/timezone" -import utc from "dayjs/plugin/utc" +import { Story } from "@storybook/react"; +import dayjs from "dayjs"; +import advancedFormat from "dayjs/plugin/advancedFormat"; +import timezone from "dayjs/plugin/timezone"; +import utc from "dayjs/plugin/utc"; import { defaultSchedule, emptySchedule, -} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule" -import { emptyTTL } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl" -import { mockApiError } from "testHelpers/entities" +} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"; +import { emptyTTL } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"; +import { mockApiError } from "testHelpers/entities"; import { WorkspaceScheduleForm, WorkspaceScheduleFormProps, -} from "./WorkspaceScheduleForm" +} from "./WorkspaceScheduleForm"; -dayjs.extend(advancedFormat) -dayjs.extend(utc) -dayjs.extend(timezone) +dayjs.extend(advancedFormat); +dayjs.extend(utc); +dayjs.extend(timezone); export default { title: "components/WorkspaceScheduleForm", @@ -29,20 +29,20 @@ export default { action: "onSubmit", }, }, -} +}; const Template: Story = (args) => ( -) +); const defaultInitialValues = { autostartEnabled: true, ...defaultSchedule(), autostopEnabled: true, ttl: 24, -} +}; -export const AllDisabled = Template.bind({}) +export const AllDisabled = Template.bind({}); AllDisabled.args = { initialValues: { autostartEnabled: false, @@ -50,9 +50,9 @@ AllDisabled.args = { autostopEnabled: false, ttl: emptyTTL, }, -} +}; -export const Autostart = Template.bind({}) +export const Autostart = Template.bind({}); Autostart.args = { initialValues: { autostartEnabled: true, @@ -60,24 +60,24 @@ Autostart.args = { autostopEnabled: false, ttl: emptyTTL, }, -} +}; -export const WorkspaceWillShutdownInTwoHours = Template.bind({}) +export const WorkspaceWillShutdownInTwoHours = Template.bind({}); WorkspaceWillShutdownInTwoHours.args = { initialValues: { ...defaultInitialValues, ttl: 2 }, -} +}; -export const WorkspaceWillShutdownInADay = Template.bind({}) +export const WorkspaceWillShutdownInADay = Template.bind({}); WorkspaceWillShutdownInADay.args = { initialValues: { ...defaultInitialValues, ttl: 24 }, -} +}; -export const WorkspaceWillShutdownInTwoDays = Template.bind({}) +export const WorkspaceWillShutdownInTwoDays = Template.bind({}); WorkspaceWillShutdownInTwoDays.args = { initialValues: { ...defaultInitialValues, ttl: 48 }, -} +}; -export const WithError = Template.bind({}) +export const WithError = Template.bind({}); WithError.args = { initialValues: { ...defaultInitialValues, ttl: 100 }, initialTouched: { ttl: true }, @@ -85,10 +85,10 @@ WithError.args = { message: "Something went wrong.", validations: [{ field: "ttl_ms", detail: "Invalid time until shutdown." }], }), -} +}; -export const Loading = Template.bind({}) +export const Loading = Template.bind({}); Loading.args = { initialValues: defaultInitialValues, isLoading: true, -} +}; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.ts b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.ts index ee74b3ba95..58b324deea 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.ts +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.test.ts @@ -3,8 +3,8 @@ import { ttlShutdownAt, validationSchema, WorkspaceScheduleFormValues, -} from "./WorkspaceScheduleForm" -import { zones } from "./zones" +} from "./WorkspaceScheduleForm"; +import { zones } from "./zones"; const valid: WorkspaceScheduleFormValues = { autostartEnabled: true, @@ -20,7 +20,7 @@ const valid: WorkspaceScheduleFormValues = { autostopEnabled: true, ttl: 120, -} +}; describe("validationSchema", () => { it("allows everything to be falsy when switches are off", () => { @@ -38,19 +38,19 @@ describe("validationSchema", () => { autostopEnabled: false, ttl: 0, - } - const validate = () => validationSchema.validateSync(values) - expect(validate).not.toThrow() - }) + }; + const validate = () => validationSchema.validateSync(values); + expect(validate).not.toThrow(); + }); it("disallows ttl to be negative", () => { const values: WorkspaceScheduleFormValues = { ...valid, ttl: -1, - } - const validate = () => validationSchema.validateSync(values) - expect(validate).toThrow() - }) + }; + const validate = () => validationSchema.validateSync(values); + expect(validate).toThrow(); + }); it("disallows all days-of-week to be false when autostart is enabled", () => { const values: WorkspaceScheduleFormValues = { @@ -62,10 +62,10 @@ describe("validationSchema", () => { thursday: false, friday: false, saturday: false, - } - const validate = () => validationSchema.validateSync(values) - expect(validate).toThrowError(Language.errorNoDayOfWeek) - }) + }; + const validate = () => validationSchema.validateSync(values); + expect(validate).toThrowError(Language.errorNoDayOfWeek); + }); it("disallows empty startTime when autostart is enabled", () => { const values: WorkspaceScheduleFormValues = { @@ -78,64 +78,64 @@ describe("validationSchema", () => { friday: false, saturday: false, startTime: "", - } - const validate = () => validationSchema.validateSync(values) - expect(validate).toThrowError(Language.errorNoTime) - }) + }; + const validate = () => validationSchema.validateSync(values); + expect(validate).toThrowError(Language.errorNoTime); + }); it("allows startTime 16:20", () => { const values: WorkspaceScheduleFormValues = { ...valid, startTime: "16:20", - } - const validate = () => validationSchema.validateSync(values) - expect(validate).not.toThrow() - }) + }; + const validate = () => validationSchema.validateSync(values); + expect(validate).not.toThrow(); + }); it("disallows startTime to be H:mm", () => { const values: WorkspaceScheduleFormValues = { ...valid, startTime: "9:30", - } - const validate = () => validationSchema.validateSync(values) - expect(validate).toThrowError(Language.errorTime) - }) + }; + const validate = () => validationSchema.validateSync(values); + expect(validate).toThrowError(Language.errorTime); + }); it("disallows startTime to be HH:m", () => { const values: WorkspaceScheduleFormValues = { ...valid, startTime: "09:5", - } - const validate = () => validationSchema.validateSync(values) - expect(validate).toThrowError(Language.errorTime) - }) + }; + const validate = () => validationSchema.validateSync(values); + expect(validate).toThrowError(Language.errorTime); + }); it("disallows an invalid startTime 24:01", () => { const values: WorkspaceScheduleFormValues = { ...valid, startTime: "24:01", - } - const validate = () => validationSchema.validateSync(values) - expect(validate).toThrowError(Language.errorTime) - }) + }; + const validate = () => validationSchema.validateSync(values); + expect(validate).toThrowError(Language.errorTime); + }); it("disallows an invalid startTime 09:60", () => { const values: WorkspaceScheduleFormValues = { ...valid, startTime: "09:60", - } - const validate = () => validationSchema.validateSync(values) - expect(validate).toThrowError(Language.errorTime) - }) + }; + const validate = () => validationSchema.validateSync(values); + expect(validate).toThrowError(Language.errorTime); + }); it("disallows an invalid timezone Canada/North", () => { const values: WorkspaceScheduleFormValues = { ...valid, timezone: "Canada/North", - } - const validate = () => validationSchema.validateSync(values) - expect(validate).toThrowError(Language.errorTimezone) - }) + }; + const validate = () => validationSchema.validateSync(values); + expect(validate).toThrowError(Language.errorTimezone); + }); it.each<[string]>(zones.map((zone) => [zone]))( `validation passes for tz=%p`, @@ -143,39 +143,39 @@ describe("validationSchema", () => { const values: WorkspaceScheduleFormValues = { ...valid, timezone: zone, - } - const validate = () => validationSchema.validateSync(values) - expect(validate).not.toThrow() + }; + const validate = () => validationSchema.validateSync(values); + expect(validate).not.toThrow(); }, - ) + ); it("allows a ttl of 7 days", () => { const values: WorkspaceScheduleFormValues = { ...valid, ttl: 24 * 7, - } - const validate = () => validationSchema.validateSync(values) - expect(validate).not.toThrowError() - }) + }; + const validate = () => validationSchema.validateSync(values); + expect(validate).not.toThrowError(); + }); it("allows a ttl of 30 days", () => { const values: WorkspaceScheduleFormValues = { ...valid, ttl: 24 * 30, - } - const validate = () => validationSchema.validateSync(values) - expect(validate).not.toThrowError() - }) + }; + const validate = () => validationSchema.validateSync(values); + expect(validate).not.toThrowError(); + }); it("disallows a ttl of 30 days + 1 hour", () => { const values: WorkspaceScheduleFormValues = { ...valid, ttl: 24 * 30 + 1, - } - const validate = () => validationSchema.validateSync(values) - expect(validate).toThrowError(Language.errorTtlMax) - }) -}) + }; + const validate = () => validationSchema.validateSync(values); + expect(validate).toThrowError(Language.errorTtlMax); + }); +}); describe("ttlShutdownAt", () => { it.each<[string, number, string]>([ @@ -205,6 +205,6 @@ describe("ttlShutdownAt", () => { `${Language.ttlCausesShutdownHelperText} 2 days ${Language.ttlCausesShutdownAfterStart}.`, ], ])("%p", (_, ttlHours, expected) => { - expect(ttlShutdownAt(ttlHours)).toEqual(expected) - }) -}) + expect(ttlShutdownAt(ttlHours)).toEqual(expected); + }); +}); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.tsx index b04fdae286..1a8dbc1a4a 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceScheduleForm.tsx @@ -1,43 +1,43 @@ -import Checkbox from "@mui/material/Checkbox" -import FormControl from "@mui/material/FormControl" -import FormControlLabel from "@mui/material/FormControlLabel" -import FormGroup from "@mui/material/FormGroup" -import FormHelperText from "@mui/material/FormHelperText" -import FormLabel from "@mui/material/FormLabel" -import MenuItem from "@mui/material/MenuItem" -import makeStyles from "@mui/styles/makeStyles" -import Switch from "@mui/material/Switch" -import TextField from "@mui/material/TextField" +import Checkbox from "@mui/material/Checkbox"; +import FormControl from "@mui/material/FormControl"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import FormGroup from "@mui/material/FormGroup"; +import FormHelperText from "@mui/material/FormHelperText"; +import FormLabel from "@mui/material/FormLabel"; +import MenuItem from "@mui/material/MenuItem"; +import makeStyles from "@mui/styles/makeStyles"; +import Switch from "@mui/material/Switch"; +import TextField from "@mui/material/TextField"; import { HorizontalForm, FormFooter, FormSection, FormFields, -} from "components/Form/Form" -import { Stack } from "components/Stack/Stack" -import dayjs from "dayjs" -import advancedFormat from "dayjs/plugin/advancedFormat" -import duration from "dayjs/plugin/duration" -import relativeTime from "dayjs/plugin/relativeTime" -import timezone from "dayjs/plugin/timezone" -import utc from "dayjs/plugin/utc" -import { FormikTouched, useFormik } from "formik" +} from "components/Form/Form"; +import { Stack } from "components/Stack/Stack"; +import dayjs from "dayjs"; +import advancedFormat from "dayjs/plugin/advancedFormat"; +import duration from "dayjs/plugin/duration"; +import relativeTime from "dayjs/plugin/relativeTime"; +import timezone from "dayjs/plugin/timezone"; +import utc from "dayjs/plugin/utc"; +import { FormikTouched, useFormik } from "formik"; import { defaultSchedule, emptySchedule, -} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule" -import { ChangeEvent, FC } from "react" -import * as Yup from "yup" -import { getFormHelpers } from "utils/formUtils" -import { zones } from "./zones" +} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"; +import { ChangeEvent, FC } from "react"; +import * as Yup from "yup"; +import { getFormHelpers } from "utils/formUtils"; +import { zones } from "./zones"; // REMARK: some plugins depend on utc, so it's listed first. Otherwise they're // sorted alphabetically. -dayjs.extend(utc) -dayjs.extend(advancedFormat) -dayjs.extend(duration) -dayjs.extend(relativeTime) -dayjs.extend(timezone) +dayjs.extend(utc); +dayjs.extend(advancedFormat); +dayjs.extend(duration); +dayjs.extend(relativeTime); +dayjs.extend(timezone); export const Language = { errorNoDayOfWeek: @@ -70,33 +70,33 @@ export const Language = { startSwitch: "Enable Autostart", stopSection: "Stop", stopSwitch: "Enable Autostop", -} +}; export interface WorkspaceScheduleFormProps { - submitScheduleError?: unknown - initialValues: WorkspaceScheduleFormValues - isLoading: boolean - onCancel: () => void - onSubmit: (values: WorkspaceScheduleFormValues) => void + submitScheduleError?: unknown; + initialValues: WorkspaceScheduleFormValues; + isLoading: boolean; + onCancel: () => void; + onSubmit: (values: WorkspaceScheduleFormValues) => void; // for storybook - initialTouched?: FormikTouched - defaultTTL: number + initialTouched?: FormikTouched; + defaultTTL: number; } export interface WorkspaceScheduleFormValues { - autostartEnabled: boolean - sunday: boolean - monday: boolean - tuesday: boolean - wednesday: boolean - thursday: boolean - friday: boolean - saturday: boolean - startTime: string - timezone: string + autostartEnabled: boolean; + sunday: boolean; + monday: boolean; + tuesday: boolean; + wednesday: boolean; + thursday: boolean; + friday: boolean; + saturday: boolean; + startTime: string; + timezone: string; - autostopEnabled: boolean - ttl: number + autostopEnabled: boolean; + ttl: number; } export const validationSchema = Yup.object({ @@ -105,10 +105,10 @@ export const validationSchema = Yup.object({ "at-least-one-day", Language.errorNoDayOfWeek, function (value) { - const parent = this.parent as WorkspaceScheduleFormValues + const parent = this.parent as WorkspaceScheduleFormValues; if (!parent.autostartEnabled) { - return true + return true; } else { return ![ parent.sunday, @@ -118,7 +118,7 @@ export const validationSchema = Yup.object({ parent.thursday, parent.friday, parent.saturday, - ].every((day) => day === false) + ].every((day) => day === false); } }, ), @@ -131,41 +131,41 @@ export const validationSchema = Yup.object({ startTime: Yup.string() .ensure() .test("required-if-autostart", Language.errorNoTime, function (value) { - const parent = this.parent as WorkspaceScheduleFormValues + const parent = this.parent as WorkspaceScheduleFormValues; if (parent.autostartEnabled) { - return value !== "" + return value !== ""; } else { - return true + return true; } }) .test("is-time-string", Language.errorTime, (value) => { if (value === "") { - return true + return true; } else if (!/^[0-9][0-9]:[0-9][0-9]$/.test(value)) { - return false + return false; } else { - const parts = value.split(":") - const HH = Number(parts[0]) - const mm = Number(parts[1]) - return HH >= 0 && HH <= 23 && mm >= 0 && mm <= 59 + const parts = value.split(":"); + const HH = Number(parts[0]); + const mm = Number(parts[1]); + return HH >= 0 && HH <= 23 && mm >= 0 && mm <= 59; } }), timezone: Yup.string() .ensure() .test("is-timezone", Language.errorTimezone, function (value) { - const parent = this.parent as WorkspaceScheduleFormValues + const parent = this.parent as WorkspaceScheduleFormValues; if (!parent.startTime) { - return true + return true; } else { // Unfortunately, there's not a good API on dayjs at this time for // evaluating a timezone. Attempt to parse today in the supplied timezone // and return as valid if the function doesn't throw. try { - dayjs.tz(dayjs(), value) - return true + dayjs.tz(dayjs(), value); + return true; } catch (e) { - return false + return false; } } }), @@ -174,14 +174,14 @@ export const validationSchema = Yup.object({ .min(0) .max(24 * 30 /* 30 days */, Language.errorTtlMax) .test("positive-if-autostop", Language.errorNoStop, function (value) { - const parent = this.parent as WorkspaceScheduleFormValues + const parent = this.parent as WorkspaceScheduleFormValues; if (parent.autostopEnabled) { - return Boolean(value) + return Boolean(value); } else { - return true + return true; } }), -}) +}); export const WorkspaceScheduleForm: FC< React.PropsWithChildren @@ -194,18 +194,18 @@ export const WorkspaceScheduleForm: FC< initialTouched, defaultTTL, }) => { - const styles = useStyles() + const styles = useStyles(); const form = useFormik({ initialValues, onSubmit, validationSchema, initialTouched, - }) + }); const formHelpers = getFormHelpers( form, submitScheduleError, - ) + ); const checkboxes: Array<{ value: boolean; name: string; label: string }> = [ { @@ -243,41 +243,41 @@ export const WorkspaceScheduleForm: FC< name: "saturday", label: Language.daySaturdayLabel, }, - ] + ]; const handleToggleAutostart = async (e: ChangeEvent) => { - form.handleChange(e) + form.handleChange(e); if (form.values.autostartEnabled) { // disable autostart, clear values await form.setValues({ ...form.values, autostartEnabled: false, ...emptySchedule, - }) + }); } else { // enable autostart, fill with defaults await form.setValues({ ...form.values, autostartEnabled: true, ...defaultSchedule(), - }) + }); } - } + }; const handleToggleAutostop = async (e: ChangeEvent) => { - form.handleChange(e) + form.handleChange(e); if (form.values.autostopEnabled) { // disable autostop, set TTL 0 - await form.setValues({ ...form.values, autostopEnabled: false, ttl: 0 }) + await form.setValues({ ...form.values, autostopEnabled: false, ttl: 0 }); } else { // enable autostop, fill with default TTL await form.setValues({ ...form.values, autostopEnabled: true, ttl: defaultTTL, - }) + }); } - } + }; return ( @@ -376,19 +376,19 @@ export const WorkspaceScheduleForm: FC< - ) -} + ); +}; export const ttlShutdownAt = (formTTL: number): string => { if (formTTL < 1) { // Passing an empty value for TTL in the form results in a number that is not zero but less than 1. - return Language.ttlCausesNoShutdownHelperText + return Language.ttlCausesNoShutdownHelperText; } else { return `${Language.ttlCausesShutdownHelperText} ${dayjs .duration(formTTL, "hours") - .humanize()} ${Language.ttlCausesShutdownAfterStart}.` + .humanize()} ${Language.ttlCausesShutdownAfterStart}.`; } -} +}; const useStyles = makeStyles((theme) => ({ daysOfWeekLabel: { @@ -400,4 +400,4 @@ const useStyles = makeStyles((theme) => ({ flexWrap: "wrap", paddingTop: theme.spacing(0.5), }, -})) +})); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx index bcf8daf834..1c59a3e1fe 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx @@ -1,30 +1,30 @@ -import { renderWithWorkspaceSettingsLayout } from "testHelpers/renderHelpers" -import userEvent from "@testing-library/user-event" -import { screen } from "@testing-library/react" +import { renderWithWorkspaceSettingsLayout } from "testHelpers/renderHelpers"; +import userEvent from "@testing-library/user-event"; +import { screen } from "@testing-library/react"; import { formValuesToAutostartRequest, formValuesToTTLRequest, -} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/formToRequest" +} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/formToRequest"; import { Autostart, scheduleToAutostart, -} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule" +} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"; import { Autostop, ttlMsToAutostop, -} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl" -import * as TypesGen from "../../../api/typesGenerated" +} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"; +import * as TypesGen from "../../../api/typesGenerated"; import { WorkspaceScheduleFormValues, Language as FormLanguage, -} from "./WorkspaceScheduleForm" -import { WorkspaceSchedulePage } from "./WorkspaceSchedulePage" -import i18next from "i18next" -import { server } from "testHelpers/server" -import { rest } from "msw" -import { MockUser, MockWorkspace } from "testHelpers/entities" +} from "./WorkspaceScheduleForm"; +import { WorkspaceSchedulePage } from "./WorkspaceSchedulePage"; +import i18next from "i18next"; +import { server } from "testHelpers/server"; +import { rest } from "msw"; +import { MockUser, MockWorkspace } from "testHelpers/entities"; -const { t } = i18next +const { t } = i18next; const validValues: WorkspaceScheduleFormValues = { autostartEnabled: true, @@ -39,7 +39,7 @@ const validValues: WorkspaceScheduleFormValues = { timezone: "Canada/Eastern", autostopEnabled: true, ttl: 120, -} +}; describe("WorkspaceSchedulePage", () => { describe("formValuesToAutostartRequest", () => { @@ -147,9 +147,9 @@ describe("WorkspaceSchedulePage", () => { }, ], ])(`formValuesToAutostartRequest(%p) return %p`, (values, request) => { - expect(formValuesToAutostartRequest(values)).toEqual(request) - }) - }) + expect(formValuesToAutostartRequest(values)).toEqual(request); + }); + }); describe("formValuesToTTLRequest", () => { it.each<[WorkspaceScheduleFormValues, TypesGen.UpdateWorkspaceTTLRequest]>([ @@ -184,9 +184,9 @@ describe("WorkspaceSchedulePage", () => { }, ], ])(`formValuesToTTLRequest(%p) returns %p`, (values, request) => { - expect(formValuesToTTLRequest(values)).toEqual(request) - }) - }) + expect(formValuesToTTLRequest(values)).toEqual(request); + }); + }); describe("scheduleToAutostart", () => { it.each<[string | undefined, Autostart]>([ @@ -241,9 +241,9 @@ describe("WorkspaceSchedulePage", () => { }, ], ])(`scheduleToAutostart(%p) returns %p`, (schedule, autostart) => { - expect(scheduleToAutostart(schedule)).toEqual(autostart) - }) - }) + expect(scheduleToAutostart(schedule)).toEqual(autostart); + }); + }); describe("ttlMsToAutostop", () => { it.each<[number | undefined, Autostop]>([ @@ -254,9 +254,9 @@ describe("WorkspaceSchedulePage", () => { // basic case [28_800_000, { autostopEnabled: true, ttl: 8 }], ])(`ttlMsToAutostop(%p) returns %p`, (ttlMs, autostop) => { - expect(ttlMsToAutostop(ttlMs)).toEqual(autostop) - }) - }) + expect(ttlMsToAutostop(ttlMs)).toEqual(autostop); + }); + }); describe("autostop", () => { it("uses template default ttl when first enabled", async () => { @@ -268,48 +268,48 @@ describe("WorkspaceSchedulePage", () => { return res( ctx.status(200), ctx.json({ ...MockWorkspace, ttl_ms: 0 }), - ) + ); }, ), - ) + ); renderWithWorkspaceSettingsLayout(, { route: `/@${MockUser.username}/${MockWorkspace.name}/schedule`, path: "/:username/:workspace/schedule", - }) - const user = userEvent.setup() + }); + const user = userEvent.setup(); const autostopToggle = await screen.findByLabelText( FormLanguage.stopSwitch, - ) + ); // enable autostop - await user.click(autostopToggle) + await user.click(autostopToggle); // find helper text that describes the mock template's 24 hour default const autostopHelperText = await screen.findByText( "Your workspace will shut down a day after", { exact: false }, - ) - expect(autostopHelperText).toBeDefined() - }) - }) + ); + expect(autostopHelperText).toBeDefined(); + }); + }); describe("autostop change dialog", () => { it("shows if autostop is changed", async () => { renderWithWorkspaceSettingsLayout(, { route: `/@${MockUser.username}/${MockWorkspace.name}/schedule`, path: "/:username/:workspace/schedule", - }) - const user = userEvent.setup() + }); + const user = userEvent.setup(); const autostopToggle = await screen.findByLabelText( FormLanguage.stopSwitch, - ) - await user.click(autostopToggle) + ); + await user.click(autostopToggle); const submitButton = await screen.findByRole("button", { name: /submit/i, - }) - await user.click(submitButton) - const title = t("dialogTitle", { ns: "workspaceSchedulePage" }) - const dialog = await screen.findByText(title) - expect(dialog).toBeInTheDocument() - }) + }); + await user.click(submitButton); + const title = t("dialogTitle", { ns: "workspaceSchedulePage" }); + const dialog = await screen.findByText(title); + expect(dialog).toBeInTheDocument(); + }); it("doesn't show if autostop is not changed", async () => { renderWithWorkspaceSettingsLayout(, { @@ -318,19 +318,19 @@ describe("WorkspaceSchedulePage", () => { extraRoutes: [ { path: "/:username/:workspace", element:
Workspace
}, ], - }) - const user = userEvent.setup() + }); + const user = userEvent.setup(); const autostartToggle = await screen.findByLabelText( FormLanguage.startSwitch, - ) - await user.click(autostartToggle) + ); + await user.click(autostartToggle); const submitButton = await screen.findByRole("button", { name: /submit/i, - }) - await user.click(submitButton) - const title = t("dialogTitle", { ns: "workspaceSchedulePage" }) - const dialog = screen.queryByText(title) - expect(dialog).not.toBeInTheDocument() - }) - }) -}) + }); + await user.click(submitButton); + const title = t("dialogTitle", { ns: "workspaceSchedulePage" }); + const dialog = screen.queryByText(title); + expect(dialog).not.toBeInTheDocument(); + }); + }); +}); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 1e734148ef..2a974c12f8 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -1,34 +1,34 @@ -import { makeStyles } from "@mui/styles" -import { useMachine } from "@xstate/react" -import { Alert } from "components/Alert/Alert" -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" -import { Loader } from "components/Loader/Loader" -import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader" -import dayjs from "dayjs" +import { makeStyles } from "@mui/styles"; +import { useMachine } from "@xstate/react"; +import { Alert } from "components/Alert/Alert"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import { Loader } from "components/Loader/Loader"; +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; +import dayjs from "dayjs"; import { scheduleToAutostart, scheduleChanged, -} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule" -import { ttlMsToAutostop } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl" -import { useWorkspaceSettingsContext } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout" -import { FC } from "react" -import { Helmet } from "react-helmet-async" -import { useTranslation } from "react-i18next" -import { Navigate, useNavigate, useParams } from "react-router-dom" -import { pageTitle } from "utils/page" -import * as TypesGen from "api/typesGenerated" -import { WorkspaceScheduleForm } from "./WorkspaceScheduleForm" -import { workspaceSchedule } from "xServices/workspaceSchedule/workspaceScheduleXService" +} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"; +import { ttlMsToAutostop } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"; +import { useWorkspaceSettingsContext } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"; +import { FC } from "react"; +import { Helmet } from "react-helmet-async"; +import { useTranslation } from "react-i18next"; +import { Navigate, useNavigate, useParams } from "react-router-dom"; +import { pageTitle } from "utils/page"; +import * as TypesGen from "api/typesGenerated"; +import { WorkspaceScheduleForm } from "./WorkspaceScheduleForm"; +import { workspaceSchedule } from "xServices/workspaceSchedule/workspaceScheduleXService"; import { formValuesToAutostartRequest, formValuesToTTLRequest, -} from "./formToRequest" -import { ErrorAlert } from "components/Alert/ErrorAlert" +} from "./formToRequest"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; const getAutostart = (workspace: TypesGen.Workspace) => - scheduleToAutostart(workspace.autostart_schedule) + scheduleToAutostart(workspace.autostart_schedule); const getAutostop = (workspace: TypesGen.Workspace) => - ttlMsToAutostop(workspace.ttl_ms) + ttlMsToAutostop(workspace.ttl_ms); const useStyles = makeStyles((theme) => ({ topMargin: { @@ -37,33 +37,33 @@ const useStyles = makeStyles((theme) => ({ pageHeader: { paddingTop: 0, }, -})) +})); export const WorkspaceSchedulePage: FC = () => { - const { t } = useTranslation("workspaceSchedulePage") - const styles = useStyles() - const params = useParams() as { username: string; workspace: string } - const navigate = useNavigate() - const username = params.username.replace("@", "") - const workspaceName = params.workspace - const { workspace } = useWorkspaceSettingsContext() + const { t } = useTranslation("workspaceSchedulePage"); + const styles = useStyles(); + const params = useParams() as { username: string; workspace: string }; + const navigate = useNavigate(); + const username = params.username.replace("@", ""); + const workspaceName = params.workspace; + const { workspace } = useWorkspaceSettingsContext(); const [scheduleState, scheduleSend] = useMachine(workspaceSchedule, { context: { workspace }, - }) + }); const { checkPermissionsError, submitScheduleError, getTemplateError, permissions, template, - } = scheduleState.context + } = scheduleState.context; if (!username || !workspaceName) { - return + return ; } if (scheduleState.matches("done")) { - return + return ; } return ( @@ -94,7 +94,7 @@ export const WorkspaceSchedulePage: FC = () => { isLoading={scheduleState.tags.has("loading")} defaultTTL={dayjs.duration(template.default_ttl_ms, "ms").asHours()} onCancel={() => { - navigate(`/@${username}/${workspaceName}`) + navigate(`/@${username}/${workspaceName}`); }} onSubmit={(values) => { scheduleSend({ @@ -109,7 +109,7 @@ export const WorkspaceSchedulePage: FC = () => { getAutostop(workspace), values, ), - }) + }); }} /> )} @@ -121,14 +121,14 @@ export const WorkspaceSchedulePage: FC = () => { cancelText={t("applyLater").toString()} hideCancel={false} onConfirm={() => { - scheduleSend("RESTART_WORKSPACE") + scheduleSend("RESTART_WORKSPACE"); }} onClose={() => { - scheduleSend("APPLY_LATER") + scheduleSend("APPLY_LATER"); }} /> - ) -} + ); +}; -export default WorkspaceSchedulePage +export default WorkspaceSchedulePage; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/formToRequest.ts b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/formToRequest.ts index d0211ec2d4..8096512e97 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/formToRequest.ts +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/formToRequest.ts @@ -1,5 +1,5 @@ -import * as TypesGen from "api/typesGenerated" -import { WorkspaceScheduleFormValues } from "./WorkspaceScheduleForm" +import * as TypesGen from "api/typesGenerated"; +import { WorkspaceScheduleFormValues } from "./WorkspaceScheduleForm"; export const formValuesToAutostartRequest = ( values: WorkspaceScheduleFormValues, @@ -7,15 +7,15 @@ export const formValuesToAutostartRequest = ( if (!values.autostartEnabled || !values.startTime) { return { schedule: "", - } + }; } - const [HH, mm] = values.startTime.split(":") + const [HH, mm] = values.startTime.split(":"); // Note: Space after CRON_TZ if timezone is defined - const preparedTZ = values.timezone ? `CRON_TZ=${values.timezone} ` : "" + const preparedTZ = values.timezone ? `CRON_TZ=${values.timezone} ` : ""; - const makeCronString = (dow: string) => `${preparedTZ}${mm} ${HH} * * ${dow}` + const makeCronString = (dow: string) => `${preparedTZ}${mm} ${HH} * * ${dow}`; const days = [ values.sunday, @@ -25,9 +25,9 @@ export const formValuesToAutostartRequest = ( values.thursday, values.friday, values.saturday, - ] + ]; - const isEveryDay = days.every((day) => day) + const isEveryDay = days.every((day) => day); const isMonThroughFri = !values.sunday && @@ -37,32 +37,32 @@ export const formValuesToAutostartRequest = ( values.thursday && values.friday && !values.saturday && - !values.sunday + !values.sunday; // Handle special cases, falling through to comma-separation if (isEveryDay) { return { schedule: makeCronString("*"), - } + }; } else if (isMonThroughFri) { return { schedule: makeCronString("1-5"), - } + }; } else { const dow = days.reduce((previous, current, idx) => { if (!current) { - return previous + return previous; } else { - const prefix = previous ? "," : "" - return previous + prefix + idx + const prefix = previous ? "," : ""; + return previous + prefix + idx; } - }, "") + }, ""); return { schedule: makeCronString(dow), - } + }; } -} +}; export const formValuesToTTLRequest = ( values: WorkspaceScheduleFormValues, @@ -73,5 +73,5 @@ export const formValuesToTTLRequest = ( values.autostopEnabled && values.ttl ? values.ttl * 60 * 60 * 1000 : undefined, - } -} + }; +}; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule.test.ts b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule.test.ts index bddeae8e89..765d654797 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule.test.ts +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule.test.ts @@ -1,70 +1,70 @@ -import { emptySchedule, scheduleChanged } from "./schedule" -import { emptyTTL } from "./ttl" +import { emptySchedule, scheduleChanged } from "./schedule"; +import { emptyTTL } from "./ttl"; describe("scheduleChanged", () => { describe("autostart", () => { it("should be true if toggle values are different", () => { - const autostart = { autostartEnabled: true, ...emptySchedule } + const autostart = { autostartEnabled: true, ...emptySchedule }; const formValues = { autostartEnabled: false, ...emptySchedule, autostopEnabled: false, ttl: emptyTTL, - } - expect(scheduleChanged(autostart, formValues)).toBe(true) - }) + }; + expect(scheduleChanged(autostart, formValues)).toBe(true); + }); it("should be true if schedule values are different", () => { - const autostart = { autostartEnabled: true, ...emptySchedule } + const autostart = { autostartEnabled: true, ...emptySchedule }; const formValues = { autostartEnabled: true, ...{ ...emptySchedule, monday: true, startTime: "09:00" }, autostopEnabled: false, ttl: emptyTTL, - } - expect(scheduleChanged(autostart, formValues)).toBe(true) - }) + }; + expect(scheduleChanged(autostart, formValues)).toBe(true); + }); it("should be false if all autostart values are the same", () => { - const autostart = { autostartEnabled: true, ...emptySchedule } + const autostart = { autostartEnabled: true, ...emptySchedule }; const formValues = { autostartEnabled: true, ...emptySchedule, autostopEnabled: false, ttl: emptyTTL, - } - expect(scheduleChanged(autostart, formValues)).toBe(false) - }) - }) + }; + expect(scheduleChanged(autostart, formValues)).toBe(false); + }); + }); describe("autostop", () => { it("should be true if toggle values are different", () => { - const autostop = { autostopEnabled: true, ttl: 1000 } + const autostop = { autostopEnabled: true, ttl: 1000 }; const formValues = { autostartEnabled: false, ...emptySchedule, autostopEnabled: false, ttl: 1000, - } - expect(scheduleChanged(autostop, formValues)).toBe(true) - }) + }; + expect(scheduleChanged(autostop, formValues)).toBe(true); + }); it("should be true if ttl values are different", () => { - const autostop = { autostopEnabled: true, ttl: 1000 } + const autostop = { autostopEnabled: true, ttl: 1000 }; const formValues = { autostartEnabled: false, ...emptySchedule, autostopEnabled: true, ttl: 2000, - } - expect(scheduleChanged(autostop, formValues)).toBe(true) - }) + }; + expect(scheduleChanged(autostop, formValues)).toBe(true); + }); it("should be false if all autostop values are the same", () => { - const autostop = { autostopEnabled: true, ttl: 1000 } + const autostop = { autostopEnabled: true, ttl: 1000 }; const formValues = { autostartEnabled: false, ...emptySchedule, autostopEnabled: true, ttl: 1000, - } - expect(scheduleChanged(autostop, formValues)).toBe(false) - }) - }) -}) + }; + expect(scheduleChanged(autostop, formValues)).toBe(false); + }); + }); +}); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule.ts b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule.ts index a8f96f8a56..b419e1191c 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule.ts +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule.ts @@ -1,34 +1,34 @@ -import * as cronParser from "cron-parser" -import dayjs from "dayjs" -import timezone from "dayjs/plugin/timezone" -import utc from "dayjs/plugin/utc" -import { extractTimezone, stripTimezone } from "../../../utils/schedule" -import { Autostop } from "./ttl" -import { WorkspaceScheduleFormValues } from "./WorkspaceScheduleForm" -import map from "lodash/map" -import some from "lodash/some" +import * as cronParser from "cron-parser"; +import dayjs from "dayjs"; +import timezone from "dayjs/plugin/timezone"; +import utc from "dayjs/plugin/utc"; +import { extractTimezone, stripTimezone } from "../../../utils/schedule"; +import { Autostop } from "./ttl"; +import { WorkspaceScheduleFormValues } from "./WorkspaceScheduleForm"; +import map from "lodash/map"; +import some from "lodash/some"; // REMARK: timezone plugin depends on UTC // // SEE: https://day.js.org/docs/en/timezone/timezone -dayjs.extend(utc) -dayjs.extend(timezone) +dayjs.extend(utc); +dayjs.extend(timezone); export interface AutostartSchedule { - sunday: boolean - monday: boolean - tuesday: boolean - wednesday: boolean - thursday: boolean - friday: boolean - saturday: boolean - startTime: string - timezone: string + sunday: boolean; + monday: boolean; + tuesday: boolean; + wednesday: boolean; + thursday: boolean; + friday: boolean; + saturday: boolean; + startTime: string; + timezone: string; } export type Autostart = { - autostartEnabled: boolean -} & AutostartSchedule + autostartEnabled: boolean; +} & AutostartSchedule; export const emptySchedule = { sunday: false, @@ -41,7 +41,7 @@ export const emptySchedule = { startTime: "", timezone: "", -} +}; export const defaultSchedule = (): AutostartSchedule => ({ sunday: false, @@ -54,20 +54,20 @@ export const defaultSchedule = (): AutostartSchedule => ({ startTime: "09:30", timezone: dayjs.tz.guess(), -}) +}); const transformSchedule = (schedule: string) => { - const timezone = extractTimezone(schedule, dayjs.tz.guess()) + const timezone = extractTimezone(schedule, dayjs.tz.guess()); - const expression = cronParser.parseExpression(stripTimezone(schedule)) + const expression = cronParser.parseExpression(stripTimezone(schedule)); - const HH = expression.fields.hour.join("").padStart(2, "0") - const mm = expression.fields.minute.join("").padStart(2, "0") + const HH = expression.fields.hour.join("").padStart(2, "0"); + const mm = expression.fields.minute.join("").padStart(2, "0"); - const weeklyFlags = [false, false, false, false, false, false, false] + const weeklyFlags = [false, false, false, false, false, false, false]; for (const day of expression.fields.dayOfWeek) { - weeklyFlags[day % 7] = true + weeklyFlags[day % 7] = true; } return { @@ -80,19 +80,19 @@ const transformSchedule = (schedule: string) => { saturday: weeklyFlags[6], startTime: `${HH}:${mm}`, timezone, - } -} + }; +}; export const scheduleToAutostart = (schedule?: string): Autostart => { if (schedule) { return { autostartEnabled: true, ...transformSchedule(schedule), - } + }; } else { - return { autostartEnabled: false, ...emptySchedule } + return { autostartEnabled: false, ...emptySchedule }; } -} +}; export const scheduleChanged = ( initialValues: Autostart | Autostop, @@ -104,4 +104,4 @@ export const scheduleChanged = ( (v: boolean | string, k: keyof typeof initialValues) => formValues[k] !== v, ), - ) + ); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl.ts b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl.ts index b77cbb2957..7118494587 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl.ts +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl.ts @@ -1,13 +1,13 @@ export interface Autostop { - autostopEnabled: boolean - ttl: number + autostopEnabled: boolean; + ttl: number; } -export const emptyTTL = 0 +export const emptyTTL = 0; -const msToHours = (ms: number) => Math.round(ms / (1000 * 60 * 60)) +const msToHours = (ms: number) => Math.round(ms / (1000 * 60 * 60)); export const ttlMsToAutostop = (ttl_ms?: number): Autostop => ttl_ms ? { autostopEnabled: true, ttl: msToHours(ttl_ms) } - : { autostopEnabled: false, ttl: 0 } + : { autostopEnabled: false, ttl: 0 }; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/zones.ts b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/zones.ts index fe2536a7e8..be1ce0e2f7 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/zones.ts +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/zones.ts @@ -1,3 +1,3 @@ -import tzData from "tzdata" +import tzData from "tzdata"; -export const zones: string[] = Object.keys(tzData.zones).sort() +export const zones: string[] = Object.keys(tzData.zones).sort(); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsForm.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsForm.tsx index ee288c24bf..9e4dd79fa8 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsForm.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsForm.tsx @@ -3,27 +3,31 @@ import { FormFooter, FormSection, HorizontalForm, -} from "components/Form/Form" -import { useFormik } from "formik" -import { FC } from "react" -import { useTranslation } from "react-i18next" -import * as Yup from "yup" -import { nameValidator, getFormHelpers, onChangeTrimmed } from "utils/formUtils" -import TextField from "@mui/material/TextField" -import { Workspace } from "api/typesGenerated" +} from "components/Form/Form"; +import { useFormik } from "formik"; +import { FC } from "react"; +import { useTranslation } from "react-i18next"; +import * as Yup from "yup"; +import { + nameValidator, + getFormHelpers, + onChangeTrimmed, +} from "utils/formUtils"; +import TextField from "@mui/material/TextField"; +import { Workspace } from "api/typesGenerated"; export type WorkspaceSettingsFormValues = { - name: string -} + name: string; +}; export const WorkspaceSettingsForm: FC<{ - isSubmitting: boolean - workspace: Workspace - error: unknown - onCancel: () => void - onSubmit: (values: WorkspaceSettingsFormValues) => void + isSubmitting: boolean; + workspace: Workspace; + error: unknown; + onCancel: () => void; + onSubmit: (values: WorkspaceSettingsFormValues) => void; }> = ({ onCancel, onSubmit, workspace, error, isSubmitting }) => { - const { t } = useTranslation("workspaceSettingsPage") + const { t } = useTranslation("workspaceSettingsPage"); const form = useFormik({ onSubmit, @@ -33,11 +37,11 @@ export const WorkspaceSettingsForm: FC<{ validationSchema: Yup.object({ name: nameValidator(t("nameLabel")), }), - }) + }); const getFieldHelpers = getFormHelpers( form, error, - ) + ); return ( @@ -58,5 +62,5 @@ export const WorkspaceSettingsForm: FC<{ - ) -} + ); +}; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx index 0abecb68a2..eb4603ae99 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx @@ -1,55 +1,55 @@ -import { makeStyles } from "@mui/styles" -import { Sidebar } from "./Sidebar" -import { Stack } from "components/Stack/Stack" -import { createContext, FC, Suspense, useContext } from "react" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "../../utils/page" -import { Loader } from "components/Loader/Loader" -import { Outlet, useParams } from "react-router-dom" -import { Margins } from "components/Margins/Margins" -import { getWorkspaceByOwnerAndName } from "api/api" -import { useQuery } from "@tanstack/react-query" +import { makeStyles } from "@mui/styles"; +import { Sidebar } from "./Sidebar"; +import { Stack } from "components/Stack/Stack"; +import { createContext, FC, Suspense, useContext } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "../../utils/page"; +import { Loader } from "components/Loader/Loader"; +import { Outlet, useParams } from "react-router-dom"; +import { Margins } from "components/Margins/Margins"; +import { getWorkspaceByOwnerAndName } from "api/api"; +import { useQuery } from "@tanstack/react-query"; const fetchWorkspaceSettings = async (owner: string, name: string) => { - const workspace = await getWorkspaceByOwnerAndName(owner, name) + const workspace = await getWorkspaceByOwnerAndName(owner, name); return { workspace, - } -} + }; +}; const useWorkspace = (owner: string, name: string) => { return useQuery({ queryKey: ["workspace", name, "settings"], queryFn: () => fetchWorkspaceSettings(owner, name), - }) -} + }); +}; const WorkspaceSettingsContext = createContext< Awaited> | undefined ->(undefined) +>(undefined); export const useWorkspaceSettingsContext = () => { - const context = useContext(WorkspaceSettingsContext) + const context = useContext(WorkspaceSettingsContext); if (!context) { throw new Error( "useWorkspaceSettingsContext must be used within a WorkspaceSettingsContext.Provider", - ) + ); } - return context -} + return context; +}; export const WorkspaceSettingsLayout: FC = () => { - const styles = useStyles() + const styles = useStyles(); const params = useParams() as { - workspace: string - username: string - } - const workspaceName = params.workspace - const username = params.username.replace("@", "") - const { data: settings } = useWorkspace(username, workspaceName) + workspace: string; + username: string; + }; + const workspaceName = params.workspace; + const username = params.username.replace("@", ""); + const { data: settings } = useWorkspace(username, workspaceName); return ( <> @@ -74,8 +74,8 @@ export const WorkspaceSettingsLayout: FC = () => { )} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ wrapper: { @@ -85,4 +85,4 @@ const useStyles = makeStyles((theme) => ({ content: { width: "100%", }, -})) +})); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx index eaf5afaeb1..0aaa510857 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx @@ -1,41 +1,41 @@ -import userEvent from "@testing-library/user-event" +import userEvent from "@testing-library/user-event"; import { renderWithWorkspaceSettingsLayout, waitForLoaderToBeRemoved, -} from "testHelpers/renderHelpers" -import WorkspaceSettingsPage from "./WorkspaceSettingsPage" -import { screen, waitFor, within } from "@testing-library/react" -import * as api from "api/api" -import { MockWorkspace } from "testHelpers/entities" +} from "testHelpers/renderHelpers"; +import WorkspaceSettingsPage from "./WorkspaceSettingsPage"; +import { screen, waitFor, within } from "@testing-library/react"; +import * as api from "api/api"; +import { MockWorkspace } from "testHelpers/entities"; test("Submit the workspace settings page successfully", async () => { // Mock the API calls that loads data jest .spyOn(api, "getWorkspaceByOwnerAndName") - .mockResolvedValueOnce(MockWorkspace) + .mockResolvedValueOnce(MockWorkspace); // Mock the API calls that submit data const patchWorkspaceSpy = jest .spyOn(api, "patchWorkspace") - .mockResolvedValue() + .mockResolvedValue(); // Setup event and rendering - const user = userEvent.setup() + const user = userEvent.setup(); renderWithWorkspaceSettingsLayout(, { route: "/@test-user/test-workspace/settings", path: "/:username/:workspace/settings", // Need this because after submit the user is redirected extraRoutes: [{ path: "/:username/:workspace", element:
}], - }) - await waitForLoaderToBeRemoved() + }); + await waitForLoaderToBeRemoved(); // Fill the form and submit - const form = screen.getByTestId("form") - const name = within(form).getByLabelText("Name") - await user.clear(name) - await user.type(within(form).getByLabelText("Name"), "new-name") - await user.click(within(form).getByRole("button", { name: "Submit" })) + const form = screen.getByTestId("form"); + const name = within(form).getByLabelText("Name"); + await user.clear(name); + await user.type(within(form).getByLabelText("Name"), "new-name"); + await user.click(within(form).getByRole("button", { name: "Submit" })); // Assert that the API calls were made with the correct data await waitFor(() => { expect(patchWorkspaceSpy).toHaveBeenCalledWith(MockWorkspace.id, { name: "new-name", - }) - }) -}) + }); + }); +}); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx index 89b927e526..af7d710b99 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx @@ -1,30 +1,30 @@ -import { Helmet } from "react-helmet-async" -import { useNavigate, useParams } from "react-router-dom" -import { pageTitle } from "utils/page" -import { useWorkspaceSettingsContext } from "./WorkspaceSettingsLayout" -import { WorkspaceSettingsPageView } from "./WorkspaceSettingsPageView" -import { useMutation } from "@tanstack/react-query" -import { displaySuccess } from "components/GlobalSnackbar/utils" -import { patchWorkspace } from "api/api" -import { WorkspaceSettingsFormValues } from "./WorkspaceSettingsForm" +import { Helmet } from "react-helmet-async"; +import { useNavigate, useParams } from "react-router-dom"; +import { pageTitle } from "utils/page"; +import { useWorkspaceSettingsContext } from "./WorkspaceSettingsLayout"; +import { WorkspaceSettingsPageView } from "./WorkspaceSettingsPageView"; +import { useMutation } from "@tanstack/react-query"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; +import { patchWorkspace } from "api/api"; +import { WorkspaceSettingsFormValues } from "./WorkspaceSettingsForm"; const WorkspaceSettingsPage = () => { const params = useParams() as { - workspace: string - username: string - } - const workspaceName = params.workspace - const username = params.username.replace("@", "") - const { workspace } = useWorkspaceSettingsContext() - const navigate = useNavigate() + workspace: string; + username: string; + }; + const workspaceName = params.workspace; + const username = params.username.replace("@", ""); + const { workspace } = useWorkspaceSettingsContext(); + const navigate = useNavigate(); const mutation = useMutation({ mutationFn: (formValues: WorkspaceSettingsFormValues) => patchWorkspace(workspace.id, { name: formValues.name }), onSuccess: (_, formValues) => { - displaySuccess("Workspace updated successfully") - navigate(`/@${username}/${formValues.name}/settings`) + displaySuccess("Workspace updated successfully"); + navigate(`/@${username}/${formValues.name}/settings`); }, - }) + }); return ( <> @@ -40,7 +40,7 @@ const WorkspaceSettingsPage = () => { onSubmit={mutation.mutate} /> - ) -} + ); +}; -export default WorkspaceSettingsPage +export default WorkspaceSettingsPage; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.stories.tsx index eb21ad342f..6d8216902d 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.stories.tsx @@ -1,10 +1,10 @@ -import { ComponentMeta, Story } from "@storybook/react" -import { MockWorkspace } from "testHelpers/entities" +import { ComponentMeta, Story } from "@storybook/react"; +import { MockWorkspace } from "testHelpers/entities"; import { WorkspaceSettingsPageView, WorkspaceSettingsPageViewProps, -} from "./WorkspaceSettingsPageView" -import { action } from "@storybook/addon-actions" +} from "./WorkspaceSettingsPageView"; +import { action } from "@storybook/addon-actions"; export default { title: "pages/WorkspaceSettingsPageView", @@ -15,11 +15,11 @@ export default { workspace: MockWorkspace, onCancel: action("cancel"), }, -} as ComponentMeta +} as ComponentMeta; const Template: Story = (args) => ( -) +); -export const Example = Template.bind({}) -Example.args = {} +export const Example = Template.bind({}); +Example.args = {}; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.tsx index bed0052ee6..87b31d6627 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.tsx @@ -1,17 +1,17 @@ -import { makeStyles } from "@mui/styles" -import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader" -import { ComponentProps, FC } from "react" -import { useTranslation } from "react-i18next" -import { WorkspaceSettingsForm } from "./WorkspaceSettingsForm" -import { Workspace } from "api/typesGenerated" +import { makeStyles } from "@mui/styles"; +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; +import { ComponentProps, FC } from "react"; +import { useTranslation } from "react-i18next"; +import { WorkspaceSettingsForm } from "./WorkspaceSettingsForm"; +import { Workspace } from "api/typesGenerated"; export type WorkspaceSettingsPageViewProps = { - error: unknown - isSubmitting: boolean - workspace: Workspace - onCancel: () => void - onSubmit: ComponentProps["onSubmit"] -} + error: unknown; + isSubmitting: boolean; + workspace: Workspace; + onCancel: () => void; + onSubmit: ComponentProps["onSubmit"]; +}; export const WorkspaceSettingsPageView: FC = ({ onCancel, @@ -20,8 +20,8 @@ export const WorkspaceSettingsPageView: FC = ({ error, workspace, }) => { - const { t } = useTranslation("workspaceSettingsPage") - const styles = useStyles() + const { t } = useTranslation("workspaceSettingsPage"); + const styles = useStyles(); return ( <> @@ -37,11 +37,11 @@ export const WorkspaceSettingsPageView: FC = ({ onSubmit={onSubmit} /> - ) -} + ); +}; const useStyles = makeStyles(() => ({ pageHeader: { paddingTop: 0, }, -})) +})); diff --git a/site/src/pages/WorkspacesPage/LastUsed.tsx b/site/src/pages/WorkspacesPage/LastUsed.tsx index a491b37019..a113250fc8 100644 --- a/site/src/pages/WorkspacesPage/LastUsed.tsx +++ b/site/src/pages/WorkspacesPage/LastUsed.tsx @@ -1,19 +1,19 @@ -import { makeStyles, useTheme } from "@mui/styles" -import { FC } from "react" -import dayjs from "dayjs" -import relativeTime from "dayjs/plugin/relativeTime" -import { colors } from "theme/colors" -import { Stack } from "components/Stack/Stack" -import { Theme } from "@mui/material/styles" +import { makeStyles, useTheme } from "@mui/styles"; +import { FC } from "react"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { colors } from "theme/colors"; +import { Stack } from "components/Stack/Stack"; +import { Theme } from "@mui/material/styles"; -dayjs.extend(relativeTime) +dayjs.extend(relativeTime); -type CircleProps = { color: string; variant?: "solid" | "outlined" } +type CircleProps = { color: string; variant?: "solid" | "outlined" }; const Circle: FC = ({ color, variant = "solid" }) => { - const styles = useCircleStyles({ color, variant }) - return
-} + const styles = useCircleStyles({ color, variant }); + return
; +}; const useCircleStyles = makeStyles((theme) => ({ root: { @@ -25,36 +25,36 @@ const useCircleStyles = makeStyles((theme) => ({ props.variant === "outlined" ? `1px solid ${props.color}` : undefined, borderRadius: 9999, }, -})) +})); interface LastUsedProps { - lastUsedAt: string + lastUsedAt: string; } export const LastUsed: FC = ({ lastUsedAt }) => { - const theme: Theme = useTheme() - const styles = useStyles() - const t = dayjs(lastUsedAt) - const now = dayjs() - let message = t.fromNow() + const theme: Theme = useTheme(); + const styles = useStyles(); + const t = dayjs(lastUsedAt); + const now = dayjs(); + let message = t.fromNow(); let circle: JSX.Element = ( - ) + ); if (t.isAfter(now.subtract(1, "hour"))) { - circle = + circle = ; // Since the agent reports on a 10m interval, // the last_used_at can be inaccurate when recent. - message = "Now" + message = "Now"; } else if (t.isAfter(now.subtract(3, "day"))) { - circle = + circle = ; } else if (t.isAfter(now.subtract(1, "month"))) { - circle = + circle = ; } else if (t.isAfter(now.subtract(100, "year"))) { - circle = + circle = ; } else { // color = theme.palette.error.light - message = "Never" + message = "Never"; } return ( @@ -67,11 +67,11 @@ export const LastUsed: FC = ({ lastUsedAt }) => { {circle} {message} - ) -} + ); +}; const useStyles = makeStyles((theme) => ({ root: { color: theme.palette.text.secondary, }, -})) +})); diff --git a/site/src/pages/WorkspacesPage/WorkspaceHelpTooltip.tsx b/site/src/pages/WorkspacesPage/WorkspaceHelpTooltip.tsx index 21f83e8eb7..fc137b7369 100644 --- a/site/src/pages/WorkspacesPage/WorkspaceHelpTooltip.tsx +++ b/site/src/pages/WorkspacesPage/WorkspaceHelpTooltip.tsx @@ -1,12 +1,12 @@ -import { FC } from "react" +import { FC } from "react"; import { HelpTooltip, HelpTooltipLink, HelpTooltipLinksGroup, HelpTooltipText, HelpTooltipTitle, -} from "components/HelpTooltip/HelpTooltip" -import { docs } from "utils/docs" +} from "components/HelpTooltip/HelpTooltip"; +import { docs } from "utils/docs"; const Language = { workspaceTooltipTitle: "What is a workspace?", @@ -15,7 +15,7 @@ const Language = { workspaceTooltipLink1: "Create Workspaces", workspaceTooltipLink2: "Connect with SSH", workspaceTooltipLink3: "Editors and IDEs", -} +}; export const WorkspaceHelpTooltip: FC = () => { return ( @@ -34,5 +34,5 @@ export const WorkspaceHelpTooltip: FC = () => { - ) -} + ); +}; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx index 9b03d98d80..aaeffadd4e 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx @@ -1,82 +1,82 @@ -import { screen, waitFor, within } from "@testing-library/react" -import { rest } from "msw" -import * as CreateDayString from "utils/createDayString" -import { MockWorkspace, MockWorkspacesResponse } from "testHelpers/entities" +import { screen, waitFor, within } from "@testing-library/react"; +import { rest } from "msw"; +import * as CreateDayString from "utils/createDayString"; +import { MockWorkspace, MockWorkspacesResponse } from "testHelpers/entities"; import { renderWithAuth, waitForLoaderToBeRemoved, -} from "testHelpers/renderHelpers" -import { server } from "testHelpers/server" -import WorkspacesPage from "./WorkspacesPage" -import { i18n } from "i18n" -import userEvent from "@testing-library/user-event" -import * as API from "api/api" -import { Workspace } from "api/typesGenerated" +} from "testHelpers/renderHelpers"; +import { server } from "testHelpers/server"; +import WorkspacesPage from "./WorkspacesPage"; +import { i18n } from "i18n"; +import userEvent from "@testing-library/user-event"; +import * as API from "api/api"; +import { Workspace } from "api/typesGenerated"; -const { t } = i18n +const { t } = i18n; describe("WorkspacesPage", () => { beforeEach(() => { // Mocking the dayjs module within the createDayString file - const mock = jest.spyOn(CreateDayString, "createDayString") - mock.mockImplementation(() => "a minute ago") - }) + const mock = jest.spyOn(CreateDayString, "createDayString"); + mock.mockImplementation(() => "a minute ago"); + }); it("renders an empty workspaces page", async () => { // Given server.use( rest.get("/api/v2/workspaces", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json({ workspaces: [], count: 0 })) + return res(ctx.status(200), ctx.json({ workspaces: [], count: 0 })); }), - ) + ); // When - renderWithAuth() + renderWithAuth(); // Then - const text = t("emptyCreateWorkspaceMessage", { ns: "workspacesPage" }) - await screen.findByText(text) - }) + const text = t("emptyCreateWorkspaceMessage", { ns: "workspacesPage" }); + await screen.findByText(text); + }); it("renders a filled workspaces page", async () => { - renderWithAuth() - await screen.findByText(`${MockWorkspace.name}1`) + renderWithAuth(); + await screen.findByText(`${MockWorkspace.name}1`); const templateDisplayNames = await screen.findAllByText( `${MockWorkspace.template_display_name}`, - ) - expect(templateDisplayNames).toHaveLength(MockWorkspacesResponse.count) - }) + ); + expect(templateDisplayNames).toHaveLength(MockWorkspacesResponse.count); + }); it("deletes only the selected workspaces", async () => { const workspaces = [ { ...MockWorkspace, id: "1" }, { ...MockWorkspace, id: "2" }, { ...MockWorkspace, id: "3" }, - ] + ]; jest .spyOn(API, "getWorkspaces") - .mockResolvedValue({ workspaces, count: workspaces.length }) - const deleteWorkspace = jest.spyOn(API, "deleteWorkspace") - const user = userEvent.setup() - renderWithAuth() - await waitForLoaderToBeRemoved() + .mockResolvedValue({ workspaces, count: workspaces.length }); + const deleteWorkspace = jest.spyOn(API, "deleteWorkspace"); + const user = userEvent.setup(); + renderWithAuth(); + await waitForLoaderToBeRemoved(); - await user.click(getWorkspaceCheckbox(workspaces[0])) - await user.click(getWorkspaceCheckbox(workspaces[1])) - await user.click(screen.getByRole("button", { name: /delete selected/i })) - await user.type(screen.getByLabelText(/type delete to confirm/i), "DELETE") - await user.click(screen.getByTestId("confirm-button")) + await user.click(getWorkspaceCheckbox(workspaces[0])); + await user.click(getWorkspaceCheckbox(workspaces[1])); + await user.click(screen.getByRole("button", { name: /delete selected/i })); + await user.type(screen.getByLabelText(/type delete to confirm/i), "DELETE"); + await user.click(screen.getByTestId("confirm-button")); await waitFor(() => { - expect(deleteWorkspace).toHaveBeenCalledTimes(2) - }) - expect(deleteWorkspace).toHaveBeenCalledWith(workspaces[0].id) - expect(deleteWorkspace).toHaveBeenCalledWith(workspaces[1].id) - }) -}) + expect(deleteWorkspace).toHaveBeenCalledTimes(2); + }); + expect(deleteWorkspace).toHaveBeenCalledWith(workspaces[0].id); + expect(deleteWorkspace).toHaveBeenCalledWith(workspaces[1].id); + }); +}); const getWorkspaceCheckbox = (workspace: Workspace) => { return within(screen.getByTestId(`checkbox-${workspace.id}`)).getByRole( "checkbox", - ) -} + ); +}; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index ad3bf1d00a..65b4262788 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,82 +1,82 @@ -import { usePagination } from "hooks/usePagination" -import { Workspace } from "api/typesGenerated" +import { usePagination } from "hooks/usePagination"; +import { Workspace } from "api/typesGenerated"; import { useDashboard, useIsWorkspaceActionsEnabled, -} from "components/Dashboard/DashboardProvider" -import { FC, useEffect, useState } from "react" -import { Helmet } from "react-helmet-async" -import { pageTitle } from "utils/page" -import { useWorkspacesData, useWorkspaceUpdate } from "./data" -import { WorkspacesPageView } from "./WorkspacesPageView" -import { useOrganizationId, usePermissions } from "hooks" -import { useTemplateFilterMenu, useStatusFilterMenu } from "./filter/menus" -import { useSearchParams } from "react-router-dom" -import { useFilter } from "components/Filter/filter" -import { useUserFilterMenu } from "components/Filter/UserFilter" -import { deleteWorkspace, getWorkspaces } from "api/api" -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" -import Box from "@mui/material/Box" -import { MONOSPACE_FONT_FAMILY } from "theme/constants" -import TextField from "@mui/material/TextField" -import { displayError } from "components/GlobalSnackbar/utils" -import { getErrorMessage } from "api/errors" +} from "components/Dashboard/DashboardProvider"; +import { FC, useEffect, useState } from "react"; +import { Helmet } from "react-helmet-async"; +import { pageTitle } from "utils/page"; +import { useWorkspacesData, useWorkspaceUpdate } from "./data"; +import { WorkspacesPageView } from "./WorkspacesPageView"; +import { useOrganizationId, usePermissions } from "hooks"; +import { useTemplateFilterMenu, useStatusFilterMenu } from "./filter/menus"; +import { useSearchParams } from "react-router-dom"; +import { useFilter } from "components/Filter/filter"; +import { useUserFilterMenu } from "components/Filter/UserFilter"; +import { deleteWorkspace, getWorkspaces } from "api/api"; +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import Box from "@mui/material/Box"; +import { MONOSPACE_FONT_FAMILY } from "theme/constants"; +import TextField from "@mui/material/TextField"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { getErrorMessage } from "api/errors"; const WorkspacesPage: FC = () => { - const [dormantWorkspaces, setDormantWorkspaces] = useState([]) + const [dormantWorkspaces, setDormantWorkspaces] = useState([]); // If we use a useSearchParams for each hook, the values will not be in sync. // So we have to use a single one, centralizing the values, and pass it to // each hook. - const searchParamsResult = useSearchParams() - const pagination = usePagination({ searchParamsResult }) - const filterProps = useWorkspacesFilter({ searchParamsResult, pagination }) + const searchParamsResult = useSearchParams(); + const pagination = usePagination({ searchParamsResult }); + const filterProps = useWorkspacesFilter({ searchParamsResult, pagination }); const { data, error, queryKey, refetch } = useWorkspacesData({ ...pagination, query: filterProps.filter.query, - }) + }); - const experimentEnabled = useIsWorkspaceActionsEnabled() + const experimentEnabled = useIsWorkspaceActionsEnabled(); // If workspace actions are enabled we need to fetch the dormant // workspaces as well. This lets us determine whether we should // show a banner to the user indicating that some of their workspaces // are at risk of being deleted. useEffect(() => { if (experimentEnabled) { - const includesDormant = filterProps.filter.query.includes("dormant_at") + const includesDormant = filterProps.filter.query.includes("dormant_at"); const dormantQuery = includesDormant ? filterProps.filter.query - : filterProps.filter.query + " dormant_at:1970-01-01" + : filterProps.filter.query + " dormant_at:1970-01-01"; if (includesDormant && data) { - setDormantWorkspaces(data.workspaces) + setDormantWorkspaces(data.workspaces); } else { getWorkspaces({ q: dormantQuery }) .then((resp) => { - setDormantWorkspaces(resp.workspaces) + setDormantWorkspaces(resp.workspaces); }) .catch(() => { // TODO - }) + }); } } else { // If the experiment isn't included then we'll pretend // like dormant workspaces don't exist. - setDormantWorkspaces([]) + setDormantWorkspaces([]); } - }, [experimentEnabled, data, filterProps.filter.query]) - const updateWorkspace = useWorkspaceUpdate(queryKey) - const [checkedWorkspaces, setCheckedWorkspaces] = useState([]) - const [isDeletingAll, setIsDeletingAll] = useState(false) - const [urlSearchParams] = searchParamsResult - const { entitlements } = useDashboard() + }, [experimentEnabled, data, filterProps.filter.query]); + const updateWorkspace = useWorkspaceUpdate(queryKey); + const [checkedWorkspaces, setCheckedWorkspaces] = useState([]); + const [isDeletingAll, setIsDeletingAll] = useState(false); + const [urlSearchParams] = searchParamsResult; + const { entitlements } = useDashboard(); const canCheckWorkspaces = - entitlements.features["workspace_batch_actions"].enabled + entitlements.features["workspace_batch_actions"].enabled; // We want to uncheck the selected workspaces always when the url changes // because of filtering or pagination useEffect(() => { - setCheckedWorkspaces([]) - }, [urlSearchParams]) + setCheckedWorkspaces([]); + }, [urlSearchParams]); return ( <> @@ -97,10 +97,10 @@ const WorkspacesPage: FC = () => { onPageChange={pagination.goToPage} filterProps={filterProps} onUpdateWorkspace={(workspace) => { - updateWorkspace.mutate(workspace) + updateWorkspace.mutate(workspace); }} onDeleteAll={() => { - setIsDeletingAll(true) + setIsDeletingAll(true); }} /> @@ -108,55 +108,55 @@ const WorkspacesPage: FC = () => { checkedWorkspaces={checkedWorkspaces} open={isDeletingAll} onClose={() => { - setIsDeletingAll(false) + setIsDeletingAll(false); }} onDelete={async () => { - await refetch() - setCheckedWorkspaces([]) + await refetch(); + setCheckedWorkspaces([]); }} /> - ) -} + ); +}; -export default WorkspacesPage +export default WorkspacesPage; type UseWorkspacesFilterOptions = { - searchParamsResult: ReturnType - pagination: ReturnType -} + searchParamsResult: ReturnType; + pagination: ReturnType; +}; const useWorkspacesFilter = ({ searchParamsResult, pagination, }: UseWorkspacesFilterOptions) => { - const orgId = useOrganizationId() + const orgId = useOrganizationId(); const filter = useFilter({ initialValue: `owner:me`, searchParamsResult, onUpdate: () => { - pagination.goToPage(1) + pagination.goToPage(1); }, - }) - const permissions = usePermissions() - const canFilterByUser = permissions.viewDeploymentValues + }); + const permissions = usePermissions(); + const canFilterByUser = permissions.viewDeploymentValues; const userMenu = useUserFilterMenu({ value: filter.values.owner, onChange: (option) => filter.update({ ...filter.values, owner: option?.value }), enabled: canFilterByUser, - }) + }); const templateMenu = useTemplateFilterMenu({ orgId, value: filter.values.template, onChange: (option) => filter.update({ ...filter.values, template: option?.value }), - }) + }); const statusMenu = useStatusFilterMenu({ value: filter.values.status, onChange: (option) => filter.update({ ...filter.values, status: option?.value }), - }) + }); return { filter, @@ -165,8 +165,8 @@ const useWorkspacesFilter = ({ template: templateMenu, status: statusMenu, }, - } -} + }; +}; const BatchDeleteConfirmation = ({ checkedWorkspaces, @@ -174,47 +174,47 @@ const BatchDeleteConfirmation = ({ onClose, onDelete, }: { - checkedWorkspaces: Workspace[] - open: boolean - onClose: () => void - onDelete: () => void + checkedWorkspaces: Workspace[]; + open: boolean; + onClose: () => void; + onDelete: () => void; }) => { - const [confirmValue, setConfirmValue] = useState("") - const [confirmError, setConfirmError] = useState(false) - const [isDeleting, setIsDeleting] = useState(false) + const [confirmValue, setConfirmValue] = useState(""); + const [confirmError, setConfirmError] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); const close = () => { if (isDeleting) { - return + return; } - onClose() - setConfirmValue("") - setConfirmError(false) - setIsDeleting(false) - } + onClose(); + setConfirmValue(""); + setConfirmError(false); + setIsDeleting(false); + }; const confirmDeletion = async () => { - setConfirmError(false) + setConfirmError(false); if (confirmValue !== "DELETE") { - setConfirmError(true) - return + setConfirmError(true); + return; } try { - setIsDeleting(true) - await Promise.all(checkedWorkspaces.map((w) => deleteWorkspace(w.id))) + setIsDeleting(true); + await Promise.all(checkedWorkspaces.map((w) => deleteWorkspace(w.id))); } catch (e) { displayError( "Error on deleting workspaces", getErrorMessage(e, "An error occurred while deleting the workspaces"), - ) + ); } finally { - close() - onDelete() + close(); + onDelete(); } - } + }; return ( { - onClose() - setConfirmValue("") - setConfirmError(false) + onClose(); + setConfirmValue(""); + setConfirmError(false); }} title={`Delete ${checkedWorkspaces?.length} ${ checkedWorkspaces.length === 1 ? "workspace" : "workspaces" @@ -233,8 +233,8 @@ const BatchDeleteConfirmation = ({ description={
{ - e.preventDefault() - await confirmDeletion() + e.preventDefault(); + await confirmDeletion(); }} > @@ -263,7 +263,7 @@ const BatchDeleteConfirmation = ({ placeholder="Type DELETE to confirm" sx={{ mt: 2 }} onChange={(e) => { - setConfirmValue(e.currentTarget.value) + setConfirmValue(e.currentTarget.value); }} error={confirmError} helperText={confirmError && "Please type DELETE to confirm"} @@ -271,5 +271,5 @@ const BatchDeleteConfirmation = ({ } /> - ) -} + ); +}; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index 42f9b83e5a..7b308cec65 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -1,12 +1,12 @@ -import { Meta, StoryObj } from "@storybook/react" -import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils" -import dayjs from "dayjs" -import uniqueId from "lodash/uniqueId" +import { Meta, StoryObj } from "@storybook/react"; +import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils"; +import dayjs from "dayjs"; +import uniqueId from "lodash/uniqueId"; import { Workspace, WorkspaceStatus, WorkspaceStatuses, -} from "api/typesGenerated" +} from "api/typesGenerated"; import { MockWorkspace, MockAppearance, @@ -16,11 +16,14 @@ import { mockApiError, MockUser, MockPendingProvisionerJob, -} from "testHelpers/entities" -import { WorkspacesPageView } from "./WorkspacesPageView" -import { DashboardProviderContext } from "components/Dashboard/DashboardProvider" -import { ComponentProps } from "react" -import { MockMenu, getDefaultFilterProps } from "components/Filter/storyHelpers" +} from "testHelpers/entities"; +import { WorkspacesPageView } from "./WorkspacesPageView"; +import { DashboardProviderContext } from "components/Dashboard/DashboardProvider"; +import { ComponentProps } from "react"; +import { + MockMenu, + getDefaultFilterProps, +} from "components/Filter/storyHelpers"; const createWorkspace = ( status: WorkspaceStatus, @@ -40,12 +43,12 @@ const createWorkspace = ( : MockWorkspace.latest_build.job, }, last_used_at: lastUsedAt, - } -} + }; +}; // This is type restricted to prevent future statuses from slipping // through the cracks unchecked! -const workspaces = WorkspaceStatuses.map((status) => createWorkspace(status)) +const workspaces = WorkspaceStatuses.map((status) => createWorkspace(status)); // Additional Workspaces depending on time const additionalWorkspaces: Record = { @@ -60,21 +63,21 @@ const additionalWorkspaces: Record = { true, dayjs().subtract(1, "month").subtract(4, "day").toString(), ), -} +}; const allWorkspaces = [ ...Object.values(workspaces), ...Object.values(additionalWorkspaces), -] +]; const MockedAppearance = { config: MockAppearance, preview: false, setPreview: () => null, save: () => null, -} +}; -type FilterProps = ComponentProps["filterProps"] +type FilterProps = ComponentProps["filterProps"]; const defaultFilterProps = getDefaultFilterProps({ query: "owner:me", @@ -88,7 +91,7 @@ const defaultFilterProps = getDefaultFilterProps({ template: undefined, status: undefined, }, -}) +}); const meta: Meta = { title: "pages/WorkspacesPageView", @@ -113,24 +116,24 @@ const meta: Meta = { ), ], -} +}; -export default meta -type Story = StoryObj +export default meta; +type Story = StoryObj; export const AllStates: Story = { args: { workspaces: allWorkspaces, count: allWorkspaces.length, }, -} +}; export const OwnerHasNoWorkspaces: Story = { args: { workspaces: [], count: 0, }, -} +}; export const NoSearchResults: Story = { args: { @@ -145,7 +148,7 @@ export const NoSearchResults: Story = { }, count: 0, }, -} +}; export const UnhealthyWorkspace: Story = { args: { @@ -159,10 +162,10 @@ export const UnhealthyWorkspace: Story = { }, ], }, -} +}; export const Error: Story = { args: { error: mockApiError({ message: "Something went wrong" }), }, -} +}; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 4fb722292e..d5daa5653c 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -1,30 +1,30 @@ -import Link from "@mui/material/Link" -import { Workspace } from "api/typesGenerated" -import { Maybe } from "components/Conditionals/Maybe" -import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase" -import { ComponentProps, FC } from "react" -import { Link as RouterLink } from "react-router-dom" -import { Margins } from "components/Margins/Margins" +import Link from "@mui/material/Link"; +import { Workspace } from "api/typesGenerated"; +import { Maybe } from "components/Conditionals/Maybe"; +import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase"; +import { ComponentProps, FC } from "react"; +import { Link as RouterLink } from "react-router-dom"; +import { Margins } from "components/Margins/Margins"; import { PageHeader, PageHeaderSubtitle, PageHeaderTitle, -} from "components/PageHeader/PageHeader" -import { Stack } from "components/Stack/Stack" -import { WorkspaceHelpTooltip } from "./WorkspaceHelpTooltip" -import { WorkspacesTable } from "pages/WorkspacesPage/WorkspacesTable" -import { useLocalStorage } from "hooks" -import { DormantWorkspaceBanner, Count } from "components/WorkspaceDeletion" -import { ErrorAlert } from "components/Alert/ErrorAlert" -import { WorkspacesFilter } from "./filter/filter" -import { hasError, isApiValidationError } from "api/errors" +} from "components/PageHeader/PageHeader"; +import { Stack } from "components/Stack/Stack"; +import { WorkspaceHelpTooltip } from "./WorkspaceHelpTooltip"; +import { WorkspacesTable } from "pages/WorkspacesPage/WorkspacesTable"; +import { useLocalStorage } from "hooks"; +import { DormantWorkspaceBanner, Count } from "components/WorkspaceDeletion"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { WorkspacesFilter } from "./filter/filter"; +import { hasError, isApiValidationError } from "api/errors"; import { PaginationStatus, TableToolbar, -} from "components/TableToolbar/TableToolbar" -import Box from "@mui/material/Box" -import Button from "@mui/material/Button" -import DeleteOutlined from "@mui/icons-material/DeleteOutlined" +} from "components/TableToolbar/TableToolbar"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import DeleteOutlined from "@mui/icons-material/DeleteOutlined"; export const Language = { pageTitle: "Workspaces", @@ -33,22 +33,22 @@ export const Language = { runningWorkspacesButton: "Running workspaces", createANewWorkspace: `Create a new workspace from a `, template: "Template", -} +}; export interface WorkspacesPageViewProps { - error: unknown - workspaces?: Workspace[] - dormantWorkspaces?: Workspace[] - checkedWorkspaces: Workspace[] - count?: number - filterProps: ComponentProps - page: number - limit: number - onPageChange: (page: number) => void - onUpdateWorkspace: (workspace: Workspace) => void - onCheckChange: (checkedWorkspaces: Workspace[]) => void - onDeleteAll: () => void - canCheckWorkspaces: boolean + error: unknown; + workspaces?: Workspace[]; + dormantWorkspaces?: Workspace[]; + checkedWorkspaces: Workspace[]; + count?: number; + filterProps: ComponentProps; + page: number; + limit: number; + onPageChange: (page: number) => void; + onUpdateWorkspace: (workspace: Workspace) => void; + onCheckChange: (checkedWorkspaces: Workspace[]) => void; + onDeleteAll: () => void; + canCheckWorkspaces: boolean; } export const WorkspacesPageView: FC< @@ -68,14 +68,14 @@ export const WorkspacesPageView: FC< onDeleteAll, canCheckWorkspaces, }) => { - const { saveLocal } = useLocalStorage() + const { saveLocal } = useLocalStorage(); const workspacesDeletionScheduled = dormantWorkspaces ?.filter((workspace) => workspace.deleting_at) - .map((workspace) => workspace.id) + .map((workspace) => workspace.id); const hasDormantWorkspace = - dormantWorkspaces !== undefined && dormantWorkspaces.length > 0 + dormantWorkspaces !== undefined && dormantWorkspaces.length > 0; return ( @@ -162,5 +162,5 @@ export const WorkspacesPageView: FC< /> )} - ) -} + ); +}; diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 99124eb234..ac412e78d0 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -1,51 +1,51 @@ -import Table from "@mui/material/Table" -import TableBody from "@mui/material/TableBody" -import TableCell from "@mui/material/TableCell" -import TableContainer from "@mui/material/TableContainer" -import TableHead from "@mui/material/TableHead" -import TableRow from "@mui/material/TableRow" -import { Workspace } from "api/typesGenerated" -import { FC, ReactNode } from "react" -import { TableEmpty } from "components/TableEmpty/TableEmpty" -import { useTranslation } from "react-i18next" +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import { Workspace } from "api/typesGenerated"; +import { FC, ReactNode } from "react"; +import { TableEmpty } from "components/TableEmpty/TableEmpty"; +import { useTranslation } from "react-i18next"; import { TableLoaderSkeleton, TableRowSkeleton, -} from "components/TableLoader/TableLoader" -import AddOutlined from "@mui/icons-material/AddOutlined" -import Button from "@mui/material/Button" -import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" -import { Link as RouterLink, useNavigate } from "react-router-dom" -import { makeStyles } from "@mui/styles" +} from "components/TableLoader/TableLoader"; +import AddOutlined from "@mui/icons-material/AddOutlined"; +import Button from "@mui/material/Button"; +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; +import { makeStyles } from "@mui/styles"; import { HelpTooltip, HelpTooltipText, HelpTooltipTitle, -} from "components/HelpTooltip/HelpTooltip" -import InfoIcon from "@mui/icons-material/InfoOutlined" -import { colors } from "theme/colors" -import { useClickableTableRow } from "hooks/useClickableTableRow" -import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight" -import Box from "@mui/material/Box" -import { AvatarData } from "components/AvatarData/AvatarData" -import { Avatar } from "components/Avatar/Avatar" -import { Stack } from "components/Stack/Stack" -import { LastUsed } from "pages/WorkspacesPage/LastUsed" -import { WorkspaceOutdatedTooltip } from "components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip" -import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge" -import { getDisplayWorkspaceTemplateName } from "utils/workspace" -import Checkbox from "@mui/material/Checkbox" -import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton" -import Skeleton from "@mui/material/Skeleton" +} from "components/HelpTooltip/HelpTooltip"; +import InfoIcon from "@mui/icons-material/InfoOutlined"; +import { colors } from "theme/colors"; +import { useClickableTableRow } from "hooks/useClickableTableRow"; +import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; +import Box from "@mui/material/Box"; +import { AvatarData } from "components/AvatarData/AvatarData"; +import { Avatar } from "components/Avatar/Avatar"; +import { Stack } from "components/Stack/Stack"; +import { LastUsed } from "pages/WorkspacesPage/LastUsed"; +import { WorkspaceOutdatedTooltip } from "components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; +import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"; +import { getDisplayWorkspaceTemplateName } from "utils/workspace"; +import Checkbox from "@mui/material/Checkbox"; +import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"; +import Skeleton from "@mui/material/Skeleton"; export interface WorkspacesTableProps { - workspaces?: Workspace[] - checkedWorkspaces: Workspace[] - error?: unknown - isUsingFilter: boolean - onUpdateWorkspace: (workspace: Workspace) => void - onCheckChange: (checkedWorkspaces: Workspace[]) => void - canCheckWorkspaces: boolean + workspaces?: Workspace[]; + checkedWorkspaces: Workspace[]; + error?: unknown; + isUsingFilter: boolean; + onUpdateWorkspace: (workspace: Workspace) => void; + onCheckChange: (checkedWorkspaces: Workspace[]) => void; + canCheckWorkspaces: boolean; } export const WorkspacesTable: FC = ({ @@ -56,8 +56,8 @@ export const WorkspacesTable: FC = ({ onCheckChange, canCheckWorkspaces, }) => { - const { t } = useTranslation("workspacesPage") - const styles = useStyles() + const { t } = useTranslation("workspacesPage"); + const styles = useStyles(); return ( @@ -76,13 +76,13 @@ export const WorkspacesTable: FC = ({ size="small" onChange={(_, checked) => { if (!workspaces) { - return + return; } if (!checked) { - onCheckChange([]) + onCheckChange([]); } else { - onCheckChange(workspaces) + onCheckChange(workspaces); } }} /> @@ -134,7 +134,7 @@ export const WorkspacesTable: FC = ({ workspaces.map((workspace) => { const checked = checkedWorkspaces.some( (w) => w.id === workspace.id, - ) + ); return ( = ({ disabled={cantBeChecked(workspace)} checked={checked} onClick={(e) => { - e.stopPropagation() + e.stopPropagation(); }} onChange={(e) => { if (e.currentTarget.checked) { - onCheckChange([...checkedWorkspaces, workspace]) + onCheckChange([...checkedWorkspaces, workspace]); } else { onCheckChange( checkedWorkspaces.filter( (w) => w.id !== workspace.id, ), - ) + ); } }} /> @@ -181,7 +181,7 @@ export const WorkspacesTable: FC = ({ templateName={workspace.template_name} templateId={workspace.template_id} onUpdateVersion={() => { - onUpdateWorkspace(workspace) + onUpdateWorkspace(workspace); }} /> )} @@ -236,24 +236,24 @@ export const WorkspacesTable: FC = ({
- ) + ); })} - ) -} + ); +}; const WorkspacesRow: FC<{ - workspace: Workspace - children: ReactNode - checked: boolean + workspace: Workspace; + children: ReactNode; + checked: boolean; }> = ({ workspace, children, checked }) => { - const navigate = useNavigate() - const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}` + const navigate = useNavigate(); + const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`; const clickable = useClickableTableRow(() => { - navigate(workspacePageLink) - }) + navigate(workspacePageLink); + }); return ( {children} - ) -} + ); +}; export const UnhealthyTooltip = () => { - const styles = useUnhealthyTooltipStyles() + const styles = useUnhealthyTooltipStyles(); return ( { Your workspace is running but some agents are unhealthy. - ) -} + ); +}; const TableLoader = ({ canCheckWorkspaces, }: { - canCheckWorkspaces: boolean + canCheckWorkspaces: boolean; }) => { return ( @@ -317,12 +317,12 @@ const TableLoader = ({ - ) -} + ); +}; const cantBeChecked = (workspace: Workspace) => { - return ["deleting", "pending"].includes(workspace.latest_build.status) -} + return ["deleting", "pending"].includes(workspace.latest_build.status); +}; const useUnhealthyTooltipStyles = makeStyles(() => ({ unhealthyIcon: { @@ -336,7 +336,7 @@ const useUnhealthyTooltipStyles = makeStyles(() => ({ opacity: 1, }, }, -})) +})); const useStyles = makeStyles((theme) => ({ withImage: { @@ -353,4 +353,4 @@ const useStyles = makeStyles((theme) => ({ maxWidth: "100%", }, }, -})) +})); diff --git a/site/src/pages/WorkspacesPage/data.ts b/site/src/pages/WorkspacesPage/data.ts index e3d71ab808..67b9b48673 100644 --- a/site/src/pages/WorkspacesPage/data.ts +++ b/site/src/pages/WorkspacesPage/data.ts @@ -3,31 +3,31 @@ import { useMutation, useQuery, useQueryClient, -} from "@tanstack/react-query" -import { getWorkspaces, updateWorkspaceVersion } from "api/api" -import { getErrorMessage } from "api/errors" +} from "@tanstack/react-query"; +import { getWorkspaces, updateWorkspaceVersion } from "api/api"; +import { getErrorMessage } from "api/errors"; import { Workspace, WorkspaceBuild, WorkspacesResponse, -} from "api/typesGenerated" -import { displayError } from "components/GlobalSnackbar/utils" -import { useState } from "react" -import { useTranslation } from "react-i18next" +} from "api/typesGenerated"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; type UseWorkspacesDataParams = { - page: number - limit: number - query: string -} + page: number; + limit: number; + query: string; +}; export const useWorkspacesData = ({ page, limit, query, }: UseWorkspacesDataParams) => { - const queryKey = ["workspaces", query, page] - const [shouldRefetch, setShouldRefetch] = useState(true) + const queryKey = ["workspaces", query, page]; + const [shouldRefetch, setShouldRefetch] = useState(true); const result = useQuery({ queryKey, queryFn: () => @@ -37,47 +37,47 @@ export const useWorkspacesData = ({ offset: page <= 0 ? 0 : (page - 1) * limit, }), onSuccess: () => { - setShouldRefetch(true) + setShouldRefetch(true); }, onError: () => { - setShouldRefetch(false) + setShouldRefetch(false); }, refetchInterval: shouldRefetch ? 5_000 : undefined, - }) + }); return { ...result, queryKey, - } -} + }; +}; export const useWorkspaceUpdate = (queryKey: QueryKey) => { - const queryClient = useQueryClient() - const { t } = useTranslation("workspacesPage") + const queryClient = useQueryClient(); + const { t } = useTranslation("workspacesPage"); return useMutation({ mutationFn: updateWorkspaceVersion, onMutate: async (workspace) => { - await queryClient.cancelQueries({ queryKey }) + await queryClient.cancelQueries({ queryKey }); queryClient.setQueryData(queryKey, (oldResponse) => { if (oldResponse) { - return assignPendingStatus(oldResponse, workspace) + return assignPendingStatus(oldResponse, workspace); } - }) + }); }, onSuccess: (workspaceBuild) => { queryClient.setQueryData(queryKey, (oldResponse) => { if (oldResponse) { - return assignLatestBuild(oldResponse, workspaceBuild) + return assignLatestBuild(oldResponse, workspaceBuild); } - }) + }); }, onError: (error) => { - const message = getErrorMessage(error, t("updateVersionError")) - displayError(message) + const message = getErrorMessage(error, t("updateVersionError")); + displayError(message); }, - }) -} + }); +}; const assignLatestBuild = ( oldResponse: WorkspacesResponse, @@ -90,13 +90,13 @@ const assignLatestBuild = ( return { ...workspace, latest_build: build, - } + }; } - return workspace + return workspace; }), - } -} + }; +}; const assignPendingStatus = ( oldResponse: WorkspacesResponse, @@ -116,10 +116,10 @@ const assignPendingStatus = ( status: "pending", }, }, - } as Workspace + } as Workspace; } - return workspaceItem + return workspaceItem; }), - } -} + }; +}; diff --git a/site/src/pages/WorkspacesPage/filter/filter.tsx b/site/src/pages/WorkspacesPage/filter/filter.tsx index 2a1b519a0c..355598a09c 100644 --- a/site/src/pages/WorkspacesPage/filter/filter.tsx +++ b/site/src/pages/WorkspacesPage/filter/filter.tsx @@ -1,10 +1,10 @@ -import { FC } from "react" -import Box from "@mui/material/Box" -import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider" -import { Avatar, AvatarProps } from "components/Avatar/Avatar" -import { Palette, PaletteColor } from "@mui/material/styles" -import { TemplateFilterMenu, StatusFilterMenu } from "./menus" -import { TemplateOption, StatusOption } from "./options" +import { FC } from "react"; +import Box from "@mui/material/Box"; +import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider"; +import { Avatar, AvatarProps } from "components/Avatar/Avatar"; +import { Palette, PaletteColor } from "@mui/material/styles"; +import { TemplateFilterMenu, StatusFilterMenu } from "./menus"; +import { TemplateOption, StatusOption } from "./options"; import { Filter, FilterMenu, @@ -13,10 +13,10 @@ import { OptionItem, SearchFieldSkeleton, useFilter, -} from "components/Filter/filter" -import { UserFilterMenu, UserMenu } from "components/Filter/UserFilter" -import { workspaceFilterQuery } from "utils/filters" -import { docs } from "utils/docs" +} from "components/Filter/filter"; +import { UserFilterMenu, UserMenu } from "components/Filter/UserFilter"; +import { workspaceFilterQuery } from "utils/filters"; +import { docs } from "utils/docs"; const PRESET_FILTERS = [ { query: workspaceFilterQuery.me, name: "My workspaces" }, @@ -29,27 +29,27 @@ const PRESET_FILTERS = [ query: workspaceFilterQuery.failed, name: "Failed workspaces", }, -] +]; export const WorkspacesFilter = ({ filter, error, menus, }: { - filter: ReturnType - error?: unknown + filter: ReturnType; + error?: unknown; menus: { - user?: UserFilterMenu - template: TemplateFilterMenu - status: StatusFilterMenu - } + user?: UserFilterMenu; + template: TemplateFilterMenu; + status: StatusFilterMenu; + }; }) => { - const presets = [...PRESET_FILTERS] + const presets = [...PRESET_FILTERS]; if (useIsWorkspaceActionsEnabled()) { presets.push({ query: workspaceFilterQuery.dormant, name: "Dormant workspaces", - }) + }); } return ( @@ -75,8 +75,8 @@ export const WorkspacesFilter = ({ } /> - ) -} + ); +}; const TemplateMenu = (menu: TemplateFilterMenu) => { return ( @@ -93,15 +93,15 @@ const TemplateMenu = (menu: TemplateFilterMenu) => { > {(itemProps) => } - ) -} + ); +}; const TemplateOptionItem = ({ option, isSelected, }: { - option: TemplateOption - isSelected?: boolean + option: TemplateOption; + isSelected?: boolean; }) => { return ( } /> - ) -} + ); +}; const TemplateAvatar: FC< AvatarProps & { templateName: string; icon?: string } @@ -125,8 +125,8 @@ const TemplateAvatar: FC< ) : ( {templateName} - ) -} + ); +}; const StatusMenu = (menu: StatusFilterMenu) => { return ( @@ -143,15 +143,15 @@ const StatusMenu = (menu: StatusFilterMenu) => { > {(itemProps) => } - ) -} + ); +}; const StatusOptionItem = ({ option, isSelected, }: { - option: StatusOption - isSelected?: boolean + option: StatusOption; + isSelected?: boolean; }) => { return ( } isSelected={isSelected} /> - ) -} + ); +}; const StatusIndicator: FC<{ option: StatusOption }> = ({ option }) => { return ( @@ -173,5 +173,5 @@ const StatusIndicator: FC<{ option: StatusOption }> = ({ option }) => { (theme.palette[option.color as keyof Palette] as PaletteColor).light, }} /> - ) -} + ); +}; diff --git a/site/src/pages/WorkspacesPage/filter/menus.ts b/site/src/pages/WorkspacesPage/filter/menus.ts index 6d77cbcd8e..890923ef9b 100644 --- a/site/src/pages/WorkspacesPage/filter/menus.ts +++ b/site/src/pages/WorkspacesPage/filter/menus.ts @@ -1,8 +1,8 @@ -import { StatusOption, TemplateOption } from "./options" -import { getTemplates } from "api/api" -import { WorkspaceStatuses } from "api/typesGenerated" -import { getDisplayWorkspaceStatus } from "utils/workspace" -import { UseFilterMenuOptions, useFilterMenu } from "components/Filter/menu" +import { StatusOption, TemplateOption } from "./options"; +import { getTemplates } from "api/api"; +import { WorkspaceStatuses } from "api/typesGenerated"; +import { getDisplayWorkspaceStatus } from "utils/workspace"; +import { UseFilterMenuOptions, useFilterMenu } from "components/Filter/menu"; export const useTemplateFilterMenu = ({ value, @@ -17,8 +17,8 @@ export const useTemplateFilterMenu = ({ value, id: "template", getSelectedOption: async () => { - const templates = await getTemplates(orgId) - const template = templates.find((template) => template.name === value) + const templates = await getTemplates(orgId); + const template = templates.find((template) => template.name === value); if (template) { return { label: @@ -27,41 +27,41 @@ export const useTemplateFilterMenu = ({ : template.name, value: template.name, icon: template.icon, - } + }; } - return null + return null; }, getOptions: async (query) => { - const templates = await getTemplates(orgId) + const templates = await getTemplates(orgId); const filteredTemplates = templates.filter( (template) => template.name.toLowerCase().includes(query.toLowerCase()) || template.display_name.toLowerCase().includes(query.toLowerCase()), - ) + ); return filteredTemplates.map((template) => ({ label: template.display_name !== "" ? template.display_name : template.name, value: template.name, icon: template.icon, - })) + })); }, - }) -} + }); +}; -export type TemplateFilterMenu = ReturnType +export type TemplateFilterMenu = ReturnType; export const useStatusFilterMenu = ({ value, onChange, }: Pick, "value" | "onChange">) => { const statusOptions = WorkspaceStatuses.map((status) => { - const display = getDisplayWorkspaceStatus(status) + const display = getDisplayWorkspaceStatus(status); return { label: display.text, value: status, color: display.type ?? "warning", - } as StatusOption - }) + } as StatusOption; + }); return useFilterMenu({ onChange, value, @@ -69,7 +69,7 @@ export const useStatusFilterMenu = ({ getSelectedOption: async () => statusOptions.find((option) => option.value === value) ?? null, getOptions: async () => statusOptions, - }) -} + }); +}; -export type StatusFilterMenu = ReturnType +export type StatusFilterMenu = ReturnType; diff --git a/site/src/pages/WorkspacesPage/filter/options.ts b/site/src/pages/WorkspacesPage/filter/options.ts index 2e1b112248..5266e4cdb9 100644 --- a/site/src/pages/WorkspacesPage/filter/options.ts +++ b/site/src/pages/WorkspacesPage/filter/options.ts @@ -1,9 +1,9 @@ -import { BaseOption } from "components/Filter/options" +import { BaseOption } from "components/Filter/options"; export type StatusOption = BaseOption & { - color: string -} + color: string; +}; export type TemplateOption = BaseOption & { - icon?: string -} + icon?: string; +}; diff --git a/site/src/pages/index.tsx b/site/src/pages/index.tsx index 9759d739e0..9753a6c840 100644 --- a/site/src/pages/index.tsx +++ b/site/src/pages/index.tsx @@ -1,8 +1,8 @@ -import { FC } from "react" -import { Navigate } from "react-router-dom" +import { FC } from "react"; +import { Navigate } from "react-router-dom"; const IndexPage: FC = () => { - return -} + return ; +}; -export default IndexPage +export default IndexPage; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index c5b272c0e4..177fedb934 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1,20 +1,20 @@ -import { withDefaultFeatures, GetLicensesResponse } from "api/api" -import { FieldError } from "api/errors" -import { everyOneGroup } from "utils/groups" -import * as Types from "api/types" -import * as TypesGen from "api/typesGenerated" -import range from "lodash/range" -import { Permissions } from "xServices/auth/authXService" -import { TemplateVersionFiles } from "utils/templateVersion" -import { FileTree } from "utils/filetree" -import { ProxyLatencyReport } from "contexts/useProxyLatency" +import { withDefaultFeatures, GetLicensesResponse } from "api/api"; +import { FieldError } from "api/errors"; +import { everyOneGroup } from "utils/groups"; +import * as Types from "api/types"; +import * as TypesGen from "api/typesGenerated"; +import range from "lodash/range"; +import { Permissions } from "xServices/auth/authXService"; +import { TemplateVersionFiles } from "utils/templateVersion"; +import { FileTree } from "utils/filetree"; +import { ProxyLatencyReport } from "contexts/useProxyLatency"; export const MockOrganization: TypesGen.Organization = { id: "fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0", name: "Test Organization", created_at: "", updated_at: "", -} +}; export const MockTemplateDAUResponse: TypesGen.DAUsResponse = { tz_hour_offset: 0, @@ -23,7 +23,7 @@ export const MockTemplateDAUResponse: TypesGen.DAUsResponse = { { date: "2022-08-29T00:00:00Z", amount: 2 }, { date: "2022-08-30T00:00:00Z", amount: 1 }, ], -} +}; export const MockDeploymentDAUResponse: TypesGen.DAUsResponse = { tz_hour_offset: 0, entries: [ @@ -31,14 +31,14 @@ export const MockDeploymentDAUResponse: TypesGen.DAUsResponse = { { date: "2022-08-29T00:00:00Z", amount: 2 }, { date: "2022-08-30T00:00:00Z", amount: 1 }, ], -} +}; export const MockSessionToken: TypesGen.LoginWithPasswordResponse = { session_token: "my-session-token", -} +}; export const MockAPIKey: TypesGen.GenerateAPIKeyResponse = { key: "my-api-key", -} +}; export const MockToken: TypesGen.APIKeyWithOwner = { id: "tBoVE3dqLl", @@ -52,7 +52,7 @@ export const MockToken: TypesGen.APIKeyWithOwner = { lifetime_seconds: 2592000, token_name: "token-one", username: "admin", -} +}; export const MockTokens: TypesGen.APIKeyWithOwner[] = [ MockToken, @@ -69,7 +69,7 @@ export const MockTokens: TypesGen.APIKeyWithOwner[] = [ token_name: "token-two", username: "admin", }, -] +]; export const MockPrimaryWorkspaceProxy: TypesGen.WorkspaceProxy = { id: "4aa23000-526a-481f-a007-0f20b98b1e12", @@ -88,7 +88,7 @@ export const MockPrimaryWorkspaceProxy: TypesGen.WorkspaceProxy = { status: "ok", checked_at: new Date().toISOString(), }, -} +}; export const MockHealthyWildWorkspaceProxy: TypesGen.WorkspaceProxy = { id: "5e2c1ab7-479b-41a9-92ce-aa85625de52c", @@ -107,7 +107,7 @@ export const MockHealthyWildWorkspaceProxy: TypesGen.WorkspaceProxy = { status: "ok", checked_at: new Date().toISOString(), }, -} +}; export const MockUnhealthyWildWorkspaceProxy: TypesGen.WorkspaceProxy = { id: "8444931c-0247-4171-842a-569d9f9cbadb", @@ -130,7 +130,7 @@ export const MockUnhealthyWildWorkspaceProxy: TypesGen.WorkspaceProxy = { }, checked_at: new Date().toISOString(), }, -} +}; export const MockWorkspaceProxies: TypesGen.WorkspaceProxy[] = [ MockPrimaryWorkspaceProxy, @@ -154,13 +154,13 @@ export const MockWorkspaceProxies: TypesGen.WorkspaceProxy[] = [ checked_at: new Date().toISOString(), }, }, -] +]; export const MockProxyLatencies: Record = { ...MockWorkspaceProxies.reduce( (acc, proxy) => { if (!proxy.healthy) { - return acc + return acc; } acc[proxy.id] = { // Make one of them inaccurate. @@ -180,19 +180,19 @@ export const MockProxyLatencies: Record = { 100) % 250, at: new Date(), - } - return acc + }; + return acc; }, {} as Record, ), -} +}; export const MockBuildInfo: TypesGen.BuildInfoResponse = { external_url: "file:///mock-url", version: "v99.999.9999+c9cdf14", dashboard_url: "https:///mock-url", workspace_proxy: false, -} +}; export const MockSupportLinks: TypesGen.LinkConfig[] = [ { @@ -211,38 +211,38 @@ export const MockSupportLinks: TypesGen.LinkConfig[] = [ "https://github.com/coder/coder/issues/new?labels=needs+grooming&body={CODER_BUILD_INFO}", icon: "", }, -] +]; export const MockUpdateCheck: TypesGen.UpdateCheckResponse = { current: true, url: "file:///mock-url", version: "v99.999.9999+c9cdf14", -} +}; export const MockOwnerRole: TypesGen.Role = { name: "owner", display_name: "Owner", -} +}; export const MockUserAdminRole: TypesGen.Role = { name: "user_admin", display_name: "User Admin", -} +}; export const MockTemplateAdminRole: TypesGen.Role = { name: "template_admin", display_name: "Template Admin", -} +}; export const MockMemberRole: TypesGen.Role = { name: "member", display_name: "Member", -} +}; export const MockAuditorRole: TypesGen.Role = { name: "auditor", display_name: "Auditor", -} +}; // assignableRole takes a role and a boolean. The boolean implies if the // actor can assign (add/remove) the role from other users. @@ -253,18 +253,18 @@ export function assignableRole( return { ...role, assignable: assignable, - } + }; } -export const MockSiteRoles = [MockUserAdminRole, MockAuditorRole] +export const MockSiteRoles = [MockUserAdminRole, MockAuditorRole]; export const MockAssignableSiteRoles = [ assignableRole(MockUserAdminRole, true), assignableRole(MockAuditorRole, true), -] +]; export const MockMemberPermissions = { viewAuditLog: false, -} +}; export const MockUser: TypesGen.User = { id: "test-user", @@ -277,7 +277,7 @@ export const MockUser: TypesGen.User = { avatar_url: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4", last_seen_at: "", login_type: "password", -} +}; export const MockUserAdmin: TypesGen.User = { id: "test-user", @@ -290,7 +290,7 @@ export const MockUserAdmin: TypesGen.User = { avatar_url: "", last_seen_at: "", login_type: "password", -} +}; export const MockUser2: TypesGen.User = { id: "test-user-2", @@ -303,7 +303,7 @@ export const MockUser2: TypesGen.User = { avatar_url: "", last_seen_at: "2022-09-14T19:12:21Z", login_type: "oidc", -} +}; export const SuspendedMockUser: TypesGen.User = { id: "suspended-mock-user", @@ -316,7 +316,7 @@ export const SuspendedMockUser: TypesGen.User = { avatar_url: "", last_seen_at: "", login_type: "password", -} +}; export const MockProvisioner: TypesGen.ProvisionerDaemon = { created_at: "", @@ -324,7 +324,7 @@ export const MockProvisioner: TypesGen.ProvisionerDaemon = { name: "Test Provisioner", provisioners: ["echo"], tags: {}, -} +}; export const MockProvisionerJob: TypesGen.ProvisionerJob = { created_at: "", @@ -335,31 +335,31 @@ export const MockProvisionerJob: TypesGen.ProvisionerJob = { tags: {}, queue_position: 0, queue_size: 0, -} +}; export const MockFailedProvisionerJob: TypesGen.ProvisionerJob = { ...MockProvisionerJob, status: "failed", -} +}; export const MockCancelingProvisionerJob: TypesGen.ProvisionerJob = { ...MockProvisionerJob, status: "canceling", -} +}; export const MockCanceledProvisionerJob: TypesGen.ProvisionerJob = { ...MockProvisionerJob, status: "canceled", -} +}; export const MockRunningProvisionerJob: TypesGen.ProvisionerJob = { ...MockProvisionerJob, status: "running", -} +}; export const MockPendingProvisionerJob: TypesGen.ProvisionerJob = { ...MockProvisionerJob, status: "pending", queue_position: 2, queue_size: 4, -} +}; export const MockTemplateVersion: TypesGen.TemplateVersion = { id: "test-template-version", created_at: "2022-05-17T17:39:01.382927298Z", @@ -376,7 +376,7 @@ You can add instructions here [Some link info](https://coder.com)`, created_by: MockUser, -} +}; export const MockTemplateVersion2: TypesGen.TemplateVersion = { id: "test-template-version-2", @@ -394,7 +394,7 @@ You can add instructions here [Some link info](https://coder.com)`, created_by: MockUser, -} +}; export const MockTemplateVersion3: TypesGen.TemplateVersion = { id: "test-template-version-3", @@ -407,7 +407,7 @@ export const MockTemplateVersion3: TypesGen.TemplateVersion = { readme: "README", created_by: MockUser, warnings: ["UNSUPPORTED_WORKSPACES"], -} +}; export const MockTemplate: TypesGen.Template = { id: "test-template", @@ -449,7 +449,7 @@ export const MockTemplate: TypesGen.Template = { time_til_dormant_autodelete_ms: 0, allow_user_autostart: false, allow_user_autostop: false, -} +}; export const MockTemplateVersionFiles: TemplateVersionFiles = { "README.md": "# Example\n\nThis is an example template.", @@ -481,7 +481,7 @@ spec { } } `, -} +}; export const MockTemplateVersionFileTree: FileTree = { "README.md": "# Example\n\nThis is an example template.", @@ -517,7 +517,7 @@ spec { "java.Dockerfile": "FROM eclipse-temurin:17-jdk-jammy", "python.Dockerfile": "FROM python:3.8-slim-buster", }, -} +}; export const MockWorkspaceApp: TypesGen.WorkspaceApp = { id: "test-app", @@ -534,7 +534,7 @@ export const MockWorkspaceApp: TypesGen.WorkspaceApp = { interval: 0, threshold: 0, }, -} +}; export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = { apps: [MockWorkspaceApp], @@ -574,7 +574,7 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = { "vscode_insiders", "web_terminal", ], -} +}; export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = { ...MockWorkspaceAgent, @@ -588,7 +588,7 @@ export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = { healthy: false, reason: "agent is not connected", }, -} +}; export const MockWorkspaceAgentOutdated: TypesGen.WorkspaceAgent = { ...MockWorkspaceAgent, @@ -612,7 +612,7 @@ export const MockWorkspaceAgentOutdated: TypesGen.WorkspaceAgent = { }, }, lifecycle_state: "ready", -} +}; export const MockWorkspaceAgentConnecting: TypesGen.WorkspaceAgent = { ...MockWorkspaceAgent, @@ -622,7 +622,7 @@ export const MockWorkspaceAgentConnecting: TypesGen.WorkspaceAgent = { version: "", latency: {}, lifecycle_state: "created", -} +}; export const MockWorkspaceAgentTimeout: TypesGen.WorkspaceAgent = { ...MockWorkspaceAgent, @@ -636,28 +636,28 @@ export const MockWorkspaceAgentTimeout: TypesGen.WorkspaceAgent = { healthy: false, reason: "agent is taking too long to connect", }, -} +}; export const MockWorkspaceAgentStarting: TypesGen.WorkspaceAgent = { ...MockWorkspaceAgent, id: "test-workspace-agent-starting", name: "a-starting-workspace-agent", lifecycle_state: "starting", -} +}; export const MockWorkspaceAgentReady: TypesGen.WorkspaceAgent = { ...MockWorkspaceAgent, id: "test-workspace-agent-ready", name: "a-ready-workspace-agent", lifecycle_state: "ready", -} +}; export const MockWorkspaceAgentStartTimeout: TypesGen.WorkspaceAgent = { ...MockWorkspaceAgent, id: "test-workspace-agent-start-timeout", name: "a-workspace-agent-timed-out-while-running-startup-script", lifecycle_state: "start_timeout", -} +}; export const MockWorkspaceAgentStartError: TypesGen.WorkspaceAgent = { ...MockWorkspaceAgent, @@ -668,7 +668,7 @@ export const MockWorkspaceAgentStartError: TypesGen.WorkspaceAgent = { healthy: false, reason: "agent startup script failed", }, -} +}; export const MockWorkspaceAgentShuttingDown: TypesGen.WorkspaceAgent = { ...MockWorkspaceAgent, @@ -679,7 +679,7 @@ export const MockWorkspaceAgentShuttingDown: TypesGen.WorkspaceAgent = { healthy: false, reason: "agent is shutting down", }, -} +}; export const MockWorkspaceAgentShutdownTimeout: TypesGen.WorkspaceAgent = { ...MockWorkspaceAgent, @@ -690,7 +690,7 @@ export const MockWorkspaceAgentShutdownTimeout: TypesGen.WorkspaceAgent = { healthy: false, reason: "agent is shutting down", }, -} +}; export const MockWorkspaceAgentShutdownError: TypesGen.WorkspaceAgent = { ...MockWorkspaceAgent, @@ -701,7 +701,7 @@ export const MockWorkspaceAgentShutdownError: TypesGen.WorkspaceAgent = { healthy: false, reason: "agent is shutting down", }, -} +}; export const MockWorkspaceAgentOff: TypesGen.WorkspaceAgent = { ...MockWorkspaceAgent, @@ -712,7 +712,7 @@ export const MockWorkspaceAgentOff: TypesGen.WorkspaceAgent = { healthy: false, reason: "agent is shutting down", }, -} +}; export const MockWorkspaceResource: TypesGen.WorkspaceResource = { agents: [ @@ -730,7 +730,7 @@ export const MockWorkspaceResource: TypesGen.WorkspaceResource = { icon: "", metadata: [{ key: "api_key", value: "12345678", sensitive: true }], daily_cost: 10, -} +}; export const MockWorkspaceResource2: TypesGen.WorkspaceResource = { agents: [ @@ -748,7 +748,7 @@ export const MockWorkspaceResource2: TypesGen.WorkspaceResource = { icon: "", metadata: [{ key: "size", value: "32GB", sensitive: false }], daily_cost: 10, -} +}; export const MockWorkspaceResource3: TypesGen.WorkspaceResource = { agents: [ @@ -766,19 +766,19 @@ export const MockWorkspaceResource3: TypesGen.WorkspaceResource = { icon: "", metadata: [{ key: "size", value: "32GB", sensitive: false }], daily_cost: 20, -} +}; export const MockWorkspaceAutostartDisabled: TypesGen.UpdateWorkspaceAutostartRequest = { schedule: "", - } + }; export const MockWorkspaceAutostartEnabled: TypesGen.UpdateWorkspaceAutostartRequest = { // Runs at 9:30am Monday through Friday using Canada/Eastern // (America/Toronto) time schedule: "CRON_TZ=Canada/Eastern 30 9 * * 1-5", - } + }; export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { build_number: 1, @@ -800,7 +800,7 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { resources: [MockWorkspaceResource], status: "running", daily_cost: 20, -} +}; export const MockFailedWorkspaceBuild = ( transition: TypesGen.WorkspaceTransition = "start", @@ -824,25 +824,25 @@ export const MockFailedWorkspaceBuild = ( resources: [], status: "failed", daily_cost: 20, -}) +}); export const MockWorkspaceBuildStop: TypesGen.WorkspaceBuild = { ...MockWorkspaceBuild, id: "2", transition: "stop", -} +}; export const MockWorkspaceBuildDelete: TypesGen.WorkspaceBuild = { ...MockWorkspaceBuild, id: "3", transition: "delete", -} +}; export const MockBuilds = [ MockWorkspaceBuild, MockWorkspaceBuildStop, MockWorkspaceBuildDelete, -] +]; export const MockWorkspace: TypesGen.Workspace = { id: "test-workspace", @@ -868,13 +868,13 @@ export const MockWorkspace: TypesGen.Workspace = { healthy: true, failing_agents: [], }, -} +}; export const MockStoppedWorkspace: TypesGen.Workspace = { ...MockWorkspace, id: "test-stopped-workspace", latest_build: { ...MockWorkspaceBuildStop, status: "stopped" }, -} +}; export const MockStoppingWorkspace: TypesGen.Workspace = { ...MockWorkspace, id: "test-stopping-workspace", @@ -883,7 +883,7 @@ export const MockStoppingWorkspace: TypesGen.Workspace = { job: MockRunningProvisionerJob, status: "stopping", }, -} +}; export const MockStartingWorkspace: TypesGen.Workspace = { ...MockWorkspace, id: "test-starting-workspace", @@ -893,7 +893,7 @@ export const MockStartingWorkspace: TypesGen.Workspace = { transition: "start", status: "starting", }, -} +}; export const MockCancelingWorkspace: TypesGen.Workspace = { ...MockWorkspace, id: "test-canceling-workspace", @@ -902,7 +902,7 @@ export const MockCancelingWorkspace: TypesGen.Workspace = { job: MockCancelingProvisionerJob, status: "canceling", }, -} +}; export const MockCanceledWorkspace: TypesGen.Workspace = { ...MockWorkspace, id: "test-canceled-workspace", @@ -911,7 +911,7 @@ export const MockCanceledWorkspace: TypesGen.Workspace = { job: MockCanceledProvisionerJob, status: "canceled", }, -} +}; export const MockFailedWorkspace: TypesGen.Workspace = { ...MockWorkspace, id: "test-failed-workspace", @@ -920,7 +920,7 @@ export const MockFailedWorkspace: TypesGen.Workspace = { job: MockFailedProvisionerJob, status: "failed", }, -} +}; export const MockDeletingWorkspace: TypesGen.Workspace = { ...MockWorkspace, id: "test-deleting-workspace", @@ -929,24 +929,24 @@ export const MockDeletingWorkspace: TypesGen.Workspace = { job: MockRunningProvisionerJob, status: "deleting", }, -} +}; export const MockWorkspaceWithDeletion = { ...MockStoppedWorkspace, deleting_at: new Date().toISOString(), -} +}; export const MockDeletedWorkspace: TypesGen.Workspace = { ...MockWorkspace, id: "test-deleted-workspace", latest_build: { ...MockWorkspaceBuildDelete, status: "deleted" }, -} +}; export const MockOutdatedWorkspace: TypesGen.Workspace = { ...MockFailedWorkspace, id: "test-outdated-workspace", outdated: true, -} +}; export const MockPendingWorkspace: TypesGen.Workspace = { ...MockWorkspace, @@ -957,7 +957,7 @@ export const MockPendingWorkspace: TypesGen.Workspace = { transition: "start", status: "pending", }, -} +}; // just over one page of workspaces export const MockWorkspacesResponse: TypesGen.WorkspacesResponse = { @@ -967,12 +967,12 @@ export const MockWorkspacesResponse: TypesGen.WorkspacesResponse = { name: `${MockWorkspace.name}${id}`, })), count: 26, -} +}; export const MockWorkspacesResponseWithDeletions = { workspaces: [...MockWorkspacesResponse.workspaces, MockWorkspaceWithDeletion], count: MockWorkspacesResponse.count + 1, -} +}; export const MockTemplateVersionParameter1: TypesGen.TemplateVersionParameter = { @@ -986,7 +986,7 @@ export const MockTemplateVersionParameter1: TypesGen.TemplateVersionParameter = options: [], required: true, ephemeral: false, - } + }; export const MockTemplateVersionParameter2: TypesGen.TemplateVersionParameter = { @@ -1003,7 +1003,7 @@ export const MockTemplateVersionParameter2: TypesGen.TemplateVersionParameter = validation_monotonic: "increasing", required: true, ephemeral: false, - } + }; export const MockTemplateVersionParameter3: TypesGen.TemplateVersionParameter = { @@ -1019,7 +1019,7 @@ export const MockTemplateVersionParameter3: TypesGen.TemplateVersionParameter = validation_regex: "^[a-z]{3}$", required: true, ephemeral: false, - } + }; export const MockTemplateVersionParameter4: TypesGen.TemplateVersionParameter = { @@ -1033,7 +1033,7 @@ export const MockTemplateVersionParameter4: TypesGen.TemplateVersionParameter = options: [], required: true, ephemeral: false, - } + }; export const MockTemplateVersionParameter5: TypesGen.TemplateVersionParameter = { @@ -1050,7 +1050,7 @@ export const MockTemplateVersionParameter5: TypesGen.TemplateVersionParameter = validation_monotonic: "decreasing", required: true, ephemeral: false, - } + }; export const MockTemplateVersionVariable1: TypesGen.TemplateVersionVariable = { name: "first_variable", @@ -1060,7 +1060,7 @@ export const MockTemplateVersionVariable1: TypesGen.TemplateVersionVariable = { default_value: "abc", required: false, sensitive: false, -} +}; export const MockTemplateVersionVariable2: TypesGen.TemplateVersionVariable = { name: "second_variable", @@ -1070,7 +1070,7 @@ export const MockTemplateVersionVariable2: TypesGen.TemplateVersionVariable = { default_value: "3", required: false, sensitive: false, -} +}; export const MockTemplateVersionVariable3: TypesGen.TemplateVersionVariable = { name: "third_variable", @@ -1080,7 +1080,7 @@ export const MockTemplateVersionVariable3: TypesGen.TemplateVersionVariable = { default_value: "false", required: false, sensitive: false, -} +}; export const MockTemplateVersionVariable4: TypesGen.TemplateVersionVariable = { name: "fourth_variable", @@ -1090,7 +1090,7 @@ export const MockTemplateVersionVariable4: TypesGen.TemplateVersionVariable = { default_value: "", required: true, sensitive: true, -} +}; export const MockTemplateVersionVariable5: TypesGen.TemplateVersionVariable = { name: "fifth_variable", @@ -1100,7 +1100,7 @@ export const MockTemplateVersionVariable5: TypesGen.TemplateVersionVariable = { default_value: "", required: true, sensitive: false, -} +}; // requests the MockWorkspace export const MockWorkspaceRequest: TypesGen.CreateWorkspaceRequest = { @@ -1112,26 +1112,26 @@ export const MockWorkspaceRequest: TypesGen.CreateWorkspaceRequest = { value: MockTemplateVersionParameter1.default_value, }, ], -} +}; export const MockUserAgent: Types.UserAgent = { browser: "Chrome 99.0.4844", device: "Other", ip_address: "11.22.33.44", os: "Windows 10", -} +}; export const MockAuthMethods: TypesGen.AuthMethods = { password: { enabled: true }, github: { enabled: false }, oidc: { enabled: false, signInText: "", iconUrl: "" }, -} +}; export const MockAuthMethodsWithPasswordType: TypesGen.AuthMethods = { ...MockAuthMethods, github: { enabled: true }, oidc: { enabled: true, signInText: "", iconUrl: "" }, -} +}; export const MockGitSSHKey: TypesGen.GitSSHKey = { user_id: "1fa0200f-7331-4524-a364-35770666caa7", @@ -1139,7 +1139,7 @@ export const MockGitSSHKey: TypesGen.GitSSHKey = { updated_at: "2022-05-16T15:29:10.302441433Z", public_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFJOQRIM7kE30rOzrfy+/+R+nQGCk7S9pioihy+2ARbq", -} +}; export const MockWorkspaceBuildLogs: TypesGen.ProvisionerJobLog[] = [ { @@ -1412,7 +1412,7 @@ export const MockWorkspaceBuildLogs: TypesGen.ProvisionerJobLog[] = [ stage: "Cleaning Up", output: "", }, -] +]; export const MockWorkspaceExtendedBuildLogs: TypesGen.ProvisionerJobLog[] = [ { @@ -1721,28 +1721,28 @@ export const MockWorkspaceExtendedBuildLogs: TypesGen.ProvisionerJobLog[] = [ stage: "Cleaning Up", output: "", }, -] +]; export const MockCancellationMessage = { message: "Job successfully canceled", -} +}; type MockAPIInput = { - message?: string - detail?: string - validations?: FieldError[] -} + message?: string; + detail?: string; + validations?: FieldError[]; +}; type MockAPIOutput = { - isAxiosError: true + isAxiosError: true; response: { data: { - message: string - detail: string | undefined - validations: FieldError[] | undefined - } - } -} + message: string; + detail: string | undefined; + validations: FieldError[] | undefined; + }; + }; +}; export const mockApiError = ({ message, @@ -1758,7 +1758,7 @@ export const mockApiError = ({ validations: validations ?? undefined, }, }, -}) +}); export const MockEntitlements: TypesGen.Entitlements = { errors: [], @@ -1773,7 +1773,7 @@ export const MockEntitlements: TypesGen.Entitlements = { require_telemetry: false, trial: false, refreshed_at: "2022-05-20T16:45:57.122Z", -} +}; export const MockEntitlementsWithWarnings: TypesGen.Entitlements = { errors: [], @@ -1798,7 +1798,7 @@ export const MockEntitlementsWithWarnings: TypesGen.Entitlements = { entitlement: "entitled", }, }), -} +}; export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = { errors: [], @@ -1813,7 +1813,7 @@ export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = { entitlement: "entitled", }, }), -} +}; export const MockEntitlementsWithScheduling: TypesGen.Entitlements = { errors: [], @@ -1828,12 +1828,12 @@ export const MockEntitlementsWithScheduling: TypesGen.Entitlements = { entitlement: "entitled", }, }), -} +}; export const MockExperiments: TypesGen.Experiment[] = [ "workspace_actions", "moons", -] +]; export const MockAuditLog: TypesGen.AuditLog = { id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", @@ -1861,7 +1861,7 @@ export const MockAuditLog: TypesGen.AuditLog = { user: MockUser, resource_link: "/@admin/bruno-dev", is_deleted: false, -} +}; export const MockAuditLog2: TypesGen.AuditLog = { ...MockAuditLog, @@ -1891,14 +1891,14 @@ export const MockAuditLog2: TypesGen.AuditLog = { secret: false, }, }, -} +}; export const MockWorkspaceCreateAuditLogForDifferentOwner = { ...MockAuditLog, additional_fields: { workspace_owner: "Member", }, -} +}; export const MockAuditLogWithWorkspaceBuild: TypesGen.AuditLog = { ...MockAuditLog, @@ -1910,12 +1910,12 @@ export const MockAuditLogWithWorkspaceBuild: TypesGen.AuditLog = { additional_fields: { workspace_name: "test2", }, -} +}; export const MockAuditLogWithDeletedResource: TypesGen.AuditLog = { ...MockAuditLog, is_deleted: true, -} +}; export const MockAuditLogGitSSH: TypesGen.AuditLog = { ...MockAuditLog, @@ -1931,7 +1931,7 @@ export const MockAuditLogGitSSH: TypesGen.AuditLog = { secret: false, }, }, -} +}; export const MockAuditOauthConvert: TypesGen.AuditLog = { ...MockAuditLog, @@ -1967,7 +1967,7 @@ export const MockAuditOauthConvert: TypesGen.AuditLog = { secret: false, }, }, -} +}; export const MockAuditLogSuccessfulLogin: TypesGen.AuditLog = { ...MockAuditLog, @@ -1976,17 +1976,17 @@ export const MockAuditLogSuccessfulLogin: TypesGen.AuditLog = { action: "login", status_code: 201, description: "{user} logged in", -} +}; export const MockAuditLogUnsuccessfulLoginKnownUser: TypesGen.AuditLog = { ...MockAuditLogSuccessfulLogin, status_code: 401, -} +}; export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = { credits_consumed: 0, budget: 100, -} +}; export const MockGroup: TypesGen.Group = { id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", @@ -1997,7 +1997,7 @@ export const MockGroup: TypesGen.Group = { members: [MockUser, MockUser2], quota_allowance: 5, source: "user", -} +}; export const MockTemplateACL: TypesGen.TemplateACL = { group: [ @@ -2005,12 +2005,12 @@ export const MockTemplateACL: TypesGen.TemplateACL = { { ...MockGroup, role: "admin" }, ], users: [{ ...MockUser, role: "use" }], -} +}; export const MockTemplateACLEmpty: TypesGen.TemplateACL = { group: [], users: [], -} +}; export const MockTemplateExample: TypesGen.TemplateExample = { id: "aws-windows", @@ -2021,7 +2021,7 @@ export const MockTemplateExample: TypesGen.TemplateExample = { "\n# aws-ecs\n\nThis is a sample template for running a Coder workspace on ECS. It assumes there\nis a pre-existing ECS cluster with EC2-based compute to host the workspace.\n\n## Architecture\n\nThis workspace is built using the following AWS resources:\n\n- Task definition - the container definition, includes the image, command, volume(s)\n- ECS service - manages the task definition\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n", icon: "/icon/aws.png", tags: ["aws", "cloud"], -} +}; export const MockTemplateExample2: TypesGen.TemplateExample = { id: "aws-linux", @@ -2032,7 +2032,7 @@ export const MockTemplateExample2: TypesGen.TemplateExample = { '\n# aws-linux\n\nTo get started, run `coder templates init`. When prompted, select this template.\nFollow the on-screen instructions to proceed.\n\n## Authentication\n\nThis template assumes that coderd is run in an environment that is authenticated\nwith AWS. For example, run `aws configure import` to import credentials on the\nsystem and user running coderd. For other ways to authenticate [consult the\nTerraform docs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration).\n\n## Required permissions / policy\n\nThe following sample policy allows Coder to create EC2 instances and modify\ninstances provisioned by Coder:\n\n```json\n{\n "Version": "2012-10-17",\n "Statement": [\n {\n "Sid": "VisualEditor0",\n "Effect": "Allow",\n "Action": [\n "ec2:GetDefaultCreditSpecification",\n "ec2:DescribeIamInstanceProfileAssociations",\n "ec2:DescribeTags",\n "ec2:CreateTags",\n "ec2:RunInstances",\n "ec2:DescribeInstanceCreditSpecifications",\n "ec2:DescribeImages",\n "ec2:ModifyDefaultCreditSpecification",\n "ec2:DescribeVolumes"\n ],\n "Resource": "*"\n },\n {\n "Sid": "CoderResources",\n "Effect": "Allow",\n "Action": [\n "ec2:DescribeInstances",\n "ec2:DescribeInstanceAttribute",\n "ec2:UnmonitorInstances",\n "ec2:TerminateInstances",\n "ec2:StartInstances",\n "ec2:StopInstances",\n "ec2:DeleteTags",\n "ec2:MonitorInstances",\n "ec2:CreateTags",\n "ec2:RunInstances",\n "ec2:ModifyInstanceAttribute",\n "ec2:ModifyInstanceCreditSpecification"\n ],\n "Resource": "arn:aws:ec2:*:*:instance/*",\n "Condition": {\n "StringEquals": {\n "aws:ResourceTag/Coder_Provisioned": "true"\n }\n }\n }\n ]\n}\n```\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n', icon: "/icon/aws.png", tags: ["aws", "cloud"], -} +}; export const MockPermissions: Permissions = { createGroup: true, @@ -2047,53 +2047,53 @@ export const MockPermissions: Permissions = { viewDeploymentStats: true, viewGitAuthConfig: true, editWorkspaceProxies: true, -} +}; export const MockDeploymentConfig: Types.DeploymentConfig = { config: { enable_terraform_debug_mode: true, }, options: [], -} +}; export const MockAppearance: TypesGen.AppearanceConfig = { logo_url: "", service_banner: { enabled: false, }, -} +}; export const MockWorkspaceBuildParameter1: TypesGen.WorkspaceBuildParameter = { name: MockTemplateVersionParameter1.name, value: "mock-abc", -} +}; export const MockWorkspaceBuildParameter2: TypesGen.WorkspaceBuildParameter = { name: MockTemplateVersionParameter2.name, value: "3", -} +}; export const MockWorkspaceBuildParameter3: TypesGen.WorkspaceBuildParameter = { name: MockTemplateVersionParameter3.name, value: "my-database", -} +}; export const MockWorkspaceBuildParameter4: TypesGen.WorkspaceBuildParameter = { name: MockTemplateVersionParameter4.name, value: "immutable-value", -} +}; export const MockWorkspaceBuildParameter5: TypesGen.WorkspaceBuildParameter = { name: MockTemplateVersionParameter5.name, value: "5", -} +}; export const MockTemplateVersionGitAuth: TypesGen.TemplateVersionGitAuth = { id: "github", type: "github", authenticate_url: "https://example.com/gitauth/github", authenticated: false, -} +}; export const MockDeploymentStats: TypesGen.DeploymentStats = { aggregated_from: "2023-03-06T19:08:55.211625Z", @@ -2118,12 +2118,12 @@ export const MockDeploymentStats: TypesGen.DeploymentStats = { rx_bytes: 15613513253, tx_bytes: 36113513253, }, -} +}; export const MockDeploymentSSH: TypesGen.SSHConfigResponse = { hostname_prefix: " coder.", ssh_config_options: {}, -} +}; export const MockWorkspaceAgentLogs: TypesGen.WorkspaceAgentLog[] = [ { @@ -2151,7 +2151,7 @@ export const MockWorkspaceAgentLogs: TypesGen.WorkspaceAgentLog[] = [ output: "Installing v4.8.3 of the amd64 release from GitHub.", level: "info", }, -] +]; export const MockLicenseResponse: GetLicensesResponse[] = [ { @@ -2193,7 +2193,7 @@ export const MockLicenseResponse: GetLicensesResponse[] = [ license_expires: 1682346425, }, }, -] +]; export const MockHealth = { time: "2023-08-01T16:51:03.29792825Z", @@ -2612,7 +2612,7 @@ export const MockHealth = { error: null, }, coder_version: "v0.27.1-devel+c575292", -} +}; export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsResponse = { @@ -2621,4 +2621,4 @@ export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsRe { process_name: "go", network: "", port: 8080 }, { process_name: "", network: "", port: 8081 }, ], - } + }; diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 8c379985ca..059c82b43b 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -1,19 +1,19 @@ -import { rest } from "msw" -import { WorkspaceBuildTransition } from "../api/types" -import { CreateWorkspaceBuildRequest } from "../api/typesGenerated" -import { permissionsToCheck } from "../xServices/auth/authXService" -import * as M from "./entities" -import { MockGroup, MockWorkspaceQuota } from "./entities" -import fs from "fs" -import path from "path" +import { rest } from "msw"; +import { WorkspaceBuildTransition } from "../api/types"; +import { CreateWorkspaceBuildRequest } from "../api/typesGenerated"; +import { permissionsToCheck } from "../xServices/auth/authXService"; +import * as M from "./entities"; +import { MockGroup, MockWorkspaceQuota } from "./entities"; +import fs from "fs"; +import path from "path"; export const handlers = [ rest.get("/api/v2/templates/:templateId/daus", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockTemplateDAUResponse)) + return res(ctx.status(200), ctx.json(M.MockTemplateDAUResponse)); }), rest.get("/api/v2/insights/daus", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockDeploymentDAUResponse)) + return res(ctx.status(200), ctx.json(M.MockDeploymentDAUResponse)); }), // Workspace proxies rest.get("/api/v2/regions", async (req, res, ctx) => { @@ -22,7 +22,7 @@ export const handlers = [ ctx.json({ regions: M.MockWorkspaceProxies, }), - ) + ); }), rest.get("/api/v2/workspaceproxies", async (req, res, ctx) => { return res( @@ -30,26 +30,26 @@ export const handlers = [ ctx.json({ regions: M.MockWorkspaceProxies, }), - ) + ); }), // build info rest.get("/api/v2/buildinfo", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockBuildInfo)) + return res(ctx.status(200), ctx.json(M.MockBuildInfo)); }), // experiments rest.get("/api/v2/experiments", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockExperiments)) + return res(ctx.status(200), ctx.json(M.MockExperiments)); }), // update check rest.get("/api/v2/updatecheck", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockUpdateCheck)) + return res(ctx.status(200), ctx.json(M.MockUpdateCheck)); }), // organizations rest.get("/api/v2/organizations/:organizationId", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockOrganization)) + return res(ctx.status(200), ctx.json(M.MockOrganization)); }), rest.get( "api/v2/organizations/:organizationId/templates/examples", @@ -57,39 +57,39 @@ export const handlers = [ return res( ctx.status(200), ctx.json([M.MockTemplateExample, M.MockTemplateExample2]), - ) + ); }, ), rest.get( "/api/v2/organizations/:organizationId/templates/:templateId", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockTemplate)) + return res(ctx.status(200), ctx.json(M.MockTemplate)); }, ), rest.get( "/api/v2/organizations/:organizationId/templates", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json([M.MockTemplate])) + return res(ctx.status(200), ctx.json([M.MockTemplate])); }, ), // templates rest.get("/api/v2/templates/:templateId", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockTemplate)) + return res(ctx.status(200), ctx.json(M.MockTemplate)); }), rest.get("/api/v2/templates/:templateId/versions", async (req, res, ctx) => { return res( ctx.status(200), ctx.json([M.MockTemplateVersion2, M.MockTemplateVersion]), - ) + ); }), rest.patch("/api/v2/templates/:templateId", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockTemplate)) + return res(ctx.status(200), ctx.json(M.MockTemplate)); }), rest.get( "/api/v2/templateversions/:templateVersionId", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockTemplateVersion)) + return res(ctx.status(200), ctx.json(M.MockTemplateVersion)); }, ), rest.get( @@ -98,35 +98,35 @@ export const handlers = [ return res( ctx.status(200), ctx.json([M.MockWorkspaceResource, M.MockWorkspaceResource2]), - ) + ); }, ), rest.get( "/api/v2/templateversions/:templateVersionId/rich-parameters", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json([])) + return res(ctx.status(200), ctx.json([])); }, ), rest.get( "/api/v2/templateversions/:templateVersionId/gitauth", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json([])) + return res(ctx.status(200), ctx.json([])); }, ), rest.get( "api/v2/organizations/:organizationId/templates/:templateName/versions/:templateVersionName", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockTemplateVersion)) + return res(ctx.status(200), ctx.json(M.MockTemplateVersion)); }, ), rest.get( "api/v2/organizations/:organizationId/templates/:templateName/versions/:templateVersionName/previous", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockTemplateVersion2)) + return res(ctx.status(200), ctx.json(M.MockTemplateVersion2)); }, ), rest.delete("/api/v2/templates/:templateId", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockTemplate)) + return res(ctx.status(200), ctx.json(M.MockTemplate)); }), // users @@ -137,10 +137,10 @@ export const handlers = [ users: [M.MockUser, M.MockUser2, M.SuspendedMockUser], count: 26, }), - ) + ); }), rest.post("/api/v2/users", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockUser)) + return res(ctx.status(200), ctx.json(M.MockUser)); }), rest.get("/api/v2/users/:userid/login-type", async (req, res, ctx) => { return res( @@ -148,106 +148,106 @@ export const handlers = [ ctx.json({ login_type: "password", }), - ) + ); }), rest.get("/api/v2/users/me/organizations", (req, res, ctx) => { - return res(ctx.status(200), ctx.json([M.MockOrganization])) + return res(ctx.status(200), ctx.json([M.MockOrganization])); }), rest.get( "/api/v2/users/me/organizations/:organizationId", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockOrganization)) + return res(ctx.status(200), ctx.json(M.MockOrganization)); }, ), rest.post("/api/v2/users/login", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockSessionToken)) + return res(ctx.status(200), ctx.json(M.MockSessionToken)); }), rest.post("/api/v2/users/logout", async (req, res, ctx) => { - return res(ctx.status(200)) + return res(ctx.status(200)); }), rest.get("/api/v2/users/me", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockUser)) + return res(ctx.status(200), ctx.json(M.MockUser)); }), rest.get("/api/v2/users/me/keys", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockAPIKey)) + return res(ctx.status(200), ctx.json(M.MockAPIKey)); }), rest.get("/api/v2/users/authmethods", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockAuthMethods)) + return res(ctx.status(200), ctx.json(M.MockAuthMethods)); }), rest.get("/api/v2/users/roles", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockSiteRoles)) + return res(ctx.status(200), ctx.json(M.MockSiteRoles)); }), rest.post("/api/v2/authcheck", async (req, res, ctx) => { const permissions = [ ...Object.keys(permissionsToCheck), "canUpdateTemplate", "updateWorkspace", - ] + ]; const response = permissions.reduce((obj, permission) => { return { ...obj, [permission]: true, - } - }, {}) + }; + }, {}); - return res(ctx.status(200), ctx.json(response)) + return res(ctx.status(200), ctx.json(response)); }), rest.get("/api/v2/users/:userId/gitsshkey", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockGitSSHKey)) + return res(ctx.status(200), ctx.json(M.MockGitSSHKey)); }), rest.get( "/api/v2/users/:userId/workspace/:workspaceName", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockWorkspace)) + return res(ctx.status(200), ctx.json(M.MockWorkspace)); }, ), // First user rest.get("/api/v2/users/first", async (req, res, ctx) => { - return res(ctx.status(200)) + return res(ctx.status(200)); }), rest.post("/api/v2/users/first", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockUser)) + return res(ctx.status(200), ctx.json(M.MockUser)); }), // workspaces rest.get("/api/v2/workspaces", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockWorkspacesResponse)) + return res(ctx.status(200), ctx.json(M.MockWorkspacesResponse)); }), rest.get("/api/v2/workspaces/:workspaceId", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockWorkspace)) + return res(ctx.status(200), ctx.json(M.MockWorkspace)); }), rest.put( "/api/v2/workspaces/:workspaceId/autostart", async (req, res, ctx) => { - return res(ctx.status(200)) + return res(ctx.status(200)); }, ), rest.put("/api/v2/workspaces/:workspaceId/ttl", async (req, res, ctx) => { - return res(ctx.status(200)) + return res(ctx.status(200)); }), rest.put("/api/v2/workspaces/:workspaceId/extend", async (req, res, ctx) => { - return res(ctx.status(200)) + return res(ctx.status(200)); }), // workspace builds rest.post("/api/v2/workspaces/:workspaceId/builds", async (req, res, ctx) => { - const { transition } = req.body as CreateWorkspaceBuildRequest + const { transition } = req.body as CreateWorkspaceBuildRequest; const transitionToBuild = { start: M.MockWorkspaceBuild, stop: M.MockWorkspaceBuildStop, delete: M.MockWorkspaceBuildDelete, - } - const result = transitionToBuild[transition as WorkspaceBuildTransition] - return res(ctx.status(200), ctx.json(result)) + }; + const result = transitionToBuild[transition as WorkspaceBuildTransition]; + return res(ctx.status(200), ctx.json(result)); }), rest.get("/api/v2/workspaces/:workspaceId/builds", async (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockBuilds)) + return res(ctx.status(200), ctx.json(M.MockBuilds)); }), rest.get( "/api/v2/users/:username/workspace/:workspaceName/builds/:buildNumber", (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockWorkspaceBuild)) + return res(ctx.status(200), ctx.json(M.MockWorkspaceBuild)); }, ), rest.get( @@ -256,100 +256,100 @@ export const handlers = [ return res( ctx.status(200), ctx.json([M.MockWorkspaceResource, M.MockWorkspaceResource2]), - ) + ); }, ), rest.patch( "/api/v2/workspacebuilds/:workspaceBuildId/cancel", (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockCancellationMessage)) + return res(ctx.status(200), ctx.json(M.MockCancellationMessage)); }, ), rest.get( "/api/v2/workspacebuilds/:workspaceBuildId/logs", (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockWorkspaceBuildLogs)) + return res(ctx.status(200), ctx.json(M.MockWorkspaceBuildLogs)); }, ), rest.get("/api/v2/entitlements", (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockEntitlements)) + return res(ctx.status(200), ctx.json(M.MockEntitlements)); }), // Audit rest.get("/api/v2/audit", (req, res, ctx) => { - const filter = req.url.searchParams.get("q") as string + const filter = req.url.searchParams.get("q") as string; const logs = filter === "resource_type:workspace action:create" ? [M.MockAuditLog] - : [M.MockAuditLog, M.MockAuditLog2] + : [M.MockAuditLog, M.MockAuditLog2]; return res( ctx.status(200), ctx.json({ audit_logs: logs, count: logs.length, }), - ) + ); }), // Applications host rest.get("/api/v2/applications/host", (req, res, ctx) => { - return res(ctx.status(200), ctx.json({ host: "*.dev.coder.com" })) + return res(ctx.status(200), ctx.json({ host: "*.dev.coder.com" })); }), // Groups rest.get("/api/v2/organizations/:organizationId/groups", (req, res, ctx) => { - return res(ctx.status(200), ctx.json([MockGroup])) + return res(ctx.status(200), ctx.json([MockGroup])); }), rest.post( "/api/v2/organizations/:organizationId/groups", async (req, res, ctx) => { - return res(ctx.status(201), ctx.json(M.MockGroup)) + return res(ctx.status(201), ctx.json(M.MockGroup)); }, ), rest.get("/api/v2/groups/:groupId", (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MockGroup)) + return res(ctx.status(200), ctx.json(MockGroup)); }), rest.patch("/api/v2/groups/:groupId", (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MockGroup)) + return res(ctx.status(200), ctx.json(MockGroup)); }), rest.delete("/api/v2/groups/:groupId", (req, res, ctx) => { - return res(ctx.status(204)) + return res(ctx.status(204)); }), rest.get("/api/v2/workspace-quota/:userId", (req, res, ctx) => { - return res(ctx.status(200), ctx.json(MockWorkspaceQuota)) + return res(ctx.status(200), ctx.json(MockWorkspaceQuota)); }), rest.get("/api/v2/appearance", (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockAppearance)) + return res(ctx.status(200), ctx.json(M.MockAppearance)); }), rest.get("/api/v2/deployment/stats", (_, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockDeploymentStats)) + return res(ctx.status(200), ctx.json(M.MockDeploymentStats)); }), rest.get( "/api/v2/workspacebuilds/:workspaceBuildId/parameters", (_, res, ctx) => { - return res(ctx.status(200), ctx.json([M.MockWorkspaceBuildParameter1])) + return res(ctx.status(200), ctx.json([M.MockWorkspaceBuildParameter1])); }, ), rest.get("/api/v2/files/:fileId", (_, res, ctx) => { const fileBuffer = fs.readFileSync( path.resolve(__dirname, "./templateFiles.tar"), - ) + ); return res( ctx.set("Content-Length", fileBuffer.byteLength.toString()), ctx.set("Content-Type", "application/octet-stream"), // Respond with the "ArrayBuffer". ctx.body(fileBuffer), - ) + ); }), rest.get( @@ -362,7 +362,7 @@ export const handlers = [ M.MockTemplateVersionParameter2, M.MockTemplateVersionParameter3, ]), - ) + ); }, ), @@ -376,23 +376,23 @@ export const handlers = [ M.MockTemplateVersionVariable2, M.MockTemplateVersionVariable3, ]), - ) + ); }, ), rest.get("/api/v2/deployment/ssh", (_, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockDeploymentSSH)) + return res(ctx.status(200), ctx.json(M.MockDeploymentSSH)); }), rest.get("/api/v2/workspaceagents/:agent/logs", (_, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockWorkspaceAgentLogs)) + return res(ctx.status(200), ctx.json(M.MockWorkspaceAgentLogs)); }), rest.get("/api/v2/debug/health", (_, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockHealth)) + return res(ctx.status(200), ctx.json(M.MockHealth)); }), rest.get("/api/v2/workspaceagents/:agent/listening-ports", (_, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockListeningPortsResponse)) + return res(ctx.status(200), ctx.json(M.MockListeningPortsResponse)); }), -] +]; diff --git a/site/src/testHelpers/localstorage.ts b/site/src/testHelpers/localstorage.ts index 331a311323..bff92d8f9f 100644 --- a/site/src/testHelpers/localstorage.ts +++ b/site/src/testHelpers/localstorage.ts @@ -1,22 +1,22 @@ export const localStorageMock = () => { - const store = {} as Record + const store = {} as Record; return { getItem: (key: string): string => { - return store[key] + return store[key]; }, setItem: (key: string, value: string) => { - store[key] = value + store[key] = value; }, clear: () => { Object.keys(store).forEach((key) => { - delete store[key] - }) + delete store[key]; + }); }, removeItem: (key: string) => { - delete store[key] + delete store[key]; }, - } -} + }; +}; -Object.defineProperty(window, "localStorage", { value: localStorageMock() }) +Object.defineProperty(window, "localStorage", { value: localStorageMock() }); diff --git a/site/src/testHelpers/renderHelpers.tsx b/site/src/testHelpers/renderHelpers.tsx index 47862b5d29..48596bf5fe 100644 --- a/site/src/testHelpers/renderHelpers.tsx +++ b/site/src/testHelpers/renderHelpers.tsx @@ -2,29 +2,29 @@ import { render as tlRender, screen, waitForElementToBeRemoved, -} from "@testing-library/react" -import { AppProviders } from "app" -import { DashboardLayout } from "components/Dashboard/DashboardLayout" -import { i18n } from "i18n" -import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout" -import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout" -import { I18nextProvider } from "react-i18next" +} from "@testing-library/react"; +import { AppProviders } from "app"; +import { DashboardLayout } from "components/Dashboard/DashboardLayout"; +import { i18n } from "i18n"; +import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout"; +import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"; +import { I18nextProvider } from "react-i18next"; import { RouterProvider, createMemoryRouter, RouteObject, -} from "react-router-dom" -import { RequireAuth } from "../components/RequireAuth/RequireAuth" -import { MockUser } from "./entities" -import { ReactNode } from "react" +} from "react-router-dom"; +import { RequireAuth } from "../components/RequireAuth/RequireAuth"; +import { MockUser } from "./entities"; +import { ReactNode } from "react"; const baseRender = (element: ReactNode) => { return tlRender( {element} , - ) -} + ); +}; export const renderWithRouter = ( router: ReturnType, @@ -32,8 +32,8 @@ export const renderWithRouter = ( return { ...baseRender(), router, - } -} + }; +}; export const render = (element: ReactNode) => { return renderWithRouter( @@ -46,22 +46,22 @@ export const render = (element: ReactNode) => { ], { initialEntries: ["/"] }, ), - ) -} + ); +}; type RenderWithAuthOptions = { // The current URL, /workspaces/123 - route?: string + route?: string; // The route path, /workspaces/:workspaceId - path?: string + path?: string; // Extra routes to add to the router. It is helpful when having redirecting // routes or multiple routes during the test flow - extraRoutes?: RouteObject[] + extraRoutes?: RouteObject[]; // The same as extraRoutes but for routes that don't require authentication - nonAuthenticatedRoutes?: RouteObject[] + nonAuthenticatedRoutes?: RouteObject[]; // In case you want to render a layout inside of it - children?: RouteObject["children"] -} + children?: RouteObject["children"]; +}; export function renderWithAuth( element: JSX.Element, @@ -79,16 +79,16 @@ export function renderWithAuth( children: [{ path, element, children }, ...extraRoutes], }, ...nonAuthenticatedRoutes, - ] + ]; const renderResult = renderWithRouter( createMemoryRouter(routes, { initialEntries: [route] }), - ) + ); return { user: MockUser, ...renderResult, - } + }; } export function renderWithTemplateSettingsLayout( @@ -116,16 +116,16 @@ export function renderWithTemplateSettingsLayout( ], }, ...nonAuthenticatedRoutes, - ] + ]; const renderResult = renderWithRouter( createMemoryRouter(routes, { initialEntries: [route] }), - ) + ); return { user: MockUser, ...renderResult, - } + }; } export function renderWithWorkspaceSettingsLayout( @@ -153,16 +153,16 @@ export function renderWithWorkspaceSettingsLayout( ], }, ...nonAuthenticatedRoutes, - ] + ]; const renderResult = renderWithRouter( createMemoryRouter(routes, { initialEntries: [route] }), - ) + ); return { user: MockUser, ...renderResult, - } + }; } export const waitForLoaderToBeRemoved = (): Promise => @@ -171,4 +171,4 @@ export const waitForLoaderToBeRemoved = (): Promise => // some of the endpoints waitForElementToBeRemoved(() => screen.queryByTestId("loader"), { timeout: 5_000, - }) + }); diff --git a/site/src/testHelpers/server.ts b/site/src/testHelpers/server.ts index 1040745284..9bb8a8709c 100644 --- a/site/src/testHelpers/server.ts +++ b/site/src/testHelpers/server.ts @@ -1,5 +1,5 @@ -import { setupServer } from "msw/node" -import { handlers } from "./handlers" +import { setupServer } from "msw/node"; +import { handlers } from "./handlers"; // This configures a request mocking server with the given request handlers. -export const server = setupServer(...handlers) +export const server = setupServer(...handlers); diff --git a/site/src/testHelpers/styleMock.ts b/site/src/testHelpers/styleMock.ts index b1c6ea436a..ff8b4c5632 100644 --- a/site/src/testHelpers/styleMock.ts +++ b/site/src/testHelpers/styleMock.ts @@ -1 +1 @@ -export default {} +export default {}; diff --git a/site/src/theme/colors.ts b/site/src/theme/colors.ts index c137ea08a3..f65edb2d17 100644 --- a/site/src/theme/colors.ts +++ b/site/src/theme/colors.ts @@ -222,4 +222,4 @@ export const colors = { 2: "hsl(315, 95%, 96%)", 1: "hsl(315, 100%, 98%)", }, -} +}; diff --git a/site/src/theme/constants.ts b/site/src/theme/constants.ts index c5f832f398..eef5b8dab0 100644 --- a/site/src/theme/constants.ts +++ b/site/src/theme/constants.ts @@ -1,11 +1,11 @@ -export const borderRadius = 8 -export const borderRadiusSm = 6 +export const borderRadius = 8; +export const borderRadiusSm = 6; export const MONOSPACE_FONT_FAMILY = - "'IBM Plex Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'Liberation Mono', 'Monaco', 'Courier New', Courier, monospace" -export const BODY_FONT_FAMILY = `"Inter", sans-serif` -export const navHeight = 62 -export const containerWidth = 1380 -export const containerWidthMedium = 1080 -export const sidePadding = 24 -export const CardPadding = 20 -export const dashboardContentBottomPadding = 8 * 6 + "'IBM Plex Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'Liberation Mono', 'Monaco', 'Courier New', Courier, monospace"; +export const BODY_FONT_FAMILY = `"Inter", sans-serif`; +export const navHeight = 62; +export const containerWidth = 1380; +export const containerWidthMedium = 1080; +export const sidePadding = 24; +export const CardPadding = 20; +export const dashboardContentBottomPadding = 8 * 6; diff --git a/site/src/theme/globalFonts.ts b/site/src/theme/globalFonts.ts index 7714377aca..d840a8738b 100644 --- a/site/src/theme/globalFonts.ts +++ b/site/src/theme/globalFonts.ts @@ -1,8 +1,8 @@ // Monospace fonts used for code, button styles, and banners -import "@fontsource/ibm-plex-mono/400.css" -import "@fontsource/ibm-plex-mono/600.css" +import "@fontsource/ibm-plex-mono/400.css"; +import "@fontsource/ibm-plex-mono/600.css"; // Main body copy font -import "@fontsource/inter/300.css" -import "@fontsource/inter/400.css" -import "@fontsource/inter/500.css" -import "@fontsource/inter/600.css" +import "@fontsource/inter/300.css"; +import "@fontsource/inter/400.css"; +import "@fontsource/inter/500.css"; +import "@fontsource/inter/600.css"; diff --git a/site/src/theme/index.ts b/site/src/theme/index.ts index eefbaf7a25..7cc5f34d8c 100644 --- a/site/src/theme/index.ts +++ b/site/src/theme/index.ts @@ -1 +1 @@ -export { dark } from "./theme" +export { dark } from "./theme"; diff --git a/site/src/theme/theme.ts b/site/src/theme/theme.ts index a1cb95092b..ffe8c48507 100644 --- a/site/src/theme/theme.ts +++ b/site/src/theme/theme.ts @@ -1,17 +1,17 @@ -import { colors } from "./colors" -import { ThemeOptions, createTheme, Theme } from "@mui/material/styles" -import { BODY_FONT_FAMILY, borderRadius } from "./constants" +import { colors } from "./colors"; +import { ThemeOptions, createTheme, Theme } from "@mui/material/styles"; +import { BODY_FONT_FAMILY, borderRadius } from "./constants"; // MUI does not have aligned heights for buttons and inputs so we have to "hack" it a little bit -export const BUTTON_LG_HEIGHT = 40 -export const BUTTON_MD_HEIGHT = 36 -export const BUTTON_SM_HEIGHT = 32 +export const BUTTON_LG_HEIGHT = 40; +export const BUTTON_MD_HEIGHT = 36; +export const BUTTON_SM_HEIGHT = 32; -export type PaletteIndex = keyof Theme["palette"] +export type PaletteIndex = keyof Theme["palette"]; export type PaletteStatusIndex = Extract< PaletteIndex, "error" | "warning" | "info" | "success" -> +>; export let dark = createTheme({ palette: { @@ -80,7 +80,7 @@ export let dark = createTheme({ shape: { borderRadius, }, -}) +}); dark = createTheme(dark, { components: { @@ -461,4 +461,4 @@ dark = createTheme(dark, { }, }, }, -} as ThemeOptions) +} as ThemeOptions); diff --git a/site/src/utils/colors.ts b/site/src/utils/colors.ts index 054e2cb6e9..822f32bce8 100644 --- a/site/src/utils/colors.ts +++ b/site/src/utils/colors.ts @@ -6,18 +6,18 @@ export function hslToHex(hsl: string): string { .replace(")", "") .replaceAll("%", "") .split(",") - .map(Number) + .map(Number); - const hDecimal = l / 100 - const a = (s * Math.min(hDecimal, 1 - hDecimal)) / 100 + const hDecimal = l / 100; + const a = (s * Math.min(hDecimal, 1 - hDecimal)) / 100; const f = (n: number) => { - const k = (n + h / 30) % 12 - const color = hDecimal - a * Math.max(Math.min(k - 3, 9 - k, 1), -1) + const k = (n + h / 30) % 12; + const color = hDecimal - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); // Convert to Hex and prefix with "0" if required return Math.round(255 * color) .toString(16) - .padStart(2, "0") - } - return `#${f(0)}${f(8)}${f(4)}` + .padStart(2, "0"); + }; + return `#${f(0)}${f(8)}${f(4)}`; } diff --git a/site/src/utils/combineClasses.test.ts b/site/src/utils/combineClasses.test.ts index 21ec349db1..6e81d533a2 100644 --- a/site/src/utils/combineClasses.test.ts +++ b/site/src/utils/combineClasses.test.ts @@ -1,10 +1,10 @@ -import { combineClasses } from "./combineClasses" +import { combineClasses } from "./combineClasses"; const staticStyles = { text: "MuiText", success: "MuiText-Green", warning: "MuiText-Red", -} +}; describe("combineClasses", () => { it.each([ @@ -26,6 +26,6 @@ describe("combineClasses", () => { [{ [staticStyles.text]: true, [staticStyles.success]: false }, "MuiText"], [[staticStyles.text, staticStyles.success], "MuiText MuiText-Green"], ])(`classNames(%p) returns %p`, (staticClasses, result) => { - expect(combineClasses(staticClasses)).toBe(result) - }) -}) + expect(combineClasses(staticClasses)).toBe(result); + }); +}); diff --git a/site/src/utils/combineClasses.ts b/site/src/utils/combineClasses.ts index 041cac03ef..5153f0e4b5 100644 --- a/site/src/utils/combineClasses.ts +++ b/site/src/utils/combineClasses.ts @@ -3,10 +3,10 @@ export const appendCSSString = ( cssClass: string, ): string => { if (cssString === "") { - return cssClass + return cssClass; } - return `${cssString} ${cssClass}` -} + return `${cssString} ${cssClass}`; +}; /** * @param classes May be an object or an array. When using an object, each key @@ -26,22 +26,22 @@ export const combineClasses = ( | Array | Record = {}, ): string | undefined => { - let result = "" + let result = ""; if (Array.isArray(classes)) { for (const cssClass of classes) { if (cssClass) { - result = appendCSSString(result, cssClass) + result = appendCSSString(result, cssClass); } } } else { for (const cssClass in classes) { - const useClass = classes[cssClass] + const useClass = classes[cssClass]; if (useClass) { - result = appendCSSString(result, cssClass) + result = appendCSSString(result, cssClass); } } } - return result.length > 0 ? result : undefined -} + return result.length > 0 ? result : undefined; +}; diff --git a/site/src/utils/createDayString.ts b/site/src/utils/createDayString.ts index a48472e22a..0b58124c3f 100644 --- a/site/src/utils/createDayString.ts +++ b/site/src/utils/createDayString.ts @@ -1,12 +1,12 @@ -import dayjs from "dayjs" -import relativeTime from "dayjs/plugin/relativeTime" +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; -dayjs.extend(relativeTime) +dayjs.extend(relativeTime); /** * Returns a human-readable string describing the passing of time * Broken into its own module for testing purposes */ export function createDayString(time: string): string { - return dayjs().to(dayjs(time)) + return dayjs().to(dayjs(time)); } diff --git a/site/src/utils/delay.ts b/site/src/utils/delay.ts index cdbcf00bdf..35ee50b121 100644 --- a/site/src/utils/delay.ts +++ b/site/src/utils/delay.ts @@ -1,4 +1,4 @@ export const delay = (ms: number): Promise => new Promise((res) => { - setTimeout(res, ms) - }) + setTimeout(res, ms); + }); diff --git a/site/src/utils/deployOptions.ts b/site/src/utils/deployOptions.ts index 4f4ad3e64c..b50ef931c2 100644 --- a/site/src/utils/deployOptions.ts +++ b/site/src/utils/deployOptions.ts @@ -1,41 +1,41 @@ -import { useMemo } from "react" -import { DeploymentGroup, DeploymentOption } from "../api/types" +import { useMemo } from "react"; +import { DeploymentGroup, DeploymentOption } from "../api/types"; const deploymentOptions = ( options: DeploymentOption[], ...names: string[] ): DeploymentOption[] => { - const found: DeploymentOption[] = [] + const found: DeploymentOption[] = []; for (const name of names) { - const option = options.find((o) => o.name === name) + const option = options.find((o) => o.name === name); if (option) { - found.push(option) + found.push(option); } else { - throw new Error(`Deployment option ${name} not found`) + throw new Error(`Deployment option ${name} not found`); } } - return found -} + return found; +}; export const useDeploymentOptions = ( options: DeploymentOption[], ...names: string[] ): DeploymentOption[] => { - return useMemo(() => deploymentOptions(options, ...names), [options, names]) -} + return useMemo(() => deploymentOptions(options, ...names), [options, names]); +}; export const deploymentGroupHasParent = ( group: DeploymentGroup | undefined, parent: string, ): boolean => { if (!group) { - return false + return false; } if (group.parent) { - return deploymentGroupHasParent(group.parent, parent) + return deploymentGroupHasParent(group.parent, parent); } if (group.name === parent) { - return true + return true; } - return false -} + return false; +}; diff --git a/site/src/utils/docs.ts b/site/src/utils/docs.ts index 3fee2d5490..12ed7cc58a 100644 --- a/site/src/utils/docs.ts +++ b/site/src/utils/docs.ts @@ -1,28 +1,28 @@ -const DEFAULT_DOCS_URL = "https://coder.com/docs/v2/latest" +const DEFAULT_DOCS_URL = "https://coder.com/docs/v2/latest"; // Add cache to avoid DOM reading all the time -let CACHED_DOCS_URL: string | undefined +let CACHED_DOCS_URL: string | undefined; export const docs = (path: string) => { - return `${getBaseDocsURL()}${path}` -} + return `${getBaseDocsURL()}${path}`; +}; const getBaseDocsURL = () => { if (!CACHED_DOCS_URL) { const docsUrl = document .querySelector('meta[property="docs-url"]') - ?.getAttribute("content") - const isValidDocsURL = docsUrl && isURL(docsUrl) - CACHED_DOCS_URL = isValidDocsURL ? docsUrl : DEFAULT_DOCS_URL + ?.getAttribute("content"); + const isValidDocsURL = docsUrl && isURL(docsUrl); + CACHED_DOCS_URL = isValidDocsURL ? docsUrl : DEFAULT_DOCS_URL; } - return CACHED_DOCS_URL -} + return CACHED_DOCS_URL; +}; const isURL = (value: string) => { try { - new URL(value) - return true + new URL(value); + return true; } catch { - return false + return false; } -} +}; diff --git a/site/src/utils/ellipsizeText.test.ts b/site/src/utils/ellipsizeText.test.ts index eeb3f12dd3..4c563a8e24 100644 --- a/site/src/utils/ellipsizeText.test.ts +++ b/site/src/utils/ellipsizeText.test.ts @@ -1,5 +1,5 @@ -import { ellipsizeText } from "./ellipsizeText" -import { Nullable } from "./nullable" +import { ellipsizeText } from "./ellipsizeText"; +import { Nullable } from "./nullable"; describe("ellipsizeText", () => { it.each([ @@ -15,7 +15,7 @@ describe("ellipsizeText", () => { maxLength: number | undefined, output: Nullable, ) => { - expect(ellipsizeText(str, maxLength)).toBe(output) + expect(ellipsizeText(str, maxLength)).toBe(output); }, - ) -}) + ); +}); diff --git a/site/src/utils/ellipsizeText.ts b/site/src/utils/ellipsizeText.ts index 0899e65c38..3d66ad9ed9 100644 --- a/site/src/utils/ellipsizeText.ts +++ b/site/src/utils/ellipsizeText.ts @@ -1,4 +1,4 @@ -import { Nullable } from "./nullable" +import { Nullable } from "./nullable"; /** Truncates and ellipsizes text if it's longer than maxLength */ export const ellipsizeText = ( @@ -6,7 +6,9 @@ export const ellipsizeText = ( maxLength = 80, ): string | undefined => { if (typeof text !== "string") { - return + return; } - return text.length <= maxLength ? text : `${text.substr(0, maxLength - 3)}...` -} + return text.length <= maxLength + ? text + : `${text.substr(0, maxLength - 3)}...`; +}; diff --git a/site/src/utils/events.test.ts b/site/src/utils/events.test.ts index 805c1f8759..4d38275cdc 100644 --- a/site/src/utils/events.test.ts +++ b/site/src/utils/events.test.ts @@ -1,18 +1,18 @@ -import { dispatchCustomEvent, isCustomEvent } from "./events" +import { dispatchCustomEvent, isCustomEvent } from "./events"; describe("events", () => { describe("dispatchCustomEvent", () => { it("dispatch a custom event", (done) => { - const eventDetail = { title: "Event title" } + const eventDetail = { title: "Event title" }; window.addEventListener("eventType", (event) => { if (isCustomEvent(event)) { - expect(event.detail).toEqual(eventDetail) - done() + expect(event.detail).toEqual(eventDetail); + done(); } - }) + }); - dispatchCustomEvent("eventType", eventDetail) - }) - }) -}) + dispatchCustomEvent("eventType", eventDetail); + }); + }); +}); diff --git a/site/src/utils/events.ts b/site/src/utils/events.ts index 7627c06267..b694464157 100644 --- a/site/src/utils/events.ts +++ b/site/src/utils/events.ts @@ -10,14 +10,14 @@ export const dispatchCustomEvent = ( detail?: D, target: EventTarget = window, ): CustomEvent => { - const event = new CustomEvent(eventType, { detail }) + const event = new CustomEvent(eventType, { detail }); - target.dispatchEvent(event) + target.dispatchEvent(event); - return event -} + return event; +}; /** Annotates a custom event listener with descriptive type information. */ -export type CustomEventListener = (event: CustomEvent) => void +export type CustomEventListener = (event: CustomEvent) => void; /** * Determines if an Event object is a CustomEvent. @@ -28,5 +28,5 @@ export type CustomEventListener = (event: CustomEvent) => void export const isCustomEvent = ( event: CustomEvent | Event, ): event is CustomEvent => { - return Boolean((event as CustomEvent).detail) -} + return Boolean((event as CustomEvent).detail); +}; diff --git a/site/src/utils/filetree.test.ts b/site/src/utils/filetree.test.ts index 18b63788de..77e2c0febc 100644 --- a/site/src/utils/filetree.test.ts +++ b/site/src/utils/filetree.test.ts @@ -7,32 +7,32 @@ import { removeFile, createFile, traverse, -} from "./filetree" +} from "./filetree"; test("createFile() set file into the file tree", () => { let fileTree: FileTree = { "main.tf": "terraform", images: { "java.Dockerfile": "java dockerfile" }, - } + }; fileTree = createFile( "images/python.Dockerfile", fileTree, "python dockerfile", - ) + ); expect((fileTree.images as FileTree)["python.Dockerfile"]).toEqual( "python dockerfile", - ) -}) + ); +}); test("getFileContent() return the file content from the file tree", () => { const fileTree: FileTree = { "main.tf": "terraform content", images: { "java.Dockerfile": "java dockerfile" }, - } + }; expect(getFileContent("images/java.Dockerfile", fileTree)).toEqual( "java dockerfile", - ) -}) + ); +}); test("removeFile() removes a file from a folder", () => { let fileTree: FileTree = { @@ -41,16 +41,16 @@ test("removeFile() removes a file from a folder", () => { "java.Dockerfile": "java dockerfile", "python.Dockerfile": "python Dockerfile", }, - } - fileTree = removeFile("images/python.Dockerfile", fileTree) + }; + fileTree = removeFile("images/python.Dockerfile", fileTree); const expectedFileTree = { "main.tf": "terraform content", images: { "java.Dockerfile": "java dockerfile", }, - } - expect(expectedFileTree).toEqual(fileTree) -}) + }; + expect(expectedFileTree).toEqual(fileTree); +}); test("removeFile() removes a file from root", () => { let fileTree: FileTree = { @@ -59,16 +59,16 @@ test("removeFile() removes a file from root", () => { "java.Dockerfile": "java dockerfile", "python.Dockerfile": "python Dockerfile", }, - } - fileTree = removeFile("main.tf", fileTree) + }; + fileTree = removeFile("main.tf", fileTree); const expectedFileTree = { images: { "java.Dockerfile": "java dockerfile", "python.Dockerfile": "python Dockerfile", }, - } - expect(expectedFileTree).toEqual(fileTree) -}) + }; + expect(expectedFileTree).toEqual(fileTree); +}); test("moveFile() moves a file from in file tree", () => { let fileTree: FileTree = { @@ -77,12 +77,12 @@ test("moveFile() moves a file from in file tree", () => { "java.Dockerfile": "java dockerfile", "python.Dockerfile": "python Dockerfile", }, - } + }; fileTree = moveFile( "images/java.Dockerfile", "other/java.Dockerfile", fileTree, - ) + ); const expectedFileTree = { "main.tf": "terraform content", images: { @@ -91,37 +91,37 @@ test("moveFile() moves a file from in file tree", () => { other: { "java.Dockerfile": "java dockerfile", }, - } - expect(fileTree).toEqual(expectedFileTree) -}) + }; + expect(fileTree).toEqual(expectedFileTree); +}); test("existsFile() returns if there is or not a file", () => { const fileTree: FileTree = { "main.tf": "terraform content", images: { "java.Dockerfile": "java dockerfile" }, - } - expect(existsFile("images/java.Dockerfile", fileTree)).toBeTruthy() - expect(existsFile("no-existent-path", fileTree)).toBeFalsy() -}) + }; + expect(existsFile("images/java.Dockerfile", fileTree)).toBeTruthy(); + expect(existsFile("no-existent-path", fileTree)).toBeFalsy(); +}); test("isFolder() returns when a path is a folder or not", () => { const fileTree: FileTree = { "main.tf": "terraform content", images: { "java.Dockerfile": "java dockerfile" }, - } - expect(isFolder("images", fileTree)).toBeTruthy() - expect(isFolder("images/java.Dockerfile", fileTree)).toBeFalsy() -}) + }; + expect(isFolder("images", fileTree)).toBeTruthy(); + expect(isFolder("images/java.Dockerfile", fileTree)).toBeFalsy(); +}); test("traverse() go trough all the file tree files", () => { const fileTree: FileTree = { "main.tf": "terraform content", images: { "java.Dockerfile": "java dockerfile" }, - } - const filePaths: string[] = [] + }; + const filePaths: string[] = []; traverse(fileTree, (_content, _filename, fullPath) => { - filePaths.push(fullPath) - }) - const expectedFilePaths = ["main.tf", "images", "images/java.Dockerfile"] - expect(filePaths).toEqual(expectedFilePaths) -}) + filePaths.push(fullPath); + }); + const expectedFilePaths = ["main.tf", "images", "images/java.Dockerfile"]; + expect(filePaths).toEqual(expectedFilePaths); +}); diff --git a/site/src/utils/filetree.ts b/site/src/utils/filetree.ts index 9b5ea60b0a..833af78c92 100644 --- a/site/src/utils/filetree.ts +++ b/site/src/utils/filetree.ts @@ -1,11 +1,11 @@ -import set from "lodash/set" -import has from "lodash/has" -import unset from "lodash/unset" -import get from "lodash/get" +import set from "lodash/set"; +import has from "lodash/has"; +import unset from "lodash/unset"; +import get from "lodash/get"; export type FileTree = { - [key: string]: FileTree | string -} + [key: string]: FileTree | string; +}; export const createFile = ( path: string, @@ -13,71 +13,71 @@ export const createFile = ( value: string, ): FileTree => { if (existsFile(path, fileTree)) { - throw new Error(`File ${path} already exists`) + throw new Error(`File ${path} already exists`); } - const pathError = validatePath(path, fileTree) + const pathError = validatePath(path, fileTree); if (pathError) { - throw new Error(pathError) + throw new Error(pathError); } - return set(fileTree, path.split("/"), value) -} + return set(fileTree, path.split("/"), value); +}; export const validatePath = ( path: string, fileTree: FileTree, ): string | undefined => { - const paths = path.split("/") - paths.pop() // The last item is the filename + const paths = path.split("/"); + paths.pop(); // The last item is the filename for (let i = 0; i <= paths.length; i++) { - const path = paths.slice(0, i + 1) - const pathStr = path.join("/") + const path = paths.slice(0, i + 1); + const pathStr = path.join("/"); if (existsFile(pathStr, fileTree) && !isFolder(pathStr, fileTree)) { - return `Invalid path. The path ${path} is not a folder` + return `Invalid path. The path ${path} is not a folder`; } } -} +}; export const updateFile = ( path: string, content: FileTree | string, fileTree: FileTree, ): FileTree => { - return set(fileTree, path.split("/"), content) -} + return set(fileTree, path.split("/"), content); +}; export const existsFile = (path: string, fileTree: FileTree) => { - return has(fileTree, path.split("/")) -} + return has(fileTree, path.split("/")); +}; export const removeFile = (path: string, fileTree: FileTree) => { - const updatedFileTree = { ...fileTree } - unset(updatedFileTree, path.split("/")) - return updatedFileTree -} + const updatedFileTree = { ...fileTree }; + unset(updatedFileTree, path.split("/")); + return updatedFileTree; +}; export const moveFile = ( currentPath: string, newPath: string, fileTree: FileTree, ) => { - const content = getFileContent(currentPath, fileTree) + const content = getFileContent(currentPath, fileTree); if (typeof content !== "string") { - throw new Error("Move folders is not allowed") + throw new Error("Move folders is not allowed"); } - fileTree = removeFile(currentPath, fileTree) - fileTree = createFile(newPath, fileTree, content) - return fileTree -} + fileTree = removeFile(currentPath, fileTree); + fileTree = createFile(newPath, fileTree, content); + return fileTree; +}; export const getFileContent = (path: string, fileTree: FileTree) => { - return get(fileTree, path.split("/")) as string | FileTree -} + return get(fileTree, path.split("/")) as string | FileTree; +}; export const isFolder = (path: string, fileTree: FileTree) => { - const content = getFileContent(path, fileTree) - return typeof content === "object" -} + const content = getFileContent(path, fileTree); + return typeof content === "object"; +}; export const traverse = ( fileTree: FileTree, @@ -89,11 +89,11 @@ export const traverse = ( parent?: string, ) => { Object.keys(fileTree).forEach((filename) => { - const fullPath = parent ? `${parent}/${filename}` : filename - const content = fileTree[filename] - callback(content, filename, fullPath) + const fullPath = parent ? `${parent}/${filename}` : filename; + const content = fileTree[filename]; + callback(content, filename, fullPath); if (typeof content === "object") { - traverse(content, callback, fullPath) + traverse(content, callback, fullPath); } - }) -} + }); +}; diff --git a/site/src/utils/filters.test.ts b/site/src/utils/filters.test.ts index 0f0ad348d2..f5eb2d0767 100644 --- a/site/src/utils/filters.test.ts +++ b/site/src/utils/filters.test.ts @@ -1,5 +1,5 @@ -import * as TypesGen from "../api/typesGenerated" -import { queryToFilter } from "./filters" +import * as TypesGen from "../api/typesGenerated"; +import { queryToFilter } from "./filters"; describe("queryToFilter", () => { it.each< @@ -15,6 +15,6 @@ describe("queryToFilter", () => { [" key:val owner:me ", { q: "key:val owner:me" }], ["status:failed", { q: "status:failed" }], ])(`query=%p, filter=%p`, (query, filter) => { - expect(queryToFilter(query)).toEqual(filter) - }) -}) + expect(queryToFilter(query)).toEqual(filter); + }); +}); diff --git a/site/src/utils/filters.ts b/site/src/utils/filters.ts index 8f477278b5..e01b423ea7 100644 --- a/site/src/utils/filters.ts +++ b/site/src/utils/filters.ts @@ -1,13 +1,13 @@ -import * as TypesGen from "../api/typesGenerated" +import * as TypesGen from "../api/typesGenerated"; export const queryToFilter = ( query?: string, ): TypesGen.WorkspaceFilter | TypesGen.UsersRequest => { - const preparedQuery = query?.trim().replace(/ +/g, " ") + const preparedQuery = query?.trim().replace(/ +/g, " "); return { q: preparedQuery, - } -} + }; +}; export const workspaceFilterQuery = { me: "owner:me", @@ -15,9 +15,9 @@ export const workspaceFilterQuery = { running: "status:running", failed: "status:failed", dormant: "dormant_at:1970-01-01", -} +}; export const userFilterQuery = { active: "status:active", all: "", -} +}; diff --git a/site/src/utils/formUtils.test.ts b/site/src/utils/formUtils.test.ts index ec495d50c1..fab07cb7d6 100644 --- a/site/src/utils/formUtils.test.ts +++ b/site/src/utils/formUtils.test.ts @@ -1,15 +1,15 @@ -import { FormikContextType } from "formik/dist/types" -import { getFormHelpers, nameValidator, onChangeTrimmed } from "./formUtils" -import { mockApiError } from "testHelpers/entities" +import { FormikContextType } from "formik/dist/types"; +import { getFormHelpers, nameValidator, onChangeTrimmed } from "./formUtils"; +import { mockApiError } from "testHelpers/entities"; interface TestType { - untouchedGoodField: string - untouchedBadField: string - touchedGoodField: string - touchedBadField: string + untouchedGoodField: string; + untouchedBadField: string; + touchedGoodField: string; + touchedBadField: string; } -const mockHandleChange = jest.fn() +const mockHandleChange = jest.fn(); const form = { errors: { @@ -32,42 +32,42 @@ const form = { onBlur: jest.fn(), onChange: jest.fn(), value: "", - } + }; }, -} as unknown as FormikContextType +} as unknown as FormikContextType; -const nameSchema = nameValidator("name") +const nameSchema = nameValidator("name"); describe("form util functions", () => { describe("getFormHelpers", () => { describe("without API errors", () => { - const getFieldHelpers = getFormHelpers(form) - const untouchedGoodResult = getFieldHelpers("untouchedGoodField") - const untouchedBadResult = getFieldHelpers("untouchedBadField") - const touchedGoodResult = getFieldHelpers("touchedGoodField") - const touchedBadResult = getFieldHelpers("touchedBadField") + const getFieldHelpers = getFormHelpers(form); + const untouchedGoodResult = getFieldHelpers("untouchedGoodField"); + const untouchedBadResult = getFieldHelpers("untouchedBadField"); + const touchedGoodResult = getFieldHelpers("touchedGoodField"); + const touchedBadResult = getFieldHelpers("touchedBadField"); it("populates the 'field props'", () => { - expect(untouchedGoodResult.name).toEqual("untouchedGoodField") - expect(untouchedGoodResult.onBlur).toBeDefined() - expect(untouchedGoodResult.onChange).toBeDefined() - expect(untouchedGoodResult.value).toBeDefined() - }) + expect(untouchedGoodResult.name).toEqual("untouchedGoodField"); + expect(untouchedGoodResult.onBlur).toBeDefined(); + expect(untouchedGoodResult.onChange).toBeDefined(); + expect(untouchedGoodResult.value).toBeDefined(); + }); it("sets the id to the name", () => { - expect(untouchedGoodResult.id).toEqual("untouchedGoodField") - }) + expect(untouchedGoodResult.id).toEqual("untouchedGoodField"); + }); it("sets error to true if touched and invalid", () => { - expect(untouchedGoodResult.error).toBeFalsy - expect(untouchedBadResult.error).toBeFalsy - expect(touchedGoodResult.error).toBeFalsy - expect(touchedBadResult.error).toBeTruthy - }) + expect(untouchedGoodResult.error).toBeFalsy; + expect(untouchedBadResult.error).toBeFalsy; + expect(touchedGoodResult.error).toBeFalsy; + expect(touchedBadResult.error).toBeTruthy; + }); it("sets helperText to the error message if touched and invalid", () => { - expect(untouchedGoodResult.helperText).toBeUndefined - expect(untouchedBadResult.helperText).toBeUndefined - expect(touchedGoodResult.helperText).toBeUndefined - expect(touchedBadResult.helperText).toEqual("oops!") - }) - }) + expect(untouchedGoodResult.helperText).toBeUndefined; + expect(untouchedBadResult.helperText).toBeUndefined; + expect(touchedGoodResult.helperText).toBeUndefined; + expect(touchedBadResult.helperText).toEqual("oops!"); + }); + }); describe("with API errors", () => { it("shows an error if there is only an API error", () => { const getFieldHelpers = getFormHelpers( @@ -80,17 +80,17 @@ describe("form util functions", () => { }, ], }), - ) - const result = getFieldHelpers("touchedGoodField") - expect(result.error).toBeTruthy() - expect(result.helperText).toEqual("API error!") - }) + ); + const result = getFieldHelpers("touchedGoodField"); + expect(result.error).toBeTruthy(); + expect(result.helperText).toEqual("API error!"); + }); it("shows an error if there is only a validation error", () => { - const getFieldHelpers = getFormHelpers(form, {}) - const result = getFieldHelpers("touchedBadField") - expect(result.error).toBeTruthy() - expect(result.helperText).toEqual("oops!") - }) + const getFieldHelpers = getFormHelpers(form, {}); + const result = getFieldHelpers("touchedBadField"); + expect(result.error).toBeTruthy(); + expect(result.helperText).toEqual("oops!"); + }); it("shows the API error if both are present", () => { const getFieldHelpers = getFormHelpers( form, @@ -102,57 +102,57 @@ describe("form util functions", () => { }, ], }), - ) - const result = getFieldHelpers("touchedBadField") - expect(result.error).toBeTruthy() - expect(result.helperText).toEqual("API error!") - }) - }) - }) + ); + const result = getFieldHelpers("touchedBadField"); + expect(result.error).toBeTruthy(); + expect(result.helperText).toEqual("API error!"); + }); + }); + }); describe("onChangeTrimmed", () => { it("calls handleChange with trimmed value", () => { const event = { target: { value: " hello " }, - } as React.ChangeEvent - onChangeTrimmed(form)(event) + } as React.ChangeEvent; + onChangeTrimmed(form)(event); expect(mockHandleChange).toHaveBeenCalledWith({ target: { value: "hello" }, - }) - }) - }) + }); + }); + }); describe("nameValidator", () => { it("allows a 1-letter name", () => { - const validate = () => nameSchema.validateSync("a") - expect(validate).not.toThrow() - }) + const validate = () => nameSchema.validateSync("a"); + expect(validate).not.toThrow(); + }); it("allows a 32-letter name", () => { - const input = Array(32).fill("a").join("") - const validate = () => nameSchema.validateSync(input) - expect(validate).not.toThrow() - }) + const input = Array(32).fill("a").join(""); + const validate = () => nameSchema.validateSync(input); + expect(validate).not.toThrow(); + }); it("allows 'test-3' to be used as name", () => { - const validate = () => nameSchema.validateSync("test-3") - expect(validate).not.toThrow() - }) + const validate = () => nameSchema.validateSync("test-3"); + expect(validate).not.toThrow(); + }); it("allows '3-test' to be used as a name", () => { - const validate = () => nameSchema.validateSync("3-test") - expect(validate).not.toThrow() - }) + const validate = () => nameSchema.validateSync("3-test"); + expect(validate).not.toThrow(); + }); it("disallows a 33-letter name", () => { - const input = Array(33).fill("a").join("") - const validate = () => nameSchema.validateSync(input) - expect(validate).toThrow() - }) + const input = Array(33).fill("a").join(""); + const validate = () => nameSchema.validateSync(input); + expect(validate).toThrow(); + }); it("disallows a space", () => { - const validate = () => nameSchema.validateSync("test 3") - expect(validate).toThrow() - }) - }) -}) + const validate = () => nameSchema.validateSync("test 3"); + expect(validate).toThrow(); + }); + }); +}); diff --git a/site/src/utils/formUtils.ts b/site/src/utils/formUtils.ts index 828f6ed103..bfa041f6af 100644 --- a/site/src/utils/formUtils.ts +++ b/site/src/utils/formUtils.ts @@ -1,36 +1,36 @@ -import { isApiValidationError, mapApiErrorToFieldErrors } from "api/errors" -import { FormikContextType, FormikErrors, getIn } from "formik" +import { isApiValidationError, mapApiErrorToFieldErrors } from "api/errors"; +import { FormikContextType, FormikErrors, getIn } from "formik"; import { ChangeEvent, ChangeEventHandler, FocusEventHandler, ReactNode, -} from "react" -import * as Yup from "yup" +} from "react"; +import * as Yup from "yup"; const Language = { nameRequired: (name: string): string => { - return name ? `Please enter a ${name.toLowerCase()}.` : "Required" + return name ? `Please enter a ${name.toLowerCase()}.` : "Required"; }, nameInvalidChars: (name: string): string => { - return `${name} must start with a-Z or 0-9 and can contain a-Z, 0-9 or -` + return `${name} must start with a-Z or 0-9 and can contain a-Z, 0-9 or -`; }, nameTooLong: (name: string, len: number): string => { - return `${name} cannot be longer than ${len} characters` + return `${name} cannot be longer than ${len} characters`; }, templateDisplayNameInvalidChars: (name: string): string => { - return `${name} must start and end with non-whitespace character` + return `${name} must start and end with non-whitespace character`; }, -} +}; interface FormHelpers { - name: string - onBlur: FocusEventHandler - onChange: ChangeEventHandler - id: string - value?: string | number - error: boolean - helperText?: ReactNode + name: string; + onBlur: FocusEventHandler; + onChange: ChangeEventHandler; + id: string; + value?: string | number; + error: boolean; + helperText?: ReactNode; } export const getFormHelpers = @@ -45,44 +45,44 @@ export const getFormHelpers = ? (mapApiErrorToFieldErrors( error.response.data, ) as FormikErrors & { [key: string]: string }) - : undefined + : undefined; // Since the fieldName can be a path string like parameters[0].value we need to use getIn - const touched = Boolean(getIn(form.touched, fieldName.toString())) - const formError = getIn(form.errors, fieldName.toString()) + const touched = Boolean(getIn(form.touched, fieldName.toString())); + const formError = getIn(form.errors, fieldName.toString()); // Since the field in the form can be diff from the backend, we need to // check for both when getting the error - const apiField = backendFieldName ?? fieldName - const apiError = apiValidationErrors?.[apiField.toString()] - const errorToDisplay = apiError ?? formError + const apiField = backendFieldName ?? fieldName; + const apiError = apiValidationErrors?.[apiField.toString()]; + const errorToDisplay = apiError ?? formError; return { ...form.getFieldProps(fieldName), id: fieldName.toString(), error: touched && Boolean(errorToDisplay), helperText: touched ? errorToDisplay ?? helperText : helperText, - } - } + }; + }; export const onChangeTrimmed = (form: FormikContextType, callback?: () => void) => (event: ChangeEvent): void => { - event.target.value = event.target.value.trim() - form.handleChange(event) - callback && callback() - } + event.target.value = event.target.value.trim(); + form.handleChange(event); + callback && callback(); + }; // REMARK: Keep these consts in sync with coderd/httpapi/httpapi.go -const maxLenName = 32 -const templateDisplayNameMaxLength = 64 -const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/ -const templateDisplayNameRE = /^[^\s](.*[^\s])?$/ +const maxLenName = 32; +const templateDisplayNameMaxLength = 64; +const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/; +const templateDisplayNameRE = /^[^\s](.*[^\s])?$/; // REMARK: see #1756 for name/username semantics export const nameValidator = (name: string): Yup.StringSchema => Yup.string() .required(Language.nameRequired(name)) .matches(usernameRE, Language.nameInvalidChars(name)) - .max(maxLenName, Language.nameTooLong(name, maxLenName)) + .max(maxLenName, Language.nameTooLong(name, maxLenName)); export const templateDisplayNameValidator = ( displayName: string, @@ -96,6 +96,6 @@ export const templateDisplayNameValidator = ( templateDisplayNameMaxLength, Language.nameTooLong(displayName, templateDisplayNameMaxLength), ) - .optional() + .optional(); -export const iconValidator = Yup.string().label("Icon").max(256) +export const iconValidator = Yup.string().label("Icon").max(256); diff --git a/site/src/utils/gitAuth.ts b/site/src/utils/gitAuth.ts index 11a15668f1..66b6ac9bc3 100644 --- a/site/src/utils/gitAuth.ts +++ b/site/src/utils/gitAuth.ts @@ -1 +1 @@ -export const REFRESH_GITAUTH_BROADCAST_CHANNEL = "gitauth_refresh" +export const REFRESH_GITAUTH_BROADCAST_CHANNEL = "gitauth_refresh"; diff --git a/site/src/utils/groups.ts b/site/src/utils/groups.ts index f35236fca9..7b89f8e6d1 100644 --- a/site/src/utils/groups.ts +++ b/site/src/utils/groups.ts @@ -1,4 +1,4 @@ -import { Group } from "api/typesGenerated" +import { Group } from "api/typesGenerated"; export const everyOneGroup = (organizationId: string): Group => ({ id: organizationId, @@ -9,7 +9,7 @@ export const everyOneGroup = (organizationId: string): Group => ({ avatar_url: "", quota_allowance: 0, source: "user", -}) +}); /** * Returns true if the provided group is the 'Everyone' group. @@ -21,21 +21,21 @@ export const everyOneGroup = (organizationId: string): Group => ({ * organization ID. */ export const isEveryoneGroup = (group: Group): boolean => - group.id === group.organization_id + group.id === group.organization_id; export const getGroupSubtitle = (group: Group): string => { // It is the everyone group when a group id is the same of the org id if (group.id === group.organization_id) { - return `All users` + return `All users`; } if (!group.members) { - return `0 members` + return `0 members`; } if (group.members.length === 1) { - return `1 member` + return `1 member`; } - return `${group.members.length} members` -} + return `${group.members.length} members`; +}; diff --git a/site/src/utils/latency.ts b/site/src/utils/latency.ts index 1767742d4d..0a7f45d551 100644 --- a/site/src/utils/latency.ts +++ b/site/src/utils/latency.ts @@ -1,16 +1,16 @@ -import { Theme } from "@mui/material/styles" +import { Theme } from "@mui/material/styles"; export const getLatencyColor = (theme: Theme, latency?: number) => { if (!latency) { - return theme.palette.text.secondary + return theme.palette.text.secondary; } - let color = theme.palette.success.light + let color = theme.palette.success.light; if (latency >= 150 && latency < 300) { - color = theme.palette.warning.light + color = theme.palette.warning.light; } else if (latency >= 300) { - color = theme.palette.error.light + color = theme.palette.error.light; } - return color -} + return color; +}; diff --git a/site/src/utils/nullable.ts b/site/src/utils/nullable.ts index dda3b129e8..6a9361e503 100644 --- a/site/src/utils/nullable.ts +++ b/site/src/utils/nullable.ts @@ -2,4 +2,4 @@ * A Nullable may be its concrete type, `null` or `undefined` * @remark Exact opposite of the native TS type NonNullable */ -export type Nullable = null | undefined | T +export type Nullable = null | undefined | T; diff --git a/site/src/utils/page.ts b/site/src/utils/page.ts index 96f67e8596..965d27e0d7 100644 --- a/site/src/utils/page.ts +++ b/site/src/utils/page.ts @@ -1,4 +1,4 @@ export const pageTitle = (prefix: string | string[]): string => { - const title = Array.isArray(prefix) ? prefix.join(" · ") : prefix - return `${title} - Coder` -} + const title = Array.isArray(prefix) ? prefix.join(" · ") : prefix; + return `${title} - Coder`; +}; diff --git a/site/src/utils/portForward.ts b/site/src/utils/portForward.ts index ce03d54dc4..d2162d948e 100644 --- a/site/src/utils/portForward.ts +++ b/site/src/utils/portForward.ts @@ -5,10 +5,10 @@ export const portForwardURL = ( workspaceName: string, username: string, ): string => { - const { location } = window + const { location } = window; const subdomain = `${ isNaN(port) ? 3000 : port - }--${agentName}--${workspaceName}--${username}` - return `${location.protocol}//${host}`.replace("*", subdomain) -} + }--${agentName}--${workspaceName}--${username}`; + return `${location.protocol}//${host}`.replace("*", subdomain); +}; diff --git a/site/src/utils/random.ts b/site/src/utils/random.ts index e4d51da67b..7db0d8fe02 100644 --- a/site/src/utils/random.ts +++ b/site/src/utils/random.ts @@ -7,13 +7,13 @@ * @see */ export const generateRandomString = (bytes: number): string => { - const byteArr = window.crypto.getRandomValues(new Uint8Array(bytes)) + const byteArr = window.crypto.getRandomValues(new Uint8Array(bytes)); // The types for `map` don't seem to support mapping from one array type to // another and `String.fromCharCode.apply` wants `number[]` so loop like this // instead. - const strArr: string[] = [] + const strArr: string[] = []; for (const byte of byteArr) { - strArr.push(String.fromCharCode(byte)) + strArr.push(String.fromCharCode(byte)); } - return btoa(strArr.join("")) -} + return btoa(strArr.join("")); +}; diff --git a/site/src/utils/redirect.test.ts b/site/src/utils/redirect.test.ts index c4be138286..77ad787d5a 100644 --- a/site/src/utils/redirect.test.ts +++ b/site/src/utils/redirect.test.ts @@ -1,20 +1,20 @@ -import { embedRedirect, retrieveRedirect } from "./redirect" +import { embedRedirect, retrieveRedirect } from "./redirect"; describe("redirect helper functions", () => { describe("embedRedirect", () => { it("embeds the page to return to in the URL", () => { - const result = embedRedirect("/workspaces", "/page") - expect(result).toEqual("/page?redirect=%2Fworkspaces") - }) + const result = embedRedirect("/workspaces", "/page"); + expect(result).toEqual("/page?redirect=%2Fworkspaces"); + }); it("defaults to navigating to the login page", () => { - const result = embedRedirect("/workspaces") - expect(result).toEqual("/login?redirect=%2Fworkspaces") - }) - }) + const result = embedRedirect("/workspaces"); + expect(result).toEqual("/login?redirect=%2Fworkspaces"); + }); + }); describe("retrieveRedirect", () => { it("retrieves the page to return to from the URL", () => { - const result = retrieveRedirect("?redirect=%2Fworkspaces") - expect(result).toEqual("/workspaces") - }) - }) -}) + const result = retrieveRedirect("?redirect=%2Fworkspaces"); + expect(result).toEqual("/workspaces"); + }); + }); +}); diff --git a/site/src/utils/redirect.ts b/site/src/utils/redirect.ts index 6bee5afe53..efaf0f3738 100644 --- a/site/src/utils/redirect.ts +++ b/site/src/utils/redirect.ts @@ -8,7 +8,7 @@ export const embedRedirect = ( returnTo: string, navigateTo = "/login", -): string => `${navigateTo}?redirect=${encodeURIComponent(returnTo)}` +): string => `${navigateTo}?redirect=${encodeURIComponent(returnTo)}`; /** * Retrieves a url from the query string of the current URL @@ -16,8 +16,8 @@ export const embedRedirect = ( * @returns the URL to redirect to */ export const retrieveRedirect = (search: string): string => { - const defaultRedirect = "/" - const searchParams = new URLSearchParams(search) - const redirect = searchParams.get("redirect") - return redirect ? redirect : defaultRedirect -} + const defaultRedirect = "/"; + const searchParams = new URLSearchParams(search); + const redirect = searchParams.get("redirect"); + return redirect ? redirect : defaultRedirect; +}; diff --git a/site/src/utils/richParameters.test.ts b/site/src/utils/richParameters.test.ts index 66fceadd57..d2a0bdc7a2 100644 --- a/site/src/utils/richParameters.test.ts +++ b/site/src/utils/richParameters.test.ts @@ -1,5 +1,5 @@ -import { TemplateVersionParameter } from "api/typesGenerated" -import { getInitialRichParameterValues } from "./richParameters" +import { TemplateVersionParameter } from "api/typesGenerated"; +import { getInitialRichParameterValues } from "./richParameters"; test("getInitialRichParameterValues return default value when default build parameter is not valid", () => { const templateParameters: TemplateVersionParameter[] = [ @@ -41,13 +41,13 @@ test("getInitialRichParameterValues return default value when default build para required: false, ephemeral: false, }, - ] + ]; - const cpuParameter = templateParameters[0] + const cpuParameter = templateParameters[0]; const [cpuParameterInitialValue] = getInitialRichParameterValues( templateParameters, [{ name: cpuParameter.name, value: "100" }], - ) + ); - expect(cpuParameterInitialValue.value).toBe(cpuParameter.default_value) -}) + expect(cpuParameterInitialValue.value).toBe(cpuParameter.default_value); +}); diff --git a/site/src/utils/richParameters.ts b/site/src/utils/richParameters.ts index 23a0b06b78..80ceb88630 100644 --- a/site/src/utils/richParameters.ts +++ b/site/src/utils/richParameters.ts @@ -1,9 +1,9 @@ import { TemplateVersionParameter, WorkspaceBuildParameter, -} from "api/typesGenerated" -import { useTranslation } from "react-i18next" -import * as Yup from "yup" +} from "api/typesGenerated"; +import { useTranslation } from "react-i18next"; +import * as Yup from "yup"; export const getInitialRichParameterValues = ( templateParameters: TemplateVersionParameter[], @@ -12,42 +12,42 @@ export const getInitialRichParameterValues = ( return templateParameters.map((parameter) => { const existentBuildParameter = buildParameters?.find( (p) => p.name === parameter.name, - ) + ); const shouldReturnTheDefaultValue = !existentBuildParameter || !isValidValue(parameter, existentBuildParameter) || - parameter.ephemeral + parameter.ephemeral; if (shouldReturnTheDefaultValue) { return { name: parameter.name, value: parameter.default_value, - } + }; } - return existentBuildParameter - }) -} + return existentBuildParameter; + }); +}; const isValidValue = ( templateParam: TemplateVersionParameter, buildParam: WorkspaceBuildParameter, ) => { if (templateParam.options.length > 0) { - const validValues = templateParam.options.map((option) => option.value) - return validValues.includes(buildParam.value) + const validValues = templateParam.options.map((option) => option.value); + return validValues.includes(buildParam.value); } - return true -} + return true; +}; export const useValidationSchemaForRichParameters = ( ns: string, templateParameters?: TemplateVersionParameter[], lastBuildParameters?: WorkspaceBuildParameter[], ): Yup.AnySchema => { - const { t } = useTranslation(ns) + const { t } = useTranslation(ns); if (!templateParameters) { - return Yup.object() + return Yup.object(); } return Yup.array() @@ -55,10 +55,10 @@ export const useValidationSchemaForRichParameters = ( Yup.object().shape({ name: Yup.string().required(), value: Yup.string().test("verify with template", (val, ctx) => { - const name = ctx.parent.name + const name = ctx.parent.name; const templateParameter = templateParameters.find( (parameter) => parameter.name === name, - ) + ); if (templateParameter) { switch (templateParameter.type) { case "number": @@ -72,7 +72,7 @@ export const useValidationSchemaForRichParameters = ( message: t("validationNumberLesserThan", { min: templateParameter.validation_min, }).toString(), - }) + }); } } else if ( !templateParameter.validation_min && @@ -84,7 +84,7 @@ export const useValidationSchemaForRichParameters = ( message: t("validationNumberGreaterThan", { max: templateParameter.validation_max, }).toString(), - }) + }); } } else if ( templateParameter.validation_min && @@ -100,7 +100,7 @@ export const useValidationSchemaForRichParameters = ( min: templateParameter.validation_min, max: templateParameter.validation_max, }).toString(), - }) + }); } } @@ -110,7 +110,7 @@ export const useValidationSchemaForRichParameters = ( ) { const lastBuildParameter = lastBuildParameters.find( (last) => last.name === name, - ) + ); if (lastBuildParameter) { switch (templateParameter.validation_monotonic) { case "increasing": @@ -120,9 +120,9 @@ export const useValidationSchemaForRichParameters = ( message: t("validationNumberNotIncreasing", { last: lastBuildParameter.value, }).toString(), - }) + }); } - break + break; case "decreasing": if (Number(lastBuildParameter.value) < Number(val)) { return ctx.createError({ @@ -130,23 +130,23 @@ export const useValidationSchemaForRichParameters = ( message: t("validationNumberNotDecreasing", { last: lastBuildParameter.value, }).toString(), - }) + }); } - break + break; } } } - break + break; case "string": { if ( !templateParameter.validation_regex || templateParameter.validation_regex.length === 0 ) { - return true + return true; } - const regex = new RegExp(templateParameter.validation_regex) + const regex = new RegExp(templateParameter.validation_regex); if (val && !regex.test(val)) { return ctx.createError({ path: ctx.path, @@ -154,15 +154,15 @@ export const useValidationSchemaForRichParameters = ( error: templateParameter.validation_error, pattern: templateParameter.validation_regex, }).toString(), - }) + }); } } - break + break; } } - return true + return true; }), }), ) - .required() -} + .required(); +}; diff --git a/site/src/utils/schedule.test.ts b/site/src/utils/schedule.test.ts index 2268674097..99bd202fb9 100644 --- a/site/src/utils/schedule.test.ts +++ b/site/src/utils/schedule.test.ts @@ -1,7 +1,7 @@ -import dayjs from "dayjs" -import duration from "dayjs/plugin/duration" -import { Workspace } from "../api/typesGenerated" -import * as Mocks from "../testHelpers/entities" +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import { Workspace } from "../api/typesGenerated"; +import * as Mocks from "../testHelpers/entities"; import { deadlineExtensionMax, deadlineExtensionMin, @@ -10,11 +10,11 @@ import { getMaxDeadlineChange, getMinDeadline, stripTimezone, -} from "./schedule" +} from "./schedule"; -dayjs.extend(duration) -const now = dayjs() -const startTime = dayjs(Mocks.MockWorkspaceBuild.updated_at) +dayjs.extend(duration); +const now = dayjs(); +const startTime = dayjs(Mocks.MockWorkspaceBuild.updated_at); describe("util/schedule", () => { describe("stripTimezone", () => { @@ -23,9 +23,9 @@ describe("util/schedule", () => { ["CRON_TZ=America/Central 0 8 1,2,4,5", "0 8 1,2,4,5"], ["30 9 1-5", "30 9 1-5"], ])(`stripTimezone(%p) returns %p`, (input, expected) => { - expect(stripTimezone(input)).toBe(expected) - }) - }) + expect(stripTimezone(input)).toBe(expected); + }); + }); describe("extractTimezone", () => { it.each<[string, string]>([ @@ -33,10 +33,10 @@ describe("util/schedule", () => { ["CRON_TZ=America/Central 0 8 1,2,4,5", "America/Central"], ["30 9 1-5", "UTC"], ])(`extractTimezone(%p) returns %p`, (input, expected) => { - expect(extractTimezone(input)).toBe(expected) - }) - }) -}) + expect(extractTimezone(input)).toBe(expected); + }); + }); +}); describe("maxDeadline", () => { const workspace: Workspace = { @@ -45,32 +45,32 @@ describe("maxDeadline", () => { ...Mocks.MockWorkspaceBuild, deadline: startTime.add(8, "hours").utc().format(), }, - } + }; it("should be 24 hours from the workspace start time", () => { - const delta = getMaxDeadline(workspace).diff(startTime) - expect(delta).toEqual(deadlineExtensionMax.asMilliseconds()) - }) -}) + const delta = getMaxDeadline(workspace).diff(startTime); + expect(delta).toEqual(deadlineExtensionMax.asMilliseconds()); + }); +}); describe("minDeadline", () => { it("should never be less than 30 minutes", () => { - const delta = getMinDeadline().diff(now) - expect(delta).toBeGreaterThanOrEqual(deadlineExtensionMin.asMilliseconds()) - }) -}) + const delta = getMinDeadline().diff(now); + expect(delta).toBeGreaterThanOrEqual(deadlineExtensionMin.asMilliseconds()); + }); +}); describe("getMaxDeadlineChange", () => { it("should return the number of hours you can add before hitting the max deadline", () => { - const deadline = dayjs() - const maxDeadline = dayjs().add(1, "hour").add(40, "minutes") + const deadline = dayjs(); + const maxDeadline = dayjs().add(1, "hour").add(40, "minutes"); // you can only add one hour even though the max is 1:40 away - expect(getMaxDeadlineChange(deadline, maxDeadline)).toEqual(1) - }) + expect(getMaxDeadlineChange(deadline, maxDeadline)).toEqual(1); + }); it("should return the number of hours you can subtract before hitting the min deadline", () => { - const deadline = dayjs().add(2, "hours").add(40, "minutes") - const minDeadline = dayjs() + const deadline = dayjs().add(2, "hours").add(40, "minutes"); + const minDeadline = dayjs(); // you can only subtract 2 hours even though the min is 2:40 less - expect(getMaxDeadlineChange(deadline, minDeadline)).toEqual(2) - }) -}) + expect(getMaxDeadlineChange(deadline, minDeadline)).toEqual(2); + }); +}); diff --git a/site/src/utils/schedule.ts b/site/src/utils/schedule.ts index 1bd71185f3..bbb406f3dc 100644 --- a/site/src/utils/schedule.ts +++ b/site/src/utils/schedule.ts @@ -1,20 +1,20 @@ -import cronstrue from "cronstrue" -import dayjs, { Dayjs } from "dayjs" -import advancedFormat from "dayjs/plugin/advancedFormat" -import duration from "dayjs/plugin/duration" -import relativeTime from "dayjs/plugin/relativeTime" -import timezone from "dayjs/plugin/timezone" -import utc from "dayjs/plugin/utc" -import { Workspace } from "../api/typesGenerated" -import { isWorkspaceOn } from "./workspace" +import cronstrue from "cronstrue"; +import dayjs, { Dayjs } from "dayjs"; +import advancedFormat from "dayjs/plugin/advancedFormat"; +import duration from "dayjs/plugin/duration"; +import relativeTime from "dayjs/plugin/relativeTime"; +import timezone from "dayjs/plugin/timezone"; +import utc from "dayjs/plugin/utc"; +import { Workspace } from "../api/typesGenerated"; +import { isWorkspaceOn } from "./workspace"; // REMARK: some plugins depend on utc, so it's listed first. Otherwise they're // sorted alphabetically. -dayjs.extend(utc) -dayjs.extend(advancedFormat) -dayjs.extend(duration) -dayjs.extend(relativeTime) -dayjs.extend(timezone) +dayjs.extend(utc); +dayjs.extend(advancedFormat); +dayjs.extend(duration); +dayjs.extend(relativeTime); +dayjs.extend(timezone); /** * @fileoverview Client-side counterpart of the coderd/autostart/schedule Go @@ -26,14 +26,14 @@ dayjs.extend(timezone) * DEFAULT_TIMEZONE is the default timezone that crontab assumes unless one is * specified. */ -const DEFAULT_TIMEZONE = "UTC" +const DEFAULT_TIMEZONE = "UTC"; /** * stripTimezone strips a leading timezone from a schedule string */ export const stripTimezone = (raw: string): string => { - return raw.replace(/CRON_TZ=\S*\s/, "") -} + return raw.replace(/CRON_TZ=\S*\s/, ""); +}; /** * extractTimezone returns a leading timezone from a schedule string if one is @@ -43,14 +43,14 @@ export const extractTimezone = ( raw: string, defaultTZ = DEFAULT_TIMEZONE, ): string => { - const matches = raw.match(/CRON_TZ=\S*\s/g) + const matches = raw.match(/CRON_TZ=\S*\s/g); if (matches && matches.length > 0) { - return matches[0].replace(/CRON_TZ=/, "").trim() + return matches[0].replace(/CRON_TZ=/, "").trim(); } else { - return defaultTZ + return defaultTZ; } -} +}; /** Language used in the schedule components */ export const Language = { @@ -59,7 +59,7 @@ export const Language = { afterStart: "after start", autostartLabel: "Starts at", autostopLabel: "Stops at", -} +}; export const autostartDisplay = (schedule: string | undefined): string => { if (schedule) { @@ -70,11 +70,11 @@ export const autostartDisplay = (schedule: string | undefined): string => { }) // We don't want to keep the At because it is on the label .replace("At", "") - ) + ); } else { - return Language.manual + return Language.manual; } -} +}; export const isShuttingDown = ( workspace: Workspace, @@ -82,16 +82,16 @@ export const isShuttingDown = ( ): boolean => { if (!deadline) { if (!workspace.latest_build.deadline) { - return false + return false; } - deadline = dayjs(workspace.latest_build.deadline).utc() + deadline = dayjs(workspace.latest_build.deadline).utc(); } - const now = dayjs().utc() - return isWorkspaceOn(workspace) && now.isAfter(deadline) -} + const now = dayjs().utc(); + return isWorkspaceOn(workspace) && now.isAfter(deadline); +}; export const autostopDisplay = (workspace: Workspace): string => { - const ttl = workspace.ttl_ms + const ttl = workspace.ttl_ms; if (isWorkspaceOn(workspace) && workspace.latest_build.deadline) { // Workspace is on --> derive from latest_build.deadline. Note that the @@ -100,26 +100,26 @@ export const autostopDisplay = (workspace: Workspace): string => { // represent the previously defined ttl. Thus, we always derive from the // deadline as the source of truth. - const deadline = dayjs(workspace.latest_build.deadline).utc() + const deadline = dayjs(workspace.latest_build.deadline).utc(); if (isShuttingDown(workspace, deadline)) { - return Language.workspaceShuttingDownLabel + return Language.workspaceShuttingDownLabel; } else { - return deadline.tz(dayjs.tz.guess()).format("MMM D, YYYY h:mm A") + return deadline.tz(dayjs.tz.guess()).format("MMM D, YYYY h:mm A"); } } else if (!ttl || ttl < 1) { // If the workspace is not on, and the ttl is 0 or undefined, then the // workspace is set to manually shutdown. - return Language.manual + return Language.manual; } else { // The workspace has a ttl set, but is either in an unknown state or is // not running. Therefore, we derive from workspace.ttl. - const duration = dayjs.duration(ttl, "milliseconds") - return `${duration.humanize()} ${Language.afterStart}` + const duration = dayjs.duration(ttl, "milliseconds"); + return `${duration.humanize()} ${Language.afterStart}`; } -} +}; -export const deadlineExtensionMin = dayjs.duration(30, "minutes") -export const deadlineExtensionMax = dayjs.duration(24, "hours") +export const deadlineExtensionMin = dayjs.duration(30, "minutes"); +export const deadlineExtensionMax = dayjs.duration(24, "hours"); /** * Depends on the time the workspace was last updated and a global constant. @@ -130,10 +130,10 @@ export function getMaxDeadline(ws: Workspace | undefined): dayjs.Dayjs { // note: we count runtime from updated_at as started_at counts from the start of // the workspace build process, which can take a while. if (ws === undefined) { - throw Error("Cannot calculate max deadline because workspace is undefined") + throw Error("Cannot calculate max deadline because workspace is undefined"); } - const startedAt = dayjs(ws.latest_build.updated_at) - return startedAt.add(deadlineExtensionMax) + const startedAt = dayjs(ws.latest_build.updated_at); + return startedAt.add(deadlineExtensionMax); } /** @@ -141,11 +141,11 @@ export function getMaxDeadline(ws: Workspace | undefined): dayjs.Dayjs { * @returns the earliest datetime at which the workspace can be automatically shut down. */ export function getMinDeadline(): dayjs.Dayjs { - return dayjs().add(deadlineExtensionMin) + return dayjs().add(deadlineExtensionMin); } export const getDeadline = (workspace: Workspace): dayjs.Dayjs => - dayjs(workspace.latest_build.deadline).utc() + dayjs(workspace.latest_build.deadline).utc(); /** * Get number of hours you can add or subtract to the current deadline before hitting the max or min deadline. @@ -156,4 +156,4 @@ export const getDeadline = (workspace: Workspace): dayjs.Dayjs => export const getMaxDeadlineChange = ( deadline: dayjs.Dayjs, extremeDeadline: dayjs.Dayjs, -): number => Math.abs(deadline.diff(extremeDeadline, "hours")) +): number => Math.abs(deadline.diff(extremeDeadline, "hours")); diff --git a/site/src/utils/starterTemplates.ts b/site/src/utils/starterTemplates.ts index 628cf21aa8..0caddfb9a8 100644 --- a/site/src/utils/starterTemplates.ts +++ b/site/src/utils/starterTemplates.ts @@ -1,24 +1,24 @@ -import { TemplateExample } from "api/typesGenerated" +import { TemplateExample } from "api/typesGenerated"; -export type StarterTemplatesByTag = Record +export type StarterTemplatesByTag = Record; export const getTemplatesByTag = ( templates: TemplateExample[], ): StarterTemplatesByTag => { const tags: StarterTemplatesByTag = { all: templates, - } + }; templates.forEach((template) => { template.tags.forEach((tag) => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- this can be undefined if (tags[tag]) { - tags[tag].push(template) + tags[tag].push(template); } else { - tags[tag] = [template] + tags[tag] = [template]; } - }) - }) + }); + }); - return tags -} + return tags; +}; diff --git a/site/src/utils/tar.test.ts b/site/src/utils/tar.test.ts index e5bdeebb0d..4ed8b32876 100644 --- a/site/src/utils/tar.test.ts +++ b/site/src/utils/tar.test.ts @@ -1,60 +1,60 @@ -import { TarReader, TarWriter, ITarFileInfo, TarFileTypeCodes } from "./tar" +import { TarReader, TarWriter, ITarFileInfo, TarFileTypeCodes } from "./tar"; -const mtime = 1666666666666 +const mtime = 1666666666666; test("tar", async () => { // Write - const writer = new TarWriter() - writer.addFile("a.txt", "hello", { mtime }) - writer.addFile("b.txt", new Blob(["world"]), { mtime }) - writer.addFile("c.txt", "", { mtime }) - writer.addFolder("etc", { mtime }) + const writer = new TarWriter(); + writer.addFile("a.txt", "hello", { mtime }); + writer.addFile("b.txt", new Blob(["world"]), { mtime }); + writer.addFile("c.txt", "", { mtime }); + writer.addFolder("etc", { mtime }); writer.addFile("etc/d.txt", "Some text content", { mtime, user: "coder", group: "codergroup", mode: parseInt("777", 8), - }) - const blob = (await writer.write()) as Blob + }); + const blob = (await writer.write()) as Blob; // Read - const reader = new TarReader() - const fileInfos = await reader.readFile(blob) + const reader = new TarReader(); + const fileInfos = await reader.readFile(blob); verifyFile(fileInfos[0], reader.getTextFile(fileInfos[0].name) as string, { name: "a.txt", content: "hello", - }) + }); verifyFile(fileInfos[1], reader.getTextFile(fileInfos[1].name) as string, { name: "b.txt", content: "world", - }) + }); verifyFile(fileInfos[2], reader.getTextFile(fileInfos[2].name) as string, { name: "c.txt", content: "", - }) + }); verifyFolder(fileInfos[3], { name: "etc", - }) + }); verifyFile(fileInfos[4], reader.getTextFile(fileInfos[4].name) as string, { name: "etc/d.txt", content: "Some text content", - }) - expect(fileInfos[4].group).toEqual("codergroup") - expect(fileInfos[4].user).toEqual("coder") - expect(fileInfos[4].mode).toEqual(parseInt("777", 8)) -}) + }); + expect(fileInfos[4].group).toEqual("codergroup"); + expect(fileInfos[4].user).toEqual("coder"); + expect(fileInfos[4].mode).toEqual(parseInt("777", 8)); +}); function verifyFile( info: ITarFileInfo, content: string, expected: { name: string; content: string }, ) { - expect(info.name).toEqual(expected.name) - expect(info.size).toEqual(expected.content.length) - expect(content).toEqual(expected.content) + expect(info.name).toEqual(expected.name); + expect(info.size).toEqual(expected.content.length); + expect(content).toEqual(expected.content); } function verifyFolder(info: ITarFileInfo, expected: { name: string }) { - expect(info.name).toEqual(expected.name) - expect(info.type).toEqual(TarFileTypeCodes.Dir) + expect(info.name).toEqual(expected.name); + expect(info.type).toEqual(TarFileTypeCodes.Dir); } diff --git a/site/src/utils/tar.ts b/site/src/utils/tar.ts index 57b59adac3..a243006c27 100644 --- a/site/src/utils/tar.ts +++ b/site/src/utils/tar.ts @@ -1,87 +1,87 @@ // Based on https://github.com/gera2ld/tarjs // and https://github.com/ankitrohatgi/tarballjs/blob/master/tarball.js -type TarFileType = string +type TarFileType = string; // Added the most common codes export const TarFileTypeCodes = { File: "0", Dir: "5", -} -const encoder = new TextEncoder() -const utf8Encode = (input: string) => encoder.encode(input) -const decoder = new TextDecoder() -const utf8Decode = (input: Uint8Array) => decoder.decode(input) +}; +const encoder = new TextEncoder(); +const utf8Encode = (input: string) => encoder.encode(input); +const decoder = new TextDecoder(); +const utf8Decode = (input: Uint8Array) => decoder.decode(input); export interface ITarFileInfo { - name: string - type: TarFileType - size: number - mode: number - mtime: number - user: string - group: string - headerOffset: number + name: string; + type: TarFileType; + size: number; + mode: number; + mtime: number; + user: string; + group: string; + headerOffset: number; } export interface ITarWriteItem { - name: string - type: TarFileType - data: ArrayBuffer | Promise | null - size: number - opts?: Partial + name: string; + type: TarFileType; + data: ArrayBuffer | Promise | null; + size: number; + opts?: Partial; } export interface ITarWriteOptions { - uid: number - gid: number - mode: number - mtime: number - user: string - group: string + uid: number; + gid: number; + mode: number; + mtime: number; + user: string; + group: string; } export class TarReader { - public fileInfo: ITarFileInfo[] = [] - private _buffer: ArrayBuffer | null = null + public fileInfo: ITarFileInfo[] = []; + private _buffer: ArrayBuffer | null = null; constructor() { - this.reset() + this.reset(); } get buffer() { if (!this._buffer) { - throw new Error("Buffer is not set") + throw new Error("Buffer is not set"); } - return this._buffer + return this._buffer; } reset() { - this.fileInfo = [] - this._buffer = null + this.fileInfo = []; + this._buffer = null; } async readFile(file: ArrayBuffer | Uint8Array | Blob) { - this.reset() - this._buffer = await getArrayBuffer(file) - this.readFileInfo() - return this.fileInfo + this.reset(); + this._buffer = await getArrayBuffer(file); + this.readFileInfo(); + return this.fileInfo; } private readFileInfo() { - this.fileInfo = [] - let offset = 0 + this.fileInfo = []; + let offset = 0; while (offset < this.buffer.byteLength - 512) { - const fileName = this.readFileName(offset) + const fileName = this.readFileName(offset); if (!fileName) { - break + break; } - const fileType = this.readFileType(offset) - const fileSize = this.readFileSize(offset) - const fileMode = this.readFileMode(offset) - const fileMtime = this.readFileMtime(offset) - const fileUser = this.readFileUser(offset) - const fileGroup = this.readFileGroup(offset) + const fileType = this.readFileType(offset); + const fileSize = this.readFileSize(offset); + const fileMode = this.readFileMode(offset); + const fileMtime = this.readFileMtime(offset); + const fileUser = this.readFileUser(offset); + const fileGroup = this.readFileGroup(offset); this.fileInfo.push({ name: fileName, @@ -92,91 +92,91 @@ export class TarReader { mtime: fileMtime, user: fileUser, group: fileGroup, - }) + }); - offset += 512 + 512 * Math.floor((fileSize + 511) / 512) + offset += 512 + 512 * Math.floor((fileSize + 511) / 512); } } private readString(offset: number, maxSize: number) { - let size = 0 - let view = new Uint8Array(this.buffer, offset, maxSize) + let size = 0; + let view = new Uint8Array(this.buffer, offset, maxSize); while (size < maxSize && view[size]) { - size += 1 + size += 1; } - view = new Uint8Array(this.buffer, offset, size) - return utf8Decode(view) + view = new Uint8Array(this.buffer, offset, size); + return utf8Decode(view); } private readFileName(offset: number) { - return this.readString(offset, 100) + return this.readString(offset, 100); } private readFileMode(offset: number) { - const mode = this.readString(offset + 100, 8) - return parseInt(mode, 8) + const mode = this.readString(offset + 100, 8); + return parseInt(mode, 8); } private readFileMtime(offset: number) { - const mtime = this.readString(offset + 136, 12) - return parseInt(mtime, 8) + const mtime = this.readString(offset + 136, 12); + return parseInt(mtime, 8); } private readFileUser(offset: number) { - return this.readString(offset + 265, 32) + return this.readString(offset + 265, 32); } private readFileGroup(offset: number) { - return this.readString(offset + 297, 32) + return this.readString(offset + 297, 32); } private readFileType(offset: number) { - const typeView = new Uint8Array(this.buffer, offset + 156, 1) - const typeStr = String.fromCharCode(typeView[0]) - return typeStr + const typeView = new Uint8Array(this.buffer, offset + 156, 1); + const typeStr = String.fromCharCode(typeView[0]); + return typeStr; } private readFileSize(offset: number) { // offset = 124, length = 12 - const view = new Uint8Array(this.buffer, offset + 124, 12) - const sizeStr = utf8Decode(view) - return parseInt(sizeStr, 8) + const view = new Uint8Array(this.buffer, offset + 124, 12); + const sizeStr = utf8Decode(view); + return parseInt(sizeStr, 8); } private readFileBlob(offset: number, size: number, mimetype: string) { - const view = new Uint8Array(this.buffer, offset, size) - return new Blob([view], { type: mimetype }) + const view = new Uint8Array(this.buffer, offset, size); + return new Blob([view], { type: mimetype }); } private readTextFile(offset: number, size: number) { - const view = new Uint8Array(this.buffer, offset, size) - return utf8Decode(view) + const view = new Uint8Array(this.buffer, offset, size); + return utf8Decode(view); } getTextFile(filename: string) { - const item = this.fileInfo.find((info) => info.name === filename) + const item = this.fileInfo.find((info) => info.name === filename); if (item) { - return this.readTextFile(item.headerOffset + 512, item.size) + return this.readTextFile(item.headerOffset + 512, item.size); } } getFileBlob(filename: string, mimetype = "") { - const item = this.fileInfo.find((info) => info.name === filename) + const item = this.fileInfo.find((info) => info.name === filename); if (item) { - return this.readFileBlob(item.headerOffset + 512, item.size, mimetype) + return this.readFileBlob(item.headerOffset + 512, item.size, mimetype); } } } export class TarWriter { - private fileData: ITarWriteItem[] = [] - private _buffer: ArrayBuffer | null = null + private fileData: ITarWriteItem[] = []; + private _buffer: ArrayBuffer | null = null; get buffer() { if (!this._buffer) { - throw new Error("Buffer is not set") + throw new Error("Buffer is not set"); } - return this._buffer + return this._buffer; } addFile( @@ -184,16 +184,16 @@ export class TarWriter { file: string | ArrayBuffer | Uint8Array | Blob, opts?: Partial, ) { - const data = getArrayBuffer(file) - const size = (data as ArrayBuffer).byteLength ?? (file as Blob).size + const data = getArrayBuffer(file); + const size = (data as ArrayBuffer).byteLength ?? (file as Blob).size; const item: ITarWriteItem = { name, type: TarFileTypeCodes.File, data, size, opts, - } - this.fileData.push(item) + }; + this.fileData.push(item); } addFolder(name: string, opts?: Partial) { @@ -203,109 +203,113 @@ export class TarWriter { data: null, size: 0, opts, - }) + }); } private createBuffer() { const dataSize = this.fileData.reduce( (prev, item) => prev + 512 + 512 * Math.floor((item.size + 511) / 512), 0, - ) - const bufSize = 10240 * Math.floor((dataSize + 10240 - 1) / 10240) - this._buffer = new ArrayBuffer(bufSize) + ); + const bufSize = 10240 * Math.floor((dataSize + 10240 - 1) / 10240); + this._buffer = new ArrayBuffer(bufSize); } async write() { - this.createBuffer() - const view = new Uint8Array(this.buffer) - let offset = 0 + this.createBuffer(); + const view = new Uint8Array(this.buffer); + let offset = 0; for (const item of this.fileData) { // write header - this.writeFileName(item.name, offset) - this.writeFileType(item.type, offset) - this.writeFileSize(item.size, offset) - this.fillHeader(offset, item.opts as Partial, item.type) - this.writeChecksum(offset) + this.writeFileName(item.name, offset); + this.writeFileType(item.type, offset); + this.writeFileSize(item.size, offset); + this.fillHeader( + offset, + item.opts as Partial, + item.type, + ); + this.writeChecksum(offset); // write data - const data = new Uint8Array((await item.data) as ArrayBuffer) - view.set(data, offset + 512) - offset += 512 + 512 * Math.floor((item.size + 511) / 512) + const data = new Uint8Array((await item.data) as ArrayBuffer); + view.set(data, offset + 512); + offset += 512 + 512 * Math.floor((item.size + 511) / 512); } // Required so it works in the browser and node. if (typeof Blob !== "undefined") { - return new Blob([this.buffer], { type: "application/x-tar" }) + return new Blob([this.buffer], { type: "application/x-tar" }); } else { - return this.buffer + return this.buffer; } } private writeString(str: string, offset: number, size: number) { - const strView = utf8Encode(str) - const view = new Uint8Array(this.buffer, offset, size) + const strView = utf8Encode(str); + const view = new Uint8Array(this.buffer, offset, size); for (let i = 0; i < size; i += 1) { - view[i] = i < strView.length ? strView[i] : 0 + view[i] = i < strView.length ? strView[i] : 0; } } private writeFileName(name: string, offset: number) { // offset: 0 - this.writeString(name, offset, 100) + this.writeString(name, offset, 100); } private writeFileType(type: TarFileType, offset: number) { // offset: 156 - const typeView = new Uint8Array(this.buffer, offset + 156, 1) - typeView[0] = type.charCodeAt(0) + const typeView = new Uint8Array(this.buffer, offset + 156, 1); + typeView[0] = type.charCodeAt(0); } private writeFileSize(size: number, offset: number) { // offset: 124 - const sizeStr = size.toString(8).padStart(11, "0") - this.writeString(sizeStr, offset + 124, 12) + const sizeStr = size.toString(8).padStart(11, "0"); + this.writeString(sizeStr, offset + 124, 12); } private writeFileMode(mode: number, offset: number) { // offset: 100 - this.writeString(mode.toString(8).padStart(7, "0"), offset + 100, 8) + this.writeString(mode.toString(8).padStart(7, "0"), offset + 100, 8); } private writeFileUid(uid: number, offset: number) { // offset: 108 - this.writeString(uid.toString(8).padStart(7, "0"), offset + 108, 8) + this.writeString(uid.toString(8).padStart(7, "0"), offset + 108, 8); } private writeFileGid(gid: number, offset: number) { // offset: 116 - this.writeString(gid.toString(8).padStart(7, "0"), offset + 116, 8) + this.writeString(gid.toString(8).padStart(7, "0"), offset + 116, 8); } private writeFileMtime(mtime: number, offset: number) { // offset: 136 - this.writeString(mtime.toString(8).padStart(11, "0"), offset + 136, 12) + this.writeString(mtime.toString(8).padStart(11, "0"), offset + 136, 12); } private writeFileUser(user: string, offset: number) { // offset: 265 - this.writeString(user, offset + 265, 32) + this.writeString(user, offset + 265, 32); } private writeFileGroup(group: string, offset: number) { // offset: 297 - this.writeString(group, offset + 297, 32) + this.writeString(group, offset + 297, 32); } private writeChecksum(offset: number) { // offset: 148 - this.writeString(" ", offset + 148, 8) // first fill with spaces + this.writeString(" ", offset + 148, 8); // first fill with spaces // add up header bytes - const header = new Uint8Array(this.buffer, offset, 512) - let chksum = 0 + const header = new Uint8Array(this.buffer, offset, 512); + let chksum = 0; for (let i = 0; i < 512; i += 1) { - chksum += header[i] + chksum += header[i]; } - this.writeString(chksum.toString(8), offset + 148, 8) + this.writeString(chksum.toString(8), offset + 148, 8); } private fillHeader( @@ -321,30 +325,30 @@ export class TarWriter { user: "tarballjs", group: "tarballjs", ...opts, - } + }; - this.writeFileMode(mode, offset) - this.writeFileUid(uid, offset) - this.writeFileGid(gid, offset) - this.writeFileMtime(mtime, offset) + this.writeFileMode(mode, offset); + this.writeFileUid(uid, offset); + this.writeFileGid(gid, offset); + this.writeFileMtime(mtime, offset); - this.writeString("ustar", offset + 257, 6) // magic string - this.writeString("00", offset + 263, 2) // magic version + this.writeString("ustar", offset + 257, 6); // magic string + this.writeString("00", offset + 263, 2); // magic version - this.writeFileUser(user, offset) - this.writeFileGroup(group, offset) + this.writeFileUser(user, offset); + this.writeFileGroup(group, offset); } } function getArrayBuffer(file: string | ArrayBuffer | Uint8Array | Blob) { if (typeof file === "string") { - return utf8Encode(file).buffer + return utf8Encode(file).buffer; } if (file instanceof ArrayBuffer) { - return file + return file; } if (ArrayBuffer.isView(file)) { - return new Uint8Array(file).buffer + return new Uint8Array(file).buffer; } - return file.arrayBuffer() + return file.arrayBuffer(); } diff --git a/site/src/utils/templateVersion.ts b/site/src/utils/templateVersion.ts index 36d88af3b2..f0ebbdd4af 100644 --- a/site/src/utils/templateVersion.ts +++ b/site/src/utils/templateVersion.ts @@ -1,46 +1,46 @@ -import * as API from "api/api" -import { TemplateVersion } from "api/typesGenerated" -import { FileTree, createFile } from "./filetree" -import { TarReader } from "./tar" +import * as API from "api/api"; +import { TemplateVersion } from "api/typesGenerated"; +import { FileTree, createFile } from "./filetree"; +import { TarReader } from "./tar"; /** * Content by filename */ -export type TemplateVersionFiles = Record +export type TemplateVersionFiles = Record; export const getTemplateVersionFiles = async ( version: TemplateVersion, ): Promise => { - const files: TemplateVersionFiles = {} - const tarFile = await API.getFile(version.job.file_id) - const tarReader = new TarReader() - await tarReader.readFile(tarFile) + const files: TemplateVersionFiles = {}; + const tarFile = await API.getFile(version.job.file_id); + const tarReader = new TarReader(); + await tarReader.readFile(tarFile); for (const file of tarReader.fileInfo) { if (isAllowedFile(file.name)) { - files[file.name] = tarReader.getTextFile(file.name) as string + files[file.name] = tarReader.getTextFile(file.name) as string; } } - return files -} + return files; +}; -export const allowedExtensions = ["tf", "md", "Dockerfile", "protobuf"] +export const allowedExtensions = ["tf", "md", "Dockerfile", "protobuf"]; export const isAllowedFile = (name: string) => { - return allowedExtensions.some((ext) => name.endsWith(ext)) -} + return allowedExtensions.some((ext) => name.endsWith(ext)); +}; export const createTemplateVersionFileTree = async ( tarReader: TarReader, ): Promise => { - let fileTree: FileTree = {} + let fileTree: FileTree = {}; for (const file of tarReader.fileInfo) { if (isAllowedFile(file.name)) { fileTree = createFile( file.name, fileTree, tarReader.getTextFile(file.name) as string, - ) + ); } } - return fileTree -} + return fileTree; +}; diff --git a/site/src/utils/templates.ts b/site/src/utils/templates.ts index 3f9aae69e5..9c54afd0aa 100644 --- a/site/src/utils/templates.ts +++ b/site/src/utils/templates.ts @@ -1,20 +1,20 @@ -import dayjs from "dayjs" -import duration from "dayjs/plugin/duration" -import relativeTime from "dayjs/plugin/relativeTime" +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import relativeTime from "dayjs/plugin/relativeTime"; -dayjs.extend(duration) -dayjs.extend(relativeTime) +dayjs.extend(duration); +dayjs.extend(relativeTime); export const formatTemplateActiveDevelopers = (num?: number): string => { if (num === undefined || num < 0) { // Loading - return "-" + return "-"; } - return num.toString() -} + return num.toString(); +}; export const formatTemplateBuildTime = (buildTimeMs?: number): string => { return buildTimeMs === undefined ? "Unknown" - : `${Math.round(dayjs.duration(buildTimeMs, "milliseconds").asSeconds())}s` -} + : `${Math.round(dayjs.duration(buildTimeMs, "milliseconds").asSeconds())}s`; +}; diff --git a/site/src/utils/workspace.test.ts b/site/src/utils/workspace.test.ts index 26bb223c6b..25f9933a5c 100644 --- a/site/src/utils/workspace.test.ts +++ b/site/src/utils/workspace.test.ts @@ -1,13 +1,13 @@ -import dayjs from "dayjs" -import * as TypesGen from "../api/typesGenerated" -import * as Mocks from "../testHelpers/entities" +import dayjs from "dayjs"; +import * as TypesGen from "../api/typesGenerated"; +import * as Mocks from "../testHelpers/entities"; import { defaultWorkspaceExtension, getDisplayVersionStatus, getDisplayWorkspaceBuildInitiatedBy, getDisplayWorkspaceTemplateName, isWorkspaceOn, -} from "./workspace" +} from "./workspace"; describe("util > workspace", () => { describe("isWorkspaceOn", () => { @@ -47,11 +47,11 @@ describe("util > workspace", () => { }, transition, }, - } - expect(isWorkspaceOn(workspace)).toBe(isOn) + }; + expect(isWorkspaceOn(workspace)).toBe(isOn); }, - ) - }) + ); + }); describe("defaultWorkspaceExtension", () => { it.each<[string, TypesGen.PutExtendWorkspaceRequest]>([ @@ -71,9 +71,9 @@ describe("util > workspace", () => { }, ], ])(`defaultWorkspaceExtension(%p) returns %p`, (startTime, request) => { - expect(defaultWorkspaceExtension(dayjs(startTime))).toEqual(request) - }) - }) + expect(defaultWorkspaceExtension(dayjs(startTime))).toEqual(request); + }); + }); describe("getDisplayWorkspaceBuildInitiatedBy", () => { it.each<[TypesGen.WorkspaceBuild, string]>([ @@ -95,10 +95,10 @@ describe("util > workspace", () => { ])( `getDisplayWorkspaceBuildInitiatedBy(%p) returns %p`, (build, initiatedBy) => { - expect(getDisplayWorkspaceBuildInitiatedBy(build)).toEqual(initiatedBy) + expect(getDisplayWorkspaceBuildInitiatedBy(build)).toEqual(initiatedBy); }, - ) - }) + ); + }); describe("getDisplayVersionStatus", () => { it.each<[string, string, string, boolean]>([ @@ -115,28 +115,28 @@ describe("util > workspace", () => { const { displayVersion, outdated } = getDisplayVersionStatus( agentVersion, serverVersion, - ) - expect(displayVersion).toEqual(expectedVersion) - expect(expectedOutdated).toEqual(outdated) + ); + expect(displayVersion).toEqual(expectedVersion); + expect(expectedOutdated).toEqual(outdated); }, - ) - }) + ); + }); describe("getDisplayWorkspaceTemplateName", () => { it("display name is not set", async () => { const workspace: TypesGen.Workspace = { ...Mocks.MockWorkspace, template_display_name: "", - } - const displayed = getDisplayWorkspaceTemplateName(workspace) - expect(displayed).toEqual(workspace.template_name) - }) + }; + const displayed = getDisplayWorkspaceTemplateName(workspace); + expect(displayed).toEqual(workspace.template_name); + }); it("display name is set", async () => { const workspace: TypesGen.Workspace = { ...Mocks.MockWorkspace, - } - const displayed = getDisplayWorkspaceTemplateName(workspace) - expect(displayed).toEqual(workspace.template_display_name) - }) - }) -}) + }; + const displayed = getDisplayWorkspaceTemplateName(workspace); + expect(displayed).toEqual(workspace.template_display_name); + }); + }); +}); diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index 53f065d98b..b70a32e46f 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -1,20 +1,20 @@ -import { Theme } from "@mui/material/styles" -import dayjs from "dayjs" -import duration from "dayjs/plugin/duration" -import minMax from "dayjs/plugin/minMax" -import utc from "dayjs/plugin/utc" -import semver from "semver" -import * as TypesGen from "../api/typesGenerated" -import i18next from "i18next" -import CircularProgress from "@mui/material/CircularProgress" -import ErrorIcon from "@mui/icons-material/ErrorOutline" -import StopIcon from "@mui/icons-material/StopOutlined" -import PlayIcon from "@mui/icons-material/PlayArrowOutlined" -import QueuedIcon from "@mui/icons-material/HourglassEmpty" +import { Theme } from "@mui/material/styles"; +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import minMax from "dayjs/plugin/minMax"; +import utc from "dayjs/plugin/utc"; +import semver from "semver"; +import * as TypesGen from "../api/typesGenerated"; +import i18next from "i18next"; +import CircularProgress from "@mui/material/CircularProgress"; +import ErrorIcon from "@mui/icons-material/ErrorOutline"; +import StopIcon from "@mui/icons-material/StopOutlined"; +import PlayIcon from "@mui/icons-material/PlayArrowOutlined"; +import QueuedIcon from "@mui/icons-material/HourglassEmpty"; -dayjs.extend(duration) -dayjs.extend(utc) -dayjs.extend(minMax) +dayjs.extend(duration); +dayjs.extend(utc); +dayjs.extend(minMax); const DisplayWorkspaceBuildStatusLanguage = { succeeded: "Succeeded", @@ -23,11 +23,11 @@ const DisplayWorkspaceBuildStatusLanguage = { canceling: "Canceling", canceled: "Canceled", failed: "Failed", -} +}; const DisplayAgentVersionLanguage = { unknown: "Unknown", -} +}; export const getDisplayWorkspaceBuildStatus = ( theme: Theme, @@ -39,73 +39,73 @@ export const getDisplayWorkspaceBuildStatus = ( type: "success", color: theme.palette.success.light, status: DisplayWorkspaceBuildStatusLanguage.succeeded, - } as const + } as const; case "pending": return { type: "secondary", color: theme.palette.text.secondary, status: DisplayWorkspaceBuildStatusLanguage.pending, - } as const + } as const; case "running": return { type: "info", color: theme.palette.primary.main, status: DisplayWorkspaceBuildStatusLanguage.running, - } as const + } as const; case "failed": return { type: "error", color: theme.palette.text.secondary, status: DisplayWorkspaceBuildStatusLanguage.failed, - } as const + } as const; case "canceling": return { type: "warning", color: theme.palette.warning.light, status: DisplayWorkspaceBuildStatusLanguage.canceling, - } as const + } as const; case "canceled": return { type: "secondary", color: theme.palette.text.secondary, status: DisplayWorkspaceBuildStatusLanguage.canceled, - } as const + } as const; } -} +}; export const getDisplayWorkspaceBuildInitiatedBy = ( build: TypesGen.WorkspaceBuild, ): string => { switch (build.reason) { case "initiator": - return build.initiator_name + return build.initiator_name; case "autostart": case "autostop": - return "Coder" + return "Coder"; } -} +}; const getWorkspaceBuildDurationInSeconds = ( build: TypesGen.WorkspaceBuild, ): number | undefined => { - const isCompleted = build.job.started_at && build.job.completed_at + const isCompleted = build.job.started_at && build.job.completed_at; if (!isCompleted) { - return + return; } - const startedAt = dayjs(build.job.started_at) - const completedAt = dayjs(build.job.completed_at) - return completedAt.diff(startedAt, "seconds") -} + const startedAt = dayjs(build.job.started_at); + const completedAt = dayjs(build.job.completed_at); + return completedAt.diff(startedAt, "seconds"); +}; export const displayWorkspaceBuildDuration = ( build: TypesGen.WorkspaceBuild, inProgressLabel = "In progress", ): string => { - const duration = getWorkspaceBuildDurationInSeconds(build) - return duration ? `${duration} seconds` : inProgressLabel -} + const duration = getWorkspaceBuildDurationInSeconds(build); + return duration ? `${duration} seconds` : inProgressLabel; +}; export const getDisplayVersionStatus = ( agentVersion: string, @@ -115,36 +115,36 @@ export const getDisplayVersionStatus = ( return { displayVersion: agentVersion || DisplayAgentVersionLanguage.unknown, outdated: false, - } + }; } else if (semver.lt(agentVersion, serverVersion)) { return { displayVersion: agentVersion, outdated: true, - } + }; } else { return { displayVersion: agentVersion, outdated: false, - } + }; } -} +}; export const isWorkspaceOn = (workspace: TypesGen.Workspace): boolean => { - const transition = workspace.latest_build.transition - const status = workspace.latest_build.job.status - return transition === "start" && status === "succeeded" -} + const transition = workspace.latest_build.transition; + const status = workspace.latest_build.job.status; + return transition === "start" && status === "succeeded"; +}; export const defaultWorkspaceExtension = ( __startDate?: dayjs.Dayjs, ): TypesGen.PutExtendWorkspaceRequest => { - const now = __startDate ? dayjs(__startDate) : dayjs() - const fourHoursFromNow = now.add(4, "hours").utc() + const now = __startDate ? dayjs(__startDate) : dayjs(); + const fourHoursFromNow = now.add(4, "hours").utc(); return { deadline: fourHoursFromNow.format(), - } -} + }; +}; // You can see the favicon designs here: https://www.figma.com/file/YIGBkXUcnRGz2ZKNmLaJQf/Coder-v2-Design?node-id=560%3A620 @@ -153,139 +153,139 @@ type FaviconType = | "favicon-success" | "favicon-error" | "favicon-warning" - | "favicon-running" + | "favicon-running"; export const getFaviconByStatus = ( build: TypesGen.WorkspaceBuild, ): FaviconType => { switch (build.status) { case undefined: - return "favicon" + return "favicon"; case "running": - return "favicon-success" + return "favicon-success"; case "starting": - return "favicon-running" + return "favicon-running"; case "stopping": - return "favicon-running" + return "favicon-running"; case "stopped": - return "favicon" + return "favicon"; case "deleting": - return "favicon" + return "favicon"; case "deleted": - return "favicon" + return "favicon"; case "canceling": - return "favicon-warning" + return "favicon-warning"; case "canceled": - return "favicon" + return "favicon"; case "failed": - return "favicon-error" + return "favicon-error"; case "pending": - return "favicon" + return "favicon"; } -} +}; export const getDisplayWorkspaceTemplateName = ( workspace: TypesGen.Workspace, ): string => { return workspace.template_display_name.length > 0 ? workspace.template_display_name - : workspace.template_name -} + : workspace.template_name; +}; export const getDisplayWorkspaceStatus = ( workspaceStatus: TypesGen.WorkspaceStatus, provisionerJob?: TypesGen.ProvisionerJob, ) => { - const { t } = i18next + const { t } = i18next; switch (workspaceStatus) { case undefined: return { text: t("workspaceStatus.loading", { ns: "common" }), icon: , - } as const + } as const; case "running": return { type: "success", text: t("workspaceStatus.running", { ns: "common" }), icon: , - } as const + } as const; case "starting": return { type: "success", text: t("workspaceStatus.starting", { ns: "common" }), icon: , - } as const + } as const; case "stopping": return { type: "warning", text: t("workspaceStatus.stopping", { ns: "common" }), icon: , - } as const + } as const; case "stopped": return { type: "warning", text: t("workspaceStatus.stopped", { ns: "common" }), icon: , - } as const + } as const; case "deleting": return { type: "warning", text: t("workspaceStatus.deleting", { ns: "common" }), icon: , - } as const + } as const; case "deleted": return { type: "error", text: t("workspaceStatus.deleted", { ns: "common" }), icon: , - } as const + } as const; case "canceling": return { type: "warning", text: t("workspaceStatus.canceling", { ns: "common" }), icon: , - } as const + } as const; case "canceled": return { type: "warning", text: t("workspaceStatus.canceled", { ns: "common" }), icon: , - } as const + } as const; case "failed": return { type: "error", text: t("workspaceStatus.failed", { ns: "common" }), icon: , - } as const + } as const; case "pending": return { type: "info", text: getPendingWorkspaceStatusText(provisionerJob), icon: , - } as const + } as const; } -} +}; const getPendingWorkspaceStatusText = ( provisionerJob?: TypesGen.ProvisionerJob, ): string => { - const { t } = i18next + const { t } = i18next; if (!provisionerJob || provisionerJob.queue_size === 0) { - return t("workspaceStatus.pending", { ns: "common" }) + return t("workspaceStatus.pending", { ns: "common" }); } - return "Position in queue: " + provisionerJob.queue_position -} + return "Position in queue: " + provisionerJob.queue_position; +}; const LoadingIcon = () => { - return -} + return ; +}; export const hasJobError = (workspace: TypesGen.Workspace) => { - return workspace.latest_build.job.error !== undefined -} + return workspace.latest_build.job.error !== undefined; +}; export const paramsUsedToCreateWorkspace = ( param: TypesGen.TemplateVersionParameter, -) => !param.ephemeral +) => !param.ephemeral; diff --git a/site/src/xServices/appearance/appearanceXService.ts b/site/src/xServices/appearance/appearanceXService.ts index 901854a452..cd6f0bc949 100644 --- a/site/src/xServices/appearance/appearanceXService.ts +++ b/site/src/xServices/appearance/appearanceXService.ts @@ -1,18 +1,18 @@ -import { displayError, displaySuccess } from "components/GlobalSnackbar/utils" -import { assign, createMachine } from "xstate" -import * as API from "../../api/api" -import { AppearanceConfig } from "../../api/typesGenerated" -import { getErrorMessage } from "api/errors" +import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; +import { assign, createMachine } from "xstate"; +import * as API from "../../api/api"; +import { AppearanceConfig } from "../../api/typesGenerated"; +import { getErrorMessage } from "api/errors"; export type AppearanceContext = { - appearance?: AppearanceConfig - getAppearanceError?: unknown - preview: boolean -} + appearance?: AppearanceConfig; + getAppearanceError?: unknown; + preview: boolean; +}; export type AppearanceEvent = | { type: "SET_PREVIEW_APPEARANCE"; appearance: AppearanceConfig } - | { type: "SAVE_APPEARANCE"; appearance: AppearanceConfig } + | { type: "SAVE_APPEARANCE"; appearance: AppearanceConfig }; export const appearanceMachine = createMachine( { @@ -72,7 +72,7 @@ export const appearanceMachine = createMachine( actions: (_, error) => { displayError( getErrorMessage(error, "Failed to update appearance settings."), - ) + ); }, }, }, @@ -86,7 +86,7 @@ export const appearanceMachine = createMachine( preview: (_) => true, }), notifyUpdateAppearanceSuccess: () => { - displaySuccess("Successfully updated appearance settings!") + displaySuccess("Successfully updated appearance settings!"); }, assignAppearance: assign({ appearance: (_, event) => event.data as AppearanceConfig, @@ -102,19 +102,19 @@ export const appearanceMachine = createMachine( services: { getAppearance: async () => { // Appearance is injected by the Coder server into the HTML document. - const appearance = document.querySelector("meta[property=appearance]") + const appearance = document.querySelector("meta[property=appearance]"); if (appearance) { - const rawContent = appearance.getAttribute("content") + const rawContent = appearance.getAttribute("content"); try { - return JSON.parse(rawContent as string) + return JSON.parse(rawContent as string); } catch (ex) { // Ignore this and fetch as normal! } } - return API.getAppearance() + return API.getAppearance(); }, setAppearance: (_, event) => API.updateAppearance(event.appearance), }, }, -) +); diff --git a/site/src/xServices/auth/authMethodsXService.ts b/site/src/xServices/auth/authMethodsXService.ts index 50a66acaa2..d2a776b71b 100644 --- a/site/src/xServices/auth/authMethodsXService.ts +++ b/site/src/xServices/auth/authMethodsXService.ts @@ -1,10 +1,10 @@ -import { assign, createMachine } from "xstate" -import * as TypeGen from "api/typesGenerated" -import * as API from "api/api" +import { assign, createMachine } from "xstate"; +import * as TypeGen from "api/typesGenerated"; +import * as API from "api/api"; export interface AuthMethodsContext { - authMethods?: TypeGen.AuthMethods - error?: unknown + authMethods?: TypeGen.AuthMethods; + error?: unknown; } export const authMethodsXService = createMachine( @@ -16,8 +16,8 @@ export const authMethodsXService = createMachine( context: {} as AuthMethodsContext, services: {} as { getAuthMethods: { - data: TypeGen.AuthMethods - } + data: TypeGen.AuthMethods; + }; }, }, context: {}, @@ -52,4 +52,4 @@ export const authMethodsXService = createMachine( getAuthMethods: () => API.getAuthMethods(), }, }, -) +); diff --git a/site/src/xServices/auth/authXService.ts b/site/src/xServices/auth/authXService.ts index f3362edec7..ac52bd23ba 100644 --- a/site/src/xServices/auth/authXService.ts +++ b/site/src/xServices/auth/authXService.ts @@ -1,11 +1,11 @@ -import { assign, createMachine } from "xstate" -import * as API from "../../api/api" -import * as TypesGen from "../../api/typesGenerated" -import { displaySuccess } from "../../components/GlobalSnackbar/utils" +import { assign, createMachine } from "xstate"; +import * as API from "../../api/api"; +import * as TypesGen from "../../api/typesGenerated"; +import { displaySuccess } from "../../components/GlobalSnackbar/utils"; export const Language = { successProfileUpdate: "Updated settings.", -} +}; export const checks = { readAllUsers: "readAllUsers", @@ -20,7 +20,7 @@ export const checks = { viewGitAuthConfig: "viewGitAuthConfig", viewDeploymentStats: "viewDeploymentStats", editWorkspaceProxies: "editWorkspaceProxies", -} as const +} as const; export const permissionsToCheck = { [checks.readAllUsers]: { @@ -95,31 +95,31 @@ export const permissionsToCheck = { }, action: "create", }, -} as const +} as const; -export type Permissions = Record +export type Permissions = Record; export type AuthenticatedData = { - user: TypesGen.User - permissions: Permissions -} + user: TypesGen.User; + permissions: Permissions; +}; export type UnauthenticatedData = { - hasFirstUser: boolean - authMethods: TypesGen.AuthMethods -} -export type AuthData = AuthenticatedData | UnauthenticatedData + hasFirstUser: boolean; + authMethods: TypesGen.AuthMethods; +}; +export type AuthData = AuthenticatedData | UnauthenticatedData; export const isAuthenticated = (data?: AuthData): data is AuthenticatedData => - data !== undefined && "user" in data + data !== undefined && "user" in data; const loadInitialAuthData = async (): Promise => { - let authenticatedUser: TypesGen.User | undefined + let authenticatedUser: TypesGen.User | undefined; // User is injected by the Coder server into the HTML document. - const userMeta = document.querySelector("meta[property=user]") + const userMeta = document.querySelector("meta[property=user]"); if (userMeta) { - const rawContent = userMeta.getAttribute("content") + const rawContent = userMeta.getAttribute("content"); try { - authenticatedUser = JSON.parse(rawContent as string) as TypesGen.User + authenticatedUser = JSON.parse(rawContent as string) as TypesGen.User; } catch (ex) { // Ignore this and fetch as normal! } @@ -127,69 +127,69 @@ const loadInitialAuthData = async (): Promise => { // If we have the user from the meta tag, we can skip this! if (!authenticatedUser) { - authenticatedUser = await API.getAuthenticatedUser() + authenticatedUser = await API.getAuthenticatedUser(); } if (authenticatedUser) { const permissions = (await API.checkAuthorization({ checks: permissionsToCheck, - })) as Permissions + })) as Permissions; return { user: authenticatedUser, permissions, - } + }; } const [hasFirstUser, authMethods] = await Promise.all([ API.hasFirstUser(), API.getAuthMethods(), - ]) + ]); return { hasFirstUser, authMethods, - } -} + }; +}; const signIn = async ( email: string, password: string, ): Promise => { - await API.login(email, password) + await API.login(email, password); const [user, permissions] = await Promise.all([ API.getAuthenticatedUser(), API.checkAuthorization({ checks: permissionsToCheck, }), - ]) + ]); return { user: user as TypesGen.User, permissions: permissions as Permissions, - } -} + }; +}; const signOut = async () => { const [authMethods] = await Promise.all([ API.getAuthMethods(), // Anticipate and load the auth methods API.logout(), - ]) + ]); return { hasFirstUser: true, authMethods, - } as UnauthenticatedData -} + } as UnauthenticatedData; +}; export interface AuthContext { - error?: unknown - updateProfileError?: unknown - data?: AuthData + error?: unknown; + updateProfileError?: unknown; + data?: AuthData; } export type AuthEvent = | { type: "SIGN_OUT" } | { type: "SIGN_IN"; email: string; password: string } - | { type: "UPDATE_PROFILE"; data: TypesGen.UpdateUserProfileRequest } + | { type: "UPDATE_PROFILE"; data: TypesGen.UpdateUserProfileRequest }; export const authMachine = /** @xstate-layout N4IgpgJg5mDOIC5QEMCuAXAFgZXc9YAdADYD2yEAlgHZQCS1l6lyxAghpgCL7IDEEUtSI0AbqQDWRNFlz4iZCjXqNmrDlh54EY0gGN8lIQG0ADAF0z5xKAAOpWEyPUbIAB6IAjAFZThACwATAAcAGwAzOGewZ7hoYH+ADQgAJ6Igaam3oSeGf7BcbnBAJyBAL5lyTI4eAQk5FS0DE7qnFr8gsKEulKE1XJ1io0qLextvDrU4gbMJhbGntZIIPaOsy7LHgg+fkFhkdGx8Ump6bEA7ITn56HnkaFFpRVVnAMKDcrNamOavAJCIimkmkr1q7yUTVULB+3AmuhmzisxkCSzsDicQlcWx2ARCESiMTiCWSaQQgUC5124UCxQKDxCT0qIH6YPqEJG3w0sLwfDAACc+aQ+YRbMR8AAzIUAWz6oPkbOGX2hXPak2mhjmlgsrlWGI2oGxvlx+wJR2JpzJ-lM4UIphKgXuj3KTJZ8scUGEEAA8hg+Ng6ABxAByAH06EGrDr0essV5TOdslloqZPKZ-Od-NESV4Hjb-OESqFvAyEudgs9mXK6u7GJD-l0ekQawxI8tdTHNl5ybtPKnPKFin3ivns2TU3mi1FrmFSuEK67q5QPZ9qLyBUKRWL0JK+TLm9RW2i1s5Y9tu4Q8ZksiFgmnR93PLbad5h6FMgm5y6q02l56GH7A1DL0AFUABVDxWaMT07BAogHQhgmCfwBxvbwiwKe87WyYIM1ibxPHzG5PxeWRWRrSAGBFQVxUoYgRAgOi+GAgAFLg2FAgBRENmIAJS9AAxOgABkOIg9toINdIi0fcJzn7dMshfXtR2ibx-EIUJkOKbwiVw4p52-QhyIgSjbGo2iiFQWwIEMWhmPMxjOkBcRegXH8PQo6gqNIGi6MIKybOYOyHLANV9A1A95m1NsoMxGDPGfHIiPCfxvDSwcCJUwsci0nT4j0gzSLdX9PO83zLOs2yoHsnyLLXQVhVFCVpVlIrFw8kyvLM2q-ICqqavKsKEU1MTYv1dwvESzxktS9LexOUlonyHKBzyilM30r82vc2soB9dB62c4EjN-fbRuPOLJNghK-HJckX2KFLimHFTSkCQhZIKUwByQotnRImpiuXWh9sO7ogV6GszsWKMLvGrY4n8dSbn8bTfFyXx01e8JsLU580unG5CsB9rdtB-kGs3ZrdxOj0zuio89VPWTTHenHTEegpwm+nDwkwzSPuKIjEPOckkOJt5CD0IQaKgVA+WUUDMDAfjKD5WB0GA2B+QA4MwwjBnILh09b1CHIOdTN8Uoe+9pve0WhZKHHNJRiomWoUgIDgVw3NhpmYIAWlCFS3w04cCwyDmKWpYjK22hUV1GFVeD9jsroD1MAhxule1zfNimDi1DnU0IYgyUoblTBIJbIkrvQwVOJIm2CE2CK5olKCIskQrLvovfCkOua14kQmugd2hhG8u5uC78FKHRCO4cPzQvSUCI4LxpUpEMiNNQjH0nPKn+GvDiM3KW52dcO+vmLQSNuEvzRC0uQtDZIPnbSu68rj9PAiCKuNaKOslMw31HJEbIA5874SvOEbSH9aZ-i6iFboDEwC-3igXM28RfB2kCH9I4o4gg2kflaeSalyShH3ltEmn9OplQsqgvyHsOLrj5Bgq669tIfTki7RSGVXpBBWtpXSmYcIIOMqZFBlA0GEApkKDhzcuHZFkvJSkc1PCYUzgRVaojojnAkXXKRPUKqBWUANCyijsQPGyEPO0s0lKZSLtlHRIj8obUMcDPaDcYrGxgvPG0dwaTHAIimfI-M2Z6WmlA8RNDJbS2oLLeWitlaq3VprbW7DfH+yumlPwj83zBBvKXYI3hbaiyuDSMsj00Lpk0m7MoQA */ @@ -203,17 +203,17 @@ export const authMachine = events: {} as AuthEvent, services: {} as { loadInitialAuthData: { - data: Awaited> - } + data: Awaited>; + }; signIn: { - data: Awaited> - } + data: Awaited>; + }; updateProfile: { - data: TypesGen.User - } + data: TypesGen.User; + }; signOut: { - data: Awaited> - } + data: Awaited>; + }; }, }, initial: "loadingInitialAuthData", @@ -359,14 +359,14 @@ export const authMachine = signOut, updateProfile: async ({ data }, event) => { if (!data) { - throw new Error("Authenticated data is not loaded yet") + throw new Error("Authenticated data is not loaded yet"); } if (isAuthenticated(data)) { - return API.updateProfile(data.user.id, event.data) + return API.updateProfile(data.user.id, event.data); } - throw new Error("User not authenticated") + throw new Error("User not authenticated"); }, }, actions: { @@ -385,26 +385,26 @@ export const authMachine = updateUser: assign({ data: (context, event) => { if (!context.data) { - throw new Error("No authentication data loaded") + throw new Error("No authentication data loaded"); } return { ...context.data, user: event.data, - } + }; }, }), assignUpdateProfileError: assign({ updateProfileError: (_, event) => event.data, }), notifySuccessProfileUpdate: () => { - displaySuccess(Language.successProfileUpdate) + displaySuccess(Language.successProfileUpdate); }, clearUpdateProfileError: assign({ updateProfileError: (_) => undefined, }), redirect: (_, _data) => { - window.location.href = location.origin + window.location.href = location.origin; }, }, guards: { @@ -414,4 +414,4 @@ export const authMachine = hasRedirectUrl: (_, { data }) => Boolean(data), }, }, - ) + ); diff --git a/site/src/xServices/buildInfo/buildInfoXService.ts b/site/src/xServices/buildInfo/buildInfoXService.ts index 9d8775f4ff..58f2905d57 100644 --- a/site/src/xServices/buildInfo/buildInfoXService.ts +++ b/site/src/xServices/buildInfo/buildInfoXService.ts @@ -1,10 +1,10 @@ -import { assign, createMachine } from "xstate" -import * as API from "../../api/api" -import * as TypesGen from "../../api/typesGenerated" +import { assign, createMachine } from "xstate"; +import * as API from "../../api/api"; +import * as TypesGen from "../../api/typesGenerated"; export interface BuildInfoContext { - getBuildInfoError?: unknown - buildInfo?: TypesGen.BuildInfoResponse + getBuildInfoError?: unknown; + buildInfo?: TypesGen.BuildInfoResponse; } export const buildInfoMachine = createMachine( @@ -16,8 +16,8 @@ export const buildInfoMachine = createMachine( context: {} as BuildInfoContext, services: {} as { getBuildInfo: { - data: TypesGen.BuildInfoResponse - } + data: TypesGen.BuildInfoResponse; + }; }, }, context: { @@ -55,17 +55,17 @@ export const buildInfoMachine = createMachine( services: { getBuildInfo: async () => { // Build info is injected by the Coder server into the HTML document. - const buildInfo = document.querySelector("meta[property=build-info]") + const buildInfo = document.querySelector("meta[property=build-info]"); if (buildInfo) { - const rawContent = buildInfo.getAttribute("content") + const rawContent = buildInfo.getAttribute("content"); try { - return JSON.parse(rawContent as string) + return JSON.parse(rawContent as string); } catch (ex) { // Ignore this and fetch as normal! } } - return API.getBuildInfo() + return API.getBuildInfo(); }, }, actions: { @@ -85,4 +85,4 @@ export const buildInfoMachine = createMachine( })), }, }, -) +); diff --git a/site/src/xServices/createTemplate/createTemplateXService.ts b/site/src/xServices/createTemplate/createTemplateXService.ts index b9b4a951cc..725ec9d141 100644 --- a/site/src/xServices/createTemplate/createTemplateXService.ts +++ b/site/src/xServices/createTemplate/createTemplateXService.ts @@ -7,7 +7,7 @@ import { getTemplateVersionLogs, getTemplateVersionVariables, getTemplateByName, -} from "api/api" +} from "api/api"; import { ProvisionerJob, ProvisionerJobLog, @@ -18,14 +18,14 @@ import { TemplateVersionVariable, UploadResponse, VariableValue, -} from "api/typesGenerated" -import { displayError } from "components/GlobalSnackbar/utils" +} from "api/typesGenerated"; +import { displayError } from "components/GlobalSnackbar/utils"; import { TemplateAutostopRequirementDaysValue, calculateAutostopRequirementDaysValue, -} from "pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/AutostopRequirementHelperText" -import { delay } from "utils/delay" -import { assign, createMachine } from "xstate" +} from "pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm/AutostopRequirementHelperText"; +import { delay } from "utils/delay"; +import { assign, createMachine } from "xstate"; // for creating a new template: // 1. upload template tar or use the example ID @@ -40,41 +40,41 @@ import { assign, createMachine } from "xstate" const provisioner: ProvisionerType = // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Playwright needs to use a different provisioner type! - typeof (window as any).playwright !== "undefined" ? "echo" : "terraform" + typeof (window as any).playwright !== "undefined" ? "echo" : "terraform"; export interface CreateTemplateData { - name: string - display_name: string - description: string - icon: string - default_ttl_hours: number - max_ttl_hours: number - autostop_requirement_days_of_week: TemplateAutostopRequirementDaysValue - autostop_requirement_weeks: number - allow_user_autostart: boolean - allow_user_autostop: boolean - allow_user_cancel_workspace_jobs: boolean - parameter_values_by_name?: Record - user_variable_values?: VariableValue[] - allow_everyone_group_access: boolean + name: string; + display_name: string; + description: string; + icon: string; + default_ttl_hours: number; + max_ttl_hours: number; + autostop_requirement_days_of_week: TemplateAutostopRequirementDaysValue; + autostop_requirement_weeks: number; + allow_user_autostart: boolean; + allow_user_autostop: boolean; + allow_user_cancel_workspace_jobs: boolean; + parameter_values_by_name?: Record; + user_variable_values?: VariableValue[]; + allow_everyone_group_access: boolean; } interface CreateTemplateContext { - organizationId: string - error?: unknown - jobError?: string - jobLogs?: ProvisionerJobLog[] - starterTemplate?: TemplateExample - exampleId?: string | null // It can be null because it is being passed from query string - version?: TemplateVersion - templateData?: CreateTemplateData - variables?: TemplateVersionVariable[] + organizationId: string; + error?: unknown; + jobError?: string; + jobLogs?: ProvisionerJobLog[]; + starterTemplate?: TemplateExample; + exampleId?: string | null; // It can be null because it is being passed from query string + version?: TemplateVersion; + templateData?: CreateTemplateData; + variables?: TemplateVersionVariable[]; // file is used in the FE to show the filename and some other visual stuff // uploadedFile is the response from the server to use in the API - file?: File - uploadResponse?: UploadResponse + file?: File; + uploadResponse?: UploadResponse; // When wanting to duplicate a Template - templateNameToCopy: string | null // It can be null because it is passed from query string - copiedTemplate?: Template + templateNameToCopy: string | null; // It can be null because it is passed from query string + copiedTemplate?: Template; } export const createTemplateMachine = @@ -91,38 +91,38 @@ export const createTemplateMachine = | { type: "REMOVE_FILE" }, services: {} as { uploadFile: { - data: UploadResponse - } + data: UploadResponse; + }; loadStarterTemplate: { - data: TemplateExample - } + data: TemplateExample; + }; createFirstVersion: { - data: TemplateVersion - } + data: TemplateVersion; + }; createVersionWithParametersAndVariables: { - data: TemplateVersion - } + data: TemplateVersion; + }; waitForJobToBeCompleted: { - data: TemplateVersion - } + data: TemplateVersion; + }; checkParametersAndVariables: { data: { - variables?: TemplateVersionVariable[] - } - } + variables?: TemplateVersionVariable[]; + }; + }; createTemplate: { - data: Template - } + data: Template; + }; loadVersionLogs: { - data: ProvisionerJobLog[] - } + data: ProvisionerJobLog[]; + }; copyTemplateData: { data: { - template: Template - version: TemplateVersion - variables: TemplateVersionVariable[] - } - } + template: Template; + version: TemplateVersion; + variables: TemplateVersionVariable[]; + }; + }; }, }, tsTypes: {} as import("./createTemplateXService.typegen").Typegen0, @@ -323,38 +323,38 @@ export const createTemplateMachine = uploadFile: (_, { file }) => uploadTemplateFile(file), loadStarterTemplate: async ({ organizationId, exampleId }) => { if (!exampleId) { - throw new Error(`Example ID is not defined.`) + throw new Error(`Example ID is not defined.`); } - const examples = await getTemplateExamples(organizationId) + const examples = await getTemplateExamples(organizationId); const starterTemplate = examples.find( (example) => example.id === exampleId, - ) + ); if (!starterTemplate) { - throw new Error(`Example ${exampleId} not found.`) + throw new Error(`Example ${exampleId} not found.`); } - return starterTemplate + return starterTemplate; }, copyTemplateData: async ({ organizationId, templateNameToCopy }) => { if (!organizationId) { - throw new Error("No organization ID provided") + throw new Error("No organization ID provided"); } if (!templateNameToCopy) { - throw new Error("No template name to copy provided") + throw new Error("No template name to copy provided"); } const template = await getTemplateByName( organizationId, templateNameToCopy, - ) + ); const [version, variables] = await Promise.all([ getTemplateVersion(template.active_version_id), getTemplateVersionVariables(template.active_version_id), - ]) + ]); return { template, version, variables, - } + }; }, createFirstVersion: async ({ organizationId, @@ -369,14 +369,14 @@ export const createTemplateMachine = example_id: exampleId, provisioner: provisioner, tags: {}, - }) + }); } if (templateNameToCopy) { if (!version) { throw new Error( "Can't copy template due to a missing template version", - ) + ); } return createTemplateVersion(organizationId, { @@ -384,7 +384,7 @@ export const createTemplateMachine = file_id: version.job.file_id, provisioner: provisioner, tags: {}, - }) + }); } if (uploadResponse) { @@ -393,10 +393,10 @@ export const createTemplateMachine = file_id: uploadResponse.hash, provisioner: provisioner, tags: {}, - }) + }); } - throw new Error("No file or example provided") + throw new Error("No file or example provided"); }, createVersionWithParametersAndVariables: async ({ organizationId, @@ -404,10 +404,10 @@ export const createTemplateMachine = version, }) => { if (!version) { - throw new Error("No previous version found") + throw new Error("No previous version found"); } if (!templateData) { - throw new Error("No template data defined") + throw new Error("No template data defined"); } return createTemplateVersion(organizationId, { @@ -416,53 +416,53 @@ export const createTemplateMachine = provisioner: provisioner, user_variable_values: templateData.user_variable_values, tags: {}, - }) + }); }, waitForJobToBeCompleted: async ({ version }) => { if (!version) { - throw new Error("Version not defined") + throw new Error("Version not defined"); } - let job = version.job + let job = version.job; while (isPendingOrRunning(job)) { - version = await getTemplateVersion(version.id) - job = version.job + version = await getTemplateVersion(version.id); + job = version.job; // Delay the verification in two seconds to not overload the server // with too many requests Maybe at some point we could have a // websocket for template version Also, preferred doing this way to // avoid a new state since we don't need to reflect it on the UI if (isPendingOrRunning(job)) { - await delay(2_000) + await delay(2_000); } } - return version + return version; }, checkParametersAndVariables: async ({ version }) => { if (!version) { - throw new Error("Version not defined") + throw new Error("Version not defined"); } let promiseVariables: Promise | undefined = - undefined + undefined; if (isMissingVariables(version)) { - promiseVariables = getTemplateVersionVariables(version.id) + promiseVariables = getTemplateVersionVariables(version.id); } - const [variables] = await Promise.all([promiseVariables]) + const [variables] = await Promise.all([promiseVariables]); return { variables, - } + }; }, createTemplate: async ({ organizationId, version, templateData }) => { if (!version) { - throw new Error("Version not defined") + throw new Error("Version not defined"); } if (!templateData) { - throw new Error("Template data not defined") + throw new Error("Template data not defined"); } const { @@ -473,7 +473,7 @@ export const createTemplateMachine = autostop_requirement_days_of_week, autostop_requirement_weeks, ...safeTemplateData - } = templateData + } = templateData; return createTemplate(organizationId, { ...safeTemplateData, @@ -488,21 +488,21 @@ export const createTemplateMachine = ), weeks: templateData.autostop_requirement_weeks, }, - }) + }); }, loadVersionLogs: ({ version }) => { if (!version) { - throw new Error("Version is not set") + throw new Error("Version is not set"); } - return getTemplateVersionLogs(version.id) + return getTemplateVersionLogs(version.id); }, }, actions: { assignError: assign({ error: (_, { data }) => data }), assignJobError: assign({ jobError: (_, { data }) => data.job.error }), displayUploadError: () => { - displayError("Error on upload the file.") + displayError("Error on upload the file."); }, assignStarterTemplate: assign({ starterTemplate: (_, { data }) => data, @@ -536,19 +536,19 @@ export const createTemplateMachine = hasNoParametersOrVariables: (_, { data }) => data.variables === undefined, hasParametersOrVariables: (_, { data }) => { - return data.variables.length > 0 + return data.variables.length > 0; }, }, }, - ) + ); const isMissingVariables = (version: TemplateVersion) => { return Boolean( version.job.error_code && version.job.error_code === "REQUIRED_TEMPLATE_VARIABLES", - ) -} + ); +}; const isPendingOrRunning = (job: ProvisionerJob) => { - return job.status === "pending" || job.status === "running" -} + return job.status === "pending" || job.status === "running"; +}; diff --git a/site/src/xServices/createWorkspace/createWorkspaceXService.ts b/site/src/xServices/createWorkspace/createWorkspaceXService.ts index 0df9f13a1c..68bfb8ad79 100644 --- a/site/src/xServices/createWorkspace/createWorkspaceXService.ts +++ b/site/src/xServices/createWorkspace/createWorkspaceXService.ts @@ -4,7 +4,7 @@ import { getTemplateByName, getTemplateVersionGitAuth, getTemplateVersionRichParameters, -} from "api/api" +} from "api/api"; import { CreateWorkspaceRequest, Template, @@ -13,37 +13,37 @@ import { User, Workspace, WorkspaceBuildParameter, -} from "api/typesGenerated" -import { assign, createMachine } from "xstate" -import { paramsUsedToCreateWorkspace } from "utils/workspace" -import { REFRESH_GITAUTH_BROADCAST_CHANNEL } from "utils/gitAuth" +} from "api/typesGenerated"; +import { assign, createMachine } from "xstate"; +import { paramsUsedToCreateWorkspace } from "utils/workspace"; +import { REFRESH_GITAUTH_BROADCAST_CHANNEL } from "utils/gitAuth"; -export type CreateWorkspaceMode = "form" | "auto" +export type CreateWorkspaceMode = "form" | "auto"; type CreateWorkspaceContext = { - organizationId: string - templateName: string - mode: CreateWorkspaceMode - defaultName: string - error?: unknown + organizationId: string; + templateName: string; + mode: CreateWorkspaceMode; + defaultName: string; + error?: unknown; // Form - template?: Template - parameters?: TemplateVersionParameter[] - permissions?: Record - gitAuth?: TemplateVersionGitAuth[] + template?: Template; + parameters?: TemplateVersionParameter[]; + permissions?: Record; + gitAuth?: TemplateVersionGitAuth[]; // Used on auto-create - defaultBuildParameters?: WorkspaceBuildParameter[] -} + defaultBuildParameters?: WorkspaceBuildParameter[]; +}; type CreateWorkspaceEvent = { - type: "CREATE_WORKSPACE" - request: CreateWorkspaceRequest - owner: User -} + type: "CREATE_WORKSPACE"; + request: CreateWorkspaceRequest; + owner: User; +}; type RefreshGitAuthEvent = { - type: "REFRESH_GITAUTH" -} + type: "REFRESH_GITAUTH"; +}; export const createWorkspaceMachine = /** @xstate-layout N4IgpgJg5mDOIC5QGMBOYCGAXMB1A9qgNawAOGyYAyltmAHQxZYCWAdlACpgC2pANnVgBiCPjYN2AN3xEGaTDgLEyFarRyMwzdl14ChCafmTYW4gNoAGALrWbiUKXywWrcY5AAPRABYATAA0IACeiACMvgCs9ADssQBsVgDM-v5JyQnJUQkAvrnBCnTKJOSUNHRaOhzcfII4ImIS9MZy9EVKhKVqFZpMrDX69XBGbDKm7mz2FuEOSCDOrpOePgjJAJzr9AAcVr4J677bCeFWB3vBYQgBvvQJB-5R6+GxVulPUfmF6MVdquUaBj9XS1AwNYRgVCoQj0MEAM0IPHaP06KjK6kqwMGdUMxgm5imtnsnkWbgJK0QGy2u32h2Op3OvkuiH8vis9HCDxO0XC2z5CX8XxAHTwf3RvSB2gGehxOCoyAAFrwMKJxJIxrJ5CjRWieoCqtLQcN5UqeBhRuMzJYibYSS4yR55qtTv52bFeYlfPEor4XuFmddwlscqz1mdfMlfZlfEKRSV-hi+lKQUM6CblRCoTD4YjkYodd0AZjk9iwdRFcqLSYrYS7Lb5qTlk6Im83R6El7Yj6-QH4gl6M8ctsoht7skrJ8CsLtfHxfqsTKywAFDCoDA8bSQxpqloatpxsV64vVRfDFdrjc4VCwKv4611uZOe1N0DOgXhOKRjZf-axR4Btl2XWWJngSKJtgCHJwnSWMZ0PIskxPI06HPddN2vTNoVQWF6gRVAkQPXUEMlJDUxwVDLy3W8a2mesnyWclmwQTl-A-WIv3WH8Ej-KIAyyW4Em2Z5XV5fx1m48JYPzWcj0Qw0yLAABxNwAEEAFcsAVVVmlaLVpPgxMSPk2UlNUjSFWoyZaMfBZn0Y18WSDfsfUEvlfEHViA2SbZ-HoDZwPSe4omCwUp0IwtDINFMTOUrB1M0zDs1w3NwoTCUotLYZYviiy8Rom0bMbezvEc8T6BcvkII8-1Qj8dZfP2R4fXEziUliKTfiIyKK2QIhdCXSEeBYWBXHEbcdL3eQlV6gb8OG0a2FgYkGzsx0HIQfwfJiKJXmSZJoO88IRwAqwP22aD3OeLshP2DrUQi9Ker6jhZqGkaCRESEsJw7A8II6aiFe+aPuW+iHTYCkNqiKxtgHVi+XWYK2SZWqNr5HZslAiNoJSSdvn0rr0rhFh+H4frV3XEQAGEACUAFEVM4OmAH1cAAeRpgBpKglxUqm6dB2yGLWkq0cecrdv2-xDuO1GXluRH9s5H1wKObi7oLNL9WJ0nyYvEQqDpgAZOmqc4Zm2dwAA5OmacFoqRdWcd0asSWkZuoJUbSftpc2vZ6rZDsXg1mTiLzMwOFDsBtPVGR9zgwn9Q6XQo8sglrLtYWIaYzbxZ2lIpZl5IA3O+hLvWeleSiVj6pDgzHpRFODMS7Cc3w8P7q1ypk8jgy0-ve3Vuz9bc+2yWDvO2WrjE2HMcybZYiOHJYm2fIpzYfAIDgTxUrnOhM-ByGAFoEgDE-6CsS+3n8d0nl9aW8enAmHvnEtTyEA+X1F4cOWryM0k5JyTiAZWS3DSKBdIC9zjZDronY8xkyzpjNJ-YqqxXjORXuEfaEYAh9gAtLO4aQNgenqkdSMsCX7wOisuCmlFrwoMdhETazl9hCQ2NLKwFd1gnRiCkMM51og8k2BQruclqFZTMppBhw9RZBldvQTa0FzgdnuF5Y4ZdgrumyI8JyC8RF700E9fqg1gZjWkZDUBWwDgjjElgnawE1GxAUUJPYglwIjjeDGMKCdKGaB1mTF6tD4ArSzpDfabwdiXxApseqtjT6o1SE4txrVXTpHSF4-GnVfF6QjlAKO5ic5RCOn5LsbwjpHWeJyAMZVThHUgvcCSvp9GyRyTgCABT1rhN8rsV2MTYmgQDC6BR7xuznByCcZpYcvqEA6aLSxdxFa2OyCBWIAYimwxXvwoMrp3GrzXkAA */ @@ -58,18 +58,18 @@ export const createWorkspaceMachine = services: {} as { loadFormData: { data: { - template: Template - permissions: CreateWSPermissions - parameters: TemplateVersionParameter[] - gitAuth: TemplateVersionGitAuth[] - } - } + template: Template; + permissions: CreateWSPermissions; + parameters: TemplateVersionParameter[]; + gitAuth: TemplateVersionGitAuth[]; + }; + }; createWorkspace: { - data: Workspace - } + data: Workspace; + }; autoCreateWorkspace: { - data: Workspace - } + data: Workspace; + }; }, }, initial: "checkingMode", @@ -113,9 +113,9 @@ export const createWorkspaceMachine = { src: () => (callback) => { const channel = watchGitAuthRefresh(() => { - callback("REFRESH_GITAUTH") - }) - return () => channel.close() + callback("REFRESH_GITAUTH"); + }); + return () => channel.close(); }, }, ], @@ -150,7 +150,7 @@ export const createWorkspaceMachine = { services: { createWorkspace: ({ organizationId }, { request, owner }) => { - return createWorkspace(organizationId, owner.id, request) + return createWorkspace(organizationId, owner.id, request); }, autoCreateWorkspace: async ({ templateName, @@ -158,31 +158,34 @@ export const createWorkspaceMachine = defaultBuildParameters, defaultName, }) => { - const template = await getTemplateByName(organizationId, templateName) + const template = await getTemplateByName( + organizationId, + templateName, + ); return createWorkspace(organizationId, "me", { template_id: template.id, name: defaultName, rich_parameter_values: defaultBuildParameters, - }) + }); }, loadFormData: async ({ templateName, organizationId }) => { const [template, permissions] = await Promise.all([ getTemplateByName(organizationId, templateName), checkCreateWSPermissions(organizationId), - ]) + ]); const [parameters, gitAuth] = await Promise.all([ getTemplateVersionRichParameters(template.active_version_id).then( (p) => p.filter(paramsUsedToCreateWorkspace), ), getTemplateVersionGitAuth(template.active_version_id), - ]) + ]); return { template, permissions, parameters, gitAuth, - } + }; }, }, actions: { @@ -190,7 +193,7 @@ export const createWorkspaceMachine = return { ...ctx, ...event.data, - } + }; }), assignError: assign({ error: (_, event) => event.data, @@ -200,7 +203,7 @@ export const createWorkspaceMachine = }), }, }, - ) + ); const checkCreateWSPermissions = async (organizationId: string) => { // HACK: below, we pass in * for the owner_id, which is a hacky way of checking if the @@ -215,19 +218,19 @@ const checkCreateWSPermissions = async (organizationId: string) => { }, action: "create", }, - } as const + } as const; return checkAuthorization({ checks: permissionsToCheck, - }) as Promise> -} + }) as Promise>; +}; export const watchGitAuthRefresh = (callback: () => void) => { - const bc = new BroadcastChannel(REFRESH_GITAUTH_BROADCAST_CHANNEL) - bc.addEventListener("message", callback) - return bc -} + const bc = new BroadcastChannel(REFRESH_GITAUTH_BROADCAST_CHANNEL); + bc.addEventListener("message", callback); + return bc; +}; export type CreateWSPermissions = Awaited< ReturnType -> +>; diff --git a/site/src/xServices/deploymentConfig/deploymentConfigMachine.ts b/site/src/xServices/deploymentConfig/deploymentConfigMachine.ts index 52ecc37223..505888eeaa 100644 --- a/site/src/xServices/deploymentConfig/deploymentConfigMachine.ts +++ b/site/src/xServices/deploymentConfig/deploymentConfigMachine.ts @@ -1,7 +1,7 @@ -import { DAUsResponse } from "./../../api/typesGenerated" -import { getDeploymentValues, getDeploymentDAUs } from "api/api" -import { createMachine, assign } from "xstate" -import { DeploymentConfig } from "api/types" +import { DAUsResponse } from "./../../api/typesGenerated"; +import { getDeploymentValues, getDeploymentDAUs } from "api/api"; +import { createMachine, assign } from "xstate"; +import { DeploymentConfig } from "api/types"; export const deploymentConfigMachine = createMachine( { @@ -10,19 +10,19 @@ export const deploymentConfigMachine = createMachine( schema: { context: {} as { - deploymentValues?: DeploymentConfig - getDeploymentValuesError?: unknown - deploymentDAUs?: DAUsResponse - getDeploymentDAUsError?: unknown + deploymentValues?: DeploymentConfig; + getDeploymentValuesError?: unknown; + deploymentDAUs?: DAUsResponse; + getDeploymentDAUsError?: unknown; }, events: {} as { type: "LOAD" }, services: {} as { getDeploymentValues: { - data: DeploymentConfig - } + data: DeploymentConfig; + }; getDeploymentDAUs: { - data: DAUsResponse - } + data: DAUsResponse; + }; }, }, tsTypes: {} as import("./deploymentConfigMachine.typegen").Typegen0, @@ -65,7 +65,7 @@ export const deploymentConfigMachine = createMachine( services: { getDeploymentValues: getDeploymentValues, getDeploymentDAUs: async () => { - return getDeploymentDAUs() + return getDeploymentDAUs(); }, }, actions: { @@ -83,4 +83,4 @@ export const deploymentConfigMachine = createMachine( }), }, }, -) +); diff --git a/site/src/xServices/deploymentStats/deploymentStatsMachine.ts b/site/src/xServices/deploymentStats/deploymentStatsMachine.ts index aacbd97230..6301764af4 100644 --- a/site/src/xServices/deploymentStats/deploymentStatsMachine.ts +++ b/site/src/xServices/deploymentStats/deploymentStatsMachine.ts @@ -1,6 +1,6 @@ -import { getDeploymentStats } from "api/api" -import { DeploymentStats } from "api/typesGenerated" -import { assign, createMachine } from "xstate" +import { getDeploymentStats } from "api/api"; +import { DeploymentStats } from "api/typesGenerated"; +import { assign, createMachine } from "xstate"; export const deploymentStatsMachine = createMachine( { @@ -10,14 +10,14 @@ export const deploymentStatsMachine = createMachine( schema: { context: {} as { - deploymentStats?: DeploymentStats - getDeploymentStatsError?: unknown + deploymentStats?: DeploymentStats; + getDeploymentStatsError?: unknown; }, events: {} as { type: "RELOAD" }, services: {} as { getDeploymentStats: { - data: DeploymentStats - } + data: DeploymentStats; + }; }, }, tsTypes: {} as import("./deploymentStatsMachine.typegen").Typegen0, @@ -57,4 +57,4 @@ export const deploymentStatsMachine = createMachine( }), }, }, -) +); diff --git a/site/src/xServices/entitlements/entitlementsSelectors.test.ts b/site/src/xServices/entitlements/entitlementsSelectors.test.ts index 9d179457a6..f1c3dc56cb 100644 --- a/site/src/xServices/entitlements/entitlementsSelectors.test.ts +++ b/site/src/xServices/entitlements/entitlementsSelectors.test.ts @@ -1,34 +1,34 @@ -import { getFeatureVisibility } from "./entitlementsSelectors" +import { getFeatureVisibility } from "./entitlementsSelectors"; describe("getFeatureVisibility", () => { it("returns empty object if there is no license", () => { const result = getFeatureVisibility(false, { audit_log: { entitlement: "entitled", enabled: true }, - }) - expect(result).toEqual(expect.objectContaining({})) - }) + }); + expect(result).toEqual(expect.objectContaining({})); + }); it("returns false for a feature that is not enabled", () => { const result = getFeatureVisibility(true, { audit_log: { entitlement: "entitled", enabled: false }, - }) - expect(result).toEqual(expect.objectContaining({ audit_log: false })) - }) + }); + expect(result).toEqual(expect.objectContaining({ audit_log: false })); + }); it("returns false for a feature that is not entitled", () => { const result = getFeatureVisibility(true, { audit_log: { entitlement: "not_entitled", enabled: true }, - }) - expect(result).toEqual(expect.objectContaining({ audit_log: false })) - }) + }); + expect(result).toEqual(expect.objectContaining({ audit_log: false })); + }); it("returns true for a feature that is in grace period", () => { const result = getFeatureVisibility(true, { audit_log: { entitlement: "grace_period", enabled: true }, - }) - expect(result).toEqual(expect.objectContaining({ audit_log: true })) - }) + }); + expect(result).toEqual(expect.objectContaining({ audit_log: true })); + }); it("returns true for a feature that is in entitled", () => { const result = getFeatureVisibility(true, { audit_log: { entitlement: "entitled", enabled: true }, - }) - expect(result).toEqual(expect.objectContaining({ audit_log: true })) - }) -}) + }); + expect(result).toEqual(expect.objectContaining({ audit_log: true })); + }); +}); diff --git a/site/src/xServices/entitlements/entitlementsSelectors.ts b/site/src/xServices/entitlements/entitlementsSelectors.ts index 5777700604..b2940ba93b 100644 --- a/site/src/xServices/entitlements/entitlementsSelectors.ts +++ b/site/src/xServices/entitlements/entitlementsSelectors.ts @@ -1,4 +1,4 @@ -import { Entitlements, Feature, FeatureName } from "api/typesGenerated" +import { Entitlements, Feature, FeatureName } from "api/typesGenerated"; /** * @param hasLicense true if Enterprise edition @@ -11,19 +11,19 @@ export const getFeatureVisibility = ( ): Record => { if (hasLicense) { const permissionPairs = Object.keys(features).map((feature) => { - const { entitlement, limit, actual, enabled } = features[feature] - const entitled = ["entitled", "grace_period"].includes(entitlement) - const limitCompliant = limit && actual ? limit >= actual : true - return [feature, entitled && limitCompliant && enabled] - }) - return Object.fromEntries(permissionPairs) + const { entitlement, limit, actual, enabled } = features[feature]; + const entitled = ["entitled", "grace_period"].includes(entitlement); + const limitCompliant = limit && actual ? limit >= actual : true; + return [feature, entitled && limitCompliant && enabled]; + }); + return Object.fromEntries(permissionPairs); } else { - return {} + return {}; } -} +}; export const selectFeatureVisibility = ( entitlements: Entitlements, ): Record => { - return getFeatureVisibility(entitlements.has_license, entitlements.features) -} + return getFeatureVisibility(entitlements.has_license, entitlements.features); +}; diff --git a/site/src/xServices/entitlements/entitlementsXService.ts b/site/src/xServices/entitlements/entitlementsXService.ts index e213159c33..61420ace5f 100644 --- a/site/src/xServices/entitlements/entitlementsXService.ts +++ b/site/src/xServices/entitlements/entitlementsXService.ts @@ -1,11 +1,11 @@ -import { assign, createMachine } from "xstate" -import * as API from "../../api/api" -import { Entitlements } from "../../api/typesGenerated" +import { assign, createMachine } from "xstate"; +import * as API from "../../api/api"; +import { Entitlements } from "../../api/typesGenerated"; export type EntitlementsContext = { - entitlements?: Entitlements - getEntitlementsError?: unknown -} + entitlements?: Entitlements; + getEntitlementsError?: unknown; +}; export const entitlementsMachine = createMachine( { @@ -74,7 +74,7 @@ export const entitlementsMachine = createMachine( }), assignGetEntitlementsError: assign({ getEntitlementsError: (_, event) => { - return event.data + return event.data; }, }), clearGetEntitlementsError: assign({ @@ -83,24 +83,24 @@ export const entitlementsMachine = createMachine( }, services: { refreshEntitlements: async () => { - return API.refreshEntitlements() + return API.refreshEntitlements(); }, getEntitlements: async () => { // Entitlements is injected by the Coder server into the HTML document. const entitlements = document.querySelector( "meta[property=entitlements]", - ) + ); if (entitlements) { - const rawContent = entitlements.getAttribute("content") + const rawContent = entitlements.getAttribute("content"); try { - return JSON.parse(rawContent as string) + return JSON.parse(rawContent as string); } catch (ex) { // Ignore this and fetch as normal! } } - return API.getEntitlements() + return API.getEntitlements(); }, }, }, -) +); diff --git a/site/src/xServices/experiments/experimentsMachine.ts b/site/src/xServices/experiments/experimentsMachine.ts index 53bd5a7d86..d96a59559c 100644 --- a/site/src/xServices/experiments/experimentsMachine.ts +++ b/site/src/xServices/experiments/experimentsMachine.ts @@ -1,10 +1,10 @@ -import { getExperiments } from "api/api" -import { Experiment } from "api/typesGenerated" -import { createMachine, assign } from "xstate" +import { getExperiments } from "api/api"; +import { Experiment } from "api/typesGenerated"; +import { createMachine, assign } from "xstate"; export interface ExperimentsContext { - experiments?: Experiment[] - getExperimentsError?: unknown + experiments?: Experiment[]; + getExperimentsError?: unknown; } export const experimentsMachine = createMachine( @@ -16,8 +16,8 @@ export const experimentsMachine = createMachine( context: {} as ExperimentsContext, services: {} as { getExperiments: { - data: Experiment[] - } + data: Experiment[]; + }; }, }, initial: "gettingExperiments", @@ -52,17 +52,19 @@ export const experimentsMachine = createMachine( services: { getExperiments: async () => { // Experiments is injected by the Coder server into the HTML document. - const experiments = document.querySelector("meta[property=experiments]") + const experiments = document.querySelector( + "meta[property=experiments]", + ); if (experiments) { - const rawContent = experiments.getAttribute("content") + const rawContent = experiments.getAttribute("content"); try { - return JSON.parse(rawContent as string) + return JSON.parse(rawContent as string); } catch (ex) { // Ignore this and fetch as normal! } } - return getExperiments() + return getExperiments(); }, }, actions: { @@ -82,4 +84,4 @@ export const experimentsMachine = createMachine( })), }, }, -) +); diff --git a/site/src/xServices/groups/createGroupXService.ts b/site/src/xServices/groups/createGroupXService.ts index 404c79325d..69dc6b3f7e 100644 --- a/site/src/xServices/groups/createGroupXService.ts +++ b/site/src/xServices/groups/createGroupXService.ts @@ -1,23 +1,23 @@ -import { createGroup } from "api/api" -import { CreateGroupRequest, Group } from "api/typesGenerated" -import { createMachine, assign } from "xstate" +import { createGroup } from "api/api"; +import { CreateGroupRequest, Group } from "api/typesGenerated"; +import { createMachine, assign } from "xstate"; export const createGroupMachine = createMachine( { id: "createGroupMachine", schema: { context: {} as { - organizationId: string - error?: unknown + organizationId: string; + error?: unknown; }, services: {} as { createGroup: { - data: Group - } + data: Group; + }; }, events: {} as { - type: "CREATE" - data: CreateGroupRequest + type: "CREATE"; + data: CreateGroupRequest; }, }, tsTypes: {} as import("./createGroupXService.typegen").Typegen0, @@ -56,4 +56,4 @@ export const createGroupMachine = createMachine( }), }, }, -) +); diff --git a/site/src/xServices/groups/editGroupXService.ts b/site/src/xServices/groups/editGroupXService.ts index ef919b7db7..755a4863e5 100644 --- a/site/src/xServices/groups/editGroupXService.ts +++ b/site/src/xServices/groups/editGroupXService.ts @@ -1,34 +1,34 @@ -import { getGroup, patchGroup } from "api/api" -import { getErrorMessage } from "api/errors" -import { Group } from "api/typesGenerated" -import { displayError } from "components/GlobalSnackbar/utils" -import { assign, createMachine } from "xstate" +import { getGroup, patchGroup } from "api/api"; +import { getErrorMessage } from "api/errors"; +import { Group } from "api/typesGenerated"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { assign, createMachine } from "xstate"; export const editGroupMachine = createMachine( { id: "editGroup", schema: { context: {} as { - groupId: string - group?: Group - error?: unknown + groupId: string; + group?: Group; + error?: unknown; }, services: {} as { loadGroup: { - data: Group - } + data: Group; + }; updateGroup: { - data: Group - } + data: Group; + }; }, events: {} as { - type: "UPDATE" + type: "UPDATE"; data: { - display_name: string - name: string - avatar_url: string - quota_allowance: number - } + display_name: string; + name: string; + avatar_url: string; + quota_allowance: number; + }; }, }, tsTypes: {} as import("./editGroupXService.typegen").Typegen0, @@ -74,14 +74,14 @@ export const editGroupMachine = createMachine( updateGroup: ({ group }, { data }) => { if (!group) { - throw new Error("Group not defined.") + throw new Error("Group not defined."); } return patchGroup(group.id, { ...data, add_users: [], remove_users: [], - }) + }); }, }, actions: { @@ -89,12 +89,12 @@ export const editGroupMachine = createMachine( group: (_, { data }) => data, }), displayLoadGroupError: (_, { data }) => { - const message = getErrorMessage(data, "Failed to the group.") - displayError(message) + const message = getErrorMessage(data, "Failed to the group."); + displayError(message); }, assignError: assign({ error: (_, event) => event.data, }), }, }, -) +); diff --git a/site/src/xServices/groups/groupXService.ts b/site/src/xServices/groups/groupXService.ts index d9aa63fa23..8d33d06700 100644 --- a/site/src/xServices/groups/groupXService.ts +++ b/site/src/xServices/groups/groupXService.ts @@ -1,55 +1,55 @@ -import { deleteGroup, getGroup, patchGroup, checkAuthorization } from "api/api" -import { getErrorMessage } from "api/errors" -import { AuthorizationResponse, Group } from "api/typesGenerated" -import { displayError, displaySuccess } from "components/GlobalSnackbar/utils" -import { assign, createMachine } from "xstate" +import { deleteGroup, getGroup, patchGroup, checkAuthorization } from "api/api"; +import { getErrorMessage } from "api/errors"; +import { AuthorizationResponse, Group } from "api/typesGenerated"; +import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; +import { assign, createMachine } from "xstate"; export const groupMachine = createMachine( { id: "group", schema: { context: {} as { - groupId: string - group?: Group - permissions?: AuthorizationResponse - addMemberCallback?: () => void - removingMember?: string + groupId: string; + group?: Group; + permissions?: AuthorizationResponse; + addMemberCallback?: () => void; + removingMember?: string; }, services: {} as { loadGroup: { - data: Group - } + data: Group; + }; loadPermissions: { - data: AuthorizationResponse - } + data: AuthorizationResponse; + }; addMember: { - data: Group - } + data: Group; + }; removeMember: { - data: Group - } + data: Group; + }; deleteGroup: { - data: unknown - } + data: unknown; + }; }, events: {} as | { - type: "ADD_MEMBER" - userId: string - callback: () => void + type: "ADD_MEMBER"; + userId: string; + callback: () => void; } | { - type: "REMOVE_MEMBER" - userId: string + type: "REMOVE_MEMBER"; + userId: string; } | { - type: "DELETE" + type: "DELETE"; } | { - type: "CONFIRM_DELETE" + type: "CONFIRM_DELETE"; } | { - type: "CANCEL_DELETE" + type: "CANCEL_DELETE"; }, }, tsTypes: {} as import("./groupXService.typegen").Typegen0, @@ -178,7 +178,7 @@ export const groupMachine = createMachine( }), addMember: ({ group }, { userId }) => { if (!group) { - throw new Error("Group not defined.") + throw new Error("Group not defined."); } return patchGroup(group.id, { @@ -186,11 +186,11 @@ export const groupMachine = createMachine( display_name: "", add_users: [userId], remove_users: [], - }) + }); }, removeMember: ({ group }, { userId }) => { if (!group) { - throw new Error("Group not defined.") + throw new Error("Group not defined."); } return patchGroup(group.id, { @@ -198,14 +198,14 @@ export const groupMachine = createMachine( display_name: "", add_users: [], remove_users: [userId], - }) + }); }, deleteGroup: ({ group }) => { if (!group) { - throw new Error("Group not defined.") + throw new Error("Group not defined."); } - return deleteGroup(group.id) + return deleteGroup(group.id); }, }, actions: { @@ -216,26 +216,26 @@ export const groupMachine = createMachine( addMemberCallback: (_, { callback }) => callback, }), displayLoadGroupError: (_, { data }) => { - const message = getErrorMessage(data, "Failed to load the group.") - displayError(message) + const message = getErrorMessage(data, "Failed to load the group."); + displayError(message); }, displayAddMemberError: (_, { data }) => { const message = getErrorMessage( data, "Failed to add member to the group.", - ) - displayError(message) + ); + displayError(message); }, callAddMemberCallback: ({ addMemberCallback }) => { if (addMemberCallback) { - addMemberCallback() + addMemberCallback(); } }, // Optimistically remove the user from members removeUserFromMembers: assign({ group: ({ group }, { userId }) => { if (!group) { - throw new Error("Group is not defined.") + throw new Error("Group is not defined."); } return { @@ -243,30 +243,33 @@ export const groupMachine = createMachine( members: group.members.filter( (currentMember) => currentMember.id !== userId, ), - } + }; }, }), displayRemoveMemberError: (_, { data }) => { const message = getErrorMessage( data, "Failed to remove member from the group.", - ) - displayError(message) + ); + displayError(message); }, displayRemoveMemberSuccess: () => { - displaySuccess("Member removed successfully.") + displaySuccess("Member removed successfully."); }, displayDeleteGroupError: (_, { data }) => { - const message = getErrorMessage(data, "Failed to delete group.") - displayError(message) + const message = getErrorMessage(data, "Failed to delete group."); + displayError(message); }, assignPermissions: assign({ permissions: (_, { data }) => data, }), displayLoadPermissionsError: (_, { data }) => { - const message = getErrorMessage(data, "Failed to load the permissions.") - displayError(message) + const message = getErrorMessage( + data, + "Failed to load the permissions.", + ); + displayError(message); }, }, }, -) +); diff --git a/site/src/xServices/groups/groupsXService.ts b/site/src/xServices/groups/groupsXService.ts index ff17e94dc5..3552838724 100644 --- a/site/src/xServices/groups/groupsXService.ts +++ b/site/src/xServices/groups/groupsXService.ts @@ -1,8 +1,8 @@ -import { getGroups } from "api/api" -import { getErrorMessage } from "api/errors" -import { Group } from "api/typesGenerated" -import { displayError } from "components/GlobalSnackbar/utils" -import { assign, createMachine } from "xstate" +import { getGroups } from "api/api"; +import { getErrorMessage } from "api/errors"; +import { Group } from "api/typesGenerated"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { assign, createMachine } from "xstate"; export const groupsMachine = createMachine( { @@ -10,14 +10,14 @@ export const groupsMachine = createMachine( predictableActionArguments: true, schema: { context: {} as { - organizationId: string - shouldFetchGroups: boolean - groups?: Group[] + organizationId: string; + shouldFetchGroups: boolean; + groups?: Group[]; }, services: {} as { loadGroups: { - data: Group[] - } + data: Group[]; + }; }, }, tsTypes: {} as import("./groupsXService.typegen").Typegen0, @@ -52,9 +52,9 @@ export const groupsMachine = createMachine( groups: (_, { data }) => data, }), displayLoadingGroupsError: (_, { data }) => { - const message = getErrorMessage(data, "Error on loading groups.") - displayError(message) + const message = getErrorMessage(data, "Error on loading groups."); + displayError(message); }, }, }, -) +); diff --git a/site/src/xServices/pagination/paginationXService.ts b/site/src/xServices/pagination/paginationXService.ts index 66073b8927..8ec366048f 100644 --- a/site/src/xServices/pagination/paginationXService.ts +++ b/site/src/xServices/pagination/paginationXService.ts @@ -1,17 +1,17 @@ -import { ActorRefFrom, createMachine, sendParent, assign } from "xstate" +import { ActorRefFrom, createMachine, sendParent, assign } from "xstate"; export interface PaginationContext { - page: number - limit: number + page: number; + limit: number; } export type PaginationEvent = | { type: "NEXT_PAGE" } | { type: "PREVIOUS_PAGE" } | { type: "GO_TO_PAGE"; page: number } - | { type: "RESET_PAGE" } + | { type: "RESET_PAGE" }; -export type PaginationMachineRef = ActorRefFrom +export type PaginationMachineRef = ActorRefFrom; export const paginationMachine = /** @xstate-layout N4IgpgJg5mDOIC5QAcCGUCWA7VAXDA9lgLKoDGAFtmAMQByAogBoAqA+gAoCCA4gwNoAGALqIUBWBnxExIAB6IATADZBAOgAcGgCwBWDYMEHBAdm2KANCACeiEwEZ7axboCcbkw8-3XGgL5+VmiYONIk5FRYtBwASgwAagCSAPIAqgDKnLwCIrLIElKEWLIKCAC09hrKarq6AMwarsraGormuiYaVrYIDk4u7q7e3r4BQejYeEWklNQ0PMlsLIvcfEKiSCD5kmElduq69vrKyiaCjqfu3YgauupatYp1eia+o4FbE6HTEXNx6Qx2KschtxDsintyvZqlVzmdGs0tIpbtcENplIoanV7M8jrpFCZntoAh8sAQIHA8l8pkQZpEwGoAE5gVAQHpgwoyTalZSuNTuV6CFzabSCFqdVFmdTPMU+dHnZrKMafEI08KzKJ5Aq7blKJwC1xC3QisUaCU2RDKQ5qGXaOWqaHokl+IA */ @@ -63,4 +63,4 @@ export const paginationMachine = }), }, }, - ) + ); diff --git a/site/src/xServices/quotas/quotasXService.ts b/site/src/xServices/quotas/quotasXService.ts index bc6f2e8e67..811cbea04f 100644 --- a/site/src/xServices/quotas/quotasXService.ts +++ b/site/src/xServices/quotas/quotasXService.ts @@ -1,12 +1,12 @@ -import { assign, createMachine } from "xstate" -import * as API from "../../api/api" -import { WorkspaceQuota } from "../../api/typesGenerated" +import { assign, createMachine } from "xstate"; +import * as API from "../../api/api"; +import { WorkspaceQuota } from "../../api/typesGenerated"; export type QuotaContext = { - username: string - quota?: WorkspaceQuota - getQuotaError?: unknown -} + username: string; + quota?: WorkspaceQuota; + getQuotaError?: unknown; +}; export const quotaMachine = createMachine( { @@ -60,4 +60,4 @@ export const quotaMachine = createMachine( getQuota: ({ username }) => API.getWorkspaceQuota(username), }, }, -) +); diff --git a/site/src/xServices/roles/siteRolesXService.ts b/site/src/xServices/roles/siteRolesXService.ts index 447a30b252..6fe7b96d8c 100644 --- a/site/src/xServices/roles/siteRolesXService.ts +++ b/site/src/xServices/roles/siteRolesXService.ts @@ -1,17 +1,17 @@ -import { assign, createMachine } from "xstate" -import * as API from "../../api/api" -import * as TypesGen from "../../api/typesGenerated" -import { displayError } from "../../components/GlobalSnackbar/utils" +import { assign, createMachine } from "xstate"; +import * as API from "../../api/api"; +import * as TypesGen from "../../api/typesGenerated"; +import { displayError } from "../../components/GlobalSnackbar/utils"; export const Language = { getRolesError: "Error on get the roles.", -} +}; type SiteRolesContext = { - hasPermission: boolean - roles?: TypesGen.AssignableRoles[] - getRolesError: unknown -} + hasPermission: boolean; + roles?: TypesGen.AssignableRoles[]; + getRolesError: unknown; +}; export const siteRolesMachine = createMachine( { @@ -63,7 +63,7 @@ export const siteRolesMachine = createMachine( getRolesError: (_, event) => event.data, }), displayGetRolesError: () => { - displayError(Language.getRolesError) + displayError(Language.getRolesError); }, clearGetRolesError: assign({ getRolesError: (_) => undefined, @@ -76,4 +76,4 @@ export const siteRolesMachine = createMachine( hasPermission: ({ hasPermission }) => hasPermission, }, }, -) +); diff --git a/site/src/xServices/setup/setupXService.ts b/site/src/xServices/setup/setupXService.ts index c5dd918679..4cac37851b 100644 --- a/site/src/xServices/setup/setupXService.ts +++ b/site/src/xServices/setup/setupXService.ts @@ -1,16 +1,16 @@ -import * as API from "api/api" -import * as TypesGen from "api/typesGenerated" -import { assign, createMachine } from "xstate" +import * as API from "api/api"; +import * as TypesGen from "api/typesGenerated"; +import { assign, createMachine } from "xstate"; export interface SetupContext { - error?: unknown - firstUser?: TypesGen.CreateFirstUserRequest + error?: unknown; + firstUser?: TypesGen.CreateFirstUserRequest; } export type SetupEvent = { - type: "CREATE_FIRST_USER" - firstUser: TypesGen.CreateFirstUserRequest -} + type: "CREATE_FIRST_USER"; + firstUser: TypesGen.CreateFirstUserRequest; +}; export const setupMachine = /** @xstate-layout N4IgpgJg5mDOIC5QGUwBcCuAHZaCGaYAdAJYQA2YAxAMIBKAogIIAqDA+gGICSdyL7AKrIGdRKCwB7WCTQlJAO3EgAHogDMAViKaAnPoDsABgBMRgCy6jpkwBoQAT0QBGI+qIA2N0ePOAHJqa-qYAviH2qJg4+IREAMYATmAEJApQnCQJsGiCsGAJVBCKxKkAbpIA1sSJyYQZWTl5CcpSMnKKymoI6ibuzmZGuiYG6kYemn4eHvZOCObOzjom-X7qHsZGmuq6mmER6Ni4BNVJKWn12bn5VPkJkglEWOQEAGb3ALbxp3WZl00t0lk8iUSFUGl07g8-XMxlWmmsWnUMxcUyIzg86hhPgWvg8JjC4RACkkEDgykihxiJQoYABbWBnUQflcRAMZlc6jWwwMfmRCDM2ksfgMzl0Bisbj85j8exAFOixy+tVS6V+jXydKBHVBXQAtDsiOyeVp-OoFrpnHyzboiKZ1CMDCNzEY-H4xbL5UdYi81VcEjRvpBNe0QaAuoEbZzpfaRh4rFM+eYTOZbcsTBD0b0bB6DgrCMGGTrELrzYajM5jUFVubLY4NIsvKM2eMtKZNOMCSEgA */ @@ -24,8 +24,8 @@ export const setupMachine = events: {} as SetupEvent, services: {} as { createFirstUser: { - data: TypesGen.CreateFirstUserResponse - } + data: TypesGen.CreateFirstUserResponse; + }; }, }, initial: "idle", @@ -78,4 +78,4 @@ export const setupMachine = }), }, }, - ) + ); diff --git a/site/src/xServices/sshKey/sshKeyXService.ts b/site/src/xServices/sshKey/sshKeyXService.ts index 135a8a4039..e8cbfb8e1c 100644 --- a/site/src/xServices/sshKey/sshKeyXService.ts +++ b/site/src/xServices/sshKey/sshKeyXService.ts @@ -1,21 +1,21 @@ -import { getUserSSHKey, regenerateUserSSHKey } from "api/api" -import { GitSSHKey } from "api/typesGenerated" -import { displaySuccess } from "components/GlobalSnackbar/utils" -import { createMachine, assign } from "xstate" -import { i18n } from "i18n" +import { getUserSSHKey, regenerateUserSSHKey } from "api/api"; +import { GitSSHKey } from "api/typesGenerated"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; +import { createMachine, assign } from "xstate"; +import { i18n } from "i18n"; -const { t } = i18n +const { t } = i18n; interface Context { - sshKey?: GitSSHKey - getSSHKeyError?: unknown - regenerateSSHKeyError?: unknown + sshKey?: GitSSHKey; + getSSHKeyError?: unknown; + regenerateSSHKeyError?: unknown; } type Events = | { type: "REGENERATE_SSH_KEY" } | { type: "CONFIRM_REGENERATE_SSH_KEY" } - | { type: "CANCEL_REGENERATE_SSH_KEY" } + | { type: "CANCEL_REGENERATE_SSH_KEY" }; export const sshKeyMachine = createMachine( { @@ -26,11 +26,11 @@ export const sshKeyMachine = createMachine( events: {} as Events, services: {} as { getSSHKey: { - data: GitSSHKey - } + data: GitSSHKey; + }; regenerateSSHKey: { - data: GitSSHKey - } + data: GitSSHKey; + }; }, }, tsTypes: {} as import("./sshKeyXService.typegen").Typegen0, @@ -118,8 +118,8 @@ export const sshKeyMachine = createMachine( notifySuccessSSHKeyRegenerated: () => { displaySuccess( t("sshRegenerateSuccessMessage", { ns: "userSettingsPage" }), - ) + ); }, }, }, -) +); diff --git a/site/src/xServices/starterTemplates/starterTemplateXService.ts b/site/src/xServices/starterTemplates/starterTemplateXService.ts index 077b5b8d4f..5104b9729b 100644 --- a/site/src/xServices/starterTemplates/starterTemplateXService.ts +++ b/site/src/xServices/starterTemplates/starterTemplateXService.ts @@ -1,12 +1,12 @@ -import { getTemplateExamples } from "api/api" -import { TemplateExample } from "api/typesGenerated" -import { assign, createMachine } from "xstate" +import { getTemplateExamples } from "api/api"; +import { TemplateExample } from "api/typesGenerated"; +import { assign, createMachine } from "xstate"; export interface StarterTemplateContext { - organizationId: string - exampleId: string - starterTemplate?: TemplateExample - error?: unknown + organizationId: string; + exampleId: string; + starterTemplate?: TemplateExample; + error?: unknown; } export const starterTemplateMachine = createMachine( @@ -17,8 +17,8 @@ export const starterTemplateMachine = createMachine( context: {} as StarterTemplateContext, services: {} as { loadStarterTemplate: { - data: TemplateExample - } + data: TemplateExample; + }; }, }, tsTypes: {} as import("./starterTemplateXService.typegen").Typegen0, @@ -49,14 +49,14 @@ export const starterTemplateMachine = createMachine( { services: { loadStarterTemplate: async ({ organizationId, exampleId }) => { - const examples = await getTemplateExamples(organizationId) + const examples = await getTemplateExamples(organizationId); const starterTemplate = examples.find( (example) => example.id === exampleId, - ) + ); if (!starterTemplate) { - throw new Error(`Example ${exampleId} not found.`) + throw new Error(`Example ${exampleId} not found.`); } - return starterTemplate + return starterTemplate; }, }, actions: { @@ -68,4 +68,4 @@ export const starterTemplateMachine = createMachine( }), }, }, -) +); diff --git a/site/src/xServices/starterTemplates/starterTemplatesXService.ts b/site/src/xServices/starterTemplates/starterTemplatesXService.ts index 3e07ce39da..7771b4538a 100644 --- a/site/src/xServices/starterTemplates/starterTemplatesXService.ts +++ b/site/src/xServices/starterTemplates/starterTemplatesXService.ts @@ -1,15 +1,15 @@ -import { getTemplateExamples } from "api/api" -import { TemplateExample } from "api/typesGenerated" +import { getTemplateExamples } from "api/api"; +import { TemplateExample } from "api/typesGenerated"; import { getTemplatesByTag, StarterTemplatesByTag, -} from "utils/starterTemplates" -import { assign, createMachine } from "xstate" +} from "utils/starterTemplates"; +import { assign, createMachine } from "xstate"; export interface StarterTemplatesContext { - organizationId: string - starterTemplatesByTag?: StarterTemplatesByTag - error?: unknown + organizationId: string; + starterTemplatesByTag?: StarterTemplatesByTag; + error?: unknown; } export const starterTemplatesMachine = createMachine( @@ -20,8 +20,8 @@ export const starterTemplatesMachine = createMachine( context: {} as StarterTemplatesContext, services: {} as { loadStarterTemplates: { - data: TemplateExample[] - } + data: TemplateExample[]; + }; }, }, tsTypes: {} as import("./starterTemplatesXService.typegen").Typegen0, @@ -63,4 +63,4 @@ export const starterTemplatesMachine = createMachine( }), }, }, -) +); diff --git a/site/src/xServices/template/searchUsersAndGroupsXService.ts b/site/src/xServices/template/searchUsersAndGroupsXService.ts index 2b962dce96..aee89da281 100644 --- a/site/src/xServices/template/searchUsersAndGroupsXService.ts +++ b/site/src/xServices/template/searchUsersAndGroupsXService.ts @@ -1,12 +1,12 @@ -import { getGroups, getTemplateACLAvailable, getUsers } from "api/api" -import { Group, User } from "api/typesGenerated" -import { queryToFilter } from "utils/filters" -import { everyOneGroup } from "utils/groups" -import { assign, createMachine } from "xstate" +import { getGroups, getTemplateACLAvailable, getUsers } from "api/api"; +import { Group, User } from "api/typesGenerated"; +import { queryToFilter } from "utils/filters"; +import { everyOneGroup } from "utils/groups"; +import { assign, createMachine } from "xstate"; export type SearchUsersAndGroupsEvent = | { type: "SEARCH"; query: string } - | { type: "CLEAR_RESULTS" } + | { type: "CLEAR_RESULTS" }; export const searchUsersAndGroupsMachine = createMachine( { @@ -14,19 +14,19 @@ export const searchUsersAndGroupsMachine = createMachine( predictableActionArguments: true, schema: { context: {} as { - organizationId: string - templateID?: string - userResults: User[] - groupResults: Group[] + organizationId: string; + templateID?: string; + userResults: User[]; + groupResults: Group[]; }, events: {} as SearchUsersAndGroupsEvent, services: {} as { search: { data: { - users: User[] - groups: Group[] - } - } + users: User[]; + groups: Group[]; + }; + }; }, }, tsTypes: {} as import("./searchUsersAndGroupsXService.typegen").Typegen0, @@ -58,22 +58,22 @@ export const searchUsersAndGroupsMachine = createMachine( { services: { search: async ({ organizationId, templateID }, { query }) => { - let users, groups + let users, groups; if (templateID && templateID !== "") { const res = await getTemplateACLAvailable( templateID, queryToFilter(query), - ) - users = res.users - groups = res.groups + ); + users = res.users; + groups = res.groups; } else { const [userRes, groupsRes] = await Promise.all([ getUsers(queryToFilter(query)), getGroups(organizationId), - ]) + ]); - users = userRes.users - groups = groupsRes + users = userRes.users; + groups = groupsRes; } // The Everyone groups is not returned by the API so we have to add it @@ -81,7 +81,7 @@ export const searchUsersAndGroupsMachine = createMachine( return { users: users, groups: [everyOneGroup(organizationId), ...groups], - } + }; }, }, actions: { @@ -98,4 +98,4 @@ export const searchUsersAndGroupsMachine = createMachine( queryHasMinLength: (_, { query }) => query.length >= 3, }, }, -) +); diff --git a/site/src/xServices/template/templateACLXService.ts b/site/src/xServices/template/templateACLXService.ts index 4ff85299c6..9b0b1e7462 100644 --- a/site/src/xServices/template/templateACLXService.ts +++ b/site/src/xServices/template/templateACLXService.ts @@ -1,78 +1,78 @@ -import { getTemplateACL, updateTemplateACL } from "api/api" +import { getTemplateACL, updateTemplateACL } from "api/api"; import { TemplateACL, TemplateGroup, TemplateRole, TemplateUser, -} from "api/typesGenerated" -import { displaySuccess } from "components/GlobalSnackbar/utils" -import { assign, createMachine } from "xstate" +} from "api/typesGenerated"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; +import { assign, createMachine } from "xstate"; export const templateACLMachine = createMachine( { schema: { context: {} as { - templateId: string - templateACL?: TemplateACL + templateId: string; + templateACL?: TemplateACL; // User - userToBeAdded?: TemplateUser - userToBeUpdated?: TemplateUser - addUserCallback?: () => void + userToBeAdded?: TemplateUser; + userToBeUpdated?: TemplateUser; + addUserCallback?: () => void; // Group - groupToBeAdded?: TemplateGroup - groupToBeUpdated?: TemplateGroup - addGroupCallback?: () => void + groupToBeAdded?: TemplateGroup; + groupToBeUpdated?: TemplateGroup; + addGroupCallback?: () => void; }, services: {} as { loadTemplateACL: { - data: TemplateACL - } + data: TemplateACL; + }; // User addUser: { - data: unknown - } + data: unknown; + }; updateUser: { - data: unknown - } + data: unknown; + }; // Group addGroup: { - data: unknown - } + data: unknown; + }; updateGroup: { - data: unknown - } + data: unknown; + }; }, events: {} as // User | { - type: "ADD_USER" - user: TemplateUser - role: TemplateRole - onDone: () => void + type: "ADD_USER"; + user: TemplateUser; + role: TemplateRole; + onDone: () => void; } | { - type: "UPDATE_USER_ROLE" - user: TemplateUser - role: TemplateRole + type: "UPDATE_USER_ROLE"; + user: TemplateUser; + role: TemplateRole; } | { - type: "REMOVE_USER" - user: TemplateUser + type: "REMOVE_USER"; + user: TemplateUser; } // Group | { - type: "ADD_GROUP" - group: TemplateGroup - role: TemplateRole - onDone: () => void + type: "ADD_GROUP"; + group: TemplateGroup; + role: TemplateRole; + onDone: () => void; } | { - type: "UPDATE_GROUP_ROLE" - group: TemplateGroup - role: TemplateRole + type: "UPDATE_GROUP_ROLE"; + group: TemplateGroup; + role: TemplateRole; } | { - type: "REMOVE_GROUP" - group: TemplateGroup + type: "REMOVE_GROUP"; + group: TemplateGroup; }, }, tsTypes: {} as import("./templateACLXService.typegen").Typegen0, @@ -235,20 +235,20 @@ export const templateACLMachine = createMachine( addUserToTemplateACL: assign({ templateACL: ({ templateACL, userToBeAdded }) => { if (!userToBeAdded) { - throw new Error("No user to be added") + throw new Error("No user to be added"); } if (!templateACL) { - throw new Error("Template ACL is not loaded yet") + throw new Error("Template ACL is not loaded yet"); } return { ...templateACL, users: [...templateACL.users, userToBeAdded], - } + }; }, }), runAddUserCallback: ({ addUserCallback }) => { if (addUserCallback) { - addUserCallback() + addUserCallback(); } }, assignUserToBeUpdated: assign({ @@ -257,42 +257,42 @@ export const templateACLMachine = createMachine( updateUserOnTemplateACL: assign({ templateACL: ({ templateACL, userToBeUpdated }) => { if (!userToBeUpdated) { - throw new Error("No user to be added") + throw new Error("No user to be added"); } if (!templateACL) { - throw new Error("Template ACL is not loaded yet") + throw new Error("Template ACL is not loaded yet"); } return { ...templateACL, users: templateACL.users.map((oldTemplateUser) => { return oldTemplateUser.id === userToBeUpdated.id ? userToBeUpdated - : oldTemplateUser + : oldTemplateUser; }), - } + }; }, }), clearUserToBeUpdated: assign({ userToBeUpdated: (_) => undefined, }), displayUpdateUserSuccessMessage: () => { - displaySuccess("User role update successfully!") + displaySuccess("User role update successfully!"); }, removeUserFromTemplateACL: assign({ templateACL: ({ templateACL }, { user }) => { if (!templateACL) { - throw new Error("Template ACL is not loaded yet") + throw new Error("Template ACL is not loaded yet"); } return { ...templateACL, users: templateACL.users.filter((oldTemplateUser) => { - return oldTemplateUser.id !== user.id + return oldTemplateUser.id !== user.id; }), - } + }; }, }), displayRemoveUserSuccessMessage: () => { - displaySuccess("User removed successfully!") + displaySuccess("User removed successfully!"); }, // Group assignGroupToBeAdded: assign({ @@ -302,20 +302,20 @@ export const templateACLMachine = createMachine( addGroupToTemplateACL: assign({ templateACL: ({ templateACL, groupToBeAdded }) => { if (!groupToBeAdded) { - throw new Error("No group to be added") + throw new Error("No group to be added"); } if (!templateACL) { - throw new Error("Template ACL is not loaded yet") + throw new Error("Template ACL is not loaded yet"); } return { ...templateACL, group: [...templateACL.group, groupToBeAdded], - } + }; }, }), runAddGroupCallback: ({ addGroupCallback }) => { if (addGroupCallback) { - addGroupCallback() + addGroupCallback(); } }, assignGroupToBeUpdated: assign({ @@ -324,43 +324,43 @@ export const templateACLMachine = createMachine( updateGroupOnTemplateACL: assign({ templateACL: ({ templateACL, groupToBeUpdated }) => { if (!groupToBeUpdated) { - throw new Error("No group to be added") + throw new Error("No group to be added"); } if (!templateACL) { - throw new Error("Template ACL is not loaded yet") + throw new Error("Template ACL is not loaded yet"); } return { ...templateACL, group: templateACL.group.map((oldTemplateGroup) => { return oldTemplateGroup.id === groupToBeUpdated.id ? groupToBeUpdated - : oldTemplateGroup + : oldTemplateGroup; }), - } + }; }, }), clearGroupToBeUpdated: assign({ groupToBeUpdated: (_) => undefined, }), displayUpdateGroupSuccessMessage: () => { - displaySuccess("Group role update successfully!") + displaySuccess("Group role update successfully!"); }, removeGroupFromTemplateACL: assign({ templateACL: ({ templateACL }, { group }) => { if (!templateACL) { - throw new Error("Template ACL is not loaded yet") + throw new Error("Template ACL is not loaded yet"); } return { ...templateACL, group: templateACL.group.filter((oldTemplateGroup) => { - return oldTemplateGroup.id !== group.id + return oldTemplateGroup.id !== group.id; }), - } + }; }, }), displayRemoveGroupSuccessMessage: () => { - displaySuccess("Group removed successfully!") + displaySuccess("Group removed successfully!"); }, }, }, -) +); diff --git a/site/src/xServices/template/templateVariablesXService.ts b/site/src/xServices/template/templateVariablesXService.ts index a408a05315..78aeae7386 100644 --- a/site/src/xServices/template/templateVariablesXService.ts +++ b/site/src/xServices/template/templateVariablesXService.ts @@ -3,37 +3,37 @@ import { getTemplateVersion, getTemplateVersionVariables, updateActiveTemplateVersion, -} from "api/api" +} from "api/api"; import { CreateTemplateVersionRequest, Template, TemplateVersion, TemplateVersionVariable, -} from "api/typesGenerated" -import { assign, createMachine } from "xstate" -import { delay } from "utils/delay" -import { Message } from "api/types" +} from "api/typesGenerated"; +import { assign, createMachine } from "xstate"; +import { delay } from "utils/delay"; +import { Message } from "api/types"; type TemplateVariablesContext = { - organizationId: string + organizationId: string; - template: Template - activeTemplateVersion?: TemplateVersion - templateVariables?: TemplateVersionVariable[] + template: Template; + activeTemplateVersion?: TemplateVersion; + templateVariables?: TemplateVersionVariable[]; - createTemplateVersionRequest?: CreateTemplateVersionRequest - newTemplateVersion?: TemplateVersion + createTemplateVersionRequest?: CreateTemplateVersionRequest; + newTemplateVersion?: TemplateVersion; - getTemplateDataError?: unknown - updateTemplateError?: unknown + getTemplateDataError?: unknown; + updateTemplateError?: unknown; - jobError?: TemplateVersion["job"]["error"] -} + jobError?: TemplateVersion["job"]["error"]; +}; type UpdateTemplateEvent = { - type: "UPDATE_TEMPLATE_EVENT" - request: CreateTemplateVersionRequest -} + type: "UPDATE_TEMPLATE_EVENT"; + request: CreateTemplateVersionRequest; +}; export const templateVariablesMachine = createMachine( { @@ -45,20 +45,20 @@ export const templateVariablesMachine = createMachine( events: {} as UpdateTemplateEvent, services: {} as { getActiveTemplateVersion: { - data: TemplateVersion - } + data: TemplateVersion; + }; getTemplateVariables: { - data: TemplateVersionVariable[] - } + data: TemplateVersionVariable[]; + }; createNewTemplateVersion: { - data: TemplateVersion - } + data: TemplateVersion; + }; waitForJobToBeCompleted: { - data: TemplateVersion - } + data: TemplateVersion; + }; updateTemplate: { - data: Message - } + data: Message; + }; }, }, initial: "gettingActiveTemplateVersion", @@ -163,44 +163,44 @@ export const templateVariablesMachine = createMachine( { services: { getActiveTemplateVersion: ({ template }) => { - return getTemplateVersion(template.active_version_id) + return getTemplateVersion(template.active_version_id); }, getTemplateVariables: ({ template }) => { - return getTemplateVersionVariables(template.active_version_id) + return getTemplateVersionVariables(template.active_version_id); }, createNewTemplateVersion: ({ organizationId, createTemplateVersionRequest, }) => { if (!createTemplateVersionRequest) { - throw new Error("Missing request body") + throw new Error("Missing request body"); } return createTemplateVersion( organizationId, createTemplateVersionRequest, - ) + ); }, waitForJobToBeCompleted: async ({ newTemplateVersion }) => { if (!newTemplateVersion) { - throw new Error("Template version is undefined") + throw new Error("Template version is undefined"); } - let status = newTemplateVersion.job.status + let status = newTemplateVersion.job.status; while (["pending", "running"].includes(status)) { - newTemplateVersion = await getTemplateVersion(newTemplateVersion.id) - status = newTemplateVersion.job.status - await delay(2_000) + newTemplateVersion = await getTemplateVersion(newTemplateVersion.id); + status = newTemplateVersion.job.status; + await delay(2_000); } - return newTemplateVersion + return newTemplateVersion; }, updateTemplate: ({ template, newTemplateVersion }) => { if (!newTemplateVersion) { - throw new Error("New template version is undefined") + throw new Error("New template version is undefined"); } return updateActiveTemplateVersion(template.id, { id: newTemplateVersion.id, - }) + }); }, }, actions: { @@ -237,8 +237,8 @@ export const templateVariablesMachine = createMachine( }, guards: { hasJobError: (_, { data }) => { - return Boolean(data.job.error) + return Boolean(data.job.error); }, }, }, -) +); diff --git a/site/src/xServices/templateVersion/templateVersionXService.ts b/site/src/xServices/templateVersion/templateVersionXService.ts index 407324a0f2..4cad1e061c 100644 --- a/site/src/xServices/templateVersion/templateVersionXService.ts +++ b/site/src/xServices/templateVersion/templateVersionXService.ts @@ -3,25 +3,25 @@ import { GetPreviousTemplateVersionByNameResponse, getTemplateByName, getTemplateVersionByName, -} from "api/api" -import { Template, TemplateVersion } from "api/typesGenerated" +} from "api/api"; +import { Template, TemplateVersion } from "api/typesGenerated"; import { getTemplateVersionFiles, TemplateVersionFiles, -} from "utils/templateVersion" -import { assign, createMachine } from "xstate" +} from "utils/templateVersion"; +import { assign, createMachine } from "xstate"; export interface TemplateVersionMachineContext { - orgId: string - templateName: string - versionName: string - template?: Template - currentVersion?: TemplateVersion - currentFiles?: TemplateVersionFiles - error?: unknown + orgId: string; + templateName: string; + versionName: string; + template?: Template; + currentVersion?: TemplateVersion; + currentFiles?: TemplateVersionFiles; + error?: unknown; // Get file diffs - previousVersion?: TemplateVersion - previousFiles?: TemplateVersionFiles + previousVersion?: TemplateVersion; + previousFiles?: TemplateVersionFiles; } export const templateVersionMachine = createMachine( @@ -33,21 +33,21 @@ export const templateVersionMachine = createMachine( services: {} as { loadVersions: { data: { - currentVersion: GetPreviousTemplateVersionByNameResponse - previousVersion: GetPreviousTemplateVersionByNameResponse - } - } + currentVersion: GetPreviousTemplateVersionByNameResponse; + previousVersion: GetPreviousTemplateVersionByNameResponse; + }; + }; loadTemplate: { data: { - template: Template - } - } + template: Template; + }; + }; loadFiles: { data: { - currentFiles: TemplateVersionFiles - previousFiles: TemplateVersionFiles - } - } + currentFiles: TemplateVersionFiles; + previousFiles: TemplateVersionFiles; + }; + }; }, }, tsTypes: {} as import("./templateVersionXService.typegen").Typegen0, @@ -142,38 +142,38 @@ export const templateVersionMachine = createMachine( const [currentVersion, previousVersion] = await Promise.all([ getTemplateVersionByName(orgId, templateName, versionName), getPreviousTemplateVersionByName(orgId, templateName, versionName), - ]) + ]); return { currentVersion, previousVersion, - } + }; }, loadTemplate: async ({ orgId, templateName }) => { - const template = await getTemplateByName(orgId, templateName) + const template = await getTemplateByName(orgId, templateName); return { template, - } + }; }, loadFiles: async ({ currentVersion, previousVersion }) => { if (!currentVersion) { - throw new Error("Version is not defined") + throw new Error("Version is not defined"); } const loadFilesPromises: ReturnType[] = - [] - loadFilesPromises.push(getTemplateVersionFiles(currentVersion)) + []; + loadFilesPromises.push(getTemplateVersionFiles(currentVersion)); if (previousVersion) { - loadFilesPromises.push(getTemplateVersionFiles(previousVersion)) + loadFilesPromises.push(getTemplateVersionFiles(previousVersion)); } const [currentFiles, previousFiles] = await Promise.all( loadFilesPromises, - ) + ); return { currentFiles, previousFiles, - } + }; }, }, }, -) +); diff --git a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts index 3f8ebc26a4..a91cf7dfee 100644 --- a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts +++ b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts @@ -6,28 +6,28 @@ import { UploadResponse, VariableValue, WorkspaceResource, -} from "api/typesGenerated" -import { assign, createMachine } from "xstate" -import * as API from "api/api" -import { FileTree, traverse } from "utils/filetree" -import { isAllowedFile } from "utils/templateVersion" -import { TarReader, TarWriter } from "utils/tar" -import { PublishVersionData } from "pages/TemplateVersionEditorPage/types" +} from "api/typesGenerated"; +import { assign, createMachine } from "xstate"; +import * as API from "api/api"; +import { FileTree, traverse } from "utils/filetree"; +import { isAllowedFile } from "utils/templateVersion"; +import { TarReader, TarWriter } from "utils/tar"; +import { PublishVersionData } from "pages/TemplateVersionEditorPage/types"; export interface TemplateVersionEditorMachineContext { - orgId: string - templateId?: string - fileTree?: FileTree - uploadResponse?: UploadResponse - version?: TemplateVersion - resources?: WorkspaceResource[] - buildLogs?: ProvisionerJobLog[] - tarReader?: TarReader - publishingError?: unknown - lastSuccessfulPublishedVersion?: TemplateVersion - lastSuccessfulPublishIsDefault?: boolean - missingVariables?: TemplateVersionVariable[] - missingVariableValues?: VariableValue[] + orgId: string; + templateId?: string; + fileTree?: FileTree; + uploadResponse?: UploadResponse; + version?: TemplateVersion; + resources?: WorkspaceResource[]; + buildLogs?: ProvisionerJobLog[]; + tarReader?: TarReader; + publishingError?: unknown; + lastSuccessfulPublishedVersion?: TemplateVersion; + lastSuccessfulPublishIsDefault?: boolean; + missingVariables?: TemplateVersionVariable[]; + missingVariableValues?: VariableValue[]; } export const templateVersionEditorMachine = createMachine( @@ -40,9 +40,9 @@ export const templateVersionEditorMachine = createMachine( events: {} as | { type: "INITIALIZE"; tarReader: TarReader } | { - type: "CREATE_VERSION" - fileTree: FileTree - templateId: string + type: "CREATE_VERSION"; + fileTree: FileTree; + templateId: string; } | { type: "CANCEL_VERSION" } | { type: "SET_MISSING_VARIABLE_VALUES"; values: VariableValue[] } @@ -55,26 +55,26 @@ export const templateVersionEditorMachine = createMachine( services: {} as { uploadTar: { - data: UploadResponse - } + data: UploadResponse; + }; createBuild: { - data: TemplateVersion - } + data: TemplateVersion; + }; cancelBuild: { - data: void - } + data: void; + }; fetchVersion: { - data: TemplateVersion - } + data: TemplateVersion; + }; getResources: { - data: WorkspaceResource[] - } + data: WorkspaceResource[]; + }; publishingVersion: { - data: { isActiveVersion: boolean } - } + data: { isActiveVersion: boolean }; + }; loadMissingVariables: { - data: TemplateVersionVariable[] - } + data: TemplateVersionVariable[]; + }; }, }, tsTypes: {} as import("./templateVersionEditorXService.typegen").Typegen0, @@ -267,8 +267,8 @@ export const templateVersionEditorMachine = createMachine( }), addBuildLog: assign({ buildLogs: (context, event) => { - const previousLogs = context.buildLogs ?? [] - return [...previousLogs, event.log] + const previousLogs = context.buildLogs ?? []; + return [...previousLogs, event.log]; }, // Instead of periodically fetching the version, // we just assume the state is running after the first log. @@ -276,7 +276,7 @@ export const templateVersionEditorMachine = createMachine( // The machine fetches the version after the log stream ends anyways! version: (context) => { if (!context.version || context.buildLogs?.length !== 0) { - return context.version + return context.version; } return { ...context.version, @@ -284,7 +284,7 @@ export const templateVersionEditorMachine = createMachine( ...context.version.job, status: "running" as ProvisionerJobStatus, }, - } + }; }, }), assignTarReader: assign({ @@ -307,12 +307,12 @@ export const templateVersionEditorMachine = createMachine( services: { uploadTar: async ({ fileTree, tarReader }) => { if (!fileTree) { - throw new Error("file tree must to be set") + throw new Error("file tree must to be set"); } if (!tarReader) { - throw new Error("tar reader must to be set") + throw new Error("tar reader must to be set"); } - const tar = new TarWriter() + const tar = new TarWriter(); // Add previous non editable files for (const file of tarReader.fileInfo) { @@ -323,7 +323,7 @@ export const templateVersionEditorMachine = createMachine( mtime: file.mtime, user: file.user, group: file.group, - }) + }); } else { tar.addFile( file.name, @@ -334,7 +334,7 @@ export const templateVersionEditorMachine = createMachine( user: file.user, group: file.group, }, - ) + ); } } } @@ -342,22 +342,22 @@ export const templateVersionEditorMachine = createMachine( traverse(fileTree, (content, _filename, fullPath) => { // When a file is deleted. Don't add it to the tar. if (content === undefined) { - return + return; } if (typeof content === "string") { - tar.addFile(fullPath, content) - return + tar.addFile(fullPath, content); + return; } - tar.addFolder(fullPath) - }) - const blob = (await tar.write()) as Blob - return API.uploadTemplateFile(new File([blob], "template.tar")) + tar.addFolder(fullPath); + }); + const blob = (await tar.write()) as Blob; + return API.uploadTemplateFile(new File([blob], "template.tar")); }, createBuild: (ctx) => { if (!ctx.uploadResponse) { - throw new Error("no upload response") + throw new Error("no upload response"); } return API.createTemplateVersion(ctx.orgId, { provisioner: "terraform", @@ -366,49 +366,49 @@ export const templateVersionEditorMachine = createMachine( template_id: ctx.templateId, file_id: ctx.uploadResponse.hash, user_variable_values: ctx.missingVariableValues, - }) + }); }, fetchVersion: (ctx) => { if (!ctx.version) { - throw new Error("template version must be set") + throw new Error("template version must be set"); } - return API.getTemplateVersion(ctx.version.id) + return API.getTemplateVersion(ctx.version.id); }, watchBuildLogs: ({ version }) => async (callback) => { if (!version) { - throw new Error("version must be set") + throw new Error("version must be set"); } const socket = API.watchBuildLogsByTemplateVersionId(version.id, { onMessage: (log) => { - callback({ type: "ADD_BUILD_LOG", log }) + callback({ type: "ADD_BUILD_LOG", log }); }, onDone: () => { - callback({ type: "BUILD_DONE" }) + callback({ type: "BUILD_DONE" }); }, onError: (error) => { - console.error(error) + console.error(error); }, - }) + }); return () => { - socket.close() - } + socket.close(); + }; }, getResources: (ctx) => { if (!ctx.version) { - throw new Error("template version must be set") + throw new Error("template version must be set"); } - return API.getTemplateVersionResources(ctx.version.id) + return API.getTemplateVersionResources(ctx.version.id); }, cancelBuild: async (ctx) => { if (!ctx.version) { - return + return; } if (ctx.version.job.status === "running") { - await API.cancelTemplateVersionBuild(ctx.version.id) + await API.cancelTemplateVersionBuild(ctx.version.id); } }, publishingVersion: async ( @@ -416,12 +416,13 @@ export const templateVersionEditorMachine = createMachine( { name, message, isActiveVersion }, ) => { if (!version) { - throw new Error("Version is not set") + throw new Error("Version is not set"); } if (!templateId) { - throw new Error("Template is not set") + throw new Error("Template is not set"); } - const haveChanges = name !== version.name || message !== version.message + const haveChanges = + name !== version.name || message !== version.message; await Promise.all([ haveChanges ? API.patchTemplateVersion(version.id, { name, message }) @@ -431,22 +432,22 @@ export const templateVersionEditorMachine = createMachine( id: version.id, }) : Promise.resolve(), - ]) + ]); - return { isActiveVersion } + return { isActiveVersion }; }, loadMissingVariables: ({ version }) => { if (!version) { - throw new Error("Version is not set") + throw new Error("Version is not set"); } - const variables = API.getTemplateVersionVariables(version.id) - return variables + const variables = API.getTemplateVersionVariables(version.id); + return variables; }, }, guards: { jobFailedWithMissingVariables: (_, { data }) => { - return data.job.error_code === "REQUIRED_TEMPLATE_VARIABLES" + return data.job.error_code === "REQUIRED_TEMPLATE_VARIABLES"; }, }, }, -) +); diff --git a/site/src/xServices/templates/templatesXService.ts b/site/src/xServices/templates/templatesXService.ts index 42249b0410..2a57833fb6 100644 --- a/site/src/xServices/templates/templatesXService.ts +++ b/site/src/xServices/templates/templatesXService.ts @@ -1,14 +1,14 @@ -import { Permissions } from "xServices/auth/authXService" -import { assign, createMachine } from "xstate" -import * as API from "../../api/api" -import * as TypesGen from "../../api/typesGenerated" +import { Permissions } from "xServices/auth/authXService"; +import { assign, createMachine } from "xstate"; +import * as API from "../../api/api"; +import * as TypesGen from "../../api/typesGenerated"; export interface TemplatesContext { - organizationId: string - permissions: Permissions - templates?: TypesGen.Template[] - examples?: TypesGen.TemplateExample[] - error?: unknown + organizationId: string; + permissions: Permissions; + templates?: TypesGen.Template[]; + examples?: TypesGen.TemplateExample[]; + error?: unknown; } export const templatesMachine = createMachine( @@ -21,10 +21,10 @@ export const templatesMachine = createMachine( services: {} as { load: { data: { - templates: TypesGen.Template[] - examples: TypesGen.TemplateExample[] - } - } + templates: TypesGen.Template[]; + examples: TypesGen.TemplateExample[]; + }; + }; }, }, initial: "loading", @@ -65,13 +65,13 @@ export const templatesMachine = createMachine( permissions.createTemplates ? API.getTemplateExamples(organizationId) : Promise.resolve([]), - ]) + ]); return { templates, examples, - } + }; }, }, }, -) +); diff --git a/site/src/xServices/terminal/terminalXService.ts b/site/src/xServices/terminal/terminalXService.ts index 8216c23ff2..5bfee07b47 100644 --- a/site/src/xServices/terminal/terminalXService.ts +++ b/site/src/xServices/terminal/terminalXService.ts @@ -1,41 +1,41 @@ -import { assign, createMachine } from "xstate" -import * as API from "../../api/api" -import * as Types from "../../api/types" -import * as TypesGen from "../../api/typesGenerated" +import { assign, createMachine } from "xstate"; +import * as API from "../../api/api"; +import * as Types from "../../api/types"; +import * as TypesGen from "../../api/typesGenerated"; export interface TerminalContext { - workspaceError?: unknown - workspace?: TypesGen.Workspace - workspaceAgent?: TypesGen.WorkspaceAgent - workspaceAgentError?: unknown - websocket?: WebSocket - websocketError?: unknown - websocketURL?: string - websocketURLError?: unknown + workspaceError?: unknown; + workspace?: TypesGen.Workspace; + workspaceAgent?: TypesGen.WorkspaceAgent; + workspaceAgentError?: unknown; + websocket?: WebSocket; + websocketError?: unknown; + websocketURL?: string; + websocketURLError?: unknown; // Assigned by connecting! // The workspace agent is entirely optional. If the agent is omitted the // first agent will be used. - agentName?: string - username?: string - workspaceName?: string - reconnection?: string - command?: string + agentName?: string; + username?: string; + workspaceName?: string; + reconnection?: string; + command?: string; // If baseURL is not..... - baseURL?: string + baseURL?: string; } export type TerminalEvent = | { - type: "CONNECT" - agentName?: string - reconnection?: string - workspaceName?: string - username?: string + type: "CONNECT"; + agentName?: string; + reconnection?: string; + workspaceName?: string; + username?: string; } | { type: "WRITE"; request: Types.ReconnectingPTYRequest } | { type: "READ"; data: ArrayBuffer } - | { type: "DISCONNECT" } + | { type: "DISCONNECT" }; export const terminalMachine = /** @xstate-layout N4IgpgJg5mDOIC5QBcwCcC2BLAdgQwBsBlZPVAOljGQFcAHAYggHscxLSLVNdCSz2VWnQDaABgC6iUHWawsyLK2kgAHogAcAVgCM5MQGYdAJgMaALOYDsV8zq0AaEAE9ExgGwHyATmPf3VmI6Yub+5gYAvhFO3Nj4xJyC1PTkMMgA6sxoANawdHgAxuxpijhQmTl5hWBMrOy4AG7M2cXUFbn5ReJSSCCy8orKveoI5qbkOhq23hozflruxuZOrghGXuZaRsFiWlbeWuYa7lEx6HF8iZTJdKltWR3Vd8il5Q9VRQzoaFnkdARkABmWQwz3aHzA3RU-QUShwKhGYy8k2msw080WyxcbmMYnIoW8hO8ZkMYisOlOIFivASAmer3BnTAAEEYDhkLU2ORGs1Whl3kzWWB2VDejDBvDhogdAYrFoJuYxJjdmJvCENCs3Fo8e4glpjFptWSDhpKdT4vwKCVcG9KoK2Rzvr9-kCQWCBdUhSLJNC5LChqARotNQgpniNAYtAdjFYDL4pidolTzjTLXyGWAAEZEZgFFrIACqACUADKc+o4JotZ45vPUYsl0UyP0ShGIWVWchGA27Akzbwh4LjDQmAk6AmBbxmlMWq7WsrpLO1-MNr5oH5oP4A5DAzA13Mr0tNvotuFthBWYyDo56ULGewWHTk0zGac8Wd0gqsNgFV7l7mVry5BfjgP7IMe4pnlKobmO45D3mG7ihFMBgBIOhgaPoizar4xIRv4b4XLSFAgWBNprhuW6unupFgL+EGngGaiIMG2IIPYtj4psMYaGIxh+Do9iEamVy0b+kAMOkRYAJIACoAKIMQMUGBogdiYZs3Z+N444GqYg42F4kY6LqmxWLMUaREm5qXJ+350agEAMEW8nMgAIkp-qSqpow6N45BIRYkYRgsBxyoOASdnG2gyu43jcUhwkfiR9niU5bnSUQADCADyAByeXyVlsmea20F+Zho6EksgU6WIGpsZMuj6OZfimLB0bmEltkUBAWCwGJjkMLlBVFSVPpiox3nMexV6Na+lI4MwEBwCoNnEWAvrKUxIwALRjCGu1yuQRpiCEBpyrshLdRt1zCFtXnnu4IabCdZ1nWMezalGFLWTOPVJMI7p2tUD1lT5hyDgYXjvR9KrxSZXV-e+AN3SkaSMk8862o8RRgypM1DiGL4+FY7iGvqniXvYSNnCjt1COj9wg0UlA0AURSwPAk3bdNQbQ-o3YLCYHGwcTRwBeE-GYjToRWDdab0jamNFF6yD4ztiD+PKgTdtYljuAEBjE3G+J+HFI7ah45IK3O1AZtmB71qWGt84gezofe+j2Lq7h+XxSFWXTRGK4NNqu09fFYUssyLPxCyOI1hjyh4CzW1b3jy8jIeialjkR9BhzweTiwBPqOm2Asg5TOYXbqmY6xISEtt0n1A155ABc+aheiGCYfhGAnthWIO33kOSxwRn3vgBFEURAA */ @@ -49,17 +49,17 @@ export const terminalMachine = events: {} as TerminalEvent, services: {} as { getWorkspace: { - data: TypesGen.Workspace - } + data: TypesGen.Workspace; + }; getWorkspaceAgent: { - data: TypesGen.WorkspaceAgent - } + data: TypesGen.WorkspaceAgent; + }; getWebsocketURL: { - data: string - } + data: string; + }; connect: { - data: WebSocket - } + data: WebSocket; + }; }, }, initial: "setup", @@ -180,109 +180,109 @@ export const terminalMachine = services: { getWorkspace: async (context) => { if (!context.workspaceName) { - throw new Error("workspace name not set") + throw new Error("workspace name not set"); } return API.getWorkspaceByOwnerAndName( context.username, context.workspaceName, - ) + ); }, getWorkspaceAgent: async (context) => { if (!context.workspace || !context.workspaceName) { - throw new Error("workspace or workspace name is not set") + throw new Error("workspace or workspace name is not set"); } const agent = context.workspace.latest_build.resources .map((resource) => { if (!resource.agents || resource.agents.length === 0) { - return + return; } if (!context.agentName) { - return resource.agents[0] + return resource.agents[0]; } return resource.agents.find( (agent) => agent.name === context.agentName, - ) + ); }) - .filter((a) => a)[0] + .filter((a) => a)[0]; if (!agent) { - throw new Error("no agent found with id") + throw new Error("no agent found with id"); } - return agent + return agent; }, getWebsocketURL: async (context) => { if (!context.workspaceAgent) { - throw new Error("workspace agent is not set") + throw new Error("workspace agent is not set"); } if (!context.reconnection) { - throw new Error("reconnection ID is not set") + throw new Error("reconnection ID is not set"); } - let baseURL = context.baseURL || "" + let baseURL = context.baseURL || ""; if (!baseURL) { - baseURL = `${location.protocol}//${location.host}` + baseURL = `${location.protocol}//${location.host}`; } const query = new URLSearchParams({ reconnect: context.reconnection, - }) + }); if (context.command) { - query.set("command", context.command) + query.set("command", context.command); } - const url = new URL(baseURL) - url.protocol = url.protocol === "https:" ? "wss:" : "ws:" + const url = new URL(baseURL); + url.protocol = url.protocol === "https:" ? "wss:" : "ws:"; if (!url.pathname.endsWith("/")) { - url.pathname + "/" + url.pathname + "/"; } - url.pathname += `api/v2/workspaceagents/${context.workspaceAgent.id}/pty` - url.search = "?" + query.toString() + url.pathname += `api/v2/workspaceagents/${context.workspaceAgent.id}/pty`; + url.search = "?" + query.toString(); // If the URL is just the primary API, we don't need a signed token to // connect. if (!context.baseURL) { - return url.toString() + return url.toString(); } // Do ticket issuance and set the query parameter. const tokenRes = await API.issueReconnectingPTYSignedToken({ url: url.toString(), agentID: context.workspaceAgent.id, - }) - query.set("coder_signed_app_token_23db1dde", tokenRes.signed_token) - url.search = "?" + query.toString() + }); + query.set("coder_signed_app_token_23db1dde", tokenRes.signed_token); + url.search = "?" + query.toString(); - return url.toString() + return url.toString(); }, connect: (context) => (send) => { return new Promise((resolve, reject) => { if (!context.workspaceAgent) { - return reject("workspace agent is not set") + return reject("workspace agent is not set"); } if (!context.websocketURL) { - return reject("websocket URL is not set") + return reject("websocket URL is not set"); } - const socket = new WebSocket(context.websocketURL) - socket.binaryType = "arraybuffer" + const socket = new WebSocket(context.websocketURL); + socket.binaryType = "arraybuffer"; socket.addEventListener("open", () => { - resolve(socket) - }) + resolve(socket); + }); socket.addEventListener("error", () => { - reject(new Error("socket errored")) - }) + reject(new Error("socket errored")); + }); socket.addEventListener("close", () => { send({ type: "DISCONNECT", - }) - }) + }); + }); socket.addEventListener("message", (event) => { send({ type: "READ", data: event.data, - }) - }) - }) + }); + }); + }); }, }, actions: { @@ -334,16 +334,16 @@ export const terminalMachine = })), sendMessage: (context, event) => { if (!context.websocket) { - throw new Error("websocket doesn't exist") + throw new Error("websocket doesn't exist"); } context.websocket.send( new TextEncoder().encode(JSON.stringify(event.request)), - ) + ); }, disconnect: (context: TerminalContext) => { // Code 1000 is a successful exit! - context.websocket?.close(1000) + context.websocket?.close(1000); }, }, }, - ) + ); diff --git a/site/src/xServices/updateCheck/updateCheckXService.test.ts b/site/src/xServices/updateCheck/updateCheckXService.test.ts index a12668d835..0da6168955 100644 --- a/site/src/xServices/updateCheck/updateCheckXService.test.ts +++ b/site/src/xServices/updateCheck/updateCheckXService.test.ts @@ -1,17 +1,17 @@ -import { waitFor } from "@testing-library/react" -import { MockPermissions, MockUpdateCheck } from "testHelpers/entities" -import { interpret } from "xstate" +import { waitFor } from "@testing-library/react"; +import { MockPermissions, MockUpdateCheck } from "testHelpers/entities"; +import { interpret } from "xstate"; import { clearDismissedVersionOnLocal, getDismissedVersionOnLocal, saveDismissedVersionOnLocal, updateCheckMachine, -} from "./updateCheckXService" +} from "./updateCheckXService"; describe("updateCheckMachine", () => { beforeEach(() => { - clearDismissedVersionOnLocal() - }) + clearDismissedVersionOnLocal(); + }); it("is dismissed when does not have permission to see it", () => { const machine = updateCheckMachine.withContext({ @@ -19,12 +19,12 @@ describe("updateCheckMachine", () => { ...MockPermissions, viewUpdateCheck: false, }, - }) + }); - const updateCheckService = interpret(machine) - updateCheckService.start() - expect(updateCheckService.state.matches("dismissed")).toBeTruthy() - }) + const updateCheckService = interpret(machine); + updateCheckService.start(); + expect(updateCheckService.state.matches("dismissed")).toBeTruthy(); + }); it("is dismissed when it is already using current version", async () => { const machine = updateCheckMachine @@ -42,15 +42,15 @@ describe("updateCheckMachine", () => { current: true, }), }, - }) + }); - const updateCheckService = interpret(machine) - updateCheckService.start() + const updateCheckService = interpret(machine); + updateCheckService.start(); await waitFor(() => { - expect(updateCheckService.state.matches("dismissed")).toBeTruthy() - }) - }) + expect(updateCheckService.state.matches("dismissed")).toBeTruthy(); + }); + }); it("is dismissed when it was dismissed previously", async () => { const machine = updateCheckMachine @@ -68,16 +68,16 @@ describe("updateCheckMachine", () => { current: false, }), }, - }) + }); - saveDismissedVersionOnLocal(MockUpdateCheck.version) - const updateCheckService = interpret(machine) - updateCheckService.start() + saveDismissedVersionOnLocal(MockUpdateCheck.version); + const updateCheckService = interpret(machine); + updateCheckService.start(); await waitFor(() => { - expect(updateCheckService.state.matches("dismissed")).toBeTruthy() - }) - }) + expect(updateCheckService.state.matches("dismissed")).toBeTruthy(); + }); + }); it("shows when has permission and is outdated", async () => { const machine = updateCheckMachine @@ -95,15 +95,15 @@ describe("updateCheckMachine", () => { current: false, }), }, - }) + }); - const updateCheckService = interpret(machine) - updateCheckService.start() + const updateCheckService = interpret(machine); + updateCheckService.start(); await waitFor(() => { - expect(updateCheckService.state.matches("show")).toBeTruthy() - }) - }) + expect(updateCheckService.state.matches("show")).toBeTruthy(); + }); + }); it("it is dismissed when the DISMISS event happens", async () => { const machine = updateCheckMachine @@ -121,18 +121,18 @@ describe("updateCheckMachine", () => { current: false, }), }, - }) + }); - const updateCheckService = interpret(machine) - updateCheckService.start() + const updateCheckService = interpret(machine); + updateCheckService.start(); await waitFor(() => { - expect(updateCheckService.state.matches("show")).toBeTruthy() - }) + expect(updateCheckService.state.matches("show")).toBeTruthy(); + }); - updateCheckService.send("DISMISS") + updateCheckService.send("DISMISS"); await waitFor(() => { - expect(updateCheckService.state.matches("dismissed")).toBeTruthy() - }) - expect(getDismissedVersionOnLocal()).toEqual(MockUpdateCheck.version) - }) -}) + expect(updateCheckService.state.matches("dismissed")).toBeTruthy(); + }); + expect(getDismissedVersionOnLocal()).toEqual(MockUpdateCheck.version); + }); +}); diff --git a/site/src/xServices/updateCheck/updateCheckXService.ts b/site/src/xServices/updateCheck/updateCheckXService.ts index 7ab85abd67..7005d2f042 100644 --- a/site/src/xServices/updateCheck/updateCheckXService.ts +++ b/site/src/xServices/updateCheck/updateCheckXService.ts @@ -1,15 +1,15 @@ -import { assign, createMachine } from "xstate" -import { getUpdateCheck } from "api/api" -import { AuthorizationResponse, UpdateCheckResponse } from "api/typesGenerated" -import { checks, Permissions } from "xServices/auth/authXService" +import { assign, createMachine } from "xstate"; +import { getUpdateCheck } from "api/api"; +import { AuthorizationResponse, UpdateCheckResponse } from "api/typesGenerated"; +import { checks, Permissions } from "xServices/auth/authXService"; export interface UpdateCheckContext { - permissions: Permissions - updateCheck?: UpdateCheckResponse - error?: unknown + permissions: Permissions; + updateCheck?: UpdateCheckResponse; + error?: unknown; } -export type UpdateCheckEvent = { type: "DISMISS" } +export type UpdateCheckEvent = { type: "DISMISS" }; export const updateCheckMachine = createMachine( { @@ -21,11 +21,11 @@ export const updateCheckMachine = createMachine( events: {} as UpdateCheckEvent, services: {} as { checkPermissions: { - data: AuthorizationResponse - } + data: AuthorizationResponse; + }; getUpdateCheck: { - data: UpdateCheckResponse - } + data: UpdateCheckResponse; + }; }, }, initial: "checkingPermissions", @@ -90,33 +90,33 @@ export const updateCheckMachine = createMachine( }), setDismissedVersion: ({ updateCheck }) => { if (!updateCheck) { - throw new Error("Update check is not set") + throw new Error("Update check is not set"); } - saveDismissedVersionOnLocal(updateCheck.version) + saveDismissedVersionOnLocal(updateCheck.version); }, }, guards: { canViewUpdateCheck: ({ permissions }) => permissions[checks.viewUpdateCheck] || false, shouldShowUpdateCheck: (_, { data }) => { - const isNotDismissed = getDismissedVersionOnLocal() !== data.version - const isOutdated = !data.current - return isNotDismissed && isOutdated + const isNotDismissed = getDismissedVersionOnLocal() !== data.version; + const isOutdated = !data.current; + return isNotDismissed && isOutdated; }, }, }, -) +); // Exporting to be used in the tests export const saveDismissedVersionOnLocal = (version: string): void => { - window.localStorage.setItem("dismissedVersion", version) -} + window.localStorage.setItem("dismissedVersion", version); +}; export const getDismissedVersionOnLocal = (): string | undefined => { - return localStorage.getItem("dismissedVersion") ?? undefined -} + return localStorage.getItem("dismissedVersion") ?? undefined; +}; export const clearDismissedVersionOnLocal = (): void => { - localStorage.removeItem("dismissedVersion") -} + localStorage.removeItem("dismissedVersion"); +}; diff --git a/site/src/xServices/userSecuritySettings/userSecuritySettingsXService.ts b/site/src/xServices/userSecuritySettings/userSecuritySettingsXService.ts index adccf76dba..f7ce3b6c41 100644 --- a/site/src/xServices/userSecuritySettings/userSecuritySettingsXService.ts +++ b/site/src/xServices/userSecuritySettings/userSecuritySettingsXService.ts @@ -1,15 +1,15 @@ -import { assign, createMachine } from "xstate" -import * as API from "api/api" -import { UpdateUserPasswordRequest } from "api/typesGenerated" -import { displaySuccess } from "components/GlobalSnackbar/utils" -import { t } from "i18next" +import { assign, createMachine } from "xstate"; +import * as API from "api/api"; +import { UpdateUserPasswordRequest } from "api/typesGenerated"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; +import { t } from "i18next"; interface Context { - userId: string - error?: unknown + userId: string; + error?: unknown; } -type Events = { type: "UPDATE_SECURITY"; data: UpdateUserPasswordRequest } +type Events = { type: "UPDATE_SECURITY"; data: UpdateUserPasswordRequest }; export const userSecuritySettingsMachine = createMachine( { @@ -61,14 +61,14 @@ export const userSecuritySettingsMachine = createMachine( notifyUpdate: () => { displaySuccess( t("securityUpdateSuccessMessage", { ns: "userSettingsPage" }), - ) + ); }, assignError: assign({ error: (_, event) => event.data, }), redirectToHome: () => { - window.location.href = location.origin + window.location.href = location.origin; }, }, }, -) +); diff --git a/site/src/xServices/users/createUserXService.ts b/site/src/xServices/users/createUserXService.ts index ddc0e0db08..2ffb96033d 100644 --- a/site/src/xServices/users/createUserXService.ts +++ b/site/src/xServices/users/createUserXService.ts @@ -1,19 +1,19 @@ -import { assign, createMachine } from "xstate" -import * as API from "../../api/api" -import * as TypesGen from "../../api/typesGenerated" -import { displaySuccess } from "../../components/GlobalSnackbar/utils" +import { assign, createMachine } from "xstate"; +import * as API from "../../api/api"; +import * as TypesGen from "../../api/typesGenerated"; +import { displaySuccess } from "../../components/GlobalSnackbar/utils"; export const Language = { createUserSuccess: "Successfully created user.", -} +}; export interface CreateUserContext { - error?: unknown + error?: unknown; } export type CreateUserEvent = | { type: "CREATE"; user: TypesGen.CreateUserRequest } - | { type: "CANCEL_CREATE_USER" } + | { type: "CANCEL_CREATE_USER" }; export const createUserMachine = createMachine( { @@ -25,8 +25,8 @@ export const createUserMachine = createMachine( events: {} as CreateUserEvent, services: {} as { createUser: { - data: TypesGen.User - } + data: TypesGen.User; + }; }, }, initial: "idle", @@ -65,8 +65,8 @@ export const createUserMachine = createMachine( }), clearError: assign({ error: (_) => undefined }), displayCreateUserSuccess: () => { - displaySuccess(Language.createUserSuccess) + displaySuccess(Language.createUserSuccess); }, }, }, -) +); diff --git a/site/src/xServices/users/searchUserXService.ts b/site/src/xServices/users/searchUserXService.ts index d86e94c5a8..62b23a727b 100644 --- a/site/src/xServices/users/searchUserXService.ts +++ b/site/src/xServices/users/searchUserXService.ts @@ -1,11 +1,11 @@ -import { getUsers } from "api/api" -import { User } from "api/typesGenerated" -import { queryToFilter } from "utils/filters" -import { assign, createMachine } from "xstate" +import { getUsers } from "api/api"; +import { User } from "api/typesGenerated"; +import { queryToFilter } from "utils/filters"; +import { assign, createMachine } from "xstate"; export type AutocompleteEvent = | { type: "SEARCH"; query: string } - | { type: "CLEAR_RESULTS" } + | { type: "CLEAR_RESULTS" }; export const searchUserMachine = createMachine( { @@ -13,13 +13,13 @@ export const searchUserMachine = createMachine( predictableActionArguments: true, schema: { context: {} as { - searchResults?: User[] + searchResults?: User[]; }, events: {} as AutocompleteEvent, services: {} as { searchUsers: { - data: User[] - } + data: User[]; + }; }, }, context: { @@ -62,4 +62,4 @@ export const searchUserMachine = createMachine( }), }, }, -) +); diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index 7b5f2f12b5..1f2b904350 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -1,21 +1,21 @@ -import { getPaginationData } from "components/PaginationWidget/utils" +import { getPaginationData } from "components/PaginationWidget/utils"; import { PaginationContext, paginationMachine, PaginationMachineRef, -} from "xServices/pagination/paginationXService" -import { assign, createMachine, send, spawn } from "xstate" -import * as API from "../../api/api" -import { getErrorMessage } from "../../api/errors" -import * as TypesGen from "../../api/typesGenerated" +} from "xServices/pagination/paginationXService"; +import { assign, createMachine, send, spawn } from "xstate"; +import * as API from "../../api/api"; +import { getErrorMessage } from "../../api/errors"; +import * as TypesGen from "../../api/typesGenerated"; import { displayError, displaySuccess, -} from "../../components/GlobalSnackbar/utils" -import { queryToFilter } from "../../utils/filters" -import { generateRandomString } from "../../utils/random" +} from "../../components/GlobalSnackbar/utils"; +import { queryToFilter } from "../../utils/filters"; +import { generateRandomString } from "../../utils/random"; -const usersPaginationId = "usersPagination" +const usersPaginationId = "usersPagination"; export const Language = { getUsersError: "Error getting users.", @@ -29,60 +29,60 @@ export const Language = { resetUserPasswordError: "Error on resetting the user password.", updateUserRolesSuccess: "Successfully updated the user roles.", updateUserRolesError: "Error on updating the user roles.", -} +}; export interface UsersContext { // Get users - users?: TypesGen.User[] - filter: string - getUsersError?: unknown + users?: TypesGen.User[]; + filter: string; + getUsersError?: unknown; // Suspend user - userIdToSuspend?: TypesGen.User["id"] - usernameToSuspend?: TypesGen.User["username"] - suspendUserError?: unknown + userIdToSuspend?: TypesGen.User["id"]; + usernameToSuspend?: TypesGen.User["username"]; + suspendUserError?: unknown; // Delete user - userIdToDelete?: TypesGen.User["id"] - usernameToDelete?: TypesGen.User["username"] - deleteUserError?: unknown + userIdToDelete?: TypesGen.User["id"]; + usernameToDelete?: TypesGen.User["username"]; + deleteUserError?: unknown; // Activate user - userIdToActivate?: TypesGen.User["id"] - usernameToActivate?: TypesGen.User["username"] - activateUserError?: unknown + userIdToActivate?: TypesGen.User["id"]; + usernameToActivate?: TypesGen.User["username"]; + activateUserError?: unknown; // Reset user password - userIdToResetPassword?: TypesGen.User["id"] - resetUserPasswordError?: unknown - newUserPassword?: string + userIdToResetPassword?: TypesGen.User["id"]; + resetUserPasswordError?: unknown; + newUserPassword?: string; // Update user roles - userIdToUpdateRoles?: TypesGen.User["id"] - updateUserRolesError?: unknown - paginationContext: PaginationContext - paginationRef: PaginationMachineRef - count: number + userIdToUpdateRoles?: TypesGen.User["id"]; + updateUserRolesError?: unknown; + paginationContext: PaginationContext; + paginationRef: PaginationMachineRef; + count: number; } export type UsersEvent = | { type: "GET_USERS"; query?: string } // Suspend events | { - type: "SUSPEND_USER" - userId: TypesGen.User["id"] - username: TypesGen.User["username"] + type: "SUSPEND_USER"; + userId: TypesGen.User["id"]; + username: TypesGen.User["username"]; } | { type: "CONFIRM_USER_SUSPENSION" } | { type: "CANCEL_USER_SUSPENSION" } // Delete events | { - type: "DELETE_USER" - userId: TypesGen.User["id"] - username: TypesGen.User["username"] + type: "DELETE_USER"; + userId: TypesGen.User["id"]; + username: TypesGen.User["username"]; } | { type: "CONFIRM_USER_DELETE" } | { type: "CANCEL_USER_DELETE" } // Activate events | { - type: "ACTIVATE_USER" - userId: TypesGen.User["id"] - username: TypesGen.User["username"] + type: "ACTIVATE_USER"; + userId: TypesGen.User["id"]; + username: TypesGen.User["username"]; } | { type: "CONFIRM_USER_ACTIVATION" } | { type: "CANCEL_USER_ACTIVATION" } @@ -92,14 +92,14 @@ export type UsersEvent = | { type: "CANCEL_USER_PASSWORD_RESET" } // Update roles events | { - type: "UPDATE_USER_ROLES" - userId: TypesGen.User["id"] - roles: TypesGen.Role["name"][] + type: "UPDATE_USER_ROLES"; + userId: TypesGen.User["id"]; + roles: TypesGen.Role["name"][]; } // Filter | { type: "UPDATE_FILTER"; query: string } // Pagination - | { type: "UPDATE_PAGE"; page: string } + | { type: "UPDATE_PAGE"; page: string }; export const usersMachine = /** @xstate-layout N4IgpgJg5mDOIC5QFdZgE6wMoBcCGOYAdLPujgJYB2UACnlNQRQPZUDEA2gAwC6ioAA4tYFSmwEgAHogC0ARgBMigBxEAnCoCsAdhWrdK9QBYdAGhABPRPO7ruRAMzdj3HYuNbFANm5b1AL4BFqgY2PiERDA4lDQAqmiY7BBsxNQAbiwA1sTRCWE8-EggwqLiVJIyCArc8upOxt6K-o7qjloq3ioW1ghKPkQu6m2KOraaw0EhieEEuWAx1FD5SRjoLOhEggA2BABmGwC2UQsrsIWSpWKsFcVVCqqOGoqOKnUqKrW15lY2ivI6IiNRqOHR6bhdXRTEChTC4OZECgQbZgdhYOJYWgAUQAcgARAD6GKxACULsUruVKnIdLoiPJjPItF42p0fI4ejYtMZjE4ASoxi4dI4lCpobDZpEkSj2HisQAZLEAFSxRKwpPJQhE1wkdxp3nqAJM3i0zm8tPknL63gNRB0TQBTNBHRM4pm8KlyNRAEEAMJKgCSADVvSq1Rq+JdtVS9dUdMYnkN1C9uM0Teofr15CpXEQPt95IXC952m6wh60l72CSseqleGSQTaN6sFgAOoAeRJeM1JWjN2pca8RH+bmar3tdUUVve3me8kcIqU3G0NrLcIilZlcVoeNDquJjZJHcVWF7lIHsdk8d5As6ybeHxLxitym4Dh5b1pYx0LhUjnXSUt1RHc9zDZsAHEsXPftdVAe4GT0IEMz0UYtAhV51BnZwHDsTQDQXZNNEAitESrUD9wJAAxAN5RVMlIwpWDbnguR5BNXlEPUbNmk+Ixp1+PpFHULQgUcbxTH8YTCy8EjNyIABjNg9godBDhWLBUEEMAqFENh2F9DscRokkAFkGwJdFMVxLAAyMmCykvVjqg8eQ82UdQJJ5DxaWZGdmUUDRWi8VM+LGbw5IRJSqBUtSNK0nS9I4X1vRxX0FQsqzsRxWz7MYrVHLg6Q5GMbQgWZbiS3tbhWktQT2P+PMBR0DMM0dLQIuCGF3Xk6LYvUxI8TAFFygMoyTPMw8CTlRUVQcnUWOKlzlEGBlHATPRhn0JkZy6XlSuMUZamEox7UiyI+tUgaMCGkabgM1L0vlCyZuVaD8r7QrFvuYwM3pewRUhTxPkzGx7XqYTGTayTtEUc7iEuuLEm9BTKHSZh9MM4yAzMiy-UDENAzyooCoWwdZBeNQfBccT0NqbkOhnHNAXaPxVHsJpTB0eHFOUq6VhRtGMeSx6Mqm-Hg1DOycXmmNnNkdC51KlqlHaYTRgErM2lvZkDUO37-ELHnYASqgICWFZklSREqEyHISFNiAVllpyltaJ5fscXivd0UqsPq0q3MLFrzRzV5MONx2LcSdg1g2LZdhwA41Id2BtLN52PovIqqhNUTOgI2xF3tUEZy5kcXXNQ6Vx8TrpnLeSIGGhZo4wK2qDSW3smIJuRrATOSc+snY1cD83hC1lPG8OqswkiHGghX8Du0Ywed7lv4hjuPNh2fYjiIdfCAHqMvsHc03PfdpPI6xkF26eqS1E5pUNUbQM1GHm8FRih0diZYY5SB3G2dtiBfyFkfRILsc6IGCiOX8vgmTsTGJ5F89UwQOBDtoU0pgPCry6hKUiYCf7ME3m3beCc94pyIb-fukCs7MTPtPIgCDhJuE+O7bwM49BPBFCYXQoxhjsTFPgnqUU+ZIwwPQWAsAADuGwIAkjgAsMa2NcZTWbK2Ts3YCQ1jrFA76bEPiDFaP+G0UkXhaFfLUNQWhsx6xeM4dweD64bjETFfmiQpGyPkYotAOAHppTFuqRsGj2xdkJLo5U+jya2MBDTWkyhaQGk6K+WwolGjuH+KVaeDIeboCUYsUh6AvFyPQBAduncQFEHyX4lYJT5HRNjKCD2RYRIgyaIoPwM5uJuXEsKDqjwVZ5IKX-OpeBpGlPKeQ3eSd941NOJ48Z3iymNOchJJ4bRFyXy6MmLo3TPIaAFJ0xcug3CMh5sgQQEASH-wwCSFgKJYAVOAd3IglzrkQLuQ8uAqy3adAaHYdiSgOivBnogf4tpWhbQBK4ZQuSRENwRO8m5Kx7mPNjugdYO9E7J2OMiz56A0U-PoafWMJo3I+AZLXcc7FLGCThXObiYxRie2DvDdgFEww0TohGQe2cDHVFsAmekvgTnsV+mkq09gHDOHWs0TpU5OpdSoCwJu8BigEPkqQPA5Alj0EYFQYWJ9h7ywXAyPMdgEzV02bSKVC47SfHfLSX65pSwItcZEaIoyZjGrlktBQSh6guAhI0aejJhQzjcKJA0Xt2g8i8Aqnm0owC+tdghO+eYTRoSLo0DM2F4xEGwZ00wbg1bc3dUBXm7iJHoE0mnRKrt+XkwlcY0wrI6jZlqP5YcbRjQeDsLoM6FbSKI2uugW6LcipNqvNk5hph1rTw6HUPZD8cxAhzEdQNp1nHdURRdcRY7BbEL9dO+WpVo2lSjQKEUjJ2hM25KtNoisvaHXNJHetZtW7oFTdAhAbxmF2HjKCIwIl1b+SZGJES75RimIBGvZu3qMA-oFQKO0edxJVW8kYXazI7ToT9nTAUrph3yWoSixIyHBxlQkrYaDnwOqaFBn0N4c5bGsm0MdcYPNR1jImT4gplGZ0SXpKaAdYJQRtHNFYjwThWoLg8JVESwy-GIeKUsyZgnnIMjcuGuVEIl2mCsamJwXsBR2BeOKuuu6PXEHxV+ol6rSZ+qqCKQYyYMNoRBsKVBvR-j-ITHPcS7DXhWc1XMTT-qATGa4jxDoK5kxWjeL0mqIJ5NczwUEIAA */ @@ -111,26 +111,26 @@ export const usersMachine = events: {} as UsersEvent, services: {} as { getUsers: { - data: TypesGen.GetUsersResponse - } + data: TypesGen.GetUsersResponse; + }; createUser: { - data: TypesGen.User - } + data: TypesGen.User; + }; suspendUser: { - data: TypesGen.User - } + data: TypesGen.User; + }; deleteUser: { - data: undefined - } + data: undefined; + }; activateUser: { - data: TypesGen.User - } + data: TypesGen.User; + }; updateUserPassword: { - data: undefined - } + data: undefined; + }; updateUserRoles: { - data: TypesGen.User - } + data: TypesGen.User; + }; }, }, predictableActionArguments: true, @@ -355,53 +355,53 @@ export const usersMachine = // when it is mocked. This happen in the UsersPage tests inside of the // "shows a success message and refresh the page" test case. getUsers: (context) => { - const { offset, limit } = getPaginationData(context.paginationRef) + const { offset, limit } = getPaginationData(context.paginationRef); return API.getUsers({ ...queryToFilter(context.filter), offset, limit, - }) + }); }, suspendUser: (context) => { if (!context.userIdToSuspend) { - throw new Error("userIdToSuspend is undefined") + throw new Error("userIdToSuspend is undefined"); } - return API.suspendUser(context.userIdToSuspend) + return API.suspendUser(context.userIdToSuspend); }, deleteUser: (context) => { if (!context.userIdToDelete) { - throw new Error("userIdToDelete is undefined") + throw new Error("userIdToDelete is undefined"); } - return API.deleteUser(context.userIdToDelete) + return API.deleteUser(context.userIdToDelete); }, activateUser: (context) => { if (!context.userIdToActivate) { - throw new Error("userIdToActivate is undefined") + throw new Error("userIdToActivate is undefined"); } - return API.activateUser(context.userIdToActivate) + return API.activateUser(context.userIdToActivate); }, resetUserPassword: (context) => { if (!context.userIdToResetPassword) { - throw new Error("userIdToResetPassword is undefined") + throw new Error("userIdToResetPassword is undefined"); } if (!context.newUserPassword) { - throw new Error("newUserPassword not generated") + throw new Error("newUserPassword not generated"); } return API.updateUserPassword(context.userIdToResetPassword, { password: context.newUserPassword, old_password: "", - }) + }); }, updateUserRoles: (context, event) => { if (!context.userIdToUpdateRoles) { - throw new Error("userIdToUpdateRoles is undefined") + throw new Error("userIdToUpdateRoles is undefined"); } - return API.updateUserRoles(event.roles, context.userIdToUpdateRoles) + return API.updateUserRoles(event.roles, context.userIdToUpdateRoles); }, }, @@ -484,51 +484,51 @@ export const usersMachine = updateUserRolesError: (_) => undefined, }), displaySuspendSuccess: () => { - displaySuccess(Language.suspendUserSuccess) + displaySuccess(Language.suspendUserSuccess); }, displaySuspendedErrorMessage: (context) => { const message = getErrorMessage( context.suspendUserError, Language.suspendUserError, - ) - displayError(message) + ); + displayError(message); }, displayDeleteSuccess: () => { - displaySuccess(Language.deleteUserSuccess) + displaySuccess(Language.deleteUserSuccess); }, displayDeleteErrorMessage: (context) => { const message = getErrorMessage( context.deleteUserError, Language.deleteUserError, - ) - displayError(message) + ); + displayError(message); }, displayActivateSuccess: () => { - displaySuccess(Language.activateUserSuccess) + displaySuccess(Language.activateUserSuccess); }, displayActivatedErrorMessage: (context) => { const message = getErrorMessage( context.activateUserError, Language.activateUserError, - ) - displayError(message) + ); + displayError(message); }, displayResetPasswordSuccess: () => { - displaySuccess(Language.resetUserPasswordSuccess) + displaySuccess(Language.resetUserPasswordSuccess); }, displayResetPasswordErrorMessage: (context) => { const message = getErrorMessage( context.resetUserPasswordError, Language.resetUserPasswordError, - ) - displayError(message) + ); + displayError(message); }, displayUpdateRolesErrorMessage: (context) => { const message = getErrorMessage( context.updateUserRolesError, Language.updateUserRolesError, - ) - displayError(message) + ); + displayError(message); }, generateRandomPassword: assign({ newUserPassword: (_) => generateRandomString(12), @@ -536,12 +536,12 @@ export const usersMachine = updateUserRolesInTheList: assign({ users: ({ users }, event) => { if (!users) { - return users + return users; } return users.map((u) => { - return u.id === event.data.id ? event.data : u - }) + return u.id === event.data.id ? event.data : u; + }); }, }), assignPaginationRef: assign({ @@ -554,4 +554,4 @@ export const usersMachine = sendResetPage: send({ type: "RESET_PAGE" }, { to: usersPaginationId }), }, }, - ) + ); diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 1706d1af73..7b602809b3 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -1,76 +1,78 @@ -import { getErrorMessage } from "api/errors" -import dayjs from "dayjs" -import { workspaceScheduleBannerMachine } from "xServices/workspaceSchedule/workspaceScheduleBannerXService" -import { assign, createMachine, send } from "xstate" -import * as API from "../../api/api" -import * as Types from "../../api/types" -import * as TypesGen from "../../api/typesGenerated" +import { getErrorMessage } from "api/errors"; +import dayjs from "dayjs"; +import { workspaceScheduleBannerMachine } from "xServices/workspaceSchedule/workspaceScheduleBannerXService"; +import { assign, createMachine, send } from "xstate"; +import * as API from "../../api/api"; +import * as Types from "../../api/types"; +import * as TypesGen from "../../api/typesGenerated"; import { displayError, displaySuccess, -} from "../../components/GlobalSnackbar/utils" +} from "../../components/GlobalSnackbar/utils"; const latestBuild = (builds: TypesGen.WorkspaceBuild[]) => { // Cloning builds to not change the origin object with the sort() return [...builds].sort((a, b) => { - return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() - })[0] -} + return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(); + })[0]; +}; const moreBuildsAvailable = ( context: WorkspaceContext, event: { - type: "REFRESH_TIMELINE" - checkRefresh?: boolean - data?: TypesGen.ServerSentEvent["data"] + type: "REFRESH_TIMELINE"; + checkRefresh?: boolean; + data?: TypesGen.ServerSentEvent["data"]; }, ) => { // No need to refresh the timeline if it is not loaded if (!context.builds) { - return false + return false; } if (!event.checkRefresh) { - return true + return true; } // After we refresh a workspace, we want to check if the latest // build was updated before refreshing the timeline so as to not over fetch the builds - const latestBuildInTimeline = latestBuild(context.builds) - return event.data.latest_build.updated_at !== latestBuildInTimeline.updated_at -} + const latestBuildInTimeline = latestBuild(context.builds); + return ( + event.data.latest_build.updated_at !== latestBuildInTimeline.updated_at + ); +}; -type Permissions = Record, boolean> +type Permissions = Record, boolean>; export interface WorkspaceContext { // Initial data - orgId: string - username: string - workspaceName: string + orgId: string; + username: string; + workspaceName: string; - error?: unknown + error?: unknown; // our server side events instance - eventSource?: EventSource - workspace?: TypesGen.Workspace - template?: TypesGen.Template - permissions?: Permissions - templateVersion?: TypesGen.TemplateVersion - deploymentValues?: TypesGen.DeploymentValues - build?: TypesGen.WorkspaceBuild + eventSource?: EventSource; + workspace?: TypesGen.Workspace; + template?: TypesGen.Template; + permissions?: Permissions; + templateVersion?: TypesGen.TemplateVersion; + deploymentValues?: TypesGen.DeploymentValues; + build?: TypesGen.WorkspaceBuild; // Builds - builds?: TypesGen.WorkspaceBuild[] - getBuildsError?: unknown - missedParameters?: TypesGen.TemplateVersionParameter[] + builds?: TypesGen.WorkspaceBuild[]; + getBuildsError?: unknown; + missedParameters?: TypesGen.TemplateVersionParameter[]; // error creating a new WorkspaceBuild - buildError?: unknown - cancellationMessage?: Types.Message - cancellationError?: unknown + buildError?: unknown; + cancellationMessage?: Types.Message; + cancellationError?: unknown; // debug - createBuildLogLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"] + createBuildLogLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"]; // SSH Config - sshPrefix?: string + sshPrefix?: string; // Change version - templateVersionIdToChange?: TypesGen.TemplateVersion["id"] + templateVersionIdToChange?: TypesGen.TemplateVersion["id"]; } export type WorkspaceEvent = @@ -82,28 +84,28 @@ export type WorkspaceEvent = | { type: "CANCEL_DELETE" } | { type: "UPDATE"; buildParameters?: TypesGen.WorkspaceBuildParameter[] } | { - type: "CHANGE_VERSION" - templateVersionId: TypesGen.TemplateVersion["id"] - buildParameters?: TypesGen.WorkspaceBuildParameter[] + type: "CHANGE_VERSION"; + templateVersionId: TypesGen.TemplateVersion["id"]; + buildParameters?: TypesGen.WorkspaceBuildParameter[]; } | { type: "CANCEL" } | { - type: "REFRESH_TIMELINE" - checkRefresh?: boolean - data?: TypesGen.ServerSentEvent["data"] + type: "REFRESH_TIMELINE"; + checkRefresh?: boolean; + data?: TypesGen.ServerSentEvent["data"]; } | { type: "EVENT_SOURCE_ERROR"; error: unknown } | { type: "INCREASE_DEADLINE"; hours: number } | { type: "DECREASE_DEADLINE"; hours: number } | { type: "RETRY_BUILD" } - | { type: "ACTIVATE" } + | { type: "ACTIVATE" }; export const checks = { readWorkspace: "readWorkspace", updateWorkspace: "updateWorkspace", updateTemplate: "updateTemplate", viewDeploymentValues: "viewDeploymentValues", -} as const +} as const; const permissionsToCheck = ( workspace: TypesGen.Workspace, @@ -139,7 +141,7 @@ const permissionsToCheck = ( }, action: "read", }, - }) as const + }) as const; export const workspaceMachine = createMachine( { @@ -151,38 +153,38 @@ export const workspaceMachine = createMachine( events: {} as WorkspaceEvent, services: {} as { loadInitialWorkspaceData: { - data: Awaited> - } + data: Awaited>; + }; updateWorkspace: { - data: TypesGen.WorkspaceBuild - } + data: TypesGen.WorkspaceBuild; + }; changeWorkspaceVersion: { - data: TypesGen.WorkspaceBuild - } + data: TypesGen.WorkspaceBuild; + }; startWorkspace: { - data: TypesGen.WorkspaceBuild - } + data: TypesGen.WorkspaceBuild; + }; stopWorkspace: { - data: TypesGen.WorkspaceBuild - } + data: TypesGen.WorkspaceBuild; + }; deleteWorkspace: { - data: TypesGen.WorkspaceBuild - } + data: TypesGen.WorkspaceBuild; + }; cancelWorkspace: { - data: Types.Message - } + data: Types.Message; + }; activateWorkspace: { - data: Types.Message - } + data: Types.Message; + }; listening: { - data: TypesGen.ServerSentEvent - } + data: TypesGen.ServerSentEvent; + }; getBuilds: { - data: TypesGen.WorkspaceBuild[] - } + data: TypesGen.WorkspaceBuild[]; + }; getSSHPrefix: { - data: TypesGen.SSHConfigResponse - } + data: TypesGen.SSHConfigResponse; + }; }, }, initial: "loadInitialData", @@ -532,7 +534,7 @@ export const workspaceMachine = createMachine( }), displayCancellationMessage: (context) => { if (context.cancellationMessage) { - displaySuccess(context.cancellationMessage.message) + displaySuccess(context.cancellationMessage.message); } }, assignCancellationError: assign({ @@ -553,7 +555,7 @@ export const workspaceMachine = createMachine( workspace: (_, event) => event.data, }), logWatchWorkspaceWarning: (_, event) => { - console.error("Watch workspace error:", event) + console.error("Watch workspace error:", event); }, // Timeline assignBuilds: assign({ @@ -573,19 +575,19 @@ export const workspaceMachine = createMachine( const message = getErrorMessage( data, "Error getting the deployment ssh configuration.", - ) - displayError(message) + ); + displayError(message); }, displayActivateError: (_, { data }) => { - const message = getErrorMessage(data, "Error activate workspace.") - displayError(message) + const message = getErrorMessage(data, "Error activate workspace."); + displayError(message); }, assignMissedParameters: assign({ missedParameters: (_, { data }) => { if (!(data instanceof API.MissingBuildParameters)) { - throw new Error("data is not a MissingBuildParameters error") + throw new Error("data is not a MissingBuildParameters error"); } - return data.parameters + return data.parameters; }, }), // Debug mode when build fails @@ -603,16 +605,16 @@ export const workspaceMachine = createMachine( guards: { moreBuildsAvailable, isMissingBuildParameterError: (_, { data }) => { - return data instanceof API.MissingBuildParameters + return data instanceof API.MissingBuildParameters; }, lastBuildWasStarting: ({ workspace }) => { - return workspace?.latest_build.transition === "start" + return workspace?.latest_build.transition === "start"; }, lastBuildWasStopping: ({ workspace }) => { - return workspace?.latest_build.transition === "stop" + return workspace?.latest_build.transition === "stop"; }, lastBuildWasDeleting: ({ workspace }) => { - return workspace?.latest_build.transition === "delete" + return workspace?.latest_build.transition === "delete"; }, isChangingVersion: ({ templateVersionIdToChange }) => Boolean(templateVersionIdToChange), @@ -623,28 +625,28 @@ export const workspaceMachine = createMachine( ({ workspace }, { buildParameters }) => async (send) => { if (!workspace) { - throw new Error("Workspace is not set") + throw new Error("Workspace is not set"); } - const build = await API.updateWorkspace(workspace, buildParameters) - send({ type: "REFRESH_TIMELINE" }) - return build + const build = await API.updateWorkspace(workspace, buildParameters); + send({ type: "REFRESH_TIMELINE" }); + return build; }, changeWorkspaceVersion: ({ workspace, templateVersionIdToChange }, { buildParameters }) => async (send) => { if (!workspace) { - throw new Error("Workspace is not set") + throw new Error("Workspace is not set"); } if (!templateVersionIdToChange) { - throw new Error("Template version id to change is not set") + throw new Error("Template version id to change is not set"); } const build = await API.changeWorkspaceVersion( workspace, templateVersionIdToChange, buildParameters, - ) - send({ type: "REFRESH_TIMELINE" }) - return build + ); + send({ type: "REFRESH_TIMELINE" }); + return build; }, startWorkspace: (context, data) => async (send) => { if (context.workspace) { @@ -653,11 +655,11 @@ export const workspaceMachine = createMachine( context.workspace.latest_build.template_version_id, context.createBuildLogLevel, "buildParameters" in data ? data.buildParameters : undefined, - ) - send({ type: "REFRESH_TIMELINE" }) - return startWorkspacePromise + ); + send({ type: "REFRESH_TIMELINE" }); + return startWorkspacePromise; } else { - throw Error("Cannot start workspace without workspace id") + throw Error("Cannot start workspace without workspace id"); } }, stopWorkspace: (context) => async (send) => { @@ -665,11 +667,11 @@ export const workspaceMachine = createMachine( const stopWorkspacePromise = await API.stopWorkspace( context.workspace.id, context.createBuildLogLevel, - ) - send({ type: "REFRESH_TIMELINE" }) - return stopWorkspacePromise + ); + send({ type: "REFRESH_TIMELINE" }); + return stopWorkspacePromise; } else { - throw Error("Cannot stop workspace without workspace id") + throw Error("Cannot stop workspace without workspace id"); } }, deleteWorkspace: async (context) => { @@ -677,22 +679,22 @@ export const workspaceMachine = createMachine( const deleteWorkspacePromise = await API.deleteWorkspace( context.workspace.id, context.createBuildLogLevel, - ) - send({ type: "REFRESH_TIMELINE" }) - return deleteWorkspacePromise + ); + send({ type: "REFRESH_TIMELINE" }); + return deleteWorkspacePromise; } else { - throw Error("Cannot delete workspace without workspace id") + throw Error("Cannot delete workspace without workspace id"); } }, cancelWorkspace: (context) => async (send) => { if (context.workspace) { const cancelWorkspacePromise = await API.cancelWorkspaceBuild( context.workspace.latest_build.id, - ) - send({ type: "REFRESH_TIMELINE" }) - return cancelWorkspacePromise + ); + send({ type: "REFRESH_TIMELINE" }); + return cancelWorkspacePromise; } else { - throw Error("Cannot cancel workspace without build id") + throw Error("Cannot cancel workspace without build id"); } }, activateWorkspace: (context) => async (send) => { @@ -700,44 +702,44 @@ export const workspaceMachine = createMachine( const activateWorkspacePromise = await API.updateWorkspaceDormancy( context.workspace.id, false, - ) - send({ type: "REFRESH_WORKSPACE", data: activateWorkspacePromise }) - return activateWorkspacePromise + ); + send({ type: "REFRESH_WORKSPACE", data: activateWorkspacePromise }); + return activateWorkspacePromise; } else { - throw Error("Cannot activate workspace without workspace id") + throw Error("Cannot activate workspace without workspace id"); } }, listening: (context) => (send) => { if (!context.eventSource) { - send({ type: "EVENT_SOURCE_ERROR", error: "error initializing sse" }) - return + send({ type: "EVENT_SOURCE_ERROR", error: "error initializing sse" }); + return; } context.eventSource.addEventListener("data", (event) => { // refresh our workspace with each SSE - send({ type: "REFRESH_WORKSPACE", data: JSON.parse(event.data) }) + send({ type: "REFRESH_WORKSPACE", data: JSON.parse(event.data) }); // refresh our timeline send({ type: "REFRESH_TIMELINE", checkRefresh: true, data: JSON.parse(event.data), - }) + }); // refresh - }) + }); // handle any error events returned by our sse context.eventSource.addEventListener("error", (event) => { - send({ type: "EVENT_SOURCE_ERROR", error: event }) - }) + send({ type: "EVENT_SOURCE_ERROR", error: event }); + }); // handle any sse implementation exceptions context.eventSource.onerror = () => { - send({ type: "EVENT_SOURCE_ERROR", error: "sse error" }) - } + send({ type: "EVENT_SOURCE_ERROR", error: "sse error" }); + }; return () => { - context.eventSource?.close() - } + context.eventSource?.close(); + }; }, getBuilds: async (context) => { if (context.workspace) { @@ -746,18 +748,18 @@ export const workspaceMachine = createMachine( return await API.getWorkspaceBuilds( context.workspace.id, dayjs().add(-30, "day").toDate(), - ) + ); } else { - throw Error("Cannot get builds without id") + throw Error("Cannot get builds without id"); } }, scheduleBannerMachine: workspaceScheduleBannerMachine, getSSHPrefix: async () => { - return API.getDeploymentSSHConfig() + return API.getDeploymentSSHConfig(); }, }, }, -) +); async function loadInitialWorkspaceData({ orgId, @@ -770,26 +772,26 @@ async function loadInitialWorkspaceData({ { include_deleted: true, }, - ) - const template = await API.getTemplateByName(orgId, workspace.template_name) + ); + const template = await API.getTemplateByName(orgId, workspace.template_name); const [templateVersion, permissions] = await Promise.all([ API.getTemplateVersion(template.active_version_id), API.checkAuthorization({ checks: permissionsToCheck(workspace, template), }), - ]) + ]); const canViewDeploymentValues = Boolean( (permissions as Permissions)?.viewDeploymentValues, - ) + ); const deploymentValues = canViewDeploymentValues ? (await API.getDeploymentValues())?.config - : undefined + : undefined; return { workspace, template, templateVersion, permissions, deploymentValues, - } + }; } diff --git a/site/src/xServices/workspaceAgentLogs/workspaceAgentLogsXService.ts b/site/src/xServices/workspaceAgentLogs/workspaceAgentLogsXService.ts index e1a44ce085..8df5737a91 100644 --- a/site/src/xServices/workspaceAgentLogs/workspaceAgentLogsXService.ts +++ b/site/src/xServices/workspaceAgentLogs/workspaceAgentLogsXService.ts @@ -1,12 +1,12 @@ -import * as API from "api/api" -import { createMachine, assign } from "xstate" -import { Line } from "components/WorkspaceBuildLogs/Logs/Logs" +import * as API from "api/api"; +import { createMachine, assign } from "xstate"; +import { Line } from "components/WorkspaceBuildLogs/Logs/Logs"; // Logs are stored as the Line interface to make rendering // much more efficient. Instead of mapping objects each time, we're // able to just pass the array of logs to the component. export interface LineWithID extends Line { - id: number + id: number; } export const workspaceAgentLogsMachine = createMachine( @@ -16,23 +16,23 @@ export const workspaceAgentLogsMachine = createMachine( schema: { events: {} as | { - type: "ADD_LOGS" - logs: LineWithID[] + type: "ADD_LOGS"; + logs: LineWithID[]; } | { - type: "FETCH_LOGS" + type: "FETCH_LOGS"; } | { - type: "DONE" + type: "DONE"; }, context: {} as { - agentID: string - logs?: LineWithID[] + agentID: string; + logs?: LineWithID[]; }, services: {} as { getLogs: { - data: LineWithID[] - } + data: LineWithID[]; + }; }, }, tsTypes: {} as import("./workspaceAgentLogsXService.typegen").Typegen0, @@ -84,9 +84,9 @@ export const workspaceAgentLogsMachine = createMachine( })), ), streamLogs: (ctx) => async (callback) => { - let after = 0 + let after = 0; if (ctx.logs && ctx.logs.length > 0) { - after = ctx.logs[ctx.logs.length - 1].id + after = ctx.logs[ctx.logs.length - 1].id; } const socket = API.watchWorkspaceAgentLogs(ctx.agentID, { @@ -100,19 +100,19 @@ export const workspaceAgentLogsMachine = createMachine( output: log.output, time: log.created_at, })), - }) + }); }, onDone: () => { - callback({ type: "DONE" }) + callback({ type: "DONE" }); }, onError: (error) => { - console.error(error) + console.error(error); }, - }) + }); return () => { - socket.close() - } + socket.close(); + }; }, }, actions: { @@ -121,10 +121,10 @@ export const workspaceAgentLogsMachine = createMachine( }), addLogs: assign({ logs: (context, event) => { - const previousLogs = context.logs ?? [] - return [...previousLogs, ...event.logs] + const previousLogs = context.logs ?? []; + return [...previousLogs, ...event.logs]; }, }), }, }, -) +); diff --git a/site/src/xServices/workspaceBuild/workspaceBuildXService.ts b/site/src/xServices/workspaceBuild/workspaceBuildXService.ts index 1156a7b06c..65e750d426 100644 --- a/site/src/xServices/workspaceBuild/workspaceBuildXService.ts +++ b/site/src/xServices/workspaceBuild/workspaceBuildXService.ts @@ -1,34 +1,34 @@ -import { assign, createMachine } from "xstate" -import * as API from "../../api/api" -import { ProvisionerJobLog, WorkspaceBuild } from "../../api/typesGenerated" +import { assign, createMachine } from "xstate"; +import * as API from "../../api/api"; +import { ProvisionerJobLog, WorkspaceBuild } from "../../api/typesGenerated"; type LogsContext = { // Build - username: string - workspaceName: string - buildNumber: number - buildId: string + username: string; + workspaceName: string; + buildNumber: number; + buildId: string; // Used to reference logs before + after. - timeCursor: Date - build?: WorkspaceBuild - getBuildError?: unknown + timeCursor: Date; + build?: WorkspaceBuild; + getBuildError?: unknown; // Logs - logs?: ProvisionerJobLog[] -} + logs?: ProvisionerJobLog[]; +}; type LogsEvent = | { - type: "ADD_LOG" - log: ProvisionerJobLog + type: "ADD_LOG"; + log: ProvisionerJobLog; } | { - type: "BUILD_DONE" + type: "BUILD_DONE"; } | { - type: "RESET" - buildNumber: number - timeCursor: Date - } + type: "RESET"; + buildNumber: number; + timeCursor: Date; + }; export const workspaceBuildMachine = createMachine( { @@ -40,11 +40,11 @@ export const workspaceBuildMachine = createMachine( events: {} as LogsEvent, services: {} as { getWorkspaceBuild: { - data: WorkspaceBuild - } + data: WorkspaceBuild; + }; getLogs: { - data: ProvisionerJobLog[] - } + data: ProvisionerJobLog[]; + }; }, }, initial: "gettingBuild", @@ -132,8 +132,8 @@ export const workspaceBuildMachine = createMachine( }), addLog: assign({ logs: (context, event) => { - const previousLogs = context.logs ?? [] - return [...previousLogs, event.log] + const previousLogs = context.logs ?? []; + return [...previousLogs, event.log]; }, }), }, @@ -148,26 +148,26 @@ export const workspaceBuildMachine = createMachine( API.getWorkspaceBuildLogs(ctx.buildId, ctx.timeCursor), streamWorkspaceBuildLogs: (ctx) => async (callback) => { if (!ctx.logs) { - throw new Error("logs must be set") + throw new Error("logs must be set"); } const after = - ctx.logs.length > 0 ? ctx.logs[ctx.logs.length - 1].id : undefined + ctx.logs.length > 0 ? ctx.logs[ctx.logs.length - 1].id : undefined; const socket = API.watchBuildLogsByBuildId(ctx.buildId, { after, onMessage: (log) => { - callback({ type: "ADD_LOG", log }) + callback({ type: "ADD_LOG", log }); }, onDone: () => { - callback({ type: "BUILD_DONE" }) + callback({ type: "BUILD_DONE" }); }, onError: (err) => { - console.error(err) + console.error(err); }, - }) + }); return () => { - socket.close() - } + socket.close(); + }; }, }, }, -) +); diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts index 61a684621f..9bd25aa5e6 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts @@ -2,42 +2,42 @@ * @fileoverview workspaceScheduleBanner is an xstate machine backing a form, * presented as an Alert/banner, for reactively updating a workspace schedule. */ -import { getErrorMessage } from "api/errors" -import { Workspace } from "api/typesGenerated" -import dayjs from "dayjs" -import minMax from "dayjs/plugin/minMax" -import { getDeadline, getMaxDeadline, getMinDeadline } from "utils/schedule" -import { assign, createMachine } from "xstate" -import * as API from "../../api/api" +import { getErrorMessage } from "api/errors"; +import { Workspace } from "api/typesGenerated"; +import dayjs from "dayjs"; +import minMax from "dayjs/plugin/minMax"; +import { getDeadline, getMaxDeadline, getMinDeadline } from "utils/schedule"; +import { assign, createMachine } from "xstate"; +import * as API from "../../api/api"; import { displayError, displaySuccess, -} from "../../components/GlobalSnackbar/utils" +} from "../../components/GlobalSnackbar/utils"; -dayjs.extend(minMax) +dayjs.extend(minMax); export const Language = { errorExtension: "Failed to update workspace shutdown time.", successExtension: "Updated workspace shutdown time.", -} +}; export interface WorkspaceScheduleBannerContext { - workspace: Workspace + workspace: Workspace; } export type WorkspaceScheduleBannerEvent = | { - type: "INCREASE_DEADLINE" - hours: number + type: "INCREASE_DEADLINE"; + hours: number; } | { - type: "DECREASE_DEADLINE" - hours: number + type: "DECREASE_DEADLINE"; + hours: number; } | { - type: "REFRESH_WORKSPACE" - workspace: Workspace - } + type: "REFRESH_WORKSPACE"; + workspace: Workspace; + }; export const workspaceScheduleBannerMachine = createMachine( { @@ -95,10 +95,10 @@ export const workspaceScheduleBannerMachine = createMachine( actions: { // This error does not have a detail, so using the snackbar is okay displayFailureMessage: (_, event) => { - displayError(getErrorMessage(event.data, Language.errorExtension)) + displayError(getErrorMessage(event.data, Language.errorExtension)); }, displaySuccessMessage: () => { - displaySuccess(Language.successExtension) + displaySuccess(Language.successExtension); }, assignWorkspace: assign((_, event) => ({ workspace: event.workspace, @@ -108,29 +108,29 @@ export const workspaceScheduleBannerMachine = createMachine( services: { increaseDeadline: async (context, event) => { if (!context.workspace.latest_build.deadline) { - throw Error("Deadline is undefined.") + throw Error("Deadline is undefined."); } const proposedDeadline = getDeadline(context.workspace).add( event.hours, "hours", - ) + ); const newDeadline = dayjs.min( proposedDeadline, getMaxDeadline(context.workspace), - ) - await API.putWorkspaceExtension(context.workspace.id, newDeadline) + ); + await API.putWorkspaceExtension(context.workspace.id, newDeadline); }, decreaseDeadline: async (context, event) => { if (!context.workspace.latest_build.deadline) { - throw Error("Deadline is undefined.") + throw Error("Deadline is undefined."); } const proposedDeadline = getDeadline(context.workspace).subtract( event.hours, "hours", - ) - const newDeadline = dayjs.max(proposedDeadline, getMinDeadline()) - await API.putWorkspaceExtension(context.workspace.id, newDeadline) + ); + const newDeadline = dayjs.max(proposedDeadline, getMinDeadline()); + await API.putWorkspaceExtension(context.workspace.id, newDeadline); }, }, }, -) +); diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts index 8b1795c01c..6056aeecde 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts @@ -2,32 +2,32 @@ * @fileoverview workspaceSchedule is an xstate machine backing a form to CRUD * an individual workspace's schedule. */ -import { assign, createMachine } from "xstate" -import * as API from "../../api/api" -import * as TypesGen from "../../api/typesGenerated" +import { assign, createMachine } from "xstate"; +import * as API from "../../api/api"; +import * as TypesGen from "../../api/typesGenerated"; -type Permissions = Record, boolean> +type Permissions = Record, boolean>; export interface WorkspaceScheduleContext { - getWorkspaceError?: unknown + getWorkspaceError?: unknown; /** * Each workspace has their own schedule (start and ttl). For this reason, we * re-fetch the workspace to ensure we're up-to-date. As a result, this * machine is partially influenced by workspaceXService. */ - workspace: TypesGen.Workspace - template?: TypesGen.Template - getTemplateError?: unknown - permissions?: Permissions - checkPermissionsError?: unknown - submitScheduleError?: unknown - autostopChanged?: boolean - shouldRestartWorkspace?: boolean + workspace: TypesGen.Workspace; + template?: TypesGen.Template; + getTemplateError?: unknown; + permissions?: Permissions; + checkPermissionsError?: unknown; + submitScheduleError?: unknown; + autostopChanged?: boolean; + shouldRestartWorkspace?: boolean; } export const checks = { updateWorkspace: "updateWorkspace", -} as const +} as const; const permissionsToCheck = (workspace: TypesGen.Workspace) => ({ @@ -39,18 +39,18 @@ const permissionsToCheck = (workspace: TypesGen.Workspace) => }, action: "update", }, - }) as const + }) as const; export type WorkspaceScheduleEvent = | { - type: "SUBMIT_SCHEDULE" - autostart: TypesGen.UpdateWorkspaceAutostartRequest - autostartChanged: boolean - ttl: TypesGen.UpdateWorkspaceTTLRequest - autostopChanged: boolean + type: "SUBMIT_SCHEDULE"; + autostart: TypesGen.UpdateWorkspaceAutostartRequest; + autostartChanged: boolean; + ttl: TypesGen.UpdateWorkspaceTTLRequest; + autostopChanged: boolean; } | { type: "RESTART_WORKSPACE" } - | { type: "APPLY_LATER" } + | { type: "APPLY_LATER" }; export const workspaceSchedule = /** @xstate-layout N4IgpgJg5mDOIC5QHcD2AnA1rADgQwGMwBlAgC0gFcAbEgFzzrAGIBxAUQBUB9AdQHkASgGliABQCCAYXYBtAAwBdRKBypYASzobUAOxUgAHogCMAJgB0AVgAcAThsAWeXZMA2O2YDsdq1YA0IACeiGbyFl428o4AzFYmMT4xbl5mAL5pgWhYuIQk5FS0xAxMFjB02rpQvBjY+ETMEHpgFhq6AG6omC3lNTn1YArKSCBqmtp6BsYI7tbybmZ+9mYO8u6OgSEIMY6OFo5eC34mdqkLNhlZtblEpBQQNPSMPWAVbdXXA8xg6OgYFjhqIwAGYYAC2ZVefTqeSGBjGWh0+hG02SNmsXhMVhWdkcbkcNm8Xk2oRcFgW7hi8h8djcVkuIGyMNuBQeRRKLzeVTEPzBGlgmj0sEazVaHS6LQKBEwPPQfIFSNgcJGCImyNA0wAtOEYrjsa4vD5bMkEiSEG44uTFosCa41lY3AymTd8vdHsVnpCuVBZfLBbphT8-ugAUC6KC5RYpTLefz-UqlPD1IjJijEMkrBE3G4bO5cfN5GYzGadnsDgt5rmnFZHL4nZ88ndCk9Sjh0HAwLo6AAxcHMYgAVQAQgBZACSPGIUgAEuwACIDgAyckTKuTaqmiEzqTMdOzJixJi82OLwUQNic5MxrjxusS5nr-UbrPdHIssEoACM+d6m2yWE0ugtG0nTdO+X4-n+jzKqo65IpuMx7CYF7ZjYMSJLihpeBsZ4zO4Jj7HahomPINaEo4j7Mq6zYeqUH7flolRQFBtAikBYqgS09GQS+tCyCYwyweM8Fpggjg1hYu5UrqZiOCsjjmGaB5uARtYkfq7jUjYjqZIyDYsm67KetxjHvCxLBBv8gIguC4EMXQ5kwaMcGphq6YpBEB4JFYuqxN4Jhmq4bj7CkiReNEpykSYlEuuZtFcWQqDIO8ghwAw6B0HOGh4NQqBQMwgjsMQnASIIPACCI4jSCugnOcJrlGOeMT7LYyExMhClybESmJMFrU2OFDo+OYOlXE+Bk0W+sCJclVSpbA6WZdluX5RIYhiIuACa3CLhInDsIITmqiJbnbG4Oq1gpaxOHmMRKWY2kWPYObnehOwxDFAxxW+lnoGwXB8EIoiSDIR0ueqjXbFYdgWKcVZmCN5x+EpBJPa4ngmLE8jtV4GS6boqAQHABjOl9vEtmASb1RDWqo75GmGr4aFuGampYpYTgI-IUS+Nzhy47ppPPoZFOtBAtBUymNOmOEUQ1nidJWMe1IPazdIWDsXMHseXgxFEAtjVR32euUTHQi6ksbqJXgWPI8y1rJix23ESso3sdvRLrKxmDEjvpIL+nUf+8VekxvpxoqlsnZD6K5nYLhHnJNjYmsZoEjbuu+ASJqdbrn3C5Nnpth2Xa9nKUcNdMuxPdjaFOFS2ErGakTNQjDu3qkJF2PnE3B1NEGmVU5kV9LMzhNDSvzBa1IqZ4OFbLSMMWj7OZoXrhIfQH41B6+xkzSlaV4BlWU5XlI8ISReyYbJ5h2A4cnI7hI2ZtmcSRNpOZxP7huxeTIe-efUSthMyyU8PHO+R4LRKQks4HMmMDhfzWNFLeRs-5vkApTNc1MEK6hAe4c6pE6QDTCCzJ+tZY4nD1qkdCqQBYZCAA */ @@ -64,8 +64,8 @@ export const workspaceSchedule = events: {} as WorkspaceScheduleEvent, services: {} as { getTemplate: { - data: TypesGen.Template - } + data: TypesGen.Template; + }; }, }, initial: "gettingPermissions", @@ -188,7 +188,7 @@ export const workspaceSchedule = return API.startWorkspace( context.workspace.id, context.template.active_version_id, - ) + ); } }, }, @@ -196,38 +196,38 @@ export const workspaceSchedule = services: { getTemplate: async (context) => { if (context.workspace) { - return await API.getTemplate(context.workspace.template_id) + return await API.getTemplate(context.workspace.template_id); } else { - throw Error("Can't fetch template without workspace.") + throw Error("Can't fetch template without workspace."); } }, checkPermissions: async (context) => { if (context.workspace) { return await API.checkAuthorization({ checks: permissionsToCheck(context.workspace), - }) + }); } else { throw Error( "Cannot check permissions without both workspace and user id", - ) + ); } }, submitSchedule: async (context, event) => { if (!context.workspace?.id) { // This state is theoretically impossible, but helps TS - throw new Error("Failed to load workspace.") + throw new Error("Failed to load workspace."); } if (event.autostartChanged) { await API.putWorkspaceAutostart( context.workspace.id, event.autostart, - ) + ); } if (event.autostopChanged) { - await API.putWorkspaceAutostop(context.workspace.id, event.ttl) + await API.putWorkspaceAutostop(context.workspace.id, event.ttl); } }, }, }, - ) + ); diff --git a/site/vite.config.ts b/site/vite.config.ts index 8ba6321d17..b0fa3e8be5 100644 --- a/site/vite.config.ts +++ b/site/vite.config.ts @@ -1,22 +1,22 @@ -import react from "@vitejs/plugin-react" -import path from "path" -import { defineConfig, PluginOption } from "vite" -import { visualizer } from "rollup-plugin-visualizer" -import checker from "vite-plugin-checker" +import react from "@vitejs/plugin-react"; +import path from "path"; +import { defineConfig, PluginOption } from "vite"; +import { visualizer } from "rollup-plugin-visualizer"; +import checker from "vite-plugin-checker"; const plugins: PluginOption[] = [ react(), checker({ typescript: true, }), -] +]; if (process.env.STATS !== undefined) { plugins.push( visualizer({ filename: "./stats/index.html", }), - ) + ); } export default defineConfig({ @@ -53,13 +53,13 @@ export default defineConfig({ proxyReq.setHeader( "origin", process.env.CODER_HOST || "http://localhost:3000", - ) + ); } socket.on("error", (error) => { - console.error(error) - }) - }) + console.error(error); + }); + }); }, }, "/swagger": { @@ -82,4 +82,4 @@ export default defineConfig({ xServices: path.resolve(__dirname, "./src/xServices"), }, }, -}) +});