From ae95b829aaaf9a570e8cd315e21ea6e276b4c73c Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Wed, 6 Aug 2025 13:59:07 +0530 Subject: [PATCH 1/7] wip --- packages/base/src/commands/BaseCommandIDs.ts | 1 + packages/base/src/commands/index.ts | 49 +++++++++++++ packages/base/src/dialogs/attributeTable.tsx | 73 +++++++++++++++++++ .../base/src/dialogs/attributeTableDialog.tsx | 34 +++++++++ python/jupytergis_lab/src/index.ts | 6 ++ 5 files changed, 163 insertions(+) create mode 100644 packages/base/src/dialogs/attributeTable.tsx create mode 100644 packages/base/src/dialogs/attributeTableDialog.tsx diff --git a/packages/base/src/commands/BaseCommandIDs.ts b/packages/base/src/commands/BaseCommandIDs.ts index b700e6849..170cb6746 100644 --- a/packages/base/src/commands/BaseCommandIDs.ts +++ b/packages/base/src/commands/BaseCommandIDs.ts @@ -49,3 +49,4 @@ export const selectCompleter = 'jupytergis:selectConsoleCompleter'; export const addAnnotation = 'jupytergis:addAnnotation'; export const zoomToLayer = 'jupytergis:zoomToLayer'; export const downloadGeoJSON = 'jupytergis:downloadGeoJSON'; +export const openAttributeTable = 'jupytergis:openAttributeTable'; diff --git a/packages/base/src/commands/index.ts b/packages/base/src/commands/index.ts index d702984cf..1129495f3 100644 --- a/packages/base/src/commands/index.ts +++ b/packages/base/src/commands/index.ts @@ -22,6 +22,7 @@ import { fromLonLat } from 'ol/proj'; import { CommandIDs, icons } from '../constants'; import { ProcessingFormDialog } from '../dialogs/ProcessingFormDialog'; +import { AttributeTableWidget } from '../dialogs/attributeTable'; import { LayerBrowserWidget } from '../dialogs/layerBrowserDialog'; import { LayerCreationFormDialog } from '../dialogs/layerCreationFormDialog'; import { SymbologyWidget } from '../dialogs/symbology/symbologyDialog'; @@ -843,6 +844,40 @@ export function addCommands( icon: targetWithCenterIcon, }); + commands.addCommand(CommandIDs.openAttributeTable, { + label: trans.__('Open Attribute Table'), + isEnabled: () => { + const selectedLayer = getSingleSelectedLayer(tracker); + return selectedLayer + ? ['VectorLayer', 'VectorTileLayer', 'ShapefileLayer'].includes( + selectedLayer.type, + ) + : false; + }, + execute: async () => { + const currentWidget = tracker.currentWidget; + if (!currentWidget) { + return; + } + + const model = currentWidget.model; + const selectedLayers = model.localState?.selected?.value; + + if (!selectedLayers) { + console.warn('No layer selected'); + return; + } + + const layerId = Object.keys(selectedLayers)[0]; + + // You’ll replace this with the real attribute table dialog/component + console.log('Open attribute table for layer:', layerId); + + // TODO: Launch your React dialog here once ready + // new AttributeTableDialog({ model, layerId }).launch(); + }, +}); + loadKeybindings(commands, keybindings); } @@ -887,6 +922,20 @@ namespace Private { }; } + export function createAttributeTableDialog(tracker: JupyterGISTracker) { + return async () => { + const current = tracker.currentWidget; + if (!current) { + return; + } + + const dialog = new AttributeTableWidget({ + model: current.model + }); + await dialog.launch(); + }; +} + export function createEntry({ tracker, formSchemaRegistry, diff --git a/packages/base/src/dialogs/attributeTable.tsx b/packages/base/src/dialogs/attributeTable.tsx new file mode 100644 index 000000000..745f4851a --- /dev/null +++ b/packages/base/src/dialogs/attributeTable.tsx @@ -0,0 +1,73 @@ +import { IJupyterGISModel } from '@jupytergis/schema'; +import { Dialog } from '@jupyterlab/apputils'; +import React from 'react'; +import DataGrid from 'react-data-grid'; + +import { useGetProperties } from '@/src/dialogs/symbology/hooks/useGetProperties'; + +export interface IAttributeTableProps { + model: IJupyterGISModel; + layerId: string; +} + +const AttributeTable: React.FC = ({ model, layerId }) => { + const [columns, setColumns] = React.useState([]); + const [rows, setRows] = React.useState([]); + + React.useEffect(() => { + const fetchFeatures = async () => { + // const features = await model.getFeaturesInExtent(layerId); + const { featureProperties } = useGetProperties({ + layerId, + model: model, + }); + + if (!featureProperties) { + setColumns([]); + setRows([]); + return; + } + + // Assume properties of first feature determine columns + const sample = featureProperties[0] || {}; + const keys = Object.keys(sample); + + const cols = keys.map(key => ({ + key, + name: key, + resizable: true, + sortable: true, + })); + + const rowData = featureProperties.map((f: any, i: number) => ({ + id: i, + ...f.properties, + })); + + setColumns(cols); + setRows(rowData); + }; + + fetchFeatures(); + }, [model, layerId]); + + return ( +
+ +
+ ); +}; + +export class AttributeTableWidget extends Dialog { + constructor(model: IJupyterGISModel, layerId: string) { + const body = ; + + super({ + title: 'Attribute Table', + body, + }); + + this.id = 'jupytergis::attributeTable'; + this.addClass('jp-gis-attribute-table-dialog'); + } +} diff --git a/packages/base/src/dialogs/attributeTableDialog.tsx b/packages/base/src/dialogs/attributeTableDialog.tsx new file mode 100644 index 000000000..f3a5eed92 --- /dev/null +++ b/packages/base/src/dialogs/attributeTableDialog.tsx @@ -0,0 +1,34 @@ +// import { IJupyterGISModel } from '@jupytergis/schema'; +// import { Dialog } from '@jupyterlab/apputils'; +// import React, { useEffect, useState } from 'react'; + +// import { AttributeTableWidget } from './attributeTable'; + + +// interface IAttributeTableWidgetProps { +// model: IJupyterGISModel; +// } + +// const AttributeTableDialog: React.FC = ({ model }) => { +// const [layerId, setLayerId] = useState(null); + +// useEffect(() => { +// const selected = model.localState?.selected?.value; +// if (selected) { +// setLayerId(Object.keys(selected)[0]); +// } +// }, [model.localState]); + +// if (!layerId) return null; + +// return ; +// }; + +// export class AttributeTableWidget extends Dialog { +// constructor(options: IAttributeTableWidgetProps) { +// const body = ; +// super({ title: 'Attribute Table', body }); +// this.id = 'jupytergis::attributeTableWidget'; +// this.addClass('jp-gis-attribute-table-dialog'); +// } +// } diff --git a/python/jupytergis_lab/src/index.ts b/python/jupytergis_lab/src/index.ts index 33c76502d..4370215d0 100644 --- a/python/jupytergis_lab/src/index.ts +++ b/python/jupytergis_lab/src/index.ts @@ -116,6 +116,12 @@ const plugin: JupyterFrontEndPlugin = { rank: 2, }); + app.contextMenu.addItem({ + command: CommandIDs.openAttributeTable, + selector: '.jp-gis-layerItem', + rank: 2, + }); + // Create the Download submenu const downloadSubmenu = new Menu({ commands: app.commands }); downloadSubmenu.title.label = translator.load('jupyterlab').__('Download'); From b2c130b5dc0dcd6c9ea0d2e35fae0b8f31bc2368 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Tue, 12 Aug 2025 12:51:47 +0530 Subject: [PATCH 2/7] add react-data-grid --- packages/base/package.json | 1 + yarn.lock | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/packages/base/package.json b/packages/base/package.json index 1314a2639..45d880592 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -92,6 +92,7 @@ "proj4": "2.19.3", "proj4-list": "1.0.4", "react": "^18.0.1", + "react-data-grid": "^7.0.0-beta.57", "react-day-picker": "^9.7.0", "shpjs": "^6.1.0", "styled-components": "^5.3.6", diff --git a/yarn.lock b/yarn.lock index ce5199693..a1d2fae46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -898,6 +898,7 @@ __metadata: proj4: 2.19.3 proj4-list: 1.0.4 react: ^18.0.1 + react-data-grid: ^7.0.0-beta.57 react-day-picker: ^9.7.0 rimraf: ^3.0.2 shpjs: ^6.1.0 @@ -5006,6 +5007,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^1.1.1": + version: 1.2.1 + resolution: "clsx@npm:1.2.1" + checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12 + languageName: node + linkType: hard + "clsx@npm:^2.1.1": version: 2.1.1 resolution: "clsx@npm:2.1.1" @@ -10612,6 +10620,18 @@ __metadata: languageName: node linkType: hard +"react-data-grid@npm:^7.0.0-beta.57": + version: 7.0.0-canary.49 + resolution: "react-data-grid@npm:7.0.0-canary.49" + dependencies: + clsx: ^1.1.1 + peerDependencies: + react: ^16.14 || ^17.0 + react-dom: ^16.14 || ^17.0 + checksum: fe57d441a5e56dac39a0f7e06979a27d5606515653ead31fc06f9c2fe08ebaf88ff4ab70f1565dd356b343cf4612137dea2d188a40a7e246b85dd5aa2589da04 + languageName: node + linkType: hard + "react-day-picker@npm:^9.7.0": version: 9.8.1 resolution: "react-day-picker@npm:9.8.1" From fee71b83b7dbdb4dd8ffcfe6fdbfe50af12ae299 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Tue, 12 Aug 2025 17:56:08 +0530 Subject: [PATCH 3/7] Working --- packages/base/src/commands/index.ts | 75 +++++++++---------- packages/base/src/dialogs/attributeTable.tsx | 56 +++++++------- .../dialogs/symbology/hooks/useGetFeatures.ts | 61 +++++++++++++++ 3 files changed, 123 insertions(+), 69 deletions(-) create mode 100644 packages/base/src/dialogs/symbology/hooks/useGetFeatures.ts diff --git a/packages/base/src/commands/index.ts b/packages/base/src/commands/index.ts index 1129495f3..07516517f 100644 --- a/packages/base/src/commands/index.ts +++ b/packages/base/src/commands/index.ts @@ -845,38 +845,35 @@ export function addCommands( }); commands.addCommand(CommandIDs.openAttributeTable, { - label: trans.__('Open Attribute Table'), - isEnabled: () => { - const selectedLayer = getSingleSelectedLayer(tracker); - return selectedLayer - ? ['VectorLayer', 'VectorTileLayer', 'ShapefileLayer'].includes( - selectedLayer.type, - ) - : false; - }, - execute: async () => { - const currentWidget = tracker.currentWidget; - if (!currentWidget) { - return; - } - - const model = currentWidget.model; - const selectedLayers = model.localState?.selected?.value; + label: trans.__('Open Attribute Table'), + isEnabled: () => { + const selectedLayer = getSingleSelectedLayer(tracker); + return selectedLayer + ? ['VectorLayer', 'VectorTileLayer', 'ShapefileLayer'].includes( + selectedLayer.type, + ) + : false; + }, + execute: async () => { + const currentWidget = tracker.currentWidget; + if (!currentWidget) { + return; + } - if (!selectedLayers) { - console.warn('No layer selected'); - return; - } + const model = currentWidget.model; + const selectedLayers = model.localState?.selected?.value; - const layerId = Object.keys(selectedLayers)[0]; + if (!selectedLayers) { + console.warn('No layer selected'); + return; + } - // You’ll replace this with the real attribute table dialog/component - console.log('Open attribute table for layer:', layerId); + const layerId = Object.keys(selectedLayers)[0]; - // TODO: Launch your React dialog here once ready - // new AttributeTableDialog({ model, layerId }).launch(); - }, -}); + console.log('Open attribute table for layer:', layerId); + Private.createAttributeTableDialog(tracker, layerId)(); + }, + }); loadKeybindings(commands, keybindings); } @@ -922,19 +919,17 @@ namespace Private { }; } - export function createAttributeTableDialog(tracker: JupyterGISTracker) { - return async () => { - const current = tracker.currentWidget; - if (!current) { - return; - } + export function createAttributeTableDialog(tracker: JupyterGISTracker, layerId: string) { + return async () => { + const current = tracker.currentWidget; + if (!current) { + return; + } - const dialog = new AttributeTableWidget({ - model: current.model - }); - await dialog.launch(); - }; -} + const dialog = new AttributeTableWidget(current.model, layerId); + await dialog.launch(); + }; + } export function createEntry({ tracker, diff --git a/packages/base/src/dialogs/attributeTable.tsx b/packages/base/src/dialogs/attributeTable.tsx index 745f4851a..279693880 100644 --- a/packages/base/src/dialogs/attributeTable.tsx +++ b/packages/base/src/dialogs/attributeTable.tsx @@ -3,7 +3,7 @@ import { Dialog } from '@jupyterlab/apputils'; import React from 'react'; import DataGrid from 'react-data-grid'; -import { useGetProperties } from '@/src/dialogs/symbology/hooks/useGetProperties'; +import { useGetFeatures } from './symbology/hooks/useGetFeatures'; export interface IAttributeTableProps { model: IJupyterGISModel; @@ -14,42 +14,40 @@ const AttributeTable: React.FC = ({ model, layerId }) => { const [columns, setColumns] = React.useState([]); const [rows, setRows] = React.useState([]); - React.useEffect(() => { - const fetchFeatures = async () => { - // const features = await model.getFeaturesInExtent(layerId); - const { featureProperties } = useGetProperties({ - layerId, - model: model, - }); - - if (!featureProperties) { - setColumns([]); - setRows([]); - return; - } + const { features, isLoading, error } = useGetFeatures({ layerId, model }); - // Assume properties of first feature determine columns - const sample = featureProperties[0] || {}; - const keys = Object.keys(sample); + React.useEffect(() => { + if (isLoading) {return;} + if (error) { + console.error('[AttributeTable] Error loading features:', error); + return; + } + if (!features.length) { + console.warn('[AttributeTable] No features found.'); + setColumns([]); + setRows([]); + return; + } - const cols = keys.map(key => ({ + const sampleProps = features[0]?.properties ?? {}; + const cols = [ + { key: 'sno', name: 'S. No.', resizable: true, sortable: true }, + ...Object.keys(sampleProps).map(key => ({ key, name: key, resizable: true, sortable: true, - })); - - const rowData = featureProperties.map((f: any, i: number) => ({ - id: i, - ...f.properties, - })); + })), + ]; - setColumns(cols); - setRows(rowData); - }; + const rowData = features.map((f, i) => ({ + sno: i + 1, + ...f.properties, + })); - fetchFeatures(); - }, [model, layerId]); + setColumns(cols); + setRows(rowData); + }, [features, isLoading, error]); return (
diff --git a/packages/base/src/dialogs/symbology/hooks/useGetFeatures.ts b/packages/base/src/dialogs/symbology/hooks/useGetFeatures.ts new file mode 100644 index 000000000..9ba174bc8 --- /dev/null +++ b/packages/base/src/dialogs/symbology/hooks/useGetFeatures.ts @@ -0,0 +1,61 @@ +import { GeoJSONFeature1, IJupyterGISModel } from '@jupytergis/schema'; +import { useEffect, useState } from 'react'; + +import { loadFile } from '@/src/tools'; + +interface IUseGetFeaturesProps { + layerId?: string; + model: IJupyterGISModel; +} + +interface IUseGetFeaturesResult { + features: GeoJSONFeature1[]; + isLoading: boolean; + error?: Error; +} + +export const useGetFeatures = ({ + layerId, + model, +}: IUseGetFeaturesProps): IUseGetFeaturesResult => { + const [features, setFeatures] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(); + + const fetchFeatures = async () => { + if (!layerId) { + return; + } + + try { + const layer = model.getLayer(layerId); + const source = model.getSource(layer?.parameters?.source); + + if (!source) { + throw new Error('Source not found'); + } + + const data = await loadFile({ + filepath: source.parameters?.path, + type: 'GeoJSONSource', + model: model, + }); + + if (!data) { + throw new Error('Failed to read GeoJSON data'); + } + + setFeatures(data.features || []); + } catch (err) { + setError(err as Error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchFeatures(); + }, [model, layerId]); + + return { features, isLoading, error }; +}; From f97fc3aaf5f978d06d427fccd3b53015dfefdbb5 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Tue, 12 Aug 2025 17:57:12 +0530 Subject: [PATCH 4/7] lint --- packages/base/src/commands/index.ts | 5 ++++- packages/base/src/dialogs/attributeTable.tsx | 4 +++- packages/base/src/dialogs/attributeTableDialog.tsx | 1 - 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/base/src/commands/index.ts b/packages/base/src/commands/index.ts index 07516517f..b45cd39df 100644 --- a/packages/base/src/commands/index.ts +++ b/packages/base/src/commands/index.ts @@ -919,7 +919,10 @@ namespace Private { }; } - export function createAttributeTableDialog(tracker: JupyterGISTracker, layerId: string) { + export function createAttributeTableDialog( + tracker: JupyterGISTracker, + layerId: string, + ) { return async () => { const current = tracker.currentWidget; if (!current) { diff --git a/packages/base/src/dialogs/attributeTable.tsx b/packages/base/src/dialogs/attributeTable.tsx index 279693880..7b90066c6 100644 --- a/packages/base/src/dialogs/attributeTable.tsx +++ b/packages/base/src/dialogs/attributeTable.tsx @@ -17,7 +17,9 @@ const AttributeTable: React.FC = ({ model, layerId }) => { const { features, isLoading, error } = useGetFeatures({ layerId, model }); React.useEffect(() => { - if (isLoading) {return;} + if (isLoading) { + return; + } if (error) { console.error('[AttributeTable] Error loading features:', error); return; diff --git a/packages/base/src/dialogs/attributeTableDialog.tsx b/packages/base/src/dialogs/attributeTableDialog.tsx index f3a5eed92..e046416c8 100644 --- a/packages/base/src/dialogs/attributeTableDialog.tsx +++ b/packages/base/src/dialogs/attributeTableDialog.tsx @@ -4,7 +4,6 @@ // import { AttributeTableWidget } from './attributeTable'; - // interface IAttributeTableWidgetProps { // model: IJupyterGISModel; // } From 9ee904d8597cb2aa2e13e66fa9a33487513c49ad Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Tue, 12 Aug 2025 17:58:02 +0530 Subject: [PATCH 5/7] not needed --- .../base/src/dialogs/attributeTableDialog.tsx | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 packages/base/src/dialogs/attributeTableDialog.tsx diff --git a/packages/base/src/dialogs/attributeTableDialog.tsx b/packages/base/src/dialogs/attributeTableDialog.tsx deleted file mode 100644 index e046416c8..000000000 --- a/packages/base/src/dialogs/attributeTableDialog.tsx +++ /dev/null @@ -1,33 +0,0 @@ -// import { IJupyterGISModel } from '@jupytergis/schema'; -// import { Dialog } from '@jupyterlab/apputils'; -// import React, { useEffect, useState } from 'react'; - -// import { AttributeTableWidget } from './attributeTable'; - -// interface IAttributeTableWidgetProps { -// model: IJupyterGISModel; -// } - -// const AttributeTableDialog: React.FC = ({ model }) => { -// const [layerId, setLayerId] = useState(null); - -// useEffect(() => { -// const selected = model.localState?.selected?.value; -// if (selected) { -// setLayerId(Object.keys(selected)[0]); -// } -// }, [model.localState]); - -// if (!layerId) return null; - -// return ; -// }; - -// export class AttributeTableWidget extends Dialog { -// constructor(options: IAttributeTableWidgetProps) { -// const body = ; -// super({ title: 'Attribute Table', body }); -// this.id = 'jupytergis::attributeTableWidget'; -// this.addClass('jp-gis-attribute-table-dialog'); -// } -// } From e2d010a4f189be1e6b79908745c01e52d5962d1c Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Wed, 13 Aug 2025 13:46:57 +0530 Subject: [PATCH 6/7] some styling --- packages/base/src/dialogs/attributeTable.tsx | 23 ++++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/base/src/dialogs/attributeTable.tsx b/packages/base/src/dialogs/attributeTable.tsx index 7b90066c6..e74375b8c 100644 --- a/packages/base/src/dialogs/attributeTable.tsx +++ b/packages/base/src/dialogs/attributeTable.tsx @@ -38,13 +38,13 @@ const AttributeTable: React.FC = ({ model, layerId }) => { key, name: key, resizable: true, - sortable: true, - })), + sortable: true + })) ]; const rowData = features.map((f, i) => ({ sno: i + 1, - ...f.properties, + ...f.properties })); setColumns(cols); @@ -52,19 +52,28 @@ const AttributeTable: React.FC = ({ model, layerId }) => { }, [features, isLoading, error]); return ( -
- +
+
); }; export class AttributeTableWidget extends Dialog { constructor(model: IJupyterGISModel, layerId: string) { - const body = ; + const body = ( +
+ +
+ ); super({ title: 'Attribute Table', - body, + body }); this.id = 'jupytergis::attributeTable'; From 10cc59c8931df0639910cddb7014073c584d0cde Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Fri, 19 Sep 2025 17:03:14 +0530 Subject: [PATCH 7/7] fix --- packages/base/src/commands/index.ts | 2 ++ packages/base/src/dialogs/attributeTable.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/base/src/commands/index.ts b/packages/base/src/commands/index.ts index 72a66c729..b916f68b5 100644 --- a/packages/base/src/commands/index.ts +++ b/packages/base/src/commands/index.ts @@ -879,6 +879,8 @@ export function addCommands( console.log('Open attribute table for layer:', layerId); Private.createAttributeTableDialog(tracker, layerId)(); + }, + }); // Panel visibility commands commands.addCommand(CommandIDs.toggleLeftPanel, { label: trans.__('Toggle Left Panel'), diff --git a/packages/base/src/dialogs/attributeTable.tsx b/packages/base/src/dialogs/attributeTable.tsx index e74375b8c..53beafec1 100644 --- a/packages/base/src/dialogs/attributeTable.tsx +++ b/packages/base/src/dialogs/attributeTable.tsx @@ -38,13 +38,13 @@ const AttributeTable: React.FC = ({ model, layerId }) => { key, name: key, resizable: true, - sortable: true - })) + sortable: true, + })), ]; const rowData = features.map((f, i) => ({ sno: i + 1, - ...f.properties + ...f.properties, })); setColumns(cols); @@ -73,7 +73,7 @@ export class AttributeTableWidget extends Dialog { super({ title: 'Attribute Table', - body + body, }); this.id = 'jupytergis::attributeTable';