Skip to content

Commit

Permalink
feat: ✨ add resource handler
Browse files Browse the repository at this point in the history
  • Loading branch information
Pauline Didier committed Dec 5, 2024
1 parent 97ff55c commit 7a29f1c
Show file tree
Hide file tree
Showing 17 changed files with 543 additions and 4 deletions.
5 changes: 2 additions & 3 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
"lint": "prettier --check src/",
"lint-fix": "prettier -l src/ --write",
"debug": "NODE_ENV=development node --loader ts-node/esm --inspect=127.0.0.1 src/index.js",
"test": "NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test jest --detectOpenHandles --no-cache --runInBand --logHeapUsage --forceExit",
"test-validate": "NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test jest --detectOpenHandles --no-cache ./src/resources/gram/v1/validation/validateModel.spec.ts"
"test": "NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test jest --detectOpenHandles --no-cache --runInBand --logHeapUsage --forceExit"
},
"main": "dist/index.js",
"repository": {
Expand Down Expand Up @@ -66,4 +65,4 @@
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.2"
}
}
}
4 changes: 4 additions & 0 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { systemsRouter } from "./resources/gram/v1/systems/router.js";
import { validationRouter } from "./resources/gram/v1/validation/router.js";
import { getFlowAttributes } from "./resources/gram/v1/attributes/get.js";
import { flowsRouter } from "./resources/gram/v1/flows/router.js";
import { resourceRouter } from "./resources/gram/v1/resources/router.js";

export async function createApp(dal: DataAccessLayer) {
// Start constructing the app.
Expand Down Expand Up @@ -232,6 +233,9 @@ export async function createApp(dal: DataAccessLayer) {
// Model Validation
authenticatedRoutes.use("/validate", validationRouter(dal));

// Resources
authenticatedRoutes.use("/resources", resourceRouter(dal));

// Report Routes
authenticatedRoutes.get(
"/reports/system-compliance",
Expand Down
56 changes: 56 additions & 0 deletions api/src/resources/gram/v1/resources/resource.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { DataAccessLayer } from "@gram/core/dist/data/dal.js";
import { createTestApp } from "../../../../test-util/app.js";
import { sampleUserToken } from "../../../../test-util/sampleTokens.js";
import { testResourceProvider } from "../../../../test-util/testResourceHandler.js";
import request from "supertest";
import { createSampleModel } from "../../../../test-util/model.js";
describe("getResources", () => {
let app: any;
let token: string;
let dal: DataAccessLayer;
let validModelId: string;
beforeAll(async () => {
({ app, dal } = await createTestApp());
token = await sampleUserToken();
validModelId = await createSampleModel(dal);
//dal.resourceHandler.register(testResourceProvider);
});

it("should return 401 on un-authenticated request", async () => {
const res = await request(app).get("/api/v1/resources/12323");
expect(res.status).toBe(401);
});
it("should return 401 when using invalid user token", async () => {
const res = await request(app)
.get("/api/v1/resources/12323")
.set("Authorization", "invalidtoken");
expect(res.status).toBe(401);
});

it("should return 200 on successful get resources", async () => {
const res = await request(app)
.get("/api/v1/resources/" + validModelId)
.set("Authorization", token);
expect(res.status).toBe(200);
});

it("should return an empty list if no resource provider is registered ", async () => {
const res = await request(app)
.get("/api/v1/resources/" + validModelId)
.set("Authorization", token);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body.length).toEqual(0);
});

it("should return the resources from the registered resource providers", async () => {
dal.resourceHandler.register(testResourceProvider);
const res = await request(app)
.get("/api/v1/resources/" + validModelId)
.set("Authorization", token);
const result = res.body[0];
expect(result.id).toEqual("test-resource-id");
expect(result.displayName).toEqual("Test Resource");
expect(result.type).toEqual("external entity");
expect(result.systemId).toEqual("another-mocked-system-id");
});
});
13 changes: 13 additions & 0 deletions api/src/resources/gram/v1/resources/resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Request, Response } from "express";
import { DataAccessLayer } from "@gram/core/dist/data/dal.js";

export function getResources(dal: DataAccessLayer) {
return async (req: Request, res: Response) => {
const modelId = req.params.id;
const model = await dal.modelService.getById(modelId);
if (model && model.systemId) {
const resources = await dal.resourceHandler.getResources(model.systemId);
return res.json(resources);
}
};
}
11 changes: 11 additions & 0 deletions api/src/resources/gram/v1/resources/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DataAccessLayer } from "@gram/core/dist/data/dal.js";
import express from "express";
import { errorWrap } from "../../../../util/errorHandler.js";
import { getResources } from "./resource.js";

