sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #05467
[Merge] ~petermakowski/maas-site-manager:add-hidden-columns-MAASENG-1391 into maas-site-manager:main
Peter Makowski has proposed merging ~petermakowski/maas-site-manager:add-hidden-columns-MAASENG-1391 into maas-site-manager:main.
Commit message:
add hidden columns MAASENG-1391
Requested reviews:
MAAS Committers (maas-committers)
For more details, see:
https://code.launchpad.net/~petermakowski/maas-site-manager/+git/site-manager/+merge/438107
- add Placeholder component
- add response delay for more realistic server response time when using the mock server
- add e2e test for hiding sites table columns
- ignore stateless components in react/no-multi-comp eslint rule (all this was doing is maas-ui was encouraging writing functions instead of React components to bypass this rule without having to create lots of small files)
--
Your team MAAS Committers is requested to review the proposed merge of ~petermakowski/maas-site-manager:add-hidden-columns-MAASENG-1391 into maas-site-manager:main.
diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js
index 5d90fb2..242261d 100644
--- a/frontend/.eslintrc.js
+++ b/frontend/.eslintrc.js
@@ -119,7 +119,7 @@ module.exports = {
{
files: ["src/**/*.tsx"],
rules: {
- "react/no-multi-comp": ["error", { ignoreStateless: false }],
+ "react/no-multi-comp": ["error", { ignoreStateless: true }],
},
},
{
diff --git a/frontend/package.json b/frontend/package.json
index c3c0f84..726b091 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -15,6 +15,7 @@
"@canonical/react-components": "0.38.0",
"@tanstack/react-table": "8.7.9",
"axios": "1.3.4",
+ "classnames": "2.3.2",
"date-fns": "2.29.3",
"date-fns-tz": "2.0.0",
"lodash": "4.17.21",
@@ -22,6 +23,7 @@
"react-dom": "18.2.0",
"react-query": "3.39.3",
"react-router-dom": "6.8.1",
+ "use-local-storage-state": "18.1.2",
"vanilla-framework": "3.11.0"
},
"devDependencies": {
diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts
index cf34b9f..d57e481 100644
--- a/frontend/playwright.config.ts
+++ b/frontend/playwright.config.ts
@@ -6,7 +6,7 @@ import { defineConfig, devices } from "@playwright/test";
*/
import dotenv from "dotenv";
-dotenv.config();
+dotenv.config({ path: "../.env" });
/**
* See https://playwright.dev/docs/test-configuration.
@@ -37,7 +37,7 @@ export default defineConfig({
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
- baseURL: `http://localhost:${process.env.UI_PORT}`,
+ baseURL: `http://localhost:${process.env.VITE_UI_PORT}`,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
index c8495ac..5314e6f 100644
--- a/frontend/src/api/types.ts
+++ b/frontend/src/api/types.ts
@@ -18,9 +18,13 @@ export type Site = {
};
};
-export type Sites = {
- items: Site[];
+export type PaginatedQueryResult = {
+ items: unknown[];
total: number;
page: number;
size: number;
};
+
+export type SitesQueryResult = PaginatedQueryResult & {
+ items: Site[];
+};
diff --git a/frontend/src/components/Placeholder/Placeholder.scss b/frontend/src/components/Placeholder/Placeholder.scss
new file mode 100644
index 0000000..cc9ed21
--- /dev/null
+++ b/frontend/src/components/Placeholder/Placeholder.scss
@@ -0,0 +1,25 @@
+.p-placeholder {
+ animation: shine 1.5s infinite ease-out;
+ background-color: $color-mid-x-light;
+ background-image: linear-gradient(
+ to right,
+ $color-mid-x-light calc(50% - 2rem),
+ $color-light 50%,
+ $color-mid-x-light calc(50% + 2rem)
+ );
+ background-size: 300% 100%;
+ color: transparent;
+ overflow: hidden;
+ pointer-events: none;
+ text-indent: -100%;
+}
+
+@keyframes shine {
+ 0% {
+ background-position: right;
+ }
+
+ 100% {
+ background-position: left;
+ }
+}
diff --git a/frontend/src/components/Placeholder/Placeholder.test.tsx b/frontend/src/components/Placeholder/Placeholder.test.tsx
new file mode 100644
index 0000000..e963ac9
--- /dev/null
+++ b/frontend/src/components/Placeholder/Placeholder.test.tsx
@@ -0,0 +1,26 @@
+import { render, screen } from "../../test-utils";
+
+import Placeholder from "./Placeholder";
+
+describe("Placeholder", () => {
+ it("always hides placeholder text passed as a text prop", () => {
+ const { rerender } = render(<Placeholder text="Placeholder text" />);
+ expect(screen.queryByText(/Placeholder text/)).not.toBeInTheDocument();
+ rerender(<Placeholder isLoading text="Placeholder text" />);
+ expect(screen.queryByText(/Placeholder text/)).toHaveAttribute("aria-hidden", "true");
+ });
+
+ it("hides the children when loading", () => {
+ const { rerender } = render(<Placeholder>Placeholder children</Placeholder>);
+ expect(screen.getByText(/Placeholder children/)).toBeInTheDocument();
+ rerender(<Placeholder isLoading>Placeholder children</Placeholder>);
+ expect(screen.queryByText(/Placeholder children/)).toHaveAttribute("aria-hidden", "true");
+ });
+
+ it("does not return placeholder styles when isLoading is false", () => {
+ const { rerender } = render(<Placeholder>Placeholder children</Placeholder>);
+ expect(screen.queryByTestId("placeholder")).not.toBeInTheDocument();
+ rerender(<Placeholder isLoading>Placeholder children</Placeholder>);
+ expect(screen.getByTestId("placeholder")).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/components/Placeholder/Placeholder.tsx b/frontend/src/components/Placeholder/Placeholder.tsx
new file mode 100644
index 0000000..cab700c
--- /dev/null
+++ b/frontend/src/components/Placeholder/Placeholder.tsx
@@ -0,0 +1,38 @@
+import type { ReactNode } from "react";
+
+import classNames from "classnames";
+
+import "./Placeholder.scss";
+
+type Props = {
+ className?: string;
+ isLoading?: boolean;
+} & (
+ | {
+ text?: never;
+ children: ReactNode;
+ }
+ | {
+ text: string;
+ children?: never;
+ }
+);
+
+const Placeholder = ({ text, children, className, isLoading = false }: Props) => {
+ const delay = Math.floor(Math.random() * 750);
+ if (isLoading) {
+ return (
+ <span
+ aria-hidden={true}
+ className={classNames("p-placeholder", className)}
+ data-testid="placeholder"
+ style={{ animationDelay: `${delay}ms` }}
+ >
+ {text || children}
+ </span>
+ );
+ }
+ return <>{children}</>;
+};
+
+export default Placeholder;
diff --git a/frontend/src/components/Placeholder/index.ts b/frontend/src/components/Placeholder/index.ts
new file mode 100644
index 0000000..72011f2
--- /dev/null
+++ b/frontend/src/components/Placeholder/index.ts
@@ -0,0 +1 @@
+export { default } from "./Placeholder";
diff --git a/frontend/src/components/SitesList/SitesList.test.tsx b/frontend/src/components/SitesList/SitesList.test.tsx
index 7b067dd..b944366 100644
--- a/frontend/src/components/SitesList/SitesList.test.tsx
+++ b/frontend/src/components/SitesList/SitesList.test.tsx
@@ -27,7 +27,7 @@ it("renders header", () => {
it("displays loading text", () => {
render(<SitesList />);
- expect(screen.getByText(/loading/i)).toBeInTheDocument();
+ expect(within(screen.getByRole("table", { name: /sites/i })).getByText(/loading/i)).toBeInTheDocument();
});
it("displays populated sites table", async () => {
diff --git a/frontend/src/components/SitesList/SitesList.tsx b/frontend/src/components/SitesList/SitesList.tsx
index 5583a75..682c17a 100644
--- a/frontend/src/components/SitesList/SitesList.tsx
+++ b/frontend/src/components/SitesList/SitesList.tsx
@@ -3,13 +3,11 @@ import { useSitesQuery } from "../../hooks/api";
import SitesTable from "./components/SitesTable";
const SitesList = () => {
- const query = useSitesQuery();
+ const { data, isLoading, isFetchedAfterMount } = useSitesQuery();
return (
<div>
- <h2 className="p-heading--4">{query?.data?.items?.length || ""} MAAS Regions</h2>
- {query.isLoading && !query.isFetchedAfterMount ? "Loading..." : null}
- {query.isFetchedAfterMount && query.data ? <SitesTable data={query.data.items} /> : null}
+ <SitesTable data={data} isFetchedAfterMount={isFetchedAfterMount} isLoading={isLoading} />
</div>
);
};
diff --git a/frontend/src/components/SitesList/components/ColumnsVisibilityControl.scss b/frontend/src/components/SitesList/components/ColumnsVisibilityControl.scss
new file mode 100644
index 0000000..8343345
--- /dev/null
+++ b/frontend/src/components/SitesList/components/ColumnsVisibilityControl.scss
@@ -0,0 +1,26 @@
+@include vf-p-icon-settings;
+
+.columns-visibility-checkbox {
+ &:first-of-type label {
+ padding-top: $sph--small;
+ }
+ label {
+ padding: 0 $sph--large $sph--x-small $sph--large;
+ text-transform: capitalize;
+ width: 100%;
+ text-indent: 0;
+ }
+}
+
+[class^="p-button"].has-icon.columns-visibility-toggle {
+ @extend %vf-input-elements;
+ padding-left: $spv--small;
+ text-align: left;
+ width: 100%;
+ text-transform: capitalize;
+
+ i[class*="p-icon"]:last-child {
+ margin-left: 0;
+ margin-right: $spv--x-small;
+ }
+}
diff --git a/frontend/src/components/SitesList/components/ColumnsVisibilityControl.tsx b/frontend/src/components/SitesList/components/ColumnsVisibilityControl.tsx
new file mode 100644
index 0000000..c799b88
--- /dev/null
+++ b/frontend/src/components/SitesList/components/ColumnsVisibilityControl.tsx
@@ -0,0 +1,44 @@
+import { ContextualMenu, Icon, CheckboxInput } from "@canonical/react-components";
+
+import "./ColumnsVisibilityControl.scss";
+import type { SitesColumn } from "./SitesTable";
+
+function ColumnsVisibilityControl({ columns }: { columns: SitesColumn[] }) {
+ return (
+ <ContextualMenu
+ className="filter-accordion"
+ constrainPanelWidth
+ dropdownProps={{ "aria-label": "columns menu" }}
+ position="left"
+ toggleClassName="columns-visibility-toggle has-icon"
+ toggleLabel={
+ <>
+ <Icon name="settings" /> Columns
+ </>
+ }
+ toggleLabelFirst={true}
+ >
+ <div className="columns-visibility-select-wrapper u-no-padding--top">
+ {columns
+ .filter((column) => column.id !== "select")
+ .map((column) => {
+ return (
+ <div className="columns-visibility-checkbox">
+ <CheckboxInput
+ aria-label={column.id}
+ key={column.id}
+ label={column.id}
+ {...{
+ checked: column.getIsVisible(),
+ onChange: column.getToggleVisibilityHandler(),
+ }}
+ />
+ </div>
+ );
+ })}
+ </div>
+ </ContextualMenu>
+ );
+}
+
+export default ColumnsVisibilityControl;
diff --git a/frontend/src/components/SitesList/components/SitesTable.scss b/frontend/src/components/SitesList/components/SitesTable.scss
new file mode 100644
index 0000000..37b435f
--- /dev/null
+++ b/frontend/src/components/SitesList/components/SitesTable.scss
@@ -0,0 +1,5 @@
+.sites-table {
+ thead th:first-child {
+ width: 3rem;
+ }
+}
diff --git a/frontend/src/components/SitesList/components/SitesTable.test.tsx b/frontend/src/components/SitesList/components/SitesTable.test.tsx
index cb4ff72..38fce0d 100644
--- a/frontend/src/components/SitesList/components/SitesTable.test.tsx
+++ b/frontend/src/components/SitesList/components/SitesTable.test.tsx
@@ -18,14 +18,14 @@ afterEach(() => {
});
it("displays an empty sites table", () => {
- render(<SitesTable data={[]} />);
+ render(<SitesTable data={{ items: [], total: 0, page: 1, size: 0 }} isFetchedAfterMount={true} isLoading={false} />);
expect(screen.getByRole("table", { name: /sites/i })).toBeInTheDocument();
});
it("displays rows with details for each site", () => {
const items = sites().items as Site[];
- render(<SitesTable data={items} />);
+ render(<SitesTable data={{ items, total: 1, page: 1, size: 1 }} isFetchedAfterMount={true} isLoading={false} />);
expect(screen.getByRole("table", { name: /sites/i })).toBeInTheDocument();
@@ -41,7 +41,9 @@ it("displays correct local time", () => {
vi.setSystemTime(date);
const item = site({ timezone: "CET" });
- render(<SitesTable data={[item]} />);
+ render(
+ <SitesTable data={{ items: [item], total: 1, page: 1, size: 1 }} isFetchedAfterMount={true} isLoading={false} />,
+ );
expect(screen.getByRole("table", { name: /sites/i })).toBeInTheDocument();
expect(screen.getByText(/13:00 \(local time\)/i)).toBeInTheDocument();
diff --git a/frontend/src/components/SitesList/components/SitesTable.tsx b/frontend/src/components/SitesList/components/SitesTable.tsx
index 1aa8054..7791887 100644
--- a/frontend/src/components/SitesList/components/SitesTable.tsx
+++ b/frontend/src/components/SitesList/components/SitesTable.tsx
@@ -8,22 +8,37 @@ import {
getFilteredRowModel,
getPaginationRowModel,
} from "@tanstack/react-table";
-import type { ColumnDef } from "@tanstack/react-table";
+import type { ColumnDef, Column } from "@tanstack/react-table";
import { format } from "date-fns";
import { utcToZonedTime } from "date-fns-tz";
import pick from "lodash/fp/pick";
+import useLocalStorageState from "use-local-storage-state";
-import type { Site } from "../../../api/types";
+import type { SitesQueryResult } from "../../../api/types";
+import type { UseSitesQueryResult } from "../../../hooks/api";
+
+import "./SitesTable.scss";
+import SitesTableControls from "./SitesTableControls";
const createAccessor =
<T, K extends keyof T>(keys: K[] | K) =>
(row: T) =>
pick(keys, row);
-const SitesTable = ({ data }: { data: Site[] }) => {
- const [columnVisibility, setColumnVisibility] = useState({});
+export type Site = SitesQueryResult["items"][number];
+export type SitesColumnDef = ColumnDef<Site, Partial<Site>>;
+export type SitesColumn = Column<Site, unknown>;
+
+const SitesTable = ({
+ data,
+ isFetchedAfterMount,
+ isLoading,
+}: Pick<UseSitesQueryResult, "data" | "isLoading" | "isFetchedAfterMount">) => {
+ const [columnVisibility, setColumnVisibility] = useLocalStorageState("sitesTableColumnVisibility", {
+ defaultValue: {},
+ });
- const columns = useMemo<ColumnDef<Site, Partial<Site>>[]>(
+ const columns = useMemo<SitesColumnDef[]>(
() => [
{
id: "select",
@@ -155,7 +170,7 @@ const SitesTable = ({ data }: { data: Site[] }) => {
const [rowSelection, setRowSelection] = useState({});
const table = useReactTable<Site>({
- data: data || [],
+ data: data?.items || [],
columns,
state: {
rowSelection,
@@ -175,39 +190,46 @@ const SitesTable = ({ data }: { data: Site[] }) => {
});
return (
- <table aria-label="sites" className="u-table-layout--auto">
- <thead>
- {table.getHeaderGroups().map((headerGroup) => (
- <tr key={headerGroup.id}>
- {headerGroup.headers.map((header) => {
+ <>
+ <SitesTableControls allColumns={table.getAllLeafColumns()} data={data} isLoading={isLoading} />
+ <table aria-label="sites" className="sites-table">
+ <thead>
+ {table.getHeaderGroups().map((headerGroup) => (
+ <tr key={headerGroup.id}>
+ {headerGroup.headers.map((header) => {
+ return (
+ <th colSpan={header.colSpan} key={header.id}>
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
+ {header.column.getCanResize() && (
+ <div
+ className={`resizer ${header.column.getIsResizing() ? "isResizing" : ""}`}
+ onMouseDown={header.getResizeHandler()}
+ onTouchStart={header.getResizeHandler()}
+ ></div>
+ )}
+ </th>
+ );
+ })}
+ </tr>
+ ))}
+ </thead>
+ {isLoading && !isFetchedAfterMount ? (
+ <caption>Loading...</caption>
+ ) : (
+ <tbody>
+ {table.getRowModel().rows.map((row) => {
return (
- <th colSpan={header.colSpan} key={header.id}>
- {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
- {header.column.getCanResize() && (
- <div
- className={`resizer ${header.column.getIsResizing() ? "isResizing" : ""}`}
- onMouseDown={header.getResizeHandler()}
- onTouchStart={header.getResizeHandler()}
- ></div>
- )}
- </th>
+ <tr key={row.id}>
+ {row.getVisibleCells().map((cell) => {
+ return <td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>;
+ })}
+ </tr>
);
})}
- </tr>
- ))}
- </thead>
- <tbody>
- {table.getRowModel().rows.map((row) => {
- return (
- <tr key={row.id}>
- {row.getVisibleCells().map((cell) => {
- return <td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>;
- })}
- </tr>
- );
- })}
- </tbody>
- </table>
+ </tbody>
+ )}
+ </table>
+ </>
);
};
diff --git a/frontend/src/components/SitesList/components/SitesTableControls.tsx b/frontend/src/components/SitesList/components/SitesTableControls.tsx
new file mode 100644
index 0000000..076e506
--- /dev/null
+++ b/frontend/src/components/SitesList/components/SitesTableControls.tsx
@@ -0,0 +1,31 @@
+import { Row, Col } from "@canonical/react-components";
+
+import type { UseSitesQueryResult } from "../../../hooks/api";
+import Placeholder from "../../Placeholder";
+
+import ColumnsVisibilityControl from "./ColumnsVisibilityControl";
+import type { SitesColumn } from "./SitesTable";
+
+const SitesCount = ({ data, isLoading }: Pick<UseSitesQueryResult, "data" | "isLoading">) =>
+ isLoading ? <Placeholder isLoading={isLoading} text="xx" /> : <span>{`${data?.items?.length || ""}`}</span>;
+
+const SitesTableControls = ({
+ data,
+ isLoading,
+ allColumns,
+}: { allColumns: SitesColumn[] } & Pick<UseSitesQueryResult, "data" | "isLoading">) => {
+ return (
+ <Row>
+ <Col size={10}>
+ <h2 className="p-heading--4">
+ <SitesCount data={data} isLoading={isLoading} /> MAAS Regions
+ </h2>
+ </Col>
+ <Col size={2}>
+ <ColumnsVisibilityControl columns={allColumns} />
+ </Col>
+ </Row>
+ );
+};
+
+export default SitesTableControls;
diff --git a/frontend/src/hooks/api.ts b/frontend/src/hooks/api.ts
index 9ae36ae..71943b2 100644
--- a/frontend/src/hooks/api.ts
+++ b/frontend/src/hooks/api.ts
@@ -1,6 +1,7 @@
import { useQuery } from "react-query";
import { getSites } from "../api/handlers";
-import type { Sites } from "../api/types";
+import type { SitesQueryResult } from "../api/types";
-export const useSitesQuery = () => useQuery<Sites>("/api/sites", getSites);
+export const useSitesQuery = () => useQuery<SitesQueryResult>("/api/sites", getSites);
+export type UseSitesQueryResult = ReturnType<typeof useSitesQuery>;
diff --git a/frontend/src/mocks/browser.ts b/frontend/src/mocks/browser.ts
index a0feda2..cdc1e21 100644
--- a/frontend/src/mocks/browser.ts
+++ b/frontend/src/mocks/browser.ts
@@ -6,6 +6,6 @@ import { sites } from "./factories";
export const worker = setupWorker(
rest.get(urls.sites, (_req, res, ctx) => {
- return res(ctx.json(sites()));
+ return res(ctx.delay(), ctx.json(sites()));
}),
);
diff --git a/frontend/tests/sites.spec.ts b/frontend/tests/sites.spec.ts
new file mode 100644
index 0000000..072e2fa
--- /dev/null
+++ b/frontend/tests/sites.spec.ts
@@ -0,0 +1,26 @@
+import { test, expect } from "@playwright/test";
+
+test.beforeEach(async ({ page }) => {
+ await page.goto("/sites");
+});
+
+test("can hide table columns", async ({ page }) => {
+ const columnsCount = 6;
+ const columnHeaders = await page.locator("th");
+ await expect(columnHeaders).toHaveCount(columnsCount);
+ await expect(columnHeaders).toHaveText(["", /name/i, /connection/i, /country/i, /local time/i, /machines/i]);
+ await page.getByRole("button", { name: "Columns" }).click();
+ await page.getByLabel("submenu").getByRole("checkbox", { name: "connection" }).click({ force: true });
+
+ const updatedColumnHeaders = await page.locator("th");
+ expect(updatedColumnHeaders).toHaveCount(columnsCount - 1);
+
+ await expect(columnHeaders).toHaveText(["", /name/i, /country/i, /local time/i, /machines/i]);
+
+ await page.reload();
+
+ // verify that the hidden column is still hidden after refresh
+ const refreshedColumnHeaders = await page.locator("th");
+ expect(refreshedColumnHeaders).toHaveCount(columnsCount - 1);
+ await expect(refreshedColumnHeaders).toHaveText(["", /name/i, /country/i, /local time/i, /machines/i]);
+});
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index cde0616..078a2de 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -8,4 +8,9 @@ dotenv.config({ path: "../.env" });
export default defineConfig({
plugins: [react()],
server: { port: Number(process.env.VITE_UI_PORT) },
+ css: {
+ preprocessorOptions: {
+ scss: { additionalData: `@import "node_modules/vanilla-framework"; @include vanilla;` },
+ },
+ },
});
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 3d2f493..8b0b656 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -2332,6 +2332,11 @@ classnames@2.3.1:
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
+classnames@2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
+ integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
+
cli-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
@@ -5400,6 +5405,11 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
+use-local-storage-state@18.1.2:
+ version "18.1.2"
+ resolved "https://registry.yarnpkg.com/use-local-storage-state/-/use-local-storage-state-18.1.2.tgz#f131c0aa3803742ca261c547cdfd9d61e848581d"
+ integrity sha512-V+kYQNC5R0N/JDpsg6b4ED5UaItKJcSvbne68DwJDZWHxGMQBiF41ATktFIOyet3PIq30d2qtzVp/2aB6hQ8Bg==
+
use-ssr@^1.0.22:
version "1.0.24"
resolved "https://registry.yarnpkg.com/use-ssr/-/use-ssr-1.0.24.tgz#213a3df58f5ab9268e6fe1a57ad0a9de91e514d1"
Follow ups
-
[Merge] ~petermakowski/maas-site-manager:add-hidden-columns-MAASENG-1391 into maas-site-manager:main
From: Peter Makowski, 2023-03-01
-
Re: [UNITTESTS] -b add-hidden-columns-MAASENG-1391 lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - TESTS FAILED
From: MAAS Lander, 2023-03-01
-
Re: [Merge] ~petermakowski/maas-site-manager:add-hidden-columns-MAASENG-1391 into maas-site-manager:main
From: Jones Ogolo, 2023-03-01
-
[Merge] ~petermakowski/maas-site-manager:add-hidden-columns-MAASENG-1391 into maas-site-manager:main
From: MAAS Lander, 2023-03-01
-
Re: [Merge] -b add-hidden-columns-MAASENG-1391 lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - LANDING FAILED
From: MAAS Lander, 2023-03-01
-
[Merge] ~petermakowski/maas-site-manager:add-hidden-columns-MAASENG-1391 into maas-site-manager:main
From: MAAS Lander, 2023-03-01
-
[Merge] ~petermakowski/maas-site-manager:add-hidden-columns-MAASENG-1391 into maas-site-manager:main
From: Peter Makowski, 2023-03-01
-
Re: [Merge] ~petermakowski/maas-site-manager:add-hidden-columns-MAASENG-1391 into maas-site-manager:main
From: Nick De Villiers, 2023-03-01
-
Re: [UNITTESTS] -b add-hidden-columns-MAASENG-1391 lp:~petermakowski/maas-site-manager/+git/site-manager into -b main lp:~maas-committers/maas-site-manager - TESTS PASS
From: MAAS Lander, 2023-03-01
-
[Merge] ~petermakowski/maas-site-manager:add-hidden-columns-MAASENG-1391 into maas-site-manager:main
From: Peter Makowski, 2023-03-01