Skip to content

Commit

Permalink
✨ Add matrix profile workflow page
Browse files Browse the repository at this point in the history
  • Loading branch information
agmangas committed Oct 9, 2024
1 parent f636cd9 commit 1dbf7a3
Show file tree
Hide file tree
Showing 10 changed files with 639 additions and 156 deletions.
8 changes: 7 additions & 1 deletion moderate_ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
Global,
MantineProvider,
} from "@mantine/core";

import { useLocalStorage } from "@mantine/hooks";
import { NotificationsProvider } from "@mantine/notifications";
import { useKeycloak } from "@react-keycloak/web";
Expand All @@ -29,6 +28,7 @@ import { useTranslation } from "react-i18next";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import { getBaseApiUrl } from "./api/utils";
import { buildKeycloakAuthProvider } from "./auth-provider/keycloak";
import { useRefreshToken } from "./auth-provider/utils";
import { FooterLinks } from "./components/FooterLinks";
import { HeaderMegaMenu } from "./components/HeaderMegaMenu";
import { Catalogue } from "./pages/Catalogue";
Expand All @@ -39,6 +39,7 @@ import { AssetObjectShow } from "./pages/asset-objects/Show";
import { AssetCreate, AssetEdit, AssetList, AssetShow } from "./pages/assets";
import { Login } from "./pages/login";
import { NotebookExploratory } from "./pages/notebooks/Exploratory";
import { MatrixProfileWorkflow } from "./pages/notebooks/MatrixProfile";
import { dataProvider } from "./rest-data-provider";
import { ResourceNames } from "./types";

