Skip to content

Commit

Permalink
Merge branch 'main' into fix-tics
Browse files Browse the repository at this point in the history
  • Loading branch information
tmerten authored Feb 4, 2025
2 parents dacf2b8 + 811ccda commit da6cd39
Show file tree
Hide file tree
Showing 62 changed files with 1,366 additions and 1,483 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/cypress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ jobs:
with:
name: cypress-screenshots
path: cypress/screenshots
- name: Collect MAAS logs
if: failure()
shell: bash
run: journalctl -u snap.maas.pebble.service --since="1 hour ago" > cypress/maas_logs.txt
- uses: actions/upload-artifact@v4
if: failure()
with:
name: maas_logs
path: cypress/maas_logs.txt
- name: Create issue on failure
# Create an issue if the job fails on push to main or 3.* branches
if: failure() && github.event_name == 'push' && contains(fromJson('["main", "3.*"]'), env.BRANCH_NAME)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Community contributions are most welcome, and there are a number of ways to part
- [Submit bugs and feature requests](https://maas.io/docs/how-to-review-and-report-bugs)
- [Assist with code review](https://github.com/canonical/maas-ui/pulls)
- [Submit bugs for the MAAS website](https://github.com/canonical/maas.io)
- [Contribute to MAAS documentation](https://maas.io/docs/writing-guide)
- [Contribute to MAAS documentation](https://maas.io/docs/how-to-contribute-to-maas-documentation)

When submitting a PR, please take note that MAAS UI uses the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) format. To help you conform to this, you can run `yarn commit` instead of `git commit` for an interactive prompt.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"typed-redux-saga": "1.5.0",
"typescript": "5.6.3",
"vanilla-framework": "4.16.0",
"vite": "5.2.14",
"vite": "5.4.12",
"vite-plugin-svgr": "4.2.0",
"vite-tsconfig-paths": "5.1.3",
"yup": "0.32.11"
Expand Down
1 change: 1 addition & 0 deletions scripts/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ app.use(
onProxyReq(proxyReq) {
// Django's CSRF protection requires requests to come from the correct
// protocol, so this makes XHR requests work when using TLS certs.
proxyReq.setHeader("Origin", `${process.env.MAAS_URL.replace(/\/$/, "")}`);
proxyReq.setHeader("Referer", `${process.env.MAAS_URL}${proxyReq.path}`);
},
secure: false,
Expand Down
4 changes: 0 additions & 4 deletions src/app/Routes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,6 @@ const routes: { title: string; path: string }[] = [
title: "Zones",
path: urls.zones.index,
},
{
title: "test-zone",
path: urls.zones.details({ id: 1 }),
},
{
title: "Network Discovery",
path: urls.networkDiscovery.index,
Expand Down
11 changes: 1 addition & 10 deletions src/app/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const SubnetDetails = lazy(() => import("@/app/subnets/views/SubnetDetails"));
const SubnetsList = lazy(() => import("@/app/subnets/views/SubnetsList"));
const VLANDetails = lazy(() => import("@/app/subnets/views/VLANDetails"));
const Tags = lazy(() => import("@/app/tags/views/Tags"));
const ZoneDetails = lazy(() => import("@/app/zones/views/ZoneDetails"));
const ZonesList = lazy(() => import("@/app/zones/views/ZonesList"));

const Routes = (): JSX.Element => (
Expand All @@ -52,21 +51,13 @@ const Routes = (): JSX.Element => (
}
path={urls.machines.index}
/>
<Route
element={
<ErrorBoundary>
<ZoneDetails />
</ErrorBoundary>
}
path={`${urls.zones.details(null)}/*`}
/>
<Route
element={
<ErrorBoundary>
<ZonesList />
</ErrorBoundary>
}
path={`${urls.zones.index}/*`}
path={`${urls.zones.index}`}
/>
<Route
element={
Expand Down
5 changes: 0 additions & 5 deletions src/app/api/query-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,19 @@ type QueryKeySubcategories<T extends QueryKeyCategories> = keyof QueryKeys[T];
export type QueryKey =
QueryKeys[QueryKeyCategories][QueryKeySubcategories<QueryKeyCategories>];

// first element of the queryKeys array
// Idk what Peter meant by the above comment, but QueryModel is basically QueryKey but not in an array from what I understand.
export type QueryModel = QueryKey[number];

// 5 minutes feels rather long for default stale time.
export const defaultQueryOptions = {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 15 * 60 * 1000, // 15 minutes
refetchOnWindowFocus: true,
} as const;

// 0 is far too quick lol but at least we're not using this.
export const realTimeQueryOptions = {
staleTime: 0,
cacheTime: 60 * 1000, // 1 minute
} as const;

// This just creates a query client and provides the options specified above.
export const createQueryClient = () =>
new QueryClient({
defaultOptions: {
Expand Down
2 changes: 1 addition & 1 deletion src/app/base/components/AppSideNavigation/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const navGroups: NavGroup[] = [
url: urls.tags.index,
},
{
highlight: [urls.zones.index, urls.zones.details(null)],
highlight: [urls.zones.index],
label: "AZs",
url: urls.zones.index,
},
Expand Down
4 changes: 0 additions & 4 deletions src/app/base/components/GenericTable/GenericTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ describe("GenericTable", () => {

const mockFilterCells = vi.fn(() => true);
const mockFilterHeaders = vi.fn(() => true);
const mockGetRowId = vi.fn((row) => row.id.toString());

it("renders table with headers and rows", () => {
render(
Expand All @@ -76,7 +75,6 @@ describe("GenericTable", () => {
data={data}
filterCells={mockFilterCells}
filterHeaders={mockFilterHeaders}
getRowId={mockGetRowId}
rowSelection={{}}
setRowSelection={vi.fn}
/>
Expand All @@ -96,7 +94,6 @@ describe("GenericTable", () => {
data={[]}
filterCells={mockFilterCells}
filterHeaders={mockFilterHeaders}
getRowId={mockGetRowId}
noData={<span>No data</span>}
rowSelection={{}}
setRowSelection={vi.fn}
Expand All @@ -113,7 +110,6 @@ describe("GenericTable", () => {
data={data}
filterCells={mockFilterCells}
filterHeaders={mockFilterHeaders}
getRowId={mockGetRowId}
rowSelection={{}}
setRowSelection={vi.fn}
/>
Expand Down
148 changes: 90 additions & 58 deletions src/app/base/components/GenericTable/GenericTable.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,124 @@
import type { Dispatch, ReactNode, SetStateAction } from "react";
import { useMemo, useState } from "react";
import {
type Dispatch,
type ReactNode,
type SetStateAction,
useMemo,
useState,
} from "react";

import { DynamicTable } from "@canonical/maas-react-components";
import { Button } from "@canonical/react-components";
import type {
Column,
Row,
ColumnDef,
ColumnSort,
ExpandedState,
GroupingState,
ExpandedState,
SortingState,
Header,
Row,
RowSelectionState,
SortingState,
} from "@tanstack/react-table";
import {
flexRender,
getCoreRowModel,
getExpandedRowModel,
getGroupedRowModel,
useReactTable,
flexRender,
} from "@tanstack/react-table";
import classNames from "classnames";

import TableCheckbox from "@/app/base/components/GenericTable/TableCheckbox";
import TableHeader from "@/app/base/components/GenericTable/TableHeader";

import "./_index.scss";
import SortingIndicator from "./SortingIndicator";

type GenericTableProps<T> = {
ariaLabel?: string;
type GenericTableProps<T extends { id: string | number }> = {
canSelect?: boolean;
columns: ColumnDef<T, Partial<T>>[];
data: T[];
filterCells: (row: Row<T>, column: Column<T>) => boolean;
filterHeaders: (header: Header<T, unknown>) => boolean;
getRowId: (
originalRow: T,
index: number,
parent?: Row<T> | undefined
) => string;
filterCells?: (row: Row<T>, column: Column<T>) => boolean;
filterHeaders?: (header: Header<T, unknown>) => boolean;
groupBy?: string[];
noData?: ReactNode;
pin?: { value: string; isTop: boolean }[];
sortBy?: ColumnSort[];
rowSelection: RowSelectionState;
rowSelection?: RowSelectionState;
setRowSelection?: Dispatch<SetStateAction<RowSelectionState>>;
};

const GenericTable = <T,>({
ariaLabel,
const GenericTable = <T extends { id: string | number }>({
canSelect = false,
columns,
data,
filterCells,
filterHeaders,
getRowId,
filterCells = () => true,
filterHeaders = () => true,
groupBy,
sortBy,
noData,
pin,
sortBy,
rowSelection,
setRowSelection,
}: GenericTableProps<T>) => {
const [grouping, setGrouping] = useState<GroupingState>(groupBy ?? []);
const [expanded, setExpanded] = useState<ExpandedState>(true);
const [sorting, setSorting] = useState<SortingState>(sortBy ?? []);

const sortedData = useMemo(() => {
if (canSelect) {
columns = [
{
id: "select",
accessorKey: "id",
enableSorting: false,
header: "",
cell: ({ row }) =>
!row.getIsGrouped() ? <TableCheckbox row={row} /> : null,
},
...columns,
];

if (groupBy) {
columns = [
{
id: "group-select",
accessorKey: "id",
enableSorting: false,
header: ({ table }) => <TableCheckbox.All table={table} />,
cell: ({ row }) =>
row.getIsGrouped() ? <TableCheckbox.Group row={row} /> : null,
},
...columns,
];
}
}

data = useMemo(() => {
return [...data].sort((a, b) => {
if (pin && pin.length > 0 && grouping.length > 0) {
for (const { value, isTop } of pin) {
const groupId = grouping[0];
const aValue = a[groupId as keyof typeof a];
const bValue = b[groupId as keyof typeof b];

if (aValue === value && bValue !== value) {
return isTop ? -1 : 1;
}
if (bValue === value && aValue !== value) {
return isTop ? 1 : -1;
}
}
}

for (const groupId of grouping) {
const aGroupValue = a[groupId as keyof typeof a];
const bGroupValue = b[groupId as keyof typeof b];
if (aGroupValue < bGroupValue) {
return -1;
}
if (aGroupValue > bGroupValue) {
return 1;
}
}

for (const { id, desc } of sorting) {
const aValue = a[id as keyof typeof a];
const bValue = b[id as keyof typeof b];
Expand All @@ -75,10 +131,10 @@ const GenericTable = <T,>({
}
return 0;
});
}, [data, sorting]);
}, [data, sorting, grouping, pin]);

const table = useReactTable<T>({
data: sortedData,
data,
columns,
state: {
grouping,
Expand All @@ -99,42 +155,18 @@ const GenericTable = <T,>({
getCoreRowModel: getCoreRowModel(),
getGroupedRowModel: getGroupedRowModel(),
groupedColumnMode: false,
enableRowSelection: true,
enableMultiRowSelection: true,
getRowId,
enableRowSelection: canSelect,
enableMultiRowSelection: canSelect,
getRowId: (originalRow) => originalRow.id.toString(),
});

return (
<DynamicTable
aria-label={ariaLabel}
className="p-table-dynamic--with-select generic-table"
variant={"full-height"}
>
<DynamicTable className="p-generic-table" variant="full-height">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.filter(filterHeaders).map((header) => (
<th className={classNames(`${header.column.id}`)} key={header.id}>
{header.column.getCanSort() ? (
<Button
appearance="link"
className="p-button--table-header"
onClick={header.column.getToggleSortingHandler()}
type="button"
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
<SortingIndicator header={header} />
</Button>
) : (
flexRender(
header.column.columnDef.header,
header.getContext()
)
)}
</th>
<TableHeader header={header} key={header.id} />
))}
</tr>
))}
Expand All @@ -144,15 +176,15 @@ const GenericTable = <T,>({
) : (
<DynamicTable.Body>
{table.getRowModel().rows.map((row) => {
const { getIsGrouped, id, index, getVisibleCells } = row;
const { getIsGrouped, id, getVisibleCells } = row;
const isIndividualRow = !getIsGrouped();
return (
<tr
className={classNames({
"individual-row": isIndividualRow,
"group-row": !isIndividualRow,
})}
key={id + index}
key={id}
>
{getVisibleCells()
.filter((cell) => filterCells(row, cell.column))
Expand Down
Loading

0 comments on commit da6cd39

Please sign in to comment.