v1.0
- added task rendering instead of molecule - fied a lot of errors - changes experiment page to show generic experiment info
This commit is contained in:
@@ -1,49 +0,0 @@
|
||||
import axios, { AxiosError } from "axios";
|
||||
import type { ConvertSchema } from "Types/ApiCalls/ConvertBackendCallsTypes";
|
||||
|
||||
export async function ConvertMoleculeToStandart(
|
||||
data: ConvertSchema,
|
||||
): Promise<string | AxiosError> {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
import.meta.env.VITE_MOLECULAR_BACKEND_URL + "/convert",
|
||||
{
|
||||
text: data.inputText,
|
||||
format: data.inputFormat,
|
||||
convert_3d: data.make_3d,
|
||||
add_hydrogen: data.add_h,
|
||||
optimize_geometry: data.optimize,
|
||||
},
|
||||
);
|
||||
|
||||
// Response from the FastAPI JSONResponse
|
||||
return response.data.molfile;
|
||||
} catch (error) {
|
||||
//Error handling
|
||||
if (axios.isAxiosError(error)) {
|
||||
// Do something with the axios error...
|
||||
return error;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function GetInFormats(): Promise<
|
||||
{ [key: string]: string } | AxiosError
|
||||
> {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
import.meta.env.VITE_MOLECULAR_BACKEND_URL + "/informats",
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
//Error handling
|
||||
if (axios.isAxiosError(error)) {
|
||||
// Do something with the axios error...
|
||||
return error;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,8 +61,6 @@ export const updatePasswordWithRedirect = async (
|
||||
redirectUri:
|
||||
successRedirectUrl ||
|
||||
window.location.origin +
|
||||
"/" +
|
||||
import.meta.env.VITE_BASE_PATH +
|
||||
"/" +
|
||||
routes.SettingsPage.path +
|
||||
"?password_updated=true",
|
||||
@@ -85,7 +83,6 @@ export const SendEmailVerification = async () => {
|
||||
action: "VERIFY_EMAIL",
|
||||
redirectUri:
|
||||
window.location.origin +
|
||||
import.meta.env.VITE_BASE_PATH +
|
||||
"/" +
|
||||
routes.SettingsPage.path +
|
||||
"?email_sent=true",
|
||||
|
||||
@@ -1,58 +1,59 @@
|
||||
import React, { useRef, useEffect, useState, useMemo } from "react";
|
||||
import { LoadingOverlay } from "@mantine/core";
|
||||
import React, { useRef, useEffect, useState } 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;
|
||||
index: number;
|
||||
plugin: string;
|
||||
mode: "List" | "Editor";
|
||||
taskData: string;
|
||||
simProgress: string;
|
||||
qubits_needed: number;
|
||||
onUpdate?: (data: string, qubits_needed: number) => void;
|
||||
}
|
||||
|
||||
export const IframePlugin: React.FC<IframePluginProps> = ({
|
||||
pluginUrl,
|
||||
index,
|
||||
plugin,
|
||||
mode,
|
||||
taskData,
|
||||
simProgress,
|
||||
qubits_needed,
|
||||
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
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
// 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,
|
||||
taskData: taskData,
|
||||
simProgress: simProgress,
|
||||
qubits_needed: qubits_needed,
|
||||
theme: theme.theme,
|
||||
mode: mode,
|
||||
},
|
||||
};
|
||||
|
||||
iframeRef.current.contentWindow.postMessage(message, "*");
|
||||
}, [taskData, theme.theme, isIframeReady]);
|
||||
setIsVisible(true);
|
||||
}, [taskData, theme.theme, isIframeReady, mode, simProgress]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
if (!isIframeReady || !iframeRef.current?.contentWindow) return;
|
||||
const message = {
|
||||
type: "update",
|
||||
};
|
||||
|
||||
iframeRef.current.contentWindow.postMessage(message, "*");
|
||||
}
|
||||
}, [index]);
|
||||
|
||||
// Listen for messages from iframe
|
||||
useEffect(() => {
|
||||
@@ -65,7 +66,8 @@ export const IframePlugin: React.FC<IframePluginProps> = ({
|
||||
|
||||
// Handle plugin updates
|
||||
if (event.data.type === "plugin-update" && onUpdate) {
|
||||
onUpdate(event.data.data);
|
||||
console.log(event.data);
|
||||
onUpdate(event.data.data, event.data.qubits_needed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,10 +75,33 @@ export const IframePlugin: React.FC<IframePluginProps> = ({
|
||||
return () => window.removeEventListener("message", handler);
|
||||
}, [onUpdate]);
|
||||
|
||||
// Reset ready state when URL changes (new plugin or mode)
|
||||
useEffect(() => {
|
||||
setIsIframeReady(false);
|
||||
}, [pluginUrl, mode]);
|
||||
|
||||
return memoizedIframe;
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={!isIframeReady} />
|
||||
<div
|
||||
style={{
|
||||
visibility: isVisible ? "visible" : "hidden",
|
||||
flexGrow: "1",
|
||||
maxHeight: "100%",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
srcDoc={plugin}
|
||||
sandbox="allow-scripts allow-popups allow-forms"
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
border: "none",
|
||||
pointerEvents: mode == "List" ? "none" : "initial",
|
||||
}}
|
||||
name={`Ifame#${index}`}
|
||||
id={`Ifame#${index}`}
|
||||
></iframe>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
186
src/Api/QuantumBackend/ExperimentsManagment.tsx
Normal file
186
src/Api/QuantumBackend/ExperimentsManagment.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import keycloak from "Api/Keycloak/Keycloak";
|
||||
import axios from "axios";
|
||||
import type {
|
||||
CreateExperimentRequest,
|
||||
ExperimentData,
|
||||
ExperimentListResponse,
|
||||
CreateInstanceRequest,
|
||||
SimpleInstanceData,
|
||||
UpdateInstanceRequest,
|
||||
InstanceListResponse,
|
||||
InstanceData,
|
||||
StartExperimentRequest,
|
||||
UpdateExperimentRequest,
|
||||
CreateExperimentTypeResponse,
|
||||
ExperimentTypeList,
|
||||
} from "Types/Experiment/Experiment";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: `${import.meta.env.VITE_QUANTUM_BACKEND_URL}/experiment`,
|
||||
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;
|
||||
});
|
||||
|
||||
// ============= EXPERIMENT TYPE ENDPOINTS =============
|
||||
|
||||
// 1. Get all experiment types
|
||||
export const getExperimentTypes = async (): Promise<ExperimentTypeList[]> => {
|
||||
const response = await api.get<ExperimentTypeList[]>("/types");
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 2. Create experiment type (admin only, with file uploads)
|
||||
export const createExperimentType = async (
|
||||
name: string,
|
||||
fileFrontend: File,
|
||||
fileCompSystem: File,
|
||||
description?: string,
|
||||
): Promise<CreateExperimentTypeResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append("name", name);
|
||||
formData.append("file_frontend", fileFrontend);
|
||||
formData.append("file_comp_system", fileCompSystem);
|
||||
if (description) {
|
||||
formData.append("description", description);
|
||||
}
|
||||
|
||||
const response = await api.post<CreateExperimentTypeResponse>(
|
||||
"/types",
|
||||
formData,
|
||||
{
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 3. Get frontend HTML file for experiment type
|
||||
export const getFrontendFile = async (
|
||||
experiment_type_id: number,
|
||||
): Promise<string> => {
|
||||
const response = await api.get<string>("/types/frontend", {
|
||||
params: { experiment_type_id },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ============= EXPERIMENT ENDPOINTS =============
|
||||
|
||||
// 5. Create a new experiment
|
||||
export const createExperiment = async (
|
||||
data: CreateExperimentRequest,
|
||||
): Promise<ExperimentData> => {
|
||||
const response = await api.post<ExperimentData>("", data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 6. Get single experiment by ID
|
||||
export const getExperimentById = async (
|
||||
experiment_id: number,
|
||||
): Promise<ExperimentData> => {
|
||||
const response = await api.get<ExperimentData>("", {
|
||||
params: { experiment_id },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 7. Update experiment
|
||||
export const updateExperiment = async (
|
||||
data: UpdateExperimentRequest,
|
||||
): Promise<ExperimentData> => {
|
||||
const response = await api.put<ExperimentData>("", data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 8. Get user's experiments (with pagination)
|
||||
export const getUserExperiments = async ({
|
||||
page_num = 1,
|
||||
page_size = 10,
|
||||
}: {
|
||||
page_num?: number;
|
||||
page_size?: number;
|
||||
}): Promise<ExperimentListResponse> => {
|
||||
const response = await api.get<ExperimentListResponse>("/user", {
|
||||
params: { page_num, page_size },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 9. Delete experiment
|
||||
export const deleteExperiment = async (
|
||||
experiment_id: number,
|
||||
): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>("", {
|
||||
params: { experiment_id },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ============= INSTANCE ENDPOINTS =============
|
||||
|
||||
// 10. Create instance for an experiment
|
||||
export const createInstance = async (
|
||||
data: CreateInstanceRequest,
|
||||
): Promise<SimpleInstanceData> => {
|
||||
const response = await api.post<SimpleInstanceData>("/instance", data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 11. Update instance
|
||||
export const updateInstance = async (
|
||||
data: UpdateInstanceRequest,
|
||||
): Promise<SimpleInstanceData> => {
|
||||
const response = await api.put<SimpleInstanceData>("/instance", data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 12. Get all instances of an experiment (with pagination)
|
||||
export const getExperimentInstances = async (
|
||||
experiment_id: number,
|
||||
page_num: number = 1,
|
||||
page_size: number = 6,
|
||||
): Promise<InstanceListResponse> => {
|
||||
page_num = Math.max(page_num, 1);
|
||||
const response = await api.get<InstanceListResponse>("/instance", {
|
||||
params: { experiment_id, page_num, page_size },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 13. Get single instance by ID
|
||||
export const getInstanceById = async (
|
||||
instance_id: number,
|
||||
): Promise<InstanceData> => {
|
||||
const response = await api.get<InstanceData>("/instance/id", {
|
||||
params: { instance_id },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 14. Delete instance
|
||||
export const deleteInstance = async (
|
||||
instance_id: number,
|
||||
): Promise<{ message: string }> => {
|
||||
const response = await api.delete<{ message: string }>("/instance", {
|
||||
params: { instance_id },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ============= SIMULATION ENDPOINTS =============
|
||||
|
||||
// 15. Start experiment (run simulation)
|
||||
export const startExperiment = async (
|
||||
data: StartExperimentRequest,
|
||||
): Promise<200> => {
|
||||
const response = await api.post<200>("/start", data);
|
||||
return response.data;
|
||||
};
|
||||
@@ -25,25 +25,38 @@ export const GetCurrentUserInfo = async (): Promise<UserData | undefined> => {
|
||||
}
|
||||
};
|
||||
|
||||
// In Api/QuantumBackend/UserManagement.ts
|
||||
export const UpdateCurrentUserInfo = async (
|
||||
profile_picture_path: string,
|
||||
): Promise<UserData | undefined> => {
|
||||
const response = await axios.put(
|
||||
`${import.meta.env.VITE_QUANTUM_BACKEND_URL}/user`,
|
||||
{ profile_picture_path: profile_picture_path },
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${keycloak.token}`,
|
||||
file: File,
|
||||
): Promise<{ profile_picture_path: string } | undefined> => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_QUANTUM_BACKEND_URL}/user/upload`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${keycloak.token}`,
|
||||
},
|
||||
body: formData,
|
||||
},
|
||||
withCredentials: true, // Important: allows credentials in CORS
|
||||
},
|
||||
);
|
||||
if (response.status === 200) {
|
||||
return response.data;
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to upload profile picture");
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error uploading profile picture:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// Keep your existing GetCurrentUserInfo function
|
||||
|
||||
export const GetUserByEmail = async (
|
||||
email: string,
|
||||
): Promise<UserData | undefined> => {
|
||||
|
||||
@@ -35,8 +35,7 @@ function App() {
|
||||
keycloak.updateToken(60).catch((error) => {
|
||||
console.log(error);
|
||||
keycloak.logout({
|
||||
redirectUri:
|
||||
window.location.origin + "/" + import.meta.env.VITE_BASE_PATH,
|
||||
redirectUri: window.location.origin,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
display: flex;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
position: relative;
|
||||
overflow: "hidden";
|
||||
}
|
||||
|
||||
.colored:hover {
|
||||
:not(.button-Disabled).colored:hover {
|
||||
background-color: color;
|
||||
}
|
||||
|
||||
@@ -38,7 +40,7 @@
|
||||
--hover_color: color-mix(in srgb, var(--hovercolor) 20%, transparent);
|
||||
}
|
||||
|
||||
.outline:hover {
|
||||
:not(.button-Disabled).outline:hover {
|
||||
background: var(--hover_color);
|
||||
}
|
||||
|
||||
@@ -50,7 +52,7 @@
|
||||
--hover_color: var(--hovercolor);
|
||||
}
|
||||
|
||||
.color:hover {
|
||||
:not(.button-Disabled).color:hover {
|
||||
background-color: var(--hover_color);
|
||||
}
|
||||
|
||||
@@ -103,11 +105,10 @@
|
||||
--hover_color: color-mix(in srgb, var(--color) 20%, transparent);
|
||||
}
|
||||
|
||||
.subtle:hover {
|
||||
:not(.button-Disabled).subtle:hover {
|
||||
background-color: --hover_color;
|
||||
}
|
||||
|
||||
.button-Disabled {
|
||||
background-color: var(--hover_color);
|
||||
cursor: inherit;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,8 @@ function CustomButton({
|
||||
}: CustomButtonProps) {
|
||||
return (
|
||||
<UnstyledButton
|
||||
className={
|
||||
`colored ${style} ${color} textAlign-${textAlign} ` +
|
||||
(disabled ? "button-Disabled" : "")
|
||||
}
|
||||
className={`colored ${style} ${color} textAlign-${textAlign} +
|
||||
${disabled ? "button-Disabled" : ""}`}
|
||||
style={
|
||||
color !== "primary" &&
|
||||
color !== "secondary" &&
|
||||
@@ -55,6 +53,19 @@ function CustomButton({
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{disabled && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "-2px",
|
||||
right: "-2px",
|
||||
bottom: "-2px",
|
||||
top: "-2px",
|
||||
borderRadius: "5px",
|
||||
backgroundColor: "rgba(10, 10, 10, 0.5)",
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
{icon} {icon && <Space w="sm" />}
|
||||
<Text size={textSize}>{text}</Text>
|
||||
</UnstyledButton>
|
||||
|
||||
@@ -4,8 +4,7 @@ import { useLayoutStore } from "Stores/LayoutStore";
|
||||
import { Link } from "react-router";
|
||||
import { routes } from "Routes/Routes";
|
||||
|
||||
const logoUrl =
|
||||
window.location.origin + "/" + import.meta.env.VITE_BASE_PATH + "/bitmap.png";
|
||||
const logoUrl = window.location.origin + "/bitmap.png";
|
||||
|
||||
function Header() {
|
||||
const { is_navbar_open, set_navbar_open } = useLayoutStore();
|
||||
|
||||
@@ -18,8 +18,13 @@ import {
|
||||
IconUsers,
|
||||
type IconProps,
|
||||
} from "@tabler/icons-react";
|
||||
import { Link, useLocation } from "react-router";
|
||||
import { useState, type ForwardRefExoticComponent } from "react";
|
||||
import {
|
||||
Link,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
type NavigateFunction,
|
||||
} from "react-router";
|
||||
import { useEffect, useState, type ForwardRefExoticComponent } from "react";
|
||||
import { routes } from "Routes/Routes";
|
||||
import keycloak from "Api/Keycloak/Keycloak";
|
||||
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||
@@ -33,6 +38,7 @@ interface SubtleLinkButtonProps {
|
||||
text: string;
|
||||
color: string;
|
||||
selected?: boolean;
|
||||
nav: NavigateFunction;
|
||||
}
|
||||
|
||||
function SubtleLinkButton(props: SubtleLinkButtonProps) {
|
||||
@@ -44,16 +50,48 @@ function SubtleLinkButton(props: SubtleLinkButtonProps) {
|
||||
color={props.selected ? props.color : "contrast"}
|
||||
text={props.text}
|
||||
textAlign="left"
|
||||
onClick={() => {
|
||||
props.nav(props.link);
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function Sidebar() {
|
||||
const { profile, profile_picture_path } = useAuthenticationStore();
|
||||
const { profile, set_profile_picture_path, profile_picture_path } =
|
||||
useAuthenticationStore();
|
||||
const [open, set_open] = useState(false);
|
||||
const theme = useMantineTheme();
|
||||
const location = useLocation(); // get current URL
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!profile) return;
|
||||
|
||||
const fetchAvatar = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_QUANTUM_BACKEND_URL}/user/serve/${profile.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${keycloak.token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
set_profile_picture_path(url);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load avatar:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAvatar();
|
||||
}, [profile]);
|
||||
|
||||
return (
|
||||
<div className="sidebar">
|
||||
@@ -70,8 +108,7 @@ function Sidebar() {
|
||||
text="Подтвердить"
|
||||
onClick={() =>
|
||||
keycloak.logout({
|
||||
redirectUri:
|
||||
window.location.origin + "/" + import.meta.env.VITE_BASE_PATH,
|
||||
redirectUri: window.location.origin,
|
||||
})
|
||||
}
|
||||
textSize="lg"
|
||||
@@ -94,6 +131,7 @@ function Sidebar() {
|
||||
text="Эксперименты"
|
||||
color={theme.colors.teal[7]}
|
||||
selected={location.pathname.startsWith(routes.ExperimentsPage.path)}
|
||||
nav={navigate}
|
||||
/>
|
||||
<SubtleLinkButton
|
||||
link={routes.MachinesPage.path}
|
||||
@@ -101,6 +139,7 @@ function Sidebar() {
|
||||
text="Вычислительные системы"
|
||||
color={theme.colors.violet[7]}
|
||||
selected={location.pathname.startsWith(routes.MachinesPage.path)}
|
||||
nav={navigate}
|
||||
/>
|
||||
<SubtleLinkButton
|
||||
link={routes.TeamsPage.path}
|
||||
@@ -108,6 +147,7 @@ function Sidebar() {
|
||||
text="Команды"
|
||||
color={theme.colors.grape[7]}
|
||||
selected={location.pathname.startsWith(routes.TeamsPage.path)}
|
||||
nav={navigate}
|
||||
/>
|
||||
<SubtleLinkButton
|
||||
link={routes.DocumentationPage.path}
|
||||
@@ -115,6 +155,7 @@ function Sidebar() {
|
||||
text="Документация"
|
||||
color={theme.colors.blue[7]}
|
||||
selected={location.pathname.startsWith(routes.DocumentationPage.path)}
|
||||
nav={navigate}
|
||||
/>
|
||||
</div>
|
||||
<div className="sidebar_bottom">
|
||||
@@ -134,15 +175,16 @@ function Sidebar() {
|
||||
style={{ width: "100%", height: "5px", margin: "5px" }}
|
||||
/>
|
||||
<div className="sidebar_bottom_bottom">
|
||||
<Link to={routes.SettingsPage.path} className="invisible_link">
|
||||
<CustomButton
|
||||
style="subtle"
|
||||
icon={<IconSettings2 size={26} />}
|
||||
text="Настройки"
|
||||
color="contrast"
|
||||
textSize="lg"
|
||||
/>
|
||||
</Link>
|
||||
<CustomButton
|
||||
style="subtle"
|
||||
icon={<IconSettings2 size={26} />}
|
||||
text="Настройки"
|
||||
color="contrast"
|
||||
textSize="lg"
|
||||
onClick={() => {
|
||||
navigate(routes.SettingsPage.path);
|
||||
}}
|
||||
/>
|
||||
<CustomButton
|
||||
style="subtle"
|
||||
icon={<IconLogout size={26} />}
|
||||
|
||||
@@ -1,24 +1,45 @@
|
||||
import { Card, Pill, SimpleGrid, Text, UnstyledButton } from "@mantine/core";
|
||||
import "./ExperimentsListCard.css";
|
||||
import { useNavigate } from "react-router";
|
||||
import type { Experiment, TaskData } from "Types/Experiment/Experiment";
|
||||
import type {
|
||||
ExperimentData,
|
||||
SimpleInstanceData,
|
||||
} from "Types/Experiment/Experiment";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
import type { MouseEvent } from "react";
|
||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||
import { deleteExperiment } from "Api/QuantumBackend/ExperimentsManagment";
|
||||
|
||||
function ExperimentsListCard(props: {
|
||||
experiment: Experiment;
|
||||
team: { team_id: number; team_name: string } | undefined;
|
||||
}) {
|
||||
function ExperimentsListCard(props: { experiment: ExperimentData }) {
|
||||
const navigate = useNavigate();
|
||||
const { removeExperiment, tasks } = useExperimentStore();
|
||||
const { removeExperiment } = useExperimentStore();
|
||||
|
||||
const experiment_tasks = tasks.filter(
|
||||
(a) => a.id in props.experiment.tasks_ids,
|
||||
);
|
||||
const getStatusColor = (status: string) => {
|
||||
const statusLower = status;
|
||||
|
||||
switch (statusLower) {
|
||||
case "draft":
|
||||
return "var(--mantine-color-gray-5)";
|
||||
case "in queue":
|
||||
return "var(--mantine-color-blue-5)";
|
||||
case "processing":
|
||||
case "running":
|
||||
return "var(--mantine-color-yellow-5)";
|
||||
case "complete":
|
||||
return "var(--mantine-color-green-5)";
|
||||
case "error":
|
||||
case "complete with error":
|
||||
case "failed":
|
||||
return "var(--mantine-color-red-5)";
|
||||
default:
|
||||
return "var(--mantine-color-gray-5)";
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
removeExperiment(props.experiment.id);
|
||||
deleteExperiment(props.experiment.id).then(() => {
|
||||
removeExperiment(props.experiment.id);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -34,30 +55,38 @@ function ExperimentsListCard(props: {
|
||||
Эксперимент: {props.experiment.name}
|
||||
</Text>
|
||||
<Text mb="sm" size="md">
|
||||
Команда: {props.team?.team_name}
|
||||
Команда: {props.experiment.team?.team_name}
|
||||
</Text>
|
||||
<div style={{ display: "flex", gap: "10px" }}>
|
||||
<Text size="md">Статус: </Text>
|
||||
<Pill style={{ backgroundColor: "var(--mantine-color-yellow-5)" }}>
|
||||
<Text size="md">{props.experiment.experiment_status}</Text>
|
||||
<Pill
|
||||
style={{
|
||||
backgroundColor: getStatusColor(
|
||||
props.experiment.status != undefined
|
||||
? props.experiment.status.toLowerCase()
|
||||
: "DRAFT",
|
||||
),
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
<Text size="md">{props.experiment.status}</Text>
|
||||
</Pill>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ExperimentSectionWithLine">
|
||||
<SimpleGrid cols={2} verticalSpacing="0px">
|
||||
<Text>Задачи:</Text>
|
||||
<SimpleGrid cols={2} verticalSpacing="0px" style={{ rowGap: "4px" }}>
|
||||
<Text size="sm">Задачи:</Text>
|
||||
|
||||
{experiment_tasks.map(
|
||||
{props.experiment.instance_preview.slice(0, 6).map(
|
||||
(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
task: TaskData<any>,
|
||||
instance: SimpleInstanceData,
|
||||
) => {
|
||||
return (
|
||||
<Pill
|
||||
size="md"
|
||||
size="sm"
|
||||
className="ExperimentPill"
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
border: "2px solid black",
|
||||
alignContent: "center",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
@@ -65,40 +94,50 @@ function ExperimentsListCard(props: {
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
size="md"
|
||||
size="sm"
|
||||
style={{
|
||||
textWrap: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{task.name}
|
||||
{instance.name}
|
||||
</Text>
|
||||
</Pill>
|
||||
);
|
||||
},
|
||||
)}
|
||||
{experiment_tasks.length == 0 ? (
|
||||
<Pill size="md" className="ExperimentPill2">
|
||||
{props.experiment.instances_count == 0 ? (
|
||||
<Pill size="sm" className="ExperimentPill2">
|
||||
Нет Задач
|
||||
</Pill>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{props.experiment.instances_count >
|
||||
props.experiment.instance_preview.length ? (
|
||||
<Pill size="sm" className="ExperimentPill2">
|
||||
и еще{" "}
|
||||
{props.experiment.instances_count -
|
||||
props.experiment.instance_preview.length}
|
||||
</Pill>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
<div className="RightExperimentSection">
|
||||
<div className="TopRightContainer">
|
||||
<div className="dateContainer">
|
||||
<Text size="sm">
|
||||
{new Date(
|
||||
props.experiment.date_created + "Z",
|
||||
).toLocaleDateString("ru")}{" "}
|
||||
{new Date(props.experiment.created_at + "Z").toLocaleDateString(
|
||||
"ru",
|
||||
)}{" "}
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
{new Date(
|
||||
props.experiment.date_created + "Z",
|
||||
).toLocaleTimeString("ru")}
|
||||
{new Date(props.experiment.created_at + "Z").toLocaleTimeString(
|
||||
"ru",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<UnstyledButton
|
||||
@@ -122,7 +161,7 @@ function ExperimentsListCard(props: {
|
||||
>
|
||||
<Text> Тип эксперимента: </Text>
|
||||
<Pill className="ExperimentPill">
|
||||
{props.experiment.experiment_type}
|
||||
{props.experiment.experiment_type.name}
|
||||
</Pill>
|
||||
</div>
|
||||
<Text c="dimmed" size="sm">
|
||||
|
||||
@@ -64,7 +64,8 @@ function MachinesListCard(props: SystemWithTeams) {
|
||||
//className="MachinePill"
|
||||
style={{
|
||||
marginLeft: "10px",
|
||||
backgroundColor: colors[props.system.status || "ONLINE"],
|
||||
backgroundColor:
|
||||
colors[props.system.status as "ONLINE" | "OFFLINE" | "BUSY"],
|
||||
}}
|
||||
>
|
||||
<Text size="md">{props.system.status}</Text>
|
||||
@@ -82,12 +83,12 @@ function MachinesListCard(props: SystemWithTeams) {
|
||||
</Text>
|
||||
<div style={{ display: "flex", gap: "10px", justifyContent: "right" }}>
|
||||
<Text size="sm">
|
||||
{new Date(props.system.created_at + "Z").toLocaleDateString(
|
||||
{new Date(props.system.last_updated + "Z").toLocaleDateString(
|
||||
"ru",
|
||||
)}{" "}
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
{new Date(props.system.created_at + "Z").toLocaleTimeString("ru")}
|
||||
{new Date(props.system.last_updated + "Z").toLocaleTimeString("ru")}
|
||||
</Text>
|
||||
</div>
|
||||
</Card.Section>
|
||||
|
||||
28
src/Components/ListCard/TaskListCard/TaskListCard.css
Normal file
28
src/Components/ListCard/TaskListCard/TaskListCard.css
Normal file
@@ -0,0 +1,28 @@
|
||||
.InstancesListCard {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.InstanceSectionWithLine {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.InstanceIframeSection {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.DatesContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.RightInstanceSection {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 275px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.MachinePill {
|
||||
background-color: var(--mantine-color-contrast-filled);
|
||||
color: var(--mantine-color-primary-filled);
|
||||
}
|
||||
176
src/Components/ListCard/TaskListCard/TaskListCard.tsx
Normal file
176
src/Components/ListCard/TaskListCard/TaskListCard.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import { Card, Pill, Text, UnstyledButton } from "@mantine/core";
|
||||
import { useNavigate } from "react-router";
|
||||
import type { InstanceData } from "Types/Experiment/Experiment";
|
||||
import { IconCheck, IconTrash } from "@tabler/icons-react";
|
||||
import type { MouseEvent } from "react";
|
||||
import { IframePlugin } from "Api/PluginLoader/PluginLoader";
|
||||
import "./TaskListCard.css";
|
||||
import { deleteInstance } from "Api/QuantumBackend/ExperimentsManagment";
|
||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
function InstancesListCard(props: { instance: InstanceData; plugin: string }) {
|
||||
const navigate = useNavigate();
|
||||
const { removeInstance } = useExperimentStore();
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const statusLower = status.toLowerCase();
|
||||
|
||||
switch (statusLower) {
|
||||
case "draft":
|
||||
return "var(--mantine-color-gray-5)";
|
||||
case "in queue":
|
||||
return "var(--mantine-color-blue-5)";
|
||||
case "processing":
|
||||
case "running":
|
||||
return "var(--mantine-color-yellow-5)";
|
||||
case "complete":
|
||||
case "completed":
|
||||
return "var(--mantine-color-green-5)";
|
||||
case "error":
|
||||
case "failed":
|
||||
return "var(--mantine-color-red-5)";
|
||||
default:
|
||||
return "var(--mantine-color-gray-5)";
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteInstance(props.instance.instance_id).then(() => {
|
||||
removeInstance(props.instance.instance_id);
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Задача удалена успешно",
|
||||
message: "",
|
||||
icon: <IconCheck />,
|
||||
style: { paddingLeft: "5px" },
|
||||
});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Card
|
||||
className="InstancesListCard"
|
||||
orientation="horizontal"
|
||||
onClick={(ev) => {
|
||||
console.log("A");
|
||||
ev.stopPropagation();
|
||||
navigate(props.instance.instance_id.toString());
|
||||
}}
|
||||
>
|
||||
<Card.Section
|
||||
withBorder
|
||||
inheritPadding
|
||||
px="md"
|
||||
className="InstanceSectionWithLine"
|
||||
>
|
||||
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
|
||||
Задача: {props.instance.name}
|
||||
</Text>
|
||||
<div style={{ display: "flex", gap: "10px" }}>
|
||||
<Text size="md">Статус: </Text>
|
||||
<Pill
|
||||
style={{
|
||||
backgroundColor: getStatusColor(
|
||||
props.instance.simulation_result?.status || "",
|
||||
),
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
<Text size="md">
|
||||
{props.instance.simulation_result?.status || "DRAFT"}
|
||||
</Text>
|
||||
</Pill>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "10px" }}>
|
||||
<Text size="md">ВС: </Text>
|
||||
<Pill className="MachinePill">
|
||||
<Text size="md">
|
||||
{props.instance.simulation_result?.comp_system?.system_name ||
|
||||
"--"}
|
||||
</Text>
|
||||
</Pill>
|
||||
</div>
|
||||
</Card.Section>
|
||||
<Card.Section withBorder className="InstanceIframeSection">
|
||||
{JSON.stringify(props.instance.instance_data) && (
|
||||
<IframePlugin
|
||||
plugin={props.plugin}
|
||||
mode="List"
|
||||
taskData={JSON.stringify(props.instance.instance_data)}
|
||||
index={props.instance.instance_id}
|
||||
qubits_needed={0}
|
||||
simProgress={JSON.stringify(
|
||||
props.instance.simulation_result?.simulation_result,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Card.Section>
|
||||
<Card.Section className="RightInstanceSection" inheritPadding px="sm">
|
||||
<div className="DatesContainer">
|
||||
<div className="dateContainer">
|
||||
<Text size="sm">Время начала:</Text>
|
||||
{props.instance.simulation_result ? (
|
||||
<>
|
||||
<Text size="sm">
|
||||
{new Date(
|
||||
props.instance.simulation_result?.started_at + "Z",
|
||||
).toLocaleDateString("ru")}{" "}
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
{new Date(
|
||||
props.instance.simulation_result?.started_at + "Z",
|
||||
).toLocaleTimeString("ru")}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text size="sm">--</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="dateContainer">
|
||||
<Text size="sm">Время окончания:</Text>
|
||||
{props.instance.simulation_result?.ended_at ? (
|
||||
<>
|
||||
<Text size="sm">
|
||||
{new Date(
|
||||
props.instance.simulation_result?.ended_at + "Z",
|
||||
).toLocaleDateString("ru")}{" "}
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
{new Date(
|
||||
props.instance.simulation_result?.ended_at + "Z",
|
||||
).toLocaleTimeString("ru")}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text size="sm">--</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
paddingLeft: "10px",
|
||||
}}
|
||||
>
|
||||
<UnstyledButton
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
handleDelete();
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</UnstyledButton>
|
||||
|
||||
<Text c="dimmed" size="sm">
|
||||
#{props.instance.instance_id}
|
||||
</Text>
|
||||
</div>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default InstancesListCard;
|
||||
@@ -1,16 +1,26 @@
|
||||
import { Card, Text, UnstyledButton } from "@mantine/core";
|
||||
import { IconCancel, IconCheck, IconTrash } from "@tabler/icons-react";
|
||||
import type { MouseEvent } from "react";
|
||||
import { Card, NumberInput, Text, UnstyledButton } from "@mantine/core";
|
||||
import {
|
||||
IconCancel,
|
||||
IconCheck,
|
||||
IconPencil,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import { useState, type MouseEvent } from "react";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import type { SystemTeamData, SystemWithTeams } from "Types/Machine/Machine";
|
||||
import { removeSystemFromTeam } from "Api/QuantumBackend/MachineManagment";
|
||||
import {
|
||||
giveSystemToTeam,
|
||||
removeSystemFromTeam,
|
||||
} from "Api/QuantumBackend/MachineManagment";
|
||||
import { useDeviceStore } from "Stores/DeviceStore";
|
||||
|
||||
function TeamInMachineListCard(props: {
|
||||
team: SystemTeamData;
|
||||
system: SystemWithTeams;
|
||||
}) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const { updateDevice } = useDeviceStore();
|
||||
const [count, setCount] = useState(props.team.num_qubits);
|
||||
|
||||
const handleRemovePerm = () => {
|
||||
// TODO: fix delete
|
||||
@@ -76,7 +86,69 @@ function TeamInMachineListCard(props: {
|
||||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
<Text>Количество кубит: {props.team.num_qubits}</Text>
|
||||
{isEditing && (
|
||||
<>
|
||||
<Text>Количество кубит: </Text>
|
||||
<NumberInput
|
||||
value={count}
|
||||
onChange={(ev) => {
|
||||
setCount(Number(ev.valueOf()));
|
||||
}}
|
||||
></NumberInput>
|
||||
<UnstyledButton
|
||||
onClick={() => {
|
||||
giveSystemToTeam({
|
||||
system_id: props.system.system.id,
|
||||
team_id: props.team.team.team_id,
|
||||
qubits_given: count,
|
||||
}).then(() => {
|
||||
const team = props.system.teams.find((t) => {
|
||||
return t.team.team_id == props.team.team.team_id;
|
||||
});
|
||||
if (team) {
|
||||
const updatedItem = { ...team, num_qubits: count };
|
||||
|
||||
// Create new array with the updated item in the same position
|
||||
const updatedItems = props.system.teams.map(
|
||||
(currentItem) =>
|
||||
currentItem.team.team_id === team.team.team_id
|
||||
? updatedItem
|
||||
: currentItem,
|
||||
);
|
||||
|
||||
updateDevice(props.system.system.id, {
|
||||
teams: updatedItems,
|
||||
});
|
||||
}
|
||||
|
||||
setIsEditing(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconCheck size={25} />
|
||||
</UnstyledButton>
|
||||
<UnstyledButton
|
||||
onClick={() => {
|
||||
setCount(props.team.num_qubits);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
>
|
||||
<IconCancel size={25} />
|
||||
</UnstyledButton>
|
||||
</>
|
||||
)}
|
||||
{!isEditing && (
|
||||
<>
|
||||
<Text>Количество кубит: {count}</Text>
|
||||
<UnstyledButton
|
||||
onClick={() => {
|
||||
setIsEditing(true);
|
||||
}}
|
||||
>
|
||||
<IconPencil size={25} />
|
||||
</UnstyledButton>
|
||||
</>
|
||||
)}
|
||||
</Card.Section>
|
||||
<Card.Section
|
||||
inheritPadding
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconCancel,
|
||||
IconCheck,
|
||||
IconCrown,
|
||||
IconForbid,
|
||||
@@ -196,23 +197,35 @@ function TeamMemberCard(props: { cur_team: Team; member: TeamMember }) {
|
||||
deleteMember({
|
||||
team_id: props.cur_team.id,
|
||||
user_id: props.member.user.keycloak_id,
|
||||
}).then(() => {
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Пользователь удален успешно",
|
||||
message: "",
|
||||
icon: <IconCheck />,
|
||||
style: { paddingLeft: "5px" },
|
||||
})
|
||||
.then(() => {
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Пользователь удален успешно",
|
||||
message: "",
|
||||
icon: <IconCheck />,
|
||||
style: { paddingLeft: "5px" },
|
||||
});
|
||||
updateTeam(props.cur_team.id, {
|
||||
members: [
|
||||
...props.cur_team.members.filter(
|
||||
(member) =>
|
||||
member.user.keycloak_id !=
|
||||
props.member.user.keycloak_id,
|
||||
),
|
||||
],
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Пользователя не получилось удалить",
|
||||
message: "",
|
||||
color: "red",
|
||||
icon: <IconCancel />,
|
||||
style: { paddingLeft: "5px" },
|
||||
});
|
||||
});
|
||||
updateTeam(props.cur_team.id, {
|
||||
members: [
|
||||
...props.cur_team.members.filter(
|
||||
(member) =>
|
||||
member.user.keycloak_id != props.member.user.keycloak_id,
|
||||
),
|
||||
],
|
||||
});
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
|
||||
@@ -11,6 +11,8 @@ import CustomButton from "Components/CustomButton/CustomButton";
|
||||
import { useDeviceStore } from "Stores/DeviceStore";
|
||||
import type { SystemWithTeams } from "Types/Machine/Machine";
|
||||
import { giveSystemToTeam } from "Api/QuantumBackend/MachineManagment";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { IconCancel } from "@tabler/icons-react";
|
||||
|
||||
interface AddTeamToDeviceProps {
|
||||
isOpened: boolean;
|
||||
@@ -20,7 +22,7 @@ interface AddTeamToDeviceProps {
|
||||
}
|
||||
|
||||
export function AddTeamToDevice(props: AddTeamToDeviceProps) {
|
||||
const [numQubits, setNumQubits] = useState(0);
|
||||
const [numQubits, setNumQubits] = useState(1);
|
||||
const { updateDevice } = useDeviceStore();
|
||||
const [selectedTeam, setSelectedTeam] = useState<{
|
||||
label: string;
|
||||
@@ -29,7 +31,7 @@ export function AddTeamToDevice(props: AddTeamToDeviceProps) {
|
||||
//reset on open dialog
|
||||
useEffect(() => {
|
||||
if (props.isOpened) {
|
||||
setNumQubits(0);
|
||||
setNumQubits(1);
|
||||
setSelectedTeam(undefined);
|
||||
}
|
||||
}, [props.isOpened]);
|
||||
@@ -63,14 +65,23 @@ export function AddTeamToDevice(props: AddTeamToDeviceProps) {
|
||||
});
|
||||
props.setIsOpened(false);
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => {
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Не удалось предоставить доступ",
|
||||
message: "",
|
||||
color: "red",
|
||||
icon: <IconCancel />,
|
||||
style: { paddingLeft: "5px" },
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={props.isOpened}
|
||||
onClose={handleClose}
|
||||
title=<Title size="xl">Добавить члена команды</Title>
|
||||
title=<Title size="xl">Предоставить команде права на ВС</Title>
|
||||
centered
|
||||
size="75%"
|
||||
styles={{
|
||||
@@ -98,11 +109,13 @@ export function AddTeamToDevice(props: AddTeamToDeviceProps) {
|
||||
/>
|
||||
<NumberInput
|
||||
value={numQubits}
|
||||
label="Количество кубит"
|
||||
label={`Количество кубит (Макс ${props.device.system.max_qubits})`}
|
||||
required
|
||||
onChange={(event) => {
|
||||
setNumQubits(Number(event.valueOf()));
|
||||
}}
|
||||
max={props.device.system.max_qubits}
|
||||
min={1}
|
||||
></NumberInput>
|
||||
<Space h="md" />
|
||||
<Center>
|
||||
|
||||
@@ -9,23 +9,33 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import "./NewExperiment.css";
|
||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||
//import { useExperimentStore } from "Stores/ExperimentStore";
|
||||
import CustomButton from "Components/CustomButton/CustomButton";
|
||||
import { createExperiment } from "Api/QuantumBackend/ExperimentsManagment";
|
||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||
|
||||
interface NewExperimentModalProps {
|
||||
isOpened: boolean;
|
||||
setIsOpened: (opened: boolean) => void;
|
||||
teams: { team_id: number; team_name: string }[] | undefined;
|
||||
types: { type_id: number; type_name: string }[] | undefined;
|
||||
}
|
||||
|
||||
export function NewExperimentModal(props: NewExperimentModalProps) {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const { addExperiment } = useExperimentStore();
|
||||
//const { addExperiment } = useExperimentStore();
|
||||
const [selectedTeam, setSelectedTeam] = useState<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>();
|
||||
|
||||
const [selectedType, setSelectedType] = useState<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>();
|
||||
|
||||
const { addExperiment } = useExperimentStore();
|
||||
//reset on open dialog
|
||||
useEffect(() => {
|
||||
if (props.isOpened) {
|
||||
@@ -41,18 +51,16 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
|
||||
|
||||
const handleCreateExperiment = () => {
|
||||
//TODO: add logic for backend server
|
||||
if (selectedTeam) {
|
||||
addExperiment({
|
||||
id: 1,
|
||||
if (selectedTeam && selectedType) {
|
||||
createExperiment({
|
||||
team_id: Number(selectedTeam.value),
|
||||
name: name,
|
||||
description: description,
|
||||
team_id: Number(selectedTeam?.value),
|
||||
tasks_ids: [1, 2],
|
||||
date_created: new Date(),
|
||||
experiment_status: "PROCESSING",
|
||||
experiment_type: "VQE",
|
||||
experiment_type_id: Number(selectedType.value),
|
||||
}).then((exp) => {
|
||||
addExperiment(exp);
|
||||
props.setIsOpened(false);
|
||||
});
|
||||
props.setIsOpened(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -109,9 +117,32 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
|
||||
onChange={(_value, option) => setSelectedTeam(option)}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Select
|
||||
value={selectedType?.value}
|
||||
label="Тип эксперимента"
|
||||
placeholder="Выберите тип эксперимента"
|
||||
searchable
|
||||
required
|
||||
data={
|
||||
props.types
|
||||
? props.types.map((team) => {
|
||||
return {
|
||||
value: team.type_id.toString(),
|
||||
label: `${team.type_name} (#${team.type_id})`,
|
||||
};
|
||||
})
|
||||
: []
|
||||
}
|
||||
onChange={(_value, option) => setSelectedType(option)}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Center>
|
||||
<CustomButton
|
||||
disabled={name != "" && selectedTeam ? false : true}
|
||||
disabled={
|
||||
name == "" ||
|
||||
(selectedTeam ? false : true) ||
|
||||
(selectedType ? false : true)
|
||||
}
|
||||
color="contrast"
|
||||
onClick={handleCreateExperiment}
|
||||
text="Создать эксперимент"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.StepperRoot {
|
||||
flex-direction: row-reverse !important;
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Modal,
|
||||
Space,
|
||||
Stepper,
|
||||
Title,
|
||||
Text,
|
||||
Flex,
|
||||
Select,
|
||||
Textarea,
|
||||
} from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import "./NewMolecule.css";
|
||||
import {
|
||||
IconArchiveFilled,
|
||||
IconArticleFilled,
|
||||
IconFileFilled,
|
||||
} from "@tabler/icons-react";
|
||||
import {
|
||||
ConvertMoleculeToStandart,
|
||||
GetInFormats,
|
||||
} from "Api/ConvertBackendCalls";
|
||||
import { AxiosError } from "axios";
|
||||
import CustomButton from "Components/CustomButton/CustomButton";
|
||||
|
||||
interface NewMoleculeModalProps {
|
||||
isOpened: boolean;
|
||||
setIsOpened: (opened: boolean) => void;
|
||||
}
|
||||
|
||||
export function NewMoleculeModal(props: NewMoleculeModalProps) {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [selectedMethod, setSelectedMethod] = useState(0);
|
||||
|
||||
const [inMolecule, setInMolecule] = useState("");
|
||||
const [inFormat, setInFormat] = useState<string | null>();
|
||||
|
||||
const [inOptions, setInOptions] = useState<string[] | undefined>();
|
||||
|
||||
const getOptions = () => {
|
||||
GetInFormats().then((data: { [key: string]: string } | AxiosError) => {
|
||||
if (data instanceof AxiosError) {
|
||||
//TODO: handle Error
|
||||
} else {
|
||||
const values = Object.keys(data);
|
||||
setInOptions(values);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getOptions();
|
||||
}, []);
|
||||
|
||||
//reset on open dialog
|
||||
useEffect(() => {
|
||||
if (props.isOpened) {
|
||||
setActiveStep(0);
|
||||
setSelectedMethod(0);
|
||||
setInMolecule("");
|
||||
setInFormat(undefined);
|
||||
if (inOptions == undefined) {
|
||||
getOptions();
|
||||
}
|
||||
}
|
||||
}, [props.isOpened]);
|
||||
|
||||
const handleClose = () => {
|
||||
props.setIsOpened(false);
|
||||
};
|
||||
|
||||
const handleConvert = () => {
|
||||
if (inFormat) {
|
||||
ConvertMoleculeToStandart({
|
||||
inputText: inMolecule,
|
||||
inputFormat: inFormat,
|
||||
make_3d: true,
|
||||
add_h: false,
|
||||
optimize: false,
|
||||
}).then((data: string | AxiosError) => {
|
||||
if (data instanceof AxiosError) {
|
||||
//TODO: handle Error
|
||||
} else {
|
||||
setCurrentMoleculeString(data);
|
||||
props.setIsOpened(false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
//TODO: add no format selected handling
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmptyMolecule = () => {
|
||||
props.setIsOpened(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={props.isOpened}
|
||||
onClose={handleClose}
|
||||
title=<Title size="xl">Новая молекула</Title>
|
||||
centered
|
||||
size="75%"
|
||||
styles={{
|
||||
content: { paddingLeft: "10px" },
|
||||
title: { width: "100%" },
|
||||
}}
|
||||
>
|
||||
<Stepper
|
||||
active={activeStep}
|
||||
onStepClick={setActiveStep}
|
||||
allowNextStepsSelect={false}
|
||||
classNames={{ root: "StepperRoot" }}
|
||||
styles={{
|
||||
steps: {
|
||||
paddingTop: "30px",
|
||||
paddingBottom: "30px",
|
||||
paddingLeft: "20px",
|
||||
minWidth: "0px",
|
||||
alignSelf: "center",
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
}}
|
||||
style={{ display: "flex", flexDirection: "row" }}
|
||||
orientation="vertical"
|
||||
>
|
||||
<Stepper.Step>
|
||||
<div style={{ width: "100%", textAlign: "center" }}>
|
||||
<Title size="lg">Шаг 1: Способ добавления молекулы</Title>
|
||||
<Space h="lg" />
|
||||
<Flex justify="center" gap="md" wrap="wrap">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSelectedMethod(1);
|
||||
handleEmptyMolecule();
|
||||
}}
|
||||
style={{ height: "100px", flexGrow: "1" }}
|
||||
size="lg"
|
||||
>
|
||||
<Flex direction="row" gap="lg">
|
||||
<IconFileFilled size={40} />
|
||||
<Center>
|
||||
<Text>Пустая молекула</Text>
|
||||
</Center>
|
||||
</Flex>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSelectedMethod(2);
|
||||
setActiveStep(1);
|
||||
}}
|
||||
style={{ height: "100px", flexGrow: "1" }}
|
||||
>
|
||||
<Flex direction="row" gap="lg">
|
||||
<IconArchiveFilled size="40" />
|
||||
<Center>
|
||||
<Text>Из файла / Архива</Text>
|
||||
</Center>
|
||||
</Flex>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSelectedMethod(3);
|
||||
setActiveStep(1);
|
||||
}}
|
||||
style={{ height: "100px", flexGrow: "1" }}
|
||||
>
|
||||
<Flex direction="row" gap="lg">
|
||||
<IconArticleFilled size={40} />
|
||||
<Center>
|
||||
<Text>Другой формат</Text>
|
||||
</Center>
|
||||
</Flex>
|
||||
</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
</Stepper.Step>
|
||||
<Stepper.Step>
|
||||
<div style={{ width: "100%", textAlign: "center" }}>
|
||||
{selectedMethod == 2 && (
|
||||
<div>
|
||||
<Title size="lg">Шаг 2: Выбор файла</Title>
|
||||
<Space h="lg" />
|
||||
</div>
|
||||
)}
|
||||
{selectedMethod == 3 && (
|
||||
<div>
|
||||
<Title size="md">Шаг 2: Ввод молекулы</Title>
|
||||
<Space h="lg" />
|
||||
<Textarea
|
||||
className="moleculeInput"
|
||||
value={inMolecule}
|
||||
onChange={(e) => setInMolecule(e.currentTarget.value)}
|
||||
placeholder="Введите текст молекулы"
|
||||
classNames={{
|
||||
wrapper: "moleculeInputWrapper",
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Stop arrow keys from reaching react-resizable-panels
|
||||
if (
|
||||
[
|
||||
"ArrowUp",
|
||||
"ArrowDown",
|
||||
"ArrowLeft",
|
||||
"ArrowRight",
|
||||
].includes(e.key)
|
||||
) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Select
|
||||
value={inFormat}
|
||||
onChange={(value: string | null) => setInFormat(value)}
|
||||
searchable
|
||||
data={inOptions}
|
||||
placeholder="Выберите формат"
|
||||
classNames={{
|
||||
input: "selectInFormat",
|
||||
dropdown: "selectDropDown",
|
||||
option: "selectDropDownOption",
|
||||
}}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<CustomButton
|
||||
color="accent"
|
||||
onClick={handleConvert}
|
||||
text="Преобразовать"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Stepper.Step>
|
||||
</Stepper>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
0
src/Modals/NewTask/NewTask.css
Normal file
0
src/Modals/NewTask/NewTask.css
Normal file
127
src/Modals/NewTask/NewTask.tsx
Normal file
127
src/Modals/NewTask/NewTask.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
Center,
|
||||
Modal,
|
||||
Space,
|
||||
Textarea,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import "./NewTask.css";
|
||||
import CustomButton from "Components/CustomButton/CustomButton";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||
import { createInstance } from "Api/QuantumBackend/ExperimentsManagment";
|
||||
|
||||
interface NewInstanceModalProps {
|
||||
isOpened: boolean;
|
||||
setIsOpened: (opened: boolean) => void;
|
||||
experiment_id: number;
|
||||
}
|
||||
|
||||
export function NewInstanceModal(props: NewInstanceModalProps) {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const { setInstances, instances } = useExperimentStore();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
//reset on open dialog
|
||||
useEffect(() => {
|
||||
if (props.isOpened) {
|
||||
setName("");
|
||||
setDescription("");
|
||||
}
|
||||
}, [props.isOpened]);
|
||||
|
||||
const handleClose = () => {
|
||||
props.setIsOpened(false);
|
||||
};
|
||||
|
||||
const handleCreateTeam = () => {
|
||||
//TODO: add logic for backend server
|
||||
//
|
||||
setIsLoading(true);
|
||||
createInstance({
|
||||
experiment_id: props.experiment_id,
|
||||
name: name,
|
||||
description: description,
|
||||
instance_data: "{}",
|
||||
}).then((instance) => {
|
||||
setIsLoading(false);
|
||||
if (instance) {
|
||||
setInstances([
|
||||
{
|
||||
instance_id: instance.id,
|
||||
name: instance.name,
|
||||
instance_data: "{}",
|
||||
description: instance.description,
|
||||
qubits_needed: 0,
|
||||
},
|
||||
...instances,
|
||||
]);
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Задача создана успешно",
|
||||
message: "",
|
||||
icon: <IconCheck />,
|
||||
style: { paddingLeft: "5px" },
|
||||
});
|
||||
props.setIsOpened(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={props.isOpened}
|
||||
onClose={handleClose}
|
||||
title=<Title size="xl">Новая Задача</Title>
|
||||
centered
|
||||
size="75%"
|
||||
styles={{
|
||||
content: { paddingLeft: "10px" },
|
||||
title: { width: "100%" },
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
value={name}
|
||||
label="Имя задач"
|
||||
placeholder="Введите имя задачи"
|
||||
required
|
||||
onChange={(event) => {
|
||||
setName(event.currentTarget.value);
|
||||
}}
|
||||
></TextInput>
|
||||
<Space h="md" />
|
||||
<Textarea
|
||||
value={description}
|
||||
label="Описание задачи"
|
||||
placeholder="Введите описание задачи"
|
||||
minRows={4}
|
||||
maxRows={10}
|
||||
autosize
|
||||
onChange={(event) => {
|
||||
setDescription(event.currentTarget.value);
|
||||
}}
|
||||
></Textarea>
|
||||
<Space h="md" />
|
||||
<Center>
|
||||
<CustomButton
|
||||
disabled={(name != "" ? false : true) || isLoading}
|
||||
color="contrast"
|
||||
onClick={handleCreateTeam}
|
||||
text="Создать Задачу"
|
||||
></CustomButton>
|
||||
<Space w="md" />
|
||||
<CustomButton
|
||||
color="contrast"
|
||||
style="outline"
|
||||
onClick={() => {
|
||||
props.setIsOpened(false);
|
||||
}}
|
||||
text="Отменить"
|
||||
></CustomButton>
|
||||
</Center>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,12 @@ import TeamInMachineListCard from "Components/ListCard/TeamListCard/TeamInMachin
|
||||
import { AddTeamToDevice } from "Modals/AddTeamToDevice/AddTeamToDevice";
|
||||
import { getShortTeamsList } from "Api/QuantumBackend/TeamManagement";
|
||||
|
||||
const colors = {
|
||||
ONLINE: "var(--mantine-color-green-7)",
|
||||
OFFLINE: "var(--mantine-color-red-7)",
|
||||
BUSY: "var(--mantine-color-yellow-5)",
|
||||
};
|
||||
|
||||
function DevicePage() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { device_id } = useParams();
|
||||
@@ -107,7 +113,16 @@ function DevicePage() {
|
||||
<Space h="xl" />
|
||||
<div style={{ display: "flex", flexDirection: "row", gap: "15px" }}>
|
||||
<Text size="md">Статус: </Text>
|
||||
<Pill className="ExperimentPill">
|
||||
<Pill
|
||||
className="ExperimentPill"
|
||||
style={{
|
||||
color: "white",
|
||||
backgroundColor:
|
||||
colors[
|
||||
cur_device?.system.status as "ONLINE" | "OFFLINE" | "BUSY"
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Text size="md">{cur_device?.system.status}</Text>
|
||||
</Pill>
|
||||
<div className="dateContainer">
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Box, Divider, TableOfContents, Title } from "@mantine/core";
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
TableOfContents,
|
||||
Title,
|
||||
Text,
|
||||
List,
|
||||
Code,
|
||||
Anchor,
|
||||
} from "@mantine/core";
|
||||
import "./DocumentationPage.css";
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
@@ -6,15 +15,19 @@ function DocumentationPage() {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Documentation | QMolSim</title>
|
||||
<title>Документация | QMolSim</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="See the documentation on how to setup and use the qunatum computational system"
|
||||
content="Полное руководство пользователя по системе распределенного квантово-химического расчета QMolSim."
|
||||
/>
|
||||
</Helmet>
|
||||
<Title order={1} className="docTitle">
|
||||
Документация
|
||||
Руководство пользователя
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
Автоматизированная система распределенного расчета энергии основного
|
||||
состояния молекул
|
||||
</Text>
|
||||
<Divider />
|
||||
<div className="DocumentationPage">
|
||||
<Box className="tableOfContents" visibleFrom="md">
|
||||
@@ -26,24 +39,291 @@ function DocumentationPage() {
|
||||
minDepthToOffset={0}
|
||||
depthOffset={20}
|
||||
scrollSpyOptions={{
|
||||
selector: "section h1, h2",
|
||||
selector: "section h1, section h2, section h3",
|
||||
}}
|
||||
className=""
|
||||
getControlProps={({ data }) => ({
|
||||
onClick: () =>
|
||||
data
|
||||
.getNode()
|
||||
.scrollIntoView({ behavior: "smooth", block: "center" }),
|
||||
.scrollIntoView({ behavior: "smooth", block: "start" }),
|
||||
children: data.value,
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
<div className="contents">
|
||||
<section id="introduction" style={{ height: 1000 }}>
|
||||
{/* ================= 1 ВВЕДЕНИЕ ================= */}
|
||||
<section id="introduction">
|
||||
<Title order={1}>1. Введение</Title>
|
||||
|
||||
<Title order={2} mt="md" id="application-area">
|
||||
1.1 Область применения
|
||||
</Title>
|
||||
<Text>Требования настоящего документа применяются при:</Text>
|
||||
<List>
|
||||
<List.Item>предварительных комплексных испытаниях;</List.Item>
|
||||
<List.Item>опытной эксплуатации;</List.Item>
|
||||
<List.Item>приемочных испытаниях;</List.Item>
|
||||
<List.Item>промышленной эксплуатации.</List.Item>
|
||||
</List>
|
||||
|
||||
<Title order={2} mt="md" id="capabilities">
|
||||
1.2 Краткое описание возможностей
|
||||
</Title>
|
||||
<Text>
|
||||
Автоматизированная система распределенного расчета энергии
|
||||
основного состояния молекул с помощью квантовых алгоритмов
|
||||
представляет собой распределенный веб-сервис, предназначенный для
|
||||
выполнения ресурсоемких квантово-химических расчетов с
|
||||
использованием гибридной архитектуры, состоящей из центрального
|
||||
сервера и распределенных квантовых симуляторов.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="cap-mgmt">
|
||||
Управление командной работой
|
||||
</Title>
|
||||
<Text>
|
||||
Пользователи могут создавать команды, приглашать других
|
||||
исследователей, назначать права доступа.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="cap-nodes">
|
||||
Подключение вычислительных систем
|
||||
</Title>
|
||||
<Text>
|
||||
Исследователи регистрируют в системе свои вычислительные узлы. Для
|
||||
каждого узла исследователь задает максимальное количество кубитов,
|
||||
а также предоставляет доступ командам на использование устройства.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="cap-experiments">
|
||||
Создание и запуск экспериментов
|
||||
</Title>
|
||||
<Text>
|
||||
В рамках команды пользователь создает эксперимент (набор задач для
|
||||
разных молекул). Для каждой задачи загружается или редактируется
|
||||
структура молекулы в формате XYZ, задаются квантово-химические
|
||||
параметры.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="cap-vqe">
|
||||
Распределенные вычисления VQE
|
||||
</Title>
|
||||
<Text>
|
||||
При запуске эксперимента система автоматически распределяет задачи
|
||||
по доступным вычислительным узлам с учетом их ограничений по числу
|
||||
кубит. В процессе расчета на сервер передаются промежуточные
|
||||
результаты.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="cap-fault">
|
||||
Отказоустойчивость и восстановление
|
||||
</Title>
|
||||
<Text>
|
||||
Каждый вычислительный узел каждые 5 секунд отправляет сигнал о
|
||||
своей работоспособности. При выходе узла из строя незавершенная
|
||||
задача автоматически перенаправляется в очередь и назначается на
|
||||
другой узел с сохранением промежуточных весов оптимизации.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="cap-vis">
|
||||
Визуализация молекул
|
||||
</Title>
|
||||
<Text>
|
||||
Для каждой задачи доступна интерактивная 3D-визуализация молекулы
|
||||
в шаростержневой модели.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="cap-import">
|
||||
Импорт молекулярных данных
|
||||
</Title>
|
||||
<Text>
|
||||
Система поддерживает преобразование молекул в требуемый формат из
|
||||
большинства существующих химических форматов.
|
||||
</Text>
|
||||
</section>
|
||||
<section id="quick-start" style={{ height: 1000 }}>
|
||||
<Title order={2}>1.1 Быстрое начало</Title>
|
||||
|
||||
{/* ================= 2 НАЗНАЧЕНИЕ И УСЛОВИЯ ================= */}
|
||||
<section id="purpose-conditions" style={{ marginTop: "2rem" }}>
|
||||
<Title order={1}>2. Назначение и условия применения</Title>
|
||||
|
||||
<Title order={2} mt="md" id="purpose">
|
||||
2.1 Назначение системы
|
||||
</Title>
|
||||
<Text>
|
||||
Система предназначена для автоматизированного распределенного
|
||||
расчета энергии основного состояния молекул с использованием
|
||||
квантового алгоритма VQE. Она обеспечивает создание команд
|
||||
исследователей с настройкой прав доступа, автоматическое
|
||||
распределение вычислительных задач между доступными узлами,
|
||||
мониторинг состояния вычислений и восстановление прогресса расчета
|
||||
при сбое отдельных вычислительных систем. Применение системы
|
||||
позволяет повысить скорость проведения квантово-химических
|
||||
расчетов и снизить нагрузку на пользователя по управлению
|
||||
вычислительным процессом. Система ориентирована на специалистов в
|
||||
области квантовой химии и вычислительных технологий.
|
||||
</Text>
|
||||
|
||||
<Title order={2} mt="md" id="tech-reqs">
|
||||
2.2 Требования к техническим средствам
|
||||
</Title>
|
||||
|
||||
<Title order={3} mt="sm" id="client-browser">
|
||||
Клиент-браузер:
|
||||
</Title>
|
||||
<List>
|
||||
<List.Item>Оперативная память от 4 Гб;</List.Item>
|
||||
<List.Item>Свободное пространство на диске от 2 Гб;</List.Item>
|
||||
<List.Item>Процессор 4-ядерный с частотой от 2 ГГц;</List.Item>
|
||||
<List.Item>Скорость подключения в интернет от 50 Мб/c;</List.Item>
|
||||
<List.Item>
|
||||
Наличие манипулятора "мышь" или аналогичного устройства для
|
||||
взаимодействия с интерфейсом;
|
||||
</List.Item>
|
||||
<List.Item>Наличие Клавиатуры.</List.Item>
|
||||
</List>
|
||||
|
||||
<Title order={3} mt="sm" id="client-compute">
|
||||
Клиент-ВС:
|
||||
</Title>
|
||||
<List>
|
||||
<List.Item>Оперативная память от 8 Гб;</List.Item>
|
||||
<List.Item>Свободное пространство на диске от 5 Гб;</List.Item>
|
||||
<List.Item>
|
||||
Процессор 8-ядерный с частотой от 2-4,4 ГГц;
|
||||
</List.Item>
|
||||
<List.Item>Скорость подключения в интернет от 50 Мб/с;</List.Item>
|
||||
<List.Item>
|
||||
Наличие манипулятора "мышь" или аналогичного устройства для
|
||||
взаимодействия с интерфейсом;
|
||||
</List.Item>
|
||||
<List.Item>Наличие Клавиатуры.</List.Item>
|
||||
</List>
|
||||
|
||||
<Title order={2} mt="md" id="software-reqs">
|
||||
2.3 Требования к программным средствам
|
||||
</Title>
|
||||
|
||||
<Title order={3} mt="sm" id="sw-browser">
|
||||
Клиент-браузер:
|
||||
</Title>
|
||||
<Text>
|
||||
Браузер (Safari 18.1.1, Яндекс Браузер 25.2.1, Google Chrome
|
||||
110.0.5481.100)
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="sw-compute">
|
||||
Клиент-ВС:
|
||||
</Title>
|
||||
<List>
|
||||
<List.Item>ОС Windows 10, Windows 11, MacOS, Linux</List.Item>
|
||||
<List.Item>
|
||||
Браузер (Safari 18.1.1, Яндекс Браузер 25.2.1, Google Chrome
|
||||
110.0.5481.100)
|
||||
</List.Item>
|
||||
<List.Item>Docker</List.Item>
|
||||
<List.Item>Docker-compose v2</List.Item>
|
||||
</List>
|
||||
|
||||
<Title order={2} mt="md" id="exec-conditions">
|
||||
3. Условия выполнения программы
|
||||
</Title>
|
||||
<Text>
|
||||
Для работы системы требуется веб-браузер, поддерживающий
|
||||
современные функции JavaScript (Google Chrome версии 110 и выше,
|
||||
Яндекс Браузер версии 25.2.1 и выше, Safari версии 18.1.1 и выше).
|
||||
Доступ к системе осуществляется через веб-интерфейс по адресу,
|
||||
предоставленному администратором. Для работы вычислительных узлов
|
||||
дополнительно требуется установленный Docker и Docker Compose v2
|
||||
на каждой подключаемой вычислительной системе. Необходимо наличие
|
||||
постоянного сетевого подключения к серверу для всех
|
||||
взаимодействующих компонентов системы.
|
||||
</Text>
|
||||
</section>
|
||||
|
||||
{/* ================= 4 ВЫПОЛНЕНИЕ ПРОГРАММЫ ================= */}
|
||||
<section id="execution" style={{ marginTop: "2rem" }}>
|
||||
<Title order={1}>4. Выполнение программы</Title>
|
||||
|
||||
<Title order={2} mt="md" id="install">
|
||||
4.1 Инсталяция/деинсталяция
|
||||
</Title>
|
||||
<Text>
|
||||
Клиент-браузер инсталляции и деинсталляции не требуется, для
|
||||
работы необходимо только наличие на системе совместимого браузера.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="install-compute">
|
||||
Для инсталляции клиента-ВС:
|
||||
</Title>
|
||||
<List>
|
||||
<List.Item>
|
||||
На системе необходимо наличие docker и docker-compose v2
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
Необходимо скачать контейнер с помощью команды:{" "}
|
||||
<Code>docker pull git.deowl.ru/vkrb/client:0.1.0</Code>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
Затем скачать файл docker-compose с помощью команды:{" "}
|
||||
<Code>
|
||||
curl -O
|
||||
"https://git.deowl.ru/vkrb/local_quantum_simulator/raw/branch/main/docker-compose.yml"
|
||||
</Code>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
Наконец, в той же папке необходимо создать файл переменных среды
|
||||
с названием ".env" и содержимым:
|
||||
<Code block mt="xs">
|
||||
{`PORT=5001
|
||||
STORAGE_PATH="/storage"
|
||||
RABBITMQ_HOST=rabbit.deowl.ru
|
||||
RABBITMQ_PORT=5672
|
||||
KEYCLOAK_URL=https://quantum-auth.deowl.ru
|
||||
KEYCLOAK_REALM_NAME=quant_sim-realm
|
||||
KEACLOAK_CLIENT_ID=local_quantum_sim
|
||||
QUANTUM_BACKEND_URL=https://quantum.deowl.ru`}
|
||||
</Code>
|
||||
</List.Item>
|
||||
</List>
|
||||
|
||||
<Title order={3} mt="md" id="uninstall-compute">
|
||||
Для деинсталляции клиента-ВС:
|
||||
</Title>
|
||||
<List>
|
||||
<List.Item>
|
||||
Удаляем файлы «docker-compose.yml», «.env» и папку
|
||||
«localStorage» (при ее наличие)
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
Удаляем установленное изображение с помощью команды:{" "}
|
||||
<Code>docker image rm git.deowl.ru/vkrb/client:0.1.0</Code>
|
||||
</List.Item>
|
||||
</List>
|
||||
|
||||
<Title order={2} mt="md" id="start-stop">
|
||||
4.2 Запуск / Остановка программы
|
||||
</Title>
|
||||
<Text>
|
||||
Клиент-браузер может быть открыт по ссылке:{" "}
|
||||
<Anchor href="http://quantum.deowl.ru/">
|
||||
http://quantum.deowl.ru/
|
||||
</Anchor>
|
||||
</Text>
|
||||
<Text mt="sm">
|
||||
Для запуска клиента-ВС необходимо выполнить команду, находясь в
|
||||
папке с файлом «docker-compose.yml»:{" "}
|
||||
<Code>docker compose up --d</Code>
|
||||
</Text>
|
||||
<Text mt="sm">
|
||||
Для остановки клиента-ВС: <Code>docker compose down</Code>
|
||||
</Text>
|
||||
<Text mt="sm">
|
||||
Для первичного подключения и мониторинга статуса клиента-ВС
|
||||
необходимо открыть ссылку:{" "}
|
||||
<Anchor href="http://localhost:5001/">
|
||||
http://localhost:5001/
|
||||
</Anchor>
|
||||
</Text>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.experimentButtons {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
|
||||
flex-direction: row;
|
||||
justify-content: right;
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
flex-wrap: nowrap;
|
||||
text-wrap: nowrap;
|
||||
* {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,221 @@
|
||||
import { Alert, Center, Text } from "@mantine/core";
|
||||
import {
|
||||
Alert,
|
||||
Center,
|
||||
LoadingOverlay,
|
||||
Text,
|
||||
UnstyledButton,
|
||||
SimpleGrid,
|
||||
Title,
|
||||
TextInput,
|
||||
Button,
|
||||
Group,
|
||||
Card,
|
||||
Badge,
|
||||
Collapse,
|
||||
ActionIcon,
|
||||
} from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import "./ExperimentPage.css";
|
||||
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
|
||||
//import { NewMoleculeModal } from "Modals/NewMolecule/NewMolecule";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { IconPlus, IconSettings } from "@tabler/icons-react";
|
||||
import {
|
||||
IconPlus,
|
||||
IconReload,
|
||||
IconEdit,
|
||||
IconX,
|
||||
IconCheck,
|
||||
IconChevronUp,
|
||||
IconChevronDown,
|
||||
IconCancel,
|
||||
} from "@tabler/icons-react";
|
||||
import { useParams } from "react-router";
|
||||
import CustomButton from "Components/CustomButton/CustomButton";
|
||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||
import type { ExperimentData, InstanceData } from "Types/Experiment/Experiment";
|
||||
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||
import {
|
||||
getExperimentById,
|
||||
getExperimentInstances,
|
||||
getFrontendFile,
|
||||
startExperiment,
|
||||
updateExperiment,
|
||||
} from "Api/QuantumBackend/ExperimentsManagment";
|
||||
import InstancesListCard from "Components/ListCard/TaskListCard/TaskListCard";
|
||||
import { NewInstanceModal } from "Modals/NewTask/NewTask";
|
||||
|
||||
function ExperimentPage() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isOpened, setIsOpen] = useState(false);
|
||||
const { experiment_id } = useParams();
|
||||
const { experiments, addTask } = useExperimentStore();
|
||||
const { experiments, addExperiment, instances, setInstances } =
|
||||
useExperimentStore();
|
||||
const [experiment, set_experiment] = useState<ExperimentData | undefined>(
|
||||
experiments.find((exp) => {
|
||||
return exp.id == Number(experiment_id);
|
||||
}),
|
||||
);
|
||||
|
||||
const experiment = experiments.find((exp) => {
|
||||
return exp.id == Number(experiment_id);
|
||||
});
|
||||
const { profile, is_loading } = useAuthenticationStore();
|
||||
const [is_loading_, set_is_loading] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editedName, setEditedName] = useState("");
|
||||
const [editedDesc, setEditedDesc] = useState("");
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const { loadedHtmlFiles, addLoadedHtml } = useExperimentStore();
|
||||
|
||||
const [total_experiment_inst, set_total_experiment_inst] =
|
||||
useState<number>(0);
|
||||
const [page_size, set_page_size] = useState<number>(5);
|
||||
const [cur_page, set_cur_page] = useState<number>(1);
|
||||
|
||||
const saveExperimentDetails = () => {
|
||||
if (experiment && editedName.trim()) {
|
||||
updateExperiment({
|
||||
experiment_id: experiment.id,
|
||||
name: editedName,
|
||||
description: editedDesc,
|
||||
})
|
||||
.then((updated) => {
|
||||
const updatedExperiment = {
|
||||
...experiment,
|
||||
name: updated.name,
|
||||
description: updated.description,
|
||||
};
|
||||
addExperiment(updatedExperiment);
|
||||
set_experiment(updatedExperiment);
|
||||
setIsEditing(false);
|
||||
notifications.show({
|
||||
title: "Успех",
|
||||
message: "Информация об эксперименте обновлена",
|
||||
color: "green",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.show({
|
||||
title: "Ошибка",
|
||||
message: "Не удалось обновить информацию",
|
||||
color: "red",
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEditing = () => {
|
||||
if (experiment) {
|
||||
setEditedName(experiment.name);
|
||||
setEditedDesc(experiment.description || "");
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startEditing = () => {
|
||||
if (experiment) {
|
||||
setEditedName(experiment.name);
|
||||
setEditedDesc(experiment.description || "");
|
||||
setIsEditing(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExperimentStart = () => {
|
||||
if (experiment) {
|
||||
startExperiment({ experiment_id: experiment.id })
|
||||
.then(() => {
|
||||
getExperimentById(Number(experiment_id))
|
||||
.then((exp) => {
|
||||
addExperiment(exp);
|
||||
set_experiment(exp);
|
||||
getExperimentInstances(exp.id, cur_page, page_size)
|
||||
.then((inst) => {
|
||||
setInstances(inst.instances);
|
||||
set_cur_page(inst.cur_page);
|
||||
set_total_experiment_inst(inst.total_instances);
|
||||
set_page_size(inst.page_size);
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
getFrontendFile(exp.experiment_type.id).then((file) => {
|
||||
addLoadedHtml(exp.experiment_type.id, file);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.show({
|
||||
title: "Ошибка",
|
||||
message:
|
||||
"Не удалось начать эксперимент, проверьте наличие задач и их правильность",
|
||||
color: "red",
|
||||
icon: <IconCancel />,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (profile && !is_loading) {
|
||||
if (!experiment) {
|
||||
getExperimentById(Number(experiment_id))
|
||||
.then((exp) => {
|
||||
addExperiment(exp);
|
||||
set_experiment(exp);
|
||||
getExperimentInstances(exp.id, cur_page, page_size)
|
||||
.then((inst) => {
|
||||
setInstances(inst.instances);
|
||||
set_cur_page(inst.cur_page);
|
||||
set_total_experiment_inst(inst.total_instances);
|
||||
set_page_size(inst.page_size);
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
getFrontendFile(exp.experiment_type.id).then((file) => {
|
||||
addLoadedHtml(exp.experiment_type.id, file);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
} else {
|
||||
if (
|
||||
(instances.length == 0 && cur_page != 0) ||
|
||||
(total_experiment_inst > page_size &&
|
||||
instances.length != page_size &&
|
||||
cur_page != Math.ceil(total_experiment_inst / page_size))
|
||||
) {
|
||||
getExperimentInstances(
|
||||
experiment.id,
|
||||
Math.min(cur_page, Math.ceil(total_experiment_inst / page_size)),
|
||||
page_size,
|
||||
)
|
||||
.then((inst) => {
|
||||
if (inst.instances.length == 0) {
|
||||
set_cur_page(0);
|
||||
setInstances([]);
|
||||
} else {
|
||||
setInstances(inst.instances);
|
||||
set_cur_page(inst.cur_page);
|
||||
set_total_experiment_inst(inst.total_instances);
|
||||
set_page_size(inst.page_size);
|
||||
}
|
||||
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
getFrontendFile(experiment.experiment_type.id).then((file) => {
|
||||
addLoadedHtml(experiment.experiment_type.id, file);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [profile, is_loading, instances]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -24,82 +223,350 @@ function ExperimentPage() {
|
||||
<title>
|
||||
{experiment
|
||||
? "Experiment " + experiment.id + " | QMolSim"
|
||||
: "Error |QmolSim"}
|
||||
: "Error | QMolSim"}
|
||||
</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="See the documentation on how to setup and use the qunatum computational system"
|
||||
content="See the documentation on how to setup and use the quantum computational system"
|
||||
/>
|
||||
</Helmet>
|
||||
{experiment && (
|
||||
<div className="ExperimentPage">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
marginBottom: "15px",
|
||||
}}
|
||||
>
|
||||
<div className="experimentButtons">
|
||||
<CustomButton
|
||||
color="contrast"
|
||||
onClick={() => {
|
||||
addTask(Number(experiment_id) || 0, {
|
||||
id: 1,
|
||||
name: "Задача 1",
|
||||
description: "",
|
||||
data: {},
|
||||
});
|
||||
setIsOpen(true);
|
||||
}}
|
||||
icon={<IconPlus />}
|
||||
text="Добавить задачу"
|
||||
/>
|
||||
<CustomButton
|
||||
color="contrast"
|
||||
style="outline"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
icon={<IconSettings />}
|
||||
text="Параметры эксперимента"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{experiment.tasks_ids.length > 0 && (
|
||||
<PaginationContainer
|
||||
numberOfPages={1}
|
||||
isLoading={false}
|
||||
activePage={1}
|
||||
setPage={() => {}}
|
||||
<div className="ExperimentPage">
|
||||
{experiment && (
|
||||
<NewInstanceModal
|
||||
experiment_id={experiment.id}
|
||||
isOpened={isOpened}
|
||||
setIsOpened={setIsOpen}
|
||||
/>
|
||||
)}
|
||||
<LoadingOverlay visible={is_loading_} zIndex={1000} />
|
||||
{experiment && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
marginBottom: "15px",
|
||||
}}
|
||||
>
|
||||
{experiment.tasks_ids.map((task: number) => {
|
||||
return <div>{task}</div>;
|
||||
})}
|
||||
</PaginationContainer>
|
||||
)}
|
||||
{experiment.tasks_ids.length == 0 && (
|
||||
<Alert>
|
||||
<Center>
|
||||
<Text size={"xl"} c="contrast">
|
||||
Нет задач
|
||||
</Text>
|
||||
</Center>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!experiment && (
|
||||
<Alert color="red">
|
||||
<Center>
|
||||
{" "}
|
||||
<Text c="contrast" size={"xl"}>
|
||||
Ошибка. Эксперимент не найден
|
||||
</Text>{" "}
|
||||
</Center>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="experimentButtons">
|
||||
{experiment.status == "DRAFT" && (
|
||||
<>
|
||||
<CustomButton
|
||||
color="accent"
|
||||
onClick={() => {
|
||||
handleExperimentStart();
|
||||
}}
|
||||
icon={<IconPlus />}
|
||||
text="Начать эксперимент"
|
||||
/>
|
||||
<CustomButton
|
||||
color="contrast"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
icon={<IconPlus />}
|
||||
text="Добавить задачу"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{experiment.status != "DRAFT" && (
|
||||
<UnstyledButton
|
||||
onClick={() => {
|
||||
getExperimentById(Number(experiment_id))
|
||||
.then((exp) => {
|
||||
addExperiment(exp);
|
||||
set_experiment(exp);
|
||||
getExperimentInstances(exp.id)
|
||||
.then((inst) => {
|
||||
setInstances(inst.instances);
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
getFrontendFile(exp.experiment_type.id).then(
|
||||
(file) => {
|
||||
addLoadedHtml(exp.experiment_type.id, file);
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconReload />
|
||||
</UnstyledButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Experiment Information Card */}
|
||||
{/* Experiment Information Card */}
|
||||
<Card withBorder mb="lg" shadow="sm">
|
||||
<Card.Section withBorder inheritPadding py="sm">
|
||||
<Group justify="space-between">
|
||||
<Group gap="xs">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<IconChevronUp size={16} />
|
||||
) : (
|
||||
<IconChevronDown size={16} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
<Title order={3}>Информация об эксперименте</Title>
|
||||
</Group>
|
||||
{!isEditing ? (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
leftSection={<IconEdit size={16} />}
|
||||
onClick={startEditing}
|
||||
>
|
||||
Редактировать
|
||||
</Button>
|
||||
) : (
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="filled"
|
||||
color="green"
|
||||
leftSection={<IconCheck size={14} />}
|
||||
onClick={saveExperimentDetails}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline"
|
||||
color="red"
|
||||
leftSection={<IconX size={14} />}
|
||||
onClick={cancelEditing}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</Card.Section>
|
||||
|
||||
<Collapse expanded={isExpanded} style={{ padding: "15px" }}>
|
||||
<Card.Section inheritPadding py="md">
|
||||
{!isEditing ? (
|
||||
<SimpleGrid cols={4} spacing="lg" verticalSpacing="md">
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
ID
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.id}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Тип
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.experiment_type.name}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Название
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.name}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Статус
|
||||
</Text>
|
||||
<Badge
|
||||
size="md"
|
||||
variant="filled"
|
||||
color={
|
||||
experiment.status === "DRAFT" ? "yellow" : "green"
|
||||
}
|
||||
radius="sm"
|
||||
>
|
||||
{experiment.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Команда
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.team.team_name}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Количество задач
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.instances_count}
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 2" }}>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Описание
|
||||
</Text>
|
||||
<Text size="md">
|
||||
{experiment.description || "Нет описания"}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Создан
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
{new Date(experiment.created_at).toLocaleString()}
|
||||
</Text>
|
||||
</div>
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
<SimpleGrid cols={4} spacing="lg" verticalSpacing="md">
|
||||
<div style={{ gridColumn: "span 1" }}>
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
mb={4}
|
||||
>
|
||||
Название
|
||||
</Text>
|
||||
<TextInput
|
||||
value={editedName}
|
||||
onChange={(e) => setEditedName(e.target.value)}
|
||||
placeholder="Введите название"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
mb={4}
|
||||
>
|
||||
Описание
|
||||
</Text>
|
||||
<TextInput
|
||||
value={editedDesc}
|
||||
onChange={(e) => setEditedDesc(e.target.value)}
|
||||
placeholder="Введите описание"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
ID
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.id}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Тип
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.experiment_type.name}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Команда
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.team.team_name}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Статус
|
||||
</Text>
|
||||
<Badge
|
||||
size="md"
|
||||
variant="filled"
|
||||
color={
|
||||
experiment.status === "DRAFT" ? "yellow" : "green"
|
||||
}
|
||||
radius="sm"
|
||||
>
|
||||
{experiment.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</Card.Section>
|
||||
</Collapse>
|
||||
</Card>
|
||||
|
||||
{!is_loading_ && (
|
||||
<PaginationContainer
|
||||
numberOfPages={Math.ceil(total_experiment_inst / page_size)}
|
||||
isLoading={false}
|
||||
activePage={cur_page}
|
||||
setPage={(page_num) => {
|
||||
getExperimentInstances(experiment.id, page_num, page_size)
|
||||
.then((inst) => {
|
||||
setInstances(inst.instances);
|
||||
set_cur_page(inst.cur_page);
|
||||
set_total_experiment_inst(inst.total_instances);
|
||||
set_page_size(inst.page_size);
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{instances &&
|
||||
loadedHtmlFiles.get(experiment.experiment_type.id) &&
|
||||
instances.map((inst: InstanceData) => {
|
||||
return (
|
||||
<InstancesListCard
|
||||
instance={inst}
|
||||
plugin={
|
||||
loadedHtmlFiles.get(experiment.experiment_type.id) ||
|
||||
""
|
||||
}
|
||||
key={inst.instance_id}
|
||||
></InstancesListCard>
|
||||
);
|
||||
})}
|
||||
{instances.length == 0 && !is_loading && (
|
||||
<Alert>
|
||||
<Center>
|
||||
<Text size={"xl"} c="contrast">
|
||||
Нет задач
|
||||
</Text>
|
||||
</Center>
|
||||
</Alert>
|
||||
)}
|
||||
</PaginationContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!experiment && !is_loading_ && (
|
||||
<Alert color="red">
|
||||
<Center>
|
||||
{" "}
|
||||
<Text c="contrast" size={"xl"}>
|
||||
Ошибка. Эксперимент не найден
|
||||
</Text>{" "}
|
||||
</Center>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.experimentsButtons {
|
||||
|
||||
@@ -6,23 +6,39 @@ import { IconMicroscope } from "@tabler/icons-react";
|
||||
import { NewExperimentModal } from "Modals/NewExperiment/NewExperiment";
|
||||
import ExperimentsListCard from "Components/ListCard/ExperimentsListCard";
|
||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||
import type { Experiment } from "Types/Experiment/Experiment";
|
||||
import CustomButton from "Components/CustomButton/CustomButton";
|
||||
import { Alert } from "@mantine/core";
|
||||
import { getShortTeamsList } from "Api/QuantumBackend/TeamManagement";
|
||||
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||
import {
|
||||
type ExperimentData,
|
||||
type ExperimentTypeList,
|
||||
} from "Types/Experiment/Experiment";
|
||||
import {
|
||||
getExperimentTypes,
|
||||
getUserExperiments,
|
||||
} from "Api/QuantumBackend/ExperimentsManagment";
|
||||
|
||||
function ExperimentsPage() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { experiments } = useExperimentStore();
|
||||
const { experiments, setExperiments, setInstances } = useExperimentStore();
|
||||
const [teams, setTeams] =
|
||||
useState<{ team_id: number; team_name: string }[]>();
|
||||
|
||||
const { profile, is_loading } = useAuthenticationStore();
|
||||
const [is_loading_, set_is_loading] = useState(true);
|
||||
const [exp_types, set_exp_types] = useState<ExperimentTypeList[]>([]);
|
||||
const [total_experiments, set_total_experiments] = useState<number>(0);
|
||||
const [page_size, set_page_size] = useState<number>(6);
|
||||
const [cur_page, set_cur_page] = useState<number>(1);
|
||||
|
||||
useEffect(() => {
|
||||
if (profile || !is_loading)
|
||||
getShortTeamsList()
|
||||
if (profile && !is_loading) {
|
||||
setInstances([]);
|
||||
getExperimentTypes().then((exp_types) => {
|
||||
set_exp_types(exp_types);
|
||||
});
|
||||
getShortTeamsList({})
|
||||
.then((teams) => {
|
||||
if (teams) {
|
||||
setTeams(teams);
|
||||
@@ -31,6 +47,18 @@ function ExperimentsPage() {
|
||||
.catch(() => {
|
||||
setTeams([]);
|
||||
});
|
||||
getUserExperiments({ page_num: cur_page, page_size: page_size })
|
||||
.then((expData) => {
|
||||
setExperiments(expData.experiments);
|
||||
set_total_experiments(expData.total_experiments);
|
||||
set_page_size(expData.page_size);
|
||||
set_cur_page(expData.cur_page);
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
}
|
||||
}, [profile, is_loading]);
|
||||
|
||||
return (
|
||||
@@ -46,64 +74,59 @@ function ExperimentsPage() {
|
||||
isOpened={isOpen}
|
||||
setIsOpened={setIsOpen}
|
||||
teams={teams}
|
||||
types={exp_types.map((type) => {
|
||||
return { type_id: type.id, type_name: type.name };
|
||||
})}
|
||||
/>
|
||||
<div className="ExperimentsPage">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
marginBottom: "20px",
|
||||
}}
|
||||
>
|
||||
{teams && teams.length > 0 && (
|
||||
<div className="experimentsButtons">
|
||||
<CustomButton
|
||||
color="contrast"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
icon={<IconMicroscope />}
|
||||
text="Создать эксперимент"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="experimentsButtons">
|
||||
<CustomButton
|
||||
color="contrast"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
icon={<IconMicroscope />}
|
||||
text="Создать эксперимент"
|
||||
/>
|
||||
</div>
|
||||
<div className="ExperimentsPage">
|
||||
<PaginationContainer
|
||||
numberOfPages={Math.ceil(total_experiments / page_size)}
|
||||
isLoading={teams == undefined}
|
||||
activePage={cur_page}
|
||||
setPage={(page_num) => {
|
||||
getUserExperiments({ page_num: page_num, page_size: page_size })
|
||||
.then((expData) => {
|
||||
setExperiments(expData.experiments);
|
||||
set_total_experiments(expData.total_experiments);
|
||||
set_page_size(expData.page_size);
|
||||
set_cur_page(expData.cur_page);
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{experiments.map((exp: ExperimentData) => {
|
||||
return <ExperimentsListCard experiment={exp} />;
|
||||
})}
|
||||
{teams && teams.length == 0 && (
|
||||
<Alert title="Команды не найдены" color="red">
|
||||
Создайте или войдите в команду чтобы начать работу с
|
||||
экспериментами
|
||||
</Alert>
|
||||
)}
|
||||
{teams &&
|
||||
teams.length != 0 &&
|
||||
experiments.length == 0 &&
|
||||
!is_loading_ && (
|
||||
<Alert title="Экспериментов нету" color="blue">
|
||||
Вы еще не создали не один эксперимент
|
||||
</Alert>
|
||||
)}
|
||||
</PaginationContainer>
|
||||
</div>
|
||||
<PaginationContainer
|
||||
numberOfPages={1}
|
||||
isLoading={teams == undefined}
|
||||
activePage={1}
|
||||
setPage={() => {}}
|
||||
>
|
||||
{experiments.map((exp: Experiment) => {
|
||||
return (
|
||||
<ExperimentsListCard
|
||||
experiment={{
|
||||
id: exp.id,
|
||||
name: exp.name,
|
||||
description: exp.description,
|
||||
team_id: exp.team_id,
|
||||
tasks_ids: exp.tasks_ids,
|
||||
date_created: exp.date_created,
|
||||
experiment_status: exp.experiment_status,
|
||||
experiment_type: "A",
|
||||
}}
|
||||
team={teams?.find((team) => team.team_id == exp.team_id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{teams && teams.length == 0 && (
|
||||
<Alert title="Команды не найдены" color="red">
|
||||
Создайте или войдите в команду чтобы начать работу с
|
||||
экспериментами
|
||||
</Alert>
|
||||
)}
|
||||
{teams && teams.length != 0 && experiments.length == 0 && (
|
||||
<Alert title="Экспериментов нету" color="blue">
|
||||
Вы еще не создали не один эксперимент
|
||||
</Alert>
|
||||
)}
|
||||
</PaginationContainer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
13
src/Pages/TaskPage/TaskPage.css
Normal file
13
src/Pages/TaskPage/TaskPage.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.taskButtons {
|
||||
display: flex;
|
||||
|
||||
flex-direction: row;
|
||||
justify-content: right;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
flex-wrap: nowrap;
|
||||
text-wrap: nowrap;
|
||||
* {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,100 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { IconCancel, IconPlus, IconSettings } from "@tabler/icons-react";
|
||||
import { IconCancel, IconPlus } 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";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { InstanceData } from "Types/Experiment/Experiment";
|
||||
import {
|
||||
getFrontendFile,
|
||||
getInstanceById,
|
||||
updateInstance,
|
||||
} from "Api/QuantumBackend/ExperimentsManagment";
|
||||
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||
import { SimpleGrid, TextInput, Title } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import "./TaskPage.css";
|
||||
|
||||
function TaskPage() {
|
||||
const { task_id } = useParams();
|
||||
const [data, setData] = useState<{ text: string }>({ text: "" });
|
||||
|
||||
const [instance, set_instance] = useState<InstanceData>();
|
||||
const { loadedHtmlFiles, addLoadedHtml, setInstances } = useExperimentStore();
|
||||
const { profile, is_loading } = useAuthenticationStore();
|
||||
const [, set_is_loading] = useState(true);
|
||||
const [data, setData] = useState<string>();
|
||||
const [progress, setProgress] = useState<string>();
|
||||
const [qubits_needed, set_qubits_needed] = useState<number>();
|
||||
|
||||
const [name, setName] = useState<string>();
|
||||
const [descr, setDescr] = useState<string>("");
|
||||
const [reload, setReload] = useState<number>(0);
|
||||
|
||||
const saveData = () => {
|
||||
if (instance && qubits_needed) {
|
||||
updateInstance({
|
||||
instance_id: instance.instance_id,
|
||||
name: name,
|
||||
description: descr,
|
||||
instance_data: JSON.stringify(data),
|
||||
qubits_needed: qubits_needed,
|
||||
}).then((updated) => {
|
||||
set_instance({
|
||||
instance_id: instance.instance_id,
|
||||
name: updated.name,
|
||||
description: updated.description,
|
||||
instance_data: updated.instance_data,
|
||||
qubits_needed: updated.qubits_needed,
|
||||
simulation_result: instance.simulation_result,
|
||||
});
|
||||
setName(updated.name);
|
||||
setDescr(updated.description || "");
|
||||
setData(updated.instance_data);
|
||||
});
|
||||
} else {
|
||||
if (!qubits_needed) {
|
||||
notifications.show({
|
||||
message: "Количество кубит не было определено",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ResetData = () => {
|
||||
if (instance) {
|
||||
setData(instance.instance_data);
|
||||
setName(instance.name);
|
||||
setDescr(instance.description || "");
|
||||
setReload(reload + 1);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (profile && !is_loading && task_id) {
|
||||
getInstanceById(Number(task_id))
|
||||
.then((inst) => {
|
||||
set_instance(inst);
|
||||
setData(inst.instance_data);
|
||||
setProgress(inst.simulation_result?.simulation_result);
|
||||
setName(inst.name);
|
||||
setDescr(inst.description || "");
|
||||
set_qubits_needed(inst.qubits_needed);
|
||||
getFrontendFile(1).then((file) => {
|
||||
addLoadedHtml(1, file);
|
||||
});
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
setInstances([]);
|
||||
};
|
||||
}, [profile, is_loading]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -29,39 +116,77 @@ function TaskPage() {
|
||||
marginBottom: "15px",
|
||||
}}
|
||||
>
|
||||
<div className="experimentButtons">
|
||||
<CustomButton
|
||||
color="accent"
|
||||
onClick={() => {}}
|
||||
icon={<IconPlus />}
|
||||
text="Сохранить"
|
||||
/>
|
||||
<CustomButton
|
||||
color="red"
|
||||
style="outline"
|
||||
onClick={() => {}}
|
||||
icon={<IconCancel />}
|
||||
text="Отменить"
|
||||
/>
|
||||
{(!instance?.simulation_result ||
|
||||
instance?.simulation_result?.status == "DRAFT") && (
|
||||
<>
|
||||
<div className="taskButtons">
|
||||
<CustomButton
|
||||
color="accent"
|
||||
onClick={saveData}
|
||||
disabled={
|
||||
JSON.stringify(data) ==
|
||||
JSON.stringify(instance?.instance_data) &&
|
||||
instance?.name == name &&
|
||||
(instance?.description || "") == (descr || "")
|
||||
}
|
||||
icon={<IconPlus />}
|
||||
text="Сохранить"
|
||||
/>
|
||||
<CustomButton
|
||||
color="red"
|
||||
style="outline"
|
||||
onClick={ResetData}
|
||||
icon={<IconCancel />}
|
||||
text="Отменить"
|
||||
disabled={
|
||||
JSON.stringify(data) ==
|
||||
JSON.stringify(instance?.instance_data) &&
|
||||
instance?.name == name &&
|
||||
(instance?.description || "") == (descr || "")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<SimpleGrid verticalSpacing="lg" cols={2}>
|
||||
<Title size="lg">Имя:</Title>
|
||||
<TextInput
|
||||
size="md"
|
||||
value={name}
|
||||
onChange={
|
||||
(e) => {
|
||||
setName(e.target.value);
|
||||
} //set_username(e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
<Title size="lg">Описание:</Title>
|
||||
<TextInput
|
||||
size="md"
|
||||
value={descr}
|
||||
onChange={
|
||||
(e) => {
|
||||
setDescr(e.target.value);
|
||||
} //set_email(e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</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}
|
||||
/>
|
||||
{instance && (
|
||||
<IframePlugin
|
||||
plugin={loadedHtmlFiles.get(1) || ""}
|
||||
mode="Editor"
|
||||
taskData={JSON.stringify(data)}
|
||||
qubits_needed={qubits_needed || 0}
|
||||
onUpdate={(data, qubits_need) => {
|
||||
setData(JSON.parse(data));
|
||||
set_qubits_needed(qubits_need);
|
||||
}}
|
||||
index={instance.instance_id + reload}
|
||||
simProgress={JSON.stringify(progress)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -56,7 +56,7 @@ function TeamPage() {
|
||||
addTeam(team);
|
||||
set_cur_team(team);
|
||||
set_team_name(team.name);
|
||||
set_team_descr(team.description);
|
||||
set_team_descr(team.description || "");
|
||||
set_is_loading(false);
|
||||
}
|
||||
});
|
||||
@@ -66,7 +66,7 @@ function TeamPage() {
|
||||
profile &&
|
||||
!is_loading
|
||||
) {
|
||||
getTeamSystems(Number(team_id), cur_page, 9 )
|
||||
getTeamSystems(Number(team_id), cur_page, 9)
|
||||
.then((systems) => {
|
||||
if (systems) {
|
||||
set_machines(systems);
|
||||
@@ -210,8 +210,8 @@ function TeamPage() {
|
||||
cur_team?.description == team_descr
|
||||
}
|
||||
onClick={() => {
|
||||
set_team_name(cur_team?.name);
|
||||
set_team_descr(cur_team?.description);
|
||||
set_team_name(cur_team?.name || "");
|
||||
set_team_descr(cur_team?.description || "");
|
||||
}}
|
||||
/>
|
||||
</Grid.Col>
|
||||
@@ -252,7 +252,7 @@ function TeamPage() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!cur_team && !is_loading && (
|
||||
{!cur_team && !is_loading_this && (
|
||||
<Alert color="red">
|
||||
<Center>
|
||||
{" "}
|
||||
|
||||
@@ -7,6 +7,12 @@ import {
|
||||
Switch,
|
||||
LoadingOverlay,
|
||||
Space,
|
||||
Avatar,
|
||||
Center,
|
||||
Grid,
|
||||
FileInput,
|
||||
Group,
|
||||
rem,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconChartCandle,
|
||||
@@ -15,6 +21,7 @@ import {
|
||||
IconCancel,
|
||||
IconSun,
|
||||
IconMoonStars,
|
||||
IconUpload,
|
||||
} from "@tabler/icons-react";
|
||||
import keycloak, {
|
||||
SendEmailVerification,
|
||||
@@ -29,19 +36,22 @@ import { useUserPreferencesStore } from "Stores/PreferencesStore";
|
||||
import CustomButton from "Components/CustomButton/CustomButton";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useSearchParams } from "react-router";
|
||||
import {
|
||||
GetCurrentUserInfo,
|
||||
UpdateCurrentUserInfo,
|
||||
} from "Api/QuantumBackend/UserManagement";
|
||||
import type { UserData } from "Types/User/User";
|
||||
import { UpdateCurrentUserInfo } from "Api/QuantumBackend/UserManagement";
|
||||
|
||||
function UserPage() {
|
||||
const { profile, is_loading, profile_picture_path } =
|
||||
useAuthenticationStore();
|
||||
const {
|
||||
profile,
|
||||
is_loading,
|
||||
profile_picture_path,
|
||||
set_profile_picture_path,
|
||||
} = useAuthenticationStore();
|
||||
const { theme, set_theme } = useUserPreferencesStore();
|
||||
const [username, set_username] = useState<string>("");
|
||||
const [email, set_email] = useState<string>("");
|
||||
const [pfp_path, set_pfp_path] = useState<string>("");
|
||||
const [is_editing_path, set_is_editing_path] = useState<boolean>(false);
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const updateData = () => {
|
||||
@@ -53,9 +63,7 @@ function UserPage() {
|
||||
const prof_1 = profile;
|
||||
prof_1.email = email;
|
||||
prof_1.username = username;
|
||||
useAuthenticationStore.setState({
|
||||
is_loading: true,
|
||||
});
|
||||
|
||||
updateUserData(prof_1).then((data) => {
|
||||
if (data) {
|
||||
notifications.show({
|
||||
@@ -76,15 +84,81 @@ function UserPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (file: File | null) => {
|
||||
setSelectedFile(file);
|
||||
if (file) {
|
||||
// Create preview
|
||||
const url = URL.createObjectURL(file);
|
||||
setPreviewUrl(url);
|
||||
} else {
|
||||
if (previewUrl) {
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
setPreviewUrl(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const update_pfp = async () => {
|
||||
UpdateCurrentUserInfo(pfp_path).then(() => {
|
||||
GetCurrentUserInfo().then((info: UserData | undefined) => {
|
||||
if (info && info.profile_picture_path)
|
||||
useAuthenticationStore.setState({
|
||||
profile_picture_path: info.profile_picture_path,
|
||||
});
|
||||
if (!selectedFile) return;
|
||||
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const result = await UpdateCurrentUserInfo(selectedFile);
|
||||
if (result && result.profile_picture_path) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_QUANTUM_BACKEND_URL}/user/serve/${profile?.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${keycloak.token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
set_profile_picture_path(url);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load avatar:", error);
|
||||
}
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Фотография профиля обновлена",
|
||||
message: "",
|
||||
icon: <IconCheck />,
|
||||
color: "green",
|
||||
});
|
||||
set_is_editing_path(false);
|
||||
// Clean up
|
||||
if (previewUrl) {
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
setPreviewUrl(null);
|
||||
}
|
||||
setSelectedFile(null);
|
||||
}
|
||||
} catch {
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Ошибка",
|
||||
message: "Не удалось обновить фотографию профиля",
|
||||
icon: <IconCancel />,
|
||||
color: "red",
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
set_is_editing_path(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelUpload = () => {
|
||||
set_is_editing_path(false);
|
||||
setSelectedFile(null);
|
||||
if (previewUrl) {
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
setPreviewUrl(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -103,10 +177,6 @@ function UserPage() {
|
||||
}
|
||||
}, [profile, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (profile_picture_path) set_pfp_path(profile_picture_path);
|
||||
}, [profile_picture_path]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -153,52 +223,126 @@ function UserPage() {
|
||||
overlayProps={{ radius: "sm", blur: 2 }}
|
||||
loaderProps={{ size: 50, type: "dots" }}
|
||||
/>
|
||||
<SimpleGrid verticalSpacing="lg" cols={2}>
|
||||
<Title size="lg">Имя пользователя:</Title>
|
||||
<TextInput
|
||||
size="md"
|
||||
value={username}
|
||||
onChange={(e) => set_username(e.currentTarget.value)}
|
||||
/>
|
||||
<Title size="lg">Почта:</Title>
|
||||
<TextInput
|
||||
size="md"
|
||||
value={email}
|
||||
onChange={(e) => set_email(e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<CustomButton
|
||||
color="accent"
|
||||
text="Сохранить"
|
||||
onClick={updateData}
|
||||
disabled={
|
||||
!(
|
||||
profile != null &&
|
||||
((profile.username != undefined &&
|
||||
profile.username != username) ||
|
||||
(profile.email != undefined && profile.email != email))
|
||||
)
|
||||
}
|
||||
/>
|
||||
<CustomButton
|
||||
color="error"
|
||||
text="Отменить"
|
||||
onClick={() => {
|
||||
if (profile && profile.email && profile.username) {
|
||||
set_username(profile.username);
|
||||
set_email(profile.email);
|
||||
}
|
||||
<Grid>
|
||||
<Grid.Col span={4} style={{ height: "100%" }}>
|
||||
<Center>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Title size="xl">Фотография профиля</Title>
|
||||
</div>
|
||||
</Center>
|
||||
<Center style={{ padding: "15px" }}>
|
||||
<Avatar size="xl" src={profile_picture_path} radius="xl" />
|
||||
{is_editing_path && (
|
||||
<FileInput
|
||||
placeholder="Выберите изображение"
|
||||
accept="image/png,image/jpeg,image/jpg,image/webp"
|
||||
value={selectedFile}
|
||||
onChange={handleFileChange}
|
||||
leftSection={<IconUpload size={rem(14)} />}
|
||||
clearable
|
||||
style={{ width: "100%" }}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
)}
|
||||
</Center>
|
||||
<div>
|
||||
{!is_editing_path ? (
|
||||
<CustomButton
|
||||
style="outline"
|
||||
color="contrast"
|
||||
text="Изменить"
|
||||
onClick={() => {
|
||||
set_is_editing_path(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Group justify="center" mt="md">
|
||||
<CustomButton
|
||||
style="outline"
|
||||
color="red"
|
||||
text="Отменить"
|
||||
onClick={cancelUpload}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
<CustomButton
|
||||
style="color"
|
||||
color="accent"
|
||||
text="Сохранить"
|
||||
onClick={update_pfp}
|
||||
disabled={!selectedFile}
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
</div>
|
||||
</Grid.Col>
|
||||
<Grid.Col
|
||||
span={4}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
justifyContent: "space-between",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
disabled={
|
||||
!(
|
||||
profile != null &&
|
||||
((profile.username != undefined &&
|
||||
profile.username != username) ||
|
||||
(profile.email != undefined && profile.email != email))
|
||||
)
|
||||
}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
>
|
||||
<Title size="lg">Имя пользователя:</Title>
|
||||
|
||||
<Title size="lg">Почта:</Title>
|
||||
<CustomButton
|
||||
color="accent"
|
||||
text="Сохранить"
|
||||
onClick={updateData}
|
||||
disabled={
|
||||
!(
|
||||
profile != null &&
|
||||
((profile.username != undefined &&
|
||||
profile.username != username) ||
|
||||
(profile.email != undefined &&
|
||||
profile.email != email))
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col
|
||||
span={4}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
justifyContent: "space-between",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
size="md"
|
||||
value={username}
|
||||
onChange={(e) => set_username(e.currentTarget.value)}
|
||||
/>
|
||||
<TextInput
|
||||
size="md"
|
||||
value={email}
|
||||
onChange={(e) => set_email(e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<CustomButton
|
||||
color="error"
|
||||
text="Отменить"
|
||||
onClick={() => {
|
||||
if (profile && profile.email && profile.username) {
|
||||
set_username(profile.username);
|
||||
set_email(profile.email);
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
!(
|
||||
profile != null &&
|
||||
((profile.username != undefined &&
|
||||
profile.username != username) ||
|
||||
(profile.email != undefined &&
|
||||
profile.email != email))
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<Divider my="lg" />
|
||||
<SimpleGrid verticalSpacing={"lg"} cols={2}>
|
||||
<Title
|
||||
@@ -266,60 +410,12 @@ function UserPage() {
|
||||
/>
|
||||
</div>
|
||||
</SimpleGrid>
|
||||
<Divider my="lg" />
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Title size="lg">Фотография профиля</Title>
|
||||
|
||||
<Space my="sm" />
|
||||
{!is_editing_path ? (
|
||||
<img height={350} width={350} src={profile_picture_path} />
|
||||
) : (
|
||||
<TextInput
|
||||
size="md"
|
||||
value={pfp_path}
|
||||
onChange={(e) => set_pfp_path(e.currentTarget.value)}
|
||||
/>
|
||||
)}
|
||||
<Space my="sm" />
|
||||
<div style={{ width: "15em" }}>
|
||||
{!is_editing_path ? (
|
||||
<CustomButton
|
||||
style="outline"
|
||||
color="contrast"
|
||||
text="Изменить"
|
||||
onClick={() => {
|
||||
set_is_editing_path(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<CustomButton
|
||||
style="outline"
|
||||
color="contrast"
|
||||
text="Сохранить"
|
||||
onClick={() => {
|
||||
update_pfp();
|
||||
set_is_editing_path(false);
|
||||
}}
|
||||
/>
|
||||
<Space my="sm" />
|
||||
<CustomButton
|
||||
style="outline"
|
||||
color="red"
|
||||
text="Отменить"
|
||||
onClick={() => {
|
||||
set_is_editing_path(false);
|
||||
set_pfp_path(profile_picture_path);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="preference">
|
||||
<Title size={"lg"}>Тема приложения:</Title>
|
||||
<Space h="md" />
|
||||
<Switch
|
||||
size="xl"
|
||||
defaultChecked={theme == "light"}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Breadcrumbs } from "@mantine/core";
|
||||
import { type ReactElement } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { useNavigate } from "react-router";
|
||||
import "./Breadcrumbs.css";
|
||||
import { routes } from "Routes/Routes";
|
||||
import { useLocation } from "react-router";
|
||||
@@ -46,6 +46,7 @@ function testEqual(path: string, pattern: string) {
|
||||
}
|
||||
|
||||
function BreadCrumbs() {
|
||||
const navigate = useNavigate();
|
||||
const unique_matches: string[] = getSubPaths(useLocation().pathname);
|
||||
|
||||
//find the breadcrumbs for the matched pathes
|
||||
@@ -56,13 +57,15 @@ function BreadCrumbs() {
|
||||
for (const i in routes[prop].breadcrumbs(unique_matches[u_match])) {
|
||||
if (elements.length + 1 != unique_matches.length) {
|
||||
elements.push(
|
||||
<Link
|
||||
<div
|
||||
className="invisible_link"
|
||||
to={unique_matches[u_match]}
|
||||
key={unique_matches[u_match]}
|
||||
onClick={() => {
|
||||
navigate(unique_matches[u_match]);
|
||||
}}
|
||||
>
|
||||
{routes[prop].breadcrumbs(unique_matches[u_match])[i]}
|
||||
</Link>,
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
elements.push(
|
||||
|
||||
@@ -34,7 +34,7 @@ export const routes: {
|
||||
],
|
||||
},
|
||||
TaskPage: {
|
||||
path: "/experiments/:experiment_id/:molecule_id",
|
||||
path: "/experiments/:experiment_id/:task_id",
|
||||
breadcrumbs: (path: string) => [
|
||||
<>Молекула #{path.split("/")[path.split("/").length - 1]}</>,
|
||||
],
|
||||
@@ -62,66 +62,63 @@ export const routes: {
|
||||
SettingsPage: { path: "/settings", breadcrumbs: () => [<>Настройки</>] },
|
||||
};
|
||||
|
||||
const router = createBrowserRouter(
|
||||
[
|
||||
{
|
||||
path: "/",
|
||||
element: <App />,
|
||||
children: [
|
||||
{
|
||||
path: routes.MainPage.path,
|
||||
Component: MainPage,
|
||||
},
|
||||
{
|
||||
path: routes.DocumentationPage.path,
|
||||
Component: DocumentationPage,
|
||||
},
|
||||
{
|
||||
path: routes.SettingsPage.path,
|
||||
Component: UserPage,
|
||||
},
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <App />,
|
||||
children: [
|
||||
{
|
||||
path: routes.MainPage.path,
|
||||
Component: MainPage,
|
||||
},
|
||||
{
|
||||
path: routes.DocumentationPage.path,
|
||||
Component: DocumentationPage,
|
||||
},
|
||||
{
|
||||
path: routes.SettingsPage.path,
|
||||
Component: UserPage,
|
||||
},
|
||||
|
||||
{
|
||||
element: <AuthGuard />,
|
||||
children: [
|
||||
{
|
||||
path: routes.ExperimentsPage.path,
|
||||
Component: ExperimentsPage,
|
||||
},
|
||||
{
|
||||
path: routes.ExperimentPage.path,
|
||||
Component: ExperimentPage,
|
||||
},
|
||||
{
|
||||
path: routes.TaskPage.path,
|
||||
Component: TaskPage,
|
||||
},
|
||||
{
|
||||
path: routes.TeamsPage.path,
|
||||
Component: TeamsPage,
|
||||
},
|
||||
{
|
||||
path: routes.TeamPage.path,
|
||||
Component: TeamPage,
|
||||
},
|
||||
{
|
||||
path: routes.MachinesPage.path,
|
||||
Component: DevicesPage,
|
||||
},
|
||||
{
|
||||
path: routes.MachinePage.path,
|
||||
Component: DevicePage,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <AuthGuard />,
|
||||
children: [
|
||||
{
|
||||
path: routes.ExperimentsPage.path,
|
||||
Component: ExperimentsPage,
|
||||
},
|
||||
{
|
||||
path: routes.ExperimentPage.path,
|
||||
Component: ExperimentPage,
|
||||
},
|
||||
{
|
||||
path: routes.TaskPage.path,
|
||||
Component: TaskPage,
|
||||
},
|
||||
{
|
||||
path: routes.TeamsPage.path,
|
||||
Component: TeamsPage,
|
||||
},
|
||||
{
|
||||
path: routes.TeamPage.path,
|
||||
Component: TeamPage,
|
||||
},
|
||||
{
|
||||
path: routes.MachinesPage.path,
|
||||
Component: DevicesPage,
|
||||
},
|
||||
{
|
||||
path: routes.MachinePage.path,
|
||||
Component: DevicePage,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: routes.ErrorPage.path,
|
||||
Component: ErrorPage,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{ basename: import.meta.env.VITE_BASE_PATH },
|
||||
);
|
||||
{
|
||||
path: routes.ErrorPage.path,
|
||||
Component: ErrorPage,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
export default router;
|
||||
|
||||
@@ -1,30 +1,70 @@
|
||||
import { create } from "zustand";
|
||||
import { immer } from "zustand/middleware/immer";
|
||||
|
||||
import type { Experiment, TaskData } from "Types/Experiment/Experiment";
|
||||
import type {
|
||||
ExperimentData,
|
||||
ExperimentTypeList,
|
||||
InstanceData,
|
||||
} from "Types/Experiment/Experiment";
|
||||
import { enableMapSet } from "immer";
|
||||
|
||||
// Call this once at your app's entry point (before using Immer)
|
||||
enableMapSet();
|
||||
|
||||
interface ExperimentStoreState {
|
||||
experiments: Experiment[];
|
||||
tasks: TaskData[];
|
||||
experiments: ExperimentData[];
|
||||
instances: InstanceData[];
|
||||
experimentTypes: ExperimentTypeList[] | null;
|
||||
loadedHtmlFiles: Map<number, string>; // experiment_type_id -> HTML content
|
||||
|
||||
addExperiment: (experiment: Experiment) => void;
|
||||
updateExperiment: (id: number, data: Partial<Experiment>) => void;
|
||||
setInstances: (instances: InstanceData[]) => void;
|
||||
updateInstance: (id: number, data: Partial<InstanceData>) => void;
|
||||
removeInstance: (id: number) => void;
|
||||
|
||||
setExperiments: (experiment: ExperimentData[]) => void;
|
||||
addExperiment: (experiment: ExperimentData) => void;
|
||||
updateExperiment: (id: number, data: Partial<ExperimentData>) => void;
|
||||
removeExperiment: (id: number) => void;
|
||||
|
||||
addTask: (experimentId: number, task: TaskData) => void;
|
||||
updateTask: (taskId: number, data: Partial<TaskData>) => void;
|
||||
removeTask: (experimentId: number, taskId: number) => void;
|
||||
// Only what you asked for:
|
||||
setExperimentTypes: (types: ExperimentTypeList[]) => void;
|
||||
addLoadedHtml: (typeId: number, htmlContent: string) => void;
|
||||
}
|
||||
|
||||
export const useExperimentStore = create<ExperimentStoreState>()(
|
||||
immer((set) => ({
|
||||
experiments: [],
|
||||
tasks: [],
|
||||
teams: [],
|
||||
instances: [],
|
||||
experimentTypes: null,
|
||||
loadedHtmlFiles: new Map(),
|
||||
|
||||
setExperiments: (experiments) =>
|
||||
set((state) => {
|
||||
state.experiments = experiments;
|
||||
}),
|
||||
|
||||
addExperiment: (experiment) =>
|
||||
set((state) => {
|
||||
state.experiments.push(experiment);
|
||||
state.experiments = [experiment, ...state.experiments];
|
||||
}),
|
||||
|
||||
setInstances: (instances) =>
|
||||
set((state) => {
|
||||
state.instances = instances;
|
||||
}),
|
||||
|
||||
updateInstance: (id, data) =>
|
||||
set((state) => {
|
||||
const exp = state.instances.find((e) => e.instance_id === id);
|
||||
if (!exp) return;
|
||||
|
||||
Object.assign(exp, data);
|
||||
}),
|
||||
|
||||
removeInstance: (id) =>
|
||||
set((state) => {
|
||||
state.instances = state.instances.filter((e) => e.instance_id !== id);
|
||||
}),
|
||||
|
||||
updateExperiment: (id, data) =>
|
||||
@@ -39,31 +79,15 @@ export const useExperimentStore = create<ExperimentStoreState>()(
|
||||
set((state) => {
|
||||
state.experiments = state.experiments.filter((e) => e.id !== id);
|
||||
}),
|
||||
|
||||
addTask: (experimentId, task) =>
|
||||
// Only these two new methods
|
||||
setExperimentTypes: (types) =>
|
||||
set((state) => {
|
||||
const exp = state.experiments.find((e) => e.id === experimentId);
|
||||
if (!exp) return;
|
||||
|
||||
exp.tasks_ids.push(task.id);
|
||||
state.tasks.push(task);
|
||||
state.experimentTypes = types;
|
||||
}),
|
||||
|
||||
updateTask: (taskId, data) =>
|
||||
addLoadedHtml: (typeId, htmlContent) =>
|
||||
set((state) => {
|
||||
const task = state.tasks.find((t) => t.id === taskId);
|
||||
if (!task) return;
|
||||
|
||||
Object.assign(task, data);
|
||||
}),
|
||||
|
||||
removeTask: (experimentId, taskId) =>
|
||||
set((state) => {
|
||||
const exp = state.experiments.find((e) => e.id === experimentId);
|
||||
if (!exp) return;
|
||||
|
||||
state.tasks = state.tasks.filter((t) => t.data.id !== taskId);
|
||||
exp.tasks_ids = exp.tasks_ids.filter((t) => t !== taskId);
|
||||
state.loadedHtmlFiles.set(typeId, htmlContent);
|
||||
}),
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export interface ConvertSchema {
|
||||
inputText: string;
|
||||
inputFormat: string;
|
||||
add_h: boolean;
|
||||
make_3d: boolean;
|
||||
optimize: boolean;
|
||||
}
|
||||
@@ -1,40 +1,101 @@
|
||||
export type ExperimentStatus =
|
||||
| "DRAFT"
|
||||
| "QUEUE"
|
||||
| "PROCESSING"
|
||||
| "SUCCESS"
|
||||
| "ERROR";
|
||||
|
||||
export interface Experiment {
|
||||
export interface ExperimentTypeList {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CreateExperimentTypeResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CreateExperimentRequest {
|
||||
team_id: number;
|
||||
date_created: Date;
|
||||
experiment_status: ExperimentStatus;
|
||||
experiment_type: string;
|
||||
tasks_ids: number[];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export interface TaskTypePlugin<TData = any> {
|
||||
type: string;
|
||||
// how it appears in the experiment task list
|
||||
ListItem: React.ComponentType<TaskData<TData>>;
|
||||
// full editor UI when clicking task
|
||||
Editor: React.ComponentType<TaskEditorProps<TData>>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export interface TaskData<TData = any> {
|
||||
id: number;
|
||||
experiment_type_id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
data: TData;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export interface TaskEditorProps<TData = any> {
|
||||
data: TData;
|
||||
setData: (data: TData) => void;
|
||||
export interface UpdateExperimentRequest {
|
||||
experiment_id: number;
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ExperimentData {
|
||||
id: number;
|
||||
team: {
|
||||
team_id: number;
|
||||
team_name: string;
|
||||
};
|
||||
name: string;
|
||||
description?: string;
|
||||
created_at: string;
|
||||
experiment_type: ExperimentTypeList;
|
||||
instances_count: number;
|
||||
instance_preview: SimpleInstanceData[];
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface ExperimentListResponse {
|
||||
experiments: ExperimentData[];
|
||||
cur_page: number;
|
||||
total_experiments: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
export interface CreateInstanceRequest {
|
||||
experiment_id: number;
|
||||
instance_data: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UpdateInstanceRequest {
|
||||
instance_id: number;
|
||||
name?: string;
|
||||
description?: string;
|
||||
instance_data?: string;
|
||||
qubits_needed: number;
|
||||
}
|
||||
|
||||
export interface SimpleInstanceData {
|
||||
id: number;
|
||||
instance_data: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
qubits_needed: number;
|
||||
}
|
||||
|
||||
export interface SimulationResultData {
|
||||
id: number;
|
||||
comp_system: {
|
||||
system_id: number;
|
||||
system_name: string;
|
||||
};
|
||||
simulation_result: string;
|
||||
status: string;
|
||||
started_at?: string;
|
||||
ended_at?: string;
|
||||
}
|
||||
|
||||
export interface InstanceData {
|
||||
instance_id: number;
|
||||
instance_data: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
simulation_result?: SimulationResultData;
|
||||
qubits_needed: number;
|
||||
}
|
||||
|
||||
export interface InstanceListResponse {
|
||||
instances: InstanceData[];
|
||||
cur_page: number;
|
||||
total_instances: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
export interface StartExperimentRequest {
|
||||
experiment_id: number;
|
||||
}
|
||||
|
||||
16
src/main.tsx
16
src/main.tsx
@@ -15,7 +15,7 @@ import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||
import { useUserPreferencesStore } from "Stores/PreferencesStore";
|
||||
import type { KeycloakProfile } from "keycloak-js";
|
||||
import { GetCurrentUserInfo } from "Api/QuantumBackend/UserManagement";
|
||||
import type { UserData } from "Types/User/User";
|
||||
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { IconForbid } from "@tabler/icons-react";
|
||||
|
||||
@@ -71,11 +71,8 @@ async function bootstrap() {
|
||||
onLoad: "check-sso",
|
||||
pkceMethod: "S256",
|
||||
silentCheckSsoRedirectUri:
|
||||
window.location.origin +
|
||||
"/" +
|
||||
import.meta.env.VITE_BASE_PATH +
|
||||
"/silent-check-sso.html",
|
||||
silentCheckSsoFallback: false,
|
||||
window.location.origin + "/silent-check-sso.html",
|
||||
silentCheckSsoFallback: true,
|
||||
})
|
||||
.then((authenticated: boolean) => {
|
||||
if (authenticated) {
|
||||
@@ -85,12 +82,7 @@ async function bootstrap() {
|
||||
profile: profile,
|
||||
});
|
||||
GetCurrentUserInfo()
|
||||
.then((info: UserData | undefined) => {
|
||||
if (info && info.profile_picture_path)
|
||||
useAuthenticationStore.setState({
|
||||
profile_picture_path: info.profile_picture_path,
|
||||
});
|
||||
})
|
||||
.then()
|
||||
.catch(() => {
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
|
||||
Reference in New Issue
Block a user