Skip to content

Commit 713fc6f

Browse files
Merge pull request #2231 from bluewave-labs/sw-016-sep-25-add-mapping-from-model-inventory-to-projects-and-framework
added-used-in-projects-dropdown-in-add-new-model
2 parents e75e65f + a88613b commit 713fc6f

File tree

10 files changed

+209
-7
lines changed

10 files changed

+209
-7
lines changed

Clients/src/domain/interfaces/i.modelInventory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface IModelInventory {
1313
biases?: string;
1414
limitations?: string;
1515
hosting_provider?: string;
16+
used_in_projects: string[];
1617
is_demo?: boolean;
1718
created_at?: Date;
1819
updated_at?: Date;

Clients/src/domain/types/Project.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export type Project = {
2020
framework: {
2121
project_framework_id: number;
2222
framework_id: number;
23+
name: string;
2324
}[];
2425
monitored_regulations_and_standards: string[];
2526
is_organizational?: boolean;

Clients/src/presentation/components/Modals/NewModelInventory/index.tsx

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import { KeyboardArrowDown } from "@mui/icons-material";
3232
import { ReactComponent as GreyDownArrowIcon } from "../../../assets/icons/chevron-down-grey.svg";
3333
import { useModalKeyHandling } from "../../../../application/hooks/useModalKeyHandling";
3434
import modelInventoryOptions from "../../../utils/model-inventory.json";
35+
import {getAllProjects} from "../../../../application/repository/project.repository"
36+
import { Project } from "../../../../domain/types/Project";
3537

3638
interface NewModelInventoryProps {
3739
isOpen: boolean;
@@ -51,10 +53,11 @@ interface NewModelInventoryFormValues {
5153
security_assessment: boolean;
5254
status: ModelInventoryStatus;
5355
status_date: string;
54-
reference_link: string,
55-
biases: string,
56-
limitations: string,
57-
hosting_provider: string,
56+
reference_link: string;
57+
biases: string;
58+
limitations: string;
59+
hosting_provider: string;
60+
used_in_projects: string[]
5861
}
5962

6063
interface NewModelInventoryFormErrors {
@@ -66,6 +69,7 @@ interface NewModelInventoryFormErrors {
6669
capabilities?: string;
6770
status?: string;
6871
status_date?: string;
72+
used_in_projects?: string;
6973
}
7074

7175
const initialState: NewModelInventoryFormValues = {
@@ -82,6 +86,7 @@ const initialState: NewModelInventoryFormValues = {
8286
biases: "",
8387
limitations: "",
8488
hosting_provider: "",
89+
used_in_projects : [],
8590
};
8691

8792
const statusOptions = [
@@ -170,6 +175,41 @@ const NewModelInventory: FC<NewModelInventoryProps> = ({
170175
}
171176
};
172177

178+
const [projectList, setProjects] = useState<Project[]>([]);
179+
const [, setProjectsLoading] = useState(true);
180+
181+
useEffect(() => {
182+
const fetchProjects = async () => {
183+
try {
184+
setProjectsLoading(true);
185+
const response = await getAllProjects();
186+
if (response?.data) {
187+
setProjects(response.data);
188+
}
189+
} catch (error) {
190+
console.error("Error fetching projects:", error);
191+
} finally {
192+
setProjectsLoading(false);
193+
}
194+
};
195+
196+
fetchProjects();
197+
}, []);
198+
199+
const combinedList = useMemo(() => {
200+
const targetFrameworks = ["ISO 42001", "ISO 27001"];
201+
202+
return projectList.flatMap((project) => {
203+
// Get enabled framework names for this project
204+
const enabledFrameworks = project.framework?.map((f) => f.name) || [];
205+
206+
// Only include target frameworks that are enabled
207+
return targetFrameworks
208+
.filter((fw) => enabledFrameworks.includes(fw))
209+
.map((fw) => `${project.project_title.trim()} - ${fw}`);
210+
});
211+
}, [projectList]);
212+
173213
// Transform users to the format expected by SelectComponent
174214
const userOptions = useMemo(() => {
175215
return users.map((user) => ({
@@ -219,6 +259,14 @@ const NewModelInventory: FC<NewModelInventoryProps> = ({
219259
[]
220260
);
221261

262+
const handleSelectUsedInProjectChange = useCallback(
263+
(_event: React.SyntheticEvent, newValue: string[]) => {
264+
setValues((prev) => ({ ...prev, used_in_projects: newValue }));
265+
setErrors((prev) => ({ ...prev, used_in_projects: "" }));
266+
},
267+
[]
268+
);
269+
222270
const handleDateChange = useCallback((newDate: Dayjs | null) => {
223271
if (newDate?.isValid()) {
224272
setValues((prev) => ({
@@ -664,6 +712,68 @@ const NewModelInventory: FC<NewModelInventoryProps> = ({
664712
)}
665713
</Stack>
666714

715+
<Stack>
716+
<Typography
717+
sx={{
718+
fontSize: 13,
719+
fontWeight: 400,
720+
mb: theme.spacing(2),
721+
color: theme.palette.text.secondary,
722+
}}
723+
>
724+
Used in projects
725+
</Typography>
726+
<Autocomplete
727+
multiple
728+
id="projects-framework"
729+
size="small"
730+
value={values.used_in_projects}
731+
options={combinedList}
732+
onChange={handleSelectUsedInProjectChange}
733+
getOptionLabel={(option) => option}
734+
noOptionsText={
735+
values.used_in_projects.length === combinedList.length
736+
? "All projects selected"
737+
: "No options"
738+
}
739+
renderOption={(props, option) => (
740+
<Box component="li" {...props}>
741+
<Typography sx={{ fontSize: 13, fontWeight: 400 }}>
742+
{option}
743+
</Typography>
744+
</Box>
745+
)}
746+
filterSelectedOptions
747+
popupIcon={<GreyDownArrowIcon />}
748+
renderInput={(params) => (
749+
<TextField
750+
{...params}
751+
error={!!errors.used_in_projects}
752+
placeholder="Select projects-framework"
753+
sx={capabilitiesRenderInputStyle}
754+
/>
755+
)}
756+
sx={{
757+
backgroundColor: theme.palette.background.main,
758+
...capabilitiesSxStyle,
759+
}}
760+
slotProps={capabilitiesSlotProps}
761+
/>
762+
{errors.used_in_projects && (
763+
<Typography
764+
variant="caption"
765+
sx={{
766+
mt: 1,
767+
color: "#f04438",
768+
fontWeight: 300,
769+
fontSize: 11,
770+
}}
771+
>
772+
{errors.used_in_projects}
773+
</Typography>
774+
)}
775+
</Stack>
776+
667777
<Stack
668778
direction={"row"}
669779
gap={theme.spacing(8)}

Clients/src/presentation/pages/ModelInventory/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,7 @@ const ModelInventory: React.FC = () => {
797797
biases: selectedModelInventory.biases || "",
798798
limitations: selectedModelInventory.limitations || "",
799799
hosting_provider: selectedModelInventory.hosting_provider || "",
800+
used_in_projects: selectedModelInventory.used_in_projects,
800801
}
801802
: undefined
802803
}

Servers/controllers/modelInventory.ctrl.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export async function createNewModelInventory(req: Request, res: Response) {
122122
biases,
123123
limitations,
124124
hosting_provider,
125+
used_in_projects,
125126
is_demo,
126127
} = req.body;
127128

@@ -151,6 +152,7 @@ export async function createNewModelInventory(req: Request, res: Response) {
151152
biases,
152153
limitations,
153154
hosting_provider,
155+
used_in_projects,
154156
is_demo,
155157
});
156158

@@ -210,6 +212,7 @@ export async function updateModelInventoryById(req: Request, res: Response) {
210212
biases,
211213
limitations,
212214
hosting_provider,
215+
used_in_projects,
213216
is_demo,
214217
} = req.body;
215218

@@ -256,6 +259,7 @@ export async function updateModelInventoryById(req: Request, res: Response) {
256259
status,
257260
status_date,
258261
reference_link,
262+
used_in_projects,
259263
biases,
260264
limitations,
261265
hosting_provider,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
const { getTenantHash } = require("../../dist/tools/getTenantHash");
4+
5+
/** @type {import('sequelize-cli').Migration} */
6+
module.exports = {
7+
async up(queryInterface, Sequelize) {
8+
const transaction = await queryInterface.sequelize.transaction();
9+
try {
10+
const organizations = await queryInterface.sequelize.query(
11+
`SELECT id FROM organizations;`,
12+
{ transaction }
13+
);
14+
15+
for (let organization of organizations[0]) {
16+
const tenantHash = getTenantHash(organization.id);
17+
18+
// Add column with TEXT type and default value
19+
await queryInterface.sequelize.query(
20+
`ALTER TABLE "${tenantHash}".model_inventories
21+
ADD COLUMN used_in_projects TEXT NOT NULL DEFAULT '';`,
22+
{ transaction }
23+
);
24+
}
25+
26+
await transaction.commit();
27+
} catch (error) {
28+
await transaction.rollback();
29+
throw error;
30+
}
31+
},
32+
33+
async down(queryInterface, Sequelize) {
34+
const transaction = await queryInterface.sequelize.transaction();
35+
try {
36+
const organizations = await queryInterface.sequelize.query(
37+
`SELECT id FROM organizations;`,
38+
{ transaction }
39+
);
40+
41+
for (let organization of organizations[0]) {
42+
const tenantHash = getTenantHash(organization.id);
43+
44+
await queryInterface.sequelize.query(
45+
`ALTER TABLE "${tenantHash}".model_inventories
46+
DROP COLUMN used_in_projects;`,
47+
{ transaction }
48+
);
49+
}
50+
51+
await transaction.commit();
52+
} catch (error) {
53+
await transaction.rollback();
54+
throw error;
55+
}
56+
}
57+
};

Servers/domain.layer/interfaces/i.modelInventory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface IModelInventory {
1515
biases?: string;
1616
limitations?: string;
1717
hosting_provider?: string;
18+
used_in_projects: string;
1819
is_demo?: boolean;
1920
created_at?: Date;
2021
updated_at?: Date;

Servers/domain.layer/models/modelInventory/modelInventory.model.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ export class ModelInventoryModel
9898
})
9999
hosting_provider!: string;
100100

101+
@Column({
102+
type: DataType.TEXT,
103+
allowNull: false,
104+
})
105+
used_in_projects!: string;
106+
101107
@Column({
102108
type: DataType.BOOLEAN,
103109
allowNull: false,
@@ -198,6 +204,14 @@ export class ModelInventoryModel
198204
this.hosting_provider
199205
);
200206
}
207+
208+
if (!this.used_in_projects?.trim()) {
209+
throw new ValidationException(
210+
"Used in projects is required",
211+
"used_in_projects",
212+
this.used_in_projects
213+
);
214+
}
201215
}
202216

203217
/**
@@ -301,6 +315,7 @@ export class ModelInventoryModel
301315
biases: this.biases,
302316
limitations: this.limitations,
303317
hosting_provider: this.hosting_provider,
318+
used_in_projects: this.used_in_projects? this.used_in_projects.split(", ").filter((use) => use.trim()) : [],
304319
is_demo: this.is_demo,
305320
created_at: this.created_at?.toISOString(),
306321
updated_at: this.updated_at?.toISOString(),
@@ -335,6 +350,7 @@ export class ModelInventoryModel
335350
biases: this.biases,
336351
limitations: this.limitations,
337352
hosting_provider: this.hosting_provider,
353+
used_in_projects: this.used_in_projects? this.used_in_projects.split(", ").filter((use) => use.trim()) : [],
338354
is_demo: this.is_demo,
339355
created_at: this.created_at?.toISOString(),
340356
updated_at: this.updated_at?.toISOString(),
@@ -408,6 +424,9 @@ export class ModelInventoryModel
408424
biases: data.biases || "",
409425
limitations: data.limitations || "",
410426
hosting_provider: data.hosting_provider || "",
427+
used_in_projects: Array.isArray(data.used_in_projects)
428+
? data.used_in_projects.join(", ")
429+
: data.used_in_projects || "",
411430
is_demo: data.is_demo || false,
412431
created_at: new Date(),
413432
updated_at: new Date(),
@@ -465,6 +484,11 @@ export class ModelInventoryModel
465484
if (data.hosting_provider !== undefined) {
466485
existingModel.hosting_provider = data.hosting_provider;
467486
}
487+
if (data.used_in_projects !== undefined) {
488+
existingModel.used_in_projects = Array.isArray(data.used_in_projects)
489+
? data.used_in_projects.join(", ")
490+
: data.used_in_projects;
491+
}
468492
if (data.is_demo !== undefined) {
469493
existingModel.is_demo = data.is_demo;
470494
}

Servers/scripts/createNewTenant.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,7 @@ export const createNewTenant = async (organization_id: number, transaction: Tran
667667
biases VARCHAR(255) NOT NULL,
668668
limitations VARCHAR(255) NOT NULL,
669669
hosting_provider VARCHAR(255) NOT NULL,
670+
used_in_projects TEXT NOT NULL,
670671
is_demo BOOLEAN NOT NULL DEFAULT false,
671672
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
672673
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,

Servers/utils/modelInventory.utils.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ export const createNewModelInventoryQuery = async (
4040

4141
try {
4242
const result = await sequelize.query(
43-
`INSERT INTO "${tenant}".model_inventories (provider_model, provider, model, version, approver, capabilities, security_assessment, status, status_date, reference_link, biases, limitations, hosting_provider, is_demo, created_at, updated_at)
44-
VALUES (:provider_model, :provider, :model, :version, :approver, :capabilities, :security_assessment, :status, :status_date, :reference_link, :biases, :limitations, :hosting_provider, :is_demo, :created_at, :updated_at) RETURNING *`,
43+
`INSERT INTO "${tenant}".model_inventories (provider_model, provider, model, version, approver, capabilities, security_assessment, status, status_date, reference_link, biases, limitations, hosting_provider, used_in_projects, is_demo, created_at, updated_at)
44+
VALUES (:provider_model, :provider, :model, :version, :approver, :capabilities, :security_assessment, :status, :status_date, :reference_link, :biases, :limitations, :hosting_provider, :used_in_projects, :is_demo, :created_at, :updated_at) RETURNING *`,
4545
{
4646
replacements: {
4747
provider_model: modelInventory.provider_model || '',
@@ -59,6 +59,7 @@ export const createNewModelInventoryQuery = async (
5959
biases: modelInventory.biases,
6060
limitations: modelInventory.limitations,
6161
hosting_provider: modelInventory.hosting_provider,
62+
used_in_projects: modelInventory.used_in_projects,
6263
is_demo: modelInventory.is_demo,
6364
created_at: created_at,
6465
updated_at: created_at,
@@ -86,7 +87,7 @@ export const updateModelInventoryByIdQuery = async (
8687
try {
8788
// First update the record
8889
await sequelize.query(
89-
`UPDATE "${tenant}".model_inventories SET provider_model = :provider_model, provider = :provider, model = :model, version = :version, approver = :approver, capabilities = :capabilities, security_assessment = :security_assessment, status = :status, status_date = :status_date, reference_link = :reference_link, biases = :biases, limitations = :limitations, hosting_provider = :hosting_provider, is_demo = :is_demo, updated_at = :updated_at WHERE id = :id`,
90+
`UPDATE "${tenant}".model_inventories SET provider_model = :provider_model, provider = :provider, model = :model, version = :version, approver = :approver, capabilities = :capabilities, security_assessment = :security_assessment, status = :status, status_date = :status_date, reference_link = :reference_link, biases = :biases, limitations = :limitations, hosting_provider = :hosting_provider, used_in_projects = :used_in_projects, is_demo = :is_demo, updated_at = :updated_at WHERE id = :id`,
9091
{
9192
replacements: {
9293
id,
@@ -105,6 +106,7 @@ export const updateModelInventoryByIdQuery = async (
105106
biases: modelInventory.biases,
106107
limitations: modelInventory.limitations,
107108
hosting_provider: modelInventory.hosting_provider,
109+
used_in_projects: modelInventory.used_in_projects,
108110
is_demo: modelInventory.is_demo,
109111
updated_at,
110112
},

0 commit comments

Comments
 (0)