Initial commit
This commit is contained in:
24
.gitignore
vendored
Executable file
24
.gitignore
vendored
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
43
README.md
Executable file
43
README.md
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
# Автоматизированная распределенная система расчета энергии основного состояния молекул с помощью квантовых алгоритмов
|
||||||
|
|
||||||
|
Этот репозиторий содержит фронтенд-часть проекта для дипломной работы бакалавра.
|
||||||
|
|
||||||
|
## Структура репозитория
|
||||||
|
|
||||||
|
- `public/` — Публичные файлы (иконки, шрифты)
|
||||||
|
- `src/`
|
||||||
|
- `Components/` — Компоненты интерфейса
|
||||||
|
- `pages/` — Страницы приложения
|
||||||
|
- `Routes/` — Маршруты приложения
|
||||||
|
- `Stores/` — Логика для управления состоянием
|
||||||
|
- `Types/` — Определения типов для API
|
||||||
|
- `App.tsx` — Главный компонент приложения
|
||||||
|
- `GlobalVars.ts` — Глобальные переменные
|
||||||
|
- `main.tsx` — Точка входа в приложение
|
||||||
|
- `vite.config.ts` — Конфигурация для Vite
|
||||||
|
- `tsconfig.app.json` — Конфигурация TypeScript для приложения
|
||||||
|
- `tsconfig.json` — Основная конфигурация TypeScript
|
||||||
|
- `tsconfig.node.json` — Конфигурация TypeScript для Node.js (для Vite)
|
||||||
|
- `package.json` — Зависимости и скрипты проекта
|
||||||
|
- `package-lock.json` — Замороженные версии зависимостей
|
||||||
|
- `eslint.config.js` — Конфигурация для ESLint
|
||||||
|
|
||||||
|
## Установка и запуск
|
||||||
|
|
||||||
|
### Требования
|
||||||
|
|
||||||
|
- Node.js >= v24.1.0
|
||||||
|
- npm >= 11.6.2
|
||||||
|
|
||||||
|
### Установка зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Компилирование и запуск сервера
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
23
eslint.config.js
Executable file
23
eslint.config.js
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default tseslint.config([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs['recommended-latest'],
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
14
index.html
Executable file
14
index.html
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="preload" href="/Quicking.otf" as="font" type="font/otf" crossorigin>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>QMolSim</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
4356
package-lock.json
generated
Executable file
4356
package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
43
package.json
Executable file
43
package.json
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "vkrb_frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mantine/core": "^8.3.0",
|
||||||
|
"@mantine/hooks": "^8.3.0",
|
||||||
|
"@mantine/notifications": "^8.3.1",
|
||||||
|
"@tabler/icons-react": "^3.34.1",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"immer": "^11.1.4",
|
||||||
|
"keycloak-js": "^26.2.3",
|
||||||
|
"miew-react": "^0.11.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-helmet": "^6.1.0",
|
||||||
|
"react-resizable-panels": "^4.5.4",
|
||||||
|
"react-router": "^7.9.6",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.33.0",
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"@types/react-helmet": "^6.1.11",
|
||||||
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
|
"eslint": "^9.33.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"globals": "^16.3.0",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"typescript-eslint": "^8.39.1",
|
||||||
|
"vite": "^7.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/Quicking.otf
Executable file
BIN
public/Quicking.otf
Executable file
Binary file not shown.
BIN
public/bitmap.png
Executable file
BIN
public/bitmap.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
8
public/silent-check-sso.html
Normal file
8
public/silent-check-sso.html
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
parent.postMessage(location.href, location.origin);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
46
src/Api/ConvertBackendCalls.tsx
Executable file
46
src/Api/ConvertBackendCalls.tsx
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
import axios, { AxiosError } from "axios";
|
||||||
|
import type { ConvertSchema } from "Types/ApiCalls/ConvertBackendCallsTypes";
|
||||||
|
|
||||||
|
const openbableBackendRoot = "http://localhost:1654";
|
||||||
|
|
||||||
|
export async function ConvertMoleculeToStandart(
|
||||||
|
data: ConvertSchema,
|
||||||
|
): Promise<string | AxiosError> {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(openbableBackendRoot + "/convert", {
|
||||||
|
text: data.inputText,
|
||||||
|
format: data.inputFormat,
|
||||||
|
convert_3d: data.make_3d,
|
||||||
|
add_hydrogen: data.add_h,
|
||||||
|
optimize_geometry: data.optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Response from the FastAPI JSONResponse
|
||||||
|
return response.data.molfile;
|
||||||
|
} catch (error) {
|
||||||
|
//Error handling
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
// Do something with the axios error...
|
||||||
|
return error;
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetInFormats(): Promise<
|
||||||
|
{ [key: string]: string } | AxiosError
|
||||||
|
> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(openbableBackendRoot + "/informats");
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
//Error handling
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
// Do something with the axios error...
|
||||||
|
return error;
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/Api/Keycloak/Keycloak.tsx
Normal file
28
src/Api/Keycloak/Keycloak.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { keycloakURI } from "GlobalVars";
|
||||||
|
import Keycloak from "keycloak-js";
|
||||||
|
|
||||||
|
const keycloak = new Keycloak({
|
||||||
|
url: keycloakURI,
|
||||||
|
realm: "dev-realm",
|
||||||
|
clientId: "react-frontend",
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: `${keycloak.authServerUrl}/realms/${keycloak.realm}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default keycloak;
|
||||||
|
|
||||||
|
export async function getProfile(keycloak: Keycloak) {
|
||||||
|
return keycloak.loadUserProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleSubmit = async (profile) => {
|
||||||
|
try {
|
||||||
|
await api.put("/account", profile);
|
||||||
|
alert("Profile updated!");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
27
src/App.css
Executable file
27
src/App.css
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
.body {
|
||||||
|
height: 95%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#App {
|
||||||
|
height: 100svh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: 0 1 autol;
|
||||||
|
}
|
||||||
|
|
||||||
|
#Header {
|
||||||
|
box-shadow: rgba(99, 99, 99, 0.2) 0px 0px 5px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#Navbar {
|
||||||
|
box-shadow: rgba(99, 99, 99, 0.2) 0px 0px 5px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible_link {
|
||||||
|
text-decoration: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
104
src/App.tsx
Executable file
104
src/App.tsx
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
import "./App.css";
|
||||||
|
|
||||||
|
import { AppShell, useMantineTheme } from "@mantine/core";
|
||||||
|
|
||||||
|
import Header from "Components/Layout/Header/Header";
|
||||||
|
import "@mantine/notifications/styles.css";
|
||||||
|
import "@mantine/core/styles.css";
|
||||||
|
import { Notifications } from "@mantine/notifications";
|
||||||
|
import { useLayoutStore } from "Stores/LayoutStore";
|
||||||
|
import {
|
||||||
|
defaultAnimationDuration,
|
||||||
|
headerHeight,
|
||||||
|
sideMenuWidth,
|
||||||
|
} from "GlobalVars";
|
||||||
|
import Sidebar from "Components/Layout/Sidebar/Sidebar";
|
||||||
|
import { Outlet, useLocation } from "react-router";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useMediaQuery } from "@mantine/hooks";
|
||||||
|
import BreadCrumbs from "Routes/Breadcrumbs/Breadcrumbs";
|
||||||
|
import keycloak, { getProfile } from "Api/Keycloak/Keycloak";
|
||||||
|
import { AuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
|
import type { KeycloakProfile } from "keycloak-js";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const { is_navbar_open, set_navbar_open } = useLayoutStore();
|
||||||
|
const { set_token, set_profile } = AuthenticationStore();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const isMobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm}`);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMobile)
|
||||||
|
setTimeout(() => {
|
||||||
|
set_navbar_open(false);
|
||||||
|
}, 100);
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const refreshToken = () => {
|
||||||
|
keycloak
|
||||||
|
.updateToken(60)
|
||||||
|
.then((refreshed) => {
|
||||||
|
if (refreshed) {
|
||||||
|
set_token(keycloak.token);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
keycloak.logout();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setInterval(refreshToken, 30000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>QMolSim</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Compute ground state energy of molecules using a distributed system"
|
||||||
|
/>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<Notifications />
|
||||||
|
|
||||||
|
<AppShell
|
||||||
|
header={{ height: headerHeight }}
|
||||||
|
aside={{
|
||||||
|
width: sideMenuWidth,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: {
|
||||||
|
desktop: !is_navbar_open,
|
||||||
|
mobile: !is_navbar_open,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
transitionDuration={defaultAnimationDuration}
|
||||||
|
>
|
||||||
|
<AppShell.Header id="Header" zIndex={1000}>
|
||||||
|
<Header />
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
|
<AppShell.Aside id="Navbar">
|
||||||
|
<Sidebar />
|
||||||
|
</AppShell.Aside>
|
||||||
|
|
||||||
|
<AppShell.Main className="body">
|
||||||
|
<>
|
||||||
|
<BreadCrumbs />
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
58
src/Components/CardWithButton/CardWithButton.css
Executable file
58
src/Components/CardWithButton/CardWithButton.css
Executable file
@@ -0,0 +1,58 @@
|
|||||||
|
.tile {
|
||||||
|
--bgsize: 7px;
|
||||||
|
--bgoffset: 7px;
|
||||||
|
|
||||||
|
width: 25vw;
|
||||||
|
min-width: 275px;
|
||||||
|
max-width: 300px;
|
||||||
|
|
||||||
|
margin: 0px 10px;
|
||||||
|
padding: 20px 30px;
|
||||||
|
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: var(--mantine-color-contrast-filled);
|
||||||
|
border-radius: var(--paper-radius);
|
||||||
|
transform: translateY(0px);
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
/*Move up on hover*/
|
||||||
|
&:hover {
|
||||||
|
transform: scale(105%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Background 45 degree lines */
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
border-radius: var(--paper-radius);
|
||||||
|
background-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgba(255, 255, 255, 0) 33.33%,
|
||||||
|
var(--mantine-color-contrast-filled) 33.33%,
|
||||||
|
var(--mantine-color-contrast-filled) 50%,
|
||||||
|
rgba(255, 255, 255, 0) 50%,
|
||||||
|
rgba(255, 255, 255, 0) 83.33%,
|
||||||
|
var(--mantine-color-contrast-filled) 83.33%,
|
||||||
|
var(--mantine-color-contrast-filled) 100%
|
||||||
|
);
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -2;
|
||||||
|
top: var(--bgoffset);
|
||||||
|
left: var(--bgoffset);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: var(--bgsize) var(--bgsize);
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
background: var(--mantine-color-body);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*make it so the button is on bottom of card*/
|
||||||
|
.card_text {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
36
src/Components/CardWithButton/CardWithButton.tsx
Executable file
36
src/Components/CardWithButton/CardWithButton.tsx
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Card, Text, Button, Title, Space } from "@mantine/core";
|
||||||
|
import "./CardWithButton.css";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
|
||||||
|
interface ButtonWithTextProps {
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
buttonText: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ButtonWithText(props: ButtonWithTextProps) {
|
||||||
|
return (
|
||||||
|
<Card className="tile" shadow="sm" padding="lg" radius="md" withBorder>
|
||||||
|
<Text fw={650} mb="sm" size="md">
|
||||||
|
{props.title}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" className="card_text">
|
||||||
|
{props.text}
|
||||||
|
</Text>
|
||||||
|
<Space h="md" />
|
||||||
|
<Link to={props.link} className="invisible_link">
|
||||||
|
<CustomButton
|
||||||
|
color="contrast"
|
||||||
|
text={props.buttonText}
|
||||||
|
style="color"
|
||||||
|
textAlign="center"
|
||||||
|
textSize="lg"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ButtonWithText;
|
||||||
85
src/Components/CodeTextArea/CodeTextArea.tsx
Executable file
85
src/Components/CodeTextArea/CodeTextArea.tsx
Executable file
@@ -0,0 +1,85 @@
|
|||||||
|
import { Textarea } from "@mantine/core";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
interface TextAreaProps {
|
||||||
|
text: string;
|
||||||
|
onTextChange: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CodeTextArea(props: TextAreaProps) {
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
const lineRef = useRef(null);
|
||||||
|
|
||||||
|
//Synchronize scrolling between the
|
||||||
|
useEffect(() => {
|
||||||
|
const inputEl = document.querySelector(".MoleculeEditInput");
|
||||||
|
const lineEl = document.querySelector(".MoleculeEditLineNumber");
|
||||||
|
if (!inputEl || !lineEl) return;
|
||||||
|
|
||||||
|
let isSyncing = false; // prevents circular scroll events
|
||||||
|
let activeEl = null; // element currently being scrolled
|
||||||
|
|
||||||
|
const syncScroll = (source: Element, target: Element) => {
|
||||||
|
if (isSyncing) return;
|
||||||
|
isSyncing = true;
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
target.scrollTop = source.scrollTop;
|
||||||
|
isSyncing = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onScroll = (e: Event) => {
|
||||||
|
activeEl = e.target;
|
||||||
|
if (activeEl === inputEl) {
|
||||||
|
syncScroll(inputEl, lineEl);
|
||||||
|
} else {
|
||||||
|
syncScroll(lineEl, inputEl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inputEl.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
lineEl.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
inputEl.removeEventListener("scroll", onScroll);
|
||||||
|
lineEl.removeEventListener("scroll", onScroll);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
className="MoleculeEdit"
|
||||||
|
value={props.text}
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={(e) => {
|
||||||
|
props.onTextChange(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
wrap="no-wrap"
|
||||||
|
leftSection={
|
||||||
|
<div className="MoleculeEditLineNumber" ref={lineRef}>
|
||||||
|
{Array.from({ length: props.text.split("\n").length }, (_, i) => {
|
||||||
|
if (i > 1) {
|
||||||
|
return i - 1;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}).join("\n")}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
classNames={{
|
||||||
|
input: "MoleculeEditInput",
|
||||||
|
wrapper: "MoleculeEditWrapper",
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
// Stop arrow keys from reaching react-resizable-panels
|
||||||
|
if (
|
||||||
|
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)
|
||||||
|
) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></Textarea>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
src/Components/CustomButton/CustomButton.css
Normal file
105
src/Components/CustomButton/CustomButton.css
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
.colored {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colored:hover {
|
||||||
|
background-color: color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textAlign-left {
|
||||||
|
text-align: left;
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textAlign-right {
|
||||||
|
text-align: right;
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textAlign-center {
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
|
justify-items: center;
|
||||||
|
* {
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
.outline {
|
||||||
|
border-color: var(--color);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline:hover {
|
||||||
|
background: color-mix(in srgb, var(--hovercolor) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ###################### */
|
||||||
|
|
||||||
|
.color {
|
||||||
|
background: var(--color);
|
||||||
|
color: var(--textcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color:hover {
|
||||||
|
background-color: var(--hovercolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
--color: var(--mantine-color-primary-filled);
|
||||||
|
--textcolor: var(--mantine-color-contrast-filled);
|
||||||
|
--hovercolor: var(--mantine-color-secondary-filled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
--color: var(--mantine-color-secondary-filled);
|
||||||
|
--textcolor: var(--mantine-color-secondaryContrast-filled);
|
||||||
|
--hovercolor: var(--mantine-color-primary-filled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast {
|
||||||
|
--color: var(--mantine-color-contrast-filled);
|
||||||
|
--textcolor: var(--mantine-color-primary-filled);
|
||||||
|
--hovercolor: var(--mantine-color-secondaryContrast-filled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-contrast {
|
||||||
|
--color: var(--mantine-color-secondaryContrast-filled);
|
||||||
|
--textcolor: var(--mantine-color-secondary-filled);
|
||||||
|
--hovercolor: var(--mantine-color-contrast-filled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent {
|
||||||
|
--color: var(--mantine-color-accent-filled);
|
||||||
|
--textcolor: white;
|
||||||
|
--hovercolor: var(--mantine-color-accent-filled-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
--color: var(--mantine-color-yellow-filled);
|
||||||
|
--textcolor: white;
|
||||||
|
--hovercolor: var(--mantine-color-yellow-filled-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
--color: var(--mantine-color-red-filled);
|
||||||
|
--textcolor: white;
|
||||||
|
--hovercolor: var(--mantine-color-red-filled-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ###################### */
|
||||||
|
|
||||||
|
.subtle {
|
||||||
|
color: var(--color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtle:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--color) 20%, transparent);
|
||||||
|
}
|
||||||
62
src/Components/CustomButton/CustomButton.tsx
Normal file
62
src/Components/CustomButton/CustomButton.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { Space, Text, UnstyledButton, type MantineSize } from "@mantine/core";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import "./CustomButton.css";
|
||||||
|
|
||||||
|
interface CustomButtonProps {
|
||||||
|
text?: string;
|
||||||
|
icon?: ReactElement;
|
||||||
|
style?: "outline" | "color" | "subtle";
|
||||||
|
color?:
|
||||||
|
| "primary"
|
||||||
|
| "secondary"
|
||||||
|
| "contrast"
|
||||||
|
| "secondary-cotrast"
|
||||||
|
| "accent"
|
||||||
|
| "warning"
|
||||||
|
| "error"
|
||||||
|
| string;
|
||||||
|
textAlign?: "left" | "center" | "right";
|
||||||
|
textSize?: MantineSize;
|
||||||
|
onClick?: React.MouseEventHandler;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomButton({
|
||||||
|
text,
|
||||||
|
icon,
|
||||||
|
style = "color",
|
||||||
|
color = "contrast",
|
||||||
|
textAlign = "center",
|
||||||
|
textSize = "md",
|
||||||
|
onClick = () => {},
|
||||||
|
disabled = false,
|
||||||
|
}: CustomButtonProps) {
|
||||||
|
console.log(color);
|
||||||
|
return (
|
||||||
|
<UnstyledButton
|
||||||
|
className={`colored ${style} ${color} textAlign-${textAlign}`}
|
||||||
|
style={
|
||||||
|
color !== "primary" &&
|
||||||
|
color !== "secondary" &&
|
||||||
|
color !== "contrast" &&
|
||||||
|
color !== "secondary-contrast" &&
|
||||||
|
color !== "accent" &&
|
||||||
|
color !== "warning" &&
|
||||||
|
color !== "error"
|
||||||
|
? {
|
||||||
|
"--color": color,
|
||||||
|
"--textcolor": "white",
|
||||||
|
"--hovercolor": `color-mix(in srgb,${color} 90%, black)`,
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{icon} {icon && <Space w="sm" />}
|
||||||
|
<Text size={textSize}>{text}</Text>
|
||||||
|
</UnstyledButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomButton;
|
||||||
23
src/Components/Layout/Header/Header.css
Executable file
23
src/Components/Layout/Header/Header.css
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
.logo {
|
||||||
|
height: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerLogo {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-family: "Quicking";
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 5px;
|
||||||
|
}
|
||||||
33
src/Components/Layout/Header/Header.tsx
Executable file
33
src/Components/Layout/Header/Header.tsx
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Burger, Image, Text } from "@mantine/core";
|
||||||
|
import "./Header.css";
|
||||||
|
import { useLayoutStore } from "Stores/LayoutStore";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import { baseUrl } from "GlobalVars";
|
||||||
|
import { routes } from "Routes/Routes";
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
const { is_navbar_open, set_navbar_open } = useLayoutStore();
|
||||||
|
return (
|
||||||
|
<div className="header">
|
||||||
|
<Link className="headerLogo invisible_link" to={routes.MainPage.path}>
|
||||||
|
<Image
|
||||||
|
className="logo"
|
||||||
|
w="auto"
|
||||||
|
fit="contain"
|
||||||
|
src={baseUrl + "/bitmap.png"}
|
||||||
|
alt="image"
|
||||||
|
/>
|
||||||
|
<Text size="xl">QMolSim</Text>
|
||||||
|
</Link>
|
||||||
|
<Burger
|
||||||
|
aria-label="sidebar toggle"
|
||||||
|
opened={is_navbar_open}
|
||||||
|
onClick={() => {
|
||||||
|
set_navbar_open(!is_navbar_open);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header;
|
||||||
64
src/Components/Layout/Sidebar/Sidebar.css
Executable file
64
src/Components/Layout/Sidebar/Sidebar.css
Executable file
@@ -0,0 +1,64 @@
|
|||||||
|
.sidebar {
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar_top {
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar_bottom {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar_bottom_top {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar_bottom_bottom {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
min-width: 0px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userInfo {
|
||||||
|
padding-left: 15px;
|
||||||
|
gap: 5px;
|
||||||
|
min-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar_bottom_button {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
padding-top: 3px;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar_bottom_button:hover {
|
||||||
|
background-color: var(--mantine-color-secondary-filled);
|
||||||
|
}
|
||||||
165
src/Components/Layout/Sidebar/Sidebar.tsx
Executable file
165
src/Components/Layout/Sidebar/Sidebar.tsx
Executable file
@@ -0,0 +1,165 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Modal,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
UnstyledButton,
|
||||||
|
useMantineTheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import "./Sidebar.css";
|
||||||
|
import {
|
||||||
|
IconDevicesPc,
|
||||||
|
IconFileDescription,
|
||||||
|
IconFlaskFilled,
|
||||||
|
IconLogout,
|
||||||
|
IconSettings2,
|
||||||
|
IconUsers,
|
||||||
|
type Icon,
|
||||||
|
type IconProps,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { Link, useLocation } from "react-router";
|
||||||
|
import { useState, type ForwardRefExoticComponent } from "react";
|
||||||
|
import { routes } from "Routes/Routes";
|
||||||
|
import keycloak from "Api/Keycloak/Keycloak";
|
||||||
|
import { AuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
|
import { baseUri, baseUrl } from "GlobalVars";
|
||||||
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
|
||||||
|
interface SubtleLinkButtonProps {
|
||||||
|
link: string;
|
||||||
|
Icon: ForwardRefExoticComponent<IconProps & React.RefAttributes<Icon>>;
|
||||||
|
text: string;
|
||||||
|
color: string;
|
||||||
|
selected?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubtleLinkButton(props: SubtleLinkButtonProps) {
|
||||||
|
return (
|
||||||
|
<Link to={props.link} className="invisible_link">
|
||||||
|
<CustomButton
|
||||||
|
icon={<props.Icon color={props.color} size={20} />}
|
||||||
|
style="subtle"
|
||||||
|
color={props.selected ? props.color : "contrast"}
|
||||||
|
text={props.text}
|
||||||
|
textAlign="left"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sidebar() {
|
||||||
|
const { profile } = AuthenticationStore();
|
||||||
|
const [open, set_open] = useState(false);
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
const location = useLocation(); // get current URL
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sidebar">
|
||||||
|
<Modal
|
||||||
|
opened={open}
|
||||||
|
onClose={() => set_open(false)}
|
||||||
|
title=<Title size="lg">Выйти?</Title>
|
||||||
|
centered
|
||||||
|
withCloseButton={false}
|
||||||
|
size="auto"
|
||||||
|
>
|
||||||
|
<div className="sidebar_bottom_bottom">
|
||||||
|
<CustomButton
|
||||||
|
text="Подтвердить"
|
||||||
|
onClick={() => keycloak.logout({ redirectUri: baseUri + baseUrl })}
|
||||||
|
textSize="lg"
|
||||||
|
color="contrast"
|
||||||
|
></CustomButton>
|
||||||
|
<CustomButton
|
||||||
|
onClick={() => set_open(false)}
|
||||||
|
text="Отменить"
|
||||||
|
textSize="lg"
|
||||||
|
style="outline"
|
||||||
|
color="contrast"
|
||||||
|
></CustomButton>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<div className="sidebar_top">
|
||||||
|
<SubtleLinkButton
|
||||||
|
link={routes.ExperimentsPage.path}
|
||||||
|
Icon={IconFlaskFilled}
|
||||||
|
text="Эксперименты"
|
||||||
|
color={theme.colors.teal[7]}
|
||||||
|
selected={location.pathname.startsWith(routes.ExperimentsPage.path)}
|
||||||
|
/>
|
||||||
|
<SubtleLinkButton
|
||||||
|
link={routes.MachinesPage.path}
|
||||||
|
Icon={IconDevicesPc}
|
||||||
|
text="Вычислительные системы"
|
||||||
|
color={theme.colors.violet[7]}
|
||||||
|
selected={location.pathname.startsWith(routes.MachinesPage.path)}
|
||||||
|
/>
|
||||||
|
<SubtleLinkButton
|
||||||
|
link={routes.TeamsPage.path}
|
||||||
|
Icon={IconUsers}
|
||||||
|
text="Команды"
|
||||||
|
color={theme.colors.grape[7]}
|
||||||
|
selected={location.pathname.startsWith(routes.TeamsPage.path)}
|
||||||
|
/>
|
||||||
|
<SubtleLinkButton
|
||||||
|
link={routes.DocumentationPage.path}
|
||||||
|
Icon={IconFileDescription}
|
||||||
|
text="Документация"
|
||||||
|
color={theme.colors.blue[7]}
|
||||||
|
selected={location.pathname.startsWith(routes.DocumentationPage.path)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="sidebar_bottom">
|
||||||
|
{keycloak.authenticated && (
|
||||||
|
<>
|
||||||
|
<div className="sidebar_bottom_top">
|
||||||
|
<Avatar radius="xl" />
|
||||||
|
<Stack className="userInfo">
|
||||||
|
<Text size="md">{profile?.username}</Text>
|
||||||
|
<Text size="sm" c="dimmed" truncate="end">
|
||||||
|
{profile?.email}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<Divider
|
||||||
|
color="secondaryContrast"
|
||||||
|
style={{ width: "100%", height: "5px", margin: "5px" }}
|
||||||
|
/>
|
||||||
|
<div className="sidebar_bottom_bottom">
|
||||||
|
<Link to={routes.SettingsPage.path} className="invisible_link">
|
||||||
|
<CustomButton
|
||||||
|
style="subtle"
|
||||||
|
icon={<IconSettings2 size={26} />}
|
||||||
|
text="Настройки"
|
||||||
|
color="contrast"
|
||||||
|
textSize="lg"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
<CustomButton
|
||||||
|
style="subtle"
|
||||||
|
icon={<IconLogout size={26} />}
|
||||||
|
text="Выйти"
|
||||||
|
textSize="lg"
|
||||||
|
color="contrast"
|
||||||
|
onClick={() => {
|
||||||
|
set_open(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!keycloak.authenticated && (
|
||||||
|
<UnstyledButton onClick={() => keycloak.login()}>
|
||||||
|
Войти / Зарегестрироваться
|
||||||
|
</UnstyledButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
50
src/Components/ListCard/ExperimentsListCard.css
Normal file
50
src/Components/ListCard/ExperimentsListCard.css
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
.ExperimentsListCard {
|
||||||
|
height: 120px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExperimentSectionWithLine {
|
||||||
|
border-right: 1px solid var(--mantine-color-contrast-filled);
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExperimentPill {
|
||||||
|
background-color: var(--mantine-color-contrast-filled);
|
||||||
|
color: var(--mantine-color-primary-filled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExperimentPill2 {
|
||||||
|
border: 2px solid var(--mantine-color-accent-filled);
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--mantine-color-accent-filled);
|
||||||
|
* {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: right;
|
||||||
|
gap: 5px;
|
||||||
|
.p {
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.TopRightContainer {
|
||||||
|
justify-content: right;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 15px;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RightExperimentSection {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
120
src/Components/ListCard/ExperimentsListCard.tsx
Normal file
120
src/Components/ListCard/ExperimentsListCard.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { Card, Pill, SimpleGrid, Text, UnstyledButton } from "@mantine/core";
|
||||||
|
import "./ExperimentsListCard.css";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import type { Experiment, TaskData } from "Types/Experiment/Experiment";
|
||||||
|
import { IconTrash } from "@tabler/icons-react";
|
||||||
|
import type { MouseEvent } from "react";
|
||||||
|
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||||
|
|
||||||
|
function ExperimentsListCard(props: Experiment) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { selectExperiment, removeExperiment, tasks, teams } =
|
||||||
|
useExperimentStore();
|
||||||
|
|
||||||
|
const team = teams.find((team) => {
|
||||||
|
return team.id == props.team_id;
|
||||||
|
});
|
||||||
|
|
||||||
|
const experiment_tasks = tasks.filter((a) => a.id in props.tasks_ids);
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
removeExperiment(props.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className="ExperimentsListCard"
|
||||||
|
onClick={() => {
|
||||||
|
console.log(props.id);
|
||||||
|
selectExperiment(props.id);
|
||||||
|
navigate(props.id.toString());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SimpleGrid cols={3}>
|
||||||
|
<div className="ExperimentSectionWithLine">
|
||||||
|
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
|
||||||
|
{props.name}
|
||||||
|
</Text>
|
||||||
|
<Text mb="sm" size="md">
|
||||||
|
Team: {team ? team.name : "ERROR"}
|
||||||
|
</Text>
|
||||||
|
<Pill className="ExperimentPill">
|
||||||
|
<Text mb="sm" size="md">
|
||||||
|
Статус: {props.experiment_status}
|
||||||
|
</Text>
|
||||||
|
</Pill>
|
||||||
|
</div>
|
||||||
|
<div className="ExperimentSectionWithLine">
|
||||||
|
<SimpleGrid cols={2} verticalSpacing="0px">
|
||||||
|
<Text>Задачи:</Text>
|
||||||
|
{experiment_tasks.map((task: TaskData<any>) => {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
textWrap: "nowrap",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{task.name}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{experiment_tasks.length == 0 ? (
|
||||||
|
<Pill
|
||||||
|
size="md"
|
||||||
|
className="ExperimentPill2"
|
||||||
|
style={{
|
||||||
|
textWrap: "nowrap",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Нет Задач
|
||||||
|
</Pill>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</SimpleGrid>
|
||||||
|
</div>
|
||||||
|
<div className="RightExperimentSection">
|
||||||
|
<div className="TopRightContainer">
|
||||||
|
<div className="dateContainer">
|
||||||
|
<Text size="sm">{props.date_created.toLocaleDateString()} </Text>
|
||||||
|
<Text size="sm">{props.date_created.toLocaleTimeString()}</Text>
|
||||||
|
</div>
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={(e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete();
|
||||||
|
}}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</UnstyledButton>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flexGrow: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "right",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{" "}
|
||||||
|
Тип эксперимента:{" "}
|
||||||
|
<Pill className="ExperimentPill2">{props.experiment_type}</Pill>
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Text color="secondary" size="sm">
|
||||||
|
#{props.id}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExperimentsListCard;
|
||||||
22
src/Components/MoleculeViewer/MoleculeViewer.css
Executable file
22
src/Components/MoleculeViewer/MoleculeViewer.css
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
.moleculeViewer {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.atomEditor {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: md;
|
||||||
|
right: 10px;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.atomEditor * {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
269
src/Components/MoleculeViewer/MoleculeViewer.tsx
Executable file
269
src/Components/MoleculeViewer/MoleculeViewer.tsx
Executable file
@@ -0,0 +1,269 @@
|
|||||||
|
import Viewer from "miew-react";
|
||||||
|
import Miew from "miew";
|
||||||
|
import {
|
||||||
|
Paper,
|
||||||
|
UnstyledButton,
|
||||||
|
useMantineColorScheme,
|
||||||
|
useMantineTheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import "./MoleculeViewer.css";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import {
|
||||||
|
IconAlertHexagon,
|
||||||
|
IconArrowBarBoth,
|
||||||
|
IconRefresh,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import MoleculeViewerMenu from "./MoleculeViewerMenu";
|
||||||
|
import type { SelectedAtom } from "Types/Experiment/MoleculeEdit/MoleculeEdit";
|
||||||
|
|
||||||
|
const callbackOnResizeFinish = (
|
||||||
|
dom_elem: HTMLElement,
|
||||||
|
beginning_callback: () => void,
|
||||||
|
end_callback: () => void,
|
||||||
|
delay = 100, // ms after resize "finishes"
|
||||||
|
) => {
|
||||||
|
let timeoutId: number | undefined;
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
beginning_callback();
|
||||||
|
if (timeoutId) {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
end_callback();
|
||||||
|
}, delay);
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(dom_elem);
|
||||||
|
|
||||||
|
return () => resizeObserver.disconnect(); // cleanup helper
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MoleculeViewerProps {
|
||||||
|
moleculeData: string;
|
||||||
|
selectedAtom: SelectedAtom | null;
|
||||||
|
setSelectedAtom: (selectedAtom: SelectedAtom | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MoleculeViewer(props: MoleculeViewerProps) {
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
// объект загруженного редактора молекул для редактирования
|
||||||
|
const [miew, setMiew] = useState<Miew | null>(null);
|
||||||
|
const molViewerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [viewingData, setViewingData] = useState<string>(props.moleculeData);
|
||||||
|
|
||||||
|
//при загрузке сохраняем объект miew
|
||||||
|
const onInitMiew = (miew: Miew) => {
|
||||||
|
setMiew(miew);
|
||||||
|
if (miew && viewingData) {
|
||||||
|
//прогружаем молекулу
|
||||||
|
miew
|
||||||
|
.load(viewingData, {
|
||||||
|
sourceType: "immediate",
|
||||||
|
fileType: "xyz",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (props.selectedAtom) {
|
||||||
|
props.setSelectedAtom(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.message == "Operation cancelled") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifications.show({
|
||||||
|
color: "orange",
|
||||||
|
radius: "md",
|
||||||
|
title: "Ошибка при отображении молекулы",
|
||||||
|
message:
|
||||||
|
"Проверте правильность написания кода молекулы, в нем содержатся ошибки",
|
||||||
|
icon: <IconAlertHexagon />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
miew.setOptions({
|
||||||
|
settings: {
|
||||||
|
bg: {
|
||||||
|
color: theme.colors.secondaryDark[7],
|
||||||
|
},
|
||||||
|
fogAlpha: 0.7,
|
||||||
|
axes: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//мемоизируем объект чтобы он не перегружаля при изменении [miew] состояния
|
||||||
|
const MemoViewer = useMemo(
|
||||||
|
() => <Viewer onInit={onInitMiew} />,
|
||||||
|
[viewingData],
|
||||||
|
);
|
||||||
|
|
||||||
|
//Замена станартного обработчика нажатия на молекулы
|
||||||
|
useEffect(() => {
|
||||||
|
if (molViewerRef.current && miew) {
|
||||||
|
//INFO: при изменении размера окна обносить webgl
|
||||||
|
const cleanup = callbackOnResizeFinish(
|
||||||
|
molViewerRef.current,
|
||||||
|
handleResizeStart,
|
||||||
|
handleResizeEnd,
|
||||||
|
);
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew.removeEventListener("newpick");
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew.addEventListener("newpick", handleClick);
|
||||||
|
return () => {
|
||||||
|
cleanup();
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew.removeEventListener("newpick");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [molViewerRef, miew]);
|
||||||
|
|
||||||
|
//On selected Atom change, highlight it
|
||||||
|
useEffect(() => {
|
||||||
|
if (miew) {
|
||||||
|
if (!props.selectedAtom) {
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew.select("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew.select("serial " + props.selectedAtom.serial, false);
|
||||||
|
//спрятать информацию встроенную
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew._msgAtomInfo.style.opacity = 0.0;
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew._msgAtomInfo.style.height = "0px";
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew._msgAtomInfo.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
}, [props.selectedAtom, miew]);
|
||||||
|
|
||||||
|
//при смене темы обновляем фон MoleculeViewer
|
||||||
|
useEffect(() => {
|
||||||
|
if (miew) {
|
||||||
|
miew.setOptions({
|
||||||
|
settings: {
|
||||||
|
bg: {
|
||||||
|
color: theme.colors.secondaryDark[7],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [colorScheme]);
|
||||||
|
|
||||||
|
//при нажятии на атом выбираем его
|
||||||
|
const handleClick = (pick: { type: string; obj: { atom: SelectedAtom } }) => {
|
||||||
|
if (miew) {
|
||||||
|
if (pick.obj.atom) {
|
||||||
|
props.setSelectedAtom(pick.obj.atom);
|
||||||
|
} else {
|
||||||
|
props.setSelectedAtom(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResizeStart = () => {
|
||||||
|
setIsResizing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResizeEnd = () => {
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew._onResize();
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew._picker.handleResize();
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsResizing(false);
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
document.body.style.overflow = "hidden"; // disable page scroll
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
document.body.style.overflow = "auto"; // re-enable scroll
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshDisplay = () => {
|
||||||
|
if (props.moleculeData != viewingData) {
|
||||||
|
setViewingData(props.moleculeData);
|
||||||
|
} else {
|
||||||
|
if (miew) {
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew.resetView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={molViewerRef} className="moleculeViewer">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: theme.colors.secondaryDark[7],
|
||||||
|
borderRadius: "5px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
{MemoViewer}
|
||||||
|
{isResizing && viewingData && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
backgroundColor: theme.colors.secondaryDark[7], // optional dim
|
||||||
|
opacity: 0.5,
|
||||||
|
zIndex: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconArrowBarBoth size={80} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{props.selectedAtom && (
|
||||||
|
<Paper className="atomEditor" radius="md" p="md">
|
||||||
|
<MoleculeViewerMenu selectedAtom={props.selectedAtom} miew={miew} />
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: "10px",
|
||||||
|
top: "10px",
|
||||||
|
display: "flex",
|
||||||
|
zIndex: 5,
|
||||||
|
width: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UnstyledButton onClick={refreshDisplay}>
|
||||||
|
<IconRefresh size={25} />
|
||||||
|
</UnstyledButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MoleculeViewer;
|
||||||
29
src/Components/MoleculeViewer/MoleculeViewerMenu.tsx
Executable file
29
src/Components/MoleculeViewer/MoleculeViewerMenu.tsx
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Text, Divider } from "@mantine/core";
|
||||||
|
import Miew from "miew";
|
||||||
|
import "./MoleculeViewer.css";
|
||||||
|
import type { SelectedAtom } from "Types/Experiment/MoleculeEdit/MoleculeEdit";
|
||||||
|
|
||||||
|
interface MoleculeViewerMenuProps {
|
||||||
|
selectedAtom: SelectedAtom;
|
||||||
|
miew: Miew | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MoleculeViewerMenu(props: MoleculeViewerMenuProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="moleculeViewerMenu">
|
||||||
|
<>
|
||||||
|
<Text ta="center">
|
||||||
|
Атом: {props.selectedAtom.name} ({props.selectedAtom.serial})
|
||||||
|
</Text>
|
||||||
|
<Divider my="sm" />
|
||||||
|
<Text ta="center">Положение X: {props.selectedAtom.position.x}</Text>
|
||||||
|
<Text ta="center">Положение Y: {props.selectedAtom.position.y}</Text>
|
||||||
|
<Text ta="center">Положение Z: {props.selectedAtom.position.z}</Text>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MoleculeViewerMenu;
|
||||||
16
src/Components/PaginationContainer/PaginationContainer.css
Normal file
16
src/Components/PaginationContainer/PaginationContainer.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
.PaginationContainer {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PaginationContents {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
12
src/Components/PaginationContainer/PaginationContainer.tsx
Normal file
12
src/Components/PaginationContainer/PaginationContainer.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Pagination } from "@mantine/core";
|
||||||
|
import "./PaginationContainer.css";
|
||||||
|
import type { PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
export function PaginationContainer(a: PropsWithChildren) {
|
||||||
|
return (
|
||||||
|
<div className="PaginationContainer">
|
||||||
|
<div className="PaginationContents">{a.children}</div>
|
||||||
|
<Pagination total={10} color="accent" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
src/Components/StripedBg/Background.css
Executable file
45
src/Components/StripedBg/Background.css
Executable file
@@ -0,0 +1,45 @@
|
|||||||
|
.background-light {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white
|
||||||
|
linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(110, 156, 223, 0.125) 0%,
|
||||||
|
rgba(110, 156, 223, 0.125) 14.286%,
|
||||||
|
rgba(110, 156, 223, 0.25) 14.286%,
|
||||||
|
rgba(110, 156, 223, 0.25) 28.571%,
|
||||||
|
rgba(110, 156, 223, 0.375) 28.571%,
|
||||||
|
rgba(110, 156, 223, 0.375) 42.857%,
|
||||||
|
rgba(110, 156, 223, 0.5) 42.857%,
|
||||||
|
rgba(110, 156, 223, 0.5) 57.143%,
|
||||||
|
rgba(110, 156, 223, 0.725) 57.143%,
|
||||||
|
rgba(110, 156, 223, 0.725) 71.429%,
|
||||||
|
rgba(110, 156, 223, 0.85) 71.429%,
|
||||||
|
rgba(110, 156, 223, 0.85) 85.714%,
|
||||||
|
rgba(110, 156, 223, 1) 85.714% 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-dark {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: black
|
||||||
|
linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(36, 54, 162, 0.353) 0%,
|
||||||
|
rgba(36, 54, 162, 0.353) 14.286%,
|
||||||
|
rgba(36, 54, 162, 0.5) 14.286%,
|
||||||
|
rgba(36, 54, 162, 0.5) 28.571%,
|
||||||
|
rgba(36, 54, 162, 0.612) 28.571%,
|
||||||
|
rgba(36, 54, 162, 0.612) 42.857%,
|
||||||
|
rgba(36, 54, 162, 0.707) 42.857%,
|
||||||
|
rgba(36, 54, 162, 0.707) 57.143%,
|
||||||
|
rgba(36, 54, 162, 0.851) 57.143%,
|
||||||
|
rgba(36, 54, 162, 0.851) 71.429%,
|
||||||
|
rgba(36, 54, 162, 0.921) 71.429%,
|
||||||
|
rgba(36, 54, 162, 0.921) 85.714%,
|
||||||
|
rgba(36, 54, 162, 1) 85.714% 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/Components/StripedBg/Background.tsx
Executable file
13
src/Components/StripedBg/Background.tsx
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
import { useMantineColorScheme } from "@mantine/core";
|
||||||
|
import "./Background.css";
|
||||||
|
|
||||||
|
function Background() {
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={colorScheme == "dark" ? "background-dark" : "background-light"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Background;
|
||||||
11
src/GlobalVars.ts
Executable file
11
src/GlobalVars.ts
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
export const baseUrl = "/quantum";
|
||||||
|
export const baseDomain = "localhost";
|
||||||
|
export const basePort = 8001;
|
||||||
|
export const keycloakURI = "http://auth.localhost";
|
||||||
|
export const baseUri = `http://${baseDomain}:${basePort}`;
|
||||||
|
//AppShell vars
|
||||||
|
export const headerHeight = 50;
|
||||||
|
export const sideMenuWidth = 280;
|
||||||
|
|
||||||
|
// Global Style vars
|
||||||
|
export const defaultAnimationDuration = 100;
|
||||||
0
src/Modals/NewExperiment/NewExperiment.css
Executable file
0
src/Modals/NewExperiment/NewExperiment.css
Executable file
116
src/Modals/NewExperiment/NewExperiment.tsx
Executable file
116
src/Modals/NewExperiment/NewExperiment.tsx
Executable file
@@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Textarea,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
UnstyledButton,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useEffect, useState, type ChangeEvent } from "react";
|
||||||
|
import "./NewExperiment.css";
|
||||||
|
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||||
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
|
||||||
|
interface NewExperimentModalProps {
|
||||||
|
isOpened: boolean;
|
||||||
|
setIsOpened: (opened: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewExperimentModal(props: NewExperimentModalProps) {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const { addExperiment } = useExperimentStore();
|
||||||
|
//reset on open dialog
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.isOpened) {
|
||||||
|
setName("");
|
||||||
|
setDescription("");
|
||||||
|
}
|
||||||
|
}, [props.isOpened]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
props.setIsOpened(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateExperiment = () => {
|
||||||
|
//TODO: add logic for backend server
|
||||||
|
|
||||||
|
addExperiment({
|
||||||
|
id: 1,
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
team_id: 1,
|
||||||
|
tasks_ids: [],
|
||||||
|
date_created: new Date(),
|
||||||
|
experiment_status: "DRAFT",
|
||||||
|
experiment_type: "a",
|
||||||
|
});
|
||||||
|
props.setIsOpened(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={props.isOpened}
|
||||||
|
onClose={handleClose}
|
||||||
|
title=<Title size="xl">Новый эксперимент</Title>
|
||||||
|
centered
|
||||||
|
size="75%"
|
||||||
|
styles={{
|
||||||
|
content: { paddingLeft: "10px" },
|
||||||
|
title: { width: "100%" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
value={name}
|
||||||
|
label="Имя эксперимента"
|
||||||
|
placeholder="Введите имя эксперимента"
|
||||||
|
required
|
||||||
|
onChange={(event) => {
|
||||||
|
setName(event.currentTarget.value);
|
||||||
|
}}
|
||||||
|
></TextInput>
|
||||||
|
<Space h="md" />
|
||||||
|
<Textarea
|
||||||
|
value={description}
|
||||||
|
label="Описание эксперимента"
|
||||||
|
placeholder="Введите описание экспериментаё"
|
||||||
|
minRows={4}
|
||||||
|
maxRows={10}
|
||||||
|
autosize
|
||||||
|
onChange={(event) => {
|
||||||
|
setDescription(event.currentTarget.value);
|
||||||
|
}}
|
||||||
|
></Textarea>
|
||||||
|
<Space h="md" />
|
||||||
|
<Select
|
||||||
|
value=""
|
||||||
|
label="Команда эксперимента"
|
||||||
|
placeholder="Выберите команду эксперимента"
|
||||||
|
searchable
|
||||||
|
data={[]}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
<Space h="md" />
|
||||||
|
<Center>
|
||||||
|
<CustomButton
|
||||||
|
disabled={name != "" ? false : true}
|
||||||
|
color="contrast"
|
||||||
|
onClick={handleCreateExperiment}
|
||||||
|
text="Создать эксперимент"
|
||||||
|
></CustomButton>
|
||||||
|
<Space w="md" />
|
||||||
|
<CustomButton
|
||||||
|
color="contrast"
|
||||||
|
style="outline"
|
||||||
|
onClick={() => {
|
||||||
|
props.setIsOpened(false);
|
||||||
|
}}
|
||||||
|
text="Отменить"
|
||||||
|
></CustomButton>
|
||||||
|
</Center>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
src/Modals/NewMolecule/NewMolecule.css
Executable file
3
src/Modals/NewMolecule/NewMolecule.css
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
.StepperRoot {
|
||||||
|
flex-direction: row-reverse !important;
|
||||||
|
}
|
||||||
243
src/Modals/NewMolecule/NewMolecule.tsx
Executable file
243
src/Modals/NewMolecule/NewMolecule.tsx
Executable file
@@ -0,0 +1,243 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Modal,
|
||||||
|
Space,
|
||||||
|
Stepper,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
Flex,
|
||||||
|
Select,
|
||||||
|
Textarea,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import "./NewMolecule.css";
|
||||||
|
import {
|
||||||
|
IconArchiveFilled,
|
||||||
|
IconArticleFilled,
|
||||||
|
IconFileFilled,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import {
|
||||||
|
ConvertMoleculeToStandart,
|
||||||
|
GetInFormats,
|
||||||
|
} from "Api/ConvertBackendCalls";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { useMoleculeEditStore } from "Stores/MoleculeEditStore";
|
||||||
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
|
||||||
|
interface NewMoleculeModalProps {
|
||||||
|
isOpened: boolean;
|
||||||
|
setIsOpened: (opened: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewMoleculeModal(props: NewMoleculeModalProps) {
|
||||||
|
const { setCurrentMoleculeString } = useMoleculeEditStore();
|
||||||
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
|
const [selectedMethod, setSelectedMethod] = useState(0);
|
||||||
|
|
||||||
|
const [inMolecule, setInMolecule] = useState("");
|
||||||
|
const [inFormat, setInFormat] = useState<string | null>();
|
||||||
|
|
||||||
|
const [inOptions, setInOptions] = useState<string[] | undefined>();
|
||||||
|
|
||||||
|
const getOptions = () => {
|
||||||
|
GetInFormats().then((data: { [key: string]: string } | AxiosError) => {
|
||||||
|
if (data instanceof AxiosError) {
|
||||||
|
//TODO: handle Error
|
||||||
|
} else {
|
||||||
|
const values = Object.keys(data);
|
||||||
|
setInOptions(values);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getOptions();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
//reset on open dialog
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.isOpened) {
|
||||||
|
setActiveStep(0);
|
||||||
|
setSelectedMethod(0);
|
||||||
|
setInMolecule("");
|
||||||
|
setInFormat(undefined);
|
||||||
|
if (inOptions == undefined) {
|
||||||
|
getOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [props.isOpened]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
props.setIsOpened(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConvert = () => {
|
||||||
|
if (inFormat) {
|
||||||
|
ConvertMoleculeToStandart({
|
||||||
|
inputText: inMolecule,
|
||||||
|
inputFormat: inFormat,
|
||||||
|
make_3d: true,
|
||||||
|
add_h: false,
|
||||||
|
optimize: false,
|
||||||
|
}).then((data: string | AxiosError) => {
|
||||||
|
if (data instanceof AxiosError) {
|
||||||
|
//TODO: handle Error
|
||||||
|
} else {
|
||||||
|
setCurrentMoleculeString(data);
|
||||||
|
props.setIsOpened(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//TODO: add no format selected handling
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEmptyMolecule = () => {
|
||||||
|
props.setIsOpened(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={props.isOpened}
|
||||||
|
onClose={handleClose}
|
||||||
|
title=<Title size="xl">Новая молекула</Title>
|
||||||
|
centered
|
||||||
|
size="75%"
|
||||||
|
styles={{
|
||||||
|
content: { paddingLeft: "10px" },
|
||||||
|
title: { width: "100%" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stepper
|
||||||
|
active={activeStep}
|
||||||
|
onStepClick={setActiveStep}
|
||||||
|
allowNextStepsSelect={false}
|
||||||
|
classNames={{ root: "StepperRoot" }}
|
||||||
|
styles={{
|
||||||
|
steps: {
|
||||||
|
paddingTop: "30px",
|
||||||
|
paddingBottom: "30px",
|
||||||
|
paddingLeft: "20px",
|
||||||
|
minWidth: "0px",
|
||||||
|
alignSelf: "center",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
style={{ display: "flex", flexDirection: "row" }}
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<Stepper.Step>
|
||||||
|
<div style={{ width: "100%", textAlign: "center" }}>
|
||||||
|
<Title size="lg">Шаг 1: Способ добавления молекулы</Title>
|
||||||
|
<Space h="lg" />
|
||||||
|
<Flex justify="center" gap="md" wrap="wrap">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedMethod(1);
|
||||||
|
handleEmptyMolecule();
|
||||||
|
}}
|
||||||
|
style={{ height: "100px", flexGrow: "1" }}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<Flex direction="row" gap="lg">
|
||||||
|
<IconFileFilled size={40} />
|
||||||
|
<Center>
|
||||||
|
<Text>Пустая молекула</Text>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedMethod(2);
|
||||||
|
setActiveStep(1);
|
||||||
|
}}
|
||||||
|
style={{ height: "100px", flexGrow: "1" }}
|
||||||
|
>
|
||||||
|
<Flex direction="row" gap="lg">
|
||||||
|
<IconArchiveFilled size="40" />
|
||||||
|
<Center>
|
||||||
|
<Text>Из файла / Архива</Text>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedMethod(3);
|
||||||
|
setActiveStep(1);
|
||||||
|
}}
|
||||||
|
style={{ height: "100px", flexGrow: "1" }}
|
||||||
|
>
|
||||||
|
<Flex direction="row" gap="lg">
|
||||||
|
<IconArticleFilled size={40} />
|
||||||
|
<Center>
|
||||||
|
<Text>Другой формат</Text>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
</Stepper.Step>
|
||||||
|
<Stepper.Step>
|
||||||
|
<div style={{ width: "100%", textAlign: "center" }}>
|
||||||
|
{selectedMethod == 2 && (
|
||||||
|
<div>
|
||||||
|
<Title size="lg">Шаг 2: Выбор файла</Title>
|
||||||
|
<Space h="lg" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedMethod == 3 && (
|
||||||
|
<div>
|
||||||
|
<Title size="md">Шаг 2: Ввод молекулы</Title>
|
||||||
|
<Space h="lg" />
|
||||||
|
<Textarea
|
||||||
|
className="moleculeInput"
|
||||||
|
value={inMolecule}
|
||||||
|
onChange={(e) => setInMolecule(e.currentTarget.value)}
|
||||||
|
placeholder="Введите текст молекулы"
|
||||||
|
classNames={{
|
||||||
|
wrapper: "moleculeInputWrapper",
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
// Stop arrow keys from reaching react-resizable-panels
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
"ArrowUp",
|
||||||
|
"ArrowDown",
|
||||||
|
"ArrowLeft",
|
||||||
|
"ArrowRight",
|
||||||
|
].includes(e.key)
|
||||||
|
) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Space h="md" />
|
||||||
|
<Select
|
||||||
|
value={inFormat}
|
||||||
|
onChange={(value: string | null) => setInFormat(value)}
|
||||||
|
searchable
|
||||||
|
data={inOptions}
|
||||||
|
placeholder="Выберите формат"
|
||||||
|
classNames={{
|
||||||
|
input: "selectInFormat",
|
||||||
|
dropdown: "selectDropDown",
|
||||||
|
option: "selectDropDownOption",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Space h="md" />
|
||||||
|
<CustomButton
|
||||||
|
color="accent"
|
||||||
|
onClick={handleConvert}
|
||||||
|
text="Преобразовать"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Stepper.Step>
|
||||||
|
</Stepper>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
src/Pages/DocumentationPage/DocumentationPage.css
Executable file
26
src/Pages/DocumentationPage/DocumentationPage.css
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
.DocumentationPage {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contents {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableOfContents {
|
||||||
|
position: sticky;
|
||||||
|
top: 60px;
|
||||||
|
width: 150px;
|
||||||
|
height: min-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docTitle {
|
||||||
|
padding-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoverCard {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
}
|
||||||
103
src/Pages/DocumentationPage/DocumentationPage.tsx
Executable file
103
src/Pages/DocumentationPage/DocumentationPage.tsx
Executable file
@@ -0,0 +1,103 @@
|
|||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
Menu,
|
||||||
|
TableOfContents,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import "./DocumentationPage.css";
|
||||||
|
import { IconMenu2 } from "@tabler/icons-react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
|
function DocumentationPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>Documentation | QMolSim</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="See the documentation on how to setup and use the qunatum computational system"
|
||||||
|
/>
|
||||||
|
</Helmet>
|
||||||
|
<Title order={1} className="docTitle">
|
||||||
|
Документация
|
||||||
|
</Title>
|
||||||
|
<Divider />
|
||||||
|
<div className="DocumentationPage">
|
||||||
|
<Box className="tableOfContents" visibleFrom="md">
|
||||||
|
<TableOfContents
|
||||||
|
variant="light"
|
||||||
|
color="accent"
|
||||||
|
size="sm"
|
||||||
|
radius="sm"
|
||||||
|
scrollSpyOptions={{
|
||||||
|
selector: "section h2",
|
||||||
|
}}
|
||||||
|
getControlProps={({ data }) => ({
|
||||||
|
onClick: () =>
|
||||||
|
data
|
||||||
|
.getNode()
|
||||||
|
.scrollIntoView({ behavior: "smooth", block: "center" }),
|
||||||
|
children: data.value,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box className="hoverCard" hiddenFrom="md">
|
||||||
|
<Menu
|
||||||
|
width={280}
|
||||||
|
shadow="md"
|
||||||
|
openDelay={100}
|
||||||
|
closeDelay={100}
|
||||||
|
closeOnClickOutside
|
||||||
|
closeOnItemClick
|
||||||
|
floatingStrategy="fixed"
|
||||||
|
>
|
||||||
|
<Menu.Target>
|
||||||
|
<ActionIcon
|
||||||
|
aria-label="navigation"
|
||||||
|
variant="gradient"
|
||||||
|
gradient={{ from: "blue", to: "cyan", deg: 90 }}
|
||||||
|
radius={"50%"}
|
||||||
|
size={40}
|
||||||
|
>
|
||||||
|
<IconMenu2 size={25} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<TableOfContents
|
||||||
|
variant="light"
|
||||||
|
color="accent"
|
||||||
|
size="sm"
|
||||||
|
radius="sm"
|
||||||
|
scrollSpyOptions={{
|
||||||
|
selector: "section h2",
|
||||||
|
}}
|
||||||
|
getControlProps={({ data }) => ({
|
||||||
|
onClick: () =>
|
||||||
|
data
|
||||||
|
.getNode()
|
||||||
|
.scrollIntoView({ behavior: "smooth", block: "center" }),
|
||||||
|
children: data.value,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
<div className="contents">
|
||||||
|
<section id="introduction" style={{ height: 1000 }}>
|
||||||
|
<Title order={2}>Introduction</Title>
|
||||||
|
</section>
|
||||||
|
<section id="features" style={{ height: 1000 }}>
|
||||||
|
<Title order={2}>features</Title>
|
||||||
|
</section>
|
||||||
|
<section id="conclusion" style={{ height: 1000 }}>
|
||||||
|
<Title order={2}>Conslusion</Title>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DocumentationPage;
|
||||||
16
src/Pages/ExperimentsPage/ExperimentPage.css
Normal file
16
src/Pages/ExperimentsPage/ExperimentPage.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
.ExperimentPage {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experimentButtons {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: right;
|
||||||
|
width: fit-content;
|
||||||
|
margin-left: auto;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
108
src/Pages/ExperimentsPage/ExperimentPage.tsx
Normal file
108
src/Pages/ExperimentsPage/ExperimentPage.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { Alert, Center, Text } from "@mantine/core";
|
||||||
|
import "./ExperimentPage.css";
|
||||||
|
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
|
||||||
|
import { NewMoleculeModal } from "Modals/NewMolecule/NewMolecule";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { IconPlus, IconSettings } from "@tabler/icons-react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import {
|
||||||
|
useExperimentStore,
|
||||||
|
useSelectedExperiment,
|
||||||
|
} from "Stores/ExperimentStore";
|
||||||
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
function ExperimentPage() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { experiment_id } = useParams();
|
||||||
|
const { selectedExperimentId, selectExperiment } = useExperimentStore();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!selectedExperimentId ||
|
||||||
|
(Number(experiment_id) != selectedExperimentId && experiment_id)
|
||||||
|
) {
|
||||||
|
console.log(Number(experiment_id));
|
||||||
|
selectExperiment(Number(experiment_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
const experiment = useSelectedExperiment();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>
|
||||||
|
{experiment
|
||||||
|
? "Experiment " + experiment.id + " | QMolSim"
|
||||||
|
: "Error |QmolSim"}
|
||||||
|
</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="See the documentation on how to setup and use the qunatum computational system"
|
||||||
|
/>
|
||||||
|
</Helmet>
|
||||||
|
{experiment && (
|
||||||
|
<div className="ExperimentPage">
|
||||||
|
<NewMoleculeModal
|
||||||
|
isOpened={isOpen}
|
||||||
|
setIsOpened={(toOpen: boolean) => setIsOpen(toOpen)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "10px",
|
||||||
|
marginBottom: "15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="experimentButtons">
|
||||||
|
<CustomButton
|
||||||
|
color="contrast"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
icon={<IconPlus />}
|
||||||
|
text="Добавить задачу"
|
||||||
|
/>
|
||||||
|
<CustomButton
|
||||||
|
color="contrast"
|
||||||
|
style="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
icon={<IconSettings />}
|
||||||
|
text="Параметры эксперимента"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{experiment.tasks_ids.length > 0 && (
|
||||||
|
<PaginationContainer>
|
||||||
|
{experiment.tasks_ids.map((task: ExperimentTask) => {
|
||||||
|
return <div>{task.data.name}</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"} size="md">
|
||||||
|
Ошибка. Эксперимент не найден
|
||||||
|
</Text>{" "}
|
||||||
|
</Center>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExperimentPage;
|
||||||
14
src/Pages/ExperimentsPage/ExperimentsPage.css
Normal file
14
src/Pages/ExperimentsPage/ExperimentsPage.css
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.ExperimentsPage {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experimentsButtons {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: right;
|
||||||
|
width: fit-content;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
67
src/Pages/ExperimentsPage/ExperimentsPage.tsx
Normal file
67
src/Pages/ExperimentsPage/ExperimentsPage.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import "./ExperimentsPage.css";
|
||||||
|
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
|
||||||
|
import { useState } from "react";
|
||||||
|
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";
|
||||||
|
|
||||||
|
function ExperimentsPage() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { experiments, teams, tasks } = useExperimentStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>Experiments Page | QMolSim</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="See the list of all of users experiments"
|
||||||
|
/>
|
||||||
|
</Helmet>
|
||||||
|
<NewExperimentModal isOpened={isOpen} setIsOpened={setIsOpen} />
|
||||||
|
<div className="ExperimentsPage">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "10px",
|
||||||
|
marginBottom: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="experimentsButtons">
|
||||||
|
<CustomButton
|
||||||
|
color="contrast"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
icon={<IconMicroscope />}
|
||||||
|
text="Создать эксперимент"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PaginationContainer>
|
||||||
|
{experiments.map((exp: Experiment) => {
|
||||||
|
return (
|
||||||
|
<ExperimentsListCard
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</PaginationContainer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExperimentsPage;
|
||||||
65
src/Pages/ExperimentsPage/MoleculePage.css
Executable file
65
src/Pages/ExperimentsPage/MoleculePage.css
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
.MoleculeEdit {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.MoleculeEditWrapper {
|
||||||
|
display: flex !important;
|
||||||
|
flex: 1 !important;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
background-color: var(--mantine-color-secondary-filled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.MoleculeEditLineNumber {
|
||||||
|
height: 100%;
|
||||||
|
padding-top: calc(1px + var(--input-padding-y, 0rem));
|
||||||
|
padding-bottom: calc(1px + var(--input-padding-y, 0rem));
|
||||||
|
font-family:
|
||||||
|
"Fira Code", "Courier New", Courier, monospace; /* Monospace fonts */
|
||||||
|
font-size: 14px; /* Comfortable size */
|
||||||
|
line-height: 1.5; /* Spacing like an editor */
|
||||||
|
letter-spacing: 0; /* Keeps punctuation aligned */
|
||||||
|
white-space: pre-line;
|
||||||
|
overflow: scroll;
|
||||||
|
scrollbar-width: none;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MoleculeEditInput {
|
||||||
|
font-family:
|
||||||
|
"Fira Code", "Courier New", Courier, monospace; /* Monospace fonts */
|
||||||
|
font-size: 14px; /* Comfortable size */
|
||||||
|
line-height: 1.5; /* Spacing like an editor */
|
||||||
|
letter-spacing: 0; /* Keeps punctuation aligned */
|
||||||
|
white-space: pre; /* preserves spaces & tabs */
|
||||||
|
overflow-x: auto; /* horizontal scroll when needed */
|
||||||
|
overflow-y: auto; /* vertical scroll when needed */
|
||||||
|
word-wrap: normal; /* prevent wrapping */
|
||||||
|
overscroll-behavior: none;
|
||||||
|
background-color: var(--mantine-color-secondary-filled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Separator {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moleculeInput {
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moleculeInputWrapper {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moleculeInputWrapper > * {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabPanel {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border-left: 1px solid var(--tab-border-color);
|
||||||
|
border-right: 1px solid var(--tab-border-color);
|
||||||
|
border-bottom: 1px solid var(--tab-border-color);
|
||||||
|
}
|
||||||
113
src/Pages/ExperimentsPage/MoleculePage.tsx
Executable file
113
src/Pages/ExperimentsPage/MoleculePage.tsx
Executable file
@@ -0,0 +1,113 @@
|
|||||||
|
import MoleculeViewer from "Components/MoleculeViewer/MoleculeViewer";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Divider,
|
||||||
|
Tabs,
|
||||||
|
Text,
|
||||||
|
useMantineTheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { Group, Panel, Separator } from "react-resizable-panels";
|
||||||
|
import "./MoleculePage.css";
|
||||||
|
import {
|
||||||
|
IconCheck,
|
||||||
|
IconCode,
|
||||||
|
IconGripVertical,
|
||||||
|
IconList,
|
||||||
|
IconX,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { useMoleculeEditStore } from "Stores/MoleculeEditStore";
|
||||||
|
import { CodeTextArea } from "Components/CodeTextArea/CodeTextArea";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
|
function MoleculeEditorPage() {
|
||||||
|
const {
|
||||||
|
currentMoleculeString,
|
||||||
|
setCurrentMoleculeString,
|
||||||
|
selectedAtom,
|
||||||
|
setSelectedAtom,
|
||||||
|
} = useMoleculeEditStore();
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>Molecule Page | QMolSim</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Edit the atomic structure of a molecule"
|
||||||
|
/>
|
||||||
|
</Helmet>
|
||||||
|
<div className="experimentButtons">
|
||||||
|
<Button color="secondary" onClick={() => {}}>
|
||||||
|
<IconCheck style={{ marginRight: "10px" }} />
|
||||||
|
<Text c="contrast">Сохранить</Text>
|
||||||
|
</Button>
|
||||||
|
<Button color="contrast" onClick={() => {}}>
|
||||||
|
<IconX style={{ marginRight: "10px" }} color="black" />
|
||||||
|
<Text c="secondary">Отменить</Text>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", height: "85vh" }}>
|
||||||
|
<Divider style={{ margin: "10px" }} />
|
||||||
|
<Group style={{ flex: 1, gap: "5px" }}>
|
||||||
|
<Panel minSize={200} defaultSize={500}>
|
||||||
|
<Tabs
|
||||||
|
variant="outline"
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
radius="lg"
|
||||||
|
defaultValue="code"
|
||||||
|
classNames={{ panel: "tabPanel" }}
|
||||||
|
>
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Tab value="code" leftSection={<IconCode size={12} />}>
|
||||||
|
Код
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="list" leftSection={<IconList size={12} />}>
|
||||||
|
Список
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
<Tabs.Panel value="code">
|
||||||
|
<CodeTextArea
|
||||||
|
text={currentMoleculeString}
|
||||||
|
onTextChange={(text: string) => {
|
||||||
|
setCurrentMoleculeString(text);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
<Tabs.Panel value="list">
|
||||||
|
<></>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
</Panel>
|
||||||
|
<Separator>
|
||||||
|
<Center
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
background: theme.colors.dark[4],
|
||||||
|
borderRadius: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconGripVertical size={12} />
|
||||||
|
</Center>
|
||||||
|
</Separator>
|
||||||
|
<Panel style={{ flexShrink: 0 }} minSize={200}>
|
||||||
|
<MoleculeViewer
|
||||||
|
moleculeData={currentMoleculeString}
|
||||||
|
selectedAtom={selectedAtom}
|
||||||
|
setSelectedAtom={setSelectedAtom}
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MoleculeEditorPage;
|
||||||
41
src/Pages/MainPage/MainPage.css
Executable file
41
src/Pages/MainPage/MainPage.css
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
.background_holder {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slogan {
|
||||||
|
width: 100%;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 5vh;
|
||||||
|
padding-bottom: 10vh;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.slogan > * {
|
||||||
|
font-family: "Quicking";
|
||||||
|
text-align: center;
|
||||||
|
color: var(--mantine-color-contrast-filled);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiles {
|
||||||
|
width: 100%;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 40px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#documentationButton {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10vh;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
63
src/Pages/MainPage/MainPage.tsx
Executable file
63
src/Pages/MainPage/MainPage.tsx
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Title, Button, Group } from "@mantine/core";
|
||||||
|
import Background from "Components/StripedBg/Background";
|
||||||
|
import "./MainPage.css";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import ButtonWithText from "Components/CardWithButton/CardWithButton";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import { routes } from "Routes/Routes";
|
||||||
|
function MainPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Helmet>
|
||||||
|
<title>Landing Page | QMolSim</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Description of the main functionaity of the QMolSim service"
|
||||||
|
/>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<div className="background_holder">
|
||||||
|
<Background />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="slogan">
|
||||||
|
<Title size={50} textWrap="balance">
|
||||||
|
Quantum Chemistry Made easy
|
||||||
|
</Title>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Group className="tiles" justify="space-evenly">
|
||||||
|
<ButtonWithText
|
||||||
|
title="Работа с молекулами"
|
||||||
|
text="Вводите информацию о молекулах любым удобным способом. Визуализируйте и
|
||||||
|
редактируйте молекулы в простом интерфейсе."
|
||||||
|
buttonText="Начать Эксперимент"
|
||||||
|
link={routes.ExperimentsPage.path}
|
||||||
|
/>
|
||||||
|
<ButtonWithText
|
||||||
|
title="Параллельные вычисления"
|
||||||
|
text="Запускайте квантовые вычисления сразу на нескольких машинах.
|
||||||
|
Упарвляйте и следите за машинами в одном месте."
|
||||||
|
buttonText="Подключить Машину"
|
||||||
|
link={routes.MachinesPage.path}
|
||||||
|
/>
|
||||||
|
<ButtonWithText
|
||||||
|
title="Работа в команде"
|
||||||
|
text="Создавайте команы с другими людьми и настраивайте доступ ко всем
|
||||||
|
аспектам работы"
|
||||||
|
buttonText="Создать Команду"
|
||||||
|
link={routes.TeamsPage.path}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Link className="invisible_link" to={routes.DocumentationPage.path}>
|
||||||
|
<div id="documentationButton">
|
||||||
|
<Button variant="subtle" color="secondaryContrast">
|
||||||
|
Документация
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MainPage;
|
||||||
4
src/Pages/TeamsPage/TeamsPage.css
Normal file
4
src/Pages/TeamsPage/TeamsPage.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.TeamsPage {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
22
src/Pages/TeamsPage/TeamsPage.tsx
Normal file
22
src/Pages/TeamsPage/TeamsPage.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import "./TeamsPage.css";
|
||||||
|
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
|
||||||
|
|
||||||
|
function TeamsPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>Teams Page | QMolSim</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="See the documentation on how to setup and use the qunatum computational system"
|
||||||
|
/>
|
||||||
|
</Helmet>
|
||||||
|
<div className="TeamsPage">
|
||||||
|
<PaginationContainer />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TeamsPage;
|
||||||
12
src/Pages/UserPage/UserPage.css
Normal file
12
src/Pages/UserPage/UserPage.css
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.tabPanel {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border-left: 1px solid var(--tab-border-color);
|
||||||
|
border-right: 1px solid var(--tab-border-color);
|
||||||
|
border-bottom: 1px solid var(--tab-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userPage {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
166
src/Pages/UserPage/UserPage.tsx
Normal file
166
src/Pages/UserPage/UserPage.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Title,
|
||||||
|
Tabs,
|
||||||
|
TextInput,
|
||||||
|
SimpleGrid,
|
||||||
|
Chip,
|
||||||
|
Text,
|
||||||
|
Divider,
|
||||||
|
Switch,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
IconChartCandle,
|
||||||
|
IconCheck,
|
||||||
|
IconUserFilled,
|
||||||
|
IconCancel,
|
||||||
|
IconSun,
|
||||||
|
IconMoonStars,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import keycloak from "Api/Keycloak/Keycloak";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { AuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
|
import "./UserPage.css";
|
||||||
|
import { useUserPreferencesStore } from "Stores/PreferencesStore";
|
||||||
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
function UserPage() {
|
||||||
|
const { profile, token } = AuthenticationStore();
|
||||||
|
const { theme, set_theme } = useUserPreferencesStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(token);
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>User Page | QMolSim</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="See the documentation on how to setup and use the qunatum computational system"
|
||||||
|
/>
|
||||||
|
</Helmet>
|
||||||
|
<div className="userPage">
|
||||||
|
<Tabs
|
||||||
|
variant="outline"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexGrow: 1,
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
radius="lg"
|
||||||
|
defaultValue="profile"
|
||||||
|
classNames={{ panel: "tabPanel" }}
|
||||||
|
>
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="profile"
|
||||||
|
leftSection={<IconUserFilled size={20} />}
|
||||||
|
>
|
||||||
|
Профиль
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="preference"
|
||||||
|
leftSection={<IconChartCandle size={20} />}
|
||||||
|
>
|
||||||
|
Предпочтения
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
<Tabs.Panel value="profile">
|
||||||
|
<SimpleGrid verticalSpacing="lg" cols={2}>
|
||||||
|
<Title size="lg">Имя пользователя:</Title>
|
||||||
|
<TextInput size="md" value={profile?.username} />
|
||||||
|
<Title size="lg">Почта:</Title>
|
||||||
|
<TextInput size="md" value={profile?.email} />
|
||||||
|
<CustomButton color="accent" text="Сохранить" />
|
||||||
|
<CustomButton color="error" text="Отменить" />
|
||||||
|
</SimpleGrid>
|
||||||
|
<Divider my="lg" />
|
||||||
|
<SimpleGrid verticalSpacing={"lg"} cols={2}>
|
||||||
|
<Title
|
||||||
|
size="lg"
|
||||||
|
style={{ display: "flex", alignItems: "center" }}
|
||||||
|
>
|
||||||
|
Почта верифицирована:{" "}
|
||||||
|
<Chip mx="lg" size="auto" checked={profile?.emailVerified}>
|
||||||
|
{profile?.emailVerified ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconCheck
|
||||||
|
size={25}
|
||||||
|
color={"green"}
|
||||||
|
style={{ verticalAlign: "center", margin: "5px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconCancel
|
||||||
|
size={25}
|
||||||
|
color="red"
|
||||||
|
style={{
|
||||||
|
verticalAlign: "center",
|
||||||
|
margin: "5px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Chip>
|
||||||
|
</Title>
|
||||||
|
{!profile?.emailVerified && (
|
||||||
|
<CustomButton
|
||||||
|
style="outline"
|
||||||
|
color="contrast"
|
||||||
|
text="Подтвердить почту"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{profile?.emailVerified && <div></div>}
|
||||||
|
<Title size="lg">Пароль:</Title>
|
||||||
|
<CustomButton
|
||||||
|
style="outline"
|
||||||
|
color="contrast"
|
||||||
|
text="Изменить пароль"
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
<Tabs.Panel value="preference">
|
||||||
|
<Switch
|
||||||
|
size="xl"
|
||||||
|
onChange={(event) =>
|
||||||
|
set_theme(!event.currentTarget.checked ? "dark" : "light")
|
||||||
|
}
|
||||||
|
onLabel={
|
||||||
|
<IconSun
|
||||||
|
size={16}
|
||||||
|
stroke={2.5}
|
||||||
|
color="var(--mantine-color-yellow-4)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
offLabel={
|
||||||
|
<IconMoonStars
|
||||||
|
size={16}
|
||||||
|
stroke={2.5}
|
||||||
|
color="var(--mantine-color-blue-6)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserPage;
|
||||||
10
src/Routes/Breadcrumbs/Breadcrumbs.css
Executable file
10
src/Routes/Breadcrumbs/Breadcrumbs.css
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
.mantine-Breadcrumbs-root {
|
||||||
|
height: fit-content;
|
||||||
|
z-index: 5;
|
||||||
|
color: var(--mantine-color-secondaryContrast-filled) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlighted_breadcrumb {
|
||||||
|
color: var(--mantine-color-contrast-filled);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
104
src/Routes/Breadcrumbs/Breadcrumbs.tsx
Executable file
104
src/Routes/Breadcrumbs/Breadcrumbs.tsx
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
import { Breadcrumbs } from "@mantine/core";
|
||||||
|
import { type ReactElement } from "react";
|
||||||
|
import { Link, type UIMatch } from "react-router";
|
||||||
|
import "./Breadcrumbs.css";
|
||||||
|
import { routes } from "Routes/Routes";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
|
function getSubPaths(path: string) {
|
||||||
|
const parts = path.split("/").filter(Boolean);
|
||||||
|
const result = ["/"];
|
||||||
|
|
||||||
|
let current = "";
|
||||||
|
for (const part of parts) {
|
||||||
|
current += "/" + part;
|
||||||
|
result.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function testEqual(path: string, pattern: string) {
|
||||||
|
const patternParts = pattern.split("/").filter(Boolean);
|
||||||
|
const pathParts = path.split("/").filter(Boolean);
|
||||||
|
|
||||||
|
// Length must match
|
||||||
|
if (patternParts.length !== pathParts.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < patternParts.length; i++) {
|
||||||
|
const patternPart = patternParts[i];
|
||||||
|
const pathPart = pathParts[i];
|
||||||
|
|
||||||
|
// If pattern starts with ":" it's a variable → always matches
|
||||||
|
if (patternPart.startsWith(":")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise must match exactly
|
||||||
|
if (patternPart !== pathPart) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadCrumbs() {
|
||||||
|
const unique_matches: string[] = getSubPaths(useLocation().pathname);
|
||||||
|
|
||||||
|
console.log(unique_matches);
|
||||||
|
//find the breadcrumbs for the matched pathes
|
||||||
|
const elements: ReactElement[] = [];
|
||||||
|
for (const prop in routes) {
|
||||||
|
for (const u_match in unique_matches) {
|
||||||
|
if (testEqual(unique_matches[u_match], routes[prop].path)) {
|
||||||
|
console.log(unique_matches[u_match], routes[prop].path);
|
||||||
|
for (const i in routes[prop].breadcrumbs(unique_matches[u_match])) {
|
||||||
|
if (elements.length + 1 != unique_matches.length) {
|
||||||
|
elements.push(
|
||||||
|
<Link
|
||||||
|
className="invisible_link"
|
||||||
|
to={unique_matches[u_match]}
|
||||||
|
key={unique_matches[u_match]}
|
||||||
|
>
|
||||||
|
{routes[prop].breadcrumbs(unique_matches[u_match])[i]}
|
||||||
|
</Link>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
elements.push(
|
||||||
|
<div
|
||||||
|
className="highlighted_breadcrumb"
|
||||||
|
key={unique_matches[u_match]}
|
||||||
|
>
|
||||||
|
{routes[prop].breadcrumbs(unique_matches[u_match])[i]}
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//last breadcrumb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//wrong count of breadcrumbs found, meaning error
|
||||||
|
if (elements.length != unique_matches.length) {
|
||||||
|
elements.push(
|
||||||
|
<div className="highlighted_breadcrumb" key={routes.ErrorPage.path}>
|
||||||
|
{routes.ErrorPage.breadcrumbs("")}
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isGood = elements.length > 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Breadcrumbs separator="/" mb="xs">
|
||||||
|
{isGood && elements}
|
||||||
|
{!isGood && <p></p>}
|
||||||
|
</Breadcrumbs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BreadCrumbs;
|
||||||
17
src/Routes/ErrorPage.tsx
Executable file
17
src/Routes/ErrorPage.tsx
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Alert } from "@mantine/core";
|
||||||
|
import { IconExclamationCircle } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
function ErrorPage() {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
title="Page not found"
|
||||||
|
icon={<IconExclamationCircle />}
|
||||||
|
>
|
||||||
|
The page you are trying to access does not exist or has been moved.
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorPage;
|
||||||
23
src/Routes/RouterAuhGuard.tsx
Normal file
23
src/Routes/RouterAuhGuard.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import keycloak from "Api/Keycloak/Keycloak";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { Outlet } from "react-router";
|
||||||
|
|
||||||
|
export default function AuthGuard() {
|
||||||
|
const loginStarted = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!keycloak.authenticated && !loginStarted.current) {
|
||||||
|
loginStarted.current = true;
|
||||||
|
|
||||||
|
keycloak.login({
|
||||||
|
redirectUri: window.location.href,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!keycloak.authenticated) {
|
||||||
|
return <div>Redirecting to login...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
99
src/Routes/Routes.tsx
Executable file
99
src/Routes/Routes.tsx
Executable file
@@ -0,0 +1,99 @@
|
|||||||
|
import { createBrowserRouter } from "react-router";
|
||||||
|
import MainPage from "Pages/MainPage/MainPage";
|
||||||
|
import { baseUrl } from "../GlobalVars";
|
||||||
|
import App from "../App";
|
||||||
|
import ErrorPage from "./ErrorPage";
|
||||||
|
import DocumentationPage from "Pages/DocumentationPage/DocumentationPage";
|
||||||
|
import { IconHome } from "@tabler/icons-react";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import MoleculeEditorPage from "Pages/ExperimentsPage/MoleculePage";
|
||||||
|
import TeamsPage from "Pages/TeamsPage/TeamsPage";
|
||||||
|
import UserPage from "Pages/UserPage/UserPage";
|
||||||
|
import ExperimentPage from "Pages/ExperimentsPage/ExperimentPage";
|
||||||
|
import ExperimentsPage from "Pages/ExperimentsPage/ExperimentsPage";
|
||||||
|
import AuthGuard from "./RouterAuhGuard";
|
||||||
|
|
||||||
|
export const routes: {
|
||||||
|
[id: string]: { path: string; breadcrumbs: (path: string) => ReactElement[] };
|
||||||
|
} = {
|
||||||
|
MainPage: { path: "/", breadcrumbs: () => [<IconHome />] },
|
||||||
|
DocumentationPage: {
|
||||||
|
path: "/documentation",
|
||||||
|
breadcrumbs: () => [<>Документация</>],
|
||||||
|
},
|
||||||
|
ExperimentsPage: {
|
||||||
|
path: "/experiments",
|
||||||
|
breadcrumbs: () => [<>Эксперименты</>],
|
||||||
|
},
|
||||||
|
ExperimentPage: {
|
||||||
|
path: "/experiments/:experiment_id",
|
||||||
|
breadcrumbs: (path: string) => [
|
||||||
|
<>Эксперимент #{path.split("/")[path.split("/").length - 1]}</>,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
MoleculePage: {
|
||||||
|
path: "/experiments/:experiment_id/:molecule_id",
|
||||||
|
breadcrumbs: (path: string) => [
|
||||||
|
<>Молекула #{path.split("/")[path.split("/").length - 1]}</>,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
MachinesPage: {
|
||||||
|
path: "/machines",
|
||||||
|
breadcrumbs: () => [<>Вычислительные системы</>],
|
||||||
|
},
|
||||||
|
TeamsPage: { path: "/teams", breadcrumbs: () => [<>Команды</>] },
|
||||||
|
ErrorPage: { path: "/*", breadcrumbs: () => [<>Ошибка</>] },
|
||||||
|
SettingsPage: { path: "/settings", breadcrumbs: () => [<>Настройки</>] },
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createBrowserRouter(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <App />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: routes.MainPage.path,
|
||||||
|
Component: MainPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: routes.DocumentationPage.path,
|
||||||
|
Component: DocumentationPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: routes.SettingsPage.path,
|
||||||
|
Component: UserPage,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
element: <AuthGuard />, // 🔒 everything below requires auth
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: routes.ExperimentsPage.path,
|
||||||
|
Component: ExperimentsPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: routes.ExperimentPage.path,
|
||||||
|
Component: ExperimentPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: routes.MoleculePage.path,
|
||||||
|
Component: MoleculeEditorPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: routes.TeamsPage.path,
|
||||||
|
Component: TeamsPage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: routes.ErrorPage.path,
|
||||||
|
Component: ErrorPage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ basename: baseUrl },
|
||||||
|
);
|
||||||
|
export default router;
|
||||||
19
src/Stores/AuthenticationStore.tsx
Normal file
19
src/Stores/AuthenticationStore.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { KeycloakProfile } from "keycloak-js";
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface AuthernticationStoreState {
|
||||||
|
token: string | undefined;
|
||||||
|
profile: KeycloakProfile | null;
|
||||||
|
set_profile: (profile: KeycloakProfile | null) => void;
|
||||||
|
set_token: (token: string | undefined) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthenticationStore = create<AuthernticationStoreState>()(
|
||||||
|
(set) => ({
|
||||||
|
token: undefined,
|
||||||
|
profile: null,
|
||||||
|
set_profile: (profile: KeycloakProfile | null) =>
|
||||||
|
set(() => ({ profile: profile })),
|
||||||
|
set_token: (token: string | undefined) => set(() => ({ token: token })),
|
||||||
|
}),
|
||||||
|
);
|
||||||
138
src/Stores/ExperimentStore.tsx
Normal file
138
src/Stores/ExperimentStore.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
|
||||||
|
import type { Experiment, TaskData } from "Types/Experiment/Experiment";
|
||||||
|
import type { Team } from "Types/User/User";
|
||||||
|
|
||||||
|
interface ExperimentStoreState {
|
||||||
|
experiments: Experiment[];
|
||||||
|
tasks: TaskData[];
|
||||||
|
teams: Team[];
|
||||||
|
|
||||||
|
selectedExperimentId?: number;
|
||||||
|
selectedTaskId?: number;
|
||||||
|
selectedTeamId?: number;
|
||||||
|
|
||||||
|
selectExperiment: (id: number | undefined) => void;
|
||||||
|
selectTask: (id: number | undefined) => void;
|
||||||
|
|
||||||
|
addExperiment: (experiment: Experiment) => void;
|
||||||
|
updateExperiment: (id: number, data: Partial<Experiment>) => void;
|
||||||
|
removeExperiment: (id: number) => void;
|
||||||
|
|
||||||
|
addTask: (experimentId: number, task: TaskData) => void;
|
||||||
|
updateTask: (taskId: number, data: Partial<TaskData>) => void;
|
||||||
|
removeTask: (experimentId: number, taskId: number) => void;
|
||||||
|
|
||||||
|
addTeam: (team: Team) => void;
|
||||||
|
updateTeam: (teamId: number, data: Partial<TaskData>) => void;
|
||||||
|
removeTeam: (teamId: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useExperimentStore = create<ExperimentStoreState>()(
|
||||||
|
immer((set) => ({
|
||||||
|
experiments: [],
|
||||||
|
tasks: [],
|
||||||
|
teams: [],
|
||||||
|
|
||||||
|
selectedExperimentId: undefined,
|
||||||
|
selectedTaskId: undefined,
|
||||||
|
selectedTeamId: undefined,
|
||||||
|
|
||||||
|
selectExperiment: (id) =>
|
||||||
|
set((state) => {
|
||||||
|
state.selectedExperimentId = id;
|
||||||
|
}),
|
||||||
|
|
||||||
|
selectTask: (id) =>
|
||||||
|
set((state) => {
|
||||||
|
state.selectedTaskId = id;
|
||||||
|
}),
|
||||||
|
|
||||||
|
addExperiment: (experiment) =>
|
||||||
|
set((state) => {
|
||||||
|
state.experiments.push(experiment);
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateExperiment: (id, data) =>
|
||||||
|
set((state) => {
|
||||||
|
const exp = state.experiments.find((e) => e.id === id);
|
||||||
|
if (!exp) return;
|
||||||
|
|
||||||
|
Object.assign(exp, data);
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeExperiment: (id) =>
|
||||||
|
set((state) => {
|
||||||
|
state.experiments = state.experiments.filter((e) => e.id !== id);
|
||||||
|
|
||||||
|
if (state.selectedExperimentId === id) {
|
||||||
|
state.selectedExperimentId = undefined;
|
||||||
|
state.selectedTaskId = undefined;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
addTask: (experimentId, task) =>
|
||||||
|
set((state) => {
|
||||||
|
const exp = state.experiments.find((e) => e.id === experimentId);
|
||||||
|
if (!exp) return;
|
||||||
|
|
||||||
|
exp.tasks_ids.push(task.id);
|
||||||
|
state.tasks.push(task);
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateTask: (taskId, data) =>
|
||||||
|
set((state) => {
|
||||||
|
const task = state.tasks.find((t) => t.id === taskId);
|
||||||
|
if (!task) return;
|
||||||
|
|
||||||
|
Object.assign(task, data);
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeTask: (experimentId, taskId) =>
|
||||||
|
set((state) => {
|
||||||
|
const exp = state.experiments.find((e) => e.id === experimentId);
|
||||||
|
if (!exp) return;
|
||||||
|
|
||||||
|
state.tasks = state.tasks.filter((t) => t.data.id !== taskId);
|
||||||
|
exp.tasks_ids = exp.tasks_ids.filter((t) => t !== taskId);
|
||||||
|
|
||||||
|
if (state.selectedTaskId === taskId) {
|
||||||
|
state.selectedTaskId = undefined;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
addTeam: (team) =>
|
||||||
|
set((state) => {
|
||||||
|
state.teams.push(team);
|
||||||
|
}),
|
||||||
|
updateTeam: (teamID, data) =>
|
||||||
|
set((state) => {
|
||||||
|
const team = state.teams.find((t) => t.id === teamID);
|
||||||
|
if (!team) return;
|
||||||
|
|
||||||
|
Object.assign(team, data);
|
||||||
|
}),
|
||||||
|
removeTeam: (teamID) =>
|
||||||
|
set((state) => {
|
||||||
|
state.teams = state.teams.filter((t) => t.id !== teamID);
|
||||||
|
state.experiments = state.experiments.filter(
|
||||||
|
(t) => t.team_id !== teamID,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (state.selectedTeamId === teamID) {
|
||||||
|
state.selectedTeamId = undefined;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useSelectedExperiment = () =>
|
||||||
|
useExperimentStore((s) =>
|
||||||
|
s.experiments.find((e) => e.id === s.selectedExperimentId),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useSelectedTask = () =>
|
||||||
|
useExperimentStore((s) => {
|
||||||
|
return s.tasks.find((t) => t.id === s.selectedTaskId);
|
||||||
|
});
|
||||||
12
src/Stores/LayoutStore.tsx
Executable file
12
src/Stores/LayoutStore.tsx
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface LayoutStore {
|
||||||
|
is_navbar_open: boolean;
|
||||||
|
set_navbar_open: (is_dark: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLayoutStore = create<LayoutStore>()((set) => ({
|
||||||
|
is_navbar_open: false,
|
||||||
|
set_navbar_open: (is_navbar_open) =>
|
||||||
|
set(() => ({ is_navbar_open: is_navbar_open })),
|
||||||
|
}));
|
||||||
17
src/Stores/MoleculeEditStore.tsx
Executable file
17
src/Stores/MoleculeEditStore.tsx
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { SelectedAtom } from "Types/Experiment/MoleculeEdit/MoleculeEdit";
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface MoleculeEditState {
|
||||||
|
currentMoleculeString: string;
|
||||||
|
setCurrentMoleculeString: (setCurrentMoleculeString: string) => void;
|
||||||
|
selectedAtom: SelectedAtom | null;
|
||||||
|
setSelectedAtom: (selectedAtom: SelectedAtom | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMoleculeEditStore = create<MoleculeEditState>()((set) => ({
|
||||||
|
currentMoleculeString: "",
|
||||||
|
setCurrentMoleculeString: (molString) =>
|
||||||
|
set(() => ({ currentMoleculeString: molString })),
|
||||||
|
selectedAtom: null,
|
||||||
|
setSelectedAtom: (atom) => set(() => ({ selectedAtom: atom })),
|
||||||
|
}));
|
||||||
17
src/Stores/PreferencesStore.tsx
Executable file
17
src/Stores/PreferencesStore.tsx
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface UserPreferencesState {
|
||||||
|
show_hints: boolean;
|
||||||
|
set_show_hints: (is_dark: boolean) => void;
|
||||||
|
theme: "light" | "dark";
|
||||||
|
set_theme: (theme: "light" | "dark") => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserPreferencesStore = create<UserPreferencesState>()(
|
||||||
|
(set) => ({
|
||||||
|
show_hints: true,
|
||||||
|
set_show_hints: (show_hints) => set(() => ({ show_hints: show_hints })),
|
||||||
|
theme: "dark",
|
||||||
|
set_theme: (theme) => set(() => ({ theme: theme })),
|
||||||
|
}),
|
||||||
|
);
|
||||||
7
src/Types/ApiCalls/ConvertBackendCallsTypes.tsx
Executable file
7
src/Types/ApiCalls/ConvertBackendCallsTypes.tsx
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface ConvertSchema {
|
||||||
|
inputText: string;
|
||||||
|
inputFormat: string;
|
||||||
|
add_h: boolean;
|
||||||
|
make_3d: boolean;
|
||||||
|
optimize: boolean;
|
||||||
|
}
|
||||||
41
src/Types/Experiment/Experiment.tsx
Executable file
41
src/Types/Experiment/Experiment.tsx
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
//export interface Experiment {}
|
||||||
|
|
||||||
|
import type { Team } from "Types/User/User";
|
||||||
|
|
||||||
|
export type ExperimentStatus =
|
||||||
|
| "DRAFT"
|
||||||
|
| "QUEUE"
|
||||||
|
| "PROCESSING"
|
||||||
|
| "SUCCESS"
|
||||||
|
| "ERROR";
|
||||||
|
|
||||||
|
export interface Experiment {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
team_id: number;
|
||||||
|
date_created: Date;
|
||||||
|
experiment_status: ExperimentStatus;
|
||||||
|
experiment_type: string;
|
||||||
|
tasks_ids: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskTypePlugin<TData = any> {
|
||||||
|
type: string;
|
||||||
|
// how it appears in the experiment task list
|
||||||
|
ListItem: React.ComponentType<TaskData<TData>>;
|
||||||
|
// full editor UI when clicking task
|
||||||
|
Editor: React.ComponentType<TaskEditorProps<TData>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskData<TData = any> {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
data: TData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskEditorProps<TData = any> {
|
||||||
|
data: TData;
|
||||||
|
setData: (data: TData) => void;
|
||||||
|
}
|
||||||
5
src/Types/Experiment/MoleculeEdit/MoleculeEdit.tsx
Executable file
5
src/Types/Experiment/MoleculeEdit/MoleculeEdit.tsx
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface SelectedAtom {
|
||||||
|
serial: number;
|
||||||
|
name: string;
|
||||||
|
position: { x: number; y: number; z: number };
|
||||||
|
}
|
||||||
20
src/Types/User/User.tsx
Normal file
20
src/Types/User/User.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export interface Team {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
creation_date: Date;
|
||||||
|
creator: TeamMember;
|
||||||
|
team_members: TeamMember[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamMember {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
permissions: Permission[];
|
||||||
|
date_accepted: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Permission {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
29
src/index.css
Executable file
29
src/index.css
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Quicking"; /* Define a name for your font */
|
||||||
|
src: url("/Quicking.otf") format("opentype"); /* Specify the font file and format */
|
||||||
|
font-weight: normal; /* Optional: Define the weight of this font variant */
|
||||||
|
font-style: normal; /* Optional: Define the style of this font variant */
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--mantine-color-body: var(--mantine-color-primary-filled) !important;
|
||||||
|
}
|
||||||
97
src/main.tsx
Executable file
97
src/main.tsx
Executable file
@@ -0,0 +1,97 @@
|
|||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import "./index.css";
|
||||||
|
import "@mantine/core/styles.css";
|
||||||
|
import { RouterProvider } from "react-router";
|
||||||
|
import router from "Routes/Routes.tsx";
|
||||||
|
import {
|
||||||
|
colorsTuple,
|
||||||
|
createTheme,
|
||||||
|
localStorageColorSchemeManager,
|
||||||
|
MantineProvider,
|
||||||
|
virtualColor,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import keycloak from "Api/Keycloak/Keycloak";
|
||||||
|
import { AuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
|
import { useUserPreferencesStore } from "Stores/PreferencesStore";
|
||||||
|
|
||||||
|
const colorSchemeManager = localStorageColorSchemeManager({
|
||||||
|
key: "my-app-color-scheme",
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkIsDarkSchemePreferred = () =>
|
||||||
|
window?.matchMedia?.("(prefers-color-scheme:dark)")?.matches ?? false;
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
colors: {
|
||||||
|
primaryDark: colorsTuple("#212529"),
|
||||||
|
primaryLight: colorsTuple("#f1f3f5"),
|
||||||
|
secondaryDark: colorsTuple("#484d53"),
|
||||||
|
secondaryLight: colorsTuple("#b9bec4"),
|
||||||
|
contrastDark: colorsTuple("#f1f3f5"),
|
||||||
|
contrastLight: colorsTuple("#212529"),
|
||||||
|
secondaryContrastDark: colorsTuple("#b9bec4"),
|
||||||
|
secondaryContrastLight: colorsTuple("#484d53"),
|
||||||
|
primary: virtualColor({
|
||||||
|
name: "primary",
|
||||||
|
dark: "primaryDark",
|
||||||
|
light: "primaryLight",
|
||||||
|
}),
|
||||||
|
secondary: virtualColor({
|
||||||
|
name: "secondary",
|
||||||
|
dark: "secondaryDark",
|
||||||
|
light: "secondaryLight",
|
||||||
|
}),
|
||||||
|
contrast: virtualColor({
|
||||||
|
name: "contrast",
|
||||||
|
dark: "contrastDark",
|
||||||
|
light: "contrastLight",
|
||||||
|
}),
|
||||||
|
secondaryContrast: virtualColor({
|
||||||
|
name: "secondaryContrast",
|
||||||
|
dark: "secondaryContrastDark",
|
||||||
|
light: "secondaryContrastLight",
|
||||||
|
}),
|
||||||
|
accent: virtualColor({
|
||||||
|
name: "accent",
|
||||||
|
dark: "blue",
|
||||||
|
light: "blue",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const authenticated = await keycloak.init({
|
||||||
|
onLoad: "check-sso",
|
||||||
|
pkceMethod: "S256",
|
||||||
|
silentCheckSsoRedirectUri:
|
||||||
|
"http://localhost:8001/quantum/silent-check-sso.html",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authenticated) {
|
||||||
|
const profile = await keycloak.loadUserProfile();
|
||||||
|
|
||||||
|
AuthenticationStore.setState({ token: keycloak.token, profile: profile });
|
||||||
|
}
|
||||||
|
|
||||||
|
useUserPreferencesStore.setState({
|
||||||
|
theme: checkIsDarkSchemePreferred() ? "dark" : "light",
|
||||||
|
});
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(<AppProviders />);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppProviders() {
|
||||||
|
const themePreference = useUserPreferencesStore((state) => state.theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MantineProvider
|
||||||
|
colorSchemeManager={colorSchemeManager}
|
||||||
|
forceColorScheme={themePreference}
|
||||||
|
theme={theme}
|
||||||
|
>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap();
|
||||||
1
src/vite-env.d.ts
vendored
Executable file
1
src/vite-env.d.ts
vendored
Executable file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
28
tsconfig.app.json
Executable file
28
tsconfig.app.json
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"baseUrl": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Executable file
7
tsconfig.json
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
],
|
||||||
|
}
|
||||||
25
tsconfig.node.json
Executable file
25
tsconfig.node.json
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
14
vite.config.ts
Executable file
14
vite.config.ts
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
import { baseDomain, basePort, baseUrl } from "./src/GlobalVars";
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tsconfigPaths()],
|
||||||
|
server: {
|
||||||
|
host: baseDomain,
|
||||||
|
port: basePort,
|
||||||
|
},
|
||||||
|
base: baseUrl,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user