Expand All @@ -51,6 +52,7 @@ function App() {

const { keycloak, initialized } = useKeycloak();
const { t, i18n } = useTranslation();
useRefreshToken();

const toggleColorScheme = (value?: ColorScheme) =>
setColorScheme(value || (colorScheme === "dark" ? "light" : "dark"));
Expand Down Expand Up @@ -190,6 +192,10 @@ function App() {
path="exploratory"
element={<NotebookExploratory />}
/>
<Route
path="matrix-profile"
element={<MatrixProfileWorkflow />}
/>
</Route>
<Route path="/catalogue" element={<Catalogue />} />
<Route path="/tools" element={<ToolsCatalogue />} />
Expand Down
42 changes: 42 additions & 0 deletions moderate_ui/src/api/job.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import axios from "axios";
import { AssetObjectModel, WorkflowJob, WorkflowJobType } from "./types";
import { buildApiUrl } from "./utils";

export async function createMatrixProfileJob({
assetObject,
analysisVariable,
}: {
assetObject: AssetObjectModel;
analysisVariable: string;
}): Promise<WorkflowJob> {
const data = {
job_type: WorkflowJobType.MATRIX_PROFILE,
arguments: {
uploaded_s3_object_id: assetObject.data.id,
analysis_variable: analysisVariable,
},
};

const response = await axios.post(buildApiUrl("job"), data);
return response.data;
}

export async function getJob({
jobId,
withExtendedResults,
}: {
jobId: number | string;
withExtendedResults?: string | boolean;
}): Promise<WorkflowJob> {
const params: { [k: string]: string } = {};

if (withExtendedResults) {
params.with_extended_results = "true";
}

const response = await axios.get(buildApiUrl("job", jobId.toString()), {
params,
});

return response.data;
}
19 changes: 16 additions & 3 deletions moderate_ui/src/api/ping.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import axios from "axios";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { buildApiUrl } from "./utils";

export function usePing() {
export function usePing({ intervalMs }: { intervalMs?: number } = {}) {
const [pingResult, setPingResult] = useState<object | false | undefined>(
undefined
);

useEffect(() => {
const ping = useCallback(() => {
axios
.get(buildApiUrl("ping"))
.then((response) => {
Expand All @@ -19,5 +19,18 @@ export function usePing() {
});
}, []);

useEffect(() => {
if (!intervalMs) {
ping();
return;
}

const intervalId = setInterval(() => {
ping();
}, intervalMs);

return () => clearInterval(intervalId);
}, [intervalMs, ping]);

return { pingResult };
}
15 changes: 15 additions & 0 deletions moderate_ui/src/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import _ from "lodash";

export enum WorkflowJobType {
MATRIX_PROFILE = "matrix_profile",
}

export interface WorkflowJob {
arguments: { [k: string]: any };
created_at: string;
creator_username: string;
finalised_at: string | null;
id: number;
job_type: WorkflowJobType;
results: { [k: string]: any } | null;
extended_results?: { [k: string]: any };
}

export enum AssetAccessLevel {
PRIVATE = "private",
PUBLIC = "public",
Expand Down
31 changes: 31 additions & 0 deletions moderate_ui/src/auth-provider/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useKeycloak } from "@react-keycloak/web";
import { useCallback, useEffect } from "react";
import { buildKeycloakAuthProvider } from "./keycloak";

export function useRefreshToken({
intervalMs = 25000,
}: { intervalMs?: number } = {}) {
const { keycloak, initialized } = useKeycloak();

const refreshToken = useCallback(async () => {
if (!initialized) {
return;
}

const authProvider = buildKeycloakAuthProvider({ keycloak });
await authProvider.refreshToken();
}, [initialized, keycloak]);

useEffect(() => {
if (!intervalMs) {
refreshToken();
return;
}

const intervalId = setInterval(() => {
refreshToken();
}, intervalMs);

return () => clearInterval(intervalId);
}, [intervalMs, refreshToken]);
}
139 changes: 91 additions & 48 deletions moderate_ui/src/components/AssetObjectCard.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,107 @@
import { Badge, Card, Group, Text, createStyles } from "@mantine/core";
import { DateTime } from "luxon";
import { Badge, Button, Card, Group, Stack, Text } from "@mantine/core";
import {
IconBox,
IconClock,
IconExternalLink,
IconLockAccess,
} from "@tabler/icons-react";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { Asset, AssetModel, AssetObject } from "../api/types";

const useStyles = createStyles((theme) => ({
card: {
transition: "0.3s",
"&:hover": {
backgroundColor:
theme.colorScheme === "dark"
? theme.colors.dark[4]
: theme.colors.gray[1],
},
},
}));
export const AssetObjectCard: React.FC<{
asset: Asset;
assetObject: AssetObject;
maxHeight?: boolean;
}> = ({ asset, assetObject, maxHeight }) => {
const { t } = useTranslation();

const parseObjectName = (
name: string
): { name: string; extension?: string } => {
const parts = name.split("/");
const lastPart = parts[parts.length - 1];
const partsExtension = lastPart.split(".");
const [assetModel, assetObjectModel] = useMemo(() => {
const assetModel = new AssetModel(asset);
const assetObjectModel = assetModel.getObject(assetObject.id);

return {
name: partsExtension[0],
extension: partsExtension.length > 1 ? partsExtension[1] : undefined,
};
};
if (!assetObjectModel) {
throw new Error("Asset object not found");
}

export const AssetObjectCard: React.FC<{
asset: { [key: string]: any };
assetObject: { [key: string]: any };
}> = ({ assetObject, asset }) => {
const { classes } = useStyles();
return [assetModel, assetObjectModel];
}, [asset, assetObject.id]);

const { name, extension } = useMemo(
() => parseObjectName(assetObject.key),
[assetObject]
);
const features = useMemo(() => {
return [
{
label: t("catalogue.card.asset", "Asset"),
value: assetModel.data.name,
icon: IconBox,
},
{
label: t("catalogue.card.createdAt", "Uploaded"),
value: assetObjectModel.createdAt.toLocaleString(),
icon: IconClock,
},
{
label: t("catalogue.card.accessLevel", "Access level"),
value: <Badge color="gray">{assetModel.data.access_level}</Badge>,
icon: IconLockAccess,
},
];
}, [t, assetModel, assetObjectModel]);

return (
<Card
className={classes.card}
component={Link}
to={`/assets/${asset.id}/objects/show/${assetObject.id}`}
withBorder
p="sm"
shadow="sm"
p="lg"
radius="md"
withBorder
style={{ height: maxHeight ? "100%" : "inherit" }}
>
<Card.Section withBorder inheritPadding py="sm">
<Group mb="sm">
<Badge color="gray">
{DateTime.fromISO(assetObject.created_at).toLocaleString(
DateTime.DATETIME_FULL
)}
<Group position="apart" mb="xs">
<Text weight={500} truncate>
{assetObjectModel.humanName}
</Text>
{assetObjectModel.parsedKey?.ext && (
<Badge color="pink" variant="light">
{assetObjectModel.parsedKey.ext.toUpperCase()}
</Badge>
<Badge color="cyan">{extension}</Badge>
</Group>
<Text>{name}</Text>
</Card.Section>
)}
</Group>
<Button
variant="light"
leftIcon={<IconExternalLink size="1em" />}
fullWidth
mt="md"
mb="md"
radius="md"
target="_blank"
component={Link}
to={`/assets/${asset.id}/objects/show/${assetObject.id}`}
>
{t("catalogue.card.view", "View details")}
</Button>
<Stack spacing="xs">
{features.map((feature, idx) => (
<Group spacing={0} position="left" key={idx}>
<Text
color="dimmed"
size="sm"
style={{ display: "flex", alignItems: "center" }}
mr="xs"
>
<feature.icon size="1rem" style={{ marginRight: "0.25rem" }} />
{feature.label}
</Text>
<Text size="sm" truncate>
{feature.value}
</Text>
</Group>
))}
</Stack>
{assetObjectModel.description && (
<Text mt="md" size="sm" color="dimmed" lineClamp={5}>
{assetObjectModel.description}
</Text>
)}
</Card>
);
};
Loading

0 comments on commit 1dbf7a3

Please sign in to comment.