mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-10 16:25:37 +00:00
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:
parent
3f8f89be96
commit
98ec6b5f51
@ -11,17 +11,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Shadows } from '@mui/material/styles';
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
import { buttonTheme } from './button-theme';
|
||||
import { dataDisplayTheme } from './data-display-theme';
|
||||
import { defaultColors } from '../colors/defaultColors';
|
||||
import { formTheme } from './form-theme';
|
||||
import { generateAllMuiPalettes } from '../colors/generateMuiPalettes';
|
||||
import { navigationTheme } from './navigation-theme';
|
||||
import { shadows } from './shadows';
|
||||
import type { CustomColors, ThemeColors } from '../types';
|
||||
import './mui-theme-types';
|
||||
import type { Shadows } from "@mui/material/styles";
|
||||
import { createTheme } from "@mui/material/styles";
|
||||
import { defaultColors } from "../colors/defaultColors";
|
||||
import { generateAllMuiPalettes } from "../colors/generateMuiPalettes";
|
||||
import type { CustomColors, ThemeColors } from "../types";
|
||||
import { buttonTheme } from "./button-theme";
|
||||
import { dataDisplayTheme } from "./data-display-theme";
|
||||
import { formTheme } from "./form-theme";
|
||||
import "./mui-theme-types";
|
||||
import { navigationTheme } from "./navigation-theme";
|
||||
import { shadows } from "./shadows";
|
||||
|
||||
/**
|
||||
* Creates dynamic MUI theme with user customizations or default colors
|
||||
@ -127,88 +127,90 @@ export const createMuiTheme = (
|
||||
allShades: themeColors,
|
||||
},
|
||||
typography: {
|
||||
fontSize: 14,
|
||||
htmlFontSize: 14,
|
||||
fontFamily:
|
||||
'var(--font-inter, "Inter"), -apple-system, "Segoe UI", Roboto, Arial, sans-serif',
|
||||
h1: {
|
||||
fontSize: '3.75rem',
|
||||
fontSize: "3.75rem",
|
||||
fontWeight: 600,
|
||||
lineHeight: '4.5rem',
|
||||
letterSpacing: '-1.2px',
|
||||
color: 'var(--color-text-primary)',
|
||||
lineHeight: "4.5rem",
|
||||
letterSpacing: "-1.2px",
|
||||
color: "var(--color-text-primary)",
|
||||
},
|
||||
h2: {
|
||||
fontSize: '3rem',
|
||||
fontSize: "3rem",
|
||||
fontWeight: 600,
|
||||
lineHeight: '3.75rem',
|
||||
letterSpacing: '-0.96px',
|
||||
color: 'var(--color-text-primary)',
|
||||
lineHeight: "3.75rem",
|
||||
letterSpacing: "-0.96px",
|
||||
color: "var(--color-text-primary)",
|
||||
},
|
||||
h3: {
|
||||
fontSize: '2.25rem',
|
||||
fontSize: "2.25rem",
|
||||
fontWeight: 600,
|
||||
lineHeight: '2.75rem',
|
||||
letterSpacing: '-0.72px',
|
||||
color: 'var(--color-text-primary)',
|
||||
lineHeight: "2.75rem",
|
||||
letterSpacing: "-0.72px",
|
||||
color: "var(--color-text-primary)",
|
||||
},
|
||||
h4: {
|
||||
fontSize: '1.875rem',
|
||||
fontSize: "1.875rem",
|
||||
fontWeight: 600,
|
||||
lineHeight: '2.375rem',
|
||||
color: 'var(--color-text-primary)',
|
||||
lineHeight: "2.375rem",
|
||||
color: "var(--color-text-primary)",
|
||||
},
|
||||
h5: {
|
||||
fontSize: '1.5rem',
|
||||
fontSize: "1.5rem",
|
||||
fontWeight: 600,
|
||||
lineHeight: '2rem',
|
||||
color: 'var(--color-text-primary)',
|
||||
lineHeight: "2rem",
|
||||
color: "var(--color-text-primary)",
|
||||
},
|
||||
h6: {
|
||||
fontSize: '1.25rem',
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: 600,
|
||||
lineHeight: '1.875rem',
|
||||
color: 'var(--color-text-primary)',
|
||||
lineHeight: "1.875rem",
|
||||
color: "var(--color-text-primary)",
|
||||
},
|
||||
subtitle1: {
|
||||
fontSize: '1.125rem',
|
||||
lineHeight: '1.75rem',
|
||||
fontSize: "1.125rem",
|
||||
lineHeight: "1.75rem",
|
||||
fontWeight: 400,
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: "var(--color-text-secondary)",
|
||||
},
|
||||
subtitle2: {
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.5rem',
|
||||
fontSize: "1rem",
|
||||
lineHeight: "1.5rem",
|
||||
fontWeight: 500,
|
||||
color: 'var(--color-text-secondary)',
|
||||
color: "var(--color-text-secondary)",
|
||||
},
|
||||
body1: {
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.5rem',
|
||||
fontSize: "1rem",
|
||||
lineHeight: "1.5rem",
|
||||
fontWeight: 400,
|
||||
color: 'var(--color-text-tertiary)',
|
||||
color: "var(--color-text-tertiary)",
|
||||
},
|
||||
body2: {
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: '1.25rem',
|
||||
fontSize: "0.875rem",
|
||||
lineHeight: "1.25rem",
|
||||
fontWeight: 400,
|
||||
color: 'var(--color-text-tertiary)',
|
||||
color: "var(--color-text-tertiary)",
|
||||
},
|
||||
caption: {
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: '1.125rem',
|
||||
fontSize: "0.75rem",
|
||||
lineHeight: "1.125rem",
|
||||
fontWeight: 400,
|
||||
color: 'var(--color-text-quaternary)',
|
||||
color: "var(--color-text-quaternary)",
|
||||
},
|
||||
overline: {
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: '1.125rem',
|
||||
fontSize: "0.75rem",
|
||||
lineHeight: "1.125rem",
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase' as const,
|
||||
letterSpacing: '0.5px',
|
||||
color: 'var(--color-text-quaternary)',
|
||||
textTransform: "uppercase" as const,
|
||||
letterSpacing: "0.5px",
|
||||
color: "var(--color-text-quaternary)",
|
||||
},
|
||||
button: {
|
||||
fontSize: '0.875rem',
|
||||
textTransform: 'none' as const,
|
||||
fontSize: "0.875rem",
|
||||
textTransform: "none" as const,
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
@ -217,32 +219,32 @@ export const createMuiTheme = (
|
||||
borderRadius: 8,
|
||||
},
|
||||
shadows: [
|
||||
'none',
|
||||
"none",
|
||||
shadows.xs,
|
||||
shadows.sm,
|
||||
shadows.md,
|
||||
shadows.lg,
|
||||
shadows.xl,
|
||||
shadows['2xl'],
|
||||
'0px 32px 64px -12px rgba(10, 13, 18, 0.14), 0px 5px 5px -2.5px rgba(10, 13, 18, 0.04)',
|
||||
shadows["2xl"],
|
||||
"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
|
||||
'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 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 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 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 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 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)",
|
||||
] as Shadows,
|
||||
components: componentThemes,
|
||||
});
|
||||
|
@ -35,6 +35,7 @@ import {
|
||||
} from './rest/settingConfigAPI';
|
||||
import { getBasePath } from './utils/HistoryUtils';
|
||||
|
||||
import GlobalStyles from '@mui/material/GlobalStyles';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import {
|
||||
createMuiTheme,
|
||||
@ -106,6 +107,7 @@ const App: FC = () => {
|
||||
<ErrorBoundary>
|
||||
<AntDConfigProvider>
|
||||
<ThemeProvider theme={muiTheme}>
|
||||
<GlobalStyles styles={{ html: { fontSize: '14px' } }} />
|
||||
<SnackbarProvider
|
||||
Components={{
|
||||
default: SnackbarContent,
|
||||
|
@ -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;
|
@ -27,13 +27,16 @@ import AppBadge from '../../../common/Badge/Badge.component';
|
||||
import { EditIconButton } from '../../../common/IconButtons/EditIconButton';
|
||||
import { TestCaseStatusModal } from '../../TestCaseStatusModal/TestCaseStatusModal.component';
|
||||
import '../incident-manager.style.less';
|
||||
import InlineTestCaseIncidentStatus from './InlineTestCaseIncidentStatus.component';
|
||||
import { TestCaseStatusIncidentManagerProps } from './TestCaseIncidentManagerStatus.interface';
|
||||
|
||||
const TestCaseIncidentManagerStatus = ({
|
||||
data,
|
||||
onSubmit,
|
||||
hasPermission,
|
||||
newLook = false,
|
||||
headerName,
|
||||
isInline = false,
|
||||
}: TestCaseStatusIncidentManagerProps) => {
|
||||
const [isEditStatus, setIsEditStatus] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
@ -107,6 +110,16 @@ const TestCaseIncidentManagerStatus = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (isInline) {
|
||||
return (
|
||||
<InlineTestCaseIncidentStatus
|
||||
data={data}
|
||||
hasEditPermission={hasEditPermission}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Space
|
||||
|
@ -19,4 +19,5 @@ export interface TestCaseStatusIncidentManagerProps {
|
||||
hasPermission?: boolean;
|
||||
newLook?: boolean;
|
||||
headerName?: string;
|
||||
isInline?: boolean;
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Typography as MuiTypography } from '@mui/material';
|
||||
import { Col, Row, Skeleton, Typography } from 'antd';
|
||||
import { ColumnsType, TablePaginationConfig } from 'antd/lib/table';
|
||||
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'),
|
||||
dataIndex: 'name',
|
||||
@ -244,7 +278,7 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
|
||||
title: t('label.column'),
|
||||
dataIndex: 'entityLink',
|
||||
key: 'column',
|
||||
width: 150,
|
||||
width: 120,
|
||||
render: (entityLink) => {
|
||||
const isColumn = entityLink.includes('::columns::');
|
||||
if (isColumn) {
|
||||
@ -256,7 +290,7 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
|
||||
<Typography.Paragraph
|
||||
className="m-0"
|
||||
data-testid={name}
|
||||
style={{ maxWidth: 150 }}>
|
||||
style={{ maxWidth: 120 }}>
|
||||
{name}
|
||||
</Typography.Paragraph>
|
||||
);
|
||||
@ -279,13 +313,41 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
{
|
||||
title: t('label.last-run'),
|
||||
title: t('label.incident'),
|
||||
dataIndex: 'testCaseResult',
|
||||
key: 'lastRun',
|
||||
width: 150,
|
||||
sorter: true,
|
||||
render: (result: TestCaseResult) => {
|
||||
return <DateTimeDisplay timestamp={result?.timestamp} />;
|
||||
key: 'incident',
|
||||
width: 120,
|
||||
render: (_, record) => {
|
||||
const testCaseResult = testCaseStatus.find(
|
||||
(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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -11,7 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
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 QueryString from 'qs';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
@ -45,7 +45,6 @@ import {
|
||||
import { getPrioritizedEditPermission } from '../../../../../utils/PermissionsUtils';
|
||||
import { getEntityDetailsPath } from '../../../../../utils/RouterUtils';
|
||||
import ErrorPlaceHolder from '../../../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import NextPrevious from '../../../../common/NextPrevious/NextPrevious';
|
||||
import { NextPreviousProps } from '../../../../common/NextPrevious/NextPrevious.interface';
|
||||
import Searchbar from '../../../../common/SearchBarComponent/SearchBar.component';
|
||||
import SummaryCardV1 from '../../../../common/SummaryCard/SummaryCardV1';
|
||||
@ -75,7 +74,6 @@ export const QualityTab = () => {
|
||||
paging,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
showPagination,
|
||||
} = testCasePaging;
|
||||
|
||||
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) => {
|
||||
navigate(
|
||||
{
|
||||
@ -405,39 +422,23 @@ export const QualityTab = () => {
|
||||
</Box>
|
||||
|
||||
{isTestCaseTab && (
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<DataQualityTab
|
||||
removeTableBorder
|
||||
afterDeleteAction={async (...params) => {
|
||||
await fetchAllTests(...params); // Update current count when Create / Delete operation performed
|
||||
params?.length &&
|
||||
(await getResourceLimit('dataQuality', true, true));
|
||||
}}
|
||||
breadcrumbData={tableBreadcrumb}
|
||||
fetchTestCases={handleSortTestCase}
|
||||
isEditAllowed={editTest}
|
||||
isLoading={isTestsLoading}
|
||||
showTableColumn={false}
|
||||
testCases={allTestCases}
|
||||
onTestCaseResultUpdate={onTestCaseUpdate}
|
||||
onTestUpdate={onTestCaseUpdate}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
{showPagination && (
|
||||
<NextPrevious
|
||||
isNumberBased
|
||||
currentPage={currentPage}
|
||||
isLoading={isTestsLoading}
|
||||
pageSize={pageSize}
|
||||
paging={paging}
|
||||
pagingHandler={handleTestCasePageChange}
|
||||
onShowSizeChange={handlePageSizeChange}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<DataQualityTab
|
||||
removeTableBorder
|
||||
afterDeleteAction={async (...params) => {
|
||||
await fetchAllTests(...params); // Update current count when Create / Delete operation performed
|
||||
params?.length &&
|
||||
(await getResourceLimit('dataQuality', true, true));
|
||||
}}
|
||||
breadcrumbData={tableBreadcrumb}
|
||||
fetchTestCases={handleSortTestCase}
|
||||
isEditAllowed={editTest}
|
||||
isLoading={isTestsLoading}
|
||||
pagingData={pagingData}
|
||||
showTableColumn={false}
|
||||
testCases={allTestCases}
|
||||
onTestCaseResultUpdate={onTestCaseUpdate}
|
||||
onTestUpdate={onTestCaseUpdate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{qualityTab === EntityTabs.PIPELINE && (
|
||||
|
Loading…
x
Reference in New Issue
Block a user