From 759a504241ef3cfffea567bfd099e9adb89da42e Mon Sep 17 00:00:00 2001 From: amitkumar489 Date: Mon, 15 Jun 2026 08:08:32 +0000 Subject: [PATCH 1/3] migration from mui to bui in frontend Signed-off-by: amitkumar489 --- .../.changeset/cruel-nails-happen.md | 5 + .../plugins/servicenow/package.json | 9 +- .../components/Servicenow/CustomSvgIcon.tsx | 14 +- .../EntityServicenowContent.module.css | 19 ++ .../EntityServicenowContent.test.tsx | 124 ++++---- .../Servicenow/EntityServicenowContent.tsx | 280 ++++++++++++------ .../Servicenow/IncidentsFilter.module.css | 14 + .../components/Servicenow/IncidentsFilter.tsx | 13 +- .../Servicenow/IncidentsTableBody.module.css | 7 + .../Servicenow/IncidentsTableBody.tsx | 43 ++- .../IncidentsTableHeader.module.css | 13 + .../Servicenow/IncidentsTableHeader.tsx | 65 ++-- .../Servicenow/IncidentsTableRow.module.css | 27 ++ .../Servicenow/IncidentsTableRow.test.tsx | 38 ++- .../Servicenow/IncidentsTableRow.tsx | 84 +++--- .../IncidentEnumFilter.module.css | 79 +++++ .../shared-components/IncidentEnumFilter.tsx | 93 +++--- .../servicenow/src/utils/incidentUtils.tsx | 77 ++--- 18 files changed, 631 insertions(+), 373 deletions(-) create mode 100644 workspaces/servicenow/.changeset/cruel-nails-happen.md create mode 100644 workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.module.css create mode 100644 workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsFilter.module.css create mode 100644 workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableBody.module.css create mode 100644 workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.module.css create mode 100644 workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.module.css create mode 100644 workspaces/servicenow/plugins/servicenow/src/components/shared-components/IncidentEnumFilter.module.css diff --git a/workspaces/servicenow/.changeset/cruel-nails-happen.md b/workspaces/servicenow/.changeset/cruel-nails-happen.md new file mode 100644 index 00000000000..13ad5f9b8de --- /dev/null +++ b/workspaces/servicenow/.changeset/cruel-nails-happen.md @@ -0,0 +1,5 @@ +--- +'@backstage-community/plugin-servicenow': major +--- + +The ServiceNow plugin is actively transitioning from Material-UI (MUI) to Backstage UI (BUI). Progress includes: migrated layout components using BUI's Box and Text, replaced MUI icons with Remix Icons, and implemented CSS Modules for styling with proper spacing (24px 16px 24px 20px) and alternating row backgrounds. Remaining work involves replacing MUI Table components (TableBody, TableRow, TableCell, TablePagination, TableSortLabel) and Form components (Autocomplete, Checkbox, TextField). Once completed, the plugin will achieve full BUI compliance, removing all MUI dependencies while maintaining functionality and design consistency. diff --git a/workspaces/servicenow/plugins/servicenow/package.json b/workspaces/servicenow/plugins/servicenow/package.json index 8905a6c38b8..721d90479e3 100644 --- a/workspaces/servicenow/plugins/servicenow/package.json +++ b/workspaces/servicenow/plugins/servicenow/package.json @@ -55,10 +55,10 @@ "@backstage/frontend-plugin-api": "^0.16.2", "@backstage/plugin-catalog-react": "^2.1.4", "@backstage/theme": "^0.7.3", - "@mui/icons-material": "^5.15.17", - "@mui/lab": "^5.0.0-alpha.153", - "@mui/material": "^5.17.1", - "@mui/styles": "5.18.0", + "@backstage/ui": "^0.15.0", + "@mui/material": "^9.1.1", + "@remixicon/react": "^4.7.0", + "react-aria-components": "^1.4.0", "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0", "react-router-dom": "^6.0.0", "react-use": "^17.2.4" @@ -75,7 +75,6 @@ "@backstage/dev-utils": "^1.1.22", "@backstage/frontend-test-utils": "^0.5.2", "@backstage/test-utils": "^1.7.17", - "@material-ui/core": "^4.12.4", "@playwright/test": "1.60.0", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.0.0", diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/CustomSvgIcon.tsx b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/CustomSvgIcon.tsx index 894ebb75f35..0ce226271fe 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/CustomSvgIcon.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/CustomSvgIcon.tsx @@ -14,16 +14,22 @@ * limitations under the License. */ -import SvgIcon from '@mui/material/SvgIcon'; - export const CustomSvgIcon = ({ path, viewBox = '0 0 24 24', + size = 16, + style = {}, ...props }: any) => { return ( - + {path} - + ); }; diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.module.css b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.module.css new file mode 100644 index 00000000000..0b2c1235d56 --- /dev/null +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.module.css @@ -0,0 +1,19 @@ +@layer components { + .errorContainer { + padding: var(--bui-space-4); + color: var(--bui-status-error); + } + + .emptyContent { + padding: var(--bui-space-4); + display: flex; + justify-content: center; + } + + .loadingRow { + display: flex; + align-items: center; + justify-content: center; + padding: var(--bui-space-6); + } +} diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.test.tsx b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.test.tsx index 485abfccb90..3bfad2b0160 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.test.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.test.tsx @@ -29,8 +29,6 @@ import userEvent from '@testing-library/user-event'; import { MemoryRouter } from 'react-router-dom'; -import { ThemeProvider, createTheme } from '@material-ui/core'; - import { of } from 'rxjs'; import { serviceNowApiRef } from '../../api/ServiceNowBackendClient'; @@ -87,8 +85,6 @@ const mockTranslationApi = { ), }; -const theme = createTheme(); - describe('ServicenowContent', () => { const mockServiceNowApi = { getIncidents: jest.fn(), @@ -105,22 +101,20 @@ describe('ServicenowContent', () => { it('renders the table with incident rows', async () => { render( - - - - - - - + + + + + , ); @@ -149,22 +143,20 @@ describe('ServicenowContent', () => { it('displays pagination dropdown', async () => { render( - - - - - - - + + + + + , ); @@ -191,22 +183,20 @@ describe('ServicenowContent', () => { render( - - - - - - - + + + + + , ); @@ -224,22 +214,20 @@ describe('ServicenowContent', () => { const user = userEvent.setup(); render( - - - - - - - + + + + + , ); diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.tsx b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.tsx index 36e2e4889b1..ddf40623157 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.tsx @@ -14,21 +14,23 @@ * limitations under the License. */ -import { useEffect, useState, MouseEvent, ChangeEvent, useMemo } from 'react'; +import { useEffect, useState, MouseEvent, useMemo } from 'react'; import { identityApiRef, useApi } from '@backstage/core-plugin-api'; import { CatalogFilterLayout, useEntity, } from '@backstage/plugin-catalog-react'; -import { Table } from '@backstage/core-components'; - -import Box from '@mui/material/Box'; -import CircularProgress from '@mui/material/CircularProgress'; -import TableBody from '@mui/material/TableBody'; -import TableRow from '@mui/material/TableRow'; -import TableCell from '@mui/material/TableCell'; -import TablePagination from '@mui/material/TablePagination'; +import { Progress } from '@backstage/core-components'; +import { Box, ButtonIcon } from '@backstage/ui'; +import { + RiArrowLeftSLine, + RiArrowRightSLine, + RiSkipLeftLine, + RiSkipRightLine, + RiSearchLine, + RiCloseLine, +} from '@remixicon/react'; import { Order, @@ -52,6 +54,7 @@ import { serviceNowApiRef } from '../../api/ServiceNowBackendClient'; import useUserEmail from '../../hooks/useUserEmail'; import { useUpdateQueryParams } from '../../hooks/useQueryHelpers'; import { useTranslation } from '../../hooks/useTranslation'; +import styles from './EntityServicenowContent.module.css'; export const EntityServicenowContent = () => { const { t } = useTranslation(); @@ -171,14 +174,6 @@ export const EntityServicenowContent = () => { userEmail, ]); - const handleRowsPerPageChange = (event: ChangeEvent) => { - const newLimit = parseInt(event.target.value, 10); - updateQueryParams({ - limit: String(newLimit), - offset: '0', - }); - }; - const handlePageChange = (_event: unknown, page: number) => { setOffset(page * rowsPerPage); }; @@ -193,51 +188,110 @@ export const EntityServicenowContent = () => { setOffset(0); }; - const handleSearch = (str: string) => { - setInput(str); + const IncidentsTableHeaderComponent = () => { + return ( + + ); }; - const IncidentsTableHeaderComponent = () => ( - - ); - - const IncidentsTableBodyComponent = () => - loading ? ( - - - - - - - - - - ) : ( - - ); + const IncidentsTableBodyComponent = () => { + if (loading) { + return ( +
+
+
+ + + +
+
+
+ ); + } + return ; + }; const TablePaginationComponent = () => { + const rowsPerPageOptions = [5, 10, 20, 50, 100]; return ( - ({ - value: n, - label: t('table.labelRowsSelect', { count: String(n) }), - }))} - component="div" - sx={{ mr: 1 }} - count={count ?? 0} - rowsPerPage={rowsPerPage} - page={pageNumber} - onPageChange={handlePageChange} - onRowsPerPageChange={handleRowsPerPageChange} - labelRowsPerPage={null} - showFirstButton - showLastButton - /> +
+ + + {offset + 1}-{Math.min(offset + rowsPerPage, count)} of {count} + +
+ } + onPress={() => setOffset(0)} + isDisabled={pageNumber === 0} + variant="secondary" + /> + } + onPress={() => handlePageChange(null as any, pageNumber - 1)} + isDisabled={pageNumber === 0} + variant="secondary" + /> + } + onPress={() => handlePageChange(null as any, pageNumber + 1)} + isDisabled={(pageNumber + 1) * rowsPerPage >= count} + variant="secondary" + /> + } + onPress={() => + setOffset(Math.floor((count - 1) / rowsPerPage) * rowsPerPage) + } + isDisabled={(pageNumber + 1) * rowsPerPage >= count} + variant="secondary" + /> +
+
); }; @@ -249,43 +303,89 @@ export const EntityServicenowContent = () => { {error ? ( - + {t('errors.loadingIncidents', { error })} ) : ( - +
+

+ {count === 0 + ? t('page.title') + : t('page.titleWithCount', { count: String(count) })} +

+
+ + setInput(e.target.value)} + style={{ + padding: '0 8px 4px 0', + border: 'none', + background: 'transparent', + fontSize: '0.875rem', + outline: 'none', + color: 'var(--bui-fg-primary)', + minWidth: '200px', + paddingRight: input ? '24px' : '0', }} - > - {t('table.emptyContent')} - - ) - } - isLoading={loading} - /> + /> + {input && ( + + )} +
+
+
+ + +
+ + )} diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsFilter.module.css b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsFilter.module.css new file mode 100644 index 00000000000..8c7db35e0ac --- /dev/null +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsFilter.module.css @@ -0,0 +1,14 @@ +@layer components { + .filterContainer { + display: flex; + flex-direction: column; + gap: var(--bui-space-4); + min-width: 200px; + } + + @media (min-width: 1200px) { + .filterContainer { + min-width: auto; + } + } +} diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsFilter.tsx b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsFilter.tsx index d5e59aba0e5..6a9565e8801 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsFilter.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsFilter.tsx @@ -17,14 +17,14 @@ import { useMemo, useCallback } from 'react'; import { SelectItem } from '@backstage/core-components'; - -import Box from '@mui/material/Box'; +import { Box } from '@backstage/ui'; import { IncidentEnumFilter } from '../shared-components/IncidentEnumFilter'; import { useIncidentStateMap, usePriorityMap } from '../../utils/incidentUtils'; import { useQueryArrayFilter } from '../../hooks/useQueryArrayFilter'; import { useUpdateQueryParams } from '../../hooks/useQueryHelpers'; import { useTranslation } from '../../hooks/useTranslation'; +import styles from './IncidentsFilter.module.css'; export const IncidentsFilter = () => { const { t } = useTranslation(); @@ -58,14 +58,7 @@ export const IncidentsFilter = () => { ); return ( - + { const { t } = useTranslation(); - const incidentsListColumns = useIncidentsListColumns(); if (rows?.length > 0) { return ( - +
{rows.map(row => ( ))} - +
); } return ( - - - - - {t('table.emptyContent')} - - - - +
+
+
+ {t('table.emptyContent')} +
+
+
); }; diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.module.css b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.module.css new file mode 100644 index 00000000000..5c495d07f27 --- /dev/null +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.module.css @@ -0,0 +1,13 @@ +@layer components { + .headerRow { + margin-left: 6px; + border-bottom: 1px solid #e0e0e0; + } + + .headerCell { + line-height: 1.5rem; + font-size: 0.875rem; + padding: 24px 16px 24px 20px; + font-weight: 700; + } +} diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.tsx b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.tsx index 26a8b540a04..504fa42bfa7 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.tsx @@ -15,15 +15,13 @@ */ import { MouseEvent } from 'react'; -import TableCell from '@mui/material/TableCell'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import TableSortLabel from '@mui/material/TableSortLabel'; +import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'; import type { Order } from '@backstage-community/plugin-servicenow-common'; import { useIncidentsListColumns } from './IncidentsListColumns'; import { IncidentTableField } from '../../types'; +import styles from './IncidentsTableHeader.module.css'; type IncidentsTableHeaderProps = { order: Order; @@ -47,36 +45,63 @@ export const IncidentsTableHeader = ({ }; return ( - - +
+
{incidentsListColumns.map(column => ( - - {column.title} - - + {orderBy === column.field && + (orderBy === column.field && order === 'asc' ? ( + + ) : ( + + ))} + +
))} - - +
+ ); }; diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.module.css b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.module.css new file mode 100644 index 00000000000..c79b5d9e717 --- /dev/null +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.module.css @@ -0,0 +1,27 @@ +@layer components { + .tableRow { + font-size: 0.875rem; + } + + .tableRow:nth-child(even) { + background-color: #f5f5f5; + } + + .tableRow:last-child td, + .tableRow:last-child th { + border: 0; + } + + .tableCellStyle { + line-height: 1.5rem; + font-size: 0.875rem; + padding: 24px 16px 24px 20px; + } + + .descriptionCell { + max-width: 208px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.test.tsx b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.test.tsx index 35f025570b1..002c911b212 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.test.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.test.tsx @@ -16,14 +16,22 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import PendingOutlinedIcon from '@mui/icons-material/PendingOutlined'; -import KeyboardDoubleArrowUpIcon from '@mui/icons-material/KeyboardDoubleArrowUp'; +import { + RiArrowUpLine, + RiArrowDownLine, + RiAlertLine, + RiListOrdered2, + RiTimeLine, + RiPauseLine, + RiCheckLine, +} from '@remixicon/react'; import { IncidentsTableRow } from './IncidentsTableRow'; import type { IncidentsData } from '../../types'; -jest.mock('@mui/styles', () => ({ - makeStyles: () => () => ({}), +jest.mock('react-aria-components', () => ({ + TooltipTrigger: ({ children }: any) => <>{children}, + Tooltip: ({ children }: any) =>
{children}
, })); jest.mock('../../hooks/useTranslation', () => ({ @@ -42,19 +50,19 @@ jest.mock('../../hooks/useTranslation', () => ({ jest.mock('../../utils/incidentUtils', () => ({ renderStatusLabel: jest.fn((data?: { label: string }) => data?.label || ''), usePriorityMap: () => ({ - 1: { Icon: PendingOutlinedIcon, color: '#C9190B', label: 'Critical' }, - 2: { Icon: KeyboardDoubleArrowUpIcon, color: '#EC7A08', label: 'High' }, - 3: { Icon: PendingOutlinedIcon, color: '#F0AB00', label: 'Moderate' }, - 4: { Icon: PendingOutlinedIcon, color: '#2B9AF3', label: 'Low' }, - 5: { Icon: PendingOutlinedIcon, color: '#6A6E73', label: 'Planning' }, + 1: { Icon: RiAlertLine, color: '#C9190B', label: 'Critical' }, + 2: { Icon: RiArrowUpLine, color: '#EC7A08', label: 'High' }, + 3: { Icon: RiListOrdered2, color: '#F0AB00', label: 'Moderate' }, + 4: { Icon: RiArrowDownLine, color: '#2B9AF3', label: 'Low' }, + 5: { Icon: RiListOrdered2, color: '#6A6E73', label: 'Planning' }, }), useIncidentStateMap: () => ({ - 1: { Icon: PendingOutlinedIcon, color: '#6A6E73', label: 'New' }, - 2: { Icon: PendingOutlinedIcon, color: '#6A6E73', label: 'In Progress' }, - 3: { Icon: PendingOutlinedIcon, color: '#6A6E73', label: 'On Hold' }, - 6: { Icon: PendingOutlinedIcon, color: '#3E8635', label: 'Resolved' }, - 7: { Icon: PendingOutlinedIcon, color: '#6A6E73', label: 'Closed' }, - 8: { Icon: PendingOutlinedIcon, color: '#6A6E73', label: 'Cancelled' }, + 1: { Icon: RiTimeLine, color: '#6A6E73', label: 'New' }, + 2: { Icon: RiArrowUpLine, color: '#6A6E73', label: 'In Progress' }, + 3: { Icon: RiPauseLine, color: '#6A6E73', label: 'On Hold' }, + 6: { Icon: RiCheckLine, color: '#3E8635', label: 'Resolved' }, + 7: { Icon: RiArrowDownLine, color: '#6A6E73', label: 'Closed' }, + 8: { Icon: RiArrowDownLine, color: '#6A6E73', label: 'Cancelled' }, }), })); diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.tsx b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.tsx index bcebfdae46d..26331c4f4fa 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.tsx @@ -14,13 +14,9 @@ * limitations under the License. */ -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import IconButton from '@mui/material/IconButton'; -import TableCell from '@mui/material/TableCell'; -import TableRow from '@mui/material/TableRow'; -import { makeStyles } from '@mui/styles'; -import Tooltip from '@mui/material/Tooltip'; -import Typography from '@mui/material/Typography'; +import { RiExternalLinkLine } from '@remixicon/react'; +import { ButtonIcon } from '@backstage/ui'; +import { TooltipTrigger, Tooltip } from 'react-aria-components'; import { convertDateFormat } from '../../utils/stringUtils'; import { @@ -30,64 +26,52 @@ import { } from '../../utils/incidentUtils'; import type { IncidentsData } from '../../types'; import { useTranslation } from '../../hooks/useTranslation'; - -const useStyles = makeStyles(() => ({ - tableCellStyle: { - lineHeight: '1.5rem', - fontSize: '0.875rem', - }, -})); +import styles from './IncidentsTableRow.module.css'; export const IncidentsTableRow = ({ data }: { data: IncidentsData }) => { - const classes = useStyles(); const { t } = useTranslation(); const priorityMap = usePriorityMap(); const incidentStateMap = useIncidentStateMap(); return ( - - +
{data.number} - - - - +
+
+ + {data?.shortDescription} - - - - + + {data?.description} + +
+
{convertDateFormat(data?.sysCreatedOn)} - - +
+
{renderStatusLabel(priorityMap[data?.priority])} - +
- +
{renderStatusLabel(incidentStateMap[data?.incidentState])} - - - - window.open(data.url, '_blank')}> - - - - - +
+
+ + } + onPress={() => window.open(data.url, '_blank')} + variant="secondary" + /> + {t('actions.openInServicenow')} + +
+ ); }; diff --git a/workspaces/servicenow/plugins/servicenow/src/components/shared-components/IncidentEnumFilter.module.css b/workspaces/servicenow/plugins/servicenow/src/components/shared-components/IncidentEnumFilter.module.css new file mode 100644 index 00000000000..590164ea4ff --- /dev/null +++ b/workspaces/servicenow/plugins/servicenow/src/components/shared-components/IncidentEnumFilter.module.css @@ -0,0 +1,79 @@ +@layer components { + .filterBox { + display: flex; + flex-direction: column; + } + + .label { + transform: initial; + font-weight: bold; + font-size: 0.875rem; + color: var(--bui-fg-primary); + margin-bottom: var(--bui-space-2); + } + + .selectContainer { + position: relative; + } + + .selectButton { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: var(--bui-space-2) var(--bui-space-3); + border: 1px solid var(--bui-border-default); + border-radius: var(--bui-radius-1); + background-color: var(--bui-bg-surface-1); + color: var(--bui-fg-primary); + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s ease-in-out; + } + + .selectButton:hover { + border-color: var(--bui-border-strong); + } + + .selectButton:focus { + outline: 2px solid var(--bui-border-focus); + outline-offset: 2px; + } + + .selectValue { + flex: 1; + text-align: left; + } + + .dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: var(--bui-space-1); + background-color: var(--bui-bg-surface-1); + border: 1px solid var(--bui-border-default); + border-radius: var(--bui-radius-1); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + z-index: 1000; + max-height: 300px; + overflow-y: auto; + } + + .option { + display: flex; + align-items: center; + padding: var(--bui-space-2) var(--bui-space-3); + gap: var(--bui-space-2); + cursor: pointer; + transition: background-color 0.2s ease-in-out; + } + + .option:hover { + background-color: var(--bui-bg-surface-2); + } + + .optionContent { + flex: 1; + } +} diff --git a/workspaces/servicenow/plugins/servicenow/src/components/shared-components/IncidentEnumFilter.tsx b/workspaces/servicenow/plugins/servicenow/src/components/shared-components/IncidentEnumFilter.tsx index 20a8f3674f1..a78be5495c7 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/shared-components/IncidentEnumFilter.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/shared-components/IncidentEnumFilter.tsx @@ -14,16 +14,12 @@ * limitations under the License. */ +import React from 'react'; import { SelectItem } from '@backstage/core-components'; -import Autocomplete from '@mui/material/Autocomplete'; -import Box from '@mui/material/Box'; -import Checkbox from '@mui/material/Checkbox'; -import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; -import CheckBoxIcon from '@mui/icons-material/CheckBox'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import InputLabel from '@mui/material/InputLabel'; -import TextField from '@mui/material/TextField'; +import { Box, Checkbox, TextField } from '@backstage/ui'; +import { RiArrowDownSLine } from '@remixicon/react'; import { renderStatusLabel, StatusData } from '../../utils/incidentUtils'; +import styles from './IncidentEnumFilter.module.css'; export interface IncidentEnumFilterProps { label: string; @@ -40,6 +36,7 @@ export const IncidentEnumFilter = ({ value, onChange, }: IncidentEnumFilterProps) => { + const [isOpen, setIsOpen] = React.useState(false); const items: SelectItem[] = Object.entries(dataMap).map( ([key, itemValue]) => ({ value: key, @@ -47,51 +44,47 @@ export const IncidentEnumFilter = ({ }), ); - return ( - - theme.typography.body2.fontSize, - fontFamily: theme => theme.typography.fontFamily, - color: theme => theme.palette.text.primary, - }} - > - {label} - - option.value === val.value} - getOptionLabel={option => option.label} - onChange={onChange} - renderOption={(renderProps, option, { selected }) => { - const { key, ...optionProps } = renderProps; - const statusData = dataMap[Number(option.value)]; + const handleSelect = (item: SelectItem) => { + const newValue = value.some(v => v.value === item.value) + ? value.filter(v => v.value !== item.value) + : [...value, item]; + onChange(null, newValue); + }; - return ( -
  • - } - checkedIcon={} - checked={selected} - /> - {renderStatusLabel(statusData)} -
  • - ); - }} - size="small" - value={value} - popupIcon={ - + +
    + + {isOpen && ( +
    + {items.map(item => ( + + ))} +
    + )} +
    ); }; diff --git a/workspaces/servicenow/plugins/servicenow/src/utils/incidentUtils.tsx b/workspaces/servicenow/plugins/servicenow/src/utils/incidentUtils.tsx index 9c0ef7aac6c..5f0cc43d6dc 100644 --- a/workspaces/servicenow/plugins/servicenow/src/utils/incidentUtils.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/utils/incidentUtils.tsx @@ -14,18 +14,19 @@ * limitations under the License. */ import { ElementType, useMemo } from 'react'; -import LabelImportantIcon from '@mui/icons-material/LabelImportant'; -import KeyboardDoubleArrowDownIcon from '@mui/icons-material/KeyboardDoubleArrowDown'; -import KeyboardDoubleArrowUpIcon from '@mui/icons-material/KeyboardDoubleArrowUp'; -import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; -import PendingOutlinedIcon from '@mui/icons-material/PendingOutlined'; -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline'; +import { + RiAlertLine, + RiArrowUpLine, + RiArrowDownLine, + RiListOrdered2, + RiTimeLine, + RiCheckLine, + RiPauseLine, +} from '@remixicon/react'; import { ModerateIcon } from '../components/Servicenow/ModerateIcon'; import { InProgressIcon } from '../components/Servicenow/InProgressIcon'; import { ClosedIcon } from '../components/Servicenow/ClosedIcon'; -import Typography from '@mui/material/Typography'; import { useTranslation } from '../hooks/useTranslation'; export interface StatusData { @@ -35,33 +36,30 @@ export interface StatusData { transform?: string; } -const iconStyleBase = { marginRight: 8 }; -const typographyStyleBase = { display: 'flex', alignItems: 'center' }; - /** * @deprecated Use usePriorityMap hook for translated labels */ export const PRIORITY_MAP: Record = { 1: { - Icon: LabelImportantIcon, + Icon: RiAlertLine, color: '#C9190B', label: 'Critical', transform: 'rotate(-90deg)', }, - 2: { Icon: KeyboardDoubleArrowUpIcon, color: '#EC7A08', label: 'High' }, + 2: { Icon: RiArrowUpLine, color: '#EC7A08', label: 'High' }, 3: { Icon: ModerateIcon, color: '#F0AB00', label: 'Moderate' }, - 4: { Icon: KeyboardDoubleArrowDownIcon, color: '#2B9AF3', label: 'Low' }, - 5: { Icon: FormatListNumberedIcon, color: '#6A6E73', label: 'Planning' }, + 4: { Icon: RiArrowDownLine, color: '#2B9AF3', label: 'Low' }, + 5: { Icon: RiListOrdered2, color: '#6A6E73', label: 'Planning' }, }; /** * @deprecated Use useIncidentStateMap hook for translated labels */ export const INCIDENT_STATE_MAP: Record = { - 1: { Icon: PendingOutlinedIcon, color: '#6A6E73', label: 'New' }, + 1: { Icon: RiTimeLine, color: '#6A6E73', label: 'New' }, 2: { Icon: InProgressIcon, color: '#6A6E73', label: 'In Progress' }, - 3: { Icon: PauseCircleOutlineIcon, color: '#6A6E73', label: 'On Hold' }, - 6: { Icon: CheckCircleOutlineIcon, color: '#3E8635', label: 'Resolved' }, + 3: { Icon: RiPauseLine, color: '#6A6E73', label: 'On Hold' }, + 6: { Icon: RiCheckLine, color: '#3E8635', label: 'Resolved' }, 7: { Icon: ClosedIcon, color: '#6A6E73', label: 'Closed' }, 8: { Icon: ClosedIcon, color: '#6A6E73', label: 'Cancelled' }, }; @@ -75,13 +73,13 @@ export const usePriorityMap = (): Record => { return useMemo( () => ({ 1: { - Icon: LabelImportantIcon, + Icon: RiAlertLine, color: '#C9190B', label: t('priority.critical'), transform: 'rotate(-90deg)', }, 2: { - Icon: KeyboardDoubleArrowUpIcon, + Icon: RiArrowUpLine, color: '#EC7A08', label: t('priority.high'), }, @@ -91,12 +89,12 @@ export const usePriorityMap = (): Record => { label: t('priority.moderate'), }, 4: { - Icon: KeyboardDoubleArrowDownIcon, + Icon: RiArrowDownLine, color: '#2B9AF3', label: t('priority.low'), }, 5: { - Icon: FormatListNumberedIcon, + Icon: RiListOrdered2, color: '#6A6E73', label: t('priority.planning'), }, @@ -114,7 +112,7 @@ export const useIncidentStateMap = (): Record => { return useMemo( () => ({ 1: { - Icon: PendingOutlinedIcon, + Icon: RiTimeLine, color: '#6A6E73', label: t('incidentState.new'), }, @@ -124,12 +122,12 @@ export const useIncidentStateMap = (): Record => { label: t('incidentState.inProgress'), }, 3: { - Icon: PauseCircleOutlineIcon, + Icon: RiPauseLine, color: '#6A6E73', label: t('incidentState.onHold'), }, 6: { - Icon: CheckCircleOutlineIcon, + Icon: RiCheckLine, color: '#3E8635', label: t('incidentState.resolved'), }, @@ -152,18 +150,25 @@ export const renderStatusLabel = (data?: StatusData) => { if (!data) return ''; const { Icon, color, label, transform } = data; + if (!Icon) return label; + + const iconProps = { + size: 16, + style: { + color, + marginRight: 8, + flexShrink: 0, + ...(transform ? { transform } : {}), + }, + }; + return ( - - - {label} - +
    + + + {label} + +
    ); }; From ce9411441feb49ad07ef8378cebc34fd6942e55b Mon Sep 17 00:00:00 2001 From: amitkumar489 Date: Mon, 15 Jun 2026 11:53:30 +0000 Subject: [PATCH 2/3] them work on style Signed-off-by: amitkumar489 --- .../EntityServicenowContent.module.css | 95 +++++++++++++++++++ .../Servicenow/EntityServicenowContent.tsx | 95 +++---------------- .../IncidentsTableHeader.module.css | 33 ++++++- .../Servicenow/IncidentsTableHeader.tsx | 40 +------- .../Servicenow/IncidentsTableRow.module.css | 2 +- .../IncidentEnumFilter.module.css | 4 +- 6 files changed, 147 insertions(+), 122 deletions(-) diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.module.css b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.module.css index 0b2c1235d56..ee1ca4367f5 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.module.css +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.module.css @@ -16,4 +16,99 @@ justify-content: center; padding: var(--bui-space-6); } + + .tableRowGroup { + display: table-row-group; + } + + .tableRow { + display: table-row; + } + + .tableCellLoading { + display: table-cell; + padding: 24px 16px 24px 20px; + text-align: center; + width: 100%; + } + + .paginationContainer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 16px; + margin-top: 24px; + } + + .rowsPerPageSelect { + padding: 4px 8px; + border-radius: 4px; + border: none; + background-color: transparent; + color: var(--bui-fg-primary); + font-size: 0.875rem; + cursor: pointer; + } + + .paginationInfo { + font-size: 0.875rem; + color: var(--bui-fg-secondary); + } + + .paginationButtons { + display: flex; + gap: 8px; + } + + .tableHeader { + display: flex; + justify-content: space-between; + align-items: flex-end; + margin-bottom: 24px; + } + + .titleHeading { + margin: 0; + } + + .searchContainer { + display: flex; + align-items: center; + gap: 8px; + padding-bottom: 8px; + border-bottom: 2px solid #e0e0e0; + position: relative; + } + + .searchInput { + padding: 0 8px 4px 0; + border: none; + background: transparent; + font-size: 0.875rem; + outline: none; + color: var(--bui-fg-primary); + min-width: 200px; + } + + .clearSearchButton { + position: absolute; + right: 0; + background: none; + border: none; + cursor: pointer; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + color: var(--bui-fg-secondary); + } + + .table { + display: table; + width: 100%; + border-collapse: collapse; + border-radius: 4px; + overflow: hidden; + border: 1px solid #e0e0e0; + } } diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.tsx b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.tsx index ddf40623157..177525f1d20 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.tsx @@ -201,16 +201,9 @@ export const EntityServicenowContent = () => { const IncidentsTableBodyComponent = () => { if (loading) { return ( -
    -
    -
    +
    +
    +
    @@ -225,15 +218,7 @@ export const EntityServicenowContent = () => { const TablePaginationComponent = () => { const rowsPerPageOptions = [5, 10, 20, 50, 100]; return ( -
    +
    - + {offset + 1}-{Math.min(offset + rowsPerPage, count)} of {count} -
    +
    } onPress={() => setOffset(0)} @@ -308,29 +283,13 @@ export const EntityServicenowContent = () => { ) : (
    -
    -

    +
    +

    {count === 0 ? t('page.title') : t('page.titleWithCount', { count: String(count) })} -

    -
    +

    +
    { value={input} onChange={e => setInput(e.target.value)} style={{ - padding: '0 8px 4px 0', - border: 'none', - background: 'transparent', - fontSize: '0.875rem', - outline: 'none', - color: 'var(--bui-fg-primary)', - minWidth: '200px', paddingRight: input ? '24px' : '0', }} + className={styles.searchInput} /> {input && (
    -
    +
    diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.module.css b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.module.css index 5c495d07f27..cb5be327678 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.module.css +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.module.css @@ -1,13 +1,42 @@ @layer components { .headerRow { + display: table-header-group; margin-left: 6px; - border-bottom: 1px solid #e0e0e0; + border-bottom: 1px solid var(--bui-border-2); + } + + .headerRowContent { + display: table-row; + border-bottom: 1px solid var(--bui-border-2); + background-color: var(--bui-bg-app); } .headerCell { + display: table-cell; + padding: 24px 16px 24px 20px; + text-align: left; + color: var(--bui-fg-secondary); line-height: 1.5rem; font-size: 0.875rem; - padding: 24px 16px 24px 20px; font-weight: 700; } + + .headerButton { + background: none; + border: none; + padding: 0; + display: flex; + align-items: center; + gap: 4px; + font-size: 0.875rem; + font-weight: bold; + color: inherit; + font-family: inherit; + cursor: pointer; + } + + .headerButton:disabled { + opacity: 0.5; + cursor: default; + } } diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.tsx b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.tsx index 504fa42bfa7..4012ca62d29 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.tsx @@ -45,44 +45,12 @@ export const IncidentsTableHeader = ({ }; return ( -
    -
    +
    +
    {incidentsListColumns.map(column => ( -
    +
    + variant="secondary" + aria-label={t('actions.clearSearch')} + /> )}
    diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsFilter.tsx b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsFilter.tsx index 6a9565e8801..94275f670a2 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsFilter.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsFilter.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useMemo, useCallback } from 'react'; +import { useMemo, useCallback, useState } from 'react'; import { SelectItem } from '@backstage/core-components'; import { Box } from '@backstage/ui'; @@ -28,6 +28,7 @@ import styles from './IncidentsFilter.module.css'; export const IncidentsFilter = () => { const { t } = useTranslation(); + const [openDropdown, setOpenDropdown] = useState(null); const stateFilter = useQueryArrayFilter('state'); const priorityFilter = useQueryArrayFilter('priority'); const updateQueryParams = useUpdateQueryParams(); @@ -57,6 +58,14 @@ export const IncidentsFilter = () => { [updateQueryParams], ); + const handleStateToggle = useCallback(() => { + setOpenDropdown(openDropdown === 'state' ? null : 'state'); + }, [openDropdown]); + + const handlePriorityToggle = useCallback(() => { + setOpenDropdown(openDropdown === 'priority' ? null : 'priority'); + }, [openDropdown]); + return ( { dataMap={incidentStateMap} value={stateValue} onChange={handleStateChange} + isOpen={openDropdown === 'state'} + onToggle={handleStateToggle} /> { dataMap={priorityMap} value={priorityValue} onChange={handlePriorityChange} + isOpen={openDropdown === 'priority'} + onToggle={handlePriorityToggle} /> ); diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableBody.tsx b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableBody.tsx index 867c7fd40f2..d41e79b5405 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableBody.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableBody.tsx @@ -16,11 +16,13 @@ import { IncidentsTableRow } from './IncidentsTableRow'; import type { IncidentsData } from '../../types'; +import { useIncidentsListColumns } from './IncidentsListColumns'; import { useTranslation } from '../../hooks/useTranslation'; import styles from './IncidentsTableBody.module.css'; export const IncidentsTableBody = ({ rows }: { rows: IncidentsData[] }) => { const { t } = useTranslation(); + const columns = useIncidentsListColumns(); if (rows?.length > 0) { return ( @@ -46,6 +48,15 @@ export const IncidentsTableBody = ({ rows }: { rows: IncidentsData[] }) => { > {t('table.emptyContent')}
    + {Array.from({ length: columns.length - 1 }).map((_, index) => ( +
    + ))}
    ); diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.tsx b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.tsx index 4012ca62d29..9fefadb4064 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.tsx @@ -44,31 +44,52 @@ export const IncidentsTableHeader = ({ onRequestSort(event, property); }; + const getSortState = (column: IncidentTableField) => { + if (orderBy !== column) { + return 'none'; + } + return order === 'asc' ? 'ascending' : 'descending'; + }; + return (
    - {incidentsListColumns.map(column => ( -
    - -
    - ))} + +
    + ); + })}
    ); diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.module.css b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.module.css index bcffbe2dc67..7d0478654b1 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.module.css +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.module.css @@ -7,9 +7,8 @@ background-color: var(--bui-bg-neutral-1); } - .tableRow:last-child td, - .tableRow:last-child th { - border: 0; + .tableRow:last-child { + border-bottom: 0; } .tableCellStyle { diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.tsx b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.tsx index 26331c4f4fa..9c0285da188 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.tsx @@ -26,6 +26,7 @@ import { } from '../../utils/incidentUtils'; import type { IncidentsData } from '../../types'; import { useTranslation } from '../../hooks/useTranslation'; +import { Text } from '@backstage/ui'; import styles from './IncidentsTableRow.module.css'; export const IncidentsTableRow = ({ data }: { data: IncidentsData }) => { @@ -46,9 +47,9 @@ export const IncidentsTableRow = ({ data }: { data: IncidentsData }) => {
    - + {data?.shortDescription} - + {data?.description}
    @@ -66,7 +67,9 @@ export const IncidentsTableRow = ({ data }: { data: IncidentsData }) => { } - onPress={() => window.open(data.url, '_blank')} + onPress={() => + window.open(data.url, '_blank', 'noopener,noreferrer') + } variant="secondary" /> {t('actions.openInServicenow')} diff --git a/workspaces/servicenow/plugins/servicenow/src/components/shared-components/IncidentEnumFilter.tsx b/workspaces/servicenow/plugins/servicenow/src/components/shared-components/IncidentEnumFilter.tsx index a78be5495c7..c95c74213f2 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/shared-components/IncidentEnumFilter.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/shared-components/IncidentEnumFilter.tsx @@ -28,6 +28,8 @@ export interface IncidentEnumFilterProps { dataMap: Record; value: SelectItem[]; onChange: (event: any, value: SelectItem[]) => void; + isOpen?: boolean; + onToggle?: () => void; } export const IncidentEnumFilter = ({ @@ -35,8 +37,21 @@ export const IncidentEnumFilter = ({ dataMap, value, onChange, + isOpen: externalIsOpen, + onToggle, }: IncidentEnumFilterProps) => { - const [isOpen, setIsOpen] = React.useState(false); + const [internalIsOpen, setInternalIsOpen] = React.useState(false); + + // Use external isOpen if provided (controlled), otherwise use internal state (uncontrolled) + const isOpen = externalIsOpen !== undefined ? externalIsOpen : internalIsOpen; + + const handleToggle = () => { + if (onToggle) { + onToggle(); + } else { + setInternalIsOpen(!internalIsOpen); + } + }; const items: SelectItem[] = Object.entries(dataMap).map( ([key, itemValue]) => ({ value: key, @@ -57,7 +72,7 @@ export const IncidentEnumFilter = ({