Skip to content

Commit 34cb471

Browse files
Konstantinov-InnokentiiMaxim MordasovFerril
authored
Support prescribed labels (#3848)
# What this PR does **Cleanup label typing:** 1. LabelParam -> two separate types LabekKey and LabelValue 2. LabelData -> renamed to LabelPair. 3. LabelKeyData -> renamed to LabelOption Data is not giving any info about what this type represents. 4. Remove LabelsData and LabelsKeysData types. They are just list of types listed above and with new naming it feels obsolete. 5. ValueData removed. LabelPair is used instead. 6. Rework AlertGroupCustomLabel to use LabelKey type for key to make type system more consistent. Name model type AlertGroupCustomLabel**DB** and api type AlertGroupCustomLabel**API** to clearly distinguish them. **Split update_labels_cache into two tasks** update_label_option_cache and update_label_pairs_cache. Original task was expecting array of LabelsData (now it's LabelPair) OR one LabelKeyData ( now it's LabelOption). I believe having one function with two sp different argument types makes it more complicated for understanding. **Make OnCall backend support prescribed labels**. OnCall will sync and store "prescribed" field for key and values, so Label dropdown able to disable editing for certain labels. ## Which issue(s) this PR fixes ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --------- Co-authored-by: Maxim Mordasov <[email protected]> Co-authored-by: Yulya Artyukhina <[email protected]>
1 parent bad750c commit 34cb471

File tree

8 files changed

+234
-87
lines changed

8 files changed

+234
-87
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@
124124
"@grafana/data": "^10.2.3",
125125
"@grafana/faro-web-sdk": "^1.0.0-beta4",
126126
"@grafana/faro-web-tracing": "^1.0.0-beta4",
127-
"@grafana/labels": "~1.4.4",
127+
"@grafana/labels": "1.5.0",
128128
"@grafana/runtime": "^10.2.2",
129129
"@grafana/scenes": "^1.28.0",
130130
"@grafana/schema": "^10.2.2",

src/components/LabelTag/LabelTag.tsx

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React from 'react';
2+
3+
import { css } from '@emotion/css';
4+
import { GrafanaTheme2 } from '@grafana/data';
5+
import { HorizontalGroup, getTagColorsFromName, useStyles2 } from '@grafana/ui';
6+
import tinycolor2 from 'tinycolor2';
7+
8+
export interface LabelTagProps {
9+
label: string;
10+
value: string;
11+
size?: LabelTagSize;
12+
}
13+
14+
export type LabelTagSize = 'md' | 'sm';
15+
16+
export const LabelTag: React.FC<LabelTagProps> = (props: LabelTagProps) => {
17+
const { label, value, size = 'sm' } = props;
18+
19+
const color = getLabelColor(label);
20+
21+
const styles = useStyles2((theme) => getStyles(theme, color, size));
22+
23+
return (
24+
<div className={styles.wrapper} role="listitem">
25+
<HorizontalGroup spacing="none">
26+
<div className={styles.label}>{label ?? ''}</div>
27+
<div className={styles.value}>{value}</div>
28+
</HorizontalGroup>
29+
</div>
30+
);
31+
};
32+
33+
function getLabelColor(input: string): string {
34+
return getTagColorsFromName(input).color;
35+
}
36+
37+
const getStyles = (theme: GrafanaTheme2, color?: string, size?: string) => {
38+
const backgroundColor = color ?? theme.colors.secondary.main;
39+
40+
const borderColor = theme.isDark
41+
? tinycolor2(backgroundColor).lighten(5).toString()
42+
: tinycolor2(backgroundColor).darken(5).toString();
43+
44+
const valueBackgroundColor = theme.isDark
45+
? tinycolor2(backgroundColor).darken(5).toString()
46+
: tinycolor2(backgroundColor).lighten(5).toString();
47+
48+
const fontColor = color
49+
? tinycolor2.mostReadable(backgroundColor, ['#000', '#fff']).toString()
50+
: theme.colors.text.primary;
51+
52+
const padding =
53+
size === 'md' ? `${theme.spacing(0.33)} ${theme.spacing(1)}` : `${theme.spacing(0.2)} ${theme.spacing(0.6)}`;
54+
55+
return {
56+
wrapper: css`
57+
color: ${fontColor};
58+
font-size: ${theme.typography.bodySmall.fontSize};
59+
60+
border-radius: ${theme.shape.borderRadius(2)};
61+
`,
62+
label: css`
63+
display: flex;
64+
align-items: center;
65+
color: inherit;
66+
67+
padding: ${padding};
68+
background: ${backgroundColor};
69+
70+
border: solid 1px ${borderColor};
71+
border-top-left-radius: ${theme.shape.borderRadius(2)};
72+
border-bottom-left-radius: ${theme.shape.borderRadius(2)};
73+
`,
74+
value: css`
75+
color: inherit;
76+
padding: ${padding};
77+
background: ${valueBackgroundColor};
78+
79+
border: solid 1px ${borderColor};
80+
border-left: none;
81+
border-top-right-radius: ${theme.shape.borderRadius(2)};
82+
border-bottom-right-radius: ${theme.shape.borderRadius(2)};
83+
`,
84+
};
85+
};

src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx

+29-28
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { ChangeEvent, useCallback, useState } from 'react';
1+
import React, { ChangeEvent, useState } from 'react';
22

33
import { ServiceLabels } from '@grafana/labels';
44
import {
@@ -22,6 +22,7 @@ import { RenderConditionally } from 'components/RenderConditionally/RenderCondit
2222
import { Text } from 'components/Text/Text';
2323
import { IntegrationTemplate } from 'containers/IntegrationTemplate/IntegrationTemplate';
2424
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
25+
import { splitToGroups } from 'models/label/label.helpers';
2526
import { LabelsErrors } from 'models/label/label.types';
2627
import { ApiSchemas } from 'network/oncall-api/api.types';
2728
import { LabelTemplateOptions } from 'pages/integration/IntegrationCommon.config';
@@ -281,36 +282,34 @@ const CustomLabels = (props: CustomLabelsProps) => {
281282
});
282283
};
283284

