From 31d2e0c1d6b708a1e7b9171aa30b56cc5e7c521a Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 19 Jan 2024 20:31:55 +0000 Subject: [PATCH 1/2] chore: remove `useLocalStorage` hook --- site/src/components/Abbr/Abbr.stories.tsx | 25 +++++++------ .../Dashboard/useUpdateCheck.test.tsx | 4 +- .../VSCodeDesktopButton.tsx | 28 ++++++-------- site/src/contexts/ProxyContext.test.tsx | 4 +- site/src/contexts/ProxyContext.tsx | 4 +- site/src/hooks/useLocalStorage.ts | 19 ---------- .../TemplateSchedulePage.tsx | 9 ++--- .../TemplateVersionEditor.tsx | 6 +-- .../WorkspacePage/WorkspacePage.test.tsx | 2 +- site/src/testHelpers/localStorage.ts | 37 +++++++++++++++++++ site/src/testHelpers/localstorage.ts | 22 ----------- 11 files changed, 76 insertions(+), 84 deletions(-) delete mode 100644 site/src/hooks/useLocalStorage.ts create mode 100644 site/src/testHelpers/localStorage.ts delete mode 100644 site/src/testHelpers/localstorage.ts diff --git a/site/src/components/Abbr/Abbr.stories.tsx b/site/src/components/Abbr/Abbr.stories.tsx index b47546dcb05ce..1d746c7599388 100644 --- a/site/src/components/Abbr/Abbr.stories.tsx +++ b/site/src/components/Abbr/Abbr.stories.tsx @@ -1,12 +1,6 @@ -import { type PropsWithChildren } from "react"; import type { Meta, StoryObj } from "@storybook/react"; import { Abbr } from "./Abbr"; -// Just here to make the abbreviated part more obvious in the component library -const Underline = ({ children }: PropsWithChildren) => ( - {children} -); - const meta: Meta = { title: "components/Abbr", component: Abbr, @@ -34,9 +28,9 @@ export const InlinedShorthand: Story = {

