fix(variable_hydration): limiting variable hydration to changing variables (#18071)

pull/18177/head
Ariel Salem 2020-05-20 09:22:00 -07:00 committed by GitHub
parent 0360d0d5c0
commit 0c44328419
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 756 additions and 27 deletions

View File

@ -10,6 +10,7 @@
1. [18066](https://github.com/influxdata/influxdb/pull/18066): Fixed bug that wasn't persisting timeFormat for Graph + Single Stat selections
1. [17959](https://github.com/influxdata/influxdb/pull/17959): Authorizer now exposes full permission set
1. [18071](https://github.com/influxdata/influxdb/pull/18071): Fixed issue that was causing variable selections to hydrate all variable values
### UI Improvements

View File

@ -11,7 +11,7 @@ const GraphTips: FC = () => (
color={ComponentColor.Primary}
testID="graphtips-question-mark"
tooltipContents={
<span>
<>
<h1>Graph Tips:</h1>
<p>
<code>Click + Drag</code> Zoom in (X or Y)
@ -22,11 +22,11 @@ const GraphTips: FC = () => (
</p>
<h1>Static Legend Tips:</h1>
<p>
<code>Click</code>Focus on single Series
<code>Click</code> Focus on single Series
<br />
<code>Shift + Click</code> Show/Hide single Series
</p>
</span>
</>
}
/>
</>

View File

@ -147,6 +147,34 @@ export const hydrateVariables = (skipCache?: boolean) => async (
await hydration.promise
}
export const hydrateChangedVariable = (variableID: string) => async (
dispatch: Dispatch<Action>,
getState: GetState
) => {
const state = getState()
const org = getOrg(state)
const variable = getVariableFromState(state, variableID)
const hydration = hydrateVars([variable], getAllVariablesFromState(state), {
orgID: org.id,
url: state.links.query.self,
skipCache: true,
})
hydration.on('status', (variable, status) => {
if (status === RemoteDataState.Loading) {
dispatch(setVariable(variable.id, status))
return
}
if (status === RemoteDataState.Done) {
const _variable = normalize<Variable, VariableEntities, string>(
variable,
variableSchema
)
dispatch(setVariable(variable.id, RemoteDataState.Done, _variable))
}
})
await hydration.promise
}
export const getVariable = (id: string) => async (
dispatch: Dispatch<Action>
) => {
@ -397,7 +425,6 @@ export const selectValue = (variableID: string, selected: string) => async (
const state = getState()
const contextID = currentContext(state)
const variable = getVariableFromState(state, variableID)
// Validate that we can make this selection
const vals = normalizeValues(variable)
if (!vals.includes(selected)) {
@ -405,7 +432,7 @@ export const selectValue = (variableID: string, selected: string) => async (
}
await dispatch(selectValueInState(contextID, variableID, selected))
dispatch(hydrateVariables(true))
// only hydrate the changedVariable
dispatch(hydrateChangedVariable(variableID))
dispatch(updateQueryVars({[variable.name]: selected}))
}

View File

@ -97,9 +97,16 @@ class VariableDropdown extends PureComponent<Props> {
}
private handleSelect = (selectedValue: string) => {
const {variableID, onSelectValue, onSelect} = this.props
const {
variableID,
onSelectValue,
onSelect,
selectedValue: prevSelectedValue,
} = this.props
onSelectValue(variableID, selectedValue)
if (prevSelectedValue !== selectedValue) {
onSelectValue(variableID, selectedValue)
}
if (onSelect) {
onSelect()

View File

@ -1,6 +1,7 @@
// Types
import {Variable, RemoteDataState} from 'src/types'
import {VariableAssignment} from 'src/types/ast'
import {VariableNode} from 'src/variables/utils/hydrateVars'
export const defaultVariableAssignments: VariableAssignment[] = [
{
@ -78,3 +79,652 @@ export const createMapVariable = (
},
status: RemoteDataState.Done,
})
export const defaultSubGraph: VariableNode[] = [
{
variable: {
id: '05b740973c68e000',
orgID: '05b740945a91b000',
name: 'static',
description: '',
selected: ['defbuck'],
arguments: {
type: 'constant',
values: ['beans', 'defbuck'],
},
createdAt: '2020-05-19T06:00:00.113169-07:00',
updatedAt: '2020-05-19T06:00:00.113169-07:00',
labels: [],
links: {
self: '/api/v2/variables/05b740973c68e000',
labels: '/api/v2/variables/05b740973c68e000/labels',
org: '/api/v2/orgs/05b740945a91b000',
},
status: RemoteDataState.Done,
},
values: null,
parents: [
{
variable: {
id: '05b740974228e000',
orgID: '05b740945a91b000',
name: 'dependent',
description: '',
selected: [],
arguments: {
type: 'query',
values: {
query:
'from(bucket: v.static)\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "test")\n |> keep(columns: ["container_name"])\n |> rename(columns: {"container_name": "_value"})\n |> last()\n |> group()',
language: 'flux',
results: [],
},
},
createdAt: '2020-05-19T06:00:00.136597-07:00',
updatedAt: '2020-05-19T06:00:00.136597-07:00',
labels: [],
links: {
self: '/api/v2/variables/05b740974228e000',
labels: '/api/v2/variables/05b740974228e000/labels',
org: '/api/v2/orgs/05b740945a91b000',
},
status: RemoteDataState.Loading,
},
cancel: () => {},
values: null,
parents: [],
children: [
{
variable: {
orgID: '',
id: 'timeRangeStart',
name: 'timeRangeStart',
arguments: {
type: 'system',
values: [
[
{
magnitude: 1,
unit: 'h',
},
],
],
},
status: RemoteDataState.Done,
labels: [],
selected: [],
},
values: null,
parents: [null],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
{
variable: {
orgID: '',
id: 'timeRangeStop',
name: 'timeRangeStop',
arguments: {
type: 'system',
values: ['now()'],
},
status: RemoteDataState.Done,
labels: [],
selected: [],
},
values: null,
parents: [null],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
],
status: RemoteDataState.NotStarted,
},
],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
]
export const defaultGraph: VariableNode[] = [
{
variable: {
id: '05b740973c68e000',
orgID: '05b740945a91b000',
name: 'static',
description: '',
selected: ['defbuck'],
arguments: {
type: 'constant',
values: ['beans', 'defbuck'],
},
createdAt: '2020-05-19T06:00:00.113169-07:00',
updatedAt: '2020-05-19T06:00:00.113169-07:00',
labels: [],
links: {
self: '/api/v2/variables/05b740973c68e000',
labels: '/api/v2/variables/05b740973c68e000/labels',
org: '/api/v2/orgs/05b740945a91b000',
},
status: RemoteDataState.Done,
},
values: null,
parents: [
{
cancel: () => {},
variable: {
id: '05b740974228e000',
orgID: '05b740945a91b000',
name: 'dependent',
description: '',
selected: [],
arguments: {
type: 'query',
values: {
query:
'from(bucket: v.static)\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "test")\n |> keep(columns: ["container_name"])\n |> rename(columns: {"container_name": "_value"})\n |> last()\n |> group()',
language: 'flux',
results: [],
},
},
createdAt: '2020-05-19T06:00:00.136597-07:00',
updatedAt: '2020-05-19T06:00:00.136597-07:00',
labels: [],
links: {
self: '/api/v2/variables/05b740974228e000',
labels: '/api/v2/variables/05b740974228e000/labels',
org: '/api/v2/orgs/05b740945a91b000',
},
status: RemoteDataState.Loading,
},
values: null,
parents: [],
children: [
{
variable: {
orgID: '',
id: 'timeRangeStart',
name: 'timeRangeStart',
arguments: {
type: 'system',
values: [
[
{
magnitude: 1,
unit: 'h',
},
],
],
},
status: RemoteDataState.Done,
labels: [],
selected: [],
},
values: null,
parents: [null],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
{
variable: {
orgID: '',
id: 'timeRangeStop',
name: 'timeRangeStop',
arguments: {
type: 'system',
values: ['now()'],
},
status: RemoteDataState.Done,
labels: [],
selected: [],
},
values: null,
parents: [null],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
],
status: RemoteDataState.NotStarted,
},
],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
{
variable: {
id: '05b740974228e000',
orgID: '05b740945a91b000',
name: 'dependent',
description: '',
selected: [],
arguments: {
type: 'query',
values: {
query:
'from(bucket: v.static)\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "test")\n |> keep(columns: ["container_name"])\n |> rename(columns: {"container_name": "_value"})\n |> last()\n |> group()',
language: 'flux',
results: [],
},
},
createdAt: '2020-05-19T06:00:00.136597-07:00',
updatedAt: '2020-05-19T06:00:00.136597-07:00',
labels: [],
links: {
self: '/api/v2/variables/05b740974228e000',
labels: '/api/v2/variables/05b740974228e000/labels',
org: '/api/v2/orgs/05b740945a91b000',
},
status: RemoteDataState.Loading,
},
values: null,
parents: [],
children: [
{
variable: {
id: '05b740973c68e000',
orgID: '05b740945a91b000',
name: 'static',
description: '',
selected: ['defbuck'],
arguments: {
type: 'constant',
values: ['beans', 'defbuck'],
},
createdAt: '2020-05-19T06:00:00.113169-07:00',
updatedAt: '2020-05-19T06:00:00.113169-07:00',
labels: [],
links: {
self: '/api/v2/variables/05b740973c68e000',
labels: '/api/v2/variables/05b740973c68e000/labels',
org: '/api/v2/orgs/05b740945a91b000',
},
status: RemoteDataState.Done,
},
values: null,
parents: [null],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
{
variable: {
orgID: '',
id: 'timeRangeStart',
name: 'timeRangeStart',
arguments: {
type: 'system',
values: [
[
{
magnitude: 1,
unit: 'h',
},
],
],
},
status: RemoteDataState.Done,
labels: [],
selected: [],
},
values: null,
parents: [null],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
{
variable: {
orgID: '',
id: 'timeRangeStop',
name: 'timeRangeStop',
arguments: {
type: 'system',
values: ['now()'],
},
status: RemoteDataState.Done,
labels: [],
selected: [],
},
values: null,
parents: [null],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
],
status: RemoteDataState.NotStarted,
cancel: () => {},
},
{
variable: {
orgID: '',
id: 'timeRangeStart',
name: 'timeRangeStart',
arguments: {
type: 'system',
values: [
[
{
magnitude: 1,
unit: 'h',
},
],
],
},
status: RemoteDataState.Done,
labels: [],
selected: [],
},
values: null,
parents: [
{
variable: {
id: '05b740974228e000',
orgID: '05b740945a91b000',
name: 'dependent',
description: '',
selected: [],
arguments: {
type: 'query',
values: {
query:
'from(bucket: v.static)\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "test")\n |> keep(columns: ["container_name"])\n |> rename(columns: {"container_name": "_value"})\n |> last()\n |> group()',
language: 'flux',
results: [],
},
},
createdAt: '2020-05-19T06:00:00.136597-07:00',
updatedAt: '2020-05-19T06:00:00.136597-07:00',
labels: [],
links: {
self: '/api/v2/variables/05b740974228e000',
labels: '/api/v2/variables/05b740974228e000/labels',
org: '/api/v2/orgs/05b740945a91b000',
},
status: RemoteDataState.Loading,
},
values: null,
parents: [],
children: [
{
variable: {
id: '05b740973c68e000',
orgID: '05b740945a91b000',
name: 'static',
description: '',
selected: ['defbuck'],
arguments: {
type: 'constant',
values: ['beans', 'defbuck'],
},
createdAt: '2020-05-19T06:00:00.113169-07:00',
updatedAt: '2020-05-19T06:00:00.113169-07:00',
labels: [],
links: {
self: '/api/v2/variables/05b740973c68e000',
labels: '/api/v2/variables/05b740973c68e000/labels',
org: '/api/v2/orgs/05b740945a91b000',
},
status: RemoteDataState.Done,
},
values: null,
parents: [null],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
{
variable: {
orgID: '',
id: 'timeRangeStop',
name: 'timeRangeStop',
arguments: {
type: 'system',
values: ['now()'],
},
status: RemoteDataState.Done,
labels: [],
selected: [],
},
values: null,
parents: [null],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
],
status: RemoteDataState.NotStarted,
cancel: () => {},
},
],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
{
variable: {
orgID: '',
id: 'timeRangeStop',
name: 'timeRangeStop',
arguments: {
type: 'system',
values: ['now()'],
},
status: RemoteDataState.Done,
labels: [],
selected: [],
},
values: null,
parents: [
{
variable: {
id: '05b740974228e000',
orgID: '05b740945a91b000',
name: 'dependent',
description: '',
selected: [],
arguments: {
type: 'query',
values: {
query:
'from(bucket: v.static)\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "test")\n |> keep(columns: ["container_name"])\n |> rename(columns: {"container_name": "_value"})\n |> last()\n |> group()',
language: 'flux',
results: [],
},
},
createdAt: '2020-05-19T06:00:00.136597-07:00',
updatedAt: '2020-05-19T06:00:00.136597-07:00',
labels: [],
links: {
self: '/api/v2/variables/05b740974228e000',
labels: '/api/v2/variables/05b740974228e000/labels',
org: '/api/v2/orgs/05b740945a91b000',
},
status: RemoteDataState.Loading,
},
values: null,
parents: [],
children: [
{
variable: {
id: '05b740973c68e000',
orgID: '05b740945a91b000',
name: 'static',
description: '',
selected: ['defbuck'],
arguments: {
type: 'constant',
values: ['beans', 'defbuck'],
},
createdAt: '2020-05-19T06:00:00.113169-07:00',
updatedAt: '2020-05-19T06:00:00.113169-07:00',
labels: [],
links: {
self: '/api/v2/variables/05b740973c68e000',
labels: '/api/v2/variables/05b740973c68e000/labels',
org: '/api/v2/orgs/05b740945a91b000',
},
status: RemoteDataState.Done,
},
values: null,
parents: [null],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
{
variable: {
orgID: '',
id: 'timeRangeStart',
name: 'timeRangeStart',
arguments: {
type: 'system',
values: [
[
{
magnitude: 1,
unit: 'h',
},
],
],
},
status: RemoteDataState.Done,
labels: [],
selected: [],
},
values: null,
parents: [null],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
null,
],
status: RemoteDataState.NotStarted,
cancel: () => {},
},
],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
{
variable: {
orgID: '',
id: 'windowPeriod',
name: 'windowPeriod',
arguments: {
type: 'system',
values: [10000],
},
status: RemoteDataState.Done,
labels: [],
selected: [],
},
values: null,
parents: [],
children: [],
status: RemoteDataState.Done,
cancel: () => {},
},
]
export const defaultVariable: Variable = {
id: '05b73f4bffe8e000',
orgID: '05b73f49a1d1b000',
name: 'static',
description: '',
selected: ['defbuck'],
arguments: {type: 'constant', values: ['beans', 'defbuck']},
createdAt: '2020-05-19T05:54:20.927477-07:00',
updatedAt: '2020-05-19T05:54:20.927477-07:00',
labels: [],
links: {
self: '/api/v2/variables/05b73f4bffe8e000',
labels: '/api/v2/variables/05b73f4bffe8e000/labels',
org: '/api/v2/orgs/05b73f49a1d1b000',
},
status: RemoteDataState.Done,
}
export const associatedVariable: Variable = {
id: '05b740974228e000',
orgID: '05b740945a91b000',
name: 'dependent',
description: '',
selected: [],
arguments: {
type: 'query',
values: {
query:
'from(bucket: v.static)\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "test")\n |> keep(columns: ["container_name"])\n |> rename(columns: {"container_name": "_value"})\n |> last()\n |> group()',
language: 'flux',
results: [],
},
},
createdAt: '2020-05-19T06:00:00.136597-07:00',
updatedAt: '2020-05-19T06:00:00.136597-07:00',
labels: [],
links: {
self: '/api/v2/variables/05b740974228e000',
labels: '/api/v2/variables/05b740974228e000/labels',
org: '/api/v2/orgs/05b740945a91b000',
},
status: RemoteDataState.Loading,
}
export const timeRangeStartVariable: Variable = {
orgID: '',
id: 'timeRangeStart',
name: 'timeRangeStart',
arguments: {
type: 'system',
values: [
[
{
magnitude: 1,
unit: 'h',
},
],
],
},
status: RemoteDataState.Done,
labels: [],
selected: [],
}
export const defaultVariables: Variable[] = [
defaultVariable,
associatedVariable,
timeRangeStartVariable,
{
orgID: '',
id: 'timeRangeStop',
name: 'timeRangeStop',
arguments: {
type: 'system',
values: ['now()'],
},
status: RemoteDataState.Done,
labels: [],
selected: [],
},
{
orgID: '',
id: 'windowPeriod',
name: 'windowPeriod',
arguments: {
type: 'system',
values: [10000],
},
status: RemoteDataState.Done,
labels: [],
selected: [],
},
]

View File

@ -1,9 +1,19 @@
// Utils
import {ValueFetcher} from 'src/variables/utils/ValueFetcher'
import {hydrateVars} from 'src/variables/utils/hydrateVars'
import {
hydrateVars,
createVariableGraph,
findSubgraph,
} from 'src/variables/utils/hydrateVars'
// Mocks
import {createVariable} from 'src/variables/mocks'
import {
createVariable,
associatedVariable,
defaultVariable,
defaultVariables,
timeRangeStartVariable,
} from 'src/variables/mocks'
// Types
import {Variable, CancellationError, RemoteDataState} from 'src/types'
@ -109,21 +119,18 @@ describe('hydrate vars', () => {
// f [fontcolor = "green"]
// g [fontcolor = "green"]
// }
//
// NOTE: these return falsy and not an empty array, because they are skipped
// within hydrateVars as not belonging to the graph
expect(
actual.filter(v => v.id === 'a')[0].arguments.values.results
).toBeFalsy()
).toEqual([])
expect(
actual.filter(v => v.id === 'b')[0].arguments.values.results
).toBeFalsy()
).toEqual([])
expect(
actual.filter(v => v.id === 'c')[0].arguments.values.results
).toBeFalsy()
).toEqual([])
expect(
actual.filter(v => v.id === 'd')[0].arguments.values.results
).toBeFalsy()
).toEqual([])
expect(
actual.filter(v => v.id === 'e')[0].arguments.values.results
@ -241,11 +248,9 @@ describe('hydrate vars', () => {
actual.filter(v => v.id === 'a')[0].arguments.values.results
).toEqual(['aVal'])
expect(actual.filter(v => v.id === 'a')[0].selected).toEqual(['aVal'])
expect(actual.filter(v => v.id === 'b')[0].arguments.values).toEqual({
k: 'v',
})
expect(actual.filter(v => v.id === 'b')[0].selected).toEqual(['k'])
})
// This ensures that the update of a dependant variable updates the
@ -319,3 +324,38 @@ describe('hydrate vars', () => {
cancel()
})
})
describe('findSubgraph', () => {
test('should return the update variable with all associated parents', async () => {
const variableGraph = await createVariableGraph(defaultVariables)
const actual = await findSubgraph(variableGraph, [defaultVariable])
// returns the single subgraph result
expect(actual.length).toEqual(1)
const [subgraph] = actual
// expect the subgraph to return the passed in variable
expect(subgraph.variable).toEqual(defaultVariable)
// expect the parent to be returned with the returning variable
expect(subgraph.parents[0].variable).toEqual(associatedVariable)
})
test('should return the variable with no parents when no association exists', async () => {
const a = createVariable('a', 'f(x: v.b)')
const variableGraph = await createVariableGraph([...defaultVariables, a])
const actual = await findSubgraph(variableGraph, [a])
expect(actual.length).toEqual(1)
const [subgraph] = actual
// expect the subgraph to return the passed in variable
expect(subgraph.variable).toEqual(a)
// expect the parent to be returned with the returning variable
expect(subgraph.parents).toEqual([])
})
test('should return the update default (timeRange) variable with associated parents', async () => {
const variableGraph = await createVariableGraph(defaultVariables)
const actual = await findSubgraph(variableGraph, [timeRangeStartVariable])
expect(actual.length).toEqual(1)
const [subgraph] = actual
// expect the subgraph to return the passed in variable
expect(subgraph.variable).toEqual(timeRangeStartVariable)
// expect the parent to be returned with the returning variable
expect(subgraph.parents[0].variable).toEqual(associatedVariable)
})
})

View File

@ -121,14 +121,15 @@ const collectAncestors = (
- The node for one of the passed variables depends on this node
*/
const findSubgraph = (
export const findSubgraph = (
graph: VariableNode[],
variables: Variable[]
): VariableNode[] => {
const subgraph: Set<VariableNode> = new Set()
// use an ID array to reduce the chance of reference errors
const varIDs = variables.map(v => v.id)
// create an array of IDs to reference later
const graphIDs = []
for (const node of graph) {
const shouldKeep =
varIDs.includes(node.variable.id) ||
@ -138,12 +139,18 @@ const findSubgraph = (
if (shouldKeep) {
subgraph.add(node)
graphIDs.push(node.variable.id)
}
}
const removeDupAncestors = (n: VariableNode) => {
const {id} = n.variable
return !graphIDs.includes(id)
}
for (const node of subgraph) {
node.parents = node.parents.filter(node => subgraph.has(node))
node.children = node.children.filter(node => subgraph.has(node))
node.parents = node.parents.filter(removeDupAncestors)
node.children = node.children.filter(removeDupAncestors)
}
return [...subgraph]
@ -223,7 +230,6 @@ const hydrateVarsHelper = async (
if (node.status !== RemoteDataState.Loading) {
node.status = RemoteDataState.Loading
on.fire('status', node.variable, node.status)
collectAncestors(node)
.filter(parent => parent.variable.arguments.type === 'query')
.forEach(parent => {
@ -487,9 +493,7 @@ export const hydrateVars = (
// from the main execution thread, allowing external services to
// register listeners for the loading state changes
Promise.resolve()
.then(() => {
return Promise.all(findLeaves(graph).map(resolve))
})
.then(() => Promise.all(findLeaves(graph).map(resolve)))
.then(() => {
deferred.resolve(extractResult(graph))
})