284-
const cachedOnLoadKeys = useCallback(() => {
285+
const onLoadKeys = async (search?: string) => {
285286
let result = undefined;
286-
return async (search?: string) => {
287-
if (!result) {
288-
try {
289-
result = await labelsStore.loadKeys();
290-
} catch (error) {
291-
openErrorNotification('There was an error processing your request. Please try again');
292-
}
293-
}
294287

295-
return result.filter((k) => k.name.toLowerCase().includes(search.toLowerCase()));
296-
};
297-
}, []);
288+
try {
289+
result = await labelsStore.loadKeys(search);
290+
} catch (error) {
291+
openErrorNotification('There was an error processing your request. Please try again');
292+
}
293+
294+
const groups = splitToGroups(result);
295+
296+
return groups;
297+
};
298298

299-
const cachedOnLoadValuesForKey = useCallback(() => {
299+
const onLoadValuesForKey = async (key: string, search?: string) => {
300300
let result = undefined;
301-
return async (key: string, search?: string) => {
302-
if (!result) {
303-
try {
304-
const { values } = await labelsStore.loadValuesForKey(key, search);
305-
result = values;
306-
} catch (error) {
307-
openErrorNotification('There was an error processing your request. Please try again');
308-
}
309-
}
310301

311-
return result.filter((k) => k.name.toLowerCase().includes(search.toLowerCase()));
312-
};
313-
}, []);
302+
try {
303+
const { values } = await labelsStore.loadValuesForKey(key, search);
304+
result = values;
305+
} catch (error) {
306+
openErrorNotification('There was an error processing your request. Please try again');
307+
}
308+
309+
const groups = splitToGroups(result);
310+
311+
return groups;
312+
};
314313

