Skip to content

Commit 7bbbc3a

Browse files
authored
Merge pull request #504 from newrelic/update-workloads-status
feat: refactor workload status calculation and display of incidents
2 parents b9d318e + 2b5f417 commit 7bbbc3a

File tree

9 files changed

+264
-97
lines changed

9 files changed

+264
-97
lines changed

src/components/signal-detail-sidebar/incidents.js

+14-5
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,32 @@ import {
77
durationStringForViolation,
88
formatTimestamp,
99
generateIncidentsList,
10+
isWorkload,
1011
} from '../../utils';
1112
import { SIGNAL_TYPES, UI_CONTENT } from '../../constants';
1213

1314
const Incidents = ({ type, data, timeWindow }) => {
1415
const [bannerMessage, setBannerMessage] = useState('');
1516
const [incidentsList, setIncidentsList] = useState([]);
1617
const [maxIncidentsShown, setMaxIncidentsShown] = useState(1);
18+
const [shouldShowAllIncidents, setShouldShowAllIncidents] = useState(false);
1719

1820
useEffect(() => {
1921
if (!data) return;
2022

2123
const incids = generateIncidentsList({ type, data, timeWindow });
24+
const showAll = type === SIGNAL_TYPES.ALERT || isWorkload(data);
25+
let bnrMsg = '';
26+
if (isWorkload(data)) {
27+
bnrMsg = UI_CONTENT.SIGNAL.DETAILS.WORKLOAD_RULES_DISCLAIMER;
28+
}
29+
if (!incids.length) {
30+
bnrMsg = UI_CONTENT.SIGNAL.DETAILS.NO_RECENT_INCIDENTS;
31+
}
2232

23-
setBannerMessage(
24-
incids.length ? '' : UI_CONTENT.SIGNAL.DETAILS.NO_RECENT_INCIDENTS
25-
);
26-
setMaxIncidentsShown(type === SIGNAL_TYPES.ALERT ? incids.length || 0 : 1);
33+
setBannerMessage(bnrMsg);
34+
setShouldShowAllIncidents(showAll);
35+
setMaxIncidentsShown(() => (showAll ? incids.length || 0 : 1));
2736
setIncidentsList(incids);
2837
}, [type, data, timeWindow]);
2938

@@ -96,7 +105,7 @@ const Incidents = ({ type, data, timeWindow }) => {
96105
[]
97106
)}
98107
</div>
99-
{type === SIGNAL_TYPES.ENTITY && incidentsList?.length > 1 ? (
108+
{type === !shouldShowAllIncidents && incidentsList?.length > 1 ? (
100109
<div className="incidents-footer">
101110
<Button
102111
variant={Button.VARIANT.SECONDARY}

src/components/signal-detail-sidebar/index.js

+96-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useContext, useEffect, useState } from 'react';
1+
import React, { useContext, useEffect, useRef, useState } from 'react';
22
import PropTypes from 'prop-types';
33

44
import {
@@ -7,18 +7,20 @@ import {
77
HeadingText,
88
InlineMessage,
99
Link,
10+
NerdGraphQuery,
1011
SectionMessage,
1112
navigation,
1213
} from 'nr1';
1314

14-
import { SIGNAL_TYPES } from '../../constants';
15+
import { SIGNAL_TYPES, STATUSES } from '../../constants';
1516

1617
import Incidents from './incidents';
1718
import GoldenMetrics from './golden-metrics';
1819
import { AppContext } from '../../contexts';
1920

2021
import typesList from '../../../nerdlets/signal-selection/types.json';
21-
import { formatTimestamp } from '../../utils';
22+
import { formatTimestamp, isWorkload } from '../../utils';
23+
import { statusesFromGuidsArray, workloadEntitiesQuery } from '../../queries';
2224

2325
const NO_ENTITY_TYPE = '(unknown entity type)';
2426

@@ -36,6 +38,8 @@ const SignalDetailSidebar = ({ guid, name, type, data, timeWindow }) => {
3638
const [hasAccessToEntity, setHasAccessToEntity] = useState(false);
3739
const [signalAccount, setSignalAccount] = useState();
3840
const [detailLinkText, setDetailLinkText] = useState('View entity details');
41+
const [incidentsData, setIncidentsData] = useState(null);
42+
const prevTimeWindow = useRef({});
3943

4044
useEffect(() => {
4145
const [acctId, condId] = ((atob(guid) || '').split('|') || []).reduce(
@@ -59,6 +63,94 @@ const SignalDetailSidebar = ({ guid, name, type, data, timeWindow }) => {
5963
}
6064
}, [guid, type, account, accounts]);
6165

66+
useEffect(() => {
67+
if (
68+
type !== SIGNAL_TYPES.ENTITY ||
69+
!isWorkload(data) ||
70+
!(
71+
data.statusValueCode === 3 ||
72+
data.statusValueCode === 2 ||
73+
data.alertSeverity === STATUSES.CRITICAL ||
74+
data.alertSeverity === STATUSES.WARNING
75+
)
76+
) {
77+
setIncidentsData(data);
78+
return;
79+
}
80+
81+
let shouldClearData = false;
82+
const { start: prevStart, end: prevEnd } = prevTimeWindow.current || {};
83+
if (timeWindow.start !== prevStart || timeWindow.end !== prevEnd) {
84+
shouldClearData = true;
85+
prevTimeWindow.current = timeWindow;
86+
}
87+
88+
const loadWorkloadIncidents = async (g) => {
89+
const {
90+
data: {
91+
actor: {
92+
entity: { relatedEntities: { results = [] } = {} } = {},
93+
} = {},
94+
} = {},
95+
error,
96+
} = await NerdGraphQuery.query({
97+
query: workloadEntitiesQuery,
98+
variables: { guid: g },
99+
});
100+
if (error) {
101+
console.error('Error listing workload entities', error);
102+
return;
103+
}
104+
const workloadEntitiesGuids = results?.reduce(
105+
(acc, { target: { guid: g } = {} }) => (g ? [...acc, g] : acc),
106+
[]
107+
);
108+
const {
109+
data: { actor: { __typename, ...entitiesObj } = {} } = {}, // eslint-disable-line no-unused-vars
110+
error: error2,
111+
} = await NerdGraphQuery.query({
112+
query: statusesFromGuidsArray([workloadEntitiesGuids], timeWindow),
113+
});
114+
if (error2) {
115+
console.error('Error loading workload entities', error2);
116+
return;
117+
}
118+
const violationsKey =
119+
timeWindow?.start && timeWindow?.end
120+
? 'alertViolations'
121+
: 'recentAlertViolations';
122+
Object.keys(entitiesObj).forEach((entitiesKey) => {
123+
entitiesObj[entitiesKey]?.forEach((entity) => {
124+
if (isWorkload(entity)) {
125+
loadWorkloadIncidents(entity.guid);
126+
return;
127+
}
128+
const violations = entity?.[violationsKey] || [];
129+
if (violations.length) {
130+
setIncidentsData((d) =>
131+
d && !shouldClearData
132+
? {
133+
...d,
134+
[violationsKey]: [
135+
...(d[violationsKey] || []),
136+
...violations,
137+
],
138+
}
139+
: {
140+
...data,
141+
[violationsKey]: [
142+
...(data[violationsKey] || []),
143+
...violations,
144+
],
145+
}
146+
);
147+
}
148+
});
149+
});
150+
};
151+
loadWorkloadIncidents(guid);
152+
}, [guid, type, data, timeWindow]);
153+
62154
return (
63155
<div className="signal-detail-sidebar">
64156
<div className="signal-header">
@@ -98,7 +190,7 @@ const SignalDetailSidebar = ({ guid, name, type, data, timeWindow }) => {
98190
</div>
99191
{hasAccessToEntity ? (
100192
<>
101-
<Incidents type={type} data={data} timeWindow={timeWindow} />
193+
<Incidents type={type} data={incidentsData} timeWindow={timeWindow} />
102194
{type === SIGNAL_TYPES.ENTITY ? (
103195
<GoldenMetrics guid={guid} data={data} timeWindow={timeWindow} />
104196
) : null}

src/components/stages/index.js

+65-65
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
incidentsSearchQuery,
3838
issuesForConditionsQuery,
3939
statusesFromGuidsArray,
40+
workloadsStatusesQuery,
4041
} from '../../queries';
4142
import {
4243
addSignalStatuses,
@@ -45,8 +46,11 @@ import {
4546
batchAlertConditionsByAccount,
4647
batchedIncidentIdsFromIssuesQuery,
4748
entitiesDetailsFromQueryResults,
49+
fifteenMinutesAgo,
50+
getWorstWorkloadStatusValue,
4851
guidsToArray,
4952
incidentsFromIncidentsBlocks,
53+
isWorkload,
5054
signalDetailsObject,
5155
statusFromStatuses,
5256
threeDaysAgo,
@@ -61,7 +65,6 @@ import {
6165
UI_CONTENT,
6266
ALERT_STATUSES,
6367
MAX_PARAMS_IN_QUERY,
64-
WORKLOAD_TYPE,
6568
} from '../../constants';
6669
import { useDebugLogger } from '../../hooks';
6770

@@ -121,11 +124,11 @@ const Stages = forwardRef(
121124
async (entitiesGuids, timeWindow, isForCache) => {
122125
setIsLoading?.(true);
123126
clearTimeout(entitiesStatusTimeoutId.current);
124-
const entitesGuidsArray = guidsToArray(
127+
const entitiesGuidsArray = guidsToArray(
125128
{ entitiesGuids },
126129
MAX_PARAMS_IN_QUERY
127130
);
128-
const query = statusesFromGuidsArray(entitesGuidsArray, timeWindow);
131+
const query = statusesFromGuidsArray(entitiesGuidsArray, timeWindow);
129132
debugString(query, 'Entities query');
130133
const { data: { actor = {} } = {}, error } = await NerdGraphQuery.query(
131134
{
@@ -144,66 +147,6 @@ const Stages = forwardRef(
144147
return;
145148
}
146149
const entitiesStatusesObj = entitiesDetailsFromQueryResults(actor);
147-
const { workloads, workloadEntities } = Object.keys(
148-
entitiesStatusesObj
149-
).reduce(
150-
(acc, cur) => {
151-
const { type: t, relatedEntities } = entitiesStatusesObj[cur];
152-
if (t === WORKLOAD_TYPE && relatedEntities?.results?.length) {
153-
const { workloadEntitiesGuids, guidsWOStatus } =
154-
relatedEntities.results.reduce(
155-
({ workloadEntitiesGuids, guidsWOStatus }, re) => {
156-
const g = re?.target?.entity?.guid;
157-
if (!g) return { workloadEntitiesGuids, guidsWOStatus };
158-
return {
159-
workloadEntitiesGuids: [...workloadEntitiesGuids, g],
160-
guidsWOStatus:
161-
g in entitiesStatusesObj || guidsWOStatus.includes(g)
162-
? guidsWOStatus
163-
: [...guidsWOStatus, g],
164-
};
165-
},
166-
{
167-
workloadEntitiesGuids: [],
168-
guidsWOStatus: acc.workloadEntities,
169-
}
170-
);
171-
return {
172-
...acc,
173-
workloads: {
174-
...acc.workloads,
175-
[cur]: workloadEntitiesGuids,
176-
},
177-
workloadEntities: guidsWOStatus,
178-
};
179-
} else {
180-
return acc;
181-
}
182-
},
183-
{ workloads: {}, workloadEntities: [] }
184-
);
185-
if (Object.keys(workloads).length && workloadEntities.length) {
186-
const workloadEntitiesStatuses = await fetchEntitiesStatus(
187-
workloadEntities,
188-
timeWindow,
189-
true
190-
);
191-
const key =
192-
timeWindow?.start && timeWindow?.end
193-
? 'alertViolations'
194-
: 'recentAlertViolations';
195-
Object.keys(workloads).forEach((wlg) => {
196-
entitiesStatusesObj[wlg][key] = workloads[wlg]?.reduce(
197-
(acc, cur) => [
198-
...acc,
199-
...(entitesGuidsArray[cur]?.[key] ||
200-
workloadEntitiesStatuses[cur]?.[key] ||
201-
[]),
202-
],
203-
entitiesStatusesObj[wlg][key] || []
204-
);
205-
});
206-
}
207150

208151
if (isForCache) return entitiesStatusesObj;
209152
if (statusTimeoutDelay.current && !timeWindow) {
@@ -535,14 +478,71 @@ const Stages = forwardRef(
535478
}
536479
setIsLoading(true);
537480

481+
const { [SIGNAL_TYPES.ENTITY]: entitiesGuids = [] } = guids;
482+
const workloads = entitiesGuids?.reduce((acc, cur) => {
483+
const [acctId, domain, type] = atob(cur)?.split('|') || [];
484+
return acctId && isWorkload({ domain, type })
485+
? {
486+
...acc,
487+
[acctId]: [...(acc[acctId] || []), cur],
488+
}
489+
: acc;
490+
}, {});
491+
let workloadsStatuses = {};
492+
if (Object.keys(workloads)?.length) {
493+
const { data: { actor: w = {} } = {}, error } =
494+
await NerdGraphQuery.query({
495+
query: workloadsStatusesQuery(workloads, {
496+
start: fifteenMinutesAgo(timeBands?.[0]?.start),
497+
end: timeBands?.[timeBands.length - 1]?.end,
498+
}),
499+
});
500+
if (!error && w) {
501+
workloadsStatuses = Object.keys(w)?.reduce((acc, key) => {
502+
if (key === '__typename') return acc;
503+
const r = w[key].results || [];
504+
return {
505+
...acc,
506+
...r.reduce(
507+
(acc2, { statusValueCode, timestamp, workloadGuid }) => ({
508+
...acc2,
509+
[workloadGuid]: [
510+
...(acc2[workloadGuid] || []),
511+
{ statusValueCode, timestamp },
512+
],
513+
}),
514+
{}
515+
),
516+
};
517+
}, {});
518+
}
519+
}
520+
538521
timeBands.forEach(async (timeWindow, idx) => {
539522
const { key, alertsStatusesObj } = timeBandsDataArray[idx] || {};
540523
const timeWindowCachedData = timeBandDataCache.current.get(key);
541524
if (!timeWindowCachedData) {
542-
const { [SIGNAL_TYPES.ENTITY]: entitiesGuids = [] } = guids;
543-
const entitiesStatusesObj = entitiesGuids.length
525+
let entitiesStatusesObj = entitiesGuids.length
544526
? await fetchEntitiesStatus(entitiesGuids, timeWindow, true)
545527
: {};
528+
entitiesStatusesObj = Object.keys(entitiesStatusesObj)?.reduce(
529+
(acc, cur) => {
530+
const e = entitiesStatusesObj[cur];
531+
if (isWorkload(e))
532+
return {
533+
...acc,
534+
[cur]: {
535+
...e,
536+
statusValueCode: getWorstWorkloadStatusValue(
537+
workloadsStatuses[e.guid],
538+
timeWindow
539+
),
540+
},
541+
};
542+
return { ...acc, [cur]: e };
543+
},
544+
{}
545+
);
546546
const timeWindowStatuses = {
547547
[SIGNAL_TYPES.ENTITY]: entitiesStatusesObj,
548548
[SIGNAL_TYPES.ALERT]: alertsStatusesObj,

src/constants/entities.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
export const WORKLOAD_TYPE = 'WORKLOAD';
1+
export const WORKLOAD = {
2+
DOMAIN: 'NR1',
3+
TYPE: 'WORKLOAD',
4+
};

src/constants/ui-content.js

+2
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ export const UI_CONTENT = {
139139
'No open incidents. Showing the most recent incident.',
140140
ALERTING_SL_NO_INCIDENT:
141141
"This Service Level is in an unhealthy state, however we can't find any correlated incidents. It is likely that the Service Level is out of compliance - to investigate further, please open the entity view (linked above).",
142+
WORKLOAD_RULES_DISCLAIMER:
143+
'Please note that if your workload has custom health status rules, it is possible that some issues listed here may not be applicable to the workload health. You can review your workload health status rules to confirm.',
142144
},
143145
TOOLTIP: {
144146
WORKLOAD_UNKNOWN: 'The status of this workload is unknown',

0 commit comments

Comments
 (0)