From c2ca3f983aae62ff9c8da40f4f3403de672edae4 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 7 May 2026 15:21:48 -0400 Subject: [PATCH 01/15] Fleet UI: Update empty states in Controls --- .../EmptyState/EmptyState.stories.tsx | 1 + .../ScriptBatchProgress.tests.tsx | 14 +++-- .../ScriptBatchProgress.tsx | 35 +++++++++--- .../cards/ScriptLibrary/ScriptLibrary.tsx | 33 ++++++++---- .../ScriptUploader/ScriptUploader.tsx | 3 +- .../ManageControlsPage/Scripts/helpers.tsx | 15 ++++-- .../BootstrapPackage.tests.tsx | 6 +-- .../BootstrapPackage/BootstrapPackage.tsx | 9 +++- .../Variables/Variables.tests.tsx | 53 +++++++++++++++---- .../Variables/Variables.tsx | 8 ++- website/config/routes.js | 1 + 11 files changed, 137 insertions(+), 41 deletions(-) diff --git a/frontend/components/EmptyState/EmptyState.stories.tsx b/frontend/components/EmptyState/EmptyState.stories.tsx index c52858df328..c443bd2c7ec 100644 --- a/frontend/components/EmptyState/EmptyState.stories.tsx +++ b/frontend/components/EmptyState/EmptyState.stories.tsx @@ -3,6 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import Button from "components/buttons/Button"; import EmptyState from "./EmptyState"; +import CustomLink from "components/CustomLink"; const meta: Meta = { title: "Components/EmptyState", diff --git a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tests.tsx b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tests.tsx index 4c9af299274..a7b3b2f2671 100644 --- a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tests.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tests.tsx @@ -12,9 +12,7 @@ import { ScriptBatchStatus } from "interfaces/script"; import { createMockBatchScriptSummary } from "__mocks__/scriptMock"; -import ScriptBatchProgress, { - EMPTY_STATE_DETAILS, -} from "./ScriptBatchProgress"; +import ScriptBatchProgress from "./ScriptBatchProgress"; import { ScriptsLocation } from "../../Scripts"; const waitForLoadingToFinish = async (container: HTMLElement) => { @@ -114,6 +112,12 @@ const getTestLocation = (status: ScriptBatchStatus): ScriptsLocation => ({ search: `?status=${status}`, }); +const EMPTY_STATE_TEXT: Record = { + started: "Scripts running on multiple hosts will appear here.", + scheduled: "Scheduled scripts will appear here.", + finished: "Completed or canceled batch scripts will appear here.", +}; + const testTabURLNavAndEmpty = async (status: ScriptBatchStatus) => { const render = createCustomRenderer({ withBackendMock: true, @@ -144,7 +148,9 @@ const testTabURLNavAndEmpty = async (status: ScriptBatchStatus) => { await waitForLoadingToFinish(container); - expect(screen.getByText(EMPTY_STATE_DETAILS[status])).toBeInTheDocument(); + expect( + screen.getByText(EMPTY_STATE_TEXT[status]) + ).toBeInTheDocument(); cleanup(); }; diff --git a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tsx b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tsx index b4431877403..c06253fb074 100644 --- a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tsx @@ -12,6 +12,7 @@ import scriptsAPI, { import { isValidScriptBatchStatus, ScriptBatchStatus } from "interfaces/script"; import { COLORS } from "styles/var/colors"; +import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants"; import Spinner from "components/Spinner"; import ProgressBar from "components/ProgressBar"; @@ -22,9 +23,10 @@ import PaginatedList, { IPaginatedListHandle } from "components/PaginatedList"; import ListItem from "components/ListItem"; import Icon from "components/Icon/Icon"; import EmptyState from "components/EmptyState"; +import CustomLink from "components/CustomLink"; import { IScriptsCommonProps } from "../../ScriptsNavItems"; -import getWhen from "../../helpers"; +import { getWhen } from "../../helpers"; const baseClass = "script-batch-progress"; @@ -34,19 +36,36 @@ const STATUS_BY_INDEX: ScriptBatchStatus[] = [ "finished", ]; -export const EMPTY_STATE_DETAILS: Record = { - started: "When a script is run on multiple hosts, progress will appear here.", - scheduled: - "When a script is scheduled to run in the future, it will appear here.", - finished: - "When a batch script is completed or canceled, historical results will appear here.", +export const EMPTY_STATE_DETAILS: Record = { + started: ( + <> + Scripts running on multiple hosts will appear here.
+ + + ), + scheduled: ( + <> + Scheduled scripts will appear here. +
+ + + ), + finished: <>Completed or canceled batch scripts will appear here., }; const getEmptyState = (status: ScriptBatchStatus) => { return ( ); diff --git a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx index 8e5d318df1b..aa1d85fb1b6 100644 --- a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx @@ -19,6 +19,7 @@ import Spinner from "components/Spinner"; import Pagination from "components/Pagination"; import SectionHeader from "components/SectionHeader"; import EmptyState from "components/EmptyState"; +import Button from "components/buttons/Button"; import UploadList from "../../../../../components/UploadList"; import DeleteScriptModal from "../../components/DeleteScriptModal"; @@ -26,8 +27,9 @@ import EditScriptModal from "../../components/EditScriptModal"; import ScriptUploadModal from "../../components/ScriptUploadModal"; import ScriptListHeading from "../../components/ScriptListHeading"; import ScriptListItem from "../../components/ScriptListItem"; -import ScriptUploader from "../../components/ScriptUploader"; import { IScriptsCommonProps } from "../../ScriptsNavItems"; +import { SCRIPT_UPLOADER_EMPTY_STATE_TEXT } from "../../helpers"; + const baseClass = "script-library"; @@ -183,19 +185,32 @@ const ScriptLibrary = ({ router, teamId, location }: IScriptLibraryProps) => { ); + const canUploadScripts = + isTechnician || (!config.server_settings.scripts_disabled && isPremiumTier); + return (
{config.server_settings.scripts_disabled && renderScriptsDisabledBanner()} {renderScriptsList()} - {!isLoading && - currentPage === 0 && - !scripts?.length && - (isTechnician ? ( - - ) : ( - setShowAddScriptModal(true)} /> - ))} + {!isLoading && currentPage === 0 && !scripts?.length && ( + setShowAddScriptModal(true)}> + Upload + + ) : undefined + } + /> + )} {showDeleteScriptModal && selectedScript.current && ( { +export const SCRIPT_UPLOADER_EMPTY_STATE_TEXT = ( + <> + Upload shell (.sh) or Python (.py) for macOS and Linux, +
+ or PowerShell (.ps1) for Windows. + +); + +export const SCRIPT_UPLOADER_TEXT = + "Shell (.sh) or Python (.py) for macOS and Linux, or PowerShell (.ps1) for Windows"; + +export const getWhen = (summary: IScriptBatchSummaryV2) => { const { batch_execution_id: id, not_before, @@ -73,5 +84,3 @@ const getWhen = (summary: IScriptBatchSummaryV2) => { return null; } }; - -export default getWhen; diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/BootstrapPackage.tests.tsx b/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/BootstrapPackage.tests.tsx index 1adbdc2a90d..29463450730 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/BootstrapPackage.tests.tsx +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/BootstrapPackage.tests.tsx @@ -60,7 +60,7 @@ describe("BootstrapPackage", () => { await waitFor(() => { expect( - screen.getByText(/turn on automatic enrollment/) + screen.getByText(/Turn on MDM and automatic enrollment/) ).toBeInTheDocument(); }); expect( @@ -83,7 +83,7 @@ describe("BootstrapPackage", () => { await waitFor(() => { expect( - screen.getByText(/turn on automatic enrollment/) + screen.getByText(/Turn on MDM and automatic enrollment/) ).toBeInTheDocument(); }); }); @@ -105,7 +105,7 @@ describe("BootstrapPackage", () => { await waitFor(() => { expect( - screen.getByText(/turn on automatic enrollment/) + screen.getByText(/Turn on MDM and automatic enrollment/) ).toBeInTheDocument(); }); }); diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/BootstrapPackage.tsx b/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/BootstrapPackage.tsx index ba60550dc50..e28108d1185 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/BootstrapPackage.tsx +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/BootstrapPackage.tsx @@ -220,7 +220,14 @@ const BootstrapPackage = ({ + Turn on MDM and automatic enrollment to deploy a custom bootstrap + package. +
+ Supported on macOS. + + } primaryButton={
Date: Thu, 7 May 2026 15:26:36 -0400 Subject: [PATCH 02/15] lint --- .../cards/ScriptBatchProgress/ScriptBatchProgress.tests.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tests.tsx b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tests.tsx index a7b3b2f2671..4e8ba86a4a8 100644 --- a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tests.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tests.tsx @@ -148,9 +148,7 @@ const testTabURLNavAndEmpty = async (status: ScriptBatchStatus) => { await waitForLoadingToFinish(container); - expect( - screen.getByText(EMPTY_STATE_TEXT[status]) - ).toBeInTheDocument(); + expect(screen.getByText(EMPTY_STATE_TEXT[status])).toBeInTheDocument(); cleanup(); }; From fc0bac3ec9abebd1c6faaa8fa4acae4d860dca99 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 7 May 2026 15:48:50 -0400 Subject: [PATCH 03/15] lint --- .../Scripts/cards/ScriptLibrary/ScriptLibrary.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx index aa1d85fb1b6..837673c22c5 100644 --- a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx @@ -30,7 +30,6 @@ import ScriptListItem from "../../components/ScriptListItem"; import { IScriptsCommonProps } from "../../ScriptsNavItems"; import { SCRIPT_UPLOADER_EMPTY_STATE_TEXT } from "../../helpers"; - const baseClass = "script-library"; const SCRIPTS_PER_PAGE = 10; @@ -197,11 +196,7 @@ const ScriptLibrary = ({ router, teamId, location }: IScriptLibraryProps) => { setShowAddScriptModal(true)}> From 9771dbf47b5ae1c45ebcec90c7cb459addaa67b1 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 7 May 2026 15:51:21 -0400 Subject: [PATCH 04/15] lint --- frontend/components/EmptyState/EmptyState.stories.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/components/EmptyState/EmptyState.stories.tsx b/frontend/components/EmptyState/EmptyState.stories.tsx index c443bd2c7ec..c52858df328 100644 --- a/frontend/components/EmptyState/EmptyState.stories.tsx +++ b/frontend/components/EmptyState/EmptyState.stories.tsx @@ -3,7 +3,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import Button from "components/buttons/Button"; import EmptyState from "./EmptyState"; -import CustomLink from "components/CustomLink"; const meta: Meta = { title: "Components/EmptyState", From ff7546ada996518cb8782d2b6dc4501821689c82 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 7 May 2026 17:13:43 -0400 Subject: [PATCH 05/15] Fix add script gate, add tests --- .../ScriptBatchDetailsPage.tsx | 2 +- .../ScriptLibrary/ScriptLibrary.tests.tsx | 90 +++++++++++++++++++ .../cards/ScriptLibrary/ScriptLibrary.tsx | 3 +- 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx diff --git a/frontend/pages/ManageControlsPage/Scripts/ScriptBatchDetailsPage/ScriptBatchDetailsPage.tsx b/frontend/pages/ManageControlsPage/Scripts/ScriptBatchDetailsPage/ScriptBatchDetailsPage.tsx index 873b0d90036..93fac9d810e 100644 --- a/frontend/pages/ManageControlsPage/Scripts/ScriptBatchDetailsPage/ScriptBatchDetailsPage.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/ScriptBatchDetailsPage/ScriptBatchDetailsPage.tsx @@ -43,7 +43,7 @@ import TabNav from "components/TabNav"; import TabText from "components/TabText"; import ViewAllHostsLink from "components/ViewAllHostsLink"; -import getWhen from "../helpers"; +import { getWhen } from "../helpers"; import CancelScriptBatchModal from "../components/CancelScriptBatchModal"; import ScriptBatchHostsTable from "./components/ScriptBatchHostsTable"; diff --git a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx new file mode 100644 index 00000000000..0bceaf50d89 --- /dev/null +++ b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx @@ -0,0 +1,90 @@ +import React from "react"; +import { screen, waitFor } from "@testing-library/react"; +import { + baseUrl, + createCustomRenderer, + createMockRouter, +} from "test/test-utils"; +import mockServer from "test/mock-server"; +import { http, HttpResponse } from "msw"; + +import ScriptLibrary from "./ScriptLibrary"; +import { ScriptsLocation } from "../../Scripts"; + +const mockRouter = createMockRouter(); + +const mockLocation: ScriptsLocation = { + pathname: "/controls/scripts/library", + query: {}, + search: "", +}; + +const emptyScriptsHandler = http.get(baseUrl("/scripts"), () => + HttpResponse.json({ + scripts: [], + meta: { has_next_results: false, has_previous_results: false }, + }) +); + +const baseProps = { + router: mockRouter, + teamId: 1, + location: mockLocation, +}; + +describe("ScriptLibrary", () => { + it("renders empty state with Upload CTA for non-technician users", async () => { + mockServer.use(emptyScriptsHandler); + + const render = createCustomRenderer({ + withBackendMock: true, + context: { + app: { + isGlobalAdmin: true, + isGlobalTechnician: false, + isTeamTechnician: false, + config: { + server_settings: { scripts_disabled: false }, + }, + }, + }, + }); + + render(); + + await waitFor(() => { + expect(screen.getByText("No scripts")).toBeInTheDocument(); + }); + expect( + screen.getByRole("button", { name: /upload/i }) + ).toBeInTheDocument(); + expect( + screen.getByText(/Upload shell \(.sh\) or Python \(.py\)/i) + ).toBeInTheDocument(); + }); + + it("renders empty state without Upload CTA for technician users", async () => { + mockServer.use(emptyScriptsHandler); + + const render = createCustomRenderer({ + withBackendMock: true, + context: { + app: { + isGlobalTechnician: true, + config: { + server_settings: { scripts_disabled: false }, + }, + }, + }, + }); + + render(); + + await waitFor(() => { + expect(screen.getByText("No scripts")).toBeInTheDocument(); + }); + expect( + screen.queryByRole("button", { name: /upload/i }) + ).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx index 837673c22c5..035f67c0e9f 100644 --- a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx @@ -184,8 +184,7 @@ const ScriptLibrary = ({ router, teamId, location }: IScriptLibraryProps) => { ); - const canUploadScripts = - isTechnician || (!config.server_settings.scripts_disabled && isPremiumTier); + const canUploadScripts = !isTechnician; return (
From 17e47e404d6d34e41a7e277cbd55c806a75e0403 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 8 May 2026 09:30:03 -0400 Subject: [PATCH 06/15] Update tests --- .../ScriptLibrary/ScriptLibrary.tests.tsx | 64 +++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx index 0bceaf50d89..4775bbc0e69 100644 --- a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx @@ -32,8 +32,8 @@ const baseProps = { location: mockLocation, }; -describe("ScriptLibrary", () => { - it("renders empty state with Upload CTA for non-technician users", async () => { +describe("ScriptLibrary empty state", () => { + it("renders Upload CTA and info text for global admin", async () => { mockServer.use(emptyScriptsHandler); const render = createCustomRenderer({ @@ -41,8 +41,6 @@ describe("ScriptLibrary", () => { context: { app: { isGlobalAdmin: true, - isGlobalTechnician: false, - isTeamTechnician: false, config: { server_settings: { scripts_disabled: false }, }, @@ -63,7 +61,32 @@ describe("ScriptLibrary", () => { ).toBeInTheDocument(); }); - it("renders empty state without Upload CTA for technician users", async () => { + it("renders Upload CTA even when scripts are disabled (managing library is still allowed)", async () => { + mockServer.use(emptyScriptsHandler); + + const render = createCustomRenderer({ + withBackendMock: true, + context: { + app: { + isGlobalAdmin: true, + config: { + server_settings: { scripts_disabled: true }, + }, + }, + }, + }); + + render(); + + await waitFor(() => { + expect(screen.getByText("No scripts")).toBeInTheDocument(); + }); + expect( + screen.getByRole("button", { name: /upload/i }) + ).toBeInTheDocument(); + }); + + it("hides Upload CTA and info text for global technician", async () => { mockServer.use(emptyScriptsHandler); const render = createCustomRenderer({ @@ -86,5 +109,36 @@ describe("ScriptLibrary", () => { expect( screen.queryByRole("button", { name: /upload/i }) ).not.toBeInTheDocument(); + expect( + screen.queryByText(/Upload shell \(.sh\) or Python \(.py\)/i) + ).not.toBeInTheDocument(); + }); + + it("hides Upload CTA and info text for team technician", async () => { + mockServer.use(emptyScriptsHandler); + + const render = createCustomRenderer({ + withBackendMock: true, + context: { + app: { + isTeamTechnician: true, + config: { + server_settings: { scripts_disabled: false }, + }, + }, + }, + }); + + render(); + + await waitFor(() => { + expect(screen.getByText("No scripts")).toBeInTheDocument(); + }); + expect( + screen.queryByRole("button", { name: /upload/i }) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/Upload shell \(.sh\) or Python \(.py\)/i) + ).not.toBeInTheDocument(); }); }); From 6e272cc46d9b18d62e962246fe07a1b3800fc6e5 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 8 May 2026 13:18:08 -0400 Subject: [PATCH 07/15] Fix lint --- .../Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx index 4775bbc0e69..93b38d8032b 100644 --- a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tests.tsx @@ -53,9 +53,7 @@ describe("ScriptLibrary empty state", () => { await waitFor(() => { expect(screen.getByText("No scripts")).toBeInTheDocument(); }); - expect( - screen.getByRole("button", { name: /upload/i }) - ).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /upload/i })).toBeInTheDocument(); expect( screen.getByText(/Upload shell \(.sh\) or Python \(.py\)/i) ).toBeInTheDocument(); @@ -81,9 +79,7 @@ describe("ScriptLibrary empty state", () => { await waitFor(() => { expect(screen.getByText("No scripts")).toBeInTheDocument(); }); - expect( - screen.getByRole("button", { name: /upload/i }) - ).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /upload/i })).toBeInTheDocument(); }); it("hides Upload CTA and info text for global technician", async () => { From 5270a86a28c641e1cb5f4b2a8c241bf6b36c73ee Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 8 May 2026 13:42:15 -0400 Subject: [PATCH 08/15] Move secrets over to variables naming --- .../interfaces/{secrets.ts => variables.ts} | 4 +- .../Variables/Variables.tests.tsx | 8 +- .../Variables/Variables.tsx | 87 ++++++++++--------- .../ManageControlsPage/Variables/_styles.scss | 4 +- .../AddCustomVariableModal.tsx | 8 +- .../DeleteCustomVariableModal.tsx | 20 ++--- frontend/services/entities/secrets.tsx | 42 --------- frontend/services/entities/variables.tsx | 42 +++++++++ frontend/utilities/endpoints.ts | 4 +- 9 files changed, 113 insertions(+), 106 deletions(-) rename frontend/interfaces/{secrets.ts => variables.ts} (63%) delete mode 100644 frontend/services/entities/secrets.tsx create mode 100644 frontend/services/entities/variables.tsx diff --git a/frontend/interfaces/secrets.ts b/frontend/interfaces/variables.ts similarity index 63% rename from frontend/interfaces/secrets.ts rename to frontend/interfaces/variables.ts index 5db7b844049..c624d400453 100644 --- a/frontend/interfaces/secrets.ts +++ b/frontend/interfaces/variables.ts @@ -1,11 +1,11 @@ -export interface ISecret { +export interface IVariable { id: number; name: string; created_at: string; updated_at: string; } -export interface ISecretPayload { +export interface IVariablePayload { name: string; value: string; } diff --git a/frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx b/frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx index 3bf456b7996..506bf21f6f6 100644 --- a/frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx +++ b/frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx @@ -1,7 +1,7 @@ import React from "react"; import { screen, waitFor } from "@testing-library/react"; -import { ISecret } from "interfaces/secrets"; +import { IVariable } from "interfaces/variables"; import { UserEvent } from "@testing-library/user-event"; import { createCustomRenderer } from "test/test-utils"; import { http, HttpResponse } from "msw"; @@ -97,7 +97,7 @@ describe("Custom variables", () => { }); describe("non-empty state", () => { - const mockSecrets: ISecret[] = [ + const mockSecrets: IVariable[] = [ { name: "SECRET_UNO", id: 1, @@ -111,7 +111,7 @@ describe("Custom variables", () => { updated_at: new Date().toISOString(), }, ]; - const secretsResponse: { secrets: ISecret[] } = { secrets: [] }; + const secretsResponse: { secrets: IVariable[] } = { secrets: [] }; // Mock the scripts endpoint to return our two test scripts. const secretsHandler = http.get(baseUrl("/custom_variables"), () => { return HttpResponse.json({ @@ -136,7 +136,7 @@ describe("Custom variables", () => { value, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), - } as ISecret; + } as IVariable; secretsResponse.secrets.push(newSecret); return HttpResponse.json(newSecret); } diff --git a/frontend/pages/ManageControlsPage/Variables/Variables.tsx b/frontend/pages/ManageControlsPage/Variables/Variables.tsx index d005ce38762..ee968b784a9 100644 --- a/frontend/pages/ManageControlsPage/Variables/Variables.tsx +++ b/frontend/pages/ManageControlsPage/Variables/Variables.tsx @@ -2,8 +2,10 @@ import React, { useContext, useEffect, useRef, useState } from "react"; import { useQuery } from "react-query"; -import secretsAPI, { IListSecretsResponse } from "services/entities/secrets"; -import { ISecret } from "interfaces/secrets"; +import variablesAPI, { + IListVariablesResponse, +} from "services/entities/variables"; +import { IVariable } from "interfaces/variables"; import { AppContext } from "context/app"; @@ -24,17 +26,19 @@ import DeleteCustomVariableModal from "./components/DeleteCustomVariableModal"; const baseClass = "variables"; -export const SECRETS_PAGE_SIZE = 20; +export const VARIABLES_PAGE_SIZE = 20; const Variables = () => { - const paginatedListRef = useRef>(null); + const paginatedListRef = useRef>(null); const [copyMessage, setCopyMessage] = useState(""); - const [copiedSecretName, setCopiedSecretName] = useState(""); + const [copiedVariableName, setCopiedVariableName] = useState(""); const copyMessageTimeoutIdRef = useRef(null); const [showDeleteModal, setShowDeleteModal] = useState(false); - const [secretToDelete, setSecretToDelete] = useState(); + const [variableToDelete, setVariableToDelete] = useState< + IVariable | undefined + >(); const [showAddModal, setShowAddModal] = useState(false); const [pageNumber, setPageNumber] = useState(0); @@ -44,12 +48,12 @@ const Variables = () => { const canEdit = isGlobalAdmin || isGlobalMaintainer; - const apiParams = { page: pageNumber, per_page: SECRETS_PAGE_SIZE }; + const apiParams = { page: pageNumber, per_page: VARIABLES_PAGE_SIZE }; const { data, isFetching: isLoading, refetch } = useQuery< - IListSecretsResponse, + IListVariablesResponse, Error, - IListSecretsResponse - >(["secrets", apiParams], () => secretsAPI.getSecrets(apiParams)); + IListVariablesResponse + >(["variables", apiParams], () => variablesAPI.getVariables(apiParams)); // Open add modal via query param (e.g. from command palette) useEffect(() => { @@ -66,45 +70,45 @@ const Variables = () => { } }, []); - const onClickAddSecret = () => { + const onClickAddVariable = () => { setShowAddModal(true); }; - const onSaveSecret = () => { + const onSaveVariable = () => { setShowAddModal(false); refetch(); }; - const onDeleteSecret = () => { + const onDeleteVariable = () => { setShowDeleteModal(false); refetch(); }; - const onClickDeleteSecret = (secret: ISecret) => { - setSecretToDelete(secret); + const onClickDeleteVariable = (variable: IVariable) => { + setVariableToDelete(variable); setShowDeleteModal(true); }; - const getTokenFromSecretName = (secretName: string): string => { - return `$FLEET_SECRET_${secretName.toUpperCase()}`; + const getTokenFromVariableName = (variableName: string): string => { + return `$FLEET_SECRET_${variableName.toUpperCase()}`; }; - const onCopySecretName = (evt: React.MouseEvent, secretName: string) => { + const onCopyVariableName = (evt: React.MouseEvent, variableName: string) => { evt.preventDefault(); if (copyMessageTimeoutIdRef.current) { clearTimeout(copyMessageTimeoutIdRef.current); } - setCopiedSecretName(secretName); - stringToClipboard(getTokenFromSecretName(secretName)) + setCopiedVariableName(variableName); + stringToClipboard(getTokenFromVariableName(variableName)) .then(() => setCopyMessage("Copied!")) .catch(() => setCopyMessage("Copy failed")); // Clear message after 1 second copyMessageTimeoutIdRef.current = setTimeout(() => { setCopyMessage(""); - setCopiedSecretName(""); + setCopiedVariableName(""); }, 1000); return false; @@ -119,27 +123,27 @@ const Variables = () => { }; }, []); - const renderSecretRow = (secret: ISecret) => ( + const renderVariableRow = (variable: IVariable) => ( <> - + Updated{" "} - •{" "} - {getTokenFromSecretName(secret.name)} + {" "} + • {getTokenFromVariableName(variable.name)} - {copyMessage && copiedSecretName === secret.name && ( + {copyMessage && copiedVariableName === variable.name && ( {`${copyMessage} `} @@ -152,7 +156,7 @@ const Variables = () => { variant="icon" onClick={(e: React.MouseEvent) => { e.stopPropagation(); - onClickDeleteSecret(secret); + onClickDeleteVariable(variable); }} > <> @@ -212,7 +216,10 @@ const Variables = () => { canEdit ? ( ( - )} @@ -223,7 +230,7 @@ const Variables = () => { {showAddModal && ( setShowAddModal(false)} - onSave={onSaveSecret} + onSave={onSaveVariable} /> )}
@@ -240,7 +247,7 @@ const Variables = () => { + ( + + )} + /> ) : undefined } /> diff --git a/frontend/pages/ManageControlsPage/Variables/components/AddCustomVariableModal/AddCustomVariableModal.tsx b/frontend/pages/ManageControlsPage/Variables/components/AddCustomVariableModal/AddCustomVariableModal.tsx index a2e20ae179c..ab15ca83be3 100644 --- a/frontend/pages/ManageControlsPage/Variables/components/AddCustomVariableModal/AddCustomVariableModal.tsx +++ b/frontend/pages/ManageControlsPage/Variables/components/AddCustomVariableModal/AddCustomVariableModal.tsx @@ -69,11 +69,11 @@ const AddCustomVariableModal = ({ onSave(); } catch (error) { if (hasStatusKey(error) && error.status === 409) { - renderFlash("error", "A secret with this name already exists."); + renderFlash("error", "A variable with this name already exists."); } else { renderFlash( "error", - "An error occurred while saving the secret. Please try again." + "An error occurred while saving the variable. Please try again." ); } } finally { From f6b86c6425121b7aa5ea407a943a9619c6eaaa5b Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 8 May 2026 16:05:07 -0400 Subject: [PATCH 10/15] iserror check for empty state --- .../Scripts/cards/ScriptLibrary/ScriptLibrary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx index 5647867f6aa..28c1c348542 100644 --- a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptLibrary/ScriptLibrary.tsx @@ -192,7 +192,7 @@ const ScriptLibrary = ({ router, teamId, location }: IScriptLibraryProps) => { {config.server_settings.scripts_disabled && renderScriptsDisabledBanner()} {renderScriptsList()} - {!isLoading && currentPage === 0 && !scripts?.length && ( + {!isLoading && !isError && currentPage === 0 && !scripts?.length && ( Date: Mon, 11 May 2026 10:08:43 -0400 Subject: [PATCH 11/15] Copy change --- .../SetupExperience/cards/RunScript/RunScript.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/RunScript/RunScript.tsx b/frontend/pages/ManageControlsPage/SetupExperience/cards/RunScript/RunScript.tsx index 12b3536f50a..6a2424266fd 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/RunScript/RunScript.tsx +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/RunScript/RunScript.tsx @@ -101,7 +101,7 @@ const RunScript = ({ currentTeamId, router }: ISetupExperienceCardProps) => { router.push(PATHS.ADMIN_INTEGRATIONS_MDM)}> Turn on From ea93090a20b7ddfec15ec6fc10d21e0828408c51 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Mon, 11 May 2026 10:17:51 -0400 Subject: [PATCH 12/15] Tweaks --- frontend/components/EmptyState/_styles.scss | 2 +- .../SetupExperience/cards/SetupAssistant/SetupAssistant.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/components/EmptyState/_styles.scss b/frontend/components/EmptyState/_styles.scss index d2f4dfdaa20..7b9961dd83f 100644 --- a/frontend/components/EmptyState/_styles.scss +++ b/frontend/components/EmptyState/_styles.scss @@ -158,7 +158,7 @@ $ghost-cell-padding-x: $pad-large; // 24px, matches Figma // Form variant — ghost form fields with a save button // ---------------------------------------------------------- &--form { - height: 320px; + height: $ghost-table-height; .empty-state__ghost-table { display: block; diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/SetupAssistant/SetupAssistant.tsx b/frontend/pages/ManageControlsPage/SetupExperience/cards/SetupAssistant/SetupAssistant.tsx index 2014d7bc381..631c3557d6c 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/SetupAssistant/SetupAssistant.tsx +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/SetupAssistant/SetupAssistant.tsx @@ -128,7 +128,7 @@ const SetupAssistant = ({ router.push(PATHS.ADMIN_INTEGRATIONS_MDM)}> Turn on From b70445c8bcaeee43cd8156289f7f1caace83d348 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 13 May 2026 14:32:14 -0400 Subject: [PATCH 13/15] meta object, ts vs tsx --- .../ManageControlsPage/Variables/Variables.tests.tsx | 12 ++++++++---- .../entities/{variables.tsx => variables.ts} | 0 2 files changed, 8 insertions(+), 4 deletions(-) rename frontend/services/entities/{variables.tsx => variables.ts} (100%) diff --git a/frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx b/frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx index 506bf21f6f6..c4870129a9b 100644 --- a/frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx +++ b/frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx @@ -43,8 +43,10 @@ describe("Custom variables", () => { return HttpResponse.json({ custom_variables: [], count: 0, - has_prev_results: false, - has_next_results: false, + meta: { + has_previous_results: false, + has_next_results: false, + }, }); }); @@ -117,8 +119,10 @@ describe("Custom variables", () => { return HttpResponse.json({ custom_variables: secretsResponse.secrets, count: mockSecrets.length, - has_prev_results: false, - has_next_results: false, + meta: { + has_previous_results: false, + has_next_results: false, + }, }); }); const addSecretHandler = http.post( diff --git a/frontend/services/entities/variables.tsx b/frontend/services/entities/variables.ts similarity index 100% rename from frontend/services/entities/variables.tsx rename to frontend/services/entities/variables.ts From 8e8eff19a812208aa648d53d520cc071dd308661 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 13 May 2026 15:32:12 -0400 Subject: [PATCH 14/15] post ai-review nits and tweaks --- .../ScriptBatchProgress.tsx | 2 +- .../cards/RunScript/RunScript.tests.tsx | 2 +- .../SetupAssistant/SetupAssistant.tests.tsx | 4 +- .../Variables/Variables.tests.tsx | 126 +++++++++--------- .../Variables/Variables.tsx | 1 - .../AddCustomVariableModal.tsx | 28 ++-- .../services/mock_service/mocks/config.ts | 6 +- .../services/mock_service/mocks/responses.ts | 48 +++---- 8 files changed, 108 insertions(+), 109 deletions(-) diff --git a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tsx b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tsx index c06253fb074..121bc24f850 100644 --- a/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/cards/ScriptBatchProgress/ScriptBatchProgress.tsx @@ -36,7 +36,7 @@ const STATUS_BY_INDEX: ScriptBatchStatus[] = [ "finished", ]; -export const EMPTY_STATE_DETAILS: Record = { +const EMPTY_STATE_DETAILS: Record = { started: ( <> Scripts running on multiple hosts will appear here.
diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/RunScript/RunScript.tests.tsx b/frontend/pages/ManageControlsPage/SetupExperience/cards/RunScript/RunScript.tests.tsx index 4fce5d6e83e..730a2402ff7 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/RunScript/RunScript.tests.tsx +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/RunScript/RunScript.tests.tsx @@ -30,7 +30,7 @@ describe("RunScript", () => { render(); expect( - await screen.findByText(/turn on automatic enrollment/) + await screen.findByText(/Turn on MDM and automatic enrollment/i) ).toBeInTheDocument(); expect( screen.getByText(/Upload a script to run on macOS hosts/) diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/SetupAssistant/SetupAssistant.tests.tsx b/frontend/pages/ManageControlsPage/SetupExperience/cards/SetupAssistant/SetupAssistant.tests.tsx index 95954780e74..ac228e20a9d 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/SetupAssistant/SetupAssistant.tests.tsx +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/SetupAssistant/SetupAssistant.tests.tsx @@ -49,11 +49,11 @@ describe("SetupAssistant", () => { await waitFor(() => { expect( - screen.getByText(/first turn on automatic enrollment/) + screen.getByText(/Additional configuration required/) ).toBeInTheDocument(); }); expect( - screen.getByText(/Add an automatic enrollment profile/) + screen.getByText(/Turn on MDM and automatic enrollment to customize/) ).toBeVisible(); }); diff --git a/frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx b/frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx index c4870129a9b..9be8d1ffb7d 100644 --- a/frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx +++ b/frontend/pages/ManageControlsPage/Variables/Variables.tests.tsx @@ -39,7 +39,7 @@ describe("Custom variables", () => { }, }); describe("empty state", () => { - const emptySecretsHandler = http.get(baseUrl("/custom_variables"), () => { + const emptyVariablesHandler = http.get(baseUrl("/custom_variables"), () => { return HttpResponse.json({ custom_variables: [], count: 0, @@ -54,7 +54,7 @@ describe("Custom variables", () => { mockServer.resetHandlers(); }); it("renders with Add CTA and edit info when user can edit", async () => { - mockServer.use(emptySecretsHandler); + mockServer.use(emptyVariablesHandler); render(); await waitFor(() => { @@ -71,7 +71,7 @@ describe("Custom variables", () => { }); it("renders without Add CTA and with read-only info when user cannot edit", async () => { - mockServer.use(emptySecretsHandler); + mockServer.use(emptyVariablesHandler); const renderReadOnly = createCustomRenderer({ withBackendMock: true, @@ -99,7 +99,7 @@ describe("Custom variables", () => { }); describe("non-empty state", () => { - const mockSecrets: IVariable[] = [ + const mockVariables: IVariable[] = [ { name: "SECRET_UNO", id: 1, @@ -113,19 +113,19 @@ describe("Custom variables", () => { updated_at: new Date().toISOString(), }, ]; - const secretsResponse: { secrets: IVariable[] } = { secrets: [] }; - // Mock the scripts endpoint to return our two test scripts. - const secretsHandler = http.get(baseUrl("/custom_variables"), () => { + const variablesResponse: { variables: IVariable[] } = { variables: [] }; + // Mock the variables endpoint to return our two test variables. + const variablesHandler = http.get(baseUrl("/custom_variables"), () => { return HttpResponse.json({ - custom_variables: secretsResponse.secrets, - count: mockSecrets.length, + custom_variables: variablesResponse.variables, + count: variablesResponse.variables.length, meta: { has_previous_results: false, has_next_results: false, }, }); }); - const addSecretHandler = http.post( + const addVariableHandler = http.post( baseUrl("/custom_variables"), async ({ request }) => { const { name, value } = (await request.json()) as { @@ -134,26 +134,26 @@ describe("Custom variables", () => { }; // const name = formData.get("name"); // const value = formData.get("value"); - const newSecret = { - id: mockSecrets.length + 1, + const newVariable = { + id: mockVariables.length + 1, name, value, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), } as IVariable; - secretsResponse.secrets.push(newSecret); - return HttpResponse.json(newSecret); + variablesResponse.variables.push(newVariable); + return HttpResponse.json(newVariable); } ); - const deleteSecretHandler = http.delete( + const deleteVariableHandler = http.delete( baseUrl("/custom_variables/:id"), async ({ request }) => { const id = request.url.split("/").pop(); if (!id) { - throw new Error("Secret ID not found in request URL"); + throw new Error("Variable ID not found in request URL"); } - secretsResponse.secrets = secretsResponse.secrets.filter( - (secret) => secret.id !== parseInt(id, 10) + variablesResponse.variables = variablesResponse.variables.filter( + (variable) => variable.id !== parseInt(id, 10) ); return HttpResponse.json({ success: true }); } @@ -161,13 +161,13 @@ describe("Custom variables", () => { beforeEach(async () => { // Wait for the query stale timer to expire. await new Promise((resolve) => setTimeout(resolve, 250)); - mockServer.use(secretsHandler); - mockServer.use(addSecretHandler); - mockServer.use(deleteSecretHandler); - secretsResponse.secrets = [...mockSecrets]; + mockServer.use(variablesHandler); + mockServer.use(addVariableHandler); + mockServer.use(deleteVariableHandler); + variablesResponse.variables = [...mockVariables]; }); - it("renders when secrets are saved", async () => { + it("renders when variables are saved", async () => { render(); await waitFor( () => { @@ -184,48 +184,48 @@ describe("Custom variables", () => { it("renders the add button disabled in GitOps mode", async () => { renderInGOM(); - let addSecretButton; + let addVariableButton; await waitFor(() => { - addSecretButton = screen.getByRole("button", { + addVariableButton = screen.getByRole("button", { name: /Add custom variable/, }); - expect(addSecretButton).toBeInTheDocument(); + expect(addVariableButton).toBeInTheDocument(); }); - if (!addSecretButton) { + if (!addVariableButton) { throw new Error("Add custom variable button not found"); } - expect(addSecretButton).toHaveAttribute("disabled"); - expect(addSecretButton).toHaveClass("button--disabled"); + expect(addVariableButton).toHaveAttribute("disabled"); + expect(addVariableButton).toHaveClass("button--disabled"); // Tooltip behavior covered in GitOpsModeWrapper.tests.tsx; omitted here due to flakiness }); - it("deleting a secret is successful in GitOps mode", async () => { + it("deleting a variable is successful in GitOps mode", async () => { const { user } = renderInGOM(); await waitFor(() => { expect(screen.getByText("Add custom variable")).toBeInTheDocument(); }); // Get the element with SECRET_UNO in it. - let secretUno: HTMLElement | null = null; + let variableUno: HTMLElement | null = null; await waitFor(() => { - secretUno = screen.getByText("SECRET_UNO"); - expect(secretUno).toBeInTheDocument(); + variableUno = screen.getByText("SECRET_UNO"); + expect(variableUno).toBeInTheDocument(); }); - if (secretUno === null) { - throw new Error("Secret not found"); + if (variableUno === null) { + throw new Error("Variable not found"); } // Find the element with .paginated-list__row class that is ancestor to that element. - const secretUnoRow = (secretUno as HTMLElement).closest( + const variableUnoRow = (variableUno as HTMLElement).closest( ".paginated-list__row" ); - expect(secretUnoRow).toBeInTheDocument(); - if (!secretUnoRow) { - throw new Error("Secret row not found"); + expect(variableUnoRow).toBeInTheDocument(); + if (!variableUnoRow) { + throw new Error("Variable row not found"); } // Find the element with data-id="trash-icon" - const trashIcon = secretUnoRow.querySelector( + const trashIcon = variableUnoRow.querySelector( "[data-testid='trash-icon']" ); expect(trashIcon).toBeInTheDocument(); @@ -253,8 +253,8 @@ describe("Custom variables", () => { }); }); - describe("adding a new secret", () => { - const getAddSecretUI = async () => { + describe("adding a new variable", () => { + const getAddVariableUI = async () => { let nameInput; let valueInput; let saveButton; @@ -275,20 +275,20 @@ describe("Custom variables", () => { let user: UserEvent; beforeEach(async () => { ({ user } = render()); - let addSecretButton; + let addVariableButton; await waitFor(() => { - addSecretButton = screen.getByRole("button", { + addVariableButton = screen.getByRole("button", { name: /Add custom variable/, }); - expect(addSecretButton).toBeInTheDocument(); + expect(addVariableButton).toBeInTheDocument(); }); - if (!addSecretButton) { + if (!addVariableButton) { throw new Error("Add custom variable button not found"); } - await user.click(addSecretButton); + await user.click(addVariableButton); }); it("is successful with valid name and value", async () => { - const { nameInput, valueInput, saveButton } = await getAddSecretUI(); + const { nameInput, valueInput, saveButton } = await getAddVariableUI(); await user.type(nameInput, "New_Secret"); await user.type(valueInput, "Secret Value"); await user.click(saveButton); @@ -299,7 +299,7 @@ describe("Custom variables", () => { }); }); it("does not allow saving without name", async () => { - const { valueInput, saveButton } = await getAddSecretUI(); + const { valueInput, saveButton } = await getAddVariableUI(); await user.type(valueInput, "Secret Value"); await user.click(saveButton); await waitFor(() => { @@ -308,7 +308,7 @@ describe("Custom variables", () => { }); }); it("does not allow saving without value", async () => { - const { nameInput, saveButton } = await getAddSecretUI(); + const { nameInput, saveButton } = await getAddVariableUI(); await user.type(nameInput, "Secret Name"); await user.click(saveButton); await waitFor(() => { @@ -317,7 +317,7 @@ describe("Custom variables", () => { }); }); it("does not allow saving with invalid name", async () => { - const { nameInput, valueInput, saveButton } = await getAddSecretUI(); + const { nameInput, valueInput, saveButton } = await getAddVariableUI(); await user.type(nameInput, "COOL!"); // Invalid name await user.type(valueInput, "Secret Value"); await user.click(saveButton); @@ -331,7 +331,7 @@ describe("Custom variables", () => { }); }); it("does not allow saving very long name", async () => { - const { nameInput, valueInput, saveButton } = await getAddSecretUI(); + const { nameInput, valueInput, saveButton } = await getAddVariableUI(); await user.type(nameInput, new Array(256).fill("A").join("")); // Invalid name await user.type(valueInput, "a value"); await user.click(saveButton); @@ -344,30 +344,30 @@ describe("Custom variables", () => { }); }); - it("deleting a secret is successful", async () => { + it("deleting a variable is successful", async () => { const { user } = render(); await waitFor(() => { expect(screen.getByText("Add custom variable")).toBeInTheDocument(); }); // Get the element with SECRET_UNO in it. - let secretUno: HTMLElement | null = null; + let variableUno: HTMLElement | null = null; await waitFor(() => { - secretUno = screen.getByText("SECRET_UNO"); - expect(secretUno).toBeInTheDocument(); + variableUno = screen.getByText("SECRET_UNO"); + expect(variableUno).toBeInTheDocument(); }); - if (secretUno === null) { - throw new Error("Secret not found"); + if (variableUno === null) { + throw new Error("Variable not found"); } // Find the element with .paginated-list__row class that is ancestor to that element. - const secretUnoRow = (secretUno as HTMLElement).closest( + const variableUnoRow = (variableUno as HTMLElement).closest( ".paginated-list__row" ); - expect(secretUnoRow).toBeInTheDocument(); - if (!secretUnoRow) { - throw new Error("Secret row not found"); + expect(variableUnoRow).toBeInTheDocument(); + if (!variableUnoRow) { + throw new Error("Variable row not found"); } // Find the element with data-id="trash-icon" - const trashIcon = secretUnoRow.querySelector( + const trashIcon = variableUnoRow.querySelector( "[data-testid='trash-icon']" ); expect(trashIcon).toBeInTheDocument(); diff --git a/frontend/pages/ManageControlsPage/Variables/Variables.tsx b/frontend/pages/ManageControlsPage/Variables/Variables.tsx index ee968b784a9..f68a3bb8a99 100644 --- a/frontend/pages/ManageControlsPage/Variables/Variables.tsx +++ b/frontend/pages/ManageControlsPage/Variables/Variables.tsx @@ -265,7 +265,6 @@ const Variables = () => { data={data?.custom_variables || []} currentPage={pageNumber} onChangePage={setPageNumber} - onClickRow={(variable) => variable} heading={
Custom variables diff --git a/frontend/pages/ManageControlsPage/Variables/components/AddCustomVariableModal/AddCustomVariableModal.tsx b/frontend/pages/ManageControlsPage/Variables/components/AddCustomVariableModal/AddCustomVariableModal.tsx index ab15ca83be3..eb67100ba39 100644 --- a/frontend/pages/ManageControlsPage/Variables/components/AddCustomVariableModal/AddCustomVariableModal.tsx +++ b/frontend/pages/ManageControlsPage/Variables/components/AddCustomVariableModal/AddCustomVariableModal.tsx @@ -24,8 +24,8 @@ const AddCustomVariableModal = ({ onCancel, onSave, }: AddCustomVariableModalProps) => { - const [secretName, setSecretName] = useState(""); - const [secretValue, setSecretValue] = useState(""); + const [variableName, setVariableName] = useState(""); + const [variableValue, setVariableValue] = useState(""); const [isSaving, setIsSaving] = useState(false); const { renderFlash } = useContext(NotificationContext); @@ -34,22 +34,22 @@ const AddCustomVariableModal = ({ formValidation, setFormValidation, ] = useState(() => - validateFormData({ name: secretName, value: secretValue }) + validateFormData({ name: variableName, value: variableValue }) ); const onInputChange = (update: { name: string; value: string }) => { const name = update.name; let value = update.value; if (name === "name") { - value = value.trimRight().toUpperCase(); - setSecretName(value); + value = value.trimEnd().toUpperCase(); + setVariableName(value); } else if (name === "value") { - setSecretValue(value); + setVariableValue(value); } setFormValidation( validateFormData({ - name: secretName, - value: secretValue, + name: variableName, + value: variableValue, [update.name]: value, }) ); @@ -60,8 +60,8 @@ const AddCustomVariableModal = ({ if (validation.isValid) { setIsSaving(true); const newVariable: IVariablePayload = { - name: secretName, - value: secretValue, + name: variableName, + value: variableValue, }; try { await variablesAPI.addVariable(newVariable); @@ -89,21 +89,21 @@ const AddCustomVariableModal = ({
You can use this in your script or configuration profile as - “$FLEET_SECRET_{secretName}”. + “$FLEET_SECRET_{variableName}”. } error={formValidation.name?.message} />