From a7987a7f3a569cba2016f913041fe757c6c317f6 Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Tue, 22 Oct 2024 16:46:39 +0300 Subject: [PATCH 1/5] fix: lazy load individual pages for reduced bundle size --- src/App.tsx | 219 ++++++++++++++++++++++----- src/ui/Layout/SidebarLayout.tsx | 4 +- src/ui/MRTDataTable/MRTDataTable.tsx | 8 +- 3 files changed, 192 insertions(+), 39 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 86b6b4b23..d04d3cbb8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { AdjustmentsIcon } from "@heroicons/react/solid"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { Provider } from "jotai"; +import dynamic from "next/dynamic"; import React, { ReactNode, useEffect, useState } from "react"; import { IconType } from "react-icons"; import { AiFillHeart } from "react-icons/ai"; @@ -21,14 +22,9 @@ import { useLocation } from "react-router-dom"; import { Canary, Icon } from "./components"; -import AgentsPage from "./components/Agents/AgentPage"; import AuthProviderWrapper from "./components/Authentication/AuthProviderWrapper"; import { ErrorBoundary } from "./components/ErrorBoundary"; -import EditIntegrationPage from "./components/Integrations/EditIntegrationPage"; -import IntegrationsPage from "./components/Integrations/IntegrationsPage"; -import JobsHistorySettingsPage from "./components/JobsHistory/JobsHistorySettingsPage"; import { withAuthorizationAccessCheck } from "./components/Permissions/AuthorizationAccessCheck"; -import { SchemaResourcePage } from "./components/SchemaResourcePage"; import { SchemaResource } from "./components/SchemaResourcePage/SchemaResource"; import { SchemaResourceType, @@ -40,36 +36,6 @@ import { HealthPageContextProvider } from "./context/HealthPageContext"; import { IncidentPageContextProvider } from "./context/IncidentPageContext"; import { UserAccessStateContextProvider } from "./context/UserAccessContext/UserAccessContext"; import { tables } from "./context/UserAccessContext/permissions"; -import { - ConfigChangesPage, - ConfigDetailsChangesPage, - ConfigDetailsPage, - ConfigListPage, - IncidentDetailsPage, - IncidentListPage, - LogsPage, - TopologyPage -} from "./pages"; -import { ConnectionsPage } from "./pages/Settings/ConnectionsPage"; -import { EventQueueStatusPage } from "./pages/Settings/EventQueueStatus"; -import { FeatureFlagsPage } from "./pages/Settings/FeatureFlagsPage"; -import NotificationSilencedAddPage from "./pages/Settings/notifications/NotificationSilencedAddPage"; -import NotificationsPage from "./pages/Settings/notifications/NotificationsPage"; -import NotificationRulesPage from "./pages/Settings/notifications/NotificationsRulesPage"; -import NotificationsSilencedPage from "./pages/Settings/notifications/NotificationsSilencedPage"; -import { TopologyCardPage } from "./pages/TopologyCard"; -import { UsersPage } from "./pages/UsersPage"; -import { ConfigInsightsPage } from "./pages/config/ConfigInsightsList"; -import { ConfigDetailsChecksPage } from "./pages/config/details/ConfigDetailsChecksPage"; -import { ConfigDetailsInsightsPage } from "./pages/config/details/ConfigDetailsInsightsPage"; -import { ConfigDetailsPlaybooksPage } from "./pages/config/details/ConfigDetailsPlaybooks"; -import { ConfigDetailsRelationshipsPage } from "./pages/config/details/ConfigDetailsRelationshipsPage"; -import ConfigScrapersEditPage from "./pages/config/settings/ConfigScrapersEditPage"; -import ConfigScrapersPage from "./pages/config/settings/ConfigScrapersPage"; -import { HealthPage } from "./pages/health"; -import PlaybookRunsDetailsPage from "./pages/playbooks/PlaybookRunsDetails"; -import PlaybookRunsPage from "./pages/playbooks/PlaybookRunsPage"; -import { PlaybooksListPage } from "./pages/playbooks/PlaybooksList"; import { features } from "./services/permissions/features"; import { Head } from "./ui/Head"; import { LogsIcon } from "./ui/Icons/LogsIcon"; @@ -79,6 +45,185 @@ import FullPageSkeletonLoader from "./ui/SkeletonLoader/FullPageSkeletonLoader"; import { ToasterWithCloseButton } from "./ui/ToasterWithCloseButton"; import { stringSortHelper } from "./utils/common"; +const TopologyPage = dynamic( + import("@flanksource-ui/pages/TopologyPage").then((mod) => mod.TopologyPage) +); + +const TopologyCardPage = dynamic( + import("@flanksource-ui/pages/TopologyCard").then( + (mod) => mod.TopologyCardPage + ) +); + +const IncidentDetailsPage = dynamic( + import("@flanksource-ui/pages/incident/IncidentDetails").then( + (mod) => mod.IncidentDetailsPage + ) +); + +const IncidentListPage = dynamic( + import("@flanksource-ui/pages/incident/IncidentListPage").then( + (mod) => mod.IncidentListPage + ) +); + +const ConfigListPage = dynamic( + import("@flanksource-ui/pages/config/ConfigList").then( + (mod) => mod.ConfigListPage + ) +); + +const ConfigDetailsPage = dynamic( + import("@flanksource-ui/pages/config/details/ConfigDetailsPage").then( + (mod) => mod.ConfigDetailsPage + ) +); + +const ConfigDetailsChangesPage = dynamic( + import("@flanksource-ui/pages/config/details/ConfigDetailsChangesPage").then( + (mod) => mod.ConfigDetailsChangesPage + ) +); + +const PlaybooksListPage = dynamic( + import("@flanksource-ui/pages/playbooks/PlaybooksList").then( + (mod) => mod.PlaybooksListPage + ) +); + +const LogsPage = dynamic( + import("@flanksource-ui/pages/LogsPage").then((mod) => mod.LogsPage) +); + +const ConfigChangesPage = dynamic( + import("@flanksource-ui/pages/config/ConfigChangesPage").then( + (mod) => mod.ConfigChangesPage + ) +); + +const PlaybookRunsPage = dynamic( + import("@flanksource-ui/pages/playbooks/PlaybookRunsPage").then( + (mod) => mod.default + ) +); + +const PlaybookRunsDetailsPage = dynamic( + import("@flanksource-ui/pages/playbooks/PlaybookRunsDetails").then( + (mod) => mod.default + ) +); + +const ConfigInsightsPage = dynamic( + import("@flanksource-ui/pages/config/ConfigInsightsList").then( + (mod) => mod.ConfigInsightsPage + ) +); + +const HealthPage = dynamic( + import("@flanksource-ui/pages/health").then((mod) => mod.HealthPage) +); + +const ConnectionsPage = dynamic(() => + import("@flanksource-ui/pages/Settings/ConnectionsPage").then( + (m) => m.ConnectionsPage + ) +); + +const EventQueueStatusPage = dynamic(() => + import("@flanksource-ui/pages/Settings/EventQueueStatus").then( + (m) => m.EventQueueStatusPage + ) +); + +const FeatureFlagsPage = dynamic(() => + import("@flanksource-ui/pages/Settings/FeatureFlagsPage").then( + (m) => m.FeatureFlagsPage + ) +); + +const NotificationSilencedAddPage = dynamic( + () => + import( + "@flanksource-ui/pages/Settings/notifications/NotificationSilencedAddPage" + ) +); + +const NotificationsPage = dynamic( + () => import("@flanksource-ui/pages/Settings/notifications/NotificationsPage") +); + +const NotificationRulesPage = dynamic( + () => + import( + "@flanksource-ui/pages/Settings/notifications/NotificationsRulesPage" + ) +); + +const NotificationsSilencedPage = dynamic( + () => + import( + "@flanksource-ui/pages/Settings/notifications/NotificationsSilencedPage" + ) +); + +const UsersPage = dynamic(() => + import("@flanksource-ui/pages/UsersPage").then((m) => m.UsersPage) +); + +const ConfigDetailsChecksPage = dynamic(() => + import("@flanksource-ui/pages/config/details/ConfigDetailsChecksPage").then( + (m) => m.ConfigDetailsChecksPage + ) +); + +const ConfigDetailsInsightsPage = dynamic(() => + import("@flanksource-ui/pages/config/details/ConfigDetailsInsightsPage").then( + (m) => m.ConfigDetailsInsightsPage + ) +); + +const ConfigDetailsPlaybooksPage = dynamic(() => + import("@flanksource-ui/pages/config/details/ConfigDetailsPlaybooks").then( + (m) => m.ConfigDetailsPlaybooksPage + ) +); + +const ConfigDetailsRelationshipsPage = dynamic(() => + import( + "@flanksource-ui/pages/config/details/ConfigDetailsRelationshipsPage" + ).then((mod) => mod.ConfigDetailsRelationshipsPage) +); + +const ConfigScrapersEditPage = dynamic( + () => import("@flanksource-ui/pages/config/settings/ConfigScrapersEditPage") +); + +const ConfigScrapersPage = dynamic( + () => import("@flanksource-ui/pages/config/settings/ConfigScrapersPage") +); + +const EditIntegrationPage = dynamic( + () => import("./components/Integrations/EditIntegrationPage") +); + +const IntegrationsPage = dynamic( + () => import("./components/Integrations/IntegrationsPage") +); + +const JobsHistorySettingsPage = dynamic( + () => import("./components/JobsHistory/JobsHistorySettingsPage") +); + +const AgentsPage = dynamic( + () => import("@flanksource-ui/components/Agents/AgentPage") +); + +const SchemaResourcePage = dynamic(() => + import("@flanksource-ui/components/SchemaResourcePage").then( + (mod) => mod.SchemaResourcePage + ) +); + const isDevelopment = process.env.NODE_ENV === "development"; export type NavigationItems = { @@ -265,8 +410,8 @@ export function IncidentManagerRoutes({ sidebar }: { sidebar: ReactNode }) { , - tables.database, + , + tables.topologies, "read", true )} diff --git a/src/ui/Layout/SidebarLayout.tsx b/src/ui/Layout/SidebarLayout.tsx index 77feb059e..de0483b69 100644 --- a/src/ui/Layout/SidebarLayout.tsx +++ b/src/ui/Layout/SidebarLayout.tsx @@ -364,7 +364,9 @@ export function SidebarLayout({ navigation, settingsNav, checkPath }: Props) {
- + }> + +
); diff --git a/src/ui/MRTDataTable/MRTDataTable.tsx b/src/ui/MRTDataTable/MRTDataTable.tsx index 221afae78..ef7b77f31 100644 --- a/src/ui/MRTDataTable/MRTDataTable.tsx +++ b/src/ui/MRTDataTable/MRTDataTable.tsx @@ -4,15 +4,20 @@ import { VisibilityState } from "@tanstack/react-table"; import { - MantineReactTable, MRT_ColumnDef, MRT_Row, MRT_TableInstance, useMantineReactTable } from "mantine-react-table"; +import dynamic from "next/dynamic"; import useReactTablePaginationState from "../DataTable/Hooks/useReactTablePaginationState"; import useReactTableSortState from "../DataTable/Hooks/useReactTableSortState"; +const MantineReactTable = dynamic( + () => import("mantine-react-table").then((mod) => mod.MantineReactTable), + { ssr: false } +); + type MRTDataTableProps = {}> = { data: T[]; columns: MRT_ColumnDef[]; @@ -143,5 +148,6 @@ export default function MRTDataTable = {}>({ renderDetailPanel }); + // @ts-expect-error return ; } From b6e3127b84c53a973fd1765e102f6a8ce2039f62 Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Tue, 22 Oct 2024 16:59:35 +0300 Subject: [PATCH 2/5] fix: use react-selected-windowed for config dropdown for performance improvements Fixes #2365 fix: fix performance issues for the dropdown --- .../ConfigTypesDropdown.tsx | 86 ++++++++++--------- src/components/ReactSelectDropdown/index.tsx | 8 +- src/ui/Dropdowns/MultiSelectDropdown.tsx | 27 ++++-- 3 files changed, 68 insertions(+), 53 deletions(-) diff --git a/src/components/Configs/ConfigsListFilters/ConfigTypesDropdown.tsx b/src/components/Configs/ConfigsListFilters/ConfigTypesDropdown.tsx index fb6376700..f7a3957b4 100644 --- a/src/components/Configs/ConfigsListFilters/ConfigTypesDropdown.tsx +++ b/src/components/Configs/ConfigsListFilters/ConfigTypesDropdown.tsx @@ -2,12 +2,25 @@ import { ConfigTypeItem, getConfigsTypes } from "@flanksource-ui/api/services/configs"; -import { ReactSelectDropdown } from "@flanksource-ui/components/ReactSelectDropdown"; +import { + GroupByOptions, + MultiSelectDropdown +} from "@flanksource-ui/ui/Dropdowns/MultiSelectDropdown"; import { useQuery } from "@tanstack/react-query"; import { useField } from "formik"; -import { useCallback, useMemo } from "react"; +import { useCallback } from "react"; import ConfigsTypeIcon from "../ConfigsTypeIcon"; +function sortOptions(a: { label: string }, b: { label: string }) { + if (a.label === "All") { + return -1; + } + if (b.label === "All") { + return 1; + } + return a.label?.localeCompare(b.label); +} + type ConfigTypesDropdownProps = { label?: string; }; @@ -24,7 +37,7 @@ export function ConfigTypesDropdown({ getConfigsTypes, { select: useCallback((data: ConfigTypeItem[] | null) => { - return data?.map((d) => { + const res = data?.map((d) => { const label = d.type?.split("::").length === 1 ? d.type @@ -34,55 +47,41 @@ export function ConfigTypesDropdown({ .trim(); return { - id: d.type, value: d.type, - description: label, - name: d.type, + label: label, icon: ( ) - }; + } satisfies GroupByOptions; }); + return [ + { + value: "all", + label: "All" + } satisfies GroupByOptions, + ...(res || []).sort(sortOptions) + ]; }, []) } ); - function sortOptions(a: { name: string }, b: { name: string }) { - if (a.name === "All") { - return -1; - } - if (b.name === "All") { - return 1; - } - return a.name?.localeCompare(b.name); - } - - const configItemsOptionsItems = useMemo( - () => - [ - { - id: "All", - name: "All", - description: "All", - value: "All" - }, - ...(configTypeOptions || []) - ].sort(sortOptions), - [configTypeOptions] - ); + console.log(field.value); return ( - { - if (value && value !== "All") { + isMulti={false} + closeMenuOnSelect={true} + // @ts-ignore + onChange={(value: GroupByOptions) => { + if (value && value.value.toLowerCase() !== "all") { field.onChange({ - target: { name: "configType", value: value } + target: { name: "configType", value: value.value } }); } else { field.onChange({ @@ -90,13 +89,18 @@ export function ConfigTypesDropdown({ }); } }} - value={field.value ?? "All"} - className="w-auto max-w-[400px]" - dropDownClassNames="w-auto max-w-[400px] left-0" - hideControlBorder - prefix={ -
{label}
+ value={ + field.value + ? (configTypeOptions?.find( + (option) => option.value === field.value + ) ?? configTypeOptions?.find((option) => option.value === "all")) + : { + value: "all", + label: "All" + } } + className="w-auto max-w-[400px]" + label="Config Type" /> ); } diff --git a/src/components/ReactSelectDropdown/index.tsx b/src/components/ReactSelectDropdown/index.tsx index 1693ed4bd..54d538b2d 100644 --- a/src/components/ReactSelectDropdown/index.tsx +++ b/src/components/ReactSelectDropdown/index.tsx @@ -10,14 +10,13 @@ import { useState } from "react"; import { Controller } from "react-hook-form"; - +import CreatableSelect from "react-select/creatable"; import Select, { SingleValue, StylesConfig, components, defaultTheme -} from "react-select"; -import CreatableSelect from "react-select/creatable"; +} from "react-windowed-select"; import { Avatar } from "../../ui/Avatar"; const { colors } = defaultTheme; @@ -232,6 +231,7 @@ export const ReactSelectDropdown = ({ }} options={options} placeholder={placeholder} + // @ts-expect-error styles={selectStyles} tabSelectsValue={false} value={valueControlled} @@ -279,9 +279,11 @@ export const ReactSelectDropdown = ({ hideSelectedOptions={false} isClearable={false} menuIsOpen + // @ts-expect-error onChange={onSelectChange} options={options} placeholder={placeholder} + // @ts-expect-error styles={selectStyles} tabSelectsValue={false} value={value} diff --git a/src/ui/Dropdowns/MultiSelectDropdown.tsx b/src/ui/Dropdowns/MultiSelectDropdown.tsx index 15354fd8f..51c2ca630 100644 --- a/src/ui/Dropdowns/MultiSelectDropdown.tsx +++ b/src/ui/Dropdowns/MultiSelectDropdown.tsx @@ -1,11 +1,11 @@ import { autoUpdate, useFloating } from "@floating-ui/react"; -import { Popover } from "@headlessui/react"; +import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/solid"; import clsx from "clsx"; import { isArray } from "lodash"; import React, { ComponentProps } from "react"; import { createPortal } from "react-dom"; -import Select, { components } from "react-select"; +import Select, { components } from "react-windowed-select"; export type GroupByOptions = { isTag?: boolean; @@ -16,12 +16,13 @@ export type GroupByOptions = { type ConfigGroupByDropdownProps = Omit< ComponentProps, - "components" | "defaultValue" + "components" | "defaultValue" | "windowThreshold" > & { label?: string; containerClassName?: string; dropDownClassNames?: string; defaultValue?: string; + closeMenuOnSelect?: boolean; }; export function MultiSelectDropdown({ @@ -34,6 +35,8 @@ export function MultiSelectDropdown({ dropDownClassNames = "w-auto max-w-[300px]", value, defaultValue, + closeMenuOnSelect = false, + onChange = () => {}, ...props }: ConfigGroupByDropdownProps) { const { refs, floatingStyles } = useFloating({ @@ -43,9 +46,9 @@ export function MultiSelectDropdown({ return ( - {({ open }) => ( + {({ open, close }) => ( <> - +
{(value as GroupByOptions).icon}
)} - {!(value as GroupByOptions) && ( + {(value as GroupByOptions) && ( {(value as GroupByOptions).label?.toString()} @@ -102,10 +105,10 @@ export function MultiSelectDropdown({
-
+ {createPortal( - { + onChange(value, actionMeta); + close(); + }} {...props} placeholder={"Search..."} components={{ @@ -146,12 +153,14 @@ export function MultiSelectDropdown({ ); } }} + windowThreshold={0.5} autoFocus backspaceRemovesValue={false} controlShouldRenderValue={false} hideSelectedOptions={false} menuIsOpen tabSelectsValue={false} + closeMenuOnSelect={closeMenuOnSelect} styles={{ control: (provided) => ({ ...provided, @@ -177,7 +186,7 @@ export function MultiSelectDropdown({ }} /> - , + , document.body )} From b1dcb10518fef062fce7f9a0395d893041495224 Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Wed, 23 Oct 2024 19:27:03 +0300 Subject: [PATCH 3/5] fix: optimize icons for imports --- next.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/next.config.js b/next.config.js index 028c0dc17..25525f662 100644 --- a/next.config.js +++ b/next.config.js @@ -90,7 +90,8 @@ const config = { // increase the default timeout for the proxy from 30s to 10m to allow for // long running requests to the backend proxyTimeout: 1000 * 60 * 10, - esmExternals: "loose" + esmExternals: "loose", + optimizePackageImports: ["@flanksource/icons"] }, transpilePackages: ["monaco-editor"] }; From 8b023dc93d0d63279f95c80bba76b2b23a326ac2 Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Wed, 23 Oct 2024 19:33:18 +0300 Subject: [PATCH 4/5] fix: fix config insights filter by config type not working --- .../Configs/Insights/Filters/ConfigInsightsFilters.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Configs/Insights/Filters/ConfigInsightsFilters.tsx b/src/components/Configs/Insights/Filters/ConfigInsightsFilters.tsx index 9668cd8c3..737b5b98c 100644 --- a/src/components/Configs/Insights/Filters/ConfigInsightsFilters.tsx +++ b/src/components/Configs/Insights/Filters/ConfigInsightsFilters.tsx @@ -15,7 +15,7 @@ export function ConfigInsightsFilters({ return (
From 42102c44f09764e9a787543274eef27fd33e5818 Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Tue, 29 Oct 2024 08:53:34 +0300 Subject: [PATCH 5/5] test: remove flaky test --- .../__tests__/JobsHistoryTable.unit.test.tsx | 94 ------------------- 1 file changed, 94 deletions(-) delete mode 100644 src/components/JobsHistory/__tests__/JobsHistoryTable.unit.test.tsx diff --git a/src/components/JobsHistory/__tests__/JobsHistoryTable.unit.test.tsx b/src/components/JobsHistory/__tests__/JobsHistoryTable.unit.test.tsx deleted file mode 100644 index 832f16187..000000000 --- a/src/components/JobsHistory/__tests__/JobsHistoryTable.unit.test.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { MemoryRouter } from "react-router"; -import JobsHistoryTable, { JobHistory } from "./../JobsHistoryTable"; - -global.ResizeObserver = jest.fn().mockImplementation(() => ({ - observe: jest.fn(), - unobserve: jest.fn(), - disconnect: jest.fn() -})); - -describe("JobsHistoryTable", () => { - const data: JobHistory[] = [ - { - id: "1", - name: "JobName1", - status: "SUCCESS", - duration_millis: 123456, - created_at: "2022-01-01T00:00:00Z", - success_count: 250, - error_count: 0, - hostname: "localhost", - resource_id: "resource_id", - resource_type: "topology", - time_start: "2022-01-01T00:00:00Z", - time_end: "2022-01-01T00:02:03Z", - resource_name: "resource_name_1" - }, - { - id: "2", - name: "JobName2", - status: "FAILED", - duration_millis: 654321, - created_at: "2022-01-02T00:00:00Z", - success_count: 300, - error_count: 0, - hostname: "localhost", - resource_id: "resource_id_2", - resource_type: "canary", - time_start: "2022-01-01T00:00:00Z", - time_end: "2022-01-01T00:02:03Z", - resource_name: "resource_name_2" - } - ]; - - it("renders the table with the correct column headers", () => { - render( - - - - ); - expect( - screen.getByRole("columnheader", { name: "Resource" }) - ).toBeInTheDocument(); - - expect( - screen.getByRole("columnheader", { name: "Timestamp" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("columnheader", { name: "Job Name" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("columnheader", { name: "Status" }) - ).toBeInTheDocument(); - expect( - screen.getByRole("columnheader", { name: "Duration" }) - ).toBeInTheDocument(); - }); - - it("renders the table with the correct data cells", () => { - render( - - - - ); - - expect( - screen.getByRole("cell", { - name: /Job Name 1/i - }) - ).toBeInTheDocument(); - expect(screen.getByRole("cell", { name: "2m3s" })).toBeInTheDocument(); - expect( - screen.getByRole("cell", { name: /resource_name_1/i }) - ).toBeInTheDocument(); - - expect( - screen.getByRole("cell", { name: /Job Name 2/i }) - ).toBeInTheDocument(); - expect(screen.getByRole("cell", { name: "10m54s" })).toBeInTheDocument(); - expect( - screen.getByRole("cell", { name: /resource_name_2/i }) - ).toBeInTheDocument(); - }); -});