terrier-engine 4.7.3 → 4.7.5

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.
@@ -11,6 +11,7 @@ import {Dropdown} from "../../terrier/dropdowns"
11
11
  import DiveEditor from "../dives/dive-editor"
12
12
  import Messages from "tuff-core/messages"
13
13
  import Arrays from "tuff-core/arrays"
14
+ import {ColumnValidationError} from "./validation"
14
15
 
15
16
  const log = new Logger("Columns")
16
17
 
@@ -57,9 +58,25 @@ export type ColumnRef = {
57
58
  alias?: string
58
59
  grouped?: boolean
59
60
  function?: AggFunction | DateFunction
61
+ errors?: ColumnValidationError[]
60
62
  }
61
63
 
62
64
 
65
+ /**
66
+ * Computes the name of the resulting select clause.
67
+ * @param table
68
+ * @param col
69
+ */
70
+ function computeSelectName(table: TableRef, col: ColumnRef): string {
71
+ if (col.alias?.length) {
72
+ return col.alias
73
+ } else if (table.prefix?.length) {
74
+ return `${table.prefix}${col.name}`
75
+ } else {
76
+ return col.name
77
+ }
78
+ }
79
+
63
80
 
64
81
  ////////////////////////////////////////////////////////////////////////////////
65
82
  // Rendering
