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..fb9d32925a2 100644 --- a/workspaces/servicenow/plugins/servicenow/package.json +++ b/workspaces/servicenow/plugins/servicenow/package.json @@ -55,17 +55,17 @@ "@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", - "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0", + "@backstage/ui": "^0.15.0", + "@mui/material": "^9.1.1", + "@remixicon/react": "^4.7.0", + "react-aria-components": "^1.4.0", + "react-dom": "^18.0.0", "react-router-dom": "^6.0.0", "react-use": "^17.2.4" }, "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", "react-router-dom": "^6.0.0" }, "devDependencies": { @@ -75,14 +75,13 @@ "@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", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.0.0", "msw": "^1.0.0", - "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react": "^18.0.0", "rxjs": "^7.8.2" }, "files": [ 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..ee1ca4367f5 --- /dev/null +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/EntityServicenowContent.module.css @@ -0,0 +1,114 @@ +@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); + } + + .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.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..c2b6f48fc2f 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, Text } 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,15 +174,7 @@ 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) => { + const handlePageChange = (page: number) => { setOffset(page * rowsPerPage); }; @@ -193,51 +188,90 @@ 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 - /> +
+ + + {count === 0 + ? '0 of 0' + : `${offset + 1}-${Math.min( + offset + rowsPerPage, + count, + )} of ${count}`} + +
+ } + onPress={() => setOffset(0)} + isDisabled={pageNumber === 0} + variant="secondary" + /> + } + onPress={() => handlePageChange(pageNumber - 1)} + isDisabled={pageNumber === 0} + variant="secondary" + /> + } + onPress={() => handlePageChange(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 +283,46 @@ export const EntityServicenowContent = () => { {error ? ( - + {t('errors.loadingIncidents', { error })} ) : ( - +
+

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

+
+ + setInput(e.target.value)} + style={{ + paddingRight: input ? '24px' : '0', }} - > - {t('table.emptyContent')} - - ) - } - isLoading={loading} - /> + className={styles.searchInput} + /> + {input && ( + } + onPress={() => setInput('')} + className={styles.clearSearchButton} + variant="secondary" + aria-label={t('actions.clearSearch')} + /> + )} +
+
+
+ + +
+ + )} 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..94275f670a2 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsFilter.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsFilter.tsx @@ -14,20 +14,21 @@ * limitations under the License. */ -import { useMemo, useCallback } from 'react'; +import { useMemo, useCallback, useState } 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(); + const [openDropdown, setOpenDropdown] = useState(null); const stateFilter = useQueryArrayFilter('state'); const priorityFilter = useQueryArrayFilter('priority'); const updateQueryParams = useUpdateQueryParams(); @@ -57,21 +58,24 @@ export const IncidentsFilter = () => { [updateQueryParams], ); + const handleStateToggle = useCallback(() => { + setOpenDropdown(openDropdown === 'state' ? null : 'state'); + }, [openDropdown]); + + const handlePriorityToggle = useCallback(() => { + setOpenDropdown(openDropdown === 'priority' ? null : 'priority'); + }, [openDropdown]); + return ( - + { dataMap={priorityMap} value={priorityValue} onChange={handlePriorityChange} + isOpen={openDropdown === 'priority'} + onToggle={handlePriorityToggle} /> ); diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableBody.module.css b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableBody.module.css new file mode 100644 index 00000000000..65254b301c1 --- /dev/null +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableBody.module.css @@ -0,0 +1,7 @@ +@layer components { + .emptyRow { + padding: var(--bui-space-4); + display: flex; + justify-content: center; + } +} diff --git a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableBody.tsx b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableBody.tsx index 98d71bdbbe9..d41e79b5405 100644 --- a/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableBody.tsx +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableBody.tsx @@ -14,46 +14,50 @@ * limitations under the License. */ -import Box from '@mui/material/Box'; -import TableBody from '@mui/material/TableBody'; -import TableRow from '@mui/material/TableRow'; -import TableCell from '@mui/material/TableCell'; - -import { useIncidentsListColumns } from './IncidentsListColumns'; 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 incidentsListColumns = useIncidentsListColumns(); + const columns = useIncidentsListColumns(); if (rows?.length > 0) { return ( - +
{rows.map(row => ( ))} - +
); } return ( - - - - +
+
+ {t('table.emptyContent')} +
+ {Array.from({ length: columns.length - 1 }).map((_, index) => ( +
- {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..cb5be327678 --- /dev/null +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableHeader.module.css @@ -0,0 +1,42 @@ +@layer components { + .headerRow { + display: table-header-group; + margin-left: 6px; + 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; + 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 26a8b540a04..9fefadb4064 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; @@ -46,37 +44,53 @@ export const IncidentsTableHeader = ({ onRequestSort(event, property); }; + const getSortState = (column: IncidentTableField) => { + if (orderBy !== column) { + return 'none'; + } + return order === 'asc' ? 'ascending' : 'descending'; + }; + return ( - - - {incidentsListColumns.map(column => ( - - +
+ {incidentsListColumns.map(column => { + const isSorted = orderBy === column.field; + const sortState = getSortState(column.field as IncidentTableField); + const sortLabel = + sortState === 'none' + ? `${column.title}, not sorted` + : `${column.title}, sorted ${sortState}`; + + return ( +
- {column.title} - - - ))} - - + +
+ ); + })} +
+ ); }; 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..7d0478654b1 --- /dev/null +++ b/workspaces/servicenow/plugins/servicenow/src/components/Servicenow/IncidentsTableRow.module.css @@ -0,0 +1,26 @@ +@layer components { + .tableRow { + font-size: 0.875rem; + } + + .tableRow:nth-child(even) { + background-color: var(--bui-bg-neutral-1); + } + + .tableRow:last-child { + border-bottom: 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..9c0285da188 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,55 @@ 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 { Text } from '@backstage/ui'; +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', 'noopener,noreferrer') + } + 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..6e42bb22e6c --- /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-2); + 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-fg-solid); + 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..c95c74213f2 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; @@ -32,6 +28,8 @@ export interface IncidentEnumFilterProps { dataMap: Record; value: SelectItem[]; onChange: (event: any, value: SelectItem[]) => void; + isOpen?: boolean; + onToggle?: () => void; } export const IncidentEnumFilter = ({ @@ -39,7 +37,21 @@ export const IncidentEnumFilter = ({ dataMap, value, onChange, + isOpen: externalIsOpen, + onToggle, }: IncidentEnumFilterProps) => { + 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, @@ -47,51 +59,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/translations/de.ts b/workspaces/servicenow/plugins/servicenow/src/translations/de.ts index 2cb6097bade..fa87e558035 100644 --- a/workspaces/servicenow/plugins/servicenow/src/translations/de.ts +++ b/workspaces/servicenow/plugins/servicenow/src/translations/de.ts @@ -50,6 +50,7 @@ const servicenowTranslationDe = createTranslationMessages({ 'table.columns.actions': 'Aktionen', 'table.emptyContent': 'Keine Datensätze gefunden', 'actions.openInServicenow': 'In ServiceNow öffnen', + 'actions.clearSearch': 'Suche löschen', }, }); diff --git a/workspaces/servicenow/plugins/servicenow/src/translations/es.ts b/workspaces/servicenow/plugins/servicenow/src/translations/es.ts index 82743f6559a..5cb0042ca46 100644 --- a/workspaces/servicenow/plugins/servicenow/src/translations/es.ts +++ b/workspaces/servicenow/plugins/servicenow/src/translations/es.ts @@ -50,6 +50,7 @@ const servicenowTranslationEs = createTranslationMessages({ 'table.columns.actions': 'Acciones', 'table.emptyContent': 'No se encontraron registros', 'actions.openInServicenow': 'Abrir en ServiceNow', + 'actions.clearSearch': 'Limpiar búsqueda', }, }); diff --git a/workspaces/servicenow/plugins/servicenow/src/translations/fr.ts b/workspaces/servicenow/plugins/servicenow/src/translations/fr.ts index 74e01da17ff..27a19f87acb 100644 --- a/workspaces/servicenow/plugins/servicenow/src/translations/fr.ts +++ b/workspaces/servicenow/plugins/servicenow/src/translations/fr.ts @@ -51,6 +51,7 @@ const servicenowTranslationFr = createTranslationMessages({ 'table.columns.actions': 'Actions', 'table.emptyContent': 'Aucun enregistrement trouvé', 'actions.openInServicenow': 'Ouvrir dans ServiceNow', + 'actions.clearSearch': 'Effacer la recherche', }, }); diff --git a/workspaces/servicenow/plugins/servicenow/src/translations/it.ts b/workspaces/servicenow/plugins/servicenow/src/translations/it.ts index b94afa967a4..1e865546070 100644 --- a/workspaces/servicenow/plugins/servicenow/src/translations/it.ts +++ b/workspaces/servicenow/plugins/servicenow/src/translations/it.ts @@ -51,6 +51,7 @@ const servicenowTranslationIt = createTranslationMessages({ 'table.columns.actions': 'Azioni', 'table.emptyContent': 'Nessun risultato trovato', 'actions.openInServicenow': 'Apri in ServiceNow', + 'actions.clearSearch': 'Cancella ricerca', }, }); diff --git a/workspaces/servicenow/plugins/servicenow/src/translations/ja.ts b/workspaces/servicenow/plugins/servicenow/src/translations/ja.ts index 1e8982311b3..b71fb84425b 100644 --- a/workspaces/servicenow/plugins/servicenow/src/translations/ja.ts +++ b/workspaces/servicenow/plugins/servicenow/src/translations/ja.ts @@ -51,6 +51,7 @@ const servicenowTranslationJa = createTranslationMessages({ 'table.columns.actions': 'アクション', 'table.emptyContent': 'レコードが見つかりません', 'actions.openInServicenow': 'ServiceNow で開く', + 'actions.clearSearch': '検索をクリア', }, }); diff --git a/workspaces/servicenow/plugins/servicenow/src/translations/ref.ts b/workspaces/servicenow/plugins/servicenow/src/translations/ref.ts index 3277b34f2ce..8ce77231888 100644 --- a/workspaces/servicenow/plugins/servicenow/src/translations/ref.ts +++ b/workspaces/servicenow/plugins/servicenow/src/translations/ref.ts @@ -57,6 +57,7 @@ export const servicenowMessages = { }, actions: { openInServicenow: 'Open in ServiceNow', + clearSearch: 'Clear search', }, }; 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} + +
    ); };