Skip to content

Commit 1ba59d9

Browse files
authored
feat(5342): schema composition coordination between LSP, monaco-editor, and UI session store (#5349)
This PR is about a division of responsibilities. The LSP is responsible for generating the correct flux (applyEdits), the session store is the src of truth for UI state, the monaco-editor has control over everything within the code block and determines the pixel location of the lines, and UI elements are our clickable affordances. And then we need something to coordinate communication btwn the parts -- that is the LspConnectionManager. the LspConnectionManger holds a pointer reference to each object, and holds the minimum state necessary to do it's job (e.g. diff'ing old versus new session, to minimize updates and prevent looping callbacks).
1 parent e7eb610 commit 1ba59d9

File tree

9 files changed

+323
-20
lines changed

9 files changed

+323
-20
lines changed

src/dataExplorer/components/FluxQueryBuilder.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {SidebarProvider} from 'src/dataExplorer/context/sidebar'
2222
import {
2323
PersistanceProvider,
2424
PersistanceContext,
25+
DEFAULT_SCHEMA,
2526
} from 'src/dataExplorer/context/persistance'
2627
import ResultsPane from 'src/dataExplorer/components/ResultsPane'
2728
import Sidebar from 'src/dataExplorer/components/Sidebar'
@@ -42,7 +43,7 @@ const FluxQueryBuilder: FC = () => {
4243
setStatus(RemoteDataState.NotStarted)
4344
setResult(null)
4445
setQuery('')
45-
setSelection({bucket: null, measurement: ''})
46+
setSelection(JSON.parse(JSON.stringify(DEFAULT_SCHEMA)))
4647
}, [setQuery, setStatus, setResult, setSelection, cancel])
4748

4849
return (

src/dataExplorer/context/fluxQueryBuilder.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,7 @@ export const FluxQueryBuilderProvider: FC = ({children}) => {
8686
}, [selection.bucket])
8787

