added comp_systems and modular instances frontend
This commit is contained in:
32
index.html
32
index.html
@@ -1,14 +1,24 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="preload" href="/Quicking.otf" as="font" type="font/otf" crossorigin>
|
<link
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
rel="preload"
|
||||||
<title>QMolSim</title>
|
href="/Quicking.otf"
|
||||||
</head>
|
as="font"
|
||||||
<body>
|
type="font/otf"
|
||||||
<div id="root"></div>
|
crossorigin
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
/>
|
||||||
</body>
|
<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>
|
</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 keycloak from "Api/Keycloak/Keycloak";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import type { Team, TeamMember } from "Types/Team/Team";
|
import type {
|
||||||
|
Team,
|
||||||
export interface TeamCreateRequest {
|
TeamMember,
|
||||||
name: string;
|
TeamCreateRequest,
|
||||||
description?: string | null;
|
TeamCreateResponse,
|
||||||
}
|
TeamUpdateRequest,
|
||||||
|
TeamListRequest,
|
||||||
export interface TeamUpdateRequest {
|
TeamsListResponse,
|
||||||
team_id: number;
|
MemberAddRequest,
|
||||||
name: string;
|
MemberDeleteRequest,
|
||||||
description?: string | null;
|
} from "Types/Team/Team";
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: `${import.meta.env.VITE_QUANTUM_BACKEND_URL}/team`,
|
baseURL: `${import.meta.env.VITE_QUANTUM_BACKEND_URL}/team`,
|
||||||
@@ -84,11 +52,13 @@ export const getTeams = async (
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getShortTeamsList = async (): Promise<
|
export const getShortTeamsList = async (
|
||||||
{ team_id: number; team_name: string }[]
|
params: Partial<{ permission: string }>,
|
||||||
> => {
|
): Promise<{ team_id: number; team_name: string }[]> => {
|
||||||
const response =
|
const response = await api.get<{ team_id: number; team_name: string }[]>(
|
||||||
await api.get<{ team_id: number; team_name: string }[]>("short_list");
|
"short_list",
|
||||||
|
{ headers: {}, params },
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ function App() {
|
|||||||
if (keycloak.authenticated)
|
if (keycloak.authenticated)
|
||||||
keycloak.updateToken(60).catch((error) => {
|
keycloak.updateToken(60).catch((error) => {
|
||||||
console.log(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,
|
IconLogout,
|
||||||
IconSettings2,
|
IconSettings2,
|
||||||
IconUsers,
|
IconUsers,
|
||||||
type Icon,
|
|
||||||
type IconProps,
|
type IconProps,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { Link, useLocation } from "react-router";
|
import { Link, useLocation } from "react-router";
|
||||||
@@ -28,7 +27,9 @@ import CustomButton from "Components/CustomButton/CustomButton";
|
|||||||
|
|
||||||
interface SubtleLinkButtonProps {
|
interface SubtleLinkButtonProps {
|
||||||
link: string;
|
link: string;
|
||||||
Icon: ForwardRefExoticComponent<IconProps & React.RefAttributes<Icon>>;
|
Icon: ForwardRefExoticComponent<
|
||||||
|
IconProps & React.RefAttributes<SVGSVGElement>
|
||||||
|
>;
|
||||||
text: string;
|
text: string;
|
||||||
color: string;
|
color: string;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
|
|||||||
@@ -36,27 +36,48 @@ function ExperimentsListCard(props: {
|
|||||||
<Text mb="sm" size="md">
|
<Text mb="sm" size="md">
|
||||||
Команда: {props.team?.team_name}
|
Команда: {props.team?.team_name}
|
||||||
</Text>
|
</Text>
|
||||||
<Pill className="ExperimentPill">
|
<div style={{ display: "flex", gap: "10px" }}>
|
||||||
<Text size="md">Статус: {props.experiment.experiment_status}</Text>
|
<Text size="md">Статус: </Text>
|
||||||
</Pill>
|
<Pill style={{ backgroundColor: "var(--mantine-color-yellow-5)" }}>
|
||||||
|
<Text size="md">{props.experiment.experiment_status}</Text>
|
||||||
|
</Pill>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ExperimentSectionWithLine">
|
<div className="ExperimentSectionWithLine">
|
||||||
<SimpleGrid cols={2} verticalSpacing="0px">
|
<SimpleGrid cols={2} verticalSpacing="0px">
|
||||||
<Text>Задачи:</Text>
|
<Text>Задачи:</Text>
|
||||||
{experiment_tasks.map((task: TaskData<any>) => {
|
|
||||||
return (
|
{experiment_tasks.map(
|
||||||
<Text
|
(
|
||||||
size="md"
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
style={{
|
task: TaskData<any>,
|
||||||
textWrap: "nowrap",
|
) => {
|
||||||
textOverflow: "ellipsis",
|
return (
|
||||||
overflow: "hidden",
|
<Pill
|
||||||
}}
|
size="md"
|
||||||
>
|
style={{
|
||||||
{task.name}
|
backgroundColor: "transparent",
|
||||||
</Text>
|
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 ? (
|
{experiment_tasks.length == 0 ? (
|
||||||
<Pill size="md" className="ExperimentPill2">
|
<Pill size="md" className="ExperimentPill2">
|
||||||
Нет Задач
|
Нет Задач
|
||||||
@@ -70,10 +91,14 @@ function ExperimentsListCard(props: {
|
|||||||
<div className="TopRightContainer">
|
<div className="TopRightContainer">
|
||||||
<div className="dateContainer">
|
<div className="dateContainer">
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
{props.experiment.date_created.toLocaleDateString("ru")}{" "}
|
{new Date(
|
||||||
|
props.experiment.date_created + "Z",
|
||||||
|
).toLocaleDateString("ru")}{" "}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
{props.experiment.date_created.toLocaleTimeString("ru")}
|
{new Date(
|
||||||
|
props.experiment.date_created + "Z",
|
||||||
|
).toLocaleTimeString("ru")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<UnstyledButton
|
<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 {
|
.TeamMemberCard {
|
||||||
margin-top: 10px;
|
min-height: 100px;
|
||||||
gap: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,13 @@ function TeamMemberCard(props: { cur_team: Team; member: TeamMember }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padding="sm" withBorder orientation="horizontal">
|
<Card
|
||||||
|
padding="sm"
|
||||||
|
withBorder
|
||||||
|
orientation="horizontal"
|
||||||
|
className="TeamMemberCard"
|
||||||
|
key={props.member.user.keycloak_id}
|
||||||
|
>
|
||||||
<Card.Section
|
<Card.Section
|
||||||
inheritPadding
|
inheritPadding
|
||||||
px="xs"
|
px="xs"
|
||||||
@@ -168,7 +174,6 @@ function TeamMemberCard(props: { cur_team: Team; member: TeamMember }) {
|
|||||||
placeholder="Выберите разрешения"
|
placeholder="Выберите разрешения"
|
||||||
searchable
|
searchable
|
||||||
data={props.cur_team.creator.permissions}
|
data={props.cur_team.creator.permissions}
|
||||||
classNames={{ input: "MantineInput" }}
|
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
set_selected_permissions(value);
|
set_selected_permissions(value);
|
||||||
}}
|
}}
|
||||||
@@ -227,10 +232,10 @@ function TeamMemberCard(props: { cur_team: Team; member: TeamMember }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="sm" style={{ textAlign: "right" }}>
|
<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>
|
||||||
<Text size="sm" style={{ textAlign: "right" }}>
|
<Text size="sm" style={{ textAlign: "right" }}>
|
||||||
{new Date(props.member.joined_at).toLocaleTimeString("ru")}
|
{new Date(props.member.joined_at + "Z").toLocaleTimeString("ru")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -90,17 +90,19 @@ function TeamsListCard(props: Team) {
|
|||||||
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
|
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
|
||||||
Команда: {props.name}
|
Команда: {props.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text mb="sm" size="md">
|
<div style={{ display: "flex" }}>
|
||||||
Владелец:
|
<Text mb="sm" size="md">
|
||||||
|
Владелец:
|
||||||
|
</Text>
|
||||||
<Pill className="TeamPill" style={{ marginLeft: "10px" }}>
|
<Pill className="TeamPill" style={{ marginLeft: "10px" }}>
|
||||||
<Text size="md">{props.creator.user.username}</Text>
|
<Text size="md">{props.creator.user.username}</Text>
|
||||||
</Pill>
|
</Pill>
|
||||||
</Text>
|
</div>
|
||||||
<div className="membersPills">
|
<div className="membersPills">
|
||||||
<Text>Члены:</Text>
|
<Text>Члены:</Text>
|
||||||
{props.members.map((team: TeamMember) => {
|
{props.members.map((team: TeamMember) => {
|
||||||
return (
|
return (
|
||||||
<Pill className="TeamPill" style={{ marginLeft: "10px" }}>
|
<Pill className="TeamPill">
|
||||||
<Text
|
<Text
|
||||||
size="md"
|
size="md"
|
||||||
style={{
|
style={{
|
||||||
@@ -170,10 +172,10 @@ function TeamsListCard(props: Team) {
|
|||||||
<div className="TopRightContainer">
|
<div className="TopRightContainer">
|
||||||
<div className="dateContainer">
|
<div className="dateContainer">
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
{new Date(props.created_at).toLocaleDateString()}{" "}
|
{new Date(props.created_at + "Z").toLocaleDateString()}{" "}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
{new Date(props.created_at).toLocaleTimeString()}
|
{new Date(props.created_at + "Z").toLocaleTimeString()}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
{props.creator.user.keycloak_id == profile?.id && (
|
{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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ export function AddMember(props: NewExperimentModalProps) {
|
|||||||
label="Почта члена команды"
|
label="Почта члена команды"
|
||||||
placeholder="Введите почту"
|
placeholder="Введите почту"
|
||||||
required
|
required
|
||||||
classNames={{ input: "MantineInput" }}
|
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setEmail(event.currentTarget.value);
|
setEmail(event.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
@@ -110,7 +109,6 @@ export function AddMember(props: NewExperimentModalProps) {
|
|||||||
placeholder="Выберите разрешения"
|
placeholder="Выберите разрешения"
|
||||||
searchable
|
searchable
|
||||||
data={props.permissions}
|
data={props.permissions}
|
||||||
classNames={{ input: "MantineInput" }}
|
|
||||||
onChange={(value) => setSelectedPerms(value)}
|
onChange={(value) => setSelectedPerms(value)}
|
||||||
/>
|
/>
|
||||||
<Space h="md" />
|
<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,
|
name: name,
|
||||||
description: description,
|
description: description,
|
||||||
team_id: Number(selectedTeam?.value),
|
team_id: Number(selectedTeam?.value),
|
||||||
tasks_ids: [],
|
tasks_ids: [1, 2],
|
||||||
date_created: new Date(),
|
date_created: new Date(),
|
||||||
experiment_status: "DRAFT",
|
experiment_status: "PROCESSING",
|
||||||
experiment_type: "a",
|
experiment_type: "VQE",
|
||||||
});
|
});
|
||||||
props.setIsOpened(false);
|
props.setIsOpened(false);
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,6 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
|
|||||||
label="Имя эксперимента"
|
label="Имя эксперимента"
|
||||||
placeholder="Введите имя эксперимента"
|
placeholder="Введите имя эксперимента"
|
||||||
required
|
required
|
||||||
classNames={{ input: "MantineInput" }}
|
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setName(event.currentTarget.value);
|
setName(event.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
@@ -86,7 +85,6 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
|
|||||||
minRows={4}
|
minRows={4}
|
||||||
maxRows={10}
|
maxRows={10}
|
||||||
autosize
|
autosize
|
||||||
classNames={{ input: "MantineInput" }}
|
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setDescription(event.currentTarget.value);
|
setDescription(event.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
@@ -108,7 +106,6 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
|
|||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
classNames={{ input: "MantineInput" }}
|
|
||||||
onChange={(_value, option) => setSelectedTeam(option)}
|
onChange={(_value, option) => setSelectedTeam(option)}
|
||||||
/>
|
/>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
GetInFormats,
|
GetInFormats,
|
||||||
} from "Api/ConvertBackendCalls";
|
} from "Api/ConvertBackendCalls";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { useMoleculeEditStore } from "Stores/MoleculeEditStore";
|
|
||||||
import CustomButton from "Components/CustomButton/CustomButton";
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
|
||||||
interface NewMoleculeModalProps {
|
interface NewMoleculeModalProps {
|
||||||
@@ -31,7 +30,6 @@ interface NewMoleculeModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NewMoleculeModal(props: NewMoleculeModalProps) {
|
export function NewMoleculeModal(props: NewMoleculeModalProps) {
|
||||||
const { setCurrentMoleculeString } = useMoleculeEditStore();
|
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
const [selectedMethod, setSelectedMethod] = useState(0);
|
const [selectedMethod, setSelectedMethod] = useState(0);
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export function NewTeamModal(props: NewTeamModalProps) {
|
|||||||
label="Имя команлы"
|
label="Имя команлы"
|
||||||
placeholder="Введите имя команды"
|
placeholder="Введите имя команды"
|
||||||
required
|
required
|
||||||
classNames={{ input: "MantineInput" }}
|
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setName(event.currentTarget.value);
|
setName(event.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
@@ -88,7 +87,6 @@ export function NewTeamModal(props: NewTeamModalProps) {
|
|||||||
minRows={4}
|
minRows={4}
|
||||||
maxRows={10}
|
maxRows={10}
|
||||||
autosize
|
autosize
|
||||||
classNames={{ input: "MantineInput" }}
|
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setDescription(event.currentTarget.value);
|
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 { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
|
||||||
import { Alert } from "@mantine/core";
|
import { Alert } from "@mantine/core";
|
||||||
import { Link } from "react-router";
|
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() {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -16,17 +86,33 @@ function DevicesPage() {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="DevicesPage">
|
<div className="DevicesPage">
|
||||||
<PaginationContainer
|
<PaginationContainer
|
||||||
numberOfPages={1}
|
numberOfPages={Math.ceil(totalDevices / page_size)}
|
||||||
isLoading={false}
|
isLoading={isLoading}
|
||||||
activePage={1}
|
activePage={cur_page}
|
||||||
setPage={() => {}}
|
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">
|
{devices.length == 0 && !isLoading && (
|
||||||
Для того, чтобы добавить устройство, простмотрите{" "}
|
<Alert title="Подключенные устройства не найдены" color="blue">
|
||||||
<Link to={"documentaion"} className="invisible_link">
|
Для того, чтобы добавить устройство, простмотрите{" "}
|
||||||
документацию
|
<Link to={"documentaion"} className="invisible_link">
|
||||||
</Link>
|
документацию
|
||||||
</Alert>
|
</Link>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{devices.map((machine) => {
|
||||||
|
return <MachinesListCard {...machine} />;
|
||||||
|
})}
|
||||||
</PaginationContainer>
|
</PaginationContainer>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Alert, Center, Text } from "@mantine/core";
|
import { Alert, Center, Text } from "@mantine/core";
|
||||||
import "./ExperimentPage.css";
|
import "./ExperimentPage.css";
|
||||||
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
|
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
|
||||||
import { NewMoleculeModal } from "Modals/NewMolecule/NewMolecule";
|
//import { NewMoleculeModal } from "Modals/NewMolecule/NewMolecule";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { IconPlus, IconSettings } from "@tabler/icons-react";
|
import { IconPlus, IconSettings } from "@tabler/icons-react";
|
||||||
@@ -12,7 +12,7 @@ import { useExperimentStore } from "Stores/ExperimentStore";
|
|||||||
function ExperimentPage() {
|
function ExperimentPage() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { experiment_id } = useParams();
|
const { experiment_id } = useParams();
|
||||||
const { experiments } = useExperimentStore();
|
const { experiments, addTask } = useExperimentStore();
|
||||||
|
|
||||||
const experiment = experiments.find((exp) => {
|
const experiment = experiments.find((exp) => {
|
||||||
return exp.id == Number(experiment_id);
|
return exp.id == Number(experiment_id);
|
||||||
@@ -33,10 +33,6 @@ function ExperimentPage() {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
{experiment && (
|
{experiment && (
|
||||||
<div className="ExperimentPage">
|
<div className="ExperimentPage">
|
||||||
<NewMoleculeModal
|
|
||||||
isOpened={isOpen}
|
|
||||||
setIsOpened={(toOpen: boolean) => setIsOpen(toOpen)}
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -49,6 +45,12 @@ function ExperimentPage() {
|
|||||||
<CustomButton
|
<CustomButton
|
||||||
color="contrast"
|
color="contrast"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
addTask(Number(experiment_id) || 0, {
|
||||||
|
id: 1,
|
||||||
|
name: "Задача 1",
|
||||||
|
description: "",
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}}
|
}}
|
||||||
icon={<IconPlus />}
|
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 {
|
.TeamPage {
|
||||||
flex-grow: 1;
|
height: calc(100vh - 140px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -9,3 +9,24 @@
|
|||||||
.TeamData {
|
.TeamData {
|
||||||
margin-bottom: 15px;
|
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,
|
Center,
|
||||||
Grid,
|
Grid,
|
||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
|
Tabs,
|
||||||
Text,
|
Text,
|
||||||
Textarea,
|
Textarea,
|
||||||
TextInput,
|
TextInput,
|
||||||
@@ -23,35 +24,59 @@ import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
|||||||
import TeamMemberCard from "Components/ListCard/TeamListCard/TeamMemberCard/TeamMemberCard";
|
import TeamMemberCard from "Components/ListCard/TeamListCard/TeamMemberCard/TeamMemberCard";
|
||||||
import { AddMember } from "Modals/AddMember/AddMember";
|
import { AddMember } from "Modals/AddMember/AddMember";
|
||||||
import { notifications } from "@mantine/notifications";
|
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() {
|
function TeamPage() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { team_id } = useParams();
|
const { team_id } = useParams();
|
||||||
const { addTeam, teams, updateTeam } = useTeamStore();
|
const { addTeam, teams, updateTeam } = useTeamStore();
|
||||||
const { profile } = useAuthenticationStore();
|
const { profile, is_loading } = useAuthenticationStore();
|
||||||
const [is_loading, set_is_loading] = useState(true);
|
const [is_loading_this, set_is_loading] = useState(true);
|
||||||
|
|
||||||
const [cur_team, set_cur_team] = useState<Team>();
|
const [cur_team, set_cur_team] = useState<Team>();
|
||||||
|
|
||||||
const [team_name, set_team_name] = useState(cur_team?.name);
|
const [team_name, set_team_name] = useState(cur_team?.name || "");
|
||||||
const [team_descr, set_team_descr] = useState(cur_team?.description);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!cur_team && profile) {
|
if (!cur_team && profile && !is_loading) {
|
||||||
getTeam(Number(team_id))
|
getTeam(Number(team_id)).then((team) => {
|
||||||
.then((team) => {
|
if (team) {
|
||||||
if (team) {
|
addTeam(team);
|
||||||
addTeam(team);
|
set_cur_team(team);
|
||||||
set_cur_team(team);
|
set_team_name(team.name);
|
||||||
set_team_name(team.name);
|
set_team_descr(team.description);
|
||||||
set_team_descr(team.description);
|
set_is_loading(false);
|
||||||
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(() => {
|
.catch(() => {
|
||||||
set_is_loading(false);
|
set_is_loading(false);
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
|
if (machines && cur_team && profile) {
|
||||||
if (profile) {
|
if (profile) {
|
||||||
set_is_loading(false);
|
set_is_loading(false);
|
||||||
}
|
}
|
||||||
@@ -59,7 +84,7 @@ function TeamPage() {
|
|||||||
if (teams.find((e) => e.id === Number(team_id)) != cur_team) {
|
if (teams.find((e) => e.id === Number(team_id)) != cur_team) {
|
||||||
set_cur_team(teams.find((e) => e.id === Number(team_id)));
|
set_cur_team(teams.find((e) => e.id === Number(team_id)));
|
||||||
}
|
}
|
||||||
}, [cur_team, profile, teams]);
|
}, [cur_team, profile, teams, machines, cur_page]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -79,147 +104,210 @@ function TeamPage() {
|
|||||||
team={cur_team}
|
team={cur_team}
|
||||||
/>
|
/>
|
||||||
<div className="TeamPage">
|
<div className="TeamPage">
|
||||||
<LoadingOverlay
|
<Tabs
|
||||||
visible={is_loading}
|
defaultValue="team"
|
||||||
zIndex={1000}
|
classNames={{ panel: "tabPanel" }}
|
||||||
overlayProps={{ radius: "sm", blur: 2 }}
|
style={{ display: "flex", flexDirection: "column", height: "100%" }}
|
||||||
loaderProps={{ size: 50, type: "dots" }}
|
>
|
||||||
/>
|
<Tabs.List>
|
||||||
|
<Tabs.Tab value="team">Команда</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="machines">Системы</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
<Card className="TeamData">
|
<Tabs.Panel value="team">
|
||||||
<Grid>
|
<LoadingOverlay
|
||||||
<Grid.Col span={3}>
|
visible={is_loading_this}
|
||||||
<Title size={"xl"}>Имя команды:</Title>
|
zIndex={1000}
|
||||||
</Grid.Col>
|
overlayProps={{ radius: "sm", blur: 2 }}
|
||||||
<Grid.Col span={9}>
|
loaderProps={{ size: 50, type: "dots" }}
|
||||||
<TextInput
|
/>
|
||||||
size="md"
|
|
||||||
value={team_name}
|
<Card className="TeamData">
|
||||||
onChange={(val) => {
|
<Grid>
|
||||||
set_team_name(val.target.value);
|
<Grid.Col span={3}>
|
||||||
}}
|
<Title size={"xl"}>Имя команды:</Title>
|
||||||
/>
|
</Grid.Col>
|
||||||
</Grid.Col>
|
<Grid.Col span={9}>
|
||||||
<Grid.Col span={3}>
|
<TextInput
|
||||||
<Title size={"xl"}>Описание:</Title>
|
size="md"
|
||||||
</Grid.Col>
|
value={team_name}
|
||||||
<Grid.Col span={9}>
|
onChange={(val) => {
|
||||||
<Textarea
|
set_team_name(val.target.value);
|
||||||
size="md"
|
}}
|
||||||
value={team_descr ? team_descr : ""}
|
/>
|
||||||
onChange={(val) => {
|
</Grid.Col>
|
||||||
set_team_descr(val.target.value);
|
<Grid.Col span={3}>
|
||||||
}}
|
<Title size={"xl"}>Описание:</Title>
|
||||||
minRows={2}
|
</Grid.Col>
|
||||||
maxRows={4}
|
<Grid.Col span={9}>
|
||||||
/>
|
<Textarea
|
||||||
</Grid.Col>
|
size="md"
|
||||||
<Grid.Col span={3}>
|
value={team_descr ? team_descr : ""}
|
||||||
<Title size={"xl"}>Создатель:</Title>
|
onChange={(val) => {
|
||||||
</Grid.Col>
|
set_team_descr(val.target.value);
|
||||||
<Grid.Col
|
}}
|
||||||
span={9}
|
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={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexWrap: "nowrap",
|
||||||
|
gap: "20px",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "10px",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Title order={2} style={{ textWrap: "nowrap" }}>
|
||||||
radius="xl"
|
Члены команды:{" "}
|
||||||
src={cur_team?.creator.user.profile_picture_path}
|
</Title>
|
||||||
/>
|
<div>
|
||||||
<Text size="md" style={{ textDecoration: "underline" }}>
|
<CustomButton
|
||||||
{cur_team?.creator.user.username} (
|
color="contrast"
|
||||||
{cur_team?.creator.user.email})
|
onClick={() => {
|
||||||
</Text>
|
setIsOpen(true);
|
||||||
</Grid.Col>
|
}}
|
||||||
<Grid.Col span={6}>
|
icon={<IconPlus />}
|
||||||
<CustomButton
|
text="Добавить члена"
|
||||||
color="accent"
|
/>
|
||||||
text="Сохранить"
|
</div>
|
||||||
disabled={
|
</div>
|
||||||
cur_team?.name == team_name &&
|
{cur_team && cur_team.members.length > 0 && (
|
||||||
cur_team?.description == team_descr
|
<div className="TeamMembers">
|
||||||
}
|
{cur_team.members.map((member: TeamMember) => {
|
||||||
onClick={() => {
|
return (
|
||||||
if (cur_team && team_name)
|
<TeamMemberCard
|
||||||
updateTeamRequest({
|
cur_team={cur_team}
|
||||||
team_id: cur_team.id,
|
member={member}
|
||||||
name: team_name,
|
key={member.user.keycloak_id}
|
||||||
description: team_descr,
|
/>
|
||||||
}).then((team) => {
|
);
|
||||||
updateTeam(team.team_id, {
|
})}
|
||||||
name: team.name,
|
</div>
|
||||||
description: team.description,
|
)}
|
||||||
});
|
{!cur_team && !is_loading && (
|
||||||
notifications.show({
|
<Alert color="red">
|
||||||
radius: "md",
|
<Center>
|
||||||
title: "Информация команды обновлена успешно",
|
{" "}
|
||||||
message: "",
|
<Text c="contrast" size={"xl"}>
|
||||||
icon: <IconCheck />,
|
Ошибка. Команда не найдена
|
||||||
style: { paddingLeft: "5px" },
|
</Text>{" "}
|
||||||
});
|
</Center>
|
||||||
});
|
</Alert>
|
||||||
}}
|
)}
|
||||||
/>
|
</Tabs.Panel>
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6}>
|
<Tabs.Panel value="machines">
|
||||||
<CustomButton
|
<div
|
||||||
color="error"
|
style={{
|
||||||
text="Отменить"
|
display: "flex",
|
||||||
disabled={
|
flex: "1",
|
||||||
cur_team?.name == team_name &&
|
flexDirection: "column",
|
||||||
cur_team?.description == team_descr
|
overflow: "scroll",
|
||||||
}
|
paddingRight: "10px",
|
||||||
onClick={() => {
|
margin: "0px",
|
||||||
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);
|
|
||||||
}}
|
}}
|
||||||
icon={<IconPlus />}
|
>
|
||||||
text="Добавить члена"
|
{!is_loading && (
|
||||||
/>
|
<PaginationContainer
|
||||||
</div>
|
numberOfPages={Math.ceil(
|
||||||
</div>
|
machines.total_systems / machines?.page_size,
|
||||||
{cur_team && cur_team.members.length > 0 && (
|
)}
|
||||||
<div className="TeamMembers">
|
activePage={cur_page}
|
||||||
{cur_team.members.map((member: TeamMember) => {
|
isLoading={is_loading}
|
||||||
return <TeamMemberCard cur_team={cur_team} member={member} />;
|
setPage={set_page}
|
||||||
})}
|
>
|
||||||
</div>
|
{machines?.systems.length == 0 && (
|
||||||
)}
|
<Alert color="accent">
|
||||||
{!cur_team && !is_loading && (
|
<Center>
|
||||||
<Alert color="red">
|
{" "}
|
||||||
<Center>
|
<Text c="contrast" size={"xl"}>
|
||||||
{" "}
|
Нет вычислительных систем
|
||||||
<Text c="contrast" size={"xl"}>
|
</Text>{" "}
|
||||||
Ошибка. Команда не найдена
|
</Center>
|
||||||
</Text>{" "}
|
</Alert>
|
||||||
</Center>
|
)}
|
||||||
</Alert>
|
{machines?.systems.length > 0 &&
|
||||||
)}
|
machines.systems.map((machine) => {
|
||||||
|
return (
|
||||||
|
<TeamMachinesListCard
|
||||||
|
{...machine}
|
||||||
|
key={machine.system.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</PaginationContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ function BreadCrumbs() {
|
|||||||
for (const prop in routes) {
|
for (const prop in routes) {
|
||||||
for (const u_match in unique_matches) {
|
for (const u_match in unique_matches) {
|
||||||
if (testEqual(unique_matches[u_match], routes[prop].path)) {
|
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])) {
|
for (const i in routes[prop].breadcrumbs(unique_matches[u_match])) {
|
||||||
if (elements.length + 1 != unique_matches.length) {
|
if (elements.length + 1 != unique_matches.length) {
|
||||||
elements.push(
|
elements.push(
|
||||||
|
|||||||
@@ -1,26 +1,39 @@
|
|||||||
import keycloak from "Api/Keycloak/Keycloak";
|
import keycloak from "Api/Keycloak/Keycloak";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
|
|
||||||
export default function AuthGuard() {
|
export default function AuthGuard() {
|
||||||
const loginStarted = useRef(false);
|
const loginStarted = useRef(false);
|
||||||
const { is_loading } = useAuthenticationStore();
|
const { is_loading } = useAuthenticationStore();
|
||||||
|
const [retry, set_retry] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
try {
|
||||||
keycloak &&
|
if (
|
||||||
!keycloak.authenticated &&
|
keycloak &&
|
||||||
!loginStarted.current &&
|
keycloak.login &&
|
||||||
!is_loading
|
!keycloak.authenticated &&
|
||||||
) {
|
!loginStarted.current &&
|
||||||
loginStarted.current = true;
|
!is_loading
|
||||||
|
) {
|
||||||
|
loginStarted.current = true;
|
||||||
|
|
||||||
keycloak.login({
|
keycloak.login({
|
||||||
redirectUri: window.location.origin,
|
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 (
|
if (
|
||||||
keycloak &&
|
keycloak &&
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import ErrorPage from "./ErrorPage";
|
|||||||
import DocumentationPage from "Pages/DocumentationPage/DocumentationPage";
|
import DocumentationPage from "Pages/DocumentationPage/DocumentationPage";
|
||||||
import { IconHome } from "@tabler/icons-react";
|
import { IconHome } from "@tabler/icons-react";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import MoleculeEditorPage from "Pages/ExperimentsPage/MoleculePage";
|
|
||||||
import TeamsPage from "Pages/TeamsPage/TeamsPage";
|
import TeamsPage from "Pages/TeamsPage/TeamsPage";
|
||||||
import UserPage from "Pages/UserPage/UserPage";
|
import UserPage from "Pages/UserPage/UserPage";
|
||||||
import ExperimentPage from "Pages/ExperimentsPage/ExperimentPage";
|
import ExperimentPage from "Pages/ExperimentsPage/ExperimentPage";
|
||||||
@@ -13,6 +12,8 @@ import ExperimentsPage from "Pages/ExperimentsPage/ExperimentsPage";
|
|||||||
import AuthGuard from "./RouterAuhGuard";
|
import AuthGuard from "./RouterAuhGuard";
|
||||||
import DevicesPage from "Pages/DevicesPage/DevicesPage";
|
import DevicesPage from "Pages/DevicesPage/DevicesPage";
|
||||||
import TeamPage from "Pages/TeamsPage/TeamPage/TeamPage";
|
import TeamPage from "Pages/TeamsPage/TeamPage/TeamPage";
|
||||||
|
import DevicePage from "Pages/DevicesPage/DevicePage/DevicePage";
|
||||||
|
import TaskPage from "Pages/TaskPage/TaskPage";
|
||||||
|
|
||||||
export const routes: {
|
export const routes: {
|
||||||
[id: string]: { path: string; breadcrumbs: (path: string) => ReactElement[] };
|
[id: string]: { path: string; breadcrumbs: (path: string) => ReactElement[] };
|
||||||
@@ -32,7 +33,7 @@ export const routes: {
|
|||||||
<>Эксперимент #{path.split("/")[path.split("/").length - 1]}</>,
|
<>Эксперимент #{path.split("/")[path.split("/").length - 1]}</>,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
MoleculePage: {
|
TaskPage: {
|
||||||
path: "/experiments/:experiment_id/:molecule_id",
|
path: "/experiments/:experiment_id/:molecule_id",
|
||||||
breadcrumbs: (path: string) => [
|
breadcrumbs: (path: string) => [
|
||||||
<>Молекула #{path.split("/")[path.split("/").length - 1]}</>,
|
<>Молекула #{path.split("/")[path.split("/").length - 1]}</>,
|
||||||
@@ -42,6 +43,14 @@ export const routes: {
|
|||||||
path: "/machines",
|
path: "/machines",
|
||||||
breadcrumbs: () => [<>Вычислительные системы</>],
|
breadcrumbs: () => [<>Вычислительные системы</>],
|
||||||
},
|
},
|
||||||
|
MachinePage: {
|
||||||
|
path: "/machines/:device_id",
|
||||||
|
breadcrumbs: (path: string) => [
|
||||||
|
<>
|
||||||
|
Вычислительная система #{path.split("/")[path.split("/").length - 1]}
|
||||||
|
</>,
|
||||||
|
],
|
||||||
|
},
|
||||||
TeamsPage: { path: "/teams", breadcrumbs: () => [<>Команды</>] },
|
TeamsPage: { path: "/teams", breadcrumbs: () => [<>Команды</>] },
|
||||||
TeamPage: {
|
TeamPage: {
|
||||||
path: "/teams/:team_id",
|
path: "/teams/:team_id",
|
||||||
@@ -84,8 +93,8 @@ const router = createBrowserRouter(
|
|||||||
Component: ExperimentPage,
|
Component: ExperimentPage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: routes.MoleculePage.path,
|
path: routes.TaskPage.path,
|
||||||
Component: MoleculeEditorPage,
|
Component: TaskPage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: routes.TeamsPage.path,
|
path: routes.TeamsPage.path,
|
||||||
@@ -99,6 +108,10 @@ const router = createBrowserRouter(
|
|||||||
path: routes.MachinesPage.path,
|
path: routes.MachinesPage.path,
|
||||||
Component: DevicesPage,
|
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[];
|
tasks_ids: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export interface TaskTypePlugin<TData = any> {
|
export interface TaskTypePlugin<TData = any> {
|
||||||
type: string;
|
type: string;
|
||||||
// how it appears in the experiment task list
|
// how it appears in the experiment task list
|
||||||
@@ -24,6 +25,7 @@ export interface TaskTypePlugin<TData = any> {
|
|||||||
Editor: React.ComponentType<TaskEditorProps<TData>>;
|
Editor: React.ComponentType<TaskEditorProps<TData>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export interface TaskData<TData = any> {
|
export interface TaskData<TData = any> {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -31,6 +33,7 @@ export interface TaskData<TData = any> {
|
|||||||
data: TData;
|
data: TData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export interface TaskEditorProps<TData = any> {
|
export interface TaskEditorProps<TData = any> {
|
||||||
data: TData;
|
data: TData;
|
||||||
setData: (data: TData) => void;
|
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;
|
creator: TeamMember;
|
||||||
members: 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