Improve repository overview

- Sort repositories alphanumerically case insensitive per namespace
- Make the namespaces collapsible and store the collapsed state in local storage

Committed-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2023-08-22 19:59:53 +02:00
parent 9c0d064491
commit 2efcbfa759
11 changed files with 109 additions and 80 deletions

View File

@@ -65,6 +65,7 @@ export * from "./usePluginCenterAuthInfo";
export * from "./compare";
export * from "./utils";
export * from "./links";
export * from "./localStorage";
export { useNamespaceOptions, useGroupOptions, useUserOptions } from "./useAutocompleteOptions";
export { default as ApiProvider } from "./ApiProvider";

View File

@@ -0,0 +1,45 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { useEffect, useState } from "react";
export function useLocalStorage<T>(
key: string,
initialValue: T
): [value: T, setValue: (value: T | ((previousConfig: T) => T)) => void] {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
return initialValue;
}
});
useEffect(() => localStorage.setItem(key, JSON.stringify(value)), [key, value]);
return [value, setValue];
}

View File

@@ -30,7 +30,7 @@ import {
Repository,
RepositoryCollection,
RepositoryCreation,
RepositoryTypeCollection
RepositoryTypeCollection,
} from "@scm-manager/ui-types";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { apiClient } from "./apiclient";
@@ -72,7 +72,7 @@ export const useRepositories = (request?: UseRepositoriesRequest): ApiResult<Rep
}
return useQuery<RepositoryCollection, Error>(
["repositories", request?.namespace?.namespace, request?.search || "", request?.page || 0],
() => apiClient.get(`${link}?${createQueryString(queryParams)}`).then(response => response.json()),
() => apiClient.get(`${link}?${createQueryString(queryParams)}`).then((response) => response.json()),
{
enabled: !request?.disabled,
onSuccess: (repositories: RepositoryCollection) => {
@@ -80,7 +80,7 @@ export const useRepositories = (request?: UseRepositoriesRequest): ApiResult<Rep
repositories._embedded?.repositories.forEach((repository: Repository) => {
queryClient.setQueryData(["repository", repository.namespace, repository.name], repository);
});
}
},
}
);
};
@@ -98,14 +98,14 @@ const createRepository = (link: string) => {
}
return apiClient
.post(createLink, request.repository, "application/vnd.scmm-repository+json;v=2")
.then(response => {
.then((response) => {
const location = response.headers.get("Location");
if (!location) {
throw new Error("Server does not return required Location header");
}
return apiClient.get(location);
})
.then(response => response.json());
.then((response) => response.json());
};
};
@@ -117,10 +117,10 @@ export const useCreateRepository = () => {
const { mutate, data, isLoading, error } = useMutation<Repository, Error, CreateRepositoryRequest>(
createRepository(link),
{
onSuccess: repository => {
onSuccess: (repository) => {
queryClient.setQueryData(["repository", repository.namespace, repository.name], repository);
return queryClient.invalidateQueries(["repositories"]);
}
},
}
);
return {
@@ -129,7 +129,7 @@ export const useCreateRepository = () => {
},
isLoading,
error,
repository: data
repository: data,
};
};
@@ -139,7 +139,7 @@ export const useRepositoryTypes = () => useIndexJsonResource<RepositoryTypeColle
export const useRepository = (namespace: string, name: string): ApiResult<Repository> => {
const link = useRequiredIndexLink("repositories");
return useQuery<Repository, Error>(["repository", namespace, name], () =>
apiClient.get(concat(link, namespace, name)).then(response => response.json())
apiClient.get(concat(link, namespace, name)).then((response) => response.json())
);
};
@@ -150,7 +150,7 @@ export type UseDeleteRepositoryOptions = {
export const useDeleteRepository = (options?: UseDeleteRepositoryOptions) => {
const queryClient = useQueryClient();
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
repository => {
(repository) => {
const link = requiredLink(repository, "delete");
return apiClient.delete(link);
},
@@ -161,21 +161,21 @@ export const useDeleteRepository = (options?: UseDeleteRepositoryOptions) => {
}
queryClient.removeQueries(repoQueryKey(repository));
await queryClient.invalidateQueries(["repositories"]);
}
},
}
);
return {
remove: (repository: Repository) => mutate(repository),
isLoading,
error,
isDeleted: !!data
isDeleted: !!data,
};
};
export const useUpdateRepository = () => {
const queryClient = useQueryClient();
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
repository => {
(repository) => {
const link = requiredLink(repository, "update");
return apiClient.put(link, repository, "application/vnd.scmm-repository+json;v=2");
},
@@ -183,21 +183,21 @@ export const useUpdateRepository = () => {
onSuccess: async (_, repository) => {
await queryClient.invalidateQueries(repoQueryKey(repository));
await queryClient.invalidateQueries(["repositories"]);
}
},
}
);
return {
update: (repository: Repository) => mutate(repository),
isLoading,
error,
isUpdated: !!data
isUpdated: !!data,
};
};
export const useArchiveRepository = () => {
const queryClient = useQueryClient();
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
repository => {
(repository) => {
const link = requiredLink(repository, "archive");
return apiClient.post(link);
},
@@ -205,21 +205,21 @@ export const useArchiveRepository = () => {
onSuccess: async (_, repository) => {
await queryClient.invalidateQueries(repoQueryKey(repository));
await queryClient.invalidateQueries(["repositories"]);
}
},
}
);
return {
archive: (repository: Repository) => mutate(repository),
isLoading,
error,
isArchived: !!data
isArchived: !!data,
};
};
export const useUnarchiveRepository = () => {
const queryClient = useQueryClient();
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
repository => {
(repository) => {
const link = requiredLink(repository, "unarchive");
return apiClient.post(link);
},
@@ -227,35 +227,35 @@ export const useUnarchiveRepository = () => {
onSuccess: async (_, repository) => {
await queryClient.invalidateQueries(repoQueryKey(repository));
await queryClient.invalidateQueries(["repositories"]);
}
},
}
);
return {
unarchive: (repository: Repository) => mutate(repository),
isLoading,
error,
isUnarchived: !!data
isUnarchived: !!data,
};
};
export const useRunHealthCheck = () => {
const queryClient = useQueryClient();
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
repository => {
(repository) => {
const link = requiredLink(repository, "runHealthCheck");
return apiClient.post(link);
},
{
onSuccess: async (_, repository) => {
await queryClient.invalidateQueries(repoQueryKey(repository));
}
},
}
);
return {
runHealthCheck: (repository: Repository) => mutate(repository),
isLoading,
error,
isRunning: !!data
isRunning: !!data,
};
};
@@ -264,7 +264,7 @@ export const useExportInfo = (repository: Repository): ApiResultWithFetching<Exp
//TODO Refetch while exporting to update the page
const { isLoading, isFetching, error, data } = useQuery<ExportInfo, Error>(
["repository", repository.namespace, repository.name, "exportInfo"],
() => apiClient.get(link).then(response => response.json()),
() => apiClient.get(link).then((response) => response.json()),
{}
);
@@ -272,7 +272,7 @@ export const useExportInfo = (repository: Repository): ApiResultWithFetching<Exp
isLoading,
isFetching,
error: error instanceof NotFoundError ? null : error,
data
data,
};
};
@@ -315,14 +315,14 @@ export const useExportRepository = () => {
const id = setInterval(() => {
apiClient
.get(infolink)
.then(r => r.json())
.then((r) => r.json())
.then((info: ExportInfo) => {
if (info._links.download) {
clearInterval(id);
resolve(info);
}
})
.catch(e => {
.catch((e) => {
clearInterval(id);
reject(e);
});
@@ -335,21 +335,21 @@ export const useExportRepository = () => {
onSuccess: async (_, { repository }) => {
await queryClient.invalidateQueries(repoQueryKey(repository));
await queryClient.invalidateQueries(["repositories"]);
}
},
}
);
return {
exportRepository: (repository: Repository, options: ExportOptions) => mutate({ repository, options }),
isLoading,
error,
data
data,
};
};
export const usePaths = (repository: Repository, revision: string): ApiResult<Paths> => {
const link = requiredLink(repository, "paths").replace("{revision}", revision);
return useQuery<Paths, Error>(repoQueryKey(repository, "paths", revision), () =>
apiClient.get(link).then(response => response.json())
apiClient.get(link).then((response) => response.json())
);
};
@@ -370,7 +370,7 @@ export const useRenameRepository = (repository: Repository) => {
const { mutate, isLoading, error, data } = useMutation<unknown, Error, RenameRepositoryRequest>(
({ name, namespace }) => apiClient.post(url, { namespace, name }, "application/vnd.scmm-repository+json;v=2"),
{
onSuccess: () => queryClient.removeQueries(repoQueryKey(repository))
onSuccess: () => queryClient.removeQueries(repoQueryKey(repository)),
}
);
@@ -378,7 +378,7 @@ export const useRenameRepository = (repository: Repository) => {
renameRepository: (namespace: string, name: string) => mutate({ namespace, name }),
isLoading,
error,
isRenamed: !!data
isRenamed: !!data,
};
};