Added Team mamagement with backend integration
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
.env
|
.env
|
||||||
|
package-lock.json
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
4390
package-lock.json
generated
4390
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -10,17 +10,17 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^8.3.0",
|
"@mantine/core": "^9.1.1",
|
||||||
"@mantine/hooks": "^8.3.0",
|
"@mantine/hooks": "^9.1.1",
|
||||||
"@mantine/notifications": "^8.3.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": "^18.2.0",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^18.2.0",
|
"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",
|
||||||
|
|||||||
117
src/Api/QuantumBackend/TeamManagement.tsx
Normal file
117
src/Api/QuantumBackend/TeamManagement.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import keycloak from "Api/Keycloak/Keycloak";
|
||||||
|
import axios from "axios";
|
||||||
|
import type { Team, TeamMember } from "Types/Team/Team";
|
||||||
|
|
||||||
|
export interface TeamCreateRequest {
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamUpdateRequest {
|
||||||
|
team_id: number;
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamCreateResponse {
|
||||||
|
team_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamListRequest {
|
||||||
|
page_num: number;
|
||||||
|
page_size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamDeleteRequest {
|
||||||
|
team_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemberAddRequest {
|
||||||
|
team_id: number;
|
||||||
|
user_id: string;
|
||||||
|
permissions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemberDeleteRequest {
|
||||||
|
team_id: number;
|
||||||
|
user_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamsListResponse {
|
||||||
|
teams: Team[];
|
||||||
|
cur_page: number;
|
||||||
|
total_teams: number;
|
||||||
|
page_size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: `${import.meta.env.VITE_QUANTUM_BACKEND_URL}/team`,
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Create team
|
||||||
|
export const createTeam = async (
|
||||||
|
data: TeamCreateRequest,
|
||||||
|
): Promise<TeamCreateResponse> => {
|
||||||
|
const response = await api.post<TeamCreateResponse>("", data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTeamRequest = async (
|
||||||
|
data: TeamUpdateRequest,
|
||||||
|
): Promise<TeamUpdateRequest> => {
|
||||||
|
const response = await api.put<TeamUpdateRequest>("", data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Get teams list (paginated)
|
||||||
|
export const getTeams = async (
|
||||||
|
params: Partial<TeamListRequest>,
|
||||||
|
): Promise<TeamsListResponse> => {
|
||||||
|
const response = await api.get<TeamsListResponse>("", {
|
||||||
|
headers: {},
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getShortTeamsList = async (): Promise<
|
||||||
|
{ team_id: number; team_name: string }[]
|
||||||
|
> => {
|
||||||
|
const response =
|
||||||
|
await api.get<{ team_id: number; team_name: string }[]>("short_list");
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Get single team by ID
|
||||||
|
export const getTeam = async (teamId: number): Promise<Team> => {
|
||||||
|
const response = await api.get<Team>(`${teamId}`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Delete team
|
||||||
|
export const deleteTeam = async (teamId: number): Promise<void> => {
|
||||||
|
await api.delete("", { data: { team_id: teamId } });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addMember = async (
|
||||||
|
data: MemberAddRequest,
|
||||||
|
): Promise<TeamMember> => {
|
||||||
|
const response = await api.put<TeamMember>("user", data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteMember = async (
|
||||||
|
data: MemberDeleteRequest,
|
||||||
|
): Promise<void> => {
|
||||||
|
await api.delete("user", { data: data });
|
||||||
|
};
|
||||||
@@ -28,25 +28,34 @@ export const GetCurrentUserInfo = async (): Promise<UserData | undefined> => {
|
|||||||
export const UpdateCurrentUserInfo = async (
|
export const UpdateCurrentUserInfo = async (
|
||||||
profile_picture_path: string,
|
profile_picture_path: string,
|
||||||
): Promise<UserData | undefined> => {
|
): Promise<UserData | undefined> => {
|
||||||
try {
|
const response = await axios.put(
|
||||||
// Trigger the UPDATE_PASSWORD required action
|
`${import.meta.env.VITE_QUANTUM_BACKEND_URL}/user`,
|
||||||
// This will redirect the user to Keycloak's password change page
|
{ profile_picture_path: profile_picture_path },
|
||||||
const response = await axios.put(
|
{
|
||||||
`${import.meta.env.VITE_QUANTUM_BACKEND_URL}/user`,
|
headers: {
|
||||||
{ profile_picture_path: profile_picture_path },
|
"Content-Type": "application/json",
|
||||||
{
|
Authorization: `Bearer ${keycloak.token}`,
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${keycloak.token}`,
|
|
||||||
},
|
|
||||||
withCredentials: true, // Important: allows credentials in CORS
|
|
||||||
},
|
},
|
||||||
);
|
withCredentials: true, // Important: allows credentials in CORS
|
||||||
if (response.status === 200) {
|
},
|
||||||
return response.data;
|
);
|
||||||
}
|
if (response.status === 200) {
|
||||||
} catch (error) {
|
return response.data;
|
||||||
console.error("Error Getting User Data", error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GetUserByEmail = async (
|
||||||
|
email: string,
|
||||||
|
): Promise<UserData | undefined> => {
|
||||||
|
const response = await axios.get(
|
||||||
|
`${import.meta.env.VITE_QUANTUM_BACKEND_URL}/user/${email}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${keycloak.token}`,
|
||||||
|
},
|
||||||
|
withCredentials: true, // Important: allows credentials in CORS
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|||||||
14
src/App.tsx
14
src/App.tsx
@@ -14,10 +14,9 @@ import { useEffect } from "react";
|
|||||||
import { useMediaQuery } from "@mantine/hooks";
|
import { useMediaQuery } from "@mantine/hooks";
|
||||||
import BreadCrumbs from "Routes/Breadcrumbs/Breadcrumbs";
|
import BreadCrumbs from "Routes/Breadcrumbs/Breadcrumbs";
|
||||||
import keycloak from "Api/Keycloak/Keycloak";
|
import keycloak from "Api/Keycloak/Keycloak";
|
||||||
import { AuthenticationStore } from "Stores/AuthenticationStore";
|
|
||||||
function App() {
|
function App() {
|
||||||
const { is_navbar_open, set_navbar_open } = useLayoutStore();
|
const { is_navbar_open, set_navbar_open } = useLayoutStore();
|
||||||
const { set_token } = AuthenticationStore();
|
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -32,14 +31,9 @@ function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const refreshToken = () => {
|
const refreshToken = () => {
|
||||||
keycloak
|
if (keycloak.authenticated)
|
||||||
.updateToken(60)
|
keycloak.updateToken(60).catch((error) => {
|
||||||
.then((refreshed) => {
|
console.log(error);
|
||||||
if (refreshed) {
|
|
||||||
set_token(keycloak.token);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
keycloak.logout();
|
keycloak.logout();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ function CustomButton({
|
|||||||
onClick = () => {},
|
onClick = () => {},
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: CustomButtonProps) {
|
}: CustomButtonProps) {
|
||||||
console.log(color);
|
|
||||||
return (
|
return (
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
className={
|
className={
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { Link, useLocation } from "react-router";
|
|||||||
import { useState, type ForwardRefExoticComponent } from "react";
|
import { 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 { AuthenticationStore } from "Stores/AuthenticationStore";
|
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
import CustomButton from "Components/CustomButton/CustomButton";
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
|
||||||
interface SubtleLinkButtonProps {
|
interface SubtleLinkButtonProps {
|
||||||
@@ -49,7 +49,7 @@ function SubtleLinkButton(props: SubtleLinkButtonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Sidebar() {
|
function Sidebar() {
|
||||||
const { profile } = AuthenticationStore();
|
const { profile, 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
|
||||||
@@ -120,7 +120,7 @@ function Sidebar() {
|
|||||||
{keycloak.authenticated && (
|
{keycloak.authenticated && (
|
||||||
<>
|
<>
|
||||||
<div className="sidebar_bottom_top">
|
<div className="sidebar_bottom_top">
|
||||||
<Avatar radius="xl" />
|
<Avatar radius="xl" src={profile_picture_path} />
|
||||||
<Stack className="userInfo">
|
<Stack className="userInfo">
|
||||||
<Text size="md">{profile?.username}</Text>
|
<Text size="md">{profile?.username}</Text>
|
||||||
<Text size="sm" c="dimmed" truncate="end">
|
<Text size="sm" c="dimmed" truncate="end">
|
||||||
|
|||||||
@@ -19,7 +19,11 @@
|
|||||||
color: var(--mantine-color-accent-filled);
|
color: var(--mantine-color-accent-filled);
|
||||||
* {
|
* {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,42 +6,38 @@ 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";
|
||||||
|
|
||||||
function ExperimentsListCard(props: Experiment) {
|
function ExperimentsListCard(props: {
|
||||||
|
experiment: Experiment;
|
||||||
|
team: { team_id: number; team_name: string } | undefined;
|
||||||
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { selectExperiment, removeExperiment, tasks, teams } =
|
const { removeExperiment, tasks } = useExperimentStore();
|
||||||
useExperimentStore();
|
|
||||||
|
|
||||||
const team = teams.find((team) => {
|
const experiment_tasks = tasks.filter(
|
||||||
return team.id == props.team_id;
|
(a) => a.id in props.experiment.tasks_ids,
|
||||||
});
|
);
|
||||||
|
|
||||||
const experiment_tasks = tasks.filter((a) => a.id in props.tasks_ids);
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
removeExperiment(props.id);
|
removeExperiment(props.experiment.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="ExperimentsListCard"
|
className="ExperimentsListCard"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log(props.id);
|
navigate(props.experiment.id.toString());
|
||||||
selectExperiment(props.id);
|
|
||||||
navigate(props.id.toString());
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SimpleGrid cols={3}>
|
<SimpleGrid cols={3}>
|
||||||
<div className="ExperimentSectionWithLine">
|
<div className="ExperimentSectionWithLine">
|
||||||
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
|
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
|
||||||
{props.name}
|
Эксперимент: {props.experiment.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text mb="sm" size="md">
|
<Text mb="sm" size="md">
|
||||||
Team: {team ? team.name : "ERROR"}
|
Команда: {props.team?.team_name}
|
||||||
</Text>
|
</Text>
|
||||||
<Pill className="ExperimentPill">
|
<Pill className="ExperimentPill">
|
||||||
<Text mb="sm" size="md">
|
<Text size="md">Статус: {props.experiment.experiment_status}</Text>
|
||||||
Статус: {props.experiment_status}
|
|
||||||
</Text>
|
|
||||||
</Pill>
|
</Pill>
|
||||||
</div>
|
</div>
|
||||||
<div className="ExperimentSectionWithLine">
|
<div className="ExperimentSectionWithLine">
|
||||||
@@ -62,15 +58,7 @@ function ExperimentsListCard(props: Experiment) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{experiment_tasks.length == 0 ? (
|
{experiment_tasks.length == 0 ? (
|
||||||
<Pill
|
<Pill size="md" className="ExperimentPill2">
|
||||||
size="md"
|
|
||||||
className="ExperimentPill2"
|
|
||||||
style={{
|
|
||||||
textWrap: "nowrap",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Нет Задач
|
Нет Задач
|
||||||
</Pill>
|
</Pill>
|
||||||
) : (
|
) : (
|
||||||
@@ -81,8 +69,12 @@ function ExperimentsListCard(props: Experiment) {
|
|||||||
<div className="RightExperimentSection">
|
<div className="RightExperimentSection">
|
||||||
<div className="TopRightContainer">
|
<div className="TopRightContainer">
|
||||||
<div className="dateContainer">
|
<div className="dateContainer">
|
||||||
<Text size="sm">{props.date_created.toLocaleDateString()} </Text>
|
<Text size="sm">
|
||||||
<Text size="sm">{props.date_created.toLocaleTimeString()}</Text>
|
{props.experiment.date_created.toLocaleDateString("ru")}{" "}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm">
|
||||||
|
{props.experiment.date_created.toLocaleTimeString("ru")}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
onClick={(e: MouseEvent) => {
|
onClick={(e: MouseEvent) => {
|
||||||
@@ -100,16 +92,16 @@ function ExperimentsListCard(props: Experiment) {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "right",
|
justifyContent: "right",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text>
|
<Text> Тип эксперимента: </Text>
|
||||||
{" "}
|
<Pill className="ExperimentPill">
|
||||||
Тип эксперимента:{" "}
|
{props.experiment.experiment_type}
|
||||||
<Pill className="ExperimentPill2">{props.experiment_type}</Pill>
|
</Pill>
|
||||||
</Text>
|
|
||||||
</div>
|
</div>
|
||||||
<Text color="secondary" size="sm">
|
<Text c="dimmed" size="sm">
|
||||||
#{props.id}
|
#{props.experiment.id}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.TeamMembers {
|
||||||
|
margin-top: 10px;
|
||||||
|
gap: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Card,
|
||||||
|
MultiSelect,
|
||||||
|
Pill,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
UnstyledButton,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
IconCheck,
|
||||||
|
IconCrown,
|
||||||
|
IconForbid,
|
||||||
|
IconPencil,
|
||||||
|
IconTrash,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
import type { Team, TeamMember } from "Types/Team/Team";
|
||||||
|
import "./TeamMemberCard.css";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { addMember, deleteMember } from "Api/QuantumBackend/TeamManagement";
|
||||||
|
import { useTeamStore } from "Stores/TeamsStore";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
|
function TeamMemberCard(props: { cur_team: Team; member: TeamMember }) {
|
||||||
|
const { updateTeam } = useTeamStore();
|
||||||
|
const [is_editing, set_is_editing] = useState<boolean>(false);
|
||||||
|
const [selected_permissions, set_selected_permissions] = useState<string[]>(
|
||||||
|
props.member.permissions,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card padding="sm" withBorder orientation="horizontal">
|
||||||
|
<Card.Section
|
||||||
|
inheritPadding
|
||||||
|
px="xs"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent:
|
||||||
|
props.member.user.keycloak_id !=
|
||||||
|
props.cur_team.creator.user.keycloak_id
|
||||||
|
? "center"
|
||||||
|
: "",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.member.user.keycloak_id ==
|
||||||
|
props.cur_team.creator.user.keycloak_id && <IconCrown />}
|
||||||
|
<Avatar radius="xl" src={props.member.user.profile_picture_path} />
|
||||||
|
</Card.Section>
|
||||||
|
<Card.Section
|
||||||
|
withBorder
|
||||||
|
inheritPadding
|
||||||
|
px="xs"
|
||||||
|
style={{ width: "250px" }}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Text size="xl">{props.member.user.username}</Text>
|
||||||
|
<Text size="md">{props.member.user.email}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Card.Section>
|
||||||
|
|
||||||
|
<Card.Section
|
||||||
|
inheritPadding
|
||||||
|
px="md"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexGrow: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<div style={{ display: "flex", gap: "20px" }}>
|
||||||
|
<Text size="md">Разрешения: </Text>
|
||||||
|
|
||||||
|
{props.member.user.keycloak_id !=
|
||||||
|
props.cur_team.creator.user.keycloak_id &&
|
||||||
|
(is_editing ? (
|
||||||
|
<div style={{ display: "flex", gap: "10px" }}>
|
||||||
|
<CustomButton
|
||||||
|
style="outline"
|
||||||
|
text="Сохранить"
|
||||||
|
textSize="sm"
|
||||||
|
icon={<IconPencil size={15} />}
|
||||||
|
onClick={() => {
|
||||||
|
addMember({
|
||||||
|
team_id: props.cur_team.id,
|
||||||
|
user_id: props.member.user.keycloak_id,
|
||||||
|
permissions: selected_permissions,
|
||||||
|
})
|
||||||
|
.then((member) => {
|
||||||
|
if (member) {
|
||||||
|
updateTeam(props.cur_team.id, {
|
||||||
|
members: [
|
||||||
|
...props.cur_team.members.filter(
|
||||||
|
(member) =>
|
||||||
|
member.user.keycloak_id !=
|
||||||
|
props.member.user.keycloak_id,
|
||||||
|
),
|
||||||
|
member,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
set_is_editing(false);
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
title: "Права изменены успешно",
|
||||||
|
message: "",
|
||||||
|
icon: <IconCheck />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
title: "Ошибка изменения прав",
|
||||||
|
message: "",
|
||||||
|
color: "red",
|
||||||
|
icon: <IconForbid />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CustomButton
|
||||||
|
style="outline"
|
||||||
|
text="Отменить"
|
||||||
|
color="red"
|
||||||
|
textSize="sm"
|
||||||
|
icon={<IconPencil size={15} />}
|
||||||
|
onClick={() => {
|
||||||
|
set_is_editing(false);
|
||||||
|
set_selected_permissions(props.member.permissions);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<CustomButton
|
||||||
|
style="outline"
|
||||||
|
text="Изменить"
|
||||||
|
textSize="sm"
|
||||||
|
icon={<IconPencil size={15} />}
|
||||||
|
onClick={() => {
|
||||||
|
set_is_editing(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
gap: "10px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!is_editing ? (
|
||||||
|
props.member.permissions.map((perm) => (
|
||||||
|
<Pill className="TeamPill">{perm}</Pill>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<MultiSelect
|
||||||
|
value={selected_permissions}
|
||||||
|
label=""
|
||||||
|
placeholder="Выберите разрешения"
|
||||||
|
searchable
|
||||||
|
data={props.cur_team.creator.permissions}
|
||||||
|
classNames={{ input: "MantineInput" }}
|
||||||
|
onChange={(value) => {
|
||||||
|
set_selected_permissions(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Card.Section>
|
||||||
|
<Card.Section
|
||||||
|
inheritPadding
|
||||||
|
px="md"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteMember({
|
||||||
|
team_id: props.cur_team.id,
|
||||||
|
user_id: props.member.user.keycloak_id,
|
||||||
|
}).then(() => {
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
title: "Пользователь удален успешно",
|
||||||
|
message: "",
|
||||||
|
icon: <IconCheck />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
updateTeam(props.cur_team.id, {
|
||||||
|
members: [
|
||||||
|
...props.cur_team.members.filter(
|
||||||
|
(member) =>
|
||||||
|
member.user.keycloak_id != props.member.user.keycloak_id,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
width: "20px",
|
||||||
|
alignSelf: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</UnstyledButton>
|
||||||
|
<Stack gap={"5px"}>
|
||||||
|
<Text style={{ alignSelf: "flex-end" }}>Добавлен:</Text>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="sm" style={{ textAlign: "right" }}>
|
||||||
|
{new Date(props.member.joined_at).toLocaleDateString("ru")}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" style={{ textAlign: "right" }}>
|
||||||
|
{new Date(props.member.joined_at).toLocaleTimeString("ru")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Card.Section>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TeamMemberCard;
|
||||||
56
src/Components/ListCard/TeamListCard/TeamsListCard.css
Normal file
56
src/Components/ListCard/TeamListCard/TeamsListCard.css
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
.TeamsListCard {
|
||||||
|
height: 120px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TeamSection {
|
||||||
|
border-right: 1px solid var(--mantine-color-contrast-filled);
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TeamPill {
|
||||||
|
background-color: var(--mantine-color-contrast-filled);
|
||||||
|
color: var(--mantine-color-primary-filled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.TeamPill2 {
|
||||||
|
border: 2px solid var(--mantine-color-accent-filled);
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--mantine-color-accent-filled);
|
||||||
|
* {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.membersPills {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: right;
|
||||||
|
gap: 5px;
|
||||||
|
.p {
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.TopRightContainer {
|
||||||
|
justify-content: right;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 15px;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RightTeamSection {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
211
src/Components/ListCard/TeamListCard/TeamsListCard.tsx
Normal file
211
src/Components/ListCard/TeamListCard/TeamsListCard.tsx
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import { Card, Pill, Text, UnstyledButton } from "@mantine/core";
|
||||||
|
import "./TeamsListCard.css";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import type { Team, TeamMember } from "Types/Team/Team";
|
||||||
|
import {
|
||||||
|
IconCancel,
|
||||||
|
IconCheck,
|
||||||
|
IconDoorExit,
|
||||||
|
IconTrash,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import type { MouseEvent } from "react";
|
||||||
|
import { useTeamStore } from "Stores/TeamsStore";
|
||||||
|
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
|
import { deleteMember, deleteTeam } from "Api/QuantumBackend/TeamManagement";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
|
function TeamsListCard(props: Team) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { removeTeam, setTotalTeams, totalTeams } = useTeamStore();
|
||||||
|
const { profile } = useAuthenticationStore();
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
// TODO: fix delete
|
||||||
|
deleteTeam(props.id)
|
||||||
|
.then(() => {
|
||||||
|
removeTeam(props.id);
|
||||||
|
setTotalTeams(totalTeams - 1);
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
title: "Команда удалена успешно",
|
||||||
|
message: "",
|
||||||
|
icon: <IconCheck />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
color: "red",
|
||||||
|
title: "Не удалось удалить команду",
|
||||||
|
message: "",
|
||||||
|
icon: <IconCancel />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLeave = () => {
|
||||||
|
// TODO: fix delete
|
||||||
|
if (profile && profile.id) {
|
||||||
|
deleteMember({ team_id: props.id, user_id: profile.id })
|
||||||
|
.then(() => {
|
||||||
|
removeTeam(props.id);
|
||||||
|
setTotalTeams(totalTeams - 1);
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
title: "Успешно вышел из команды",
|
||||||
|
message: "",
|
||||||
|
icon: <IconCheck />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
color: "red",
|
||||||
|
title: "Не удалось выйти из команды",
|
||||||
|
message: "",
|
||||||
|
icon: <IconCancel />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className="TeamsListCard"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(props.id.toString());
|
||||||
|
}}
|
||||||
|
orientation="horizontal"
|
||||||
|
>
|
||||||
|
<Card.Section
|
||||||
|
withBorder
|
||||||
|
inheritPadding
|
||||||
|
px="xs"
|
||||||
|
style={{ width: "430px" }}
|
||||||
|
>
|
||||||
|
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
|
||||||
|
Команда: {props.name}
|
||||||
|
</Text>
|
||||||
|
<Text mb="sm" size="md">
|
||||||
|
Владелец:
|
||||||
|
<Pill className="TeamPill" style={{ marginLeft: "10px" }}>
|
||||||
|
<Text size="md">{props.creator.user.username}</Text>
|
||||||
|
</Pill>
|
||||||
|
</Text>
|
||||||
|
<div className="membersPills">
|
||||||
|
<Text>Члены:</Text>
|
||||||
|
{props.members.map((team: TeamMember) => {
|
||||||
|
return (
|
||||||
|
<Pill className="TeamPill" style={{ marginLeft: "10px" }}>
|
||||||
|
<Text
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
textWrap: "nowrap",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{team.user.username}
|
||||||
|
</Text>
|
||||||
|
</Pill>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Card.Section>
|
||||||
|
<Card.Section
|
||||||
|
inheritPadding
|
||||||
|
px="xs"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
flexGrow: "1",
|
||||||
|
gap: "15px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>Разрешения:</Text>
|
||||||
|
<div
|
||||||
|
className="membersPills"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.members
|
||||||
|
.find((member) => {
|
||||||
|
return profile && member.user.keycloak_id == profile?.id;
|
||||||
|
})
|
||||||
|
?.permissions.map((perm) => {
|
||||||
|
return (
|
||||||
|
<Pill className="TeamPill" style={{ marginLeft: "10px" }}>
|
||||||
|
<Text
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
textWrap: "nowrap",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{perm}
|
||||||
|
</Text>
|
||||||
|
</Pill>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Card.Section>
|
||||||
|
<Card.Section
|
||||||
|
inheritPadding
|
||||||
|
px="md"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="TopRightContainer">
|
||||||
|
<div className="dateContainer">
|
||||||
|
<Text size="sm">
|
||||||
|
{new Date(props.created_at).toLocaleDateString()}{" "}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm">
|
||||||
|
{new Date(props.created_at).toLocaleTimeString()}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
{props.creator.user.keycloak_id == profile?.id && (
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={(e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete();
|
||||||
|
}}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</UnstyledButton>
|
||||||
|
)}
|
||||||
|
{props.creator.user.keycloak_id != profile?.id && (
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={(e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleLeave();
|
||||||
|
}}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<IconDoorExit size={20} />
|
||||||
|
</UnstyledButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Text c="dimmed" size="sm" style={{ alignSelf: "flex-end" }}>
|
||||||
|
#{props.id}
|
||||||
|
</Text>
|
||||||
|
</Card.Section>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TeamsListCard;
|
||||||
@@ -1,17 +1,31 @@
|
|||||||
import { Pagination } from "@mantine/core";
|
import { LoadingOverlay, Pagination } from "@mantine/core";
|
||||||
import "./PaginationContainer.css";
|
import "./PaginationContainer.css";
|
||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
|
|
||||||
interface PaginationContainerProps extends PropsWithChildren {
|
interface PaginationContainerProps extends PropsWithChildren {
|
||||||
numberOfPages: number;
|
numberOfPages: number;
|
||||||
|
isLoading: boolean;
|
||||||
|
activePage: number;
|
||||||
|
setPage: (page_num: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PaginationContainer(a: PaginationContainerProps) {
|
export function PaginationContainer(a: PaginationContainerProps) {
|
||||||
return (
|
return (
|
||||||
<div className="PaginationContainer">
|
<div className="PaginationContainer">
|
||||||
|
<LoadingOverlay
|
||||||
|
visible={a.isLoading}
|
||||||
|
zIndex={1000}
|
||||||
|
overlayProps={{ radius: "sm", blur: 2 }}
|
||||||
|
loaderProps={{ size: 50, type: "dots" }}
|
||||||
|
/>
|
||||||
<div className="PaginationContents">{a.children}</div>
|
<div className="PaginationContents">{a.children}</div>
|
||||||
{a.numberOfPages > 1 && (
|
{a.numberOfPages > 1 && (
|
||||||
<Pagination total={a.numberOfPages} color="accent" />
|
<Pagination
|
||||||
|
total={a.numberOfPages}
|
||||||
|
value={a.activePage}
|
||||||
|
onChange={a.setPage}
|
||||||
|
color="accent"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
136
src/Modals/AddMember/AddMember.tsx
Normal file
136
src/Modals/AddMember/AddMember.tsx
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import {
|
||||||
|
Center,
|
||||||
|
Modal,
|
||||||
|
MultiSelect,
|
||||||
|
Space,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
import { addMember } from "Api/QuantumBackend/TeamManagement";
|
||||||
|
import { GetUserByEmail } from "Api/QuantumBackend/UserManagement";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { IconForbid } from "@tabler/icons-react";
|
||||||
|
import { useTeamStore } from "Stores/TeamsStore";
|
||||||
|
import type { Team } from "Types/Team/Team";
|
||||||
|
|
||||||
|
interface NewExperimentModalProps {
|
||||||
|
isOpened: boolean;
|
||||||
|
setIsOpened: (opened: boolean) => void;
|
||||||
|
permissions: string[] | undefined;
|
||||||
|
team: Team | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddMember(props: NewExperimentModalProps) {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [selectedPerms, setSelectedPerms] = useState<string[] | undefined>();
|
||||||
|
const { updateTeam } = useTeamStore();
|
||||||
|
//reset on open dialog
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.isOpened) {
|
||||||
|
setEmail("");
|
||||||
|
setSelectedPerms(undefined);
|
||||||
|
}
|
||||||
|
}, [props.isOpened]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
props.setIsOpened(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddMember = () => {
|
||||||
|
if (props.team)
|
||||||
|
//TODO: add logic for backend server
|
||||||
|
GetUserByEmail(email)
|
||||||
|
.then((member) => {
|
||||||
|
if (member && props.team) {
|
||||||
|
addMember({
|
||||||
|
team_id: props.team.id,
|
||||||
|
user_id: member.keycloak_id,
|
||||||
|
permissions: selectedPerms ? selectedPerms : [],
|
||||||
|
})
|
||||||
|
.then((member) => {
|
||||||
|
if (member && props.team) {
|
||||||
|
updateTeam(props.team.id, {
|
||||||
|
members: [...props.team.members, member],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
props.setIsOpened(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
title: "Ошибка добавления пользователя в команду",
|
||||||
|
message: "",
|
||||||
|
color: "red",
|
||||||
|
icon: <IconForbid />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
color: "red",
|
||||||
|
title: "Пользователь с почтой не найден",
|
||||||
|
message: "",
|
||||||
|
icon: <IconForbid />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={props.isOpened}
|
||||||
|
onClose={handleClose}
|
||||||
|
title=<Title size="xl">Добавить члена команды</Title>
|
||||||
|
centered
|
||||||
|
size="75%"
|
||||||
|
styles={{
|
||||||
|
content: { paddingLeft: "10px" },
|
||||||
|
title: { width: "100%" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
value={email}
|
||||||
|
label="Почта члена команды"
|
||||||
|
placeholder="Введите почту"
|
||||||
|
required
|
||||||
|
classNames={{ input: "MantineInput" }}
|
||||||
|
onChange={(event) => {
|
||||||
|
setEmail(event.currentTarget.value);
|
||||||
|
}}
|
||||||
|
></TextInput>
|
||||||
|
<MultiSelect
|
||||||
|
value={selectedPerms}
|
||||||
|
label="Разрешения члена"
|
||||||
|
placeholder="Выберите разрешения"
|
||||||
|
searchable
|
||||||
|
data={props.permissions}
|
||||||
|
classNames={{ input: "MantineInput" }}
|
||||||
|
onChange={(value) => setSelectedPerms(value)}
|
||||||
|
/>
|
||||||
|
<Space h="md" />
|
||||||
|
<Center>
|
||||||
|
<CustomButton
|
||||||
|
disabled={email != "" ? false : true}
|
||||||
|
color="contrast"
|
||||||
|
onClick={handleAddMember}
|
||||||
|
text="Добавить члена"
|
||||||
|
></CustomButton>
|
||||||
|
<Space w="md" />
|
||||||
|
<CustomButton
|
||||||
|
color="contrast"
|
||||||
|
style="outline"
|
||||||
|
onClick={() => {
|
||||||
|
props.setIsOpened(false);
|
||||||
|
}}
|
||||||
|
text="Отменить"
|
||||||
|
></CustomButton>
|
||||||
|
</Center>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,17 +15,23 @@ import CustomButton from "Components/CustomButton/CustomButton";
|
|||||||
interface NewExperimentModalProps {
|
interface NewExperimentModalProps {
|
||||||
isOpened: boolean;
|
isOpened: boolean;
|
||||||
setIsOpened: (opened: boolean) => void;
|
setIsOpened: (opened: boolean) => void;
|
||||||
|
teams: { team_id: number; team_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<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}>();
|
||||||
//reset on open dialog
|
//reset on open dialog
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.isOpened) {
|
if (props.isOpened) {
|
||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
|
setSelectedTeam(undefined);
|
||||||
}
|
}
|
||||||
}, [props.isOpened]);
|
}, [props.isOpened]);
|
||||||
|
|
||||||
@@ -35,18 +41,19 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
|
|||||||
|
|
||||||
const handleCreateExperiment = () => {
|
const handleCreateExperiment = () => {
|
||||||
//TODO: add logic for backend server
|
//TODO: add logic for backend server
|
||||||
|
if (selectedTeam) {
|
||||||
addExperiment({
|
addExperiment({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: name,
|
name: name,
|
||||||
description: description,
|
description: description,
|
||||||
team_id: 1,
|
team_id: Number(selectedTeam?.value),
|
||||||
tasks_ids: [],
|
tasks_ids: [],
|
||||||
date_created: new Date(),
|
date_created: new Date(),
|
||||||
experiment_status: "DRAFT",
|
experiment_status: "DRAFT",
|
||||||
experiment_type: "a",
|
experiment_type: "a",
|
||||||
});
|
});
|
||||||
props.setIsOpened(false);
|
props.setIsOpened(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -86,18 +93,28 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
|
|||||||
></Textarea>
|
></Textarea>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Select
|
<Select
|
||||||
value=""
|
value={selectedTeam?.value}
|
||||||
label="Команда эксперимента"
|
label="Команда эксперимента"
|
||||||
placeholder="Выберите команду эксперимента"
|
placeholder="Выберите команду эксперимента"
|
||||||
searchable
|
searchable
|
||||||
data={[]}
|
required
|
||||||
|
data={
|
||||||
|
props.teams
|
||||||
|
? props.teams.map((team) => {
|
||||||
|
return {
|
||||||
|
value: team.team_id.toString(),
|
||||||
|
label: `${team.team_name} (#${team.team_id})`,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
}
|
||||||
classNames={{ input: "MantineInput" }}
|
classNames={{ input: "MantineInput" }}
|
||||||
onChange={() => {}}
|
onChange={(_value, option) => setSelectedTeam(option)}
|
||||||
/>
|
/>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Center>
|
<Center>
|
||||||
<CustomButton
|
<CustomButton
|
||||||
disabled={name != "" ? false : true}
|
disabled={name != "" && selectedTeam ? false : true}
|
||||||
color="contrast"
|
color="contrast"
|
||||||
onClick={handleCreateExperiment}
|
onClick={handleCreateExperiment}
|
||||||
text="Создать эксперимент"
|
text="Создать эксперимент"
|
||||||
|
|||||||
0
src/Modals/NewTeam/NewTeam.css
Normal file
0
src/Modals/NewTeam/NewTeam.css
Normal file
116
src/Modals/NewTeam/NewTeam.tsx
Normal file
116
src/Modals/NewTeam/NewTeam.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
Center,
|
||||||
|
Modal,
|
||||||
|
Space,
|
||||||
|
Textarea,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import "./NewTeam.css";
|
||||||
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
import { useTeamStore } from "Stores/TeamsStore";
|
||||||
|
import { createTeam, getTeam } from "Api/QuantumBackend/TeamManagement";
|
||||||
|
import { IconCheck } from "@tabler/icons-react";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
|
interface NewTeamModalProps {
|
||||||
|
isOpened: boolean;
|
||||||
|
setIsOpened: (opened: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewTeamModal(props: NewTeamModalProps) {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const { addTeam, setTotalTeams, totalTeams } = useTeamStore();
|
||||||
|
//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
|
||||||
|
//
|
||||||
|
createTeam({ name: name, description: description }).then((team) => {
|
||||||
|
if (team) {
|
||||||
|
getTeam(team.team_id).then((team) => {
|
||||||
|
if (team) {
|
||||||
|
setTotalTeams(totalTeams + 1);
|
||||||
|
addTeam(team);
|
||||||
|
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
|
||||||
|
classNames={{ input: "MantineInput" }}
|
||||||
|
onChange={(event) => {
|
||||||
|
setName(event.currentTarget.value);
|
||||||
|
}}
|
||||||
|
></TextInput>
|
||||||
|
<Space h="md" />
|
||||||
|
<Textarea
|
||||||
|
value={description}
|
||||||
|
label="Описание команды"
|
||||||
|
placeholder="Введите описание команды"
|
||||||
|
minRows={4}
|
||||||
|
maxRows={10}
|
||||||
|
autosize
|
||||||
|
classNames={{ input: "MantineInput" }}
|
||||||
|
onChange={(event) => {
|
||||||
|
setDescription(event.currentTarget.value);
|
||||||
|
}}
|
||||||
|
></Textarea>
|
||||||
|
<Space h="md" />
|
||||||
|
<Center>
|
||||||
|
<CustomButton
|
||||||
|
disabled={name != "" ? false : true}
|
||||||
|
color="contrast"
|
||||||
|
onClick={handleCreateTeam}
|
||||||
|
text="Создать Команду"
|
||||||
|
></CustomButton>
|
||||||
|
<Space w="md" />
|
||||||
|
<CustomButton
|
||||||
|
color="contrast"
|
||||||
|
style="outline"
|
||||||
|
onClick={() => {
|
||||||
|
props.setIsOpened(false);
|
||||||
|
}}
|
||||||
|
text="Отменить"
|
||||||
|
></CustomButton>
|
||||||
|
</Center>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,7 +15,12 @@ function DevicesPage() {
|
|||||||
/>
|
/>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="DevicesPage">
|
<div className="DevicesPage">
|
||||||
<PaginationContainer numberOfPages={1}>
|
<PaginationContainer
|
||||||
|
numberOfPages={1}
|
||||||
|
isLoading={false}
|
||||||
|
activePage={1}
|
||||||
|
setPage={() => {}}
|
||||||
|
>
|
||||||
<Alert title="Подключенные устройства не найдены" color="blue">
|
<Alert title="Подключенные устройства не найдены" color="blue">
|
||||||
Для того, чтобы добавить устройство, простмотрите{" "}
|
Для того, чтобы добавить устройство, простмотрите{" "}
|
||||||
<Link to={"documentaion"} className="invisible_link">
|
<Link to={"documentaion"} className="invisible_link">
|
||||||
|
|||||||
@@ -6,25 +6,17 @@ import { useState } from "react";
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { IconPlus, IconSettings } from "@tabler/icons-react";
|
import { IconPlus, IconSettings } from "@tabler/icons-react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import {
|
|
||||||
useExperimentStore,
|
|
||||||
useSelectedExperiment,
|
|
||||||
} from "Stores/ExperimentStore";
|
|
||||||
import CustomButton from "Components/CustomButton/CustomButton";
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||||
|
|
||||||
function ExperimentPage() {
|
function ExperimentPage() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { experiment_id } = useParams();
|
const { experiment_id } = useParams();
|
||||||
const { selectedExperimentId, selectExperiment } = useExperimentStore();
|
const { experiments } = useExperimentStore();
|
||||||
|
|
||||||
if (
|
const experiment = experiments.find((exp) => {
|
||||||
!selectedExperimentId ||
|
return exp.id == Number(experiment_id);
|
||||||
(Number(experiment_id) != selectedExperimentId && experiment_id)
|
});
|
||||||
) {
|
|
||||||
console.log(Number(experiment_id));
|
|
||||||
selectExperiment(Number(experiment_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
const experiment = useSelectedExperiment();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -74,7 +66,12 @@ function ExperimentPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{experiment.tasks_ids.length > 0 && (
|
{experiment.tasks_ids.length > 0 && (
|
||||||
<PaginationContainer numberOfPages={1}>
|
<PaginationContainer
|
||||||
|
numberOfPages={1}
|
||||||
|
isLoading={false}
|
||||||
|
activePage={1}
|
||||||
|
setPage={() => {}}
|
||||||
|
>
|
||||||
{experiment.tasks_ids.map((task: number) => {
|
{experiment.tasks_ids.map((task: number) => {
|
||||||
return <div>{task}</div>;
|
return <div>{task}</div>;
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import "./ExperimentsPage.css";
|
import "./ExperimentsPage.css";
|
||||||
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
|
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { IconMicroscope } from "@tabler/icons-react";
|
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";
|
||||||
@@ -9,10 +9,29 @@ import { useExperimentStore } from "Stores/ExperimentStore";
|
|||||||
import type { Experiment } from "Types/Experiment/Experiment";
|
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 { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
|
|
||||||
function ExperimentsPage() {
|
function ExperimentsPage() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { experiments, teams } = useExperimentStore();
|
const { experiments } = useExperimentStore();
|
||||||
|
const [teams, setTeams] =
|
||||||
|
useState<{ team_id: number; team_name: string }[]>();
|
||||||
|
|
||||||
|
const { profile, is_loading } = useAuthenticationStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (profile || !is_loading)
|
||||||
|
getShortTeamsList()
|
||||||
|
.then((teams) => {
|
||||||
|
if (teams) {
|
||||||
|
setTeams(teams);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setTeams([]);
|
||||||
|
});
|
||||||
|
}, [profile, is_loading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -23,7 +42,11 @@ function ExperimentsPage() {
|
|||||||
content="See the list of all of users experiments"
|
content="See the list of all of users experiments"
|
||||||
/>
|
/>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<NewExperimentModal isOpened={isOpen} setIsOpened={setIsOpen} />
|
<NewExperimentModal
|
||||||
|
isOpened={isOpen}
|
||||||
|
setIsOpened={setIsOpen}
|
||||||
|
teams={teams}
|
||||||
|
/>
|
||||||
<div className="ExperimentsPage">
|
<div className="ExperimentsPage">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -33,7 +56,7 @@ function ExperimentsPage() {
|
|||||||
marginBottom: "20px",
|
marginBottom: "20px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{teams.length > 0 && (
|
{teams && teams.length > 0 && (
|
||||||
<div className="experimentsButtons">
|
<div className="experimentsButtons">
|
||||||
<CustomButton
|
<CustomButton
|
||||||
color="contrast"
|
color="contrast"
|
||||||
@@ -46,27 +69,40 @@ function ExperimentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<PaginationContainer numberOfPages={1}>
|
<PaginationContainer
|
||||||
|
numberOfPages={1}
|
||||||
|
isLoading={teams == undefined}
|
||||||
|
activePage={1}
|
||||||
|
setPage={() => {}}
|
||||||
|
>
|
||||||
{experiments.map((exp: Experiment) => {
|
{experiments.map((exp: Experiment) => {
|
||||||
return (
|
return (
|
||||||
<ExperimentsListCard
|
<ExperimentsListCard
|
||||||
id={exp.id}
|
experiment={{
|
||||||
name={exp.name}
|
id: exp.id,
|
||||||
description={exp.description}
|
name: exp.name,
|
||||||
team_id={exp.team_id}
|
description: exp.description,
|
||||||
tasks_ids={exp.tasks_ids}
|
team_id: exp.team_id,
|
||||||
date_created={exp.date_created}
|
tasks_ids: exp.tasks_ids,
|
||||||
experiment_status={exp.experiment_status}
|
date_created: exp.date_created,
|
||||||
experiment_type="A"
|
experiment_status: exp.experiment_status,
|
||||||
|
experiment_type: "A",
|
||||||
|
}}
|
||||||
|
team={teams?.find((team) => team.team_id == exp.team_id)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{teams.length == 0 && (
|
{teams && teams.length == 0 && (
|
||||||
<Alert title="Команды не найдены" color="red">
|
<Alert title="Команды не найдены" color="red">
|
||||||
Создайте или войдите в команду чтобы начать работу с
|
Создайте или войдите в команду чтобы начать работу с
|
||||||
экспериментами
|
экспериментами
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
{teams && teams.length != 0 && experiments.length == 0 && (
|
||||||
|
<Alert title="Экспериментов нету" color="blue">
|
||||||
|
Вы еще не создали не один эксперимент
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
</PaginationContainer>
|
</PaginationContainer>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
11
src/Pages/TeamsPage/TeamPage/TeamPage.css
Normal file
11
src/Pages/TeamsPage/TeamPage/TeamPage.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.TeamPage {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TeamData {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
228
src/Pages/TeamsPage/TeamPage/TeamPage.tsx
Normal file
228
src/Pages/TeamsPage/TeamPage/TeamPage.tsx
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Avatar,
|
||||||
|
Card,
|
||||||
|
Center,
|
||||||
|
Grid,
|
||||||
|
LoadingOverlay,
|
||||||
|
Text,
|
||||||
|
Textarea,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import "./TeamPage.css";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { IconCheck, IconPlus } from "@tabler/icons-react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
|
import { useTeamStore } from "Stores/TeamsStore";
|
||||||
|
import { getTeam, updateTeamRequest } from "Api/QuantumBackend/TeamManagement";
|
||||||
|
import type { Team, TeamMember } from "Types/Team/Team";
|
||||||
|
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
|
import TeamMemberCard from "Components/ListCard/TeamListCard/TeamMemberCard/TeamMemberCard";
|
||||||
|
import { AddMember } from "Modals/AddMember/AddMember";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
|
function TeamPage() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { team_id } = useParams();
|
||||||
|
const { addTeam, teams, updateTeam } = useTeamStore();
|
||||||
|
const { profile } = useAuthenticationStore();
|
||||||
|
const [is_loading, set_is_loading] = useState(true);
|
||||||
|
|
||||||
|
const [cur_team, set_cur_team] = useState<Team>();
|
||||||
|
|
||||||
|
const [team_name, set_team_name] = useState(cur_team?.name);
|
||||||
|
const [team_descr, set_team_descr] = useState(cur_team?.description);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cur_team && profile) {
|
||||||
|
getTeam(Number(team_id))
|
||||||
|
.then((team) => {
|
||||||
|
if (team) {
|
||||||
|
addTeam(team);
|
||||||
|
set_cur_team(team);
|
||||||
|
set_team_name(team.name);
|
||||||
|
set_team_descr(team.description);
|
||||||
|
set_is_loading(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
set_is_loading(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (profile) {
|
||||||
|
set_is_loading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (teams.find((e) => e.id === Number(team_id)) != cur_team) {
|
||||||
|
set_cur_team(teams.find((e) => e.id === Number(team_id)));
|
||||||
|
}
|
||||||
|
}, [cur_team, profile, teams]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>
|
||||||
|
{cur_team ? "Team " + cur_team.id + " | QMolSim" : "Error | QmolSim"}
|
||||||
|
</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="See the information on the currently selected team"
|
||||||
|
/>
|
||||||
|
</Helmet>
|
||||||
|
<AddMember
|
||||||
|
isOpened={isOpen}
|
||||||
|
setIsOpened={setIsOpen}
|
||||||
|
permissions={cur_team?.creator.permissions}
|
||||||
|
team={cur_team}
|
||||||
|
/>
|
||||||
|
<div className="TeamPage">
|
||||||
|
<LoadingOverlay
|
||||||
|
visible={is_loading}
|
||||||
|
zIndex={1000}
|
||||||
|
overlayProps={{ radius: "sm", blur: 2 }}
|
||||||
|
loaderProps={{ size: 50, type: "dots" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card className="TeamData">
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<Title size={"xl"}>Имя команды:</Title>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={9}>
|
||||||
|
<TextInput
|
||||||
|
size="md"
|
||||||
|
value={team_name}
|
||||||
|
onChange={(val) => {
|
||||||
|
set_team_name(val.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<Title size={"xl"}>Описание:</Title>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={9}>
|
||||||
|
<Textarea
|
||||||
|
size="md"
|
||||||
|
value={team_descr ? team_descr : ""}
|
||||||
|
onChange={(val) => {
|
||||||
|
set_team_descr(val.target.value);
|
||||||
|
}}
|
||||||
|
minRows={2}
|
||||||
|
maxRows={4}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<Title size={"xl"}>Создатель:</Title>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col
|
||||||
|
span={9}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
radius="xl"
|
||||||
|
src={cur_team?.creator.user.profile_picture_path}
|
||||||
|
/>
|
||||||
|
<Text size="md" style={{ textDecoration: "underline" }}>
|
||||||
|
{cur_team?.creator.user.username} (
|
||||||
|
{cur_team?.creator.user.email})
|
||||||
|
</Text>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<CustomButton
|
||||||
|
color="accent"
|
||||||
|
text="Сохранить"
|
||||||
|
disabled={
|
||||||
|
cur_team?.name == team_name &&
|
||||||
|
cur_team?.description == team_descr
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
if (cur_team && team_name)
|
||||||
|
updateTeamRequest({
|
||||||
|
team_id: cur_team.id,
|
||||||
|
name: team_name,
|
||||||
|
description: team_descr,
|
||||||
|
}).then((team) => {
|
||||||
|
updateTeam(team.team_id, {
|
||||||
|
name: team.name,
|
||||||
|
description: team.description,
|
||||||
|
});
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
title: "Информация команды обновлена успешно",
|
||||||
|
message: "",
|
||||||
|
icon: <IconCheck />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<CustomButton
|
||||||
|
color="error"
|
||||||
|
text="Отменить"
|
||||||
|
disabled={
|
||||||
|
cur_team?.name == team_name &&
|
||||||
|
cur_team?.description == team_descr
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
set_team_name(cur_team?.name);
|
||||||
|
set_team_descr(cur_team?.description);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "20px",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Title order={2} style={{ textWrap: "nowrap" }}>
|
||||||
|
Члены команды:{" "}
|
||||||
|
</Title>
|
||||||
|
<div>
|
||||||
|
<CustomButton
|
||||||
|
color="contrast"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
icon={<IconPlus />}
|
||||||
|
text="Добавить члена"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{cur_team && cur_team.members.length > 0 && (
|
||||||
|
<div className="TeamMembers">
|
||||||
|
{cur_team.members.map((member: TeamMember) => {
|
||||||
|
return <TeamMemberCard cur_team={cur_team} member={member} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!cur_team && !is_loading && (
|
||||||
|
<Alert color="red">
|
||||||
|
<Center>
|
||||||
|
{" "}
|
||||||
|
<Text c="contrast" size={"xl"}>
|
||||||
|
Ошибка. Команда не найдена
|
||||||
|
</Text>{" "}
|
||||||
|
</Center>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TeamPage;
|
||||||
@@ -3,14 +3,79 @@ import "./TeamsPage.css";
|
|||||||
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
|
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
|
||||||
import CustomButton from "Components/CustomButton/CustomButton";
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
import { IconUsersGroup } from "@tabler/icons-react";
|
import { IconUsersGroup } from "@tabler/icons-react";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Alert } from "@mantine/core";
|
import { Alert } from "@mantine/core";
|
||||||
|
import { NewTeamModal } from "Modals/NewTeam/NewTeam";
|
||||||
|
import { useTeamStore } from "Stores/TeamsStore";
|
||||||
|
import { getTeams } from "Api/QuantumBackend/TeamManagement";
|
||||||
|
import { useAuthenticationStore as useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
|
import TeamsListCard from "Components/ListCard/TeamListCard/TeamsListCard";
|
||||||
|
|
||||||
function TeamsPage() {
|
function TeamsPage() {
|
||||||
const [, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { teams, setTeams, setTotalTeams, totalTeams, removeTeam } =
|
||||||
|
useTeamStore();
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const { profile } = useAuthenticationStore();
|
||||||
|
const [cur_page, set_cur_page] = useState<number>(1);
|
||||||
|
const [page_size, set_page_size] = useState<number>(256);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
if (profile) {
|
||||||
|
getTeams({ page_num: cur_page })
|
||||||
|
.then((teams) => {
|
||||||
|
setTeams(teams.teams);
|
||||||
|
set_cur_page(teams.cur_page);
|
||||||
|
setTotalTeams(teams.total_teams);
|
||||||
|
set_page_size(teams.page_size);
|
||||||
|
setIsLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => setIsLoading(false));
|
||||||
|
}
|
||||||
|
}, [profile]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (profile) {
|
||||||
|
if (teams && teams.length > page_size) {
|
||||||
|
removeTeam(teams[teams.length - 1].id);
|
||||||
|
}
|
||||||
|
if (teams && cur_page > Math.ceil(totalTeams / page_size)) {
|
||||||
|
setIsLoading(true);
|
||||||
|
getTeams({ page_num: Math.max(1, cur_page - 1) })
|
||||||
|
.then((teams) => {
|
||||||
|
setTeams(teams.teams);
|
||||||
|
set_cur_page(teams.cur_page);
|
||||||
|
setTotalTeams(teams.total_teams);
|
||||||
|
set_page_size(teams.page_size);
|
||||||
|
setIsLoading(false);
|
||||||
|
scroll({ top: 0 });
|
||||||
|
})
|
||||||
|
.catch(() => setIsLoading(false));
|
||||||
|
set_cur_page(Math.max(1, cur_page - 1));
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
teams &&
|
||||||
|
teams.length < Math.min(page_size, totalTeams) &&
|
||||||
|
cur_page != Math.ceil(totalTeams / page_size)
|
||||||
|
) {
|
||||||
|
getTeams({ page_num: cur_page })
|
||||||
|
.then((teams) => {
|
||||||
|
setTeams(teams.teams);
|
||||||
|
set_cur_page(teams.cur_page);
|
||||||
|
setTotalTeams(teams.total_teams);
|
||||||
|
set_page_size(teams.page_size);
|
||||||
|
setIsLoading(false);
|
||||||
|
scroll({ top: 0 });
|
||||||
|
})
|
||||||
|
.catch(() => setIsLoading(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [profile, teams]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<NewTeamModal isOpened={isOpen} setIsOpened={setIsOpen} />
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Teams Page | QMolSim</title>
|
<title>Teams Page | QMolSim</title>
|
||||||
<meta
|
<meta
|
||||||
@@ -26,13 +91,35 @@ function TeamsPage() {
|
|||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}}
|
}}
|
||||||
icon={<IconUsersGroup />}
|
icon={<IconUsersGroup />}
|
||||||
text="Создать эксперимент"
|
text="Создать Команду"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<PaginationContainer numberOfPages={1}>
|
<PaginationContainer
|
||||||
<Alert title="Команды не найдены" color="blue">
|
numberOfPages={Math.ceil(totalTeams / page_size)}
|
||||||
Создайте или войдите в команду чтобы начать работу с экспериментами
|
isLoading={isLoading}
|
||||||
</Alert>
|
activePage={cur_page}
|
||||||
|
setPage={(page) => {
|
||||||
|
getTeams({ page_num: page })
|
||||||
|
.then((teams) => {
|
||||||
|
setTeams(teams.teams);
|
||||||
|
set_cur_page(teams.cur_page);
|
||||||
|
setTotalTeams(teams.total_teams);
|
||||||
|
set_page_size(teams.page_size);
|
||||||
|
setIsLoading(false);
|
||||||
|
scroll({ top: 0 });
|
||||||
|
})
|
||||||
|
.catch(() => setIsLoading(false));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{teams.length == 0 && !isLoading && (
|
||||||
|
<Alert title="Команды не найдены" color="blue">
|
||||||
|
Создайте или войдите в команду чтобы начать работу с
|
||||||
|
экспериментами
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{teams.map((team) => {
|
||||||
|
return <TeamsListCard {...team} />;
|
||||||
|
})}
|
||||||
</PaginationContainer>
|
</PaginationContainer>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import keycloak, {
|
|||||||
} from "Api/Keycloak/Keycloak";
|
} from "Api/Keycloak/Keycloak";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { AuthenticationStore } from "Stores/AuthenticationStore";
|
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
import "./UserPage.css";
|
import "./UserPage.css";
|
||||||
import { useUserPreferencesStore } from "Stores/PreferencesStore";
|
import { useUserPreferencesStore } from "Stores/PreferencesStore";
|
||||||
import CustomButton from "Components/CustomButton/CustomButton";
|
import CustomButton from "Components/CustomButton/CustomButton";
|
||||||
@@ -35,7 +35,8 @@ import {
|
|||||||
} from "Api/QuantumBackend/UserManagement";
|
} from "Api/QuantumBackend/UserManagement";
|
||||||
import type { UserData } from "Types/User/User";
|
import type { UserData } from "Types/User/User";
|
||||||
function UserPage() {
|
function UserPage() {
|
||||||
const { profile, is_loading, profile_picture_path } = AuthenticationStore();
|
const { profile, is_loading, 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>("");
|
||||||
@@ -49,15 +50,13 @@ function UserPage() {
|
|||||||
((profile.username && profile.username != username) ||
|
((profile.username && profile.username != username) ||
|
||||||
(profile.email && profile.email != email))
|
(profile.email && profile.email != email))
|
||||||
) {
|
) {
|
||||||
console.log("A");
|
|
||||||
const prof_1 = profile;
|
const prof_1 = profile;
|
||||||
prof_1.email = email;
|
prof_1.email = email;
|
||||||
prof_1.username = username;
|
prof_1.username = username;
|
||||||
AuthenticationStore.setState({
|
useAuthenticationStore.setState({
|
||||||
is_loading: true,
|
is_loading: true,
|
||||||
});
|
});
|
||||||
updateUserData(prof_1).then((data) => {
|
updateUserData(prof_1).then((data) => {
|
||||||
console.log(data);
|
|
||||||
if (data) {
|
if (data) {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
radius: "md",
|
radius: "md",
|
||||||
@@ -67,7 +66,7 @@ function UserPage() {
|
|||||||
style: { paddingLeft: "5px" },
|
style: { paddingLeft: "5px" },
|
||||||
});
|
});
|
||||||
keycloak.loadUserProfile().then((profile) => {
|
keycloak.loadUserProfile().then((profile) => {
|
||||||
AuthenticationStore.setState({
|
useAuthenticationStore.setState({
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
profile: profile,
|
profile: profile,
|
||||||
});
|
});
|
||||||
@@ -81,7 +80,7 @@ function UserPage() {
|
|||||||
UpdateCurrentUserInfo(pfp_path).then(() => {
|
UpdateCurrentUserInfo(pfp_path).then(() => {
|
||||||
GetCurrentUserInfo().then((info: UserData | undefined) => {
|
GetCurrentUserInfo().then((info: UserData | undefined) => {
|
||||||
if (info && info.profile_picture_path)
|
if (info && info.profile_picture_path)
|
||||||
AuthenticationStore.setState({
|
useAuthenticationStore.setState({
|
||||||
profile_picture_path: info.profile_picture_path,
|
profile_picture_path: info.profile_picture_path,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -184,6 +183,12 @@ function UserPage() {
|
|||||||
<CustomButton
|
<CustomButton
|
||||||
color="error"
|
color="error"
|
||||||
text="Отменить"
|
text="Отменить"
|
||||||
|
onClick={() => {
|
||||||
|
if (profile && profile.email && profile.username) {
|
||||||
|
set_username(profile.username);
|
||||||
|
set_email(profile.email);
|
||||||
|
}
|
||||||
|
}}
|
||||||
disabled={
|
disabled={
|
||||||
!(
|
!(
|
||||||
profile != null &&
|
profile != null &&
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ function testEqual(path: string, pattern: string) {
|
|||||||
function BreadCrumbs() {
|
function BreadCrumbs() {
|
||||||
const unique_matches: string[] = getSubPaths(useLocation().pathname);
|
const unique_matches: string[] = getSubPaths(useLocation().pathname);
|
||||||
|
|
||||||
console.log(unique_matches);
|
|
||||||
//find the breadcrumbs for the matched pathes
|
//find the breadcrumbs for the matched pathes
|
||||||
const elements: ReactElement[] = [];
|
const elements: ReactElement[] = [];
|
||||||
for (const prop in routes) {
|
for (const prop in routes) {
|
||||||
|
|||||||
@@ -1,21 +1,33 @@
|
|||||||
import keycloak from "Api/Keycloak/Keycloak";
|
import keycloak from "Api/Keycloak/Keycloak";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
|
import { useAuthenticationStore } from "Stores/AuthenticationStore";
|
||||||
|
|
||||||
export default function AuthGuard() {
|
export default function AuthGuard() {
|
||||||
const loginStarted = useRef(false);
|
const loginStarted = useRef(false);
|
||||||
|
const { is_loading } = useAuthenticationStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!keycloak.authenticated && !loginStarted.current) {
|
if (
|
||||||
|
keycloak &&
|
||||||
|
!keycloak.authenticated &&
|
||||||
|
!loginStarted.current &&
|
||||||
|
!is_loading
|
||||||
|
) {
|
||||||
loginStarted.current = true;
|
loginStarted.current = true;
|
||||||
|
|
||||||
keycloak.login({
|
keycloak.login({
|
||||||
redirectUri: window.location.origin,
|
redirectUri: window.location.origin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, [is_loading, loginStarted]);
|
||||||
|
|
||||||
if (!keycloak.authenticated) {
|
if (
|
||||||
|
keycloak &&
|
||||||
|
!keycloak.authenticated &&
|
||||||
|
!loginStarted.current &&
|
||||||
|
!is_loading
|
||||||
|
) {
|
||||||
return <div>Redirecting to login...</div>;
|
return <div>Redirecting to login...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import ExperimentPage from "Pages/ExperimentsPage/ExperimentPage";
|
|||||||
import ExperimentsPage from "Pages/ExperimentsPage/ExperimentsPage";
|
import ExperimentsPage from "Pages/ExperimentsPage/ExperimentsPage";
|
||||||
import AuthGuard from "./RouterAuhGuard";
|
import AuthGuard from "./RouterAuhGuard";
|
||||||
import DevicesPage from "Pages/DevicesPage/DevicesPage";
|
import DevicesPage from "Pages/DevicesPage/DevicesPage";
|
||||||
|
import TeamPage from "Pages/TeamsPage/TeamPage/TeamPage";
|
||||||
|
|
||||||
export const routes: {
|
export const routes: {
|
||||||
[id: string]: { path: string; breadcrumbs: (path: string) => ReactElement[] };
|
[id: string]: { path: string; breadcrumbs: (path: string) => ReactElement[] };
|
||||||
@@ -42,6 +43,12 @@ export const routes: {
|
|||||||
breadcrumbs: () => [<>Вычислительные системы</>],
|
breadcrumbs: () => [<>Вычислительные системы</>],
|
||||||
},
|
},
|
||||||
TeamsPage: { path: "/teams", breadcrumbs: () => [<>Команды</>] },
|
TeamsPage: { path: "/teams", breadcrumbs: () => [<>Команды</>] },
|
||||||
|
TeamPage: {
|
||||||
|
path: "/teams/:team_id",
|
||||||
|
breadcrumbs: (path: string) => [
|
||||||
|
<>Команда #{path.split("/")[path.split("/").length - 1]}</>,
|
||||||
|
],
|
||||||
|
},
|
||||||
ErrorPage: { path: "/*", breadcrumbs: () => [<>Ошибка</>] },
|
ErrorPage: { path: "/*", breadcrumbs: () => [<>Ошибка</>] },
|
||||||
SettingsPage: { path: "/settings", breadcrumbs: () => [<>Настройки</>] },
|
SettingsPage: { path: "/settings", breadcrumbs: () => [<>Настройки</>] },
|
||||||
};
|
};
|
||||||
@@ -84,6 +91,10 @@ const router = createBrowserRouter(
|
|||||||
path: routes.TeamsPage.path,
|
path: routes.TeamsPage.path,
|
||||||
Component: TeamsPage,
|
Component: TeamsPage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: routes.TeamPage.path,
|
||||||
|
Component: TeamPage,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: routes.MachinesPage.path,
|
path: routes.MachinesPage.path,
|
||||||
Component: DevicesPage,
|
Component: DevicesPage,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface AuthernticationStoreState {
|
|||||||
set_is_loading: (is_loading: boolean) => void;
|
set_is_loading: (is_loading: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthenticationStore = create<AuthernticationStoreState>()(
|
export const useAuthenticationStore = create<AuthernticationStoreState>()(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
is_loading: true,
|
is_loading: true,
|
||||||
profile: null,
|
profile: null,
|
||||||
|
|||||||
@@ -2,19 +2,10 @@ 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 { Experiment, TaskData } from "Types/Experiment/Experiment";
|
||||||
import type { Team } from "Types/User/User";
|
|
||||||
|
|
||||||
interface ExperimentStoreState {
|
interface ExperimentStoreState {
|
||||||
experiments: Experiment[];
|
experiments: Experiment[];
|
||||||
tasks: TaskData[];
|
tasks: TaskData[];
|
||||||
teams: Team[];
|
|
||||||
|
|
||||||
selectedExperimentId?: number;
|
|
||||||
selectedTaskId?: number;
|
|
||||||
selectedTeamId?: number;
|
|
||||||
|
|
||||||
selectExperiment: (id: number | undefined) => void;
|
|
||||||
selectTask: (id: number | undefined) => void;
|
|
||||||
|
|
||||||
addExperiment: (experiment: Experiment) => void;
|
addExperiment: (experiment: Experiment) => void;
|
||||||
updateExperiment: (id: number, data: Partial<Experiment>) => void;
|
updateExperiment: (id: number, data: Partial<Experiment>) => void;
|
||||||
@@ -23,10 +14,6 @@ interface ExperimentStoreState {
|
|||||||
addTask: (experimentId: number, task: TaskData) => void;
|
addTask: (experimentId: number, task: TaskData) => void;
|
||||||
updateTask: (taskId: number, data: Partial<TaskData>) => void;
|
updateTask: (taskId: number, data: Partial<TaskData>) => void;
|
||||||
removeTask: (experimentId: number, taskId: number) => void;
|
removeTask: (experimentId: number, taskId: number) => void;
|
||||||
|
|
||||||
addTeam: (team: Team) => void;
|
|
||||||
updateTeam: (teamId: number, data: Partial<TaskData>) => void;
|
|
||||||
removeTeam: (teamId: number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useExperimentStore = create<ExperimentStoreState>()(
|
export const useExperimentStore = create<ExperimentStoreState>()(
|
||||||
@@ -35,20 +22,6 @@ export const useExperimentStore = create<ExperimentStoreState>()(
|
|||||||
tasks: [],
|
tasks: [],
|
||||||
teams: [],
|
teams: [],
|
||||||
|
|
||||||
selectedExperimentId: undefined,
|
|
||||||
selectedTaskId: undefined,
|
|
||||||
selectedTeamId: undefined,
|
|
||||||
|
|
||||||
selectExperiment: (id) =>
|
|
||||||
set((state) => {
|
|
||||||
state.selectedExperimentId = id;
|
|
||||||
}),
|
|
||||||
|
|
||||||
selectTask: (id) =>
|
|
||||||
set((state) => {
|
|
||||||
state.selectedTaskId = id;
|
|
||||||
}),
|
|
||||||
|
|
||||||
addExperiment: (experiment) =>
|
addExperiment: (experiment) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.experiments.push(experiment);
|
state.experiments.push(experiment);
|
||||||
@@ -65,11 +38,6 @@ export const useExperimentStore = create<ExperimentStoreState>()(
|
|||||||
removeExperiment: (id) =>
|
removeExperiment: (id) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.experiments = state.experiments.filter((e) => e.id !== id);
|
state.experiments = state.experiments.filter((e) => e.id !== id);
|
||||||
|
|
||||||
if (state.selectedExperimentId === id) {
|
|
||||||
state.selectedExperimentId = undefined;
|
|
||||||
state.selectedTaskId = undefined;
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
addTask: (experimentId, task) =>
|
addTask: (experimentId, task) =>
|
||||||
@@ -96,43 +64,6 @@ export const useExperimentStore = create<ExperimentStoreState>()(
|
|||||||
|
|
||||||
state.tasks = state.tasks.filter((t) => t.data.id !== taskId);
|
state.tasks = state.tasks.filter((t) => t.data.id !== taskId);
|
||||||
exp.tasks_ids = exp.tasks_ids.filter((t) => t !== taskId);
|
exp.tasks_ids = exp.tasks_ids.filter((t) => t !== taskId);
|
||||||
|
|
||||||
if (state.selectedTaskId === taskId) {
|
|
||||||
state.selectedTaskId = undefined;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
addTeam: (team) =>
|
|
||||||
set((state) => {
|
|
||||||
state.teams.push(team);
|
|
||||||
}),
|
|
||||||
updateTeam: (teamID, data) =>
|
|
||||||
set((state) => {
|
|
||||||
const team = state.teams.find((t) => t.id === teamID);
|
|
||||||
if (!team) return;
|
|
||||||
|
|
||||||
Object.assign(team, data);
|
|
||||||
}),
|
|
||||||
removeTeam: (teamID) =>
|
|
||||||
set((state) => {
|
|
||||||
state.teams = state.teams.filter((t) => t.id !== teamID);
|
|
||||||
state.experiments = state.experiments.filter(
|
|
||||||
(t) => t.team_id !== teamID,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (state.selectedTeamId === teamID) {
|
|
||||||
state.selectedTeamId = undefined;
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useSelectedExperiment = () =>
|
|
||||||
useExperimentStore((s) =>
|
|
||||||
s.experiments.find((e) => e.id === s.selectedExperimentId),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const useSelectedTask = () =>
|
|
||||||
useExperimentStore((s) => {
|
|
||||||
return s.tasks.find((t) => t.id === s.selectedTaskId);
|
|
||||||
});
|
|
||||||
|
|||||||
46
src/Stores/TeamsStore.tsx
Normal file
46
src/Stores/TeamsStore.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
|
||||||
|
import type { Team } from "Types/Team/Team";
|
||||||
|
|
||||||
|
interface TeamStoreState {
|
||||||
|
teams: Team[];
|
||||||
|
setTeams: (teams: Team[]) => void;
|
||||||
|
totalTeams: number;
|
||||||
|
setTotalTeams: (num: number) => void;
|
||||||
|
addTeam: (team: Team) => void;
|
||||||
|
updateTeam: (teamId: number, data: Partial<Team>) => void;
|
||||||
|
removeTeam: (teamId: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTeamStore = create<TeamStoreState>()(
|
||||||
|
immer((set) => ({
|
||||||
|
teams: [],
|
||||||
|
totalTeams: 0,
|
||||||
|
setTotalTeams: (num) =>
|
||||||
|
set((state) => {
|
||||||
|
state.totalTeams = num;
|
||||||
|
}),
|
||||||
|
|
||||||
|
addTeam: (team) =>
|
||||||
|
set((state) => {
|
||||||
|
state.teams = [team, ...state.teams];
|
||||||
|
}),
|
||||||
|
updateTeam: (teamID, data) =>
|
||||||
|
set((state) => {
|
||||||
|
const team = state.teams.find((t) => t.id === teamID);
|
||||||
|
if (!team) return;
|
||||||
|
|
||||||
|
Object.assign(team, data);
|
||||||
|
}),
|
||||||
|
|
||||||
|
setTeams: (teams) =>
|
||||||
|
set((state) => {
|
||||||
|
state.teams = teams;
|
||||||
|
}),
|
||||||
|
removeTeam: (teamID) =>
|
||||||
|
set((state) => {
|
||||||
|
state.teams = state.teams.filter((t) => t.id !== teamID);
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
);
|
||||||
16
src/Types/Team/Team.tsx
Normal file
16
src/Types/Team/Team.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { UserData } from "Types/User/User";
|
||||||
|
|
||||||
|
export interface TeamMember {
|
||||||
|
user: UserData;
|
||||||
|
joined_at: string; // ISO datetime string
|
||||||
|
permissions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Team {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
created_at: Date; // ISO datetime string
|
||||||
|
creator: TeamMember;
|
||||||
|
members: TeamMember[];
|
||||||
|
}
|
||||||
@@ -1,24 +1,3 @@
|
|||||||
export interface Team {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
creation_date: Date;
|
|
||||||
creator: TeamMember;
|
|
||||||
team_members: TeamMember[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TeamMember {
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
permissions: Permission[];
|
|
||||||
date_accepted: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Permission {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserData {
|
export interface UserData {
|
||||||
keycloak_id: string;
|
keycloak_id: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
31
src/main.tsx
31
src/main.tsx
@@ -11,11 +11,13 @@ import {
|
|||||||
virtualColor,
|
virtualColor,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import keycloak from "Api/Keycloak/Keycloak";
|
import keycloak from "Api/Keycloak/Keycloak";
|
||||||
import { AuthenticationStore } from "Stores/AuthenticationStore";
|
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 type { UserData } from "Types/User/User";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { IconForbid } from "@tabler/icons-react";
|
||||||
|
|
||||||
const colorSchemeManager = localStorageColorSchemeManager({
|
const colorSchemeManager = localStorageColorSchemeManager({
|
||||||
key: "my-app-color-scheme",
|
key: "my-app-color-scheme",
|
||||||
@@ -63,7 +65,7 @@ const theme = createTheme({
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
AuthenticationStore.setState({ is_loading: true });
|
useAuthenticationStore.setState({ is_loading: true });
|
||||||
keycloak
|
keycloak
|
||||||
.init({
|
.init({
|
||||||
onLoad: "check-sso",
|
onLoad: "check-sso",
|
||||||
@@ -78,20 +80,31 @@ async function bootstrap() {
|
|||||||
.then((authenticated: boolean) => {
|
.then((authenticated: boolean) => {
|
||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
keycloak.loadUserProfile().then((profile: KeycloakProfile) => {
|
keycloak.loadUserProfile().then((profile: KeycloakProfile) => {
|
||||||
AuthenticationStore.setState({
|
useAuthenticationStore.setState({
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
profile: profile,
|
profile: profile,
|
||||||
});
|
});
|
||||||
GetCurrentUserInfo().then((info: UserData | undefined) => {
|
GetCurrentUserInfo()
|
||||||
if (info && info.profile_picture_path)
|
.then((info: UserData | undefined) => {
|
||||||
AuthenticationStore.setState({
|
if (info && info.profile_picture_path)
|
||||||
profile_picture_path: info.profile_picture_path,
|
useAuthenticationStore.setState({
|
||||||
|
profile_picture_path: info.profile_picture_path,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notifications.show({
|
||||||
|
radius: "md",
|
||||||
|
title: "Ошибка получения данных пользователя",
|
||||||
|
message: "",
|
||||||
|
color: "red",
|
||||||
|
icon: <IconForbid />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log("Keycloak authentication failed.");
|
console.log("Keycloak authentication failed.");
|
||||||
AuthenticationStore.setState({
|
useAuthenticationStore.setState({
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user