@spectric/ui 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.gitlab-ci.yml +28 -0
  2. package/.nvmrc +1 -0
  3. package/.storybook/analyze.sh +4 -0
  4. package/.storybook/main.ts +55 -0
  5. package/.storybook/preview.ts +42 -0
  6. package/.vscode/extensions.json +5 -0
  7. package/.vscode/settings.json +41 -0
  8. package/README.MD +50 -0
  9. package/html-include.png +0 -0
  10. package/package.json +33 -0
  11. package/src/classes/BitArray.ts +48 -0
  12. package/src/classes/DisposibleElement.ts +108 -0
  13. package/src/components/Banner.ts +102 -0
  14. package/src/components/Bitdisplay.ts +383 -0
  15. package/src/components/Button.ts +121 -0
  16. package/src/components/Header.ts +125 -0
  17. package/src/components/Page.ts +157 -0
  18. package/src/components/Panel.ts +56 -0
  19. package/src/components/ThemeProvider.ts +251 -0
  20. package/src/components/button.css.ts +160 -0
  21. package/src/components/configurations/classifications.ts +194 -0
  22. package/src/components/dialog/dialog.css.ts +50 -0
  23. package/src/components/dialog/dialog.ts +163 -0
  24. package/src/components/dialog/index.ts +1 -0
  25. package/src/components/header.css.ts +38 -0
  26. package/src/components/index.ts +10 -0
  27. package/src/components/input.css +75 -0
  28. package/src/components/input.ts +312 -0
  29. package/src/components/page.css.ts +158 -0
  30. package/src/components/panel.css.ts +44 -0
  31. package/src/components/query_bar/QueryBar.css +48 -0
  32. package/src/components/query_bar/QueryBar.ts +378 -0
  33. package/src/components/query_bar/index.ts +2 -0
  34. package/src/components/query_bar/querylanguage/kuery/ast/_generated_/kuery.js +3186 -0
  35. package/src/components/query_bar/querylanguage/kuery/ast/ast.ts +113 -0
  36. package/src/components/query_bar/querylanguage/kuery/ast/index.ts +31 -0
  37. package/src/components/query_bar/querylanguage/kuery/ast/kuery.peg +417 -0
  38. package/src/components/query_bar/querylanguage/kuery/functions/and.ts +55 -0
  39. package/src/components/query_bar/querylanguage/kuery/functions/exists.ts +62 -0
  40. package/src/components/query_bar/querylanguage/kuery/functions/index.ts +47 -0
  41. package/src/components/query_bar/querylanguage/kuery/functions/is.ts +211 -0
  42. package/src/components/query_bar/querylanguage/kuery/functions/nested.ts +63 -0
  43. package/src/components/query_bar/querylanguage/kuery/functions/not.ts +53 -0
  44. package/src/components/query_bar/querylanguage/kuery/functions/or.ts +56 -0
  45. package/src/components/query_bar/querylanguage/kuery/functions/range.ts +163 -0
  46. package/src/components/query_bar/querylanguage/kuery/functions/utils/get_fields.ts +49 -0
  47. package/src/components/query_bar/querylanguage/kuery/functions/utils/get_full_field_name_node.ts +87 -0
  48. package/src/components/query_bar/querylanguage/kuery/index.ts +38 -0
  49. package/src/components/query_bar/querylanguage/kuery/kuery_syntax_error.ts +76 -0
  50. package/src/components/query_bar/querylanguage/kuery/node_types/function.ts +75 -0
  51. package/src/components/query_bar/querylanguage/kuery/node_types/index.ts +46 -0
  52. package/src/components/query_bar/querylanguage/kuery/node_types/literal.ts +42 -0
  53. package/src/components/query_bar/querylanguage/kuery/node_types/named_arg.ts +47 -0
  54. package/src/components/query_bar/querylanguage/kuery/node_types/types.ts +108 -0
  55. package/src/components/query_bar/querylanguage/kuery/node_types/wildcard.ts +80 -0
  56. package/src/components/query_bar/querylanguage/kuery/types.ts +52 -0
  57. package/src/components/query_bar/querylanguage/outputTypes/toCQL.ts +122 -0
  58. package/src/components/query_bar/querylanguage/outputTypes/toMongo.ts +103 -0
  59. package/src/components/query_bar/querylanguage/utils.ts +35 -0
  60. package/src/components/query_bar/types.ts +59 -0
  61. package/src/components/splitview/index.ts +1 -0
  62. package/src/components/splitview/splitview.css.ts +66 -0
  63. package/src/components/splitview/splitview.ts +183 -0
  64. package/src/components/types.ts +35 -0
  65. package/src/index.ts +1 -0
  66. package/src/stories/Banner.stories.ts +46 -0
  67. package/src/stories/BitDisplay.stories.ts +68 -0
  68. package/src/stories/Button.stories.ts +138 -0
  69. package/src/stories/Header.stories.ts +55 -0
  70. package/src/stories/Page.stories.ts +108 -0
  71. package/src/stories/QueryBar.stories.ts +63 -0
  72. package/src/stories/Splitview.stories.ts +52 -0
  73. package/src/stories/fixtures/Bits.ts +15 -0
  74. package/src/stories/fixtures/ExampleContent.ts +102 -0
  75. package/src/stories/fixtures/data.ts +30 -0
  76. package/src/stories/fixtures/lorumipsum.ts +19 -0
  77. package/src/stories/input.stories.ts +77 -0
  78. package/src/stories/tsconfig.json +35 -0
  79. package/src/utils/debounce.ts +18 -0
  80. package/src/utils/spread.ts +71 -0
  81. package/src/vite-env.d.ts +1 -0
  82. package/test/__init__.py +9 -0
  83. package/test/elastic.py +9 -0
  84. package/test/interface.py +16 -0
  85. package/tsconfig.json +29 -0
  86. package/vite.config.js +34 -0
  87. package/vue-example.png +0 -0
  88. package/vue-include.png +0 -0
