sqlite-hub 0.9.1 → 0.9.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.
- package/.github/workflows/ci.yml +36 -0
- package/README.md +2 -2
- package/bin/sqlite-hub.js +1 -1
- package/frontend/index.html +3 -158
- package/frontend/js/app.js +231 -5
- package/frontend/js/components/connectionCard.js +62 -87
- package/frontend/js/components/emptyState.js +20 -23
- package/frontend/js/components/modal.js +158 -199
- package/frontend/js/components/pageHeader.js +1 -1
- package/frontend/js/components/queryEditor.js +16 -30
- package/frontend/js/components/queryHistoryDetail.js +93 -164
- package/frontend/js/components/queryHistoryPanel.js +90 -100
- package/frontend/js/components/queryResults.js +3 -1
- package/frontend/js/components/rowEditorPanel.js +76 -56
- package/frontend/js/components/tableDesignerEditor.js +91 -116
- package/frontend/js/lib/queryChartOptions.js +5 -1
- package/frontend/js/lib/queryCharts.js +50 -2
- package/frontend/js/store.js +161 -23
- package/frontend/js/utils/tableDesigner.js +8 -2
- package/frontend/js/views/charts.js +126 -73
- package/frontend/js/views/data.js +147 -133
- package/frontend/js/views/editor.js +1 -0
- package/frontend/js/views/mediaTagging.js +131 -164
- package/frontend/js/views/structure.js +52 -48
- package/frontend/styles/base.css +57 -13
- package/frontend/styles/components.css +166 -96
- package/frontend/styles/layout.css +1 -1
- package/frontend/styles/structure-graph.css +11 -11
- package/frontend/styles/tailwind.css +81 -0
- package/frontend/styles/tailwind.generated.css +1 -0
- package/frontend/styles/tokens.css +50 -7
- package/frontend/styles/utilities.css +21 -0
- package/frontend/styles/views.css +94 -86
- package/package.json +16 -3
- package/server/routes/mediaTagging.js +2 -10
- package/server/routes/sql.js +35 -10
- package/server/server.js +24 -0
- package/server/services/sqlite/dataBrowserService.js +25 -5
- package/server/services/sqlite/exportService.js +4 -2
- package/server/services/sqlite/introspection.js +2 -2
- package/server/services/sqlite/mediaTaggingService.js +166 -53
- package/server/services/sqlite/structureService.js +2 -2
- package/server/services/sqlite/tableDesigner/sql.js +19 -3
- package/server/services/storage/appStateStore.js +227 -87
- package/server/services/storage/queryHistoryChartUtils.js +19 -2
- package/server/utils/appPaths.js +55 -19
- package/server/utils/fileValidation.js +94 -8
- package/tailwind.config.cjs +73 -0
- package/tests/security-paths.test.js +84 -0
- package/tests/sql-identifier-safety.test.js +66 -0
- package/.npmingnore +0 -4
- package/changelog.md +0 -77
- package/docs/DESIGN_GUIDELINES.md +0 -36
- package/scripts/publish_brew.sh +0 -466
- package/scripts/publish_npm.sh +0 -241
- package/shortkeys.md +0 -5
|
@@ -50,15 +50,7 @@ function renderReadonlyField(label, value) {
|
|
|
50
50
|
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
|
|
51
51
|
<div class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
52
52
|
<span>${escapeHtml(displayLabel)}</span>
|
|
53
|
-
${badges
|
|
54
|
-
.map(
|
|
55
|
-
(badge) => `
|
|
56
|
-
<span class="border border-outline-variant/20 bg-surface-container px-2 py-1 text-[9px] text-primary-container">
|
|
57
|
-
${escapeHtml(badge)}
|
|
58
|
-
</span>
|
|
59
|
-
`
|
|
60
|
-
)
|
|
61
|
-
.join("")}
|
|
53
|
+
${badges.map((badge) => renderFieldBadge(badge)).join("")}
|
|
62
54
|
</div>
|
|
63
55
|
${
|
|
64
56
|
jsonPreview
|
|
@@ -72,37 +64,68 @@ function renderReadonlyField(label, value) {
|
|
|
72
64
|
function renderEditableField(field) {
|
|
73
65
|
const badges = Array.isArray(field.badges) ? field.badges : [];
|
|
74
66
|
const jsonPreview = getJsonPreview(field.value);
|
|
67
|
+
const inputType = field.inputType === "number" ? "number" : "text";
|
|
68
|
+
const numberStep = field.numberStep === "1" ? "1" : "any";
|
|
75
69
|
|
|
76
70
|
return `
|
|
77
71
|
<label class="block space-y-2">
|
|
78
72
|
<span class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
79
73
|
<span>${escapeHtml(field.label ?? field.name)}</span>
|
|
80
|
-
${badges
|
|
81
|
-
.map(
|
|
82
|
-
(badge) => `
|
|
83
|
-
<span class="border border-outline-variant/20 bg-surface-container px-2 py-1 text-[9px] text-primary-container">
|
|
84
|
-
${escapeHtml(badge)}
|
|
85
|
-
</span>
|
|
86
|
-
`
|
|
87
|
-
)
|
|
88
|
-
.join("")}
|
|
74
|
+
${badges.map((badge) => renderFieldBadge(badge)).join("")}
|
|
89
75
|
</span>
|
|
90
76
|
${
|
|
91
77
|
jsonPreview
|
|
92
78
|
? renderJsonViewer(jsonPreview, "JSON Preview")
|
|
93
79
|
: ""
|
|
94
80
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
81
|
+
${
|
|
82
|
+
inputType === "number" && !jsonPreview
|
|
83
|
+
? `
|
|
84
|
+
<input
|
|
85
|
+
class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
86
|
+
name="field:${escapeHtml(field.name)}"
|
|
87
|
+
step="${escapeHtml(numberStep)}"
|
|
88
|
+
type="number"
|
|
89
|
+
value="${escapeHtml(field.value ?? "")}"
|
|
90
|
+
/>
|
|
91
|
+
`
|
|
92
|
+
: `
|
|
93
|
+
<textarea
|
|
94
|
+
class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container ${
|
|
95
|
+
jsonPreview ? "min-h-[14rem] font-mono leading-6" : "min-h-[56px]"
|
|
96
|
+
}"
|
|
97
|
+
name="field:${escapeHtml(field.name)}"
|
|
98
|
+
spellcheck="false"
|
|
99
|
+
>${escapeHtml(field.value ?? "")}</textarea>
|
|
100
|
+
`
|
|
101
|
+
}
|
|
102
102
|
</label>
|
|
103
103
|
`;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
function getFieldBadgeClassName(tone) {
|
|
107
|
+
if (tone === "primary-key") {
|
|
108
|
+
return "border-primary-container/35 bg-primary-container/15 text-primary-container";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (tone === "foreign-key") {
|
|
112
|
+
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return "border-outline-variant/20 bg-surface-container text-on-surface-variant";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function renderFieldBadge(badge) {
|
|
119
|
+
const label = typeof badge === "object" ? badge.label : badge;
|
|
120
|
+
const tone = typeof badge === "object" ? badge.tone : "";
|
|
121
|
+
|
|
122
|
+
return `
|
|
123
|
+
<span class="border px-2 py-1 text-[9px] ${getFieldBadgeClassName(tone)}">
|
|
124
|
+
${escapeHtml(label)}
|
|
125
|
+
</span>
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
|
|
106
129
|
export function renderRowEditorPanel({
|
|
107
130
|
title,
|
|
108
131
|
sectionLabel = "Row Editor",
|
|
@@ -126,43 +149,40 @@ export function renderRowEditorPanel({
|
|
|
126
149
|
}) {
|
|
127
150
|
const canSubmit = !disabledMessage && editableFields.length > 0;
|
|
128
151
|
const canDelete = !disabledMessage && deleteEnabled;
|
|
129
|
-
const formId =
|
|
152
|
+
const formId = [formName, "-panel-form"].join("");
|
|
130
153
|
const headerActions = [
|
|
131
154
|
reloadAction
|
|
132
|
-
?
|
|
133
|
-
<button
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
>
|
|
138
|
-
Reload
|
|
139
|
-
</button>
|
|
140
|
-
`
|
|
155
|
+
? [
|
|
156
|
+
'<button class="standard-button" data-action="',
|
|
157
|
+
escapeHtml(reloadAction),
|
|
158
|
+
'" type="button">Reload</button>',
|
|
159
|
+
].join("")
|
|
141
160
|
: "",
|
|
142
161
|
canSubmit
|
|
143
|
-
?
|
|
144
|
-
<button
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
`
|
|
162
|
+
? [
|
|
163
|
+
'<button class="standard-button" form="',
|
|
164
|
+
escapeHtml(formId),
|
|
165
|
+
'" type="submit" ',
|
|
166
|
+
saving || deleting ? "disabled" : "",
|
|
167
|
+
">",
|
|
168
|
+
escapeHtml(saving ? "Saving..." : submitLabel),
|
|
169
|
+
"</button>",
|
|
170
|
+
].join("")
|
|
153
171
|
: "",
|
|
154
172
|
canDelete
|
|
155
|
-
?
|
|
156
|
-
<button
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
173
|
+
? [
|
|
174
|
+
'<button class="delete-button" data-action="',
|
|
175
|
+
escapeHtml(deleteAction),
|
|
176
|
+
'" ',
|
|
177
|
+
deleteRowIndex === null
|
|
178
|
+
? ""
|
|
179
|
+
: ['data-row-index="', escapeHtml(String(deleteRowIndex)), '"'].join(""),
|
|
180
|
+
' type="button" ',
|
|
181
|
+
saving || deleting ? "disabled" : "",
|
|
182
|
+
">",
|
|
183
|
+
escapeHtml(deleting ? "Deleting..." : deleteLabel),
|
|
184
|
+
"</button>",
|
|
185
|
+
].join("")
|
|
166
186
|
: "",
|
|
167
187
|
]
|
|
168
188
|
.filter(Boolean)
|
|
@@ -57,123 +57,98 @@ function getReferencedColumns(draft, catalogTables, referencesTable) {
|
|
|
57
57
|
|
|
58
58
|
function renderColumnRow(column, draft, catalogTables) {
|
|
59
59
|
const referenceColumns = getReferencedColumns(draft, catalogTables, column.referencesTable);
|
|
60
|
+
const typeOptions = draft.supportedTypes
|
|
61
|
+
.map((type) =>
|
|
62
|
+
[
|
|
63
|
+
'<option value="',
|
|
64
|
+
escapeHtml(type),
|
|
65
|
+
'" ',
|
|
66
|
+
type === column.type ? "selected" : "",
|
|
67
|
+
">",
|
|
68
|
+
escapeHtml(type),
|
|
69
|
+
"</option>",
|
|
70
|
+
].join("")
|
|
71
|
+
)
|
|
72
|
+
.join("");
|
|
73
|
+
const referenceTableOptions = catalogTables
|
|
74
|
+
.map((table) =>
|
|
75
|
+
[
|
|
76
|
+
'<option value="',
|
|
77
|
+
escapeHtml(table.name),
|
|
78
|
+
'" ',
|
|
79
|
+
table.name === column.referencesTable ? "selected" : "",
|
|
80
|
+
">",
|
|
81
|
+
escapeHtml(table.name),
|
|
82
|
+
"</option>",
|
|
83
|
+
].join("")
|
|
84
|
+
)
|
|
85
|
+
.join("");
|
|
86
|
+
const referenceColumnOptions = referenceColumns
|
|
87
|
+
.map((name) =>
|
|
88
|
+
[
|
|
89
|
+
'<option value="',
|
|
90
|
+
escapeHtml(name),
|
|
91
|
+
'" ',
|
|
92
|
+
name === column.referencesColumn ? "selected" : "",
|
|
93
|
+
">",
|
|
94
|
+
escapeHtml(name),
|
|
95
|
+
"</option>",
|
|
96
|
+
].join("")
|
|
97
|
+
)
|
|
98
|
+
.join("");
|
|
60
99
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
data-field="primaryKey"
|
|
114
|
-
type="checkbox"
|
|
115
|
-
${column.primaryKey ? "checked" : ""}
|
|
116
|
-
/>
|
|
117
|
-
<span>PK</span>
|
|
118
|
-
</label>
|
|
119
|
-
<input
|
|
120
|
-
class="table-designer-field"
|
|
121
|
-
data-bind="table-designer-column-field"
|
|
122
|
-
data-column-id="${escapeHtml(column.id)}"
|
|
123
|
-
data-field="defaultValue"
|
|
124
|
-
placeholder="SQL default"
|
|
125
|
-
spellcheck="false"
|
|
126
|
-
type="text"
|
|
127
|
-
value="${escapeHtml(column.defaultValue)}"
|
|
128
|
-
/>
|
|
129
|
-
<select
|
|
130
|
-
class="table-designer-field"
|
|
131
|
-
data-bind="table-designer-column-field"
|
|
132
|
-
data-column-id="${escapeHtml(column.id)}"
|
|
133
|
-
data-field="referencesTable"
|
|
134
|
-
>
|
|
135
|
-
<option value="">No FK table</option>
|
|
136
|
-
${catalogTables
|
|
137
|
-
.map(
|
|
138
|
-
(table) => `
|
|
139
|
-
<option
|
|
140
|
-
value="${escapeHtml(table.name)}"
|
|
141
|
-
${table.name === column.referencesTable ? "selected" : ""}
|
|
142
|
-
>
|
|
143
|
-
${escapeHtml(table.name)}
|
|
144
|
-
</option>
|
|
145
|
-
`
|
|
146
|
-
)
|
|
147
|
-
.join("")}
|
|
148
|
-
</select>
|
|
149
|
-
<select
|
|
150
|
-
class="table-designer-field"
|
|
151
|
-
data-bind="table-designer-column-field"
|
|
152
|
-
data-column-id="${escapeHtml(column.id)}"
|
|
153
|
-
data-field="referencesColumn"
|
|
154
|
-
>
|
|
155
|
-
<option value="">No FK column</option>
|
|
156
|
-
${referenceColumns
|
|
157
|
-
.map(
|
|
158
|
-
(name) => `
|
|
159
|
-
<option value="${escapeHtml(name)}" ${name === column.referencesColumn ? "selected" : ""}>
|
|
160
|
-
${escapeHtml(name)}
|
|
161
|
-
</option>
|
|
162
|
-
`
|
|
163
|
-
)
|
|
164
|
-
.join("")}
|
|
165
|
-
</select>
|
|
166
|
-
<button
|
|
167
|
-
class="delete-button"
|
|
168
|
-
data-action="remove-table-designer-column"
|
|
169
|
-
data-column-id="${escapeHtml(column.id)}"
|
|
170
|
-
type="button"
|
|
171
|
-
>
|
|
172
|
-
<span class="material-symbols-outlined text-base">delete</span>
|
|
173
|
-
<span>Remove</span>
|
|
174
|
-
</button>
|
|
175
|
-
</div>
|
|
176
|
-
`;
|
|
100
|
+
const columnId = escapeHtml(column.id);
|
|
101
|
+
|
|
102
|
+
return [
|
|
103
|
+
'<div class="table-designer-grid__row">',
|
|
104
|
+
'<input class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
|
|
105
|
+
columnId,
|
|
106
|
+
'" data-field="name" placeholder="column_name" spellcheck="false" type="text" value="',
|
|
107
|
+
escapeHtml(column.name),
|
|
108
|
+
'" />',
|
|
109
|
+
'<select class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
|
|
110
|
+
columnId,
|
|
111
|
+
'" data-field="type">',
|
|
112
|
+
typeOptions,
|
|
113
|
+
"</select>",
|
|
114
|
+
'<label class="standard-checkbox table-designer-check table-designer-checkbox-override">',
|
|
115
|
+
'<input data-bind="table-designer-column-flag" data-column-id="',
|
|
116
|
+
columnId,
|
|
117
|
+
'" data-field="notNull" type="checkbox" ',
|
|
118
|
+
column.notNull ? "checked" : "",
|
|
119
|
+
' /><span>Not null</span></label>',
|
|
120
|
+
'<label class="standard-checkbox table-designer-check table-designer-checkbox-override">',
|
|
121
|
+
'<input data-bind="table-designer-column-flag" data-column-id="',
|
|
122
|
+
columnId,
|
|
123
|
+
'" data-field="unique" type="checkbox" ',
|
|
124
|
+
column.unique ? "checked" : "",
|
|
125
|
+
' /><span>Unique</span></label>',
|
|
126
|
+
'<label class="standard-checkbox table-designer-check table-designer-checkbox-override">',
|
|
127
|
+
'<input data-bind="table-designer-column-flag" data-column-id="',
|
|
128
|
+
columnId,
|
|
129
|
+
'" data-field="primaryKey" type="checkbox" ',
|
|
130
|
+
column.primaryKey ? "checked" : "",
|
|
131
|
+
' /><span>PK</span></label>',
|
|
132
|
+
'<input class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
|
|
133
|
+
columnId,
|
|
134
|
+
'" data-field="defaultValue" placeholder="SQL default" spellcheck="false" type="text" value="',
|
|
135
|
+
escapeHtml(column.defaultValue),
|
|
136
|
+
'" />',
|
|
137
|
+
'<select class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
|
|
138
|
+
columnId,
|
|
139
|
+
'" data-field="referencesTable"><option value="">No FK table</option>',
|
|
140
|
+
referenceTableOptions,
|
|
141
|
+
"</select>",
|
|
142
|
+
'<select class="table-designer-field" data-bind="table-designer-column-field" data-column-id="',
|
|
143
|
+
columnId,
|
|
144
|
+
'" data-field="referencesColumn"><option value="">No FK column</option>',
|
|
145
|
+
referenceColumnOptions,
|
|
146
|
+
"</select>",
|
|
147
|
+
'<button class="delete-button" data-action="remove-table-designer-column" data-column-id="',
|
|
148
|
+
columnId,
|
|
149
|
+
'" type="button"><span class="material-symbols-outlined text-base">delete</span><span>Remove</span></button>',
|
|
150
|
+
"</div>",
|
|
151
|
+
].join("");
|
|
177
152
|
}
|
|
178
153
|
|
|
179
154
|
function renderColumnGrid(draft, catalogTables) {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
formatQueryChartAxisValue,
|
|
6
6
|
getAnalysisColumn,
|
|
7
7
|
sortQueryChartRows,
|
|
8
|
+
sortQueryChartRowsByNumericColumn,
|
|
8
9
|
} from "./queryCharts.js";
|
|
9
10
|
|
|
10
11
|
const CHART_PALETTE = ["#FCE300", "#2DFAFF", "#FFB4AB", "#CDC7AB", "#7DD3FC", "#86EFAC"];
|
|
@@ -55,7 +56,10 @@ function buildLineLabelConfig(enabled) {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export function buildBarChartOption(chart, rows) {
|
|
58
|
-
const sortedRows =
|
|
59
|
+
const sortedRows =
|
|
60
|
+
chart.config.sort_by === "y"
|
|
61
|
+
? sortQueryChartRowsByNumericColumn(rows, chart.config.y_column, chart.config.sort_direction)
|
|
62
|
+
: sortQueryChartRows(rows, chart.config.x_column, chart.config.sort_direction);
|
|
59
63
|
|
|
60
64
|
return {
|
|
61
65
|
...buildCommonOption(),
|
|
@@ -312,7 +312,43 @@ function getFirstColumn(columns, predicate) {
|
|
|
312
312
|
|
|
313
313
|
function getNextNumericColumn(analysis, excludedNames = []) {
|
|
314
314
|
const excluded = new Set(excludedNames);
|
|
315
|
-
|
|
315
|
+
const candidates = (analysis?.numberColumns ?? []).filter((column) => !excluded.has(column.name));
|
|
316
|
+
|
|
317
|
+
if (!candidates.length) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const scoredCandidates = candidates
|
|
322
|
+
.map((column, index) => ({
|
|
323
|
+
column,
|
|
324
|
+
index,
|
|
325
|
+
score: getNumericMeasureScore(column.name),
|
|
326
|
+
}))
|
|
327
|
+
.sort((left, right) => right.score - left.score || left.index - right.index);
|
|
328
|
+
|
|
329
|
+
return scoredCandidates[0]?.column ?? null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getNumericMeasureScore(columnName = "") {
|
|
333
|
+
const normalized = String(columnName ?? "")
|
|
334
|
+
.trim()
|
|
335
|
+
.toLowerCase();
|
|
336
|
+
|
|
337
|
+
let score = 0;
|
|
338
|
+
|
|
339
|
+
if (
|
|
340
|
+
/(count|total|sum|avg|average|amount|score|size|value|price|cost|rate|percent|pct|percentage|number)/.test(
|
|
341
|
+
normalized
|
|
342
|
+
)
|
|
343
|
+
) {
|
|
344
|
+
score += 20;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (/(^|_)(id|uuid|pk|key)$/.test(normalized) || /(^|_)id$/.test(normalized)) {
|
|
348
|
+
score -= 40;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return score;
|
|
316
352
|
}
|
|
317
353
|
|
|
318
354
|
function getPrimaryDimensionColumn(analysis) {
|
|
@@ -406,7 +442,8 @@ export function buildSuggestedChartConfig(chartType, analysis) {
|
|
|
406
442
|
y_column: firstNumeric?.name ?? "",
|
|
407
443
|
show_legend: true,
|
|
408
444
|
show_labels: false,
|
|
409
|
-
|
|
445
|
+
sort_by: "y",
|
|
446
|
+
sort_direction: "desc",
|
|
410
447
|
};
|
|
411
448
|
}
|
|
412
449
|
}
|
|
@@ -524,6 +561,17 @@ export function sortQueryChartRows(rows, columnName, direction = "asc") {
|
|
|
524
561
|
);
|
|
525
562
|
}
|
|
526
563
|
|
|
564
|
+
export function sortQueryChartRowsByNumericColumn(rows, columnName, direction = "asc") {
|
|
565
|
+
const multiplier = direction === "desc" ? -1 : 1;
|
|
566
|
+
|
|
567
|
+
return [...(rows ?? [])].sort((left, right) => {
|
|
568
|
+
const leftValue = coerceNumericChartValue(left?.[columnName]);
|
|
569
|
+
const rightValue = coerceNumericChartValue(right?.[columnName]);
|
|
570
|
+
|
|
571
|
+
return compareValues(leftValue, rightValue) * multiplier;
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
527
575
|
export function formatQueryChartAxisValue(value) {
|
|
528
576
|
if (value === null || value === undefined) {
|
|
529
577
|
return "NULL";
|