v1.0
- added task rendering instead of molecule - fied a lot of errors - changes experiment page to show generic experiment info
This commit is contained in:
112
.gita/workflows/build-and-push.yml
Normal file
112
.gita/workflows/build-and-push.yml
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
name: Build and Deploy React Package
|
||||||
|
|
||||||
|
# Controls when the workflow will run. Here, it runs on every push to the 'main' branch.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
# Environment variables used across the workflow
|
||||||
|
env:
|
||||||
|
# The URL of your Gitea instance (without http:// or https://)
|
||||||
|
GITEA_INSTANCE_URL: git.deowl.ru
|
||||||
|
# The full name of your package (e.g., 'myusername/myproject')
|
||||||
|
PACKAGE_NAME: vkrb/quantum_frontend
|
||||||
|
# Node.js version to use
|
||||||
|
NODE_VERSION: "25.9.0"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-publish:
|
||||||
|
# Runs the job on a runner with the 'ubuntu-latest' label.
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# 1. Check out your repository code so the workflow can access it.
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# 2. Set up Node.js environment
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
|
# 3. Install pnpm globally
|
||||||
|
- name: Install pnpm
|
||||||
|
run: npm install -g pnpm
|
||||||
|
|
||||||
|
# 4. Install dependencies with pnpm
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
# 5. Build the React/Vite application
|
||||||
|
# This assumes your vite.config.js/ts is configured to output to 'dist'
|
||||||
|
- name: Build application
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
# 6. Create a tarball of the build artifacts
|
||||||
|
- name: Create package tarball
|
||||||
|
run: |
|
||||||
|
# Create a directory for the package
|
||||||
|
mkdir -p package_artifact
|
||||||
|
|
||||||
|
# Copy build artifacts (adjust path if your build outputs to a different directory)
|
||||||
|
cp -r dist package_artifact/
|
||||||
|
|
||||||
|
# Optionally copy other important files
|
||||||
|
# cp package.json package_artifact/
|
||||||
|
# cp README.md package_artifact/
|
||||||
|
|
||||||
|
# Create tarball
|
||||||
|
tar -czf package.tar.gz -C package_artifact .
|
||||||
|
|
||||||
|
# Generate checksum for integrity
|
||||||
|
sha256sum package.tar.gz > package.tar.gz.sha256
|
||||||
|
|
||||||
|
# 7. Log in to Gitea Package Registry
|
||||||
|
- name: Log in to Gitea Package Registry
|
||||||
|
run: |
|
||||||
|
# Create .netrc file for authentication with Gitea
|
||||||
|
cat > ~/.netrc << EOF
|
||||||
|
machine ${{ env.GITEA_INSTANCE_URL }}
|
||||||
|
login ${{ gitea.repository_owner }}
|
||||||
|
password ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 8. Upload the package to Gitea Generic Package Registry
|
||||||
|
- name: Upload to Gitea Generic Package Registry
|
||||||
|
run: |
|
||||||
|
# The format for Gitea generic packages:
|
||||||
|
# https://{GITEA_INSTANCE_URL}/api/packages/{owner}/generic/{package-name}/{version}/{file-name}
|
||||||
|
|
||||||
|
# Generate version from git commit SHA and timestamp
|
||||||
|
VERSION="${{ gitea.sha }}"
|
||||||
|
|
||||||
|
# Upload the tarball
|
||||||
|
curl --fail-with-body \
|
||||||
|
--netrc \
|
||||||
|
--upload-file package.tar.gz \
|
||||||
|
"https://${{ env.GITEA_INSTANCE_URL }}/api/packages/${{ gitea.repository_owner }}/generic/${{ env.PACKAGE_NAME }}/${VERSION}/package.tar.gz"
|
||||||
|
|
||||||
|
# Upload the checksum file
|
||||||
|
curl --fail-with-body \
|
||||||
|
--netrc \
|
||||||
|
--upload-file package.tar.gz.sha256 \
|
||||||
|
"https://${{ env.GITEA_INSTANCE_URL }}/api/packages/${{ gitea.repository_owner }}/generic/${{ env.PACKAGE_NAME }}/${VERSION}/package.tar.gz.sha256"
|
||||||
|
|
||||||
|
# 9. (Optional) Create a latest version for convenience
|
||||||
|
- name: Update latest version
|
||||||
|
run: |
|
||||||
|
# Upload as 'latest' version
|
||||||
|
curl --fail-with-body \
|
||||||
|
--netrc \
|
||||||
|
--upload-file package.tar.gz \
|
||||||
|
"https://${{ env.GITEA_INSTANCE_URL }}/api/packages/${{ gitea.repository_owner }}/generic/${{ env.PACKAGE_NAME }}/latest/package.tar.gz"
|
||||||
|
|
||||||
|
curl --fail-with-body \
|
||||||
|
--netrc \
|
||||||
|
--upload-file package.tar.gz.sha256 \
|
||||||
|
"https://${{ env.GITEA_INSTANCE_URL }}/api/packages/${{ gitea.repository_owner }}/generic/${{ env.PACKAGE_NAME }}/latest/package.tar.gz.sha256"
|
||||||
|
|
||||||
|
# 10. Clean up
|
||||||
|
- name: Clean up
|
||||||
|
run: rm -f package.tar.gz package.tar.gz.sha256
|
||||||
56
package.json
56
package.json
@@ -10,36 +10,36 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^9.1.1",
|
"@mantine/core": "9.1.1",
|
||||||
"@mantine/hooks": "^9.1.1",
|
"@mantine/hooks": "9.1.1",
|
||||||
"@mantine/notifications": "^9.1.1",
|
"@mantine/notifications": "9.1.1",
|
||||||
"@tabler/icons-react": "^3.34.1",
|
"@tabler/icons-react": "3.34.1",
|
||||||
"axios": "^1.13.2",
|
"axios": "1.13.2",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "17.4.2",
|
||||||
"immer": "^11.1.4",
|
"immer": "11.1.4",
|
||||||
"keycloak-js": "^26.2.3",
|
"keycloak-js": "26.2.3",
|
||||||
"miew-react": "^0.11.0",
|
"miew-react": "0.11.0",
|
||||||
"react": "^19.2.5",
|
"react": "19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "19.2.5",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "6.1.0",
|
||||||
"react-resizable-panels": "^4.5.4",
|
"react-resizable-panels": "4.5.4",
|
||||||
"react-router": "^7.9.6",
|
"react-router": "7.9.6",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.33.0",
|
"@eslint/js": "9.33.0",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "25.6.0",
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "18.2.0",
|
||||||
"@types/react-dom": "^18.2.0",
|
"@types/react-dom": "18.2.0",
|
||||||
"@types/react-helmet": "^6.1.11",
|
"@types/react-helmet": "6.1.11",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "5.0.2",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "9.33.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "0.4.20",
|
||||||
"globals": "^16.3.0",
|
"globals": "16.3.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.39.1",
|
"typescript-eslint": "8.39.1",
|
||||||
"vite": "^7.1.2"
|
"vite": "7.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2921
pnpm-lock.yaml
generated
Normal file
2921
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
5975
public/index.html
5975
public/index.html
File diff suppressed because one or more lines are too long
@@ -1,49 +0,0 @@
|
|||||||
import axios, { AxiosError } from "axios";
|
|
||||||
import type { ConvertSchema } from "Types/ApiCalls/ConvertBackendCallsTypes";
|
|
||||||
|
|
||||||
export async function ConvertMoleculeToStandart(
|
|
||||||
data: ConvertSchema,
|
|
||||||
): Promise<string | AxiosError> {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(
|
|
||||||
import.meta.env.VITE_MOLECULAR_BACKEND_URL + "/convert",
|
|
||||||
{
|
|
||||||
text: data.inputText,
|
|
||||||
format: data.inputFormat,
|
|
||||||
convert_3d: data.make_3d,
|
|
||||||
add_hydrogen: data.add_h,
|
|
||||||
optimize_geometry: data.optimize,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Response from the FastAPI JSONResponse
|
|
||||||
return response.data.molfile;
|
|
||||||
} catch (error) {
|
|
||||||
//Error handling
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
// Do something with the axios error...
|
|
||||||
return error;
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GetInFormats(): Promise<
|
|
||||||
{ [key: string]: string } | AxiosError
|
|
||||||
> {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
import.meta.env.VITE_MOLECULAR_BACKEND_URL + "/informats",
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
//Error handling
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
// Do something with the axios error...
|
|
||||||
return error;
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -61,8 +61,6 @@ export const updatePasswordWithRedirect = async (
|
|||||||
redirectUri:
|
redirectUri:
|
||||||
successRedirectUrl ||
|
successRedirectUrl ||
|
||||||
window.location.origin +
|
window.location.origin +
|
||||||
"/" +
|
|
||||||
import.meta.env.VITE_BASE_PATH +
|
|
||||||
"/" +
|
"/" +
|
||||||
routes.SettingsPage.path +
|
routes.SettingsPage.path +
|
||||||
"?password_updated=true",
|
"?password_updated=true",
|
||||||
@@ -85,7 +83,6 @@ export const SendEmailVerification = async () => {
|
|||||||
action: "VERIFY_EMAIL",
|
action: "VERIFY_EMAIL",
|
||||||
redirectUri:
|
redirectUri:
|
||||||
window.location.origin +
|
window.location.origin +
|
||||||
import.meta.env.VITE_BASE_PATH +
|
|
||||||
"/" +
|
"/" +
|
||||||
routes.SettingsPage.path +
|
routes.SettingsPage.path +
|
||||||
"?email_sent=true",
|
"?email_sent=true",
|
||||||
|
|||||||
@@ -1,58 +1,59 @@
|
|||||||
import React, { useRef, useEffect, useState, useMemo } from "react";
|
import { LoadingOverlay } from "@mantine/core";
|
||||||
|
import React, { useRef, useEffect, useState } from "react";
|
||||||
import { useUserPreferencesStore } from "Stores/PreferencesStore";
|
import { useUserPreferencesStore } from "Stores/PreferencesStore";
|
||||||
import type { TaskData } from "Types/Experiment/Experiment";
|
|
||||||
|
|
||||||
interface IframePluginProps {
|
interface IframePluginProps {
|
||||||
pluginUrl: string;
|
index: number;
|
||||||
mode: "list" | "editor";
|
plugin: string;
|
||||||
taskData: TaskData<any>;
|
mode: "List" | "Editor";
|
||||||
onUpdate?: (data: any) => void;
|
taskData: string;
|
||||||
|
simProgress: string;
|
||||||
|
qubits_needed: number;
|
||||||
|
onUpdate?: (data: string, qubits_needed: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IframePlugin: React.FC<IframePluginProps> = ({
|
export const IframePlugin: React.FC<IframePluginProps> = ({
|
||||||
pluginUrl,
|
index,
|
||||||
|
plugin,
|
||||||
mode,
|
mode,
|
||||||
taskData,
|
taskData,
|
||||||
|
simProgress,
|
||||||
|
qubits_needed,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
}) => {
|
}) => {
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
const theme = useUserPreferencesStore();
|
const theme = useUserPreferencesStore();
|
||||||
const [isIframeReady, setIsIframeReady] = useState(false);
|
const [isIframeReady, setIsIframeReady] = useState(false);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
// Memoize the iframe component to prevent recreation when taskData changes
|
|
||||||
const memoizedIframe = useMemo(() => {
|
|
||||||
const url = `${pluginUrl}?mode=${mode}`;
|
|
||||||
return (
|
|
||||||
<iframe
|
|
||||||
ref={iframeRef}
|
|
||||||
src={url}
|
|
||||||
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
flexGrow: 1,
|
|
||||||
border: "none",
|
|
||||||
}}
|
|
||||||
title="Plugin"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}, [pluginUrl, mode]); // Only recreate when pluginUrl or mode changes
|
|
||||||
|
|
||||||
// Send data to iframe when taskData or theme changes, or when iframe becomes ready
|
// Send data to iframe when taskData or theme changes, or when iframe becomes ready
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isIframeReady || !iframeRef.current?.contentWindow) return;
|
if (!isIframeReady || !iframeRef.current?.contentWindow) return;
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
type: "plugin-data",
|
type: "plugin-data",
|
||||||
data: {
|
data: {
|
||||||
taskData,
|
taskData: taskData,
|
||||||
|
simProgress: simProgress,
|
||||||
|
qubits_needed: qubits_needed,
|
||||||
theme: theme.theme,
|
theme: theme.theme,
|
||||||
|
mode: mode,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
iframeRef.current.contentWindow.postMessage(message, "*");
|
iframeRef.current.contentWindow.postMessage(message, "*");
|
||||||
}, [taskData, theme.theme, isIframeReady]);
|
setIsVisible(true);
|
||||||
|
}, [taskData, theme.theme, isIframeReady, mode, simProgress]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isVisible) {
|
||||||
|
if (!isIframeReady || !iframeRef.current?.contentWindow) return;
|
||||||
|
const message = {
|
||||||
|
type: "update",
|
||||||
|
};
|
||||||
|
|
||||||
|
iframeRef.current.contentWindow.postMessage(message, "*");
|
||||||
|
}
|
||||||
|
}, [index]);
|
||||||
|
|
||||||
// Listen for messages from iframe
|
// Listen for messages from iframe
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -65,7 +66,8 @@ export const IframePlugin: React.FC<IframePluginProps> = ({
|
|||||||
|
|
||||||
// Handle plugin updates
|
// Handle plugin updates
|
||||||
if (event.data.type === "plugin-update" && onUpdate) {
|
if (event.data.type === "plugin-update" && onUpdate) {
|
||||||
onUpdate(event.data.data);
|
console.log(event.data);
|
||||||
|
onUpdate(event.data.data, event.data.qubits_needed);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,10 +75,33 @@ export const IframePlugin: React.FC<IframePluginProps> = ({
|
|||||||
return () => window.removeEventListener("message", handler);
|
return () => window.removeEventListener("message", handler);
|
||||||
}, [onUpdate]);
|
}, [onUpdate]);
|
||||||
|
|
||||||
// Reset ready state when URL changes (new plugin or mode)
|
return (
|
||||||
useEffect(() => {
|
<>
|
||||||
setIsIframeReady(false);
|
<LoadingOverlay visible={!isIframeReady} />
|
||||||
}, [pluginUrl, mode]);
|
<div
|
||||||
|
style={{
|
||||||
return memoizedIframe;
|
visibility: isVisible ? "visible" : "hidden",
|
||||||
|
flexGrow: "1",
|
||||||
|
maxHeight: "100%",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
srcDoc={plugin}
|
||||||
|
sandbox="allow-scripts allow-popups allow-forms"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flexGrow: 1,
|
||||||
|
border: "none",
|
||||||
|
pointerEvents: mode == "List" ? "none" : "initial",
|
||||||
|
}}
|
||||||
|
name={`Ifame#${index}`}
|
||||||
|
id={`Ifame#${index}`}
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
186
src/Api/QuantumBackend/ExperimentsManagment.tsx
Normal file
186
src/Api/QuantumBackend/ExperimentsManagment.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import keycloak from "Api/Keycloak/Keycloak";
|
||||||
|
import axios from "axios";
|
||||||
|
import type {
|
||||||
|
CreateExperimentRequest,
|
||||||
|
ExperimentData,
|
||||||
|
ExperimentListResponse,
|
||||||
|
CreateInstanceRequest,
|
||||||
|
SimpleInstanceData,
|
||||||
|
UpdateInstanceRequest,
|
||||||
|
InstanceListResponse,
|
||||||
|
InstanceData,
|
||||||
|
StartExperimentRequest,
|
||||||
|
UpdateExperimentRequest,
|
||||||
|
CreateExperimentTypeResponse,
|
||||||
|
ExperimentTypeList,
|
||||||
|
} from "Types/Experiment/Experiment";
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: `${import.meta.env.VITE_QUANTUM_BACKEND_URL}/experiment`,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add auth token to requests
|
||||||
|
api.interceptors.request.use((config) => {
|
||||||
|
const token = keycloak.token;
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============= EXPERIMENT TYPE ENDPOINTS =============
|
||||||
|
|
||||||
|
// 1. Get all experiment types
|
||||||
|
export const getExperimentTypes = async (): Promise<ExperimentTypeList[]> => {
|
||||||
|
const response = await api.get<ExperimentTypeList[]>("/types");
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Create experiment type (admin only, with file uploads)
|
||||||
|
export const createExperimentType = async (
|
||||||
|
name: string,
|
||||||
|
fileFrontend: File,
|
||||||
|
fileCompSystem: File,
|
||||||
|
description?: string,
|
||||||
|
): Promise<CreateExperimentTypeResponse> => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("name", name);
|
||||||
|
formData.append("file_frontend", fileFrontend);
|
||||||
|
formData.append("file_comp_system", fileCompSystem);
|
||||||
|
if (description) {
|
||||||
|
formData.append("description", description);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post<CreateExperimentTypeResponse>(
|
||||||
|
"/types",
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Get frontend HTML file for experiment type
|
||||||
|
export const getFrontendFile = async (
|
||||||
|
experiment_type_id: number,
|
||||||
|
): Promise<string> => {
|
||||||
|
const response = await api.get<string>("/types/frontend", {
|
||||||
|
params: { experiment_type_id },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============= EXPERIMENT ENDPOINTS =============
|
||||||
|
|
||||||
|
// 5. Create a new experiment
|
||||||
|
export const createExperiment = async (
|
||||||
|
data: CreateExperimentRequest,
|
||||||
|
): Promise<ExperimentData> => {
|
||||||
|
const response = await api.post<ExperimentData>("", data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 6. Get single experiment by ID
|
||||||
|
export const getExperimentById = async (
|
||||||
|
experiment_id: number,
|
||||||
|
): Promise<ExperimentData> => {
|
||||||
|
const response = await api.get<ExperimentData>("", {
|
||||||
|
params: { experiment_id },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 7. Update experiment
|
||||||
|
export const updateExperiment = async (
|
||||||
|
data: UpdateExperimentRequest,
|
||||||
|
): Promise<ExperimentData> => {
|
||||||
|
const response = await api.put<ExperimentData>("", data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 8. Get user's experiments (with pagination)
|
||||||
|
export const getUserExperiments = async ({
|
||||||
|
page_num = 1,
|
||||||
|
page_size = 10,
|
||||||
|
}: {
|
||||||
|
page_num?: number;
|
||||||
|
page_size?: number;
|
||||||
|
}): Promise<ExperimentListResponse> => {
|
||||||
|
const response = await api.get<ExperimentListResponse>("/user", {
|
||||||
|
params: { page_num, page_size },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 9. Delete experiment
|
||||||
|
export const deleteExperiment = async (
|
||||||
|
experiment_id: number,
|
||||||
|
): Promise<{ message: string }> => {
|
||||||
|
const response = await api.delete<{ message: string }>("", {
|
||||||
|
params: { experiment_id },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============= INSTANCE ENDPOINTS =============
|
||||||
|
|
||||||
|
// 10. Create instance for an experiment
|
||||||
|
export const createInstance = async (
|
||||||
|
data: CreateInstanceRequest,
|
||||||
|
): Promise<SimpleInstanceData> => {
|
||||||
|
const response = await api.post<SimpleInstanceData>("/instance", data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 11. Update instance
|
||||||
|
export const updateInstance = async (
|
||||||
|
data: UpdateInstanceRequest,
|
||||||
|
): Promise<SimpleInstanceData> => {
|
||||||
|
const response = await api.put<SimpleInstanceData>("/instance", data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 12. Get all instances of an experiment (with pagination)
|
||||||
|
export const getExperimentInstances = async (
|
||||||
|
experiment_id: number,
|
||||||
|
page_num: number = 1,
|
||||||
|
page_size: number = 6,
|
||||||
|
): Promise<InstanceListResponse> => {
|
||||||
|
page_num = Math.max(page_num, 1);
|
||||||
|
const response = await api.get<InstanceListResponse>("/instance", {
|
||||||
|
params: { experiment_id, page_num, page_size },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 13. Get single instance by ID
|
||||||
|
export const getInstanceById = async (
|
||||||
|
instance_id: number,
|
||||||
|
): Promise<InstanceData> => {
|
||||||
|
const response = await api.get<InstanceData>("/instance/id", {
|
||||||
|
params: { instance_id },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 14. Delete instance
|
||||||
|
export const deleteInstance = async (
|
||||||
|
instance_id: number,
|
||||||
|
): Promise<{ message: string }> => {
|
||||||
|
const response = await api.delete<{ message: string }>("/instance", {
|
||||||
|
params: { instance_id },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============= SIMULATION ENDPOINTS =============
|
||||||
|
|
||||||
|
// 15. Start experiment (run simulation)
|
||||||
|
export const startExperiment = async (
|
||||||
|
data: StartExperimentRequest,
|
||||||
|
): Promise<200> => {
|
||||||
|
const response = await api.post<200>("/start", data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
@@ -25,25 +25,38 @@ export const GetCurrentUserInfo = async (): Promise<UserData | undefined> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// In Api/QuantumBackend/UserManagement.ts
|
||||||
export const UpdateCurrentUserInfo = async (
|
export const UpdateCurrentUserInfo = async (
|
||||||
profile_picture_path: string,
|
file: File,
|
||||||
): Promise<UserData | undefined> => {
|
): Promise<{ profile_picture_path: string } | undefined> => {
|
||||||
const response = await axios.put(
|
try {
|
||||||
`${import.meta.env.VITE_QUANTUM_BACKEND_URL}/user`,
|
const formData = new FormData();
|
||||||
{ profile_picture_path: profile_picture_path },
|
formData.append("file", file);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_QUANTUM_BACKEND_URL}/user/upload`,
|
||||||
{
|
{
|
||||||
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${keycloak.token}`,
|
Authorization: `Bearer ${keycloak.token}`,
|
||||||
},
|
},
|
||||||
withCredentials: true, // Important: allows credentials in CORS
|
body: formData,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (response.status === 200) {
|
|
||||||
return response.data;
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to upload profile picture");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uploading profile picture:", error);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Keep your existing GetCurrentUserInfo function
|
||||||
|
|
||||||
export const GetUserByEmail = async (
|
export const GetUserByEmail = async (
|
||||||
email: string,
|
email: string,
|
||||||
): Promise<UserData | undefined> => {
|
): Promise<UserData | undefined> => {
|
||||||
|
|||||||
@@ -35,8 +35,7 @@ function App() {
|
|||||||
keycloak.updateToken(60).catch((error) => {
|
keycloak.updateToken(60).catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
keycloak.logout({
|
keycloak.logout({
|
||||||
redirectUri:
|
redirectUri: window.location.origin,
|
||||||
window.location.origin + "/" + import.meta.env.VITE_BASE_PATH,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,9 +5,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
|
position: relative;
|
||||||
|
overflow: "hidden";
|
||||||
}
|
}
|
||||||
|
|
||||||
.colored:hover {
|
:not(.button-Disabled).colored:hover {
|
||||||
background-color: color;
|
background-color: color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +40,7 @@
|
|||||||
--hover_color: color-mix(in srgb, var(--hovercolor) 20%, transparent);
|
--hover_color: color-mix(in srgb, var(--hovercolor) 20%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.outline:hover {
|
:not(.button-Disabled).outline:hover {
|
||||||
background: var(--hover_color);
|
background: var(--hover_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@
|
|||||||
--hover_color: var(--hovercolor);
|
--hover_color: var(--hovercolor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.color:hover {
|
:not(.button-Disabled).color:hover {
|
||||||
background-color: var(--hover_color);
|
background-color: var(--hover_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,11 +105,10 @@
|
|||||||
--hover_color: color-mix(in srgb, var(--color) 20%, transparent);
|
--hover_color: color-mix(in srgb, var(--color) 20%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtle:hover {
|
:not(.button-Disabled).subtle:hover {
|
||||||
background-color: --hover_color;
|
background-color: --hover_color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-Disabled {
|
.button-Disabled {
|
||||||
background-color: var(--hover_color);
|
cursor: default;
|
||||||
cursor: inherit;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,8 @@ function CustomButton({
|
|||||||
}: CustomButtonProps) {
|
}: CustomButtonProps) {
|
||||||
return (
|
return (
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
className={
|
className={`colored ${style} ${color} textAlign-${textAlign} +
|
||||||
`colored ${style} ${color} textAlign-${textAlign} ` +
|
${disabled ? "button-Disabled" : ""}`}
|
||||||
(disabled ? "button-Disabled" : "")
|
|
||||||
}
|
|
||||||
style={
|
style={
|
||||||
color !== "primary" &&
|
color !== "primary" &&
|
||||||
color !== "secondary" &&
|
color !== "secondary" &&
|
||||||
@@ -55,6 +53,19 @@ function CustomButton({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
|
{disabled && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: "-2px",
|
||||||
|
right: "-2px",
|
||||||
|
bottom: "-2px",
|
||||||
|
top: "-2px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
backgroundColor: "rgba(10, 10, 10, 0.5)",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
{icon} {icon && <Space w="sm" />}
|
{icon} {icon && <Space w="sm" />}
|
||||||
<Text size={textSize}>{text}</Text>
|
<Text size={textSize}>{text}</Text>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { useLayoutStore } from "Stores/LayoutStore";
|
|||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { routes } from "Routes/Routes";
|
import { routes } from "Routes/Routes";
|
||||||
|
|
||||||
const logoUrl =
|
const logoUrl = window.location.origin + "/bitmap.png";
|
||||||
window.location.origin + "/" + import.meta.env.VITE_BASE_PATH + "/bitmap.png";
|
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
const { is_navbar_open, set_navbar_open } = useLayoutStore();
|
const { is_navbar_open, set_navbar_open } = useLayoutStore();
|
||||||
|
|||||||
@@ -18,8 +18,13 @@ import {
|
|||||||
IconUsers,
|
IconUsers,
|
||||||
type IconProps,
|
type IconProps,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { Link, useLocation } from "react-router";
|
import {
|
||||||
import { useState, type ForwardRefExoticComponent } from "react";
|
Link,
|
||||||
|
useLocation,
|
||||||
|
useNavigate,
|
||||||
|
type NavigateFunction,
|
||||||
|
} from "react-router";
|
||||||
|
import { useEffect, useState, type ForwardRefExoticComponent } from "react";
|
||||||
import { routes } from "Routes/Routes";
|
import { routes } from "Routes/Routes";
|
||||||
import keycloak from "Api/Keycloak/Keycloak";
|
import keycloak from "Api/Keycloak/Keycloak";
|
||||||
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
@@ -33,6 +38,7 @@ interface SubtleLinkButtonProps {
|
|||||||
text: string;
|
text: string;
|
||||||
color: string;
|
color: string;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
|
nav: NavigateFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubtleLinkButton(props: SubtleLinkButtonProps) {
|
function SubtleLinkButton(props: SubtleLinkButtonProps) {
|
||||||
@@ -44,16 +50,48 @@ function SubtleLinkButton(props: SubtleLinkButtonProps) {
|
|||||||
color={props.selected ? props.color : "contrast"}
|
color={props.selected ? props.color : "contrast"}
|
||||||
text={props.text}
|
text={props.text}
|
||||||
textAlign="left"
|
textAlign="left"
|
||||||
|
onClick={() => {
|
||||||
|
props.nav(props.link);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Sidebar() {
|
function Sidebar() {
|
||||||
const { profile, profile_picture_path } = useAuthenticationStore();
|
const { profile, set_profile_picture_path, profile_picture_path } =
|
||||||
|
useAuthenticationStore();
|
||||||
const [open, set_open] = useState(false);
|
const [open, set_open] = useState(false);
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const location = useLocation(); // get current URL
|
const location = useLocation(); // get current URL
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!profile) return;
|
||||||
|
|
||||||
|
const fetchAvatar = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_QUANTUM_BACKEND_URL}/user/serve/${profile.id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${keycloak.token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
set_profile_picture_path(url);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load avatar:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAvatar();
|
||||||
|
}, [profile]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sidebar">
|
<div className="sidebar">
|
||||||
@@ -70,8 +108,7 @@ function Sidebar() {
|
|||||||
text="Подтвердить"
|
text="Подтвердить"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
keycloak.logout({
|
keycloak.logout({
|
||||||
redirectUri:
|
redirectUri: window.location.origin,
|
||||||
window.location.origin + "/" + import.meta.env.VITE_BASE_PATH,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
textSize="lg"
|
textSize="lg"
|
||||||
@@ -94,6 +131,7 @@ function Sidebar() {
|
|||||||
text="Эксперименты"
|
text="Эксперименты"
|
||||||
color={theme.colors.teal[7]}
|
color={theme.colors.teal[7]}
|
||||||
selected={location.pathname.startsWith(routes.ExperimentsPage.path)}
|
selected={location.pathname.startsWith(routes.ExperimentsPage.path)}
|
||||||
|
nav={navigate}
|
||||||
/>
|
/>
|
||||||
<SubtleLinkButton
|
<SubtleLinkButton
|
||||||
link={routes.MachinesPage.path}
|
link={routes.MachinesPage.path}
|
||||||
@@ -101,6 +139,7 @@ function Sidebar() {
|
|||||||
text="Вычислительные системы"
|
text="Вычислительные системы"
|
||||||
color={theme.colors.violet[7]}
|
color={theme.colors.violet[7]}
|
||||||
selected={location.pathname.startsWith(routes.MachinesPage.path)}
|
selected={location.pathname.startsWith(routes.MachinesPage.path)}
|
||||||
|
nav={navigate}
|
||||||
/>
|
/>
|
||||||
<SubtleLinkButton
|
<SubtleLinkButton
|
||||||
link={routes.TeamsPage.path}
|
link={routes.TeamsPage.path}
|
||||||
@@ -108,6 +147,7 @@ function Sidebar() {
|
|||||||
text="Команды"
|
text="Команды"
|
||||||
color={theme.colors.grape[7]}
|
color={theme.colors.grape[7]}
|
||||||
selected={location.pathname.startsWith(routes.TeamsPage.path)}
|
selected={location.pathname.startsWith(routes.TeamsPage.path)}
|
||||||
|
nav={navigate}
|
||||||
/>
|
/>
|
||||||
<SubtleLinkButton
|
<SubtleLinkButton
|
||||||
link={routes.DocumentationPage.path}
|
link={routes.DocumentationPage.path}
|
||||||
@@ -115,6 +155,7 @@ function Sidebar() {
|
|||||||
text="Документация"
|
text="Документация"
|
||||||
color={theme.colors.blue[7]}
|
color={theme.colors.blue[7]}
|
||||||
selected={location.pathname.startsWith(routes.DocumentationPage.path)}
|
selected={location.pathname.startsWith(routes.DocumentationPage.path)}
|
||||||
|
nav={navigate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="sidebar_bottom">
|
<div className="sidebar_bottom">
|
||||||
@@ -134,15 +175,16 @@ function Sidebar() {
|
|||||||
style={{ width: "100%", height: "5px", margin: "5px" }}
|
style={{ width: "100%", height: "5px", margin: "5px" }}
|
||||||
/>
|
/>
|
||||||
<div className="sidebar_bottom_bottom">
|
<div className="sidebar_bottom_bottom">
|
||||||
<Link to={routes.SettingsPage.path} className="invisible_link">
|
|
||||||
<CustomButton
|
<CustomButton
|
||||||
style="subtle"
|
style="subtle"
|
||||||
icon={<IconSettings2 size={26} />}
|
icon={<IconSettings2 size={26} />}
|
||||||
text="Настройки"
|
text="Настройки"
|
||||||
color="contrast"
|
color="contrast"
|
||||||
textSize="lg"
|
textSize="lg"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(routes.SettingsPage.path);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Link>
|
|
||||||
<CustomButton
|
<CustomButton
|
||||||
style="subtle"
|
style="subtle"
|
||||||
icon={<IconLogout size={26} />}
|
icon={<IconLogout size={26} />}
|
||||||
|
|||||||
@@ -1,24 +1,45 @@
|
|||||||
import { Card, Pill, SimpleGrid, Text, UnstyledButton } from "@mantine/core";
|
import { Card, Pill, SimpleGrid, Text, UnstyledButton } from "@mantine/core";
|
||||||
import "./ExperimentsListCard.css";
|
import "./ExperimentsListCard.css";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import type { Experiment, TaskData } from "Types/Experiment/Experiment";
|
import type {
|
||||||
|
ExperimentData,
|
||||||
|
SimpleInstanceData,
|
||||||
|
} from "Types/Experiment/Experiment";
|
||||||
import { IconTrash } from "@tabler/icons-react";
|
import { IconTrash } from "@tabler/icons-react";
|
||||||
import type { MouseEvent } from "react";
|
import type { MouseEvent } from "react";
|
||||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||||
|
import { deleteExperiment } from "Api/QuantumBackend/ExperimentsManagment";
|
||||||
|
|
||||||
function ExperimentsListCard(props: {
|
function ExperimentsListCard(props: { experiment: ExperimentData }) {
|
||||||
experiment: Experiment;
|
|
||||||
team: { team_id: number; team_name: string } | undefined;
|
|
||||||
}) {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { removeExperiment, tasks } = useExperimentStore();
|
const { removeExperiment } = useExperimentStore();
|
||||||
|
|
||||||
const experiment_tasks = tasks.filter(
|
const getStatusColor = (status: string) => {
|
||||||
(a) => a.id in props.experiment.tasks_ids,
|
const statusLower = status;
|
||||||
);
|
|
||||||
|
switch (statusLower) {
|
||||||
|
case "draft":
|
||||||
|
return "var(--mantine-color-gray-5)";
|
||||||
|
case "in queue":
|
||||||
|
return "var(--mantine-color-blue-5)";
|
||||||
|
case "processing":
|
||||||
|
case "running":
|
||||||
|
return "var(--mantine-color-yellow-5)";
|
||||||
|
case "complete":
|
||||||
|
return "var(--mantine-color-green-5)";
|
||||||
|
case "error":
|
||||||
|
case "complete with error":
|
||||||
|
case "failed":
|
||||||
|
return "var(--mantine-color-red-5)";
|
||||||
|
default:
|
||||||
|
return "var(--mantine-color-gray-5)";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
|
deleteExperiment(props.experiment.id).then(() => {
|
||||||
removeExperiment(props.experiment.id);
|
removeExperiment(props.experiment.id);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -34,30 +55,38 @@ function ExperimentsListCard(props: {
|
|||||||
Эксперимент: {props.experiment.name}
|
Эксперимент: {props.experiment.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text mb="sm" size="md">
|
<Text mb="sm" size="md">
|
||||||
Команда: {props.team?.team_name}
|
Команда: {props.experiment.team?.team_name}
|
||||||
</Text>
|
</Text>
|
||||||
<div style={{ display: "flex", gap: "10px" }}>
|
<div style={{ display: "flex", gap: "10px" }}>
|
||||||
<Text size="md">Статус: </Text>
|
<Text size="md">Статус: </Text>
|
||||||
<Pill style={{ backgroundColor: "var(--mantine-color-yellow-5)" }}>
|
<Pill
|
||||||
<Text size="md">{props.experiment.experiment_status}</Text>
|
style={{
|
||||||
|
backgroundColor: getStatusColor(
|
||||||
|
props.experiment.status != undefined
|
||||||
|
? props.experiment.status.toLowerCase()
|
||||||
|
: "DRAFT",
|
||||||
|
),
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="md">{props.experiment.status}</Text>
|
||||||
</Pill>
|
</Pill>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ExperimentSectionWithLine">
|
<div className="ExperimentSectionWithLine">
|
||||||
<SimpleGrid cols={2} verticalSpacing="0px">
|
<SimpleGrid cols={2} verticalSpacing="0px" style={{ rowGap: "4px" }}>
|
||||||
<Text>Задачи:</Text>
|
<Text size="sm">Задачи:</Text>
|
||||||
|
|
||||||
{experiment_tasks.map(
|
{props.experiment.instance_preview.slice(0, 6).map(
|
||||||
(
|
(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
task: TaskData<any>,
|
instance: SimpleInstanceData,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<Pill
|
<Pill
|
||||||
size="md"
|
size="sm"
|
||||||
|
className="ExperimentPill"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "transparent",
|
|
||||||
border: "2px solid black",
|
|
||||||
alignContent: "center",
|
alignContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
@@ -65,40 +94,50 @@ function ExperimentsListCard(props: {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
size="md"
|
size="sm"
|
||||||
style={{
|
style={{
|
||||||
textWrap: "nowrap",
|
textWrap: "nowrap",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{task.name}
|
{instance.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Pill>
|
</Pill>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
{experiment_tasks.length == 0 ? (
|
{props.experiment.instances_count == 0 ? (
|
||||||
<Pill size="md" className="ExperimentPill2">
|
<Pill size="sm" className="ExperimentPill2">
|
||||||
Нет Задач
|
Нет Задач
|
||||||
</Pill>
|
</Pill>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
{props.experiment.instances_count >
|
||||||
|
props.experiment.instance_preview.length ? (
|
||||||
|
<Pill size="sm" className="ExperimentPill2">
|
||||||
|
и еще{" "}
|
||||||
|
{props.experiment.instances_count -
|
||||||
|
props.experiment.instance_preview.length}
|
||||||
|
</Pill>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</div>
|
</div>
|
||||||
<div className="RightExperimentSection">
|
<div className="RightExperimentSection">
|
||||||
<div className="TopRightContainer">
|
<div className="TopRightContainer">
|
||||||
<div className="dateContainer">
|
<div className="dateContainer">
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
{new Date(
|
{new Date(props.experiment.created_at + "Z").toLocaleDateString(
|
||||||
props.experiment.date_created + "Z",
|
"ru",
|
||||||
).toLocaleDateString("ru")}{" "}
|
)}{" "}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
{new Date(
|
{new Date(props.experiment.created_at + "Z").toLocaleTimeString(
|
||||||
props.experiment.date_created + "Z",
|
"ru",
|
||||||
).toLocaleTimeString("ru")}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
@@ -122,7 +161,7 @@ function ExperimentsListCard(props: {
|
|||||||
>
|
>
|
||||||
<Text> Тип эксперимента: </Text>
|
<Text> Тип эксперимента: </Text>
|
||||||
<Pill className="ExperimentPill">
|
<Pill className="ExperimentPill">
|
||||||
{props.experiment.experiment_type}
|
{props.experiment.experiment_type.name}
|
||||||
</Pill>
|
</Pill>
|
||||||
</div>
|
</div>
|
||||||
<Text c="dimmed" size="sm">
|
<Text c="dimmed" size="sm">
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ function MachinesListCard(props: SystemWithTeams) {
|
|||||||
//className="MachinePill"
|
//className="MachinePill"
|
||||||
style={{
|
style={{
|
||||||
marginLeft: "10px",
|
marginLeft: "10px",
|
||||||
backgroundColor: colors[props.system.status || "ONLINE"],
|
backgroundColor:
|
||||||
|
colors[props.system.status as "ONLINE" | "OFFLINE" | "BUSY"],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="md">{props.system.status}</Text>
|
<Text size="md">{props.system.status}</Text>
|
||||||
@@ -82,12 +83,12 @@ function MachinesListCard(props: SystemWithTeams) {
|
|||||||
</Text>
|
</Text>
|
||||||
<div style={{ display: "flex", gap: "10px", justifyContent: "right" }}>
|
<div style={{ display: "flex", gap: "10px", justifyContent: "right" }}>
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
{new Date(props.system.created_at + "Z").toLocaleDateString(
|
{new Date(props.system.last_updated + "Z").toLocaleDateString(
|
||||||
"ru",
|
"ru",
|
||||||
)}{" "}
|
)}{" "}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
{new Date(props.system.created_at + "Z").toLocaleTimeString("ru")}
|
{new Date(props.system.last_updated + "Z").toLocaleTimeString("ru")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
|
|||||||
28
src/Components/ListCard/TaskListCard/TaskListCard.css
Normal file
28
src/Components/ListCard/TaskListCard/TaskListCard.css
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
.InstancesListCard {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.InstanceSectionWithLine {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.InstanceIframeSection {
|
||||||
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DatesContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.RightInstanceSection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 275px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MachinePill {
|
||||||
|
background-color: var(--mantine-color-contrast-filled);
|
||||||
|
color: var(--mantine-color-primary-filled);
|
||||||
|
}
|
||||||
176
src/Components/ListCard/TaskListCard/TaskListCard.tsx
Normal file
176
src/Components/ListCard/TaskListCard/TaskListCard.tsx
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { Card, Pill, Text, UnstyledButton } from "@mantine/core";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import type { InstanceData } from "Types/Experiment/Experiment";
|
||||||
|
import { IconCheck, IconTrash } from "@tabler/icons-react";
|
||||||
|
import type { MouseEvent } from "react";
|
||||||
|
import { IframePlugin } from "Api/PluginLoader/PluginLoader";
|
||||||
|
import "./TaskListCard.css";
|
||||||
|
import { deleteInstance } from "Api/QuantumBackend/ExperimentsManagment";
|
||||||
|
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
|
function InstancesListCard(props: { instance: InstanceData; plugin: string }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { removeInstance } = useExperimentStore();
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const statusLower = status.toLowerCase();
|
||||||
|
|
||||||
|
switch (statusLower) {
|
||||||
|
case "draft":
|
||||||
|
return "var(--mantine-color-gray-5)";
|
||||||
|
case "in queue":
|
||||||
|
return "var(--mantine-color-blue-5)";
|
||||||
|
case "processing":
|
||||||
|
case "running":
|
||||||
|
return "var(--mantine-color-yellow-5)";
|
||||||
|
case "complete":
|
||||||
|
case "completed":
|
||||||
|
return "var(--mantine-color-green-5)";
|
||||||
|
case "error":
|
||||||
|
case "failed":
|
||||||
|
return "var(--mantine-color-red-5)";
|
||||||
|
default:
|
||||||
|
return "var(--mantine-color-gray-5)";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
deleteInstance(props.instance.instance_id).then(() => {
|
||||||
|
removeInstance(props.instance.instance_id);
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
title: "Задача удалена успешно",
|
||||||
|
message: "",
|
||||||
|
icon: <IconCheck />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className="InstancesListCard"
|
||||||
|
orientation="horizontal"
|
||||||
|
onClick={(ev) => {
|
||||||
|
console.log("A");
|
||||||
|
ev.stopPropagation();
|
||||||
|
navigate(props.instance.instance_id.toString());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card.Section
|
||||||
|
withBorder
|
||||||
|
inheritPadding
|
||||||
|
px="md"
|
||||||
|
className="InstanceSectionWithLine"
|
||||||
|
>
|
||||||
|
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
|
||||||
|
Задача: {props.instance.name}
|
||||||
|
</Text>
|
||||||
|
<div style={{ display: "flex", gap: "10px" }}>
|
||||||
|
<Text size="md">Статус: </Text>
|
||||||
|
<Pill
|
||||||
|
style={{
|
||||||
|
backgroundColor: getStatusColor(
|
||||||
|
props.instance.simulation_result?.status || "",
|
||||||
|
),
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="md">
|
||||||
|
{props.instance.simulation_result?.status || "DRAFT"}
|
||||||
|
</Text>
|
||||||
|
</Pill>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: "10px" }}>
|
||||||
|
<Text size="md">ВС: </Text>
|
||||||
|
<Pill className="MachinePill">
|
||||||
|
<Text size="md">
|
||||||
|
{props.instance.simulation_result?.comp_system?.system_name ||
|
||||||
|
"--"}
|
||||||
|
</Text>
|
||||||
|
</Pill>
|
||||||
|
</div>
|
||||||
|
</Card.Section>
|
||||||
|
<Card.Section withBorder className="InstanceIframeSection">
|
||||||
|
{JSON.stringify(props.instance.instance_data) && (
|
||||||
|
<IframePlugin
|
||||||
|
plugin={props.plugin}
|
||||||
|
mode="List"
|
||||||
|
taskData={JSON.stringify(props.instance.instance_data)}
|
||||||
|
index={props.instance.instance_id}
|
||||||
|
qubits_needed={0}
|
||||||
|
simProgress={JSON.stringify(
|
||||||
|
props.instance.simulation_result?.simulation_result,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Card.Section>
|
||||||
|
<Card.Section className="RightInstanceSection" inheritPadding px="sm">
|
||||||
|
<div className="DatesContainer">
|
||||||
|
<div className="dateContainer">
|
||||||
|
<Text size="sm">Время начала:</Text>
|
||||||
|
{props.instance.simulation_result ? (
|
||||||
|
<>
|
||||||
|
<Text size="sm">
|
||||||
|
{new Date(
|
||||||
|
props.instance.simulation_result?.started_at + "Z",
|
||||||
|
).toLocaleDateString("ru")}{" "}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm">
|
||||||
|
{new Date(
|
||||||
|
props.instance.simulation_result?.started_at + "Z",
|
||||||
|
).toLocaleTimeString("ru")}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Text size="sm">--</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="dateContainer">
|
||||||
|
<Text size="sm">Время окончания:</Text>
|
||||||
|
{props.instance.simulation_result?.ended_at ? (
|
||||||
|
<>
|
||||||
|
<Text size="sm">
|
||||||
|
{new Date(
|
||||||
|
props.instance.simulation_result?.ended_at + "Z",
|
||||||
|
).toLocaleDateString("ru")}{" "}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm">
|
||||||
|
{new Date(
|
||||||
|
props.instance.simulation_result?.ended_at + "Z",
|
||||||
|
).toLocaleTimeString("ru")}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Text size="sm">--</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingLeft: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={(e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete();
|
||||||
|
}}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</UnstyledButton>
|
||||||
|
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
#{props.instance.instance_id}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Card.Section>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InstancesListCard;
|
||||||
@@ -1,16 +1,26 @@
|
|||||||
import { Card, Text, UnstyledButton } from "@mantine/core";
|
import { Card, NumberInput, Text, UnstyledButton } from "@mantine/core";
|
||||||
import { IconCancel, IconCheck, IconTrash } from "@tabler/icons-react";
|
import {
|
||||||
import type { MouseEvent } from "react";
|
IconCancel,
|
||||||
|
IconCheck,
|
||||||
|
IconPencil,
|
||||||
|
IconTrash,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { useState, type MouseEvent } from "react";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import type { SystemTeamData, SystemWithTeams } from "Types/Machine/Machine";
|
import type { SystemTeamData, SystemWithTeams } from "Types/Machine/Machine";
|
||||||
import { removeSystemFromTeam } from "Api/QuantumBackend/MachineManagment";
|
import {
|
||||||
|
giveSystemToTeam,
|
||||||
|
removeSystemFromTeam,
|
||||||
|
} from "Api/QuantumBackend/MachineManagment";
|
||||||
import { useDeviceStore } from "Stores/DeviceStore";
|
import { useDeviceStore } from "Stores/DeviceStore";
|
||||||
|
|
||||||
function TeamInMachineListCard(props: {
|
function TeamInMachineListCard(props: {
|
||||||
team: SystemTeamData;
|
team: SystemTeamData;
|
||||||
system: SystemWithTeams;
|
system: SystemWithTeams;
|
||||||
}) {
|
}) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const { updateDevice } = useDeviceStore();
|
const { updateDevice } = useDeviceStore();
|
||||||
|
const [count, setCount] = useState(props.team.num_qubits);
|
||||||
|
|
||||||
const handleRemovePerm = () => {
|
const handleRemovePerm = () => {
|
||||||
// TODO: fix delete
|
// TODO: fix delete
|
||||||
@@ -76,7 +86,69 @@ function TeamInMachineListCard(props: {
|
|||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text>Количество кубит: {props.team.num_qubits}</Text>
|
{isEditing && (
|
||||||
|
<>
|
||||||
|
<Text>Количество кубит: </Text>
|
||||||
|
<NumberInput
|
||||||
|
value={count}
|
||||||
|
onChange={(ev) => {
|
||||||
|
setCount(Number(ev.valueOf()));
|
||||||
|
}}
|
||||||
|
></NumberInput>
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={() => {
|
||||||
|
giveSystemToTeam({
|
||||||
|
system_id: props.system.system.id,
|
||||||
|
team_id: props.team.team.team_id,
|
||||||
|
qubits_given: count,
|
||||||
|
}).then(() => {
|
||||||
|
const team = props.system.teams.find((t) => {
|
||||||
|
return t.team.team_id == props.team.team.team_id;
|
||||||
|
});
|
||||||
|
if (team) {
|
||||||
|
const updatedItem = { ...team, num_qubits: count };
|
||||||
|
|
||||||
|
// Create new array with the updated item in the same position
|
||||||
|
const updatedItems = props.system.teams.map(
|
||||||
|
(currentItem) =>
|
||||||
|
currentItem.team.team_id === team.team.team_id
|
||||||
|
? updatedItem
|
||||||
|
: currentItem,
|
||||||
|
);
|
||||||
|
|
||||||
|
updateDevice(props.system.system.id, {
|
||||||
|
teams: updatedItems,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsEditing(false);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconCheck size={25} />
|
||||||
|
</UnstyledButton>
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={() => {
|
||||||
|
setCount(props.team.num_qubits);
|
||||||
|
setIsEditing(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconCancel size={25} />
|
||||||
|
</UnstyledButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isEditing && (
|
||||||
|
<>
|
||||||
|
<Text>Количество кубит: {count}</Text>
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditing(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconPencil size={25} />
|
||||||
|
</UnstyledButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
<Card.Section
|
<Card.Section
|
||||||
inheritPadding
|
inheritPadding
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
|
IconCancel,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconCrown,
|
IconCrown,
|
||||||
IconForbid,
|
IconForbid,
|
||||||
@@ -196,7 +197,8 @@ function TeamMemberCard(props: { cur_team: Team; member: TeamMember }) {
|
|||||||
deleteMember({
|
deleteMember({
|
||||||
team_id: props.cur_team.id,
|
team_id: props.cur_team.id,
|
||||||
user_id: props.member.user.keycloak_id,
|
user_id: props.member.user.keycloak_id,
|
||||||
}).then(() => {
|
})
|
||||||
|
.then(() => {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
radius: "md",
|
radius: "md",
|
||||||
title: "Пользователь удален успешно",
|
title: "Пользователь удален успешно",
|
||||||
@@ -208,10 +210,21 @@ function TeamMemberCard(props: { cur_team: Team; member: TeamMember }) {
|
|||||||
members: [
|
members: [
|
||||||
...props.cur_team.members.filter(
|
...props.cur_team.members.filter(
|
||||||
(member) =>
|
(member) =>
|
||||||
member.user.keycloak_id != props.member.user.keycloak_id,
|
member.user.keycloak_id !=
|
||||||
|
props.member.user.keycloak_id,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
title: "Пользователя не получилось удалить",
|
||||||
|
message: "",
|
||||||
|
color: "red",
|
||||||
|
icon: <IconCancel />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import CustomButton from "Components/CustomButton/CustomButton";
|
|||||||
import { useDeviceStore } from "Stores/DeviceStore";
|
import { useDeviceStore } from "Stores/DeviceStore";
|
||||||
import type { SystemWithTeams } from "Types/Machine/Machine";
|
import type { SystemWithTeams } from "Types/Machine/Machine";
|
||||||
import { giveSystemToTeam } from "Api/QuantumBackend/MachineManagment";
|
import { giveSystemToTeam } from "Api/QuantumBackend/MachineManagment";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { IconCancel } from "@tabler/icons-react";
|
||||||
|
|
||||||
interface AddTeamToDeviceProps {
|
interface AddTeamToDeviceProps {
|
||||||
isOpened: boolean;
|
isOpened: boolean;
|
||||||
@@ -20,7 +22,7 @@ interface AddTeamToDeviceProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AddTeamToDevice(props: AddTeamToDeviceProps) {
|
export function AddTeamToDevice(props: AddTeamToDeviceProps) {
|
||||||
const [numQubits, setNumQubits] = useState(0);
|
const [numQubits, setNumQubits] = useState(1);
|
||||||
const { updateDevice } = useDeviceStore();
|
const { updateDevice } = useDeviceStore();
|
||||||
const [selectedTeam, setSelectedTeam] = useState<{
|
const [selectedTeam, setSelectedTeam] = useState<{
|
||||||
label: string;
|
label: string;
|
||||||
@@ -29,7 +31,7 @@ export function AddTeamToDevice(props: AddTeamToDeviceProps) {
|
|||||||
//reset on open dialog
|
//reset on open dialog
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.isOpened) {
|
if (props.isOpened) {
|
||||||
setNumQubits(0);
|
setNumQubits(1);
|
||||||
setSelectedTeam(undefined);
|
setSelectedTeam(undefined);
|
||||||
}
|
}
|
||||||
}, [props.isOpened]);
|
}, [props.isOpened]);
|
||||||
@@ -63,14 +65,23 @@ export function AddTeamToDevice(props: AddTeamToDeviceProps) {
|
|||||||
});
|
});
|
||||||
props.setIsOpened(false);
|
props.setIsOpened(false);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
title: "Не удалось предоставить доступ",
|
||||||
|
message: "",
|
||||||
|
color: "red",
|
||||||
|
icon: <IconCancel />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
opened={props.isOpened}
|
opened={props.isOpened}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
title=<Title size="xl">Добавить члена команды</Title>
|
title=<Title size="xl">Предоставить команде права на ВС</Title>
|
||||||
centered
|
centered
|
||||||
size="75%"
|
size="75%"
|
||||||
styles={{
|
styles={{
|
||||||
@@ -98,11 +109,13 @@ export function AddTeamToDevice(props: AddTeamToDeviceProps) {
|
|||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={numQubits}
|
value={numQubits}
|
||||||
label="Количество кубит"
|
label={`Количество кубит (Макс ${props.device.system.max_qubits})`}
|
||||||
required
|
required
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setNumQubits(Number(event.valueOf()));
|
setNumQubits(Number(event.valueOf()));
|
||||||
}}
|
}}
|
||||||
|
max={props.device.system.max_qubits}
|
||||||
|
min={1}
|
||||||
></NumberInput>
|
></NumberInput>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Center>
|
<Center>
|
||||||
|
|||||||
@@ -9,23 +9,33 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import "./NewExperiment.css";
|
import "./NewExperiment.css";
|
||||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
//import { useExperimentStore } from "Stores/ExperimentStore";
|
||||||
import CustomButton from "Components/CustomButton/CustomButton";
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
import { createExperiment } from "Api/QuantumBackend/ExperimentsManagment";
|
||||||
|
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||||
|
|
||||||
interface NewExperimentModalProps {
|
interface NewExperimentModalProps {
|
||||||
isOpened: boolean;
|
isOpened: boolean;
|
||||||
setIsOpened: (opened: boolean) => void;
|
setIsOpened: (opened: boolean) => void;
|
||||||
teams: { team_id: number; team_name: string }[] | undefined;
|
teams: { team_id: number; team_name: string }[] | undefined;
|
||||||
|
types: { type_id: number; type_name: string }[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NewExperimentModal(props: NewExperimentModalProps) {
|
export function NewExperimentModal(props: NewExperimentModalProps) {
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const { addExperiment } = useExperimentStore();
|
//const { addExperiment } = useExperimentStore();
|
||||||
const [selectedTeam, setSelectedTeam] = useState<{
|
const [selectedTeam, setSelectedTeam] = useState<{
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const [selectedType, setSelectedType] = useState<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { addExperiment } = useExperimentStore();
|
||||||
//reset on open dialog
|
//reset on open dialog
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.isOpened) {
|
if (props.isOpened) {
|
||||||
@@ -41,18 +51,16 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
|
|||||||
|
|
||||||
const handleCreateExperiment = () => {
|
const handleCreateExperiment = () => {
|
||||||
//TODO: add logic for backend server
|
//TODO: add logic for backend server
|
||||||
if (selectedTeam) {
|
if (selectedTeam && selectedType) {
|
||||||
addExperiment({
|
createExperiment({
|
||||||
id: 1,
|
team_id: Number(selectedTeam.value),
|
||||||
name: name,
|
name: name,
|
||||||
description: description,
|
description: description,
|
||||||
team_id: Number(selectedTeam?.value),
|
experiment_type_id: Number(selectedType.value),
|
||||||
tasks_ids: [1, 2],
|
}).then((exp) => {
|
||||||
date_created: new Date(),
|
addExperiment(exp);
|
||||||
experiment_status: "PROCESSING",
|
|
||||||
experiment_type: "VQE",
|
|
||||||
});
|
|
||||||
props.setIsOpened(false);
|
props.setIsOpened(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,9 +117,32 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
|
|||||||
onChange={(_value, option) => setSelectedTeam(option)}
|
onChange={(_value, option) => setSelectedTeam(option)}
|
||||||
/>
|
/>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
|
<Select
|
||||||
|
value={selectedType?.value}
|
||||||
|
label="Тип эксперимента"
|
||||||
|
placeholder="Выберите тип эксперимента"
|
||||||
|
searchable
|
||||||
|
required
|
||||||
|
data={
|
||||||
|
props.types
|
||||||
|
? props.types.map((team) => {
|
||||||
|
return {
|
||||||
|
value: team.type_id.toString(),
|
||||||
|
label: `${team.type_name} (#${team.type_id})`,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
onChange={(_value, option) => setSelectedType(option)}
|
||||||
|
/>
|
||||||
|
<Space h="md" />
|
||||||
<Center>
|
<Center>
|
||||||
<CustomButton
|
<CustomButton
|
||||||
disabled={name != "" && selectedTeam ? false : true}
|
disabled={
|
||||||
|
name == "" ||
|
||||||
|
(selectedTeam ? false : true) ||
|
||||||
|
(selectedType ? false : true)
|
||||||
|
}
|
||||||
color="contrast"
|
color="contrast"
|
||||||
onClick={handleCreateExperiment}
|
onClick={handleCreateExperiment}
|
||||||
text="Создать эксперимент"
|
text="Создать эксперимент"
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
.StepperRoot {
|
|
||||||
flex-direction: row-reverse !important;
|
|
||||||
}
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Center,
|
|
||||||
Modal,
|
|
||||||
Space,
|
|
||||||
Stepper,
|
|
||||||
Title,
|
|
||||||
Text,
|
|
||||||
Flex,
|
|
||||||
Select,
|
|
||||||
Textarea,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import "./NewMolecule.css";
|
|
||||||
import {
|
|
||||||
IconArchiveFilled,
|
|
||||||
IconArticleFilled,
|
|
||||||
IconFileFilled,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import {
|
|
||||||
ConvertMoleculeToStandart,
|
|
||||||
GetInFormats,
|
|
||||||
} from "Api/ConvertBackendCalls";
|
|
||||||
import { AxiosError } from "axios";
|
|
||||||
import CustomButton from "Components/CustomButton/CustomButton";
|
|
||||||
|
|
||||||
interface NewMoleculeModalProps {
|
|
||||||
isOpened: boolean;
|
|
||||||
setIsOpened: (opened: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NewMoleculeModal(props: NewMoleculeModalProps) {
|
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
|
||||||
const [selectedMethod, setSelectedMethod] = useState(0);
|
|
||||||
|
|
||||||
const [inMolecule, setInMolecule] = useState("");
|
|
||||||
const [inFormat, setInFormat] = useState<string | null>();
|
|
||||||
|
|
||||||
const [inOptions, setInOptions] = useState<string[] | undefined>();
|
|
||||||
|
|
||||||
const getOptions = () => {
|
|
||||||
GetInFormats().then((data: { [key: string]: string } | AxiosError) => {
|
|
||||||
if (data instanceof AxiosError) {
|
|
||||||
//TODO: handle Error
|
|
||||||
} else {
|
|
||||||
const values = Object.keys(data);
|
|
||||||
setInOptions(values);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getOptions();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
//reset on open dialog
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.isOpened) {
|
|
||||||
setActiveStep(0);
|
|
||||||
setSelectedMethod(0);
|
|
||||||
setInMolecule("");
|
|
||||||
setInFormat(undefined);
|
|
||||||
if (inOptions == undefined) {
|
|
||||||
getOptions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [props.isOpened]);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
props.setIsOpened(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConvert = () => {
|
|
||||||
if (inFormat) {
|
|
||||||
ConvertMoleculeToStandart({
|
|
||||||
inputText: inMolecule,
|
|
||||||
inputFormat: inFormat,
|
|
||||||
make_3d: true,
|
|
||||||
add_h: false,
|
|
||||||
optimize: false,
|
|
||||||
}).then((data: string | AxiosError) => {
|
|
||||||
if (data instanceof AxiosError) {
|
|
||||||
//TODO: handle Error
|
|
||||||
} else {
|
|
||||||
setCurrentMoleculeString(data);
|
|
||||||
props.setIsOpened(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
//TODO: add no format selected handling
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEmptyMolecule = () => {
|
|
||||||
props.setIsOpened(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
opened={props.isOpened}
|
|
||||||
onClose={handleClose}
|
|
||||||
title=<Title size="xl">Новая молекула</Title>
|
|
||||||
centered
|
|
||||||
size="75%"
|
|
||||||
styles={{
|
|
||||||
content: { paddingLeft: "10px" },
|
|
||||||
title: { width: "100%" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stepper
|
|
||||||
active={activeStep}
|
|
||||||
onStepClick={setActiveStep}
|
|
||||||
allowNextStepsSelect={false}
|
|
||||||
classNames={{ root: "StepperRoot" }}
|
|
||||||
styles={{
|
|
||||||
steps: {
|
|
||||||
paddingTop: "30px",
|
|
||||||
paddingBottom: "30px",
|
|
||||||
paddingLeft: "20px",
|
|
||||||
minWidth: "0px",
|
|
||||||
alignSelf: "center",
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
style={{ display: "flex", flexDirection: "row" }}
|
|
||||||
orientation="vertical"
|
|
||||||
>
|
|
||||||
<Stepper.Step>
|
|
||||||
<div style={{ width: "100%", textAlign: "center" }}>
|
|
||||||
<Title size="lg">Шаг 1: Способ добавления молекулы</Title>
|
|
||||||
<Space h="lg" />
|
|
||||||
<Flex justify="center" gap="md" wrap="wrap">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedMethod(1);
|
|
||||||
handleEmptyMolecule();
|
|
||||||
}}
|
|
||||||
style={{ height: "100px", flexGrow: "1" }}
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
<Flex direction="row" gap="lg">
|
|
||||||
<IconFileFilled size={40} />
|
|
||||||
<Center>
|
|
||||||
<Text>Пустая молекула</Text>
|
|
||||||
</Center>
|
|
||||||
</Flex>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedMethod(2);
|
|
||||||
setActiveStep(1);
|
|
||||||
}}
|
|
||||||
style={{ height: "100px", flexGrow: "1" }}
|
|
||||||
>
|
|
||||||
<Flex direction="row" gap="lg">
|
|
||||||
<IconArchiveFilled size="40" />
|
|
||||||
<Center>
|
|
||||||
<Text>Из файла / Архива</Text>
|
|
||||||
</Center>
|
|
||||||
</Flex>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedMethod(3);
|
|
||||||
setActiveStep(1);
|
|
||||||
}}
|
|
||||||
style={{ height: "100px", flexGrow: "1" }}
|
|
||||||
>
|
|
||||||
<Flex direction="row" gap="lg">
|
|
||||||
<IconArticleFilled size={40} />
|
|
||||||
<Center>
|
|
||||||
<Text>Другой формат</Text>
|
|
||||||
</Center>
|
|
||||||
</Flex>
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</div>
|
|
||||||
</Stepper.Step>
|
|
||||||
<Stepper.Step>
|
|
||||||
<div style={{ width: "100%", textAlign: "center" }}>
|
|
||||||
{selectedMethod == 2 && (
|
|
||||||
<div>
|
|
||||||
<Title size="lg">Шаг 2: Выбор файла</Title>
|
|
||||||
<Space h="lg" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{selectedMethod == 3 && (
|
|
||||||
<div>
|
|
||||||
<Title size="md">Шаг 2: Ввод молекулы</Title>
|
|
||||||
<Space h="lg" />
|
|
||||||
<Textarea
|
|
||||||
className="moleculeInput"
|
|
||||||
value={inMolecule}
|
|
||||||
onChange={(e) => setInMolecule(e.currentTarget.value)}
|
|
||||||
placeholder="Введите текст молекулы"
|
|
||||||
classNames={{
|
|
||||||
wrapper: "moleculeInputWrapper",
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
// Stop arrow keys from reaching react-resizable-panels
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
"ArrowUp",
|
|
||||||
"ArrowDown",
|
|
||||||
"ArrowLeft",
|
|
||||||
"ArrowRight",
|
|
||||||
].includes(e.key)
|
|
||||||
) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Space h="md" />
|
|
||||||
<Select
|
|
||||||
value={inFormat}
|
|
||||||
onChange={(value: string | null) => setInFormat(value)}
|
|
||||||
searchable
|
|
||||||
data={inOptions}
|
|
||||||
placeholder="Выберите формат"
|
|
||||||
classNames={{
|
|
||||||
input: "selectInFormat",
|
|
||||||
dropdown: "selectDropDown",
|
|
||||||
option: "selectDropDownOption",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Space h="md" />
|
|
||||||
<CustomButton
|
|
||||||
color="accent"
|
|
||||||
onClick={handleConvert}
|
|
||||||
text="Преобразовать"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Stepper.Step>
|
|
||||||
</Stepper>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
0
src/Modals/NewTask/NewTask.css
Normal file
0
src/Modals/NewTask/NewTask.css
Normal file
127
src/Modals/NewTask/NewTask.tsx
Normal file
127
src/Modals/NewTask/NewTask.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import {
|
||||||
|
Center,
|
||||||
|
Modal,
|
||||||
|
Space,
|
||||||
|
Textarea,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import "./NewTask.css";
|
||||||
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
import { IconCheck } from "@tabler/icons-react";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||||
|
import { createInstance } from "Api/QuantumBackend/ExperimentsManagment";
|
||||||
|
|
||||||
|
interface NewInstanceModalProps {
|
||||||
|
isOpened: boolean;
|
||||||
|
setIsOpened: (opened: boolean) => void;
|
||||||
|
experiment_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewInstanceModal(props: NewInstanceModalProps) {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const { setInstances, instances } = useExperimentStore();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
//reset on open dialog
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.isOpened) {
|
||||||
|
setName("");
|
||||||
|
setDescription("");
|
||||||
|
}
|
||||||
|
}, [props.isOpened]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
props.setIsOpened(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateTeam = () => {
|
||||||
|
//TODO: add logic for backend server
|
||||||
|
//
|
||||||
|
setIsLoading(true);
|
||||||
|
createInstance({
|
||||||
|
experiment_id: props.experiment_id,
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
instance_data: "{}",
|
||||||
|
}).then((instance) => {
|
||||||
|
setIsLoading(false);
|
||||||
|
if (instance) {
|
||||||
|
setInstances([
|
||||||
|
{
|
||||||
|
instance_id: instance.id,
|
||||||
|
name: instance.name,
|
||||||
|
instance_data: "{}",
|
||||||
|
description: instance.description,
|
||||||
|
qubits_needed: 0,
|
||||||
|
},
|
||||||
|
...instances,
|
||||||
|
]);
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
title: "Задача создана успешно",
|
||||||
|
message: "",
|
||||||
|
icon: <IconCheck />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
props.setIsOpened(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={props.isOpened}
|
||||||
|
onClose={handleClose}
|
||||||
|
title=<Title size="xl">Новая Задача</Title>
|
||||||
|
centered
|
||||||
|
size="75%"
|
||||||
|
styles={{
|
||||||
|
content: { paddingLeft: "10px" },
|
||||||
|
title: { width: "100%" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
value={name}
|
||||||
|
label="Имя задач"
|
||||||
|
placeholder="Введите имя задачи"
|
||||||
|
required
|
||||||
|
onChange={(event) => {
|
||||||
|
setName(event.currentTarget.value);
|
||||||
|
}}
|
||||||
|
></TextInput>
|
||||||
|
<Space h="md" />
|
||||||
|
<Textarea
|
||||||
|
value={description}
|
||||||
|
label="Описание задачи"
|
||||||
|
placeholder="Введите описание задачи"
|
||||||
|
minRows={4}
|
||||||
|
maxRows={10}
|
||||||
|
autosize
|
||||||
|
onChange={(event) => {
|
||||||
|
setDescription(event.currentTarget.value);
|
||||||
|
}}
|
||||||
|
></Textarea>
|
||||||
|
<Space h="md" />
|
||||||
|
<Center>
|
||||||
|
<CustomButton
|
||||||
|
disabled={(name != "" ? false : true) || isLoading}
|
||||||
|
color="contrast"
|
||||||
|
onClick={handleCreateTeam}
|
||||||
|
text="Создать Задачу"
|
||||||
|
></CustomButton>
|
||||||
|
<Space w="md" />
|
||||||
|
<CustomButton
|
||||||
|
color="contrast"
|
||||||
|
style="outline"
|
||||||
|
onClick={() => {
|
||||||
|
props.setIsOpened(false);
|
||||||
|
}}
|
||||||
|
text="Отменить"
|
||||||
|
></CustomButton>
|
||||||
|
</Center>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,6 +21,12 @@ import TeamInMachineListCard from "Components/ListCard/TeamListCard/TeamInMachin
|
|||||||
import { AddTeamToDevice } from "Modals/AddTeamToDevice/AddTeamToDevice";
|
import { AddTeamToDevice } from "Modals/AddTeamToDevice/AddTeamToDevice";
|
||||||
import { getShortTeamsList } from "Api/QuantumBackend/TeamManagement";
|
import { getShortTeamsList } from "Api/QuantumBackend/TeamManagement";
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
ONLINE: "var(--mantine-color-green-7)",
|
||||||
|
OFFLINE: "var(--mantine-color-red-7)",
|
||||||
|
BUSY: "var(--mantine-color-yellow-5)",
|
||||||
|
};
|
||||||
|
|
||||||
function DevicePage() {
|
function DevicePage() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { device_id } = useParams();
|
const { device_id } = useParams();
|
||||||
@@ -107,7 +113,16 @@ function DevicePage() {
|
|||||||
<Space h="xl" />
|
<Space h="xl" />
|
||||||
<div style={{ display: "flex", flexDirection: "row", gap: "15px" }}>
|
<div style={{ display: "flex", flexDirection: "row", gap: "15px" }}>
|
||||||
<Text size="md">Статус: </Text>
|
<Text size="md">Статус: </Text>
|
||||||
<Pill className="ExperimentPill">
|
<Pill
|
||||||
|
className="ExperimentPill"
|
||||||
|
style={{
|
||||||
|
color: "white",
|
||||||
|
backgroundColor:
|
||||||
|
colors[
|
||||||
|
cur_device?.system.status as "ONLINE" | "OFFLINE" | "BUSY"
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Text size="md">{cur_device?.system.status}</Text>
|
<Text size="md">{cur_device?.system.status}</Text>
|
||||||
</Pill>
|
</Pill>
|
||||||
<div className="dateContainer">
|
<div className="dateContainer">
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { Box, Divider, TableOfContents, Title } from "@mantine/core";
|
import {
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
TableOfContents,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
List,
|
||||||
|
Code,
|
||||||
|
Anchor,
|
||||||
|
} from "@mantine/core";
|
||||||
import "./DocumentationPage.css";
|
import "./DocumentationPage.css";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
@@ -6,15 +15,19 @@ function DocumentationPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Documentation | QMolSim</title>
|
<title>Документация | QMolSim</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="See the documentation on how to setup and use the qunatum computational system"
|
content="Полное руководство пользователя по системе распределенного квантово-химического расчета QMolSim."
|
||||||
/>
|
/>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Title order={1} className="docTitle">
|
<Title order={1} className="docTitle">
|
||||||
Документация
|
Руководство пользователя
|
||||||
</Title>
|
</Title>
|
||||||
|
<Text size="sm" c="dimmed" mb="md">
|
||||||
|
Автоматизированная система распределенного расчета энергии основного
|
||||||
|
состояния молекул
|
||||||
|
</Text>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className="DocumentationPage">
|
<div className="DocumentationPage">
|
||||||
<Box className="tableOfContents" visibleFrom="md">
|
<Box className="tableOfContents" visibleFrom="md">
|
||||||
@@ -26,24 +39,291 @@ function DocumentationPage() {
|
|||||||
minDepthToOffset={0}
|
minDepthToOffset={0}
|
||||||
depthOffset={20}
|
depthOffset={20}
|
||||||
scrollSpyOptions={{
|
scrollSpyOptions={{
|
||||||
selector: "section h1, h2",
|
selector: "section h1, section h2, section h3",
|
||||||
}}
|
}}
|
||||||
className=""
|
|
||||||
getControlProps={({ data }) => ({
|
getControlProps={({ data }) => ({
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
data
|
data
|
||||||
.getNode()
|
.getNode()
|
||||||
.scrollIntoView({ behavior: "smooth", block: "center" }),
|
.scrollIntoView({ behavior: "smooth", block: "start" }),
|
||||||
children: data.value,
|
children: data.value,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<div className="contents">
|
<div className="contents">
|
||||||
<section id="introduction" style={{ height: 1000 }}>
|
{/* ================= 1 ВВЕДЕНИЕ ================= */}
|
||||||
|
<section id="introduction">
|
||||||
<Title order={1}>1. Введение</Title>
|
<Title order={1}>1. Введение</Title>
|
||||||
|
|
||||||
|
<Title order={2} mt="md" id="application-area">
|
||||||
|
1.1 Область применения
|
||||||
|
</Title>
|
||||||
|
<Text>Требования настоящего документа применяются при:</Text>
|
||||||
|
<List>
|
||||||
|
<List.Item>предварительных комплексных испытаниях;</List.Item>
|
||||||
|
<List.Item>опытной эксплуатации;</List.Item>
|
||||||
|
<List.Item>приемочных испытаниях;</List.Item>
|
||||||
|
<List.Item>промышленной эксплуатации.</List.Item>
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Title order={2} mt="md" id="capabilities">
|
||||||
|
1.2 Краткое описание возможностей
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
Автоматизированная система распределенного расчета энергии
|
||||||
|
основного состояния молекул с помощью квантовых алгоритмов
|
||||||
|
представляет собой распределенный веб-сервис, предназначенный для
|
||||||
|
выполнения ресурсоемких квантово-химических расчетов с
|
||||||
|
использованием гибридной архитектуры, состоящей из центрального
|
||||||
|
сервера и распределенных квантовых симуляторов.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Title order={3} mt="sm" id="cap-mgmt">
|
||||||
|
Управление командной работой
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
Пользователи могут создавать команды, приглашать других
|
||||||
|
исследователей, назначать права доступа.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Title order={3} mt="sm" id="cap-nodes">
|
||||||
|
Подключение вычислительных систем
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
Исследователи регистрируют в системе свои вычислительные узлы. Для
|
||||||
|
каждого узла исследователь задает максимальное количество кубитов,
|
||||||
|
а также предоставляет доступ командам на использование устройства.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Title order={3} mt="sm" id="cap-experiments">
|
||||||
|
Создание и запуск экспериментов
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
В рамках команды пользователь создает эксперимент (набор задач для
|
||||||
|
разных молекул). Для каждой задачи загружается или редактируется
|
||||||
|
структура молекулы в формате XYZ, задаются квантово-химические
|
||||||
|
параметры.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Title order={3} mt="sm" id="cap-vqe">
|
||||||
|
Распределенные вычисления VQE
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
При запуске эксперимента система автоматически распределяет задачи
|
||||||
|
по доступным вычислительным узлам с учетом их ограничений по числу
|
||||||
|
кубит. В процессе расчета на сервер передаются промежуточные
|
||||||
|
результаты.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Title order={3} mt="sm" id="cap-fault">
|
||||||
|
Отказоустойчивость и восстановление
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
Каждый вычислительный узел каждые 5 секунд отправляет сигнал о
|
||||||
|
своей работоспособности. При выходе узла из строя незавершенная
|
||||||
|
задача автоматически перенаправляется в очередь и назначается на
|
||||||
|
другой узел с сохранением промежуточных весов оптимизации.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Title order={3} mt="sm" id="cap-vis">
|
||||||
|
Визуализация молекул
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
Для каждой задачи доступна интерактивная 3D-визуализация молекулы
|
||||||
|
в шаростержневой модели.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Title order={3} mt="sm" id="cap-import">
|
||||||
|
Импорт молекулярных данных
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
Система поддерживает преобразование молекул в требуемый формат из
|
||||||
|
большинства существующих химических форматов.
|
||||||
|
</Text>
|
||||||
</section>
|
</section>
|
||||||
<section id="quick-start" style={{ height: 1000 }}>
|
|
||||||
<Title order={2}>1.1 Быстрое начало</Title>
|
{/* ================= 2 НАЗНАЧЕНИЕ И УСЛОВИЯ ================= */}
|
||||||
|
<section id="purpose-conditions" style={{ marginTop: "2rem" }}>
|
||||||
|
<Title order={1}>2. Назначение и условия применения</Title>
|
||||||
|
|
||||||
|
<Title order={2} mt="md" id="purpose">
|
||||||
|
2.1 Назначение системы
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
Система предназначена для автоматизированного распределенного
|
||||||
|
расчета энергии основного состояния молекул с использованием
|
||||||
|
квантового алгоритма VQE. Она обеспечивает создание команд
|
||||||
|
исследователей с настройкой прав доступа, автоматическое
|
||||||
|
распределение вычислительных задач между доступными узлами,
|
||||||
|
мониторинг состояния вычислений и восстановление прогресса расчета
|
||||||
|
при сбое отдельных вычислительных систем. Применение системы
|
||||||
|
позволяет повысить скорость проведения квантово-химических
|
||||||
|
расчетов и снизить нагрузку на пользователя по управлению
|
||||||
|
вычислительным процессом. Система ориентирована на специалистов в
|
||||||
|
области квантовой химии и вычислительных технологий.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Title order={2} mt="md" id="tech-reqs">
|
||||||
|
2.2 Требования к техническим средствам
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Title order={3} mt="sm" id="client-browser">
|
||||||
|
Клиент-браузер:
|
||||||
|
</Title>
|
||||||
|
<List>
|
||||||
|
<List.Item>Оперативная память от 4 Гб;</List.Item>
|
||||||
|
<List.Item>Свободное пространство на диске от 2 Гб;</List.Item>
|
||||||
|
<List.Item>Процессор 4-ядерный с частотой от 2 ГГц;</List.Item>
|
||||||
|
<List.Item>Скорость подключения в интернет от 50 Мб/c;</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
Наличие манипулятора "мышь" или аналогичного устройства для
|
||||||
|
взаимодействия с интерфейсом;
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>Наличие Клавиатуры.</List.Item>
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Title order={3} mt="sm" id="client-compute">
|
||||||
|
Клиент-ВС:
|
||||||
|
</Title>
|
||||||
|
<List>
|
||||||
|
<List.Item>Оперативная память от 8 Гб;</List.Item>
|
||||||
|
<List.Item>Свободное пространство на диске от 5 Гб;</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
Процессор 8-ядерный с частотой от 2-4,4 ГГц;
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>Скорость подключения в интернет от 50 Мб/с;</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
Наличие манипулятора "мышь" или аналогичного устройства для
|
||||||
|
взаимодействия с интерфейсом;
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>Наличие Клавиатуры.</List.Item>
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Title order={2} mt="md" id="software-reqs">
|
||||||
|
2.3 Требования к программным средствам
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Title order={3} mt="sm" id="sw-browser">
|
||||||
|
Клиент-браузер:
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
Браузер (Safari 18.1.1, Яндекс Браузер 25.2.1, Google Chrome
|
||||||
|
110.0.5481.100)
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Title order={3} mt="sm" id="sw-compute">
|
||||||
|
Клиент-ВС:
|
||||||
|
</Title>
|
||||||
|
<List>
|
||||||
|
<List.Item>ОС Windows 10, Windows 11, MacOS, Linux</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
Браузер (Safari 18.1.1, Яндекс Браузер 25.2.1, Google Chrome
|
||||||
|
110.0.5481.100)
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>Docker</List.Item>
|
||||||
|
<List.Item>Docker-compose v2</List.Item>
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Title order={2} mt="md" id="exec-conditions">
|
||||||
|
3. Условия выполнения программы
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
Для работы системы требуется веб-браузер, поддерживающий
|
||||||
|
современные функции JavaScript (Google Chrome версии 110 и выше,
|
||||||
|
Яндекс Браузер версии 25.2.1 и выше, Safari версии 18.1.1 и выше).
|
||||||
|
Доступ к системе осуществляется через веб-интерфейс по адресу,
|
||||||
|
предоставленному администратором. Для работы вычислительных узлов
|
||||||
|
дополнительно требуется установленный Docker и Docker Compose v2
|
||||||
|
на каждой подключаемой вычислительной системе. Необходимо наличие
|
||||||
|
постоянного сетевого подключения к серверу для всех
|
||||||
|
взаимодействующих компонентов системы.
|
||||||
|
</Text>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ================= 4 ВЫПОЛНЕНИЕ ПРОГРАММЫ ================= */}
|
||||||
|
<section id="execution" style={{ marginTop: "2rem" }}>
|
||||||
|
<Title order={1}>4. Выполнение программы</Title>
|
||||||
|
|
||||||
|
<Title order={2} mt="md" id="install">
|
||||||
|
4.1 Инсталяция/деинсталяция
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
Клиент-браузер инсталляции и деинсталляции не требуется, для
|
||||||
|
работы необходимо только наличие на системе совместимого браузера.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Title order={3} mt="sm" id="install-compute">
|
||||||
|
Для инсталляции клиента-ВС:
|
||||||
|
</Title>
|
||||||
|
<List>
|
||||||
|
<List.Item>
|
||||||
|
На системе необходимо наличие docker и docker-compose v2
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
Необходимо скачать контейнер с помощью команды:{" "}
|
||||||
|
<Code>docker pull git.deowl.ru/vkrb/client:0.1.0</Code>
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
Затем скачать файл docker-compose с помощью команды:{" "}
|
||||||
|
<Code>
|
||||||
|
curl -O
|
||||||
|
"https://git.deowl.ru/vkrb/local_quantum_simulator/raw/branch/main/docker-compose.yml"
|
||||||
|
</Code>
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
Наконец, в той же папке необходимо создать файл переменных среды
|
||||||
|
с названием ".env" и содержимым:
|
||||||
|
<Code block mt="xs">
|
||||||
|
{`PORT=5001
|
||||||
|
STORAGE_PATH="/storage"
|
||||||
|
RABBITMQ_HOST=rabbit.deowl.ru
|
||||||
|
RABBITMQ_PORT=5672
|
||||||
|
KEYCLOAK_URL=https://quantum-auth.deowl.ru
|
||||||
|
KEYCLOAK_REALM_NAME=quant_sim-realm
|
||||||
|
KEACLOAK_CLIENT_ID=local_quantum_sim
|
||||||
|
QUANTUM_BACKEND_URL=https://quantum.deowl.ru`}
|
||||||
|
</Code>
|
||||||
|
</List.Item>
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Title order={3} mt="md" id="uninstall-compute">
|
||||||
|
Для деинсталляции клиента-ВС:
|
||||||
|
</Title>
|
||||||
|
<List>
|
||||||
|
<List.Item>
|
||||||
|
Удаляем файлы «docker-compose.yml», «.env» и папку
|
||||||
|
«localStorage» (при ее наличие)
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
Удаляем установленное изображение с помощью команды:{" "}
|
||||||
|
<Code>docker image rm git.deowl.ru/vkrb/client:0.1.0</Code>
|
||||||
|
</List.Item>
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Title order={2} mt="md" id="start-stop">
|
||||||
|
4.2 Запуск / Остановка программы
|
||||||
|
</Title>
|
||||||
|
<Text>
|
||||||
|
Клиент-браузер может быть открыт по ссылке:{" "}
|
||||||
|
<Anchor href="http://quantum.deowl.ru/">
|
||||||
|
http://quantum.deowl.ru/
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
<Text mt="sm">
|
||||||
|
Для запуска клиента-ВС необходимо выполнить команду, находясь в
|
||||||
|
папке с файлом «docker-compose.yml»:{" "}
|
||||||
|
<Code>docker compose up --d</Code>
|
||||||
|
</Text>
|
||||||
|
<Text mt="sm">
|
||||||
|
Для остановки клиента-ВС: <Code>docker compose down</Code>
|
||||||
|
</Text>
|
||||||
|
<Text mt="sm">
|
||||||
|
Для первичного подключения и мониторинга статуса клиента-ВС
|
||||||
|
необходимо открыть ссылку:{" "}
|
||||||
|
<Anchor href="http://localhost:5001/">
|
||||||
|
http://localhost:5001/
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,15 +2,19 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.experimentButtons {
|
.experimentButtons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: right;
|
justify-content: space-between;
|
||||||
width: fit-content;
|
|
||||||
margin-left: auto;
|
width: 100%;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
|
* {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,221 @@
|
|||||||
import { Alert, Center, Text } from "@mantine/core";
|
import {
|
||||||
|
Alert,
|
||||||
|
Center,
|
||||||
|
LoadingOverlay,
|
||||||
|
Text,
|
||||||
|
UnstyledButton,
|
||||||
|
SimpleGrid,
|
||||||
|
Title,
|
||||||
|
TextInput,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Card,
|
||||||
|
Badge,
|
||||||
|
Collapse,
|
||||||
|
ActionIcon,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
import "./ExperimentPage.css";
|
import "./ExperimentPage.css";
|
||||||
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
|
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
|
||||||
//import { NewMoleculeModal } from "Modals/NewMolecule/NewMolecule";
|
import { useEffect, useState } from "react";
|
||||||
import { useState } from "react";
|
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { IconPlus, IconSettings } from "@tabler/icons-react";
|
import {
|
||||||
|
IconPlus,
|
||||||
|
IconReload,
|
||||||
|
IconEdit,
|
||||||
|
IconX,
|
||||||
|
IconCheck,
|
||||||
|
IconChevronUp,
|
||||||
|
IconChevronDown,
|
||||||
|
IconCancel,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import CustomButton from "Components/CustomButton/CustomButton";
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||||
|
import type { ExperimentData, InstanceData } from "Types/Experiment/Experiment";
|
||||||
|
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
|
import {
|
||||||
|
getExperimentById,
|
||||||
|
getExperimentInstances,
|
||||||
|
getFrontendFile,
|
||||||
|
startExperiment,
|
||||||
|
updateExperiment,
|
||||||
|
} from "Api/QuantumBackend/ExperimentsManagment";
|
||||||
|
import InstancesListCard from "Components/ListCard/TaskListCard/TaskListCard";
|
||||||
|
import { NewInstanceModal } from "Modals/NewTask/NewTask";
|
||||||
|
|
||||||
function ExperimentPage() {
|
function ExperimentPage() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpened, setIsOpen] = useState(false);
|
||||||
const { experiment_id } = useParams();
|
const { experiment_id } = useParams();
|
||||||
const { experiments, addTask } = useExperimentStore();
|
const { experiments, addExperiment, instances, setInstances } =
|
||||||
|
useExperimentStore();
|
||||||
const experiment = experiments.find((exp) => {
|
const [experiment, set_experiment] = useState<ExperimentData | undefined>(
|
||||||
|
experiments.find((exp) => {
|
||||||
return exp.id == Number(experiment_id);
|
return exp.id == Number(experiment_id);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { profile, is_loading } = useAuthenticationStore();
|
||||||
|
const [is_loading_, set_is_loading] = useState(true);
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editedName, setEditedName] = useState("");
|
||||||
|
const [editedDesc, setEditedDesc] = useState("");
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const { loadedHtmlFiles, addLoadedHtml } = useExperimentStore();
|
||||||
|
|
||||||
|
const [total_experiment_inst, set_total_experiment_inst] =
|
||||||
|
useState<number>(0);
|
||||||
|
const [page_size, set_page_size] = useState<number>(5);
|
||||||
|
const [cur_page, set_cur_page] = useState<number>(1);
|
||||||
|
|
||||||
|
const saveExperimentDetails = () => {
|
||||||
|
if (experiment && editedName.trim()) {
|
||||||
|
updateExperiment({
|
||||||
|
experiment_id: experiment.id,
|
||||||
|
name: editedName,
|
||||||
|
description: editedDesc,
|
||||||
|
})
|
||||||
|
.then((updated) => {
|
||||||
|
const updatedExperiment = {
|
||||||
|
...experiment,
|
||||||
|
name: updated.name,
|
||||||
|
description: updated.description,
|
||||||
|
};
|
||||||
|
addExperiment(updatedExperiment);
|
||||||
|
set_experiment(updatedExperiment);
|
||||||
|
setIsEditing(false);
|
||||||
|
notifications.show({
|
||||||
|
title: "Успех",
|
||||||
|
message: "Информация об эксперименте обновлена",
|
||||||
|
color: "green",
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notifications.show({
|
||||||
|
title: "Ошибка",
|
||||||
|
message: "Не удалось обновить информацию",
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEditing = () => {
|
||||||
|
if (experiment) {
|
||||||
|
setEditedName(experiment.name);
|
||||||
|
setEditedDesc(experiment.description || "");
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startEditing = () => {
|
||||||
|
if (experiment) {
|
||||||
|
setEditedName(experiment.name);
|
||||||
|
setEditedDesc(experiment.description || "");
|
||||||
|
setIsEditing(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExperimentStart = () => {
|
||||||
|
if (experiment) {
|
||||||
|
startExperiment({ experiment_id: experiment.id })
|
||||||
|
.then(() => {
|
||||||
|
getExperimentById(Number(experiment_id))
|
||||||
|
.then((exp) => {
|
||||||
|
addExperiment(exp);
|
||||||
|
set_experiment(exp);
|
||||||
|
getExperimentInstances(exp.id, cur_page, page_size)
|
||||||
|
.then((inst) => {
|
||||||
|
setInstances(inst.instances);
|
||||||
|
set_cur_page(inst.cur_page);
|
||||||
|
set_total_experiment_inst(inst.total_instances);
|
||||||
|
set_page_size(inst.page_size);
|
||||||
|
set_is_loading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
set_is_loading(false);
|
||||||
|
});
|
||||||
|
getFrontendFile(exp.experiment_type.id).then((file) => {
|
||||||
|
addLoadedHtml(exp.experiment_type.id, file);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
set_is_loading(false);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notifications.show({
|
||||||
|
title: "Ошибка",
|
||||||
|
message:
|
||||||
|
"Не удалось начать эксперимент, проверьте наличие задач и их правильность",
|
||||||
|
color: "red",
|
||||||
|
icon: <IconCancel />,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (profile && !is_loading) {
|
||||||
|
if (!experiment) {
|
||||||
|
getExperimentById(Number(experiment_id))
|
||||||
|
.then((exp) => {
|
||||||
|
addExperiment(exp);
|
||||||
|
set_experiment(exp);
|
||||||
|
getExperimentInstances(exp.id, cur_page, page_size)
|
||||||
|
.then((inst) => {
|
||||||
|
setInstances(inst.instances);
|
||||||
|
set_cur_page(inst.cur_page);
|
||||||
|
set_total_experiment_inst(inst.total_instances);
|
||||||
|
set_page_size(inst.page_size);
|
||||||
|
set_is_loading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
set_is_loading(false);
|
||||||
|
});
|
||||||
|
getFrontendFile(exp.experiment_type.id).then((file) => {
|
||||||
|
addLoadedHtml(exp.experiment_type.id, file);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
set_is_loading(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
(instances.length == 0 && cur_page != 0) ||
|
||||||
|
(total_experiment_inst > page_size &&
|
||||||
|
instances.length != page_size &&
|
||||||
|
cur_page != Math.ceil(total_experiment_inst / page_size))
|
||||||
|
) {
|
||||||
|
getExperimentInstances(
|
||||||
|
experiment.id,
|
||||||
|
Math.min(cur_page, Math.ceil(total_experiment_inst / page_size)),
|
||||||
|
page_size,
|
||||||
|
)
|
||||||
|
.then((inst) => {
|
||||||
|
if (inst.instances.length == 0) {
|
||||||
|
set_cur_page(0);
|
||||||
|
setInstances([]);
|
||||||
|
} else {
|
||||||
|
setInstances(inst.instances);
|
||||||
|
set_cur_page(inst.cur_page);
|
||||||
|
set_total_experiment_inst(inst.total_instances);
|
||||||
|
set_page_size(inst.page_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
set_is_loading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
set_is_loading(false);
|
||||||
|
});
|
||||||
|
getFrontendFile(experiment.experiment_type.id).then((file) => {
|
||||||
|
addLoadedHtml(experiment.experiment_type.id, file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [profile, is_loading, instances]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -24,15 +223,24 @@ function ExperimentPage() {
|
|||||||
<title>
|
<title>
|
||||||
{experiment
|
{experiment
|
||||||
? "Experiment " + experiment.id + " | QMolSim"
|
? "Experiment " + experiment.id + " | QMolSim"
|
||||||
: "Error |QmolSim"}
|
: "Error | QMolSim"}
|
||||||
</title>
|
</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="See the documentation on how to setup and use the qunatum computational system"
|
content="See the documentation on how to setup and use the quantum computational system"
|
||||||
/>
|
/>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
{experiment && (
|
|
||||||
<div className="ExperimentPage">
|
<div className="ExperimentPage">
|
||||||
|
{experiment && (
|
||||||
|
<NewInstanceModal
|
||||||
|
experiment_id={experiment.id}
|
||||||
|
isOpened={isOpened}
|
||||||
|
setIsOpened={setIsOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<LoadingOverlay visible={is_loading_} zIndex={1000} />
|
||||||
|
{experiment && (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -42,44 +250,300 @@ function ExperimentPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="experimentButtons">
|
<div className="experimentButtons">
|
||||||
|
{experiment.status == "DRAFT" && (
|
||||||
|
<>
|
||||||
|
<CustomButton
|
||||||
|
color="accent"
|
||||||
|
onClick={() => {
|
||||||
|
handleExperimentStart();
|
||||||
|
}}
|
||||||
|
icon={<IconPlus />}
|
||||||
|
text="Начать эксперимент"
|
||||||
|
/>
|
||||||
<CustomButton
|
<CustomButton
|
||||||
color="contrast"
|
color="contrast"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
addTask(Number(experiment_id) || 0, {
|
|
||||||
id: 1,
|
|
||||||
name: "Задача 1",
|
|
||||||
description: "",
|
|
||||||
data: {},
|
|
||||||
});
|
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}}
|
}}
|
||||||
icon={<IconPlus />}
|
icon={<IconPlus />}
|
||||||
text="Добавить задачу"
|
text="Добавить задачу"
|
||||||
/>
|
/>
|
||||||
<CustomButton
|
</>
|
||||||
color="contrast"
|
)}
|
||||||
style="outline"
|
{experiment.status != "DRAFT" && (
|
||||||
|
<UnstyledButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpen(true);
|
getExperimentById(Number(experiment_id))
|
||||||
|
.then((exp) => {
|
||||||
|
addExperiment(exp);
|
||||||
|
set_experiment(exp);
|
||||||
|
getExperimentInstances(exp.id)
|
||||||
|
.then((inst) => {
|
||||||
|
setInstances(inst.instances);
|
||||||
|
set_is_loading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
set_is_loading(false);
|
||||||
|
});
|
||||||
|
getFrontendFile(exp.experiment_type.id).then(
|
||||||
|
(file) => {
|
||||||
|
addLoadedHtml(exp.experiment_type.id, file);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
set_is_loading(false);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
icon={<IconSettings />}
|
>
|
||||||
text="Параметры эксперимента"
|
<IconReload />
|
||||||
|
</UnstyledButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Experiment Information Card */}
|
||||||
|
{/* Experiment Information Card */}
|
||||||
|
<Card withBorder mb="lg" shadow="sm">
|
||||||
|
<Card.Section withBorder inheritPadding py="sm">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Group gap="xs">
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<IconChevronUp size={16} />
|
||||||
|
) : (
|
||||||
|
<IconChevronDown size={16} />
|
||||||
|
)}
|
||||||
|
</ActionIcon>
|
||||||
|
<Title order={3}>Информация об эксперименте</Title>
|
||||||
|
</Group>
|
||||||
|
{!isEditing ? (
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
leftSection={<IconEdit size={16} />}
|
||||||
|
onClick={startEditing}
|
||||||
|
>
|
||||||
|
Редактировать
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Group gap="xs">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="filled"
|
||||||
|
color="green"
|
||||||
|
leftSection={<IconCheck size={14} />}
|
||||||
|
onClick={saveExperimentDetails}
|
||||||
|
>
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline"
|
||||||
|
color="red"
|
||||||
|
leftSection={<IconX size={14} />}
|
||||||
|
onClick={cancelEditing}
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Card.Section>
|
||||||
|
|
||||||
|
<Collapse expanded={isExpanded} style={{ padding: "15px" }}>
|
||||||
|
<Card.Section inheritPadding py="md">
|
||||||
|
{!isEditing ? (
|
||||||
|
<SimpleGrid cols={4} spacing="lg" verticalSpacing="md">
|
||||||
|
<div>
|
||||||
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||||
|
ID
|
||||||
|
</Text>
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
{experiment.id}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||||
|
Тип
|
||||||
|
</Text>
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
{experiment.experiment_type.name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||||
|
Название
|
||||||
|
</Text>
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
{experiment.name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||||
|
Статус
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
size="md"
|
||||||
|
variant="filled"
|
||||||
|
color={
|
||||||
|
experiment.status === "DRAFT" ? "yellow" : "green"
|
||||||
|
}
|
||||||
|
radius="sm"
|
||||||
|
>
|
||||||
|
{experiment.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||||
|
Команда
|
||||||
|
</Text>
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
{experiment.team.team_name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||||
|
Количество задач
|
||||||
|
</Text>
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
{experiment.instances_count}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ gridColumn: "span 2" }}>
|
||||||
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||||
|
Описание
|
||||||
|
</Text>
|
||||||
|
<Text size="md">
|
||||||
|
{experiment.description || "Нет описания"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||||
|
Создан
|
||||||
|
</Text>
|
||||||
|
<Text size="sm">
|
||||||
|
{new Date(experiment.created_at).toLocaleString()}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</SimpleGrid>
|
||||||
|
) : (
|
||||||
|
<SimpleGrid cols={4} spacing="lg" verticalSpacing="md">
|
||||||
|
<div style={{ gridColumn: "span 1" }}>
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c="dimmed"
|
||||||
|
tt="uppercase"
|
||||||
|
fw={700}
|
||||||
|
mb={4}
|
||||||
|
>
|
||||||
|
Название
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
value={editedName}
|
||||||
|
onChange={(e) => setEditedName(e.target.value)}
|
||||||
|
placeholder="Введите название"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div style={{ gridColumn: "span 3" }}>
|
||||||
{experiment.tasks_ids.length > 0 && (
|
<Text
|
||||||
<PaginationContainer
|
size="xs"
|
||||||
numberOfPages={1}
|
c="dimmed"
|
||||||
isLoading={false}
|
tt="uppercase"
|
||||||
activePage={1}
|
fw={700}
|
||||||
setPage={() => {}}
|
mb={4}
|
||||||
>
|
>
|
||||||
{experiment.tasks_ids.map((task: number) => {
|
Описание
|
||||||
return <div>{task}</div>;
|
</Text>
|
||||||
})}
|
<TextInput
|
||||||
</PaginationContainer>
|
value={editedDesc}
|
||||||
|
onChange={(e) => setEditedDesc(e.target.value)}
|
||||||
|
placeholder="Введите описание"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||||
|
ID
|
||||||
|
</Text>
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
{experiment.id}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||||
|
Тип
|
||||||
|
</Text>
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
{experiment.experiment_type.name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||||
|
Команда
|
||||||
|
</Text>
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
{experiment.team.team_name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text size="xs" c="dimmed" tt="uppercase" fw={700}>
|
||||||
|
Статус
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
size="md"
|
||||||
|
variant="filled"
|
||||||
|
color={
|
||||||
|
experiment.status === "DRAFT" ? "yellow" : "green"
|
||||||
|
}
|
||||||
|
radius="sm"
|
||||||
|
>
|
||||||
|
{experiment.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</SimpleGrid>
|
||||||
)}
|
)}
|
||||||
{experiment.tasks_ids.length == 0 && (
|
</Card.Section>
|
||||||
|
</Collapse>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{!is_loading_ && (
|
||||||
|
<PaginationContainer
|
||||||
|
numberOfPages={Math.ceil(total_experiment_inst / page_size)}
|
||||||
|
isLoading={false}
|
||||||
|
activePage={cur_page}
|
||||||
|
setPage={(page_num) => {
|
||||||
|
getExperimentInstances(experiment.id, page_num, page_size)
|
||||||
|
.then((inst) => {
|
||||||
|
setInstances(inst.instances);
|
||||||
|
set_cur_page(inst.cur_page);
|
||||||
|
set_total_experiment_inst(inst.total_instances);
|
||||||
|
set_page_size(inst.page_size);
|
||||||
|
set_is_loading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
set_is_loading(false);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{instances &&
|
||||||
|
loadedHtmlFiles.get(experiment.experiment_type.id) &&
|
||||||
|
instances.map((inst: InstanceData) => {
|
||||||
|
return (
|
||||||
|
<InstancesListCard
|
||||||
|
instance={inst}
|
||||||
|
plugin={
|
||||||
|
loadedHtmlFiles.get(experiment.experiment_type.id) ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
key={inst.instance_id}
|
||||||
|
></InstancesListCard>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{instances.length == 0 && !is_loading && (
|
||||||
<Alert>
|
<Alert>
|
||||||
<Center>
|
<Center>
|
||||||
<Text size={"xl"} c="contrast">
|
<Text size={"xl"} c="contrast">
|
||||||
@@ -88,9 +552,11 @@ function ExperimentPage() {
|
|||||||
</Center>
|
</Center>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PaginationContainer>
|
||||||
)}
|
)}
|
||||||
{!experiment && (
|
</>
|
||||||
|
)}
|
||||||
|
{!experiment && !is_loading_ && (
|
||||||
<Alert color="red">
|
<Alert color="red">
|
||||||
<Center>
|
<Center>
|
||||||
{" "}
|
{" "}
|
||||||
@@ -100,6 +566,7 @@ function ExperimentPage() {
|
|||||||
</Center>
|
</Center>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.experimentsButtons {
|
.experimentsButtons {
|
||||||
|
|||||||
@@ -6,23 +6,39 @@ import { IconMicroscope } from "@tabler/icons-react";
|
|||||||
import { NewExperimentModal } from "Modals/NewExperiment/NewExperiment";
|
import { NewExperimentModal } from "Modals/NewExperiment/NewExperiment";
|
||||||
import ExperimentsListCard from "Components/ListCard/ExperimentsListCard";
|
import ExperimentsListCard from "Components/ListCard/ExperimentsListCard";
|
||||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||||
import type { Experiment } from "Types/Experiment/Experiment";
|
|
||||||
import CustomButton from "Components/CustomButton/CustomButton";
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
import { Alert } from "@mantine/core";
|
import { Alert } from "@mantine/core";
|
||||||
import { getShortTeamsList } from "Api/QuantumBackend/TeamManagement";
|
import { getShortTeamsList } from "Api/QuantumBackend/TeamManagement";
|
||||||
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
|
import {
|
||||||
|
type ExperimentData,
|
||||||
|
type ExperimentTypeList,
|
||||||
|
} from "Types/Experiment/Experiment";
|
||||||
|
import {
|
||||||
|
getExperimentTypes,
|
||||||
|
getUserExperiments,
|
||||||
|
} from "Api/QuantumBackend/ExperimentsManagment";
|
||||||
|
|
||||||
function ExperimentsPage() {
|
function ExperimentsPage() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { experiments } = useExperimentStore();
|
const { experiments, setExperiments, setInstances } = useExperimentStore();
|
||||||
const [teams, setTeams] =
|
const [teams, setTeams] =
|
||||||
useState<{ team_id: number; team_name: string }[]>();
|
useState<{ team_id: number; team_name: string }[]>();
|
||||||
|
|
||||||
const { profile, is_loading } = useAuthenticationStore();
|
const { profile, is_loading } = useAuthenticationStore();
|
||||||
|
const [is_loading_, set_is_loading] = useState(true);
|
||||||
|
const [exp_types, set_exp_types] = useState<ExperimentTypeList[]>([]);
|
||||||
|
const [total_experiments, set_total_experiments] = useState<number>(0);
|
||||||
|
const [page_size, set_page_size] = useState<number>(6);
|
||||||
|
const [cur_page, set_cur_page] = useState<number>(1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (profile || !is_loading)
|
if (profile && !is_loading) {
|
||||||
getShortTeamsList()
|
setInstances([]);
|
||||||
|
getExperimentTypes().then((exp_types) => {
|
||||||
|
set_exp_types(exp_types);
|
||||||
|
});
|
||||||
|
getShortTeamsList({})
|
||||||
.then((teams) => {
|
.then((teams) => {
|
||||||
if (teams) {
|
if (teams) {
|
||||||
setTeams(teams);
|
setTeams(teams);
|
||||||
@@ -31,6 +47,18 @@ function ExperimentsPage() {
|
|||||||
.catch(() => {
|
.catch(() => {
|
||||||
setTeams([]);
|
setTeams([]);
|
||||||
});
|
});
|
||||||
|
getUserExperiments({ page_num: cur_page, page_size: page_size })
|
||||||
|
.then((expData) => {
|
||||||
|
setExperiments(expData.experiments);
|
||||||
|
set_total_experiments(expData.total_experiments);
|
||||||
|
set_page_size(expData.page_size);
|
||||||
|
set_cur_page(expData.cur_page);
|
||||||
|
set_is_loading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
set_is_loading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
}, [profile, is_loading]);
|
}, [profile, is_loading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,17 +74,11 @@ function ExperimentsPage() {
|
|||||||
isOpened={isOpen}
|
isOpened={isOpen}
|
||||||
setIsOpened={setIsOpen}
|
setIsOpened={setIsOpen}
|
||||||
teams={teams}
|
teams={teams}
|
||||||
|
types={exp_types.map((type) => {
|
||||||
|
return { type_id: type.id, type_name: type.name };
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
<div className="ExperimentsPage">
|
<div className="ExperimentsPage">
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "10px",
|
|
||||||
marginBottom: "20px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{teams && teams.length > 0 && (
|
|
||||||
<div className="experimentsButtons">
|
<div className="experimentsButtons">
|
||||||
<CustomButton
|
<CustomButton
|
||||||
color="contrast"
|
color="contrast"
|
||||||
@@ -67,30 +89,27 @@ function ExperimentsPage() {
|
|||||||
text="Создать эксперимент"
|
text="Создать эксперимент"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="ExperimentsPage">
|
||||||
</div>
|
|
||||||
<PaginationContainer
|
<PaginationContainer
|
||||||
numberOfPages={1}
|
numberOfPages={Math.ceil(total_experiments / page_size)}
|
||||||
isLoading={teams == undefined}
|
isLoading={teams == undefined}
|
||||||
activePage={1}
|
activePage={cur_page}
|
||||||
setPage={() => {}}
|
setPage={(page_num) => {
|
||||||
>
|
getUserExperiments({ page_num: page_num, page_size: page_size })
|
||||||
{experiments.map((exp: Experiment) => {
|
.then((expData) => {
|
||||||
return (
|
setExperiments(expData.experiments);
|
||||||
<ExperimentsListCard
|
set_total_experiments(expData.total_experiments);
|
||||||
experiment={{
|
set_page_size(expData.page_size);
|
||||||
id: exp.id,
|
set_cur_page(expData.cur_page);
|
||||||
name: exp.name,
|
set_is_loading(false);
|
||||||
description: exp.description,
|
})
|
||||||
team_id: exp.team_id,
|
.catch(() => {
|
||||||
tasks_ids: exp.tasks_ids,
|
set_is_loading(false);
|
||||||
date_created: exp.date_created,
|
});
|
||||||
experiment_status: exp.experiment_status,
|
|
||||||
experiment_type: "A",
|
|
||||||
}}
|
}}
|
||||||
team={teams?.find((team) => team.team_id == exp.team_id)}
|
>
|
||||||
/>
|
{experiments.map((exp: ExperimentData) => {
|
||||||
);
|
return <ExperimentsListCard experiment={exp} />;
|
||||||
})}
|
})}
|
||||||
{teams && teams.length == 0 && (
|
{teams && teams.length == 0 && (
|
||||||
<Alert title="Команды не найдены" color="red">
|
<Alert title="Команды не найдены" color="red">
|
||||||
@@ -98,13 +117,17 @@ function ExperimentsPage() {
|
|||||||
экспериментами
|
экспериментами
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{teams && teams.length != 0 && experiments.length == 0 && (
|
{teams &&
|
||||||
|
teams.length != 0 &&
|
||||||
|
experiments.length == 0 &&
|
||||||
|
!is_loading_ && (
|
||||||
<Alert title="Экспериментов нету" color="blue">
|
<Alert title="Экспериментов нету" color="blue">
|
||||||
Вы еще не создали не один эксперимент
|
Вы еще не создали не один эксперимент
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</PaginationContainer>
|
</PaginationContainer>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/Pages/TaskPage/TaskPage.css
Normal file
13
src/Pages/TaskPage/TaskPage.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.taskButtons {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: right;
|
||||||
|
gap: 15px;
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
* {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,100 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { IconCancel, IconPlus, IconSettings } from "@tabler/icons-react";
|
import { IconCancel, IconPlus } from "@tabler/icons-react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import CustomButton from "Components/CustomButton/CustomButton";
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
import { IframePlugin } from "Api/PluginLoader/PluginLoader";
|
import { IframePlugin } from "Api/PluginLoader/PluginLoader";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import type { InstanceData } from "Types/Experiment/Experiment";
|
||||||
|
import {
|
||||||
|
getFrontendFile,
|
||||||
|
getInstanceById,
|
||||||
|
updateInstance,
|
||||||
|
} from "Api/QuantumBackend/ExperimentsManagment";
|
||||||
|
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
|
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||||
|
import { SimpleGrid, TextInput, Title } from "@mantine/core";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import "./TaskPage.css";
|
||||||
|
|
||||||
function TaskPage() {
|
function TaskPage() {
|
||||||
const { task_id } = useParams();
|
const { task_id } = useParams();
|
||||||
const [data, setData] = useState<{ text: string }>({ text: "" });
|
|
||||||
|
const [instance, set_instance] = useState<InstanceData>();
|
||||||
|
const { loadedHtmlFiles, addLoadedHtml, setInstances } = useExperimentStore();
|
||||||
|
const { profile, is_loading } = useAuthenticationStore();
|
||||||
|
const [, set_is_loading] = useState(true);
|
||||||
|
const [data, setData] = useState<string>();
|
||||||
|
const [progress, setProgress] = useState<string>();
|
||||||
|
const [qubits_needed, set_qubits_needed] = useState<number>();
|
||||||
|
|
||||||
|
const [name, setName] = useState<string>();
|
||||||
|
const [descr, setDescr] = useState<string>("");
|
||||||
|
const [reload, setReload] = useState<number>(0);
|
||||||
|
|
||||||
|
const saveData = () => {
|
||||||
|
if (instance && qubits_needed) {
|
||||||
|
updateInstance({
|
||||||
|
instance_id: instance.instance_id,
|
||||||
|
name: name,
|
||||||
|
description: descr,
|
||||||
|
instance_data: JSON.stringify(data),
|
||||||
|
qubits_needed: qubits_needed,
|
||||||
|
}).then((updated) => {
|
||||||
|
set_instance({
|
||||||
|
instance_id: instance.instance_id,
|
||||||
|
name: updated.name,
|
||||||
|
description: updated.description,
|
||||||
|
instance_data: updated.instance_data,
|
||||||
|
qubits_needed: updated.qubits_needed,
|
||||||
|
simulation_result: instance.simulation_result,
|
||||||
|
});
|
||||||
|
setName(updated.name);
|
||||||
|
setDescr(updated.description || "");
|
||||||
|
setData(updated.instance_data);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!qubits_needed) {
|
||||||
|
notifications.show({
|
||||||
|
message: "Количество кубит не было определено",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResetData = () => {
|
||||||
|
if (instance) {
|
||||||
|
setData(instance.instance_data);
|
||||||
|
setName(instance.name);
|
||||||
|
setDescr(instance.description || "");
|
||||||
|
setReload(reload + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (profile && !is_loading && task_id) {
|
||||||
|
getInstanceById(Number(task_id))
|
||||||
|
.then((inst) => {
|
||||||
|
set_instance(inst);
|
||||||
|
setData(inst.instance_data);
|
||||||
|
setProgress(inst.simulation_result?.simulation_result);
|
||||||
|
setName(inst.name);
|
||||||
|
setDescr(inst.description || "");
|
||||||
|
set_qubits_needed(inst.qubits_needed);
|
||||||
|
getFrontendFile(1).then((file) => {
|
||||||
|
addLoadedHtml(1, file);
|
||||||
|
});
|
||||||
|
set_is_loading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
set_is_loading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setInstances([]);
|
||||||
|
};
|
||||||
|
}, [profile, is_loading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -29,41 +116,79 @@ function TaskPage() {
|
|||||||
marginBottom: "15px",
|
marginBottom: "15px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="experimentButtons">
|
{(!instance?.simulation_result ||
|
||||||
|
instance?.simulation_result?.status == "DRAFT") && (
|
||||||
|
<>
|
||||||
|
<div className="taskButtons">
|
||||||
<CustomButton
|
<CustomButton
|
||||||
color="accent"
|
color="accent"
|
||||||
onClick={() => {}}
|
onClick={saveData}
|
||||||
|
disabled={
|
||||||
|
JSON.stringify(data) ==
|
||||||
|
JSON.stringify(instance?.instance_data) &&
|
||||||
|
instance?.name == name &&
|
||||||
|
(instance?.description || "") == (descr || "")
|
||||||
|
}
|
||||||
icon={<IconPlus />}
|
icon={<IconPlus />}
|
||||||
text="Сохранить"
|
text="Сохранить"
|
||||||
/>
|
/>
|
||||||
<CustomButton
|
<CustomButton
|
||||||
color="red"
|
color="red"
|
||||||
style="outline"
|
style="outline"
|
||||||
onClick={() => {}}
|
onClick={ResetData}
|
||||||
icon={<IconCancel />}
|
icon={<IconCancel />}
|
||||||
text="Отменить"
|
text="Отменить"
|
||||||
/>
|
disabled={
|
||||||
</div>
|
JSON.stringify(data) ==
|
||||||
</div>
|
JSON.stringify(instance?.instance_data) &&
|
||||||
<IframePlugin
|
instance?.name == name &&
|
||||||
pluginUrl={
|
(instance?.description || "") == (descr || "")
|
||||||
window.location.origin +
|
|
||||||
"/" +
|
|
||||||
import.meta.env.VITE_BASE_PATH +
|
|
||||||
"/" +
|
|
||||||
"index.html"
|
|
||||||
}
|
}
|
||||||
mode="editor"
|
|
||||||
taskData={{
|
|
||||||
id: Number(task_id),
|
|
||||||
name: "Молекула 1",
|
|
||||||
description: "",
|
|
||||||
data: data,
|
|
||||||
}}
|
|
||||||
onUpdate={setData}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<SimpleGrid verticalSpacing="lg" cols={2}>
|
||||||
|
<Title size="lg">Имя:</Title>
|
||||||
|
<TextInput
|
||||||
|
size="md"
|
||||||
|
value={name}
|
||||||
|
onChange={
|
||||||
|
(e) => {
|
||||||
|
setName(e.target.value);
|
||||||
|
} //set_username(e.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Title size="lg">Описание:</Title>
|
||||||
|
<TextInput
|
||||||
|
size="md"
|
||||||
|
value={descr}
|
||||||
|
onChange={
|
||||||
|
(e) => {
|
||||||
|
setDescr(e.target.value);
|
||||||
|
} //set_email(e.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{instance && (
|
||||||
|
<IframePlugin
|
||||||
|
plugin={loadedHtmlFiles.get(1) || ""}
|
||||||
|
mode="Editor"
|
||||||
|
taskData={JSON.stringify(data)}
|
||||||
|
qubits_needed={qubits_needed || 0}
|
||||||
|
onUpdate={(data, qubits_need) => {
|
||||||
|
setData(JSON.parse(data));
|
||||||
|
set_qubits_needed(qubits_need);
|
||||||
|
}}
|
||||||
|
index={instance.instance_id + reload}
|
||||||
|
simProgress={JSON.stringify(progress)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ function TeamPage() {
|
|||||||
addTeam(team);
|
addTeam(team);
|
||||||
set_cur_team(team);
|
set_cur_team(team);
|
||||||
set_team_name(team.name);
|
set_team_name(team.name);
|
||||||
set_team_descr(team.description);
|
set_team_descr(team.description || "");
|
||||||
set_is_loading(false);
|
set_is_loading(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -210,8 +210,8 @@ function TeamPage() {
|
|||||||
cur_team?.description == team_descr
|
cur_team?.description == team_descr
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
set_team_name(cur_team?.name);
|
set_team_name(cur_team?.name || "");
|
||||||
set_team_descr(cur_team?.description);
|
set_team_descr(cur_team?.description || "");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
@@ -252,7 +252,7 @@ function TeamPage() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!cur_team && !is_loading && (
|
{!cur_team && !is_loading_this && (
|
||||||
<Alert color="red">
|
<Alert color="red">
|
||||||
<Center>
|
<Center>
|
||||||
{" "}
|
{" "}
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
Space,
|
Space,
|
||||||
|
Avatar,
|
||||||
|
Center,
|
||||||
|
Grid,
|
||||||
|
FileInput,
|
||||||
|
Group,
|
||||||
|
rem,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconChartCandle,
|
IconChartCandle,
|
||||||
@@ -15,6 +21,7 @@ import {
|
|||||||
IconCancel,
|
IconCancel,
|
||||||
IconSun,
|
IconSun,
|
||||||
IconMoonStars,
|
IconMoonStars,
|
||||||
|
IconUpload,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import keycloak, {
|
import keycloak, {
|
||||||
SendEmailVerification,
|
SendEmailVerification,
|
||||||
@@ -29,19 +36,22 @@ import { useUserPreferencesStore } from "Stores/PreferencesStore";
|
|||||||
import CustomButton from "Components/CustomButton/CustomButton";
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { useSearchParams } from "react-router";
|
import { useSearchParams } from "react-router";
|
||||||
import {
|
import { UpdateCurrentUserInfo } from "Api/QuantumBackend/UserManagement";
|
||||||
GetCurrentUserInfo,
|
|
||||||
UpdateCurrentUserInfo,
|
|
||||||
} from "Api/QuantumBackend/UserManagement";
|
|
||||||
import type { UserData } from "Types/User/User";
|
|
||||||
function UserPage() {
|
function UserPage() {
|
||||||
const { profile, is_loading, profile_picture_path } =
|
const {
|
||||||
useAuthenticationStore();
|
profile,
|
||||||
|
is_loading,
|
||||||
|
profile_picture_path,
|
||||||
|
set_profile_picture_path,
|
||||||
|
} = useAuthenticationStore();
|
||||||
const { theme, set_theme } = useUserPreferencesStore();
|
const { theme, set_theme } = useUserPreferencesStore();
|
||||||
const [username, set_username] = useState<string>("");
|
const [username, set_username] = useState<string>("");
|
||||||
const [email, set_email] = useState<string>("");
|
const [email, set_email] = useState<string>("");
|
||||||
const [pfp_path, set_pfp_path] = useState<string>("");
|
|
||||||
const [is_editing_path, set_is_editing_path] = useState<boolean>(false);
|
const [is_editing_path, set_is_editing_path] = useState<boolean>(false);
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const updateData = () => {
|
const updateData = () => {
|
||||||
@@ -53,9 +63,7 @@ function UserPage() {
|
|||||||
const prof_1 = profile;
|
const prof_1 = profile;
|
||||||
prof_1.email = email;
|
prof_1.email = email;
|
||||||
prof_1.username = username;
|
prof_1.username = username;
|
||||||
useAuthenticationStore.setState({
|
|
||||||
is_loading: true,
|
|
||||||
});
|
|
||||||
updateUserData(prof_1).then((data) => {
|
updateUserData(prof_1).then((data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
@@ -76,15 +84,81 @@ function UserPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (file: File | null) => {
|
||||||
|
setSelectedFile(file);
|
||||||
|
if (file) {
|
||||||
|
// Create preview
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
setPreviewUrl(url);
|
||||||
|
} else {
|
||||||
|
if (previewUrl) {
|
||||||
|
URL.revokeObjectURL(previewUrl);
|
||||||
|
setPreviewUrl(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const update_pfp = async () => {
|
const update_pfp = async () => {
|
||||||
UpdateCurrentUserInfo(pfp_path).then(() => {
|
if (!selectedFile) return;
|
||||||
GetCurrentUserInfo().then((info: UserData | undefined) => {
|
|
||||||
if (info && info.profile_picture_path)
|
setIsUploading(true);
|
||||||
useAuthenticationStore.setState({
|
try {
|
||||||
profile_picture_path: info.profile_picture_path,
|
const result = await UpdateCurrentUserInfo(selectedFile);
|
||||||
});
|
if (result && result.profile_picture_path) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_QUANTUM_BACKEND_URL}/user/serve/${profile?.id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${keycloak.token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
set_profile_picture_path(url);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load avatar:", error);
|
||||||
|
}
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
title: "Фотография профиля обновлена",
|
||||||
|
message: "",
|
||||||
|
icon: <IconCheck />,
|
||||||
|
color: "green",
|
||||||
});
|
});
|
||||||
|
set_is_editing_path(false);
|
||||||
|
// Clean up
|
||||||
|
if (previewUrl) {
|
||||||
|
URL.revokeObjectURL(previewUrl);
|
||||||
|
setPreviewUrl(null);
|
||||||
|
}
|
||||||
|
setSelectedFile(null);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
title: "Ошибка",
|
||||||
|
message: "Не удалось обновить фотографию профиля",
|
||||||
|
icon: <IconCancel />,
|
||||||
|
color: "red",
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false);
|
||||||
|
set_is_editing_path(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelUpload = () => {
|
||||||
|
set_is_editing_path(false);
|
||||||
|
setSelectedFile(null);
|
||||||
|
if (previewUrl) {
|
||||||
|
URL.revokeObjectURL(previewUrl);
|
||||||
|
setPreviewUrl(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -103,10 +177,6 @@ function UserPage() {
|
|||||||
}
|
}
|
||||||
}, [profile, searchParams]);
|
}, [profile, searchParams]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (profile_picture_path) set_pfp_path(profile_picture_path);
|
|
||||||
}, [profile_picture_path]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -153,20 +223,70 @@ function UserPage() {
|
|||||||
overlayProps={{ radius: "sm", blur: 2 }}
|
overlayProps={{ radius: "sm", blur: 2 }}
|
||||||
loaderProps={{ size: 50, type: "dots" }}
|
loaderProps={{ size: 50, type: "dots" }}
|
||||||
/>
|
/>
|
||||||
<SimpleGrid verticalSpacing="lg" cols={2}>
|
<Grid>
|
||||||
|
<Grid.Col span={4} style={{ height: "100%" }}>
|
||||||
|
<Center>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
<Title size="xl">Фотография профиля</Title>
|
||||||
|
</div>
|
||||||
|
</Center>
|
||||||
|
<Center style={{ padding: "15px" }}>
|
||||||
|
<Avatar size="xl" src={profile_picture_path} radius="xl" />
|
||||||
|
{is_editing_path && (
|
||||||
|
<FileInput
|
||||||
|
placeholder="Выберите изображение"
|
||||||
|
accept="image/png,image/jpeg,image/jpg,image/webp"
|
||||||
|
value={selectedFile}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
leftSection={<IconUpload size={rem(14)} />}
|
||||||
|
clearable
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
disabled={isUploading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Center>
|
||||||
|
<div>
|
||||||
|
{!is_editing_path ? (
|
||||||
|
<CustomButton
|
||||||
|
style="outline"
|
||||||
|
color="contrast"
|
||||||
|
text="Изменить"
|
||||||
|
onClick={() => {
|
||||||
|
set_is_editing_path(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Group justify="center" mt="md">
|
||||||
|
<CustomButton
|
||||||
|
style="outline"
|
||||||
|
color="red"
|
||||||
|
text="Отменить"
|
||||||
|
onClick={cancelUpload}
|
||||||
|
disabled={isUploading}
|
||||||
|
/>
|
||||||
|
<CustomButton
|
||||||
|
style="color"
|
||||||
|
color="accent"
|
||||||
|
text="Сохранить"
|
||||||
|
onClick={update_pfp}
|
||||||
|
disabled={!selectedFile}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col
|
||||||
|
span={4}
|
||||||
|
style={{
|
||||||
|
flexGrow: 1,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Title size="lg">Имя пользователя:</Title>
|
<Title size="lg">Имя пользователя:</Title>
|
||||||
<TextInput
|
|
||||||
size="md"
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => set_username(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<Title size="lg">Почта:</Title>
|
|
||||||
<TextInput
|
|
||||||
size="md"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => set_email(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
<Title size="lg">Почта:</Title>
|
||||||
<CustomButton
|
<CustomButton
|
||||||
color="accent"
|
color="accent"
|
||||||
text="Сохранить"
|
text="Сохранить"
|
||||||
@@ -176,10 +296,32 @@ function UserPage() {
|
|||||||
profile != null &&
|
profile != null &&
|
||||||
((profile.username != undefined &&
|
((profile.username != undefined &&
|
||||||
profile.username != username) ||
|
profile.username != username) ||
|
||||||
(profile.email != undefined && profile.email != email))
|
(profile.email != undefined &&
|
||||||
|
profile.email != email))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col
|
||||||
|
span={4}
|
||||||
|
style={{
|
||||||
|
flexGrow: 1,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
size="md"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => set_username(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
size="md"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => set_email(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
<CustomButton
|
<CustomButton
|
||||||
color="error"
|
color="error"
|
||||||
text="Отменить"
|
text="Отменить"
|
||||||
@@ -194,11 +336,13 @@ function UserPage() {
|
|||||||
profile != null &&
|
profile != null &&
|
||||||
((profile.username != undefined &&
|
((profile.username != undefined &&
|
||||||
profile.username != username) ||
|
profile.username != username) ||
|
||||||
(profile.email != undefined && profile.email != email))
|
(profile.email != undefined &&
|
||||||
|
profile.email != email))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
<Divider my="lg" />
|
<Divider my="lg" />
|
||||||
<SimpleGrid verticalSpacing={"lg"} cols={2}>
|
<SimpleGrid verticalSpacing={"lg"} cols={2}>
|
||||||
<Title
|
<Title
|
||||||
@@ -266,60 +410,12 @@ function UserPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<Divider my="lg" />
|
|
||||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
||||||
<Title size="lg">Фотография профиля</Title>
|
|
||||||
|
|
||||||
<Space my="sm" />
|
|
||||||
{!is_editing_path ? (
|
|
||||||
<img height={350} width={350} src={profile_picture_path} />
|
|
||||||
) : (
|
|
||||||
<TextInput
|
|
||||||
size="md"
|
|
||||||
value={pfp_path}
|
|
||||||
onChange={(e) => set_pfp_path(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Space my="sm" />
|
|
||||||
<div style={{ width: "15em" }}>
|
|
||||||
{!is_editing_path ? (
|
|
||||||
<CustomButton
|
|
||||||
style="outline"
|
|
||||||
color="contrast"
|
|
||||||
text="Изменить"
|
|
||||||
onClick={() => {
|
|
||||||
set_is_editing_path(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CustomButton
|
|
||||||
style="outline"
|
|
||||||
color="contrast"
|
|
||||||
text="Сохранить"
|
|
||||||
onClick={() => {
|
|
||||||
update_pfp();
|
|
||||||
set_is_editing_path(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Space my="sm" />
|
|
||||||
<CustomButton
|
|
||||||
style="outline"
|
|
||||||
color="red"
|
|
||||||
text="Отменить"
|
|
||||||
onClick={() => {
|
|
||||||
set_is_editing_path(false);
|
|
||||||
set_pfp_path(profile_picture_path);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|
||||||
<Tabs.Panel value="preference">
|
<Tabs.Panel value="preference">
|
||||||
|
<Title size={"lg"}>Тема приложения:</Title>
|
||||||
|
<Space h="md" />
|
||||||
<Switch
|
<Switch
|
||||||
size="xl"
|
size="xl"
|
||||||
defaultChecked={theme == "light"}
|
defaultChecked={theme == "light"}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Breadcrumbs } from "@mantine/core";
|
import { Breadcrumbs } from "@mantine/core";
|
||||||
import { type ReactElement } from "react";
|
import { type ReactElement } from "react";
|
||||||
import { Link } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import "./Breadcrumbs.css";
|
import "./Breadcrumbs.css";
|
||||||
import { routes } from "Routes/Routes";
|
import { routes } from "Routes/Routes";
|
||||||
import { useLocation } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
@@ -46,6 +46,7 @@ function testEqual(path: string, pattern: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function BreadCrumbs() {
|
function BreadCrumbs() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const unique_matches: string[] = getSubPaths(useLocation().pathname);
|
const unique_matches: string[] = getSubPaths(useLocation().pathname);
|
||||||
|
|
||||||
//find the breadcrumbs for the matched pathes
|
//find the breadcrumbs for the matched pathes
|
||||||
@@ -56,13 +57,15 @@ function BreadCrumbs() {
|
|||||||
for (const i in routes[prop].breadcrumbs(unique_matches[u_match])) {
|
for (const i in routes[prop].breadcrumbs(unique_matches[u_match])) {
|
||||||
if (elements.length + 1 != unique_matches.length) {
|
if (elements.length + 1 != unique_matches.length) {
|
||||||
elements.push(
|
elements.push(
|
||||||
<Link
|
<div
|
||||||
className="invisible_link"
|
className="invisible_link"
|
||||||
to={unique_matches[u_match]}
|
|
||||||
key={unique_matches[u_match]}
|
key={unique_matches[u_match]}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(unique_matches[u_match]);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{routes[prop].breadcrumbs(unique_matches[u_match])[i]}
|
{routes[prop].breadcrumbs(unique_matches[u_match])[i]}
|
||||||
</Link>,
|
</div>,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
elements.push(
|
elements.push(
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const routes: {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
TaskPage: {
|
TaskPage: {
|
||||||
path: "/experiments/:experiment_id/:molecule_id",
|
path: "/experiments/:experiment_id/:task_id",
|
||||||
breadcrumbs: (path: string) => [
|
breadcrumbs: (path: string) => [
|
||||||
<>Молекула #{path.split("/")[path.split("/").length - 1]}</>,
|
<>Молекула #{path.split("/")[path.split("/").length - 1]}</>,
|
||||||
],
|
],
|
||||||
@@ -62,8 +62,7 @@ export const routes: {
|
|||||||
SettingsPage: { path: "/settings", breadcrumbs: () => [<>Настройки</>] },
|
SettingsPage: { path: "/settings", breadcrumbs: () => [<>Настройки</>] },
|
||||||
};
|
};
|
||||||
|
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter([
|
||||||
[
|
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <App />,
|
element: <App />,
|
||||||
@@ -121,7 +120,5 @@ const router = createBrowserRouter(
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
]);
|
||||||
{ basename: import.meta.env.VITE_BASE_PATH },
|
|
||||||
);
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,30 +1,70 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { immer } from "zustand/middleware/immer";
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
|
||||||
import type { Experiment, TaskData } from "Types/Experiment/Experiment";
|
import type {
|
||||||
|
ExperimentData,
|
||||||
|
ExperimentTypeList,
|
||||||
|
InstanceData,
|
||||||
|
} from "Types/Experiment/Experiment";
|
||||||
|
import { enableMapSet } from "immer";
|
||||||
|
|
||||||
|
// Call this once at your app's entry point (before using Immer)
|
||||||
|
enableMapSet();
|
||||||
|
|
||||||
interface ExperimentStoreState {
|
interface ExperimentStoreState {
|
||||||
experiments: Experiment[];
|
experiments: ExperimentData[];
|
||||||
tasks: TaskData[];
|
instances: InstanceData[];
|
||||||
|
experimentTypes: ExperimentTypeList[] | null;
|
||||||
|
loadedHtmlFiles: Map<number, string>; // experiment_type_id -> HTML content
|
||||||
|
|
||||||
addExperiment: (experiment: Experiment) => void;
|
setInstances: (instances: InstanceData[]) => void;
|
||||||
updateExperiment: (id: number, data: Partial<Experiment>) => void;
|
updateInstance: (id: number, data: Partial<InstanceData>) => void;
|
||||||
|
removeInstance: (id: number) => void;
|
||||||
|
|
||||||
|
setExperiments: (experiment: ExperimentData[]) => void;
|
||||||
|
addExperiment: (experiment: ExperimentData) => void;
|
||||||
|
updateExperiment: (id: number, data: Partial<ExperimentData>) => void;
|
||||||
removeExperiment: (id: number) => void;
|
removeExperiment: (id: number) => void;
|
||||||
|
|
||||||
addTask: (experimentId: number, task: TaskData) => void;
|
// Only what you asked for:
|
||||||
updateTask: (taskId: number, data: Partial<TaskData>) => void;
|
setExperimentTypes: (types: ExperimentTypeList[]) => void;
|
||||||
removeTask: (experimentId: number, taskId: number) => void;
|
addLoadedHtml: (typeId: number, htmlContent: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useExperimentStore = create<ExperimentStoreState>()(
|
export const useExperimentStore = create<ExperimentStoreState>()(
|
||||||
immer((set) => ({
|
immer((set) => ({
|
||||||
experiments: [],
|
experiments: [],
|
||||||
tasks: [],
|
|
||||||
teams: [],
|
teams: [],
|
||||||
|
instances: [],
|
||||||
|
experimentTypes: null,
|
||||||
|
loadedHtmlFiles: new Map(),
|
||||||
|
|
||||||
|
setExperiments: (experiments) =>
|
||||||
|
set((state) => {
|
||||||
|
state.experiments = experiments;
|
||||||
|
}),
|
||||||
|
|
||||||
addExperiment: (experiment) =>
|
addExperiment: (experiment) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.experiments.push(experiment);
|
state.experiments = [experiment, ...state.experiments];
|
||||||
|
}),
|
||||||
|
|
||||||
|
setInstances: (instances) =>
|
||||||
|
set((state) => {
|
||||||
|
state.instances = instances;
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateInstance: (id, data) =>
|
||||||
|
set((state) => {
|
||||||
|
const exp = state.instances.find((e) => e.instance_id === id);
|
||||||
|
if (!exp) return;
|
||||||
|
|
||||||
|
Object.assign(exp, data);
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeInstance: (id) =>
|
||||||
|
set((state) => {
|
||||||
|
state.instances = state.instances.filter((e) => e.instance_id !== id);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateExperiment: (id, data) =>
|
updateExperiment: (id, data) =>
|
||||||
@@ -39,31 +79,15 @@ export const useExperimentStore = create<ExperimentStoreState>()(
|
|||||||
set((state) => {
|
set((state) => {
|
||||||
state.experiments = state.experiments.filter((e) => e.id !== id);
|
state.experiments = state.experiments.filter((e) => e.id !== id);
|
||||||
}),
|
}),
|
||||||
|
// Only these two new methods
|
||||||
addTask: (experimentId, task) =>
|
setExperimentTypes: (types) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const exp = state.experiments.find((e) => e.id === experimentId);
|
state.experimentTypes = types;
|
||||||
if (!exp) return;
|
|
||||||
|
|
||||||
exp.tasks_ids.push(task.id);
|
|
||||||
state.tasks.push(task);
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateTask: (taskId, data) =>
|
addLoadedHtml: (typeId, htmlContent) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const task = state.tasks.find((t) => t.id === taskId);
|
state.loadedHtmlFiles.set(typeId, htmlContent);
|
||||||
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);
|
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface ConvertSchema {
|
|
||||||
inputText: string;
|
|
||||||
inputFormat: string;
|
|
||||||
add_h: boolean;
|
|
||||||
make_3d: boolean;
|
|
||||||
optimize: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,101 @@
|
|||||||
export type ExperimentStatus =
|
export interface ExperimentTypeList {
|
||||||
| "DRAFT"
|
|
||||||
| "QUEUE"
|
|
||||||
| "PROCESSING"
|
|
||||||
| "SUCCESS"
|
|
||||||
| "ERROR";
|
|
||||||
|
|
||||||
export interface Experiment {
|
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateExperimentTypeResponse {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateExperimentRequest {
|
||||||
team_id: number;
|
team_id: number;
|
||||||
date_created: Date;
|
experiment_type_id: number;
|
||||||
experiment_status: ExperimentStatus;
|
|
||||||
experiment_type: string;
|
|
||||||
tasks_ids: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export interface TaskTypePlugin<TData = any> {
|
|
||||||
type: string;
|
|
||||||
// how it appears in the experiment task list
|
|
||||||
ListItem: React.ComponentType<TaskData<TData>>;
|
|
||||||
// full editor UI when clicking task
|
|
||||||
Editor: React.ComponentType<TaskEditorProps<TData>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export interface TaskData<TData = any> {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description?: string;
|
||||||
data: TData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
export interface UpdateExperimentRequest {
|
||||||
export interface TaskEditorProps<TData = any> {
|
experiment_id: number;
|
||||||
data: TData;
|
name?: string;
|
||||||
setData: (data: TData) => void;
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExperimentData {
|
||||||
|
id: number;
|
||||||
|
team: {
|
||||||
|
team_id: number;
|
||||||
|
team_name: string;
|
||||||
|
};
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
created_at: string;
|
||||||
|
experiment_type: ExperimentTypeList;
|
||||||
|
instances_count: number;
|
||||||
|
instance_preview: SimpleInstanceData[];
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExperimentListResponse {
|
||||||
|
experiments: ExperimentData[];
|
||||||
|
cur_page: number;
|
||||||
|
total_experiments: number;
|
||||||
|
page_size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateInstanceRequest {
|
||||||
|
experiment_id: number;
|
||||||
|
instance_data: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateInstanceRequest {
|
||||||
|
instance_id: number;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
instance_data?: string;
|
||||||
|
qubits_needed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SimpleInstanceData {
|
||||||
|
id: number;
|
||||||
|
instance_data: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
qubits_needed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SimulationResultData {
|
||||||
|
id: number;
|
||||||
|
comp_system: {
|
||||||
|
system_id: number;
|
||||||
|
system_name: string;
|
||||||
|
};
|
||||||
|
simulation_result: string;
|
||||||
|
status: string;
|
||||||
|
started_at?: string;
|
||||||
|
ended_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceData {
|
||||||
|
instance_id: number;
|
||||||
|
instance_data: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
simulation_result?: SimulationResultData;
|
||||||
|
qubits_needed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceListResponse {
|
||||||
|
instances: InstanceData[];
|
||||||
|
cur_page: number;
|
||||||
|
total_instances: number;
|
||||||
|
page_size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StartExperimentRequest {
|
||||||
|
experiment_id: number;
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/main.tsx
16
src/main.tsx
@@ -15,7 +15,7 @@ import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
|||||||
import { useUserPreferencesStore } from "Stores/PreferencesStore";
|
import { useUserPreferencesStore } from "Stores/PreferencesStore";
|
||||||
import type { KeycloakProfile } from "keycloak-js";
|
import type { KeycloakProfile } from "keycloak-js";
|
||||||
import { GetCurrentUserInfo } from "Api/QuantumBackend/UserManagement";
|
import { GetCurrentUserInfo } from "Api/QuantumBackend/UserManagement";
|
||||||
import type { UserData } from "Types/User/User";
|
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { IconForbid } from "@tabler/icons-react";
|
import { IconForbid } from "@tabler/icons-react";
|
||||||
|
|
||||||
@@ -71,11 +71,8 @@ async function bootstrap() {
|
|||||||
onLoad: "check-sso",
|
onLoad: "check-sso",
|
||||||
pkceMethod: "S256",
|
pkceMethod: "S256",
|
||||||
silentCheckSsoRedirectUri:
|
silentCheckSsoRedirectUri:
|
||||||
window.location.origin +
|
window.location.origin + "/silent-check-sso.html",
|
||||||
"/" +
|
silentCheckSsoFallback: true,
|
||||||
import.meta.env.VITE_BASE_PATH +
|
|
||||||
"/silent-check-sso.html",
|
|
||||||
silentCheckSsoFallback: false,
|
|
||||||
})
|
})
|
||||||
.then((authenticated: boolean) => {
|
.then((authenticated: boolean) => {
|
||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
@@ -85,12 +82,7 @@ async function bootstrap() {
|
|||||||
profile: profile,
|
profile: profile,
|
||||||
});
|
});
|
||||||
GetCurrentUserInfo()
|
GetCurrentUserInfo()
|
||||||
.then((info: UserData | undefined) => {
|
.then()
|
||||||
if (info && info.profile_picture_path)
|
|
||||||
useAuthenticationStore.setState({
|
|
||||||
profile_picture_path: info.profile_picture_path,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
radius: "md",
|
radius: "md",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default defineConfig({
|
|||||||
preview: {
|
preview: {
|
||||||
port: Number(process.env.VITE_PORT),
|
port: Number(process.env.VITE_PORT),
|
||||||
},
|
},
|
||||||
base: "/" + process.env.VITE_BASE_PATH,
|
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
|
|||||||
Reference in New Issue
Block a user