Skip to content

Commit

Permalink
feat(dashboard): optimisation récup liste de datastores #521
Browse files Browse the repository at this point in the history
  • Loading branch information
ocruze committed Oct 1, 2024
1 parent c6ff94a commit bf4a82f
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 80 deletions.
9 changes: 8 additions & 1 deletion assets/@types/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,15 @@ import {
UserKeyDetailsResponseDtoUserKeyInfoDto,
UserKeyResponseDto,
MetadataResponseDto,
CommunityUserDto,
} from "./entrepot";

export interface CartesCommunityMember extends CommunityMemberDto {
community?: CommunityUserDto & {
is_sandbox?: boolean;
};
}

/** user */
export type CartesUser = {
id: string;
Expand All @@ -41,7 +48,7 @@ export type CartesUser = {
first_name?: string | null;
last_name?: string | null;
roles: string[];
communities_member: CommunityMemberDto[];
communities_member: CartesCommunityMember[];
account_creation_date: string;
last_api_call_date: string;
};
Expand Down
8 changes: 8 additions & 0 deletions assets/entrepot/api/datastore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ const get = (datastoreId: string, otherOptions: RequestInit = {}) => {
});
};

const getSandbox = (otherOptions: RequestInit = {}) => {
const url = SymfonyRouting.generate("cartesgouvfr_api_datastore_get_sandbox");
return jsonFetch<Datastore>(url, {
...otherOptions,
});
};

const getEndpoints = (datastoreId: string, query: { type?: string; open?: boolean }, otherOptions: RequestInit = {}) => {
const url = SymfonyRouting.generate("cartesgouvfr_api_datastore_get_endpoints", { datastoreId, ...query });
return jsonFetch<DatastoreEndpoint[]>(url, {
Expand Down Expand Up @@ -69,6 +76,7 @@ const removePermission = (datastoreId: string, permissionId: string) => {

const datastore = {
get,
getSandbox,
getEndpoints,
getPermission,
getPermissions,
Expand Down
6 changes: 3 additions & 3 deletions assets/entrepot/api/user.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import SymfonyRouting from "../../modules/Routing";

import { Datastore, UserKeyDetailedWithAccessesResponseDto, UserKeyWithAccessesResponseDto } from "../../@types/app";
import { CartesUser, Datastore, UserKeyDetailedWithAccessesResponseDto, UserKeyWithAccessesResponseDto } from "../../@types/app";
import { PermissionWithOfferingsDetailsResponseDto, UserKeyCreateDtoUserKeyInfoDto, UserKeyResponseDto, UserKeyUpdateDto } from "../../@types/entrepot";
import { jsonFetch } from "../../modules/jsonFetch";

const getMe = () => {
const getMe = (otherOptions: RequestInit = {}) => {
const url = SymfonyRouting.generate("cartesgouvfr_api_user_me");
return jsonFetch(url);
return jsonFetch<CartesUser>(url, { ...otherOptions });
};

const getDatastoresList = (otherOptions: RequestInit = {}) => {
Expand Down
179 changes: 107 additions & 72 deletions assets/entrepot/pages/dashboard/DashboardPro.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { fr } from "@codegouvfr/react-dsfr";
import Button from "@codegouvfr/react-dsfr/Button";
import { Tile } from "@codegouvfr/react-dsfr/Tile";
import { useMutation } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { declareComponentKeys } from "i18nifty";
import { useEffect } from "react";

import { CartesUser, Datastore } from "../../../@types/app";
import AppLayout from "../../../components/Layout/AppLayout";
import LoadingText from "../../../components/Utils/LoadingText";
import LoadingIcon from "../../../components/Utils/LoadingIcon";
import { datastoreNavItems } from "../../../config/datastoreNavItems";
import useDatastoreList from "../../../hooks/useDatastoreList";
import { Translations, useTranslation } from "../../../i18n/i18n";
import Translator from "../../../modules/Translator";
import RQKeys from "../../../modules/entrepot/RQKeys";
import { CartesApiException } from "../../../modules/jsonFetch";
import { routes } from "../../../router/router";
import { useApiEspaceCoStore } from "../../../stores/ApiEspaceCoStore";
Expand All @@ -22,13 +24,33 @@ import humanCoopSvgUrl from "@codegouvfr/react-dsfr/dsfr/artwork/pictograms/envi
import padlockSvgUrl from "@codegouvfr/react-dsfr/dsfr/artwork/pictograms/system/padlock.svg";

const DashboardPro = () => {
const datastoreListQuery = useDatastoreList();
const { t } = useTranslation("DashboardPro");

const navItems = datastoreNavItems();

const { user } = useAuthStore();
const user = useAuthStore((state) => state.user);
const setUser = useAuthStore((state) => state.setUser);
const isApiEspaceCoDefined = useApiEspaceCoStore((state) => state.isUrlDefined);

const { t } = useTranslation("DashboardPro");
const userQuery = useQuery<CartesUser, CartesApiException>({
queryKey: RQKeys.user_me(),
queryFn: ({ signal }) => api.user.getMe({ signal }),
initialData: user ?? undefined,
staleTime: 15000,
});

// requête exprès pour récupérer le datastore bac à sable
const sandboxDatastoreQuery = useQuery<Datastore, CartesApiException>({
queryKey: RQKeys.datastore("sandbox"),
queryFn: ({ signal }) => api.datastore.getSandbox({ signal }),
staleTime: 3600000,
});

useEffect(() => {
if (userQuery.data !== undefined) {
setUser(userQuery.data);
}
}, [setUser, userQuery.data]);

const { mutate } = useMutation<undefined, CartesApiException>({
mutationFn: () => {
Expand Down Expand Up @@ -57,78 +79,91 @@ const DashboardPro = () => {

return (
<AppLayout navItems={navItems} documentTitle={t("document_title")} infoBannerMsg={infoBannerMsg}>
{datastoreListQuery.isLoading ? (
<LoadingText />
) : (
<>
<h1>Bienvenue {user?.first_name ?? user?.user_name}</h1>

<div className={fr.cx("fr-grid-row", "fr-grid-row--gutters", "fr-mb-3w")}>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<Tile
linkProps={routes.my_account().link}
imageUrl={avatarSvgUrl}
desc="Consulter et modifier mes informations personnelles"
orientation="horizontal"
title="Mon compte"
/>
</div>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<h1>Bienvenue {user?.first_name ?? user?.user_name}</h1>

<div className={fr.cx("fr-grid-row", "fr-grid-row--gutters", "fr-mb-3w")}>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<Tile
linkProps={routes.my_account().link}
imageUrl={avatarSvgUrl}
desc="Consulter et modifier mes informations personnelles"
orientation="horizontal"
title="Mon compte"
/>
</div>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<Tile
linkProps={routes.my_access_keys().link}
imageUrl={padlockSvgUrl}
desc="Consulter et modifier mes clés d'accès aux services privés"
orientation="horizontal"
title="Mes clés d’accès"
/>
</div>
</div>

<h2>Espaces de travail {(sandboxDatastoreQuery.isLoading || userQuery.isFetching) && <LoadingIcon largeIcon={true} />}</h2>

<div className={fr.cx("fr-grid-row", "fr-grid-row--gutters")}>
{/* si l'utilisateur ne fait pas déjà partie du bac à sable, on affiche une Tile "spéciale" dont le click va ajouter l'utilisateur dans le bac à sable */}
{/* si l'utilisateur en fait déjà partie, on n'affiche pas cette Tile, on boucle tout simplement sur ses datastores (user.communities_member), voir plus bas */}
{sandboxDatastoreQuery.data !== undefined &&
user?.communities_member.find((community) => community.community?.datastore === sandboxDatastoreQuery.data._id) === undefined && (
<div className={fr.cx("fr-col-12", "fr-col-sm-6", "fr-col-md-4", "fr-col-lg-3")}>
<Tile
linkProps={routes.my_access_keys().link}
imageUrl={padlockSvgUrl}
desc="Consulter et modifier mes clés d'accès aux services privés"
orientation="horizontal"
title="Mes clés d’accès"
linkProps={{
...routes.datasheet_list({ datastoreId: sandboxDatastoreQuery.data._id }).link,
onClick: () => handleClick(sandboxDatastoreQuery.data._id),
}}
grey={true}
title={sandboxDatastoreQuery.data.name}
desc={t("datastore_for_tests")}
/>
</div>
</div>

<h2>Espaces de travail</h2>

<div className={fr.cx("fr-grid-row", "fr-grid-row--gutters")}>
{datastoreListQuery.data?.map((datastore) => {
const link = { ...routes.datasheet_list({ datastoreId: datastore._id }).link, onClick: () => handleClick(datastore._id) };
return (
<div key={datastore._id} className={fr.cx("fr-col-12", "fr-col-sm-6", "fr-col-md-4", "fr-col-lg-3")}>
<Tile
linkProps={link}
grey={true}
title={datastore.name}
desc={datastore.is_sandbox === true ? t("datastore_for_tests") : undefined}
/>
</div>
);
})}
</div>

<div className={fr.cx("fr-grid-row", "fr-grid-row--gutters")}>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<Tile
linkProps={routes.datastore_create_request().link}
imageUrl={mailSendSvgUrl}
desc="Contacter le support pour créer un nouvel espace de travail"
orientation="horizontal"
title={Translator.trans("datastore_creation_request.title")}
/>
</div>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
)}

{user?.communities_member.map((community) => {
const datastoreId = community.community?.datastore;
if (datastoreId === undefined) return null;

return (
<div key={datastoreId} className={fr.cx("fr-col-12", "fr-col-sm-6", "fr-col-md-4", "fr-col-lg-3")}>
<Tile
linkProps={routes.join_community().link}
imageUrl={humanCoopSvgUrl}
desc="Demander à rejoindre une communauté publique"
orientation="horizontal"
title={Translator.trans("communities_list.title")}
linkProps={routes.datasheet_list({ datastoreId }).link}
grey={true}
title={community.community?.name}
desc={community.community?.is_sandbox === true ? t("datastore_for_tests") : undefined}
/>
</div>
</div>

{isApiEspaceCoDefined() && (
<div className={fr.cx("fr-grid-row", "fr-grid-row--left", "fr-mt-4w")}>
<Button linkProps={routes.espaceco_community_list().link}>{t("espaceco_frontoffice_list")}</Button>
</div>
)}
</>
);
})}
</div>

<div className={fr.cx("fr-grid-row", "fr-grid-row--gutters")}>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<Tile
linkProps={routes.datastore_create_request().link}
imageUrl={mailSendSvgUrl}
desc="Contacter le support pour créer un nouvel espace de travail"
orientation="horizontal"
title={Translator.trans("datastore_creation_request.title")}
/>
</div>
<div className={fr.cx("fr-col-12", "fr-col-sm-6")}>
<Tile
linkProps={routes.join_community().link}
imageUrl={humanCoopSvgUrl}
desc="Demander à rejoindre une communauté publique"
orientation="horizontal"
title={Translator.trans("communities_list.title")}
/>
</div>
</div>

{isApiEspaceCoDefined() && (
<div className={fr.cx("fr-grid-row", "fr-grid-row--left", "fr-mt-4w")}>
<Button linkProps={routes.espaceco_community_list().link}>{t("espaceco_frontoffice_list")}</Button>
</div>
)}
</AppLayout>
);
Expand Down
1 change: 1 addition & 0 deletions assets/modules/entrepot/RQKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const RQKeys = {
my_keys: (): string[] => ["my_keys"],
my_key: (keyId: string): string[] => ["my_key", keyId],
my_permissions: (): string[] => ["my_permissions"],
user_me: (): string[] => ["user", "me"],

accesses_request: (fileIdentifier: string): string[] => ["accesses_request", fileIdentifier],
};
Expand Down
20 changes: 19 additions & 1 deletion src/Controller/Entrepot/DatastoreController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Exception\ApiException;
use App\Exception\CartesApiException;
use App\Services\EntrepotApi\DatastoreApiService;
use App\Services\EntrepotApi\ServiceAccount;
use App\Services\SandboxService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
Expand All @@ -26,10 +27,27 @@ class DatastoreController extends AbstractController implements ApiControllerInt
{
public function __construct(
private DatastoreApiService $datastoreApiService,
private SandboxService $sandboxService
private SandboxService $sandboxService,
private ServiceAccount $serviceAccount
) {
}

#[Route('/sandbox', name: 'get_sandbox', methods: ['GET'])]
public function getSandboxDatastore(): JsonResponse
{
try {
$sandboxCommunity = $this->serviceAccount->getSandboxCommunity();
$datastore = $sandboxCommunity['datastore'];

$datastore['is_sandbox'] = true;
$datastore['name'] = 'Découverte';

return $this->json($datastore);
} catch (ApiException $ex) {
throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex);
}
}

#[Route('/{datastoreId}', name: 'get', methods: ['GET'])]
public function getDatastore(string $datastoreId): JsonResponse
{
Expand Down
12 changes: 11 additions & 1 deletion src/Controller/Entrepot/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Dto\User\UserKeyDTO;
use App\Exception\ApiException;
use App\Exception\CartesApiException;
use App\Security\User;
use App\Services\EntrepotApi\ServiceAccount;
use App\Services\EntrepotApi\UserApiService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
Expand All @@ -30,7 +31,16 @@ public function __construct(
#[Route('/me', name: 'me')]
public function getCurrentUser(): JsonResponse
{
return $this->json($this->getUser());
/** @var User */
$user = $this->getUser();

$apiUserInfo = $this->userApiService->getMe();

if (array_key_exists('communities_member', $apiUserInfo)) {
$user->setCommunitiesMember($apiUserInfo['communities_member']);
}

return $this->json($user);
}

#[Route('/me/keys', name: 'keys')]
Expand Down
12 changes: 11 additions & 1 deletion src/Security/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class User implements UserInterface
private ?string $firstName;
private ?string $lastName;

/** @var array<string,mixed> */
/** @var array<mixed> */
private array $communitiesMember = [];

private \DateTimeInterface $accountCreationDate;
Expand Down Expand Up @@ -106,6 +106,16 @@ public function getCommunitiesMember(): array
return $this->communitiesMember;
}

/**
* @param array<mixed> $communitiesMember
*/
public function setCommunitiesMember(array $communitiesMember): self
{
$this->communitiesMember = $communitiesMember;

return $this;
}

public function getAccountCreationDate(): ?\DateTimeInterface
{
return $this->accountCreationDate;
Expand Down
Loading

0 comments on commit bf4a82f

Please sign in to comment.