added comp_systems and modular instances frontend

This commit is contained in:
2026-05-06 14:46:16 +03:00
parent 45c157b910
commit 26623dc2c1
44 changed files with 7739 additions and 899 deletions

View File

@@ -1,14 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="preload" href="/Quicking.otf" as="font" type="font/otf" crossorigin>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QMolSim</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link
rel="preload"
href="/Quicking.otf"
as="font"
type="font/otf"
crossorigin
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QMolSim</title>
<style type="text/css">
body {
background: none transparent;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

5975
public/index.html Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,82 @@
import React, { useRef, useEffect, useState, useMemo } from "react";
import { useUserPreferencesStore } from "Stores/PreferencesStore";
import type { TaskData } from "Types/Experiment/Experiment";
interface IframePluginProps {
pluginUrl: string;
mode: "list" | "editor";
taskData: TaskData<any>;
onUpdate?: (data: any) => void;
}
export const IframePlugin: React.FC<IframePluginProps> = ({
pluginUrl,
mode,
taskData,
onUpdate,
}) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const theme = useUserPreferencesStore();
const [isIframeReady, setIsIframeReady] = useState(false);
// Memoize the iframe component to prevent recreation when taskData changes
const memoizedIframe = useMemo(() => {
const url = `${pluginUrl}?mode=${mode}`;
return (
<iframe
ref={iframeRef}
src={url}
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
style={{
width: "100%",
display: "flex",
flexDirection: "column",
flexGrow: 1,
border: "none",
}}
title="Plugin"
/>
);
}, [pluginUrl, mode]); // Only recreate when pluginUrl or mode changes
// Send data to iframe when taskData or theme changes, or when iframe becomes ready
useEffect(() => {
if (!isIframeReady || !iframeRef.current?.contentWindow) return;
const message = {
type: "plugin-data",
data: {
taskData,
theme: theme.theme,
},
};
iframeRef.current.contentWindow.postMessage(message, "*");
}, [taskData, theme.theme, isIframeReady]);
// Listen for messages from iframe
useEffect(() => {
const handler = (event: MessageEvent) => {
// Handle iframe ready signal
if (event.data.type === "plugin-ready") {
setIsIframeReady(true);
return;
}
// Handle plugin updates
if (event.data.type === "plugin-update" && onUpdate) {
onUpdate(event.data.data);
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, [onUpdate]);
// Reset ready state when URL changes (new plugin or mode)
useEffect(() => {
setIsIframeReady(false);
}, [pluginUrl, mode]);
return memoizedIframe;
};

View File

@@ -0,0 +1,110 @@
import keycloak from "Api/Keycloak/Keycloak";
import axios from "axios";
import type {
ComputationalSystemCreateRequest,
ComputationalSystemCreateResponse,
ComputationalSystemEditRequest,
ComputationalSystemListResponse,
SystemWithTeams,
SystemStatusResponse,
GiveSystemToTeamRequest,
RemoveSystemFromTeamRequest,
ComputationalSystemDeleteRequest,
} from "Types/Machine/Machine";
const api = axios.create({
baseURL: `${import.meta.env.VITE_QUANTUM_BACKEND_URL}/machine`,
headers: { "Content-Type": "application/json" },
});
// Add auth token to requests
api.interceptors.request.use((config) => {
const token = keycloak.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 1. Create or get computational system
export const createOrGetComputationalSystem = async (
data: ComputationalSystemCreateRequest,
): Promise<ComputationalSystemCreateResponse> => {
const response = await api.post<ComputationalSystemCreateResponse>("", data);
return response.data;
};
// 2. Edit computational system (owner only)
export const editComputationalSystem = async (
data: ComputationalSystemEditRequest,
): Promise<ComputationalSystemEditRequest> => {
const response = await api.put<ComputationalSystemEditRequest>("", data);
return response.data;
};
// 3. Get my systems (owned by current user)
export const getMySystems = async ({
page_num = 1,
page_size = undefined,
}): Promise<ComputationalSystemListResponse> => {
const response = await api.get<ComputationalSystemListResponse>("", {
params: { page_num, page_size },
});
return response.data;
};
// 4. Get team systems (systems shared with a team)
export const getTeamSystems = async (
team_id: number,
page_num: number = 1,
page_size: number = 10,
): Promise<ComputationalSystemListResponse> => {
const response = await api.get<ComputationalSystemListResponse>("/team", {
params: { team_id, page_num, page_size },
});
return response.data;
};
// 5. Get computational system by ID
export const getComputationalSystemById = async (
system_id: number,
): Promise<SystemWithTeams> => {
const response = await api.get<SystemWithTeams>("/system", {
params: { system_id },
});
return response.data;
};
// 6. Get system status
export const getSystemStatus = async (
system_id: number,
): Promise<SystemStatusResponse> => {
const response = await api.get<SystemStatusResponse>("/status", {
params: { system_id },
});
return response.data;
};
// 7. Give system to team (share access)
export const giveSystemToTeam = async (
data: GiveSystemToTeamRequest,
): Promise<GiveSystemToTeamRequest> => {
const response = await api.put<GiveSystemToTeamRequest>("/team", data);
return response.data;
};
// 8. Remove system from team (revoke access)
export const removeSystemFromTeam = async (
data: RemoveSystemFromTeamRequest,
): Promise<{ message: string }> => {
const response = await api.delete<{ message: string }>("/team", { data });
return response.data;
};
// 9. Delete computational system (owner only)
export const deleteComputationalSystem = async (
data: ComputationalSystemDeleteRequest,
): Promise<{ message: string }> => {
const response = await api.delete<{ message: string }>("", { data });
return response.data;
};

View File

@@ -1,48 +1,16 @@
import keycloak from "Api/Keycloak/Keycloak";
import axios from "axios";
import type { Team, TeamMember } from "Types/Team/Team";
export interface TeamCreateRequest {
name: string;
description?: string | null;
}
export interface TeamUpdateRequest {
team_id: number;
name: string;
description?: string | null;
}
export interface TeamCreateResponse {
team_id: number;
}
export interface TeamListRequest {
page_num: number;
page_size: number;
}
export interface TeamDeleteRequest {
team_id: number;
}
export interface MemberAddRequest {
team_id: number;
user_id: string;
permissions: string[];
}
export interface MemberDeleteRequest {
team_id: number;
user_id: string;
}
export interface TeamsListResponse {
teams: Team[];
cur_page: number;
total_teams: number;
page_size: number;
}
import type {
Team,
TeamMember,
TeamCreateRequest,
TeamCreateResponse,
TeamUpdateRequest,
TeamListRequest,
TeamsListResponse,
MemberAddRequest,
MemberDeleteRequest,
} from "Types/Team/Team";
const api = axios.create({
baseURL: `${import.meta.env.VITE_QUANTUM_BACKEND_URL}/team`,
@@ -84,11 +52,13 @@ export const getTeams = async (
return response.data;
};
export const getShortTeamsList = async (): Promise<
{ team_id: number; team_name: string }[]
> => {
const response =
await api.get<{ team_id: number; team_name: string }[]>("short_list");
export const getShortTeamsList = async (
params: Partial<{ permission: string }>,
): Promise<{ team_id: number; team_name: string }[]> => {
const response = await api.get<{ team_id: number; team_name: string }[]>(
"short_list",
{ headers: {}, params },
);
return response.data;
};

View File

@@ -34,7 +34,10 @@ function App() {
if (keycloak.authenticated)
keycloak.updateToken(60).catch((error) => {
console.log(error);
keycloak.logout();
keycloak.logout({
redirectUri:
window.location.origin + "/" + import.meta.env.VITE_BASE_PATH,
});
});
};

View File

@@ -1,85 +0,0 @@
import { Textarea } from "@mantine/core";
import { useEffect, useRef } from "react";
interface TextAreaProps {
text: string;
onTextChange: (text: string) => void;
}
export function CodeTextArea(props: TextAreaProps) {
const inputRef = useRef(null);
const lineRef = useRef(null);
//Synchronize scrolling between the
useEffect(() => {
const inputEl = document.querySelector(".MoleculeEditInput");
const lineEl = document.querySelector(".MoleculeEditLineNumber");
if (!inputEl || !lineEl) return;
let isSyncing = false; // prevents circular scroll events
let activeEl = null; // element currently being scrolled
const syncScroll = (source: Element, target: Element) => {
if (isSyncing) return;
isSyncing = true;
requestAnimationFrame(() => {
target.scrollTop = source.scrollTop;
isSyncing = false;
});
};
const onScroll = (e: Event) => {
activeEl = e.target;
if (activeEl === inputEl) {
syncScroll(inputEl, lineEl);
} else {
syncScroll(lineEl, inputEl);
}
};
inputEl.addEventListener("scroll", onScroll, { passive: true });
lineEl.addEventListener("scroll", onScroll, { passive: true });
return () => {
inputEl.removeEventListener("scroll", onScroll);
lineEl.removeEventListener("scroll", onScroll);
};
}, []);
return (
<Textarea
className="MoleculeEdit"
value={props.text}
ref={inputRef}
onChange={(e) => {
props.onTextChange(e.currentTarget.value);
}}
autoFocus
wrap="no-wrap"
leftSection={
<div className="MoleculeEditLineNumber" ref={lineRef}>
{Array.from({ length: props.text.split("\n").length }, (_, i) => {
if (i > 1) {
return i - 1;
} else {
return "";
}
}).join("\n")}
</div>
}
classNames={{
input: "MoleculeEditInput",
wrapper: "MoleculeEditWrapper",
}}
onKeyDown={(e) => {
// Stop arrow keys from reaching react-resizable-panels
if (
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)
) {
e.stopPropagation();
}
}}
></Textarea>
);
}

View File

@@ -16,7 +16,6 @@ import {
IconLogout,
IconSettings2,
IconUsers,
type Icon,
type IconProps,
} from "@tabler/icons-react";
import { Link, useLocation } from "react-router";
@@ -28,7 +27,9 @@ import CustomButton from "Components/CustomButton/CustomButton";
interface SubtleLinkButtonProps {
link: string;
Icon: ForwardRefExoticComponent<IconProps & React.RefAttributes<Icon>>;
Icon: ForwardRefExoticComponent<
IconProps & React.RefAttributes<SVGSVGElement>
>;
text: string;
color: string;
selected?: boolean;

View File

@@ -36,27 +36,48 @@ function ExperimentsListCard(props: {
<Text mb="sm" size="md">
Команда: {props.team?.team_name}
</Text>
<Pill className="ExperimentPill">
<Text size="md">Статус: {props.experiment.experiment_status}</Text>
</Pill>
<div style={{ display: "flex", gap: "10px" }}>
<Text size="md">Статус: </Text>
<Pill style={{ backgroundColor: "var(--mantine-color-yellow-5)" }}>
<Text size="md">{props.experiment.experiment_status}</Text>
</Pill>
</div>
</div>
<div className="ExperimentSectionWithLine">
<SimpleGrid cols={2} verticalSpacing="0px">
<Text>Задачи:</Text>
{experiment_tasks.map((task: TaskData<any>) => {
return (
<Text
size="md"
style={{
textWrap: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{task.name}
</Text>
);
})}
{experiment_tasks.map(
(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
task: TaskData<any>,
) => {
return (
<Pill
size="md"
style={{
backgroundColor: "transparent",
border: "2px solid black",
alignContent: "center",
alignItems: "center",
justifyContent: "center",
justifyItems: "center",
}}
>
<Text
size="md"
style={{
textWrap: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{task.name}
</Text>
</Pill>
);
},
)}
{experiment_tasks.length == 0 ? (
<Pill size="md" className="ExperimentPill2">
Нет Задач
@@ -70,10 +91,14 @@ function ExperimentsListCard(props: {
<div className="TopRightContainer">
<div className="dateContainer">
<Text size="sm">
{props.experiment.date_created.toLocaleDateString("ru")}{" "}
{new Date(
props.experiment.date_created + "Z",
).toLocaleDateString("ru")}{" "}
</Text>
<Text size="sm">
{props.experiment.date_created.toLocaleTimeString("ru")}
{new Date(
props.experiment.date_created + "Z",
).toLocaleTimeString("ru")}
</Text>
</div>
<UnstyledButton

View File

@@ -0,0 +1,56 @@
.MachinesListCard {
height: 85px;
padding: 12px;
}
.MachineSection {
border-right: 1px solid var(--mantine-color-contrast-filled);
padding-right: 10px;
}
.MachinePill {
background-color: var(--mantine-color-contrast-filled);
color: var(--mantine-color-primary-filled);
}
.MachinePill2 {
border: 2px solid var(--mantine-color-accent-filled);
background-color: transparent;
color: var(--mantine-color-accent-filled);
* {
width: 100%;
text-align: center;
}
}
.membersPills {
display: flex;
flex-direction: row;
gap: 10px;
}
.dateContainer {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: right;
gap: 5px;
.p {
text-wrap: nowrap;
}
}
.TopRightContainer {
justify-content: right;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 15px;
justify-items: center;
}
.RightMachineSection {
text-align: right;
padding-right: 10px;
display: flex;
flex-direction: column;
}

View File

@@ -0,0 +1,193 @@
import { Card, Pill, Text, UnstyledButton } from "@mantine/core";
import "./MachinesListCard.css";
import { useNavigate } from "react-router";
import { IconCancel, IconCheck, IconTrash } from "@tabler/icons-react";
import type { MouseEvent } from "react";
import { notifications } from "@mantine/notifications";
import { useDeviceStore } from "Stores/DeviceStore";
import { deleteComputationalSystem } from "Api/QuantumBackend/MachineManagment";
import type { SystemWithTeams } from "Types/Machine/Machine";
const colors = {
ONLINE: "var(--mantine-color-green-7)",
OFFLINE: "var(--mantine-color-red-7)",
BUSY: "var(--mantine-color-yellow-5)",
};
function MachinesListCard(props: SystemWithTeams) {
const navigate = useNavigate();
const { setTotalDevices, totalDevices, removeDevice } = useDeviceStore();
const handleDelete = () => {
// TODO: fix delete
deleteComputationalSystem({ system_id: props.system.id })
.then(() => {
removeDevice(props.system.id);
setTotalDevices(totalDevices - 1);
notifications.show({
radius: "md",
title: "Устройство удалено успешно",
message: "",
icon: <IconCheck />,
style: { paddingLeft: "5px" },
});
})
.catch(() => {
notifications.show({
radius: "md",
color: "red",
title: "Не удалось удалить устройство",
message: "",
icon: <IconCancel />,
style: { paddingLeft: "5px" },
});
});
};
return (
<Card
className="MachinesListCard"
onClick={() => {
navigate(props.system.id.toString());
}}
orientation="horizontal"
>
<Card.Section inheritPadding pl="sm" pr="xl">
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
Устройство: {props.system.system_name}
</Text>
<div style={{ display: "flex" }}>
<Text mb="sm" size="md">
Статус:
</Text>
<Pill
//className="MachinePill"
style={{
marginLeft: "10px",
backgroundColor: colors[props.system.status || "ONLINE"],
}}
>
<Text size="md">{props.system.status}</Text>
</Pill>
</div>
</Card.Section>
<Card.Section
withBorder
inheritPadding
px="xs"
style={{ textAlign: "right" }}
>
<Text mb="sm" size="md">
Дата обновления статуса:
</Text>
<div style={{ display: "flex", gap: "10px", justifyContent: "right" }}>
<Text size="sm">
{new Date(props.system.created_at + "Z").toLocaleDateString(
"ru",
)}{" "}
</Text>
<Text size="sm">
{new Date(props.system.created_at + "Z").toLocaleTimeString("ru")}
</Text>
</div>
</Card.Section>
<Card.Section
inheritPadding
px="xs"
withBorder
style={{
flexGrow: "1",
gap: "15px",
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
}}
>
<Text>Команды с доступом:</Text>
<div
className="membersPills"
style={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
}}
>
{props.teams.map((team) => {
return (
<Pill className="MachinePill" style={{ marginLeft: "10px" }}>
<Text
size="md"
style={{
textWrap: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{team.team.team_name}
</Text>
</Pill>
);
})}
{props.teams.length == 0 && (
<Pill className="MachinePill2" style={{ marginLeft: "10px" }}>
<Text
size="md"
style={{
textWrap: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
Команд нет
</Text>
</Pill>
)}
</div>
</Card.Section>
<Card.Section
inheritPadding
px="md"
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}
>
<div className="TopRightContainer">
<div className="dateContainer">
<Text size="sm">
{new Date(props.system.created_at + "Z").toLocaleDateString(
"ru",
)}{" "}
</Text>
<Text size="sm">
{new Date(props.system.created_at + "Z").toLocaleTimeString("ru")}
</Text>
</div>
<UnstyledButton
onClick={(e: MouseEvent) => {
e.stopPropagation();
handleDelete();
}}
style={{ cursor: "pointer" }}
>
<IconTrash size={20} />
</UnstyledButton>
</div>
<div className="TopRightContainer">
<div className="dateContainer">
<Text size="sm">Максимальное количество кубит:</Text>
<Text size="sm" style={{ fontWeight: "bold" }}>
{props.system.max_qubits}
</Text>
</div>
<Text c="dimmed" size="sm" style={{ alignSelf: "flex-end" }}>
#{props.system.id}
</Text>
</div>
</Card.Section>
</Card>
);
}
export default MachinesListCard;

View File

@@ -0,0 +1,136 @@
import { Card, Pill, Text, UnstyledButton } from "@mantine/core";
import { useNavigate } from "react-router";
import { IconCancel, IconCheck, IconTrash } from "@tabler/icons-react";
import type { MouseEvent } from "react";
import { notifications } from "@mantine/notifications";
import { useDeviceStore } from "Stores/DeviceStore";
import { removeSystemFromTeam } from "Api/QuantumBackend/MachineManagment";
import type { SystemWithTeams } from "Types/Machine/Machine";
function TeamMachinesListCard(props: SystemWithTeams) {
const navigate = useNavigate();
const { setTotalDevices, totalDevices, removeDevice } = useDeviceStore();
const handleLeave = () => {
// TODO: fix delete
if (props.system) {
removeSystemFromTeam({
team_id: props.teams[0].team.team_id,
system_id: props.system.id,
})
.then(() => {
removeDevice(props.teams[0].team.team_id);
setTotalDevices(totalDevices - 1);
notifications.show({
radius: "md",
title: "Устройство удалено успешно",
message: "",
icon: <IconCheck />,
style: { paddingLeft: "5px" },
});
})
.catch(() => {
notifications.show({
radius: "md",
color: "red",
title: "Не удалось удалить устройство",
message: "",
icon: <IconCancel />,
style: { paddingLeft: "5px" },
});
});
}
};
return (
<Card
onClick={() => {
navigate(props.system.id.toString());
}}
orientation="horizontal"
>
<Card.Section
inheritPadding
withBorder
px="xs"
style={{ width: "250px" }}
>
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
Устройство: {props.system.system_name}
</Text>
</Card.Section>
<Card.Section
inheritPadding
withBorder
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
flexGrow: "1",
}}
px="xs"
>
<div style={{ display: "flex" }}>
<Text mb="sm" size="md">
Статус:
</Text>
<Pill className="MachinePill" style={{ marginLeft: "10px" }}>
<Text size="md">{props.system.status}</Text>
</Pill>
</div>
<div style={{ display: "flex", gap: "15px" }}>
<Text mb="sm" size="md">
Дата обновления статуса:
</Text>
<Text size="md">
{new Date(props.system.created_at + "Z").toLocaleDateString(
"ru",
)}{" "}
</Text>
<Text size="md">
{new Date(props.system.created_at + "Z").toLocaleTimeString("ru")}
</Text>
</div>
</Card.Section>
<Card.Section
inheritPadding
px="xs"
withBorder
style={{
gap: "15px",
display: "flex",
flexDirection: "row",
}}
>
<Text size="md">Максимальное количество кубит:</Text>
<Text size="md" style={{ fontWeight: "bold" }}>
{props.system.max_qubits}
</Text>
</Card.Section>
<Card.Section
inheritPadding
px="md"
style={{
width: "50px",
}}
>
<UnstyledButton
onClick={(e: MouseEvent) => {
e.stopPropagation();
handleLeave();
}}
style={{ cursor: "pointer" }}
>
<IconTrash size={20} />
</UnstyledButton>
</Card.Section>
<Card.Section inheritPadding px="md">
<Text c="dimmed" size="md" style={{ width: "20px" }}>
#{props.system.id}
</Text>
</Card.Section>
</Card>
);
}
export default TeamMachinesListCard;

View File

@@ -0,0 +1,116 @@
import { Card, Text, UnstyledButton } from "@mantine/core";
import { IconCancel, IconCheck, IconTrash } from "@tabler/icons-react";
import type { MouseEvent } from "react";
import { notifications } from "@mantine/notifications";
import type { SystemTeamData, SystemWithTeams } from "Types/Machine/Machine";
import { removeSystemFromTeam } from "Api/QuantumBackend/MachineManagment";
import { useDeviceStore } from "Stores/DeviceStore";
function TeamInMachineListCard(props: {
team: SystemTeamData;
system: SystemWithTeams;
}) {
const { updateDevice } = useDeviceStore();
const handleRemovePerm = () => {
// TODO: fix delete
removeSystemFromTeam({
team_id: props.team.team.team_id,
system_id: props.system.system.id,
})
.then(() => {
updateDevice(props.system.system.id, {
teams: props.system.teams.filter(
(team) => team.team.team_id != props.team.team.team_id,
),
});
notifications.show({
radius: "md",
title: "Команда удалена успешно",
message: "",
icon: <IconCheck />,
style: { paddingLeft: "5px" },
});
})
.catch(() => {
notifications.show({
radius: "md",
color: "red",
title: "Не удалось удалить команду",
message: "",
icon: <IconCancel />,
style: { paddingLeft: "5px" },
});
});
};
return (
<Card orientation="horizontal">
<Card.Section
withBorder
inheritPadding
px="xs"
style={{
width: "430px",
display: "flex",
flexDirection: "row",
gap: "15px",
}}
>
<Text size="md" style={{ textDecorationLine: "underline" }}>
Команда: {props.team.team.team_name}
</Text>
<Text c="dimmed" size="md">
#{props.team.team.team_id}
</Text>
</Card.Section>
<Card.Section
inheritPadding
px="xs"
withBorder
style={{
flexGrow: "1",
gap: "15px",
display: "flex",
flexDirection: "row",
}}
>
<Text>Количество кубит: {props.team.num_qubits}</Text>
</Card.Section>
<Card.Section
inheritPadding
px="md"
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}
>
<div className="TopRightContainer">
<div className="dateContainer">
<Text size="sm">
{new Date(props.team.created_at + "Z").toLocaleDateString(
"ru",
)}{" "}
</Text>
<Text size="sm">
{new Date(props.team.created_at + "Z").toLocaleTimeString("ru")}
</Text>
</div>
<UnstyledButton
onClick={(e: MouseEvent) => {
e.stopPropagation();
handleRemovePerm();
}}
style={{ cursor: "pointer" }}
>
<IconTrash size={20} />
</UnstyledButton>
</div>
</Card.Section>
</Card>
);
}
export default TeamInMachineListCard;

View File

@@ -1,6 +1,3 @@
.TeamMembers {
margin-top: 10px;
gap: 10px;
display: flex;
flex-direction: column;
.TeamMemberCard {
min-height: 100px;
}

View File

@@ -30,7 +30,13 @@ function TeamMemberCard(props: { cur_team: Team; member: TeamMember }) {
);
return (
<Card padding="sm" withBorder orientation="horizontal">
<Card
padding="sm"
withBorder
orientation="horizontal"
className="TeamMemberCard"
key={props.member.user.keycloak_id}
>
<Card.Section
inheritPadding
px="xs"
@@ -168,7 +174,6 @@ function TeamMemberCard(props: { cur_team: Team; member: TeamMember }) {
placeholder="Выберите разрешения"
searchable
data={props.cur_team.creator.permissions}
classNames={{ input: "MantineInput" }}
onChange={(value) => {
set_selected_permissions(value);
}}
@@ -227,10 +232,10 @@ function TeamMemberCard(props: { cur_team: Team; member: TeamMember }) {
}}
>
<Text size="sm" style={{ textAlign: "right" }}>
{new Date(props.member.joined_at).toLocaleDateString("ru")}
{new Date(props.member.joined_at + "Z").toLocaleDateString("ru")}
</Text>
<Text size="sm" style={{ textAlign: "right" }}>
{new Date(props.member.joined_at).toLocaleTimeString("ru")}
{new Date(props.member.joined_at + "Z").toLocaleTimeString("ru")}
</Text>
</div>
</Stack>

View File

@@ -90,17 +90,19 @@ function TeamsListCard(props: Team) {
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
Команда: {props.name}
</Text>
<Text mb="sm" size="md">
Владелец:
<div style={{ display: "flex" }}>
<Text mb="sm" size="md">
Владелец:
</Text>
<Pill className="TeamPill" style={{ marginLeft: "10px" }}>
<Text size="md">{props.creator.user.username}</Text>
</Pill>
</Text>
</div>
<div className="membersPills">
<Text>Члены:</Text>
{props.members.map((team: TeamMember) => {
return (
<Pill className="TeamPill" style={{ marginLeft: "10px" }}>
<Pill className="TeamPill">
<Text
size="md"
style={{
@@ -170,10 +172,10 @@ function TeamsListCard(props: Team) {
<div className="TopRightContainer">
<div className="dateContainer">
<Text size="sm">
{new Date(props.created_at).toLocaleDateString()}{" "}
{new Date(props.created_at + "Z").toLocaleDateString()}{" "}
</Text>
<Text size="sm">
{new Date(props.created_at).toLocaleTimeString()}
{new Date(props.created_at + "Z").toLocaleTimeString()}
</Text>
</div>
{props.creator.user.keycloak_id == profile?.id && (

View File

@@ -1,22 +0,0 @@
.moleculeViewer {
height: 100%;
width: 100%;
min-width: 0;
box-sizing: border-box;
overflow: auto;
position: relative;
overscroll-behavior: contain;
touch-action: none;
}
.atomEditor {
position: absolute;
border-radius: md;
right: 10px;
top: 10px;
}
.atomEditor * {
width: 100%;
height: auto;
}

View File

@@ -1,269 +0,0 @@
import Viewer from "miew-react";
import Miew from "miew";
import {
Paper,
UnstyledButton,
useMantineColorScheme,
useMantineTheme,
} from "@mantine/core";
import { useEffect, useMemo, useRef, useState } from "react";
import "./MoleculeViewer.css";
import { notifications } from "@mantine/notifications";
import {
IconAlertHexagon,
IconArrowBarBoth,
IconRefresh,
} from "@tabler/icons-react";
import MoleculeViewerMenu from "./MoleculeViewerMenu";
import type { SelectedAtom } from "Types/Experiment/MoleculeEdit/MoleculeEdit";
const callbackOnResizeFinish = (
dom_elem: HTMLElement,
beginning_callback: () => void,
end_callback: () => void,
delay = 100, // ms after resize "finishes"
) => {
let timeoutId: number | undefined;
const resizeObserver = new ResizeObserver(() => {
beginning_callback();
if (timeoutId) {
window.clearTimeout(timeoutId);
}
timeoutId = window.setTimeout(() => {
end_callback();
}, delay);
});
resizeObserver.observe(dom_elem);
return () => resizeObserver.disconnect(); // cleanup helper
};
interface MoleculeViewerProps {
moleculeData: string;
selectedAtom: SelectedAtom | null;
setSelectedAtom: (selectedAtom: SelectedAtom | null) => void;
}
function MoleculeViewer(props: MoleculeViewerProps) {
const theme = useMantineTheme();
const { colorScheme } = useMantineColorScheme();
// объект загруженного редактора молекул для редактирования
const [miew, setMiew] = useState<Miew | null>(null);
const molViewerRef = useRef<HTMLDivElement>(null);
const [isResizing, setIsResizing] = useState<boolean>(false);
const [viewingData, setViewingData] = useState<string>(props.moleculeData);
//при загрузке сохраняем объект miew
const onInitMiew = (miew: Miew) => {
setMiew(miew);
if (miew && viewingData) {
//прогружаем молекулу
miew
.load(viewingData, {
sourceType: "immediate",
fileType: "xyz",
})
.then(() => {
if (props.selectedAtom) {
props.setSelectedAtom(null);
}
})
.catch((error) => {
if (error.message == "Operation cancelled") {
return;
}
notifications.show({
color: "orange",
radius: "md",
title: "Ошибка при отображении молекулы",
message:
"Проверте правильность написания кода молекулы, в нем содержатся ошибки",
icon: <IconAlertHexagon />,
style: { paddingLeft: "5px" },
});
});
}
miew.setOptions({
settings: {
bg: {
color: theme.colors.secondaryDark[7],
},
fogAlpha: 0.7,
axes: true,
},
});
};
//мемоизируем объект чтобы он не перегружаля при изменении [miew] состояния
const MemoViewer = useMemo(
() => <Viewer onInit={onInitMiew} />,
[viewingData],
);
//Замена станартного обработчика нажатия на молекулы
useEffect(() => {
if (molViewerRef.current && miew) {
//INFO: при изменении размера окна обносить webgl
const cleanup = callbackOnResizeFinish(
molViewerRef.current,
handleResizeStart,
handleResizeEnd,
);
//@ts-expect-error Miew class not implementing typescript correctly
miew.removeEventListener("newpick");
//@ts-expect-error Miew class not implementing typescript correctly
miew.addEventListener("newpick", handleClick);
return () => {
cleanup();
//@ts-expect-error Miew class not implementing typescript correctly
miew.removeEventListener("newpick");
};
}
}, [molViewerRef, miew]);
//On selected Atom change, highlight it
useEffect(() => {
if (miew) {
if (!props.selectedAtom) {
//@ts-expect-error Miew class not implementing typescript correctly
miew.select("");
return;
}
//@ts-expect-error Miew class not implementing typescript correctly
miew.select("serial " + props.selectedAtom.serial, false);
//спрятать информацию встроенную
//@ts-expect-error Miew class not implementing typescript correctly
miew._msgAtomInfo.style.opacity = 0.0;
//@ts-expect-error Miew class not implementing typescript correctly
miew._msgAtomInfo.style.height = "0px";
//@ts-expect-error Miew class not implementing typescript correctly
miew._msgAtomInfo.style.overflow = "hidden";
}
}, [props.selectedAtom, miew]);
//при смене темы обновляем фон MoleculeViewer
useEffect(() => {
if (miew) {
miew.setOptions({
settings: {
bg: {
color: theme.colors.secondaryDark[7],
},
},
});
}
}, [colorScheme]);
//при нажятии на атом выбираем его
const handleClick = (pick: { type: string; obj: { atom: SelectedAtom } }) => {
if (miew) {
if (pick.obj.atom) {
props.setSelectedAtom(pick.obj.atom);
} else {
props.setSelectedAtom(null);
}
}
};
const handleResizeStart = () => {
setIsResizing(true);
};
const handleResizeEnd = () => {
//@ts-expect-error Miew class not implementing typescript correctly
miew._onResize();
//@ts-expect-error Miew class not implementing typescript correctly
miew._picker.handleResize();
setTimeout(() => {
setIsResizing(false);
}, 50);
};
const handleMouseEnter = () => {
document.body.style.overflow = "hidden"; // disable page scroll
};
const handleMouseLeave = () => {
document.body.style.overflow = "auto"; // re-enable scroll
};
const refreshDisplay = () => {
if (props.moleculeData != viewingData) {
setViewingData(props.moleculeData);
} else {
if (miew) {
//@ts-expect-error Miew class not implementing typescript correctly
miew.resetView();
}
}
};
return (
<div ref={molViewerRef} className="moleculeViewer">
<div
style={{
width: "100%",
height: "100%",
backgroundColor: theme.colors.secondaryDark[7],
borderRadius: "5px",
overflow: "hidden",
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{MemoViewer}
{isResizing && viewingData && (
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: theme.colors.secondaryDark[7], // optional dim
opacity: 0.5,
zIndex: 10,
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<IconArrowBarBoth size={80} />
</div>
</div>
)}
</div>
{props.selectedAtom && (
<Paper className="atomEditor" radius="md" p="md">
<MoleculeViewerMenu selectedAtom={props.selectedAtom} miew={miew} />
</Paper>
)}
<div
style={{
position: "absolute",
left: "10px",
top: "10px",
display: "flex",
zIndex: 5,
width: "auto",
}}
>
<UnstyledButton onClick={refreshDisplay}>
<IconRefresh size={25} />
</UnstyledButton>
</div>
</div>
);
}
export default MoleculeViewer;

View File

@@ -1,29 +0,0 @@
import { Text, Divider } from "@mantine/core";
import Miew from "miew";
import "./MoleculeViewer.css";
import type { SelectedAtom } from "Types/Experiment/MoleculeEdit/MoleculeEdit";
interface MoleculeViewerMenuProps {
selectedAtom: SelectedAtom;
miew: Miew | null;
}
function MoleculeViewerMenu(props: MoleculeViewerMenuProps) {
return (
<>
<div className="moleculeViewerMenu">
<>
<Text ta="center">
Атом: {props.selectedAtom.name} ({props.selectedAtom.serial})
</Text>
<Divider my="sm" />
<Text ta="center">Положение X: {props.selectedAtom.position.x}</Text>
<Text ta="center">Положение Y: {props.selectedAtom.position.y}</Text>
<Text ta="center">Положение Z: {props.selectedAtom.position.z}</Text>
</>
</div>
</>
);
}
export default MoleculeViewerMenu;

View File

@@ -13,4 +13,5 @@
display: flex;
flex-direction: column;
gap: 10px;
padding-bottom: 10px;
}

View File

@@ -99,7 +99,6 @@ export function AddMember(props: NewExperimentModalProps) {
label="Почта члена команды"
placeholder="Введите почту"
required
classNames={{ input: "MantineInput" }}
onChange={(event) => {
setEmail(event.currentTarget.value);
}}
@@ -110,7 +109,6 @@ export function AddMember(props: NewExperimentModalProps) {
placeholder="Выберите разрешения"
searchable
data={props.permissions}
classNames={{ input: "MantineInput" }}
onChange={(value) => setSelectedPerms(value)}
/>
<Space h="md" />

View File

@@ -0,0 +1,127 @@
import {
Center,
Modal,
NumberInput,
Select,
Space,
Title,
} from "@mantine/core";
import { useEffect, useState } from "react";
import CustomButton from "Components/CustomButton/CustomButton";
import { useDeviceStore } from "Stores/DeviceStore";
import type { SystemWithTeams } from "Types/Machine/Machine";
import { giveSystemToTeam } from "Api/QuantumBackend/MachineManagment";
interface AddTeamToDeviceProps {
isOpened: boolean;
setIsOpened: (opened: boolean) => void;
teams: { team_id: number; team_name: string }[] | undefined;
device: SystemWithTeams;
}
export function AddTeamToDevice(props: AddTeamToDeviceProps) {
const [numQubits, setNumQubits] = useState(0);
const { updateDevice } = useDeviceStore();
const [selectedTeam, setSelectedTeam] = useState<{
label: string;
value: string;
}>();
//reset on open dialog
useEffect(() => {
if (props.isOpened) {
setNumQubits(0);
setSelectedTeam(undefined);
}
}, [props.isOpened]);
const handleClose = () => {
props.setIsOpened(false);
};
const handleAddMember = () => {
if (props.device && selectedTeam)
giveSystemToTeam({
system_id: props.device.system.id,
team_id: Number(selectedTeam.value),
qubits_given: numQubits,
})
.then((team) => {
updateDevice(props.device.system.id, {
teams: [
...props.device.teams,
{
team: {
team_id: team.system_id,
team_name:
props.teams?.find((team_) => team_.team_id == team.team_id)
?.team_name || "",
},
num_qubits: team.qubits_given,
created_at: new Date(),
},
],
});
props.setIsOpened(false);
})
.catch(() => {});
};
return (
<Modal
opened={props.isOpened}
onClose={handleClose}
title=<Title size="xl">Добавить члена команды</Title>
centered
size="75%"
styles={{
content: { paddingLeft: "10px" },
title: { width: "100%" },
}}
>
<Select
value={selectedTeam?.value}
label="Команда"
placeholder="Выберите команду"
searchable
required
data={
props.teams
? props.teams.map((team) => {
return {
value: team.team_id.toString(),
label: `${team.team_name} (#${team.team_id})`,
};
})
: []
}
onChange={(_value, option) => setSelectedTeam(option)}
/>
<NumberInput
value={numQubits}
label="Количество кубит"
required
onChange={(event) => {
setNumQubits(Number(event.valueOf()));
}}
></NumberInput>
<Space h="md" />
<Center>
<CustomButton
disabled={selectedTeam != undefined ? false : true}
color="contrast"
onClick={handleAddMember}
text="Добавить команду"
></CustomButton>
<Space w="md" />
<CustomButton
color="contrast"
style="outline"
onClick={() => {
props.setIsOpened(false);
}}
text="Отменить"
></CustomButton>
</Center>
</Modal>
);
}

View File

@@ -47,10 +47,10 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
name: name,
description: description,
team_id: Number(selectedTeam?.value),
tasks_ids: [],
tasks_ids: [1, 2],
date_created: new Date(),
experiment_status: "DRAFT",
experiment_type: "a",
experiment_status: "PROCESSING",
experiment_type: "VQE",
});
props.setIsOpened(false);
}
@@ -73,7 +73,6 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
label="Имя эксперимента"
placeholder="Введите имя эксперимента"
required
classNames={{ input: "MantineInput" }}
onChange={(event) => {
setName(event.currentTarget.value);
}}
@@ -86,7 +85,6 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
minRows={4}
maxRows={10}
autosize
classNames={{ input: "MantineInput" }}
onChange={(event) => {
setDescription(event.currentTarget.value);
}}
@@ -108,7 +106,6 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
})
: []
}
classNames={{ input: "MantineInput" }}
onChange={(_value, option) => setSelectedTeam(option)}
/>
<Space h="md" />

View File

@@ -22,7 +22,6 @@ import {
GetInFormats,
} from "Api/ConvertBackendCalls";
import { AxiosError } from "axios";
import { useMoleculeEditStore } from "Stores/MoleculeEditStore";
import CustomButton from "Components/CustomButton/CustomButton";
interface NewMoleculeModalProps {
@@ -31,7 +30,6 @@ interface NewMoleculeModalProps {
}
export function NewMoleculeModal(props: NewMoleculeModalProps) {
const { setCurrentMoleculeString } = useMoleculeEditStore();
const [activeStep, setActiveStep] = useState(0);
const [selectedMethod, setSelectedMethod] = useState(0);

View File

@@ -75,7 +75,6 @@ export function NewTeamModal(props: NewTeamModalProps) {
label="Имя команлы"
placeholder="Введите имя команды"
required
classNames={{ input: "MantineInput" }}
onChange={(event) => {
setName(event.currentTarget.value);
}}
@@ -88,7 +87,6 @@ export function NewTeamModal(props: NewTeamModalProps) {
minRows={4}
maxRows={10}
autosize
classNames={{ input: "MantineInput" }}
onChange={(event) => {
setDescription(event.currentTarget.value);
}}

View File

@@ -0,0 +1,194 @@
import {
Alert,
Card,
Center,
LoadingOverlay,
Pill,
Space,
Text,
Title,
} from "@mantine/core";
import { useEffect, useState } from "react";
import { Helmet } from "react-helmet";
import { IconPlus } from "@tabler/icons-react";
import { useParams } from "react-router";
import CustomButton from "Components/CustomButton/CustomButton";
import { useAuthenticationStore } from "Stores/AuthenticationStore";
import { useDeviceStore } from "Stores/DeviceStore";
import { getComputationalSystemById } from "Api/QuantumBackend/MachineManagment";
import type { SystemTeamData, SystemWithTeams } from "Types/Machine/Machine";
import TeamInMachineListCard from "Components/ListCard/TeamListCard/TeamInMachineCard/TeamInMachine";
import { AddTeamToDevice } from "Modals/AddTeamToDevice/AddTeamToDevice";
import { getShortTeamsList } from "Api/QuantumBackend/TeamManagement";
function DevicePage() {
const [isOpen, setIsOpen] = useState(false);
const { device_id } = useParams();
const { setDevices, devices } = useDeviceStore();
const { profile } = useAuthenticationStore();
const [is_loading, set_is_loading] = useState(true);
const [cur_device, set_cur_device] = useState<SystemWithTeams | undefined>(
devices.find((e) => e.system.id === Number(device_id)),
);
const [teams, setTeams] =
useState<{ team_id: number; team_name: string }[]>();
useEffect(() => {
if (!cur_device && profile) {
getComputationalSystemById(Number(device_id))
.then((device) => {
if (device) {
console.log(device);
setDevices([device]);
set_cur_device(device);
set_is_loading(false);
}
})
.catch(() => {
set_is_loading(false);
});
} else {
if (profile) {
set_is_loading(false);
}
}
if (devices.find((e) => e.system.id === Number(device_id)) != cur_device) {
set_cur_device(devices.find((e) => e.system.id === Number(device_id)));
}
if (profile && !teams) {
getShortTeamsList({ permission: "manage_machines" })
.then((teams) => {
if (teams) {
setTeams(teams);
}
})
.catch(() => {
setTeams([]);
});
}
}, [cur_device, profile, devices]);
return (
<>
<Helmet>
<title>
{cur_device
? "Device " + cur_device.system.id + " | QMolSim"
: "Error | QmolSim"}
</title>
<meta
name="description"
content="See the information on the currently selected team"
/>
</Helmet>
{cur_device && (
<AddTeamToDevice
isOpened={isOpen}
setIsOpened={setIsOpen}
device={cur_device}
teams={teams}
/>
)}
<div className="TeamPage">
<LoadingOverlay
visible={is_loading}
zIndex={1000}
overlayProps={{ radius: "sm", blur: 2 }}
loaderProps={{ size: 50, type: "dots" }}
/>
<Card className="TeamData">
<div>
<Title order={3}>
Устройство: {cur_device ? cur_device.system.system_name : ""}
</Title>
<Space h="xl" />
<div style={{ display: "flex", flexDirection: "row", gap: "15px" }}>
<Text size="md">Статус: </Text>
<Pill className="ExperimentPill">
<Text size="md">{cur_device?.system.status}</Text>
</Pill>
<div className="dateContainer">
<Text size={"lg"}>Статус обновлен: </Text>
<Text size="md" style={{ alignContent: "center" }}>
{new Date(
cur_device?.system.last_updated
? cur_device?.system.last_updated + "Z"
: "",
).toLocaleDateString("ru")}{" "}
</Text>
<Text size="md" style={{ alignContent: "center" }}>
{new Date(
cur_device?.system.last_updated
? cur_device?.system.last_updated + "Z"
: "",
).toLocaleTimeString("ru")}
</Text>
</div>
</div>
<Space h="md" />
<Text size="lg">
Максимальное количество кубит:{" "}
<b>{cur_device?.system.max_qubits}</b>
</Text>
</div>
</Card>
<div
style={{
display: "flex",
flexWrap: "nowrap",
gap: "20px",
alignItems: "center",
}}
>
<Title order={3} style={{ textWrap: "nowrap" }}>
Команды с доступом:
</Title>
<div>
<CustomButton
color="contrast"
onClick={() => {
setIsOpen(true);
}}
icon={<IconPlus />}
text="Добавить команду"
/>
</div>
</div>
{cur_device && cur_device.teams.length > 0 && (
<div className="TeamMembers">
{cur_device.teams.map((team: SystemTeamData) => {
return <TeamInMachineListCard team={team} system={cur_device} />;
})}
</div>
)}
{cur_device && cur_device.teams.length == 0 && (
<div className="TeamMembers">
<Alert>
<Center>
{" "}
<Text c="contrast" size={"xl"}>
Команд с доступом нет
</Text>{" "}
</Center>
</Alert>
</div>
)}
{!cur_device && !is_loading && (
<Alert color="red">
<Center>
{" "}
<Text c="contrast" size={"xl"}>
Ошибка. Команда не найдена
</Text>{" "}
</Center>
</Alert>
)}
</div>
</>
);
}
export default DevicePage;

View File

@@ -0,0 +1,5 @@
.DevicesPage {
display: flex;
flex-direction: column;
flex-grow: 1;
}

View File

@@ -3,8 +3,78 @@ import "./DevicesPage.css";
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
import { Alert } from "@mantine/core";
import { Link } from "react-router";
import { useDeviceStore } from "Stores/DeviceStore";
import { useEffect, useState } from "react";
import { useAuthenticationStore } from "Stores/AuthenticationStore";
import { getMySystems } from "Api/QuantumBackend/MachineManagment";
import MachinesListCard from "Components/ListCard/MachineListCard/MachinesListCard";
function DevicesPage() {
const { devices, setDevices, setTotalDevices, totalDevices, removeDevice } =
useDeviceStore();
const [isLoading, setIsLoading] = useState(true);
const { profile } = useAuthenticationStore();
const [cur_page, set_cur_page] = useState<number>(1);
const [page_size, set_page_size] = useState<number>(256);
useEffect(() => {
setIsLoading(true);
if (profile) {
getMySystems({ page_num: cur_page })
.then((devices) => {
setDevices(devices.systems);
set_cur_page(devices.cur_page);
setTotalDevices(devices.total_systems);
set_page_size(devices.page_size);
setIsLoading(false);
})
.catch(() => setIsLoading(false));
}
}, [profile]);
useEffect(() => {
if (profile) {
if (devices && devices.length > 0 && devices.length > page_size) {
removeDevice(devices[devices.length - 1].system.id);
}
if (
devices &&
devices.length > 0 &&
cur_page > Math.ceil(totalDevices / page_size)
) {
setIsLoading(true);
getMySystems({ page_num: Math.max(1, cur_page - 1) })
.then((devices) => {
setDevices(devices.systems);
set_cur_page(devices.cur_page);
setTotalDevices(devices.total_systems);
set_page_size(devices.page_size);
setIsLoading(false);
scroll({ top: 0 });
})
.catch(() => setIsLoading(false));
set_cur_page(Math.max(1, cur_page - 1));
}
if (
devices &&
devices.length > 0 &&
devices.length < Math.min(page_size, totalDevices) &&
cur_page != Math.ceil(totalDevices / page_size)
) {
getMySystems({ page_num: cur_page })
.then((devices) => {
setDevices(devices.systems);
set_cur_page(devices.cur_page);
setTotalDevices(devices.total_systems);
set_page_size(devices.page_size);
setIsLoading(false);
scroll({ top: 0 });
})
.catch(() => setIsLoading(false));
}
}
}, [profile, devices]);
return (
<>
<Helmet>
@@ -16,17 +86,33 @@ function DevicesPage() {
</Helmet>
<div className="DevicesPage">
<PaginationContainer
numberOfPages={1}
isLoading={false}
activePage={1}
setPage={() => {}}
numberOfPages={Math.ceil(totalDevices / page_size)}
isLoading={isLoading}
activePage={cur_page}
setPage={(page) => {
getMySystems({ page_num: page })
.then((devices) => {
setDevices(devices.systems);
set_cur_page(devices.cur_page);
setTotalDevices(devices.total_systems);
set_page_size(devices.page_size);
setIsLoading(false);
scroll({ top: 0 });
})
.catch(() => setIsLoading(false));
}}
>
<Alert title="Подключенные устройства не найдены" color="blue">
Для того, чтобы добавить устройство, простмотрите{" "}
<Link to={"documentaion"} className="invisible_link">
документацию
</Link>
</Alert>
{devices.length == 0 && !isLoading && (
<Alert title="Подключенные устройства не найдены" color="blue">
Для того, чтобы добавить устройство, простмотрите{" "}
<Link to={"documentaion"} className="invisible_link">
документацию
</Link>
</Alert>
)}
{devices.map((machine) => {
return <MachinesListCard {...machine} />;
})}
</PaginationContainer>
</div>
</>

View File

@@ -1,7 +1,7 @@
import { Alert, Center, Text } from "@mantine/core";
import "./ExperimentPage.css";
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
import { NewMoleculeModal } from "Modals/NewMolecule/NewMolecule";
//import { NewMoleculeModal } from "Modals/NewMolecule/NewMolecule";
import { useState } from "react";
import { Helmet } from "react-helmet";
import { IconPlus, IconSettings } from "@tabler/icons-react";
@@ -12,7 +12,7 @@ import { useExperimentStore } from "Stores/ExperimentStore";
function ExperimentPage() {
const [isOpen, setIsOpen] = useState(false);
const { experiment_id } = useParams();
const { experiments } = useExperimentStore();
const { experiments, addTask } = useExperimentStore();
const experiment = experiments.find((exp) => {
return exp.id == Number(experiment_id);
@@ -33,10 +33,6 @@ function ExperimentPage() {
</Helmet>
{experiment && (
<div className="ExperimentPage">
<NewMoleculeModal
isOpened={isOpen}
setIsOpened={(toOpen: boolean) => setIsOpen(toOpen)}
/>
<div
style={{
display: "flex",
@@ -49,6 +45,12 @@ function ExperimentPage() {
<CustomButton
color="contrast"
onClick={() => {
addTask(Number(experiment_id) || 0, {
id: 1,
name: "Задача 1",
description: "",
data: {},
});
setIsOpen(true);
}}
icon={<IconPlus />}

View File

@@ -1,65 +0,0 @@
.MoleculeEdit {
height: 100%;
display: flex;
}
.MoleculeEditWrapper {
display: flex !important;
flex: 1 !important;
min-width: 0;
min-height: 0;
background-color: var(--mantine-color-secondary-filled);
}
.MoleculeEditLineNumber {
height: 100%;
padding-top: calc(1px + var(--input-padding-y, 0rem));
padding-bottom: calc(1px + var(--input-padding-y, 0rem));
font-family:
"Fira Code", "Courier New", Courier, monospace; /* Monospace fonts */
font-size: 14px; /* Comfortable size */
line-height: 1.5; /* Spacing like an editor */
letter-spacing: 0; /* Keeps punctuation aligned */
white-space: pre-line;
overflow: scroll;
scrollbar-width: none;
overscroll-behavior: none;
cursor: default;
}
.MoleculeEditInput {
font-family:
"Fira Code", "Courier New", Courier, monospace; /* Monospace fonts */
font-size: 14px; /* Comfortable size */
line-height: 1.5; /* Spacing like an editor */
letter-spacing: 0; /* Keeps punctuation aligned */
white-space: pre; /* preserves spaces & tabs */
overflow-x: auto; /* horizontal scroll when needed */
overflow-y: auto; /* vertical scroll when needed */
word-wrap: normal; /* prevent wrapping */
overscroll-behavior: none;
background-color: var(--mantine-color-secondary-filled);
}
.Separator {
width: 10px;
}
.moleculeInput {
height: 250px;
}
.moleculeInputWrapper {
height: 100%;
}
.moleculeInputWrapper > * {
height: 100%;
}
.tabPanel {
flex: 1;
padding: 10px;
border-left: 1px solid var(--tab-border-color);
border-right: 1px solid var(--tab-border-color);
border-bottom: 1px solid var(--tab-border-color);
}

View File

@@ -1,113 +0,0 @@
import MoleculeViewer from "Components/MoleculeViewer/MoleculeViewer";
import {
Button,
Center,
Divider,
Tabs,
Text,
useMantineTheme,
} from "@mantine/core";
import { Group, Panel, Separator } from "react-resizable-panels";
import "./MoleculePage.css";
import {
IconCheck,
IconCode,
IconGripVertical,
IconList,
IconX,
} from "@tabler/icons-react";
import { useMoleculeEditStore } from "Stores/MoleculeEditStore";
import { CodeTextArea } from "Components/CodeTextArea/CodeTextArea";
import { Helmet } from "react-helmet";
function MoleculeEditorPage() {
const {
currentMoleculeString,
setCurrentMoleculeString,
selectedAtom,
setSelectedAtom,
} = useMoleculeEditStore();
const theme = useMantineTheme();
return (
<>
<Helmet>
<title>Molecule Page | QMolSim</title>
<meta
name="description"
content="Edit the atomic structure of a molecule"
/>
</Helmet>
<div className="experimentButtons">
<Button color="secondary" onClick={() => {}}>
<IconCheck style={{ marginRight: "10px" }} />
<Text c="contrast">Сохранить</Text>
</Button>
<Button color="contrast" onClick={() => {}}>
<IconX style={{ marginRight: "10px" }} color="black" />
<Text c="secondary">Отменить</Text>
</Button>
</div>
<div style={{ display: "flex", flexDirection: "column", height: "85vh" }}>
<Divider style={{ margin: "10px" }} />
<Group style={{ flex: 1, gap: "5px" }}>
<Panel minSize={200} defaultSize={500}>
<Tabs
variant="outline"
style={{
height: "100%",
display: "flex",
flexDirection: "column",
}}
radius="lg"
defaultValue="code"
classNames={{ panel: "tabPanel" }}
>
<Tabs.List>
<Tabs.Tab value="code" leftSection={<IconCode size={12} />}>
Код
</Tabs.Tab>
<Tabs.Tab value="list" leftSection={<IconList size={12} />}>
Список
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="code">
<CodeTextArea
text={currentMoleculeString}
onTextChange={(text: string) => {
setCurrentMoleculeString(text);
}}
/>
</Tabs.Panel>
<Tabs.Panel value="list">
<></>
</Tabs.Panel>
</Tabs>
</Panel>
<Separator>
<Center
style={{
height: "100%",
background: theme.colors.dark[4],
borderRadius: "5px",
}}
>
<IconGripVertical size={12} />
</Center>
</Separator>
<Panel style={{ flexShrink: 0 }} minSize={200}>
<MoleculeViewer
moleculeData={currentMoleculeString}
selectedAtom={selectedAtom}
setSelectedAtom={setSelectedAtom}
/>
</Panel>
</Group>
</div>
</>
);
}
export default MoleculeEditorPage;

View File

@@ -0,0 +1,70 @@
import { Helmet } from "react-helmet";
import { IconCancel, IconPlus, IconSettings } from "@tabler/icons-react";
import { useParams } from "react-router";
import CustomButton from "Components/CustomButton/CustomButton";
import { IframePlugin } from "Api/PluginLoader/PluginLoader";
import { useState } from "react";
function TaskPage() {
const { task_id } = useParams();
const [data, setData] = useState<{ text: string }>({ text: "" });
return (
<>
<Helmet>
<title>
{task_id ? "Task " + task_id + " | QMolSim" : "Error |QmolSim"}
</title>
<meta
name="description"
content="See the documentation on how to setup and use the qunatum computational system"
/>
</Helmet>
<div className="ExperimentPage">
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
marginBottom: "15px",
}}
>
<div className="experimentButtons">
<CustomButton
color="accent"
onClick={() => {}}
icon={<IconPlus />}
text="Сохранить"
/>
<CustomButton
color="red"
style="outline"
onClick={() => {}}
icon={<IconCancel />}
text="Отменить"
/>
</div>
</div>
<IframePlugin
pluginUrl={
window.location.origin +
"/" +
import.meta.env.VITE_BASE_PATH +
"/" +
"index.html"
}
mode="editor"
taskData={{
id: Number(task_id),
name: "Молекула 1",
description: "",
data: data,
}}
onUpdate={setData}
/>
</div>
</>
);
}
export default TaskPage;

View File

@@ -1,5 +1,5 @@
.TeamPage {
flex-grow: 1;
height: calc(100vh - 140px);
display: flex;
flex-direction: column;
position: relative;
@@ -9,3 +9,24 @@
.TeamData {
margin-bottom: 15px;
}
.TeamMembers {
overflow: scroll;
display: flex;
flex-direction: column;
flex: 1;
gap: 15px;
margin-top: 15px;
border: 1px solid var(--tab-border-color);
padding: 15px;
}
.tabPanel {
height: 100%;
display: flex;
flex-direction: column;
padding: 10px;
border-left: 1px solid var(--tab-border-color);
border-right: 1px solid var(--tab-border-color);
border-bottom: 1px solid var(--tab-border-color);
}

View File

@@ -5,6 +5,7 @@ import {
Center,
Grid,
LoadingOverlay,
Tabs,
Text,
Textarea,
TextInput,
@@ -23,35 +24,59 @@ import { useAuthenticationStore } from "Stores/AuthenticationStore";
import TeamMemberCard from "Components/ListCard/TeamListCard/TeamMemberCard/TeamMemberCard";
import { AddMember } from "Modals/AddMember/AddMember";
import { notifications } from "@mantine/notifications";
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
import { getTeamSystems } from "Api/QuantumBackend/MachineManagment";
import type { ComputationalSystemListResponse } from "Types/Machine/Machine";
import TeamMachinesListCard from "Components/ListCard/MachineListCard/TeamMachineListCard/TeamMachineListCard";
function TeamPage() {
const [isOpen, setIsOpen] = useState(false);
const { team_id } = useParams();
const { addTeam, teams, updateTeam } = useTeamStore();
const { profile } = useAuthenticationStore();
const [is_loading, set_is_loading] = useState(true);
const { profile, is_loading } = useAuthenticationStore();
const [is_loading_this, set_is_loading] = useState(true);
const [cur_team, set_cur_team] = useState<Team>();
const [team_name, set_team_name] = useState(cur_team?.name);
const [team_descr, set_team_descr] = useState(cur_team?.description);
const [team_name, set_team_name] = useState(cur_team?.name || "");
const [team_descr, set_team_descr] = useState(cur_team?.description || "");
const [machines, set_machines] = useState<ComputationalSystemListResponse>({
systems: [],
cur_page: 0,
total_systems: 0,
page_size: 0,
});
const [cur_page, set_page] = useState<number>(1);
useEffect(() => {
if (!cur_team && profile) {
getTeam(Number(team_id))
.then((team) => {
if (team) {
addTeam(team);
set_cur_team(team);
set_team_name(team.name);
set_team_descr(team.description);
set_is_loading(false);
if (!cur_team && profile && !is_loading) {
getTeam(Number(team_id)).then((team) => {
if (team) {
addTeam(team);
set_cur_team(team);
set_team_name(team.name);
set_team_descr(team.description);
set_is_loading(false);
}
});
}
if (
(!machines || cur_page != machines?.cur_page) &&
profile &&
!is_loading
) {
getTeamSystems(Number(team_id), cur_page, 9 )
.then((systems) => {
if (systems) {
set_machines(systems);
}
})
.catch(() => {
set_is_loading(false);
});
} else {
}
if (machines && cur_team && profile) {
if (profile) {
set_is_loading(false);
}
@@ -59,7 +84,7 @@ function TeamPage() {
if (teams.find((e) => e.id === Number(team_id)) != cur_team) {
set_cur_team(teams.find((e) => e.id === Number(team_id)));
}
}, [cur_team, profile, teams]);
}, [cur_team, profile, teams, machines, cur_page]);
return (
<>
@@ -79,147 +104,210 @@ function TeamPage() {
team={cur_team}
/>
<div className="TeamPage">
<LoadingOverlay
visible={is_loading}
zIndex={1000}
overlayProps={{ radius: "sm", blur: 2 }}
loaderProps={{ size: 50, type: "dots" }}
/>
<Tabs
defaultValue="team"
classNames={{ panel: "tabPanel" }}
style={{ display: "flex", flexDirection: "column", height: "100%" }}
>
<Tabs.List>
<Tabs.Tab value="team">Команда</Tabs.Tab>
<Tabs.Tab value="machines">Системы</Tabs.Tab>
</Tabs.List>
<Card className="TeamData">
<Grid>
<Grid.Col span={3}>
<Title size={"xl"}>Имя команды:</Title>
</Grid.Col>
<Grid.Col span={9}>
<TextInput
size="md"
value={team_name}
onChange={(val) => {
set_team_name(val.target.value);
}}
/>
</Grid.Col>
<Grid.Col span={3}>
<Title size={"xl"}>Описание:</Title>
</Grid.Col>
<Grid.Col span={9}>
<Textarea
size="md"
value={team_descr ? team_descr : ""}
onChange={(val) => {
set_team_descr(val.target.value);
}}
minRows={2}
maxRows={4}
/>
</Grid.Col>
<Grid.Col span={3}>
<Title size={"xl"}>Создатель:</Title>
</Grid.Col>
<Grid.Col
span={9}
<Tabs.Panel value="team">
<LoadingOverlay
visible={is_loading_this}
zIndex={1000}
overlayProps={{ radius: "sm", blur: 2 }}
loaderProps={{ size: 50, type: "dots" }}
/>
<Card className="TeamData">
<Grid>
<Grid.Col span={3}>
<Title size={"xl"}>Имя команды:</Title>
</Grid.Col>
<Grid.Col span={9}>
<TextInput
size="md"
value={team_name}
onChange={(val) => {
set_team_name(val.target.value);
}}
/>
</Grid.Col>
<Grid.Col span={3}>
<Title size={"xl"}>Описание:</Title>
</Grid.Col>
<Grid.Col span={9}>
<Textarea
size="md"
value={team_descr ? team_descr : ""}
onChange={(val) => {
set_team_descr(val.target.value);
}}
minRows={2}
maxRows={4}
/>
</Grid.Col>
<Grid.Col span={3}>
<Title size={"xl"}>Создатель:</Title>
</Grid.Col>
<Grid.Col
span={9}
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: "10px",
}}
>
<Avatar
radius="xl"
src={cur_team?.creator.user.profile_picture_path}
/>
<Text size="md" style={{ textDecoration: "underline" }}>
{cur_team?.creator.user.username} (
{cur_team?.creator.user.email})
</Text>
</Grid.Col>
<Grid.Col span={6}>
<CustomButton
color="accent"
text="Сохранить"
disabled={
cur_team?.name == team_name &&
cur_team?.description == team_descr
}
onClick={() => {
if (cur_team && team_name)
updateTeamRequest({
team_id: cur_team.id,
name: team_name,
description: team_descr,
}).then((team) => {
updateTeam(team.team_id, {
name: team.name,
description: team.description,
});
notifications.show({
radius: "md",
title: "Информация команды обновлена успешно",
message: "",
icon: <IconCheck />,
style: { paddingLeft: "5px" },
});
});
}}
/>
</Grid.Col>
<Grid.Col span={6}>
<CustomButton
color="error"
text="Отменить"
disabled={
cur_team?.name == team_name &&
cur_team?.description == team_descr
}
onClick={() => {
set_team_name(cur_team?.name);
set_team_descr(cur_team?.description);
}}
/>
</Grid.Col>
</Grid>
</Card>
<div
style={{
display: "flex",
flexDirection: "row",
flexWrap: "nowrap",
gap: "20px",
alignItems: "center",
gap: "10px",
}}
>
<Avatar
radius="xl"
src={cur_team?.creator.user.profile_picture_path}
/>
<Text size="md" style={{ textDecoration: "underline" }}>
{cur_team?.creator.user.username} (
{cur_team?.creator.user.email})
</Text>
</Grid.Col>
<Grid.Col span={6}>
<CustomButton
color="accent"
text="Сохранить"
disabled={
cur_team?.name == team_name &&
cur_team?.description == team_descr
}
onClick={() => {
if (cur_team && team_name)
updateTeamRequest({
team_id: cur_team.id,
name: team_name,
description: team_descr,
}).then((team) => {
updateTeam(team.team_id, {
name: team.name,
description: team.description,
});
notifications.show({
radius: "md",
title: "Информация команды обновлена успешно",
message: "",
icon: <IconCheck />,
style: { paddingLeft: "5px" },
});
});
}}
/>
</Grid.Col>
<Grid.Col span={6}>
<CustomButton
color="error"
text="Отменить"
disabled={
cur_team?.name == team_name &&
cur_team?.description == team_descr
}
onClick={() => {
set_team_name(cur_team?.name);
set_team_descr(cur_team?.description);
}}
/>
</Grid.Col>
</Grid>
</Card>
<div
style={{
display: "flex",
flexWrap: "nowrap",
gap: "20px",
alignItems: "center",
}}
>
<Title order={2} style={{ textWrap: "nowrap" }}>
Члены команды:{" "}
</Title>
<div>
<CustomButton
color="contrast"
onClick={() => {
setIsOpen(true);
<Title order={2} style={{ textWrap: "nowrap" }}>
Члены команды:{" "}
</Title>
<div>
<CustomButton
color="contrast"
onClick={() => {
setIsOpen(true);
}}
icon={<IconPlus />}
text="Добавить члена"
/>
</div>
</div>
{cur_team && cur_team.members.length > 0 && (
<div className="TeamMembers">
{cur_team.members.map((member: TeamMember) => {
return (
<TeamMemberCard
cur_team={cur_team}
member={member}
key={member.user.keycloak_id}
/>
);
})}
</div>
)}
{!cur_team && !is_loading && (
<Alert color="red">
<Center>
{" "}
<Text c="contrast" size={"xl"}>
Ошибка. Команда не найдена
</Text>{" "}
</Center>
</Alert>
)}
</Tabs.Panel>
<Tabs.Panel value="machines">
<div
style={{
display: "flex",
flex: "1",
flexDirection: "column",
overflow: "scroll",
paddingRight: "10px",
margin: "0px",
}}
icon={<IconPlus />}
text="Добавить члена"
/>
</div>
</div>
{cur_team && cur_team.members.length > 0 && (
<div className="TeamMembers">
{cur_team.members.map((member: TeamMember) => {
return <TeamMemberCard cur_team={cur_team} member={member} />;
})}
</div>
)}
{!cur_team && !is_loading && (
<Alert color="red">
<Center>
{" "}
<Text c="contrast" size={"xl"}>
Ошибка. Команда не найдена
</Text>{" "}
</Center>
</Alert>
)}
>
{!is_loading && (
<PaginationContainer
numberOfPages={Math.ceil(
machines.total_systems / machines?.page_size,
)}
activePage={cur_page}
isLoading={is_loading}
setPage={set_page}
>
{machines?.systems.length == 0 && (
<Alert color="accent">
<Center>
{" "}
<Text c="contrast" size={"xl"}>
Нет вычислительных систем
</Text>{" "}
</Center>
</Alert>
)}
{machines?.systems.length > 0 &&
machines.systems.map((machine) => {
return (
<TeamMachinesListCard
{...machine}
key={machine.system.id}
/>
);
})}
</PaginationContainer>
)}
</div>
</Tabs.Panel>
</Tabs>
</div>
</>
);

View File

@@ -53,7 +53,6 @@ function BreadCrumbs() {
for (const prop in routes) {
for (const u_match in unique_matches) {
if (testEqual(unique_matches[u_match], routes[prop].path)) {
console.log(unique_matches[u_match], routes[prop].path);
for (const i in routes[prop].breadcrumbs(unique_matches[u_match])) {
if (elements.length + 1 != unique_matches.length) {
elements.push(

View File

@@ -1,26 +1,39 @@
import keycloak from "Api/Keycloak/Keycloak";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { Outlet } from "react-router";
import { useAuthenticationStore } from "Stores/AuthenticationStore";
export default function AuthGuard() {
const loginStarted = useRef(false);
const { is_loading } = useAuthenticationStore();
const [retry, set_retry] = useState(0);
useEffect(() => {
if (
keycloak &&
!keycloak.authenticated &&
!loginStarted.current &&
!is_loading
) {
loginStarted.current = true;
try {
if (
keycloak &&
keycloak.login &&
!keycloak.authenticated &&
!loginStarted.current &&
!is_loading
) {
loginStarted.current = true;
keycloak.login({
redirectUri: window.location.origin,
});
keycloak.login({
redirectUri: window.location.origin,
});
}
} catch {
if (retry < 5) {
console.log("retry auth");
setTimeout(() => {
set_retry(retry + 1);
}, 1);
} else {
console.log("error");
}
}
}, [is_loading, loginStarted]);
}, [is_loading, loginStarted, retry]);
if (
keycloak &&

View File

@@ -5,7 +5,6 @@ import ErrorPage from "./ErrorPage";
import DocumentationPage from "Pages/DocumentationPage/DocumentationPage";
import { IconHome } from "@tabler/icons-react";
import type { ReactElement } from "react";
import MoleculeEditorPage from "Pages/ExperimentsPage/MoleculePage";
import TeamsPage from "Pages/TeamsPage/TeamsPage";
import UserPage from "Pages/UserPage/UserPage";
import ExperimentPage from "Pages/ExperimentsPage/ExperimentPage";
@@ -13,6 +12,8 @@ import ExperimentsPage from "Pages/ExperimentsPage/ExperimentsPage";
import AuthGuard from "./RouterAuhGuard";
import DevicesPage from "Pages/DevicesPage/DevicesPage";
import TeamPage from "Pages/TeamsPage/TeamPage/TeamPage";
import DevicePage from "Pages/DevicesPage/DevicePage/DevicePage";
import TaskPage from "Pages/TaskPage/TaskPage";
export const routes: {
[id: string]: { path: string; breadcrumbs: (path: string) => ReactElement[] };
@@ -32,7 +33,7 @@ export const routes: {
<>Эксперимент #{path.split("/")[path.split("/").length - 1]}</>,
],
},
MoleculePage: {
TaskPage: {
path: "/experiments/:experiment_id/:molecule_id",
breadcrumbs: (path: string) => [
<>Молекула #{path.split("/")[path.split("/").length - 1]}</>,
@@ -42,6 +43,14 @@ export const routes: {
path: "/machines",
breadcrumbs: () => [<>Вычислительные системы</>],
},
MachinePage: {
path: "/machines/:device_id",
breadcrumbs: (path: string) => [
<>
Вычислительная система #{path.split("/")[path.split("/").length - 1]}
</>,
],
},
TeamsPage: { path: "/teams", breadcrumbs: () => [<>Команды</>] },
TeamPage: {
path: "/teams/:team_id",
@@ -84,8 +93,8 @@ const router = createBrowserRouter(
Component: ExperimentPage,
},
{
path: routes.MoleculePage.path,
Component: MoleculeEditorPage,
path: routes.TaskPage.path,
Component: TaskPage,
},
{
path: routes.TeamsPage.path,
@@ -99,6 +108,10 @@ const router = createBrowserRouter(
path: routes.MachinesPage.path,
Component: DevicesPage,
},
{
path: routes.MachinePage.path,
Component: DevicePage,
},
],
},

View File

@@ -0,0 +1,40 @@
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import type { SystemWithTeams } from "Types/Machine/Machine";
interface DeviceStoreState {
devices: SystemWithTeams[];
setDevices: (devices: SystemWithTeams[]) => void;
totalDevices: number;
setTotalDevices: (num: number) => void;
updateDevice: (deviceID: number, data: Partial<SystemWithTeams>) => void;
removeDevice: (deviceID: number) => void;
}
export const useDeviceStore = create<DeviceStoreState>()(
immer((set) => ({
devices: [],
totalDevices: 0,
setTotalDevices: (num) =>
set((state) => {
state.totalDevices = num;
}),
updateDevice: (deviceID, data) =>
set((state) => {
const device = state.devices.find((t) => t.system.id === deviceID);
if (!device) return;
Object.assign(device, data);
}),
setDevices: (devices) =>
set((state) => {
state.devices = devices;
}),
removeDevice: (deviceID) =>
set((state) => {
state.devices = state.devices.filter((t) => t.system.id !== deviceID);
}),
})),
);

View File

@@ -1,17 +0,0 @@
import type { SelectedAtom } from "Types/Experiment/MoleculeEdit/MoleculeEdit";
import { create } from "zustand";
interface MoleculeEditState {
currentMoleculeString: string;
setCurrentMoleculeString: (setCurrentMoleculeString: string) => void;
selectedAtom: SelectedAtom | null;
setSelectedAtom: (selectedAtom: SelectedAtom | null) => void;
}
export const useMoleculeEditStore = create<MoleculeEditState>()((set) => ({
currentMoleculeString: "",
setCurrentMoleculeString: (molString) =>
set(() => ({ currentMoleculeString: molString })),
selectedAtom: null,
setSelectedAtom: (atom) => set(() => ({ selectedAtom: atom })),
}));

View File

@@ -16,6 +16,7 @@ export interface Experiment {
tasks_ids: number[];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface TaskTypePlugin<TData = any> {
type: string;
// how it appears in the experiment task list
@@ -24,6 +25,7 @@ export interface TaskTypePlugin<TData = any> {
Editor: React.ComponentType<TaskEditorProps<TData>>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface TaskData<TData = any> {
id: number;
name: string;
@@ -31,6 +33,7 @@ export interface TaskData<TData = any> {
data: TData;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface TaskEditorProps<TData = any> {
data: TData;
setData: (data: TData) => void;

View File

@@ -1,5 +0,0 @@
export interface SelectedAtom {
serial: number;
name: string;
position: { x: number; y: number; z: number };
}

View File

@@ -0,0 +1,69 @@
import type { UserData } from "Types/User/User";
// Request types
export interface ComputationalSystemCreateRequest {
system_name: string;
max_qubits: number;
status_name?: string; // Optional, defaults to "ONLINE"
}
export interface ComputationalSystemEditRequest {
system_id: number;
system_name?: string;
max_qubits?: number;
}
export interface GiveSystemToTeamRequest {
system_id: number;
team_id: number;
qubits_given: number;
}
export interface RemoveSystemFromTeamRequest {
system_id: number;
team_id: number;
}
export interface ComputationalSystemDeleteRequest {
system_id: number;
}
// Response types
export interface ComputationalSystemCreateResponse {
system_id: number;
}
export interface ComputationalSystem {
id: number;
system_name: string;
max_qubits: number;
status?: string;
last_updated: Date; // datetime from backend will be string
owner: UserData;
created_at: Date;
}
export interface SystemTeamData {
team: { team_id: number; team_name: string };
num_qubits: number;
created_at: Date;
}
export interface SystemWithTeams {
system: ComputationalSystem;
teams: SystemTeamData[];
}
export interface ComputationalSystemListResponse {
systems: SystemWithTeams[];
cur_page: number;
total_systems: number;
page_size: number;
}
export interface SystemStatusResponse {
system_id: number;
status: string;
description?: string;
last_updated: string;
}

View File

@@ -14,3 +14,45 @@ export interface Team {
creator: TeamMember;
members: TeamMember[];
}
export interface TeamCreateRequest {
name: string;
description?: string | null;
}
export interface TeamUpdateRequest {
team_id: number;
name: string;
description?: string | null;
}
export interface TeamCreateResponse {
team_id: number;
}
export interface TeamListRequest {
page_num: number;
page_size: number;
}
export interface TeamDeleteRequest {
team_id: number;
}
export interface MemberAddRequest {
team_id: number;
user_id: string;
permissions: string[];
}
export interface MemberDeleteRequest {
team_id: number;
user_id: string;
}
export interface TeamsListResponse {
teams: Team[];
cur_page: number;
total_teams: number;
page_size: number;
}