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:
marthasharkey 2024-07-03 14:58:07 +01:00 committed by GitHub
parent 4bb82fae4a
commit ee39fd7f53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 164 additions and 29 deletions

View File

@ -72,6 +72,11 @@ function entryTitle(key: string) {
content: ',';
}
.key {
color: blue;
text-decoration: underline;
}
.viewonly .key {
color: darkred;
text-decoration: none;
}
</style>

View File

@ -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>

View File

@ -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

View File

@ -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]]

View File

@ -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

View File

@ -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

View File

@ -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