mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-08 16:42:10 +01:00
implement react-query for edge-cases (#1711)
When initially implementing react-query, we focussed on core features. This pull request now replaces the remaining apiClient usages in ui-components and ui-webapp with react-query hooks.
This commit is contained in:
committed by
GitHub
parent
2cd46ce8a0
commit
e1239aff92
82
scm-ui/ui-api/src/apiKeys.ts
Normal file
82
scm-ui/ui-api/src/apiKeys.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 { ApiKey, ApiKeyCreation, ApiKeysCollection, ApiKeyWithToken, Me, User } from "@scm-manager/ui-types";
|
||||
import { ApiResult } from "./base";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { apiClient } from "./apiclient";
|
||||
import { requiredLink } from "./links";
|
||||
|
||||
const CONTENT_TYPE_API_KEY = "application/vnd.scmm-apiKey+json;v=2";
|
||||
|
||||
export const useApiKeys = (user: User | Me): ApiResult<ApiKeysCollection> =>
|
||||
useQuery(["user", user.name, "apiKeys"], () => apiClient.get(requiredLink(user, "apiKeys")).then((r) => r.json()));
|
||||
|
||||
const createApiKey =
|
||||
(link: string) =>
|
||||
async (key: ApiKeyCreation): Promise<ApiKeyWithToken> => {
|
||||
const creationResponse = await apiClient.post(link, key, CONTENT_TYPE_API_KEY);
|
||||
const location = creationResponse.headers.get("Location");
|
||||
if (!location) {
|
||||
throw new Error("Server does not return required Location header");
|
||||
}
|
||||
const locationResponse = await apiClient.get(location);
|
||||
const [apiKey, token] = await Promise.all<ApiKey, string>([locationResponse.json(), creationResponse.text()]);
|
||||
return { ...apiKey, token } as ApiKeyWithToken;
|
||||
};
|
||||
|
||||
export const useCreateApiKey = (user: User | Me, apiKeys: ApiKeysCollection) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate, data, isLoading, error, reset } = useMutation<ApiKeyWithToken, Error, ApiKeyCreation>(
|
||||
createApiKey(requiredLink(apiKeys, "create")),
|
||||
{
|
||||
onSuccess: () => queryClient.invalidateQueries(["user", user.name, "apiKeys"]),
|
||||
}
|
||||
);
|
||||
return {
|
||||
create: (key: ApiKeyCreation) => mutate(key),
|
||||
isLoading,
|
||||
error,
|
||||
apiKey: data,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
|
||||
export const useDeleteApiKey = (user: User | Me) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate, isLoading, error, data } = useMutation<unknown, Error, ApiKey>(
|
||||
(apiKey) => {
|
||||
const deleteUrl = requiredLink(apiKey, "delete");
|
||||
return apiClient.delete(deleteUrl);
|
||||
},
|
||||
{
|
||||
onSuccess: () => queryClient.invalidateQueries(["user", user.name, "apiKeys"]),
|
||||
}
|
||||
);
|
||||
return {
|
||||
remove: (apiKey: ApiKey) => mutate(apiKey),
|
||||
isLoading,
|
||||
error,
|
||||
isDeleted: !!data,
|
||||
};
|
||||
};
|
||||
@@ -22,22 +22,24 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { IndexResources, Link } from "@scm-manager/ui-types";
|
||||
import { HalRepresentation, IndexResources, Link } from "@scm-manager/ui-types";
|
||||
import { useQuery } from "react-query";
|
||||
import { apiClient } from "./apiclient";
|
||||
import { useLegacyContext } from "./LegacyContext";
|
||||
import { MissingLinkError, UnauthorizedError } from "./errors";
|
||||
import { requiredLink } from "./links";
|
||||
|
||||
export type ApiResult<T> = {
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
data?: T;
|
||||
};
|
||||
export type DeleteFunction<T> = (entity: T) => void;
|
||||
|
||||
export const useIndex = (): ApiResult<IndexResources> => {
|
||||
const legacy = useLegacyContext();
|
||||
return useQuery<IndexResources, Error>("index", () => apiClient.get("/").then(response => response.json()), {
|
||||
onSuccess: index => {
|
||||
return useQuery<IndexResources, Error>("index", () => apiClient.get("/").then((response) => response.json()), {
|
||||
onSuccess: (index) => {
|
||||
// ensure legacy code is notified
|
||||
if (legacy.onIndexFetched) {
|
||||
legacy.onIndexFetched(index);
|
||||
@@ -49,7 +51,7 @@ export const useIndex = (): ApiResult<IndexResources> => {
|
||||
// This only happens once because the error response automatically invalidates the cookie.
|
||||
// In this event, we have to try the request once again.
|
||||
return error instanceof UnauthorizedError && failureCount === 0;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -94,7 +96,20 @@ export const useVersion = (): string => {
|
||||
|
||||
export const useIndexJsonResource = <T>(name: string): ApiResult<T> => {
|
||||
const link = useIndexLink(name);
|
||||
return useQuery<T, Error>(name, () => apiClient.get(link!).then(response => response.json()), {
|
||||
enabled: !!link
|
||||
return useQuery<T, Error>(name, () => apiClient.get(link!).then((response) => response.json()), {
|
||||
enabled: !!link,
|
||||
});
|
||||
};
|
||||
|
||||
export const useJsonResource = <T>(entity: HalRepresentation, name: string, key: string[]): ApiResult<T> =>
|
||||
useQuery<T, Error>(key, () => apiClient.get(requiredLink(entity, name)).then((response) => response.json()));
|
||||
|
||||
export function fetchResourceFromLocationHeader(response: Response) {
|
||||
const location = response.headers.get("Location");
|
||||
if (!location) {
|
||||
throw new Error("Server does not return required Location header");
|
||||
}
|
||||
return apiClient.get(location);
|
||||
}
|
||||
|
||||
export const getResponseJson = (response: Response) => response.json();
|
||||
|
||||
@@ -36,9 +36,9 @@ describe("Test branches hooks", () => {
|
||||
type: "hg",
|
||||
_links: {
|
||||
branches: {
|
||||
href: "/hog/branches"
|
||||
}
|
||||
}
|
||||
href: "/hog/branches",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const develop: Branch = {
|
||||
@@ -46,16 +46,16 @@ describe("Test branches hooks", () => {
|
||||
revision: "42",
|
||||
_links: {
|
||||
delete: {
|
||||
href: "/hog/branches/develop"
|
||||
}
|
||||
}
|
||||
href: "/hog/branches/develop",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const branches: BranchCollection = {
|
||||
_embedded: {
|
||||
branches: [develop]
|
||||
branches: [develop],
|
||||
},
|
||||
_links: {}
|
||||
_links: {},
|
||||
};
|
||||
|
||||
const queryClient = createInfiniteCachingClient();
|
||||
@@ -73,7 +73,7 @@ describe("Test branches hooks", () => {
|
||||
fetchMock.getOnce("/api/v2/hog/branches", branches);
|
||||
|
||||
const { result, waitFor } = renderHook(() => useBranches(repository), {
|
||||
wrapper: createWrapper(undefined, queryClient)
|
||||
wrapper: createWrapper(undefined, queryClient),
|
||||
});
|
||||
await waitFor(() => {
|
||||
return !!result.current.data;
|
||||
@@ -94,7 +94,7 @@ describe("Test branches hooks", () => {
|
||||
"repository",
|
||||
"hitchhiker",
|
||||
"heart-of-gold",
|
||||
"branches"
|
||||
"branches",
|
||||
]);
|
||||
expect(data).toEqual(branches);
|
||||
});
|
||||
@@ -105,7 +105,7 @@ describe("Test branches hooks", () => {
|
||||
fetchMock.getOnce("/api/v2/hog/branches/develop", develop);
|
||||
|
||||
const { result, waitFor } = renderHook(() => useBranch(repository, "develop"), {
|
||||
wrapper: createWrapper(undefined, queryClient)
|
||||
wrapper: createWrapper(undefined, queryClient),
|
||||
});
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
@@ -128,14 +128,14 @@ describe("Test branches hooks", () => {
|
||||
fetchMock.postOnce("/api/v2/hog/branches", {
|
||||
status: 201,
|
||||
headers: {
|
||||
Location: "/hog/branches/develop"
|
||||
}
|
||||
Location: "/hog/branches/develop",
|
||||
},
|
||||
});
|
||||
|
||||
fetchMock.getOnce("/api/v2/hog/branches/develop", develop);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useCreateBranch(repository), {
|
||||
wrapper: createWrapper(undefined, queryClient)
|
||||
wrapper: createWrapper(undefined, queryClient),
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
@@ -160,7 +160,7 @@ describe("Test branches hooks", () => {
|
||||
"hitchhiker",
|
||||
"heart-of-gold",
|
||||
"branch",
|
||||
"develop"
|
||||
"develop",
|
||||
]);
|
||||
expect(branch).toEqual(develop);
|
||||
});
|
||||
@@ -177,11 +177,11 @@ describe("Test branches hooks", () => {
|
||||
describe("useDeleteBranch tests", () => {
|
||||
const deleteBranch = async () => {
|
||||
fetchMock.deleteOnce("/api/v2/hog/branches/develop", {
|
||||
status: 204
|
||||
status: 204,
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useDeleteBranch(repository), {
|
||||
wrapper: createWrapper(undefined, queryClient)
|
||||
wrapper: createWrapper(undefined, queryClient),
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
@@ -198,12 +198,12 @@ describe("Test branches hooks", () => {
|
||||
expect(isDeleted).toBe(true);
|
||||
});
|
||||
|
||||
it("should invalidate branch", async () => {
|
||||
it("should delete branch cache", async () => {
|
||||
queryClient.setQueryData(["repository", "hitchhiker", "heart-of-gold", "branch", "develop"], develop);
|
||||
await deleteBranch();
|
||||
|
||||
const queryState = queryClient.getQueryState(["repository", "hitchhiker", "heart-of-gold", "branch", "develop"]);
|
||||
expect(queryState!.isInvalidated).toBe(true);
|
||||
expect(queryState).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should invalidate cached branches list", async () => {
|
||||
|
||||
@@ -88,7 +88,7 @@ export const useDeleteBranch = (repository: Repository) => {
|
||||
},
|
||||
{
|
||||
onSuccess: async (_, branch) => {
|
||||
await queryClient.invalidateQueries(branchQueryKey(repository, branch));
|
||||
queryClient.removeQueries(branchQueryKey(repository, branch));
|
||||
await queryClient.invalidateQueries(repoQueryKey(repository, "branches"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,21 +21,13 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { File } from "@scm-manager/ui-types";
|
||||
import { useQuery } from "react-query";
|
||||
import { apiClient } from "./apiclient";
|
||||
import { requiredLink } from "./links";
|
||||
import { ApiResult } from "./base";
|
||||
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
|
||||
export const CONTENT_TYPE_PASSWORD_CHANGE = "application/vnd.scmm-passwordChange+json;v=2";
|
||||
export function changePassword(url: string, oldPassword: string, newPassword: string) {
|
||||
return apiClient
|
||||
.put(
|
||||
url,
|
||||
{
|
||||
oldPassword,
|
||||
newPassword
|
||||
},
|
||||
CONTENT_TYPE_PASSWORD_CHANGE
|
||||
)
|
||||
.then(response => {
|
||||
return response;
|
||||
});
|
||||
}
|
||||
export const useFileContent = (file: File): ApiResult<string> => {
|
||||
const selfLink = requiredLink(file, "self");
|
||||
return useQuery(["fileContent", selfLink], () => apiClient.get(selfLink).then((response) => response.text()));
|
||||
};
|
||||
@@ -22,11 +22,93 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { ApiResult, useRequiredIndexLink } from "./base";
|
||||
import { useQuery } from "react-query";
|
||||
import { ApiResult, fetchResourceFromLocationHeader, getResponseJson, useRequiredIndexLink } from "./base";
|
||||
import { useMutation, useQuery } from "react-query";
|
||||
import { apiClient } from "./apiclient";
|
||||
import { Repository, RepositoryCreation, RepositoryType, RepositoryUrlImport } from "@scm-manager/ui-types";
|
||||
import { requiredLink } from "./links";
|
||||
|
||||
export const useImportLog = (logId: string) : ApiResult<string> => {
|
||||
export const useImportLog = (logId: string): ApiResult<string> => {
|
||||
const link = useRequiredIndexLink("importLog").replace("{logId}", logId);
|
||||
return useQuery<string, Error>(["importLog", logId], () => apiClient.get(link).then(response => response.text()));
|
||||
}
|
||||
return useQuery<string, Error>(["importLog", logId], () => apiClient.get(link).then((response) => response.text()));
|
||||
};
|
||||
|
||||
export const useImportRepositoryFromUrl = (repositoryType: RepositoryType) => {
|
||||
const url = requiredLink(repositoryType, "import", "url");
|
||||
const { isLoading, error, data, mutate } = useMutation<Repository, Error, RepositoryUrlImport>((repo) =>
|
||||
apiClient
|
||||
.post(url, repo, "application/vnd.scmm-repository+json;v=2")
|
||||
.then(fetchResourceFromLocationHeader)
|
||||
.then(getResponseJson)
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
importRepositoryFromUrl: (repository: RepositoryUrlImport) => mutate(repository),
|
||||
importedRepository: data,
|
||||
};
|
||||
};
|
||||
|
||||
const importRepository = (url: string, repository: RepositoryCreation, file: File, password?: string) => {
|
||||
return apiClient
|
||||
.postBinary(url, (formData) => {
|
||||
formData.append("bundle", file, file?.name);
|
||||
formData.append("repository", JSON.stringify({ ...repository, password }));
|
||||
})
|
||||
.then(fetchResourceFromLocationHeader)
|
||||
.then(getResponseJson);
|
||||
};
|
||||
|
||||
type ImportRepositoryFromBundleRequest = {
|
||||
repository: RepositoryCreation;
|
||||
file: File;
|
||||
compressed?: boolean;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
export const useImportRepositoryFromBundle = (repositoryType: RepositoryType) => {
|
||||
const url = requiredLink(repositoryType, "import", "bundle");
|
||||
const { isLoading, error, data, mutate } = useMutation<Repository, Error, ImportRepositoryFromBundleRequest>(
|
||||
({ repository, file, compressed, password }) =>
|
||||
importRepository(compressed ? url + "?compressed=true" : url, repository, file, password)
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
importRepositoryFromBundle: (repository: RepositoryCreation, file: File, compressed?: boolean, password?: string) =>
|
||||
mutate({
|
||||
repository,
|
||||
file,
|
||||
compressed,
|
||||
password,
|
||||
}),
|
||||
importedRepository: data,
|
||||
};
|
||||
};
|
||||
|
||||
type ImportFullRepositoryRequest = {
|
||||
repository: RepositoryCreation;
|
||||
file: File;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
export const useImportFullRepository = (repositoryType: RepositoryType) => {
|
||||
const { isLoading, error, data, mutate } = useMutation<Repository, Error, ImportFullRepositoryRequest>(
|
||||
({ repository, file, password }) =>
|
||||
importRepository(requiredLink(repositoryType, "import", "fullImport"), repository, file, password)
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
importFullRepository: (repository: RepositoryCreation, file: File, password?: string) =>
|
||||
mutate({
|
||||
repository,
|
||||
file,
|
||||
password,
|
||||
}),
|
||||
importedRepository: data,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -50,6 +50,9 @@ export * from "./import";
|
||||
export * from "./diff";
|
||||
export * from "./notifications";
|
||||
export * from "./configLink";
|
||||
export * from "./apiKeys";
|
||||
export * from "./publicKeys";
|
||||
export * from "./fileContent";
|
||||
export * from "./history";
|
||||
export * from "./contentType";
|
||||
export * from "./annotations";
|
||||
|
||||
@@ -60,4 +60,40 @@ describe("requireLink tests", () => {
|
||||
};
|
||||
expect(() => requiredLink(object, "spaceship")).toThrowError();
|
||||
});
|
||||
|
||||
it("should return sub-link if it exists", () => {
|
||||
const object = {
|
||||
_links: {
|
||||
spaceship: [
|
||||
{
|
||||
name: "one",
|
||||
href: "/v2/one"
|
||||
},
|
||||
{
|
||||
name: "two",
|
||||
href: "/v2/two"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
expect(requiredLink(object, "spaceship", "one")).toBe("/v2/one");
|
||||
});
|
||||
|
||||
it("should throw error, if sub-link does not exist in link array", () => {
|
||||
const object = {
|
||||
_links: {
|
||||
spaceship: [
|
||||
{
|
||||
name: "one",
|
||||
href: "/v2/one"
|
||||
},
|
||||
{
|
||||
name: "two",
|
||||
href: "/v2/two"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
expect(() => requiredLink(object, "spaceship", "three")).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,14 +21,32 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { HalRepresentation } from "@scm-manager/ui-types";
|
||||
import { HalRepresentation, Link } from "@scm-manager/ui-types";
|
||||
import { MissingLinkError } from "./errors";
|
||||
|
||||
export const requiredLink = (object: HalRepresentation, name: string) => {
|
||||
export const requiredLink = (object: HalRepresentation, name: string, subName?: string): string => {
|
||||
const link = object._links[name];
|
||||
if (!link) {
|
||||
throw new MissingLinkError(`could not find link with name ${name}`);
|
||||
}
|
||||
if (Array.isArray(link)) {
|
||||
if (subName) {
|
||||
const subLink = link.find((l: Link) => l.name === subName);
|
||||
if (subLink) {
|
||||
return subLink.href;
|
||||
}
|
||||
throw new Error(`could not return href, sub-link ${subName} in ${name} does not exist`);
|
||||
}
|
||||
throw new Error(`could not return href, link ${name} is a multi link`);
|
||||
}
|
||||
return link.href;
|
||||
};
|
||||
|
||||
export const objectLink = (object: HalRepresentation, name: string) => {
|
||||
const link = object._links[name];
|
||||
if (!link) {
|
||||
return null;
|
||||
}
|
||||
if (Array.isArray(link)) {
|
||||
throw new Error(`could not return href, link ${name} is a multi link`);
|
||||
}
|
||||
|
||||
@@ -21,18 +21,21 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { ApiResult, useIndexJsonResource } from "./base";
|
||||
import { ApiResult, useIndexJsonResource, useJsonResource } from "./base";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import {
|
||||
GlobalPermissionsCollection,
|
||||
Group,
|
||||
Namespace,
|
||||
Permission,
|
||||
PermissionCollection,
|
||||
PermissionCreateEntry,
|
||||
Repository,
|
||||
RepositoryVerbs
|
||||
RepositoryVerbs,
|
||||
User,
|
||||
} from "@scm-manager/ui-types";
|
||||
import { apiClient } from "./apiclient";
|
||||
import { requiredLink } from "./links";
|
||||
import { objectLink, requiredLink } from "./links";
|
||||
import { repoQueryKey } from "./keys";
|
||||
import { useRepositoryRoles } from "./repository-roles";
|
||||
|
||||
@@ -40,6 +43,9 @@ export const useRepositoryVerbs = (): ApiResult<RepositoryVerbs> => {
|
||||
return useIndexJsonResource<RepositoryVerbs>("repositoryVerbs");
|
||||
};
|
||||
|
||||
/**
|
||||
* *IMPORTANT NOTE:* These are actually *REPOSITORY* permissions.
|
||||
*/
|
||||
export const useAvailablePermissions = () => {
|
||||
const roles = useRepositoryRoles();
|
||||
const verbs = useRepositoryVerbs();
|
||||
@@ -47,14 +53,14 @@ export const useAvailablePermissions = () => {
|
||||
if (roles.data && verbs.data) {
|
||||
data = {
|
||||
repositoryVerbs: verbs.data.verbs,
|
||||
repositoryRoles: roles.data._embedded.repositoryRoles
|
||||
repositoryRoles: roles.data._embedded.repositoryRoles,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading: roles.isLoading || verbs.isLoading,
|
||||
error: roles.error || verbs.error,
|
||||
data
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -73,21 +79,21 @@ const createQueryKey = (namespaceOrRepository: Namespace | Repository) => {
|
||||
export const usePermissions = (namespaceOrRepository: Namespace | Repository): ApiResult<PermissionCollection> => {
|
||||
const link = requiredLink(namespaceOrRepository, "permissions");
|
||||
const queryKey = createQueryKey(namespaceOrRepository);
|
||||
return useQuery<PermissionCollection, Error>(queryKey, () => apiClient.get(link).then(response => response.json()));
|
||||
return useQuery<PermissionCollection, Error>(queryKey, () => apiClient.get(link).then((response) => response.json()));
|
||||
};
|
||||
|
||||
const createPermission = (link: string) => {
|
||||
return (permission: PermissionCreateEntry) => {
|
||||
return apiClient
|
||||
.post(link, permission, "application/vnd.scmm-repositoryPermission+json")
|
||||
.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());
|
||||
};
|
||||
};
|
||||
|
||||
@@ -100,21 +106,21 @@ export const useCreatePermission = (namespaceOrRepository: Namespace | Repositor
|
||||
onSuccess: () => {
|
||||
const queryKey = createQueryKey(namespaceOrRepository);
|
||||
return queryClient.invalidateQueries(queryKey);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
create: (permission: PermissionCreateEntry) => mutate(permission),
|
||||
permission: data
|
||||
permission: data,
|
||||
};
|
||||
};
|
||||
|
||||
export const useUpdatePermission = (namespaceOrRepository: Namespace | Repository) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { isLoading, error, mutate, data } = useMutation<unknown, Error, Permission>(
|
||||
permission => {
|
||||
(permission) => {
|
||||
const link = requiredLink(permission, "update");
|
||||
return apiClient.put(link, permission, "application/vnd.scmm-repositoryPermission+json");
|
||||
},
|
||||
@@ -122,21 +128,21 @@ export const useUpdatePermission = (namespaceOrRepository: Namespace | Repositor
|
||||
onSuccess: () => {
|
||||
const queryKey = createQueryKey(namespaceOrRepository);
|
||||
return queryClient.invalidateQueries(queryKey);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
update: (permission: Permission) => mutate(permission),
|
||||
isUpdated: !!data
|
||||
isUpdated: !!data,
|
||||
};
|
||||
};
|
||||
|
||||
export const useDeletePermission = (namespaceOrRepository: Namespace | Repository) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { isLoading, error, mutate, data } = useMutation<unknown, Error, Permission>(
|
||||
permission => {
|
||||
(permission) => {
|
||||
const link = requiredLink(permission, "delete");
|
||||
return apiClient.delete(link);
|
||||
},
|
||||
@@ -144,13 +150,53 @@ export const useDeletePermission = (namespaceOrRepository: Namespace | Repositor
|
||||
onSuccess: () => {
|
||||
const queryKey = createQueryKey(namespaceOrRepository);
|
||||
return queryClient.invalidateQueries(queryKey);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
remove: (permission: Permission) => mutate(permission),
|
||||
isDeleted: !!data
|
||||
isDeleted: !!data,
|
||||
};
|
||||
};
|
||||
|
||||
const userPermissionsKey = (user: User) => ["user", user.name, "permissions"];
|
||||
const groupPermissionsKey = (group: Group) => ["group", group.name, "permissions"];
|
||||
|
||||
export const useGroupPermissions = (group: Group) =>
|
||||
useJsonResource<GlobalPermissionsCollection>(group, "permissions", groupPermissionsKey(group));
|
||||
export const useUserPermissions = (user: User) =>
|
||||
useJsonResource<GlobalPermissionsCollection>(user, "permissions", userPermissionsKey(user));
|
||||
export const useAvailableGlobalPermissions = () =>
|
||||
useIndexJsonResource<Omit<GlobalPermissionsCollection, "_links">>("permissions");
|
||||
|
||||
const useSetEntityPermissions = (permissionCollection?: GlobalPermissionsCollection, key?: string[]) => {
|
||||
const queryClient = useQueryClient();
|
||||
const url = permissionCollection ? objectLink(permissionCollection, "overwrite") : null;
|
||||
const { isLoading, error, mutate, data } = useMutation<unknown, Error, string[]>(
|
||||
(permissions) =>
|
||||
apiClient.put(
|
||||
url!,
|
||||
{
|
||||
permissions,
|
||||
},
|
||||
"application/vnd.scmm-permissionCollection+json;v=2"
|
||||
),
|
||||
{
|
||||
onSuccess: () => queryClient.invalidateQueries(key),
|
||||
}
|
||||
);
|
||||
const setPermissions = (permissions: string[]) => mutate(permissions);
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
setPermissions: url ? setPermissions : undefined,
|
||||
isUpdated: !!data,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSetUserPermissions = (user: User, permissions?: GlobalPermissionsCollection) =>
|
||||
useSetEntityPermissions(permissions, userPermissionsKey(user));
|
||||
export const useSetGroupPermissions = (group: Group, permissions?: GlobalPermissionsCollection) =>
|
||||
useSetEntityPermissions(permissions, groupPermissionsKey(group));
|
||||
|
||||
83
scm-ui/ui-api/src/publicKeys.ts
Normal file
83
scm-ui/ui-api/src/publicKeys.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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 { Me, PublicKey, PublicKeyCreation, PublicKeysCollection, User } from "@scm-manager/ui-types";
|
||||
import { ApiResult } from "./base";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { apiClient } from "./apiclient";
|
||||
import { requiredLink } from "./links";
|
||||
|
||||
export const CONTENT_TYPE_PUBLIC_KEY = "application/vnd.scmm-publicKey+json;v=2";
|
||||
|
||||
export const usePublicKeys = (user: User | Me): ApiResult<PublicKeysCollection> =>
|
||||
useQuery(["user", user.name, "publicKeys"], () =>
|
||||
apiClient.get(requiredLink(user, "publicKeys")).then((r) => r.json())
|
||||
);
|
||||
|
||||
const createPublicKey =
|
||||
(link: string) =>
|
||||
async (key: PublicKeyCreation): Promise<PublicKey> => {
|
||||
const creationResponse = await apiClient.post(link, key, CONTENT_TYPE_PUBLIC_KEY);
|
||||
const location = creationResponse.headers.get("Location");
|
||||
if (!location) {
|
||||
throw new Error("Server does not return required Location header");
|
||||
}
|
||||
const apiKeyResponse = await apiClient.get(location);
|
||||
return apiKeyResponse.json();
|
||||
};
|
||||
|
||||
export const useCreatePublicKey = (user: User | Me, publicKeys: PublicKeysCollection) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate, data, isLoading, error, reset } = useMutation<PublicKey, Error, PublicKeyCreation>(
|
||||
createPublicKey(requiredLink(publicKeys, "create")),
|
||||
{
|
||||
onSuccess: () => queryClient.invalidateQueries(["user", user.name, "publicKeys"]),
|
||||
}
|
||||
);
|
||||
return {
|
||||
create: (key: PublicKeyCreation) => mutate(key),
|
||||
isLoading,
|
||||
error,
|
||||
apiKey: data,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
|
||||
export const useDeletePublicKey = (user: User | Me) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate, isLoading, error, data } = useMutation<unknown, Error, PublicKey>(
|
||||
(publicKey) => {
|
||||
const deleteUrl = requiredLink(publicKey, "delete");
|
||||
return apiClient.delete(deleteUrl);
|
||||
},
|
||||
{
|
||||
onSuccess: () => queryClient.invalidateQueries(["user", user.name, "publicKeys"]),
|
||||
}
|
||||
);
|
||||
return {
|
||||
remove: (publicKey: PublicKey) => mutate(publicKey),
|
||||
isLoading,
|
||||
error,
|
||||
isDeleted: !!data,
|
||||
};
|
||||
};
|
||||
@@ -30,17 +30,17 @@ import {
|
||||
Repository,
|
||||
RepositoryCollection,
|
||||
RepositoryCreation,
|
||||
RepositoryTypeCollection
|
||||
RepositoryTypeCollection,
|
||||
} from "@scm-manager/ui-types";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { apiClient } from "./apiclient";
|
||||
import { ApiResult, useIndexJsonResource, useRequiredIndexLink } from "./base";
|
||||
import { createQueryString } from "./utils";
|
||||
import { requiredLink } from "./links";
|
||||
import { objectLink, requiredLink } from "./links";
|
||||
import { repoQueryKey } from "./keys";
|
||||
import { concat } from "./urls";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NotFoundError } from "./errors";
|
||||
import { MissingLinkError, NotFoundError } from "./errors";
|
||||
|
||||
export type UseRepositoriesRequest = {
|
||||
namespace?: Namespace;
|
||||
@@ -56,7 +56,7 @@ export const useRepositories = (request?: UseRepositoriesRequest): ApiResult<Rep
|
||||
const link = namespaceLink || indexLink;
|
||||
|
||||
const queryParams: Record<string, string> = {
|
||||
sortBy: "namespaceAndName"
|
||||
sortBy: "namespaceAndName",
|
||||
};
|
||||
if (request?.search) {
|
||||
queryParams.q = request.search;
|
||||
@@ -66,7 +66,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) => {
|
||||
@@ -74,7 +74,7 @@ export const useRepositories = (request?: UseRepositoriesRequest): ApiResult<Rep
|
||||
repositories._embedded.repositories.forEach((repository: Repository) => {
|
||||
queryClient.setQueryData(["repository", repository.namespace, repository.name], repository);
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -92,14 +92,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());
|
||||
};
|
||||
};
|
||||
|
||||
@@ -111,10 +111,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 {
|
||||
@@ -123,7 +123,7 @@ export const useCreateRepository = () => {
|
||||
},
|
||||
isLoading,
|
||||
error,
|
||||
repository: data
|
||||
repository: data,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -133,7 +133,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())
|
||||
);
|
||||
};
|
||||
|
||||
@@ -144,7 +144,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);
|
||||
},
|
||||
@@ -155,21 +155,21 @@ export const useDeleteRepository = (options?: UseDeleteRepositoryOptions) => {
|
||||
}
|
||||
await queryClient.invalidateQueries(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");
|
||||
},
|
||||
@@ -177,21 +177,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);
|
||||
},
|
||||
@@ -199,21 +199,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);
|
||||
},
|
||||
@@ -221,35 +221,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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -258,14 +258,14 @@ export const useExportInfo = (repository: Repository): ApiResult<ExportInfo> =>
|
||||
//TODO Refetch while exporting to update the page
|
||||
const { isLoading, 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()),
|
||||
{}
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error: error instanceof NotFoundError ? null : error,
|
||||
data
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -308,14 +308,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);
|
||||
});
|
||||
@@ -328,20 +328,49 @@ 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())
|
||||
);
|
||||
};
|
||||
|
||||
type RenameRepositoryRequest = {
|
||||
name: string;
|
||||
namespace: string;
|
||||
};
|
||||
|
||||
export const useRenameRepository = (repository: Repository) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const url = objectLink(repository, "renameWithNamespace") || objectLink(repository, "rename");
|
||||
|
||||
if (!url) {
|
||||
throw new MissingLinkError(`could not find rename link on repository ${repository.namespace}/${repository.name}`);
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
renameRepository: (namespace: string, name: string) => mutate({ namespace, name }),
|
||||
isLoading,
|
||||
error,
|
||||
isRenamed: !!data,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,10 +24,11 @@
|
||||
|
||||
import { ApiResult, useRequiredIndexLink } from "./base";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { Link, User, UserCollection, UserCreation } from "@scm-manager/ui-types";
|
||||
import {Link, Me, User, UserCollection, UserCreation} from "@scm-manager/ui-types";
|
||||
import { apiClient } from "./apiclient";
|
||||
import { createQueryString } from "./utils";
|
||||
import { concat } from "./urls";
|
||||
import { requiredLink } from "./links";
|
||||
|
||||
export type UseUsersRequest = {
|
||||
page?: number | string;
|
||||
@@ -48,11 +49,11 @@ export const useUsers = (request?: UseUsersRequest): ApiResult<UserCollection> =
|
||||
|
||||
return useQuery<UserCollection, Error>(
|
||||
["users", request?.search || "", request?.page || 0],
|
||||
() => apiClient.get(`${indexLink}?${createQueryString(queryParams)}`).then(response => response.json()),
|
||||
() => apiClient.get(`${indexLink}?${createQueryString(queryParams)}`).then((response) => response.json()),
|
||||
{
|
||||
onSuccess: (users: UserCollection) => {
|
||||
users._embedded.users.forEach((user: User) => queryClient.setQueryData(["user", user.name], user));
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -60,7 +61,7 @@ export const useUsers = (request?: UseUsersRequest): ApiResult<UserCollection> =
|
||||
export const useUser = (name: string): ApiResult<User> => {
|
||||
const indexLink = useRequiredIndexLink("users");
|
||||
return useQuery<User, Error>(["user", name], () =>
|
||||
apiClient.get(concat(indexLink, name)).then(response => response.json())
|
||||
apiClient.get(concat(indexLink, name)).then((response) => response.json())
|
||||
);
|
||||
};
|
||||
|
||||
@@ -68,14 +69,14 @@ const createUser = (link: string) => {
|
||||
return (user: UserCreation) => {
|
||||
return apiClient
|
||||
.post(link, user, "application/vnd.scmm-user+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());
|
||||
};
|
||||
};
|
||||
|
||||
@@ -83,23 +84,23 @@ export const useCreateUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const link = useRequiredIndexLink("users");
|
||||
const { mutate, data, isLoading, error } = useMutation<User, Error, UserCreation>(createUser(link), {
|
||||
onSuccess: user => {
|
||||
onSuccess: (user) => {
|
||||
queryClient.setQueryData(["user", user.name], user);
|
||||
return queryClient.invalidateQueries(["users"]);
|
||||
}
|
||||
},
|
||||
});
|
||||
return {
|
||||
create: (user: UserCreation) => mutate(user),
|
||||
isLoading,
|
||||
error,
|
||||
user: data
|
||||
user: data,
|
||||
};
|
||||
};
|
||||
|
||||
export const useUpdateUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate, isLoading, error, data } = useMutation<unknown, Error, User>(
|
||||
user => {
|
||||
(user) => {
|
||||
const updateUrl = (user._links.update as Link).href;
|
||||
return apiClient.put(updateUrl, user, "application/vnd.scmm-user+json;v=2");
|
||||
},
|
||||
@@ -107,21 +108,21 @@ export const useUpdateUser = () => {
|
||||
onSuccess: async (_, user) => {
|
||||
await queryClient.invalidateQueries(["user", user.name]);
|
||||
await queryClient.invalidateQueries(["users"]);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return {
|
||||
update: (user: User) => mutate(user),
|
||||
isLoading,
|
||||
error,
|
||||
isUpdated: !!data
|
||||
isUpdated: !!data,
|
||||
};
|
||||
};
|
||||
|
||||
export const useDeleteUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate, isLoading, error, data } = useMutation<unknown, Error, User>(
|
||||
user => {
|
||||
(user) => {
|
||||
const deleteUrl = (user._links.delete as Link).href;
|
||||
return apiClient.delete(deleteUrl);
|
||||
},
|
||||
@@ -129,14 +130,14 @@ export const useDeleteUser = () => {
|
||||
onSuccess: async (_, name) => {
|
||||
await queryClient.invalidateQueries(["user", name]);
|
||||
await queryClient.invalidateQueries(["users"]);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return {
|
||||
remove: (user: User) => mutate(user),
|
||||
isLoading,
|
||||
error,
|
||||
isDeleted: !!data
|
||||
isDeleted: !!data,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -144,7 +145,7 @@ const convertToInternal = (url: string, newPassword: string) => {
|
||||
return apiClient.put(
|
||||
url,
|
||||
{
|
||||
newPassword
|
||||
newPassword,
|
||||
},
|
||||
"application/vnd.scmm-user+json;v=2"
|
||||
);
|
||||
@@ -167,32 +168,73 @@ export const useConvertToInternal = () => {
|
||||
onSuccess: async (_, { user }) => {
|
||||
await queryClient.invalidateQueries(["user", user.name]);
|
||||
await queryClient.invalidateQueries(["users"]);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return {
|
||||
convertToInternal: (user: User, password: string) => mutate({ user, password }),
|
||||
isLoading,
|
||||
error,
|
||||
isConverted: !!data
|
||||
isConverted: !!data,
|
||||
};
|
||||
};
|
||||
|
||||
export const useConvertToExternal = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate, isLoading, error, data } = useMutation<unknown, Error, User>(
|
||||
user => convertToExternal((user._links.convertToExternal as Link).href),
|
||||
(user) => convertToExternal((user._links.convertToExternal as Link).href),
|
||||
{
|
||||
onSuccess: async (_, user) => {
|
||||
await queryClient.invalidateQueries(["user", user.name]);
|
||||
await queryClient.invalidateQueries(["users"]);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return {
|
||||
convertToExternal: (user: User) => mutate(user),
|
||||
isLoading,
|
||||
error,
|
||||
isConverted: !!data
|
||||
isConverted: !!data,
|
||||
};
|
||||
};
|
||||
|
||||
const CONTENT_TYPE_PASSWORD_OVERWRITE = "application/vnd.scmm-passwordOverwrite+json;v=2";
|
||||
|
||||
export const useSetUserPassword = (user: User) => {
|
||||
const { data, isLoading, error, mutate, reset } = useMutation<unknown, Error, string>((password) =>
|
||||
apiClient.put(
|
||||
requiredLink(user, "password"),
|
||||
{
|
||||
newPassword: password,
|
||||
},
|
||||
CONTENT_TYPE_PASSWORD_OVERWRITE
|
||||
)
|
||||
);
|
||||
return {
|
||||
setPassword: (newPassword: string) => mutate(newPassword),
|
||||
passwordOverwritten: !!data,
|
||||
isLoading,
|
||||
error,
|
||||
reset
|
||||
};
|
||||
};
|
||||
|
||||
const CONTENT_TYPE_PASSWORD_CHANGE = "application/vnd.scmm-passwordChange+json;v=2";
|
||||
|
||||
type ChangeUserPasswordRequest = {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
};
|
||||
|
||||
export const useChangeUserPassword = (user: User | Me) => {
|
||||
const { data, isLoading, error, mutate, reset } = useMutation<unknown, Error, ChangeUserPasswordRequest>((request) =>
|
||||
apiClient.put(requiredLink(user, "password"), request, CONTENT_TYPE_PASSWORD_CHANGE)
|
||||
);
|
||||
return {
|
||||
changePassword: (oldPassword: string, newPassword: string) => mutate({ oldPassword, newPassword }),
|
||||
passwordChanged: !!data,
|
||||
isLoading,
|
||||
error,
|
||||
reset
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
import React, { ChangeEvent, FC, FocusEvent, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import { File } from "@scm-manager/ui-types";
|
||||
import { createAttributesForTesting } from "../devBuild";
|
||||
import LabelWithHelpIcon from "./LabelWithHelpIcon";
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
|
||||
import React, { FC, useState, ChangeEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { File } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
handleFile: (file: File, event?: ChangeEvent<HTMLInputElement>) => void;
|
||||
|
||||
@@ -21,28 +21,23 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { HalRepresentation, HalRepresentationWithEmbedded } from "./hal";
|
||||
|
||||
import fetchMock from "fetch-mock";
|
||||
import { CONTENT_TYPE_PASSWORD_OVERWRITE, setPassword } from "./setPassword";
|
||||
export type ApiKeysCollection = HalRepresentationWithEmbedded<{ keys: ApiKey[] }>;
|
||||
|
||||
describe("password change", () => {
|
||||
const SET_PASSWORD_URL = "/users/testuser/password";
|
||||
const newPassword = "testpw123";
|
||||
export type ApiKeyBase = {
|
||||
displayName: string;
|
||||
permissionRole: string;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
export type ApiKey = HalRepresentation &
|
||||
ApiKeyBase & {
|
||||
id: string;
|
||||
created: string;
|
||||
};
|
||||
|
||||
it("should set password", done => {
|
||||
fetchMock.put("/api/v2" + SET_PASSWORD_URL, 204, {
|
||||
headers: {
|
||||
"content-type": CONTENT_TYPE_PASSWORD_OVERWRITE
|
||||
}
|
||||
});
|
||||
export type ApiKeyWithToken = ApiKey & {
|
||||
token: string;
|
||||
};
|
||||
|
||||
setPassword(SET_PASSWORD_URL, newPassword).then(content => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
export type ApiKeyCreation = ApiKeyBase;
|
||||
@@ -21,29 +21,6 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { HalRepresentation } from "./hal";
|
||||
|
||||
import fetchMock from "fetch-mock";
|
||||
import { changePassword, CONTENT_TYPE_PASSWORD_CHANGE } from "./changePassword";
|
||||
|
||||
describe("change password", () => {
|
||||
const CHANGE_PASSWORD_URL = "/me/password";
|
||||
const oldPassword = "old";
|
||||
const newPassword = "new";
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it("should update password", done => {
|
||||
fetchMock.put("/api/v2" + CHANGE_PASSWORD_URL, 204, {
|
||||
headers: {
|
||||
"content-type": CONTENT_TYPE_PASSWORD_CHANGE
|
||||
}
|
||||
});
|
||||
|
||||
changePassword(CHANGE_PASSWORD_URL, oldPassword, newPassword).then(content => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
export type GlobalPermissionsCollection = HalRepresentation & { permissions: string[] };
|
||||
@@ -21,21 +21,20 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { HalRepresentation, HalRepresentationWithEmbedded } from "./hal";
|
||||
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
export type PublicKeysCollection = HalRepresentationWithEmbedded<{
|
||||
keys: PublicKey[];
|
||||
}>;
|
||||
|
||||
export const CONTENT_TYPE_PASSWORD_OVERWRITE = "application/vnd.scmm-passwordOverwrite+json;v=2";
|
||||
export type PublicKeyBase = {
|
||||
displayName: string;
|
||||
raw: string;
|
||||
};
|
||||
|
||||
export function setPassword(url: string, password: string) {
|
||||
return apiClient
|
||||
.put(
|
||||
url,
|
||||
{
|
||||
newPassword: password
|
||||
},
|
||||
CONTENT_TYPE_PASSWORD_OVERWRITE
|
||||
)
|
||||
.then(response => {
|
||||
return response;
|
||||
});
|
||||
}
|
||||
export type PublicKey = HalRepresentation & PublicKeyBase & {
|
||||
id: string;
|
||||
created?: string;
|
||||
};
|
||||
|
||||
export type PublicKeyCreation = PublicKeyBase;
|
||||
@@ -22,7 +22,7 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { HalRepresentationWithEmbedded, Links } from "./hal";
|
||||
import { HalRepresentation, HalRepresentationWithEmbedded } from "./hal";
|
||||
|
||||
export type PermissionCreateEntry = {
|
||||
name: string;
|
||||
@@ -31,9 +31,7 @@ export type PermissionCreateEntry = {
|
||||
groupPermission: boolean;
|
||||
};
|
||||
|
||||
export type Permission = PermissionCreateEntry & {
|
||||
_links: Links;
|
||||
};
|
||||
export type Permission = PermissionCreateEntry & HalRepresentation;
|
||||
|
||||
type PermissionEmbedded = {
|
||||
permissions: Permission[];
|
||||
|
||||
@@ -67,3 +67,6 @@ export * from "./Admin";
|
||||
|
||||
export * from "./Diff";
|
||||
export * from "./Notifications";
|
||||
export * from "./ApiKeys";
|
||||
export * from "./PublicKeys";
|
||||
export * from "./GlobalPermissions";
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { FC, FormEvent, useEffect, useState } from "react";
|
||||
import {
|
||||
ErrorNotification,
|
||||
InputField,
|
||||
@@ -29,143 +29,74 @@ import {
|
||||
Notification,
|
||||
PasswordConfirmation,
|
||||
SubmitButton,
|
||||
Subtitle
|
||||
Subtitle,
|
||||
} from "@scm-manager/ui-components";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Me } from "@scm-manager/ui-types";
|
||||
import { changePassword } from "../utils/changePassword";
|
||||
import { useChangeUserPassword } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
me: Me;
|
||||
};
|
||||
|
||||
type State = {
|
||||
oldPassword: string;
|
||||
password: string;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
passwordChanged: boolean;
|
||||
passwordValid: boolean;
|
||||
const ChangeUserPassword: FC<Props> = ({ me }) => {
|
||||
const [t] = useTranslation("commons");
|
||||
const { isLoading, error, passwordChanged, changePassword, reset } = useChangeUserPassword(me);
|
||||
const [oldPassword, setOldPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [passwordValid, setPasswordValid] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (passwordChanged) {
|
||||
setOldPassword("");
|
||||
setNewPassword("");
|
||||
setPasswordValid(false);
|
||||
}
|
||||
}, [passwordChanged]);
|
||||
|
||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (newPassword) {
|
||||
changePassword(oldPassword, newPassword);
|
||||
}
|
||||
};
|
||||
|
||||
const onPasswordChange = (newValue: string, valid: boolean) => {
|
||||
setNewPassword(newValue);
|
||||
setPasswordValid(!!newValue && valid);
|
||||
};
|
||||
|
||||
let message = null;
|
||||
|
||||
if (passwordChanged) {
|
||||
message = <Notification type={"success"} children={t("password.changedSuccessfully")} onClose={reset} />;
|
||||
} else if (error) {
|
||||
message = <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={submit}>
|
||||
<Subtitle subtitle={t("password.subtitle")} />
|
||||
{message}
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<InputField
|
||||
label={t("password.currentPassword")}
|
||||
type="password"
|
||||
onChange={setOldPassword}
|
||||
value={oldPassword}
|
||||
helpText={t("password.currentPasswordHelpText")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PasswordConfirmation passwordChanged={onPasswordChange} key={passwordChanged ? "changed" : "unchanged"} />
|
||||
<Level
|
||||
right={
|
||||
<SubmitButton disabled={!passwordValid || !oldPassword} loading={isLoading} label={t("password.submit")} />
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
class ChangeUserPassword extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
oldPassword: "",
|
||||
password: "",
|
||||
loading: false,
|
||||
passwordChanged: false,
|
||||
passwordValid: false
|
||||
};
|
||||
}
|
||||
|
||||
setLoadingState = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loading: true
|
||||
});
|
||||
};
|
||||
|
||||
setErrorState = (error: Error) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
error: error,
|
||||
loading: false
|
||||
});
|
||||
};
|
||||
|
||||
setSuccessfulState = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loading: false,
|
||||
passwordChanged: true,
|
||||
oldPassword: "",
|
||||
password: ""
|
||||
});
|
||||
};
|
||||
|
||||
submit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
if (this.state.password) {
|
||||
const { oldPassword, password } = this.state;
|
||||
this.setLoadingState();
|
||||
changePassword(this.props.me._links.password.href, oldPassword, password)
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
this.setErrorState(result.error);
|
||||
} else {
|
||||
this.setSuccessfulState();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.setErrorState(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
isValid = () => {
|
||||
return this.state.oldPassword && this.state.passwordValid;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { loading, passwordChanged, error } = this.state;
|
||||
|
||||
let message = null;
|
||||
|
||||
if (passwordChanged) {
|
||||
message = (
|
||||
<Notification type={"success"} children={t("password.changedSuccessfully")} onClose={() => this.onClose()} />
|
||||
);
|
||||
} else if (error) {
|
||||
message = <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={this.submit}>
|
||||
<Subtitle subtitle={t("password.subtitle")} />
|
||||
{message}
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<InputField
|
||||
label={t("password.currentPassword")}
|
||||
type="password"
|
||||
onChange={oldPassword =>
|
||||
this.setState({
|
||||
...this.state,
|
||||
oldPassword
|
||||
})
|
||||
}
|
||||
value={this.state.oldPassword ? this.state.oldPassword : ""}
|
||||
helpText={t("password.currentPasswordHelpText")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PasswordConfirmation
|
||||
passwordChanged={this.passwordChanged}
|
||||
key={this.state.passwordChanged ? "changed" : "unchanged"}
|
||||
/>
|
||||
<Level right={<SubmitButton disabled={!this.isValid()} loading={loading} label={t("password.submit")} />} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
passwordChanged = (password: string, passwordValid: boolean) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
password,
|
||||
passwordValid: !!password && passwordValid
|
||||
});
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
passwordChanged: false
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default withTranslation("commons")(ChangeUserPassword);
|
||||
export default ChangeUserPassword;
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
SecondaryNavigationColumn,
|
||||
StateMenuContextProvider,
|
||||
SubNavigation,
|
||||
urls
|
||||
urls,
|
||||
} from "@scm-manager/ui-components";
|
||||
import ChangeUserPassword from "./ChangeUserPassword";
|
||||
import ProfileInfo from "./ProfileInfo";
|
||||
@@ -69,7 +69,7 @@ const Profile: FC = () => {
|
||||
subtitle={t("profile.error-subtitle")}
|
||||
error={{
|
||||
name: t("profile.error"),
|
||||
message: t("profile.error-message")
|
||||
message: t("profile.error-message"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -77,7 +77,7 @@ const Profile: FC = () => {
|
||||
|
||||
const extensionProps = {
|
||||
me,
|
||||
url
|
||||
url,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -94,12 +94,20 @@ const Profile: FC = () => {
|
||||
</Switch>
|
||||
)}
|
||||
{mayChangePassword && (
|
||||
<Route path={`${url}/settings/password`} render={() => <ChangeUserPassword me={me} />} />
|
||||
<Route path={`${url}/settings/password`}>
|
||||
<ChangeUserPassword me={me} />
|
||||
</Route>
|
||||
)}
|
||||
{canManagePublicKeys && (
|
||||
<Route path={`${url}/settings/publicKeys`} render={() => <SetPublicKeys user={me} />} />
|
||||
<Route path={`${url}/settings/publicKeys`}>
|
||||
<SetPublicKeys user={me} />
|
||||
</Route>
|
||||
)}
|
||||
{canManageApiKeys && (
|
||||
<Route path={`${url}/settings/apiKeys`}>
|
||||
<SetApiKeys user={me} />
|
||||
</Route>
|
||||
)}
|
||||
{canManageApiKeys && <Route path={`${url}/settings/apiKeys`} render={() => <SetApiKeys user={me} />} />}
|
||||
<ExtensionPoint name="profile.route" props={extensionProps} renderAll={true} />
|
||||
</PrimaryContentColumn>
|
||||
<SecondaryNavigationColumn>
|
||||
|
||||
@@ -25,7 +25,6 @@ import React, { FC } from "react";
|
||||
import { Route, useParams, useRouteMatch } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import { Link } from "@scm-manager/ui-types";
|
||||
import {
|
||||
CustomQueryFlexWrappedColumns,
|
||||
ErrorPage,
|
||||
@@ -37,16 +36,16 @@ import {
|
||||
SecondaryNavigationColumn,
|
||||
StateMenuContextProvider,
|
||||
SubNavigation,
|
||||
urls
|
||||
urls,
|
||||
} from "@scm-manager/ui-components";
|
||||
import { Details } from "./../components/table";
|
||||
import { EditGroupNavLink, SetPermissionsNavLink } from "./../components/navLinks";
|
||||
import EditGroup from "./EditGroup";
|
||||
import SetPermissions from "../../permissions/components/SetPermissions";
|
||||
import { useGroup } from "@scm-manager/ui-api";
|
||||
import SetGroupPermissions from "../../permissions/components/SetGroupPermissions";
|
||||
|
||||
const SingleGroup: FC = () => {
|
||||
const { name } = useParams();
|
||||
const { name } = useParams<{ name: string }>();
|
||||
const match = useRouteMatch();
|
||||
const { data: group, isLoading, error } = useGroup(name);
|
||||
const [t] = useTranslation("groups");
|
||||
@@ -63,7 +62,7 @@ const SingleGroup: FC = () => {
|
||||
|
||||
const extensionProps = {
|
||||
group,
|
||||
url
|
||||
url,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -78,7 +77,7 @@ const SingleGroup: FC = () => {
|
||||
<EditGroup group={group} />
|
||||
</Route>
|
||||
<Route path={`${url}/settings/permissions`} exact>
|
||||
<SetPermissions selectedPermissionsLink={group._links.permissions as Link} />
|
||||
<SetGroupPermissions group={group} />
|
||||
</Route>
|
||||
<ExtensionPoint name="group.route" props={extensionProps} renderAll={true} />
|
||||
</PrimaryContentColumn>
|
||||
|
||||
@@ -21,33 +21,38 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { Group } from "@scm-manager/ui-types";
|
||||
import React, { FC } from "react";
|
||||
import SetPermissions from "./SetPermissions";
|
||||
import { useGroupPermissions, useSetGroupPermissions } from "@scm-manager/ui-api";
|
||||
|
||||
import fetchMock from "fetch-mock";
|
||||
import { getContent, getLanguage } from "./SourcecodeViewer";
|
||||
type Props = {
|
||||
group: Group;
|
||||
};
|
||||
|
||||
describe("get content", () => {
|
||||
const CONTENT_URL = "/repositories/scmadmin/TestRepo/content/testContent";
|
||||
const SetGroupPermissions: FC<Props> = ({ group }) => {
|
||||
const {
|
||||
data: selectedPermissions,
|
||||
isLoading: loadingPermissions,
|
||||
error: permissionsLoadError,
|
||||
} = useGroupPermissions(group);
|
||||
const {
|
||||
isLoading: isUpdatingPermissions,
|
||||
isUpdated: permissionsUpdated,
|
||||
setPermissions,
|
||||
error: permissionsUpdateError,
|
||||
} = useSetGroupPermissions(group, selectedPermissions);
|
||||
return (
|
||||
<SetPermissions
|
||||
selectedPermissions={selectedPermissions}
|
||||
loadingPermissions={loadingPermissions}
|
||||
isUpdatingPermissions={isUpdatingPermissions}
|
||||
permissionsLoadError={permissionsLoadError || undefined}
|
||||
permissionsUpdated={permissionsUpdated}
|
||||
updatePermissions={setPermissions}
|
||||
permissionsUpdateError={permissionsUpdateError || undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it("should return content", done => {
|
||||
fetchMock.getOnce("/api/v2" + CONTENT_URL, "This is a testContent");
|
||||
|
||||
getContent(CONTENT_URL).then(content => {
|
||||
expect(content).toBe("This is a testContent");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("get correct language type", () => {
|
||||
it("should return javascript", () => {
|
||||
expect(getLanguage("JAVASCRIPT")).toBe("javascript");
|
||||
});
|
||||
it("should return nothing for plain text", () => {
|
||||
expect(getLanguage("")).toBe("");
|
||||
});
|
||||
});
|
||||
export default SetGroupPermissions;
|
||||
@@ -23,55 +23,69 @@
|
||||
*/
|
||||
import React, { FC, FormEvent, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "@scm-manager/ui-types";
|
||||
import { ErrorNotification, Level, Notification, SubmitButton, Subtitle } from "@scm-manager/ui-components";
|
||||
import { loadPermissionsForEntity, setPermissions as updatePermissions } from "./handlePermissions";
|
||||
import { ErrorNotification, Level, Loading, Notification, SubmitButton, Subtitle } from "@scm-manager/ui-components";
|
||||
import PermissionsWrapper from "./PermissionsWrapper";
|
||||
import { useRequiredIndexLink } from "@scm-manager/ui-api";
|
||||
import { useAvailableGlobalPermissions } from "@scm-manager/ui-api";
|
||||
import { GlobalPermissionsCollection } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
selectedPermissionsLink: Link;
|
||||
selectedPermissions?: GlobalPermissionsCollection;
|
||||
loadingPermissions?: boolean;
|
||||
permissionsLoadError?: Error;
|
||||
updatePermissions?: (permissions: string[]) => void;
|
||||
isUpdatingPermissions?: boolean;
|
||||
permissionsUpdated?: boolean;
|
||||
permissionsUpdateError?: Error;
|
||||
};
|
||||
|
||||
const SetPermissions: FC<Props> = ({ selectedPermissionsLink }) => {
|
||||
const SetPermissions: FC<Props> = ({
|
||||
loadingPermissions,
|
||||
isUpdatingPermissions,
|
||||
permissionsLoadError,
|
||||
permissionsUpdateError,
|
||||
updatePermissions,
|
||||
permissionsUpdated,
|
||||
selectedPermissions,
|
||||
}) => {
|
||||
const [t] = useTranslation("permissions");
|
||||
const availablePermissionLink = useRequiredIndexLink("permissions");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null | undefined>();
|
||||
const {
|
||||
data: availablePermissions,
|
||||
error: availablePermissionsLoadError,
|
||||
isLoading: isLoadingAvailablePermissions,
|
||||
} = useAvailableGlobalPermissions();
|
||||
const [permissions, setPermissions] = useState<Record<string, boolean>>({});
|
||||
const [permissionsSubmitted, setPermissionsSubmitted] = useState(false);
|
||||
const [permissionsChanged, setPermissionsChanged] = useState(false);
|
||||
const [overwritePermissionsLink, setOverwritePermissionsLink] = useState<Link | undefined>();
|
||||
const [permissions, setPermissions] = useState<{
|
||||
[key: string]: boolean;
|
||||
}>({});
|
||||
const error = permissionsLoadError || availablePermissionsLoadError;
|
||||
|
||||
useEffect(() => {
|
||||
loadPermissionsForEntity(availablePermissionLink, selectedPermissionsLink.href).then(response => {
|
||||
const { permissions, overwriteLink } = response;
|
||||
setPermissions(permissions);
|
||||
setOverwritePermissionsLink(overwriteLink);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [availablePermissionLink, selectedPermissionsLink]);
|
||||
if (selectedPermissions && availablePermissions) {
|
||||
const newPermissions: Record<string, boolean> = {};
|
||||
availablePermissions.permissions.forEach((p) => (newPermissions[p] = false));
|
||||
selectedPermissions.permissions.forEach((p) => (newPermissions[p] = true));
|
||||
setPermissions(newPermissions);
|
||||
}
|
||||
}, [availablePermissions, selectedPermissions]);
|
||||
|
||||
const setLoadingState = () => setLoading(true);
|
||||
useEffect(() => {
|
||||
if (permissionsUpdated) {
|
||||
setPermissionsSubmitted(true);
|
||||
setPermissionsChanged(false);
|
||||
}
|
||||
}, [permissionsUpdated]);
|
||||
|
||||
const setErrorState = (error: Error) => {
|
||||
setLoading(false);
|
||||
setError(error);
|
||||
};
|
||||
if (loadingPermissions || isLoadingAvailablePermissions) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const setSuccessfulState = () => {
|
||||
setLoading(false);
|
||||
setError(undefined);
|
||||
setPermissionsSubmitted(true);
|
||||
setPermissionsChanged(false);
|
||||
};
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
const valueChanged = (value: boolean, name: string) => {
|
||||
setPermissions({
|
||||
...permissions,
|
||||
[name]: value
|
||||
[name]: value,
|
||||
});
|
||||
setPermissionsChanged(true);
|
||||
};
|
||||
@@ -81,15 +95,10 @@ const SetPermissions: FC<Props> = ({ selectedPermissionsLink }) => {
|
||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (permissions) {
|
||||
setLoadingState();
|
||||
const selectedPermissions = Object.entries(permissions)
|
||||
.filter(e => e[1])
|
||||
.map(e => e[0]);
|
||||
if (overwritePermissionsLink) {
|
||||
updatePermissions(overwritePermissionsLink.href, selectedPermissions)
|
||||
.then(_ => setSuccessfulState())
|
||||
.catch(err => setErrorState(err));
|
||||
}
|
||||
.filter((e) => e[1])
|
||||
.map((e) => e[0]);
|
||||
updatePermissions!(selectedPermissions);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -99,7 +108,7 @@ const SetPermissions: FC<Props> = ({ selectedPermissionsLink }) => {
|
||||
message = (
|
||||
<Notification type={"success"} children={t("setPermissions.setPermissionsSuccessful")} onClose={onClose} />
|
||||
);
|
||||
} else if (error) {
|
||||
} else if (permissionsUpdateError) {
|
||||
message = <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
@@ -107,12 +116,12 @@ const SetPermissions: FC<Props> = ({ selectedPermissionsLink }) => {
|
||||
<form onSubmit={submit}>
|
||||
<Subtitle subtitle={t("setPermissions.subtitle")} />
|
||||
{message}
|
||||
<PermissionsWrapper permissions={permissions} onChange={valueChanged} disabled={!overwritePermissionsLink} />
|
||||
<PermissionsWrapper permissions={permissions} onChange={valueChanged} disabled={!updatePermissions} />
|
||||
<Level
|
||||
right={
|
||||
<SubmitButton
|
||||
disabled={!permissionsChanged}
|
||||
loading={loading}
|
||||
loading={isUpdatingPermissions}
|
||||
label={t("setPermissions.button")}
|
||||
testId="set-permissions-button"
|
||||
/>
|
||||
@@ -121,4 +130,5 @@ const SetPermissions: FC<Props> = ({ selectedPermissionsLink }) => {
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetPermissions;
|
||||
|
||||
@@ -21,41 +21,38 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { User } from "@scm-manager/ui-types";
|
||||
import React, { FC } from "react";
|
||||
import SetPermissions from "./SetPermissions";
|
||||
import { useSetUserPermissions, useUserPermissions } from "@scm-manager/ui-api";
|
||||
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
type Props = {
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const CONTENT_TYPE_PERMISSIONS = "application/vnd.scmm-permissionCollection+json;v=2";
|
||||
const SetUserPermissions: FC<Props> = ({ user }) => {
|
||||
const {
|
||||
data: selectedPermissions,
|
||||
isLoading: loadingPermissions,
|
||||
error: permissionsLoadError,
|
||||
} = useUserPermissions(user);
|
||||
const {
|
||||
isLoading: isUpdatingPermissions,
|
||||
isUpdated: permissionsUpdated,
|
||||
setPermissions,
|
||||
error: permissionsUpdateError,
|
||||
} = useSetUserPermissions(user, selectedPermissions);
|
||||
return (
|
||||
<SetPermissions
|
||||
selectedPermissions={selectedPermissions}
|
||||
loadingPermissions={loadingPermissions}
|
||||
isUpdatingPermissions={isUpdatingPermissions}
|
||||
permissionsLoadError={permissionsLoadError || undefined}
|
||||
permissionsUpdated={permissionsUpdated}
|
||||
updatePermissions={setPermissions}
|
||||
permissionsUpdateError={permissionsUpdateError || undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export function setPermissions(url: string, permissions: string[]) {
|
||||
return apiClient
|
||||
.put(
|
||||
url,
|
||||
{
|
||||
permissions: permissions
|
||||
},
|
||||
CONTENT_TYPE_PERMISSIONS
|
||||
)
|
||||
.then(response => {
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
export function loadPermissionsForEntity(availableUrl: string, userUrl: string) {
|
||||
return Promise.all([
|
||||
apiClient.get(availableUrl).then(response => {
|
||||
return response.json();
|
||||
}),
|
||||
apiClient.get(userUrl).then(response => {
|
||||
return response.json();
|
||||
})
|
||||
]).then(values => {
|
||||
const [availablePermissions, checkedPermissions] = values;
|
||||
const permissions = {};
|
||||
availablePermissions.permissions.forEach(p => (permissions[p] = false));
|
||||
checkedPermissions.permissions.forEach(p => (permissions[p] = true));
|
||||
return {
|
||||
permissions,
|
||||
overwriteLink: checkedPermissions._links.overwrite
|
||||
};
|
||||
});
|
||||
}
|
||||
export default SetUserPermissions;
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* 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 fetchMock from "fetch-mock";
|
||||
import { loadPermissionsForEntity } from "./handlePermissions";
|
||||
|
||||
describe("load permissions for entity", () => {
|
||||
const AVAILABLE_PERMISSIONS_URL = "/permissions";
|
||||
const USER_PERMISSIONS_URL = "/user/scmadmin/permissions";
|
||||
|
||||
const availablePermissions = `{
|
||||
"permissions": [
|
||||
"repository:read,pull:*",
|
||||
"repository:read,pull,push:*",
|
||||
"repository:*:*"
|
||||
]
|
||||
}`;
|
||||
const userPermissions = `{
|
||||
"permissions": [
|
||||
"repository:read,pull:*"
|
||||
],
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "/api/v2/users/rene/permissions"
|
||||
},
|
||||
"overwrite": {
|
||||
"href": "/api/v2/users/rene/permissions"
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.getOnce("/api/v2" + AVAILABLE_PERMISSIONS_URL, availablePermissions);
|
||||
fetchMock.getOnce("/api/v2" + USER_PERMISSIONS_URL, userPermissions);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it("should return permissions array", done => {
|
||||
loadPermissionsForEntity(AVAILABLE_PERMISSIONS_URL, USER_PERMISSIONS_URL).then(result => {
|
||||
const { permissions } = result;
|
||||
expect(Object.entries(permissions).length).toBe(3);
|
||||
expect(permissions["repository:read,pull:*"]).toBe(true);
|
||||
expect(permissions["repository:read,pull,push:*"]).toBe(false);
|
||||
expect(permissions["repository:*:*"]).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return overwrite link", done => {
|
||||
loadPermissionsForEntity(AVAILABLE_PERMISSIONS_URL, USER_PERMISSIONS_URL).then(result => {
|
||||
const { overwriteLink } = result;
|
||||
expect(overwriteLink.href).toBe("/api/v2/users/rene/permissions");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,10 +22,11 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Branch, Link, Repository } from "@scm-manager/ui-types";
|
||||
import { apiClient, ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
|
||||
import { Branch, Repository } from "@scm-manager/ui-types";
|
||||
import { ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
|
||||
import { useDeleteBranch } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
@@ -34,16 +35,12 @@ type Props = {
|
||||
|
||||
const DeleteBranch: FC<Props> = ({ repository, branch }: Props) => {
|
||||
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
|
||||
const [error, setError] = useState<Error | undefined>();
|
||||
const [t] = useTranslation("repos");
|
||||
const history = useHistory();
|
||||
const { isLoading, error, remove, isDeleted } = useDeleteBranch(repository);
|
||||
|
||||
const deleteBranch = () => {
|
||||
apiClient
|
||||
.delete((branch._links.delete as Link).href)
|
||||
.then(() => history.push(`/repo/${repository.namespace}/${repository.name}/branches/`))
|
||||
.catch(setError);
|
||||
};
|
||||
if (isDeleted) {
|
||||
return <Redirect to={`/repo/${repository.namespace}/${repository.name}/branches/`} />;
|
||||
}
|
||||
|
||||
if (!branch._links.delete) {
|
||||
return null;
|
||||
@@ -59,12 +56,13 @@ const DeleteBranch: FC<Props> = ({ repository, branch }: Props) => {
|
||||
{
|
||||
className: "is-outlined",
|
||||
label: t("branch.delete.confirmAlert.submit"),
|
||||
onClick: () => deleteBranch()
|
||||
onClick: () => remove(branch),
|
||||
isLoading,
|
||||
},
|
||||
{
|
||||
label: t("branch.delete.confirmAlert.cancel"),
|
||||
onClick: () => null
|
||||
}
|
||||
onClick: () => null,
|
||||
},
|
||||
]}
|
||||
close={() => setShowConfirmAlert(false)}
|
||||
/>
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
*/
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { FileUpload, LabelWithHelpIcon, Checkbox, InputField } from "@scm-manager/ui-components";
|
||||
import { File } from "@scm-manager/ui-types";
|
||||
import { Checkbox, FileUpload, InputField, LabelWithHelpIcon } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
@@ -44,7 +43,7 @@ const ImportFromBundleForm: FC<Props> = ({
|
||||
setCompressed,
|
||||
password,
|
||||
setPassword,
|
||||
disabled
|
||||
disabled,
|
||||
}) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
@@ -54,7 +53,7 @@ const ImportFromBundleForm: FC<Props> = ({
|
||||
<div className="column is-half is-vcentered">
|
||||
<LabelWithHelpIcon label={t("import.bundle.title")} helpText={t("import.bundle.helpText")} />
|
||||
<FileUpload
|
||||
handleFile={(file: File) => {
|
||||
handleFile={(file) => {
|
||||
setFile(file);
|
||||
setValid(!!file);
|
||||
}}
|
||||
@@ -75,7 +74,7 @@ const ImportFromBundleForm: FC<Props> = ({
|
||||
<div className="column is-half is-vcentered">
|
||||
<InputField
|
||||
value={password}
|
||||
onChange={value => setPassword(value)}
|
||||
onChange={(value) => setPassword(value)}
|
||||
type="password"
|
||||
label={t("import.bundle.password.title")}
|
||||
helpText={t("import.bundle.password.helpText")}
|
||||
|
||||
@@ -21,77 +21,56 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, FormEvent, useState } from "react";
|
||||
import { File, RepositoryCreation } from "@scm-manager/ui-types";
|
||||
import { apiClient, ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
|
||||
import React, { FC, FormEvent, useEffect, useState } from "react";
|
||||
import { Repository, RepositoryCreation, RepositoryType } from "@scm-manager/ui-types";
|
||||
import { ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import ImportFullRepositoryForm from "./ImportFullRepositoryForm";
|
||||
import { SubFormProps } from "../types";
|
||||
import { extensionPoints } from "@scm-manager/ui-extensions";
|
||||
import { useImportFullRepository } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
repositoryType: string;
|
||||
repositoryType: RepositoryType;
|
||||
setImportPending: (pending: boolean) => void;
|
||||
nameForm: React.ComponentType<SubFormProps>;
|
||||
informationForm: React.ComponentType<SubFormProps>;
|
||||
setImportedRepository: (repository: Repository) => void;
|
||||
nameForm: extensionPoints.RepositoryCreatorComponentProps["nameForm"];
|
||||
informationForm: extensionPoints.RepositoryCreatorComponentProps["informationForm"];
|
||||
};
|
||||
|
||||
const ImportFullRepository: FC<Props> = ({
|
||||
url,
|
||||
repositoryType,
|
||||
setImportPending,
|
||||
setImportedRepository,
|
||||
nameForm: NameForm,
|
||||
informationForm: InformationForm
|
||||
informationForm: InformationForm,
|
||||
}) => {
|
||||
const [repo, setRepo] = useState<RepositoryCreation>({
|
||||
name: "",
|
||||
namespace: "",
|
||||
type: repositoryType,
|
||||
type: repositoryType.name,
|
||||
contact: "",
|
||||
description: "",
|
||||
contextEntries: []
|
||||
contextEntries: [],
|
||||
});
|
||||
const [password, setPassword] = useState("");
|
||||
const [valid, setValid] = useState({ namespaceAndName: false, contact: true, file: false });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | undefined>();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const history = useHistory();
|
||||
const [t] = useTranslation("repos");
|
||||
const { importFullRepository, importedRepository, isLoading, error } = useImportFullRepository(repositoryType);
|
||||
|
||||
const handleImportLoading = (loading: boolean) => {
|
||||
setImportPending(loading);
|
||||
setLoading(loading);
|
||||
};
|
||||
useEffect(() => setRepo({...repo, type: repositoryType.name}), [repositoryType]);
|
||||
useEffect(() => setImportPending(isLoading), [isLoading]);
|
||||
useEffect(() => {
|
||||
if (importedRepository) {
|
||||
setImportedRepository(importedRepository);
|
||||
}
|
||||
}, [importedRepository]);
|
||||
|
||||
const isValid = () => Object.values(valid).every(v => v);
|
||||
const isValid = () => Object.values(valid).every((v) => v);
|
||||
|
||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const currentPath = history.location.pathname;
|
||||
setError(undefined);
|
||||
handleImportLoading(true);
|
||||
apiClient
|
||||
.postBinary(url, formData => {
|
||||
formData.append("bundle", file, file?.name);
|
||||
formData.append("repository", JSON.stringify({ ...repo, password }));
|
||||
})
|
||||
.then(response => {
|
||||
const location = response.headers.get("Location");
|
||||
return apiClient.get(location!);
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(repo => {
|
||||
handleImportLoading(false);
|
||||
if (history.location.pathname === currentPath) {
|
||||
history.push(`/repo/${repo.namespace}/${repo.name}/code/sources`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error);
|
||||
handleImportLoading(false);
|
||||
});
|
||||
importFullRepository(repo, file!, password);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -108,16 +87,16 @@ const ImportFullRepository: FC<Props> = ({
|
||||
repository={repo}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
|
||||
setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })}
|
||||
disabled={loading}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<InformationForm
|
||||
repository={repo}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
|
||||
disabled={loading}
|
||||
disabled={isLoading}
|
||||
setValid={(contact: boolean) => setValid({ ...valid, contact })}
|
||||
/>
|
||||
<Level
|
||||
right={<SubmitButton disabled={!isValid()} loading={loading} label={t("repositoryForm.submitImport")} />}
|
||||
right={<SubmitButton disabled={!isValid()} loading={isLoading} label={t("repositoryForm.submitImport")} />}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { FileUpload, InputField, LabelWithHelpIcon } from "@scm-manager/ui-components";
|
||||
import { File } from "@scm-manager/ui-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
@@ -51,7 +50,7 @@ const ImportFullRepositoryForm: FC<Props> = ({ setFile, setValid, password, setP
|
||||
<div className="column is-half is-vcentered">
|
||||
<InputField
|
||||
value={password}
|
||||
onChange={value => setPassword(value)}
|
||||
onChange={(value) => setPassword(value)}
|
||||
type="password"
|
||||
label={t("import.bundle.password.title")}
|
||||
helpText={t("import.bundle.password.helpText")}
|
||||
|
||||
@@ -21,78 +21,58 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, FormEvent, useState } from "react";
|
||||
import { File, RepositoryCreation } from "@scm-manager/ui-types";
|
||||
import { apiClient, ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
|
||||
import React, { FC, FormEvent, useEffect, useState } from "react";
|
||||
import { Repository, RepositoryCreation, RepositoryType } from "@scm-manager/ui-types";
|
||||
import { ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import ImportFromBundleForm from "./ImportFromBundleForm";
|
||||
import { SubFormProps } from "../types";
|
||||
import { extensionPoints } from "@scm-manager/ui-extensions";
|
||||
import { useImportRepositoryFromBundle } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
repositoryType: string;
|
||||
repositoryType: RepositoryType;
|
||||
setImportPending: (pending: boolean) => void;
|
||||
nameForm: React.ComponentType<SubFormProps>;
|
||||
informationForm: React.ComponentType<SubFormProps>;
|
||||
setImportedRepository: (repository: Repository) => void;
|
||||
nameForm: extensionPoints.RepositoryCreatorComponentProps["nameForm"];
|
||||
informationForm: extensionPoints.RepositoryCreatorComponentProps["informationForm"];
|
||||
};
|
||||
|
||||
const ImportRepositoryFromBundle: FC<Props> = ({
|
||||
url,
|
||||
repositoryType,
|
||||
setImportPending,
|
||||
setImportedRepository,
|
||||
nameForm: NameForm,
|
||||
informationForm: InformationForm
|
||||
informationForm: InformationForm,
|
||||
}) => {
|
||||
const [repo, setRepo] = useState<RepositoryCreation>({
|
||||
name: "",
|
||||
namespace: "",
|
||||
type: repositoryType,
|
||||
type: repositoryType.name,
|
||||
contact: "",
|
||||
description: "",
|
||||
contextEntries: []
|
||||
contextEntries: [],
|
||||
});
|
||||
const [password, setPassword] = useState("");
|
||||
const [valid, setValid] = useState({ namespaceAndName: false, contact: true, file: false });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | undefined>();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [compressed, setCompressed] = useState(true);
|
||||
const history = useHistory();
|
||||
const [t] = useTranslation("repos");
|
||||
const { importRepositoryFromBundle, importedRepository, error, isLoading } =
|
||||
useImportRepositoryFromBundle(repositoryType);
|
||||
|
||||
const handleImportLoading = (loading: boolean) => {
|
||||
setImportPending(loading);
|
||||
setLoading(loading);
|
||||
};
|
||||
useEffect(() => setRepo({...repo, type: repositoryType.name}), [repositoryType]);
|
||||
useEffect(() => setImportPending(isLoading), [isLoading]);
|
||||
useEffect(() => {
|
||||
if (importedRepository) {
|
||||
setImportedRepository(importedRepository);
|
||||
}
|
||||
}, [importedRepository]);
|
||||
|
||||
const isValid = () => Object.values(valid).every(v => v);
|
||||
const isValid = () => Object.values(valid).every((v) => v);
|
||||
|
||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const currentPath = history.location.pathname;
|
||||
setError(undefined);
|
||||
handleImportLoading(true);
|
||||
apiClient
|
||||
.postBinary(compressed ? url + "?compressed=true" : url, formData => {
|
||||
formData.append("bundle", file, file?.name);
|
||||
formData.append("repository", JSON.stringify({ ...repo, password }));
|
||||
})
|
||||
.then(response => {
|
||||
const location = response.headers.get("Location");
|
||||
return apiClient.get(location!);
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(repo => {
|
||||
handleImportLoading(false);
|
||||
if (history.location.pathname === currentPath) {
|
||||
history.push(`/repo/${repo.namespace}/${repo.name}/code/sources`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error);
|
||||
handleImportLoading(false);
|
||||
});
|
||||
importRepositoryFromBundle(repo, file!, compressed, password);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -105,23 +85,23 @@ const ImportRepositoryFromBundle: FC<Props> = ({
|
||||
setCompressed={setCompressed}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
disabled={loading}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<hr />
|
||||
<NameForm
|
||||
repository={repo}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
|
||||
setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })}
|
||||
disabled={loading}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<InformationForm
|
||||
repository={repo}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
|
||||
disabled={loading}
|
||||
disabled={isLoading}
|
||||
setValid={(contact: boolean) => setValid({ ...valid, contact })}
|
||||
/>
|
||||
<Level
|
||||
right={<SubmitButton disabled={!isValid()} loading={loading} label={t("repositoryForm.submitImport")} />}
|
||||
right={<SubmitButton disabled={!isValid()} loading={isLoading} label={t("repositoryForm.submitImport")} />}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -21,102 +21,84 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, FormEvent, useState } from "react";
|
||||
import { RepositoryCreation, RepositoryUrlImport } from "@scm-manager/ui-types";
|
||||
import React, { FC, FormEvent, useEffect, useState } from "react";
|
||||
import { Repository, RepositoryCreation, RepositoryType, RepositoryUrlImport } from "@scm-manager/ui-types";
|
||||
import ImportFromUrlForm from "./ImportFromUrlForm";
|
||||
import { apiClient, ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
|
||||
import { ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { SubFormProps } from "../types";
|
||||
import { useImportRepositoryFromUrl } from "@scm-manager/ui-api";
|
||||
import { extensionPoints } from "@scm-manager/ui-extensions";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
repositoryType: string;
|
||||
repositoryType: RepositoryType;
|
||||
setImportPending: (pending: boolean) => void;
|
||||
nameForm: React.ComponentType<SubFormProps>;
|
||||
informationForm: React.ComponentType<SubFormProps>;
|
||||
setImportedRepository: (repository: Repository) => void;
|
||||
nameForm: extensionPoints.RepositoryCreatorComponentProps["nameForm"];
|
||||
informationForm: extensionPoints.RepositoryCreatorComponentProps["informationForm"];
|
||||
};
|
||||
|
||||
const ImportRepositoryFromUrl: FC<Props> = ({
|
||||
url,
|
||||
repositoryType,
|
||||
setImportPending,
|
||||
setImportedRepository,
|
||||
nameForm: NameForm,
|
||||
informationForm: InformationForm
|
||||
informationForm: InformationForm,
|
||||
}) => {
|
||||
const [repo, setRepo] = useState<RepositoryUrlImport>({
|
||||
name: "",
|
||||
namespace: "",
|
||||
type: repositoryType,
|
||||
type: repositoryType.name,
|
||||
contact: "",
|
||||
description: "",
|
||||
importUrl: "",
|
||||
username: "",
|
||||
password: "",
|
||||
contextEntries: []
|
||||
contextEntries: [],
|
||||
});
|
||||
|
||||
const [valid, setValid] = useState({ namespaceAndName: false, contact: true, importUrl: false });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | undefined>();
|
||||
const history = useHistory();
|
||||
const [t] = useTranslation("repos");
|
||||
const { importRepositoryFromUrl, importedRepository, error, isLoading } = useImportRepositoryFromUrl(repositoryType);
|
||||
|
||||
const isValid = () => Object.values(valid).every(v => v);
|
||||
useEffect(() => setRepo({...repo, type: repositoryType.name}), [repositoryType]);
|
||||
useEffect(() => setImportPending(isLoading), [isLoading]);
|
||||
useEffect(() => {
|
||||
if (importedRepository) {
|
||||
setImportedRepository(importedRepository);
|
||||
}
|
||||
}, [importedRepository]);
|
||||
|
||||
const handleImportLoading = (loading: boolean) => {
|
||||
setImportPending(loading);
|
||||
setLoading(loading);
|
||||
};
|
||||
const isValid = () => Object.values(valid).every((v) => v);
|
||||
|
||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setError(undefined);
|
||||
const currentPath = history.location.pathname;
|
||||
handleImportLoading(true);
|
||||
apiClient
|
||||
.post(url, repo, "application/vnd.scmm-repository+json;v=2")
|
||||
.then(response => {
|
||||
const location = response.headers.get("Location");
|
||||
return apiClient.get(location!);
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(repo => {
|
||||
handleImportLoading(false);
|
||||
if (history.location.pathname === currentPath) {
|
||||
history.push(`/repo/${repo.namespace}/${repo.name}/code/sources`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
setError(error);
|
||||
handleImportLoading(false);
|
||||
});
|
||||
importRepositoryFromUrl(repo);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={submit}>
|
||||
<ErrorNotification error={error} />
|
||||
{error ? <ErrorNotification error={error} /> : null}
|
||||
<ImportFromUrlForm
|
||||
repository={repo}
|
||||
onChange={setRepo}
|
||||
setValid={(importUrl: boolean) => setValid({ ...valid, importUrl })}
|
||||
disabled={loading}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<hr />
|
||||
<NameForm
|
||||
repository={repo}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
|
||||
setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })}
|
||||
disabled={loading}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<InformationForm
|
||||
repository={repo}
|
||||
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
|
||||
disabled={loading}
|
||||
disabled={isLoading}
|
||||
setValid={(contact: boolean) => setValid({ ...valid, contact })}
|
||||
/>
|
||||
<Level
|
||||
right={<SubmitButton disabled={!isValid()} loading={loading} label={t("repositoryForm.submitImport")} />}
|
||||
right={<SubmitButton disabled={!isValid()} loading={isLoading} label={t("repositoryForm.submitImport")} />}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -25,12 +25,12 @@ import React, { FC } from "react";
|
||||
import { Redirect, useRouteMatch } from "react-router-dom";
|
||||
import RepositoryForm from "../components/form";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { ErrorNotification, Notification, Subtitle, urls } from "@scm-manager/ui-components";
|
||||
import { ErrorNotification, Subtitle, urls } from "@scm-manager/ui-components";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import RepositoryDangerZone from "./RepositoryDangerZone";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ExportRepository from "./ExportRepository";
|
||||
import { useIndexLinks, useUpdateRepository } from "@scm-manager/ui-api";
|
||||
import { useUpdateRepository } from "@scm-manager/ui-api";
|
||||
import HealthCheckWarning from "./HealthCheckWarning";
|
||||
import RunHealthCheck from "./RunHealthCheck";
|
||||
|
||||
@@ -41,7 +41,6 @@ type Props = {
|
||||
const EditRepo: FC<Props> = ({ repository }) => {
|
||||
const match = useRouteMatch();
|
||||
const { isLoading, error, update, isUpdated } = useUpdateRepository();
|
||||
const indexLinks = useIndexLinks();
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
if (isUpdated) {
|
||||
@@ -51,7 +50,7 @@ const EditRepo: FC<Props> = ({ repository }) => {
|
||||
const url = urls.matchedUrlFromMatch(match);
|
||||
const extensionProps = {
|
||||
repository,
|
||||
url
|
||||
url,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -66,7 +65,7 @@ const EditRepo: FC<Props> = ({ repository }) => {
|
||||
{(repository._links.runHealthCheck || repository.healthCheckRunning) && (
|
||||
<RunHealthCheck repository={repository} />
|
||||
)}
|
||||
<RepositoryDangerZone repository={repository} indexLinks={indexLinks} />
|
||||
<RepositoryDangerZone repository={repository} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { FC, useState } from "react";
|
||||
import { Link, RepositoryType } from "@scm-manager/ui-types";
|
||||
import React, { useState } from "react";
|
||||
import { Link, Repository, RepositoryType } from "@scm-manager/ui-types";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ImportRepositoryTypeSelect from "../components/ImportRepositoryTypeSelect";
|
||||
@@ -33,8 +33,8 @@ import { Loading, Notification, useNavigationLock } from "@scm-manager/ui-compon
|
||||
|
||||
import ImportRepositoryFromBundle from "../components/ImportRepositoryFromBundle";
|
||||
import ImportFullRepository from "../components/ImportFullRepository";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import { CreatorComponentProps } from "../types";
|
||||
import { Prompt, Redirect } from "react-router-dom";
|
||||
import { extensionPoints } from "@scm-manager/ui-extensions";
|
||||
|
||||
const ImportPendingLoading = ({ importPending }: { importPending: boolean }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
@@ -50,8 +50,13 @@ const ImportPendingLoading = ({ importPending }: { importPending: boolean }) =>
|
||||
);
|
||||
};
|
||||
|
||||
const ImportRepository: FC<CreatorComponentProps> = ({ repositoryTypes, nameForm, informationForm }) => {
|
||||
const ImportRepository: extensionPoints.RepositoryCreatorExtension["component"] = ({
|
||||
repositoryTypes,
|
||||
nameForm,
|
||||
informationForm,
|
||||
}) => {
|
||||
const [importPending, setImportPending] = useState(false);
|
||||
const [importedRepository, setImportedRepository] = useState<Repository>();
|
||||
const [repositoryType, setRepositoryType] = useState<RepositoryType | undefined>();
|
||||
const [importType, setImportType] = useState("");
|
||||
const [t] = useTranslation("repos");
|
||||
@@ -67,9 +72,9 @@ const ImportRepository: FC<CreatorComponentProps> = ({ repositoryTypes, nameForm
|
||||
if (importType === "url") {
|
||||
return (
|
||||
<ImportRepositoryFromUrl
|
||||
url={((repositoryType!._links.import as Link[])!.find((link: Link) => link.name === "url") as Link).href}
|
||||
repositoryType={repositoryType!.name}
|
||||
repositoryType={repositoryType!}
|
||||
setImportPending={setImportPending}
|
||||
setImportedRepository={setImportedRepository}
|
||||
nameForm={nameForm}
|
||||
informationForm={informationForm}
|
||||
/>
|
||||
@@ -79,9 +84,9 @@ const ImportRepository: FC<CreatorComponentProps> = ({ repositoryTypes, nameForm
|
||||
if (importType === "bundle") {
|
||||
return (
|
||||
<ImportRepositoryFromBundle
|
||||
url={((repositoryType!._links.import as Link[])!.find((link: Link) => link.name === "bundle") as Link).href}
|
||||
repositoryType={repositoryType!.name}
|
||||
repositoryType={repositoryType!}
|
||||
setImportPending={setImportPending}
|
||||
setImportedRepository={setImportedRepository}
|
||||
nameForm={nameForm}
|
||||
informationForm={informationForm}
|
||||
/>
|
||||
@@ -91,11 +96,9 @@ const ImportRepository: FC<CreatorComponentProps> = ({ repositoryTypes, nameForm
|
||||
if (importType === "fullImport") {
|
||||
return (
|
||||
<ImportFullRepository
|
||||
url={
|
||||
((repositoryType!._links.import as Link[])!.find((link: Link) => link.name === "fullImport") as Link).href
|
||||
}
|
||||
repositoryType={repositoryType!.name}
|
||||
repositoryType={repositoryType!}
|
||||
setImportPending={setImportPending}
|
||||
setImportedRepository={setImportedRepository}
|
||||
nameForm={nameForm}
|
||||
informationForm={informationForm}
|
||||
/>
|
||||
@@ -105,6 +108,10 @@ const ImportRepository: FC<CreatorComponentProps> = ({ repositoryTypes, nameForm
|
||||
throw new Error("Unknown import type");
|
||||
};
|
||||
|
||||
if (importedRepository) {
|
||||
return <Redirect to={`/repo/${importedRepository.namespace}/${importedRepository.name}/code/sources`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Prompt when={importPending} message={t("import.navigationWarning")} />
|
||||
|
||||
@@ -22,47 +22,42 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { Link, Links, Repository, CUSTOM_NAMESPACE_STRATEGY } from "@scm-manager/ui-types";
|
||||
import React, { FC, useState } from "react";
|
||||
import { CUSTOM_NAMESPACE_STRATEGY, Repository } from "@scm-manager/ui-types";
|
||||
import { Button, ButtonGroup, ErrorNotification, InputField, Level, Loading, Modal } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import * as validator from "../components/form/repositoryValidation";
|
||||
|
||||
export const CONTENT_TYPE = "application/vnd.scmm-repository+json;v=2";
|
||||
import { useNamespaceStrategies, useRenameRepository } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
indexLinks: Links;
|
||||
};
|
||||
|
||||
const RenameRepository: FC<Props> = ({ repository, indexLinks }) => {
|
||||
const history = useHistory();
|
||||
const RenameRepository: FC<Props> = ({ repository }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [name, setName] = useState(repository.name);
|
||||
const [namespace, setNamespace] = useState(repository.namespace);
|
||||
const [nameValidationError, setNameValidationError] = useState(false);
|
||||
const [namespaceValidationError, setNamespaceValidationError] = useState(false);
|
||||
const [currentNamespaceStrategie, setCurrentNamespaceStrategy] = useState("");
|
||||
const { isLoading: isRenaming, renameRepository, isRenamed, error: renamingError } = useRenameRepository(repository);
|
||||
const {
|
||||
isLoading: isLoadingNamespaceStrategies,
|
||||
error: namespaceStrategyLoadError,
|
||||
data: namespaceStrategies,
|
||||
} = useNamespaceStrategies();
|
||||
|
||||
useEffect(() => {
|
||||
apiClient
|
||||
.get((indexLinks?.namespaceStrategies as Link).href)
|
||||
.then(result => result.json())
|
||||
.then(result => setCurrentNamespaceStrategy(result.current))
|
||||
.catch(setError);
|
||||
}, [repository]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
if (isRenamed) {
|
||||
return <Redirect to={`/repo/${namespace}/${name}`} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
if (namespaceStrategyLoadError) {
|
||||
return <ErrorNotification error={namespaceStrategyLoadError} />;
|
||||
}
|
||||
|
||||
if (isLoadingNamespaceStrategies) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
@@ -88,31 +83,19 @@ const RenameRepository: FC<Props> = ({ repository, indexLinks }) => {
|
||||
value: namespace,
|
||||
onChange: handleNamespaceChange,
|
||||
errorMessage: t("validation.namespace-invalid"),
|
||||
validationError: namespaceValidationError
|
||||
validationError: namespaceValidationError,
|
||||
};
|
||||
|
||||
if (currentNamespaceStrategie === CUSTOM_NAMESPACE_STRATEGY) {
|
||||
if (namespaceStrategies!.current === CUSTOM_NAMESPACE_STRATEGY) {
|
||||
return <InputField {...props} />;
|
||||
}
|
||||
|
||||
return <ExtensionPoint name="repos.create.namespace" props={props} renderAll={false} />;
|
||||
};
|
||||
|
||||
const rename = () => {
|
||||
setLoading(true);
|
||||
const url = repository?._links?.renameWithNamespace
|
||||
? (repository?._links?.renameWithNamespace as Link).href
|
||||
: (repository?._links?.rename as Link).href;
|
||||
|
||||
apiClient
|
||||
.post(url, { name, namespace }, CONTENT_TYPE)
|
||||
.then(() => setLoading(false))
|
||||
.then(() => history.push(`/repo/${namespace}/${name}`))
|
||||
.catch(setError);
|
||||
};
|
||||
|
||||
const modalBody = (
|
||||
<div>
|
||||
{renamingError ? <ErrorNotification error={renamingError} /> : null}
|
||||
<InputField
|
||||
label={t("renameRepo.modal.label.repoName")}
|
||||
name={t("renameRepo.modal.label.repoName")}
|
||||
@@ -135,7 +118,8 @@ const RenameRepository: FC<Props> = ({ repository, indexLinks }) => {
|
||||
label={t("renameRepo.modal.button.rename")}
|
||||
disabled={!isValid}
|
||||
title={t("renameRepo.modal.button.rename")}
|
||||
action={rename}
|
||||
loading={isRenaming}
|
||||
action={() => renameRepository(namespace, name)}
|
||||
/>
|
||||
<Button
|
||||
label={t("renameRepo.modal.button.cancel")}
|
||||
@@ -165,15 +149,7 @@ const RenameRepository: FC<Props> = ({ repository, indexLinks }) => {
|
||||
{t("renameRepo.description2")}
|
||||
</p>
|
||||
}
|
||||
right={
|
||||
<Button
|
||||
label={t("renameRepo.button")}
|
||||
action={() => setShowModal(true)}
|
||||
loading={loading}
|
||||
color="warning"
|
||||
icon="edit"
|
||||
/>
|
||||
}
|
||||
right={<Button label={t("renameRepo.button")} action={() => setShowModal(true)} color="warning" icon="edit" />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { Repository, Links } from "@scm-manager/ui-types";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import RenameRepository from "./RenameRepository";
|
||||
import DeleteRepo from "./DeleteRepo";
|
||||
import styled from "styled-components";
|
||||
@@ -34,7 +34,6 @@ import UnarchiveRepo from "./UnarchiveRepo";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
indexLinks: Links;
|
||||
};
|
||||
|
||||
export const DangerZoneContainer = styled.div`
|
||||
@@ -60,12 +59,12 @@ export const DangerZoneContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const RepositoryDangerZone: FC<Props> = ({ repository, indexLinks }) => {
|
||||
const RepositoryDangerZone: FC<Props> = ({ repository }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
const dangerZone = [];
|
||||
if (repository?._links?.rename || repository?._links?.renameWithNamespace) {
|
||||
dangerZone.push(<RenameRepository repository={repository} indexLinks={indexLinks} />);
|
||||
dangerZone.push(<RenameRepository repository={repository} />);
|
||||
}
|
||||
if (repository?._links?.delete) {
|
||||
dangerZone.push(<DeleteRepo repository={repository} />);
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { getContent } from "./SourcecodeViewer";
|
||||
import { File, Link } from "@scm-manager/ui-types";
|
||||
import React, { FC } from "react";
|
||||
import { File } from "@scm-manager/ui-types";
|
||||
import { ErrorNotification, Loading, MarkdownView } from "@scm-manager/ui-components";
|
||||
import styled from "styled-components";
|
||||
import replaceBranchWithRevision from "../../ReplaceBranchWithRevision";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useFileContent } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = {
|
||||
file: File;
|
||||
@@ -39,24 +39,10 @@ const MarkdownContent = styled.div`
|
||||
`;
|
||||
|
||||
const MarkdownViewer: FC<Props> = ({ file, basePath }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [content, setContent] = useState("");
|
||||
const { isLoading, error, data: content } = useFileContent(file);
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
getContent((file._links.self as Link).href)
|
||||
.then(content => {
|
||||
setLoading(false);
|
||||
setContent(content);
|
||||
})
|
||||
.catch(error => {
|
||||
setLoading(false);
|
||||
setError(error);
|
||||
});
|
||||
}, [file]);
|
||||
|
||||
if (loading) {
|
||||
if (!content || isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,12 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { apiClient, ErrorNotification, Loading, SyntaxHighlighter } from "@scm-manager/ui-components";
|
||||
import { File, Link } from "@scm-manager/ui-types";
|
||||
import React, { FC } from "react";
|
||||
import { ErrorNotification, Loading, SyntaxHighlighter } from "@scm-manager/ui-components";
|
||||
import { File } from "@scm-manager/ui-types";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import replaceBranchWithRevision from "../../ReplaceBranchWithRevision";
|
||||
import { useFileContent } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = {
|
||||
file: File;
|
||||
@@ -33,47 +34,20 @@ type Props = {
|
||||
};
|
||||
|
||||
const SourcecodeViewer: FC<Props> = ({ file, language }) => {
|
||||
const [content, setContent] = useState("");
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentFileRevision, setCurrentFileRevision] = useState("");
|
||||
const { error, isLoading, data: content } = useFileContent(file);
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (file.revision !== currentFileRevision) {
|
||||
getContent((file._links.self as Link).href)
|
||||
.then(content => {
|
||||
setContent(content);
|
||||
setCurrentFileRevision(file.revision);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(setError);
|
||||
}
|
||||
}, [currentFileRevision, file]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
if (!content || isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
const permalink = replaceBranchWithRevision(location.pathname, file.revision);
|
||||
|
||||
const permalink = replaceBranchWithRevision(location.pathname, currentFileRevision);
|
||||
|
||||
return <SyntaxHighlighter language={getLanguage(language)} value={content} permalink={permalink} />;
|
||||
return <SyntaxHighlighter language={language.toLowerCase()} value={content} permalink={permalink} />;
|
||||
};
|
||||
|
||||
export function getLanguage(language: string) {
|
||||
return language.toLowerCase();
|
||||
}
|
||||
|
||||
export function getContent(url: string) {
|
||||
return apiClient.get(url).then(response => response.text());
|
||||
}
|
||||
|
||||
export default SourcecodeViewer;
|
||||
|
||||
@@ -21,140 +21,68 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import React, { FC, FormEvent, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { User } from "@scm-manager/ui-types";
|
||||
import {
|
||||
ErrorNotification,
|
||||
Subtitle,
|
||||
Level,
|
||||
Notification,
|
||||
PasswordConfirmation,
|
||||
SubmitButton
|
||||
SubmitButton,
|
||||
Subtitle,
|
||||
} from "@scm-manager/ui-components";
|
||||
import { setPassword } from "./setPassword";
|
||||
import { useSetUserPassword } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
user: User;
|
||||
};
|
||||
|
||||
type State = {
|
||||
password: string;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
passwordChanged: boolean;
|
||||
passwordValid: boolean;
|
||||
const SetUserPassword: FC<Props> = ({ user }) => {
|
||||
const [t] = useTranslation("users");
|
||||
const { passwordOverwritten, setPassword, error, isLoading, reset } = useSetUserPassword(user);
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [passwordValid, setPasswordValid] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (passwordOverwritten) {
|
||||
setNewPassword("");
|
||||
setPasswordValid(false);
|
||||
}
|
||||
}, [passwordOverwritten]);
|
||||
|
||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (newPassword) {
|
||||
setPassword(newPassword);
|
||||
}
|
||||
};
|
||||
|
||||
const onPasswordChange = (newValue: string, valid: boolean) => {
|
||||
setNewPassword(newValue);
|
||||
setPasswordValid(!!newValue && valid);
|
||||
};
|
||||
|
||||
let message;
|
||||
|
||||
if (passwordOverwritten) {
|
||||
message = (
|
||||
<Notification type={"success"} children={t("singleUserPassword.setPasswordSuccessful")} onClose={reset} />
|
||||
);
|
||||
} else if (error) {
|
||||
message = <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={submit}>
|
||||
<Subtitle subtitle={t("singleUserPassword.subtitle")} />
|
||||
{message}
|
||||
<PasswordConfirmation passwordChanged={onPasswordChange} key={passwordOverwritten ? "changed" : "unchanged"} />
|
||||
<Level
|
||||
right={<SubmitButton disabled={!passwordValid} loading={isLoading} label={t("singleUserPassword.button")} />}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
class SetUserPassword extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
password: "",
|
||||
loading: false,
|
||||
passwordChanged: false,
|
||||
passwordValid: false
|
||||
};
|
||||
}
|
||||
|
||||
setLoadingState = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loading: true
|
||||
});
|
||||
};
|
||||
|
||||
setErrorState = (error: Error) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
error: error,
|
||||
loading: false
|
||||
});
|
||||
};
|
||||
|
||||
setSuccessfulState = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loading: false,
|
||||
passwordChanged: true,
|
||||
password: ""
|
||||
});
|
||||
};
|
||||
|
||||
submit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
if (this.state.password) {
|
||||
const { user } = this.props;
|
||||
const { password } = this.state;
|
||||
this.setLoadingState();
|
||||
setPassword(user._links.password.href, password)
|
||||
.then((result) => {
|
||||
if (result.error) {
|
||||
this.setErrorState(result.error);
|
||||
} else {
|
||||
this.setSuccessfulState();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.setErrorState(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { loading, passwordChanged, error } = this.state;
|
||||
|
||||
let message = null;
|
||||
|
||||
if (passwordChanged) {
|
||||
message = (
|
||||
<Notification
|
||||
type={"success"}
|
||||
children={t("singleUserPassword.setPasswordSuccessful")}
|
||||
onClose={() => this.onClose()}
|
||||
/>
|
||||
);
|
||||
} else if (error) {
|
||||
message = <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={this.submit}>
|
||||
<Subtitle subtitle={t("singleUserPassword.subtitle")} />
|
||||
{message}
|
||||
<PasswordConfirmation
|
||||
passwordChanged={this.passwordChanged}
|
||||
key={this.state.passwordChanged ? "changed" : "unchanged"}
|
||||
/>
|
||||
<Level
|
||||
right={
|
||||
<SubmitButton
|
||||
disabled={!this.state.passwordValid}
|
||||
loading={loading}
|
||||
label={t("singleUserPassword.button")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
passwordChanged = (password: string, passwordValid: boolean) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
password,
|
||||
passwordValid: !!password && passwordValid
|
||||
});
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
passwordChanged: false
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default withTranslation("users")(SetUserPassword);
|
||||
export default SetUserPassword;
|
||||
|
||||
@@ -23,40 +23,34 @@
|
||||
*/
|
||||
|
||||
import React, { FC, useState } from "react";
|
||||
import {
|
||||
apiClient,
|
||||
ErrorNotification,
|
||||
InputField,
|
||||
Level,
|
||||
Loading,
|
||||
SubmitButton,
|
||||
Subtitle
|
||||
} from "@scm-manager/ui-components";
|
||||
import { ErrorNotification, InputField, Level, Loading, SubmitButton, Subtitle } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CONTENT_TYPE_API_KEY } from "./SetApiKeys";
|
||||
import RoleSelector from "../../../repos/permissions/components/RoleSelector";
|
||||
import ApiKeyCreatedModal from "./ApiKeyCreatedModal";
|
||||
import { useRepositoryRoles } from "@scm-manager/ui-api";
|
||||
import { useCreateApiKey, useRepositoryRoles } from "@scm-manager/ui-api";
|
||||
import { ApiKeysCollection, Me, User } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
createLink: string;
|
||||
refresh: () => void;
|
||||
user: User | Me;
|
||||
apiKeys: ApiKeysCollection;
|
||||
};
|
||||
|
||||
const AddApiKey: FC<Props> = ({ createLink, refresh }) => {
|
||||
const AddApiKey: FC<Props> = ({ user, apiKeys }) => {
|
||||
const [t] = useTranslation("users");
|
||||
const [isCurrentlyAddingKey, setCurrentlyAddingKey] = useState(false);
|
||||
const [errorAddingKey, setErrorAddingKey] = useState<undefined | Error>();
|
||||
const {
|
||||
isLoading: isCurrentlyAddingKey,
|
||||
error: errorAddingKey,
|
||||
apiKey: addedKey,
|
||||
create,
|
||||
reset: resetCreationHook,
|
||||
} = useCreateApiKey(user, apiKeys);
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [permissionRole, setPermissionRole] = useState("");
|
||||
const [addedKey, setAddedKey] = useState("");
|
||||
const {
|
||||
isLoading: isLoadingRepositoryRoles,
|
||||
data: availableRepositoryRoles,
|
||||
error: errorLoadingRepositoryRoles
|
||||
error: errorLoadingRepositoryRoles,
|
||||
} = useRepositoryRoles();
|
||||
const loading = isCurrentlyAddingKey || isLoadingRepositoryRoles;
|
||||
const error = errorAddingKey || errorLoadingRepositoryRoles;
|
||||
|
||||
const isValid = () => {
|
||||
return !!displayName && !!permissionRole;
|
||||
@@ -67,32 +61,21 @@ const AddApiKey: FC<Props> = ({ createLink, refresh }) => {
|
||||
setPermissionRole("");
|
||||
};
|
||||
|
||||
const addKey = () => {
|
||||
setCurrentlyAddingKey(true);
|
||||
apiClient
|
||||
.post(createLink, { displayName: displayName, permissionRole: permissionRole }, CONTENT_TYPE_API_KEY)
|
||||
.then(response => response.text())
|
||||
.then(setAddedKey)
|
||||
.then(() => setCurrentlyAddingKey(false))
|
||||
.catch(setErrorAddingKey);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
if (errorLoadingRepositoryRoles) {
|
||||
return <ErrorNotification error={errorLoadingRepositoryRoles} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
if (isLoadingRepositoryRoles) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const availableRoleNames = availableRepositoryRoles
|
||||
? availableRepositoryRoles._embedded.repositoryRoles.map(r => r.name)
|
||||
? availableRepositoryRoles._embedded.repositoryRoles.map((r) => r.name)
|
||||
: [];
|
||||
|
||||
const closeModal = () => {
|
||||
resetForm();
|
||||
refresh();
|
||||
setAddedKey("");
|
||||
resetCreationHook();
|
||||
};
|
||||
|
||||
const newKeyModal = addedKey && <ApiKeyCreatedModal addedKey={addedKey} close={closeModal} />;
|
||||
@@ -100,6 +83,7 @@ const AddApiKey: FC<Props> = ({ createLink, refresh }) => {
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
{errorAddingKey ? <ErrorNotification error={errorAddingKey} /> : null}
|
||||
<Subtitle subtitle={t("apiKey.addSubtitle")} />
|
||||
{newKeyModal}
|
||||
<InputField label={t("apiKey.displayName")} value={displayName} onChange={setDisplayName} />
|
||||
@@ -112,7 +96,14 @@ const AddApiKey: FC<Props> = ({ createLink, refresh }) => {
|
||||
role={permissionRole}
|
||||
/>
|
||||
<Level
|
||||
right={<SubmitButton label={t("apiKey.addKey")} loading={loading} disabled={!isValid()} action={addKey} />}
|
||||
right={
|
||||
<SubmitButton
|
||||
label={t("apiKey.addKey")}
|
||||
loading={isCurrentlyAddingKey}
|
||||
disabled={!isValid() || isCurrentlyAddingKey}
|
||||
action={() => create({ displayName, permissionRole })}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -26,9 +26,10 @@ import React, { FC, useRef, useState } from "react";
|
||||
import { Button, Icon, Modal } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { ApiKeyWithToken } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
addedKey: string;
|
||||
addedKey: ApiKeyWithToken;
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
@@ -46,10 +47,10 @@ const NoLeftMargin = styled.div`
|
||||
const ApiKeyCreatedModal: FC<Props> = ({ addedKey, close }) => {
|
||||
const [t] = useTranslation("users");
|
||||
const [copied, setCopied] = useState(false);
|
||||
const keyRef = useRef(null);
|
||||
const keyRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const copy = () => {
|
||||
keyRef.current.select();
|
||||
keyRef.current!.select();
|
||||
document.execCommand("copy");
|
||||
setCopied(true);
|
||||
};
|
||||
@@ -63,10 +64,15 @@ const ApiKeyCreatedModal: FC<Props> = ({ addedKey, close }) => {
|
||||
<hr />
|
||||
<div className={"columns"}>
|
||||
<div className={"column is-11"}>
|
||||
<KeyArea wrap={"soft"} ref={keyRef} className={"input"} value={addedKey} />
|
||||
<KeyArea wrap={"soft"} ref={keyRef} className={"input"} value={addedKey.token} />
|
||||
</div>
|
||||
<NoLeftMargin className={"column is-1"}>
|
||||
<Icon className={"is-hidden-mobile fa-2x"} name={copied ? "clipboard-check" : "clipboard"} title={t("apiKey.modal.clipboard")} onClick={copy} />
|
||||
<Icon
|
||||
className={"is-hidden-mobile fa-2x"}
|
||||
name={copied ? "clipboard-check" : "clipboard"}
|
||||
title={t("apiKey.modal.clipboard")}
|
||||
onClick={copy}
|
||||
/>
|
||||
</NoLeftMargin>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { DateFromNow, Icon } from "@scm-manager/ui-components";
|
||||
import { ApiKey } from "./SetApiKeys";
|
||||
import { Link } from "@scm-manager/ui-types";
|
||||
import { ApiKey } from "@scm-manager/ui-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DeleteFunction } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = {
|
||||
apiKey: ApiKey;
|
||||
onDelete: (link: string) => void;
|
||||
onDelete: DeleteFunction<ApiKey>;
|
||||
};
|
||||
|
||||
export const ApiKeyEntry: FC<Props> = ({ apiKey, onDelete }) => {
|
||||
@@ -38,7 +38,7 @@ export const ApiKeyEntry: FC<Props> = ({ apiKey, onDelete }) => {
|
||||
let deleteButton;
|
||||
if (apiKey?._links?.delete) {
|
||||
deleteButton = (
|
||||
<a className="level-item" onClick={() => onDelete((apiKey._links.delete as Link).href)}>
|
||||
<a className="level-item" onClick={() => onDelete(apiKey)}>
|
||||
<span className="icon">
|
||||
<Icon name="trash" title={t("apiKey.delete")} color="inherit" />
|
||||
</span>
|
||||
|
||||
@@ -24,13 +24,14 @@
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ApiKey, ApiKeysCollection } from "./SetApiKeys";
|
||||
import ApiKeyEntry from "./ApiKeyEntry";
|
||||
import { Notification } from "@scm-manager/ui-components";
|
||||
import { ApiKey, ApiKeysCollection } from "@scm-manager/ui-types";
|
||||
import { DeleteFunction } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = {
|
||||
apiKeys?: ApiKeysCollection;
|
||||
onDelete: (link: string) => void;
|
||||
onDelete: DeleteFunction<ApiKey>;
|
||||
};
|
||||
|
||||
const ApiKeyTable: FC<Props> = ({ apiKeys, onDelete }) => {
|
||||
|
||||
@@ -22,29 +22,14 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Collection, Links, User, Me } from "@scm-manager/ui-types";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { apiClient, ErrorNotification, Loading, Subtitle } from "@scm-manager/ui-components";
|
||||
import { Link, Me, User } from "@scm-manager/ui-types";
|
||||
import React, { FC } from "react";
|
||||
import { ErrorNotification, Loading, Subtitle } from "@scm-manager/ui-components";
|
||||
import ApiKeyTable from "./ApiKeyTable";
|
||||
import AddApiKey from "./AddApiKey";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type ApiKeysCollection = Collection & {
|
||||
_embedded: {
|
||||
keys: ApiKey[];
|
||||
};
|
||||
};
|
||||
|
||||
export type ApiKey = {
|
||||
id: string;
|
||||
displayName: string;
|
||||
permissionRole: string;
|
||||
created: string;
|
||||
_links: Links;
|
||||
};
|
||||
|
||||
export const CONTENT_TYPE_API_KEY = "application/vnd.scmm-apiKey+json;v=2";
|
||||
import { useApiKeys, useDeleteApiKey } from "@scm-manager/ui-api";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
user: User | Me;
|
||||
@@ -52,30 +37,9 @@ type Props = {
|
||||
|
||||
const SetApiKeys: FC<Props> = ({ user }) => {
|
||||
const [t] = useTranslation("users");
|
||||
const [error, setError] = useState<undefined | Error>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [apiKeys, setApiKeys] = useState<ApiKeysCollection | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
fetchApiKeys();
|
||||
}, [user]);
|
||||
|
||||
const fetchApiKeys = () => {
|
||||
setLoading(true);
|
||||
apiClient
|
||||
.get((user._links.apiKeys as Link).href)
|
||||
.then(r => r.json())
|
||||
.then(setApiKeys)
|
||||
.then(() => setLoading(false))
|
||||
.catch(setError);
|
||||
};
|
||||
|
||||
const onDelete = (link: string) => {
|
||||
apiClient
|
||||
.delete(link)
|
||||
.then(fetchApiKeys)
|
||||
.catch(setError);
|
||||
};
|
||||
const { isLoading, data: apiKeys, error: fetchError } = useApiKeys(user);
|
||||
const { error: deletionError, remove } = useDeleteApiKey(user);
|
||||
const error = deletionError || fetchError;
|
||||
|
||||
const createLink = (apiKeys?._links?.create as Link)?.href;
|
||||
|
||||
@@ -83,7 +47,7 @@ const SetApiKeys: FC<Props> = ({ user }) => {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
if (!apiKeys || isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
@@ -91,12 +55,12 @@ const SetApiKeys: FC<Props> = ({ user }) => {
|
||||
<>
|
||||
<Subtitle subtitle={t("apiKey.subtitle")} />
|
||||
<p>
|
||||
{t("apiKey.text1")} <Link to={"/admin/roles/"}>{t("apiKey.manageRoles")}</Link>
|
||||
{t("apiKey.text1")} <RouterLink to={"/admin/roles/"}>{t("apiKey.manageRoles")}</RouterLink>
|
||||
</p>
|
||||
<p>{t("apiKey.text2")}</p>
|
||||
<br />
|
||||
<ApiKeyTable apiKeys={apiKeys} onDelete={onDelete} />
|
||||
{createLink && <AddApiKey createLink={createLink} refresh={fetchApiKeys} />}
|
||||
<ApiKeyTable apiKeys={apiKeys} onDelete={remove} />
|
||||
{createLink && <AddApiKey user={user} apiKeys={apiKeys} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,28 +23,19 @@
|
||||
*/
|
||||
|
||||
import React, { FC, useState } from "react";
|
||||
import {
|
||||
ErrorNotification,
|
||||
InputField,
|
||||
Level,
|
||||
Textarea,
|
||||
SubmitButton,
|
||||
apiClient,
|
||||
Loading,
|
||||
Subtitle
|
||||
} from "@scm-manager/ui-components";
|
||||
import { ErrorNotification, InputField, Level, SubmitButton, Subtitle, Textarea } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CONTENT_TYPE_PUBLIC_KEY } from "./SetPublicKeys";
|
||||
import { Me, PublicKeysCollection, User } from "@scm-manager/ui-types";
|
||||
import { useCreatePublicKey } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = {
|
||||
createLink: string;
|
||||
refresh: () => void;
|
||||
publicKeys: PublicKeysCollection;
|
||||
user: User | Me;
|
||||
};
|
||||
|
||||
const AddPublicKey: FC<Props> = ({ createLink, refresh }) => {
|
||||
const AddPublicKey: FC<Props> = ({ user, publicKeys }) => {
|
||||
const [t] = useTranslation("users");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<undefined | Error>();
|
||||
const { isLoading, error, create } = useCreatePublicKey(user, publicKeys);
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [raw, setRaw] = useState("");
|
||||
|
||||
@@ -58,31 +49,26 @@ const AddPublicKey: FC<Props> = ({ createLink, refresh }) => {
|
||||
};
|
||||
|
||||
const addKey = () => {
|
||||
setLoading(true);
|
||||
apiClient
|
||||
.post(createLink, { displayName: displayName, raw: raw }, CONTENT_TYPE_PUBLIC_KEY)
|
||||
.then(resetForm)
|
||||
.then(refresh)
|
||||
.then(() => setLoading(false))
|
||||
.catch(setError);
|
||||
create({ raw, displayName });
|
||||
resetForm();
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
{error ? <ErrorNotification error={error} /> : null}
|
||||
<Subtitle subtitle={t("publicKey.addSubtitle")} />
|
||||
<InputField label={t("publicKey.displayName")} value={displayName} onChange={setDisplayName} />
|
||||
<Textarea name="raw" label={t("publicKey.raw")} value={raw} onChange={setRaw} />
|
||||
<Level
|
||||
right={<SubmitButton label={t("publicKey.addKey")} loading={loading} disabled={!isValid()} action={addKey} />}
|
||||
right={
|
||||
<SubmitButton
|
||||
label={t("publicKey.addKey")}
|
||||
loading={isLoading}
|
||||
disabled={!isValid() || isLoading}
|
||||
action={addKey}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { DateFromNow, Icon } from "@scm-manager/ui-components";
|
||||
import { PublicKey } from "./SetPublicKeys";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "@scm-manager/ui-types";
|
||||
import { Link, PublicKey } from "@scm-manager/ui-types";
|
||||
import { DeleteFunction } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = {
|
||||
publicKey: PublicKey;
|
||||
onDelete: (link: string) => void;
|
||||
onDelete: DeleteFunction<PublicKey>;
|
||||
};
|
||||
|
||||
export const PublicKeyEntry: FC<Props> = ({ publicKey, onDelete }) => {
|
||||
@@ -39,7 +39,7 @@ export const PublicKeyEntry: FC<Props> = ({ publicKey, onDelete }) => {
|
||||
let deleteButton;
|
||||
if (publicKey?._links?.delete) {
|
||||
deleteButton = (
|
||||
<a className="level-item" onClick={() => onDelete((publicKey._links.delete as Link).href)}>
|
||||
<a className="level-item" onClick={() => onDelete(publicKey)}>
|
||||
<span className="icon">
|
||||
<Icon name="trash" title={t("publicKey.delete")} color="inherit" />
|
||||
</span>
|
||||
|
||||
@@ -24,13 +24,14 @@
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PublicKey, PublicKeysCollection } from "./SetPublicKeys";
|
||||
import PublicKeyEntry from "./PublicKeyEntry";
|
||||
import { Notification } from "@scm-manager/ui-components";
|
||||
import { DeleteFunction } from "@scm-manager/ui-api";
|
||||
import { PublicKey, PublicKeysCollection } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
publicKeys?: PublicKeysCollection;
|
||||
onDelete: (link: string) => void;
|
||||
onDelete: DeleteFunction<PublicKey>;
|
||||
};
|
||||
|
||||
const PublicKeyTable: FC<Props> = ({ publicKeys, onDelete }) => {
|
||||
@@ -51,9 +52,9 @@ const PublicKeyTable: FC<Props> = ({ publicKeys, onDelete }) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{publicKeys?._embedded?.keys?.map((publicKey: PublicKey, index: number) => {
|
||||
return <PublicKeyEntry key={index} onDelete={onDelete} publicKey={publicKey} />;
|
||||
})}
|
||||
{publicKeys?._embedded?.keys?.map((publicKey: PublicKey, index: number) => (
|
||||
<PublicKeyEntry key={index} onDelete={onDelete} publicKey={publicKey} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
@@ -22,28 +22,13 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Collection, Link, Links, User, Me } from "@scm-manager/ui-types";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { Link, Me, User } from "@scm-manager/ui-types";
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AddPublicKey from "./AddPublicKey";
|
||||
import PublicKeyTable from "./PublicKeyTable";
|
||||
import { apiClient, ErrorNotification, Loading, Subtitle } from "@scm-manager/ui-components";
|
||||
|
||||
export type PublicKeysCollection = Collection & {
|
||||
_embedded: {
|
||||
keys: PublicKey[];
|
||||
};
|
||||
};
|
||||
|
||||
export type PublicKey = {
|
||||
id: string;
|
||||
displayName: string;
|
||||
raw: string;
|
||||
created?: string;
|
||||
_links: Links;
|
||||
};
|
||||
|
||||
export const CONTENT_TYPE_PUBLIC_KEY = "application/vnd.scmm-publicKey+json;v=2";
|
||||
import { ErrorNotification, Loading, Subtitle } from "@scm-manager/ui-components";
|
||||
import { useDeletePublicKey, usePublicKeys } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = {
|
||||
user: User | Me;
|
||||
@@ -51,30 +36,9 @@ type Props = {
|
||||
|
||||
const SetPublicKeys: FC<Props> = ({ user }) => {
|
||||
const [t] = useTranslation("users");
|
||||
const [error, setError] = useState<undefined | Error>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [publicKeys, setPublicKeys] = useState<PublicKeysCollection | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPublicKeys();
|
||||
}, [user]);
|
||||
|
||||
const fetchPublicKeys = () => {
|
||||
setLoading(true);
|
||||
apiClient
|
||||
.get((user._links.publicKeys as Link).href)
|
||||
.then(r => r.json())
|
||||
.then(setPublicKeys)
|
||||
.then(() => setLoading(false))
|
||||
.catch(setError);
|
||||
};
|
||||
|
||||
const onDelete = (link: string) => {
|
||||
apiClient
|
||||
.delete(link)
|
||||
.then(fetchPublicKeys)
|
||||
.catch(setError);
|
||||
};
|
||||
const { error: fetchingError, isLoading, data: publicKeys } = usePublicKeys(user);
|
||||
const { error: deletionError, remove } = useDeletePublicKey(user);
|
||||
const error = fetchingError || deletionError;
|
||||
|
||||
const createLink = (publicKeys?._links?.create as Link)?.href;
|
||||
|
||||
@@ -82,7 +46,7 @@ const SetPublicKeys: FC<Props> = ({ user }) => {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
if (!publicKeys || isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
@@ -91,8 +55,8 @@ const SetPublicKeys: FC<Props> = ({ user }) => {
|
||||
<Subtitle subtitle={t("publicKey.subtitle")} />
|
||||
<p>{t("publicKey.description")}</p>
|
||||
<br />
|
||||
<PublicKeyTable publicKeys={publicKeys} onDelete={onDelete} />
|
||||
{createLink && <AddPublicKey createLink={createLink} refresh={fetchPublicKeys} />}
|
||||
<PublicKeyTable publicKeys={publicKeys} onDelete={remove} />
|
||||
{createLink && <AddPublicKey publicKeys={publicKeys} user={user} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
import React, { FC } from "react";
|
||||
import { Route, useParams, useRouteMatch } from "react-router-dom";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import { Link } from "@scm-manager/ui-types";
|
||||
import {
|
||||
CustomQueryFlexWrappedColumns,
|
||||
ErrorPage,
|
||||
@@ -36,7 +35,7 @@ import {
|
||||
SecondaryNavigationColumn,
|
||||
StateMenuContextProvider,
|
||||
SubNavigation,
|
||||
urls
|
||||
urls,
|
||||
} from "@scm-manager/ui-components";
|
||||
import { Details } from "./../components/table";
|
||||
import EditUser from "./EditUser";
|
||||
@@ -45,14 +44,14 @@ import {
|
||||
SetApiKeysNavLink,
|
||||
SetPasswordNavLink,
|
||||
SetPermissionsNavLink,
|
||||
SetPublicKeysNavLink
|
||||
SetPublicKeysNavLink,
|
||||
} from "./../components/navLinks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SetUserPassword from "../components/SetUserPassword";
|
||||
import SetPermissions from "../../permissions/components/SetPermissions";
|
||||
import SetPublicKeys from "../components/publicKeys/SetPublicKeys";
|
||||
import SetApiKeys from "../components/apiKeys/SetApiKeys";
|
||||
import { useUser } from "@scm-manager/ui-api";
|
||||
import SetUserPermissions from "../../permissions/components/SetUserPermissions";
|
||||
|
||||
const SingleUser: FC = () => {
|
||||
const [t] = useTranslation("users");
|
||||
@@ -72,7 +71,7 @@ const SingleUser: FC = () => {
|
||||
|
||||
const extensionProps = {
|
||||
user,
|
||||
url
|
||||
url,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -90,7 +89,7 @@ const SingleUser: FC = () => {
|
||||
<SetUserPassword user={user} />
|
||||
</Route>
|
||||
<Route path={`${url}/settings/permissions`}>
|
||||
<SetPermissions selectedPermissionsLink={user._links.permissions as Link} />
|
||||
<SetUserPermissions user={user} />
|
||||
</Route>
|
||||
<Route path={`${url}/settings/publickeys`}>
|
||||
<SetPublicKeys user={user} />
|
||||
|
||||
Reference in New Issue
Block a user