Conversation
1fc0cb0 to
d8f39ff
Compare
767dec7 to
d9d54f8
Compare
There was a problem hiding this comment.
Charts are not created when this file is missing, even though the dashboard import API returns no error.
I am keeping it for now, mocking sqlalchemy_uri. The end goal should be dashboard import would work without this file.
There was a problem hiding this comment.
Pull request overview
This pull request introduces functionality to create and seed an AWS Benchmark dashboard in the analytics system. The dashboard visualizes cost optimization opportunities and efficiency metrics for AWS resources, aggregating data from opportunities and timeseries tables.
Changes:
- Added AWS Benchmark dashboard asset files (YAML configurations for charts, datasets, databases, and dashboard layout)
- Implemented TypeScript functions to dynamically create virtual datasets and import dashboard configurations
- Extended database connection handling to return UUIDs alongside IDs for dashboard mapping
- Added FormData upload support for dashboard ZIP imports
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
metadata.yaml |
Dashboard metadata configuration with version and timestamp |
AWS_Benchmark_Opportunities.yaml |
Dataset definition filtering opportunities table for AWS Benchmark workflows |
AWS_Benchmark_KPI_efficiency.yaml |
Virtual dataset computing efficiency KPI from opportunities and cost data |
openops_tables_connection.yaml |
Database connection configuration (placeholder, replaced during import) |
AWS_Benchmark.yaml |
Dashboard layout with 7 charts showing savings, opportunities, and breakdowns |
Unified_Cost_Efficiency_Metric_8.yaml, Total_Opportunities_2.yaml, etc. |
Chart configurations for dashboard visualizations |
create-database-connection.ts |
Enhanced to return database UUID for mapping during import |
create-aws-benchmark-dashboard.ts |
Main orchestration function for dashboard creation |
benchmark-dashboard-helpers.ts |
Utility functions for ZIP creation, import, and table resolution |
requests-helpers.ts |
Added FormData POST support for dashboard import API |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/server/api/src/app/openops-analytics/create-aws-benchmark-dashboard.ts
Outdated
Show resolved
Hide resolved
| async function createAwsBenchmarkDatasets( | ||
| token: string, | ||
| databaseId: number, | ||
| opportunitiesTableName: string, | ||
| timeseriesTableName: string, | ||
| ) { | ||
| const opportunities = await createVirtualDataset(token, { | ||
| tableName: 'AWS_Benchmark_Opportunities', | ||
| sql: `SELECT * | ||
| FROM public."${opportunitiesTableName}" | ||
| WHERE "Workflow" LIKE 'AWS Benchmark%' | ||
| `, | ||
| databaseId, | ||
| schema: 'public', | ||
| recreateIfExists: true, | ||
| }); | ||
|
|
||
| const kpi = await createVirtualDataset(token, { | ||
| tableName: 'AWS_Benchmark_KPI_efficiency', | ||
| sql: `WITH opp AS ( | ||
| SELECT | ||
| "Account" AS opp_account, | ||
| COALESCE(SUM("Estimated savings USD per month"), 0) AS opp_sum | ||
| FROM public."${opportunitiesTableName}" | ||
| WHERE "Workflow" LIKE 'AWS Benchmark%' | ||
| GROUP BY "Account" | ||
| ), | ||
| ts AS ( | ||
| SELECT | ||
| "Account" AS ts_account, | ||
| COALESCE(SUM("Value"), 0) AS ts_value | ||
| FROM public."${timeseriesTableName}" | ||
| WHERE "Workflow" = 'Run AWS Benchmark' | ||
| AND "Date" = (date_trunc('month', current_date) - interval '1 month')::date | ||
| GROUP BY "Account" | ||
| ) | ||
| SELECT | ||
| COALESCE(opp.opp_account, ts.ts_account) AS "Account", | ||
| COALESCE(opp.opp_sum, 0) AS savings, | ||
| COALESCE(ts.ts_value, 0) AS monthly_cost, | ||
| (1 - (COALESCE(opp.opp_sum, 0) / NULLIF(COALESCE(ts.ts_value, 0), 0))) * 100.0 AS kpi | ||
| FROM opp | ||
| FULL OUTER JOIN ts | ||
| ON opp.opp_account = ts.ts_account; | ||
| `, | ||
| databaseId, | ||
| schema: 'public', | ||
| recreateIfExists: true, | ||
| }); | ||
|
|
||
| return { opportunities, kpi }; |
There was a problem hiding this comment.
Missing error handling: If the dataset creation fails (either 'opportunities' or 'kpi' dataset), the function should handle the error gracefully. Currently, if createVirtualDataset throws an error, it will propagate up without any logging or cleanup. Consider wrapping the dataset creation calls in try-catch blocks to provide more informative error messages and ensure proper cleanup if needed.
There was a problem hiding this comment.
createVirtualDataset errors bubble to Fastify's global handler → HTTP 500, which is correct, and createDataset already logs each success with table name and dataset ID, while makeOpenOpsAnalyticsV1ApiRequest logs the route on failure; there's no cleanup needed since recreateIfExists: true makes the next call idempotent anyway.
| parents: | ||
| - ROOT_ID | ||
| - GRID_ID | ||
| - ROW-OryzygZMMHPbYac9DWioo |
There was a problem hiding this comment.
Inconsistent parent-child relationship: CHART-explore-8-1 declares 'ROW-OryzygZMMHPbYac9DWioo' as its parent on line 106, but ROW-ndno4IT57hnxkF0gN3Pp2 lists CHART-explore-8-1 as a child on line 179. Additionally, ROW-OryzygZMMHPbYac9DWioo is not defined anywhere in the dashboard configuration and is not listed in GRID_ID's children. This creates an inconsistent state. The parent reference should be updated to 'ROW-ndno4IT57hnxkF0gN3Pp2' to match the actual containment structure.
| - ROW-OryzygZMMHPbYac9DWioo | |
| - ROW-ndno4IT57hnxkF0gN3Pp2 |
| export function getDashboardFolderPath(providerKey: string): string { | ||
| return path.join( | ||
| process.cwd(), | ||
| 'packages', | ||
| 'server', | ||
| 'api', | ||
| 'src', | ||
| 'assets', | ||
| `${providerKey}-benchmark-dashboard`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Hard-coded relative path may break in different execution contexts: The getDashboardFolderPath function uses process.cwd() to construct the path to assets. This assumes the current working directory is the repository root, which may not be true in all execution contexts (e.g., when running tests, in production environments, or when the script is invoked from a different directory). Consider using __dirname or import.meta.url to create a path relative to the current file, or use a configurable base path from system properties.
There was a problem hiding this comment.
For the two actual execution contexts (local dev + Docker), the code is correct and consistent with existing patterns.This pattern is also an established convention in the repo ( see https://github.com/openops-cloud/openops/blob/main/packages/server/api/src/app/ai/chat/prompts.service.ts )
packages/server/api/src/app/openops-analytics/create-aws-benchmark-dashboard.ts
Outdated
Show resolved
Hide resolved
| } | ||
|
|
||
| const USERFRIENDLTABLE_SUFFIX = '_userfriendly'; | ||
|
|
There was a problem hiding this comment.
Missing JSDoc documentation for public functions: The exported functions createAwsBenchmarkDashboard, createDashboardZipBuffer, importDashboardFromZip, and resolveTableIds lack documentation comments explaining their purpose, parameters, return values, and potential side effects. Given that these are public APIs that will be used by other parts of the codebase, they should have comprehensive JSDoc comments to improve maintainability and developer experience.
| /** | |
| * Seeds the AWS Benchmark dashboard for the oldest organization. | |
| * | |
| * This function: | |
| * - Authenticates as the OpenOps Analytics admin. | |
| * - Ensures a Postgres connection for the OpenOps tables database. | |
| * - Resolves the default project and associated table IDs. | |
| * - Creates the virtual datasets required by the AWS Benchmark dashboard. | |
| * - Imports the AWS Benchmark dashboard definition using the created datasets. | |
| * | |
| * @returns A promise that resolves when the AWS Benchmark dashboard | |
| * has been successfully created and imported. | |
| * | |
| * @throws If required system properties are missing, or if the | |
| * organization or project cannot be resolved. | |
| */ |
| function addFolderToZip(folderPath: string, zipFolder: JSZip): void { | ||
| const entries = fs.readdirSync(folderPath); | ||
| for (const entry of entries) { | ||
| const fullPath = path.join(folderPath, entry); | ||
| const stat = fs.statSync(fullPath); | ||
| if (stat.isDirectory()) { | ||
| const subFolder = zipFolder.folder(entry); | ||
| if (subFolder) { | ||
| addFolderToZip(fullPath, subFolder); | ||
| } | ||
| } else { | ||
| zipFolder.file(entry, fs.readFileSync(fullPath)); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing error handling for file system operations: The addFolderToZip function performs synchronous file system operations (fs.readdirSync, fs.statSync, fs.readFileSync) without error handling. If any of these operations fail (e.g., due to missing files, permission issues, or IO errors), the error will propagate without context. Consider wrapping these operations in try-catch blocks and providing more informative error messages that include the problematic file path.
There was a problem hiding this comment.
Node's fs errors already include the offending path, and a missing asset directory is a deployment error that correctly propagates as an HTTP 500 through Fastify's global error handler — no try/catch needed.
| sql: `SELECT * | ||
| FROM public."${opportunitiesTableName}" | ||
| WHERE "Workflow" LIKE 'AWS Benchmark%' | ||
| `, | ||
| databaseId, | ||
| schema: 'public', | ||
| recreateIfExists: true, | ||
| }); | ||
|
|
||
| const kpi = await createVirtualDataset(token, { | ||
| tableName: 'AWS_Benchmark_KPI_efficiency', | ||
| sql: `WITH opp AS ( | ||
| SELECT | ||
| "Account" AS opp_account, | ||
| COALESCE(SUM("Estimated savings USD per month"), 0) AS opp_sum | ||
| FROM public."${opportunitiesTableName}" | ||
| WHERE "Workflow" LIKE 'AWS Benchmark%' | ||
| GROUP BY "Account" | ||
| ), | ||
| ts AS ( | ||
| SELECT | ||
| "Account" AS ts_account, | ||
| COALESCE(SUM("Value"), 0) AS ts_value | ||
| FROM public."${timeseriesTableName}" | ||
| WHERE "Workflow" = 'Run AWS Benchmark' | ||
| AND "Date" = (date_trunc('month', current_date) - interval '1 month')::date | ||
| GROUP BY "Account" | ||
| ) | ||
| SELECT | ||
| COALESCE(opp.opp_account, ts.ts_account) AS "Account", | ||
| COALESCE(opp.opp_sum, 0) AS savings, | ||
| COALESCE(ts.ts_value, 0) AS monthly_cost, | ||
| (1 - (COALESCE(opp.opp_sum, 0) / NULLIF(COALESCE(ts.ts_value, 0), 0))) * 100.0 AS kpi | ||
| FROM opp | ||
| FULL OUTER JOIN ts | ||
| ON opp.opp_account = ts.ts_account; |
There was a problem hiding this comment.
Potential SQL injection vulnerability: The table name variables 'opportunitiesTableName' and 'timeseriesTableName' are directly interpolated into SQL strings without proper sanitization or parameterization. While these values appear to come from internal sources (constructed from constants and table IDs), they should still be properly sanitized or validated to prevent SQL injection. Consider using parameterized queries or at least validating that these table names only contain safe characters (alphanumeric, underscores).
There was a problem hiding this comment.
No user input touches this path at any point. The numeric table.id coming from the database is typed as number and cannot contain SQL metacharacters.
packages/server/api/src/app/openops-analytics/create-aws-benchmark-dashboard.ts
Outdated
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| const dbConnection = await getOrCreatePostgresDatabaseConnection( | ||
| access_token, | ||
| system.getOrThrow(AppSystemProp.OPENOPS_TABLES_DATABASE_NAME), | ||
| system.getOrThrow(AppSystemProp.POSTGRES_PASSWORD), | ||
| system.getOrThrow(AppSystemProp.POSTGRES_PORT), | ||
| system.getOrThrow(AppSystemProp.POSTGRES_USERNAME), | ||
| system.get(AppSystemProp.OPENOPS_TABLES_DB_HOST) ?? | ||
| system.getOrThrow(AppSystemProp.POSTGRES_HOST), | ||
| 'openops_tables_connection', | ||
| ); |
There was a problem hiding this comment.
We are using this block in 2 different places. Please extract it to a method.
| return Object.fromEntries(entries); | ||
| } catch (error) { | ||
| logger.error('Could not find required tables', { tableNames, error }); | ||
| return null; |
There was a problem hiding this comment.
Throw the error here. There's no need to do it outside.
throw new Error(
'Could not resolve required table IDs for benchmark dashboard',
);
| const formData = new FormData(); | ||
|
|
||
| formData.append('formData', zipBuffer, { | ||
| filename: 'dashboard.zip', | ||
| contentType: 'application/zip', | ||
| }); | ||
|
|
||
| formData.append('overwrite', 'true'); | ||
|
|
||
| const databaseMapping = { | ||
| openops_tables_connection: databaseUuid, | ||
| }; | ||
| formData.append('database_mapping', JSON.stringify(databaseMapping)); | ||
| formData.append('dataset_mapping', JSON.stringify(datasetMapping)); | ||
|
|
||
| logger.info('Sending dashboard import request', { | ||
| databaseMapping, | ||
| datasetMapping, | ||
| }); | ||
|
|
||
| await makeOpenOpsAnalyticsPostFormData('dashboard/import/', token, formData); | ||
|
|
||
| logger.info('Successfully imported dashboard'); |
There was a problem hiding this comment.
Add a try-catch block. This way, if something goes wrong, we'll have the error log in the right place.
|
| return { opportunities, kpi }; | ||
| } | ||
|
|
||
| const USERFRIENDLYTABLE_SUFFIX = '_userfriendly'; |
There was a problem hiding this comment.
And move it to the top of the file
| const USERFRIENDLYTABLE_SUFFIX = '_userfriendly'; | |
| const USER_FRIENDLY_TABLE_SUFFIX = '_userfriendly'; |
| ); | ||
| return Object.fromEntries(entries); | ||
| } catch (error) { | ||
| logger.error('Could not find required tables', { tableNames, error }); |
There was a problem hiding this comment.
| logger.error('Could not find required tables', { tableNames, error }); | |
| logger.error('Could not resolve required table IDs for benchmark dashboard', { tableNames, error }); |
| throw new Error( | ||
| 'Could not resolve required table IDs for benchmark dashboard', | ||
| ); |
There was a problem hiding this comment.
This preserves the original stack entirely.
| throw new Error( | |
| 'Could not resolve required table IDs for benchmark dashboard', | |
| ); | |
| throw error; |



Closes OPS-3738.
I tested locally by calling
createAwsBenchmarkDashboard()frompackages/server/api/src/main.ts.I made sure the creation is idempotent, the new analytics dashboard and the resources it depends on (dataset, charts) are recreated with every run.