Adds frontend for cms page
This commit is contained in:
@@ -82,6 +82,35 @@ export interface ChangeRolePayload {
|
||||
|
||||
export type AvailabilityStatus = 'Available' | 'Maintenance' | 'NotAvailable';
|
||||
|
||||
export type CmsInstanceStatus = 'Available' | 'NotAvailable' | 'Inactive';
|
||||
|
||||
export interface CmsInstance {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
status: CmsInstanceStatus;
|
||||
disableMessage: string | null;
|
||||
lastContactedAt: string | null;
|
||||
lastStatusPushedAt: string | null;
|
||||
lastIntegrityCheckFailedAt: string | null;
|
||||
}
|
||||
|
||||
export interface CreateCmsInstanceRequest {
|
||||
name: string;
|
||||
url: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export interface UpdateCmsInstanceStatusRequest {
|
||||
status: CmsInstanceStatus;
|
||||
disableMessage: string | null;
|
||||
}
|
||||
|
||||
export interface UpdateStatusResult {
|
||||
success: boolean;
|
||||
slaveContactSuccess: boolean;
|
||||
}
|
||||
|
||||
export interface AvailabilityResponse {
|
||||
status: AvailabilityStatus;
|
||||
checkedAt: string; // ISO 8601
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { renderHook, waitFor, act } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { createElement } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { server } from '@/mocks/server';
|
||||
import { API_BASE } from '@/mocks/auth/fixtures';
|
||||
import { useAddCmsInstance } from './useAddCmsInstance';
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
});
|
||||
return ({ children }: { children: React.ReactNode }) =>
|
||||
createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
}
|
||||
|
||||
const CMS_URL = `${API_BASE}/api/v1/CmsInstances`;
|
||||
|
||||
describe('useAddCmsInstance', () => {
|
||||
it('returns created instance on success', async () => {
|
||||
const { result } = renderHook(() => useAddCmsInstance(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
name: 'Test CMS',
|
||||
url: 'http://test.example.com',
|
||||
apiKey: 'secret',
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(result.current.data?.name).toBe('Test CMS');
|
||||
});
|
||||
|
||||
it('calls onError callback on HTTP 400', async () => {
|
||||
server.use(http.post(CMS_URL, () => HttpResponse.json({}, { status: 400 })));
|
||||
|
||||
const onError = vi.fn();
|
||||
const { result } = renderHook(() => useAddCmsInstance({ onError }), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync({ name: 'X', url: 'http://x.com', apiKey: 'k' });
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
});
|
||||
|
||||
await waitFor(() => expect(onError).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useMutation, useQueryClient, type UseMutationOptions } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api-client';
|
||||
import type { CmsInstance, CreateCmsInstanceRequest } from './types';
|
||||
|
||||
export function useAddCmsInstance(
|
||||
options?: Pick<UseMutationOptions<CmsInstance, Error, CreateCmsInstanceRequest>, 'onError'>,
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<CmsInstance, Error, CreateCmsInstanceRequest>({
|
||||
mutationFn: (data) => api.post<CmsInstance>('/api/v1/CmsInstances', data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['cmsInstances'] }),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { createElement } from 'react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { server } from '@/mocks/server';
|
||||
import { API_BASE } from '@/mocks/auth/fixtures';
|
||||
import { getMockCmsInstances } from '@/mocks/cms/handlers';
|
||||
import { useCmsInstances } from './useCmsInstances';
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
});
|
||||
return ({ children }: { children: React.ReactNode }) =>
|
||||
createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
}
|
||||
|
||||
const CMS_URL = `${API_BASE}/api/v1/CmsInstances`;
|
||||
|
||||
describe('useCmsInstances', () => {
|
||||
it('starts in loading state', () => {
|
||||
const { result } = renderHook(() => useCmsInstances(), { wrapper: createWrapper() });
|
||||
expect(result.current.isPending).toBe(true);
|
||||
});
|
||||
|
||||
it('returns the instances list on success', async () => {
|
||||
const { result } = renderHook(() => useCmsInstances(), { wrapper: createWrapper() });
|
||||
await waitFor(() => expect(result.current.isPending).toBe(false));
|
||||
|
||||
expect(result.current.isError).toBe(false);
|
||||
expect(result.current.data).toHaveLength(getMockCmsInstances().length);
|
||||
expect(result.current.data?.[0].name).toBe(getMockCmsInstances()[0].name);
|
||||
});
|
||||
|
||||
it('enters error state on network error', async () => {
|
||||
server.use(http.get(CMS_URL, () => HttpResponse.json({}, { status: 500 })));
|
||||
|
||||
const { result } = renderHook(() => useCmsInstances(), { wrapper: createWrapper() });
|
||||
await waitFor(() => expect(result.current.isPending).toBe(false));
|
||||
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api-client';
|
||||
import type { CmsInstance } from './types';
|
||||
|
||||
export function useCmsInstances() {
|
||||
return useQuery<CmsInstance[], Error>({
|
||||
queryKey: ['cmsInstances'],
|
||||
queryFn: () => api.get<CmsInstance[]>('/api/v1/CmsInstances'),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { renderHook, waitFor, act } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { createElement } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { server } from '@/mocks/server';
|
||||
import { API_BASE } from '@/mocks/auth/fixtures';
|
||||
import { useUpdateCmsInstanceStatus } from './useUpdateCmsInstanceStatus';
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
});
|
||||
return ({ children }: { children: React.ReactNode }) =>
|
||||
createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
}
|
||||
|
||||
const INSTANCE_ID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
||||
const STATUS_URL = `${API_BASE}/api/v1/CmsInstances/${INSTANCE_ID}/status`;
|
||||
|
||||
describe('useUpdateCmsInstanceStatus', () => {
|
||||
it('returns UpdateStatusResult on success', async () => {
|
||||
const onSuccess = vi.fn();
|
||||
const { result } = renderHook(() => useUpdateCmsInstanceStatus({ onSuccess }), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
id: INSTANCE_ID,
|
||||
status: 'NotAvailable',
|
||||
disableMessage: 'Under maintenance',
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => expect(onSuccess).toHaveBeenCalled());
|
||||
const callArg = onSuccess.mock.calls[0][0];
|
||||
expect(callArg.success).toBe(true);
|
||||
expect(callArg.slaveContactSuccess).toBe(true);
|
||||
});
|
||||
|
||||
it('calls onError on network failure', async () => {
|
||||
server.use(http.put(STATUS_URL, () => HttpResponse.json({}, { status: 500 })));
|
||||
|
||||
const onError = vi.fn();
|
||||
const { result } = renderHook(() => useUpdateCmsInstanceStatus({ onError }), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync({
|
||||
id: INSTANCE_ID,
|
||||
status: 'Available',
|
||||
disableMessage: null,
|
||||
});
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
});
|
||||
|
||||
await waitFor(() => expect(onError).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useMutation, useQueryClient, type UseMutationOptions } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api-client';
|
||||
import type { UpdateCmsInstanceStatusRequest, UpdateStatusResult } from './types';
|
||||
|
||||
type UpdateVariables = { id: string } & UpdateCmsInstanceStatusRequest;
|
||||
|
||||
export function useUpdateCmsInstanceStatus(
|
||||
options?: Pick<UseMutationOptions<UpdateStatusResult, Error, UpdateVariables>, 'onSuccess' | 'onError'>,
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<UpdateStatusResult, Error, UpdateVariables>({
|
||||
mutationFn: ({ id, ...body }) =>
|
||||
api.put<UpdateStatusResult>(`/api/v1/CmsInstances/${id}/status`, body),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['cmsInstances'] });
|
||||
options?.onSuccess?.(data, variables, context);
|
||||
},
|
||||
onError: options?.onError,
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery, useMutation, useQueryClient, type UseMutationOptions } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api-client';
|
||||
import type { UserListItem, InviteUserPayload, InviteUserResponse, ChangeRolePayload, UserRole } from './types';
|
||||
|
||||
@@ -10,11 +10,14 @@ export function useUsers() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useInviteUser() {
|
||||
export function useInviteUser(
|
||||
options?: Pick<UseMutationOptions<InviteUserResponse, Error, InviteUserPayload>, 'onError'>,
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<InviteUserResponse, Error, InviteUserPayload>({
|
||||
mutationFn: (data) => api.post<InviteUserResponse>('/api/v1/Users/invite', data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user