Skip to content

Commit

Permalink
feat(recordings): update Automated Analysis UI (#1117)
Browse files Browse the repository at this point in the history
* update recording options UI

* update JSON report details for new API response format

* replace ReportFrame with new-style automated analysis UI

* remove View Report recording action

* only display non-default recording options

* move recording options into table column

* add refresh button
  • Loading branch information
andrewazores authored Sep 28, 2023
1 parent 14378bd commit aab3955
Show file tree
Hide file tree
Showing 20 changed files with 1,050 additions and 396 deletions.
16 changes: 8 additions & 8 deletions src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
FAILED_REPORT_MESSAGE,
NO_RECORDINGS_MESSAGE,
RECORDING_FAILURE_MESSAGE,
RuleEvaluation,
AnalysisResult,
TEMPLATE_UNSUPPORTED_MESSAGE,
} from '@app/Shared/Services/Report.service';
import { ServiceContext } from '@app/Shared/Services/Services';
Expand Down Expand Up @@ -125,7 +125,7 @@ export const AutomatedAnalysisCard: DashboardCardFC<AutomatedAnalysisCardProps>
const { t } = useTranslation();

const [targetConnectURL, setTargetConnectURL] = React.useState('');
const [evaluations, setEvaluations] = React.useState<RuleEvaluation[]>([]);
const [results, setResults] = React.useState<AnalysisResult[]>([]);

const [categorizedEvaluation, setCategorizedEvaluation] = React.useState<CategorizedRuleEvaluations[]>([]);
const [filteredCategorizedEvaluation, setFilteredCategorizedEvaluation] = React.useState<
Expand Down Expand Up @@ -155,9 +155,9 @@ export const AutomatedAnalysisCard: DashboardCardFC<AutomatedAnalysisCardProps>
}) as AutomatedAnalysisGlobalFiltersCategories;

