Add analytics tab for Risks and Model Inventory#2641
Conversation
…into hp-nov-7-add-analytics
|
Warning Rate limit exceeded@gorkem-bwl has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 20 minutes and 30 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (2)
WalkthroughAdds full historical analytics for models and risks: client API wrappers, UI drawer and charts, server controllers/routes, Sequelize models/interfaces, per-tenant migrations, history utilities with interpolation and snapshot recording, and hooks to record snapshots during create/update/delete flows. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Page as UI Page
participant Drawer as AnalyticsDrawer
participant Chart as HistoryChart
participant Repo as Client Repo
participant API as Server Controller
participant DB as Database
User->>Page: Click Analytics button
Page->>Drawer: open()
Drawer->>Chart: init(selectedParameter, timeframe)
Chart->>Repo: getTimeseries(parameter, timeframe)
Repo->>API: GET /api/{model|risk}History/timeseries?parameter=...&timeframe=...
API->>DB: query snapshots & interpolation
DB-->>API: timeseries rows
API-->>Repo: timeseries payload
Repo-->>Chart: data
Chart->>Chart: map snapshots → series
Chart-->>Drawer: render(chart)
sequenceDiagram
participant App as CRUD Operation
participant Util as entity util (modelInventory/risk)
participant History as history.utils
participant DB as Database
App->>Util: create/update/delete(...)
Util->>History: recordSnapshotIfChanged(parameter, tenant, userId, tx)
History->>DB: getLatestSnapshot(parameter)
DB-->>History: latestSnapshot/null
History->>History: compare currentCounts vs latest
alt changed
History->>DB: INSERT new snapshot
DB-->>History: new record
History-->>Util: snapshot created
else unchanged
History-->>Util: no-op
end
Util-->>App: complete
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
|
||
| return result[0]; | ||
| } catch (error) { | ||
| console.error(`Error recording history snapshot for parameter ${parameter}:`, error); |
Check failure
Code scanning / CodeQL
Use of externally-controlled format string High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
To eliminate the "Use of externally-controlled format string" risk, we should avoid embedding user-controlled input inside a template string or as part of the format string for logging. Instead, pass a constant format string with a %s substitution, and provide the untrusted data as a separate argument. Specifically:
- On line 43 of
Servers/utils/history/modelInventoryHistory.utils.ts, replace:with:console.error(`Error recording history snapshot for parameter ${parameter}:`, error);
console.error("Error recording history snapshot for parameter %s:", parameter, error);
This ensures that any format specifiers in parameter will not be interpreted as part of the format string, regardless of the runtime environment’s behavior.
No new imports or function definitions are required; this is a simple fix confined to the logging line.
| @@ -40,7 +40,7 @@ | ||
|
|
||
| return result[0]; | ||
| } catch (error) { | ||
| console.error(`Error recording history snapshot for parameter ${parameter}:`, error); | ||
| console.error("Error recording history snapshot for parameter %s:", parameter, error); | ||
| throw error; | ||
| } | ||
| } |
| `SELECT ${parameter}, COUNT(*) as count | ||
| FROM "${tenant}".model_inventories | ||
| GROUP BY ${parameter}`, |
Check failure
Code scanning / CodeQL
Database query built from user-controlled sources High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
To fix this problem, the code should only allow a safe, expected set of parameter names to be used as SQL column names. This means introducing a whitelist/checklist of allowed parameter/column names (such as 'status', 'security_assessment', or other known, safe strings), and refusing any value that does not match these. For generic handling (the else block), instead of directly interpolating parameter into the SQL string, the code should check that it is an exact match of a known column name. If not, it should throw an error or return a 400 status. Implementation requires:
- Defining a list of allowed column names near the top of the utils file.
- In
getCurrentParameterCounts, before using${parameter}in the query, check if it is in the allowed list; if not, throw an error. - No external libraries are strictly required, but you may define the allowed list where convenient.
Edit only in the shownServers/utils/history/modelInventoryHistory.utils.tssnippet.
| @@ -3,6 +3,19 @@ | ||
| import { ModelInventoryStatus } from "../../domain.layer/enums/model-inventory-status.enum"; | ||
| import { Transaction, QueryTypes } from "sequelize"; | ||
|
|
||
|
|
||
| // Allowed parameter/column names for getCurrentParameterCounts | ||
| const ALLOWED_PARAMETERS = [ | ||
| "status", | ||
| "security_assessment", | ||
| // Add other allowed model_inventories columns below as needed | ||
| "model_type", | ||
| "owner", | ||
| "created_at", | ||
| "updated_at", | ||
| // ...etc | ||
| ]; | ||
|
|
||
| /** | ||
| * Record a snapshot of parameter counts in history | ||
| */ | ||
| @@ -100,11 +113,15 @@ | ||
| } | ||
| }); | ||
| } else { | ||
| // Generic handling for other parameters | ||
| // Only allow known, allowed column names for generic parameters | ||
| if (!ALLOWED_PARAMETERS.includes(parameter)) { | ||
| throw new Error(`Invalid or disallowed parameter for model inventory counts: ${parameter}`); | ||
| } | ||
| // Safe to use: parameter is whitelisted as a column name | ||
| const paramCounts = await sequelize.query( | ||
| `SELECT ${parameter}, COUNT(*) as count | ||
| `SELECT "${parameter}", COUNT(*) as count | ||
| FROM "${tenant}".model_inventories | ||
| GROUP BY ${parameter}`, | ||
| GROUP BY "${parameter}"`, | ||
| { | ||
| type: QueryTypes.SELECT, | ||
| transaction, |
|
|
||
| return counts; | ||
| } catch (error) { | ||
| console.error(`Error getting current parameter counts for ${parameter}:`, error); |
Check failure
Code scanning / CodeQL
Use of externally-controlled format string High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
To fix this issue, we should avoid directly injecting untrusted user input into the format string within a template literal. Instead, we should use a static format string and pass the untrusted value as an argument to the logging function, e.g. passing %s as a placeholder and providing parameter as the corresponding value.
- In file
Servers/utils/history/modelInventoryHistory.utils.ts, specifically at line 122, replace:with:console.error(`Error getting current parameter counts for ${parameter}:`, error);
console.error("Error getting current parameter counts for %s:", parameter, error);
This ensures that the value for parameter is not used to control the logging format and is safely rendered as a string.
No additional imports or dependencies are required, as these are built-in JavaScript patterns.
| @@ -119,7 +119,7 @@ | ||
|
|
||
| return counts; | ||
| } catch (error) { | ||
| console.error(`Error getting current parameter counts for ${parameter}:`, error); | ||
| console.error("Error getting current parameter counts for %s:", parameter, error); | ||
| throw error; | ||
| } | ||
| } |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Clients/src/presentation/components/AddNewRiskForm/index.tsx (1)
431-455: Fix TypeScript error: Make getChangedFields generic.The function signature expects
RiskFormValues & MitigationFormValuesbut at lines 635-636 it's called with separateRiskFormValuesandMitigationFormValuestypes, causing the pipeline failure.Apply this diff to make the function generic:
- const getChangedFields = useCallback(( - original: RiskFormValues & MitigationFormValues, - current: RiskFormValues & MitigationFormValues - ) => { + const getChangedFields = useCallback(<T extends Record<string, any>>( + original: T, + current: T + ): Partial<T> => { const changedFields: any = {}; // Check each field for changes Object.keys(current).forEach((key) => { - const originalValue = original[key as keyof (RiskFormValues & MitigationFormValues)]; - const currentValue = current[key as keyof (RiskFormValues & MitigationFormValues)]; + const originalValue = original[key as keyof T]; + const currentValue = current[key as keyof T]; // For arrays, do deep comparison if (Array.isArray(originalValue) && Array.isArray(currentValue)) {
🧹 Nitpick comments (11)
Clients/src/presentation/components/AddNewRiskForm/index.tsx (2)
524-554: Well-structured field mapping with clear prefixes.The prefixing approach effectively distinguishes risk vs mitigation fields and the mapping is comprehensive.
Consider extracting the field mapping into a typed constant at the module level for better maintainability and type safety:
const FIELD_MAPPING = { risk: { riskName: 'risk_name', actionOwner: 'risk_owner', // ... other risk fields }, mitigation: { mitigationStatus: 'mitigation_status', // ... other mitigation fields } } as const;
634-664: Consider stronger typing for changedFields object.The logic for computing changed fields separately and combining with prefixes is sound, and the boolean flags correctly use prefixed keys.
Based on learnings, avoid using
anytype when a more specific type is available. Consider:- const changedFields: any = {}; + const changedFields: Record<string, any> = {};Or better yet, create a proper type:
type PrefixedRiskFields = { [K in keyof RiskFormValues as `risk_${string & K}`]: RiskFormValues[K] }; type PrefixedMitigationFields = { [K in keyof MitigationFormValues as `mitigation_${string & K}`]: MitigationFormValues[K] }; type ChangedFields = Partial<PrefixedRiskFields & PrefixedMitigationFields>; const changedFields: ChangedFields = {};Clients/src/presentation/pages/RiskManagement/index.tsx (1)
390-405: Use theme references instead of hardcoded color values.The Analytics button uses hardcoded color values (
#7F56D9,#6941C6) which should be replaced with theme references for consistency and maintainability.Apply this diff to use theme colors:
<div data-joyride-id="analytics-button"> <CustomizableButton variant="contained" text="Analytics" sx={{ - backgroundColor: "#7F56D9", - border: "1px solid #7F56D9", + backgroundColor: "primary.main", + border: "1px solid", + borderColor: "primary.main", gap: 2, "&:hover": { - backgroundColor: "#6941C6", + backgroundColor: "primary.dark", }, }} onClick={() => setIsAnalyticsDrawerOpen(true)} icon={<TrendingUp size={16} />} /> </div>Servers/scripts/createNewTenant.ts (1)
977-993: Consider adding indexes for query performance.The history tables are properly structured with appropriate constraints. However, for better query performance on time-series data retrieval, consider adding indexes on frequently queried columns.
Add indexes after table creation:
await sequelize.query(`CREATE TABLE "${tenantHash}".risk_history ( id SERIAL PRIMARY KEY, parameter VARCHAR(255) NOT NULL, snapshot_data JSONB NOT NULL DEFAULT '{}', recorded_at TIMESTAMP NOT NULL DEFAULT NOW(), triggered_by_user_id INTEGER REFERENCES public.users(id) ON DELETE SET NULL, created_at TIMESTAMP DEFAULT NOW() );`, { transaction }); // Add indexes for performance await sequelize.query(` CREATE INDEX idx_risk_history_parameter ON "${tenantHash}".risk_history(parameter); CREATE INDEX idx_risk_history_recorded_at ON "${tenantHash}".risk_history(recorded_at DESC); `, { transaction }); await sequelize.query(`CREATE TABLE "${tenantHash}".model_inventory_history ( id SERIAL PRIMARY KEY, parameter VARCHAR(255) NOT NULL, snapshot_data JSONB NOT NULL DEFAULT '{}', recorded_at TIMESTAMP NOT NULL DEFAULT NOW(), triggered_by_user_id INTEGER REFERENCES public.users(id) ON DELETE SET NULL, created_at TIMESTAMP DEFAULT NOW() );`, { transaction }); // Add indexes for performance await sequelize.query(` CREATE INDEX idx_model_inventory_history_parameter ON "${tenantHash}".model_inventory_history(parameter); CREATE INDEX idx_model_inventory_history_recorded_at ON "${tenantHash}".model_inventory_history(recorded_at DESC); `, { transaction });Clients/src/presentation/pages/ModelInventory/index.tsx (2)
877-896: Use theme references for button styling.The Analytics button reuses
addNewModelButtonStylewhich contains hardcoded color values (#13715B). Consider creating a theme-based style or using theme references directly.Replace the hardcoded style:
<CustomizableButton variant="contained" onClick={() => setIsAnalyticsDrawerOpen(true)} - sx={addNewModelButtonStyle} + sx={{ + backgroundColor: "secondary.main", + border: "1px solid", + borderColor: "secondary.main", + }} icon={<TrendingUp size={16} />} text="Analytics" />
1009-1021: Consider adding more analytics parameters for comprehensive insights.The analytics drawer currently only tracks "status" parameter. Consider adding additional parameters like provider, security_assessment status, or other relevant model inventory attributes to provide more comprehensive analytics.
Example:
<AnalyticsDrawer open={isAnalyticsDrawerOpen} onClose={() => setIsAnalyticsDrawerOpen(false)} title="Analytics & Trends" description="Track your model inventory history over time" entityName="Model" + chartType="modelInventory" availableParameters={[ { value: "status", label: "Status" }, - // Add more parameters here as needed + { value: "provider", label: "Provider" }, + { value: "security_assessment", label: "Security Assessment" }, ]} defaultParameter="status" />Servers/utils/modelInventory.utils.ts (1)
255-262: Consider tracking additional parameters.Currently only the
statusparameter is tracked for model inventory history. Based on the interface comments ini.modelInventoryHistory.tsthat mentionsecurity_assessmentas an example, consider whether other parameters likesecurity_assessmentshould also be tracked.Servers/database/migrations/20251105000000-create-model-inventory-history-table.js (1)
27-43: Review index strategy for potential redundancy.Three indexes are created:
- Single column on
parameter- Single column on
recorded_at DESC- Composite on
(parameter, recorded_at DESC)The composite index can serve queries filtering by
parameteralone, making the single-columnparameterindex potentially redundant. Consider whether all three indexes are needed based on actual query patterns, as each index adds overhead to write operations.Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1)
41-63: Satisfy the hooks dependency rule
fetchTimeseriesDatais defined outside the effect but isn’t part of the dependency array, which will tripreact-hooks/exhaustive-depsin our lint pipeline. To keep the build green, either wrap the fetcher inuseCallbackand include it in the dependency list or inline the async logic inside the effect.- useEffect(() => { - fetchTimeseriesData(); - }, [timeframe, parameter]); - - const fetchTimeseriesData = async () => { + useEffect(() => { + const fetchTimeseriesData = async () => { setLoading(true); setError(null); try { const response = await getModelInventoryTimeseries(parameter, timeframe); if (response?.data?.data) { setTimeseriesData(response.data.data.data); } } catch (err: any) { console.error("Error fetching timeseries data:", err); setError(err?.response?.data?.message || "Failed to load chart data"); } finally { setLoading(false); } }; - }; + + fetchTimeseriesData(); + }, [parameter, timeframe]);Servers/utils/history/riskHistory.utils.ts (1)
50-182: Consider extracting common counting pattern to reduce duplication.The function contains four nearly identical blocks (severity, likelihood, mitigation_status, risk_level) that follow the same pattern: query counts, initialize all possible values to 0, then update with actual counts. This duplication increases maintenance burden.
Consider extracting a helper function:
async function getCountsForEnum( column: string, possibleValues: string[], tenant: string, transaction?: Transaction ): Promise<Record<string, number>> { const counts: Record<string, number> = {}; const results = await sequelize.query( `SELECT ${column}, COUNT(*) as count FROM "${tenant}".risks WHERE is_deleted = false GROUP BY ${column}`, { type: QueryTypes.SELECT, transaction } ) as Array<{ [key: string]: any; count: string }>; possibleValues.forEach(value => counts[value] = 0); results.forEach(row => { if (row[column]) counts[row[column]] = parseInt(row.count, 10); }); return counts; }Then simplify the main function to use this helper for severity, likelihood, mitigation_status, and risk_level.
Servers/utils/history/modelInventoryHistory.utils.ts (1)
1-481: Consider abstracting shared history logic to eliminate duplication.This file is nearly identical to
riskHistory.utils.ts(Servers/utils/history/riskHistory.utils.ts), with only table names and parameter-specific logic differing. This duplication means changes must be maintained in two places.Consider creating a generic history utility that both files can use:
// historyBase.utils.ts export function createHistoryUtils<TModel>(config: { tableName: string; historyTableName: string; model: typeof Model; parameterHandlers: Record<string, ParameterHandler>; }) { return { recordHistorySnapshot: async (parameter, tenant, userId?, tx?) => { // Generic implementation using config.historyTableName }, getCurrentParameterCounts: async (parameter, tenant, tx?) => { const handler = config.parameterHandlers[parameter]; if (handler) return handler(tenant, tx); // Generic fallback with validation }, // ... other functions }; }Then instantiate for each domain:
// riskHistory.utils.ts export const riskHistoryUtils = createHistoryUtils({ tableName: 'risks', historyTableName: 'risk_history', model: RiskHistoryModel, parameterHandlers: { severity: handleSeverity, ... } });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (25)
Clients/src/application/repository/modelInventoryHistory.repository.ts(1 hunks)Clients/src/application/repository/riskHistory.repository.ts(1 hunks)Clients/src/presentation/components/AddNewRiskForm/index.tsx(4 hunks)Clients/src/presentation/components/AnalyticsDrawer/index.tsx(1 hunks)Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx(1 hunks)Clients/src/presentation/components/Charts/RiskHistoryChart.tsx(1 hunks)Clients/src/presentation/pages/ModelInventory/index.tsx(6 hunks)Clients/src/presentation/pages/RiskManagement/index.tsx(5 hunks)Servers/controllers/modelInventoryHistory.ctrl.ts(1 hunks)Servers/controllers/riskHistory.ctrl.ts(1 hunks)Servers/database/db.ts(2 hunks)Servers/database/migrations/20251105000000-create-model-inventory-history-table.js(1 hunks)Servers/database/migrations/20251107000000-create-risk-history-table.js(1 hunks)Servers/domain.layer/interfaces/i.modelInventoryHistory.ts(1 hunks)Servers/domain.layer/interfaces/i.riskHistory.ts(1 hunks)Servers/domain.layer/models/modelInventoryHistory/modelInventoryHistory.model.ts(1 hunks)Servers/domain.layer/models/riskHistory/riskHistory.model.ts(1 hunks)Servers/index.ts(2 hunks)Servers/routes/modelInventoryHistory.route.ts(1 hunks)Servers/routes/riskHistory.route.ts(1 hunks)Servers/scripts/createNewTenant.ts(1 hunks)Servers/utils/history/modelInventoryHistory.utils.ts(1 hunks)Servers/utils/history/riskHistory.utils.ts(1 hunks)Servers/utils/modelInventory.utils.ts(4 hunks)Servers/utils/risk.utils.ts(4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-11-18T05:28:01.115Z
Learnt from: MuhammadKhalilzadeh
Repo: bluewave-labs/verifywise PR: 254
File: Clients/src/presentation/components/Modals/Controlpane/index.tsx:22-24
Timestamp: 2024-11-18T05:28:01.115Z
Learning: In `Clients/src/presentation/components/Modals/Controlpane/index.tsx` (TypeScript React component), avoid changing prop types from specific types like `string` to `any` when the exact type is known. Maintaining explicit types ensures better type safety and code reliability.
Applied to files:
Clients/src/presentation/components/AddNewRiskForm/index.tsx
🧬 Code graph analysis (17)
Servers/database/migrations/20251105000000-create-model-inventory-history-table.js (1)
Servers/database/migrations/20251107000000-create-risk-history-table.js (7)
require(2-2)transaction(7-7)transaction(54-54)organizations(9-12)organizations(56-59)tenantHash(15-15)tenantHash(62-62)
Servers/controllers/riskHistory.ctrl.ts (3)
Servers/controllers/modelInventoryHistory.ctrl.ts (3)
getTimeseries(20-133)getCurrentCounts(140-187)createSnapshot(195-240)Servers/utils/statusCode.utils.ts (1)
STATUS_CODE(1-98)Servers/utils/history/riskHistory.utils.ts (4)
getTimeseriesForTimeframe(374-414)getTimeseriesWithInterpolation(250-369)getCurrentParameterCounts(50-182)recordHistorySnapshot(8-45)
Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (1)
Clients/src/application/repository/riskHistory.repository.ts (1)
getRiskTimeseries(13-29)
Servers/database/migrations/20251107000000-create-risk-history-table.js (1)
Servers/database/migrations/20251009120003-add-soft-delete-to-model-risks.js (1)
queryInterface(6-63)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1)
Clients/src/application/repository/modelInventoryHistory.repository.ts (1)
getModelInventoryTimeseries(13-29)
Servers/controllers/modelInventoryHistory.ctrl.ts (3)
Servers/utils/statusCode.utils.ts (1)
STATUS_CODE(1-98)Servers/utils/history/modelInventoryHistory.utils.ts (4)
getTimeseriesForTimeframe(317-357)getTimeseriesWithInterpolation(193-312)getCurrentParameterCounts(51-125)recordHistorySnapshot(9-46)Clients/src/application/repository/modelInventoryHistory.repository.ts (1)
getCurrentParameterCounts(37-47)
Servers/utils/risk.utils.ts (1)
Servers/utils/history/riskHistory.utils.ts (1)
recordSnapshotIfChanged(525-538)
Servers/domain.layer/models/modelInventoryHistory/modelInventoryHistory.model.ts (2)
Servers/domain.layer/models/riskHistory/riskHistory.model.ts (1)
Table(5-61)Servers/domain.layer/interfaces/i.modelInventoryHistory.ts (1)
IModelInventoryHistory(1-9)
Servers/domain.layer/models/riskHistory/riskHistory.model.ts (2)
Servers/domain.layer/models/modelInventoryHistory/modelInventoryHistory.model.ts (1)
Table(5-61)Servers/domain.layer/interfaces/i.riskHistory.ts (1)
IRiskHistory(1-9)
Servers/routes/riskHistory.route.ts (1)
Servers/controllers/riskHistory.ctrl.ts (3)
getTimeseries(20-147)getCurrentCounts(154-215)createSnapshot(223-282)
Clients/src/application/repository/modelInventoryHistory.repository.ts (2)
Clients/src/infrastructure/api/networkServices.ts (1)
apiServices(84-230)Servers/utils/history/modelInventoryHistory.utils.ts (1)
getCurrentParameterCounts(51-125)
Clients/src/application/repository/riskHistory.repository.ts (1)
Clients/src/infrastructure/api/networkServices.ts (1)
apiServices(84-230)
Servers/utils/history/modelInventoryHistory.utils.ts (2)
Servers/utils/history/riskHistory.utils.ts (9)
recordHistorySnapshot(8-45)getCurrentParameterCounts(50-182)getTimeseriesData(187-217)getTimeseriesWithInterpolation(250-369)getTimeseriesForTimeframe(374-414)getLatestSnapshot(419-443)getSnapshotAtTime(448-477)shouldRecordSnapshot(482-520)recordSnapshotIfChanged(525-538)Clients/src/application/repository/modelInventoryHistory.repository.ts (1)
getCurrentParameterCounts(37-47)
Clients/src/presentation/pages/ModelInventory/index.tsx (1)
Clients/src/presentation/pages/ModelInventory/style.ts (1)
addNewModelButtonStyle(26-29)
Servers/routes/modelInventoryHistory.route.ts (1)
Servers/controllers/modelInventoryHistory.ctrl.ts (3)
getTimeseries(20-133)getCurrentCounts(140-187)createSnapshot(195-240)
Servers/utils/modelInventory.utils.ts (2)
Servers/services/automations/automationProducer.ts (1)
enqueueAutomationAction(10-13)Servers/utils/history/modelInventoryHistory.utils.ts (1)
recordSnapshotIfChanged(468-481)
Servers/utils/history/riskHistory.utils.ts (1)
Servers/utils/history/modelInventoryHistory.utils.ts (9)
recordHistorySnapshot(9-46)getCurrentParameterCounts(51-125)getTimeseriesData(130-160)getTimeseriesWithInterpolation(193-312)getTimeseriesForTimeframe(317-357)getLatestSnapshot(362-386)getSnapshotAtTime(391-420)shouldRecordSnapshot(425-463)recordSnapshotIfChanged(468-481)
🪛 GitHub Actions: Frontend Checks
Clients/src/presentation/components/AddNewRiskForm/index.tsx
[error] 635-635: Type error: TS2345 - Argument of type 'RiskFormValues' is not assignable to parameter of type 'RiskFormValues & MitigationFormValues'. (in AddNewRiskForm)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (30)
Clients/src/presentation/components/AddNewRiskForm/index.tsx (1)
564-575: LGTM: Risk level calculations correctly reference prefixed fields.The conditional logic appropriately includes calculated risk levels when relevant fields change, and the prefixed field names align with the new approach.
Servers/domain.layer/interfaces/i.riskHistory.ts (1)
1-9: LGTM! Well-structured interface for risk history tracking.The interface design is clean and appropriate for tracking historical snapshots. The use of
Record<string, number>for JSONB counts is well-typed, and the inline comments provide helpful context for each field's purpose.Servers/index.ts (1)
36-37: LGTM! Clean route integration.The new history routes are properly imported and registered with logical placement near their related endpoints. The naming convention is consistent with the existing codebase.
Also applies to: 129-130
Clients/src/presentation/pages/RiskManagement/index.tsx (1)
530-546: LGTM! Well-configured analytics drawer integration.The AnalyticsDrawer is properly configured with appropriate parameters for risk management analytics. The available parameters (severity, likelihood, mitigation_status, risk_level) align well with the risk domain, and the default parameter choice is sensible.
Servers/routes/modelInventoryHistory.route.ts (1)
1-19: LGTM! Clean route definitions with proper authentication.The route structure is clean and properly secured with JWT authentication. The endpoints follow RESTful conventions.
Note: This file has an identical structure to
riskHistory.route.ts. While this is acceptable, consider extracting a shared route factory function if you add more similar history routes in the future to reduce duplication.Servers/routes/riskHistory.route.ts (1)
1-19: LGTM! Consistent route structure with proper authentication.The route definitions are clean and follow the same pattern as the model inventory history routes, which promotes consistency across the codebase.
Servers/database/db.ts (1)
74-75: LGTM! Proper model registration for ORM.The new history models are correctly imported and registered in the Sequelize models array, following the established pattern in the codebase.
Also applies to: 156-157
Servers/domain.layer/interfaces/i.modelInventoryHistory.ts (1)
1-9: LGTM!The interface is well-designed with clear documentation for each field. The types are appropriate for tracking historical snapshots of model inventory parameters.
Servers/utils/risk.utils.ts (3)
560-573: LGTM!The history recording is properly integrated after risk creation. Using
Promise.allfor concurrent snapshot recording is efficient, and the error handling ensures that history recording failures don't block risk creation.
918-931: LGTM!History recording after deletion follows the same pattern as creation, with proper error handling that doesn't block the operation.
826-851: No issues found—parameter mapping is correct and intentional.The mapping of
risk_level(history parameter) torisk_level_autocalculated(model field) is intentional and properly implemented. ThegetCurrentParameterCountsfunction in riskHistory.utils.ts explicitly handles this at line 130–145, querying therisk_level_autocalculatedcolumn when the parameter is'risk_level'. The reviewed code correctly uses'risk_level_autocalculated'for the change comparison and passes the semantic parameter name'risk_level'torecordSnapshotIfChanged, which is properly handled downstream.Servers/utils/modelInventory.utils.ts (2)
424-433: LGTM!The conditional history recording efficiently tracks status changes only when they occur, with proper null checking for oldModel.
498-505: LGTM!History recording after deletion follows the established pattern with proper error handling.
Servers/database/migrations/20251105000000-create-model-inventory-history-table.js (1)
16-25: Clarify the purpose of recorded_at vs created_at.The table has both
recorded_atandcreated_attimestamps. While this might be intentional (recorded_at = when the snapshot represents data from, created_at = when the row was inserted), consider documenting this distinction or evaluating if both are necessary.Clients/src/presentation/components/AnalyticsDrawer/index.tsx (2)
8-22: LGTM!The interface definitions are clear and well-structured, with appropriate optional fields and default values for backward compatibility.
137-149: LGTM with future extensibility note.The conditional chart rendering is clean and works well for the current two chart types. As mentioned in the component documentation (line 18), this drawer is designed to be reusable for other entities like vendors. Consider using a switch statement or chart type registry if more chart types are added in the future.
Clients/src/application/repository/riskHistory.repository.ts (2)
56-70: LGTM!The
createRiskHistorySnapshotfunction correctly structures the POST request and returnsresponse.dataconsistently.
37-47: Fix nested params object.Line 40 incorrectly nests the parameter inside a
paramsobject. TheapiServices.getmethod expects query parameters to be passed directly in the second argument, not nested within aparamskey.Apply this diff:
export async function getCurrentRiskParameterCounts(parameter: string): Promise<any> { try { const response = await apiServices.get("/api/riskHistory/current-counts", { - params: { parameter }, + parameter, }); return response.data; } catch (error: any) { console.error("Error fetching current risk parameter counts:", error); throw error; } }Likely an incorrect or invalid review comment.
Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (4)
89-103: Update data access after fixing repository inconsistency.Lines 94-96 access
response?.data?.data, which assumesgetRiskTimeseriesreturns the full response object. After fixing thegetRiskTimeseriesfunction inriskHistory.repository.tsto returnresponse.dataconsistently, this code should be updated to accessresponse?.datainstead.This depends on the fix suggested for
Clients/src/application/repository/riskHistory.repository.tslines 13-29.const fetchTimeseriesData = async () => { setLoading(true); setError(null); try { const response = await getRiskTimeseries(parameter, timeframe); - if (response?.data?.data) { - setTimeseriesData(response.data.data.data); + if (response?.data) { + setTimeseriesData(response.data.data); } } catch (err: any) { console.error("Error fetching timeseries data:", err); setError(err?.response?.data?.message || "Failed to load chart data"); } finally { setLoading(false); } };
110-140: LGTM!The data preparation logic is well-structured, handles edge cases (empty data), and creates properly formatted series for the chart with appropriate styling.
144-200: LGTM!The loading, error, and empty states are well-implemented with consistent styling and helpful user feedback.
226-299: LGTM!The chart configuration is comprehensive and user-friendly, with proper scaling (15% headroom), integer tick steps for count data, and accessible date formatting.
Servers/controllers/modelInventoryHistory.ctrl.ts (2)
20-133: LGTM!The
getTimeseriesendpoint has comprehensive input validation, supports multiple query modes (timeframe, custom range, default), and includes proper error handling with structured logging.
140-187: LGTM!The
getCurrentCountsendpoint is simple, focused, and properly validates input before delegating to the utility function.Clients/src/application/repository/modelInventoryHistory.repository.ts (2)
56-70: LGTM!The function correctly uses
apiServices.postwith proper body parameters and returnsresponse.dataconsistently.
37-47: Correct the API call structure.Line 40 incorrectly nests
parameterinside aparamsobject. According toapiServices.get(networkServices.ts:83-229), the second argument should be the params object directly, not nested.Apply this diff:
export async function getCurrentParameterCounts(parameter: string): Promise<any> { try { const response = await apiServices.get("/api/modelInventoryHistory/current-counts", { - params: { parameter }, + parameter, }); return response.data; } catch (error: any) { console.error("Error fetching current parameter counts:", error); throw error; } }Likely an incorrect or invalid review comment.
Servers/utils/history/riskHistory.utils.ts (2)
8-45: LGTM!The function correctly uses parameterized queries to prevent SQL injection and properly handles the snapshot creation with transaction support.
187-538: LGTM on the remaining functions!All remaining functions (
getTimeseriesData,getTimeseriesWithInterpolation,getTimeseriesForTimeframe,getLatestSnapshot,getSnapshotAtTime,shouldRecordSnapshot,recordSnapshotIfChanged) correctly use parameterized queries and implement sound interpolation and change-detection logic.Servers/utils/history/modelInventoryHistory.utils.ts (2)
9-46: LGTM!The function correctly uses parameterized queries and properly handles snapshot creation for model inventory history.
130-481: LGTM on timeseries and snapshot functions!All timeseries, interpolation, and snapshot management functions correctly use parameterized queries and implement proper logic for historical data retrieval and change detection.
| export async function getModelInventoryTimeseries( | ||
| parameter: string, | ||
| timeframe?: string, | ||
| startDate?: string, | ||
| endDate?: string, | ||
| intervalHours?: number | ||
| ): Promise<any> { | ||
| try { | ||
| const response = await apiServices.get("/modelInventoryHistory/timeseries", { | ||
| parameter, startDate, endDate, intervalHours, timeframe | ||
| }); | ||
| return response; | ||
| } catch (error: any) { | ||
| console.error("Error fetching model inventory timeseries:", error); | ||
| throw error; | ||
| } | ||
| } |
There was a problem hiding this comment.
Fix inconsistent endpoint path and return value.
Two issues here:
- Line 21 uses
/modelInventoryHistory/timeserieswithout the/api/prefix, while other functions (lines 39, 61) use/api/modelInventoryHistory/.... This inconsistency will cause routing issues. - Line 24 returns the full
responseobject, while the other functions returnresponse.data(lines 42, 65).
Apply this diff:
export async function getModelInventoryTimeseries(
parameter: string,
timeframe?: string,
startDate?: string,
endDate?: string,
intervalHours?: number
): Promise<any> {
try {
- const response = await apiServices.get("/modelInventoryHistory/timeseries", {
+ const response = await apiServices.get("/api/modelInventoryHistory/timeseries", {
parameter, startDate, endDate, intervalHours, timeframe
});
- return response;
+ return response.data;
} catch (error: any) {
console.error("Error fetching model inventory timeseries:", error);
throw error;
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function getModelInventoryTimeseries( | |
| parameter: string, | |
| timeframe?: string, | |
| startDate?: string, | |
| endDate?: string, | |
| intervalHours?: number | |
| ): Promise<any> { | |
| try { | |
| const response = await apiServices.get("/modelInventoryHistory/timeseries", { | |
| parameter, startDate, endDate, intervalHours, timeframe | |
| }); | |
| return response; | |
| } catch (error: any) { | |
| console.error("Error fetching model inventory timeseries:", error); | |
| throw error; | |
| } | |
| } | |
| export async function getModelInventoryTimeseries( | |
| parameter: string, | |
| timeframe?: string, | |
| startDate?: string, | |
| endDate?: string, | |
| intervalHours?: number | |
| ): Promise<any> { | |
| try { | |
| const response = await apiServices.get("/api/modelInventoryHistory/timeseries", { | |
| parameter, startDate, endDate, intervalHours, timeframe | |
| }); | |
| return response.data; | |
| } catch (error: any) { | |
| console.error("Error fetching model inventory timeseries:", error); | |
| throw error; | |
| } | |
| } |
🤖 Prompt for AI Agents
In Clients/src/application/repository/modelInventoryHistory.repository.ts around
lines 13 to 29, the timeseries call uses the wrong endpoint and returns the full
response: change the request path from "/modelInventoryHistory/timeseries" to
"/api/modelInventoryHistory/timeseries" to match other functions, and return
response.data (not response) so the function returns the same payload shape as
the other repository methods; preserve the same query object (parameter,
startDate, endDate, intervalHours, timeframe) when calling apiServices.get and
throw the error after logging as before.
| export async function getRiskTimeseries( | ||
| parameter: string, | ||
| timeframe?: string, | ||
| startDate?: string, | ||
| endDate?: string, | ||
| intervalHours?: number | ||
| ): Promise<any> { | ||
| try { | ||
| const response = await apiServices.get("/riskHistory/timeseries", { | ||
| parameter, startDate, endDate, intervalHours, timeframe | ||
| }); | ||
| return response; | ||
| } catch (error: any) { | ||
| console.error("Error fetching risk timeseries:", error); | ||
| throw error; | ||
| } | ||
| } |
There was a problem hiding this comment.
Inconsistent return value - returns full response instead of response.data.
Lines 24 and 42 show inconsistency: getRiskTimeseries returns the full response object while getCurrentRiskParameterCounts (line 42) and createRiskHistorySnapshot (line 65) return response.data. This inconsistency could cause issues for consumers expecting the same structure.
Apply this diff to make the return value consistent:
export async function getRiskTimeseries(
parameter: string,
timeframe?: string,
startDate?: string,
endDate?: string,
intervalHours?: number
): Promise<any> {
try {
const response = await apiServices.get("/riskHistory/timeseries", {
parameter, startDate, endDate, intervalHours, timeframe
});
- return response;
+ return response.data;
} catch (error: any) {
console.error("Error fetching risk timeseries:", error);
throw error;
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function getRiskTimeseries( | |
| parameter: string, | |
| timeframe?: string, | |
| startDate?: string, | |
| endDate?: string, | |
| intervalHours?: number | |
| ): Promise<any> { | |
| try { | |
| const response = await apiServices.get("/riskHistory/timeseries", { | |
| parameter, startDate, endDate, intervalHours, timeframe | |
| }); | |
| return response; | |
| } catch (error: any) { | |
| console.error("Error fetching risk timeseries:", error); | |
| throw error; | |
| } | |
| } | |
| export async function getRiskTimeseries( | |
| parameter: string, | |
| timeframe?: string, | |
| startDate?: string, | |
| endDate?: string, | |
| intervalHours?: number | |
| ): Promise<any> { | |
| try { | |
| const response = await apiServices.get("/riskHistory/timeseries", { | |
| parameter, startDate, endDate, intervalHours, timeframe | |
| }); | |
| return response.data; | |
| } catch (error: any) { | |
| console.error("Error fetching risk timeseries:", error); | |
| throw error; | |
| } | |
| } |
🤖 Prompt for AI Agents
In Clients/src/application/repository/riskHistory.repository.ts around lines 13
to 29, getRiskTimeseries currently returns the full API response object while
other functions return response.data; update getRiskTimeseries to return
response.data instead of response so the return shape is consistent with
getCurrentRiskParameterCounts and createRiskHistorySnapshot, and keep the
existing try/catch and error rethrow behavior (adjust the Promise<any> type only
if you want stricter typing).
| const [selectedParameter, setSelectedParameter] = useState<string>( | ||
| defaultParameter || availableParameters[0]?.value || "" | ||
| ); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Add validation for empty availableParameters.
If availableParameters is an empty array, selectedParameter will be an empty string, which could cause issues in the chart components or API calls. Consider adding validation or a fallback message when no parameters are available.
const [selectedParameter, setSelectedParameter] = useState<string>(
defaultParameter || availableParameters[0]?.value || ""
);
+
+// Add early return if no parameters available
+if (availableParameters.length === 0) {
+ return (
+ <Drawer anchor="right" open={open} onClose={onClose} sx={{ "& .MuiDrawer-paper": { width: { xs: "100%", sm: "600px", md: "800px" }, padding: "24px" } }}>
+ <Typography>No parameters available for analytics</Typography>
+ </Drawer>
+ );
+}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In Clients/src/presentation/components/AnalyticsDrawer/index.tsx around lines
65-67, the useState initializer assumes availableParameters has entries and
falls back to an empty string which can break charts or API calls; change
initialization and component behavior to explicitly handle an empty
availableParameters array by (1) initializing selectedParameter to null or
undefined when availableParameters is empty or when no defaultParameter exists,
(2) adding a conditional render or disabled state in the UI to show a clear "No
parameters available" message and prevent chart/API usage, and (3) adding guards
where selectedParameter is used (charts, API calls) to skip calls or render
placeholders when it is null/undefined. Ensure any downstream code checks for
the empty state to avoid passing an empty string to chart components or
requests.
| export async function createSnapshot(req: Request, res: Response) { | ||
| logStructured( | ||
| "processing", | ||
| "starting createSnapshot", | ||
| "createSnapshot", | ||
| "modelInventoryHistory.ctrl.ts" | ||
| ); | ||
| logger.debug("📸 Creating manual history snapshot"); | ||
|
|
||
| try { | ||
| const { parameter } = req.body; | ||
|
|
||
| if (!parameter) { | ||
| logStructured( | ||
| "error", | ||
| "parameter is required", | ||
| "createSnapshot", | ||
| "modelInventoryHistory.ctrl.ts" | ||
| ); | ||
| return res.status(400).json(STATUS_CODE[400]("Parameter is required")); | ||
| } | ||
|
|
||
| const userId = req.userId!; | ||
| const snapshot = await recordHistorySnapshot(parameter, req.tenantId!, userId); | ||
|
|
||
| logStructured( | ||
| "successful", | ||
| `snapshot created for parameter ${parameter}`, | ||
| "createSnapshot", | ||
| "modelInventoryHistory.ctrl.ts" | ||
| ); | ||
|
|
||
| return res.status(201).json( | ||
| STATUS_CODE[201](snapshot.toJSON()) | ||
| ); | ||
| } catch (error) { | ||
| logStructured( | ||
| "error", | ||
| "error creating snapshot", | ||
| "createSnapshot", | ||
| "modelInventoryHistory.ctrl.ts", | ||
| ); | ||
| logger.error("❌ Error creating snapshot:", error); | ||
| return res.status(500).json(STATUS_CODE[500]((error as Error).message)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Fix misleading comment about change_description.
The comment at lines 193-194 states "The snapshot counts will be automatically stored in change_description as JSONB", but this is incorrect. Based on the interface IModelInventoryHistory and the utility function recordHistorySnapshot, counts are stored in snapshot_data, not change_description. The change_description field is an optional text field for user-provided descriptions.
Apply this diff to fix the comment:
/**
* Manually trigger a history snapshot
* Body params:
* - parameter: string (required) - The parameter to snapshot
- * Note: The snapshot counts will be automatically stored in change_description as JSONB
+ * - description: string (optional) - Optional description of what changed
*/Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In Servers/controllers/modelInventoryHistory.ctrl.ts around lines 195 to 240,
update the misleading comment that says "The snapshot counts will be
automatically stored in change_description as JSONB" to state that counts are
stored in snapshot_data (not change_description) and that change_description is
an optional text field for user-provided descriptions; change the comment text
accordingly so it matches the IModelInventoryHistory interface and the
recordHistorySnapshot implementation.
| const intervalHours = req.query.intervalHours | ||
| ? parseInt(req.query.intervalHours as string, 10) | ||
| : 24; | ||
|
|
||
| if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { | ||
| logStructured( | ||
| "error", | ||
| "invalid date format", | ||
| "getTimeseries", | ||
| "riskHistory.ctrl.ts" | ||
| ); | ||
| return res.status(400).json( | ||
| STATUS_CODE[400]("Invalid date format. Use ISO date format.") | ||
| ); | ||
| } | ||
|
|
||
| if (startDate >= endDate) { | ||
| logStructured( | ||
| "error", | ||
| "start date must be before end date", | ||
| "getTimeseries", | ||
| "riskHistory.ctrl.ts" | ||
| ); | ||
| return res.status(400).json( | ||
| STATUS_CODE[400]("Start date must be before end date") | ||
| ); | ||
| } | ||
|
|
||
| timeseriesData = await getTimeseriesWithInterpolation( | ||
| parameter, | ||
| startDate, | ||
| endDate, | ||
| req.tenantId!, | ||
| intervalHours | ||
| ); |
There was a problem hiding this comment.
Validate intervalHours before using it
We accept any intervalHours and pass it straight to getTimeseriesWithInterpolation. A zero, negative, or non-numeric value makes the while-loop in that util never advance, tying up the worker thread and effectively DoSing the endpoint. Please validate that the parsed value is a finite, positive integer before proceeding.
- const intervalHours = req.query.intervalHours
- ? parseInt(req.query.intervalHours as string, 10)
- : 24;
+ const rawInterval = req.query.intervalHours
+ ? Number.parseInt(req.query.intervalHours as string, 10)
+ : 24;
+
+ if (!Number.isFinite(rawInterval) || rawInterval <= 0) {
+ logStructured(
+ "error",
+ "invalid intervalHours",
+ "getTimeseries",
+ "riskHistory.ctrl.ts"
+ );
+ return res
+ .status(400)
+ .json(STATUS_CODE[400]("intervalHours must be a positive integer"));
+ }
+
+ const intervalHours = rawInterval;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const intervalHours = req.query.intervalHours | |
| ? parseInt(req.query.intervalHours as string, 10) | |
| : 24; | |
| if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { | |
| logStructured( | |
| "error", | |
| "invalid date format", | |
| "getTimeseries", | |
| "riskHistory.ctrl.ts" | |
| ); | |
| return res.status(400).json( | |
| STATUS_CODE[400]("Invalid date format. Use ISO date format.") | |
| ); | |
| } | |
| if (startDate >= endDate) { | |
| logStructured( | |
| "error", | |
| "start date must be before end date", | |
| "getTimeseries", | |
| "riskHistory.ctrl.ts" | |
| ); | |
| return res.status(400).json( | |
| STATUS_CODE[400]("Start date must be before end date") | |
| ); | |
| } | |
| timeseriesData = await getTimeseriesWithInterpolation( | |
| parameter, | |
| startDate, | |
| endDate, | |
| req.tenantId!, | |
| intervalHours | |
| ); | |
| const rawInterval = req.query.intervalHours | |
| ? Number.parseInt(req.query.intervalHours as string, 10) | |
| : 24; | |
| if (!Number.isFinite(rawInterval) || rawInterval <= 0) { | |
| logStructured( | |
| "error", | |
| "invalid intervalHours", | |
| "getTimeseries", | |
| "riskHistory.ctrl.ts" | |
| ); | |
| return res | |
| .status(400) | |
| .json(STATUS_CODE[400]("intervalHours must be a positive integer")); | |
| } | |
| const intervalHours = rawInterval; | |
| if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { | |
| logStructured( | |
| "error", | |
| "invalid date format", | |
| "getTimeseries", | |
| "riskHistory.ctrl.ts" | |
| ); | |
| return res.status(400).json( | |
| STATUS_CODE[400]("Invalid date format. Use ISO date format.") | |
| ); | |
| } | |
| if (startDate >= endDate) { | |
| logStructured( | |
| "error", | |
| "start date must be before end date", | |
| "getTimeseries", | |
| "riskHistory.ctrl.ts" | |
| ); | |
| return res.status(400).json( | |
| STATUS_CODE[400]("Start date must be before end date") | |
| ); | |
| } | |
| timeseriesData = await getTimeseriesWithInterpolation( | |
| parameter, | |
| startDate, | |
| endDate, | |
| req.tenantId!, | |
| intervalHours | |
| ); |
🤖 Prompt for AI Agents
In Servers/controllers/riskHistory.ctrl.ts around lines 83 to 117, the parsed
intervalHours is not validated and a zero, negative or non-numeric value can
hang the worker; after parsing intervalHours (or defaulting to 24) validate that
it is a finite, positive integer (> 0) using Number.isFinite/Number.isInteger
(or equivalent) and reject invalid values by returning res.status(400) with a
clear message (e.g., "intervalHours must be a positive integer"); only pass the
validated integer to getTimeseriesWithInterpolation.
| CREATE TABLE "${tenantHash}".risk_history ( | ||
| id SERIAL PRIMARY KEY, | ||
| parameter VARCHAR(255) NOT NULL, | ||
| snapshot_data JSONB NOT NULL DEFAULT '{}', | ||
| recorded_at TIMESTAMP NOT NULL DEFAULT NOW(), | ||
| triggered_by_user_id INTEGER REFERENCES public.users(id) ON DELETE SET NULL, | ||
| created_at TIMESTAMP DEFAULT NOW() | ||
| ); |
There was a problem hiding this comment.
Add the missing change_description column
RiskHistoryModel (and IRiskHistory) define a change_description attribute, but the table created here does not. As soon as the code attempts to persist a snapshot with a description, Postgres will reject the insert (column "change_description" does not exist). Please align the DDL with the model by adding the nullable text column before merging.
await queryInterface.sequelize.query(`
CREATE TABLE "${tenantHash}".risk_history (
id SERIAL PRIMARY KEY,
parameter VARCHAR(255) NOT NULL,
snapshot_data JSONB NOT NULL DEFAULT '{}',
recorded_at TIMESTAMP NOT NULL DEFAULT NOW(),
triggered_by_user_id INTEGER REFERENCES public.users(id) ON DELETE SET NULL,
+ change_description TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
`, { transaction });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| CREATE TABLE "${tenantHash}".risk_history ( | |
| id SERIAL PRIMARY KEY, | |
| parameter VARCHAR(255) NOT NULL, | |
| snapshot_data JSONB NOT NULL DEFAULT '{}', | |
| recorded_at TIMESTAMP NOT NULL DEFAULT NOW(), | |
| triggered_by_user_id INTEGER REFERENCES public.users(id) ON DELETE SET NULL, | |
| created_at TIMESTAMP DEFAULT NOW() | |
| ); | |
| CREATE TABLE "${tenantHash}".risk_history ( | |
| id SERIAL PRIMARY KEY, | |
| parameter VARCHAR(255) NOT NULL, | |
| snapshot_data JSONB NOT NULL DEFAULT '{}', | |
| recorded_at TIMESTAMP NOT NULL DEFAULT NOW(), | |
| triggered_by_user_id INTEGER REFERENCES public.users(id) ON DELETE SET NULL, | |
| change_description TEXT, | |
| created_at TIMESTAMP DEFAULT NOW() | |
| ); |
🤖 Prompt for AI Agents
In Servers/database/migrations/20251107000000-create-risk-history-table.js
around lines 17 to 24 the DDL creates risk_history but omits the
change_description column defined on RiskHistoryModel/IRiskHistory; add a
nullable text column named change_description (e.g., change_description TEXT) to
the table definition (positioning it among the other columns, before created_at
if you prefer) so inserts with a description will succeed.
| } else { | ||
| // Generic handling for other parameters | ||
| const paramCounts = await sequelize.query( | ||
| `SELECT ${parameter}, COUNT(*) as count | ||
| FROM "${tenant}".model_inventories | ||
| GROUP BY ${parameter}`, | ||
| { | ||
| type: QueryTypes.SELECT, | ||
| transaction, | ||
| } | ||
| ) as Array<{ [key: string]: any; count: string }>; | ||
|
|
||
| paramCounts.forEach((row) => { | ||
| const key = row[parameter] !== null ? String(row[parameter]) : 'null'; | ||
| counts[key] = parseInt(row.count, 10); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Critical: SQL injection vulnerability in generic parameter handling.
Lines 105, 107: The parameter variable is directly interpolated into the SQL query without validation or parameterization, allowing potential SQL injection if an attacker controls the parameter value.
Apply this diff to add parameter validation:
} else {
// Generic handling for other parameters
+ // Whitelist allowed parameters to prevent SQL injection
+ const allowedParameters = ['created_at', 'updated_at', 'version', 'owner']; // Add safe column names
+ if (!allowedParameters.includes(parameter)) {
+ throw new Error(`Invalid parameter: ${parameter}`);
+ }
+
const paramCounts = await sequelize.query(
`SELECT ${parameter}, COUNT(*) as count
FROM "${tenant}".model_inventories
GROUP BY ${parameter}`,
{
type: QueryTypes.SELECT,
transaction,
}
) as Array<{ [key: string]: any; count: string }>;Alternatively, use Sequelize's query builder for safer dynamic column queries.
🤖 Prompt for AI Agents
In Servers/utils/history/modelInventoryHistory.utils.ts around lines 102 to 118,
the code injects the variable `parameter` directly into a raw SQL string
creating an SQL injection risk; replace this with a safe approach by validating
`parameter` against a predefined whitelist of allowed column names (and only
proceed if it matches exactly), and ensure `tenant` is also validated/normalized
before using it in the table name; then build the query using the validated,
constant column name (or use Sequelize column quoting via QueryInterface or
model attribute mapping) and continue to use parameterized options for
values/transactions so no user-controlled input is concatenated into SQL.
| } else { | ||
| // Generic handling for other parameters | ||
| const paramCounts = await sequelize.query( | ||
| `SELECT ${parameter}, COUNT(*) as count | ||
| FROM "${tenant}".risks | ||
| WHERE is_deleted = false | ||
| GROUP BY ${parameter}`, | ||
| { | ||
| type: QueryTypes.SELECT, | ||
| transaction, | ||
| } | ||
| ) as Array<{ [key: string]: any; count: string }>; | ||
|
|
||
| paramCounts.forEach((row) => { | ||
| const key = row[parameter] !== null ? String(row[parameter]) : 'null'; | ||
| counts[key] = parseInt(row.count, 10); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Critical: SQL injection vulnerability in generic parameter handling.
Lines 161, 164: The parameter variable is directly interpolated into the SQL query without validation or parameterization. If an attacker controls the parameter value, they could inject arbitrary SQL.
Apply this diff to use parameterized queries or implement a whitelist:
} else {
// Generic handling for other parameters
+ // Whitelist allowed parameters to prevent SQL injection
+ const allowedParameters = ['created_at', 'updated_at']; // Add other safe column names
+ if (!allowedParameters.includes(parameter)) {
+ throw new Error(`Invalid parameter: ${parameter}`);
+ }
+
const paramCounts = await sequelize.query(
`SELECT ${parameter}, COUNT(*) as count
FROM "${tenant}".risks
WHERE is_deleted = false
GROUP BY ${parameter}`,
{
type: QueryTypes.SELECT,
transaction,
}
) as Array<{ [key: string]: any; count: string }>;Alternatively, use Sequelize's query builder instead of raw SQL for safer dynamic queries.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } else { | |
| // Generic handling for other parameters | |
| const paramCounts = await sequelize.query( | |
| `SELECT ${parameter}, COUNT(*) as count | |
| FROM "${tenant}".risks | |
| WHERE is_deleted = false | |
| GROUP BY ${parameter}`, | |
| { | |
| type: QueryTypes.SELECT, | |
| transaction, | |
| } | |
| ) as Array<{ [key: string]: any; count: string }>; | |
| paramCounts.forEach((row) => { | |
| const key = row[parameter] !== null ? String(row[parameter]) : 'null'; | |
| counts[key] = parseInt(row.count, 10); | |
| }); | |
| } | |
| } else { | |
| // Generic handling for other parameters | |
| // Whitelist allowed parameters to prevent SQL injection | |
| const allowedParameters = ['created_at', 'updated_at']; // Add other safe column names | |
| if (!allowedParameters.includes(parameter)) { | |
| throw new Error(`Invalid parameter: ${parameter}`); | |
| } | |
| const paramCounts = await sequelize.query( | |
| `SELECT ${parameter}, COUNT(*) as count | |
| FROM "${tenant}".risks | |
| WHERE is_deleted = false | |
| GROUP BY ${parameter}`, | |
| { | |
| type: QueryTypes.SELECT, | |
| transaction, | |
| } | |
| ) as Array<{ [key: string]: any; count: string }>; | |
| paramCounts.forEach((row) => { | |
| const key = row[parameter] !== null ? String(row[parameter]) : 'null'; | |
| counts[key] = parseInt(row.count, 10); | |
| }); | |
| } |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (9)
Clients/src/presentation/components/AddNewRiskForm/index.tsx (2)
431-454: Avoidanyin generic types for better type safety.The generic constraint
Record<string, any>and return typeRecord<string, any>reduce type safety. Consider using more specific types or at leastunknowninstead ofany. Based on learnings.Additionally, the array comparison at lines 441-443 only works correctly for arrays of primitives or simple objects. For nested objects, consider using a deep equality library or a more robust comparison method.
Apply this diff to improve type safety:
- const getChangedFields = useCallback(<T extends Record<string, any>>(original: Partial<T>, current: Partial<T>) => { - const changedFields: Record<string, any> = {}; + const getChangedFields = useCallback(<T extends Record<string, unknown>>(original: Partial<T>, current: Partial<T>): Partial<T> => { + const changedFields: Partial<T> = {};For the array comparison, consider adding a comment clarifying the limitation:
- // For arrays, do deep comparison (use copies to avoid mutating originals) + // For arrays of primitives, do sorted comparison (use copies to avoid mutating originals) + // Note: This comparison may not work correctly for arrays of complex nested objects if (Array.isArray(originalValue) && Array.isArray(currentValue)) {
524-553: Consider type-safe field mapping.The manual field mapping is extensive and could be error-prone during maintenance. Consider using TypeScript's type system to ensure the mapping keys match the actual field names, or extract this mapping to a shared constant file for reusability.
For example, you could create a type-safe mapping helper:
// Define types for risk and mitigation field names type RiskFieldKey = keyof RiskFormValues; type MitigationFieldKey = keyof MitigationFormValues; // Create a mapping that ensures compile-time checking const createFieldMapping = () => { const riskMapping: Record<`risk_${RiskFieldKey}`, string> = { risk_riskName: 'risk_name', risk_actionOwner: 'risk_owner', // ... other risk fields }; const mitigationMapping: Record<`mitigation_${MitigationFieldKey}`, string> = { mitigation_mitigationStatus: 'mitigation_status', // ... other mitigation fields }; return { ...riskMapping, ...mitigationMapping }; };Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (3)
39-39: Define a proper type interface for timeseries data.The
any[]type bypasses TypeScript's type checking and could hide bugs. Define an interface that matches the expected shape of the API response.Consider creating an interface like:
interface TimeseriesDataPoint { timestamp: string; data: Record<ModelInventoryStatus, number>; }Then update the state declaration:
- const [timeseriesData, setTimeseriesData] = useState<any[]>([]); + const [timeseriesData, setTimeseriesData] = useState<TimeseriesDataPoint[]>([]);
66-92: Memoize expensive chart data preparation.
prepareChartDatarecalculates on every render, even whentimeseriesDatahasn't changed. This is inefficient, especially with large datasets.Apply this diff:
+ const { timestamps, series, maxValue } = React.useMemo(() => { - // Prepare data for the chart - const prepareChartData = () => { if (!timeseriesData || timeseriesData.length === 0) { return { timestamps: [], series: [], maxValue: 0 }; } const timestamps = timeseriesData.map((point) => new Date(point.timestamp)); // Get all unique status values const statusValues = Object.values(ModelInventoryStatus); const series = statusValues.map((status) => ({ label: status, data: timeseriesData.map((point) => point.data[status] || 0), color: STATUS_COLORS[status] || "#9CA3AF", curve: "monotoneX" as const, showMark: false, })); // Calculate max value across all series for proper y-axis scaling const maxValue = Math.max( ...series.flatMap((s) => s.data), 0 ); return { timestamps, series, maxValue }; - }; - - const { timestamps, series, maxValue } = prepareChartData(); + }, [timeseriesData]);
94-110: Extract magic number to a named constant.The
120added toheightappears in multiple places (lines 101, 119, 139) but its purpose isn't clear. This represents the additional space needed for title, controls, and padding.Consider extracting to a constant at the top of the file:
+const CHART_CONTAINER_EXTRA_HEIGHT = 120; // Space for title, controls, and padding + const ModelInventoryHistoryChart: React.FC<ModelInventoryHistoryChartProps> = ({Then use it consistently:
- height: height + 120, + height: height + CHART_CONTAINER_EXTRA_HEIGHT,Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (4)
8-12: Consider exporting the interface for consumers.The
RiskHistoryChartPropsinterface is not currently exported. If other components need to reference this type (e.g., when wrapping or extending this component), they won't have access to it.Apply this diff to export the interface:
-interface RiskHistoryChartProps { +export interface RiskHistoryChartProps { parameter?: string; title?: string; height?: number; }
83-83: Replaceany[]with a typed interface.Using
any[]fortimeseriesDatabypasses TypeScript's type checking and can lead to runtime errors if the data structure changes.Define an interface for the timeseries data:
interface TimeseriesPoint { timestamp: string; data: Record<string, number>; }Then update the state declaration:
- const [timeseriesData, setTimeseriesData] = useState<any[]>([]); + const [timeseriesData, setTimeseriesData] = useState<TimeseriesPoint[]>([]);
85-103: Consider wrappingfetchTimeseriesDatainuseCallback.The function
fetchTimeseriesDatais called in theuseEffectbut isn't listed in the dependency array, which may trigger ESLint warnings. While the current implementation is functionally correct (the function capturesparameterandtimeframefrom its closure), wrapping it inuseCallbackwould make the dependency flow explicit and satisfy the linter.- const fetchTimeseriesData = async () => { + const fetchTimeseriesData = useCallback(async () => { setLoading(true); setError(null); try { const response = await getRiskTimeseries(parameter, timeframe); if (response?.data?.data) { setTimeseriesData(response.data.data.data); } } catch (err: any) { console.error("Error fetching timeseries data:", err); setError(err?.response?.data?.message || "Failed to load chart data"); } finally { setLoading(false); } - }; + }, [parameter, timeframe]); useEffect(() => { fetchTimeseriesData(); - }, [timeframe, parameter]); + }, [fetchTimeseriesData]);
247-247: Extract magic numbers as named constants.The values
1.15(y-axis padding multiplier) and10(fallback maximum) are magic numbers that reduce code readability. Extracting them as named constants would make their purpose clearer.Add constants near the top of the file (after the color maps):
const Y_AXIS_PADDING_MULTIPLIER = 1.15; // Add 15% padding above max value const Y_AXIS_DEFAULT_MAX = 10; // Default max when no dataThen update line 247:
- max: maxValue > 0 ? Math.ceil(maxValue * 1.15) : 10, + max: maxValue > 0 ? Math.ceil(maxValue * Y_AXIS_PADDING_MULTIPLIER) : Y_AXIS_DEFAULT_MAX,
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
Clients/src/presentation/components/AddNewRiskForm/index.tsx(5 hunks)Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx(1 hunks)Clients/src/presentation/components/Charts/RiskHistoryChart.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-11-18T05:28:01.115Z
Learnt from: MuhammadKhalilzadeh
Repo: bluewave-labs/verifywise PR: 254
File: Clients/src/presentation/components/Modals/Controlpane/index.tsx:22-24
Timestamp: 2024-11-18T05:28:01.115Z
Learning: In `Clients/src/presentation/components/Modals/Controlpane/index.tsx` (TypeScript React component), avoid changing prop types from specific types like `string` to `any` when the exact type is known. Maintaining explicit types ensures better type safety and code reliability.
Applied to files:
Clients/src/presentation/components/AddNewRiskForm/index.tsx
🧬 Code graph analysis (2)
Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (1)
Clients/src/application/repository/riskHistory.repository.ts (1)
getRiskTimeseries(13-29)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1)
Clients/src/application/repository/modelInventoryHistory.repository.ts (1)
getModelInventoryTimeseries(13-29)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
Clients/src/presentation/components/AddNewRiskForm/index.tsx (2)
563-574: LGTM!The logic correctly determines when to include calculated risk levels based on the new prefixed field names. The separation between risk-related and mitigation-related field changes is clear and appropriate.
633-664: LGTM!The update logic correctly separates risk and mitigation changed fields, applies appropriate prefixes, and updates the deletion flag checks to use the new prefixed field names. The implementation is consistent with the new analytics infrastructure described in the PR summary.
| useEffect(() => { | ||
| fetchTimeseriesData(); | ||
| }, [timeframe, parameter]); | ||
|
|
||
| const fetchTimeseriesData = async () => { | ||
| setLoading(true); | ||
| setError(null); | ||
| try { | ||
| const response = await getModelInventoryTimeseries(parameter, timeframe); | ||
| if (response?.data?.data) { | ||
| setTimeseriesData(response.data.data.data); | ||
| } | ||
| } catch (err: any) { | ||
| console.error("Error fetching timeseries data:", err); | ||
| setError(err?.response?.data?.message || "Failed to load chart data"); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Add cleanup to prevent state updates on unmounted component.
The useEffect doesn't return a cleanup function. If the component unmounts while the API call is in flight, calling setState will trigger a React warning and potential memory leak.
Apply this diff:
useEffect(() => {
- fetchTimeseriesData();
+ let isMounted = true;
+
+ const fetchData = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await getModelInventoryTimeseries(parameter, timeframe);
+ if (isMounted && response?.data?.data) {
+ setTimeseriesData(response.data.data.data);
+ }
+ } catch (err: any) {
+ console.error("Error fetching timeseries data:", err);
+ if (isMounted) {
+ setError(err?.response?.data?.message || "Failed to load chart data");
+ }
+ } finally {
+ if (isMounted) {
+ setLoading(false);
+ }
+ }
+ };
+
+ fetchData();
+
+ return () => {
+ isMounted = false;
+ };
}, [timeframe, parameter]);
-
- const fetchTimeseriesData = async () => {
- setLoading(true);
- setError(null);
- try {
- const response = await getModelInventoryTimeseries(parameter, timeframe);
- if (response?.data?.data) {
- setTimeseriesData(response.data.data.data);
- }
- } catch (err: any) {
- console.error("Error fetching timeseries data:", err);
- setError(err?.response?.data?.message || "Failed to load chart data");
- } finally {
- setLoading(false);
- }
- };🤖 Prompt for AI Agents
In Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
around lines 41 to 59, the effect starts an async fetch but doesn't clean up, so
setState may run after unmount; fix by guarding state updates with an isMounted
flag (or AbortController if the HTTP client supports cancellation): create let
isMounted = true before calling fetchTimeseriesData, check isMounted before
calling setLoading, setTimeseriesData, and setError inside the async flow (or
attach signal to the request and skip updates if aborted), and return a cleanup
function from useEffect that sets isMounted = false (or aborts the controller)
to prevent state updates on an unmounted component.
| if (response?.data?.data) { | ||
| setTimeseriesData(response.data.data.data); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Simplify deeply nested data access.
The path response.data.data.data is confusing and fragile. If the API structure changes slightly, this will break silently.
Consider either:
- Normalizing the repository response to remove redundant nesting
- Adding clearer intermediate variables:
const response = await getModelInventoryTimeseries(parameter, timeframe);
- if (response?.data?.data) {
- setTimeseriesData(response.data.data.data);
+ const apiData = response?.data?.data;
+ if (apiData?.data) {
+ setTimeseriesData(apiData.data);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (response?.data?.data) { | |
| setTimeseriesData(response.data.data.data); | |
| } | |
| const apiData = response?.data?.data; | |
| if (apiData?.data) { | |
| setTimeseriesData(apiData.data); | |
| } |
🤖 Prompt for AI Agents
In Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
around lines 50-52, the code reads response.data.data.data which is overly
nested and fragile; replace this with a single, clear source by either updating
the repository/API layer to return a normalized payload (e.g., remove the extra
"data" wrapper) or introduce a descriptive intermediate variable (e.g., const
payload = response?.data?.data ?? response?.data ?? {}; then use payload.data or
payload.items) and use optional chaining with a sensible default when calling
setTimeseriesData so the component won’t break on small shape changes.
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
Outdated
Show resolved
Hide resolved
| margin={{ top: 20, right: 30, bottom: 80, left: 70 }} | ||
| slotProps={{ | ||
| legend: { | ||
| direction: "row" as any, |
There was a problem hiding this comment.
🧩 Analysis chain
Replace as any type assertion with proper typing.
The as any assertion bypasses type checking and may hide incompatibilities with the MUI X-Charts API. The direction property should have a proper type from the library.
Verify the correct type for the direction property in the current version of @mui/x-charts (v8.4.0):
🌐 Web query:
What are the valid types for the legend direction property in @mui/x-charts version 8.4.0?
💡 Result:
The legend direction accepts: 'horizontal' or 'vertical'. [1][2]
Fix invalid direction value in legend configuration.
The direction property only accepts 'horizontal' or 'vertical' in @mui/x-charts v8.4.0, but the code uses "row". The valid types for the legend direction property are 'horizontal' or 'vertical'. Replace direction: "row" as any with the correct value ('horizontal' or 'vertical' depending on intended layout).
🤖 Prompt for AI Agents
In Clients/src/presentation/components/Charts/RiskHistoryChart.tsx around line
265, the legend configuration uses an invalid direction value ("row" as any);
replace it with a valid @mui/x-charts v8.4.0 value — either 'horizontal' or
'vertical' depending on the intended layout (most likely 'horizontal' for a row
layout) and remove the cast.
| // padding: { top: 30 }, | ||
| // itemMarkWidth: 14, | ||
| // itemMarkHeight: 14, | ||
| // markGap: 8, | ||
| // itemGap: 24, | ||
| // labelStyle: { | ||
| // fontSize: 13, | ||
| // fontWeight: 500, | ||
| // fill: "#1c2130", | ||
| // }, |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Remove commented-out code.
Dead code should be removed before merging. If these legend customization options are needed in the future, they can be retrieved from version control history.
Apply this diff to remove the commented lines:
legend: {
direction: "row" as any,
position: { vertical: "bottom", horizontal: "center" },
- // padding: { top: 30 },
- // itemMarkWidth: 14,
- // itemMarkHeight: 14,
- // markGap: 8,
- // itemGap: 24,
- // labelStyle: {
- // fontSize: 13,
- // fontWeight: 500,
- // fill: "#1c2130",
- // },
},🤖 Prompt for AI Agents
Clients/src/presentation/components/Charts/RiskHistoryChart.tsx around lines 267
to 276: remove the block of commented-out legend customization lines (padding,
itemMarkWidth, itemMarkHeight, markGap, itemGap, labelStyle and their values) so
the file contains no dead commented code; simply delete those lines and run a
quick lint/format to ensure spacing remains consistent.
|
One of the checks has failed @HarshP4585 |
|
@rachanabasnet and @Br0wnHammer Would you please provide a review here? |
- Update drawer header font size to 15px for consistency - Restructure parameter selection layout with text on top and buttons below - Align parameter selection buttons to the right - Remove chart titles and show only timeframe toggles - Adjust chart margins and legend positioning (35px padding) - Reduce chart height from 400px to 300px - Add EmptyState component with message "There is no historical data here" - Apply consistent styling across Model Inventory and Risk Management analytics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (6)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (3)
50-52: Simplify deeply nested data access.The path
response.data.data.datais confusing and fragile. If the API structure changes slightly, this will break silently.Based on learnings.
Consider adding clearer intermediate variables:
const response = await getModelInventoryTimeseries(parameter, timeframe); - if (response?.data?.data) { - setTimeseriesData(response.data.data.data); + const apiPayload = response?.data?.data; + if (apiPayload?.data) { + setTimeseriesData(apiPayload.data); }
42-60: Add cleanup to prevent state updates on unmounted component.The
useEffectdoesn't return a cleanup function. If the component unmounts while the API call is in flight, callingsetStatewill trigger a React warning and potential memory leak.Based on learnings.
Apply this diff:
useEffect(() => { - fetchTimeseriesData(); + let isMounted = true; + + const fetchData = async () => { + setLoading(true); + setError(null); + try { + const response = await getModelInventoryTimeseries(parameter, timeframe); + if (isMounted && response?.data?.data) { + setTimeseriesData(response.data.data.data); + } + } catch (err: any) { + console.error("Error fetching timeseries data:", err); + if (isMounted) { + setError(err?.response?.data?.message || "Failed to load chart data"); + } + } finally { + if (isMounted) { + setLoading(false); + } + } + }; + + fetchData(); + + return () => { + isMounted = false; + }; }, [timeframe, parameter]); - - const fetchTimeseriesData = async () => { - setLoading(true); - setError(null); - try { - const response = await getModelInventoryTimeseries(parameter, timeframe); - if (response?.data?.data) { - setTimeseriesData(response.data.data.data); - } - } catch (err: any) { - console.error("Error fetching timeseries data:", err); - setError(err?.response?.data?.message || "Failed to load chart data"); - } finally { - setLoading(false); - } - };
204-204: Replace type assertion with proper typing.The
as anyassertion bypasses TypeScript's type checking. According to @mui/x-charts v8.4.0 documentation, thedirectionproperty accepts'horizontal'or'vertical'.Based on learnings.
Apply this diff:
legend: { hidden: false, - direction: "row" as any, + direction: "horizontal", position: { vertical: "bottom", horizontal: "center" },Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (3)
94-96: Simplify deeply nested data access.The path
response.data.data.datais confusing and fragile. If the API structure changes slightly, this will break silently.Based on learnings.
Consider adding clearer intermediate variables:
const response = await getRiskTimeseries(parameter, timeframe); - if (response?.data?.data) { - setTimeseriesData(response.data.data.data); + const apiPayload = response?.data?.data; + if (apiPayload?.data) { + setTimeseriesData(apiPayload.data); }
86-104: Add cleanup to prevent state updates on unmounted component.The
useEffectdoesn't return a cleanup function. If the component unmounts while the API call is in flight, callingsetStatewill trigger a React warning and potential memory leak.Based on learnings.
Apply this diff:
useEffect(() => { - fetchTimeseriesData(); + let isMounted = true; + + const fetchData = async () => { + setLoading(true); + setError(null); + try { + const response = await getRiskTimeseries(parameter, timeframe); + if (isMounted && response?.data?.data) { + setTimeseriesData(response.data.data.data); + } + } catch (err: any) { + console.error("Error fetching timeseries data:", err); + if (isMounted) { + setError(err?.response?.data?.message || "Failed to load chart data"); + } + } finally { + if (isMounted) { + setLoading(false); + } + } + }; + + fetchData(); + + return () => { + isMounted = false; + }; }, [timeframe, parameter]); - - const fetchTimeseriesData = async () => { - setLoading(true); - setError(null); - try { - const response = await getRiskTimeseries(parameter, timeframe); - if (response?.data?.data) { - setTimeseriesData(response.data.data.data); - } - } catch (err: any) { - console.error("Error fetching timeseries data:", err); - setError(err?.response?.data?.message || "Failed to load chart data"); - } finally { - setLoading(false); - } - };
254-254: Replace invalid direction value with proper type.The
directionproperty only accepts'horizontal'or'vertical'in @mui/x-charts v8.4.0, but the code uses"row" as any. The type assertion bypasses the error but doesn't fix the incorrect value.Based on learnings.
Apply this diff:
legend: { hidden: false, - direction: "row" as any, + direction: "horizontal", position: { vertical: "bottom", horizontal: "center" },
🧹 Nitpick comments (2)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1)
40-40: Consider adding a more specific type for timeseries data.Using
any[]reduces type safety and makes the code harder to maintain. Consider defining an interface for the timeseries data points.Example:
interface TimeseriesDataPoint { timestamp: string; data: Record<string, number>; } const [timeseriesData, setTimeseriesData] = useState<TimeseriesDataPoint[]>([]);Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (1)
84-84: Consider adding a more specific type for timeseries data.Using
any[]reduces type safety and makes the code harder to maintain. Consider defining an interface for the timeseries data points.Example:
interface TimeseriesDataPoint { timestamp: string; data: Record<string, number>; } const [timeseriesData, setTimeseriesData] = useState<TimeseriesDataPoint[]>([]);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
Clients/src/presentation/components/AnalyticsDrawer/index.tsx(1 hunks)Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx(1 hunks)Clients/src/presentation/components/Charts/RiskHistoryChart.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- Clients/src/presentation/components/AnalyticsDrawer/index.tsx
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2024-11-18T05:28:01.115Z
Learnt from: MuhammadKhalilzadeh
Repo: bluewave-labs/verifywise PR: 254
File: Clients/src/presentation/components/Modals/Controlpane/index.tsx:22-24
Timestamp: 2024-11-18T05:28:01.115Z
Learning: In `Clients/src/presentation/components/Modals/Controlpane/index.tsx` (TypeScript React component), avoid changing prop types from specific types like `string` to `any` when the exact type is known. Maintaining explicit types ensures better type safety and code reliability.
Applied to files:
Clients/src/presentation/components/Charts/RiskHistoryChart.tsxClients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
📚 Learning: 2025-01-23T05:13:38.328Z
Learnt from: samuel-coutinho
Repo: bluewave-labs/verifywise PR: 524
File: Clients/src/presentation/pages/ProjectView/ProjectSettings/index.tsx:63-63
Timestamp: 2025-01-23T05:13:38.328Z
Learning: Always handle loading and error states in components that fetch data to prevent crashes and provide better UX. Use loading indicators during data fetching and display appropriate error messages when API calls fail.
Applied to files:
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
📚 Learning: 2025-08-07T21:26:04.614Z
Learnt from: Br0wnHammer
Repo: bluewave-labs/verifywise PR: 1872
File: Clients/src/application/hooks/useDashboard.ts:12-13
Timestamp: 2025-08-07T21:26:04.614Z
Learning: In the verifywise project, the `getAllEntities` function returns a response object where the actual data is nested within a `data` property, so the correct access pattern is `response.data` rather than accessing the response directly.
Applied to files:
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
🧬 Code graph analysis (2)
Clients/src/presentation/components/Charts/RiskHistoryChart.tsx (1)
Clients/src/application/repository/riskHistory.repository.ts (1)
getRiskTimeseries(13-29)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1)
Clients/src/application/repository/modelInventoryHistory.repository.ts (1)
getModelInventoryTimeseries(13-29)
🪛 GitHub Actions: Frontend Checks
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
[error] 34-34: TS6133: 'title' is declared but its value is never read. (build step: npm run build)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (javascript-typescript)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
Outdated
Show resolved
Hide resolved
Clients/src/presentation/components/Charts/RiskHistoryChart.tsx
Outdated
Show resolved
Hide resolved
- Remove unused 'title' parameter from both chart components - Remove 'hidden' property from legend config (not a valid property) - Legend is shown by default without the hidden property 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
- Remove padding property from legend config in both chart components - Property is not supported by MUI Charts legend type - Legend positioning controlled through bottom margin instead 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (4)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (4)
50-52: Simplify deeply nested data access.The path
response.data.data.datais confusing and fragile. If the API structure changes slightly, this will break silently.Consider adding clearer intermediate variables:
const response = await getModelInventoryTimeseries(parameter, timeframe); - if (response?.data?.data) { - setTimeseriesData(response.data.data.data); + const apiData = response?.data?.data; + if (apiData?.data) { + setTimeseriesData(apiData.data); }
202-202: Remove type assertion and use correct type.The
as anytype assertion bypasses TypeScript's type checking.Check the MUI X-Charts documentation for the correct
directiontype. If "row" is valid, the type definition should accept it without assertion. Otherwise, use the proper enum or literal type:slotProps={{ legend: { - direction: "row" as any, + direction: "row", position: { vertical: "bottom", horizontal: "center" },
10-14: Remove or use the unused 'title' prop.The
titleparameter is declared but never used. Either remove it from the interface if not needed, or render it in the UI (e.g., as a chart heading).Apply this diff to render the title:
<Stack spacing={3}> + {title && ( + <Typography sx={{ fontSize: 16, fontWeight: 600, color: "#101828" }}> + {title} + </Typography> + )} <Stack direction="row" justifyContent="flex-end" alignItems="center">Or remove it from the props interface:
interface ModelInventoryHistoryChartProps { parameter?: string; - title?: string; height?: number; }
41-59: Add cleanup to prevent state updates on unmounted component.The
useEffectdoesn't return a cleanup function. If the component unmounts while the API call is in flight, callingsetStatewill trigger a React warning and potential memory leak.Apply this diff:
useEffect(() => { - fetchTimeseriesData(); + let isMounted = true; + + const fetchData = async () => { + setLoading(true); + setError(null); + try { + const response = await getModelInventoryTimeseries(parameter, timeframe); + if (isMounted && response?.data?.data) { + setTimeseriesData(response.data.data.data); + } + } catch (err: any) { + console.error("Error fetching timeseries data:", err); + if (isMounted) { + setError(err?.response?.data?.message || "Failed to load chart data"); + } + } finally { + if (isMounted) { + setLoading(false); + } + } + }; + + fetchData(); + + return () => { + isMounted = false; + }; }, [timeframe, parameter]); - - const fetchTimeseriesData = async () => { - setLoading(true); - setError(null); - try { - const response = await getModelInventoryTimeseries(parameter, timeframe); - if (response?.data?.data) { - setTimeseriesData(response.data.data.data); - } - } catch (err: any) { - console.error("Error fetching timeseries data:", err); - setError(err?.response?.data?.message || "Failed to load chart data"); - } finally { - setLoading(false); - } - };Based on learnings.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx(1 hunks)Clients/src/presentation/components/Charts/RiskHistoryChart.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- Clients/src/presentation/components/Charts/RiskHistoryChart.tsx
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-01-23T05:13:38.328Z
Learnt from: samuel-coutinho
Repo: bluewave-labs/verifywise PR: 524
File: Clients/src/presentation/pages/ProjectView/ProjectSettings/index.tsx:63-63
Timestamp: 2025-01-23T05:13:38.328Z
Learning: Always handle loading and error states in components that fetch data to prevent crashes and provide better UX. Use loading indicators during data fetching and display appropriate error messages when API calls fail.
Applied to files:
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
📚 Learning: 2025-08-07T21:26:04.614Z
Learnt from: Br0wnHammer
Repo: bluewave-labs/verifywise PR: 1872
File: Clients/src/application/hooks/useDashboard.ts:12-13
Timestamp: 2025-08-07T21:26:04.614Z
Learning: In the verifywise project, the `getAllEntities` function returns a response object where the actual data is nested within a `data` property, so the correct access pattern is `response.data` rather than accessing the response directly.
Applied to files:
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
📚 Learning: 2024-11-18T05:28:01.115Z
Learnt from: MuhammadKhalilzadeh
Repo: bluewave-labs/verifywise PR: 254
File: Clients/src/presentation/components/Modals/Controlpane/index.tsx:22-24
Timestamp: 2024-11-18T05:28:01.115Z
Learning: In `Clients/src/presentation/components/Modals/Controlpane/index.tsx` (TypeScript React component), avoid changing prop types from specific types like `string` to `any` when the exact type is known. Maintaining explicit types ensures better type safety and code reliability.
Applied to files:
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
🧬 Code graph analysis (1)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx (1)
Clients/src/application/repository/modelInventoryHistory.repository.ts (1)
getModelInventoryTimeseries(13-29)
🪛 GitHub Actions: Frontend Checks
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
[error] 204-204: TS2353: Object literal may only specify known properties, and 'padding' does not exist in type 'Partial<ChartsLegendProps | ContinuousColorLegendProps | PiecewiseColorLegendProps> & ChartsLegendPosition'.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (javascript-typescript)
Clients/src/presentation/components/Charts/ModelInventoryHistoryChart.tsx
Outdated
Show resolved
Hide resolved
- Remove itemMarkWidth, itemMarkHeight, markGap, itemGap, and labelStyle - These properties are not supported by MUI Charts legend type - Use only direction and position properties which are type-safe - Legend will use default MUI Charts styling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Describe your changes
Write your issue number after "Fixes "
Fixes #2673
Please ensure all items are checked off before requesting a review: