1import {
2  ByNamesMatcherMode,
3  DataFrame,
4  DynamicConfigValue,
5  FieldConfigSource,
6  FieldMatcherID,
7  FieldType,
8  getFieldDisplayName,
9  isSystemOverrideWithRef,
10  SystemConfigOverrideRule,
11} from '@grafana/data';
12import { GraphNGLegendEvent, SeriesVisibilityChangeMode } from '@grafana/ui';
13
14const displayOverrideRef = 'hideSeriesFrom';
15const isHideSeriesOverride = isSystemOverrideWithRef(displayOverrideRef);
16
17export const hideSeriesConfigFactory = (
18  event: GraphNGLegendEvent,
19  fieldConfig: FieldConfigSource<any>,
20  data: DataFrame[]
21): FieldConfigSource<any> => {
22  const { fieldIndex, mode } = event;
23  const { overrides } = fieldConfig;
24
25  const frame = data[fieldIndex.frameIndex];
26
27  if (!frame) {
28    return fieldConfig;
29  }
30
31  const field = frame.fields[fieldIndex.fieldIndex];
32
33  if (!field) {
34    return fieldConfig;
35  }
36
37  const displayName = getFieldDisplayName(field, frame, data);
38  const currentIndex = overrides.findIndex(isHideSeriesOverride);
39
40  if (currentIndex < 0) {
41    if (mode === SeriesVisibilityChangeMode.ToggleSelection) {
42      const override = createOverride([displayName]);
43
44      return {
45        ...fieldConfig,
46        overrides: [override, ...fieldConfig.overrides],
47      };
48    }
49
50    const displayNames = getDisplayNames(data, displayName);
51    const override = createOverride(displayNames);
52
53    return {
54      ...fieldConfig,
55      overrides: [override, ...fieldConfig.overrides],
56    };
57  }
58
59  const overridesCopy = Array.from(overrides);
60  const [current] = overridesCopy.splice(currentIndex, 1) as SystemConfigOverrideRule[];
61
62  if (mode === SeriesVisibilityChangeMode.ToggleSelection) {
63    const existing = getExistingDisplayNames(current);
64
65    if (existing[0] === displayName && existing.length === 1) {
66      return {
67        ...fieldConfig,
68        overrides: overridesCopy,
69      };
70    }
71
72    const override = createOverride([displayName]);
73
74    return {
75      ...fieldConfig,
76      overrides: [override, ...overridesCopy],
77    };
78  }
79
80  const override = createExtendedOverride(current, displayName);
81
82  if (allFieldsAreExcluded(override, data)) {
83    return {
84      ...fieldConfig,
85      overrides: overridesCopy,
86    };
87  }
88
89  return {
90    ...fieldConfig,
91    overrides: [override, ...overridesCopy],
92  };
93};
94
95const createExtendedOverride = (current: SystemConfigOverrideRule, displayName: string): SystemConfigOverrideRule => {
96  const property = current.properties.find((p) => p.id === 'custom.hideFrom');
97  const existing = getExistingDisplayNames(current);
98  const index = existing.findIndex((name) => name === displayName);
99
100  if (index < 0) {
101    existing.push(displayName);
102  } else {
103    existing.splice(index, 1);
104  }
105
106  return createOverride(existing, property);
107};
108
109const getExistingDisplayNames = (rule: SystemConfigOverrideRule): string[] => {
110  const names = rule.matcher.options?.names;
111  if (!Array.isArray(names)) {
112    return [];
113  }
114  return names;
115};
116
117const createOverride = (names: string[], property?: DynamicConfigValue): SystemConfigOverrideRule => {
118  property = property ?? {
119    id: 'custom.hideFrom',
120    value: {
121      graph: true,
122      legend: false,
123      tooltip: false,
124    },
125  };
126
127  return {
128    __systemRef: displayOverrideRef,
129    matcher: {
130      id: FieldMatcherID.byNames,
131      options: {
132        mode: ByNamesMatcherMode.exclude,
133        names: names,
134        prefix: 'All except:',
135        readOnly: true,
136      },
137    },
138    properties: [
139      {
140        ...property,
141        value: {
142          graph: true,
143          legend: false,
144          tooltip: false,
145        },
146      },
147    ],
148  };
149};
150
151const allFieldsAreExcluded = (override: SystemConfigOverrideRule, data: DataFrame[]): boolean => {
152  return getExistingDisplayNames(override).length === getDisplayNames(data).length;
153};
154
155const getDisplayNames = (data: DataFrame[], excludeName?: string): string[] => {
156  const unique = new Set<string>();
157
158  for (const frame of data) {
159    for (const field of frame.fields) {
160      if (field.type !== FieldType.number) {
161        continue;
162      }
163
164      const name = getFieldDisplayName(field, frame, data);
165
166      if (name === excludeName) {
167        continue;
168      }
169
170      unique.add(name);
171    }
172  }
173
174  return Array.from(unique);
175};
176