@@ -405,7 +422,8 @@ class SelectColumnsDropdown extends Dropdown<{modelDef: ModelDef, callback: Sele
405
422
 
406
423
  const Columns = {
407
424
  render,
408
- functionType
425
+ functionType,
426
+ computeSelectName
409
427
  }
410
428
 
411
429
  export default Columns
@@ -13,6 +13,7 @@ import Format from "../../terrier/format"
13
13
  import DiveEditor from "../dives/dive-editor"
14
14
  import Messages from "tuff-core/messages"
15
15
  import Arrays from "tuff-core/arrays"
16
+ import {SelectOptions} from "tuff-core/forms"
16
17
 
17
18
  const log = new Logger("Filters")
18
19
 
@@ -27,8 +28,33 @@ type BaseFilter = {
27
28
  edit_label?: string
28
29
  }
29
30
 
30
- export const DirectOperators = ['eq', 'ne', 'ilike', 'lt', 'gt', 'lte', 'gte'] as const
31
- export type DirectOperator = typeof DirectOperators[number]
31
+ const directOperators = ['eq', 'ne', 'ilike', 'lt', 'gt', 'lte', 'gte'] as const
32
+ export type DirectOperator = typeof directOperators[number]
33
+
34
+ /**
35
+ * Computes the operator options for the given column type.
36
+ * @param type
37
+ */
38
+ function operatorOptions(type: string): SelectOptions {
39
+ let operators: DirectOperator[] = ['eq', 'ne'] // equality is the only thing we can assume for any type
40
+ switch (type) {
41
+ case 'text':
42
+ case 'string':
43
+ operators = ['eq', 'ne', 'ilike']
44
+ break
45
+ case 'float':
46
+ case 'integer':
47
+ case 'cents':
48
+ operators = ['eq', 'ne', 'lt', 'gt', 'lte', 'gte']
49
+ break
50
+ }
51
+ return operators.map(op => {
52
+ return {
53
+ value: op,
54
+ title: operatorDisplay(op)
55
+ }
56
+ })
57
+ }
32
58
 
33
59
  export type DirectFilter = BaseFilter & {
34
60
  filter_type: 'direct'
@@ -73,7 +99,7 @@ function operatorDisplay(op: DirectOperator): string {
73
99
  case 'ne':
74
100
  return '≠'
75
101
  case 'ilike':
76
- return '~'
102
+ return ''
77
103
  case 'lt':
78
104
  return '<'
79
105
  case 'lte':
@@ -369,10 +395,8 @@ class DirectFilterEditor extends FilterEditor<DirectFilter> {
369
395
  col.div('.tt-readonly-field', {text: this.state.column})
370
396
  })
371
397
  parent.div('.operator', col => {
372
- const operatorOptions = DirectOperators.map(op => {
373
- return {title: operatorDisplay(op), value: op}
374
- })
375
- this.select(col, 'operator', operatorOptions)
398
+ const opts = operatorOptions(this.columnDef?.type || 'text')
399
+ this.select(col, 'operator', opts)
376
400
  })
377
401
  parent.div('.filter', col => {
378
402
  switch (this.state.column_type) {
@@ -11,6 +11,7 @@ import inflection from "inflection"
11
11
  import Messages from "tuff-core/messages"
12
12
  import Strings from "tuff-core/strings"
13
13
  import Arrays from "tuff-core/arrays"
14
+ import {ColumnRef} from "./columns"
14
15
 
15
16
  const log = new Logger("Queries")
16
17
 
@@ -28,13 +29,41 @@ export type Query = {
28
29
 
29
30
 
30
31
  ////////////////////////////////////////////////////////////////////////////////
31
- // Validation
32
+ // Utilities
33
+ ////////////////////////////////////////////////////////////////////////////////
34
+
35
+ type ColumnFunction = (table: TableRef, col: ColumnRef) => any
36
+
37
+ function eachColumnForTable(table: TableRef, fn: ColumnFunction) {
38
+ if (table.columns) {
39
+ for (const col of table.columns) {
40
+ fn(table, col)
41
+ }
42
+ }
43
+ if (table.joins) {
44
+ for (const joinedTable of Object.values(table.joins)) {
45
+ eachColumnForTable(joinedTable, fn)
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Recursively iterates over all columns in a query.
52
+ * @param query
53
+ */
54
+ function eachColumn(query: Query, fn: ColumnFunction) {
55
+ eachColumnForTable(query.from, fn)
56
+ }
57
+
58
+
59
+ ////////////////////////////////////////////////////////////////////////////////
60
+ // Server-Side Validation
32
61
  ////////////////////////////////////////////////////////////////////////////////
33
62
 
34
63
  /**
35
64
  * Type for the response to a server-side query validation.
36
65
  */
37
- export type QueryValidation = ApiResponse & {
66
+ export type QueryServerValidation = ApiResponse & {
38
67
  query: Query
39
68
  sql?: string
40
69
  sql_html?: string
@@ -47,8 +76,8 @@ export type QueryValidation = ApiResponse & {
47
76
  * Has the server validate the given query and generate SQL for it.
48
77
  * @param query
49
78
  */
50
- async function validate(query: Query): Promise<QueryValidation> {
51
- return await api.post<QueryValidation>("/data_dive/validate_query.json", {query})
79
+ async function validate(query: Query): Promise<QueryServerValidation> {
80
+ return await api.post<QueryServerValidation>("/data_dive/validate_query.json", {query})
52
81
  }
53
82
 
54
83
 
@@ -258,6 +287,7 @@ export class QueryModelPicker extends TerrierPart<QueryModelPickerState> {
258
287
  ////////////////////////////////////////////////////////////////////////////////
259
288
 
260
289
  const Queries = {
290
+ eachColumn,
261
291
  validate,
262
292
  preview,
263
293
  renderPreview
@@ -1,5 +1,5 @@
1
1
  import {PartTag} from "tuff-core/parts"
2
- import Queries, {Query, QueryResult, QueryValidation} from "./queries"
2
+ import Queries, {Query, QueryResult, QueryServerValidation} from "./queries"
3
3
  import Tables, {FromTableView} from "./tables"
4
4
  import {Logger} from "tuff-core/logging"
5
5
  import QueryForm, {QuerySettings, QuerySettingsColumns} from "./query-form"
@@ -9,6 +9,7 @@ import Html from "tuff-core/html"
9
9
  import ContentPart from "../../terrier/parts/content-part"
10
10
  import {TabContainerPart} from "../../terrier/tabs"
11
11
  import Messages from "tuff-core/messages"
12
+ import Validation, {QueryClientValidation} from "./validation"
12
13
 
13
14
  const log = new Logger("QueryEditor")
14
15
 
@@ -17,7 +18,7 @@ const log = new Logger("QueryEditor")
17
18
  // Keys
18
19
  ////////////////////////////////////////////////////////////////////////////////
19
20
 
20
- const validationKey = Messages.typedKey<QueryValidation>()
21
+ const validationKey = Messages.typedKey<QueryServerValidation>()
21
22
 
22
23
 
23
24
  ////////////////////////////////////////////////////////////////////////////////
@@ -60,9 +61,9 @@ class SettingsPart extends ContentPart<SubEditorState> {
60
61
 
61
62
  class SqlPart extends ContentPart<SubEditorState> {
62
63
 
63
- validation?: QueryValidation
64
+ validation?: QueryServerValidation
64
65
 
65
- setValidation(validation: QueryValidation) {
66
+ setValidation(validation: QueryServerValidation) {
66
67
  this.validation = validation
67
68
  this.dirty()
68
69
  }
@@ -156,11 +157,13 @@ export default class QueryEditor extends ContentPart<QueryEditorState> {
156
157
  settingsPart!: SettingsPart
157
158
  sqlPart!: SqlPart
158
159
  previewPart!: PreviewPart
160
+ clientValidation!: QueryClientValidation
159
161
 
160
162
  updatePreviewKey = Messages.untypedKey()
161
163
 
162
164
  async init() {
163
165
  const query = this.state.query
166
+ this.clientValidation = Validation.validateQuery(query)
164
167
 
165
168
  log.info("Initializing query editor", query)
166
169
 
@@ -180,7 +183,7 @@ export default class QueryEditor extends ContentPart<QueryEditorState> {
180
183
  this.previewPart = this.tabs.upsertTab({key: 'preview', title: 'Preview', icon: 'glyp-table', classes: ['no-padding'], click: {key: this.updatePreviewKey}},
181
184
  PreviewPart, {editor: this, query})
182
185
 
183
- this.tableEditor = this.makePart(FromTableView, {schema: this.state.schema, table: this.state.query.from})
186
+ this.tableEditor = this.makePart(FromTableView, {schema: this.state.schema, queryEditor: this, table: this.state.query.from})
184
187
 
185
188
  this.listenMessage(Tables.updatedKey, m => {
186
189
  log.info(`Table ${m.data.model} updated`, m.data)
@@ -219,10 +222,14 @@ export default class QueryEditor extends ContentPart<QueryEditorState> {
219
222
  }
220
223
 
221
224
  async validate() {
225
+ // server-side validation
222
226
  const res = await Queries.validate(this.state.query)
223
227
  log.info(`Query validated`, res)
224
228
  this.emitMessage(validationKey, res)
225
229
  this.sqlPart.setValidation(res)
230
+
231
+ // client-side validation
232
+ this.clientValidation = Validation.validateQuery(this.state.query)
226
233
  this.dirty()
227
234
  }
228
235
 
@@ -11,6 +11,7 @@ import TerrierFormPart from "../../terrier/parts/terrier-form-part"
11
11
  import DiveEditor from "../dives/dive-editor"
12
12
  import Messages from "tuff-core/messages"
13
13
  import Arrays from "tuff-core/arrays"
14
+ import QueryEditor from "./query-editor"
14
15
 
15
16
  const log = new Logger("Tables")
16
17
 
@@ -48,7 +49,9 @@ const updatedKey = Messages.typedKey<TableRef>()
48
49
  * Only keep one (the last one traversed) per table/column combination.
49
50
  * This means that some filters may clobber others, but I think it will yield
50
51
  * the desired result most of the time.
52
+ * @param schema
51
53
  * @param table
54
+ * @param filters
52
55
  */
53
56
  function computeFilterInputs(schema: SchemaDef, table: TableRef, filters: Record<string, FilterInput>) {
54
57
  for (const f of table.filters || []) {
@@ -67,7 +70,7 @@ function computeFilterInputs(schema: SchemaDef, table: TableRef, filters: Record
67
70
  // View
68
71
  ////////////////////////////////////////////////////////////////////////////////
69
72
 
70
- export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaDef, table: T }> {
73
+ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaDef, queryEditor: QueryEditor, table: T }> {
71
74
 
72
75
  schema!: SchemaDef
73
76
  table!: T
@@ -88,7 +91,7 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
88
91
  this.modelDef = this.schema.models[this.table.model]
89
92
  this.tableName = inflection.titleize(inflection.tableize(this.table.model))
90
93
  this.displayName = this.tableName
91
- this.updatedJoinedViews()
94
+ this.updateJoinedViews()
92
95
 
93
96
  this.onClick(this.editColumnsKey, _ => {
94
97
  log.info(`Edit ${this.displayName} Columns`)
@@ -155,7 +158,7 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
155
158
  log.info(`Creating joined table`, newTable)
156
159
  this.table.joins ||= {}
157
160
  this.table.joins[newTable.belongs_to] = newTable
158
- this.updatedJoinedViews()
161
+ this.updateJoinedViews()
159
162
  }
160
163
  this.app.showModal(JoinedTableEditorModal, {table, belongsTo, callback, parentTable: this.state.table as TableRef})
161
164
  }
@@ -165,9 +168,9 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
165
168
  /**
166
169
  * Re-generates all views for the joined tables.
167
170
  */
168
- updatedJoinedViews() {
171
+ updateJoinedViews() {
169
172
  const states = Object.values(this.table.joins || {}).map(table => {
170
- return {schema: this.schema, table}
173
+ return {schema: this.schema, queryEditor: this.state.queryEditor, table}
171
174
  })
172
175
  this.assignCollection('joined', JoinedTableView, states)
173
176
  for (const part of this.getCollectionParts('joined')) {
@@ -242,6 +245,12 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
242
245
  for (const col of this.table.columns) {
243
246
  section.div('.column.line', line => {
244
247
  Columns.render(line, col)
248
+ if (col.errors?.length) {
249
+ line.class('error')
250
+ for (const error of col.errors) {
251
+ line.div('.error-message').text(error.message)
252
+ }
253
+ }
245
254
  })
246
255
  }
247
256
  }
@@ -0,0 +1,73 @@
1
+ import Queries, {Query} from "./queries"
2
+ import Columns, {ColumnRef} from "./columns"
3
+
4
+ export type ColumnValidationError = {
5
+ message: string
6
+ }
7
+
8
+ export type QueryClientValidation = {
9
+ columns: ColumnValidationError[]
10
+ }
11
+
12
+ ////////////////////////////////////////////////////////////////////////////////
13
+ // Client-Side Validation
14
+ ////////////////////////////////////////////////////////////////////////////////
15
+
16
+ /**
17
+ * Validates that a query will produce valid SQL.
18
+ */
19
+ function validateQuery(query: Query): QueryClientValidation {
20
+
21
+ const validation: QueryClientValidation = {
22
+ columns: []
23
+ }
24
+
25
+ function addColumnError(col: ColumnRef, message: string) {
26
+ const error = {message}
27
+ col.errors ||= []
28
+ col.errors.push(error)
29
+ validation.columns.push(error)
30
+ }
31
+
32
+ const usedNames: Set<string> = new Set<string>()
33
+
34
+ let isGrouped = false
35
+ Queries.eachColumn(query, (table, col) => {
36
+ // clear the errors
37
+ col.errors = undefined
38
+
39
+ // determine if there's a _group by_ in the query
40
+ if (col.grouped) {
41
+ isGrouped = true
42
+ }
43
+
44
+ // each select name should only be used once
45
+ const selectName = Columns.computeSelectName(table, col)
46
+ if (usedNames.has(selectName)) {
47
+ addColumnError(col, `<strong>${selectName}</strong> has already been selected for a different column`)
48
+ }
49
+ usedNames.add(selectName)
50
+ })
51
+
52
+ // if the query is grouped, ensure that all other column refs
53
+ // are either grouped or have an aggregate function
54
+ if (isGrouped) {
55
+ Queries.eachColumn(query, (_, col) => {
56
+ if (!col.grouped && !col.function) {
57
+ addColumnError(col, `<strong>${col.name}</strong> must be grouped or have an aggregate function`)
58
+ }
59
+ })
60
+ }
61
+
62
+ return validation
63
+ }
64
+
65
+
66
+ ////////////////////////////////////////////////////////////////////////////////
67
+ // Export
68
+ ////////////////////////////////////////////////////////////////////////////////
69
+
70
+ const Validation = {
71
+ validateQuery
72
+ }
73
+ export default Validation
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "*"
6
6
  ],
7
- "version": "4.7.3",
7
+ "version": "4.7.5",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/Terrier-Tech/terrier-engine"
@@ -117,6 +117,14 @@ import EditRaw from '../images/icons/edit.svg?raw'
117
117
  // @ts-ignore
118
118
  import EditSrc from '../images/icons/edit.svg'
119
119
  // @ts-ignore
120
+ import ExistingChildRaw from '../images/icons/existing_child.svg?raw'
121
+ // @ts-ignore
122
+ import ExistingChildSrc from '../images/icons/existing_child.svg'
123
+ // @ts-ignore
124
+ import ExistingParentRaw from '../images/icons/existing_parent.svg?raw'
125
+ // @ts-ignore
126
+ import ExistingParentSrc from '../images/icons/existing_parent.svg'
127
+ // @ts-ignore
120
128
  import FeatureRaw from '../images/icons/feature.svg?raw'
121
129
  // @ts-ignore
122
130
  import FeatureSrc from '../images/icons/feature.svg'
@@ -209,6 +217,14 @@ import MinusRaw from '../images/icons/minus.svg?raw'
209
217
  // @ts-ignore
210
218
  import MinusSrc from '../images/icons/minus.svg'
211
219
  // @ts-ignore
220
+ import NewChildRaw from '../images/icons/new_child.svg?raw'
221
+ // @ts-ignore
222
+ import NewChildSrc from '../images/icons/new_child.svg'
223
+ // @ts-ignore
224
+ import NewParentRaw from '../images/icons/new_parent.svg?raw'
225
+ // @ts-ignore
226
+ import NewParentSrc from '../images/icons/new_parent.svg'
227
+ // @ts-ignore
212
228
  import NightRaw from '../images/icons/night.svg?raw'
213
229
  // @ts-ignore
214
230
  import NightSrc from '../images/icons/night.svg'
@@ -470,6 +486,14 @@ export const IconDefs: Record<HubIconName,{ raw: string, src: string }> = {
470
486
  raw: EditRaw,
471
487
  src: EditSrc,
472
488
  },
489
+ "hub-existing_child": {
490
+ raw: ExistingChildRaw,
491
+ src: ExistingChildSrc,
492
+ },
493
+ "hub-existing_parent": {
494
+ raw: ExistingParentRaw,
495
+ src: ExistingParentSrc,
496
+ },
473
497
  "hub-feature": {
474
498
  raw: FeatureRaw,
475
499
  src: FeatureSrc,
@@ -562,6 +586,14 @@ export const IconDefs: Record<HubIconName,{ raw: string, src: string }> = {
562
586
  raw: MinusRaw,
563
587
  src: MinusSrc,
564
588
  },
589
+ "hub-new_child": {
590
+ raw: NewChildRaw,
591
+ src: NewChildSrc,
592
+ },
593
+ "hub-new_parent": {
594
+ raw: NewParentRaw,
595
+ src: NewParentSrc,
596
+ },
565
597
  "hub-night": {
566
598
  raw: NightRaw,
567
599
  src: NightSrc,
@@ -713,7 +745,7 @@ export const IconDefs: Record<HubIconName,{ raw: string, src: string }> = {
713
745
  }
714
746
 
715
747
  const Names = [
716
- 'hub-active', 'hub-admin', 'hub-archive', 'hub-arrow_down', 'hub-arrow_left', 'hub-arrow_right', 'hub-arrow_up', 'hub-assign', 'hub-attachment', 'hub-back', 'hub-badge', 'hub-board', 'hub-branch', 'hub-bug', 'hub-calculator', 'hub-checkmark', 'hub-close', 'hub-clypboard', 'hub-comment', 'hub-complete', 'hub-dashboard', 'hub-data_pull', 'hub-data_update', 'hub-database', 'hub-day', 'hub-delete', 'hub-documentation', 'hub-edit', 'hub-feature', 'hub-flex', 'hub-forward', 'hub-github', 'hub-history', 'hub-home', 'hub-image', 'hub-inbox', 'hub-info', 'hub-internal', 'hub-issue', 'hub-lane', 'hub-lane_asap', 'hub-lane_days', 'hub-lane_hours', 'hub-lane_weeks', 'hub-lanes_board', 'hub-level_complete', 'hub-level_highway', 'hub-level_on_ramp', 'hub-level_parking', 'hub-metrics', 'hub-minus', 'hub-night', 'hub-origin', 'hub-pending', 'hub-plus', 'hub-post', 'hub-posts', 'hub-pr_closed', 'hub-pr_merged', 'hub-pr_open', 'hub-prioritized', 'hub-project', 'hub-question', 'hub-reaction', 'hub-read_mail', 'hub-recent', 'hub-refresh', 'hub-related_posts', 'hub-request', 'hub-settings', 'hub-status', 'hub-step_deploy', 'hub-step_develop', 'hub-step_investigate', 'hub-step_review', 'hub-step_test', 'hub-steps', 'hub-steps_board', 'hub-subscribe', 'hub-support', 'hub-terrier', 'hub-thumbs_up', 'hub-type', 'hub-unprioritized', 'hub-upload', 'hub-user', 'hub-users', 'hub-week'
748
+ 'hub-active', 'hub-admin', 'hub-archive', 'hub-arrow_down', 'hub-arrow_left', 'hub-arrow_right', 'hub-arrow_up', 'hub-assign', 'hub-attachment', 'hub-back', 'hub-badge', 'hub-board', 'hub-branch', 'hub-bug', 'hub-calculator', 'hub-checkmark', 'hub-close', 'hub-clypboard', 'hub-comment', 'hub-complete', 'hub-dashboard', 'hub-data_pull', 'hub-data_update', 'hub-database', 'hub-day', 'hub-delete', 'hub-documentation', 'hub-edit', 'hub-existing_child', 'hub-existing_parent', 'hub-feature', 'hub-flex', 'hub-forward', 'hub-github', 'hub-history', 'hub-home', 'hub-image', 'hub-inbox', 'hub-info', 'hub-internal', 'hub-issue', 'hub-lane', 'hub-lane_asap', 'hub-lane_days', 'hub-lane_hours', 'hub-lane_weeks', 'hub-lanes_board', 'hub-level_complete', 'hub-level_highway', 'hub-level_on_ramp', 'hub-level_parking', 'hub-metrics', 'hub-minus', 'hub-new_child', 'hub-new_parent', 'hub-night', 'hub-origin', 'hub-pending', 'hub-plus', 'hub-post', 'hub-posts', 'hub-pr_closed', 'hub-pr_merged', 'hub-pr_open', 'hub-prioritized', 'hub-project', 'hub-question', 'hub-reaction', 'hub-read_mail', 'hub-recent', 'hub-refresh', 'hub-related_posts', 'hub-request', 'hub-settings', 'hub-status', 'hub-step_deploy', 'hub-step_develop', 'hub-step_investigate', 'hub-step_review', 'hub-step_test', 'hub-steps', 'hub-steps_board', 'hub-subscribe', 'hub-support', 'hub-terrier', 'hub-thumbs_up', 'hub-type', 'hub-unprioritized', 'hub-upload', 'hub-user', 'hub-users', 'hub-week'
717
749
  ] as const
718
750
 
719
751
  export type HubIconName = typeof Names[number]
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="8" cy="24" r="3" fill="currentColor" fill-opacity="0.33" stroke="currentColor" stroke-linejoin="round" stroke-width="2"/><circle cx="16" cy="8" r="3" fill="currentColor" fill-opacity="0.33" stroke="currentColor" stroke-linejoin="round" stroke-width="2"/><path stroke="currentColor" stroke-linejoin="round" stroke-width="2" d="M8 21v-3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v5"/><path fill="currentColor" d="M15 11h2v5h-2zM19.415 23.65l3.826 4.464a1 1 0 0 0 1.518 0l3.826-4.463a1 1 0 0 0-.76-1.651h-7.65a1 1 0 0 0-.76 1.65Z"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="8" cy="24" r="3" fill="currentColor" fill-opacity="0.33" stroke="currentColor" stroke-linejoin="round" stroke-width="2"/><circle cx="24" cy="24" r="3" fill="currentColor" fill-opacity="0.33" stroke="currentColor" stroke-linejoin="round" stroke-width="2"/><path stroke="currentColor" stroke-linejoin="round" stroke-width="2" d="M8 21v-3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v3"/><path fill="currentColor" d="M15 9h2v7h-2z"/><path fill="currentColor" d="m11.415 8.35 3.826-4.464a1 1 0 0 1 1.518 0l3.826 4.463a1 1 0 0 1-.76 1.651h-7.65a1 1 0 0 1-.76-1.65Z"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="8" r="3" fill="currentColor" fill-opacity="0.33" stroke="currentColor" stroke-linejoin="round" stroke-width="2"/><circle cx="8" cy="24" r="3" fill="currentColor" fill-opacity="0.33" stroke="currentColor" stroke-linejoin="round" stroke-width="2"/><circle cx="24" cy="24" r="5" fill="currentColor" fill-opacity="0.33" stroke="currentColor" stroke-linejoin="round" stroke-width="2"/><path stroke="currentColor" stroke-linejoin="round" stroke-width="2" d="M8 21v-3a2 2 0 0 1 2-2h12.023a2 2 0 0 1 2 1.977l.011.977"/><path fill="currentColor" d="M15 11h2v5h-2zM24 21a1 1 0 0 1 1 1v.999L26 23a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0v-1h-1a1 1 0 0 1 0-2h1v-1a1 1 0 0 1 1-1Z"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="8" cy="24" r="3" fill="currentColor" fill-opacity="0.33" stroke="currentColor" stroke-linejoin="round" stroke-width="2"/><circle cx="24" cy="24" r="3" fill="currentColor" fill-opacity="0.33" stroke="currentColor" stroke-linejoin="round" stroke-width="2"/><path stroke="currentColor" stroke-linejoin="round" stroke-width="2" d="M8 21v-3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v3"/><path fill="currentColor" d="M15 13h2v3h-2z"/><circle cx="16" cy="8" r="5" fill="currentColor" fill-opacity="0.33" stroke="currentColor" stroke-linejoin="round" stroke-width="2"/><path fill="currentColor" d="M16 5a1 1 0 0 1 1 1v.999L18 7a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V9h-1a1 1 0 0 1 0-2h1V6a1 1 0 0 1 1-1Z"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="8" cy="24" r="3" fill="#000" fill-opacity=".25" stroke="#000" stroke-linejoin="round" stroke-width="2"/><circle cx="16" cy="8" r="3" fill="#000" fill-opacity=".25" stroke="#000" stroke-linejoin="round" stroke-width="2"/><path stroke="#000" stroke-linejoin="round" stroke-width="2" d="M8 21v-3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v5"/><path fill="#000" d="M15 11h2v5h-2zM19.415 23.65l3.826 4.464a1 1 0 0 0 1.518 0l3.826-4.463a1 1 0 0 0-.76-1.651h-7.65a1 1 0 0 0-.76 1.65Z"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="8" cy="24" r="3" fill="#000" fill-opacity=".25" stroke="#000" stroke-linejoin="round" stroke-width="2"/><circle cx="24" cy="24" r="3" fill="#000" fill-opacity=".25" stroke="#000" stroke-linejoin="round" stroke-width="2"/><path stroke="#000" stroke-linejoin="round" stroke-width="2" d="M8 21v-3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v3"/><path fill="#000" d="M15 9h2v7h-2z"/><path fill="#000" d="m11.415 8.35 3.826-4.464a1 1 0 0 1 1.518 0l3.826 4.463a1 1 0 0 1-.76 1.651h-7.65a1 1 0 0 1-.76-1.65Z"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="8" r="3" fill="#000" fill-opacity=".25" stroke="#000" stroke-linejoin="round" stroke-width="2"/><circle cx="8" cy="24" r="3" fill="#000" fill-opacity=".25" stroke="#000" stroke-linejoin="round" stroke-width="2"/><circle cx="24" cy="24" r="5" fill="#000" fill-opacity=".25" stroke="#000" stroke-linejoin="round" stroke-width="2"/><path stroke="#000" stroke-linejoin="round" stroke-width="2" d="M8 21v-3a2 2 0 0 1 2-2h12.023a2 2 0 0 1 2 1.977l.011.977"/><path fill="#000" d="M15 11h2v5h-2zM24 21a1 1 0 0 1 1 1v.999L26 23a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0v-1h-1a1 1 0 0 1 0-2h1v-1a1 1 0 0 1 1-1Z"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="8" cy="24" r="3" fill="#000" fill-opacity=".25" stroke="#000" stroke-linejoin="round" stroke-width="2"/><circle cx="24" cy="24" r="3" fill="#000" fill-opacity=".25" stroke="#000" stroke-linejoin="round" stroke-width="2"/><path stroke="#000" stroke-linejoin="round" stroke-width="2" d="M8 21v-3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v3"/><path fill="#000" d="M15 13h2v3h-2z"/><circle cx="16" cy="8" r="5" fill="#000" fill-opacity=".25" stroke="#000" stroke-linejoin="round" stroke-width="2"/><path fill="#000" d="M16 5a1 1 0 0 1 1 1v.999L18 7a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V9h-1a1 1 0 0 1 0-2h1V6a1 1 0 0 1 1-1Z"/></g></svg>
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3
+ <title>icon-existing_child</title>
4
+ <g id="icon-existing_child" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5
+ <circle id="Oval-Copy" stroke="#000000" stroke-width="2" fill-opacity="0.25" fill="#000000" stroke-linejoin="round" cx="8" cy="24" r="3"></circle>
6
+ <circle id="Oval-Copy-2" stroke="#000000" stroke-width="2" fill-opacity="0.25" fill="#000000" stroke-linejoin="round" cx="16" cy="8" r="3"></circle>
7
+ <path d="M8,21 L8,18 C8,16.8954305 8.8954305,16 10,16 L22,16 C23.1045695,16 24,16.8954305 24,18 L24,23 L24,23" id="Path-100" stroke="#000000" stroke-width="2" stroke-linejoin="round"></path>
8
+ <rect id="Rectangle" fill="#000000" x="15" y="11" width="2" height="5"></rect>
9
+ <path d="M22.1507914,20.914964 L26.6142006,24.7407434 C27.0335265,25.1001655 27.082088,25.7314655 26.7226659,26.1507914 C26.6893412,26.1896702 26.6530794,26.2259319 26.6142006,26.2592566 L22.1507914,30.085036 C21.7314655,30.4444581 21.1001655,30.3958966 20.7407434,29.9765707 C20.5853922,29.7953276 20.5,29.5644906 20.5,29.3257794 L20.5,21.6742206 C20.5,21.1219359 20.9477153,20.6742206 21.5,20.6742206 C21.7387113,20.6742206 21.9695483,20.7596128 22.1507914,20.914964 Z" id="Path-101" fill="#000000" transform="translate(24, 25.5) scale(1, -1) rotate(-90) translate(-24, -25.5)"></path>
10
+ </g>
11
+ </svg>
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3
+ <title>icon-existing_parent</title>
4
+ <g id="icon-existing_parent" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5
+ <circle id="Oval-Copy" stroke="#000000" stroke-width="2" fill-opacity="0.25" fill="#000000" stroke-linejoin="round" cx="8" cy="24" r="3"></circle>
6
+ <circle id="Oval-Copy-2" stroke="#000000" stroke-width="2" fill-opacity="0.25" fill="#000000" stroke-linejoin="round" cx="24" cy="24" r="3"></circle>
7
+ <path d="M8,21 L8,18 C8,16.8954305 8.8954305,16 10,16 L22,16 C23.1045695,16 24,16.8954305 24,18 L24,21 L24,21" id="Path-100" stroke="#000000" stroke-width="2" stroke-linejoin="round"></path>
8
+ <rect id="Rectangle" fill="#000000" x="15" y="9" width="2" height="7"></rect>
9
+ <path d="M14.1507914,1.91496403 L18.6142006,5.7407434 C19.0335265,6.10016555 19.082088,6.73146553 18.7226659,7.15079137 C18.6893412,7.18967018 18.6530794,7.22593191 18.6142006,7.2592566 L14.1507914,11.085036 C13.7314655,11.4444581 13.1001655,11.3958966 12.7407434,10.9765707 C12.5853922,10.7953276 12.5,10.5644906 12.5,10.3257794 L12.5,2.67422064 C12.5,2.12193589 12.9477153,1.67422064 13.5,1.67422064 C13.7387113,1.67422064 13.9695483,1.7596128 14.1507914,1.91496403 Z" id="Path-101" fill="#000000" transform="translate(16, 6.5) rotate(-90) translate(-16, -6.5)"></path>
10
+ </g>
11
+ </svg>
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3
+ <title>icon-new-child</title>
4
+ <g id="icon-new-child" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5
+ <circle id="Oval" stroke="#000000" stroke-width="2" fill-opacity="0.25" fill="#000000" stroke-linejoin="round" cx="16" cy="8" r="3"></circle>
6
+ <circle id="Oval-Copy" stroke="#000000" stroke-width="2" fill-opacity="0.25" fill="#000000" stroke-linejoin="round" cx="8" cy="24" r="3"></circle>
7
+ <circle id="Oval-Copy-2" stroke="#000000" stroke-width="2" fill-opacity="0.25" fill="#000000" stroke-linejoin="round" cx="24" cy="24" r="5"></circle>
8
+ <path d="M8,21 L8,18 C8,16.8954305 8.8954305,16 10,16 L22.0231969,16 C23.1186657,16 24.0102814,16.8812743 24.0230608,17.9766686 L24.0344614,18.9538745 L24.0344614,18.9538745" id="Path-100" stroke="#000000" stroke-width="2" stroke-linejoin="round"></path>
9
+ <rect id="Rectangle" fill="#000000" x="15" y="11" width="2" height="5"></rect>
10
+ <path d="M24,21 C24.5522847,21 25,21.4477153 25,22 L25,22.999 L26,23 C26.5522847,23 27,23.4477153 27,24 C27,24.5522847 26.5522847,25 26,25 L25,25 L25,26 C25,26.5522847 24.5522847,27 24,27 C23.4477153,27 23,26.5522847 23,26 L23,25 L22,25 C21.4477153,25 21,24.5522847 21,24 C21,23.4477153 21.4477153,23 22,23 L23,23 L23,22 C23,21.4477153 23.4477153,21 24,21 Z" id="Combined-Shape" fill="#000000"></path>
11
+ </g>
12
+ </svg>
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3
+ <title>icon-new_parent</title>
4
+ <g id="icon-new_parent" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5
+ <circle id="Oval-Copy" stroke="#000000" stroke-width="2" fill-opacity="0.25" fill="#000000" stroke-linejoin="round" cx="8" cy="24" r="3"></circle>
6
+ <circle id="Oval-Copy-2" stroke="#000000" stroke-width="2" fill-opacity="0.25" fill="#000000" stroke-linejoin="round" cx="24" cy="24" r="3"></circle>
7
+ <path d="M8,21 L8,18 C8,16.8954305 8.8954305,16 10,16 L22,16 C23.1045695,16 24,16.8954305 24,18 L24,21 L24,21" id="Path-100" stroke="#000000" stroke-width="2" stroke-linejoin="round"></path>
8
+ <rect id="Rectangle" fill="#000000" x="15" y="13" width="2" height="3"></rect>
9
+ <circle id="Oval-Copy-2" stroke="#000000" stroke-width="2" fill-opacity="0.25" fill="#000000" stroke-linejoin="round" cx="16" cy="8" r="5"></circle>
10
+ <path d="M16,5 C16.5522847,5 17,5.44771525 17,6 L17,6.999 L18,7 C18.5522847,7 19,7.44771525 19,8 C19,8.55228475 18.5522847,9 18,9 L17,9 L17,10 C17,10.5522847 16.5522847,11 16,11 C15.4477153,11 15,10.5522847 15,10 L15,9 L14,9 C13.4477153,9 13,8.55228475 13,8 C13,7.44771525 13.4477153,7 14,7 L15,7 L15,6 C15,5.44771525 15.4477153,5 16,5 Z" id="Combined-Shape" fill="#000000"></path>
11
+ </g>
12
+ </svg>