315314
return (
316315
<VerticalGroup>
@@ -328,8 +327,8 @@ const CustomLabels = (props: CustomLabelsProps) => {
328327
inputWidth={INPUT_WIDTH}
329328
errors={customLabelsErrors}
330329
value={alertGroupLabels.custom}
331-
onLoadKeys={cachedOnLoadKeys()}
332-
onLoadValuesForKey={cachedOnLoadValuesForKey()}
330+
onLoadKeys={onLoadKeys}
331+
onLoadValuesForKey={onLoadValuesForKey}
333332
onCreateKey={labelsStore.createKey}
334333
onUpdateKey={labelsStore.updateKey}
335334
onCreateValue={labelsStore.createValue}
@@ -377,6 +376,8 @@ const CustomLabels = (props: CustomLabelsProps) => {
377376
custom: value,
378377
});
379378
}}
379+
getIsKeyEditable={(option) => !option.prescribed}
380+
getIsValueEditable={(option) => !option.prescribed}
380381
/>
381382
<Dropdown
382383
overlay={

src/containers/Labels/Labels.tsx

+32-33
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
1+
import React, { forwardRef, useImperativeHandle, useState } from 'react';
22

3-
import { ServiceLabels, ServiceLabelsProps } from '@grafana/labels';
3+
import { ServiceLabelsProps, ServiceLabels } from '@grafana/labels';
44
import { Field, Label } from '@grafana/ui';
55
import { isEmpty } from 'lodash-es';
66
import { observer } from 'mobx-react';
77

8+
import { splitToGroups } from 'models/label/label.helpers';
89
import { LabelKeyValue } from 'models/label/label.types';
910
import { useStore } from 'state/useStore';
1011
import { openErrorNotification } from 'utils/utils';
@@ -46,20 +47,32 @@ const _Labels = observer(
4647
[value]
4748
);
4849

49-
const cachedOnLoadKeys = useCallback(() => {
50+
const onLoadKeys = async (search?: string) => {
5051
let result = undefined;
51-
return async (search?: string) => {
52-
if (!result) {
53-
try {
54-
result = await labelsStore.loadKeys();
55-
} catch (error) {
56-
openErrorNotification('There was an error processing your request. Please try again');
57-
}
58-
}
59-
60-
return result.filter((k) => k.name.toLowerCase().includes(search.toLowerCase()));
61-
};
62-
}, []);
52+
try {
53+
result = await labelsStore.loadKeys(search);
54+
} catch (error) {
55+
openErrorNotification('There was an error processing your request. Please try again');
56+
}
57+
58+
const groups = splitToGroups(result);
59+
60+
return groups;
61+
};
62+
63+
const onLoadValuesForKey = async (key: string, search?: string) => {
64+
let result = undefined;
65+
try {
66+
const { values } = await labelsStore.loadValuesForKey(key, search);
67+
result = values;
68+
} catch (error) {
69+
openErrorNotification('There was an error processing your request. Please try again');
70+
}
71+
72+
const groups = splitToGroups(result);
73+
74+
return groups;
75+
};
6376

6477
const isValid = () => {
6578
return (
@@ -86,30 +99,14 @@ const _Labels = observer(
8699
);
87100
};
88101

89-
const cachedOnLoadValuesForKey = useCallback(() => {
90-
let result = undefined;
91-
return async (key: string, search?: string) => {
92-
if (!result) {
93-
try {
94-
const { values } = await labelsStore.loadValuesForKey(key, search);
95-
result = values;
96-
} catch (error) {
97-
openErrorNotification('There was an error processing your request. Please try again');
98-
}
99-
}
100-
101-
return result.filter((k) => k.name.toLowerCase().includes(search.toLowerCase()));
102-
};
103-
}, []);
104-
105102
return (
106103
<div>
107104
<Field label={<Label description={<div className="u-padding-vertical-xs">{description}</div>}>Labels</Label>}>
108105
<ServiceLabels
109106
loadById
110107
value={value}
111-
onLoadKeys={cachedOnLoadKeys()}
112-
onLoadValuesForKey={cachedOnLoadValuesForKey()}
108+
onLoadKeys={onLoadKeys}
109+
onLoadValuesForKey={onLoadValuesForKey}
113110
onCreateKey={labelsStore.createKey}
114111
onUpdateKey={labelsStore.updateKey}
115112
onCreateValue={labelsStore.createValue}
@@ -118,6 +115,8 @@ const _Labels = observer(
118115
onUpdateError={onUpdateError}
119116
errors={isValid() ? {} : { ...propsErrors }}
120117
onDataUpdate={onChange}
118+
getIsKeyEditable={(option) => !option.prescribed}
119+
getIsValueEditable={(option) => !option.prescribed}
121120
/>
122121
</Field>
123122
</div>

src/models/label/label.helpers.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ApiSchemas } from 'network/oncall-api/api.types';
2+
3+
export const splitToGroups = (labels: Array<ApiSchemas['LabelKey']> | Array<ApiSchemas['LabelValue']>) => {
4+
return labels.reduce(
5+
(memo, option) => {
6+
memo.find(({ name }) => name === (option.prescribed ? 'System' : 'User added')).options.push(option);
7+
8+
return memo;
9+
},
10+
[
11+
{ name: 'System', id: 'system', expanded: true, options: [] },
12+
{ name: 'User added', id: 'user_added', expanded: true, options: [] },
13+
]
14+
);
15+
};

src/models/label/label.ts

+5-20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { action, observable, makeObservable, runInAction } from 'mobx';
1+
import { action, makeObservable } from 'mobx';
22

33
import { BaseStore } from 'models/base_store';
44
import { makeRequest } from 'network/network';
@@ -8,12 +8,6 @@ import { RootStore } from 'state/rootStore';
88
import { WithGlobalNotification } from 'utils/decorators';
99

1010
export class LabelStore extends BaseStore {
11-
@observable.shallow
12-
public keys: Array<ApiSchemas['LabelKey']> = [];
13-
14-
@observable.shallow
15-
public values: { [key: string]: Array<ApiSchemas['LabelValue']> } = {};
16-
1711
constructor(rootStore: RootStore) {
1812
super(rootStore);
1913

@@ -23,14 +17,12 @@ export class LabelStore extends BaseStore {
2317
}
2418

2519
@action.bound
26-
public async loadKeys() {
20+
public async loadKeys(search = '') {
2721
const { data } = await onCallApi.GET('/labels/keys/', undefined);
2822

29-
runInAction(() => {
30-
this.keys = data;
31-
});
23+
const filtered = data.filter((k) => k.name.toLowerCase().includes(search.toLowerCase()));
3224

33-
return data;
25+
return filtered;
3426
}
3527

3628
@action.bound
@@ -43,14 +35,7 @@ export class LabelStore extends BaseStore {
4335
params: { search },
4436
});
4537

46-
const filteredValues = result.values.filter((v) => v.name.toLowerCase().includes(search.toLowerCase())); // TODO remove after backend search implementation
47-
48-
runInAction(() => {
49-
this.values = {
50-
...this.values,
51-
[key]: filteredValues,
52-
};
53-
});
38+
const filteredValues = result.values.filter((v) => v.name.toLowerCase().includes(search.toLowerCase()));
5439

5540
return { ...result, values: filteredValues };
5641
}

src/network/oncall-api/autogenerated-api.types.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@ export interface components {
571571
LabelKey: {
572572
id: string;
573573
name: string;
574+
prescribed?: boolean;
574575
};
575576
LabelKeyValues: {
576577
key: components['schemas']['LabelKey'];
@@ -582,6 +583,7 @@ export interface components {
582583
LabelValue: {
583584
id: string;
584585
name: string;
586+
prescribed?: boolean;
585587
};
586588
PaginatedAlertGroupListList: {
587589
next?: string | null;

0 commit comments

Comments
 (0)