diff --git a/.github/workflows/gubbins-test.yml b/.github/workflows/gubbins-test.yml index a3f8381..f062f6b 100644 --- a/.github/workflows/gubbins-test.yml +++ b/.github/workflows/gubbins-test.yml @@ -54,12 +54,17 @@ jobs: prepare-gubbins-backend: runs-on: ubuntu-latest if: ${{ github.event_name != 'push' || github.repository == 'DIRACGrid/diracx-web' }} + defaults: + run: + # We need extglob for REFERENCE_BRANCH substitution + shell: bash -l -O extglob {0} steps: - name: Clone source run: | cd .. git clone https://github.com/DIRACGrid/diracx.git + # Prepare the gubbins extension - name: Where the magic happens (Move extensions to a temporary directory) run: | # We have to copy the code to another directory @@ -69,14 +74,61 @@ jobs: cp -r ../diracx/extensions/gubbins /tmp/ sed -i 's@../..@.@g' /tmp/gubbins/pyproject.toml sed -i 's@../../@@g' /tmp/gubbins/gubbins-*/pyproject.toml - git init /tmp/gubbins/ - name: Upload artifact uses: actions/upload-artifact@v4 with: name: gubbins path: /tmp/gubbins + include-hidden-files: true + # Prepare the gubbins image + # - Build the gubbins wheels + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Installing dependencies + run: | + cd ../diracx + python -m pip install \ + build \ + python-dateutil \ + pytz \ + readme_renderer[md] \ + requests \ + setuptools_scm + - name: Build distributions + run: | + cd ../diracx + for pkg_dir in $PWD/diracx-*; do + echo "Building $pkg_dir" + python -m build --outdir $PWD/dist $pkg_dir + done + # Also build the diracx metapackage + python -m build --outdir $PWD/dist . + # And build the gubbins package + for pkg_dir in $PWD/extensions/gubbins/gubbins-*; do + # Skip the testing package + if [[ "${pkg_dir}" =~ .*testing.* ]]; + then + echo "Do not build ${pkg_dir}"; + continue; + fi + echo "Building $pkg_dir" + python -m build --outdir $PWD/dist $pkg_dir + done + - name: "Find wheels" + id: find_wheel + run: | + cd ../diracx/dist + # We need to copy them there to be able to access them in the RUN --mount + cp diracx*.whl gubbins*.whl ../extensions/containers/services/ + for wheel_fn in *.whl; do + pkg_name=$(basename "${wheel_fn}" | cut -d '-' -f 1) + echo "${pkg_name}-wheel-name=$(ls "${pkg_name}"-*.whl)" >> $GITHUB_OUTPUT + done + + # - Build the gubbins image using the wheels - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx @@ -210,6 +262,10 @@ jobs: name: gubbins path: /tmp/gubbins + - name: Create a new git repository + run: | + git init /tmp/gubbins/ + - name: Clone diracx source run: | cd .. @@ -219,7 +275,7 @@ jobs: uses: actions/download-artifact@v4 with: name: gubbins-services-img - path: /tmp/gubbins_services_image.tar + path: /tmp/ - name: Load docker image run: docker load --input /tmp/gubbins_services_image.tar diff --git a/packages/diracx-web/test/e2e/login_out.cy.ts b/packages/diracx-web/test/e2e/loginOut.cy.ts similarity index 100% rename from packages/diracx-web/test/e2e/login_out.cy.ts rename to packages/diracx-web/test/e2e/loginOut.cy.ts diff --git a/packages/extensions/package.json b/packages/extensions/package.json index 061360e..ed4efbe 100644 --- a/packages/extensions/package.json +++ b/packages/extensions/package.json @@ -17,6 +17,11 @@ "dependencies": { "@axa-fr/react-oidc": "^7.22.6", "@dirac-grid/diracx-web-components": "0.1.0-a2", + "@mui/icons-material": "^6.1.6", + "@mui/material": "^6.1.6", + "@mui/utils": "^6.1.6", + "@mui/x-date-pickers": "^7.14.0", + "@tanstack/react-table": "^8.20.5", "autoprefixer": "10.4.19", "next": "15.0.2", "react": "^18", diff --git a/packages/extensions/src/app/(dashboard)/layout.tsx b/packages/extensions/src/app/(dashboard)/layout.tsx index 8af5ef4..710b6f8 100644 --- a/packages/extensions/src/app/(dashboard)/layout.tsx +++ b/packages/extensions/src/app/(dashboard)/layout.tsx @@ -10,8 +10,8 @@ import { DiracXWebProviders, } from "@dirac-grid/diracx-web-components/contexts"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { applicationList } from "@/gubbins/applicationList"; -import { defaultSections } from "@/gubbins/defaultUserDashboard"; +import { applicationList } from "@/gubbins/ApplicationList"; +import { defaultSections } from "@/gubbins/DefaultUserDashboard"; // Layout for the dashboard: setup the providers and the dashboard for the applications export default function DashboardLayout({ diff --git a/packages/extensions/src/app/(dashboard)/page.tsx b/packages/extensions/src/app/(dashboard)/page.tsx index 1ad1fe8..bafe743 100644 --- a/packages/extensions/src/app/(dashboard)/page.tsx +++ b/packages/extensions/src/app/(dashboard)/page.tsx @@ -3,7 +3,7 @@ import React, { useContext, useMemo } from "react"; import { useSearchParams } from "next/navigation"; import { BaseApp } from "@dirac-grid/diracx-web-components/components"; import { ApplicationsContext } from "@dirac-grid/diracx-web-components/contexts"; -import { applicationList } from "@/gubbins/applicationList"; +import { applicationList } from "@/gubbins/ApplicationList"; export default function Page() { const searchParams = useSearchParams(); // Get and set the search params from the URL diff --git a/packages/extensions/src/gubbins/ApplicationList.ts b/packages/extensions/src/gubbins/ApplicationList.ts new file mode 100644 index 0000000..1b1e2c4 --- /dev/null +++ b/packages/extensions/src/gubbins/ApplicationList.ts @@ -0,0 +1,13 @@ +import { applicationList } from "@dirac-grid/diracx-web-components/components"; +import { ApplicationMetadata } from "@dirac-grid/diracx-web-components/types"; +import ElectricScooterIcon from "@mui/icons-material/ElectricScooter"; +import OwnerMonitor from "@/gubbins/components/OwnerMonitor/OwnerMonitor"; + +// New Application List with the default ones + the Owner Monitor +const appList: ApplicationMetadata[] = [ + ...applicationList, + { name: "Owner Monitor", component: OwnerMonitor, icon: ElectricScooterIcon }, +]; + +export { appList as applicationList }; +export default appList; diff --git a/packages/extensions/src/gubbins/DefaultUserDashboard.tsx b/packages/extensions/src/gubbins/DefaultUserDashboard.tsx new file mode 100644 index 0000000..1983cae --- /dev/null +++ b/packages/extensions/src/gubbins/DefaultUserDashboard.tsx @@ -0,0 +1,29 @@ +import { DashboardGroup } from "@dirac-grid/diracx-web-components/types"; +import { BugReport } from "@mui/icons-material"; +import { applicationList } from "@/gubbins/ApplicationList"; + +// New default user sections +export const defaultSections: DashboardGroup[] = [ + { + title: "My Gubbins Apps", + extended: true, + items: [ + { + title: "Owners", + id: "OwnerMonitor1", + type: "Owner Monitor", + icon: + applicationList.find((app) => app.name === "Owner Monitor")?.icon || + BugReport, + }, + { + title: "My Jobs", + id: "JobMonitor1", + type: "Job Monitor", + icon: + applicationList.find((app) => app.name === "Job Monitor")?.icon || + BugReport, + }, + ], + }, +]; diff --git a/packages/extensions/src/gubbins/applicationList.ts b/packages/extensions/src/gubbins/applicationList.ts deleted file mode 100644 index 99f0bcf..0000000 --- a/packages/extensions/src/gubbins/applicationList.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { applicationList } from "@dirac-grid/diracx-web-components/components"; -import { ApplicationMetadata } from "@dirac-grid/diracx-web-components/types"; -import { BugReport } from "@mui/icons-material"; -import TestApp from "@/gubbins/components/TestApp/testApp"; - -// New Application List with the default ones + the Test app -const appList: ApplicationMetadata[] = [ - ...applicationList, - { name: "Test App", component: TestApp, icon: BugReport }, -]; - -export { appList as applicationList }; -export default appList; diff --git a/packages/extensions/src/gubbins/components/OwnerMonitor/OwnerMonitor.tsx b/packages/extensions/src/gubbins/components/OwnerMonitor/OwnerMonitor.tsx new file mode 100644 index 0000000..0514036 --- /dev/null +++ b/packages/extensions/src/gubbins/components/OwnerMonitor/OwnerMonitor.tsx @@ -0,0 +1,158 @@ +"use client"; +import React, { useEffect, useMemo, useState } from "react"; +import { useOidcAccessToken } from "@axa-fr/react-oidc"; +import { + fetcher, + useOIDCContext, +} from "@dirac-grid/diracx-web-components/hooks"; +import { + Alert, + Box, + Button, + Snackbar, + TextField, + Typography, +} from "@mui/material"; +import { + createColumnHelper, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { DataTable } from "@dirac-grid/diracx-web-components/components"; +import { Owner } from "@/gubbins/types/Owner"; + +/** + * Owner Monitor component + * @returns Owner Monitor component + */ +export default function OwnerMonitor() { + // Get info from the auth token + const { configuration } = useOIDCContext(); + const { accessToken } = useOidcAccessToken(configuration?.scope); + + const [owners, setOwners] = useState([]); + const [ownerName, setOwnerName] = useState(""); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 25, // Default to 25 rows per page + }); + + // Fetch the list of owners + const fetchOwners = async () => { + try { + setIsLoading(true); + const response = await fetcher([ + "/api/lollygag/get_owners", + accessToken, + ]); + + // Transform names into objects with id and name + const transformedData = response.data.map((name, index) => ({ + ownerID: index + 1, // Generate a unique ID + name, // Set the name + })); + setOwners(transformedData); + } catch (err) { + setError("Failed to fetch owners"); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchOwners(); + }, [accessToken]); + + // Handle adding a new owner + const handleAddOwner = async () => { + if (!ownerName) return setError("Owner name cannot be empty."); + try { + await fetcher([ + `/api/lollygag/insert_owner/${ownerName}`, + accessToken, + "POST", + ]); + setSuccess(`Owner "${ownerName}" added successfully.`); + setOwnerName(""); + fetchOwners(); // Refresh the owners list + } catch (err) { + setError("Failed to add owner."); + } + }; + + // Define table columns + const columnHelper = createColumnHelper(); + const columns = useMemo( + () => [ + columnHelper.accessor("ownerID", { header: "ID" }), + columnHelper.accessor("name", { header: "Owner Name" }), + ], + [columnHelper], + ); + + // Table instance + const table = useReactTable({ + data: owners, + columns, + state: { pagination }, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + }); + + return ( + + {/* Input to add owner */} + + setOwnerName(e.target.value)} + variant="outlined" + fullWidth + data-testid="owner-name-input" + /> + + + + {/* Success and Error messages */} + {error && ( + setError(null)}> + {error} + + )} + {success && ( + setSuccess(null)}> + setSuccess(null)} severity="success"> + {success} + + + )} + + {/* Owner List Table */} + + title="Owners List" + table={table} + totalRows={owners.length} + searchBody={{}} + setSearchBody={() => {}} + error={null} + isLoading={isLoading} + isValidating={isLoading} + toolbarComponents={<>} + menuItems={[]} + /> + + ); +} diff --git a/packages/extensions/src/gubbins/components/TestApp/testApp.tsx b/packages/extensions/src/gubbins/components/TestApp/testApp.tsx deleted file mode 100644 index 8edc528..0000000 --- a/packages/extensions/src/gubbins/components/TestApp/testApp.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; -import React from "react"; -import { useOidcAccessToken } from "@axa-fr/react-oidc"; -import { useOIDCContext } from "@dirac-grid/diracx-web-components/hooks"; -import { Box } from "@mui/material"; - -/** - * Build a Test page - * @returns the example component - */ -export default function UserDashboard() { - // Get info from the auth token - const { configuration } = useOIDCContext(); - const { accessTokenPayload } = useOidcAccessToken(configuration?.scope); - - if (!accessTokenPayload) { - return
Not authenticated
; - } - - return ( -
-

Hello {accessTokenPayload["preferred_username"]}👋

- -

This is a test application

-
-
- ); -} diff --git a/packages/extensions/src/gubbins/defaultUserDashboard.tsx b/packages/extensions/src/gubbins/defaultUserDashboard.tsx deleted file mode 100644 index 4bf278c..0000000 --- a/packages/extensions/src/gubbins/defaultUserDashboard.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { DashboardGroup } from "@dirac-grid/diracx-web-components/types"; -import { BugReport } from "@mui/icons-material"; -import { applicationList } from "@/gubbins/applicationList"; - -// New default user sections -export const defaultSections: DashboardGroup[] = [ - { - title: "Default Applications", - extended: true, - items: [ - { - title: "Test App", - id: "Test App 1", - type: "Test App", - icon: - applicationList.find((app) => app.name === "Test App")?.icon || - BugReport, - }, - { - title: "Dashboard", - id: "Dashboard 1", - type: "Dashboard", - icon: - applicationList.find((app) => app.name === "Dashboard")?.icon || - BugReport, - }, - { - title: "Job Monitor", - id: "Job Monitor 1", - type: "Job Monitor", - icon: - applicationList.find((app) => app.name === "Job Monitor")?.icon || - BugReport, - }, - { - title: "File Catalog", - id: "File Catalog 1", - type: "File Catalog", - icon: - applicationList.find((app) => app.name === "File Catalog")?.icon || - BugReport, - }, - ], - }, -]; diff --git a/packages/extensions/src/gubbins/types/Owner.ts b/packages/extensions/src/gubbins/types/Owner.ts new file mode 100644 index 0000000..a26ec09 --- /dev/null +++ b/packages/extensions/src/gubbins/types/Owner.ts @@ -0,0 +1,6 @@ +export interface Owner { + ownerID: number; + name: string; + // If you want to create DataTable columns for the Owner type, you can use the following snippet: + [key: string]: unknown; +} diff --git a/packages/extensions/test/e2e/login_out.cy.ts b/packages/extensions/test/e2e/loginOut.cy.ts similarity index 100% rename from packages/extensions/test/e2e/login_out.cy.ts rename to packages/extensions/test/e2e/loginOut.cy.ts diff --git a/packages/extensions/test/e2e/ownerMonitor.cy.ts b/packages/extensions/test/e2e/ownerMonitor.cy.ts new file mode 100644 index 0000000..ff81c40 --- /dev/null +++ b/packages/extensions/test/e2e/ownerMonitor.cy.ts @@ -0,0 +1,72 @@ +/// + +describe("Job Monitor", () => { + beforeEach(() => { + cy.session("login", () => { + cy.visit("/"); + //login + cy.get('[data-testid="button-login"]').click(); + cy.get("#login").type("admin@example.com"); + cy.get("#password").type("password"); + + // Find the login button and click on it + cy.get("button").click(); + // Grant access + cy.get(":nth-child(1) > form > .dex-btn").click(); + cy.url().should("include", "/auth"); + }); + + // Visit the page where the Job Monitor is rendered + cy.visit( + "/?appId=OwnerMonitor1&dashboard=%5B3Gubbins+Apps%27~extended%21true~items%21%5B-020.04~data%21%5B%5D%29%2C3Job2Job.Job4%29%5D%29%5D*Monitor-%28%27title%21%27.*1%27~type%21%270Owner2s%27~id%21%273-My+4+*%27%014320.-*_", + ); + }); + + it("should render the drawer", () => { + cy.get("header").contains("Owner Monitor").should("be.visible"); + }); + + /** Input field interactions */ + + it("adds a new owner and verifies it in the table", () => { + // Type the new owner name + cy.get('[data-testid="owner-name-input"]').type("Josephine"); + + // Click the Add Owner button + cy.contains("button", "Add Owner").click(); + + cy.get('[data-testid="virtuoso-scroller"]') + .wait(100) // Wait for rendering + .scrollTo("bottom", { ensureScrollable: false }); + + // Assert the table contains the new owner + cy.get("table tbody tr:last-child td:last-child").should( + "contain.text", + "Josephine", + ); + }); + + /** Column interactions */ + + it("should hide/show columns", () => { + // Click on the visibility icon + cy.get('[data-testid="VisibilityIcon"] > path').click(); + cy.get('[data-testid="column-visibility-popover"]').should("be.visible"); + + // Hide the "Owner Name" column + cy.get('[data-testid="column-visibility-popover"]').within(() => { + cy.contains("Owner Name").parent().find('input[type="checkbox"]').click(); + }); + + // Close the popover by clicking outside + cy.get("body").click(0, 0); + cy.get('[data-testid="column-visibility-popover"]').should("not.exist"); + + // Loop over the table column and make sure that "VO" is present + cy.get("table thead tr th").each(($th) => { + if ($th.text() === "Owner Name") { + expect($th).to.exist; + } + }); + }); +});