v1.0
- added task rendering instead of molecule - fied a lot of errors - changes experiment page to show generic experiment info
This commit is contained in:
@@ -21,6 +21,12 @@ import TeamInMachineListCard from "Components/ListCard/TeamListCard/TeamInMachin
|
||||
import { AddTeamToDevice } from "Modals/AddTeamToDevice/AddTeamToDevice";
|
||||
import { getShortTeamsList } from "Api/QuantumBackend/TeamManagement";
|
||||
|
||||
const colors = {
|
||||
ONLINE: "var(--mantine-color-green-7)",
|
||||
OFFLINE: "var(--mantine-color-red-7)",
|
||||
BUSY: "var(--mantine-color-yellow-5)",
|
||||
};
|
||||
|
||||
function DevicePage() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { device_id } = useParams();
|
||||
@@ -107,7 +113,16 @@ function DevicePage() {
|
||||
<Space h="xl" />
|
||||
<div style={{ display: "flex", flexDirection: "row", gap: "15px" }}>
|
||||
<Text size="md">Статус: </Text>
|
||||
<Pill className="ExperimentPill">
|
||||
<Pill
|
||||
className="ExperimentPill"
|
||||
style={{
|
||||
color: "white",
|
||||
backgroundColor:
|
||||
colors[
|
||||
cur_device?.system.status as "ONLINE" | "OFFLINE" | "BUSY"
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Text size="md">{cur_device?.system.status}</Text>
|
||||
</Pill>
|
||||
<div className="dateContainer">
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Box, Divider, TableOfContents, Title } from "@mantine/core";
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
TableOfContents,
|
||||
Title,
|
||||
Text,
|
||||
List,
|
||||
Code,
|
||||
Anchor,
|
||||
} from "@mantine/core";
|
||||
import "./DocumentationPage.css";
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
@@ -6,15 +15,19 @@ function DocumentationPage() {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Documentation | QMolSim</title>
|
||||
<title>Документация | QMolSim</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="See the documentation on how to setup and use the qunatum computational system"
|
||||
content="Полное руководство пользователя по системе распределенного квантово-химического расчета QMolSim."
|
||||
/>
|
||||
</Helmet>
|
||||
<Title order={1} className="docTitle">
|
||||
Документация
|
||||
Руководство пользователя
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
Автоматизированная система распределенного расчета энергии основного
|
||||
состояния молекул
|
||||
</Text>
|
||||
<Divider />
|
||||
<div className="DocumentationPage">
|
||||
<Box className="tableOfContents" visibleFrom="md">
|
||||
@@ -26,24 +39,291 @@ function DocumentationPage() {
|
||||
minDepthToOffset={0}
|
||||
depthOffset={20}
|
||||
scrollSpyOptions={{
|
||||
selector: "section h1, h2",
|
||||
selector: "section h1, section h2, section h3",
|
||||
}}
|
||||
className=""
|
||||
getControlProps={({ data }) => ({
|
||||
onClick: () =>
|
||||
data
|
||||
.getNode()
|
||||
.scrollIntoView({ behavior: "smooth", block: "center" }),
|
||||
.scrollIntoView({ behavior: "smooth", block: "start" }),
|
||||
children: data.value,
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
<div className="contents">
|
||||
<section id="introduction" style={{ height: 1000 }}>
|
||||
{/* ================= 1 ВВЕДЕНИЕ ================= */}
|
||||
<section id="introduction">
|
||||
<Title order={1}>1. Введение</Title>
|
||||
|
||||
<Title order={2} mt="md" id="application-area">
|
||||
1.1 Область применения
|
||||
</Title>
|
||||
<Text>Требования настоящего документа применяются при:</Text>
|
||||
<List>
|
||||
<List.Item>предварительных комплексных испытаниях;</List.Item>
|
||||
<List.Item>опытной эксплуатации;</List.Item>
|
||||
<List.Item>приемочных испытаниях;</List.Item>
|
||||
<List.Item>промышленной эксплуатации.</List.Item>
|
||||
</List>
|
||||
|
||||
<Title order={2} mt="md" id="capabilities">
|
||||
1.2 Краткое описание возможностей
|
||||
</Title>
|
||||
<Text>
|
||||
Автоматизированная система распределенного расчета энергии
|
||||
основного состояния молекул с помощью квантовых алгоритмов
|
||||
представляет собой распределенный веб-сервис, предназначенный для
|
||||
выполнения ресурсоемких квантово-химических расчетов с
|
||||
использованием гибридной архитектуры, состоящей из центрального
|
||||
сервера и распределенных квантовых симуляторов.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="cap-mgmt">
|
||||
Управление командной работой
|
||||
</Title>
|
||||
<Text>
|
||||
Пользователи могут создавать команды, приглашать других
|
||||
исследователей, назначать права доступа.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="cap-nodes">
|
||||
Подключение вычислительных систем
|
||||
</Title>
|
||||
<Text>
|
||||
Исследователи регистрируют в системе свои вычислительные узлы. Для
|
||||
каждого узла исследователь задает максимальное количество кубитов,
|
||||
а также предоставляет доступ командам на использование устройства.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="cap-experiments">
|
||||
Создание и запуск экспериментов
|
||||
</Title>
|
||||
<Text>
|
||||
В рамках команды пользователь создает эксперимент (набор задач для
|
||||
разных молекул). Для каждой задачи загружается или редактируется
|
||||
структура молекулы в формате XYZ, задаются квантово-химические
|
||||
параметры.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="cap-vqe">
|
||||
Распределенные вычисления VQE
|
||||
</Title>
|
||||
<Text>
|
||||
При запуске эксперимента система автоматически распределяет задачи
|
||||
по доступным вычислительным узлам с учетом их ограничений по числу
|
||||
кубит. В процессе расчета на сервер передаются промежуточные
|
||||
результаты.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="cap-fault">
|
||||
Отказоустойчивость и восстановление
|
||||
</Title>
|
||||
<Text>
|
||||
Каждый вычислительный узел каждые 5 секунд отправляет сигнал о
|
||||
своей работоспособности. При выходе узла из строя незавершенная
|
||||
задача автоматически перенаправляется в очередь и назначается на
|
||||
другой узел с сохранением промежуточных весов оптимизации.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="cap-vis">
|
||||
Визуализация молекул
|
||||
</Title>
|
||||
<Text>
|
||||
Для каждой задачи доступна интерактивная 3D-визуализация молекулы
|
||||
в шаростержневой модели.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="cap-import">
|
||||
Импорт молекулярных данных
|
||||
</Title>
|
||||
<Text>
|
||||
Система поддерживает преобразование молекул в требуемый формат из
|
||||
большинства существующих химических форматов.
|
||||
</Text>
|
||||
</section>
|
||||
<section id="quick-start" style={{ height: 1000 }}>
|
||||
<Title order={2}>1.1 Быстрое начало</Title>
|
||||
|
||||
{/* ================= 2 НАЗНАЧЕНИЕ И УСЛОВИЯ ================= */}
|
||||
<section id="purpose-conditions" style={{ marginTop: "2rem" }}>
|
||||
<Title order={1}>2. Назначение и условия применения</Title>
|
||||
|
||||
<Title order={2} mt="md" id="purpose">
|
||||
2.1 Назначение системы
|
||||
</Title>
|
||||
<Text>
|
||||
Система предназначена для автоматизированного распределенного
|
||||
расчета энергии основного состояния молекул с использованием
|
||||
квантового алгоритма VQE. Она обеспечивает создание команд
|
||||
исследователей с настройкой прав доступа, автоматическое
|
||||
распределение вычислительных задач между доступными узлами,
|
||||
мониторинг состояния вычислений и восстановление прогресса расчета
|
||||
при сбое отдельных вычислительных систем. Применение системы
|
||||
позволяет повысить скорость проведения квантово-химических
|
||||
расчетов и снизить нагрузку на пользователя по управлению
|
||||
вычислительным процессом. Система ориентирована на специалистов в
|
||||
области квантовой химии и вычислительных технологий.
|
||||
</Text>
|
||||
|
||||
<Title order={2} mt="md" id="tech-reqs">
|
||||
2.2 Требования к техническим средствам
|
||||
</Title>
|
||||
|
||||
<Title order={3} mt="sm" id="client-browser">
|
||||
Клиент-браузер:
|
||||
</Title>
|
||||
<List>
|
||||
<List.Item>Оперативная память от 4 Гб;</List.Item>
|
||||
<List.Item>Свободное пространство на диске от 2 Гб;</List.Item>
|
||||
<List.Item>Процессор 4-ядерный с частотой от 2 ГГц;</List.Item>
|
||||
<List.Item>Скорость подключения в интернет от 50 Мб/c;</List.Item>
|
||||
<List.Item>
|
||||
Наличие манипулятора "мышь" или аналогичного устройства для
|
||||
взаимодействия с интерфейсом;
|
||||
</List.Item>
|
||||
<List.Item>Наличие Клавиатуры.</List.Item>
|
||||
</List>
|
||||
|
||||
<Title order={3} mt="sm" id="client-compute">
|
||||
Клиент-ВС:
|
||||
</Title>
|
||||
<List>
|
||||
<List.Item>Оперативная память от 8 Гб;</List.Item>
|
||||
<List.Item>Свободное пространство на диске от 5 Гб;</List.Item>
|
||||
<List.Item>
|
||||
Процессор 8-ядерный с частотой от 2-4,4 ГГц;
|
||||
</List.Item>
|
||||
<List.Item>Скорость подключения в интернет от 50 Мб/с;</List.Item>
|
||||
<List.Item>
|
||||
Наличие манипулятора "мышь" или аналогичного устройства для
|
||||
взаимодействия с интерфейсом;
|
||||
</List.Item>
|
||||
<List.Item>Наличие Клавиатуры.</List.Item>
|
||||
</List>
|
||||
|
||||
<Title order={2} mt="md" id="software-reqs">
|
||||
2.3 Требования к программным средствам
|
||||
</Title>
|
||||
|
||||
<Title order={3} mt="sm" id="sw-browser">
|
||||
Клиент-браузер:
|
||||
</Title>
|
||||
<Text>
|
||||
Браузер (Safari 18.1.1, Яндекс Браузер 25.2.1, Google Chrome
|
||||
110.0.5481.100)
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="sw-compute">
|
||||
Клиент-ВС:
|
||||
</Title>
|
||||
<List>
|
||||
<List.Item>ОС Windows 10, Windows 11, MacOS, Linux</List.Item>
|
||||
<List.Item>
|
||||
Браузер (Safari 18.1.1, Яндекс Браузер 25.2.1, Google Chrome
|
||||
110.0.5481.100)
|
||||
</List.Item>
|
||||
<List.Item>Docker</List.Item>
|
||||
<List.Item>Docker-compose v2</List.Item>
|
||||
</List>
|
||||
|
||||
<Title order={2} mt="md" id="exec-conditions">
|
||||
3. Условия выполнения программы
|
||||
</Title>
|
||||
<Text>
|
||||
Для работы системы требуется веб-браузер, поддерживающий
|
||||
современные функции JavaScript (Google Chrome версии 110 и выше,
|
||||
Яндекс Браузер версии 25.2.1 и выше, Safari версии 18.1.1 и выше).
|
||||
Доступ к системе осуществляется через веб-интерфейс по адресу,
|
||||
предоставленному администратором. Для работы вычислительных узлов
|
||||
дополнительно требуется установленный Docker и Docker Compose v2
|
||||
на каждой подключаемой вычислительной системе. Необходимо наличие
|
||||
постоянного сетевого подключения к серверу для всех
|
||||
взаимодействующих компонентов системы.
|
||||
</Text>
|
||||
</section>
|
||||
|
||||
{/* ================= 4 ВЫПОЛНЕНИЕ ПРОГРАММЫ ================= */}
|
||||
<section id="execution" style={{ marginTop: "2rem" }}>
|
||||
<Title order={1}>4. Выполнение программы</Title>
|
||||
|
||||
<Title order={2} mt="md" id="install">
|
||||
4.1 Инсталяция/деинсталяция
|
||||
</Title>
|
||||
<Text>
|
||||
Клиент-браузер инсталляции и деинсталляции не требуется, для
|
||||
работы необходимо только наличие на системе совместимого браузера.
|
||||
</Text>
|
||||
|
||||
<Title order={3} mt="sm" id="install-compute">
|
||||
Для инсталляции клиента-ВС:
|
||||
</Title>
|
||||
<List>
|
||||
<List.Item>
|
||||
На системе необходимо наличие docker и docker-compose v2
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
Необходимо скачать контейнер с помощью команды:{" "}
|
||||
<Code>docker pull git.deowl.ru/vkrb/client:0.1.0</Code>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
Затем скачать файл docker-compose с помощью команды:{" "}
|
||||
<Code>
|
||||
curl -O
|
||||
"https://git.deowl.ru/vkrb/local_quantum_simulator/raw/branch/main/docker-compose.yml"
|
||||
</Code>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
Наконец, в той же папке необходимо создать файл переменных среды
|
||||
с названием ".env" и содержимым:
|
||||
<Code block mt="xs">
|
||||
{`PORT=5001
|
||||
STORAGE_PATH="/storage"
|
||||
RABBITMQ_HOST=rabbit.deowl.ru
|
||||
RABBITMQ_PORT=5672
|
||||
KEYCLOAK_URL=https://quantum-auth.deowl.ru
|
||||
KEYCLOAK_REALM_NAME=quant_sim-realm
|
||||
KEACLOAK_CLIENT_ID=local_quantum_sim
|
||||
QUANTUM_BACKEND_URL=https://quantum.deowl.ru`}
|
||||
</Code>
|
||||
</List.Item>
|
||||
</List>
|
||||
|
||||
<Title order={3} mt="md" id="uninstall-compute">
|
||||
Для деинсталляции клиента-ВС:
|
||||
</Title>
|
||||
<List>
|
||||
<List.Item>
|
||||
Удаляем файлы «docker-compose.yml», «.env» и папку
|
||||
«localStorage» (при ее наличие)
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
Удаляем установленное изображение с помощью команды:{" "}
|
||||
<Code>docker image rm git.deowl.ru/vkrb/client:0.1.0</Code>
|
||||
</List.Item>
|
||||
</List>
|
||||
|
||||
<Title order={2} mt="md" id="start-stop">
|
||||
4.2 Запуск / Остановка программы
|
||||
</Title>
|
||||
<Text>
|
||||
Клиент-браузер может быть открыт по ссылке:{" "}
|
||||
<Anchor href="http://quantum.deowl.ru/">
|
||||
http://quantum.deowl.ru/
|
||||
</Anchor>
|
||||
</Text>
|
||||
<Text mt="sm">
|
||||
Для запуска клиента-ВС необходимо выполнить команду, находясь в
|
||||
папке с файлом «docker-compose.yml»:{" "}
|
||||
<Code>docker compose up --d</Code>
|
||||
</Text>
|
||||
<Text mt="sm">
|
||||
Для остановки клиента-ВС: <Code>docker compose down</Code>
|
||||
</Text>
|
||||
<Text mt="sm">
|
||||
Для первичного подключения и мониторинга статуса клиента-ВС
|
||||
необходимо открыть ссылку:{" "}
|
||||
<Anchor href="http://localhost:5001/">
|
||||
http://localhost:5001/
|
||||
</Anchor>
|
||||
</Text>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.experimentButtons {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
|
||||
flex-direction: row;
|
||||
justify-content: right;
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
flex-wrap: nowrap;
|
||||
text-wrap: nowrap;
|
||||
* {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,221 @@
|
||||
import { Alert, Center, Text } from "@mantine/core";
|
||||
import {
|
||||
Alert,
|
||||
Center,
|
||||
LoadingOverlay,
|
||||
Text,
|
||||
UnstyledButton,
|
||||
SimpleGrid,
|
||||
Title,
|
||||
TextInput,
|
||||
Button,
|
||||
Group,
|
||||
Card,
|
||||
Badge,
|
||||
Collapse,
|
||||
ActionIcon,
|
||||
} from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import "./ExperimentPage.css";
|
||||
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
|
||||
//import { NewMoleculeModal } from "Modals/NewMolecule/NewMolecule";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { IconPlus, IconSettings } from "@tabler/icons-react";
|
||||
import {
|
||||
IconPlus,
|
||||
IconReload,
|
||||
IconEdit,
|
||||
IconX,
|
||||
IconCheck,
|
||||
IconChevronUp,
|
||||
IconChevronDown,
|
||||
IconCancel,
|
||||
} from "@tabler/icons-react";
|
||||
import { useParams } from "react-router";
|
||||
import CustomButton from "Components/CustomButton/CustomButton";
|
||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||
import type { ExperimentData, InstanceData } from "Types/Experiment/Experiment";
|
||||
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||
import {
|
||||
getExperimentById,
|
||||
getExperimentInstances,
|
||||
getFrontendFile,
|
||||
startExperiment,
|
||||
updateExperiment,
|
||||
} from "Api/QuantumBackend/ExperimentsManagment";
|
||||
import InstancesListCard from "Components/ListCard/TaskListCard/TaskListCard";
|
||||
import { NewInstanceModal } from "Modals/NewTask/NewTask";
|
||||
|
||||
function ExperimentPage() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isOpened, setIsOpen] = useState(false);
|
||||
const { experiment_id } = useParams();
|
||||
const { experiments, addTask } = useExperimentStore();
|
||||
const { experiments, addExperiment, instances, setInstances } =
|
||||
useExperimentStore();
|
||||
const [experiment, set_experiment] = useState<ExperimentData | undefined>(
|
||||
experiments.find((exp) => {
|
||||
return exp.id == Number(experiment_id);
|
||||
}),
|
||||
);
|
||||
|
||||
const experiment = experiments.find((exp) => {
|
||||
return exp.id == Number(experiment_id);
|
||||
});
|
||||
const { profile, is_loading } = useAuthenticationStore();
|
||||
const [is_loading_, set_is_loading] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editedName, setEditedName] = useState("");
|
||||
const [editedDesc, setEditedDesc] = useState("");
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const { loadedHtmlFiles, addLoadedHtml } = useExperimentStore();
|
||||
|
||||
const [total_experiment_inst, set_total_experiment_inst] =
|
||||
useState<number>(0);
|
||||
const [page_size, set_page_size] = useState<number>(5);
|
||||
const [cur_page, set_cur_page] = useState<number>(1);
|
||||
|
||||
const saveExperimentDetails = () => {
|
||||
if (experiment && editedName.trim()) {
|
||||
updateExperiment({
|
||||
experiment_id: experiment.id,
|
||||
name: editedName,
|
||||
description: editedDesc,
|
||||
})
|
||||
.then((updated) => {
|
||||
const updatedExperiment = {
|
||||
...experiment,
|
||||
name: updated.name,
|
||||
description: updated.description,
|
||||
};
|
||||
addExperiment(updatedExperiment);
|
||||
set_experiment(updatedExperiment);
|
||||
setIsEditing(false);
|
||||
notifications.show({
|
||||
title: "Успех",
|
||||
message: "Информация об эксперименте обновлена",
|
||||
color: "green",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.show({
|
||||
title: "Ошибка",
|
||||
message: "Не удалось обновить информацию",
|
||||
color: "red",
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEditing = () => {
|
||||
if (experiment) {
|
||||
setEditedName(experiment.name);
|
||||
setEditedDesc(experiment.description || "");
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startEditing = () => {
|
||||
if (experiment) {
|
||||
setEditedName(experiment.name);
|
||||
setEditedDesc(experiment.description || "");
|
||||
setIsEditing(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExperimentStart = () => {
|
||||
if (experiment) {
|
||||
startExperiment({ experiment_id: experiment.id })
|
||||
.then(() => {
|
||||
getExperimentById(Number(experiment_id))
|
||||
.then((exp) => {
|
||||
addExperiment(exp);
|
||||
set_experiment(exp);
|
||||
getExperimentInstances(exp.id, cur_page, page_size)
|
||||
.then((inst) => {
|
||||
setInstances(inst.instances);
|
||||
set_cur_page(inst.cur_page);
|
||||
set_total_experiment_inst(inst.total_instances);
|
||||
set_page_size(inst.page_size);
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
getFrontendFile(exp.experiment_type.id).then((file) => {
|
||||
addLoadedHtml(exp.experiment_type.id, file);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.show({
|
||||
title: "Ошибка",
|
||||
message:
|
||||
"Не удалось начать эксперимент, проверьте наличие задач и их правильность",
|
||||
color: "red",
|
||||
icon: <IconCancel />,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (profile && !is_loading) {
|
||||
if (!experiment) {
|
||||
getExperimentById(Number(experiment_id))
|
||||
.then((exp) => {
|
||||
addExperiment(exp);
|
||||
set_experiment(exp);
|
||||
getExperimentInstances(exp.id, cur_page, page_size)
|
||||
.then((inst) => {
|
||||
setInstances(inst.instances);
|
||||
set_cur_page(inst.cur_page);
|
||||
set_total_experiment_inst(inst.total_instances);
|
||||
set_page_size(inst.page_size);
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
getFrontendFile(exp.experiment_type.id).then((file) => {
|
||||
addLoadedHtml(exp.experiment_type.id, file);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
} else {
|
||||
if (
|
||||
(instances.length == 0 && cur_page != 0) ||
|
||||
(total_experiment_inst > page_size &&
|
||||
instances.length != page_size &&
|
||||
cur_page != Math.ceil(total_experiment_inst / page_size))
|
||||
) {
|
||||
getExperimentInstances(
|
||||
experiment.id,
|
||||
Math.min(cur_page, Math.ceil(total_experiment_inst / page_size)),
|
||||
page_size,
|
||||
)
|
||||
.then((inst) => {
|
||||
if (inst.instances.length == 0) {
|
||||
set_cur_page(0);
|
||||
setInstances([]);
|
||||
} else {
|
||||
setInstances(inst.instances);
|
||||
set_cur_page(inst.cur_page);
|
||||
set_total_experiment_inst(inst.total_instances);
|
||||
set_page_size(inst.page_size);
|
||||
}
|
||||
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
getFrontendFile(experiment.experiment_type.id).then((file) => {
|
||||
addLoadedHtml(experiment.experiment_type.id, file);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [profile, is_loading, instances]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -24,82 +223,350 @@ function ExperimentPage() {
|
||||
<title>
|
||||
{experiment
|
||||
? "Experiment " + experiment.id + " | QMolSim"
|
||||
: "Error |QmolSim"}
|
||||
: "Error | QMolSim"}
|
||||
</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="See the documentation on how to setup and use the qunatum computational system"
|
||||
content="See the documentation on how to setup and use the quantum computational system"
|
||||
/>
|
||||
</Helmet>
|
||||
{experiment && (
|
||||
<div className="ExperimentPage">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
marginBottom: "15px",
|
||||
}}
|
||||
>
|
||||
<div className="experimentButtons">
|
||||
<CustomButton
|
||||
color="contrast"
|
||||
onClick={() => {
|
||||
addTask(Number(experiment_id) || 0, {
|
||||
id: 1,
|
||||
name: "Задача 1",
|
||||
description: "",
|
||||
data: {},
|
||||
});
|
||||
setIsOpen(true);
|
||||
}}
|
||||
icon={<IconPlus />}
|
||||
text="Добавить задачу"
|
||||
/>
|
||||
<CustomButton
|
||||
color="contrast"
|
||||
style="outline"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
icon={<IconSettings />}
|
||||
text="Параметры эксперимента"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{experiment.tasks_ids.length > 0 && (
|
||||
<PaginationContainer
|
||||
numberOfPages={1}
|
||||
isLoading={false}
|
||||
activePage={1}
|
||||
setPage={() => {}}
|
||||
<div className="ExperimentPage">
|
||||
{experiment && (
|
||||
<NewInstanceModal
|
||||
experiment_id={experiment.id}
|
||||
isOpened={isOpened}
|
||||
setIsOpened={setIsOpen}
|
||||
/>
|
||||
)}
|
||||
<LoadingOverlay visible={is_loading_} zIndex={1000} />
|
||||
{experiment && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
marginBottom: "15px",
|
||||
}}
|
||||
>
|
||||
{experiment.tasks_ids.map((task: number) => {
|
||||
return <div>{task}</div>;
|
||||
})}
|
||||
</PaginationContainer>
|
||||
)}
|
||||
{experiment.tasks_ids.length == 0 && (
|
||||
<Alert>
|
||||
<Center>
|
||||
<Text size={"xl"} c="contrast">
|
||||
Нет задач
|
||||
</Text>
|
||||
</Center>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!experiment && (
|
||||
<Alert color="red">
|
||||
<Center>
|
||||
{" "}
|
||||
<Text c="contrast" size={"xl"}>
|
||||
Ошибка. Эксперимент не найден
|
||||
</Text>{" "}
|
||||
</Center>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="experimentButtons">
|
||||
{experiment.status == "DRAFT" && (
|
||||
<>
|
||||
<CustomButton
|
||||
color="accent"
|
||||
onClick={() => {
|
||||
handleExperimentStart();
|
||||
}}
|
||||
icon={<IconPlus />}
|
||||
text="Начать эксперимент"
|
||||
/>
|
||||
<CustomButton
|
||||
color="contrast"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
icon={<IconPlus />}
|
||||
text="Добавить задачу"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{experiment.status != "DRAFT" && (
|
||||
<UnstyledButton
|
||||
onClick={() => {
|
||||
getExperimentById(Number(experiment_id))
|
||||
.then((exp) => {
|
||||
addExperiment(exp);
|
||||
set_experiment(exp);
|
||||
getExperimentInstances(exp.id)
|
||||
.then((inst) => {
|
||||
setInstances(inst.instances);
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
getFrontendFile(exp.experiment_type.id).then(
|
||||
(file) => {
|
||||
addLoadedHtml(exp.experiment_type.id, file);
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconReload />
|
||||
</UnstyledButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Experiment Information Card */}
|
||||
{/* Experiment Information Card */}
|
||||
<Card withBorder mb="lg" shadow="sm">
|
||||
<Card.Section withBorder inheritPadding py="sm">
|
||||
<Group justify="space-between">
|
||||
<Group gap="xs">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<IconChevronUp size={16} />
|
||||
) : (
|
||||
<IconChevronDown size={16} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
<Title order={3}>Информация об эксперименте</Title>
|
||||
</Group>
|
||||
{!isEditing ? (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
leftSection={<IconEdit size={16} />}
|
||||
onClick={startEditing}
|
||||
>
|
||||
Редактировать
|
||||
</Button>
|
||||
) : (
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="filled"
|
||||
color="green"
|
||||
leftSection={<IconCheck size={14} />}
|
||||
onClick={saveExperimentDetails}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline"
|
||||
color="red"
|
||||
leftSection={<IconX size={14} />}
|
||||
onClick={cancelEditing}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</Card.Section>
|
||||
|
||||
<Collapse expanded={isExpanded} style={{ padding: "15px" }}>
|
||||
<Card.Section inheritPadding py="md">
|
||||
{!isEditing ? (
|
||||
<SimpleGrid cols={4} spacing="lg" verticalSpacing="md">
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
ID
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.id}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Тип
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.experiment_type.name}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Название
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.name}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Статус
|
||||
</Text>
|
||||
<Badge
|
||||
size="md"
|
||||
variant="filled"
|
||||
color={
|
||||
experiment.status === "DRAFT" ? "yellow" : "green"
|
||||
}
|
||||
radius="sm"
|
||||
>
|
||||
{experiment.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Команда
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.team.team_name}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Количество задач
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.instances_count}
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 2" }}>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Описание
|
||||
</Text>
|
||||
<Text size="md">
|
||||
{experiment.description || "Нет описания"}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Создан
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
{new Date(experiment.created_at).toLocaleString()}
|
||||
</Text>
|
||||
</div>
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
<SimpleGrid cols={4} spacing="lg" verticalSpacing="md">
|
||||
<div style={{ gridColumn: "span 1" }}>
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
mb={4}
|
||||
>
|
||||
Название
|
||||
</Text>
|
||||
<TextInput
|
||||
value={editedName}
|
||||
onChange={(e) => setEditedName(e.target.value)}
|
||||
placeholder="Введите название"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
mb={4}
|
||||
>
|
||||
Описание
|
||||
</Text>
|
||||
<TextInput
|
||||
value={editedDesc}
|
||||
onChange={(e) => setEditedDesc(e.target.value)}
|
||||
placeholder="Введите описание"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
ID
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.id}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Тип
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.experiment_type.name}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Команда
|
||||
</Text>
|
||||
<Text size="md" fw={500}>
|
||||
{experiment.team.team_name}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||
Статус
|
||||
</Text>
|
||||
<Badge
|
||||
size="md"
|
||||
variant="filled"
|
||||
color={
|
||||
experiment.status === "DRAFT" ? "yellow" : "green"
|
||||
}
|
||||
radius="sm"
|
||||
>
|
||||
{experiment.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</Card.Section>
|
||||
</Collapse>
|
||||
</Card>
|
||||
|
||||
{!is_loading_ && (
|
||||
<PaginationContainer
|
||||
numberOfPages={Math.ceil(total_experiment_inst / page_size)}
|
||||
isLoading={false}
|
||||
activePage={cur_page}
|
||||
setPage={(page_num) => {
|
||||
getExperimentInstances(experiment.id, page_num, page_size)
|
||||
.then((inst) => {
|
||||
setInstances(inst.instances);
|
||||
set_cur_page(inst.cur_page);
|
||||
set_total_experiment_inst(inst.total_instances);
|
||||
set_page_size(inst.page_size);
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{instances &&
|
||||
loadedHtmlFiles.get(experiment.experiment_type.id) &&
|
||||
instances.map((inst: InstanceData) => {
|
||||
return (
|
||||
<InstancesListCard
|
||||
instance={inst}
|
||||
plugin={
|
||||
loadedHtmlFiles.get(experiment.experiment_type.id) ||
|
||||
""
|
||||
}
|
||||
key={inst.instance_id}
|
||||
></InstancesListCard>
|
||||
);
|
||||
})}
|
||||
{instances.length == 0 && !is_loading && (
|
||||
<Alert>
|
||||
<Center>
|
||||
<Text size={"xl"} c="contrast">
|
||||
Нет задач
|
||||
</Text>
|
||||
</Center>
|
||||
</Alert>
|
||||
)}
|
||||
</PaginationContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!experiment && !is_loading_ && (
|
||||
<Alert color="red">
|
||||
<Center>
|
||||
{" "}
|
||||
<Text c="contrast" size={"xl"}>
|
||||
Ошибка. Эксперимент не найден
|
||||
</Text>{" "}
|
||||
</Center>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.experimentsButtons {
|
||||
|
||||
@@ -6,23 +6,39 @@ import { IconMicroscope } from "@tabler/icons-react";
|
||||
import { NewExperimentModal } from "Modals/NewExperiment/NewExperiment";
|
||||
import ExperimentsListCard from "Components/ListCard/ExperimentsListCard";
|
||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||
import type { Experiment } from "Types/Experiment/Experiment";
|
||||
import CustomButton from "Components/CustomButton/CustomButton";
|
||||
import { Alert } from "@mantine/core";
|
||||
import { getShortTeamsList } from "Api/QuantumBackend/TeamManagement";
|
||||
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||
import {
|
||||
type ExperimentData,
|
||||
type ExperimentTypeList,
|
||||
} from "Types/Experiment/Experiment";
|
||||
import {
|
||||
getExperimentTypes,
|
||||
getUserExperiments,
|
||||
} from "Api/QuantumBackend/ExperimentsManagment";
|
||||
|
||||
function ExperimentsPage() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { experiments } = useExperimentStore();
|
||||
const { experiments, setExperiments, setInstances } = useExperimentStore();
|
||||
const [teams, setTeams] =
|
||||
useState<{ team_id: number; team_name: string }[]>();
|
||||
|
||||
const { profile, is_loading } = useAuthenticationStore();
|
||||
const [is_loading_, set_is_loading] = useState(true);
|
||||
const [exp_types, set_exp_types] = useState<ExperimentTypeList[]>([]);
|
||||
const [total_experiments, set_total_experiments] = useState<number>(0);
|
||||
const [page_size, set_page_size] = useState<number>(6);
|
||||
const [cur_page, set_cur_page] = useState<number>(1);
|
||||
|
||||
useEffect(() => {
|
||||
if (profile || !is_loading)
|
||||
getShortTeamsList()
|
||||
if (profile && !is_loading) {
|
||||
setInstances([]);
|
||||
getExperimentTypes().then((exp_types) => {
|
||||
set_exp_types(exp_types);
|
||||
});
|
||||
getShortTeamsList({})
|
||||
.then((teams) => {
|
||||
if (teams) {
|
||||
setTeams(teams);
|
||||
@@ -31,6 +47,18 @@ function ExperimentsPage() {
|
||||
.catch(() => {
|
||||
setTeams([]);
|
||||
});
|
||||
getUserExperiments({ page_num: cur_page, page_size: page_size })
|
||||
.then((expData) => {
|
||||
setExperiments(expData.experiments);
|
||||
set_total_experiments(expData.total_experiments);
|
||||
set_page_size(expData.page_size);
|
||||
set_cur_page(expData.cur_page);
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
}
|
||||
}, [profile, is_loading]);
|
||||
|
||||
return (
|
||||
@@ -46,64 +74,59 @@ function ExperimentsPage() {
|
||||
isOpened={isOpen}
|
||||
setIsOpened={setIsOpen}
|
||||
teams={teams}
|
||||
types={exp_types.map((type) => {
|
||||
return { type_id: type.id, type_name: type.name };
|
||||
})}
|
||||
/>
|
||||
<div className="ExperimentsPage">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
marginBottom: "20px",
|
||||
}}
|
||||
>
|
||||
{teams && teams.length > 0 && (
|
||||
<div className="experimentsButtons">
|
||||
<CustomButton
|
||||
color="contrast"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
icon={<IconMicroscope />}
|
||||
text="Создать эксперимент"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="experimentsButtons">
|
||||
<CustomButton
|
||||
color="contrast"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
icon={<IconMicroscope />}
|
||||
text="Создать эксперимент"
|
||||
/>
|
||||
</div>
|
||||
<div className="ExperimentsPage">
|
||||
<PaginationContainer
|
||||
numberOfPages={Math.ceil(total_experiments / page_size)}
|
||||
isLoading={teams == undefined}
|
||||
activePage={cur_page}
|
||||
setPage={(page_num) => {
|
||||
getUserExperiments({ page_num: page_num, page_size: page_size })
|
||||
.then((expData) => {
|
||||
setExperiments(expData.experiments);
|
||||
set_total_experiments(expData.total_experiments);
|
||||
set_page_size(expData.page_size);
|
||||
set_cur_page(expData.cur_page);
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{experiments.map((exp: ExperimentData) => {
|
||||
return <ExperimentsListCard experiment={exp} />;
|
||||
})}
|
||||
{teams && teams.length == 0 && (
|
||||
<Alert title="Команды не найдены" color="red">
|
||||
Создайте или войдите в команду чтобы начать работу с
|
||||
экспериментами
|
||||
</Alert>
|
||||
)}
|
||||
{teams &&
|
||||
teams.length != 0 &&
|
||||
experiments.length == 0 &&
|
||||
!is_loading_ && (
|
||||
<Alert title="Экспериментов нету" color="blue">
|
||||
Вы еще не создали не один эксперимент
|
||||
</Alert>
|
||||
)}
|
||||
</PaginationContainer>
|
||||
</div>
|
||||
<PaginationContainer
|
||||
numberOfPages={1}
|
||||
isLoading={teams == undefined}
|
||||
activePage={1}
|
||||
setPage={() => {}}
|
||||
>
|
||||
{experiments.map((exp: Experiment) => {
|
||||
return (
|
||||
<ExperimentsListCard
|
||||
experiment={{
|
||||
id: exp.id,
|
||||
name: exp.name,
|
||||
description: exp.description,
|
||||
team_id: exp.team_id,
|
||||
tasks_ids: exp.tasks_ids,
|
||||
date_created: exp.date_created,
|
||||
experiment_status: exp.experiment_status,
|
||||
experiment_type: "A",
|
||||
}}
|
||||
team={teams?.find((team) => team.team_id == exp.team_id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{teams && teams.length == 0 && (
|
||||
<Alert title="Команды не найдены" color="red">
|
||||
Создайте или войдите в команду чтобы начать работу с
|
||||
экспериментами
|
||||
</Alert>
|
||||
)}
|
||||
{teams && teams.length != 0 && experiments.length == 0 && (
|
||||
<Alert title="Экспериментов нету" color="blue">
|
||||
Вы еще не создали не один эксперимент
|
||||
</Alert>
|
||||
)}
|
||||
</PaginationContainer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
13
src/Pages/TaskPage/TaskPage.css
Normal file
13
src/Pages/TaskPage/TaskPage.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.taskButtons {
|
||||
display: flex;
|
||||
|
||||
flex-direction: row;
|
||||
justify-content: right;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
flex-wrap: nowrap;
|
||||
text-wrap: nowrap;
|
||||
* {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,100 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { IconCancel, IconPlus, IconSettings } from "@tabler/icons-react";
|
||||
import { IconCancel, IconPlus } from "@tabler/icons-react";
|
||||
import { useParams } from "react-router";
|
||||
import CustomButton from "Components/CustomButton/CustomButton";
|
||||
import { IframePlugin } from "Api/PluginLoader/PluginLoader";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { InstanceData } from "Types/Experiment/Experiment";
|
||||
import {
|
||||
getFrontendFile,
|
||||
getInstanceById,
|
||||
updateInstance,
|
||||
} from "Api/QuantumBackend/ExperimentsManagment";
|
||||
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||
import { SimpleGrid, TextInput, Title } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import "./TaskPage.css";
|
||||
|
||||
function TaskPage() {
|
||||
const { task_id } = useParams();
|
||||
const [data, setData] = useState<{ text: string }>({ text: "" });
|
||||
|
||||
const [instance, set_instance] = useState<InstanceData>();
|
||||
const { loadedHtmlFiles, addLoadedHtml, setInstances } = useExperimentStore();
|
||||
const { profile, is_loading } = useAuthenticationStore();
|
||||
const [, set_is_loading] = useState(true);
|
||||
const [data, setData] = useState<string>();
|
||||
const [progress, setProgress] = useState<string>();
|
||||
const [qubits_needed, set_qubits_needed] = useState<number>();
|
||||
|
||||
const [name, setName] = useState<string>();
|
||||
const [descr, setDescr] = useState<string>("");
|
||||
const [reload, setReload] = useState<number>(0);
|
||||
|
||||
const saveData = () => {
|
||||
if (instance && qubits_needed) {
|
||||
updateInstance({
|
||||
instance_id: instance.instance_id,
|
||||
name: name,
|
||||
description: descr,
|
||||
instance_data: JSON.stringify(data),
|
||||
qubits_needed: qubits_needed,
|
||||
}).then((updated) => {
|
||||
set_instance({
|
||||
instance_id: instance.instance_id,
|
||||
name: updated.name,
|
||||
description: updated.description,
|
||||
instance_data: updated.instance_data,
|
||||
qubits_needed: updated.qubits_needed,
|
||||
simulation_result: instance.simulation_result,
|
||||
});
|
||||
setName(updated.name);
|
||||
setDescr(updated.description || "");
|
||||
setData(updated.instance_data);
|
||||
});
|
||||
} else {
|
||||
if (!qubits_needed) {
|
||||
notifications.show({
|
||||
message: "Количество кубит не было определено",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ResetData = () => {
|
||||
if (instance) {
|
||||
setData(instance.instance_data);
|
||||
setName(instance.name);
|
||||
setDescr(instance.description || "");
|
||||
setReload(reload + 1);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (profile && !is_loading && task_id) {
|
||||
getInstanceById(Number(task_id))
|
||||
.then((inst) => {
|
||||
set_instance(inst);
|
||||
setData(inst.instance_data);
|
||||
setProgress(inst.simulation_result?.simulation_result);
|
||||
setName(inst.name);
|
||||
setDescr(inst.description || "");
|
||||
set_qubits_needed(inst.qubits_needed);
|
||||
getFrontendFile(1).then((file) => {
|
||||
addLoadedHtml(1, file);
|
||||
});
|
||||
set_is_loading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
set_is_loading(false);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
setInstances([]);
|
||||
};
|
||||
}, [profile, is_loading]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -29,39 +116,77 @@ function TaskPage() {
|
||||
marginBottom: "15px",
|
||||
}}
|
||||
>
|
||||
<div className="experimentButtons">
|
||||
<CustomButton
|
||||
color="accent"
|
||||
onClick={() => {}}
|
||||
icon={<IconPlus />}
|
||||
text="Сохранить"
|
||||
/>
|
||||
<CustomButton
|
||||
color="red"
|
||||
style="outline"
|
||||
onClick={() => {}}
|
||||
icon={<IconCancel />}
|
||||
text="Отменить"
|
||||
/>
|
||||
{(!instance?.simulation_result ||
|
||||
instance?.simulation_result?.status == "DRAFT") && (
|
||||
<>
|
||||
<div className="taskButtons">
|
||||
<CustomButton
|
||||
color="accent"
|
||||
onClick={saveData}
|
||||
disabled={
|
||||
JSON.stringify(data) ==
|
||||
JSON.stringify(instance?.instance_data) &&
|
||||
instance?.name == name &&
|
||||
(instance?.description || "") == (descr || "")
|
||||
}
|
||||
icon={<IconPlus />}
|
||||
text="Сохранить"
|
||||
/>
|
||||
<CustomButton
|
||||
color="red"
|
||||
style="outline"
|
||||
onClick={ResetData}
|
||||
icon={<IconCancel />}
|
||||
text="Отменить"
|
||||
disabled={
|
||||
JSON.stringify(data) ==
|
||||
JSON.stringify(instance?.instance_data) &&
|
||||
instance?.name == name &&
|
||||
(instance?.description || "") == (descr || "")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<SimpleGrid verticalSpacing="lg" cols={2}>
|
||||
<Title size="lg">Имя:</Title>
|
||||
<TextInput
|
||||
size="md"
|
||||
value={name}
|
||||
onChange={
|
||||
(e) => {
|
||||
setName(e.target.value);
|
||||
} //set_username(e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
<Title size="lg">Описание:</Title>
|
||||
<TextInput
|
||||
size="md"
|
||||
value={descr}
|
||||
onChange={
|
||||
(e) => {
|
||||
setDescr(e.target.value);
|
||||
} //set_email(e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
</div>
|
||||
<IframePlugin
|
||||
pluginUrl={
|
||||
window.location.origin +
|
||||
"/" +
|
||||
import.meta.env.VITE_BASE_PATH +
|
||||
"/" +
|
||||
"index.html"
|
||||
}
|
||||
mode="editor"
|
||||
taskData={{
|
||||
id: Number(task_id),
|
||||
name: "Молекула 1",
|
||||
description: "",
|
||||
data: data,
|
||||
}}
|
||||
onUpdate={setData}
|
||||
/>
|
||||
{instance && (
|
||||
<IframePlugin
|
||||
plugin={loadedHtmlFiles.get(1) || ""}
|
||||
mode="Editor"
|
||||
taskData={JSON.stringify(data)}
|
||||
qubits_needed={qubits_needed || 0}
|
||||
onUpdate={(data, qubits_need) => {
|
||||
setData(JSON.parse(data));
|
||||
set_qubits_needed(qubits_need);
|
||||
}}
|
||||
index={instance.instance_id + reload}
|
||||
simProgress={JSON.stringify(progress)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -56,7 +56,7 @@ function TeamPage() {
|
||||
addTeam(team);
|
||||
set_cur_team(team);
|
||||
set_team_name(team.name);
|
||||
set_team_descr(team.description);
|
||||
set_team_descr(team.description || "");
|
||||
set_is_loading(false);
|
||||
}
|
||||
});
|
||||
@@ -66,7 +66,7 @@ function TeamPage() {
|
||||
profile &&
|
||||
!is_loading
|
||||
) {
|
||||
getTeamSystems(Number(team_id), cur_page, 9 )
|
||||
getTeamSystems(Number(team_id), cur_page, 9)
|
||||
.then((systems) => {
|
||||
if (systems) {
|
||||
set_machines(systems);
|
||||
@@ -210,8 +210,8 @@ function TeamPage() {
|
||||
cur_team?.description == team_descr
|
||||
}
|
||||
onClick={() => {
|
||||
set_team_name(cur_team?.name);
|
||||
set_team_descr(cur_team?.description);
|
||||
set_team_name(cur_team?.name || "");
|
||||
set_team_descr(cur_team?.description || "");
|
||||
}}
|
||||
/>
|
||||
</Grid.Col>
|
||||
@@ -252,7 +252,7 @@ function TeamPage() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!cur_team && !is_loading && (
|
||||
{!cur_team && !is_loading_this && (
|
||||
<Alert color="red">
|
||||
<Center>
|
||||
{" "}
|
||||
|
||||
@@ -7,6 +7,12 @@ import {
|
||||
Switch,
|
||||
LoadingOverlay,
|
||||
Space,
|
||||
Avatar,
|
||||
Center,
|
||||
Grid,
|
||||
FileInput,
|
||||
Group,
|
||||
rem,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconChartCandle,
|
||||
@@ -15,6 +21,7 @@ import {
|
||||
IconCancel,
|
||||
IconSun,
|
||||
IconMoonStars,
|
||||
IconUpload,
|
||||
} from "@tabler/icons-react";
|
||||
import keycloak, {
|
||||
SendEmailVerification,
|
||||
@@ -29,19 +36,22 @@ import { useUserPreferencesStore } from "Stores/PreferencesStore";
|
||||
import CustomButton from "Components/CustomButton/CustomButton";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useSearchParams } from "react-router";
|
||||
import {
|
||||
GetCurrentUserInfo,
|
||||
UpdateCurrentUserInfo,
|
||||
} from "Api/QuantumBackend/UserManagement";
|
||||
import type { UserData } from "Types/User/User";
|
||||
import { UpdateCurrentUserInfo } from "Api/QuantumBackend/UserManagement";
|
||||
|
||||
function UserPage() {
|
||||
const { profile, is_loading, profile_picture_path } =
|
||||
useAuthenticationStore();
|
||||
const {
|
||||
profile,
|
||||
is_loading,
|
||||
profile_picture_path,
|
||||
set_profile_picture_path,
|
||||
} = useAuthenticationStore();
|
||||
const { theme, set_theme } = useUserPreferencesStore();
|
||||
const [username, set_username] = useState<string>("");
|
||||
const [email, set_email] = useState<string>("");
|
||||
const [pfp_path, set_pfp_path] = useState<string>("");
|
||||
const [is_editing_path, set_is_editing_path] = useState<boolean>(false);
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const updateData = () => {
|
||||
@@ -53,9 +63,7 @@ function UserPage() {
|
||||
const prof_1 = profile;
|
||||
prof_1.email = email;
|
||||
prof_1.username = username;
|
||||
useAuthenticationStore.setState({
|
||||
is_loading: true,
|
||||
});
|
||||
|
||||
updateUserData(prof_1).then((data) => {
|
||||
if (data) {
|
||||
notifications.show({
|
||||
@@ -76,15 +84,81 @@ function UserPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (file: File | null) => {
|
||||
setSelectedFile(file);
|
||||
if (file) {
|
||||
// Create preview
|
||||
const url = URL.createObjectURL(file);
|
||||
setPreviewUrl(url);
|
||||
} else {
|
||||
if (previewUrl) {
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
setPreviewUrl(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const update_pfp = async () => {
|
||||
UpdateCurrentUserInfo(pfp_path).then(() => {
|
||||
GetCurrentUserInfo().then((info: UserData | undefined) => {
|
||||
if (info && info.profile_picture_path)
|
||||
useAuthenticationStore.setState({
|
||||
profile_picture_path: info.profile_picture_path,
|
||||
});
|
||||
if (!selectedFile) return;
|
||||
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const result = await UpdateCurrentUserInfo(selectedFile);
|
||||
if (result && result.profile_picture_path) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_QUANTUM_BACKEND_URL}/user/serve/${profile?.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${keycloak.token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
set_profile_picture_path(url);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load avatar:", error);
|
||||
}
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Фотография профиля обновлена",
|
||||
message: "",
|
||||
icon: <IconCheck />,
|
||||
color: "green",
|
||||
});
|
||||
set_is_editing_path(false);
|
||||
// Clean up
|
||||
if (previewUrl) {
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
setPreviewUrl(null);
|
||||
}
|
||||
setSelectedFile(null);
|
||||
}
|
||||
} catch {
|
||||
notifications.show({
|
||||
radius: "md",
|
||||
title: "Ошибка",
|
||||
message: "Не удалось обновить фотографию профиля",
|
||||
icon: <IconCancel />,
|
||||
color: "red",
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
set_is_editing_path(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelUpload = () => {
|
||||
set_is_editing_path(false);
|
||||
setSelectedFile(null);
|
||||
if (previewUrl) {
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
setPreviewUrl(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -103,10 +177,6 @@ function UserPage() {
|
||||
}
|
||||
}, [profile, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (profile_picture_path) set_pfp_path(profile_picture_path);
|
||||
}, [profile_picture_path]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -153,52 +223,126 @@ function UserPage() {
|
||||
overlayProps={{ radius: "sm", blur: 2 }}
|
||||
loaderProps={{ size: 50, type: "dots" }}
|
||||
/>
|
||||
<SimpleGrid verticalSpacing="lg" cols={2}>
|
||||
<Title size="lg">Имя пользователя:</Title>
|
||||
<TextInput
|
||||
size="md"
|
||||
value={username}
|
||||
onChange={(e) => set_username(e.currentTarget.value)}
|
||||
/>
|
||||
<Title size="lg">Почта:</Title>
|
||||
<TextInput
|
||||
size="md"
|
||||
value={email}
|
||||
onChange={(e) => set_email(e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<CustomButton
|
||||
color="accent"
|
||||
text="Сохранить"
|
||||
onClick={updateData}
|
||||
disabled={
|
||||
!(
|
||||
profile != null &&
|
||||
((profile.username != undefined &&
|
||||
profile.username != username) ||
|
||||
(profile.email != undefined && profile.email != email))
|
||||
)
|
||||
}
|
||||
/>
|
||||
<CustomButton
|
||||
color="error"
|
||||
text="Отменить"
|
||||
onClick={() => {
|
||||
if (profile && profile.email && profile.username) {
|
||||
set_username(profile.username);
|
||||
set_email(profile.email);
|
||||
}
|
||||
<Grid>
|
||||
<Grid.Col span={4} style={{ height: "100%" }}>
|
||||
<Center>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Title size="xl">Фотография профиля</Title>
|
||||
</div>
|
||||
</Center>
|
||||
<Center style={{ padding: "15px" }}>
|
||||
<Avatar size="xl" src={profile_picture_path} radius="xl" />
|
||||
{is_editing_path && (
|
||||
<FileInput
|
||||
placeholder="Выберите изображение"
|
||||
accept="image/png,image/jpeg,image/jpg,image/webp"
|
||||
value={selectedFile}
|
||||
onChange={handleFileChange}
|
||||
leftSection={<IconUpload size={rem(14)} />}
|
||||
clearable
|
||||
style={{ width: "100%" }}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
)}
|
||||
</Center>
|
||||
<div>
|
||||
{!is_editing_path ? (
|
||||
<CustomButton
|
||||
style="outline"
|
||||
color="contrast"
|
||||
text="Изменить"
|
||||
onClick={() => {
|
||||
set_is_editing_path(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Group justify="center" mt="md">
|
||||
<CustomButton
|
||||
style="outline"
|
||||
color="red"
|
||||
text="Отменить"
|
||||
onClick={cancelUpload}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
<CustomButton
|
||||
style="color"
|
||||
color="accent"
|
||||
text="Сохранить"
|
||||
onClick={update_pfp}
|
||||
disabled={!selectedFile}
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
</div>
|
||||
</Grid.Col>
|
||||
<Grid.Col
|
||||
span={4}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
justifyContent: "space-between",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
disabled={
|
||||
!(
|
||||
profile != null &&
|
||||
((profile.username != undefined &&
|
||||
profile.username != username) ||
|
||||
(profile.email != undefined && profile.email != email))
|
||||
)
|
||||
}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
>
|
||||
<Title size="lg">Имя пользователя:</Title>
|
||||
|
||||
<Title size="lg">Почта:</Title>
|
||||
<CustomButton
|
||||
color="accent"
|
||||
text="Сохранить"
|
||||
onClick={updateData}
|
||||
disabled={
|
||||
!(
|
||||
profile != null &&
|
||||
((profile.username != undefined &&
|
||||
profile.username != username) ||
|
||||
(profile.email != undefined &&
|
||||
profile.email != email))
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col
|
||||
span={4}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
justifyContent: "space-between",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
size="md"
|
||||
value={username}
|
||||
onChange={(e) => set_username(e.currentTarget.value)}
|
||||
/>
|
||||
<TextInput
|
||||
size="md"
|
||||
value={email}
|
||||
onChange={(e) => set_email(e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<CustomButton
|
||||
color="error"
|
||||
text="Отменить"
|
||||
onClick={() => {
|
||||
if (profile && profile.email && profile.username) {
|
||||
set_username(profile.username);
|
||||
set_email(profile.email);
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
!(
|
||||
profile != null &&
|
||||
((profile.username != undefined &&
|
||||
profile.username != username) ||
|
||||
(profile.email != undefined &&
|
||||
profile.email != email))
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<Divider my="lg" />
|
||||
<SimpleGrid verticalSpacing={"lg"} cols={2}>
|
||||
<Title
|
||||
@@ -266,60 +410,12 @@ function UserPage() {
|
||||
/>
|
||||
</div>
|
||||
</SimpleGrid>
|
||||
<Divider my="lg" />
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Title size="lg">Фотография профиля</Title>
|
||||
|
||||
<Space my="sm" />
|
||||
{!is_editing_path ? (
|
||||
<img height={350} width={350} src={profile_picture_path} />
|
||||
) : (
|
||||
<TextInput
|
||||
size="md"
|
||||
value={pfp_path}
|
||||
onChange={(e) => set_pfp_path(e.currentTarget.value)}
|
||||
/>
|
||||
)}
|
||||
<Space my="sm" />
|
||||
<div style={{ width: "15em" }}>
|
||||
{!is_editing_path ? (
|
||||
<CustomButton
|
||||
style="outline"
|
||||
color="contrast"
|
||||
text="Изменить"
|
||||
onClick={() => {
|
||||
set_is_editing_path(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<CustomButton
|
||||
style="outline"
|
||||
color="contrast"
|
||||
text="Сохранить"
|
||||
onClick={() => {
|
||||
update_pfp();
|
||||
set_is_editing_path(false);
|
||||
}}
|
||||
/>
|
||||
<Space my="sm" />
|
||||
<CustomButton
|
||||
style="outline"
|
||||
color="red"
|
||||
text="Отменить"
|
||||
onClick={() => {
|
||||
set_is_editing_path(false);
|
||||
set_pfp_path(profile_picture_path);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="preference">
|
||||
<Title size={"lg"}>Тема приложения:</Title>
|
||||
<Space h="md" />
|
||||
<Switch
|
||||
size="xl"
|
||||
defaultChecked={theme == "light"}
|
||||
|
||||
Reference in New Issue
Block a user