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