export function resourceRouter(dal: DataAccessLayer): express.Router {
const router = express.Router();

router.get("/:id", errorWrap(getResources(dal)));
return router;
}
24 changes: 24 additions & 0 deletions api/src/test-util/testResourceHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
Resource,
ResourceProvider,
} from "@gram/core/dist/resources/ResourceHandler.js";

class TestResourceProvider implements ResourceProvider {
key = "testProvider";
async listResources(systemId: string): Promise<Resource[]> {
if (systemId === "mocked-system-id") {
return [
{
id: "test-resource-id",
displayName: "Test Resource",
type: "external entity",
systemId: "another-mocked-system-id",
attributes: {},
},
];
}
return [];
}
}

export const testResourceProvider = new TestResourceProvider();
1 change: 1 addition & 0 deletions app/src/api/gram/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const api = createApi({
"User",
"Links",
"Validation",
"Resources",
],
baseQuery: fetchBaseQuery({
baseUrl: `${BASE_URL}/api/v1/`,
Expand Down
15 changes: 15 additions & 0 deletions app/src/api/gram/resources.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { api } from "./api";

const resourceApi = api.injectEndpoints({
endpoints: (build) => ({
getResources: build.query({
query: (modelId) => `/resources/${modelId}`,
transformResponse: (response) => {
return response;
},
providesTags: ["Resources"],
}),
}),
});

export const { useGetResourcesQuery } = resourceApi;
8 changes: 7 additions & 1 deletion app/src/components/model/panels/left/LeftPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ToggleLeftPanelButton } from "../../board/components/ToggleLeftPanelBut
import { COMPONENT_TYPE } from "../../board/constants";
import { useSelectedComponent } from "../../hooks/useSelectedComponent";
import { ActionItemTab } from "./ActionItemTab";
import { ResourceTab } from "./ResourceTab";
import { ComponentTab } from "./ComponentTab";
import { DataFlowTab } from "./DataFlowTab";
import { LeftFooter } from "./Footer";
Expand All @@ -16,6 +17,7 @@ const TAB = {
ACTION_ITEMS: 1,
COMPONENT: 2,
DATA_FLOW: 3,
RESOURCES: 4,
};

function TabPanel({ children, value, index, ...other }) {
Expand Down Expand Up @@ -86,6 +88,7 @@ export function LeftPanel() {
<Tabs
value={tabHck}
onChange={(_, v) => setTab(v)}
scrollButtons="auto"
textColor="inherit"
variant="fullWidth"
sx={{
Expand All @@ -102,6 +105,7 @@ export function LeftPanel() {
{isDataFlow && (
<Tab disableRipple label="DATA FLOW" value={TAB.DATA_FLOW} />
)}
<Tab disableRipple label="RESOURCES" value={TAB.RESOURCES} />
</Tabs>
</Grow>
</AppBar>
Expand All @@ -118,7 +122,9 @@ export function LeftPanel() {
<TabPanel value={tab} index={TAB.DATA_FLOW}>
{isDataFlow && <DataFlowTab />}
</TabPanel>

<TabPanel value={tab} index={TAB.RESOURCES}>
<ResourceTab />
</TabPanel>
<LeftFooter />
</Box>
);
Expand Down
91 changes: 91 additions & 0 deletions app/src/components/model/panels/left/ResourceFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
Box,
Typography,
Chip,
TextField,
InputAdornment,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import { GROUP_BY } from "./ResourceTab";
function toggleGroupBy(groupBy, setGroupBy, value) {
if (groupBy === value) {
console.log("should reset to null");
setGroupBy(null);
} else {
setGroupBy(value);
}
}

export function ResourceFilter({
groupBy,
setGroupBy,
isLoading,
searchInput,
setSearchInput,
}) {
if (isLoading) {
return null;
}
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "1em",
marginY: "1em",
}}
>
<Box
sx={{
display: "flex",
gap: "1em",
alignItems: "center",
}}
>
<Typography>Group by:</Typography>
<Box sx={{ display: "flex", gap: "0.5em" }}>
{Object.values(GROUP_BY).map((gb) => {
if (groupBy === gb.value) {
return (
<Chip
key={gb.value}
label={gb.label}
onClick={() => toggleGroupBy(groupBy, setGroupBy, gb.value)}
color="primary"
/>
);
}
return (
<Chip
key={gb.value}
label={gb.label}
onClick={() => toggleGroupBy(groupBy, setGroupBy, gb.value)}
variant="outlined"
/>
);
})}
</Box>
</Box>

<TextField
fullWidth
id="standard-basic"
placeholder="Search by id, name or system id"
variant="standard"
value={searchInput}
onChange={(event) => {
setSearchInput(event.target.value);
}}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end" onClick={() => setSearchInput("")}>
<CloseIcon />
</InputAdornment>
),
},
}}
/>
</Box>
);
}
Loading

0 comments on commit 7a29f1c

Please sign in to comment.