Skip to content

Commit da4826b

Browse files
feat(resultOptions): add group options UI (#6099)
* feat: add group option with SelectGroup cf component * chore: add group by selector when group by is selected * chore: add MultiSelectDropdown to GroupBy * style: add padding to group row * chore: add dynamic fields to group list * chore: rename flux script func name to `IMPORT_SAMPLE_DATA_SET` * feat: add `GroupKeysContext and Provider * chore: lint * chore: change `GroupOption` to `GroupType` * chore: rename `selectedGroupOption` to `selectedGroupType` * chore: reset the selected group keys when chose group types other than GroupBy * chore: more lint * feat: add `resetGroupKeys` when no selected bucket or measurement * chore: keep "_field" in the group key list * chore: export GroupType in persistance and use it in GroupBy * chore: move selected group type and group keys to persistance * chore: update flux text to get tag keys only * chore: reset selected group keys when user change the selected measurement * refactor: rename to `groupTypesButtons` * refactor: change back to `SAMPLE_DATA_SET` * chore: add DEFAULT_GROUP_OPTIONS and DEFAULT_COLUMNS * chore: deep clone DEFAULT values * style: make SelectGroup stretch to fit * chore: add comment on style why an override is necessary * chore: remove feature flag newQueryBuilder * chore: lint * refactor: use Object.entry to loop through key value pairs * chore: add resetGroupKeys to dependency list * chore: update default columns and useEffect() logic * chore: add eventing for group options
1 parent 1062bbf commit da4826b

File tree

5 files changed

+334
-25
lines changed

5 files changed

+334
-25
lines changed
Lines changed: 133 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,146 @@
1-
import React, {FC, useState} from 'react'
1+
import React, {FC, useContext, useMemo, useCallback, useEffect} from 'react'
22

33
// Components
4-
import {ToggleWithLabelTooltip} from 'src/dataExplorer/components/ToggleWithLabelTooltip'
4+
import SelectorTitle from 'src/dataExplorer/components/SelectorTitle'
5+
import {
6+
ButtonShape,
7+
SelectGroup,
8+
MultiSelectDropdown,
9+
} from '@influxdata/clockface'
10+
11+
// Contexts
12+
import {
13+
DEFAULT_GROUP_OPTIONS,
14+
GroupType,
15+
GroupOptions,
16+
PersistanceContext,
17+
} from 'src/dataExplorer/context/persistance'
18+
import {GroupKeysContext} from 'src/dataExplorer/context/groupKeys'
19+
20+
// Utilies
21+
import {toComponentStatus} from 'src/shared/utils/toComponentStatus'
22+
import {event} from 'src/cloud/utils/reporting'
523

624
// Styles
725
import './Sidebar.scss'
826

927
const GROUP_TOOLTIP = `test`
28+
const DEFAULT_COLUMNS: string[] = ['_measurement', '_field'] // only use this when the GroupType.GroupBy is selected
1029

1130
const GroupBy: FC = () => {
12-
const [group, setGroup] = useState(false)
13-
14-
return (
15-
<ToggleWithLabelTooltip
16-
label="Group"
17-
active={group}
18-
onChange={() => setGroup(current => !current)}
19-
tooltipContents={GROUP_TOOLTIP}
20-
/>
31+
const {groupKeys, loading, getGroupKeys, resetGroupKeys} =
32+
useContext(GroupKeysContext)
33+
const {selection, setSelection} = useContext(PersistanceContext)
34+
const {type: selectedGroupType, columns: selectedGroupKeys}: GroupOptions =
35+
selection.resultOptions.group
36+
37+
useEffect(
38+
() => {
39+
if (!selection.bucket || !selection.measurement) {
40+
// empty the group keys list
41+
resetGroupKeys()
42+
} else {
43+
// update the group keys list whenever the selected measurement changes
44+
getGroupKeys(selection.bucket, selection.measurement)
45+
}
46+
47+
setSelection({
48+
resultOptions: {
49+
group: JSON.parse(JSON.stringify(DEFAULT_GROUP_OPTIONS)),
50+
},
51+
})
52+
},
53+
// not including getGroupKeys() and setSelection() to avoid infinite loop
54+
[selection.bucket, selection.measurement, resetGroupKeys]
55+
)
56+
57+
const handleSelectGroupType = useCallback(
58+
(type: GroupType) => {
59+
event('Select a group type', {groupType: type})
60+
const columns: string[] =
61+
type === GroupType.GroupBy
62+
? JSON.parse(JSON.stringify(DEFAULT_COLUMNS))
63+
: []
64+
setSelection({
65+
resultOptions: {
66+
group: {type, columns},
67+
},
68+
})
69+
},
70+
[setSelection]
2171
)
72+
73+
const groupTypesButtons = useMemo(
74+
() => (
75+
<div className="result-options--item--row">
76+
<SelectGroup shape={ButtonShape.StretchToFit}>
77+
{Object.entries(GroupType).map(([key, type]) => (
78+
<SelectGroup.Option
79+
key={key}
80+
id={key}
81+
active={selectedGroupType === type}
82+
value={type}
83+
onClick={handleSelectGroupType}
84+
>
85+
{type}
86+
</SelectGroup.Option>
87+
))}
88+
</SelectGroup>
89+
</div>
90+
),
91+
[selectedGroupType, handleSelectGroupType]
92+
)
93+
94+
const handleSelectGroupKey = useCallback(
95+
(option: string): void => {
96+
let selected: string[] = []
97+
if (selectedGroupKeys.includes(option)) {
98+
selected = selectedGroupKeys.filter(item => item !== option)
99+
event('Deselect a group key', {selectedGroupKeyLength: selected.length})
100+
} else {
101+
selected = [...selectedGroupKeys, option]
102+
event('Select a group key', {selectedGroupKeyLength: selected.length})
103+
}
104+
setSelection({
105+
resultOptions: {group: {type: selectedGroupType, columns: selected}},
106+
})
107+
},
108+
[selectedGroupType, selectedGroupKeys, setSelection]
109+
)
110+
111+
const groupBySelector = useMemo(() => {
112+
return selectedGroupType === GroupType.GroupBy ? (
113+
<div className="result-options--item--row">
114+
<MultiSelectDropdown
115+
options={groupKeys}
116+
selectedOptions={
117+
!selection.bucket || !selection.measurement ? [] : selectedGroupKeys
118+
}
119+
onSelect={handleSelectGroupKey}
120+
emptyText="Select group column values"
121+
buttonStatus={toComponentStatus(loading)}
122+
/>
123+
</div>
124+
) : null
125+
}, [
126+
selectedGroupType,
127+
groupKeys,
128+
loading,
129+
selectedGroupKeys,
130+
selection.bucket,
131+
selection.measurement,
132+
handleSelectGroupKey,
133+
])
134+
135+
return useMemo(() => {
136+
return (
137+
<div className="result-options--item">
138+
<SelectorTitle label="Group" tooltipContents={GROUP_TOOLTIP} />
139+
{groupTypesButtons}
140+
{groupBySelector}
141+
</div>
142+
)
143+
}, [groupTypesButtons, groupBySelector])
22144
}
23145

24146
export {GroupBy}

src/dataExplorer/components/ResultOptions.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,42 @@
11
import React, {FC} from 'react'
2+
import {useSelector} from 'react-redux'
23

34
// Components
45
import {Accordion} from '@influxdata/clockface'
56
import {FieldsAsColumns} from 'src/dataExplorer/components/FieldsAsColumns'
67
import {GroupBy} from 'src/dataExplorer/components/GroupBy'
78
import {Aggregate} from 'src/dataExplorer/components/Aggregate'
89

10+
// Context
11+
import {GroupKeysProvider} from 'src/dataExplorer/context/groupKeys'
12+
13+
// Utils
14+
import {getOrg} from 'src/organizations/selectors'
15+
16+
// Types
17+
import {QueryScope} from 'src/shared/contexts/query'
18+
919
// Style
1020
import './Sidebar.scss'
1121

1222
const ResultOptions: FC = () => {
23+
const org = useSelector(getOrg)
24+
const scope = {
25+
org: org.id,
26+
region: window.location.origin,
27+
} as QueryScope
28+
1329
return (
14-
<Accordion className="result-options" expanded={true}>
15-
<Accordion.AccordionHeader className="result-options--header">
16-
Result Options
17-
</Accordion.AccordionHeader>
18-
<FieldsAsColumns />
19-
<GroupBy />
20-
<Aggregate />
21-
</Accordion>
30+
<GroupKeysProvider scope={scope}>
31+
<Accordion className="result-options" expanded={true}>
32+
<Accordion.AccordionHeader className="result-options--header">
33+
Result Options
34+
</Accordion.AccordionHeader>
35+
<FieldsAsColumns />
36+
<GroupBy />
37+
<Aggregate />
38+
</Accordion>
39+
</GroupKeysProvider>
2240
)
2341
}
2442

src/dataExplorer/components/Sidebar.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@
5151
padding: $cf-space-2xs 0;
5252
}
5353

54+
.cf-accordion--body-container--expanded {
55+
opacity: 1 !important; // to avoid dropdown background become transparent https://github.com/influxdata/clockface/issues/877
56+
}
57+
58+
.result-options--item {
59+
padding: $cf-space-2xs 0;
60+
61+
.result-options--item--row {
62+
padding: $cf-space-2xs 0;
63+
}
64+
}
65+
5466
.toggle-with-label-tooltip {
5567
padding: $cf-space-2xs 0;
5668
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Libraries
2+
import React, {
3+
createContext,
4+
FC,
5+
useCallback,
6+
useContext,
7+
useMemo,
8+
useState,
9+
} from 'react'
10+
11+
// Contexts
12+
import {QueryContext, QueryScope} from 'src/shared/contexts/query'
13+
14+
// Constants
15+
import {
16+
CACHING_REQUIRED_END_DATE,
17+
CACHING_REQUIRED_START_DATE,
18+
} from 'src/utils/datetime/constants'
19+
import {DEFAULT_LIMIT} from 'src/shared/constants/queryBuilder'
20+
21+
// Types
22+
import {Bucket, RemoteDataState} from 'src/types'
23+
24+
// Utils
25+
import {
26+
IMPORT_REGEXP,
27+
IMPORT_STRINGS,
28+
IMPORT_INFLUX_SCHEMA,
29+
SAMPLE_DATA_SET,
30+
FROM_BUCKET,
31+
SEARCH_STRING,
32+
} from 'src/dataExplorer/shared/utils'
33+
34+
interface GroupKeysContextType {
35+
groupKeys: Array<string>
36+
loading: RemoteDataState
37+
getGroupKeys: (
38+
bucket: Bucket,
39+
measurement: string,
40+
searchTerm?: string
41+
) => void
42+
resetGroupKeys: () => void
43+
}
44+
45+
const DEFAULT_CONTEXT: GroupKeysContextType = {
46+
groupKeys: [],
47+
loading: RemoteDataState.NotStarted,
48+
getGroupKeys: (_b: Bucket, _m: string, _s: string) => {},
49+
resetGroupKeys: () => {},
50+
}
51+
52+
export const GroupKeysContext =
53+
createContext<GroupKeysContextType>(DEFAULT_CONTEXT)
54+
55+
const INITIAL_GROUP_KEYS = [] as Array<string>
56+
57+
interface Prop {
58+
scope: QueryScope
59+
}
60+
61+
export const GroupKeysProvider: FC<Prop> = ({children, scope}) => {
62+
// Context
63+
const {query: queryAPI} = useContext(QueryContext)
64+
65+
// States
66+
const [groupKeys, setGroupKeys] = useState<Array<string>>(INITIAL_GROUP_KEYS)
67+
const [loading, setLoading] = useState<RemoteDataState>(
68+
RemoteDataState.NotStarted
69+
)
70+
71+
const getGroupKeys = useCallback(
72+
async (bucket: Bucket, measurement: string, searchTerm?: string) => {
73+
if (!bucket || !measurement) {
74+
return
75+
}
76+
77+
setLoading(RemoteDataState.Loading)
78+
79+
// Simplified version of query from this file:
80+
// src/flows/pipes/QueryBuilder/context.tsx
81+
// Note that sample buckets are not in storage level.
82+
// They are fetched dynamically from csv.
83+
// Here is the source code for handling sample data:
84+
// https://github.com/influxdata/flux/blob/master/stdlib/influxdata/influxdb/sample/sample.flux
85+
// That is why _source and query script for sample data is different
86+
let _source = IMPORT_REGEXP
87+
if (bucket.type === 'sample') {
88+
_source += SAMPLE_DATA_SET(bucket.id)
89+
} else {
90+
_source += FROM_BUCKET(bucket.name)
91+
}
92+
93+
let queryText = `${_source}
94+
|> range(start: -100y, stop: now())
95+
|> filter(fn: (r) => true)
96+
|> keys()
97+
|> keep(columns: ["_value"])
98+
|> distinct()
99+
|> filter(fn: (r) => r._value != "_time" and r._value != "_value")
100+
${searchTerm ? SEARCH_STRING(searchTerm) : ''}
101+
|> sort()
102+
|> limit(n: ${DEFAULT_LIMIT})
103+
`
104+
105+
if (bucket.type !== 'sample') {
106+
_source = `${IMPORT_REGEXP}${IMPORT_INFLUX_SCHEMA}${IMPORT_STRINGS}`
107+
queryText = `${_source}
108+
schema.measurementTagKeys(
109+
bucket: "${bucket.name}",
110+
measurement: "${measurement}",
111+
start: ${CACHING_REQUIRED_START_DATE},
112+
stop: ${CACHING_REQUIRED_END_DATE},
113+
)
114+
${searchTerm ? SEARCH_STRING(searchTerm) : ''}
115+
|> map(fn: (r) => ({r with lowercase: strings.toLower(v: r._value)}))
116+
|> sort(columns: ["lowercase"])
117+
|> limit(n: ${DEFAULT_LIMIT})
118+
`
119+
}
120+
121+
try {
122+
const resp = await queryAPI(queryText, scope)
123+
const values = (Object.values(resp.parsed.table.columns).filter(
124+
c => c.name === '_value' && c.type === 'string'
125+
)[0]?.data ?? []) as string[]
126+
127+
setGroupKeys(values)
128+
setLoading(RemoteDataState.Done)
129+
} catch (err) {
130+
console.error(
131+
`Failed to get group keys for bucket ${bucket.name} and measurement ${measurement}\n`,
132+
err.message
133+
)
134+
setLoading(RemoteDataState.Error)
135+
}
136+
},
137+
[queryAPI, scope]
138+
)
139+
140+
const resetGroupKeys = useCallback(() => {
141+
setGroupKeys(JSON.parse(JSON.stringify(INITIAL_GROUP_KEYS)))
142+
setLoading(RemoteDataState.NotStarted)
143+
}, [])
144+
145+
return useMemo(
146+
() => (
147+
<GroupKeysContext.Provider
148+
value={{groupKeys, loading, getGroupKeys, resetGroupKeys}}
149+
>
150+
{children}
151+
</GroupKeysContext.Provider>
152+
),
153+
[groupKeys, loading, children, getGroupKeys, resetGroupKeys]
154+
)
155+
}

0 commit comments

Comments
 (0)