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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 pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy