From 53440df94918de3dc66d47d8bd0cfc34c44aaf30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Ba=CC=88uerle?= Date: Fri, 22 Dec 2023 17:40:24 +0100 Subject: [PATCH] fix: gracefully fail charts if columns have changed --- backend/zeno_backend/classes/chart.py | 2 - backend/zeno_backend/database/insert.py | 8 +-- backend/zeno_backend/database/select.py | 30 +++++++- backend/zeno_backend/database/update.py | 8 +-- backend/zeno_backend/processing/filtering.py | 2 +- backend/zeno_backend/routers/chart.py | 40 +++++++---- .../report/elements/ChartElement.svelte | 15 ++-- frontend/src/lib/zenoapi/models/Chart.ts | 2 - frontend/src/lib/zenoapi/models/Operation.ts | 1 + .../src/lib/zenoapi/services/ZenoService.ts | 68 ++++++++++++++++++- .../chart/[chartIndex=integer]/+page.svelte | 22 +++--- 11 files changed, 148 insertions(+), 50 deletions(-) diff --git a/backend/zeno_backend/classes/chart.py b/backend/zeno_backend/classes/chart.py index c6cd92fe..c365272a 100644 --- a/backend/zeno_backend/classes/chart.py +++ b/backend/zeno_backend/classes/chart.py @@ -159,7 +159,6 @@ class Chart(CamelModel): type (ChartType): the type of the chart. parameters (XCParameters | TableParameters | BeeswarmParameters | RadarParameters | HeatmapParameters): the parameters of the chart. - data (str): the JSON string data of the chart. """ id: int @@ -173,7 +172,6 @@ class Chart(CamelModel): | RadarParameters | HeatmapParameters ) - data: str | None = None class ParametersEncoder(json.JSONEncoder): diff --git a/backend/zeno_backend/database/insert.py b/backend/zeno_backend/database/insert.py index fce28325..e9bc5d4c 100644 --- a/backend/zeno_backend/database/insert.py +++ b/backend/zeno_backend/database/insert.py @@ -25,7 +25,6 @@ from zeno_backend.classes.user import Author, Organization, User from zeno_backend.database.database import db_pool from zeno_backend.database.util import hash_api_key, resolve_metadata_type -from zeno_backend.processing.chart import calculate_chart_data async def api_key(user: User) -> str | None: @@ -704,18 +703,15 @@ async def chart(project: str, chart: Chart) -> int | None: Returns: int | None: the id of the newly created chart. """ - chart_data = await calculate_chart_data(chart, project) - async with db_pool.connection() as conn: async with conn.cursor() as cur: await cur.execute( - "INSERT INTO charts (name, type, parameters, data, project_uuid) " - "VALUES (%s,%s,%s,%s,%s) RETURNING id;", + "INSERT INTO charts (name, type, parameters, project_uuid) " + "VALUES (%s,%s,%s,%s) RETURNING id;", [ chart.name, chart.type, json.dumps(chart.parameters, cls=ParametersEncoder), - chart_data, project, ], ) diff --git a/backend/zeno_backend/database/select.py b/backend/zeno_backend/database/select.py index a585aa5c..7bd16e7f 100644 --- a/backend/zeno_backend/database/select.py +++ b/backend/zeno_backend/database/select.py @@ -852,7 +852,6 @@ async def charts_for_projects(project_uuids: list[str]) -> list[Chart]: name=r[1], type=r[2], parameters=json.loads(r[3]), - data=json.dumps(r[4]) if r[4] is not None else None, project_uuid=r[5], ), chart_results, @@ -1676,7 +1675,7 @@ async def chart(chart_id: int) -> Chart: chart_id (int): the ID of the chart to be fetched. Returns: - Chart | None: the requested chart. + Chart: the requested chart. """ async with db_pool.connection() as conn: async with conn.cursor() as cur: @@ -1698,11 +1697,36 @@ async def chart(chart_id: int) -> Chart: name=chart_result[0][1], type=chart_result[0][2], parameters=json.loads(chart_result[0][3]), - data=json.dumps(chart_result[0][4]) if chart_result[0][4] is not None else None, project_uuid=chart_result[0][5], ) +async def chart_data(chart_id: int) -> str | None: + """Get a chart's data. + + Args: + chart_id (int): ID of the chart to get data for. + + Returns: + str | None: chart data. + """ + async with db_pool.connection() as conn: + async with conn.cursor() as cur: + await cur.execute( + "SELECT data FROM charts WHERE id = %s", + [chart_id], + ) + chart_result = await cur.fetchall() + + if len(chart_result) == 0: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Chart could not be found.", + ) + + return json.dumps(chart_result[0][0]) if chart_result[0][0] is not None else None + + async def charts(project_uuid: str) -> list[Chart]: """Get a list of all charts created in the project. diff --git a/backend/zeno_backend/database/update.py b/backend/zeno_backend/database/update.py index d951f27c..6ce272a6 100644 --- a/backend/zeno_backend/database/update.py +++ b/backend/zeno_backend/database/update.py @@ -13,7 +13,6 @@ from zeno_backend.classes.tag import Tag from zeno_backend.classes.user import Author, Organization, User from zeno_backend.database.database import db_pool -from zeno_backend.processing.chart import calculate_chart_data async def folder(folder: Folder, project: str): @@ -60,26 +59,21 @@ async def chart(chart: Chart, project: str): chart (Chart): the chart data to use for the update. project (str): the project the user is currently working with. """ - chart_data = await calculate_chart_data(chart, project) - async with db_pool.connection() as conn: async with conn.cursor() as cur: await cur.execute( "UPDATE charts SET project_uuid = %s, name = %s, type = %s, " - "parameters = %s, data = %s, updated_at = CURRENT_TIMESTAMP " + "parameters = %s, data = NULL, updated_at = CURRENT_TIMESTAMP " "WHERE id = %s;", [ project, chart.name, chart.type, json.dumps(chart.parameters, cls=ParametersEncoder), - chart_data, chart.id, ], ) - return chart_data - async def chart_data(chart_id: int, data: str): """Add chart data to chart entry. diff --git a/backend/zeno_backend/processing/filtering.py b/backend/zeno_backend/processing/filtering.py index 5f13d7eb..40c8d642 100644 --- a/backend/zeno_backend/processing/filtering.py +++ b/backend/zeno_backend/processing/filtering.py @@ -87,7 +87,7 @@ async def filter_to_sql( raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Could not find column: {f.column.name} " - "for model {f.column.model}.", + f"for model {f.column.model}.", ) filt = ( filt diff --git a/backend/zeno_backend/routers/chart.py b/backend/zeno_backend/routers/chart.py index 62e9dab4..505d8a52 100644 --- a/backend/zeno_backend/routers/chart.py +++ b/backend/zeno_backend/routers/chart.py @@ -56,16 +56,34 @@ async def get_chart(project_uuid: str, chart_id: int, request: Request): HTTPException: error if the chart could not be fetched. Returns: - ChartResponse: chart spec and data. + ChartResponse: chart spec. """ await util.project_access_valid(project_uuid, request) - chart = await select.chart(chart_id) - if chart.data is None: - chart_data = await calculate_chart_data(chart, project_uuid) - await update.chart_data(chart_id, chart_data) - chart.data = chart_data + return await select.chart(chart_id) + + +@router.get("/chart-data/{project_uuid}/{chart_id}", response_model=str, tags=["Zeno"]) +async def get_chart_data(project_uuid, chart_id: int, request: Request): + """Get a chart's data. + + Args: + project_uuid (str): UUID of the project to get a chart from. + chart_id (int): id of the chart to be fetched. + request (Request): http request to get user information from. + + Raises: + HTTPException: error if the chart could not be fetched. - return chart + Returns: + str: chart data. + """ + await util.project_access_valid(project_uuid, request) + data = await select.chart_data(chart_id) + if data is None: + chart = await select.chart(chart_id) + data = await calculate_chart_data(chart, project_uuid) + await update.chart_data(chart_id, data) + return data @router.post( @@ -89,11 +107,6 @@ async def get_charts_for_projects(project_uuids: list[str], request: Request): for project_uuid in project_uuids: await util.project_access_valid(project_uuid, request) charts = await select.charts_for_projects(project_uuids) - for c in charts: - if c.data is None: - chart_data = await calculate_chart_data(c, c.project_uuid) - await update.chart_data(c.id, chart_data) - c.data = chart_data return charts @@ -142,7 +155,6 @@ async def add_chart( @router.patch( "/chart/{project_uuid}", - response_model=str, tags=["zeno"], dependencies=[Depends(util.auth)], ) @@ -157,7 +169,7 @@ async def update_chart(project_uuid: str, chart: Chart, request: Request): await util.project_editor(project_uuid, request) selected_chart = await select.chart(chart.id) if selected_chart.project_uuid == project_uuid: - return await update.chart(chart, project_uuid) + await update.chart(chart, project_uuid) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/frontend/src/lib/components/report/elements/ChartElement.svelte b/frontend/src/lib/components/report/elements/ChartElement.svelte index 82cbbe00..d4b7fc69 100644 --- a/frontend/src/lib/components/report/elements/ChartElement.svelte +++ b/frontend/src/lib/components/report/elements/ChartElement.svelte @@ -1,22 +1,29 @@

{chart.name}

- {#if chart.data} + {#await zenoClient.getChartData(chart.projectUuid, chart.id) then data}
- {/if} + {:catch error} +

+ Chart data could not be loaded: {error.message} +

+ {/await}
diff --git a/frontend/src/lib/zenoapi/models/Chart.ts b/frontend/src/lib/zenoapi/models/Chart.ts index 966fec52..214bfb2b 100644 --- a/frontend/src/lib/zenoapi/models/Chart.ts +++ b/frontend/src/lib/zenoapi/models/Chart.ts @@ -20,7 +20,6 @@ import type { XCParameters } from './XCParameters'; * type (ChartType): the type of the chart. * parameters (XCParameters | TableParameters | BeeswarmParameters | * RadarParameters | HeatmapParameters): the parameters of the chart. - * data (str): the JSON string data of the chart. */ export type Chart = { id: number; @@ -33,5 +32,4 @@ export type Chart = { | BeeswarmParameters | RadarParameters | HeatmapParameters; - data?: string | null; }; diff --git a/frontend/src/lib/zenoapi/models/Operation.ts b/frontend/src/lib/zenoapi/models/Operation.ts index 08d156cf..24c8db1c 100644 --- a/frontend/src/lib/zenoapi/models/Operation.ts +++ b/frontend/src/lib/zenoapi/models/Operation.ts @@ -16,6 +16,7 @@ * LIKE: like. * ILIKE: ilike. * REGEX: regex. + * NOT_REGEX: not matching regex. */ export enum Operation { EQUAL = 'EQUAL', diff --git a/frontend/src/lib/zenoapi/services/ZenoService.ts b/frontend/src/lib/zenoapi/services/ZenoService.ts index f09335fa..01e0bedf 100644 --- a/frontend/src/lib/zenoapi/services/ZenoService.ts +++ b/frontend/src/lib/zenoapi/services/ZenoService.ts @@ -281,7 +281,7 @@ export class ZenoService { * HTTPException: error if the chart could not be fetched. * * Returns: - * ChartResponse: chart spec and data. + * ChartResponse: chart spec. * @param chartId * @param projectUuid * @returns Chart Successful Response @@ -303,6 +303,72 @@ export class ZenoService { }); } + /** + * Get Chart Data + * Get a chart's data. + * + * Args: + * project_uuid (str): UUID of the project to get a chart from. + * chart_id (int): id of the chart to be fetched. + * request (Request): http request to get user information from. + * + * Raises: + * HTTPException: error if the chart could not be fetched. + * + * Returns: + * str: chart data. + * @param projectUuid + * @param chartId + * @returns string Successful Response + * @throws ApiError + */ + public getChartData(projectUuid: any, chartId: number): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/chart-data/{project_uuid}/{chart_id}', + path: { + project_uuid: projectUuid, + chart_id: chartId + }, + errors: { + 422: `Validation Error` + } + }); + } + + /** + * Get Chart Data + * Get a chart's data. + * + * Args: + * project_uuid (str): UUID of the project to get a chart from. + * chart_id (int): id of the chart to be fetched. + * request (Request): http request to get user information from. + * + * Raises: + * HTTPException: error if the chart could not be fetched. + * + * Returns: + * str: chart data. + * @param projectUuid + * @param chartId + * @returns string Successful Response + * @throws ApiError + */ + public getChartData1(projectUuid: any, chartId: number): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/chart-data/{project_uuid}/{chart_id}', + path: { + project_uuid: projectUuid, + chart_id: chartId + }, + errors: { + 422: `Validation Error` + } + }); + } + /** * Get Charts For Projects * Get all charts for a list of projects. diff --git a/frontend/src/routes/(app)/project/[uuid]/[name]/chart/[chartIndex=integer]/+page.svelte b/frontend/src/routes/(app)/project/[uuid]/[name]/chart/[chartIndex=integer]/+page.svelte index af480dae..4fcae0e1 100644 --- a/frontend/src/routes/(app)/project/[uuid]/[name]/chart/[chartIndex=integer]/+page.svelte +++ b/frontend/src/routes/(app)/project/[uuid]/[name]/chart/[chartIndex=integer]/+page.svelte @@ -18,8 +18,8 @@ let mounted = false; let isChartEdit: boolean | undefined; let chart = data.chart; - let chartData = chart.data ? JSON.parse(chart.data) : undefined; let updatingData = false; + let chartDataRequest = zenoClient.getChartData(chart.projectUuid, chart.id); $: updateEditUrl(isChartEdit); $: updateChart(chart); @@ -39,8 +39,8 @@ function updateChart(chart: Chart) { updatingData = true; if (mounted && $project && $project.editor && browser) { - zenoClient.updateChart($project.uuid, chart).then((d) => { - chartData = JSON.parse(d); + zenoClient.updateChart($project.uuid, chart).then(() => { + chartDataRequest = zenoClient.getChartData(chart.projectUuid, chart.id); updatingData = false; }); } else { @@ -66,11 +66,13 @@ {:else} {/if} - {#if chartData} -
- - - -
- {/if} + {#await chartDataRequest then data} + + + + {:catch error} +

+ Chart data could not be loaded: {error.message} +

+ {/await}