@@ -0,0 +1,378 @@
1
+ import { html, LitElement, PropertyValues } from "lit";
2
+ import * as kuery from "./querylanguage/kuery"
3
+ import { customElement, property, query, queryAsync, state } from "lit/decorators.js";
4
+ import { SpectricInput } from "../input";
5
+ import { JsonObject } from "./types";
6
+ import { HTMLElementTagWithEvents, ReactElementWithPropsAndEvents } from "../types";
7
+ import "./QueryBar.css"
8
+ import { DialogElement } from "../dialog";
9
+ import { createRef, ref } from "lit/directives/ref.js";
10
+ import { SpectricButton } from "../Button";
11
+ export type FieldTypes = {
12
+ name: string;
13
+ type: "string" | "number" | "boolean"
14
+ format?: "date-time"
15
+ }
16
+ type SuggestionType = 'conjunction' | "field" | 'operator' | 'value'
17
+ type Suggestion = {
18
+ fieldName: string
19
+ end: number
20
+ prefix: string
21
+ start: number
22
+ suffix: string
23
+ suggestionTypes: (SuggestionType)[]
24
+ text: string
25
+ type: "cursor"
26
+ }
27
+ export enum SupportedLanguages {
28
+ MONGO = "toMongo",
29
+ CQL = "toCql",
30
+ DSL = "toDSL",
31
+ AST = "AST"
32
+ }
33
+ type SupportedLanguagesTypes = `${SupportedLanguages}`
34
+ interface QueryEventMap {
35
+ "change": (event: CustomEvent<string | kuery.KueryNode | JsonObject>) => void;
36
+ }
37
+
38
+ export interface IQueryProps {
39
+ /**
40
+ * The output of the query in a specific format
41
+ */
42
+ outputLanguage: SupportedLanguagesTypes;
43
+ /**
44
+ * The output of the query
45
+ */
46
+ value: string;
47
+ /**
48
+ * Fields that are used for the auto complete
49
+ */
50
+ fields: FieldTypes[];
51
+ /**
52
+ * Callback that will provide values for specific fields
53
+ */
54
+ getValuesForField: (field: string, text: string) => Promise<string[]>;
55
+ }
56
+ type LabelValue = {
57
+ label?: string;
58
+ value: string;
59
+ }
60
+ type Completion = LabelValue & {
61
+ start: number;
62
+ end: number;
63
+ type: SuggestionType
64
+ onSelect?: () => Promise<string | undefined | number>
65
+ }
66
+ const NumberOperators: Record<string, LabelValue> = {
67
+ "eq": { value: " : ", label: " equals some value" },
68
+ "gt": { value: " > ", label: " is greater than some value" },
69
+ "lt": { value: " < ", label: " is less than some value" },
70
+ "gte": { value: " >= ", label: " is greater than or equal to some value" },
71
+ "lte": { value: " <= ", label: " is less than or equal to some value" }
72
+ }
73
+ const StringOperators: Record<string, LabelValue> = {
74
+ "eq": { value: ": ", label: " equals some value" },
75
+ "exists": { value: ": *", label: " exists in any form" }
76
+ }
77
+ const BoolOperators: LabelValue[] = [{ value: ": true", label: " value is true" },
78
+ { value: ": false", label: "value is false" }
79
+ ]
80
+ //Date operators are the same as number but we want a string value
81
+ const DateOperators: Record<string, LabelValue> = Object.fromEntries(Object.entries(NumberOperators).map(([key, labelValue]) => [key, { value: labelValue.value, label: (labelValue.label || "").replace("value", "date-time") }]))
82
+ /**
83
+ * The Query component will take Opensearch Dashboard Query language and transform it into various outputs
84
+ */
85
+ @customElement('spectric-query')
86
+ export class SpectricQuery extends LitElement implements IQueryProps {
87
+ private uuid: string;
88
+ constructor() {
89
+ super()
90
+ this.uuid = crypto.randomUUID()
91
+ }
92
+ protected createRenderRoot(): HTMLElement | DocumentFragment {
93
+ return this
94
+ }
95
+
96
+ /**
97
+ * The internal value.
98
+ */
99
+ protected _value: string = '';
100
+ private suggestion?: Suggestion;
101
+ /**
102
+ * The value of the input.
103
+ */
104
+ @property({ type: String, reflect: true })
105
+ get value() {
106
+ //pull directly from the input
107
+ if (this._input) {
108
+ return String(this._input.value)
109
+ }
110
+ // but before then _value will work fine
111
+ return this._value;
112
+ }
113
+
114
+ set value(value) {
115
+ const oldValue = this._value;
116
+ this._value = value;
117
+ this.requestUpdate('value', oldValue);
118
+ // we set the value directly on the input (when available)
119
+ // so that programatic manipulation updates the UI correctly
120
+ if (this._input) {
121
+ this._input.value = String(value);
122
+ }
123
+ }
124
+ @property({ type: String, reflect: true })
125
+ outputLanguage: SupportedLanguagesTypes = "AST";
126
+ @state()
127
+ private completions: Completion[] = [];
128
+ @state()
129
+ private completionIndex = 0;
130
+ @property({ type: Array, reflect: true })
131
+ fields: FieldTypes[] = [];
132
+
133
+ @query(".autocomplete")
134
+ _autocomplete?: HTMLDivElement
135
+
136
+ @queryAsync(".autocomplete")
137
+ //@ts-expect-error
138
+ _asyncAutocomplete: Promise<HTMLDivElement>
139
+
140
+ /**
141
+ * The underlying input element
142
+ */
143
+ @query('spectric-input')
144
+ protected _input!: SpectricInput;
145
+ _parseQuery = (e: InputEvent | undefined = undefined) => {
146
+ let ast;
147
+ if (this.value == "") {
148
+ return
149
+ }
150
+ try {
151
+ if (e && e.data == "(") {
152
+ //Auto close parentheses or parsing and suggestions fail
153
+ this.value = this.value + " )"
154
+ this._input.setSelectionRange(this.value.length - 2, this.value.length - 2)
155
+ }
156
+ let value = this.value;
157
+ if (this._input.selectionStart !== null) {
158
+ value = value.substring(0, this._input.selectionStart) + "@kuery-cursor@" + value.substring(this._input.selectionStart)
159
+ }
160
+ //FIXME: make auto complete work well.
161
+ let suggestions = kuery.parse(value, { parseCursor: true, cursorSymbol: "@kuery-cursor@", allowLeadingWildcards: false }) as unknown as Suggestion;
162
+ this.autoComplete(suggestions)
163
+
164
+ ast = kuery.parse(this.value, { allowLeadingWildcards: false });
165
+ } catch (e: any) {
166
+ // this.completions = []
167
+ // this._input.invalid = true;
168
+ // let [expect, _, arrow] = e.message.split("\n")
169
+ // this._input.invalidText = html`&#160;&#160;${arrow} ${expect}`;
170
+ return
171
+ }
172
+ let output
173
+ if (this.outputLanguage == "AST") {
174
+ output = ast
175
+ } else {
176
+ output = kuery[this.outputLanguage](ast, this.fields)
177
+ }
178
+ let event = new CustomEvent("change", { detail: output })
179
+ this.dispatchEvent(event)
180
+ }
181
+
182
+ getValuesForField = async (field: string, text: string): Promise<string[]> => {
183
+ console.log("getValuesForField isn't set no values returned", field, text)
184
+ return []
185
+ }
186
+ async autoComplete(suggestion: Suggestion) {
187
+ this.completions = []
188
+ if (suggestion.type !== "cursor") {
189
+ return
190
+ }
191
+ let completions = []
192
+ this.suggestion = suggestion
193
+ let { start, end } = suggestion
194
+ for (let type of suggestion.suggestionTypes) {
195
+ if (type == "conjunction" && suggestion.text.endsWith(' ')) {
196
+ completions.push(...["and ", "or "].map(value => ({ type, value, start: end, end: end })))
197
+ }
198
+ if (type === "field") {
199
+ let fieldCompletions = this.fields.filter(field => field.name.includes(suggestion.fieldName) || field.name.includes(suggestion.prefix)).map(f => {
200
+ return [{ type, value: f.name, start, end }]
201
+ }).flat()
202
+ completions.push(...fieldCompletions)
203
+ }
204
+ if (type === "operator") {
205
+ let fieldType = this.fields.find(field => field.name === suggestion.fieldName);
206
+ if (fieldType) {
207
+ if (fieldType.type === "number") {
208
+ completions.push(...Object.values(NumberOperators).map(value => ({ type, ...value, start: end, end: end })))
209
+ } else if (fieldType.type === "string") {
210
+ if (fieldType.format === "date-time") {
211
+ completions.push(...Object.values(DateOperators).map(value => ({
212
+ type, ...value, start: end, end: end, onSelect: async () => {
213
+ let values = await this.getValuesForField(suggestion.fieldName, suggestion.prefix);
214
+ if (values.length === 0) {
215
+ values = ["now-1m", "now-1d", "now-1M",]
216
+ }
217
+ let value: string | undefined;
218
+ let buttonRef = createRef<SpectricButton>()
219
+ await new Promise((resolve) => {
220
+ let dialog = DialogElement.display({}, html`
221
+ <div class="query-bar-date-quick-select">
222
+ ${values.map(v => html`<a href="#" @click=${(e: MouseEvent) => {
223
+ e.preventDefault()
224
+ value = `"${v}"`;
225
+ resolve(value); dialog.open = false
226
+ }}>${v}</a>`)}
227
+ </div>
228
+ <spectric-input variant="datetime-local" @change=${(e: any) => {
229
+ if (!e.target) {
230
+ return
231
+ }
232
+ let date = (new Date(e.target.value + ":00.000Z")).toISOString()
233
+ value = `"${date}"`
234
+ buttonRef.value!.disabled = value === undefined
235
+ }}></spectric-input>
236
+ <spectric-button ${ref(buttonRef)} .disabled=${true} @click=${() => { resolve(value); dialog.open = false }}>Submit</spectric-button>
237
+ `)
238
+ })
239
+ return value
240
+ }
241
+ })))
242
+ } else {
243
+ completions.push(...Object.values(StringOperators).map(value => ({ type, ...value, start: end, end: end })))
244
+ }
245
+ } else if (fieldType.type === "boolean") {
246
+ completions.push(...BoolOperators.map(value => ({ type, ...value, start: end, end: end })))
247
+ }
248
+ }
249
+ }
250
+ if (type === "value") {
251
+ let fieldType = this.fields.find(field => field.name === suggestion.fieldName);
252
+ if (fieldType && fieldType.type == "boolean") {
253
+ completions.push({ type, value: "true", start, end }, { type, value: "false", start, end })
254
+ } else {
255
+ console.log(`invoke callback to get values for ${suggestion.fieldName}`)
256
+ let values = await this.getValuesForField(suggestion.fieldName, suggestion.prefix);
257
+ if (fieldType?.type === "string") {
258
+ //quote the values
259
+ values = values.map(v => `"${v}"`)
260
+ }
261
+ completions.push(...values.map(value => ({ type, value, start, end })))
262
+ }
263
+
264
+ }
265
+ }
266
+ this.completions = completions
267
+ if (this.completions.length && this._autocomplete) {
268
+ let { width } = this._input.getBoundingClientRect();
269
+ this._autocomplete.showPopover();
270
+ this._autocomplete.style.width = `${width - 15}px`;
271
+ }
272
+ }
273
+ protected updated(changed: PropertyValues): void {
274
+ if (changed.has("outputLanguage")) {
275
+ this._parseQuery()
276
+ }
277
+ }
278
+ _selectCompletion = async () => {
279
+ if (!this.suggestion) {
280
+ return
281
+ }
282
+ let completion = this.completions[this.completionIndex]
283
+
284
+ let prefix = this.value.substring(0, completion.start) +
285
+ completion.value
286
+ let insertIndex = prefix.length
287
+ let afterStart = this.value.substring(completion.end)
288
+ if (completion.value.includes(afterStart)) {
289
+ this.value = prefix
290
+ } else {
291
+ this.value = prefix + this.value.substring(completion.end)
292
+ }
293
+ this._input.setSelectionRange(insertIndex, insertIndex)
294
+ if (completion.onSelect) {
295
+ let value = await completion.onSelect()
296
+ if (value !== undefined) {
297
+ this.value += value
298
+ }
299
+ }
300
+ this.completionIndex = 0;
301
+ this.completions = []
302
+ this._parseQuery()
303
+ }
304
+ _handleArrows = (e: KeyboardEvent) => {
305
+ if (e.key === "Escape") {
306
+ this.completions = []; //Escape closes the popover toplayer lets ensure the completions aren't selectable
307
+ }
308
+ if (e.key == "ArrowLeft" || e.key === "ArrowRight") {
309
+ setTimeout(this._parseQuery, 100)
310
+ }
311
+ if (!this.completions.length) {
312
+ return
313
+ }
314
+ if (["ArrowUp", "ArrowDown", 'Enter', "Tab"].includes(e.key) && this.suggestion) {
315
+ e.preventDefault();
316
+ if (e.key === "ArrowDown" || e.key === "Tab") {
317
+ this.completionIndex += 1;
318
+ if (this.completionIndex > this.completions.length - 1) {
319
+ this.completionIndex = 0
320
+ }
321
+ }
322
+ if (e.key === "ArrowUp") {
323
+ this.completionIndex -= 1;
324
+ if (this.completionIndex < 0) {
325
+ this.completionIndex = this.completions.length - 1
326
+ }
327
+ }
328
+
329
+ this._asyncAutocomplete?.then(element => {
330
+ let active = element.querySelector(".option.active");
331
+ if (active) {
332
+ active.scrollIntoView({ block: "nearest" })
333
+ }
334
+ })
335
+
336
+ if (e.key === "Enter") {
337
+ this._selectCompletion()
338
+ }
339
+ }
340
+ }
341
+ protected render() {
342
+ return html`
343
+ <spectric-input style=${`anchor-name:--${this.uuid};`} autocomplete="off" @input=${this._parseQuery} @keydown=${this._handleArrows}></spectric-input>
344
+ <div class="autocomplete" popover style=${`position-anchor: --${this.uuid};`}>
345
+ ${this.completions.map((option: Completion, index) =>
346
+ html`<div @click=${() => {
347
+ this.completionIndex = index;
348
+ this._selectCompletion()
349
+ }} class=${this.completionIndex == index ? "option active" : "option"}><span class="optiontype ${option.type}">${option.type}</span> <span class="value">${option.value}</span> <span class="label">${option.label}</span></span> </div></div>`
350
+ )}
351
+ </div>`
352
+ }
353
+ }
354
+
355
+
356
+ declare global {
357
+ interface HTMLElementTagNameMap {
358
+ "spectric-query": HTMLElementTagWithEvents<SpectricQuery, QueryEventMap>
359
+ }
360
+ namespace JSX {
361
+ interface IntrinsicElements {
362
+ /**
363
+ * {@link SpectricQuery}
364
+ */
365
+ "spectric-query": ReactElementWithPropsAndEvents<SpectricQuery, IQueryProps, QueryEventMap>
366
+ }
367
+ }
368
+ namespace React {
369
+ namespace JSX {
370
+ interface IntrinsicElements {
371
+ /**
372
+ * {@link SpectricQuery}
373
+ */
374
+ "spectric-query": ReactElementWithPropsAndEvents<SpectricQuery, IQueryProps, QueryEventMap>;
375
+ }
376
+ }
377
+ }
378
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./QueryBar"
2
+ export * from "./types"