From cc08060e308cec2bbec6f9e4d07aeccb7b28c812 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Fri, 25 Mar 2022 20:18:31 +0100 Subject: [PATCH] feat(api): dashboard 2/5 --- api/app.py | 3 +- api/chalicelib/core/dashboards2.py | 65 +++++++++++---- api/chalicelib/core/templates.py | 19 +++++ api/requirements.txt | 2 +- api/routers/core.py | 72 ---------------- api/routers/subs/dashboard.py | 27 +----- api/routers/subs/metrics.py | 129 +++++++++++++++++++++++++++++ api/schemas.py | 17 ++++ 8 files changed, 217 insertions(+), 117 deletions(-) create mode 100644 api/chalicelib/core/templates.py create mode 100644 api/routers/subs/metrics.py diff --git a/api/app.py b/api/app.py index 4d3697e92..b1ddca682 100644 --- a/api/app.py +++ b/api/app.py @@ -12,7 +12,7 @@ from routers import core, core_dynamic from routers.app import v1_api from routers.crons import core_crons from routers.crons import core_dynamic_crons -from routers.subs import dashboard, insights +from routers.subs import dashboard, insights, metrics app = FastAPI() @@ -54,6 +54,7 @@ app.include_router(core_dynamic.public_app) app.include_router(core_dynamic.app) app.include_router(core_dynamic.app_apikey) app.include_router(dashboard.app) +app.include_router(metrics.app) app.include_router(insights.app) app.include_router(v1_api.app_apikey) diff --git a/api/chalicelib/core/dashboards2.py b/api/chalicelib/core/dashboards2.py index 5f2bced2d..782532718 100644 --- a/api/chalicelib/core/dashboards2.py +++ b/api/chalicelib/core/dashboards2.py @@ -1,3 +1,5 @@ +import json + import schemas from chalicelib.utils import helper from chalicelib.utils import pg_client @@ -29,32 +31,59 @@ def get_dashboards(project_id, user_id): def get_dashboard(project_id, user_id, dashboard_id): with pg_client.PostgresClient() as cur: - pg_query = """SELECT dashboards.*, all_widgets.* + pg_query = """SELECT dashboards.*, all_template_widgets.widgets AS template_widgets, all_metric_widgets.widgets AS metric_widgets FROM dashboards - LEFT JOIN LATERAL (SELECT COALESCE(ARRAY_AGG(widgets), '{}') AS widgets - FROM widgets - INNER JOIN dashboard_widgets USING (widget_id) + LEFT JOIN LATERAL (SELECT COALESCE(JSONB_AGG(templates), '[]'::jsonb) AS widgets + FROM templates + INNER JOIN dashboard_widgets USING (template_id) WHERE dashboard_widgets.dashboard_id = dashboards.dashboard_id - AND widgets.deleted_at ISNULL - AND (widgets.project_id ISNULL OR widgets.project_id = %(projectId)s) - ) AS all_widgets ON (TRUE) + ) AS all_template_widgets ON (TRUE) + LEFT JOIN LATERAL (SELECT COALESCE(JSONB_AGG(raw_metrics), '[]') AS widgets + FROM (SELECT metrics.*, metric_series.series + FROM metrics + INNER JOIN dashboard_widgets USING (metric_id) + LEFT JOIN LATERAL (SELECT JSONB_AGG(metric_series.* ORDER BY index) AS series + FROM metric_series + WHERE metric_series.metric_id = metrics.metric_id + AND metric_series.deleted_at ISNULL + ) AS metric_series ON (TRUE) + WHERE dashboard_widgets.dashboard_id = dashboards.dashboard_id + AND metrics.deleted_at ISNULL + AND metrics.project_id = %(projectId)s) AS raw_metrics + ) AS all_metric_widgets ON (TRUE) WHERE dashboards.deleted_at ISNULL AND dashboards.project_id = %(projectId)s AND dashboard_id = %(dashboard_id)s AND (dashboards.user_id = %(userId)s OR is_public);""" params = {"userId": user_id, "projectId": project_id, "dashboard_id": dashboard_id} + print(cur.mogrify(pg_query, params)) + cur.execute(cur.mogrify(pg_query, params)) + row = cur.fetchone() + row["widgets"] = row.pop("template_widgets") + row.pop("metric_widgets") + return helper.dict_to_camel_case(row) + + +def add_widget(project_id, user_id, dashboard_id, data: schemas.AddWidgetToDashboardPayloadSchema): + ref_key = "metric_id" + if data.template_id is not None: + ref_key = "template_id" + with pg_client.PostgresClient() as cur: + pg_query = f"""INSERT INTO dashboard_widgets(dashboard_id, {ref_key}, user_id, configuration, name) + VALUES (%(dashboard_id)s, %({ref_key})s, %(userId)s, %(configuration)s::jsonb, %(name)s) + RETURNING *;""" + params = {"userId": user_id, "projectId": project_id, "dashboard_id": dashboard_id, **data.dict()} + params["configuration"] = json.dumps(params.get("configuration", {})) cur.execute(cur.mogrify(pg_query, params)) row = cur.fetchone() return helper.dict_to_camel_case(row) - -def get_widgets(project_id): - with pg_client.PostgresClient() as cur: - pg_query = f"""SELECT * - FROM widgets - WHERE deleted_at ISNULL - AND project_id = %(projectId)s;""" - params = {"projectId": project_id} - cur.execute(cur.mogrify(pg_query, params)) - rows = cur.fetchall() - return helper.list_to_camel_case(rows) +# def get_widgets(project_id): +# with pg_client.PostgresClient() as cur: +# pg_query = f"""SELECT * +# FROM widgets +# WHERE deleted_at ISNULL +# AND project_id = %(projectId)s;""" +# params = {"projectId": project_id} +# cur.execute(cur.mogrify(pg_query, params)) +# rows = cur.fetchall() +# return helper.list_to_camel_case(rows) diff --git a/api/chalicelib/core/templates.py b/api/chalicelib/core/templates.py new file mode 100644 index 000000000..8141bdd35 --- /dev/null +++ b/api/chalicelib/core/templates.py @@ -0,0 +1,19 @@ +from chalicelib.utils import helper +from chalicelib.utils import pg_client + +CATEGORY_DESCRIPTION = { + 'categ1': 'lorem', +} + + +def get_templates(): + with pg_client.PostgresClient() as cur: + pg_query = f"""SELECT category, jsonb_agg(templates ORDER BY name) AS widgets + FROM templates + GROUP BY category + ORDER BY category;""" + cur.execute(pg_query) + rows = cur.fetchall() + for r in rows: + r["description"] = CATEGORY_DESCRIPTION.get(r["category"], "") + return helper.list_to_camel_case(rows) diff --git a/api/requirements.txt b/api/requirements.txt index 4af962f4f..e5672d80a 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -8,7 +8,7 @@ jira==2.0.0 -fastapi==0.74.1 +fastapi==0.75.0 uvicorn[standard]==0.17.5 python-decouple==3.6 pydantic[email]==1.8.2 diff --git a/api/routers/core.py b/api/routers/core.py index 97a749429..53fdba667 100644 --- a/api/routers/core.py +++ b/api/routers/core.py @@ -1065,78 +1065,6 @@ def change_client_password(data: schemas.EditUserPasswordSchema = Body(...), user_id=context.user_id) -@app.post('/{projectId}/custom_metrics/try', tags=["customMetrics"]) -@app.put('/{projectId}/custom_metrics/try', tags=["customMetrics"]) -def try_custom_metric(projectId: int, data: schemas.CreateCustomMetricsSchema = Body(...), - context: schemas.CurrentContext = Depends(OR_context)): - return {"data": custom_metrics.merged_live(project_id=projectId, data=data)} - - -@app.post('/{projectId}/custom_metrics', tags=["customMetrics"]) -@app.put('/{projectId}/custom_metrics', tags=["customMetrics"]) -def add_custom_metric(projectId: int, data: schemas.CreateCustomMetricsSchema = Body(...), - context: schemas.CurrentContext = Depends(OR_context)): - return custom_metrics.create(project_id=projectId, user_id=context.user_id, data=data) - - -@app.get('/{projectId}/custom_metrics', tags=["customMetrics"]) -def get_custom_metrics(projectId: int, context: schemas.CurrentContext = Depends(OR_context)): - return {"data": custom_metrics.get_all(project_id=projectId, user_id=context.user_id)} - - -@app.get('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"]) -def get_custom_metric(projectId: int, metric_id: int, context: schemas.CurrentContext = Depends(OR_context)): - data = custom_metrics.get(project_id=projectId, user_id=context.user_id, metric_id=metric_id) - if data is None: - return {"errors": ["custom metric not found"]} - return {"data": data} - - -@app.post('/{projectId}/custom_metrics/{metric_id}/sessions', tags=["customMetrics"]) -def get_custom_metric_sessions(projectId: int, metric_id: int, - data: schemas.CustomMetricSessionsPayloadSchema = Body(...), - context: schemas.CurrentContext = Depends(OR_context)): - data = custom_metrics.get_sessions(project_id=projectId, user_id=context.user_id, metric_id=metric_id, data=data) - if data is None: - return {"errors": ["custom metric not found"]} - return {"data": data} - - -@app.post('/{projectId}/custom_metrics/{metric_id}/chart', tags=["customMetrics"]) -def get_custom_metric_chart(projectId: int, metric_id: int, data: schemas.CustomMetricChartPayloadSchema = Body(...), - context: schemas.CurrentContext = Depends(OR_context)): - data = custom_metrics.make_chart(project_id=projectId, user_id=context.user_id, metric_id=metric_id, - data=data) - if data is None: - return {"errors": ["custom metric not found"]} - return {"data": data} - - -@app.post('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"]) -@app.put('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"]) -def update_custom_metric(projectId: int, metric_id: int, data: schemas.UpdateCustomMetricsSchema = Body(...), - context: schemas.CurrentContext = Depends(OR_context)): - data = custom_metrics.update(project_id=projectId, user_id=context.user_id, metric_id=metric_id, data=data) - if data is None: - return {"errors": ["custom metric not found"]} - return {"data": data} - - -@app.post('/{projectId}/custom_metrics/{metric_id}/status', tags=["customMetrics"]) -@app.put('/{projectId}/custom_metrics/{metric_id}/status', tags=["customMetrics"]) -def update_custom_metric_state(projectId: int, metric_id: int, - data: schemas.UpdateCustomMetricsStatusSchema = Body(...), - context: schemas.CurrentContext = Depends(OR_context)): - return { - "data": custom_metrics.change_state(project_id=projectId, user_id=context.user_id, metric_id=metric_id, - status=data.active)} - - -@app.delete('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"]) -def delete_custom_metric(projectId: int, metric_id: int, context: schemas.CurrentContext = Depends(OR_context)): - return {"data": custom_metrics.delete(project_id=projectId, user_id=context.user_id, metric_id=metric_id)} - - @app.post('/{projectId}/saved_search', tags=["savedSearch"]) @app.put('/{projectId}/saved_search', tags=["savedSearch"]) def add_saved_search(projectId: int, data: schemas.SavedSearchSchema = Body(...), diff --git a/api/routers/subs/dashboard.py b/api/routers/subs/dashboard.py index 1f5e827e4..169893693 100644 --- a/api/routers/subs/dashboard.py +++ b/api/routers/subs/dashboard.py @@ -1,10 +1,9 @@ -from fastapi import Body, Depends +from fastapi import Body import schemas -from chalicelib.core import dashboard, dashboards2 +from chalicelib.core import dashboard from chalicelib.core import metadata from chalicelib.utils import helper -from or_dependencies import OR_context from routers.base import get_routers public_app, app, app_apikey = get_routers() @@ -345,25 +344,3 @@ def get_dashboard_group(projectId: int, data: schemas.MetricPayloadSchema = Body *helper.explode_widget(dashboard.get_avg_cpu(project_id=projectId, **data.dict())), *helper.explode_widget(dashboard.get_avg_fps(project_id=projectId, **data.dict())), ]} - - -@app.post('/{projectId}/dashboards', tags=["dashboard", "metrics"]) -@app.put('/{projectId}/dashboards', tags=["dashboard", "metrics"]) -def create_dashboards(projectId: int, data: schemas.CreateDashboardSchema = Body(...), - context: schemas.CurrentContext = Depends(OR_context)): - return {"data": dashboards2.create_dashboard(project_id=projectId, user_id=context.user_id, data=data)} - - -@app.get('/{projectId}/dashboards', tags=["dashboard", "metrics"]) -def get_dashboards(projectId: int, context: schemas.CurrentContext = Depends(OR_context)): - return {"data": dashboards2.get_dashboards(project_id=projectId, user_id=context.user_id)} - - -@app.get('/{projectId}/dashboards/{dashboardId}', tags=["dashboard", "metrics"]) -def get_dashboards(projectId: int, dashboardId: int, context: schemas.CurrentContext = Depends(OR_context)): - return {"data": dashboards2.get_dashboard(project_id=projectId, user_id=context.user_id, dashboard_id=dashboardId)} - - -@app.get('/{projectId}/widgets', tags=["dashboard", "metrics"]) -def get_dashboards(projectId: int, context: schemas.CurrentContext = Depends(OR_context)): - return {"data": dashboards2.get_widgets(project_id=projectId)} diff --git a/api/routers/subs/metrics.py b/api/routers/subs/metrics.py new file mode 100644 index 000000000..9c0bb64ba --- /dev/null +++ b/api/routers/subs/metrics.py @@ -0,0 +1,129 @@ +from fastapi import Body, Depends + +import schemas +from chalicelib.core import dashboards2, templates, custom_metrics +from or_dependencies import OR_context +from routers.base import get_routers + +public_app, app, app_apikey = get_routers() + + +@app.post('/{projectId}/dashboards', tags=["dashboard", "metrics"]) +@app.put('/{projectId}/dashboards', tags=["dashboard", "metrics"]) +def create_dashboards(projectId: int, data: schemas.CreateDashboardSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + return {"data": dashboards2.create_dashboard(project_id=projectId, user_id=context.user_id, data=data)} + + +@app.get('/{projectId}/dashboards', tags=["dashboard", "metrics"]) +def get_dashboards(projectId: int, context: schemas.CurrentContext = Depends(OR_context)): + return {"data": dashboards2.get_dashboards(project_id=projectId, user_id=context.user_id)} + + +@app.get('/{projectId}/dashboards/{dashboardId}', tags=["dashboard", "metrics"]) +def get_dashboards(projectId: int, dashboardId: int, context: schemas.CurrentContext = Depends(OR_context)): + return {"data": dashboards2.get_dashboard(project_id=projectId, user_id=context.user_id, dashboard_id=dashboardId)} + + +@app.post('/{projectId}/dashboards/{dashboardId}/metrics', tags=["dashboard", "metrics"]) +@app.put('/{projectId}/dashboards/{dashboardId}/metrics', tags=["dashboard", "metrics"]) +def add_widget_to_dashboards(projectId: int, dashboardId: int, + data: schemas.AddWidgetToDashboardPayloadSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + return {"data": dashboards2.add_widget(project_id=projectId, user_id=context.user_id, dashboard_id=dashboardId, + data=data)} + + +# @app.get('/{projectId}/widgets', tags=["dashboard", "metrics"]) +# def get_dashboards(projectId: int, context: schemas.CurrentContext = Depends(OR_context)): +# return {"data": dashboards2.get_widgets(project_id=projectId)} + + +@app.get('/{projectId}/metrics/templates', tags=["dashboard", "metrics"]) +def get_templates(projectId: int, context: schemas.CurrentContext = Depends(OR_context)): + return {"data": templates.get_templates()} + + +@app.post('/{projectId}/metrics/try', tags=["dashboard"]) +@app.put('/{projectId}/metrics/try', tags=["dashboard"]) +@app.post('/{projectId}/custom_metrics/try', tags=["customMetrics"]) +@app.put('/{projectId}/custom_metrics/try', tags=["customMetrics"]) +def try_custom_metric(projectId: int, data: schemas.CreateCustomMetricsSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + return {"data": custom_metrics.merged_live(project_id=projectId, data=data)} + + +@app.post('/{projectId}/metrics', tags=["dashboard"]) +@app.put('/{projectId}/metrics', tags=["dashboard"]) +@app.post('/{projectId}/custom_metrics', tags=["customMetrics"]) +@app.put('/{projectId}/custom_metrics', tags=["customMetrics"]) +def add_custom_metric(projectId: int, data: schemas.CreateCustomMetricsSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + return custom_metrics.create(project_id=projectId, user_id=context.user_id, data=data) + + +@app.get('/{projectId}/metrics', tags=["dashboard"]) +@app.get('/{projectId}/custom_metrics', tags=["customMetrics"]) +def get_custom_metrics(projectId: int, context: schemas.CurrentContext = Depends(OR_context)): + return {"data": custom_metrics.get_all(project_id=projectId, user_id=context.user_id)} + + +@app.get('/{projectId}/metrics/{metric_id}', tags=["dashboard"]) +@app.get('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"]) +def get_custom_metric(projectId: int, metric_id: int, context: schemas.CurrentContext = Depends(OR_context)): + data = custom_metrics.get(project_id=projectId, user_id=context.user_id, metric_id=metric_id) + if data is None: + return {"errors": ["custom metric not found"]} + return {"data": data} + + +@app.post('/{projectId}/metrics/{metric_id}/sessions', tags=["dashboard"]) +@app.post('/{projectId}/custom_metrics/{metric_id}/sessions', tags=["customMetrics"]) +def get_custom_metric_sessions(projectId: int, metric_id: int, + data: schemas.CustomMetricSessionsPayloadSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + data = custom_metrics.get_sessions(project_id=projectId, user_id=context.user_id, metric_id=metric_id, data=data) + if data is None: + return {"errors": ["custom metric not found"]} + return {"data": data} + + +@app.post('/{projectId}/metrics/{metric_id}/chart', tags=["dashboard"]) +@app.post('/{projectId}/custom_metrics/{metric_id}/chart', tags=["customMetrics"]) +def get_custom_metric_chart(projectId: int, metric_id: int, data: schemas.CustomMetricChartPayloadSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + data = custom_metrics.make_chart(project_id=projectId, user_id=context.user_id, metric_id=metric_id, + data=data) + if data is None: + return {"errors": ["custom metric not found"]} + return {"data": data} + + +@app.post('/{projectId}/metrics/{metric_id}', tags=["dashboard"]) +@app.put('/{projectId}/metrics/{metric_id}', tags=["dashboard"]) +@app.post('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"]) +@app.put('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"]) +def update_custom_metric(projectId: int, metric_id: int, data: schemas.UpdateCustomMetricsSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + data = custom_metrics.update(project_id=projectId, user_id=context.user_id, metric_id=metric_id, data=data) + if data is None: + return {"errors": ["custom metric not found"]} + return {"data": data} + + +@app.post('/{projectId}/metrics/{metric_id}/status', tags=["dashboard"]) +@app.put('/{projectId}/metrics/{metric_id}/status', tags=["dashboard"]) +@app.post('/{projectId}/custom_metrics/{metric_id}/status', tags=["customMetrics"]) +@app.put('/{projectId}/custom_metrics/{metric_id}/status', tags=["customMetrics"]) +def update_custom_metric_state(projectId: int, metric_id: int, + data: schemas.UpdateCustomMetricsStatusSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + return { + "data": custom_metrics.change_state(project_id=projectId, user_id=context.user_id, metric_id=metric_id, + status=data.active)} + + +@app.delete('/{projectId}/metrics/{metric_id}', tags=["dashboard"]) +@app.delete('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"]) +def delete_custom_metric(projectId: int, metric_id: int, context: schemas.CurrentContext = Depends(OR_context)): + return {"data": custom_metrics.delete(project_id=projectId, user_id=context.user_id, metric_id=metric_id)} diff --git a/api/schemas.py b/api/schemas.py index 61b1f4056..bb111d61b 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -884,3 +884,20 @@ class CreateDashboardSchema(BaseModel): class Config: alias_generator = attribute_to_camel_case + + +class AddWidgetToDashboardPayloadSchema(BaseModel): + template_id: Optional[int] = Field(default=None) + metric_id: Optional[int] = Field(default=None) + name: Optional[str] = Field(default=None) + configuration: dict = Field(default={}) + + @root_validator + def validator(cls, values): + assert bool(values.get("template_id") is not None) != bool(values.get("metric_id") is not None), \ + f"templateId or metricId should be provided, but not both at the same time" + + return values + + class Config: + alias_generator = attribute_to_camel_case