The physical pain of getting bonked on the head with a cartoon mallet lasts precisely 593{" "} - + - + . The emotional turmoil and complete embarrassment lasts forever.

), @@ -51,9 +45,9 @@ export const Acronym: Story = { }, decorators: [ (Story) => ( - + - + ), ], }; @@ -66,9 +60,16 @@ export const Initialism: Story = { }, decorators: [ (Story) => ( - + - + ), ], }; + +const styles = { + // Just here to make the abbreviated part more obvious in the component library + underlined: { + textDecoration: "underline dotted", + }, +}; diff --git a/site/src/components/Dashboard/useUpdateCheck.test.tsx b/site/src/components/Dashboard/useUpdateCheck.test.tsx index 6f5f8e5431b29..b689f39d4252e 100644 --- a/site/src/components/Dashboard/useUpdateCheck.test.tsx +++ b/site/src/components/Dashboard/useUpdateCheck.test.tsx @@ -14,7 +14,7 @@ const createWrapper = (): FC => { }; beforeEach(() => { - window.localStorage.clear(); + localStorage.clear(); }); it("is dismissed when does not have permission to see it", () => { @@ -57,7 +57,7 @@ it("is dismissed when it was dismissed previously", async () => { ); }), ); - window.localStorage.setItem("dismissedVersion", MockUpdateCheck.version); + localStorage.setItem("dismissedVersion", MockUpdateCheck.version); const { result } = renderHook(() => useUpdateCheck(true), { wrapper: createWrapper(), }); diff --git a/site/src/components/Resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx b/site/src/components/Resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx index 3e96b67f2e144..ceb03f016e459 100644 --- a/site/src/components/Resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx +++ b/site/src/components/Resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx @@ -1,14 +1,13 @@ -import { FC, PropsWithChildren, useState, useRef } from "react"; -import { getApiKey } from "api/api"; -import { VSCodeIcon } from "components/Icons/VSCodeIcon"; -import { VSCodeInsidersIcon } from "components/Icons/VSCodeInsidersIcon"; -import { AgentButton } from "components/Resources/AgentButton"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import ButtonGroup from "@mui/material/ButtonGroup"; -import { useLocalStorage } from "hooks"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; +import { type FC, useState, useRef } from "react"; +import { getApiKey } from "api/api"; import { DisplayApp } from "api/typesGenerated"; +import { VSCodeIcon } from "components/Icons/VSCodeIcon"; +import { VSCodeInsidersIcon } from "components/Icons/VSCodeInsidersIcon"; +import { AgentButton } from "components/Resources/AgentButton"; import { DisplayAppNameMap } from "../AppLink/AppLink"; export interface VSCodeDesktopButtonProps { @@ -23,12 +22,9 @@ type VSCodeVariant = "vscode" | "vscode-insiders"; const VARIANT_KEY = "vscode-variant"; -export const VSCodeDesktopButton: FC< - PropsWithChildren -> = (props) => { +export const VSCodeDesktopButton: FC = (props) => { const [isVariantMenuOpen, setIsVariantMenuOpen] = useState(false); - const localStorage = useLocalStorage(); - const previousVariant = localStorage.getLocal(VARIANT_KEY); + const previousVariant = localStorage.getItem(VARIANT_KEY); const [variant, setVariant] = useState(() => { if (!previousVariant) { return "vscode"; @@ -38,7 +34,7 @@ export const VSCodeDesktopButton: FC< const menuAnchorRef = useRef(null); const selectVariant = (variant: VSCodeVariant) => { - localStorage.saveLocal(VARIANT_KEY, variant); + localStorage.setItem(VARIANT_KEY, variant); setVariant(variant); setIsVariantMenuOpen(false); }; @@ -109,12 +105,12 @@ export const VSCodeDesktopButton: FC< ); }; -const VSCodeButton = ({ +const VSCodeButton: FC = ({ userName, workspaceName, agentName, folderPath, -}: VSCodeDesktopButtonProps) => { +}) => { const [loading, setLoading] = useState(false); return ( @@ -153,12 +149,12 @@ const VSCodeButton = ({ ); }; -const VSCodeInsidersButton = ({ +const VSCodeInsidersButton: FC = ({ userName, workspaceName, agentName, folderPath, -}: VSCodeDesktopButtonProps) => { +}) => { const [loading, setLoading] = useState(false); return ( diff --git a/site/src/contexts/ProxyContext.test.tsx b/site/src/contexts/ProxyContext.test.tsx index d2642418bf17a..5b66c89b3fe61 100644 --- a/site/src/contexts/ProxyContext.test.tsx +++ b/site/src/contexts/ProxyContext.test.tsx @@ -19,7 +19,7 @@ import { screen } from "@testing-library/react"; import { server } from "testHelpers/server"; import { rest } from "msw"; import { Region } from "api/typesGenerated"; -import "testHelpers/localstorage"; +import "testHelpers/localStorage"; import userEvent from "@testing-library/user-event"; // Mock useProxyLatency to use a hard-coded latency. 'jest.mock' must be called @@ -187,7 +187,7 @@ interface ProxyContextSelectionTest { describe("ProxyContextSelection", () => { beforeEach(() => { - window.localStorage.clear(); + localStorage.clear(); }); // A way to simulate a user clearing the proxy selection. diff --git a/site/src/contexts/ProxyContext.tsx b/site/src/contexts/ProxyContext.tsx index c661a1bc7c5db..d1b6b6057f4cd 100644 --- a/site/src/contexts/ProxyContext.tsx +++ b/site/src/contexts/ProxyContext.tsx @@ -310,11 +310,11 @@ const computeUsableURLS = (proxy?: Region): PreferredProxy => { // Local storage functions export const clearUserSelectedProxy = (): void => { - window.localStorage.removeItem("user-selected-proxy"); + localStorage.removeItem("user-selected-proxy"); }; export const saveUserSelectedProxy = (saved: Region): void => { - window.localStorage.setItem("user-selected-proxy", JSON.stringify(saved)); + localStorage.setItem("user-selected-proxy", JSON.stringify(saved)); }; export const loadUserSelectedProxy = (): Region | undefined => { diff --git a/site/src/hooks/useLocalStorage.ts b/site/src/hooks/useLocalStorage.ts deleted file mode 100644 index 10ae2889907ba..0000000000000 --- a/site/src/hooks/useLocalStorage.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const useLocalStorage = () => { - return { - saveLocal, - getLocal, - clearLocal, - }; -}; - -const saveLocal = (itemKey: string, itemValue: string): void => { - window.localStorage.setItem(itemKey, itemValue); -}; - -const getLocal = (itemKey: string): string | undefined => { - return localStorage.getItem(itemKey) ?? undefined; -}; - -const clearLocal = (itemKey: string): void => { - localStorage.removeItem(itemKey); -}; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx index 65a2b885719ee..ba76f413bda6b 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx @@ -3,13 +3,13 @@ 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 { type FC } from "react"; import { Helmet } from "react-helmet-async"; import { useNavigate, useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; import { useTemplateSettings } from "../TemplateSettingsLayout"; import { TemplateSchedulePageView } from "./TemplateSchedulePageView"; -import { useLocalStorage, useOrganizationId } from "hooks"; +import { useOrganizationId } from "hooks"; import { templateByNameKey } from "api/queries/templates"; const TemplateSchedulePage: FC = () => { @@ -21,7 +21,6 @@ const TemplateSchedulePage: FC = () => { const { entitlements } = useDashboard(); const allowAdvancedScheduling = entitlements.features["advanced_template_scheduling"].enabled; - const { clearLocal } = useLocalStorage(); const { mutate: updateTemplate, @@ -36,8 +35,8 @@ const TemplateSchedulePage: FC = () => { ); displaySuccess("Template updated successfully"); // clear browser storage of workspaces impending deletion - clearLocal("dismissedWorkspaceList"); // workspaces page - clearLocal("dismissedWorkspace"); // workspace page + localStorage.removeItem("dismissedWorkspaceList"); // workspaces page + localStorage.removeItem("dismissedWorkspace"); // workspace page }, }, ); diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx index db07c575db99e..995c4267ca586 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx @@ -64,13 +64,13 @@ export interface TemplateVersionEditorProps { defaultFileTree: FileTree; buildLogs?: ProvisionerJobLog[]; resources?: WorkspaceResource[]; - disablePreview: boolean; - disableUpdate: boolean; + disablePreview?: boolean; + disableUpdate?: boolean; onPreview: (files: FileTree) => void; onPublish: () => void; onConfirmPublish: (data: PublishVersionData) => void; onCancelPublish: () => void; - publishingError: unknown; + publishingError?: unknown; publishedVersion?: TemplateVersion; onCreateWorkspace: () => void; isAskingPublishParameters: boolean; diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 3dd0e8b478d4e..f613eaf028575 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -317,7 +317,7 @@ describe("WorkspacePage", () => { }); it("restart the workspace with one time parameters when having the confirmation dialog", async () => { - window.localStorage.removeItem(`${MockUser.id}_ignoredWarnings`); + localStorage.removeItem(`${MockUser.id}_ignoredWarnings`); jest.spyOn(api, "getWorkspaceParameters").mockResolvedValue({ templateVersionRichParameters: [ { diff --git a/site/src/testHelpers/localStorage.ts b/site/src/testHelpers/localStorage.ts new file mode 100644 index 0000000000000..428ae66b6dfce --- /dev/null +++ b/site/src/testHelpers/localStorage.ts @@ -0,0 +1,37 @@ +export const localStorageMock = (): Storage => { + const store = new Map(); + + return { + getItem: (key) => { + return store.get(key) ?? null; + }, + setItem: (key: string, value: string) => { + store.set(key, value); + }, + clear: () => { + store.clear(); + }, + removeItem: (key: string) => { + store.delete(key); + }, + + get length() { + return store.size; + }, + + key: (index) => { + const values = store.values(); + let value: IteratorResult = values.next(); + for (let i = 1; i < index && !value.done; i++) { + value = values.next(); + } + + return value.value ?? null; + }, + }; +}; + +Object.defineProperty(globalThis, "localStorage", { + value: localStorageMock(), + writable: false, +}); diff --git a/site/src/testHelpers/localstorage.ts b/site/src/testHelpers/localstorage.ts deleted file mode 100644 index bff92d8f9f0b4..0000000000000 --- a/site/src/testHelpers/localstorage.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const localStorageMock = () => { - const store = {} as Record; - - return { - getItem: (key: string): string => { - return store[key]; - }, - setItem: (key: string, value: string) => { - store[key] = value; - }, - clear: () => { - Object.keys(store).forEach((key) => { - delete store[key]; - }); - }, - removeItem: (key: string) => { - delete store[key]; - }, - }; -}; - -Object.defineProperty(window, "localStorage", { value: localStorageMock() }); From f30f7e6f0c7b460e7e978b07b87eaca1fa5ae92e Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 19 Jan 2024 22:52:40 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coderd/externalauth/externalauth.go | 2 +- coderd/workspaceagents_test.go | 2 +- enterprise/coderd/proxyhealth/proxyhealth.go | 8 ++++---- enterprise/wsproxy/wsproxy.go | 2 +- helm/provisioner/charts/libcoder-0.1.0.tgz | Bin 3010 -> 0 bytes site/src/hooks/index.ts | 1 - 6 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 helm/provisioner/charts/libcoder-0.1.0.tgz diff --git a/coderd/externalauth/externalauth.go b/coderd/externalauth/externalauth.go index 282c0d8a722b7..72d02b5139076 100644 --- a/coderd/externalauth/externalauth.go +++ b/coderd/externalauth/externalauth.go @@ -327,7 +327,7 @@ func (c *DeviceAuth) AuthorizeDevice(ctx context.Context) (*codersdk.ExternalAut case http.StatusTooManyRequests: return nil, xerrors.New("rate limit hit, unable to authorize device. please try again later") default: - return nil, fmt.Errorf("status_code=%d: %w", resp.StatusCode, err) + return nil, xerrors.Errorf("status_code=%d: %w", resp.StatusCode, err) } } if r.ErrorDescription != "" { diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 77fb6b1976ab9..0d620c991e6dd 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -1626,7 +1626,7 @@ func TestWorkspaceAgentExternalAuthListen(t *testing.T) { cancel() // We expect only 1 // In a failed test, you will likely see 9, as the last one - // gets cancelled. + // gets canceled. require.Equal(t, 1, validateCalls, "validate calls duplicated on same token") }) } diff --git a/enterprise/coderd/proxyhealth/proxyhealth.go b/enterprise/coderd/proxyhealth/proxyhealth.go index 4d77f02c5156e..33a5da7d269a8 100644 --- a/enterprise/coderd/proxyhealth/proxyhealth.go +++ b/enterprise/coderd/proxyhealth/proxyhealth.go @@ -276,9 +276,9 @@ func (p *ProxyHealth) runOnce(ctx context.Context, now time.Time) (map[uuid.UUID case err == nil && resp.StatusCode == http.StatusOK: err := json.NewDecoder(resp.Body).Decode(&status.Report) if err != nil { - isCoderErr := fmt.Errorf("proxy url %q is not a coder proxy instance, verify the url is correct", reqURL) + isCoderErr := xerrors.Errorf("proxy url %q is not a coder proxy instance, verify the url is correct", reqURL) if resp.Header.Get(codersdk.BuildVersionHeader) != "" { - isCoderErr = fmt.Errorf("proxy url %q is a coder instance, but unable to decode the response payload. Could this be a primary coderd and not a proxy?", reqURL) + isCoderErr = xerrors.Errorf("proxy url %q is a coder instance, but unable to decode the response payload. Could this be a primary coderd and not a proxy?", reqURL) } // If the response is not json, then the user likely input a bad url that returns status code 200. @@ -286,7 +286,7 @@ func (p *ProxyHealth) runOnce(ctx context.Context, now time.Time) (map[uuid.UUID if notJSONErr := codersdk.ExpectJSONMime(resp); notJSONErr != nil { err = errors.Join( isCoderErr, - fmt.Errorf("attempted to query health at %q but got back the incorrect content type: %w", reqURL, notJSONErr), + xerrors.Errorf("attempted to query health at %q but got back the incorrect content type: %w", reqURL, notJSONErr), ) status.Report.Errors = []string{ @@ -300,7 +300,7 @@ func (p *ProxyHealth) runOnce(ctx context.Context, now time.Time) (map[uuid.UUID status.Report.Errors = []string{ errors.Join( isCoderErr, - fmt.Errorf("received a status code 200, but failed to decode health report body: %w", err), + xerrors.Errorf("received a status code 200, but failed to decode health report body: %w", err), ).Error(), } status.Status = Unhealthy diff --git a/enterprise/wsproxy/wsproxy.go b/enterprise/wsproxy/wsproxy.go index 92fc98e5b2743..cbf9695bd77b6 100644 --- a/enterprise/wsproxy/wsproxy.go +++ b/enterprise/wsproxy/wsproxy.go @@ -159,7 +159,7 @@ func New(ctx context.Context, opts *Options) (*Server, error) { info, err := client.SDKClient.BuildInfo(ctx) if err != nil { return nil, fmt.Errorf("buildinfo: %w", errors.Join( - fmt.Errorf("unable to fetch build info from primary coderd. Are you sure %q is a coderd instance?", opts.DashboardURL), + xerrors.Errorf("unable to fetch build info from primary coderd. Are you sure %q is a coderd instance?", opts.DashboardURL), err, )) } diff --git a/helm/provisioner/charts/libcoder-0.1.0.tgz b/helm/provisioner/charts/libcoder-0.1.0.tgz deleted file mode 100644 index e90f7d3038e1207ac28eb804aaa83d5640294331..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3010 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