const categorizeEvaluation = React.useCallback(
(arr: RuleEvaluation[]) => {
setEvaluations(arr);
const map = new Map<string, RuleEvaluation[]>();
(arr: AnalysisResult[]) => {
setResults(arr);
const map = new Map<string, AnalysisResult[]>();
arr.forEach((evaluation) => {
const topicValue = map.get(evaluation.topic);
if (topicValue === undefined) {
Expand All @@ -170,7 +170,7 @@ export const AutomatedAnalysisCard: DashboardCardFC<AutomatedAnalysisCardProps>
const sorted = (Array.from(map) as CategorizedRuleEvaluations[]).sort();
setCategorizedEvaluation(sorted);
},
[setCategorizedEvaluation, setEvaluations],
[setCategorizedEvaluation, setResults],
);

// Will perform analysis on the first ActiveRecording which has
Expand Down Expand Up @@ -794,7 +794,7 @@ export const AutomatedAnalysisCard: DashboardCardFC<AutomatedAnalysisCardProps>

const headerLabels = React.useMemo(() => {
if (isLoading || errorMessage) return undefined;
const filtered = evaluations.filter((e) => e.score >= AutomatedAnalysisScore.ORANGE_SCORE_THRESHOLD);
const filtered = results.filter((e) => e.score >= AutomatedAnalysisScore.ORANGE_SCORE_THRESHOLD);
if (filtered.length === 0) return <AutomatedAnalysisHeaderLabel type="ok" />;
const [warnings, errors] = _.partition(filtered, (e) => e.score < AutomatedAnalysisScore.RED_SCORE_THRESHOLD);
return (
Expand All @@ -804,7 +804,7 @@ export const AutomatedAnalysisCard: DashboardCardFC<AutomatedAnalysisCardProps>
{warnings.length > 0 && <AutomatedAnalysisHeaderLabel type={'warning'} count={warnings.length} />}
</LabelGroup>
);
}, [isLoading, errorMessage, evaluations, reportSource]);
}, [isLoading, errorMessage, results, reportSource]);

const header = React.useMemo(() => {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,24 +124,24 @@ export const AutomatedAnalysisCardList: React.FC<AutomatedAnalysisCardListProps>
</Tr>
</Thead>
<Tbody>
{flatFiltered.map((evaluation) => {
{flatFiltered.map((result) => {
return (
<Tr key={evaluation.name}>
<Tr key={result.name}>
<Td dataLabel={t('NAME', { ns: 'common' })} width={10}>
{evaluation.name}
{result.name}
</Td>
<Td dataLabel={t('SCORE', { ns: 'common' })} modifier="wrap">
<Flex spaceItems={{ default: 'spaceItemsSm' }}>
<FlexItem>
{evaluation.score == AutomatedAnalysisScore.NA_SCORE
{result.score == AutomatedAnalysisScore.NA_SCORE
? t('N/A', { ns: 'common' })
: evaluation.score.toFixed(1)}
: result.score.toFixed(1)}
</FlexItem>
<FlexItem>{icon(evaluation.score)}</FlexItem>
<FlexItem>{icon(result.score)}</FlexItem>
</Flex>
</Td>
<Td modifier="breakWord" dataLabel={t('DESCRIPTION', { ns: 'common' })}>
{transformAADescription(evaluation.description)}
{transformAADescription(result)}
</Td>
</Tr>
);
Expand Down
12 changes: 6 additions & 6 deletions src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import { allowedAutomatedAnalysisFilters } from '@app/Shared/Redux/Filters/AutomatedAnalysisFilterSlice';
import { UpdateFilterOptions } from '@app/Shared/Redux/Filters/Common';
import { automatedAnalysisUpdateCategoryIntent, RootState, StateDispatch } from '@app/Shared/Redux/ReduxStore';
import { RuleEvaluation } from '@app/Shared/Services/Report.service';
import { AnalysisResult } from '@app/Shared/Services/Report.service';
import {
Dropdown,
DropdownItem,
Expand Down Expand Up @@ -46,7 +46,7 @@ export interface AutomatedAnalysisGlobalFiltersCategories {

export interface AutomatedAnalysisFiltersProps {
target: string;
evaluations: [string, RuleEvaluation[]][];
evaluations: [string, AnalysisResult[]][];
filters: AutomatedAnalysisFiltersCategories;
updateFilters: (target: string, updateFilterOptions: UpdateFilterOptions) => void;
}
Expand Down Expand Up @@ -187,7 +187,7 @@ export const AutomatedAnalysisFilters: React.FC<AutomatedAnalysisFiltersProps> =
};

export const filterAutomatedAnalysis = (
topicEvalTuple: [string, RuleEvaluation[]][],
topicEvalTuple: [string, AnalysisResult[]][],
filters: AutomatedAnalysisFiltersCategories,
globalFilters: AutomatedAnalysisGlobalFiltersCategories,
showNAScores: boolean,
Expand All @@ -202,7 +202,7 @@ export const filterAutomatedAnalysis = (
filtered = filtered.map(([topic, evaluations]) => {
return [topic, evaluations.filter((evaluation) => filters.Name.includes(evaluation.name))] as [
string,
RuleEvaluation[],
AnalysisResult[],
];
});
}
Expand All @@ -216,12 +216,12 @@ export const filterAutomatedAnalysis = (
}
return globalFilters.Score <= evaluation.score;
}),
] as [string, RuleEvaluation[]];
] as [string, AnalysisResult[]];
});
}
if (filters.Topic != null && !!filters.Topic.length) {
filtered = filtered.map(([topic, evaluations]) => {
return [topic, evaluations.filter((_) => filters.Topic.includes(topic))] as [string, RuleEvaluation[]];
return [topic, evaluations.filter((_) => filters.Topic.includes(topic))] as [string, AnalysisResult[]];
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { AutomatedAnalysisScore, RuleEvaluation } from '@app/Shared/Services/Report.service';
import { AutomatedAnalysisScore, AnalysisResult } from '@app/Shared/Services/Report.service';
import { portalRoot } from '@app/utils/utils';
import { Label, LabelProps, Popover } from '@patternfly/react-core';
import { CheckCircleIcon, ExclamationCircleIcon, InfoCircleIcon, WarningTriangleIcon } from '@patternfly/react-icons';
Expand All @@ -25,12 +25,12 @@ import { useTranslation } from 'react-i18next';
import { transformAADescription } from '../dashboard-utils';

Check warning on line 25 in src/app/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

Dependency cycle via ./AutomatedAnalysis/AutomatedAnalysisCard:40

Check warning on line 25 in src/app/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

Dependency cycle via ./AutomatedAnalysis/AutomatedAnalysisCard:40

export interface ClickableAutomatedAnalysisLabelProps {
label: RuleEvaluation;
label: AnalysisResult;
}

export const clickableAutomatedAnalysisKey = 'clickable-automated-analysis-label';

export const ClickableAutomatedAnalysisLabel: React.FC<ClickableAutomatedAnalysisLabelProps> = ({ label }) => {
export const ClickableAutomatedAnalysisLabel: React.FC<ClickableAutomatedAnalysisLabelProps> = ({ label: result }) => {
const { t } = useTranslation();

const [isHoveredOrFocused, setIsHoveredOrFocused] = React.useState(false);
Expand All @@ -50,72 +50,72 @@ export const ClickableAutomatedAnalysisLabel: React.FC<ClickableAutomatedAnalysi
const colorScheme = React.useMemo((): LabelProps['color'] => {
// TODO: use label color schemes based on settings for accessibility
// context.settings.etc.
return label.score == AutomatedAnalysisScore.NA_SCORE
return result.score == AutomatedAnalysisScore.NA_SCORE
? 'grey'
: label.score < AutomatedAnalysisScore.ORANGE_SCORE_THRESHOLD
: result.score < AutomatedAnalysisScore.ORANGE_SCORE_THRESHOLD
? 'green'
: label.score < AutomatedAnalysisScore.RED_SCORE_THRESHOLD
: result.score < AutomatedAnalysisScore.RED_SCORE_THRESHOLD
? 'orange'
: 'red';
}, [label.score]);
}, [result.score]);

const alertPopoverVariant = React.useMemo(() => {
return label.score == AutomatedAnalysisScore.NA_SCORE
return result.score == AutomatedAnalysisScore.NA_SCORE
? 'default'
: label.score < AutomatedAnalysisScore.ORANGE_SCORE_THRESHOLD
: result.score < AutomatedAnalysisScore.ORANGE_SCORE_THRESHOLD
? 'success'
: label.score < AutomatedAnalysisScore.RED_SCORE_THRESHOLD
: result.score < AutomatedAnalysisScore.RED_SCORE_THRESHOLD
? 'warning'
: 'danger';
}, [label.score]);
}, [result.score]);

const icon = React.useMemo(() => {
return label.score == AutomatedAnalysisScore.NA_SCORE ? (
return result.score == AutomatedAnalysisScore.NA_SCORE ? (
<InfoCircleIcon />
) : label.score < AutomatedAnalysisScore.ORANGE_SCORE_THRESHOLD ? (
) : result.score < AutomatedAnalysisScore.ORANGE_SCORE_THRESHOLD ? (
<CheckCircleIcon />
) : label.score < AutomatedAnalysisScore.RED_SCORE_THRESHOLD ? (
) : result.score < AutomatedAnalysisScore.RED_SCORE_THRESHOLD ? (
<WarningTriangleIcon />
) : (
<ExclamationCircleIcon />
);
}, [label.score]);
}, [result.score]);

return (
<Popover
aria-label={t('ClickableAutomatedAnalysisLabel.ARIA_LABELS.POPOVER')}
isVisible={isDescriptionVisible}
headerContent={<div className={`${clickableAutomatedAnalysisKey}-popover-header`}>{label.name}</div>}
headerContent={<div className={`${clickableAutomatedAnalysisKey}-popover-header`}>{result.name}</div>}
alertSeverityVariant={alertPopoverVariant}
alertSeverityScreenReaderText={alertPopoverVariant}
shouldOpen={() => setIsDescriptionVisible(true)}
shouldClose={() => setIsDescriptionVisible(false)}
key={`${clickableAutomatedAnalysisKey}-popover-${label.name}`}
key={`${clickableAutomatedAnalysisKey}-popover-${result.name}`}
bodyContent={
<div
className={`${clickableAutomatedAnalysisKey}-popover-body`}
key={`${clickableAutomatedAnalysisKey}-popover-body-${label.name}`}
key={`${clickableAutomatedAnalysisKey}-popover-body-${result.name}`}
>
<p className={css(alertStyle[alertPopoverVariant], `${clickableAutomatedAnalysisKey}-popover-body-score`)}>
{label.score == AutomatedAnalysisScore.NA_SCORE ? 'N/A' : label.score.toFixed(1)}
{result.score == AutomatedAnalysisScore.NA_SCORE ? 'N/A' : result.score.toFixed(1)}
</p>
{transformAADescription(label.description)}
{transformAADescription(result)}
</div>
}
appendTo={portalRoot}
>
<Label
aria-label={label.name}
aria-label={result.name}
icon={icon}
color={colorScheme}
className={isHoveredOrFocused ? `clickable-label-hovered` : ''}
onMouseEnter={handleHoveredOrFocused}
onMouseLeave={handleNonHoveredOrFocused}
onFocus={handleHoveredOrFocused}
key={`${clickableAutomatedAnalysisKey}-${label.name}`}
key={`${clickableAutomatedAnalysisKey}-${result.name}`}
isCompact
>
<span className={`${clickableAutomatedAnalysisKey}-name`}>{`${label.name}`}</span>
<span className={`${clickableAutomatedAnalysisKey}-name`}>{`${result.name}`}</span>
{
// don't use isTruncated here, it doesn't work with the popover because of helperText
}
Expand Down
68 changes: 49 additions & 19 deletions src/app/Dashboard/dashboard-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,17 @@ import cryostatLogoDark from '@app/assets/cryostat_icon_rgb_reverse.svg';
import { dashboardConfigDeleteCardIntent } from '@app/Shared/Redux/ReduxStore';
import { FeatureLevel } from '@app/Shared/Services/Settings.service';
import { withThemedIcon } from '@app/utils/withThemedIcon';
import { LabelProps, gridSpans, Button, ButtonVariant } from '@patternfly/react-core';
import {
LabelProps,
gridSpans,
Button,
ButtonVariant,
Stack,
StackItem,
Label,
Title,
Text,
} from '@patternfly/react-core';
import { FileIcon, UnknownIcon, UserIcon } from '@patternfly/react-icons';
import { nanoid } from '@reduxjs/toolkit';
import { TFunction } from 'i18next';
Expand All @@ -31,6 +41,8 @@ import { AutomatedAnalysisCardDescriptor } from './AutomatedAnalysis/AutomatedAn
import { JFRMetricsChartCardDescriptor } from './Charts/jfr/JFRMetricsChartCard';
import { MBeanMetricsChartCardDescriptor } from './Charts/mbean/MBeanMetricsChartCard';
import { JvmDetailsCardDescriptor } from './JvmDetails/JvmDetailsCard';
import { AnalysisResult, Suggestion } from '@app/Shared/Services/Report.service';
import _ from 'lodash';

export const DEFAULT_DASHBOARD_NAME = 'Default';
export const DRAGGABLE_REF_KLAZZ = `draggable-ref`;
Expand Down Expand Up @@ -403,24 +415,42 @@ export interface DashboardCardTypeProps {
actions?: JSX.Element[];
}

export const transformAADescription = (description: string): JSX.Element => {
const splitDesc = description.split('\n\n');
const boldRegex = /^([^:]+:\s?)/; // match text up to and including the first colon

export const transformAADescription = (result: AnalysisResult): JSX.Element => {
const format = (s): JSX.Element => {
if (typeof s === 'string') {
return <Text>{s}</Text>;
}
if (Array.isArray(s)) {
return (
<Stack>
{s.map((e) => (
<StackItem key={e.setting}>
<Title headingLevel={'h6'}>{e.setting}</Title>
<Label>
{e.name}={e.value}
</Label>
</StackItem>
))}
</Stack>
);
}
throw `Unrecognized item: ${s}`;
};
return (
<>
{splitDesc.map((item, index) => {
const boldMatch = item.match(boldRegex);
const boldText = boldMatch ? boldMatch[0] : '';
const restOfText = boldMatch ? item.replace(boldRegex, '') : item;
const style = index > 0 ? { paddingTop: '0.7rem' } : {};
return (
<p key={index} style={style}>
{boldText && <strong>{boldText}</strong>}
{restOfText}
</p>
);
})}
</>
<div>
{Object.entries(result.evaluation || {}).map(([k, v]) =>
v && v.length ? (
<div key={k}>
<span>
<Title headingLevel={'h5'}>{_.capitalize(k)}</Title>
{format(result.evaluation[k])}
</span>
<br />
</div>
) : (
<div key={k}></div>
),
)}
</div>
);
};
Loading

0 comments on commit aab3955

Please sign in to comment.