mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 13:02:07 +03:00
Visualise excelwork book sheets, db tables/schemas and add node adding gets to query them (#10362)
changes to current table (and db table) on index click: - node added on double click - tooltip added - cursor changes to pointer - styled as a link ![7473-table-w-links](https://github.com/enso-org/enso/assets/170310417/5e60c177-3f83-4db7-be86-fa8a9d493204) Row table types: Added on click to show value ![7473-row-links](https://github.com/enso-org/enso/assets/170310417/82f878ea-420d-4308-99bf-c77a6340a8c3) Excel Workbook connections - show sheet names in column - sheet names are clickable that add a 'read' node for the corresponding sheet - Shown in table with header title: Sheets ![7473-excel-links-sheets](https://github.com/enso-org/enso/assets/170310417/748f524e-5cca-4c20-b458-132af9a57ec1) SQLite Connection: - shows available schemas in column - schemas are clickable that add a 'query' node for the corresponding schema/table ![7473-sql-lite-links](https://github.com/enso-org/enso/assets/170310417/21a8006f-c462-4128-874d-05380d3bab00) Postgres Connection: - shows available tables in a column - tables are clickable and add a 'query' node for the corresponding table ![7473-postgres-links](https://github.com/enso-org/enso/assets/170310417/66b134ff-80fd-4995-b445-505919e25cfa) JS_Object - style keys as links ![7473-json-links](https://github.com/enso-org/enso/assets/170310417/9ec17c85-c1f4-42ab-b40a-06d6bc29d54f)
This commit is contained in:
parent
4bb82fae4a
commit
ee39fd7f53
@ -72,6 +72,11 @@ function entryTitle(key: string) {
|
||||
content: ',';
|
||||
}
|
||||
.key {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.viewonly .key {
|
||||
color: darkred;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
@ -17,7 +17,7 @@ import type {
|
||||
ColumnResizedEvent,
|
||||
ICellRendererParams,
|
||||
} from 'ag-grid-community'
|
||||
import type { ColDef, GridOptions, HeaderValueGetterParams } from 'ag-grid-enterprise'
|
||||
import type { ColDef, GridOptions } from 'ag-grid-enterprise'
|
||||
import {
|
||||
computed,
|
||||
onMounted,
|
||||
@ -39,7 +39,7 @@ export const defaultPreprocessor = [
|
||||
'1000',
|
||||
] as const
|
||||
|
||||
type Data = Error | Matrix | ObjectMatrix | UnknownTable
|
||||
type Data = Error | Matrix | ObjectMatrix | UnknownTable | Excel_Workbook
|
||||
|
||||
interface Error {
|
||||
type: undefined
|
||||
@ -60,6 +60,14 @@ interface Matrix {
|
||||
value_type: ValueType[]
|
||||
}
|
||||
|
||||
interface Excel_Workbook {
|
||||
type: 'Excel_Workbook'
|
||||
column_count: number
|
||||
all_rows_count: number
|
||||
sheet_names: string[]
|
||||
json: unknown[][]
|
||||
}
|
||||
|
||||
interface ObjectMatrix {
|
||||
type: 'Object_Matrix'
|
||||
column_count: number
|
||||
@ -79,6 +87,7 @@ interface UnknownTable {
|
||||
data: unknown[][] | undefined
|
||||
value_type: ValueType[]
|
||||
has_index_col: boolean | undefined
|
||||
links: string[] | undefined
|
||||
}
|
||||
|
||||
declare module 'ag-grid-enterprise' {
|
||||
@ -105,8 +114,15 @@ const config = useVisualizationConfig()
|
||||
|
||||
const INDEX_FIELD_NAME = '#'
|
||||
const TABLE_NODE_TYPE = 'Standard.Table.Table.Table'
|
||||
const DB_TABLE_NODE_TYPE = 'Standard.Database.DB_Table.DB_Table'
|
||||
const VECTOR_NODE_TYPE = 'Standard.Base.Data.Vector.Vector'
|
||||
const COLUMN_NODE_TYPE = 'Standard.Table.Column.Column'
|
||||
const EXCEL_WORKBOOK_NODE_TYPE = 'Standard.Table.Excel.Excel_Workbook.Excel_Workbook'
|
||||
const ROW_NODE_TYPE = 'Standard.Table.Row.Row'
|
||||
const SQLITE_CONNECTIONS_NODE_TYPE =
|
||||
'Standard.Database.Internal.SQLite.SQLite_Connection.SQLite_Connection'
|
||||
const POSTGRES_CONNECTIONS_NODE_TYPE =
|
||||
'Standard.Database.Internal.Postgres.Postgres_Connection.Postgres_Connection'
|
||||
|
||||
const rowLimit = ref(0)
|
||||
const page = ref(0)
|
||||
@ -123,7 +139,6 @@ const defaultColDef = {
|
||||
filter: true,
|
||||
resizable: true,
|
||||
minWidth: 25,
|
||||
headerValueGetter: (params: HeaderValueGetterParams) => params.colDef.field,
|
||||
cellRenderer: cellRenderer,
|
||||
cellClass: cellClass,
|
||||
}
|
||||
@ -158,6 +173,48 @@ const selectableRowLimits = computed(() => {
|
||||
})
|
||||
const wasAutomaticallyAutosized = ref(false)
|
||||
|
||||
const newNodeSelectorValues = computed(() => {
|
||||
let selector
|
||||
let identifierAction
|
||||
let tooltipValue
|
||||
let headerName
|
||||
switch (config.nodeType) {
|
||||
case COLUMN_NODE_TYPE:
|
||||
case VECTOR_NODE_TYPE:
|
||||
selector = INDEX_FIELD_NAME
|
||||
identifierAction = 'at'
|
||||
tooltipValue = 'value'
|
||||
break
|
||||
case ROW_NODE_TYPE:
|
||||
selector = 'column'
|
||||
identifierAction = 'at'
|
||||
tooltipValue = 'value'
|
||||
break
|
||||
case EXCEL_WORKBOOK_NODE_TYPE:
|
||||
selector = 'Value'
|
||||
identifierAction = 'read'
|
||||
tooltipValue = 'sheet'
|
||||
headerName = 'Sheets'
|
||||
break
|
||||
case SQLITE_CONNECTIONS_NODE_TYPE:
|
||||
case POSTGRES_CONNECTIONS_NODE_TYPE:
|
||||
selector = 'Value'
|
||||
identifierAction = 'query'
|
||||
tooltipValue = 'table'
|
||||
headerName = 'Tables'
|
||||
break
|
||||
case TABLE_NODE_TYPE:
|
||||
case DB_TABLE_NODE_TYPE:
|
||||
tooltipValue = 'row'
|
||||
}
|
||||
return {
|
||||
selector,
|
||||
identifierAction,
|
||||
tooltipValue,
|
||||
headerName,
|
||||
}
|
||||
})
|
||||
|
||||
const numberFormatGroupped = new Intl.NumberFormat(undefined, {
|
||||
style: 'decimal',
|
||||
maximumFractionDigits: 12,
|
||||
@ -273,13 +330,16 @@ function toField(name: string, valueType?: ValueType | null | undefined): ColDef
|
||||
}
|
||||
}
|
||||
|
||||
const getPattern = (index: number) =>
|
||||
Pattern.new((ast) =>
|
||||
function getAstPattern(selector: string | number, action: string) {
|
||||
return Pattern.new((ast) =>
|
||||
Ast.App.positional(
|
||||
Ast.PropertyAccess.new(ast.module, ast, Ast.identifier('at')!),
|
||||
Ast.tryNumberToEnso(index, ast.module)!,
|
||||
Ast.PropertyAccess.new(ast.module, ast, Ast.identifier(action)!),
|
||||
typeof selector === 'number' ?
|
||||
Ast.tryNumberToEnso(selector, ast.module)!
|
||||
: Ast.TextLiteral.new(selector, ast.module),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const getTablePattern = (index: number) =>
|
||||
Pattern.new((ast) =>
|
||||
@ -296,23 +356,40 @@ const getTablePattern = (index: number) =>
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
function createNode(params: CellClickedEvent) {
|
||||
if (config.nodeType === VECTOR_NODE_TYPE || config.nodeType === COLUMN_NODE_TYPE) {
|
||||
config.createNodes({
|
||||
content: getPattern(params.data[INDEX_FIELD_NAME]),
|
||||
commit: true,
|
||||
})
|
||||
}
|
||||
if (config.nodeType === TABLE_NODE_TYPE) {
|
||||
if (config.nodeType === TABLE_NODE_TYPE || config.nodeType === DB_TABLE_NODE_TYPE) {
|
||||
config.createNodes({
|
||||
content: getTablePattern(params.data[INDEX_FIELD_NAME]),
|
||||
commit: true,
|
||||
})
|
||||
}
|
||||
if (
|
||||
newNodeSelectorValues.value.selector !== undefined &&
|
||||
newNodeSelectorValues.value.selector !== null &&
|
||||
newNodeSelectorValues.value.identifierAction
|
||||
) {
|
||||
config.createNodes({
|
||||
content: getAstPattern(
|
||||
params.data[newNodeSelectorValues.value.selector],
|
||||
newNodeSelectorValues.value.identifierAction,
|
||||
),
|
||||
commit: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function indexField(): ColDef {
|
||||
return { field: INDEX_FIELD_NAME, onCellClicked: (params) => createNode(params) }
|
||||
function toLinkField(fieldName: string): ColDef {
|
||||
return {
|
||||
headerName:
|
||||
newNodeSelectorValues.value.headerName ? newNodeSelectorValues.value.headerName : fieldName,
|
||||
field: fieldName,
|
||||
onCellDoubleClicked: (params) => createNode(params),
|
||||
tooltipValueGetter: () => {
|
||||
return `Double click to view this ${newNodeSelectorValues.value.tooltipValue} in a separate node`
|
||||
},
|
||||
cellRenderer: (params: any) => `<a href='#'> ${params.value} </a>`,
|
||||
}
|
||||
}
|
||||
|
||||
/** Return a human-readable representation of an object. */
|
||||
@ -335,6 +412,7 @@ watchEffect(() => {
|
||||
value_type: undefined,
|
||||
// eslint-disable-next-line camelcase
|
||||
has_index_col: false,
|
||||
links: undefined,
|
||||
}
|
||||
const options = agGridOptions.value
|
||||
if (options.api == null) {
|
||||
@ -353,14 +431,14 @@ watchEffect(() => {
|
||||
]
|
||||
rowData = [{ Error: data_.error }]
|
||||
} else if (data_.type === 'Matrix') {
|
||||
columnDefs.push(indexField())
|
||||
columnDefs.push(toLinkField(INDEX_FIELD_NAME))
|
||||
for (let i = 0; i < data_.column_count; i++) {
|
||||
columnDefs.push(toField(i.toString()))
|
||||
}
|
||||
rowData = addRowIndex(data_.json)
|
||||
isTruncated.value = data_.all_rows_count !== data_.json.length
|
||||
} else if (data_.type === 'Object_Matrix') {
|
||||
columnDefs.push(indexField())
|
||||
columnDefs.push(toLinkField(INDEX_FIELD_NAME))
|
||||
let keys = new Set<string>()
|
||||
for (const val of data_.json) {
|
||||
if (val != null) {
|
||||
@ -374,21 +452,32 @@ watchEffect(() => {
|
||||
}
|
||||
rowData = addRowIndex(data_.json)
|
||||
isTruncated.value = data_.all_rows_count !== data_.json.length
|
||||
} else if (data_.type === 'Excel_Workbook') {
|
||||
columnDefs = [toLinkField('Value')]
|
||||
rowData = data_.sheet_names.map((name) => ({ Value: name }))
|
||||
} else if (Array.isArray(data_.json)) {
|
||||
columnDefs = [indexField(), toField('Value')]
|
||||
columnDefs = [toLinkField(INDEX_FIELD_NAME), toField('Value')]
|
||||
rowData = data_.json.map((row, i) => ({ [INDEX_FIELD_NAME]: i, Value: toRender(row) }))
|
||||
isTruncated.value = data_.all_rows_count ? data_.all_rows_count !== data_.json.length : false
|
||||
} else if (data_.json !== undefined) {
|
||||
columnDefs = [toField('Value')]
|
||||
rowData = [{ Value: toRender(data_.json) }]
|
||||
columnDefs = data_.links ? [toLinkField('Value')] : [toField('Value')]
|
||||
rowData =
|
||||
data_.links ?
|
||||
data_.links.map((link) => ({
|
||||
Value: link,
|
||||
}))
|
||||
: [{ Value: toRender(data_.json) }]
|
||||
} else {
|
||||
const dataHeader =
|
||||
('header' in data_ ? data_.header : [])?.map((v, i) => {
|
||||
const valueType = data_.value_type ? data_.value_type[i] : null
|
||||
if (config.nodeType === ROW_NODE_TYPE && v === 'column') {
|
||||
return toLinkField(v)
|
||||
}
|
||||
return toField(v, valueType)
|
||||
}) ?? []
|
||||
|
||||
columnDefs = data_.has_index_col ? [indexField(), ...dataHeader] : dataHeader
|
||||
columnDefs = data_.has_index_col ? [toLinkField(INDEX_FIELD_NAME), ...dataHeader] : dataHeader
|
||||
const rows = data_.data && data_.data.length > 0 ? data_.data[0]?.length ?? 0 : 0
|
||||
rowData = Array.from({ length: rows }, (_, i) => {
|
||||
const shift = data_.has_index_col ? 1 : 0
|
||||
@ -589,4 +678,12 @@ onUnmounted(() => {
|
||||
.TableVisualization > .ag-theme-alpine > .ag-root-wrapper.ag-layout-normal {
|
||||
border-radius: 0 0 var(--radius-default) var(--radius-default);
|
||||
}
|
||||
|
||||
a {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: darkblue;
|
||||
}
|
||||
</style>
|
||||
|
@ -286,6 +286,11 @@ type Postgres_Connection
|
||||
save_as_data_link self destination (on_existing_file:Existing_File_Behavior = Existing_File_Behavior.Error) =
|
||||
self.data_link_setup.save_as_data_link destination on_existing_file
|
||||
|
||||
## PRIVATE
|
||||
Converts this value to a JSON serializable object.
|
||||
to_js_object : JS_Object
|
||||
to_js_object self =
|
||||
JS_Object.from_pairs [["type", "Postgres_Connection"], ["links", self.connection.tables.at "Name" . to_vector]]
|
||||
|
||||
## PRIVATE
|
||||
get_encoding_name : JDBC_Connection.JDBC_Connection -> Text
|
||||
|
@ -266,3 +266,9 @@ type SQLite_Connection
|
||||
on the 'subclasses'.
|
||||
base_connection : Connection
|
||||
base_connection self = self.connection
|
||||
|
||||
## PRIVATE
|
||||
Converts this value to a JSON serializable object.
|
||||
to_js_object : JS_Object
|
||||
to_js_object self =
|
||||
JS_Object.from_pairs [["type", "SQLite_Connection"], ["links", self.connection.tables.at "Name" . to_vector]]
|
||||
|
@ -311,8 +311,9 @@ type Excel_Workbook
|
||||
Provides a JS object representation for use in visualizations.
|
||||
to_js_object : JS_Object
|
||||
to_js_object self =
|
||||
values=(self.tables.at "Name" . to_vector)
|
||||
additional_fields = case self.file of
|
||||
regular_file : File -> [["file", regular_file.path]]
|
||||
regular_file : File -> [["file", regular_file.path], ["sheet_names", values]]
|
||||
_ -> []
|
||||
JS_Object.from_pairs <|
|
||||
[["type", "Excel_Workbook"], ["xls_format", self.xls_format]] + additional_fields
|
||||
|
@ -2,7 +2,7 @@ from Standard.Base import all
|
||||
import Standard.Base.Data.Vector.Builder
|
||||
|
||||
import Standard.Table.Row.Row
|
||||
from Standard.Table import Column, Table
|
||||
from Standard.Table import Column, Table, Excel_Workbook
|
||||
|
||||
import Standard.Database.DB_Column.DB_Column
|
||||
import Standard.Database.DB_Table.DB_Table
|
||||
@ -46,12 +46,11 @@ prepare_visualization y max_rows=1000 =
|
||||
JS_Object.from_pairs [["json", value]]
|
||||
_ : Number ->
|
||||
JS_Object.from_pairs [["json", make_json_for_value x]]
|
||||
_ ->
|
||||
_ : Excel_Workbook ->
|
||||
js_value = x.to_js_object
|
||||
value = if js_value.is_a JS_Object . not then js_value else
|
||||
pairs = [['_display_text_', x.to_display_text]] + js_value.field_names.map f-> [f, make_json_for_value (js_value.get f)]
|
||||
JS_Object.from_pairs pairs
|
||||
JS_Object.from_pairs [["json", value]]
|
||||
JS_Object.from_pairs [["json", js_value], ["sheet_names", x . sheet_names], ["type", "Excel_Workbook"]]
|
||||
_ ->
|
||||
make_json_for_other x
|
||||
|
||||
result.to_text
|
||||
|
||||
@ -158,6 +157,16 @@ make_json_for_table dataframe all_rows_count include_index_col =
|
||||
pairs = [header, value_type, data, all_rows, has_index_col, ["type", "Table"]]
|
||||
JS_Object.from_pairs pairs
|
||||
|
||||
make_json_for_other : Any -> JS_Object
|
||||
make_json_for_other x =
|
||||
js_value = x.to_js_object
|
||||
value = if js_value.is_a JS_Object . not then js_value else
|
||||
pairs = [['_display_text_', x.to_display_text]] + js_value.field_names.map f-> [f, make_json_for_value (js_value.get f)]
|
||||
JS_Object.from_pairs pairs
|
||||
additional_fields = if js_value.is_a JS_Object . not then [] else
|
||||
if js_value.contains_key 'links' then [["links", js_value.get 'links']] else []
|
||||
JS_Object.from_pairs <| [["json", value]] + additional_fields
|
||||
|
||||
## PRIVATE
|
||||
Create JSON serialization of values for the table.
|
||||
make_json_for_value : Any -> Integer -> Text
|
||||
|
@ -36,6 +36,12 @@ type Foo
|
||||
to_js_object : JS_Object
|
||||
to_js_object self = JS_Object.from_pairs [["x", self.x]]
|
||||
|
||||
type Foo_Link
|
||||
Value x
|
||||
|
||||
to_js_object : JS_Object
|
||||
to_js_object self = JS_Object.from_pairs [["x", self.x], ["links", ["a", "b", "c"]]]
|
||||
|
||||
|
||||
add_specs suite_builder =
|
||||
make_json header data all_rows value_type has_index_col =
|
||||
@ -100,6 +106,12 @@ add_specs suite_builder =
|
||||
json = JS_Object.from_pairs [["json", JS_Object.from_pairs [["_display_text_", (Foo.Value 42).to_display_text],["x", 42]]]]
|
||||
vis . should_equal json.to_text
|
||||
|
||||
group_builder.specify "should handle datatypes with links" <|
|
||||
example = Foo_Link.Value "test-value"
|
||||
vis = Visualization.prepare_visualization example
|
||||
json = JS_Object.from_pairs [["json", JS_Object.from_pairs [["_display_text_", example.to_display_text],["x", "test-value"], ["links", "[a, b, c]"]]], ["links", ["a", "b", "c"]]]
|
||||
vis . should_equal json.to_text
|
||||
|
||||
group_builder.specify "should visualize value type info" <|
|
||||
make_json vt =
|
||||
js_object = vt.to_js_object
|
||||
|
Loading…
Reference in New Issue
Block a user