- added task rendering instead of molecule
- fied a lot of errors
- changes experiment page to show generic experiment info
This commit is contained in:
2026-05-25 12:55:46 +03:00
parent 26623dc2c1
commit f3372eb0ad
43 changed files with 5541 additions and 6908 deletions

View File

@@ -0,0 +1,112 @@
name: Build and Deploy React Package
# Controls when the workflow will run. Here, it runs on every push to the 'main' branch.
on:
push:
branches: ["main"]
# Environment variables used across the workflow
env:
# The URL of your Gitea instance (without http:// or https://)
GITEA_INSTANCE_URL: git.deowl.ru
# The full name of your package (e.g., 'myusername/myproject')
PACKAGE_NAME: vkrb/quantum_frontend
# Node.js version to use
NODE_VERSION: "25.9.0"
jobs:
build-and-publish:
# Runs the job on a runner with the 'ubuntu-latest' label.
runs-on: ubuntu-latest
steps:
# 1. Check out your repository code so the workflow can access it.
- name: Checkout code
uses: actions/checkout@v4
# 2. Set up Node.js environment
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
# 3. Install pnpm globally
- name: Install pnpm
run: npm install -g pnpm
# 4. Install dependencies with pnpm
- name: Install dependencies
run: pnpm install
# 5. Build the React/Vite application
# This assumes your vite.config.js/ts is configured to output to 'dist'
- name: Build application
run: pnpm run build
# 6. Create a tarball of the build artifacts
- name: Create package tarball
run: |
# Create a directory for the package
mkdir -p package_artifact
# Copy build artifacts (adjust path if your build outputs to a different directory)
cp -r dist package_artifact/
# Optionally copy other important files
# cp package.json package_artifact/
# cp README.md package_artifact/
# Create tarball
tar -czf package.tar.gz -C package_artifact .
# Generate checksum for integrity
sha256sum package.tar.gz > package.tar.gz.sha256
# 7. Log in to Gitea Package Registry
- name: Log in to Gitea Package Registry
run: |
# Create .netrc file for authentication with Gitea
cat > ~/.netrc << EOF
machine ${{ env.GITEA_INSTANCE_URL }}
login ${{ gitea.repository_owner }}
password ${{ secrets.REGISTRY_TOKEN }}
EOF
# 8. Upload the package to Gitea Generic Package Registry
- name: Upload to Gitea Generic Package Registry
run: |
# The format for Gitea generic packages:
# https://{GITEA_INSTANCE_URL}/api/packages/{owner}/generic/{package-name}/{version}/{file-name}
# Generate version from git commit SHA and timestamp
VERSION="${{ gitea.sha }}"
# Upload the tarball
curl --fail-with-body \
--netrc \
--upload-file package.tar.gz \
"https://${{ env.GITEA_INSTANCE_URL }}/api/packages/${{ gitea.repository_owner }}/generic/${{ env.PACKAGE_NAME }}/${VERSION}/package.tar.gz"
# Upload the checksum file
curl --fail-with-body \
--netrc \
--upload-file package.tar.gz.sha256 \
"https://${{ env.GITEA_INSTANCE_URL }}/api/packages/${{ gitea.repository_owner }}/generic/${{ env.PACKAGE_NAME }}/${VERSION}/package.tar.gz.sha256"
# 9. (Optional) Create a latest version for convenience
- name: Update latest version
run: |
# Upload as 'latest' version
curl --fail-with-body \
--netrc \
--upload-file package.tar.gz \
"https://${{ env.GITEA_INSTANCE_URL }}/api/packages/${{ gitea.repository_owner }}/generic/${{ env.PACKAGE_NAME }}/latest/package.tar.gz"
curl --fail-with-body \
--netrc \
--upload-file package.tar.gz.sha256 \
"https://${{ env.GITEA_INSTANCE_URL }}/api/packages/${{ gitea.repository_owner }}/generic/${{ env.PACKAGE_NAME }}/latest/package.tar.gz.sha256"
# 10. Clean up
- name: Clean up
run: rm -f package.tar.gz package.tar.gz.sha256

View File

