Skip to content

Commit ae994f1

Browse files
authored
feat: add save as notebook option from data explorer (#4352)
1 parent eb48cf0 commit ae994f1

File tree

5 files changed

+203
-1
lines changed

5 files changed

+203
-1
lines changed

cypress/e2e/shared/explorer.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,7 @@ describe('DataExplorer', () => {
757757
cy.get('[id="variable"]').click()
758758
cy.getByTestID('flux-editor').should('be.visible')
759759
cy.get('[id="dashboard"]').click()
760+
cy.getByTestID('cell--radio-button').click()
760761
cy.getByTestID('save-as-dashboard-cell--dropdown').should('be.visible')
761762

762763
// close save as
@@ -790,6 +791,7 @@ describe('DataExplorer', () => {
790791
it('can save as cell into multiple dashboards', () => {
791792
// input dashboards and cell name
792793
dashboardNames.forEach(name => {
794+
cy.getByTestID('cell--radio-button').click()
793795
cy.getByTestID('save-as-dashboard-cell--dropdown').click()
794796
cy.getByTestID('save-as-dashboard-cell--dropdown-menu').within(() => {
795797
cy.contains(name).click()
@@ -816,6 +818,7 @@ describe('DataExplorer', () => {
816818

817819
it('can create new dashboard as saving target', () => {
818820
// select and input new dashboard name and cell name
821+
cy.getByTestID('cell--radio-button').click()
819822
cy.getByTestID('save-as-dashboard-cell--dropdown').click()
820823
cy.getByTestID('save-as-dashboard-cell--dropdown-menu').within(() => {
821824
cy.getByTestID('save-as-dashboard-cell--create-new-dash').click()

cypress/e2e/shared/legends.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ describe('Legends', () => {
332332
// Without submitting the query, save it to a dashboard
333333
cy.getByTestID('save-query-as').click()
334334
cy.getByTestID('overlay--container').should('exist')
335+
cy.getByTestID('cell--radio-button').click()
335336
cy.getByTestID('save-as-dashboard-cell--dropdown').click()
336337
cy.getByTestID('save-as-dashboard-cell--create-new-dash').click()
337338
cy.getByTestID('save-as-dashboard-cell--dashboard-name')
@@ -392,6 +393,7 @@ describe('Legends', () => {
392393
// Save it to a dashboard
393394
cy.getByTestID('save-query-as').click()
394395
cy.getByTestID('overlay--container').should('exist')
396+
cy.getByTestID('cell--radio-button').click()
395397
cy.getByTestID('save-as-dashboard-cell--dropdown').click()
396398
cy.getByTestID('save-as-dashboard-cell--create-new-dash').click()
397399
cy.getByTestID('save-as-dashboard-cell--dashboard-name')

cypress/e2e/shared/queryBuilder.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ describe('The Query Builder', () => {
4545
cy.getByTestID('save-query-as').click()
4646

4747
// open the dashboard selector dropdown
48+
cy.getByTestID('cell--radio-button').click()
4849
cy.getByTestID('save-as-dashboard-cell--dropdown').click()
4950
cy.getByTestID('save-as-dashboard-cell--create-new-dash').click()
5051
cy.getByTestID('save-as-overlay--header').click() // close the blast door i mean dropdown
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Libraries
2+
import React, {FC, ChangeEvent, useState} from 'react'
3+
import {useSelector} from 'react-redux'
4+
import {useHistory} from 'react-router-dom'
5+
import {nanoid} from 'nanoid'
6+
7+
// Selectors
8+
import {getActiveTimeMachine, getSaveableView} from 'src/timeMachine/selectors'
9+
import {getOrg} from 'src/organizations/selectors'
10+
import {postNotebook} from 'src/client/notebooksRoutes'
11+
12+
// Components
13+
import {Form, Input, Button, Grid} from '@influxdata/clockface'
14+
import {AUTOREFRESH_DEFAULT} from 'src/shared/constants'
15+
16+
// Types
17+
import {
18+
Columns,
19+
InputType,
20+
ButtonType,
21+
ComponentColor,
22+
ComponentStatus,
23+
} from '@influxdata/clockface'
24+
25+
// Utils
26+
import {event, normalizeEventName} from 'src/cloud/utils/reporting'
27+
import {chartTypeName} from 'src/visualization/utils/chartTypeName'
28+
import {
29+
PROJECT_NAME,
30+
DEFAULT_PROJECT_NAME,
31+
PROJECT_NAME_PLURAL,
32+
} from 'src/flows'
33+
import {hydrate, serialize} from 'src/flows/context/flow.list'
34+
import {getBucketByName} from 'src/buckets/selectors'
35+
import {AppState} from 'src/types'
36+
37+
interface Props {
38+
dismiss: () => void
39+
}
40+
41+
const SaveAsNotebookForm: FC<Props> = ({dismiss}) => {
42+
const [notebookName, setNotebookName] = useState('')
43+
const orgID = useSelector(getOrg).id
44+
const {draftQueries, autoRefresh, timeRange} = useSelector(
45+
getActiveTimeMachine
46+
)
47+
const {properties} = useSelector(getSaveableView)
48+
let allUsedBuckets: string[] = []
49+
50+
draftQueries.forEach(draftQuery => {
51+
allUsedBuckets = allUsedBuckets.concat(draftQuery.builderConfig.buckets)
52+
})
53+
54+
const completeBuckets = useSelector((state: AppState) => {
55+
const {draftQueries: drafts} = getActiveTimeMachine(state)
56+
const buckets = drafts.flatMap(draft => draft.builderConfig.buckets)
57+
return buckets.map(name => getBucketByName(state, name))
58+
})
59+
60+
const history = useHistory()
61+
62+
const handleChangeNotebookName = (event: ChangeEvent<HTMLInputElement>) => {
63+
setNotebookName(event.target.value)
64+
}
65+
66+
const handleSubmit = async () => {
67+
event(`Data Explorer Save as ${PROJECT_NAME} Submitted`)
68+
69+
try {
70+
event(`data_explorer.save.as_${PROJECT_NAME.toLowerCase()}.success`, {
71+
which: normalizeEventName(chartTypeName(properties?.type ?? 'xy')),
72+
})
73+
const pipes: any = []
74+
75+
for (let i = 0; i < draftQueries.length; i++) {
76+
const draftQuery = draftQueries[i]
77+
let pipe: any = {
78+
id: `local_${nanoid()}`,
79+
visible: !draftQuery.hidden,
80+
}
81+
if (draftQuery.editMode === 'builder') {
82+
const bucket = completeBuckets.splice(0, 1)
83+
84+
pipe.title = `Build a Query ${i + 1}`
85+
pipe.type = 'queryBuilder'
86+
pipe = {
87+
...pipe,
88+
...draftQuery.builderConfig,
89+
buckets: bucket,
90+
}
91+
} else {
92+
pipe.title = 'Query to Run'
93+
pipe.queries = draftQuery.builderConfig
94+
pipe.activeQuery = 0
95+
pipe.type = 'rawFluxEditor'
96+
}
97+
98+
pipes.push(pipe)
99+
pipes.push({
100+
id: `local_${nanoid()}`,
101+
properties: {
102+
...properties,
103+
builderConfig: draftQuery.builderConfig,
104+
},
105+
title: `Visualize the Result ${i + 1}`,
106+
type: 'visualization',
107+
visible: true,
108+
})
109+
}
110+
111+
const flow = hydrate({
112+
name: notebookName || DEFAULT_PROJECT_NAME,
113+
range: timeRange,
114+
orgID,
115+
spec: {
116+
readOnly: false,
117+
range: timeRange,
118+
refresh: autoRefresh || AUTOREFRESH_DEFAULT,
119+
pipes,
120+
},
121+
})
122+
123+
const response = await postNotebook(serialize(flow))
124+
125+
if (response.status !== 200) {
126+
throw new Error(response.data.message)
127+
}
128+
129+
const {id} = response.data
130+
131+
// redirect to the notebook
132+
history.push(`/orgs/${orgID}/${PROJECT_NAME_PLURAL.toLowerCase()}/${id}`)
133+
} catch (error) {
134+
event(`data_explorer.save.as_${PROJECT_NAME.toLowerCase()}.failure`, {
135+
which: normalizeEventName(chartTypeName(properties?.type)),
136+
})
137+
console.error(error)
138+
dismiss()
139+
} finally {
140+
setNotebookName('')
141+
}
142+
}
143+
144+
return (
145+
<Grid>
146+
<Grid.Row>
147+
<Grid.Column widthXS={Columns.Twelve}>
148+
<Form.Element label={`${PROJECT_NAME} Name`}>
149+
<Input
150+
type={InputType.Text}
151+
placeholder={`Add optional ${PROJECT_NAME.toLowerCase()} name`}
152+
name="notebookName"
153+
value={notebookName}
154+
onChange={handleChangeNotebookName}
155+
testID={`save-as-${PROJECT_NAME.toLowerCase()}--name`}
156+
/>
157+
</Form.Element>
158+
</Grid.Column>
159+
<Grid.Column widthXS={Columns.Twelve}>
160+
<Form.Footer>
161+
<Button
162+
text="Cancel"
163+
onClick={dismiss}
164+
titleText="Cancel"
165+
type={ButtonType.Button}
166+
color={ComponentColor.Tertiary}
167+
testID={`save-as-${PROJECT_NAME.toLowerCase()}--cancel`}
168+
/>
169+
<Button
170+
text={`Save as ${PROJECT_NAME}`}
171+
testID={`save-as-${PROJECT_NAME.toLowerCase()}--submit`}
172+
color={ComponentColor.Success}
173+
type={ButtonType.Submit}
174+
onClick={handleSubmit}
175+
status={ComponentStatus.Default}
176+
/>
177+
</Form.Footer>
178+
</Grid.Column>
179+
</Grid.Row>
180+
</Grid>
181+
)
182+
}
183+
184+
export default SaveAsNotebookForm

src/dataExplorer/components/SaveAsOverlay.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {useHistory} from 'react-router-dom'
33

44
// Components
55
import SaveAsCellForm from 'src/dataExplorer/components/SaveAsCellForm'
6+
import SaveAsNotebookForm from 'src/dataExplorer/components/SaveAsNotebookForm'
67
import SaveAsTaskForm from 'src/dataExplorer/components/SaveAsTaskForm'
78
import SaveAsVariable from 'src/dataExplorer/components/SaveAsVariable'
89
import {
@@ -15,16 +16,18 @@ import {
1516

1617
// Utils
1718
import {event} from 'src/cloud/utils/reporting'
19+
import {PROJECT_NAME} from 'src/flows'
1820

1921
enum SaveAsOption {
2022
Dashboard = 'dashboard',
23+
Notebook = 'notebook',
2124
Task = 'task',
2225
Variable = 'variable',
2326
}
2427

2528
const SaveAsOverlay: FC = () => {
2629
const history = useHistory()
27-
const [saveAsOption, setSaveAsOption] = useState(SaveAsOption.Dashboard)
30+
const [saveAsOption, setSaveAsOption] = useState(SaveAsOption.Notebook)
2831
const hide = useCallback(() => {
2932
history.goBack()
3033
}, [history])
@@ -39,6 +42,8 @@ const SaveAsOverlay: FC = () => {
3942
saveAsForm = <SaveAsTaskForm dismiss={hide} />
4043
} else if (saveAsOption === SaveAsOption.Variable) {
4144
saveAsForm = <SaveAsVariable onHideOverlay={hide} />
45+
} else if (saveAsOption === SaveAsOption.Notebook) {
46+
saveAsForm = <SaveAsNotebookForm dismiss={hide} />
4247
}
4348

4449
return (
@@ -52,6 +57,13 @@ const SaveAsOverlay: FC = () => {
5257
<Overlay.Body>
5358
<Tabs.Container orientation={Orientation.Horizontal}>
5459
<Tabs alignment={Alignment.Center} size={ComponentSize.Medium}>
60+
<Tabs.Tab
61+
id={SaveAsOption.Notebook}
62+
text={`${PROJECT_NAME}`}
63+
testID={`${PROJECT_NAME.toLowerCase()}--radio-button`}
64+
onClick={() => setSaveAsOption(SaveAsOption.Notebook)}
65+
active={saveAsOption === SaveAsOption.Notebook}
66+
/>
5567
<Tabs.Tab
5668
id={SaveAsOption.Dashboard}
5769
text="Dashboard Cell"

0 commit comments

Comments
 (0)