diff --git a/backend/chainlit/__init__.py b/backend/chainlit/__init__.py index 5455fac760..635a2d2cd9 100644 --- a/backend/chainlit/__init__.py +++ b/backend/chainlit/__init__.py @@ -23,6 +23,7 @@ from chainlit.element import ( Audio, Component, + Dataframe, File, Image, Pdf, diff --git a/backend/chainlit/data/sql_alchemy.py b/backend/chainlit/data/sql_alchemy.py index 79596b7359..bb43ef7c16 100644 --- a/backend/chainlit/data/sql_alchemy.py +++ b/backend/chainlit/data/sql_alchemy.py @@ -7,7 +7,6 @@ import aiofiles import aiohttp - from chainlit.data.base import BaseDataLayer from chainlit.data.storage_clients.base import BaseStorageClient from chainlit.data.utils import queue_until_user_message diff --git a/backend/chainlit/element.py b/backend/chainlit/element.py index 56384581ec..4c26464a59 100644 --- a/backend/chainlit/element.py +++ b/backend/chainlit/element.py @@ -31,7 +31,16 @@ } ElementType = Literal[ - "image", "text", "pdf", "tasklist", "audio", "video", "file", "plotly", "component" + "image", + "text", + "pdf", + "tasklist", + "audio", + "video", + "file", + "plotly", + "dataframe", + "component", ] ElementDisplay = Literal["inline", "side", "page"] ElementSize = Literal["small", "medium", "large"] @@ -358,6 +367,25 @@ def __post_init__(self) -> None: super().__post_init__() +@dataclass +class Dataframe(Element): + """Useful to send a pandas DataFrame to the UI.""" + + type: ClassVar[ElementType] = "dataframe" + size: ElementSize = "large" + data: Any = None # The type is Any because it is checked in __post_init__. + + def __post_init__(self) -> None: + """Ensures the data is a pandas DataFrame and converts it to JSON.""" + from pandas import DataFrame + + if not isinstance(self.data, DataFrame): + raise TypeError("data must be a pandas.DataFrame") + + self.content = self.data.to_json(orient="split", date_format="iso") + super().__post_init__() + + @dataclass class Component(Element): """Useful to send a custom component to the UI.""" diff --git a/backend/poetry.lock b/backend/poetry.lock index 4ce5808c61..141bbb1a45 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -3528,6 +3528,21 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.9.2)"] +[[package]] +name = "pandas-stubs" +version = "2.2.2.240807" +description = "Type annotations for pandas" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas_stubs-2.2.2.240807-py3-none-any.whl", hash = "sha256:893919ad82be4275f0d07bb47a95d08bae580d3fdea308a7acfcb3f02e76186e"}, + {file = "pandas_stubs-2.2.2.240807.tar.gz", hash = "sha256:64a559725a57a449f46225fbafc422520b7410bff9252b661a225b5559192a93"}, +] + +[package.dependencies] +numpy = ">=1.23.5" +types-pytz = ">=2022.1.1" + [[package]] name = "pathspec" version = "0.12.1" @@ -5277,6 +5292,17 @@ files = [ {file = "types_aiofiles-23.2.0.20240623-py3-none-any.whl", hash = "sha256:70597b29fc40c8583b6d755814b2cd5fcdb6785622e82d74ef499f9066316e08"}, ] +[[package]] +name = "types-pytz" +version = "2024.2.0.20240913" +description = "Typing stubs for pytz" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-pytz-2024.2.0.20240913.tar.gz", hash = "sha256:4433b5df4a6fc587bbed41716d86a5ba5d832b4378e506f40d34bc9c81df2c24"}, + {file = "types_pytz-2024.2.0.20240913-py3-none-any.whl", hash = "sha256:a1eebf57ebc6e127a99d2fa2ba0a88d2b173784ef9b3defcc2004ab6855a44df"}, +] + [[package]] name = "types-requests" version = "2.31.0.6" @@ -5718,4 +5744,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0.0" -content-hash = "eda02c1c17a62c92406f37a9a5a81cbebf039f63bf3200258122ac564ec47de9" +content-hash = "198eaaf758a037ee459912d1b3ff1cf7b4618f8f4eb2b6f55bf9bceaf17d0b45" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 9f22ab9cf8..a4a844cedb 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -80,6 +80,7 @@ slack_bolt = "^1.18.1" discord = "^2.3.2" botbuilder-core = "^4.15.0" aiosqlite = "^0.20.0" +pandas = "^2.2.2" moto = "^5.0.14" [tool.poetry.group.dev.dependencies] @@ -94,6 +95,7 @@ mypy = "^1.7.1" types-requests = "^2.31.0.2" types-aiofiles = "^23.1.0.5" mypy-boto3-dynamodb = "^1.34.113" +pandas-stubs = { version = "^2.2.2", python = ">=3.9" } [tool.mypy] python_version = "3.9" @@ -120,7 +122,6 @@ ignore_missing_imports = true - [tool.poetry.group.custom-data] optional = true diff --git a/backend/tests/data/conftest.py b/backend/tests/data/conftest.py index e1fe80faf7..e7a98d9081 100644 --- a/backend/tests/data/conftest.py +++ b/backend/tests/data/conftest.py @@ -1,7 +1,6 @@ -import pytest - from unittest.mock import AsyncMock +import pytest from chainlit.data.storage_clients.base import BaseStorageClient from chainlit.user import User diff --git a/backend/tests/data/test_sql_alchemy.py b/backend/tests/data/test_sql_alchemy.py index b94f174512..8597b6ce40 100644 --- a/backend/tests/data/test_sql_alchemy.py +++ b/backend/tests/data/test_sql_alchemy.py @@ -2,13 +2,13 @@ from pathlib import Path import pytest +from chainlit.data.sql_alchemy import SQLAlchemyDataLayer +from chainlit.data.storage_clients.base import BaseStorageClient +from chainlit.element import Text from sqlalchemy import text from sqlalchemy.ext.asyncio import create_async_engine from chainlit import User -from chainlit.data.storage_clients.base import BaseStorageClient -from chainlit.data.sql_alchemy import SQLAlchemyDataLayer -from chainlit.element import Text @pytest.fixture @@ -23,18 +23,21 @@ async def data_layer(mock_storage_client: BaseStorageClient, tmp_path: Path): # Ref: https://docs.chainlit.io/data-persistence/custom#sql-alchemy-data-layer async with engine.begin() as conn: await conn.execute( - text(""" + text( + """ CREATE TABLE users ( "id" UUID PRIMARY KEY, "identifier" TEXT NOT NULL UNIQUE, "metadata" JSONB NOT NULL, "createdAt" TEXT ); - """) + """ + ) ) await conn.execute( - text(""" + text( + """ CREATE TABLE IF NOT EXISTS threads ( "id" UUID PRIMARY KEY, "createdAt" TEXT, @@ -45,11 +48,13 @@ async def data_layer(mock_storage_client: BaseStorageClient, tmp_path: Path): "metadata" JSONB, FOREIGN KEY ("userId") REFERENCES users("id") ON DELETE CASCADE ); - """) + """ + ) ) await conn.execute( - text(""" + text( + """ CREATE TABLE IF NOT EXISTS steps ( "id" UUID PRIMARY KEY, "name" TEXT NOT NULL, @@ -72,11 +77,13 @@ async def data_layer(mock_storage_client: BaseStorageClient, tmp_path: Path): "language" TEXT, "indent" INT ); - """) + """ + ) ) await conn.execute( - text(""" + text( + """ CREATE TABLE IF NOT EXISTS elements ( "id" UUID PRIMARY KEY, "threadId" UUID, @@ -92,11 +99,13 @@ async def data_layer(mock_storage_client: BaseStorageClient, tmp_path: Path): "forId" UUID, "mime" TEXT ); - """) + """ + ) ) await conn.execute( - text(""" + text( + """ CREATE TABLE IF NOT EXISTS feedbacks ( "id" UUID PRIMARY KEY, "forId" UUID NOT NULL, @@ -104,7 +113,8 @@ async def data_layer(mock_storage_client: BaseStorageClient, tmp_path: Path): "value" INT NOT NULL, "comment" TEXT ); - """) + """ + ) ) # Create SQLAlchemyDataLayer instance diff --git a/cypress/e2e/dataframe/main.py b/cypress/e2e/dataframe/main.py new file mode 100644 index 0000000000..a42097c491 --- /dev/null +++ b/cypress/e2e/dataframe/main.py @@ -0,0 +1,68 @@ +import pandas as pd + +import chainlit as cl + + +@cl.on_chat_start +async def start(): + # Create a sample DataFrame with more than 10 rows to test pagination functionality + data = { + "Name": [ + "Alice", + "David", + "Charlie", + "Bob", + "Eva", + "Grace", + "Hannah", + "Jack", + "Frank", + "Kara", + "Liam", + "Ivy", + "Mia", + "Noah", + "Olivia", + ], + "Age": [25, 40, 35, 30, 45, 55, 60, 70, 50, 75, 80, 65, 85, 90, 95], + "City": [ + "New York", + "Houston", + "Chicago", + "Los Angeles", + "Phoenix", + "San Antonio", + "San Diego", + "San Jose", + "Philadelphia", + "Austin", + "Fort Worth", + "Dallas", + "Jacksonville", + "Columbus", + "Charlotte", + ], + "Salary": [ + 70000, + 100000, + 90000, + 80000, + 110000, + 130000, + 140000, + 160000, + 120000, + 170000, + 180000, + 150000, + 190000, + 200000, + 210000, + ], + } + + df = pd.DataFrame(data) + + elements = [cl.Dataframe(data=df, display="inline", name="Dataframe")] + + await cl.Message(content="This message has a Dataframe", elements=elements).send() diff --git a/cypress/e2e/dataframe/spec.cy.ts b/cypress/e2e/dataframe/spec.cy.ts new file mode 100644 index 0000000000..14c59b611d --- /dev/null +++ b/cypress/e2e/dataframe/spec.cy.ts @@ -0,0 +1,41 @@ +import { runTestServer } from '../../support/testUtils'; + +describe('dataframe', () => { + before(() => { + runTestServer(); + }); + + it('should be able to display an inline dataframe', () => { + // Check if the DataFrame is rendered within the first step + cy.get('.step').should('have.length', 1); + cy.get('.step').first().find('.MuiDataGrid-main').should('have.length', 1); + + // Click the sort button in the "Age" column header to sort in ascending order + cy.get('.MuiDataGrid-columnHeader[aria-label="Age"]') + .find('button') + .first() + .click({ force: true }); + // Verify the first row's "Age" cell contains '25' after sorting + cy.get('.MuiDataGrid-row') + .first() + .find('.MuiDataGrid-cell[data-field="Age"] .MuiDataGrid-cellContent') + .should('have.text', '25'); + + // Click the "Next page" button in the pagination controls + cy.get('.MuiTablePagination-actions').find('button').eq(1).click(); + // Verify that the next page contains exactly 5 rows + cy.get('.MuiDataGrid-row').should('have.length', 5); + + // Click the input to open the dropdown + cy.get('.MuiTablePagination-select').click(); + // Select the option with the value '50' from the dropdown list + cy.get('ul.MuiMenu-list li').contains('50').click(); + // Scroll to the bottom of the virtual scroller in the MUI DataGrid + cy.get('.MuiDataGrid-virtualScroller').scrollTo('bottom'); + // Check that tha last name is Olivia + cy.get('.MuiDataGrid-row') + .last() + .find('.MuiDataGrid-cell[data-field="Name"] .MuiDataGrid-cellContent') + .should('have.text', 'Olivia'); + }); +}); diff --git a/frontend/package.json b/frontend/package.json index 88afc29a05..93a7682195 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@mui/icons-material": "^5.14.9", "@mui/lab": "^5.0.0-alpha.122", "@mui/material": "^5.14.10", + "@mui/x-data-grid": "^6.20.4", "formik": "^2.4.3", "highlight.js": "^11.9.0", "i18next": "^23.7.16", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index d227207400..06dfed2047 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@mui/material': specifier: ^5.14.10 version: 5.14.10(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mui/x-data-grid': + specifier: ^6.20.4 + version: 6.20.4(@mui/material@5.14.10(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.14.19(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) formik: specifier: ^2.4.3 version: 2.4.3(react@18.2.0) @@ -303,6 +306,10 @@ packages: resolution: {integrity: sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.25.6': + resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} + engines: {node: '>=6.9.0'} + '@babel/template@7.22.15': resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} engines: {node: '>=6.9.0'} @@ -852,6 +859,14 @@ packages: '@types/react': optional: true + '@mui/types@7.2.17': + resolution: {integrity: sha512-oyumoJgB6jDV8JFzRqjBo2daUuHpzDjoO/e3IrRhhHo/FxJlaVhET6mcNrKHUq2E+R+q3ql0qAtvQ4rfWHhAeQ==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@mui/utils@5.14.19': resolution: {integrity: sha512-qAHvTXzk7basbyqPvhgWqN6JbmI2wLB/mf97GkSlz5c76MiKYV6Ffjvw9BjKZQ1YRb8rDX9kgdjRezOcoB91oQ==} engines: {node: '>=12.0.0'} @@ -862,6 +877,25 @@ packages: '@types/react': optional: true + '@mui/utils@5.16.6': + resolution: {integrity: sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/x-data-grid@6.20.4': + resolution: {integrity: sha512-I0JhinVV4e25hD2dB+R6biPBtpGeFrXf8RwlMPQbr9gUggPmPmNtWKo8Kk2PtBBMlGtdMAgHWe7PqhmucUxU1w==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mui/material': ^5.4.1 + '@mui/system': ^5.4.1 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1242,6 +1276,9 @@ packages: '@types/prop-types@15.7.11': resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} + '@types/prop-types@15.7.13': + resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} + '@types/react-dom@18.2.22': resolution: {integrity: sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==} @@ -1548,6 +1585,10 @@ packages: resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} engines: {node: '>=6'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-alpha@1.0.4: resolution: {integrity: sha512-lr8/t5NPozTSqli+duAN+x+no/2WaKTeWvxhHGN+aXT6AJ8vPlzLa7UriyjWak0pSC2jHol9JgjBYnnHsGha9A==} @@ -3095,6 +3136,9 @@ packages: react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-markdown@9.0.1: resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==} peerDependencies: @@ -3221,6 +3265,9 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + reselect@4.1.8: + resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4004,6 +4051,10 @@ snapshots: dependencies: regenerator-runtime: 0.14.0 + '@babel/runtime@7.25.6': + dependencies: + regenerator-runtime: 0.14.0 + '@babel/template@7.22.15': dependencies: '@babel/code-frame': 7.23.5 @@ -4459,6 +4510,10 @@ snapshots: optionalDependencies: '@types/react': 18.2.0 + '@mui/types@7.2.17(@types/react@18.2.0)': + optionalDependencies: + '@types/react': 18.2.0 + '@mui/utils@5.14.19(@types/react@18.2.0)(react@18.2.0)': dependencies: '@babel/runtime': 7.23.5 @@ -4469,6 +4524,32 @@ snapshots: optionalDependencies: '@types/react': 18.2.0 + '@mui/utils@5.16.6(@types/react@18.2.0)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.25.6 + '@mui/types': 7.2.17(@types/react@18.2.0) + '@types/prop-types': 15.7.13 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.3.1 + optionalDependencies: + '@types/react': 18.2.0 + + '@mui/x-data-grid@6.20.4(@mui/material@5.14.10(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.14.19(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.25.6 + '@mui/material': 5.14.10(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mui/system': 5.14.19(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0))(@types/react@18.2.0)(react@18.2.0) + '@mui/utils': 5.16.6(@types/react@18.2.0)(react@18.2.0) + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + reselect: 4.1.8 + transitivePeerDependencies: + - '@types/react' + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4856,6 +4937,8 @@ snapshots: '@types/prop-types@15.7.11': {} + '@types/prop-types@15.7.13': {} + '@types/react-dom@18.2.22': dependencies: '@types/react': 18.2.0 @@ -5160,6 +5243,8 @@ snapshots: clsx@2.0.0: {} + clsx@2.1.1: {} + color-alpha@1.0.4: dependencies: color-parse: 1.4.3 @@ -7170,6 +7255,8 @@ snapshots: react-is@18.2.0: {} + react-is@18.3.1: {} + react-markdown@9.0.1(@types/react@18.2.0)(react@18.2.0): dependencies: '@types/hast': 3.0.4 @@ -7399,6 +7486,8 @@ snapshots: requires-port@1.0.0: {} + reselect@4.1.8: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} diff --git a/frontend/src/components/atoms/elements/Dataframe.tsx b/frontend/src/components/atoms/elements/Dataframe.tsx new file mode 100644 index 0000000000..adb0a79d20 --- /dev/null +++ b/frontend/src/components/atoms/elements/Dataframe.tsx @@ -0,0 +1,58 @@ +import { DataGrid } from '@mui/x-data-grid'; + +import { useFetch } from 'hooks/useFetch'; + +import { type IDataframeElement } from 'client-types/'; + +interface Props { + element: IDataframeElement; +} + +const DataframeElement = ({ element }: Props) => { + const { data } = useFetch(element.url || null); + + // Check if data is still being fetched + const isLoading: boolean = !data; + + let gridColumns = []; + let gridRows = []; + + // Parse data only if it exists + if (!isLoading) { + const { index, columns, data: rowData } = JSON.parse(data); + + gridColumns = columns.map((col: string) => ({ + field: col, + minWidth: 150 + })); + + gridRows = rowData.map((row: (string | number)[], idx: number) => { + const rowObj: any = { id: index[idx] }; + columns.forEach((col: string, colIdx: number) => { + rowObj[col] = row[colIdx]; + }); + return rowObj; + }); + } + + return ( + + ); +}; + +export { DataframeElement }; diff --git a/frontend/src/components/atoms/elements/Element.tsx b/frontend/src/components/atoms/elements/Element.tsx index 0ac81cb9cf..70a590310f 100644 --- a/frontend/src/components/atoms/elements/Element.tsx +++ b/frontend/src/components/atoms/elements/Element.tsx @@ -1,6 +1,7 @@ import type { IMessageElement } from 'client-types/'; import { AudioElement } from './Audio'; +import { DataframeElement } from './Dataframe'; import { FileElement } from './File'; import { ImageElement } from './Image'; import { PDFElement } from './PDF'; @@ -28,6 +29,8 @@ const Element = ({ element }: ElementProps): JSX.Element | null => { return ; case 'plotly': return ; + case 'dataframe': + return ; default: return null; } diff --git a/frontend/src/components/atoms/elements/InlinedDataframeList.tsx b/frontend/src/components/atoms/elements/InlinedDataframeList.tsx new file mode 100644 index 0000000000..9f9275b433 --- /dev/null +++ b/frontend/src/components/atoms/elements/InlinedDataframeList.tsx @@ -0,0 +1,29 @@ +import Stack from '@mui/material/Stack'; + +import type { IDataframeElement } from 'client-types/'; + +import { DataframeElement } from './Dataframe'; + +interface Props { + items: IDataframeElement[]; +} + +const InlinedDataframeList = ({ items }: Props) => ( + + {items.map((element, i) => { + return ( +
+ +
+ ); + })} +
+); + +export { InlinedDataframeList }; diff --git a/frontend/src/components/atoms/elements/InlinedElements.tsx b/frontend/src/components/atoms/elements/InlinedElements.tsx index a5d06787a0..1969dc6540 100644 --- a/frontend/src/components/atoms/elements/InlinedElements.tsx +++ b/frontend/src/components/atoms/elements/InlinedElements.tsx @@ -3,6 +3,7 @@ import Stack from '@mui/material/Stack'; import type { ElementType, IMessageElement } from 'client-types/'; import { InlinedAudioList } from './InlinedAudioList'; +import { InlinedDataframeList } from './InlinedDataframeList'; import { InlinedFileList } from './InlinedFileList'; import { InlinedImageList } from './InlinedImageList'; import { InlinedPDFList } from './InlinedPDFList'; @@ -64,6 +65,9 @@ const InlinedElements = ({ elements }: Props) => { {elementsByType.plotly?.length ? ( ) : null} + {elementsByType.dataframe?.length ? ( + + ) : null} ); }; diff --git a/frontend/src/components/atoms/elements/index.ts b/frontend/src/components/atoms/elements/index.ts index 0a520ff7e3..24b654040b 100644 --- a/frontend/src/components/atoms/elements/index.ts +++ b/frontend/src/components/atoms/elements/index.ts @@ -1,4 +1,5 @@ export { AudioElement } from './Audio'; +export { DataframeElement } from './Dataframe'; export { Element } from './Element'; export { ElementSideView } from './ElementSideView'; export { ElementView } from './ElementView'; @@ -12,6 +13,7 @@ export { VideoElement } from './Video'; // Inlined export { InlinedAudioList } from './InlinedAudioList'; +export { InlinedDataframeList } from './InlinedDataframeList'; export { InlinedElements } from './InlinedElements'; export { InlinedFileList } from './InlinedFileList'; export { InlinedImageList } from './InlinedImageList'; diff --git a/libs/react-client/src/types/element.ts b/libs/react-client/src/types/element.ts index 1d8e25ccd2..157d540afa 100644 --- a/libs/react-client/src/types/element.ts +++ b/libs/react-client/src/types/element.ts @@ -6,7 +6,8 @@ export type IElement = | IAudioElement | IVideoElement | IFileElement - | IPlotlyElement; + | IPlotlyElement + | IDataframeElement; export type IMessageElement = | IImageElement @@ -15,7 +16,8 @@ export type IMessageElement = | IAudioElement | IVideoElement | IFileElement - | IPlotlyElement; + | IPlotlyElement + | IDataframeElement; export type ElementType = IElement['type']; export type IElementSize = 'small' | 'medium' | 'large'; @@ -69,3 +71,5 @@ export interface IFileElement extends TMessageElement<'file'> { export interface IPlotlyElement extends TMessageElement<'plotly'> {} export interface ITasklistElement extends TElement<'tasklist'> {} + +export interface IDataframeElement extends TMessageElement<'dataframe'> {}