8888
const handleSelectBucket = (bucket: Bucket): void => {
89-
selection.bucket = bucket
90-
selection.measurement = ''
91-
setSelection({...selection})
89+
setSelection({bucket, measurement: ''})
9290

9391
// Reset measurement, tags, and fields
9492
resetFields()
@@ -99,8 +97,7 @@ export const FluxQueryBuilderProvider: FC = ({children}) => {
9997
}
10098

10199
const handleSelectMeasurement = (measurement: string): void => {
102-
selection.measurement = measurement
103-
setSelection({...selection})
100+
setSelection({measurement})
104101

105102
// Inject measurement
106103
injectViaLsp(ExecuteCommand.InjectionMeasurement, {

src/dataExplorer/context/persistance.tsx

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,58 @@
1-
import React, {FC, createContext} from 'react'
2-
import {TimeRange} from 'src/types'
1+
import React, {FC, createContext, useCallback} from 'react'
2+
import {TimeRange, RecursivePartial} from 'src/types'
33
import {DEFAULT_TIME_RANGE} from 'src/shared/constants/timeRanges'
44
import {useSessionStorage} from 'src/dataExplorer/shared/utils'
55
import {Bucket} from 'src/types'
6+
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
67

7-
interface MeasurementSelection {
8+
interface SchemaComposition {
9+
synced: boolean // true == can modify session's schema
10+
diverged: boolean // true == cannot re-sync. (e.g. user has typed in the composition block)
11+
}
12+
13+
export interface SchemaSelection {
814
bucket: Bucket
915
measurement: string
16+
fields: string[]
17+
composition: SchemaComposition
1018
}
1119

1220
interface ContextType {
1321
horizontal: number[]
1422
vertical: number[]
1523
range: TimeRange
1624
query: string
17-
selection: MeasurementSelection
25+
selection: SchemaSelection
1826

1927
setHorizontal: (val: number[]) => void
2028
setVertical: (val: number[]) => void
2129
setRange: (val: TimeRange) => void
2230
setQuery: (val: string) => void
23-
setSelection: (val: MeasurementSelection) => void
31+
setSelection: (val: RecursivePartial<SchemaSelection>) => void
32+
}
33+
34+
export const DEFAULT_SCHEMA = {
35+
bucket: null,
36+
measurement: null,
37+
fields: [],
38+
composition: {
39+
synced: false,
40+
diverged: false,
41+
},
2442
}
2543

2644
const DEFAULT_CONTEXT = {
2745
horizontal: [0.2],
2846
vertical: [0.25, 0.8],
2947
range: DEFAULT_TIME_RANGE,
3048
query: '',
31-
selection: {
32-
bucket: null,
33-
measurement: '',
34-
},
49+
selection: DEFAULT_SCHEMA,
3550

3651
setHorizontal: (_: number[]) => {},
3752
setVertical: (_: number[]) => {},
3853
setRange: (_: TimeRange) => {},
3954
setQuery: (_: string) => {},
40-
setSelection: (_: MeasurementSelection) => {},
55+
setSelection: (_: RecursivePartial<SchemaSelection>) => {},
4156
}
4257

4358
export const PersistanceContext = createContext<ContextType>(DEFAULT_CONTEXT)
@@ -68,6 +83,28 @@ export const PersistanceProvider: FC = ({children}) => {
6883
JSON.parse(JSON.stringify(DEFAULT_CONTEXT.selection))
6984
)
7085

86+
const setSchemaSelection = useCallback(
87+
schema => {
88+
if (
89+
isFlagEnabled('schemaComposition') &&
90+
selection.composition?.diverged
91+
) {
92+
// TODO: how message to user?
93+
return
94+
}
95+
const nextState = {
96+
...selection,
97+
...schema,
98+
composition: {
99+
...(selection.composition || {}),
100+
...(schema.composition || {}),
101+
},
102+
}
103+
setSelection(nextState)
104+
},
105+
[selection, selection.composition, setSelection]
106+
)
107+
71108
return (
72109
<PersistanceContext.Provider
73110
value={{
@@ -81,7 +118,7 @@ export const PersistanceProvider: FC = ({children}) => {
81118
setVertical,
82119
setRange,
83120
setQuery,
84-
setSelection,
121+
setSelection: setSchemaSelection,
85122
}}
86123
>
87124
{children}

src/languageSupport/languages/flux/lsp/connection.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import {format_from_js_file} from '@influxdata/flux-lsp-browser'
55
import {EditorType, Variable} from 'src/types'
66
import {buildUsedVarsOption} from 'src/variables/utils/buildVarsOption'
77

8+
// handling schema composition
9+
import {RecursivePartial} from 'src/types'
10+
import {SchemaSelection} from 'src/dataExplorer/context/persistance'
11+
812
// LSP methods
913
import {
1014
didOpen,
@@ -18,11 +22,24 @@ import {
1822
// error reporting
1923
import {reportErrorThroughHoneyBadger} from 'src/shared/utils/errors'
2024

25+
const ICON_SYNC_CLASSNAME = 'composition-sync'
26+
export const ICON_SYNC_ID = 'schema-composition-sync-icon'
27+
28+
// hardcoded in LSP
29+
const COMPOSITION_YIELD = '_editor_composition'
30+
const COMPOSITION_INIT_LINE = 1
31+
2132
class LspConnectionManager {
2233
private _worker: Worker
34+
private _editor: EditorType
2335
private _model: MonacoTypes.editor.IModel
2436
private _preludeModel: MonacoTypes.editor.IModel
2537
private _variables: Variable[] = []
38+
private _compositionStyle: string[] = []
39+
private _session: SchemaSelection
40+
private _callbackSetSession: (
41+
schema: RecursivePartial<SchemaSelection>
42+
) => void = () => null
2643

2744
constructor(worker: Worker) {
2845
this._worker = worker
@@ -56,6 +73,7 @@ class LspConnectionManager {
5673
}
5774

5875
subscribeToModel(editor: EditorType) {
76+
this._editor = editor
5977
this._model = editor.getModel()
6078

6179
this._model.onDidChangeContent(() => this.updatePreludeModel())
@@ -91,6 +109,210 @@ class LspConnectionManager {
91109
this._worker.postMessage(msg)
92110
}
93111

112+
_getCompositionBlockLines() {
113+
const query = this._model.getValue()
114+
const startLine = COMPOSITION_INIT_LINE
115+
const endLine =
116+
(query.split('\n').findIndex(line => line.includes(COMPOSITION_YIELD)) ||
117+
0) + 1
118+
return {startLine, endLine}
119+
}
120+
121+
_setSessionSync(synced: boolean) {
122+
this._callbackSetSession({
123+
composition: {synced},
124+
})
125+
}
126+
127+
_setEditorSyncToggle() {
128+
setTimeout(() => {
129+
// elements in monaco-editor. positioned by editor.
130+
const syncIcons = document.getElementsByClassName(ICON_SYNC_CLASSNAME)
131+
132+
// UI elements we control
133+
const clickableInvisibleDiv = document.getElementById(ICON_SYNC_ID)
134+
if (!syncIcons.length || !clickableInvisibleDiv) {
135+
return
136+
}
137+
138+
const [upperIcon] = syncIcons
139+
let [, lowerIcon] = syncIcons
140+
if (!lowerIcon) {
141+
lowerIcon = upperIcon
142+
}
143+
const {startLine, endLine} = this._getCompositionBlockLines()
144+
145+
// move div to match monaco-editor coordinates
146+
clickableInvisibleDiv.style.top =
147+
((upperIcon as any).offsetTop || 0) + 'px'
148+
const height =
149+
((lowerIcon as any).offsetHeight || 0) * (endLine - startLine + 1) +
150+
((upperIcon as any).offsetTop || 0)
151+
clickableInvisibleDiv.style.height = height + 'px'
152+
clickableInvisibleDiv.style.width =
153+
((upperIcon as any).offsetWidth || 0) + 'px'
154+
155+
// add listeners
156+
clickableInvisibleDiv.removeEventListener('click', () =>
157+
this._setSessionSync(!this._session.composition.synced)
158+
) // may have existing
159+
clickableInvisibleDiv.addEventListener('click', () =>
160+
this._setSessionSync(!this._session.composition.synced)
161+
)
162+
}, 1000)
163+
}
164+
165+
_editorChangeIsFromLsp(change) {
166+
return !!change.forceMoveMarkers
167+
}
168+
169+
_setEditorIrreversibleExit() {
170+
this._model.onDidChangeContent(e => {
171+
const {changes} = e
172+
changes.some(change => {
173+
const {startLine, endLine} = this._getCompositionBlockLines()
174+
if (
175+
change.range.startLineNumber >= startLine &&
176+
change.range.endLineNumber <= endLine &&
177+
!this._editorChangeIsFromLsp(change) &&
178+
!this._session.composition.diverged
179+
) {
180+
this._callbackSetSession({
181+
composition: {synced: false, diverged: true},
182+
})
183+
return
184+
}
185+
})
186+
})
187+
}
188+
189+
_setEditorBlockStyle() {
190+
const {startLine, endLine} = this._getCompositionBlockLines()
191+
192+
const startLineStyle = [
193+
{
194+
range: new MonacoTypes.Range(startLine, 1, startLine, 1),
195+
options: {
196+
linesDecorationsClassName: ICON_SYNC_CLASSNAME,
197+
},
198+
},
199+
]
200+
const endLineStyle = [
201+
{
202+
range: new MonacoTypes.Range(endLine, 1, endLine, 1),
203+
options: {
204+
linesDecorationsClassName: ICON_SYNC_CLASSNAME,
205+
},
206+
},
207+
]
208+
209+
const removeAllStyles = this._session.composition.diverged
210+
211+
this._compositionStyle = this._editor.deltaDecorations(
212+
this._compositionStyle,
213+
removeAllStyles ? [] : startLineStyle.concat(endLineStyle)
214+
)
215+
216+
const clickableInvisibleDiv = document.getElementById(ICON_SYNC_ID)
217+
clickableInvisibleDiv.style.background = this._session.composition.synced
218+
? 'blue'
219+
: 'grey'
220+
221+
if (removeAllStyles) {
222+
clickableInvisibleDiv.style.display = 'none'
223+
}
224+
}
225+
226+
_initLsp(schema: SchemaSelection) {
227+
const {bucket, measurement} = schema
228+
const payload = {bucket: bucket?.name}
229+
if (measurement) {
230+
payload['measurement'] = measurement
231+
}
232+
// TODO: finish LSP update first
233+
// this.inject(ExecuteCommand.CompositionInit, payload)
234+
}
235+
236+
_updateLsp(_: SchemaSelection) {
237+
// TODO: finish LSP update first
238+
// this.inject(ExecuteCommand.Composition<Something>, payload)
239+
}
240+
241+
_initComposition(schema: SchemaSelection) {
242+
if (!schema.composition.synced) {
243+
this._setSessionSync(true)
244+
}
245+
246+
this._initLsp(schema)
247+
248+
// handlers to trigger end composition
249+
this._setEditorSyncToggle()
250+
this._setEditorIrreversibleExit()
251+
252+
// handlers for composition block size
253+
// eventually, this could be from the LSP response. onLspMessage()
254+
this._model.onDidChangeContent(
255+
() => this._session.composition?.synced && this._setEditorBlockStyle()
256+
)
257+
258+
// TODO: for now, set style on init. Eventually use this.onLspMessage()
259+
this._setEditorBlockStyle()
260+
}
261+
262+
_restoreComposition(schema: SchemaSelection) {
263+
this._initLsp(schema)
264+
this._updateLsp(schema)
265+
}
266+
267+
onSchemaSessionChange(schema: SchemaSelection, sessionCb) {
268+
this._callbackSetSession = sessionCb
269+
const previousState = {
270+
...this._session,
271+
composition: {...(this._session?.composition || {})},
272+
}
273+
this._session = {...schema, composition: {...schema.composition}}
274+
275+
if (!schema.composition) {
276+
// FIXME: message to user, to create a new script
277+
console.error(
278+
'User has an old session, which does not support schema composition.'
279+
)
280+
return
281+
}
282+
283+
if (!previousState.bucket && schema.bucket) {
284+
// TODO: if also already have fields and tagValues, then _restoreComposition()
285+
const hasFieldsOrTagvalues = false
286+
if (hasFieldsOrTagvalues) {
287+
return this._restoreComposition(schema)
288+
}
289+
return this._initComposition(schema)
290+
}
291+
292+
// TODO: decide on tag and tagValues.
293+
// Inject on same or different lines? then...how model in session?
294+
const tagsDidUpdate = false
295+
296+
if (
297+
previousState.fields?.length != schema.fields?.length ||
298+
tagsDidUpdate
299+
) {
300+
return this._updateLsp(schema)
301+
}
302+
303+
if (previousState.composition != schema.composition) {
304+
return this._setEditorBlockStyle()
305+
}
306+
}
307+
308+
onLspMessage(_jsonrpcMiddlewareResponse: unknown) {
309+
// TODO: Q4
310+
// 1. middleware detects jsonrpc
311+
// 2. call this method
312+
// 3a. update (true-up) session store
313+
// 3b. this._setEditorBlockStyle()
314+
}
315+
94316
dispose() {
95317
this._model.onDidChangeContent(null)
96318
}

0 commit comments

Comments
 (0)