diff --git a/frontend/src/components/empty-object.js b/frontend/src/components/empty-object.js
index 1d318b51..d671e157 100644
--- a/frontend/src/components/empty-object.js
+++ b/frontend/src/components/empty-object.js
@@ -7,7 +7,7 @@ import {
EmptyStateBody,
Content,
} from '@patternfly/react-core';
-import { SearchIcon } from '@patternfly/react-icons';
+import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
import { NavLink } from 'react-router-dom';
diff --git a/frontend/src/components/filtering/active-filters.js b/frontend/src/components/filtering/active-filters.js
index ff4df493..93ef7b3f 100644
--- a/frontend/src/components/filtering/active-filters.js
+++ b/frontend/src/components/filtering/active-filters.js
@@ -14,7 +14,8 @@ import { useMemo } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { filtersToSearchParams, toTitleCase } from '../../utilities';
import { OPERATIONS } from '../../constants';
-import { ChevronRightIcon, TimesCircleIcon } from '@patternfly/react-icons';
+import ChevronRightIcon from '@patternfly/react-icons/dist/esm/icons/chevron-right-icon';
+import TimesCircleIcon from '@patternfly/react-icons/dist/esm/icons/times-circle-icon';
const BADGE_STYLE = {
margin: '0.1rem',
diff --git a/frontend/src/components/filtering/result-filter.js b/frontend/src/components/filtering/result-filter.js
index 49746441..37dbd611 100644
--- a/frontend/src/components/filtering/result-filter.js
+++ b/frontend/src/components/filtering/result-filter.js
@@ -17,7 +17,7 @@ import {
import PropTypes from 'prop-types';
import { useCallback, useContext, useEffect, useState } from 'react';
-import { TimesIcon } from '@patternfly/react-icons';
+import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
import MultiValueInput from '../multi-value-input';
import ActiveFilters from './active-filters';
diff --git a/frontend/src/components/hooks/use-table-filters.js b/frontend/src/components/hooks/use-table-filters.js
index 6f12a7fa..58db207a 100644
--- a/frontend/src/components/hooks/use-table-filters.js
+++ b/frontend/src/components/hooks/use-table-filters.js
@@ -25,7 +25,7 @@ import {
parseSearchToFilter,
} from '../../utilities';
import { IbutsuContext } from '../contexts/ibutsu-context';
-import { TimesIcon } from '@patternfly/react-icons';
+import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
import PropTypes from 'prop-types';
import { OPERATION_MODE_MAP, FILTER_MODE_MAP } from '../../constants';
diff --git a/frontend/src/components/hooks/use-widgets.js b/frontend/src/components/hooks/use-widgets.js
index 36265e73..d6ecd71a 100644
--- a/frontend/src/components/hooks/use-widgets.js
+++ b/frontend/src/components/hooks/use-widgets.js
@@ -1,20 +1,54 @@
// Assisted by watsonx Code Assistant
-import { useState, useEffect, useMemo, useContext } from 'react';
+import {
+ useState,
+ useEffect,
+ useMemo,
+ useContext,
+ lazy,
+ Suspense,
+} from 'react';
+import PropTypes from 'prop-types';
import { HttpClient } from '../../utilities/http';
import { KNOWN_WIDGETS } from '../../constants';
import { Settings } from '../../pages/settings';
import { GridItem } from '@patternfly/react-core';
-import {
- FilterHeatmapWidget,
- HEATMAP_TYPES,
-} from '../../widgets/filter-heatmap';
-import GenericAreaWidget from '../../widgets/generic-area';
-import GenericBarWidget from '../../widgets/generic-bar';
-import ImportanceComponentWidget from '../../widgets/importance-component';
import { IbutsuContext } from '../contexts/ibutsu-context';
-import ResultSummaryApex from '../../widgets/result-summary-apex';
-import ResultAggregateApex from '../../widgets/result-aggregate-apex';
-import RunAggregateApex from '../../widgets/run-aggregate-apex';
+import { WidgetSpinner } from '../loading-spinners';
+
+// Lazy load widget components for code splitting
+const FilterHeatmapWidget = lazy(() =>
+ import('../../widgets/filter-heatmap').then((module) => ({
+ default: module.FilterHeatmapWidget,
+ })),
+);
+
+const GenericAreaWidget = lazy(() => import('../../widgets/generic-area'));
+const GenericBarWidget = lazy(() => import('../../widgets/generic-bar'));
+const ImportanceComponentWidget = lazy(
+ () => import('../../widgets/importance-component'),
+);
+const ResultSummaryApex = lazy(
+ () => import('../../widgets/result-summary-apex'),
+);
+const ResultAggregateApex = lazy(
+ () => import('../../widgets/result-aggregate-apex'),
+);
+const RunAggregateApex = lazy(() => import('../../widgets/run-aggregate-apex'));
+
+// Lazy-loaded cache for HEATMAP_TYPES - only loads when actually needed
+let heatmapTypesPromise = null;
+let heatmapTypesCache = null;
+
+const getHeatmapTypes = async () => {
+ if (heatmapTypesCache) return heatmapTypesCache;
+ if (!heatmapTypesPromise) {
+ heatmapTypesPromise = import('../../widgets/filter-heatmap').then(
+ (module) => module.HEATMAP_TYPES,
+ );
+ }
+ heatmapTypesCache = await heatmapTypesPromise;
+ return heatmapTypesCache;
+};
// Move constants outside component to prevent unnecessary re-renders
const DEFAULT_COLSPAN = Object.freeze({
@@ -55,6 +89,51 @@ const ROW_SPAN = Object.freeze({
'importance-component': DEFAULT_ROWSPAN,
});
+// Wrapper component for jenkins-heatmap that handles async HEATMAP_TYPES
+const JenkinsHeatmapWrapper = ({
+ title,
+ params,
+ onDeleteClick,
+ onEditClick,
+}) => {
+ const [heatmapType, setHeatmapType] = useState(null);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ getHeatmapTypes().then((types) => {
+ if (isMounted) {
+ setHeatmapType(types.jenkins);
+ }
+ });
+
+ return () => {
+ isMounted = false;
+ };
+ }, []);
+
+ if (!heatmapType) {
+ return ;
+ }
+
+ return (
+
+ );
+};
+
+JenkinsHeatmapWrapper.propTypes = {
+ title: PropTypes.string,
+ params: PropTypes.object,
+ onDeleteClick: PropTypes.func,
+ onEditClick: PropTypes.func,
+};
+
export const useWidgets = ({
dashboardId = null,
editCallback = () => {},
@@ -105,121 +184,125 @@ export const useWidgets = ({
{...ROW_SPAN[widget.widget]}
key={`${widget.id}-${loadKey}`}
>
- {widget.type === 'widget' &&
- widget.widget === 'jenkins-heatmap' && (
- {
- deleteCallback(widget.id);
- }}
- onEditClick={() => {
- editCallback(widget.id);
- }}
- />
- )}
- {widget.type === 'widget' && widget.widget === 'filter-heatmap' && (
- {
- deleteCallback(widget.id);
- }}
- onEditClick={() => {
- editCallback(widget.id);
- }}
- />
- )}
- {widget.type === 'widget' && widget.widget === 'run-aggregator' && (
- {
- deleteCallback(widget.id);
- }}
- onEditClick={() => {
- editCallback(widget.id);
- }}
- />
- )}
- {widget.type === 'widget' && widget.widget === 'result-summary' && (
- {
- editCallback(widget.id);
- }}
- onDeleteClick={() => {
- deleteCallback(widget.id);
- }}
- />
- )}
- {widget.type === 'widget' &&
- widget.widget === 'result-aggregator' && (
- {
- deleteCallback(widget.id);
- }}
- onEditClick={() => {
- editCallback(widget.id);
- }}
- />
- )}
- {widget.type === 'widget' &&
- widget.widget === 'jenkins-line-chart' && (
- {
- deleteCallback(widget.id);
- }}
- onEditClick={() => {
- editCallback(widget.id);
- }}
- />
- )}
- {widget.type === 'widget' &&
- widget.widget === 'jenkins-bar-chart' && (
- {
- deleteCallback(widget.id);
- }}
- onEditClick={() => {
- editCallback(widget.id);
- }}
- />
- )}
- {widget.type === 'widget' &&
- widget.widget === 'importance-component' && (
- {
- deleteCallback(widget.id);
- }}
- onEditClick={() => {
- editCallback(widget.id);
- }}
- />
- )}
+ }>
+ {widget.type === 'widget' &&
+ widget.widget === 'jenkins-heatmap' && (
+ {
+ deleteCallback(widget.id);
+ }}
+ onEditClick={() => {
+ editCallback(widget.id);
+ }}
+ />
+ )}
+ {widget.type === 'widget' &&
+ widget.widget === 'filter-heatmap' && (
+ {
+ deleteCallback(widget.id);
+ }}
+ onEditClick={() => {
+ editCallback(widget.id);
+ }}
+ />
+ )}
+ {widget.type === 'widget' &&
+ widget.widget === 'run-aggregator' && (
+ {
+ deleteCallback(widget.id);
+ }}
+ onEditClick={() => {
+ editCallback(widget.id);
+ }}
+ />
+ )}
+ {widget.type === 'widget' &&
+ widget.widget === 'result-summary' && (
+ {
+ editCallback(widget.id);
+ }}
+ onDeleteClick={() => {
+ deleteCallback(widget.id);
+ }}
+ />
+ )}
+ {widget.type === 'widget' &&
+ widget.widget === 'result-aggregator' && (
+ {
+ deleteCallback(widget.id);
+ }}
+ onEditClick={() => {
+ editCallback(widget.id);
+ }}
+ />
+ )}
+ {widget.type === 'widget' &&
+ widget.widget === 'jenkins-line-chart' && (
+ {
+ deleteCallback(widget.id);
+ }}
+ onEditClick={() => {
+ editCallback(widget.id);
+ }}
+ />
+ )}
+ {widget.type === 'widget' &&
+ widget.widget === 'jenkins-bar-chart' && (
+ {
+ deleteCallback(widget.id);
+ }}
+ onEditClick={() => {
+ editCallback(widget.id);
+ }}
+ />
+ )}
+ {widget.type === 'widget' &&
+ widget.widget === 'importance-component' && (
+ {
+ deleteCallback(widget.id);
+ }}
+ onEditClick={() => {
+ editCallback(widget.id);
+ }}
+ />
+ )}
+
);
}
diff --git a/frontend/src/components/loading-spinners.js b/frontend/src/components/loading-spinners.js
new file mode 100644
index 00000000..93a9702f
--- /dev/null
+++ b/frontend/src/components/loading-spinners.js
@@ -0,0 +1,49 @@
+import { Bullseye, Spinner } from '@patternfly/react-core';
+import PropTypes from 'prop-types';
+
+/**
+ * Shared loading spinner components for consistent loading UX across the application.
+ * Use these instead of defining inline spinner components.
+ */
+
+/**
+ * Full-page spinner for route-level loading states.
+ * Centers the spinner in the full viewport height.
+ */
+export const PageSpinner = ({ ariaLabel = 'Loading page...' }) => (
+
+
+
+);
+
+PageSpinner.propTypes = {
+ ariaLabel: PropTypes.string,
+};
+
+/**
+ * Content spinner for lazy-loaded page content.
+ * Uses a smaller minimum height suitable for content sections.
+ */
+export const ContentSpinner = ({ ariaLabel = 'Loading content...' }) => (
+
+
+
+);
+
+ContentSpinner.propTypes = {
+ ariaLabel: PropTypes.string,
+};
+
+/**
+ * Widget spinner for dashboard widget loading states.
+ * Alias for ContentSpinner but semantically named for widget context.
+ */
+export const WidgetSpinner = ({ ariaLabel = 'Loading widget...' }) => (
+
+
+
+);
+
+WidgetSpinner.propTypes = {
+ ariaLabel: PropTypes.string,
+};
diff --git a/frontend/src/components/modals/edit-widget-modal.js b/frontend/src/components/modals/edit-widget-modal.js
index ef36efd4..a47ccc30 100644
--- a/frontend/src/components/modals/edit-widget-modal.js
+++ b/frontend/src/components/modals/edit-widget-modal.js
@@ -16,7 +16,7 @@ import {
Skeleton,
TextInput,
} from '@patternfly/react-core';
-import { ExclamationCircleIcon } from '@patternfly/react-icons';
+import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
import { HttpClient } from '../../utilities/http';
import { Settings } from '../../pages/settings';
diff --git a/frontend/src/components/modals/new-dashboard-modal.js b/frontend/src/components/modals/new-dashboard-modal.js
index 83b9d09b..ab003939 100644
--- a/frontend/src/components/modals/new-dashboard-modal.js
+++ b/frontend/src/components/modals/new-dashboard-modal.js
@@ -12,7 +12,7 @@ import {
ModalVariant,
TextInput,
} from '@patternfly/react-core';
-import { ExclamationCircleIcon } from '@patternfly/react-icons';
+import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
const NewDashboardModal = ({
project,
diff --git a/frontend/src/components/multi-value-input.js b/frontend/src/components/multi-value-input.js
index 6dc2be35..539544b3 100644
--- a/frontend/src/components/multi-value-input.js
+++ b/frontend/src/components/multi-value-input.js
@@ -9,7 +9,7 @@ import {
TextInputGroupUtilities,
} from '@patternfly/react-core';
-import { TimesIcon } from '@patternfly/react-icons';
+import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
const MultiValueInput = ({
onAddValue,
diff --git a/frontend/src/components/result-view.js b/frontend/src/components/result-view.js
index 954dd00c..16c19200 100644
--- a/frontend/src/components/result-view.js
+++ b/frontend/src/components/result-view.js
@@ -16,12 +16,10 @@ import {
Tab,
Button,
} from '@patternfly/react-core';
-import {
- InfoCircleIcon,
- CodeIcon,
- SearchIcon,
- FileAltIcon,
-} from '@patternfly/react-icons';
+import InfoCircleIcon from '@patternfly/react-icons/dist/esm/icons/info-circle-icon';
+import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon';
+import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
+import FileAltIcon from '@patternfly/react-icons/dist/esm/icons/file-alt-icon';
import { CodeEditor, Language } from '@patternfly/react-code-editor';
import { Link, useParams } from 'react-router-dom';
diff --git a/frontend/src/components/table-states.js b/frontend/src/components/table-states.js
index 7fbf6b35..30c8d33a 100644
--- a/frontend/src/components/table-states.js
+++ b/frontend/src/components/table-states.js
@@ -7,7 +7,8 @@ import {
EmptyStateBody,
EmptyStateFooter,
} from '@patternfly/react-core';
-import { ErrorCircleOIcon, SearchIcon } from '@patternfly/react-icons';
+import ErrorCircleOIcon from '@patternfly/react-icons/dist/esm/icons/error-circle-o-icon';
+import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
// TODO this could be one component with variable icons and textual content.
diff --git a/frontend/src/components/user-dropdown.js b/frontend/src/components/user-dropdown.js
index a70e04c5..a5f518fb 100644
--- a/frontend/src/components/user-dropdown.js
+++ b/frontend/src/components/user-dropdown.js
@@ -7,7 +7,7 @@ import {
MenuToggle,
} from '@patternfly/react-core';
-import { UserIcon } from '@patternfly/react-icons';
+import UserIcon from '@patternfly/react-icons/dist/esm/icons/user-icon';
import { useNavigate } from 'react-router-dom';
import { AuthService } from '../utilities/auth';
diff --git a/frontend/src/components/widget-header.js b/frontend/src/components/widget-header.js
index 448c5548..5db52cfe 100644
--- a/frontend/src/components/widget-header.js
+++ b/frontend/src/components/widget-header.js
@@ -1,10 +1,8 @@
import PropTypes from 'prop-types';
import { Button, CardHeader, Title } from '@patternfly/react-core';
-import {
- PficonHistoryIcon,
- TimesIcon,
- PencilAltIcon,
-} from '@patternfly/react-icons';
+import PficonHistoryIcon from '@patternfly/react-icons/dist/esm/icons/pficon-history-icon';
+import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
+import PencilAltIcon from '@patternfly/react-icons/dist/esm/icons/pencil-alt-icon';
const WidgetHeader = ({
id,
diff --git a/frontend/src/constants.js b/frontend/src/constants.js
index 3ce9a642..ea0bfb81 100644
--- a/frontend/src/constants.js
+++ b/frontend/src/constants.js
@@ -1,15 +1,13 @@
import packageJson from '../package.json';
-import {
- CheckCircleIcon,
- ChevronCircleRightIcon,
- ClockIcon,
- ExclamationCircleIcon,
- InfoAltIcon,
- QuestionCircleIcon,
- TimesCircleIcon,
- FlagIcon,
-} from '@patternfly/react-icons';
+import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon';
+import ChevronCircleRightIcon from '@patternfly/react-icons/dist/esm/icons/chevron-circle-right-icon';
+import ClockIcon from '@patternfly/react-icons/dist/esm/icons/clock-icon';
+import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
+import InfoAltIcon from '@patternfly/react-icons/dist/esm/icons/info-alt-icon';
+import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon';
+import TimesCircleIcon from '@patternfly/react-icons/dist/esm/icons/times-circle-icon';
+import FlagIcon from '@patternfly/react-icons/dist/esm/icons/flag-icon';
export const VERSION = packageJson.version;
export const MONITOR_UPLOAD_TIMEOUT = 1 * 1000; // 1 second
diff --git a/frontend/src/pages/admin/project-edit.js b/frontend/src/pages/admin/project-edit.js
index b0098d6c..f3393809 100644
--- a/frontend/src/pages/admin/project-edit.js
+++ b/frontend/src/pages/admin/project-edit.js
@@ -26,7 +26,7 @@ import {
import { Link, useNavigate, useParams } from 'react-router-dom';
import { nanoid } from 'nanoid/non-secure';
-import { TimesIcon } from '@patternfly/react-icons';
+import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
import { HttpClient } from '../../utilities/http';
import { Settings } from '../settings';
diff --git a/frontend/src/pages/admin/project-list.js b/frontend/src/pages/admin/project-list.js
index 5c32e45e..789fd478 100644
--- a/frontend/src/pages/admin/project-list.js
+++ b/frontend/src/pages/admin/project-list.js
@@ -12,11 +12,9 @@ import {
ModalHeader,
ModalVariant,
} from '@patternfly/react-core';
-import {
- PencilAltIcon,
- PlusCircleIcon,
- TrashIcon,
-} from '@patternfly/react-icons';
+import PencilAltIcon from '@patternfly/react-icons/dist/esm/icons/pencil-alt-icon';
+import PlusCircleIcon from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon';
+import TrashIcon from '@patternfly/react-icons/dist/esm/icons/trash-icon';
import { Link } from 'react-router-dom';
import { HttpClient } from '../../utilities/http';
diff --git a/frontend/src/pages/admin/user-edit.js b/frontend/src/pages/admin/user-edit.js
index 4d81fbbe..1b170657 100644
--- a/frontend/src/pages/admin/user-edit.js
+++ b/frontend/src/pages/admin/user-edit.js
@@ -26,7 +26,7 @@ import {
Title,
} from '@patternfly/react-core';
-import { TimesIcon } from '@patternfly/react-icons';
+import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
import { HttpClient } from '../../utilities/http';
import { Settings } from '../settings';
diff --git a/frontend/src/pages/admin/user-list.js b/frontend/src/pages/admin/user-list.js
index 6425fff9..f613d3fd 100644
--- a/frontend/src/pages/admin/user-list.js
+++ b/frontend/src/pages/admin/user-list.js
@@ -28,13 +28,11 @@ import usePagination from '../../components/hooks/use-pagination';
import FilterTable from '../../components/filtering/filtered-table-card';
import { FilterContext } from '../../components/contexts/filter-context';
import AdminFilter from '../../components/filtering/admin-filter';
-import {
- BanIcon,
- CheckIcon,
- LinuxIcon,
- PencilAltIcon,
- TrashIcon,
-} from '@patternfly/react-icons';
+import BanIcon from '@patternfly/react-icons/dist/esm/icons/ban-icon';
+import CheckIcon from '@patternfly/react-icons/dist/esm/icons/check-icon';
+import LinuxIcon from '@patternfly/react-icons/dist/esm/icons/linux-icon';
+import PencilAltIcon from '@patternfly/react-icons/dist/esm/icons/pencil-alt-icon';
+import TrashIcon from '@patternfly/react-icons/dist/esm/icons/trash-icon';
import { Link } from 'react-router-dom';
const COLUMNS = Object.values(USER_COLUMNS);
diff --git a/frontend/src/pages/login.js b/frontend/src/pages/login.js
index 978a671a..8cc3d8a0 100644
--- a/frontend/src/pages/login.js
+++ b/frontend/src/pages/login.js
@@ -16,16 +16,14 @@ import {
LoginPage,
TextInput,
} from '@patternfly/react-core';
-import {
- EyeIcon,
- EyeSlashIcon,
- GoogleIcon,
- FacebookIcon,
- GithubIcon,
- GitlabIcon,
- RedhatIcon,
- KeyIcon,
-} from '@patternfly/react-icons';
+import EyeIcon from '@patternfly/react-icons/dist/esm/icons/eye-icon';
+import EyeSlashIcon from '@patternfly/react-icons/dist/esm/icons/eye-slash-icon';
+import GoogleIcon from '@patternfly/react-icons/dist/esm/icons/google-icon';
+import FacebookIcon from '@patternfly/react-icons/dist/esm/icons/facebook-icon';
+import GithubIcon from '@patternfly/react-icons/dist/esm/icons/github-icon';
+import GitlabIcon from '@patternfly/react-icons/dist/esm/icons/gitlab-icon';
+import RedhatIcon from '@patternfly/react-icons/dist/esm/icons/redhat-icon';
+import KeyIcon from '@patternfly/react-icons/dist/esm/icons/key-icon';
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
import { GoogleLogin } from '@react-oauth/google';
import OAuth2Login from 'react-simple-oauth2-login';
diff --git a/frontend/src/pages/reset-password.js b/frontend/src/pages/reset-password.js
index b13e67d0..93b6c4c9 100644
--- a/frontend/src/pages/reset-password.js
+++ b/frontend/src/pages/reset-password.js
@@ -15,7 +15,8 @@ import {
LoginPage,
TextInput,
} from '@patternfly/react-core';
-import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
+import EyeIcon from '@patternfly/react-icons/dist/esm/icons/eye-icon';
+import EyeSlashIcon from '@patternfly/react-icons/dist/esm/icons/eye-slash-icon';
import { NavLink, useParams } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
diff --git a/frontend/src/pages/run.js b/frontend/src/pages/run.js
index 6dafefb9..00166496 100644
--- a/frontend/src/pages/run.js
+++ b/frontend/src/pages/run.js
@@ -24,18 +24,16 @@ import {
Content,
TreeView,
} from '@patternfly/react-core';
-import {
- CatalogIcon,
- ChevronRightIcon,
- CodeIcon,
- FileAltIcon,
- FileIcon,
- FolderIcon,
- FolderOpenIcon,
- InfoCircleIcon,
- MessagesIcon,
- RepositoryIcon,
-} from '@patternfly/react-icons';
+import CatalogIcon from '@patternfly/react-icons/dist/esm/icons/catalog-icon';
+import ChevronRightIcon from '@patternfly/react-icons/dist/esm/icons/chevron-right-icon';
+import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon';
+import FileAltIcon from '@patternfly/react-icons/dist/esm/icons/file-alt-icon';
+import FileIcon from '@patternfly/react-icons/dist/esm/icons/file-icon';
+import FolderIcon from '@patternfly/react-icons/dist/esm/icons/folder-icon';
+import FolderOpenIcon from '@patternfly/react-icons/dist/esm/icons/folder-open-icon';
+import InfoCircleIcon from '@patternfly/react-icons/dist/esm/icons/info-circle-icon';
+import MessagesIcon from '@patternfly/react-icons/dist/esm/icons/messages-icon';
+import RepositoryIcon from '@patternfly/react-icons/dist/esm/icons/repository-icon';
import { CodeEditor, Language } from '@patternfly/react-code-editor';
import { HttpClient } from '../utilities/http';
diff --git a/frontend/src/pages/sign-up.js b/frontend/src/pages/sign-up.js
index fc367f48..74ecb06a 100644
--- a/frontend/src/pages/sign-up.js
+++ b/frontend/src/pages/sign-up.js
@@ -16,7 +16,8 @@ import {
LoginPage,
TextInput,
} from '@patternfly/react-core';
-import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
+import EyeIcon from '@patternfly/react-icons/dist/esm/icons/eye-icon';
+import EyeSlashIcon from '@patternfly/react-icons/dist/esm/icons/eye-slash-icon';
import { NavLink } from 'react-router-dom';
import { AuthService } from '../utilities/auth';
diff --git a/frontend/src/pages/user-profile.js b/frontend/src/pages/user-profile.js
index a9fbbe40..ea935861 100644
--- a/frontend/src/pages/user-profile.js
+++ b/frontend/src/pages/user-profile.js
@@ -17,7 +17,9 @@ import {
Title,
Skeleton,
} from '@patternfly/react-core';
-import { CheckIcon, PencilAltIcon, TimesIcon } from '@patternfly/react-icons';
+import CheckIcon from '@patternfly/react-icons/dist/esm/icons/check-icon';
+import PencilAltIcon from '@patternfly/react-icons/dist/esm/icons/pencil-alt-icon';
+import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
import { HttpClient } from '../utilities/http';
import { Settings } from './settings';
diff --git a/frontend/src/pages/user-tokens.js b/frontend/src/pages/user-tokens.js
index bbd00a9f..40842d5b 100644
--- a/frontend/src/pages/user-tokens.js
+++ b/frontend/src/pages/user-tokens.js
@@ -10,7 +10,7 @@ import {
Content,
Title,
} from '@patternfly/react-core';
-import { PlusCircleIcon } from '@patternfly/react-icons';
+import PlusCircleIcon from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon';
import { HttpClient } from '../utilities/http';
import { Settings } from './settings';
diff --git a/frontend/src/routes/admin.js b/frontend/src/routes/admin.js
index df70fd87..cbcd9b14 100644
--- a/frontend/src/routes/admin.js
+++ b/frontend/src/routes/admin.js
@@ -1,35 +1,56 @@
+import { lazy, Suspense } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
-import AdminHome from '../pages/admin/home';
-import UserList from '../pages/admin/user-list';
-import UserEdit from '../pages/admin/user-edit';
-import ProjectList from '../pages/admin/project-list';
-import ProjectEdit from '../pages/admin/project-edit';
-
import '../app.css';
import AdminPage from './admin-page';
import { STRING_PROJECT_FIELDS, STRING_USER_FIELDS } from '../constants';
import FilterProvider from '../components/contexts/filter-context';
+import { ContentSpinner } from '../components/loading-spinners';
+
+// Lazy load admin page components for code splitting
+const AdminHome = lazy(() => import('../pages/admin/home'));
+const UserList = lazy(() => import('../pages/admin/user-list'));
+const UserEdit = lazy(() => import('../pages/admin/user-edit'));
+const ProjectList = lazy(() => import('../pages/admin/project-list'));
+const ProjectEdit = lazy(() => import('../pages/admin/project-edit'));
const Admin = () => {
return (
}>
- } />
+ }>
+
+
+ }
+ />
-
+ }>
+
+
}
/>
- } />
+ }>
+
+
+ }
+ />
-
+ }>
+
+
}
/>
@@ -41,7 +62,9 @@ const Admin = () => {
key="project_edit"
fieldOptions={STRING_USER_FIELDS}
>
-
+ }>
+
+
}
/>
diff --git a/frontend/src/routes/app.js b/frontend/src/routes/app.js
index 1e40f7dc..5ebe922d 100644
--- a/frontend/src/routes/app.js
+++ b/frontend/src/routes/app.js
@@ -1,18 +1,21 @@
-import { useEffect } from 'react';
+import { useEffect, lazy, Suspense } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
-import Dashboard from '../pages/dashboard';
-import RunList from '../pages/run-list';
-import Run from '../pages/run';
-import ResultList from '../pages/result-list';
-import Result from '../pages/result';
import IbutsuPage from './ibutsu-page';
-import View from '../pages/View';
import '../app.css';
import FilterProvider from '../components/contexts/filter-context.js';
import { RESULT_FIELDS, RUN_FIELDS } from '../constants';
+import { ContentSpinner } from '../components/loading-spinners';
+
+// Lazy load page components for code splitting
+const Dashboard = lazy(() => import('../pages/dashboard'));
+const RunList = lazy(() => import('../pages/run-list'));
+const Run = lazy(() => import('../pages/run'));
+const ResultList = lazy(() => import('../pages/result-list'));
+const Result = lazy(() => import('../pages/result'));
+const View = lazy(() => import('../pages/View'));
const App = () => {
// apparently it's good practice to set this after render via effect
@@ -25,8 +28,22 @@ const App = () => {
} />
}>
{/* Nested project routes */}
- } />
- } />
+ }>
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
{
// set key to force mount on FilterProviders
// RunList uses RunFilters
-
+ }>
+
+
}
/>
- } />
+ }>
+
+
+ }
+ />
-
+ }>
+
+
}
/>
- } />
+ }>
+
+
+ }
+ />
- } />
+ }>
+
+
+ }
+ />
} />
diff --git a/frontend/src/routes/base.js b/frontend/src/routes/base.js
index a43f7c09..52acbb95 100644
--- a/frontend/src/routes/base.js
+++ b/frontend/src/routes/base.js
@@ -1,57 +1,65 @@
+import { lazy, Suspense } from 'react';
import {
BrowserRouter as Router,
Route,
Routes,
Navigate,
} from 'react-router-dom';
-import App from './app';
-import Admin from './admin';
-import Profile from './profile';
-import Login from '../pages/login';
-import { SignUp } from '../pages/sign-up';
-import ForgotPassword from '../pages/forgot-password';
-import ResetPassword from '../pages/reset-password';
import ProtectedRoute from '../components/ProtectedRoute';
import { IbutsuContextProvider } from '../components/contexts/ibutsu-context';
+import { PageSpinner } from '../components/loading-spinners';
+
+// Lazy load route-level components for code splitting
+const App = lazy(() => import('./app'));
+const Admin = lazy(() => import('./admin'));
+const Profile = lazy(() => import('./profile'));
+const Login = lazy(() => import('../pages/login'));
+const SignUp = lazy(() =>
+ import('../pages/sign-up').then((module) => ({ default: module.SignUp })),
+);
+const ForgotPassword = lazy(() => import('../pages/forgot-password'));
+const ResetPassword = lazy(() => import('../pages/reset-password'));
export const Base = () => (
-
- } />
- } />
- } />
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
- } />
-
+ }>
+
+ } />
+ } />
+ } />
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+ } />
+
+
);
diff --git a/frontend/src/routes/ibutsu-page.js b/frontend/src/routes/ibutsu-page.js
index 4521c5f8..8f4e3386 100644
--- a/frontend/src/routes/ibutsu-page.js
+++ b/frontend/src/routes/ibutsu-page.js
@@ -11,7 +11,7 @@ import { EmptyState, EmptyStateBody, Page } from '@patternfly/react-core';
import IbutsuHeader from '../components/ibutsu-header';
import { IbutsuContext } from '../components/contexts/ibutsu-context';
import IbutsuSidebar from '../components/sidebar';
-import { ArchiveIcon } from '@patternfly/react-icons';
+import ArchiveIcon from '@patternfly/react-icons/dist/esm/icons/archive-icon';
import { ToastContainer } from 'react-toastify';
import { ALERT_TIMEOUT } from '../constants';
diff --git a/frontend/src/utilities/tables.js b/frontend/src/utilities/tables.js
index 29ee33e1..86dc08a3 100644
--- a/frontend/src/utilities/tables.js
+++ b/frontend/src/utilities/tables.js
@@ -1,6 +1,6 @@
import { Fragment, isValidElement } from 'react';
import { Badge, Label } from '@patternfly/react-core';
-import { ChevronRightIcon } from '@patternfly/react-icons';
+import ChevronRightIcon from '@patternfly/react-icons/dist/esm/icons/chevron-right-icon';
import { Link } from 'react-router-dom';
import { ICON_RESULT_MAP } from '../constants';
import RunSummary from '../components/run-summary';
diff --git a/frontend/src/views/accessibility-analysis.js b/frontend/src/views/accessibility-analysis.js
index 415d2b1e..31734f69 100644
--- a/frontend/src/views/accessibility-analysis.js
+++ b/frontend/src/views/accessibility-analysis.js
@@ -15,11 +15,9 @@ import {
Tabs,
Content,
} from '@patternfly/react-core';
-import {
- CatalogIcon,
- ChevronRightIcon,
- CodeIcon,
-} from '@patternfly/react-icons';
+import CatalogIcon from '@patternfly/react-icons/dist/esm/icons/catalog-icon';
+import ChevronRightIcon from '@patternfly/react-icons/dist/esm/icons/chevron-right-icon';
+import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon';
import {
ChartLegend,
ChartDonut,
diff --git a/frontend/src/views/jenkins-job.js b/frontend/src/views/jenkins-job.js
index 665e1ac2..db01eb80 100644
--- a/frontend/src/views/jenkins-job.js
+++ b/frontend/src/views/jenkins-job.js
@@ -1,7 +1,7 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
-import { ChevronRightIcon } from '@patternfly/react-icons';
+import ChevronRightIcon from '@patternfly/react-icons/dist/esm/icons/chevron-right-icon';
import { Link, useParams } from 'react-router-dom';
diff --git a/frontend/src/widgets/filter-heatmap.js b/frontend/src/widgets/filter-heatmap.js
index 27156481..e50a2a6a 100644
--- a/frontend/src/widgets/filter-heatmap.js
+++ b/frontend/src/widgets/filter-heatmap.js
@@ -10,12 +10,10 @@ import {
EmptyStateBody,
Content,
} from '@patternfly/react-core';
-import {
- ArrowDownIcon,
- ArrowRightIcon,
- ArrowUpIcon,
- ChartLineIcon,
-} from '@patternfly/react-icons';
+import ArrowDownIcon from '@patternfly/react-icons/dist/esm/icons/arrow-down-icon';
+import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-right-icon';
+import ArrowUpIcon from '@patternfly/react-icons/dist/esm/icons/arrow-up-icon';
+import ChartLineIcon from '@patternfly/react-icons/dist/esm/icons/chart-line-icon';
import { Link } from 'react-router-dom';
import HeatMapWrapper from '../components/heat-map-wrapper';