@@ -10,36 +10,36 @@
"preview": "vite preview"
},
"dependencies": {
"@mantine/core": "^9.1.1",
"@mantine/hooks": "^9.1.1",
"@mantine/notifications": "^9.1.1",
"@tabler/icons-react": "^3.34.1",
"axios": "^1.13.2",
"dotenv": "^17.4.2",
"immer": "^11.1.4",
"keycloak-js": "^26.2.3",
"miew-react": "^0.11.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-helmet": "^6.1.0",
"react-resizable-panels": "^4.5.4",
"react-router": "^7.9.6",
"vite-tsconfig-paths": "^5.1.4",
"zustand": "^5.0.8"
"@mantine/core": "9.1.1",
"@mantine/hooks": "9.1.1",
"@mantine/notifications": "9.1.1",
"@tabler/icons-react": "3.34.1",
"axios": "1.13.2",
"dotenv": "17.4.2",
"immer": "11.1.4",
"keycloak-js": "26.2.3",
"miew-react": "0.11.0",
"react": "19.2.5",
"react-dom": "19.2.5",
"react-helmet": "6.1.0",
"react-resizable-panels": "4.5.4",
"react-router": "7.9.6",
"vite-tsconfig-paths": "5.1.4",
"zustand": "5.0.8"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/node": "^25.6.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-helmet": "^6.1.11",
"@vitejs/plugin-react": "^5.0.2",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"@eslint/js": "9.33.0",
"@types/node": "25.6.0",
"@types/react": "18.2.0",
"@types/react-dom": "18.2.0",
"@types/react-helmet": "6.1.11",
"@vitejs/plugin-react": "5.0.2",
"eslint": "9.33.0",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.20",
"globals": "16.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2"
"typescript-eslint": "8.39.1",
"vite": "7.1.2"
}
}

2921
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -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;
}
}
}

View File

@@ -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",

View File

@@ -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>
</>
);
};

View 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;
};

View File

@@ -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 },
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: {
"Content-Type": "application/json",
Authorization: `Bearer ${keycloak.token}`,
},
withCredentials: true, // Important: allows credentials in CORS
body: formData,
},
);
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> => {

View File

@@ -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,
});
});
};

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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();

View File

@@ -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"
onClick={() => {
navigate(routes.SettingsPage.path);
}}
/>
</Link>
<CustomButton
style="subtle"
icon={<IconLogout size={26} />}

View File

@@ -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 = () => {
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">

View File

@@ -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>

View 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);
}

View 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;

View File

@@ -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

View File

@@ -8,6 +8,7 @@ import {
UnstyledButton,
} from "@mantine/core";
import {
IconCancel,
IconCheck,
IconCrown,
IconForbid,
@@ -196,7 +197,8 @@ function TeamMemberCard(props: { cur_team: Team; member: TeamMember }) {
deleteMember({
team_id: props.cur_team.id,
user_id: props.member.user.keycloak_id,
}).then(() => {
})
.then(() => {
notifications.show({
radius: "md",
title: "Пользователь удален успешно",
@@ -208,10 +210,21 @@ function TeamMemberCard(props: { cur_team: Team; member: TeamMember }) {
members: [
...props.cur_team.members.filter(
(member) =>
member.user.keycloak_id != props.member.user.keycloak_id,
member.user.keycloak_id !=
props.member.user.keycloak_id,
),
],
});
})
.catch(() => {
notifications.show({
radius: "md",
title: "Пользователя не получилось удалить",
message: "",
color: "red",
icon: <IconCancel />,
style: { paddingLeft: "5px" },
});
});
}}
style={{

View File

@@ -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>

View File

@@ -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);
});
}
};
@@ -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="Создать эксперимент"

View File

@@ -1,3 +0,0 @@
.StepperRoot {
flex-direction: row-reverse !important;
}

View File

@@ -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>
);
}

View File

