feat: Refactor theme creation and integrate global styles

- Updated createMuiTheme.ts to improve theme structure and organization.
- Added GlobalStyles to App.tsx for consistent font sizing across the application.
- Introduced InlineTestCaseIncidentStatus component for better handling of test case statuses.
- Modified TestCaseIncidentManagerStatus to conditionally render InlineTestCaseIncidentStatus based on props.
- Enhanced DataQualityTab to include a new column for failed/aborted reasons and last run timestamps.
- Updated QualityTab to streamline pagination handling and improve layout.
- Added isInline prop to TestCaseStatusIncidentManagerStatus interface for inline rendering.
This commit is contained in:
Shailesh Parmar 2025-10-08 21:38:42 +05:30
parent 3f8f89be96
commit 98ec6b5f51
7 changed files with 780 additions and 118 deletions

View File

@ -11,17 +11,17 @@
* limitations under the License. * limitations under the License.
*/ */
import type { Shadows } from '@mui/material/styles'; import type { Shadows } from "@mui/material/styles";
import { createTheme } from '@mui/material/styles'; import { createTheme } from "@mui/material/styles";
import { buttonTheme } from './button-theme'; import { defaultColors } from "../colors/defaultColors";
import { dataDisplayTheme } from './data-display-theme'; import { generateAllMuiPalettes } from "../colors/generateMuiPalettes";
import { defaultColors } from '../colors/defaultColors'; import type { CustomColors, ThemeColors } from "../types";
import { formTheme } from './form-theme'; import { buttonTheme } from "./button-theme";
import { generateAllMuiPalettes } from '../colors/generateMuiPalettes'; import { dataDisplayTheme } from "./data-display-theme";
import { navigationTheme } from './navigation-theme'; import { formTheme } from "./form-theme";
import { shadows } from './shadows'; import "./mui-theme-types";
import type { CustomColors, ThemeColors } from '../types'; import { navigationTheme } from "./navigation-theme";
import './mui-theme-types'; import { shadows } from "./shadows";
/** /**
* Creates dynamic MUI theme with user customizations or default colors * Creates dynamic MUI theme with user customizations or default colors
@ -127,88 +127,90 @@ export const createMuiTheme = (
allShades: themeColors, allShades: themeColors,
}, },
typography: { typography: {
fontSize: 14,
htmlFontSize: 14,
fontFamily: fontFamily:
'var(--font-inter, "Inter"), -apple-system, "Segoe UI", Roboto, Arial, sans-serif', 'var(--font-inter, "Inter"), -apple-system, "Segoe UI", Roboto, Arial, sans-serif',
h1: { h1: {
fontSize: '3.75rem', fontSize: "3.75rem",
fontWeight: 600, fontWeight: 600,
lineHeight: '4.5rem', lineHeight: "4.5rem",
letterSpacing: '-1.2px', letterSpacing: "-1.2px",
color: 'var(--color-text-primary)', color: "var(--color-text-primary)",
}, },
h2: { h2: {
fontSize: '3rem', fontSize: "3rem",
fontWeight: 600, fontWeight: 600,
lineHeight: '3.75rem', lineHeight: "3.75rem",
letterSpacing: '-0.96px', letterSpacing: "-0.96px",
color: 'var(--color-text-primary)', color: "var(--color-text-primary)",
}, },
h3: { h3: {
fontSize: '2.25rem', fontSize: "2.25rem",
fontWeight: 600, fontWeight: 600,
lineHeight: '2.75rem', lineHeight: "2.75rem",
letterSpacing: '-0.72px', letterSpacing: "-0.72px",
color: 'var(--color-text-primary)', color: "var(--color-text-primary)",
}, },
h4: { h4: {
fontSize: '1.875rem', fontSize: "1.875rem",
fontWeight: 600, fontWeight: 600,
lineHeight: '2.375rem', lineHeight: "2.375rem",
color: 'var(--color-text-primary)', color: "var(--color-text-primary)",
}, },
h5: { h5: {
fontSize: '1.5rem', fontSize: "1.5rem",
fontWeight: 600, fontWeight: 600,
lineHeight: '2rem', lineHeight: "2rem",
color: 'var(--color-text-primary)', color: "var(--color-text-primary)",
}, },
h6: { h6: {
fontSize: '1.25rem', fontSize: "1.25rem",
fontWeight: 600, fontWeight: 600,
lineHeight: '1.875rem', lineHeight: "1.875rem",
color: 'var(--color-text-primary)', color: "var(--color-text-primary)",
}, },
subtitle1: { subtitle1: {
fontSize: '1.125rem', fontSize: "1.125rem",
lineHeight: '1.75rem', lineHeight: "1.75rem",
fontWeight: 400, fontWeight: 400,
color: 'var(--color-text-secondary)', color: "var(--color-text-secondary)",
}, },
subtitle2: { subtitle2: {
fontSize: '1rem', fontSize: "1rem",
lineHeight: '1.5rem', lineHeight: "1.5rem",
fontWeight: 500, fontWeight: 500,
color: 'var(--color-text-secondary)', color: "var(--color-text-secondary)",
}, },
body1: { body1: {
fontSize: '1rem', fontSize: "1rem",
lineHeight: '1.5rem', lineHeight: "1.5rem",
fontWeight: 400, fontWeight: 400,
color: 'var(--color-text-tertiary)', color: "var(--color-text-tertiary)",
}, },
body2: { body2: {
fontSize: '0.875rem', fontSize: "0.875rem",
lineHeight: '1.25rem', lineHeight: "1.25rem",
fontWeight: 400, fontWeight: 400,
color: 'var(--color-text-tertiary)', color: "var(--color-text-tertiary)",
}, },
caption: { caption: {
fontSize: '0.75rem', fontSize: "0.75rem",
lineHeight: '1.125rem', lineHeight: "1.125rem",
fontWeight: 400, fontWeight: 400,
color: 'var(--color-text-quaternary)', color: "var(--color-text-quaternary)",
}, },
overline: { overline: {
fontSize: '0.75rem', fontSize: "0.75rem",
lineHeight: '1.125rem', lineHeight: "1.125rem",
fontWeight: 600, fontWeight: 600,
textTransform: 'uppercase' as const, textTransform: "uppercase" as const,
letterSpacing: '0.5px', letterSpacing: "0.5px",
color: 'var(--color-text-quaternary)', color: "var(--color-text-quaternary)",
}, },
button: { button: {
fontSize: '0.875rem', fontSize: "0.875rem",
textTransform: 'none' as const, textTransform: "none" as const,
fontWeight: 600, fontWeight: 600,
}, },
}, },
@ -217,32 +219,32 @@ export const createMuiTheme = (
borderRadius: 8, borderRadius: 8,
}, },
shadows: [ shadows: [
'none', "none",
shadows.xs, shadows.xs,
shadows.sm, shadows.sm,
shadows.md, shadows.md,
shadows.lg, shadows.lg,
shadows.xl, shadows.xl,
shadows['2xl'], shadows["2xl"],
'0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)', "0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)",
// Additional shadows for MUI's 25-shadow requirement // Additional shadows for MUI's 25-shadow requirement
'0px 1px 3px rgba(10, 13, 18, 0.1), 0px 1px 2px -1px rgba(10, 13, 18, 0.1)', "0px 1px 3px rgba(10, 13, 18, 0.1), 0px 1px 2px -1px rgba(10, 13, 18, 0.1)",
'0px 4px 6px -1px rgba(10, 13, 18, 0.1), 0px 2px 4px -2px rgba(10, 13, 18, 0.06)', "0px 4px 6px -1px rgba(10, 13, 18, 0.1), 0px 2px 4px -2px rgba(10, 13, 18, 0.06)",
'0px 12px 16px -4px rgba(10, 13, 18, 0.08), 0px 4px 6px -2px rgba(10, 13, 18, 0.03), 0px 2px 2px -1px rgba(10, 13, 18, 0.04)', "0px 12px 16px -4px rgba(10, 13, 18, 0.08), 0px 4px 6px -2px rgba(10, 13, 18, 0.03), 0px 2px 2px -1px rgba(10, 13, 18, 0.04)",
'0px 12px 16px -4px rgba(10, 13, 18, 0.08), 0px 4px 6px -2px rgba(10, 13, 18, 0.03), 0px 2px 2px -1px rgba(10, 13, 18, 0.04)', "0px 12px 16px -4px rgba(10, 13, 18, 0.08), 0px 4px 6px -2px rgba(10, 13, 18, 0.03), 0px 2px 2px -1px rgba(10, 13, 18, 0.04)",
'0px 20px 24px -4px rgba(10, 13, 18, 0.08), 0px 8px 8px -4px rgba(10, 13, 18, 0.03), 0px 3px 3px -1.5px rgba(10, 13, 18, 0.04)', "0px 20px 24px -4px rgba(10, 13, 18, 0.08), 0px 8px 8px -4px rgba(10, 13, 18, 0.03), 0px 3px 3px -1.5px rgba(10, 13, 18, 0.04)",
'0px 20px 24px -4px rgba(10, 13, 18, 0.08), 0px 8px 8px -4px rgba(10, 13, 18, 0.03), 0px 3px 3px -1.5px rgba(10, 13, 18, 0.04)', "0px 20px 24px -4px rgba(10, 13, 18, 0.08), 0px 8px 8px -4px rgba(10, 13, 18, 0.03), 0px 3px 3px -1.5px rgba(10, 13, 18, 0.04)",
'0px 24px 48px -12px rgba(10, 13, 18, 0.18), 0px 4px 4px -2px rgba(10, 13, 18, 0.04)', "0px 24px 48px -12px rgba(10, 13, 18, 0.18), 0px 4px 4px -2px rgba(10, 13, 18, 0.04)",
'0px 24px 48px -12px rgba(10, 13, 18, 0.18), 0px 4px 4px -2px rgba(10, 13, 18, 0.04)', "0px 24px 48px -12px rgba(10, 13, 18, 0.18), 0px 4px 4px -2px rgba(10, 13, 18, 0.04)",
'0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)', "0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)",
'0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)', "0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)",
'0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)', "0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)",
'0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)', "0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)",
'0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)', "0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)",
'0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)', "0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)",
'0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)', "0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)",
'0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)', "0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)",
'0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)', "0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)",
] as Shadows, ] as Shadows,
components: componentThemes, components: componentThemes,
}); });

View File

@ -35,6 +35,7 @@ import {
} from './rest/settingConfigAPI'; } from './rest/settingConfigAPI';
import { getBasePath } from './utils/HistoryUtils'; import { getBasePath } from './utils/HistoryUtils';
import GlobalStyles from '@mui/material/GlobalStyles';
import { ThemeProvider } from '@mui/material/styles'; import { ThemeProvider } from '@mui/material/styles';
import { import {
createMuiTheme, createMuiTheme,
@ -106,6 +107,7 @@ const App: FC = () => {
<ErrorBoundary> <ErrorBoundary>
<AntDConfigProvider> <AntDConfigProvider>
<ThemeProvider theme={muiTheme}> <ThemeProvider theme={muiTheme}>
<GlobalStyles styles={{ html: { fontSize: '14px' } }} />
<SnackbarProvider <SnackbarProvider
Components={{ Components={{
default: SnackbarContent, default: SnackbarContent,

View File

@ -0,0 +1,581 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ArrowBack as ArrowBackIcon,
KeyboardArrowDown as ArrowDownIcon,
KeyboardArrowUp as ArrowUpIcon,
Check as CheckIcon,
Close as CloseIcon,
} from '@mui/icons-material';
import {
Avatar,
Box,
Chip,
IconButton,
List,
ListItem,
ListItemAvatar,
ListItemButton,
ListItemText,
Menu,
MenuItem,
Popover,
TextField,
Typography,
} from '@mui/material';
import { AxiosError } from 'axios';
import { debounce, isEmpty, startCase } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { EntityType } from '../../../../enums/entity.enum';
import { CreateTestCaseResolutionStatus } from '../../../../generated/api/tests/createTestCaseResolutionStatus';
import {
EntityReference,
TestCaseFailureReasonType,
TestCaseResolutionStatusTypes,
} from '../../../../generated/tests/testCaseResolutionStatus';
import { useApplicationStore } from '../../../../hooks/useApplicationStore';
import { Option } from '../../../../pages/TasksPage/TasksPage.interface';
import { postTestCaseIncidentStatus } from '../../../../rest/incidentManagerAPI';
import { getUserAndTeamSearch } from '../../../../rest/miscAPI';
import {
getEntityName,
getEntityReferenceFromEntity,
} from '../../../../utils/EntityUtils';
import { showErrorToast } from '../../../../utils/ToastUtils';
import { TestCaseStatusIncidentManagerProps } from './TestCaseIncidentManagerStatus.interface';
interface InlineTestCaseIncidentStatusProps {
data: TestCaseStatusIncidentManagerProps['data'];
hasEditPermission: boolean;
onSubmit: TestCaseStatusIncidentManagerProps['onSubmit'];
}
const STATUS_COLORS: Record<
string,
{ bg: string; color: string; border: string }
> = {
New: { bg: '#E1D3FF', color: '#7147E8', border: '#7147E8' },
Ack: { bg: '#EBF6FE', color: '#3DA2F3', border: '#3DA2F3' },
Assigned: { bg: '#FFF6E1', color: '#D99601', border: '#D99601' },
Resolved: { bg: '#E8F5E9', color: '#4CAF50', border: '#81C784' },
};
const InlineTestCaseIncidentStatus = ({
data,
hasEditPermission,
onSubmit,
}: InlineTestCaseIncidentStatusProps) => {
const { t } = useTranslation();
const { currentUser } = useApplicationStore();
const chipRef = React.useRef<HTMLDivElement>(null);
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const [showStatusMenu, setShowStatusMenu] = useState(false);
const [showAssigneePopover, setShowAssigneePopover] = useState(false);
const [showResolvedPopover, setShowResolvedPopover] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [userOptions, setUserOptions] = useState<Option[]>([]);
const [selectedAssignee, setSelectedAssignee] =
useState<EntityReference | null>(
data?.testCaseResolutionStatusDetails?.assignee ?? null
);
const [selectedReason, setSelectedReason] =
useState<TestCaseFailureReasonType | null>(null);
const [comment, setComment] = useState('');
const statusType = data.testCaseResolutionStatusType;
const initialOptions = useMemo(() => {
const assignee = data?.testCaseResolutionStatusDetails?.assignee;
if (assignee) {
return [
{
label: getEntityName(assignee),
value: assignee.id || '',
type: assignee.type,
name: assignee.name,
displayName: assignee.displayName,
},
];
}
return [];
}, [data?.testCaseResolutionStatusDetails?.assignee]);
const searchUsers = useCallback(
async (query: string) => {
try {
const res = await getUserAndTeamSearch(query, true);
const hits = res.data.hits.hits;
const suggestOptions: Option[] = hits.map((hit) => ({
label: getEntityName(hit._source),
value: hit._id ?? '',
type: hit._source.entityType,
name: hit._source.name,
displayName: hit._source.displayName,
}));
// If there's an assigned user and it's not in the results, add it at the top
if (initialOptions.length > 0) {
const assigneeId = initialOptions[0].value;
const isAssigneeInResults = suggestOptions.some(
(opt) => opt.value === assigneeId
);
if (!isAssigneeInResults) {
setUserOptions([initialOptions[0], ...suggestOptions]);
} else {
// Move assignee to top
const filteredOptions = suggestOptions.filter(
(opt) => opt.value !== assigneeId
);
setUserOptions([initialOptions[0], ...filteredOptions]);
}
} else {
setUserOptions(suggestOptions);
}
} catch (err) {
showErrorToast(err as AxiosError);
}
},
[initialOptions]
);
const debouncedSearch = useMemo(
() => debounce(searchUsers, 300),
[searchUsers]
);
const handleSearchUsers = useCallback(
(query: string) => {
if (isEmpty(query)) {
// When search is cleared, trigger search with empty query to get default results
searchUsers('');
} else {
debouncedSearch(query);
}
},
[debouncedSearch, searchUsers]
);
const submitStatusChange = useCallback(
async (
status: TestCaseResolutionStatusTypes,
additionalData?: {
assignee?: EntityReference;
reason?: TestCaseFailureReasonType;
comment?: string;
}
) => {
setIsLoading(true);
const updatedData: CreateTestCaseResolutionStatus = {
testCaseResolutionStatusType: status,
testCaseReference: data.testCaseReference?.fullyQualifiedName ?? '',
};
if (
status === TestCaseResolutionStatusTypes.Assigned &&
additionalData?.assignee
) {
updatedData.testCaseResolutionStatusDetails = {
assignee: {
name: additionalData.assignee.name,
displayName: additionalData.assignee.displayName,
id: additionalData.assignee.id,
type: EntityType.USER,
},
};
} else if (status === TestCaseResolutionStatusTypes.Resolved) {
updatedData.testCaseResolutionStatusDetails = {
testCaseFailureReason: additionalData?.reason,
testCaseFailureComment: additionalData?.comment ?? '',
resolvedBy: currentUser
? getEntityReferenceFromEntity(currentUser, EntityType.USER)
: undefined,
};
}
try {
const responseData = await postTestCaseIncidentStatus(updatedData);
onSubmit(responseData);
setShowAssigneePopover(false);
setShowResolvedPopover(false);
setSelectedAssignee(null);
setSelectedReason(null);
setComment('');
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
},
[currentUser, data.testCaseReference?.fullyQualifiedName, onSubmit]
);
const handleStatusClick = (event: React.MouseEvent<HTMLElement>) => {
if (!hasEditPermission) {
return;
}
event.stopPropagation();
event.preventDefault();
if (chipRef.current) {
setAnchorEl(chipRef.current);
// Open directly to the current status detail screen
if (statusType === TestCaseResolutionStatusTypes.Assigned) {
// Load initial user list with empty search
searchUsers('');
setShowAssigneePopover(true);
} else if (statusType === TestCaseResolutionStatusTypes.Resolved) {
// Pre-populate with existing values
setSelectedReason(
data?.testCaseResolutionStatusDetails?.testCaseFailureReason ?? null
);
setComment(
data?.testCaseResolutionStatusDetails?.testCaseFailureComment ?? ''
);
setShowResolvedPopover(true);
} else {
// For New/Ack, show the status menu
setShowStatusMenu(true);
}
}
};
const handleCloseStatusMenu = useCallback(() => {
setShowStatusMenu(false);
setAnchorEl(null);
}, []);
const handleStatusChange = useCallback(
async (newStatus: TestCaseResolutionStatusTypes) => {
setShowStatusMenu(false);
if (newStatus === TestCaseResolutionStatusTypes.Assigned) {
// Load initial user list with empty search
searchUsers('');
setShowAssigneePopover(true);
} else if (newStatus === TestCaseResolutionStatusTypes.Resolved) {
setShowResolvedPopover(true);
} else {
setAnchorEl(null);
await submitStatusChange(newStatus);
}
},
[searchUsers, submitStatusChange]
);
const handleBackToStatusMenu = useCallback(() => {
setShowAssigneePopover(false);
setShowResolvedPopover(false);
setSelectedAssignee(
data?.testCaseResolutionStatusDetails?.assignee ?? null
);
setUserOptions([]);
setSelectedReason(null);
setComment('');
setShowStatusMenu(true);
}, [data?.testCaseResolutionStatusDetails?.assignee]);
const handleCloseAllPopovers = useCallback(() => {
setShowAssigneePopover(false);
setShowResolvedPopover(false);
setShowStatusMenu(false);
setAnchorEl(null);
setSelectedAssignee(
data?.testCaseResolutionStatusDetails?.assignee ?? null
);
setUserOptions([]);
setSelectedReason(null);
setComment('');
}, [data?.testCaseResolutionStatusDetails?.assignee]);
const handleAssigneeSelect = (user: EntityReference) => {
setSelectedAssignee(user);
};
const handleAssigneeSubmit = () => {
if (selectedAssignee) {
submitStatusChange(TestCaseResolutionStatusTypes.Assigned, {
assignee: selectedAssignee,
});
}
};
const handleResolvedSubmit = () => {
if (selectedReason && comment) {
submitStatusChange(TestCaseResolutionStatusTypes.Resolved, {
reason: selectedReason,
comment,
});
}
};
const getInitials = (name: string) => {
return name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
.slice(0, 2);
};
const statusColor = STATUS_COLORS[statusType] || STATUS_COLORS.New;
return (
<Box ref={chipRef} sx={{ display: 'inline-flex', alignItems: 'center' }}>
<Chip
deleteIcon={
hasEditPermission ? (
showStatusMenu || showAssigneePopover || showResolvedPopover ? (
<ArrowUpIcon />
) : (
<ArrowDownIcon />
)
) : undefined
}
disabled={!hasEditPermission}
label={statusType}
sx={{
px: 1,
backgroundColor: statusColor.bg,
color: statusColor.color,
border: `1px solid ${statusColor.border}`,
borderRadius: '16px',
fontWeight: 500,
fontSize: '12px',
cursor: hasEditPermission ? 'pointer' : 'default',
'& .MuiChip-label': {
px: 1,
},
'& .MuiChip-deleteIcon': {
color: statusColor.color,
fontSize: '16px',
margin: '0 4px 0 -4px',
},
'&:hover': hasEditPermission
? {
backgroundColor: statusColor.bg,
opacity: 0.8,
}
: {},
}}
onClick={handleStatusClick}
onDelete={handleStatusClick}
/>
<Menu
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={showStatusMenu}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
onClose={handleCloseStatusMenu}>
{Object.values(TestCaseResolutionStatusTypes).map((status) => (
<MenuItem
key={status}
selected={status === statusType}
sx={{
minWidth: 150,
fontWeight: status === statusType ? 600 : 400,
}}
onClick={() => handleStatusChange(status)}>
{status}
</MenuItem>
))}
</Menu>
<Popover
PaperProps={{
sx: { width: 400, maxHeight: 500 },
}}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={showAssigneePopover}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
onClose={handleCloseAllPopovers}>
<Box sx={{ p: 2 }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
mb: 2,
gap: 1,
}}>
<IconButton size="small" onClick={handleBackToStatusMenu}>
<ArrowBackIcon />
</IconButton>
<Typography sx={{ fontWeight: 600, fontSize: 16 }}>
{t('label.assigned')}
</Typography>
<Box sx={{ flex: 1 }} />
<IconButton size="small" onClick={handleCloseAllPopovers}>
<CloseIcon />
</IconButton>
<IconButton
color="primary"
disabled={!selectedAssignee || isLoading}
size="small"
onClick={handleAssigneeSubmit}>
<CheckIcon />
</IconButton>
</Box>
<TextField
fullWidth
placeholder={t('label.search')}
size="small"
sx={{ mb: 2 }}
onChange={(e) => handleSearchUsers(e.target.value)}
/>
<List sx={{ maxHeight: 350, overflow: 'auto' }}>
{userOptions.map((option) => {
const user: EntityReference = {
id: option.value,
name: option.name,
displayName: option.displayName,
type: option.type || EntityType.USER,
};
return (
<ListItem disablePadding key={option.value}>
<ListItemButton
selected={selectedAssignee?.id === option.value}
onClick={() => handleAssigneeSelect(user)}>
<ListItemAvatar>
<Avatar
sx={{
width: 32,
height: 32,
fontSize: 14,
bgcolor: '#FFE7BA',
color: '#000',
}}>
{getInitials(option.displayName || option.name || 'U')}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={option.label}
primaryTypographyProps={{
fontSize: 14,
}}
/>
</ListItemButton>
</ListItem>
);
})}
</List>
</Box>
</Popover>
<Popover
PaperProps={{
sx: { width: 400 },
}}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={showResolvedPopover}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
onClose={handleCloseAllPopovers}>
<Box sx={{ p: 2 }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
mb: 2,
gap: 1,
}}>
<IconButton size="small" onClick={handleBackToStatusMenu}>
<ArrowBackIcon />
</IconButton>
<Typography sx={{ fontWeight: 600, fontSize: 16 }}>
{t('label.resolved')}
</Typography>
<Box sx={{ flex: 1 }} />
<IconButton size="small" onClick={handleCloseAllPopovers}>
<CloseIcon />
</IconButton>
<IconButton
color="primary"
disabled={!selectedReason || !comment || isLoading}
size="small"
onClick={handleResolvedSubmit}>
<CheckIcon />
</IconButton>
</Box>
<Typography
sx={{
fontSize: 14,
fontWeight: 500,
mb: 1,
'&::after': { content: '" *"', color: 'error.main' },
}}>
{t('label.reason')}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 2 }}>
{Object.values(TestCaseFailureReasonType).map((reason) => (
<Chip
color={selectedReason === reason ? 'primary' : 'default'}
key={reason}
label={startCase(reason)}
sx={{ cursor: 'pointer' }}
variant={selectedReason === reason ? 'filled' : 'outlined'}
onClick={() => setSelectedReason(reason)}
/>
))}
</Box>
<Typography
sx={{
fontSize: 14,
fontWeight: 500,
mb: 1,
'&::after': { content: '" *"', color: 'error.main' },
}}>
{t('label.comment')}
</Typography>
<TextField
fullWidth
multiline
placeholder="Enter your comment"
rows={4}
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
</Box>
</Popover>
</Box>
);
};
export default InlineTestCaseIncidentStatus;

View File

@ -27,13 +27,16 @@ import AppBadge from '../../../common/Badge/Badge.component';
import { EditIconButton } from '../../../common/IconButtons/EditIconButton'; import { EditIconButton } from '../../../common/IconButtons/EditIconButton';
import { TestCaseStatusModal } from '../../TestCaseStatusModal/TestCaseStatusModal.component'; import { TestCaseStatusModal } from '../../TestCaseStatusModal/TestCaseStatusModal.component';
import '../incident-manager.style.less'; import '../incident-manager.style.less';
import InlineTestCaseIncidentStatus from './InlineTestCaseIncidentStatus.component';
import { TestCaseStatusIncidentManagerProps } from './TestCaseIncidentManagerStatus.interface'; import { TestCaseStatusIncidentManagerProps } from './TestCaseIncidentManagerStatus.interface';
const TestCaseIncidentManagerStatus = ({ const TestCaseIncidentManagerStatus = ({
data, data,
onSubmit, onSubmit,
hasPermission, hasPermission,
newLook = false, newLook = false,
headerName, headerName,
isInline = false,
}: TestCaseStatusIncidentManagerProps) => { }: TestCaseStatusIncidentManagerProps) => {
const [isEditStatus, setIsEditStatus] = useState<boolean>(false); const [isEditStatus, setIsEditStatus] = useState<boolean>(false);
const { t } = useTranslation(); const { t } = useTranslation();
@ -107,6 +110,16 @@ const TestCaseIncidentManagerStatus = ({
); );
} }
if (isInline) {
return (
<InlineTestCaseIncidentStatus
data={data}
hasEditPermission={hasEditPermission}
onSubmit={onSubmit}
/>
);
}
return ( return (
<> <>
<Space <Space

View File

@ -19,4 +19,5 @@ export interface TestCaseStatusIncidentManagerProps {
hasPermission?: boolean; hasPermission?: boolean;
newLook?: boolean; newLook?: boolean;
headerName?: string; headerName?: string;
isInline?: boolean;
} }

View File

@ -11,6 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Typography as MuiTypography } from '@mui/material';
import { Col, Row, Skeleton, Typography } from 'antd'; import { Col, Row, Skeleton, Typography } from 'antd';
import { ColumnsType, TablePaginationConfig } from 'antd/lib/table'; import { ColumnsType, TablePaginationConfig } from 'antd/lib/table';
import { FilterValue, SorterResult } from 'antd/lib/table/interface'; import { FilterValue, SorterResult } from 'antd/lib/table/interface';
@ -173,6 +174,39 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
); );
}, },
}, },
{
title: 'Failed/aborted Reason',
dataIndex: 'testCaseResult',
key: 'Reason',
width: 200,
render: (result: TestCaseResult) => {
return result?.result ? (
<MuiTypography
sx={{
wordBreak: 'break-word',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}>
{result.result}
</MuiTypography>
) : (
'--'
);
},
},
{
title: t('label.last-run'),
dataIndex: 'testCaseResult',
key: 'lastRun',
width: 150,
sorter: true,
render: (result: TestCaseResult) => {
return <DateTimeDisplay timestamp={result?.timestamp} />;
},
},
{ {
title: t('label.name'), title: t('label.name'),
dataIndex: 'name', dataIndex: 'name',
@ -244,7 +278,7 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
title: t('label.column'), title: t('label.column'),
dataIndex: 'entityLink', dataIndex: 'entityLink',
key: 'column', key: 'column',
width: 150, width: 120,
render: (entityLink) => { render: (entityLink) => {
const isColumn = entityLink.includes('::columns::'); const isColumn = entityLink.includes('::columns::');
if (isColumn) { if (isColumn) {
@ -256,7 +290,7 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
<Typography.Paragraph <Typography.Paragraph
className="m-0" className="m-0"
data-testid={name} data-testid={name}
style={{ maxWidth: 150 }}> style={{ maxWidth: 120 }}>
{name} {name}
</Typography.Paragraph> </Typography.Paragraph>
); );
@ -279,13 +313,41 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
sortDirections: ['ascend', 'descend'], sortDirections: ['ascend', 'descend'],
}, },
{ {
title: t('label.last-run'), title: t('label.incident'),
dataIndex: 'testCaseResult', dataIndex: 'testCaseResult',
key: 'lastRun', key: 'incident',
width: 150, width: 120,
sorter: true, render: (_, record) => {
render: (result: TestCaseResult) => { const testCaseResult = testCaseStatus.find(
return <DateTimeDisplay timestamp={result?.timestamp} />; (status) =>
status.testCaseReference?.fullyQualifiedName ===
record.fullyQualifiedName
);
if (isStatusLoading) {
return <Skeleton.Input size="small" />;
}
if (!testCaseResult) {
return '--';
}
// Check if user has permission to edit incident status
const testCasePermission = testCasePermissions.find(
(permission) =>
permission.fullyQualifiedName === record.fullyQualifiedName
);
const hasEditPermission =
isEditAllowed || testCasePermission?.EditAll;
return (
<TestCaseIncidentManagerStatus
isInline
data={testCaseResult}
hasPermission={hasEditPermission}
onSubmit={handleStatusSubmit}
/>
);
}, },
}, },
{ {

View File

@ -11,7 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Box, Grid, Stack, Tab, Tabs, useTheme } from '@mui/material'; import { Box, Grid, Stack, Tab, Tabs, useTheme } from '@mui/material';
import { Col, Form, Row, Select, Space } from 'antd'; import { Form, Select, Space } from 'antd';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import QueryString from 'qs'; import QueryString from 'qs';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
@ -45,7 +45,6 @@ import {
import { getPrioritizedEditPermission } from '../../../../../utils/PermissionsUtils'; import { getPrioritizedEditPermission } from '../../../../../utils/PermissionsUtils';
import { getEntityDetailsPath } from '../../../../../utils/RouterUtils'; import { getEntityDetailsPath } from '../../../../../utils/RouterUtils';
import ErrorPlaceHolder from '../../../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import ErrorPlaceHolder from '../../../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
import NextPrevious from '../../../../common/NextPrevious/NextPrevious';
import { NextPreviousProps } from '../../../../common/NextPrevious/NextPrevious.interface'; import { NextPreviousProps } from '../../../../common/NextPrevious/NextPrevious.interface';
import Searchbar from '../../../../common/SearchBarComponent/SearchBar.component'; import Searchbar from '../../../../common/SearchBarComponent/SearchBar.component';
import SummaryCardV1 from '../../../../common/SummaryCard/SummaryCardV1'; import SummaryCardV1 from '../../../../common/SummaryCard/SummaryCardV1';
@ -75,7 +74,6 @@ export const QualityTab = () => {
paging, paging,
handlePageChange, handlePageChange,
handlePageSizeChange, handlePageSizeChange,
showPagination,
} = testCasePaging; } = testCasePaging;
const { editTest } = useMemo(() => { const { editTest } = useMemo(() => {
@ -282,6 +280,25 @@ export const QualityTab = () => {
] ]
); );
const pagingData = useMemo(() => {
return {
isNumberBased: true,
currentPage,
isLoading: isTestsLoading,
pageSize,
paging,
pagingHandler: handleTestCasePageChange,
onShowSizeChange: handlePageSizeChange,
};
}, [
currentPage,
isTestsLoading,
pageSize,
paging,
handleTestCasePageChange,
handlePageSizeChange,
]);
const handleTabChange = (_: React.SyntheticEvent, tab: string) => { const handleTabChange = (_: React.SyntheticEvent, tab: string) => {
navigate( navigate(
{ {
@ -405,39 +422,23 @@ export const QualityTab = () => {
</Box> </Box>
{isTestCaseTab && ( {isTestCaseTab && (
<Row> <DataQualityTab
<Col span={24}> removeTableBorder
<DataQualityTab afterDeleteAction={async (...params) => {
removeTableBorder await fetchAllTests(...params); // Update current count when Create / Delete operation performed
afterDeleteAction={async (...params) => { params?.length &&
await fetchAllTests(...params); // Update current count when Create / Delete operation performed (await getResourceLimit('dataQuality', true, true));
params?.length && }}
(await getResourceLimit('dataQuality', true, true)); breadcrumbData={tableBreadcrumb}
}} fetchTestCases={handleSortTestCase}
breadcrumbData={tableBreadcrumb} isEditAllowed={editTest}
fetchTestCases={handleSortTestCase} isLoading={isTestsLoading}
isEditAllowed={editTest} pagingData={pagingData}
isLoading={isTestsLoading} showTableColumn={false}
showTableColumn={false} testCases={allTestCases}
testCases={allTestCases} onTestCaseResultUpdate={onTestCaseUpdate}
onTestCaseResultUpdate={onTestCaseUpdate} onTestUpdate={onTestCaseUpdate}
onTestUpdate={onTestCaseUpdate} />
/>
</Col>
<Col span={24}>
{showPagination && (
<NextPrevious
isNumberBased
currentPage={currentPage}
isLoading={isTestsLoading}
pageSize={pageSize}
paging={paging}
pagingHandler={handleTestCasePageChange}
onShowSizeChange={handlePageSizeChange}
/>
)}
</Col>
</Row>
)} )}
{qualityTab === EntityTabs.PIPELINE && ( {qualityTab === EntityTabs.PIPELINE && (