- 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

@@ -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 { experiments, addExperiment, instances, setInstances } =
useExperimentStore();
const [experiment, set_experiment] = useState<ExperimentData | undefined>(
experiments.find((exp) => {
return exp.id == Number(experiment_id);
}),
);
const experiment = experiments.find((exp) => {
return exp.id == Number(experiment_id);
});
const { profile, is_loading } = useAuthenticationStore();
const [is_loading_, set_is_loading] = useState(true);
const [isEditing, setIsEditing] = useState(false);
const [editedName, setEditedName] = useState("");
const [editedDesc, setEditedDesc] = useState("");
const [isExpanded, setIsExpanded] = useState(false);
const { loadedHtmlFiles, addLoadedHtml } = useExperimentStore();
const [total_experiment_inst, set_total_experiment_inst] =
useState<number>(0);
const [page_size, set_page_size] = useState<number>(5);
const [cur_page, set_cur_page] = useState<number>(1);
const saveExperimentDetails = () => {
if (experiment && editedName.trim()) {
updateExperiment({
experiment_id: experiment.id,
name: editedName,
description: editedDesc,
})
.then((updated) => {
const updatedExperiment = {
...experiment,
name: updated.name,
description: updated.description,
};
addExperiment(updatedExperiment);
set_experiment(updatedExperiment);
setIsEditing(false);
notifications.show({
title: "Успех",
message: "Информация об эксперименте обновлена",
color: "green",
});
})
.catch(() => {
notifications.show({
title: "Ошибка",
message: "Не удалось обновить информацию",
color: "red",
});
});
}
};
const cancelEditing = () => {
if (experiment) {
setEditedName(experiment.name);
setEditedDesc(experiment.description || "");
setIsEditing(false);
}
};
const startEditing = () => {
if (experiment) {
setEditedName(experiment.name);
setEditedDesc(experiment.description || "");
setIsEditing(true);
}
};
const handleExperimentStart = () => {
if (experiment) {
startExperiment({ experiment_id: experiment.id })
.then(() => {
getExperimentById(Number(experiment_id))
.then((exp) => {
addExperiment(exp);
set_experiment(exp);
getExperimentInstances(exp.id, cur_page, page_size)
.then((inst) => {
setInstances(inst.instances);
set_cur_page(inst.cur_page);
set_total_experiment_inst(inst.total_instances);
set_page_size(inst.page_size);
set_is_loading(false);
})
.catch(() => {
set_is_loading(false);
});
getFrontendFile(exp.experiment_type.id).then((file) => {
addLoadedHtml(exp.experiment_type.id, file);
});
})
.catch(() => {
set_is_loading(false);
});
})
.catch(() => {
notifications.show({
title: "Ошибка",
message:
"Не удалось начать эксперимент, проверьте наличие задач и их правильность",
color: "red",
icon: <IconCancel />,
});
});
}
};
useEffect(() => {
if (profile && !is_loading) {
if (!experiment) {
getExperimentById(Number(experiment_id))
.then((exp) => {
addExperiment(exp);
set_experiment(exp);
getExperimentInstances(exp.id, cur_page, page_size)
.then((inst) => {
setInstances(inst.instances);
set_cur_page(inst.cur_page);
set_total_experiment_inst(inst.total_instances);
set_page_size(inst.page_size);
set_is_loading(false);
})
.catch(() => {
set_is_loading(false);
});
getFrontendFile(exp.experiment_type.id).then((file) => {
addLoadedHtml(exp.experiment_type.id, file);
});
})
.catch(() => {
set_is_loading(false);
});
} else {
if (
(instances.length == 0 && cur_page != 0) ||
(total_experiment_inst > page_size &&
instances.length != page_size &&
cur_page != Math.ceil(total_experiment_inst / page_size))
) {
getExperimentInstances(
experiment.id,
Math.min(cur_page, Math.ceil(total_experiment_inst / page_size)),
page_size,
)
.then((inst) => {
if (inst.instances.length == 0) {
set_cur_page(0);
setInstances([]);
} else {
setInstances(inst.instances);
set_cur_page(inst.cur_page);
set_total_experiment_inst(inst.total_instances);
set_page_size(inst.page_size);
}
set_is_loading(false);
})
.catch(() => {
set_is_loading(false);
});
getFrontendFile(experiment.experiment_type.id).then((file) => {
addLoadedHtml(experiment.experiment_type.id, file);
});
}
}
}
}, [profile, is_loading, instances]);
return (
<>
@@ -24,82 +223,350 @@ function ExperimentPage() {
<title>
{experiment
? "Experiment " + experiment.id + " | QMolSim"
: "Error |QmolSim"}
: "Error | QMolSim"}
</title>
<meta
name="description"
content="See the documentation on how to setup and use the qunatum computational system"
content="See the documentation on how to setup and use the quantum computational system"
/>
</Helmet>
{experiment && (
<div className="ExperimentPage">
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
marginBottom: "15px",
}}
>
<div className="experimentButtons">
<CustomButton
color="contrast"
onClick={() => {
addTask(Number(experiment_id) || 0, {
id: 1,
name: "Задача 1",
description: "",
data: {},
});
setIsOpen(true);
}}
icon={<IconPlus />}
text="Добавить задачу"
/>
<CustomButton
color="contrast"
style="outline"
onClick={() => {
setIsOpen(true);
}}
icon={<IconSettings />}
text="Параметры эксперимента"
/>
</div>
</div>
{experiment.tasks_ids.length > 0 && (
<PaginationContainer
numberOfPages={1}
isLoading={false}
activePage={1}
setPage={() => {}}
<div className="ExperimentPage">
{experiment && (
<NewInstanceModal
experiment_id={experiment.id}
isOpened={isOpened}
setIsOpened={setIsOpen}
/>
)}
<LoadingOverlay visible={is_loading_} zIndex={1000} />
{experiment && (
<>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
marginBottom: "15px",
}}
>
{experiment.tasks_ids.map((task: number) => {
return <div>{task}</div>;
})}
</PaginationContainer>
)}
{experiment.tasks_ids.length == 0 && (
<Alert>
<Center>
<Text size={"xl"} c="contrast">
Нет задач
</Text>
</Center>
</Alert>
)}
</div>
)}
{!experiment && (
<Alert color="red">
<Center>
{" "}
<Text c="contrast" size={"xl"}>
Ошибка. Эксперимент не найден
</Text>{" "}
</Center>
</Alert>
)}
<div className="experimentButtons">
{experiment.status == "DRAFT" && (
<>
<CustomButton
color="accent"
onClick={() => {
handleExperimentStart();
}}
icon={<IconPlus />}
text="Начать эксперимент"
/>
<CustomButton
color="contrast"
onClick={() => {
setIsOpen(true);
}}
icon={<IconPlus />}
text="Добавить задачу"
/>
</>
)}
{experiment.status != "DRAFT" && (
<UnstyledButton
onClick={() => {
getExperimentById(Number(experiment_id))
.then((exp) => {
addExperiment(exp);
set_experiment(exp);
getExperimentInstances(exp.id)
.then((inst) => {
setInstances(inst.instances);
set_is_loading(false);
})
.catch(() => {
set_is_loading(false);
});
getFrontendFile(exp.experiment_type.id).then(
(file) => {
addLoadedHtml(exp.experiment_type.id, file);
},
);
})
.catch(() => {
set_is_loading(false);
});
}}
>
<IconReload />
</UnstyledButton>
)}
</div>
</div>
{/* Experiment Information Card */}
{/* Experiment Information Card */}
<Card withBorder mb="lg" shadow="sm">
<Card.Section withBorder inheritPadding py="sm">
<Group justify="space-between">
<Group gap="xs">
<ActionIcon
variant="subtle"
size="sm"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? (
<IconChevronUp size={16} />
) : (
<IconChevronDown size={16} />
)}
</ActionIcon>
<Title order={3}>Информация об эксперименте</Title>
</Group>
{!isEditing ? (
<Button
variant="subtle"
size="sm"
leftSection={<IconEdit size={16} />}
onClick={startEditing}
>
Редактировать
</Button>
) : (
<Group gap="xs">
<Button
size="xs"
variant="filled"
color="green"
leftSection={<IconCheck size={14} />}
onClick={saveExperimentDetails}
>
Сохранить
</Button>
<Button
size="xs"
variant="outline"
color="red"
leftSection={<IconX size={14} />}
onClick={cancelEditing}
>
Отмена
</Button>
</Group>
)}
</Group>
</Card.Section>
<Collapse expanded={isExpanded} style={{ padding: "15px" }}>
<Card.Section inheritPadding py="md">
{!isEditing ? (
<SimpleGrid cols={4} spacing="lg" verticalSpacing="md">
<div>
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
ID
</Text>
<Text size="md" fw={500}>
{experiment.id}
</Text>
</div>
<div>
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
Тип
</Text>
<Text size="md" fw={500}>
{experiment.experiment_type.name}
</Text>
</div>
<div>
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
Название
</Text>
<Text size="md" fw={500}>
{experiment.name}
</Text>
</div>
<div>
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
Статус
</Text>
<Badge
size="md"
variant="filled"
color={
experiment.status === "DRAFT" ? "yellow" : "green"
}
radius="sm"
>
{experiment.status}
</Badge>
</div>
<div>
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
Команда
</Text>
<Text size="md" fw={500}>
{experiment.team.team_name}
</Text>
</div>
<div>
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
Количество задач
</Text>
<Text size="md" fw={500}>
{experiment.instances_count}
</Text>
</div>
<div style={{ gridColumn: "span 2" }}>
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
Описание
</Text>
<Text size="md">
{experiment.description || "Нет описания"}
</Text>
</div>
<div>
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
Создан
</Text>
<Text size="sm">
{new Date(experiment.created_at).toLocaleString()}
</Text>
</div>
</SimpleGrid>
) : (
<SimpleGrid cols={4} spacing="lg" verticalSpacing="md">
<div style={{ gridColumn: "span 1" }}>
<Text
size="xs"
c="dimmed"
tt="uppercase"
fw={700}
mb={4}
>
Название
</Text>
<TextInput
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
placeholder="Введите название"
/>
</div>
<div style={{ gridColumn: "span 3" }}>
<Text
size="xs"
c="dimmed"
tt="uppercase"
fw={700}
mb={4}
>
Описание
</Text>
<TextInput
value={editedDesc}
onChange={(e) => setEditedDesc(e.target.value)}
placeholder="Введите описание"
/>
</div>
<div>
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
ID
</Text>
<Text size="md" fw={500}>
{experiment.id}
</Text>
</div>
<div>
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
Тип
</Text>
<Text size="md" fw={500}>
{experiment.experiment_type.name}
</Text>
</div>
<div>
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
Команда
</Text>
<Text size="md" fw={500}>
{experiment.team.team_name}
</Text>
</div>
<div>
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
Статус
</Text>
<Badge
size="md"
variant="filled"
color={
experiment.status === "DRAFT" ? "yellow" : "green"
}
radius="sm"
>
{experiment.status}
</Badge>
</div>
</SimpleGrid>
)}
</Card.Section>
</Collapse>
</Card>
{!is_loading_ && (
<PaginationContainer
numberOfPages={Math.ceil(total_experiment_inst / page_size)}
isLoading={false}
activePage={cur_page}
setPage={(page_num) => {
getExperimentInstances(experiment.id, page_num, page_size)
.then((inst) => {
setInstances(inst.instances);
set_cur_page(inst.cur_page);
set_total_experiment_inst(inst.total_instances);
set_page_size(inst.page_size);
set_is_loading(false);
})
.catch(() => {
set_is_loading(false);
});
}}
>
{instances &&
loadedHtmlFiles.get(experiment.experiment_type.id) &&
instances.map((inst: InstanceData) => {
return (
<InstancesListCard
instance={inst}
plugin={
loadedHtmlFiles.get(experiment.experiment_type.id) ||
""
}
key={inst.instance_id}
></InstancesListCard>
);
})}
{instances.length == 0 && !is_loading && (
<Alert>
<Center>
<Text size={"xl"} c="contrast">
Нет задач
</Text>
</Center>
</Alert>
)}
</PaginationContainer>
)}
</>
)}
{!experiment && !is_loading_ && (
<Alert color="red">
<Center>
{" "}
<Text c="contrast" size={"xl"}>
Ошибка. Эксперимент не найден
</Text>{" "}
</Center>
</Alert>
)}
</div>
</>
);
}

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,64 +74,59 @@ function ExperimentsPage() {
isOpened={isOpen}
setIsOpened={setIsOpen}
teams={teams}
types={exp_types.map((type) => {
return { type_id: type.id, type_name: type.name };
})}
/>
<div className="ExperimentsPage">
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
marginBottom: "20px",
}}
>
{teams && teams.length > 0 && (
<div className="experimentsButtons">
<CustomButton
color="contrast"
onClick={() => {
setIsOpen(true);
}}
icon={<IconMicroscope />}
text="Создать эксперимент"
/>
</div>
)}
<div className="experimentsButtons">
<CustomButton
color="contrast"
onClick={() => {
setIsOpen(true);
}}
icon={<IconMicroscope />}
text="Создать эксперимент"
/>
</div>
<div className="ExperimentsPage">
<PaginationContainer
numberOfPages={Math.ceil(total_experiments / page_size)}
isLoading={teams == undefined}
activePage={cur_page}
setPage={(page_num) => {
getUserExperiments({ page_num: page_num, page_size: page_size })
.then((expData) => {
setExperiments(expData.experiments);
set_total_experiments(expData.total_experiments);
set_page_size(expData.page_size);
set_cur_page(expData.cur_page);
set_is_loading(false);
})
.catch(() => {
set_is_loading(false);
});
}}
>
{experiments.map((exp: ExperimentData) => {
return <ExperimentsListCard experiment={exp} />;
})}
{teams && teams.length == 0 && (
<Alert title="Команды не найдены" color="red">
Создайте или войдите в команду чтобы начать работу с
экспериментами
</Alert>
)}
{teams &&
teams.length != 0 &&
experiments.length == 0 &&
!is_loading_ && (
<Alert title="Экспериментов нету" color="blue">
Вы еще не создали не один эксперимент
</Alert>
)}
</PaginationContainer>
</div>
<PaginationContainer
numberOfPages={1}
isLoading={teams == undefined}
activePage={1}
setPage={() => {}}
>
{experiments.map((exp: Experiment) => {
return (
<ExperimentsListCard
experiment={{
id: exp.id,
name: exp.name,
description: exp.description,
team_id: exp.team_id,
tasks_ids: exp.tasks_ids,
date_created: exp.date_created,
experiment_status: exp.experiment_status,
experiment_type: "A",
}}
team={teams?.find((team) => team.team_id == exp.team_id)}
/>
);
})}
{teams && teams.length == 0 && (
<Alert title="Команды не найдены" color="red">
Создайте или войдите в команду чтобы начать работу с
экспериментами
</Alert>
)}
{teams && teams.length != 0 && experiments.length == 0 && (
<Alert title="Экспериментов нету" color="blue">
Вы еще не создали не один эксперимент
</Alert>
)}
</PaginationContainer>
</div>
</>
);

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,39 +116,77 @@ function TaskPage() {
marginBottom: "15px",
}}
>
<div className="experimentButtons">
<CustomButton
color="accent"
onClick={() => {}}
icon={<IconPlus />}
text="Сохранить"
/>
<CustomButton
color="red"
style="outline"
onClick={() => {}}
icon={<IconCancel />}
text="Отменить"
/>
{(!instance?.simulation_result ||
instance?.simulation_result?.status == "DRAFT") && (
<>
<div className="taskButtons">
<CustomButton
color="accent"
onClick={saveData}
disabled={
JSON.stringify(data) ==
JSON.stringify(instance?.instance_data) &&
instance?.name == name &&
(instance?.description || "") == (descr || "")
}
icon={<IconPlus />}
text="Сохранить"
/>
<CustomButton
color="red"
style="outline"
onClick={ResetData}
icon={<IconCancel />}
text="Отменить"
disabled={
JSON.stringify(data) ==
JSON.stringify(instance?.instance_data) &&
instance?.name == name &&
(instance?.description || "") == (descr || "")
}
/>
</div>
</>
)}
<div>
<SimpleGrid verticalSpacing="lg" cols={2}>
<Title size="lg">Имя:</Title>
<TextInput
size="md"
value={name}
onChange={
(e) => {
setName(e.target.value);
} //set_username(e.currentTarget.value)
}
/>
<Title size="lg">Описание:</Title>
<TextInput
size="md"
value={descr}
onChange={
(e) => {
setDescr(e.target.value);
} //set_email(e.currentTarget.value)
}
/>
</SimpleGrid>
</div>
</div>
<IframePlugin
pluginUrl={
window.location.origin +
"/" +
import.meta.env.VITE_BASE_PATH +
"/" +
"index.html"
}
mode="editor"
taskData={{
id: Number(task_id),
name: "Молекула 1",
description: "",
data: data,
}}
onUpdate={setData}
/>
{instance && (
<IframePlugin
plugin={loadedHtmlFiles.get(1) || ""}
mode="Editor"
taskData={JSON.stringify(data)}
qubits_needed={qubits_needed || 0}
onUpdate={(data, qubits_need) => {
setData(JSON.parse(data));
set_qubits_needed(qubits_need);
}}
index={instance.instance_id + reload}
simProgress={JSON.stringify(progress)}
/>
)}
</div>
</>
);

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);
}
});
@@ -66,7 +66,7 @@ function TeamPage() {
profile &&
!is_loading
) {
getTeamSystems(Number(team_id), cur_page, 9 )
getTeamSystems(Number(team_id), cur_page, 9)
.then((systems) => {
if (systems) {
set_machines(systems);
@@ -210,8 +210,8 @@ function TeamPage() {
cur_team?.description == team_descr
}
onClick={() => {
set_team_name(cur_team?.name);
set_team_descr(cur_team?.description);
set_team_name(cur_team?.name || "");
set_team_descr(cur_team?.description || "");
}}
/>
</Grid.Col>
@@ -252,7 +252,7 @@ function TeamPage() {
})}
</div>
)}
{!cur_team && !is_loading && (
{!cur_team && !is_loading_this && (
<Alert color="red">
<Center>
{" "}

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,52 +223,126 @@ function UserPage() {
overlayProps={{ radius: "sm", blur: 2 }}
loaderProps={{ size: 50, type: "dots" }}
/>
<SimpleGrid verticalSpacing="lg" cols={2}>
<Title size="lg">Имя пользователя:</Title>
<TextInput
size="md"
value={username}
onChange={(e) => set_username(e.currentTarget.value)}
/>
<Title size="lg">Почта:</Title>
<TextInput
size="md"
value={email}
onChange={(e) => set_email(e.currentTarget.value)}
/>
<CustomButton
color="accent"
text="Сохранить"
onClick={updateData}
disabled={
!(
profile != null &&
((profile.username != undefined &&
profile.username != username) ||
(profile.email != undefined && profile.email != email))
)
}
/>
<CustomButton
color="error"
text="Отменить"
onClick={() => {
if (profile && profile.email && profile.username) {
set_username(profile.username);
set_email(profile.email);
}
<Grid>
<Grid.Col span={4} style={{ height: "100%" }}>
<Center>
<div style={{ display: "flex", flexDirection: "column" }}>
<Title size="xl">Фотография профиля</Title>
</div>
</Center>
<Center style={{ padding: "15px" }}>
<Avatar size="xl" src={profile_picture_path} radius="xl" />
{is_editing_path && (
<FileInput
placeholder="Выберите изображение"
accept="image/png,image/jpeg,image/jpg,image/webp"
value={selectedFile}
onChange={handleFileChange}
leftSection={<IconUpload size={rem(14)} />}
clearable
style={{ width: "100%" }}
disabled={isUploading}
/>
)}
</Center>
<div>
{!is_editing_path ? (
<CustomButton
style="outline"
color="contrast"
text="Изменить"
onClick={() => {
set_is_editing_path(true);
}}
/>
) : (
<Group justify="center" mt="md">
<CustomButton
style="outline"
color="red"
text="Отменить"
onClick={cancelUpload}
disabled={isUploading}
/>
<CustomButton
style="color"
color="accent"
text="Сохранить"
onClick={update_pfp}
disabled={!selectedFile}
/>
</Group>
)}
</div>
</Grid.Col>
<Grid.Col
span={4}
style={{
flexGrow: 1,
justifyContent: "space-between",
display: "flex",
flexDirection: "column",
}}
disabled={
!(
profile != null &&
((profile.username != undefined &&
profile.username != username) ||
(profile.email != undefined && profile.email != email))
)
}
/>
</SimpleGrid>
>
<Title size="lg">Имя пользователя:</Title>
<Title size="lg">Почта:</Title>
<CustomButton
color="accent"
text="Сохранить"
onClick={updateData}
disabled={
!(
profile != null &&
((profile.username != undefined &&
profile.username != username) ||
(profile.email != undefined &&
profile.email != email))
)
}
/>
</Grid.Col>
<Grid.Col
span={4}
style={{
flexGrow: 1,
justifyContent: "space-between",
display: "flex",
flexDirection: "column",
}}
>
<TextInput
size="md"
value={username}
onChange={(e) => set_username(e.currentTarget.value)}
/>
<TextInput
size="md"
value={email}
onChange={(e) => set_email(e.currentTarget.value)}
/>
<CustomButton
color="error"
text="Отменить"
onClick={() => {
if (profile && profile.email && profile.username) {
set_username(profile.username);
set_email(profile.email);
}
}}
disabled={
!(
profile != null &&
((profile.username != undefined &&
profile.username != username) ||
(profile.email != undefined &&
profile.email != email))
)
}
/>
</Grid.Col>
</Grid>
<Divider my="lg" />
<SimpleGrid verticalSpacing={"lg"} cols={2}>
<Title
@@ -266,60 +410,12 @@ function UserPage() {
/>
</div>
</SimpleGrid>
<Divider my="lg" />
<div style={{ display: "flex", flexDirection: "column" }}>
<Title size="lg">Фотография профиля</Title>
<Space my="sm" />
{!is_editing_path ? (
<img height={350} width={350} src={profile_picture_path} />
) : (
<TextInput
size="md"
value={pfp_path}
onChange={(e) => set_pfp_path(e.currentTarget.value)}
/>
)}
<Space my="sm" />
<div style={{ width: "15em" }}>
{!is_editing_path ? (
<CustomButton
style="outline"
color="contrast"
text="Изменить"
onClick={() => {
set_is_editing_path(true);
}}
/>
) : (
<>
<CustomButton
style="outline"
color="contrast"
text="Сохранить"
onClick={() => {
update_pfp();
set_is_editing_path(false);
}}
/>
<Space my="sm" />
<CustomButton
style="outline"
color="red"
text="Отменить"
onClick={() => {
set_is_editing_path(false);
set_pfp_path(profile_picture_path);
}}
/>
</>
)}
</div>
</div>
</div>
</Tabs.Panel>
<Tabs.Panel value="preference">
<Title size={"lg"}>Тема приложения:</Title>
<Space h="md" />
<Switch
size="xl"
defaultChecked={theme == "light"}