added comp_systems and modular instances frontend
This commit is contained in:
32
index.html
32
index.html
@@ -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>
|
||||
|
||||
<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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
5975
public/index.html
Normal file
5975
public/index.html
Normal file
File diff suppressed because one or more lines are too long
82
src/Api/PluginLoader/PluginLoader.tsx
Normal file
82
src/Api/PluginLoader/PluginLoader.tsx
Normal 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;
|
||||
};
|
||||
110
src/Api/QuantumBackend/MachineManagment.tsx
Normal file
110
src/Api/QuantumBackend/MachineManagment.tsx
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
56
src/Components/ListCard/MachineListCard/MachinesListCard.css
Normal file
56
src/Components/ListCard/MachineListCard/MachinesListCard.css
Normal 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;
|
||||
}
|
||||
193
src/Components/ListCard/MachineListCard/MachinesListCard.tsx
Normal file
193
src/Components/ListCard/MachineListCard/MachinesListCard.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,6 +1,3 @@
|
||||
.TeamMembers {
|
||||
margin-top: 10px;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.TeamMemberCard {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -13,4 +13,5 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
127
src/Modals/AddTeamToDevice/AddTeamToDevice.tsx
Normal file
127
src/Modals/AddTeamToDevice/AddTeamToDevice.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
194
src/Pages/DevicesPage/DevicePage/DevicePage.tsx
Normal file
194
src/Pages/DevicesPage/DevicePage/DevicePage.tsx
Normal 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;
|
||||
@@ -0,0 +1,5 @@
|
||||
.DevicesPage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
70
src/Pages/TaskPage/TaskPage.tsx
Normal file
70
src/Pages/TaskPage/TaskPage.tsx
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
40
src/Stores/DeviceStore.tsx
Normal file
40
src/Stores/DeviceStore.tsx
Normal 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);
|
||||
}),
|
||||
})),
|
||||
);
|
||||
@@ -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 })),
|
||||
}));
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface SelectedAtom {
|
||||
serial: number;
|
||||
name: string;
|
||||
position: { x: number; y: number; z: number };
|
||||
}
|
||||
69
src/Types/Machine/Machine.tsx
Normal file
69
src/Types/Machine/Machine.tsx
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user