View 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>
);
}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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 experiment = experiments.find((exp) => {
const { experiments, addExperiment, instances, setInstances } =
useExperimentStore();
const [experiment, set_experiment] = useState<ExperimentData | undefined>(
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,15 +223,24 @@ 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">
{experiment && (
<NewInstanceModal
experiment_id={experiment.id}
isOpened={isOpened}
setIsOpened={setIsOpen}
/>
)}
<LoadingOverlay visible={is_loading_} zIndex={1000} />
{experiment && (
<>
<div
style={{
display: "flex",
@@ -42,44 +250,300 @@ function ExperimentPage() {
}}
>
<div className="experimentButtons">
{experiment.status == "DRAFT" && (
<>
<CustomButton
color="accent"
onClick={() => {
handleExperimentStart();
}}
icon={<IconPlus />}
text="Начать эксперимент"
/>
<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"
</>
)}
{experiment.status != "DRAFT" && (
<UnstyledButton
onClick={() => {
setIsOpen(true);
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);
});
}}
icon={<IconSettings />}
text="Параметры эксперимента"
>
<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>
{experiment.tasks_ids.length > 0 && (
<PaginationContainer
numberOfPages={1}
isLoading={false}
activePage={1}
setPage={() => {}}
<div style={{ gridColumn: "span 3" }}>
<Text
size="xs"
c="dimmed"
tt="uppercase"
fw={700}
mb={4}
>
{experiment.tasks_ids.map((task: number) => {
return <div>{task}</div>;
})}
</PaginationContainer>
Описание
</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>
)}
{experiment.tasks_ids.length == 0 && (
</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">
@@ -88,9 +552,11 @@ function ExperimentPage() {
</Center>
</Alert>
)}
</div>
</PaginationContainer>
)}
{!experiment && (
</>
)}
{!experiment && !is_loading_ && (
<Alert color="red">
<Center>
{" "}
@@ -100,6 +566,7 @@ function ExperimentPage() {
</Center>
</Alert>
)}
</div>
</>
);
}

View File

@@ -2,6 +2,8 @@
flex-grow: 1;
display: flex;
flex-direction: column;
position: relative;
gap: 15px;
}
.experimentsButtons {

View File

@@ -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,17 +74,11 @@ 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"
@@ -67,30 +89,27 @@ function ExperimentsPage() {
text="Создать эксперимент"
/>
</div>
)}
</div>
<div className="ExperimentsPage">
<PaginationContainer
numberOfPages={1}
numberOfPages={Math.ceil(total_experiments / page_size)}
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",
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);
});
}}
team={teams?.find((team) => team.team_id == exp.team_id)}
/>
);
>
{experiments.map((exp: ExperimentData) => {
return <ExperimentsListCard experiment={exp} />;
})}
{teams && teams.length == 0 && (
<Alert title="Команды не найдены" color="red">
@@ -98,13 +117,17 @@ function ExperimentsPage() {
экспериментами
</Alert>
)}
{teams && teams.length != 0 && experiments.length == 0 && (
{teams &&
teams.length != 0 &&
experiments.length == 0 &&
!is_loading_ && (
<Alert title="Экспериментов нету" color="blue">
Вы еще не создали не один эксперимент
</Alert>
)}
</PaginationContainer>
</div>
</div>
</>
);
}

View 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;
}
}

View File

@@ -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,41 +116,79 @@ function TaskPage() {
marginBottom: "15px",
}}
>
<div className="experimentButtons">
{(!instance?.simulation_result ||
instance?.simulation_result?.status == "DRAFT") && (
<>
<div className="taskButtons">
<CustomButton
color="accent"
onClick={() => {}}
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={() => {}}
onClick={ResetData}
icon={<IconCancel />}
text="Отменить"
/>
</div>
</div>
<IframePlugin
pluginUrl={
window.location.origin +
"/" +
import.meta.env.VITE_BASE_PATH +
"/" +
"index.html"
disabled={
JSON.stringify(data) ==
JSON.stringify(instance?.instance_data) &&
instance?.name == name &&
(instance?.description || "") == (descr || "")
}
mode="editor"
taskData={{
id: Number(task_id),
name: "Молекула 1",
description: "",
data: data,
}}
onUpdate={setData}
/>
</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>
{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>
</>
);
}

View File

@@ -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);
}
});
@@ -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>
{" "}

View File

@@ -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,20 +223,70 @@ function UserPage() {
overlayProps={{ radius: "sm", blur: 2 }}
loaderProps={{ size: 50, type: "dots" }}
/>
<SimpleGrid verticalSpacing="lg" cols={2}>
<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",
}}
>
<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)}
/>
<Title size="lg">Почта:</Title>
<CustomButton
color="accent"
text="Сохранить"
@@ -176,10 +296,32 @@ function UserPage() {
profile != null &&
((profile.username != undefined &&
profile.username != username) ||
(profile.email != undefined && profile.email != email))
(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="Отменить"
@@ -194,11 +336,13 @@ function UserPage() {
profile != null &&
((profile.username != undefined &&
profile.username != username) ||
(profile.email != undefined && profile.email != email))
(profile.email != undefined &&
profile.email != email))
)
}
/>
</SimpleGrid>
</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"}

View File

@@ -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(

View File

@@ -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,8 +62,7 @@ export const routes: {
SettingsPage: { path: "/settings", breadcrumbs: () => [<>Настройки</>] },
};
const router = createBrowserRouter(
[
const router = createBrowserRouter([
{
path: "/",
element: <App />,
@@ -121,7 +120,5 @@ const router = createBrowserRouter(
},
],
},
],
{ basename: import.meta.env.VITE_BASE_PATH },
);
]);
export default router;

View File

@@ -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);
}),
})),
);

View File

@@ -1,7 +0,0 @@
export interface ConvertSchema {
inputText: string;
inputFormat: string;
add_h: boolean;
make_3d: boolean;
optimize: boolean;
}

View File

@@ -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;
}

View File

@@ -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",

View File

@@ -14,7 +14,7 @@ export default defineConfig({
preview: {
port: Number(process.env.VITE_PORT),
},
base: "/" + process.env.VITE_BASE_PATH,
build: {
rollupOptions: {
output: {