diff --git a/.storybook/main.js b/.storybook/main.js index c395485ba..d371ad4cc 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -31,4 +31,5 @@ module.exports = { core: { builder: 'webpack5', }, + staticDirs: ['./public'], }; diff --git a/.storybook/preview.js b/.storybook/preview.js index 127a08b28..0d13e4731 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,6 +1,22 @@ import '../app/assets/css'; import { pushStateLocationPlugin, UIRouter } from '@uirouter/react'; +import { initialize as initMSW, mswDecorator } from 'msw-storybook-addon'; +import { handlers } from '@/setup-tests/server-handlers'; + +// Initialize MSW +initMSW({ + onUnhandledRequest: ({ method, url }) => { + if (url.pathname.startsWith('/api')) { + console.error(`Unhandled ${method} request to ${url}. + + This exception has been only logged in the console, however, it's strongly recommended to resolve this error as you don't want unmocked data in Storybook stories. + + If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses + `); + } + }, +}); export const parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, @@ -10,6 +26,9 @@ export const parameters = { date: /Date$/, }, }, + msw: { + handlers, + }, }; export const decorators = [ @@ -18,4 +37,5 @@ export const decorators = [ ), + mswDecorator, ]; diff --git a/.storybook/public/mockServiceWorker.js b/.storybook/public/mockServiceWorker.js new file mode 100644 index 000000000..a435cfa78 --- /dev/null +++ b/.storybook/public/mockServiceWorker.js @@ -0,0 +1,328 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker (0.36.3). + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const INTEGRITY_CHECKSUM = '02f4ad4a2797f85668baf196e553d929'; +const bypassHeaderName = 'x-msw-bypass'; +const activeClientIds = new Set(); + +self.addEventListener('install', function () { + return self.skipWaiting(); +}); + +self.addEventListener('activate', async function (event) { + return self.clients.claim(); +}); + +self.addEventListener('message', async function (event) { + const clientId = event.source.id; + + if (!clientId || !self.clients) { + return; + } + + const client = await self.clients.get(clientId); + + if (!client) { + return; + } + + const allClients = await self.clients.matchAll(); + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }); + break; + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: INTEGRITY_CHECKSUM, + }); + break; + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId); + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }); + break; + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId); + break; + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId); + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId; + }); + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister(); + } + + break; + } + } +}); + +// Resolve the "main" client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId); + + if (client.frameType === 'top-level') { + return client; + } + + const allClients = await self.clients.matchAll(); + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible'; + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id); + }); +} + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event); + const response = await getResponse(event, client, requestId); + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + (async function () { + const clonedResponse = response.clone(); + sendToClient(client, { + type: 'RESPONSE', + payload: { + requestId, + type: clonedResponse.type, + ok: clonedResponse.ok, + status: clonedResponse.status, + statusText: clonedResponse.statusText, + body: clonedResponse.body === null ? null : await clonedResponse.text(), + headers: serializeHeaders(clonedResponse.headers), + redirected: clonedResponse.redirected, + }, + }); + })(); + } + + return response; +} + +async function getResponse(event, client, requestId) { + const { request } = event; + const requestClone = request.clone(); + const getOriginalResponse = () => fetch(requestClone); + + // Bypass mocking when the request client is not active. + if (!client) { + return getOriginalResponse(); + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return await getOriginalResponse(); + } + + // Bypass requests with the explicit bypass header + if (requestClone.headers.get(bypassHeaderName) === 'true') { + const cleanRequestHeaders = serializeHeaders(requestClone.headers); + + // Remove the bypass header to comply with the CORS preflight check. + delete cleanRequestHeaders[bypassHeaderName]; + + const originalRequest = new Request(requestClone, { + headers: new Headers(cleanRequestHeaders), + }); + + return fetch(originalRequest); + } + + // Send the request to the client-side MSW. + const reqHeaders = serializeHeaders(request.headers); + const body = await request.text(); + + const clientMessage = await sendToClient(client, { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + method: request.method, + headers: reqHeaders, + cache: request.cache, + mode: request.mode, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body, + bodyUsed: request.bodyUsed, + keepalive: request.keepalive, + }, + }); + + switch (clientMessage.type) { + case 'MOCK_SUCCESS': { + return delayPromise(() => respondWithMock(clientMessage), clientMessage.payload.delay); + } + + case 'MOCK_NOT_FOUND': { + return getOriginalResponse(); + } + + case 'NETWORK_ERROR': { + const { name, message } = clientMessage.payload; + const networkError = new Error(message); + networkError.name = name; + + // Rejecting a request Promise emulates a network error. + throw networkError; + } + + case 'INTERNAL_ERROR': { + const parsedBody = JSON.parse(clientMessage.payload.body); + + console.error( + `\ +[MSW] Uncaught exception in the request handler for "%s %s": + +${parsedBody.location} + +This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\ +`, + request.method, + request.url + ); + + return respondWithMock(clientMessage); + } + } + + return getOriginalResponse(); +} + +self.addEventListener('fetch', function (event) { + const { request } = event; + const accept = request.headers.get('accept') || ''; + + // Bypass server-sent events. + if (accept.includes('text/event-stream')) { + return; + } + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return; + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return; + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return; + } + + const requestId = uuidv4(); + + return event.respondWith( + handleRequest(event, requestId).catch((error) => { + if (error.name === 'NetworkError') { + console.warn('[MSW] Successfully emulated a network error for the "%s %s" request.', request.method, request.url); + return; + } + + // At this point, any exception indicates an issue with the original request/response. + console.error( + `\ +[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, + request.method, + request.url, + `${error.name}: ${error.message}` + ); + }) + ); +}); + +function serializeHeaders(headers) { + const reqHeaders = {}; + headers.forEach((value, name) => { + reqHeaders[name] = reqHeaders[name] ? [].concat(reqHeaders[name]).concat(value) : value; + }); + return reqHeaders; +} + +function sendToClient(client, message) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel(); + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error); + } + + resolve(event.data); + }; + + client.postMessage(JSON.stringify(message), [channel.port2]); + }); +} + +function delayPromise(cb, duration) { + return new Promise((resolve) => { + setTimeout(() => resolve(cb()), duration); + }); +} + +function respondWithMock(clientMessage) { + return new Response(clientMessage.payload.body, { + ...clientMessage.payload, + headers: clientMessage.payload.headers, + }); +} + +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0; + const v = c == 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} diff --git a/app/portainer/components/BoxSelector/BoxSelector.tsx b/app/portainer/components/BoxSelector/BoxSelector.tsx index 32cd106c1..fc2ac0444 100644 --- a/app/portainer/components/BoxSelector/BoxSelector.tsx +++ b/app/portainer/components/BoxSelector/BoxSelector.tsx @@ -21,7 +21,7 @@ export function BoxSelector({ onChange, }: Props) { return ( -
+
{options.map((option) => ( ( label: string, description: string, value: T, - feature: FeatureId + feature?: FeatureId ): BoxSelectorOption { return { id, icon, label, description, value, feature }; } diff --git a/app/portainer/components/TeamsSelector/TeamsSelector.mocks.ts b/app/portainer/components/TeamsSelector/TeamsSelector.mocks.ts new file mode 100644 index 000000000..477c14522 --- /dev/null +++ b/app/portainer/components/TeamsSelector/TeamsSelector.mocks.ts @@ -0,0 +1,9 @@ +import { TeamViewModel } from '@/portainer/models/team'; + +export function createMockTeam(id: number, name: string): TeamViewModel { + return { + Id: id, + Name: name, + Checked: false, + }; +} diff --git a/app/portainer/components/TeamsSelector/TeamsSelector.stories.tsx b/app/portainer/components/TeamsSelector/TeamsSelector.stories.tsx new file mode 100644 index 000000000..0f73a00c2 --- /dev/null +++ b/app/portainer/components/TeamsSelector/TeamsSelector.stories.tsx @@ -0,0 +1,28 @@ +import { Meta } from '@storybook/react'; +import { useState } from 'react'; + +import { TeamsSelector } from './TeamsSelector'; +import { createMockTeam } from './TeamsSelector.mocks'; + +const meta: Meta = { + title: 'Components/TeamsSelector', + component: TeamsSelector, +}; + +export default meta; +export { Example }; + +function Example() { + const [selectedTeams, setSelectedTeams] = useState([1]); + + const teams = [createMockTeam(1, 'team1'), createMockTeam(2, 'team2')]; + + return ( + + ); +} diff --git a/app/portainer/components/TeamsSelector/TeamsSelector.tsx b/app/portainer/components/TeamsSelector/TeamsSelector.tsx new file mode 100644 index 000000000..7cc82562d --- /dev/null +++ b/app/portainer/components/TeamsSelector/TeamsSelector.tsx @@ -0,0 +1,38 @@ +import Select from 'react-select'; + +import { Team, TeamId } from '@/portainer/teams/types'; + +interface Props { + value: TeamId[]; + onChange(value: TeamId[]): void; + teams: Team[]; + dataCy?: string; + inputId?: string; + placeholder?: string; +} + +export function TeamsSelector({ + value, + onChange, + teams, + dataCy, + inputId, + placeholder, +}: Props) { + return ( + +
-
+
@@ -88,7 +80,7 @@ message="As you are a member of multiple teams, you can select which teams(s) will be able to manage this resource." > - + You have not yet created any teams. Head over to the Teams view to manage teams.
@@ -119,7 +111,7 @@ message="You can select which user(s) will be able to manage this resource." > - + You have not yet created any users. Head over to the Users view to manage users.
diff --git a/app/portainer/components/accessControlForm/porAccessControlFormModel.js b/app/portainer/components/accessControlForm/porAccessControlFormModel.js index 09c51fe4d..de01c1d91 100644 --- a/app/portainer/components/accessControlForm/porAccessControlFormModel.js +++ b/app/portainer/components/accessControlForm/porAccessControlFormModel.js @@ -1,5 +1,8 @@ import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership'; +/** + * @deprecated use only for angularjs components. For react components use ./model.ts + */ export function AccessControlFormData() { this.AccessControlEnabled = true; this.Ownership = RCO.PRIVATE; diff --git a/app/portainer/components/accessControlForm/useLoadState.ts b/app/portainer/components/accessControlForm/useLoadState.ts new file mode 100644 index 000000000..563fff845 --- /dev/null +++ b/app/portainer/components/accessControlForm/useLoadState.ts @@ -0,0 +1,45 @@ +import { useEffect } from 'react'; +import { useQuery } from 'react-query'; + +import { getTeams } from '@/portainer/teams/teams.service'; +import * as notifications from '@/portainer/services/notifications'; +import { getUsers } from '@/portainer/services/api/userService'; +import { UserViewModel } from '@/portainer/models/user'; + +export function useLoadState() { + const { teams, isLoading: isLoadingTeams } = useTeams(); + + const { users, isLoading: isLoadingUsers } = useUsers(); + + return { teams, users, isLoading: isLoadingTeams || isLoadingUsers }; +} + +function useTeams() { + const { isError, error, isLoading, data } = useQuery('teams', () => + getTeams() + ); + + useEffect(() => { + if (isError) { + notifications.error('Failure', error as Error, 'Failed retrieving teams'); + } + }, [isError, error]); + + return { isLoading, teams: data }; +} + +function useUsers() { + const { isError, error, isLoading, data } = useQuery< + unknown, + unknown, + UserViewModel[] + >('users', () => getUsers()); + + useEffect(() => { + if (isError) { + notifications.error('Failure', error as Error, 'Failed retrieving users'); + } + }, [isError, error]); + + return { isLoading, users: data }; +} diff --git a/app/portainer/models/resourceControl/resourceControl.js b/app/portainer/models/resourceControl/resourceControl.js deleted file mode 100644 index 4e1cb7611..000000000 --- a/app/portainer/models/resourceControl/resourceControl.js +++ /dev/null @@ -1,24 +0,0 @@ -import { ResourceControlOwnership as RCO } from './resourceControlOwnership'; - -export function ResourceControlViewModel(data) { - this.Id = data.Id; - this.Type = data.Type; - this.ResourceId = data.ResourceId; - this.UserAccesses = data.UserAccesses; - this.TeamAccesses = data.TeamAccesses; - this.Public = data.Public; - this.System = data.System; - this.Ownership = determineOwnership(this); -} - -function determineOwnership(resourceControl) { - if (resourceControl.Public) { - return RCO.PUBLIC; - } else if (resourceControl.UserAccesses.length === 1 && resourceControl.TeamAccesses.length === 0) { - return RCO.PRIVATE; - } else if (resourceControl.UserAccesses.length > 1 || resourceControl.TeamAccesses.length > 0) { - return RCO.RESTRICTED; - } else { - return RCO.ADMINISTRATORS; - } -} diff --git a/app/portainer/models/resourceControl/resourceControl.ts b/app/portainer/models/resourceControl/resourceControl.ts new file mode 100644 index 000000000..05ad949c2 --- /dev/null +++ b/app/portainer/models/resourceControl/resourceControl.ts @@ -0,0 +1,84 @@ +import { ResourceControlOwnership as RCO } from './resourceControlOwnership'; + +export enum ResourceControlType { + // Container represents a resource control associated to a Docker container + Container = 1, + // Service represents a resource control associated to a Docker service + Service, + // Volume represents a resource control associated to a Docker volume + Volume, + // Network represents a resource control associated to a Docker network + Network, + // Secret represents a resource control associated to a Docker secret + Secret, + // Stack represents a resource control associated to a stack composed of Docker services + Stack, + // Config represents a resource control associated to a Docker config + Config, + // CustomTemplate represents a resource control associated to a custom template + CustomTemplate, + // ContainerGroup represents a resource control associated to an Azure container group + ContainerGroup, +} + +export interface ResourceControlResponse { + Id: number; + Type: ResourceControlType; + ResourceId: string | number; + UserAccesses: unknown[]; + TeamAccesses: unknown[]; + Public: boolean; + AdministratorsOnly: boolean; + System: boolean; +} + +export class ResourceControlViewModel { + Id: number; + + Type: ResourceControlType; + + ResourceId: string | number; + + UserAccesses: unknown[]; + + TeamAccesses: unknown[]; + + Public: boolean; + + System: boolean; + + Ownership: RCO; + + constructor(data: ResourceControlResponse) { + this.Id = data.Id; + this.Type = data.Type; + this.ResourceId = data.ResourceId; + this.UserAccesses = data.UserAccesses; + this.TeamAccesses = data.TeamAccesses; + this.Public = data.Public; + this.System = data.System; + this.Ownership = determineOwnership(this); + } +} + +function determineOwnership(resourceControl: ResourceControlViewModel) { + if (resourceControl.Public) { + return RCO.PUBLIC; + } + + if ( + resourceControl.UserAccesses.length === 1 && + resourceControl.TeamAccesses.length === 0 + ) { + return RCO.PRIVATE; + } + + if ( + resourceControl.UserAccesses.length > 1 || + resourceControl.TeamAccesses.length > 0 + ) { + return RCO.RESTRICTED; + } + + return RCO.ADMINISTRATORS; +} diff --git a/app/portainer/models/resourceControl/resourceControlOwnership.js b/app/portainer/models/resourceControl/resourceControlOwnership.js deleted file mode 100644 index ce1331069..000000000 --- a/app/portainer/models/resourceControl/resourceControlOwnership.js +++ /dev/null @@ -1,6 +0,0 @@ -export const ResourceControlOwnership = Object.freeze({ - PUBLIC: 'public', - PRIVATE: 'private', - RESTRICTED: 'restricted', - ADMINISTRATORS: 'administrators', -}); diff --git a/app/portainer/models/resourceControl/resourceControlOwnership.ts b/app/portainer/models/resourceControl/resourceControlOwnership.ts new file mode 100644 index 000000000..827e40679 --- /dev/null +++ b/app/portainer/models/resourceControl/resourceControlOwnership.ts @@ -0,0 +1,6 @@ +export enum ResourceControlOwnership { + PUBLIC = 'public', + PRIVATE = 'private', + RESTRICTED = 'restricted', + ADMINISTRATORS = 'administrators', +} diff --git a/app/portainer/services/axios.ts b/app/portainer/services/axios.ts index 7d1176743..9bbf528a8 100644 --- a/app/portainer/services/axios.ts +++ b/app/portainer/services/axios.ts @@ -1,5 +1,6 @@ -import axios, { AxiosRequestConfig } from 'axios'; +import axios, { AxiosError, AxiosRequestConfig } from 'axios'; +import PortainerError from '../error'; import { get as localStorageGet } from '../hooks/useLocalStorage'; import { @@ -40,3 +41,17 @@ export function agentInterceptor(config: AxiosRequestConfig) { } axiosApiInstance.interceptors.request.use(agentInterceptor); + +export function parseAxiosError(err: Error, msg = '') { + let resultErr = err; + let resultMsg = msg; + + if ('isAxiosError' in err) { + const axiosError = err as AxiosError; + resultErr = new Error(`${axiosError.response?.data.message}`); + const msgDetails = axiosError.response?.data.details; + resultMsg = msg ? `${msg}: ${msgDetails}` : msgDetails; + } + + return new PortainerError(resultMsg, resultErr); +} diff --git a/app/portainer/teams/teams.service.ts b/app/portainer/teams/teams.service.ts new file mode 100644 index 000000000..c4de675b8 --- /dev/null +++ b/app/portainer/teams/teams.service.ts @@ -0,0 +1,22 @@ +import axios, { parseAxiosError } from '@/portainer/services/axios'; + +import { Team, TeamId } from './types'; + +export async function getTeams() { + try { + const { data } = await axios.get(buildUrl()); + return data; + } catch (error) { + throw parseAxiosError(error as Error); + } +} + +function buildUrl(id?: TeamId) { + let url = '/teams'; + + if (id) { + url += `/${id}`; + } + + return url; +} diff --git a/app/portainer/teams/types.ts b/app/portainer/teams/types.ts new file mode 100644 index 000000000..ad2b701be --- /dev/null +++ b/app/portainer/teams/types.ts @@ -0,0 +1,6 @@ +export type TeamId = number; + +export interface Team { + Id: TeamId; + Name: string; +} diff --git a/app/portainer/users/types.ts b/app/portainer/users/types.ts new file mode 100644 index 000000000..048cd892f --- /dev/null +++ b/app/portainer/users/types.ts @@ -0,0 +1 @@ +export type UserId = number; diff --git a/app/react-tools/test-mocks.ts b/app/react-tools/test-mocks.ts new file mode 100644 index 000000000..f3305322a --- /dev/null +++ b/app/react-tools/test-mocks.ts @@ -0,0 +1,22 @@ +import _ from 'lodash'; + +export function createMockUsers(count: number) { + return _.range(1, count + 1).map((value) => ({ + Id: value, + Username: `user${value}`, + Role: _.random(1, 3), + UserTheme: '', + RoleName: '', + AuthenticationMethod: '', + Checked: false, + EndpointAuthorizations: {}, + PortainerAuthorizations: {}, + })); +} + +export function createMockTeams(count: number) { + return _.range(1, count + 1).map((value) => ({ + Id: value, + Name: `team${value}`, + })); +} diff --git a/app/react-tools/test-utils.tsx b/app/react-tools/test-utils.tsx index b19d2556d..8976783ae 100644 --- a/app/react-tools/test-utils.tsx +++ b/app/react-tools/test-utils.tsx @@ -3,6 +3,7 @@ import '@testing-library/jest-dom'; import { render, RenderOptions } from '@testing-library/react'; import { UIRouter, pushStateLocationPlugin } from '@uirouter/react'; import { PropsWithChildren, ReactElement } from 'react'; +import { QueryClient, QueryClientProvider } from 'react-query'; function Provider({ children }: PropsWithChildren) { return {children}; @@ -17,3 +18,21 @@ export * from '@testing-library/react'; // override render method export { customRender as render }; + +export function renderWithQueryClient(ui: React.ReactElement) { + const testQueryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + const { rerender, ...result } = customRender( + {ui} + ); + return { + ...result, + rerender: (rerenderUi: React.ReactElement) => + rerender( + + {rerenderUi} + + ), + }; +} diff --git a/app/setup-tests.js b/app/setup-tests/global-setup.js similarity index 100% rename from app/setup-tests.js rename to app/setup-tests/global-setup.js diff --git a/app/setup-tests/server-handlers.ts b/app/setup-tests/server-handlers.ts new file mode 100644 index 000000000..2525bf607 --- /dev/null +++ b/app/setup-tests/server-handlers.ts @@ -0,0 +1,12 @@ +import { rest } from 'msw'; + +import { createMockTeams, createMockUsers } from '../react-tools/test-mocks'; + +export const handlers = [ + rest.get('/api/teams', async (req, res, ctx) => + res(ctx.json(createMockTeams(10))) + ), + rest.get('/api/users', async (req, res, ctx) => + res(ctx.json(createMockUsers(10))) + ), +]; diff --git a/app/setup-tests/server.ts b/app/setup-tests/server.ts new file mode 100644 index 000000000..46daef33f --- /dev/null +++ b/app/setup-tests/server.ts @@ -0,0 +1,7 @@ +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +import { handlers } from './server-handlers'; + +const server = setupServer(...handlers); +export { server, rest }; diff --git a/app/setup-tests/setup-msw.ts b/app/setup-tests/setup-msw.ts new file mode 100644 index 000000000..1d27a0d36 --- /dev/null +++ b/app/setup-tests/setup-msw.ts @@ -0,0 +1,10 @@ +// test/setup-env.js +// add this to your setupFilesAfterEnv config in jest so it's imported for every test file +import { server } from './server'; + +beforeAll(() => server.listen()); +// if you need to add a handler after calling setupServer for some specific test +// this will remove that handler for the rest of them +// (which is important for test isolation): +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); diff --git a/jest.config.js b/jest.config.js index cb87d0f61..e581ffbea 100644 --- a/jest.config.js +++ b/jest.config.js @@ -54,7 +54,7 @@ module.exports = { // forceCoverageMatch: [], // A path to a module which exports an async function that is triggered once before all test suites - globalSetup: `/app/setup-tests.js`, + globalSetup: `/app/setup-tests/global-setup.js`, // A path to a module which exports an async function that is triggered once after all test suites // globalTeardown: undefined, @@ -135,7 +135,7 @@ module.exports = { // setupFiles: [], // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], + setupFilesAfterEnv: ['/app/setup-tests/setup-msw.ts'], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, diff --git a/package.json b/package.json index 962d5690c..5e63b0dd4 100644 --- a/package.json +++ b/package.json @@ -108,8 +108,10 @@ "js-base64": "^3.7.2", "js-yaml": "^3.14.0", "jwt-decode": "^3.1.2", + "lodash": "^4.17.21", "lodash-es": "^4.17.21", "moment": "^2.29.1", + "msw": "^0.36.3", "ng-file-upload": "~12.2.13", "parse-duration": "^1.0.2", "rc-slider": "^9.7.5", @@ -206,6 +208,7 @@ "load-grunt-tasks": "^3.5.2", "lodash-webpack-plugin": "^0.11.6", "mini-css-extract-plugin": "1", + "msw-storybook-addon": "^1.5.0", "ngtemplate-loader": "^2.1.0", "plop": "^2.6.0", "postcss": "7", @@ -244,4 +247,4 @@ "pre-commit": "lint-staged" } } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 320651b6c..1f0b0861f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1772,6 +1772,26 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" +"@mswjs/cookies@^0.1.6": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-0.1.6.tgz#176f77034ab6d7373ae5c94bcbac36fee8869249" + integrity sha512-A53XD5TOfwhpqAmwKdPtg1dva5wrng2gH5xMvklzbd9WLTSVU953eCRa8rtrrm6G7Cy60BOGsBRN89YQK0mlKA== + dependencies: + "@types/set-cookie-parser" "^2.4.0" + set-cookie-parser "^2.4.6" + +"@mswjs/interceptors@^0.12.7": + version "0.12.7" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.12.7.tgz#0d1cd4cd31a0f663e0455993951201faa09d0909" + integrity sha512-eGjZ3JRAt0Fzi5FgXiV/P3bJGj0NqsN7vBS0J0FO2AQRQ0jCKQS4lEFm4wvlSgKQNfeuc/Vz6d81VtU3Gkx/zg== + dependencies: + "@open-draft/until" "^1.0.3" + "@xmldom/xmldom" "^0.7.2" + debug "^4.3.2" + headers-utils "^3.0.2" + outvariant "^1.2.0" + strict-event-emitter "^0.2.0" + "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz" @@ -1819,6 +1839,11 @@ resolved "https://registry.npmjs.org/@nxmix/tokenize-ansi/-/tokenize-ansi-3.0.0.tgz" integrity sha512-37QMpFIiQ6J31tavjMFCuWs3YIqXIDCuGvPiDVofFqvgXq6vM+8LqU4sqibsvb9JX/1SIeDp+SedOqpq2qc7TA== +"@open-draft/until@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" + integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== + "@pmmmwh/react-refresh-webpack-plugin@^0.5.1": version "0.5.3" resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.3.tgz#b8f0e035f6df71b5c4126cb98de29f65188b9e7b" @@ -2171,7 +2196,7 @@ prop-types "^15.7.2" regenerator-runtime "^0.13.7" -"@storybook/addons@6.4.9": +"@storybook/addons@6.4.9", "@storybook/addons@^6.0.0": version "6.4.9" resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.4.9.tgz#43b5dabf6781d863fcec0a0b293c236b4d5d4433" integrity sha512-y+oiN2zd+pbRWwkf6aQj4tPDFn+rQkrv7fiVoMxsYub+kKyZ3CNOuTSJH+A1A+eBL6DmzocChUyO6jvZFuh6Dg== @@ -3028,6 +3053,11 @@ resolved "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + "@types/eslint-scope@^3.7.0": version "3.7.1" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.1.tgz#8dc390a7b4f9dd9f1284629efce982e41612116e" @@ -3117,6 +3147,14 @@ "@types/through" "*" rxjs "^6.4.0" +"@types/inquirer@^8.1.3": + version "8.1.3" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-8.1.3.tgz#dfda4c97cdbe304e4dceb378a80f79448ea5c8fe" + integrity sha512-AayK4ZL5ssPzR1OtnOLGAwpT0Dda3Xi/h1G0l1oJDNrowp7T1423q4Zb8/emr7tzRlCy4ssEri0LWVexAqHyKQ== + dependencies: + "@types/through" "*" + rxjs "^7.2.0" + "@types/interpret@*": version "1.1.1" resolved "https://registry.npmjs.org/@types/interpret/-/interpret-1.1.1.tgz" @@ -3178,6 +3216,11 @@ dependencies: "@types/sizzle" "*" +"@types/js-levenshtein@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5" + integrity sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g== + "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.9" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz" @@ -3362,6 +3405,13 @@ resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== +"@types/set-cookie-parser@^2.4.0": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.2.tgz#b6a955219b54151bfebd4521170723df5e13caad" + integrity sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w== + dependencies: + "@types/node" "*" + "@types/sinonjs__fake-timers@^6.0.2": version "6.0.4" resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz#0ecc1b9259b76598ef01942f547904ce61a6a77d" @@ -3860,6 +3910,11 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.6.0.tgz#2c275aa05c895eccebbfc34cfb223c6e8bd591a2" integrity sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA== +"@xmldom/xmldom@^0.7.2": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d" + integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz" @@ -4957,6 +5012,15 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + blob-tmp@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/blob-tmp/-/blob-tmp-1.0.0.tgz" @@ -5277,6 +5341,14 @@ buffer@^5.2.1: base64-js "^1.0.2" ieee754 "^1.1.4" +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + buffer@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" @@ -5539,6 +5611,14 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1, chalk@^2.4.2, chalk@~2.4. escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" @@ -5566,7 +5646,7 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@~4.1.0: +chalk@^4.1.1, chalk@~4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -5817,6 +5897,11 @@ cli-spinners@^2.0.0: resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.3.0.tgz" integrity sha512-Xs2Hf2nzrvJMFKimOR7YR0QwZ8fc0u98kdtwN1eNAZzNQgH3vK2pXzff6GJtKh7S5hoJ87ECiAiZFS2fb5Ii2w== +cli-spinners@^2.5.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" + integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== + cli-table3@0.6.0, cli-table3@~0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz" @@ -5840,6 +5925,11 @@ cli-width@^2.0.0: resolved "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz" integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + cliui@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" @@ -6187,6 +6277,11 @@ cookie@0.4.0: resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookie@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + copy-concurrently@^1.0.0: version "1.0.5" resolved "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz" @@ -8019,7 +8114,7 @@ events@^3.0.0: resolved "https://registry.npmjs.org/events/-/events-3.1.0.tgz" integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg== -events@^3.2.0: +events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -9263,6 +9358,11 @@ graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1. resolved "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= +graphql@^15.5.1: + version "15.8.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" + integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== + growly@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz" @@ -9725,6 +9825,11 @@ header-case@^1.0.0: no-case "^2.2.0" upper-case "^1.1.3" +headers-utils@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/headers-utils/-/headers-utils-3.0.2.tgz#dfc65feae4b0e34357308aefbcafa99c895e59ef" + integrity sha512-xAxZkM1dRyGV2Ou5bzMxBPNLoRCjcX+ya7KSWybQD2KwLphxsapUVK6x/02o7f4VU6GPSXch9vNY2+gkU8tYWQ== + hex-color-regex@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz" @@ -10058,16 +10163,16 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== +ieee754@^1.1.13, ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ieee754@^1.1.4: version "1.1.13" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== -ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - iferr@^0.1.5: version "0.1.5" resolved "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz" @@ -10228,6 +10333,26 @@ inquirer@^7.1.0: strip-ansi "^6.0.0" through "^2.3.6" +inquirer@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.0.tgz#f44f008dd344bbfc4b30031f45d984e034a3ac3a" + integrity sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.2.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + internal-slot@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz" @@ -10571,6 +10696,11 @@ is-installed-globally@~0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + is-lower-case@^1.1.0: version "1.1.3" resolved "https://registry.npmjs.org/is-lower-case/-/is-lower-case-1.1.3.tgz" @@ -10609,6 +10739,11 @@ is-negative-zero@^2.0.1: resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz" integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== +is-node-process@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.0.1.tgz#4fc7ac3a91e8aac58175fe0578abbc56f2831b23" + integrity sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ== + is-number-object@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz" @@ -10798,6 +10933,11 @@ is-unc-path@^1.0.0: dependencies: unc-path-regex "^0.1.2" +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-upper-case@^1.1.0: version "1.1.2" resolved "https://registry.npmjs.org/is-upper-case/-/is-upper-case-1.1.2.tgz" @@ -11446,6 +11586,11 @@ js-base64@^3.7.2: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.2.tgz#816d11d81a8aff241603d19ce5761e13e41d7745" integrity sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ== +js-levenshtein@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" + integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== + js-sha3@0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" @@ -11964,6 +12109,14 @@ log-symbols@^4.0.0: dependencies: chalk "^4.0.0" +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + log-update@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz" @@ -12532,6 +12685,40 @@ ms@2.1.2, ms@^2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +msw-storybook-addon@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/msw-storybook-addon/-/msw-storybook-addon-1.5.0.tgz#a302517921ecd994d518f2b50a8f74e562796f9a" + integrity sha512-2TmCREX+lVFlYwu+l9bwoxeI5+iXinJ7xOAahDKNRhgIrymaRTjAmCQx3h2NT7DJDvZKQcPstgCdhl03Q7ocHA== + dependencies: + "@storybook/addons" "^6.0.0" + is-node-process "^1.0.1" + +msw@^0.36.3: + version "0.36.3" + resolved "https://registry.yarnpkg.com/msw/-/msw-0.36.3.tgz#7feb243a5fcf563806d45edc027bc36144741170" + integrity sha512-Itzp/QhKaleZoslXDrNik3ramW9ynqzOdbwydX2ehBSSaZd5QoiAl/bHYcV33R6CEZcJgIX1N4s+G6XkF/bhkA== + dependencies: + "@mswjs/cookies" "^0.1.6" + "@mswjs/interceptors" "^0.12.7" + "@open-draft/until" "^1.0.3" + "@types/cookie" "^0.4.1" + "@types/inquirer" "^8.1.3" + "@types/js-levenshtein" "^1.1.0" + chalk "4.1.1" + chokidar "^3.4.2" + cookie "^0.4.1" + graphql "^15.5.1" + headers-utils "^3.0.2" + inquirer "^8.2.0" + is-node-process "^1.0.1" + js-levenshtein "^1.1.6" + node-fetch "^2.6.1" + path-to-regexp "^6.2.0" + statuses "^2.0.0" + strict-event-emitter "^0.2.0" + type-fest "^1.2.2" + yargs "^17.3.0" + multicast-dns-service-types@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz" @@ -13216,6 +13403,21 @@ ora@^3.4.0: strip-ansi "^5.2.0" wcwidth "^1.0.1" +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz" @@ -13244,6 +13446,11 @@ ospath@^1.2.2: resolved "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz" integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= +outvariant@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.2.1.tgz#e630f6cdc1dbf398ed857e36f219de4a005ccd35" + integrity sha512-bcILvFkvpMXh66+Ubax/inxbKRyWTUiiFIW2DWkiS79wakrLGn3Ydy+GvukadiyfZjaL6C7YhIem4EZSM282wA== + overlayscrollbars@^1.13.1: version "1.13.1" resolved "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-1.13.1.tgz" @@ -13599,6 +13806,11 @@ path-to-regexp@0.1.7: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38" + integrity sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg== + path-type@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz" @@ -14873,7 +15085,7 @@ read-pkg@^5.2.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.6.0: +readable-stream@^3.0.6, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -15419,6 +15631,13 @@ rxjs@^6.4.0, rxjs@^6.5.3, rxjs@^6.5.5: dependencies: tslib "^1.9.0" +rxjs@^7.2.0: + version "7.5.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.1.tgz#af73df343cbcab37628197f43ea0c8256f54b157" + integrity sha512-KExVEeZWxMZnZhUZtsJcFwz8IvPvgu4G2Z2QyqjZQzUGr32KDYuSxrEYO4w3tFFNbfLozcrKUTvTPi+E9ywJkQ== + dependencies: + tslib "^2.1.0" + rxjs@^7.4.0: version "7.4.0" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.4.0.tgz#a12a44d7eebf016f5ff2441b87f28c9a51cebc68" @@ -15693,6 +15912,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-cookie-parser@^2.4.6: + version "2.4.8" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz#d0da0ed388bc8f24e706a391f9c9e252a13c58b2" + integrity sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg== + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz" @@ -16169,6 +16393,11 @@ static-extend@^0.1.1: resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +statuses@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + store2@^2.12.0: version "2.12.0" resolved "https://registry.npmjs.org/store2/-/store2-2.12.0.tgz" @@ -16211,6 +16440,13 @@ stream-shift@^1.0.0: resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +strict-event-emitter@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.2.0.tgz#78e2f75dc6ea502e5d8a877661065a1e2deedecd" + integrity sha512-zv7K2egoKwkQkZGEaH8m+i2D0XiKzx5jNsiSul6ja2IYFvil10A59Z9Y7PPAAe5OW53dQUf9CfsHKzjZzKkm1w== + dependencies: + events "^3.3.0" + string-argv@0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz" @@ -16979,7 +17215,7 @@ tslib@^1.9.0: resolved "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.3.0: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0: version "2.3.1" resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== @@ -17052,6 +17288,11 @@ type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^1.2.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== + type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" @@ -18133,6 +18374,11 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.7: resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs-parser@^21.0.0: + version "21.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.0.tgz#a485d3966be4317426dd56bdb6a30131b281dc55" + integrity sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA== + yargs@^15.4.1: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" @@ -18176,6 +18422,19 @@ yargs@^17.0.1: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^17.3.0: + version "17.3.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.1.tgz#da56b28f32e2fd45aefb402ed9c26f42be4c07b9" + integrity sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.0.0" + yauzl@^2.10.0, yauzl@^2.4.2: version "2.10.0" resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz"