refactor(site): replace UserContext with userXService (#465)

* Install and configure XState

* userXService - typegen not working yet

* Lint, fix error transitions

* Lint

* Change initial state to handle loss of state

* Fix gitignore

* Fix types by hook or by crook

* Use xservice in all pages

* Glue/visual component separation

* Fix dependency merge

* Lint

* Remove UserContext

* Remove inspector

* Add typegen command to site/out

* Fix index page redirects

* DRY up nav and redirects

* Moves based on merge

* Moving Page helpers into Page dir

* Move xservice into src, update script

* Move and storybook navbarview

* Update docs

* Install MSW

* Reorganization, with apologies

* Missed spots

* Add mock handlers

* Configure jest for msw

* Fix typos

* Shift unit test to NavbarView

* Fix test types

* Rename NavbarView test

* Attempt at test, wip

* Fix config

* Be logged out, only warn

* Conditionally show text to help test

* Use a Context for MSW's sake

* mocks -> test_helpers

* Enable dev tools

* Format

* Fix import

* Fixes

* Lint

* run typegen postinstall

Co-authored-by: Bryan Phelps <bryan@coder.com>
This commit is contained in:
Presley Pizzo 2022-03-18 14:07:08 -04:00 committed by GitHub
parent 8fde3ed52f
commit 22f820c69b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1028 additions and 512 deletions

1
.gitignore vendored
View File

@ -23,6 +23,7 @@ site/storybook-static/
site/test-results/
site/yarn-error.log
coverage/
site/**/*.typegen.ts
# Build
dist/

View File

@ -84,6 +84,7 @@ provisionersdk/proto: provisionersdk/proto/provisioner.proto
site/out:
./scripts/yarn_install.sh
cd site && yarn typegen
cd site && yarn build
# Restores GITKEEP files!
git checkout HEAD site/out

View File

@ -48,7 +48,6 @@ To manually run the server and go through first-time set up, run the following c
You'll now be able to login and access the server.
To create a project, run:
- `dist/coder_linux_amd64/coder projects create -d /path/to/project`
### Development
@ -63,6 +62,10 @@ The `develop.sh` script does three things:
This is the recommend flow for working on the front-end, as hot-reload is set up as part of the webpack config.
Note that `./develop.sh` creates a user and allows you to log into the UI, but does not log you into the CLI, which is required for creating a project. Use the `login` command above before the `projects create` command.
While we're working on automating XState typegen, you may need to run `yarn typegen` from `site`.
## Front-End Plan
For the front-end team, we're planning on 2 phases to the 'v2' work:

View File

@ -8,3 +8,4 @@ coverage
.next
storybook-static
test-results
**/*.typegen.ts

View File

@ -1,3 +1,19 @@
import { server } from "./src/test_helpers/server"
// Establish API mocking before all tests through MSW.
beforeAll(() =>
server.listen({
onUnhandledRequest: "warn",
}),
)
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers())
// Clean up after the tests are finished.
afterAll(() => server.close())
// Helper utility to fail jest tests if a console.error is logged
// Pulled from this blog post:
// https://www.benmvp.com/blog/catch-warnings-jest-tests/

View File

@ -4,6 +4,7 @@
"repository": "https://github.com/coder/coder",
"private": true,
"scripts": {
"postinstall": "yarn typegen",
"build": "NODE_ENV=production webpack build --config=webpack.prod.ts",
"build:analyze": "NODE_ENV=production webpack --profile --progress --json --config=webpack.prod.ts > out/stats.json && webpack-bundle-analyzer out/stats.json out",
"dev": "webpack-dev-server --config=webpack.dev.ts",
@ -18,12 +19,14 @@
"storybook:build": "build-storybook",
"test": "jest --selectProjects test",
"test:coverage": "jest --selectProjects test --collectCoverage",
"test:watch": "jest --selectProjects test --watch"
"test:watch": "jest --selectProjects test --watch",
"typegen": "xstate typegen 'src/**/*.ts'"
},
"dependencies": {
"@material-ui/core": "4.9.4",
"@material-ui/icons": "4.5.1",
"@material-ui/lab": "4.0.0-alpha.42",
"@xstate/react": "^2.0.1",
"axios": "0.26.1",
"formik": "2.2.9",
"history": "5.3.0",
@ -31,6 +34,7 @@
"react-dom": "17.0.2",
"react-router-dom": "6.2.2",
"swr": "1.2.2",
"xstate": "^4.30.6",
"yup": "0.32.11"
},
"devDependencies": {
@ -43,13 +47,14 @@
"@storybook/react": "6.4.19",
"@testing-library/react": "12.1.4",
"@types/express": "4.17.13",
"@types/jest": "27.4.1",
"@types/jest": "^27.4.1",
"@types/node": "14.18.12",
"@types/react": "17.0.40",
"@types/react-dom": "17.0.13",
"@types/superagent": "4.1.15",
"@typescript-eslint/eslint-plugin": "5.15.0",
"@typescript-eslint/parser": "5.15.0",
"@xstate/cli": "^0.1.4",
"copy-webpack-plugin": "10.2.4",
"eslint": "8.11.0",
"eslint-config-prettier": "8.5.0",
@ -66,6 +71,7 @@
"jest": "27.5.1",
"jest-junit": "13.0.0",
"jest-runner-eslint": "1.0.0",
"msw": "^0.39.2",
"prettier": "2.6.0",
"react-hot-loader": "4.13.0",
"sql-formatter": "4.0.2",

View File

@ -1,5 +1,6 @@
import axios from "axios"
import { APIKeyResponse, getApiKey, login, LoginResponse, logout } from "./api"
import { getApiKey, login, logout } from "."
import { LoginResponse, APIKeyResponse } from "./types"
// Mock the axios module so that no real network requests are made, but rather
// we swap in a resolved or rejected value

View File

@ -1,26 +1,12 @@
import axios, { AxiosRequestHeaders } from "axios"
import { mutate } from "swr"
import * as Types from "./types"
const CONTENT_TYPE_JSON: AxiosRequestHeaders = {
"Content-Type": "application/json",
}
/**
* `Organization` must be kept in sync with the go struct in organizations.go
*/
export interface Organization {
id: string
name: string
created_at: string
updated_at: string
}
export interface Provisioner {
id: string
name: string
}
export const provisioners: Provisioner[] = [
export const provisioners: Types.Provisioner[] = [
{
id: "terraform",
name: "Terraform",
@ -31,25 +17,8 @@ export const provisioners: Provisioner[] = [
},
]
// This must be kept in sync with the `Project` struct in the back-end
export interface Project {
id: string
created_at: string
updated_at: string
organization_id: string
name: string
provisioner: string
active_version_id: string
}
export interface CreateProjectRequest {
name: string
organizationId: string
provisioner: string
}
export namespace Project {
export const create = async (request: CreateProjectRequest): Promise<Project> => {
export const create = async (request: Types.CreateProjectRequest): Promise<Types.Project> => {
const response = await fetch(`/api/v2/projects/${request.organizationId}/`, {
method: "POST",
headers: {
@ -68,23 +37,8 @@ export namespace Project {
}
}
export interface CreateWorkspaceRequest {
name: string
project_id: string
}
// Must be kept in sync with backend Workspace struct
export interface Workspace {
id: string
created_at: string
updated_at: string
owner_id: string
project_id: string
name: string
}
export namespace Workspace {
export const create = async (request: CreateWorkspaceRequest): Promise<Workspace> => {
export const create = async (request: Types.CreateWorkspaceRequest): Promise<Types.Workspace> => {
const response = await fetch(`/api/v2/users/me/workspaces`, {
method: "POST",
headers: {
@ -108,17 +62,13 @@ export namespace Workspace {
}
}
export interface LoginResponse {
session_token: string
}
export const login = async (email: string, password: string): Promise<LoginResponse> => {
export const login = async (email: string, password: string): Promise<Types.LoginResponse> => {
const payload = JSON.stringify({
email,
password,
})
const response = await axios.post<LoginResponse>("/api/v2/users/login", payload, {
const response = await axios.post<Types.LoginResponse>("/api/v2/users/login", payload, {
headers: { ...CONTENT_TYPE_JSON },
})
@ -129,11 +79,12 @@ export const logout = async (): Promise<void> => {
await axios.post("/api/v2/users/logout")
}
export interface APIKeyResponse {
key: string
}
export const getApiKey = async (): Promise<APIKeyResponse> => {
const response = await axios.post<APIKeyResponse>("/api/v2/users/me/keys")
export const getUser = async (): Promise<Types.UserResponse> => {
const response = await axios.get<Types.UserResponse>("/api/v2/users/me")
return response.data
}
export const getApiKey = async (): Promise<Types.APIKeyResponse> => {
const response = await axios.post<Types.APIKeyResponse>("/api/v2/users/me/keys")
return response.data
}

61
site/src/api/types.ts Normal file
View File

@ -0,0 +1,61 @@
export interface LoginResponse {
session_token: string
}
export interface UserResponse {
readonly id: string
readonly username: string
readonly email: string
readonly created_at: string
}
/**
* `Organization` must be kept in sync with the go struct in organizations.go
*/
export interface Organization {
id: string
name: string
created_at: string
updated_at: string
}
export interface Provisioner {
id: string
name: string
}
// This must be kept in sync with the `Project` struct in the back-end
export interface Project {
id: string
created_at: string
updated_at: string
organization_id: string
name: string
provisioner: string
active_version_id: string
}
export interface CreateProjectRequest {
name: string
organizationId: string
provisioner: string
}
export interface CreateWorkspaceRequest {
name: string
project_id: string
}
// Must be kept in sync with backend Workspace struct
export interface Workspace {
id: string
created_at: string
updated_at: string
owner_id: string
project_id: string
name: string
}
export interface APIKeyResponse {
key: string
}

View File

@ -2,7 +2,6 @@ import React from "react"
import CssBaseline from "@material-ui/core/CssBaseline"
import ThemeProvider from "@material-ui/styles/ThemeProvider"
import { SWRConfig } from "swr"
import { UserProvider } from "./contexts/UserContext"
import { light } from "./theme"
import { BrowserRouter as Router, Route, Routes } from "react-router-dom"
@ -15,6 +14,8 @@ import { ProjectPage } from "./pages/projects/[organization]/[project]"
import { CreateWorkspacePage } from "./pages/projects/[organization]/[project]/create"
import { WorkspacePage } from "./pages/workspaces/[workspace]"
import { HealthzPage } from "./pages/healthz"
import { AuthAndNav, RequireAuth } from "./components/Page"
import { XServiceProvider } from "./xServices/StateContext"
export const App: React.FC = () => {
return (
@ -37,28 +38,63 @@ export const App: React.FC = () => {
},
}}
>
<UserProvider>
<XServiceProvider>
<ThemeProvider theme={light}>
<CssBaseline />
<Routes>
<Route path="/">
<Route index element={<IndexPage />} />
<Route
index
element={
<RequireAuth>
<IndexPage />
</RequireAuth>
}
/>
<Route path="login" element={<SignInPage />} />
<Route path="healthz" element={<HealthzPage />} />
<Route path="cli-auth" element={<CliAuthenticationPage />} />
<Route path="projects">
<Route index element={<ProjectsPage />} />
<Route
index
element={
<AuthAndNav>
<ProjectsPage />
</AuthAndNav>
}
/>
<Route path=":organization/:project">
<Route index element={<ProjectPage />} />
<Route path="create" element={<CreateWorkspacePage />} />
<Route
index
element={
<AuthAndNav>
<ProjectPage />
</AuthAndNav>
}
/>
<Route
path="create"
element={
<RequireAuth>
<CreateWorkspacePage />
</RequireAuth>
}
/>
</Route>
</Route>
<Route path="workspaces">
<Route path=":workspace" element={<WorkspacePage />} />
<Route
path=":workspace"
element={
<AuthAndNav>
<WorkspacePage />
</AuthAndNav>
}
/>
</Route>
{/* Using path="*"" means "match anything", so this route
@ -68,7 +104,7 @@ export const App: React.FC = () => {
</Route>
</Routes>
</ThemeProvider>
</UserProvider>
</XServiceProvider>
</SWRConfig>
</Router>
)

View File

@ -0,0 +1,21 @@
import { Story } from "@storybook/react"
import React from "react"
import { NavbarView, NavbarViewProps } from "./NavbarView"
export default {
title: "Page/NavbarView",
component: NavbarView,
argTypes: {
onSignOut: { action: "Sign Out" },
},
}
const Template: Story<NavbarViewProps> = (args: NavbarViewProps) => <NavbarView {...args} />
export const Primary = Template.bind({})
Primary.args = {
user: { id: "1", username: "CathyCoder", email: "cathy@coder.com", created_at: "dawn" },
onSignOut: () => {
return Promise.resolve()
},
}

View File

@ -1,16 +1,17 @@
import React from "react"
import { screen } from "@testing-library/react"
import { render, MockUser } from "../../test_helpers"
import { Navbar } from "./index"
import { render } from "../../test_helpers"
import { MockUser } from "../../test_helpers/entities"
import { NavbarView } from "./NavbarView"
describe("Navbar", () => {
describe("NavbarView", () => {
const noop = () => {
return
}
it("renders content", async () => {
// When
render(<Navbar onSignOut={noop} />)
render(<NavbarView user={MockUser} onSignOut={noop} />)
// Then
await screen.findAllByText("Coder", { exact: false })
@ -24,7 +25,7 @@ describe("Navbar", () => {
}
// When
render(<Navbar user={mockUser} onSignOut={noop} />)
render(<NavbarView user={mockUser} onSignOut={noop} />)
// Then
// There should be a 'B' avatar!

View File

@ -0,0 +1,69 @@
import React from "react"
import Button from "@material-ui/core/Button"
import { makeStyles } from "@material-ui/core/styles"
import { Link } from "react-router-dom"
import { Logo } from "../Icons"
import { UserDropdown } from "./UserDropdown"
import { UserResponse } from "../../api/types"
export interface NavbarViewProps {
user?: UserResponse
onSignOut: () => void
}
export const NavbarView: React.FC<NavbarViewProps> = ({ user, onSignOut }) => {
const styles = useStyles()
return (
<div className={styles.root}>
<div className={styles.fixed}>
<Link to="/">
<Button className={styles.logo} variant="text">
<Logo fill="white" opacity={1} />
</Button>
</Link>
</div>
<div className={styles.fullWidth} />
<div className={styles.fixed}>{user && <UserDropdown user={user} onSignOut={onSignOut} />}</div>
</div>
)
}
const useStyles = makeStyles((theme) => ({
root: {
position: "relative",
display: "flex",
flex: "0",
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
height: "56px",
background: theme.palette.navbar.main,
marginTop: 0,
transition: "margin 150ms ease",
"@media (display-mode: standalone)": {
borderTop: `1px solid ${theme.palette.divider}`,
},
borderBottom: `1px solid #383838`,
},
fixed: {
flex: "0",
},
fullWidth: {
flex: "1",
},
logo: {
flex: "0",
height: "56px",
paddingLeft: theme.spacing(4),
paddingRight: theme.spacing(2),
borderRadius: 0,
"& svg": {
display: "block",
width: 125,
},
},
title: {
flex: "1",
textAlign: "center",
},
}))

View File

@ -11,11 +11,11 @@ import { LogoutIcon } from "../Icons"
import { BorderedMenu } from "./BorderedMenu"
import { UserProfileCard } from "../User/UserProfileCard"
import { User } from "../../contexts/UserContext"
import { UserAvatar } from "../User"
import { UserResponse } from "../../api/types"
export interface UserDropdownProps {
user: User
user: UserResponse
onSignOut: () => void
}

View File

@ -1,70 +1,13 @@
import React from "react"
import Button from "@material-ui/core/Button"
import { makeStyles } from "@material-ui/core/styles"
import { Link } from "react-router-dom"
import React, { useContext } from "react"
import { useActor } from "@xstate/react"
import { NavbarView } from "./NavbarView"
import { XServiceContext } from "../../xServices/StateContext"
import { User } from "../../contexts/UserContext"
import { Logo } from "../Icons"
import { UserDropdown } from "./UserDropdown"
export const Navbar: React.FC = () => {
const xServices = useContext(XServiceContext)
const [userState, userSend] = useActor(xServices.userXService)
const { me } = userState.context
const onSignOut = () => userSend("SIGN_OUT")
export interface NavbarProps {
user?: User
onSignOut: () => void
return <NavbarView user={me} onSignOut={onSignOut} />
}
export const Navbar: React.FC<NavbarProps> = ({ user, onSignOut }) => {
const styles = useStyles()
return (
<div className={styles.root}>
<div className={styles.fixed}>
<Link to="/">
<Button className={styles.logo} variant="text">
<Logo fill="white" opacity={1} />
</Button>
</Link>
</div>
<div className={styles.fullWidth} />
<div className={styles.fixed}>{user && <UserDropdown user={user} onSignOut={onSignOut} />}</div>
</div>
)
}
const useStyles = makeStyles((theme) => ({
root: {
position: "relative",
display: "flex",
flex: "0",
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
height: "56px",
background: theme.palette.navbar.main,
marginTop: 0,
transition: "margin 150ms ease",
"@media (display-mode: standalone)": {
borderTop: `1px solid ${theme.palette.divider}`,
},
borderBottom: `1px solid #383838`,
},
fixed: {
flex: "0",
},
fullWidth: {
flex: "1",
},
logo: {
flex: "0",
height: "56px",
paddingLeft: theme.spacing(4),
paddingRight: theme.spacing(2),
borderRadius: 0,
"& svg": {
display: "block",
width: 125,
},
},
title: {
flex: "1",
textAlign: "center",
},
}))

View File

@ -0,0 +1,12 @@
import React from "react"
import { Navbar } from "../Navbar"
import { RequireAuth, RequireAuthProps } from "./RequireAuth"
export const AuthAndNav: React.FC<RequireAuthProps> = ({ children }) => (
<RequireAuth>
<>
<Navbar />
{children}
</>
</RequireAuth>
)

View File

@ -0,0 +1,23 @@
import { useActor } from "@xstate/react"
import React, { useContext } from "react"
import { Navigate, useLocation } from "react-router"
import { XServiceContext } from "../../xServices/StateContext"
import { FullScreenLoader } from "../Loader/FullScreenLoader"
export interface RequireAuthProps {
children: JSX.Element
}
export const RequireAuth: React.FC<RequireAuthProps> = ({ children }) => {
const xServices = useContext(XServiceContext)
const [userState] = useActor(xServices.userXService)
const location = useLocation()
if (userState.matches("signedOut") || !userState.context.me) {
return <Navigate to={"/login?redirect=" + encodeURIComponent(location.pathname)} />
} else if (userState.hasTag("loading")) {
return <FullScreenLoader />
} else {
return children
}
}

View File

@ -1 +1,3 @@
export * from "./Footer"
export * from "./RequireAuth"
export * from "./AuthAndNav"

View File

@ -1,16 +1,30 @@
import { Story } from "@storybook/react"
import React from "react"
import { SignInForm, SignInProps } from "./SignInForm"
import { SignInForm, SignInFormProps } from "./SignInForm"
export default {
title: "SignIn/SignInForm",
component: SignInForm,
argTypes: {
loginHandler: { action: "Login" },
isLoading: "boolean",
authErrorMessage: "string",
onSubmit: { action: "Submit" },
},
}
const Template: Story<SignInProps> = (args) => <SignInForm {...args} />
const Template: Story<SignInFormProps> = (args: SignInFormProps) => <SignInForm {...args} />
export const Example = Template.bind({})
Example.args = {}
export const SignedOut = Template.bind({})
SignedOut.args = {
isLoading: false,
authErrorMessage: undefined,
onSubmit: () => {
return Promise.resolve()
},
}
export const Loading = Template.bind({})
Loading.args = { ...SignedOut.args, isLoading: true }
export const WithError = Template.bind({})
WithError.args = { ...SignedOut.args, authErrorMessage: "Email or password was invalid" }

View File

@ -1,82 +0,0 @@
import React from "react"
import { act, fireEvent, screen, waitFor } from "@testing-library/react"
import { history, render } from "../../test_helpers"
import { SignInForm } from "./SignInForm"
describe("SignInForm", () => {
beforeEach(() => {
history.replace("/")
})
it("renders content", async () => {
// When
render(<SignInForm />)
// Then
await screen.findByText("Sign In", { exact: false })
})
it("shows an error message if SignIn fails", async () => {
// Given
const loginHandler = (_email: string, _password: string) => Promise.reject("Unacceptable credentials")
// When
// Render the component
const { container } = render(<SignInForm loginHandler={loginHandler} />)
const inputs = container.querySelectorAll("input")
// Set username / password
fireEvent.change(inputs[0], { target: { value: "test@coder.com" } })
fireEvent.change(inputs[1], { target: { value: "password" } })
// Click sign-in
const elem = await screen.findByText("Sign In")
act(() => elem.click())
// Then
// Should see an error message
const errorMessage = await screen.findByText("The username or password is incorrect.")
expect(errorMessage).toBeDefined()
})
it("redirects when login is complete", async () => {
// Given
const loginHandler = (_email: string, _password: string) => Promise.resolve()
// When
// Render the component
const { container } = render(<SignInForm loginHandler={loginHandler} />)
// Set user / password
const inputs = container.querySelectorAll("input")
fireEvent.change(inputs[0], { target: { value: "test@coder.com" } })
fireEvent.change(inputs[1], { target: { value: "password" } })
// Click sign-in
const elem = await screen.findByText("Sign In")
act(() => elem.click())
// Then
// Should redirect because login was successful
await waitFor(() => expect(history.location.pathname).toEqual("/"))
})
it("respects ?redirect query parameter when complete", async () => {
// Given
const loginHandler = (_email: string, _password: string) => Promise.resolve()
// Set a path to redirect to after login is successful
history.replace("/login?redirect=%2Fsome%2Fother%2Fpath")
// When
// Render the component
const { container } = render(<SignInForm loginHandler={loginHandler} />)
// Set user / password
const inputs = container.querySelectorAll("input")
fireEvent.change(inputs[0], { target: { value: "test@coder.com" } })
fireEvent.change(inputs[1], { target: { value: "password" } })
// Click sign-in
const elem = await screen.findByText("Sign In")
act(() => elem.click())
// Then
// Should redirect to /some/other/path because ?redirect was specified and login was successful
await waitFor(() => expect(history.location.pathname).toEqual("/some/other/path"))
})
})

View File

@ -1,14 +1,11 @@
import { makeStyles } from "@material-ui/core/styles"
import { FormikContextType, useFormik } from "formik"
import { Location } from "history"
import { useNavigate, useLocation } from "react-router-dom"
import React from "react"
import { useSWRConfig } from "swr"
import * as Yup from "yup"
import { Welcome } from "./Welcome"
import { FormTextField } from "../Form"
import * as API from "./../../api"
import FormHelperText from "@material-ui/core/FormHelperText"
import { LoadingButton } from "./../Button"
/**
@ -40,17 +37,14 @@ const useStyles = makeStyles((theme) => ({
},
}))
export interface SignInProps {
loginHandler?: (email: string, password: string) => Promise<void>
export interface SignInFormProps {
isLoading: boolean
authErrorMessage?: string
onSubmit: ({ email, password }: { email: string; password: string }) => Promise<void>
}
export const SignInForm: React.FC<SignInProps> = ({
loginHandler = (email: string, password: string) => API.login(email, password),
}) => {
const navigate = useNavigate()
const location = useLocation()
export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMessage, onSubmit }) => {
const styles = useStyles()
const { mutate } = useSWRConfig()
const form: FormikContextType<BuiltInAuthFormValues> = useFormik<BuiltInAuthFormValues>({
initialValues: {
@ -58,18 +52,7 @@ export const SignInForm: React.FC<SignInProps> = ({
password: "",
},
validationSchema,
onSubmit: async ({ email, password }, helpers) => {
try {
await loginHandler(email, password)
// Tell SWR to invalidate the cache for the user endpoint
await mutate("/api/v2/users/me")
const redirect = getRedirectFromLocation(location)
await navigate(redirect)
} catch (err) {
helpers.setFieldError("password", "The username or password is incorrect.")
}
},
onSubmit,
})
return (
@ -104,28 +87,25 @@ export const SignInForm: React.FC<SignInProps> = ({
placeholder="Password"
variant="outlined"
/>
{authErrorMessage && (
<FormHelperText data-testid="sign-in-error" error>
{authErrorMessage}
</FormHelperText>
)}
</div>
<div className={styles.submitBtn}>
<LoadingButton
color="primary"
loading={form.isSubmitting}
loading={isLoading}
fullWidth
id="signin-form-submit"
type="submit"
variant="contained"
>
Sign In
{isLoading ? "" : "Sign In"}
</LoadingButton>
</div>
</form>
</>
)
}
const getRedirectFromLocation = (location: Location) => {
const defaultRedirect = "/"
const searchParams = new URLSearchParams(location.search)
const redirect = searchParams.get("redirect")
return redirect ? redirect : defaultRedirect
}

View File

@ -1,9 +1,9 @@
import Avatar from "@material-ui/core/Avatar"
import React from "react"
import { User } from "../../contexts/UserContext"
import { UserResponse } from "../../api/types"
export interface UserAvatarProps {
user: User
user: UserResponse
className?: string
}

View File

@ -1,12 +1,12 @@
import { makeStyles } from "@material-ui/core/styles"
import Typography from "@material-ui/core/Typography"
import React from "react"
import { UserResponse } from "../../api/types"
import { User } from "../../contexts/UserContext"
import { UserAvatar } from "./UserAvatar"
interface UserProfileCardProps {
user: User
user: UserResponse
}
export const UserProfileCard: React.FC<UserProfileCardProps> = ({ user }) => {

View File

@ -6,13 +6,13 @@ import CloudCircleIcon from "@material-ui/icons/CloudCircle"
import { Link } from "react-router-dom"
import React from "react"
import * as Constants from "./constants"
import * as API from "../../api"
import * as Types from "../../api/types"
import { WorkspaceSection } from "./WorkspaceSection"
export interface WorkspaceProps {
organization: API.Organization
workspace: API.Workspace
project: API.Project
organization: Types.Organization
workspace: Types.Workspace
project: Types.Project
}
/**

View File

@ -1,94 +0,0 @@
import React from "react"
import { SWRConfig } from "swr"
import { screen, waitFor } from "@testing-library/react"
import { User, UserProvider, useUser } from "./UserContext"
import { history, MockUser, render } from "../test_helpers"
namespace Helpers {
// Helper component that renders out the state of the `useUser` hook.
// It just renders simple text in the 'error', 'me', and 'loading' states,
// so that the test can get a peak at the state of the hook.
const TestComponent: React.FC<{ redirectOnFailure: boolean }> = ({ redirectOnFailure }) => {
const { me, error } = useUser(redirectOnFailure)
if (error) {
return <div>{`Error: ${error.toString()}`}</div>
}
if (me) {
return <div>{`Me: ${me.toString()}`}</div>
}
return <div>Loading</div>
}
// Helper to render a userContext, and all the scaffolding needed
// (an SWRConfig as well as a UserPRovider)
export const renderUserContext = (
simulatedRequest: () => Promise<User>,
redirectOnFailure: boolean,
): React.ReactElement => {
return (
// Set up an SWRConfig that works for testing - we'll simulate a request,
// and set up the cache to reset every test.
<SWRConfig
value={{
fetcher: simulatedRequest,
// Reset cache for every test. Without this, requests will be cached between test cases.
provider: () => new Map(),
}}
>
<UserProvider>
<TestComponent redirectOnFailure={redirectOnFailure} />
</UserProvider>
</SWRConfig>
)
}
}
describe("UserContext", () => {
const failingRequest = () => Promise.reject("Failed to load user")
const successfulRequest = () => Promise.resolve(MockUser)
// Reset the router to '/' before every test
beforeEach(() => {
history.replace("/")
})
it("shouldn't redirect if user fails to load and redirectOnFailure is false", async () => {
// When
render(Helpers.renderUserContext(failingRequest, false))
// Then
// Verify we get an error message
await waitFor(() => {
expect(screen.queryByText("Error:", { exact: false })).toBeDefined()
})
// ...and the route should be unchanged
expect(history.location.pathname).toEqual("/")
expect(history.location.search).toEqual("")
})
it("should redirect if user fails to load and redirectOnFailure is true", async () => {
// When
render(Helpers.renderUserContext(failingRequest, true))
// Then
// Verify we route to the login page
await waitFor(() => expect(history.location.pathname).toEqual("/login"))
await waitFor(() => expect(history.location.search).toEqual("?redirect=%2F"))
})
it("should not redirect if user loads and redirectOnFailure is true", async () => {
// When
render(Helpers.renderUserContext(successfulRequest, true))
// Then
// Verify the user is rendered
await waitFor(() => {
expect(screen.queryByText("Me:", { exact: false })).toBeDefined()
})
// ...and the route should be unchanged
expect(history.location.pathname).toEqual("/")
expect(history.location.search).toEqual("")
})
})

View File

@ -1,73 +0,0 @@
import { useLocation, useNavigate } from "react-router-dom"
import React, { useContext, useEffect } from "react"
import useSWR from "swr"
import * as API from "../api"
export interface User {
readonly id: string
readonly username: string
readonly email: string
readonly created_at: string
}
export interface UserContext {
readonly error?: Error
readonly me?: User
readonly signOut: () => Promise<void>
}
const UserContext = React.createContext<UserContext>({
signOut: () => {
return Promise.reject("Sign out API not available")
},
})
export const useUser = (redirectOnError = false): UserContext => {
const ctx = useContext(UserContext)
const navigate = useNavigate()
const { pathname } = useLocation()
const requestError = ctx.error
useEffect(() => {
if (redirectOnError && requestError) {
navigate({
pathname: "/login",
search: "?redirect=" + encodeURIComponent(pathname),
})
}
// Disabling exhaustive deps here because it can cause an
// infinite useEffect loop. Should (hopefully) go away
// when we switch to an alternate routing strategy.
}, [redirectOnError, requestError]) // eslint-disable-line react-hooks/exhaustive-deps
return ctx
}
export const UserProvider: React.FC = (props) => {
const navigate = useNavigate()
const location = useLocation()
const { data, error, mutate } = useSWR("/api/v2/users/me")
const signOut = async () => {
await API.logout()
// Tell SWR to invalidate the cache for the user endpoint
await mutate("/api/v2/users/me")
navigate({
pathname: "/login",
search: "?redirect=" + encodeURIComponent(location.pathname),
})
}
return (
<UserContext.Provider
value={{
error: error,
me: data,
signOut: signOut,
}}
>
{props.children}
</UserContext.Provider>
)
}

View File

@ -13,7 +13,7 @@ import {
FormCloseButton,
} from "../components/Form"
import { LoadingButton } from "../components/Button"
import { Organization, Project, Provisioner, CreateProjectRequest } from "./../api"
import { Organization, Project, Provisioner, CreateProjectRequest } from "../api/types"
export interface CreateProjectFormProps {
provisioners: Provisioner[]

View File

@ -6,7 +6,7 @@ import * as Yup from "yup"
import { FormCloseButton, FormTextField, FormTitle, FormSection } from "../components/Form"
import { LoadingButton } from "../components/Button"
import { Project, Workspace, CreateWorkspaceRequest } from "../api"
import { Project, Workspace, CreateWorkspaceRequest } from "../api/types"
export interface CreateWorkspaceForm {
project: Project

View File

@ -1,13 +1,17 @@
import { makeStyles } from "@material-ui/core/styles"
import React, { useEffect, useState } from "react"
import React, { useContext, useEffect, useState } from "react"
import { getApiKey } from "../api"
import { CliAuthToken } from "../components/SignIn"
import { FullScreenLoader } from "../components/Loader/FullScreenLoader"
import { useUser } from "../contexts/UserContext"
import { useActor } from "@xstate/react"
import { XServiceContext } from "../xServices/StateContext"
export const CliAuthenticationPage: React.FC = () => {
const { me } = useUser(true)
const xServices = useContext(XServiceContext)
const [userState] = useActor(xServices.userXService)
const { me } = userState.context
const styles = useStyles()
const [apiKey, setApiKey] = useState<string | null>(null)

View File

@ -1,16 +1,7 @@
import React from "react"
import { Navigate } from "react-router-dom"
import { FullScreenLoader } from "../components/Loader/FullScreenLoader"
import { useUser } from "../contexts/UserContext"
export const IndexPage: React.FC = () => {
const { me } = useUser(/* redirectOnError */ true)
if (me) {
// Once the user is logged in, just redirect them to /projects as the landing page
return <Navigate to="/projects" replace />
}
return <FullScreenLoader />
return <Navigate to="/projects" replace />
}

View File

@ -0,0 +1,52 @@
import React from "react"
import { act, fireEvent, screen } from "@testing-library/react"
import { history, render } from "../test_helpers"
import { SignInPage } from "./login"
import { server } from "../test_helpers/server"
import { rest } from "msw"
describe("SignInPage", () => {
beforeEach(() => {
history.replace("/login")
// appear logged out
server.use(
rest.get("/api/v2/users/me", (req, res, ctx) => {
return res(ctx.status(401), ctx.json({ message: "no user here" }))
}),
)
})
it("renders the sign-in form", async () => {
// When
render(<SignInPage />)
// Then
await screen.findByText("Sign In", { exact: false })
})
it("shows an error message if SignIn fails", async () => {
// Given
const { container } = render(<SignInPage />)
// Make login fail
server.use(
rest.post("/api/v2/users/login", async (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ message: "nope" }))
}),
)
// When
// Set username / password
const [username, password] = container.querySelectorAll("input")
fireEvent.change(username, { target: { value: "test@coder.com" } })
fireEvent.change(password, { target: { value: "password" } })
// Click sign-in
const signInButton = await screen.findByText("Sign In")
act(() => signInButton.click())
// Then
// Finding error by test id because it comes from the backend
const errorMessage = await screen.findByTestId("sign-in-error")
expect(errorMessage).toBeDefined()
expect(history.location.pathname).toEqual("/login")
})
})

View File

@ -1,6 +1,10 @@
import { makeStyles } from "@material-ui/core/styles"
import React from "react"
import { useActor } from "@xstate/react"
import React, { useContext } from "react"
import { SignInForm } from "./../components/SignIn"
import { Navigate, useLocation } from "react-router-dom"
import { Location } from "history"
import { XServiceContext } from "../xServices/StateContext"
export const useStyles = makeStyles((theme) => ({
root: {
@ -16,13 +20,36 @@ export const useStyles = makeStyles((theme) => ({
},
}))
const getRedirectFromLocation = (location: Location) => {
const defaultRedirect = "/"
const searchParams = new URLSearchParams(location.search)
const redirect = searchParams.get("redirect")
return redirect ? redirect : defaultRedirect
}
export const SignInPage: React.FC = () => {
const styles = useStyles()
return (
<div className={styles.root}>
<div className={styles.container}>
<SignInForm />
const location = useLocation()
const xServices = useContext(XServiceContext)
const [userState, userSend] = useActor(xServices.userXService)
const isLoading = userState.hasTag("loading")
const redirectTo = getRedirectFromLocation(location)
const authErrorMessage = userState.context.authError ? (userState.context.authError as Error).message : undefined
const onSubmit = async ({ email, password }: { email: string; password: string }) => {
userSend({ type: "SIGN_IN", email, password })
}
if (userState.matches("signedIn")) {
return <Navigate to={redirectTo} replace />
} else {
return (
<div className={styles.root}>
<div className={styles.container}>
<SignInForm isLoading={isLoading} authErrorMessage={authErrorMessage} onSubmit={onSubmit} />
</div>
</div>
</div>
)
)
}
}

View File

@ -3,8 +3,8 @@ import { makeStyles } from "@material-ui/core/styles"
import { useNavigate, useParams } from "react-router-dom"
import useSWR from "swr"
import * as Types from "../../../../api/types"
import * as API from "../../../../api"
import { useUser } from "../../../../contexts/UserContext"
import { ErrorSummary } from "../../../../components/ErrorSummary"
import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader"
import { CreateWorkspaceForm } from "../../../../forms/CreateWorkspaceForm"
@ -14,13 +14,12 @@ export const CreateWorkspacePage: React.FC = () => {
const { organization: organizationName, project: projectName } = useParams()
const navigate = useNavigate()
const styles = useStyles()
const { me } = useUser(/* redirectOnError */ true)
const { data: organizationInfo, error: organizationError } = useSWR<API.Organization, Error>(
const { data: organizationInfo, error: organizationError } = useSWR<Types.Organization, Error>(
() => `/api/v2/users/me/organizations/${organizationName}`,
)
const { data: project, error: projectError } = useSWR<API.Project, Error>(() => {
const { data: project, error: projectError } = useSWR<Types.Project, Error>(() => {
return `/api/v2/organizations/${unsafeSWRArgument(organizationInfo).id}/projects/${projectName}`
})
@ -28,7 +27,7 @@ export const CreateWorkspacePage: React.FC = () => {
navigate(`/projects/${organizationName}/${projectName}`)
}, [navigate, organizationName, projectName])
const onSubmit = async (req: API.CreateWorkspaceRequest) => {
const onSubmit = async (req: Types.CreateWorkspaceRequest) => {
const workspace = await API.Workspace.create(req)
navigate(`/workspaces/${workspace.id}`)
return workspace
@ -42,7 +41,7 @@ export const CreateWorkspacePage: React.FC = () => {
return <ErrorSummary error={projectError} />
}
if (!me || !project) {
if (!project) {
return <FullScreenLoader />
}

View File

@ -4,13 +4,11 @@ import Paper from "@material-ui/core/Paper"
import { Link, useNavigate, useParams } from "react-router-dom"
import useSWR from "swr"
import { Organization, Project, Workspace } from "../../../../api"
import { Organization, Project, Workspace } from "../../../../api/types"
import { Header } from "../../../../components/Header"
import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader"
import { Navbar } from "../../../../components/Navbar"
import { Footer } from "../../../../components/Page"
import { Column, Table } from "../../../../components/Table"
import { useUser } from "../../../../contexts/UserContext"
import { ErrorSummary } from "../../../../components/ErrorSummary"
import { firstOrItem } from "../../../../util/array"
import { EmptyState } from "../../../../components/EmptyState"
@ -18,7 +16,6 @@ import { unsafeSWRArgument } from "../../../../util"
export const ProjectPage: React.FC = () => {
const styles = useStyles()
const { me, signOut } = useUser(true)
const navigate = useNavigate()
const { project: projectName, organization: organizationName } = useParams()
@ -47,7 +44,7 @@ export const ProjectPage: React.FC = () => {
return <ErrorSummary error={workspacesError} />
}
if (!me || !projectInfo || !workspaces) {
if (!projectInfo || !workspaces) {
return <FullScreenLoader />
}
@ -89,7 +86,6 @@ export const ProjectPage: React.FC = () => {
return (
<div className={styles.root}>
<Navbar user={me} onSignOut={signOut} />
<Header
title={firstOrItem(projectName, "")}
description={firstOrItem(organizationName, "")}

View File

@ -4,20 +4,17 @@ import Paper from "@material-ui/core/Paper"
import { Link } from "react-router-dom"
import { EmptyState } from "../../components"
import { ErrorSummary } from "../../components/ErrorSummary"
import { Navbar } from "../../components/Navbar"
import { Header } from "../../components/Header"
import { Footer } from "../../components/Page"
import { Column, Table } from "../../components/Table"
import { useUser } from "../../contexts/UserContext"
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
import { Organization, Project } from "./../../api"
import { Organization, Project } from "../../api/types"
import useSWR from "swr"
import { CodeExample } from "../../components/CodeExample/CodeExample"
export const ProjectsPage: React.FC = () => {
const styles = useStyles()
const { me, signOut } = useUser(true)
const { data: orgs, error: orgsError } = useSWR<Organization[], Error>("/api/v2/users/me/organizations")
const { data: projects, error } = useSWR<Project[] | null, Error>(
orgs ? `/api/v2/organizations/${orgs[0].id}/projects` : null,
@ -31,7 +28,7 @@ export const ProjectsPage: React.FC = () => {
return <ErrorSummary error={error} />
}
if (!me || !projects || !orgs) {
if (!projects || !orgs) {
return <FullScreenLoader />
}
@ -74,7 +71,6 @@ export const ProjectsPage: React.FC = () => {
return (
<div className={styles.root}>
<Navbar user={me} onSignOut={signOut} />
<Header title="Projects" subTitle={subTitle} />
<Paper style={{ maxWidth: "1380px", margin: "1em auto", width: "100%" }}>
<Table {...tableProps} />

View File

@ -2,33 +2,30 @@ import React from "react"
import useSWR from "swr"
import { makeStyles } from "@material-ui/core/styles"
import { useParams } from "react-router-dom"
import { Navbar } from "../../components/Navbar"
import { Footer } from "../../components/Page"
import { useUser } from "../../contexts/UserContext"
import { firstOrItem } from "../../util/array"
import { ErrorSummary } from "../../components/ErrorSummary"
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
import { Workspace } from "../../components/Workspace"
import { unsafeSWRArgument } from "../../util"
import * as API from "../../api"
import * as Types from "../../api/types"
export const WorkspacePage: React.FC = () => {
const styles = useStyles()
const { workspace: workspaceQueryParam } = useParams()
const { me, signOut } = useUser(true)
const { data: workspace, error: workspaceError } = useSWR<API.Workspace, Error>(() => {
const { data: workspace, error: workspaceError } = useSWR<Types.Workspace, Error>(() => {
const workspaceParam = firstOrItem(workspaceQueryParam, null)
return `/api/v2/workspaces/${workspaceParam}`
})
// Fetch parent project
const { data: project, error: projectError } = useSWR<API.Project, Error>(() => {
const { data: project, error: projectError } = useSWR<Types.Project, Error>(() => {
return `/api/v2/projects/${unsafeSWRArgument(workspace).project_id}`
})
const { data: organization, error: organizationError } = useSWR<API.Project, Error>(() => {
const { data: organization, error: organizationError } = useSWR<Types.Project, Error>(() => {
return `/api/v2/organizations/${unsafeSWRArgument(project).organization_id}`
})
@ -44,14 +41,12 @@ export const WorkspacePage: React.FC = () => {
return <ErrorSummary error={organizationError} />
}
if (!me || !workspace || !project || !organization) {
if (!workspace || !project || !organization) {
return <FullScreenLoader />
}
return (
<div className={styles.root}>
<Navbar user={me} onSignOut={signOut} />
<div className={styles.inner}>
<Workspace organization={organization} project={project} workspace={workspace} />
</div>

View File

@ -1,7 +1,10 @@
import { User } from "../contexts/UserContext"
import { Provisioner, Organization, Project, Workspace } from "../api"
import { Provisioner, Organization, Project, Workspace, UserResponse } from "../api/types"
export const MockUser: User = {
export const MockSessionToken = { session_token: "my-session-token" }
export const MockAPIKey = { key: "my-api-key" }
export const MockUser: UserResponse = {
id: "test-user-id",
username: "TestUser",
email: "test@coder.com",

View File

@ -0,0 +1,20 @@
import { rest } from "msw"
import * as M from "./entities"
export const handlers = [
rest.post("/api/v2/users/me/workspaces", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspace))
}),
rest.post("/api/v2/users/login", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockSessionToken))
}),
rest.post("/api/v2/users/logout", async (req, res, ctx) => {
return res(ctx.status(200))
}),
rest.get("/api/v2/users/me", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockUser))
}),
rest.get("/api/v2/users/me/keys", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockAPIKey))
}),
]

View File

@ -5,13 +5,16 @@ import ThemeProvider from "@material-ui/styles/ThemeProvider"
import { dark } from "../theme"
import { createMemoryHistory } from "history"
import { unstable_HistoryRouter as HistoryRouter } from "react-router-dom"
import { XServiceProvider } from "../xServices/StateContext"
export const history = createMemoryHistory()
export const WrapperComponent: React.FC = ({ children }) => {
return (
<HistoryRouter history={history}>
<ThemeProvider theme={dark}>{children}</ThemeProvider>
<XServiceProvider>
<ThemeProvider theme={dark}>{children}</ThemeProvider>
</XServiceProvider>
</HistoryRouter>
)
}
@ -20,4 +23,4 @@ export const render = (component: React.ReactElement): RenderResult => {
return wrappedRender(<WrapperComponent>{component}</WrapperComponent>)
}
export * from "./mocks"
export * from "./entities"

View File

@ -0,0 +1,5 @@
import { setupServer } from "msw/node"
import { handlers } from "./handlers"
// This configures a request mocking server with the given request handlers.
export const server = setupServer(...handlers)

View File

@ -0,0 +1,24 @@
import React, { createContext } from "react"
import { useInterpret } from "@xstate/react"
import { ActorRefFrom } from "xstate"
import { userMachine } from "./user/userXService"
interface XServiceContextType {
userXService: ActorRefFrom<typeof userMachine>
}
/**
* Consuming this Context will not automatically cause rerenders because
* the xServices in it are static references.
*
* To use one of the xServices, `useActor` will access all its state
* (causing re-renders for any changes to that one xService) and
* `useSelector` will access just one piece of state.
*/
export const XServiceContext = createContext({} as XServiceContextType)
export const XServiceProvider: React.FC = ({ children }) => {
const userXService = useInterpret(userMachine, { devTools: true })
return <XServiceContext.Provider value={{ userXService }}>{children}</XServiceContext.Provider>
}

View File

@ -0,0 +1,145 @@
import { createMachine, assign } from "xstate"
import * as Types from "../../api/types"
import * as API from "../../api"
export interface UserContext {
getUserError?: Error | unknown // unknown is a concession while I work out typing issues
authError?: Error | unknown
me?: Types.UserResponse
}
export type UserEvent = { type: "SIGN_OUT" } | { type: "SIGN_IN"; email: string; password: string }
export const userMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QFdZgE4GUAuBDbYAdLAJZQB2kA8stgMSYCSA4gHID6jrioADgPalsJfuR4gAHogDM0gJyEATAHYAbNIAscxdunKArKo0AaEAE9EG+YX0BGABz3FD6atsbbc5QF9vp1Bg4+ESkFCTkUIzkdBCiROEAbvwA1iFk5FHiAkIiYkiSiHIADITSOvbSRRqq2m7StqYWCIr2+ja2tvVV+vqaRVW+-mhYeATE6eGR0Rjo-OiEvAA2+ABmcwC24xSZ+dkkwqLiUghlyoRF+opayq4X+sqK+o2Il6qEcq5y+hrfjt+qgxAARGwUIMGwwgiAFVhjE4oREikiOCALJgLKCfa5I4yIyEZTFZQaezFK5lPTPBC2IqKUqKC4EjqqKrFOSA4FBMbgyFQGEYOgzOYLZbYNboTao9G7TEHPKgY7SewlTQ3dRyVQ6GpVSmKMqEIz2ZRKjrKIqqJzs4actIUSBRBgsDhUKEAFQxOUO+WOhvshA0ahualsqnu8kpthUhGplVUIa+rUq9ktgVGNvIkxo9FilAR5CSqS25Ez7qxnvliCMGlK1Tk6vN5p+T3ML0Ub2U7iKXmcGg8tmTILGoXTEUzAvQs3mS1WG0LxelHrlBQQIbasc7aiKtnbV3sOpaUZXBjkwd1A0B5H4EDg4g5qcL1FoJdlOIQdlpH2qhmJzJ+DWbCB7WxSmUIl2wJM0CSTPwgStO8h0mHY+BlbEvReICaSMbQflkU0fkpHtfS3MC3C+aR9ENfR+2tMEwAhSY+XQJ8UPLACziVS5TXKAxPDI8MaSjIojSqPQezNRUqLg9I7UXPZn1Q5dqn1CMNEEi4HAecNIyVGoIweVQDH0tloNvUF4JHR951LRdvQcc4PCcAx12-fDOnOeRTU7C4SXsPtjNg4ImLLJddUIdiVBpOQKJ4psmh0ASQOPRUiI8RRfF8IA */
createMachine(
{
tsTypes: {} as import("./userXService.typegen").Typegen0,
schema: {
context: {} as UserContext,
events: {} as UserEvent,
services: {} as {
getMe: {
data: Types.UserResponse
}
signIn: {
data: Types.LoginResponse | undefined
}
},
},
context: {
me: undefined,
getUserError: undefined,
authError: undefined,
},
id: "userState",
initial: "gettingUser",
states: {
signedOut: {
on: {
SIGN_IN: {
target: "#userState.signingIn",
},
},
},
signingIn: {
invoke: {
src: "signIn",
id: "signIn",
onDone: [
{
target: "#userState.gettingUser",
actions: "clearAuthError",
},
],
onError: [
{
actions: "assignAuthError",
target: "#userState.signedOut",
},
],
},
tags: "loading",
},
gettingUser: {
invoke: {
src: "getMe",
id: "getMe",
onDone: [
{
actions: ["assignMe", "clearGetUserError"],
target: "#userState.signedIn",
},
],
onError: [
{
actions: "assignGetUserError",
target: "#userState.signedOut",
},
],
},
tags: "loading",
},
signedIn: {
on: {
SIGN_OUT: {
target: "#userState.signingOut",
},
},
},
signingOut: {
invoke: {
src: "signOut",
id: "signOut",
onDone: [
{
actions: ["unassignMe", "clearAuthError"],
target: "#userState.signedOut",
},
],
onError: [
{
actions: "assignAuthError",
target: "#userState.signedIn",
},
],
},
tags: "loading",
},
},
},
{
services: {
signIn: async (_, event: UserEvent) => {
if (event.type === "SIGN_IN") {
return await API.login(event.email, event.password)
}
},
signOut: API.logout,
getMe: API.getUser,
},
actions: {
assignMe: assign({
me: (_, event) => event.data,
}),
unassignMe: assign((context: UserContext) => ({
...context,
me: undefined,
})),
assignGetUserError: assign({
getUserError: (_, event) => event.data,
}),
clearGetUserError: assign((context: UserContext) => ({
...context,
getUserError: undefined,
})),
assignAuthError: assign({
authError: (_, event) => event.data,
}),
clearAuthError: assign((context: UserContext) => ({
...context,
authError: undefined,
})),
},
},
)

View File

@ -1658,6 +1658,26 @@
call-me-maybe "^1.0.1"
glob-to-regexp "^0.3.0"
"@mswjs/cookies@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-0.2.0.tgz#7ef2b5d7e444498bb27cf57720e61f76a4ce9f23"
integrity sha512-GTKYnIfXVP8GL8HRWrse+ujqDXCLKvu7+JoL6pvZFzS/d2i9pziByoWD69cOe35JNoSrx2DPNqrhUF+vgV3qUA==
dependencies:
"@types/set-cookie-parser" "^2.4.0"
set-cookie-parser "^2.4.6"
"@mswjs/interceptors@^0.15.1":
version "0.15.1"
resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.15.1.tgz#4a0009f56e51bc2cd3176f1507065c7d2f6c0d5e"
integrity sha512-D5B+ZJNlfvBm6ZctAfRBdNJdCHYAe2Ix4My5qfbHV5WH+3lkt3mmsjiWJzEh5ZwGDauzY487TldI275If7DJVw==
dependencies:
"@open-draft/until" "^1.0.3"
"@xmldom/xmldom" "^0.7.5"
debug "^4.3.3"
headers-polyfill "^3.0.4"
outvariant "^1.2.1"
strict-event-emitter "^0.2.0"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@ -1700,6 +1720,11 @@
mkdirp "^1.0.4"
rimraf "^3.0.2"
"@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==
"@playwright/test@1.20.0":
version "1.20.0"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.20.0.tgz#df5b1b45976d11c365e6cb60f8ec1ca7cccb84cf"
@ -2812,6 +2837,11 @@
dependencies:
"@types/node" "*"
"@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/cookiejar@*":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8"
@ -2920,7 +2950,7 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/jest@27.4.1":
"@types/jest@^27.4.1":
version "27.4.1"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d"
integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==
@ -2928,6 +2958,11 @@
jest-matcher-utils "^27.0.0"
pretty-format "^27.0.0"
"@types/js-levenshtein@^1.1.1":
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.8", "@types/json-schema@^7.0.9":
version "7.0.9"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
@ -3083,6 +3118,13 @@
"@types/mime" "^1"
"@types/node" "*"
"@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/sockjs@^0.3.33":
version "0.3.33"
resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f"
@ -3604,6 +3646,43 @@
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.6.1.tgz#0de2875ac31b46b6c5bb1ae0a7d7f0ba5678dffe"
integrity sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==
"@xmldom/xmldom@^0.7.5":
version "0.7.5"
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d"
integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==
"@xstate/cli@^0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@xstate/cli/-/cli-0.1.4.tgz#5d909b980a5e62744f90b2790be3aa2717cfb4f8"
integrity sha512-+MkeFGi+gouY8o+/GWG7/62c4WZAtsMlABi5NFBN2owIbwohJQbflc9jBMzN+U3Ho0QGC2gHBwsretsSsNkuuA==
dependencies:
"@babel/core" "^7.12.10"
"@xstate/machine-extractor" "0.6.2"
"@xstate/tools-shared" "1.1.2"
chokidar "^3.5.3"
commander "^8.0.0"
xstate "^4.29.0"
"@xstate/machine-extractor@0.6.2":
version "0.6.2"
resolved "https://registry.yarnpkg.com/@xstate/machine-extractor/-/machine-extractor-0.6.2.tgz#2fe5edb6b965fd1f45fa68644a4ef69f125c4fc0"
integrity sha512-zyDrBMDCpPestEpnWHwmJ42qLIVOqRVUKa491kmdix/vT8z/3P3Ib6MOSbD8lp2yaF49kIUDUCCkvQA6TcLRyA==
"@xstate/react@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-2.0.1.tgz#2b4717369d419e78a6c67f2dfcd1a3be9abce2d9"
integrity sha512-sT3hxyzNBw+bm7uT3BP+uXzN0MnRqiaj/U9Yl4OYaMAUJXWsRvSA/ipL7EDf0gVLRGrRhJTCsC0cjWaduAAqnw==
dependencies:
use-isomorphic-layout-effect "^1.0.0"
use-subscription "^1.3.0"
"@xstate/tools-shared@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@xstate/tools-shared/-/tools-shared-1.1.2.tgz#84660e1ff9ba48612af2d471a21905705fa62e16"
integrity sha512-/A0/3vI2N9Rr3uWKGpUkIv1GhVvQFsXB+vbf8RuSCcj5lnztnC2XEKCqzkric+T3BBViGVXuY7eh8UZw8OVPDw==
dependencies:
"@xstate/machine-extractor" "0.6.2"
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@ -4295,7 +4374,7 @@ base16@^1.0.0:
resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70"
integrity sha1-4pf2DX7BAUp6lxo568ipjAtoHnA=
base64-js@^1.0.2:
base64-js@^1.0.2, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
@ -4352,6 +4431,15 @@ bindings@^1.5.0:
dependencies:
file-uri-to-path "1.0.0"
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"
bluebird@^3.3.5, bluebird@^3.5.5:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
@ -4570,6 +4658,14 @@ buffer@^4.3.0:
ieee754 "^1.1.4"
isarray "^1.0.0"
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"
builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
@ -4731,6 +4827,14 @@ ccount@^1.0.0:
resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043"
integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==
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@^2.0.0, chalk@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@ -4748,7 +4852,7 @@ chalk@^3.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.0.0, chalk@^4.1.0:
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@ -4776,6 +4880,11 @@ character-reference-invalid@^1.0.0:
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
chardet@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@ -4882,6 +4991,18 @@ cli-boxes@^2.2.1:
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
cli-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
dependencies:
restore-cursor "^3.1.0"
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.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8"
@ -4891,6 +5012,11 @@ cli-table3@^0.6.1:
optionalDependencies:
colors "1.4.0"
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@^7.0.2:
version "7.0.4"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
@ -4909,6 +5035,11 @@ clone-deep@^4.0.1:
kind-of "^6.0.2"
shallow-clone "^3.0.0"
clone@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
@ -5036,7 +5167,7 @@ comma-separated-tokens@^1.0.0:
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
commander@8.3.0, commander@^8.3.0:
commander@8.3.0, commander@^8.0.0, commander@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
@ -5160,7 +5291,7 @@ cookie-signature@1.0.6:
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie@0.4.2:
cookie@0.4.2, cookie@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
@ -5466,7 +5597,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
dependencies:
ms "2.0.0"
debug@4, debug@4.3.3, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2:
debug@4, debug@4.3.3, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3:
version "4.3.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
@ -5555,6 +5686,13 @@ default-gateway@^6.0.3:
dependencies:
execa "^5.0.0"
defaults@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=
dependencies:
clone "^1.0.2"
define-lazy-prop@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
@ -6349,7 +6487,7 @@ eventemitter3@^4.0.0:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
events@^3.0.0, events@^3.2.0:
events@^3.0.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==
@ -6491,6 +6629,15 @@ extend@^3.0.0:
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
external-editor@^3.0.3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
dependencies:
chardet "^0.7.0"
iconv-lite "^0.4.24"
tmp "^0.0.33"
extglob@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
@ -6629,6 +6776,13 @@ figgy-pudding@^3.5.1:
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
figures@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
dependencies:
escape-string-regexp "^1.0.5"
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@ -7175,6 +7329,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96"
integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==
graphql@^16.3.0:
version "16.3.0"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.3.0.tgz#a91e24d10babf9e60c706919bb182b53ccdffc05"
integrity sha512-xm+ANmA16BzCT5pLjuXySbQVFwH3oJctUVdy81w1sV0vBU0KgDdBGtxQOUd5zqOBk/JayAFeG8Dlmeq74rjm/A==
gzip-size@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462"
@ -7366,6 +7525,11 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
headers-polyfill@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-3.0.4.tgz#cd70c815a441dd882372fcd6eda212ce997c9b18"
integrity sha512-I1DOM1EdWYntdrnCvqQtcKwSSuiTzoqOExy4v1mdcFixFZABlWP4IPHdmoLtPda0abMHqDOY4H9svhQ10DFR4w==
hex-color-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
@ -7603,7 +7767,7 @@ hyphenate-style-name@^1.0.3:
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
iconv-lite@0.4.24:
iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@ -7617,7 +7781,7 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
dependencies:
postcss "^7.0.14"
ieee754@^1.1.4:
ieee754@^1.1.13, ieee754@^1.1.4:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
@ -7696,6 +7860,26 @@ inline-style-parser@0.1.1:
resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1"
integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==
inquirer@^8.2.0:
version "8.2.1"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.1.tgz#e00022e3e8930a92662f760f020686530a84671d"
integrity sha512-pxhBaw9cyTFMjwKtkjePWDhvwzvrNGAw7En4hottzlPvz80GZaMZthdDU35aA6/f5FRZf3uhE057q8w1DE3V2g==
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.5.5"
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.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
@ -7965,6 +8149,11 @@ is-in-browser@^1.0.2, is-in-browser@^1.1.3:
resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835"
integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=
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-map@^2.0.1, is-map@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
@ -7975,6 +8164,11 @@ is-negative-zero@^2.0.1:
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==
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.6"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0"
@ -8099,6 +8293,11 @@ is-typedarray@^1.0.0:
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
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-weakmap@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
@ -8753,6 +8952,11 @@ jpeg-js@0.4.3:
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b"
integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==
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-string-escape@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"
@ -9156,6 +9360,14 @@ lodash@^4.0.1, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
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"
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@ -9580,6 +9792,31 @@ ms@2.1.3, ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
msw@^0.39.2:
version "0.39.2"
resolved "https://registry.yarnpkg.com/msw/-/msw-0.39.2.tgz#832e9274db62c43cb79854d5a69dce031c700de8"
integrity sha512-ju/HpqQpE4/qCxZ23t5Gaau0KREn4QuFzdG28nP1EpidMrymMJuIvNd32+2uGTGG031PMwrC41YW7vCxHOwyHA==
dependencies:
"@mswjs/cookies" "^0.2.0"
"@mswjs/interceptors" "^0.15.1"
"@open-draft/until" "^1.0.3"
"@types/cookie" "^0.4.1"
"@types/js-levenshtein" "^1.1.1"
chalk "4.1.1"
chokidar "^3.4.2"
cookie "^0.4.2"
graphql "^16.3.0"
headers-polyfill "^3.0.4"
inquirer "^8.2.0"
is-node-process "^1.0.1"
js-levenshtein "^1.1.6"
node-fetch "^2.6.7"
path-to-regexp "^6.2.0"
statuses "^2.0.0"
strict-event-emitter "^0.2.0"
type-fest "^1.2.2"
yargs "^17.3.1"
multicast-dns-service-types@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
@ -9593,6 +9830,11 @@ multicast-dns@^6.0.1:
dns-packet "^1.3.1"
thunky "^1.0.2"
mute-stream@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
nan@^2.12.1:
version "2.15.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
@ -9670,7 +9912,7 @@ node-dir@^0.1.10:
dependencies:
minimatch "^3.0.2"
node-fetch@2.6.7, node-fetch@^2.6.1:
node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
@ -9918,7 +10160,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
onetime@^5.1.2:
onetime@^5.1.0, onetime@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
@ -9971,11 +10213,36 @@ optionator@^0.9.1:
type-check "^0.4.0"
word-wrap "^1.2.3"
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.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
outvariant@^1.2.1:
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.yarnpkg.com/overlayscrollbars/-/overlayscrollbars-1.13.1.tgz#0b840a88737f43a946b9d87875a2f9e421d0338a"
@ -10230,6 +10497,11 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
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@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
@ -11026,7 +11298,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.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
@ -11305,6 +11577,14 @@ resolve@^2.0.0-next.3:
is-core-module "^2.2.0"
path-parse "^1.0.6"
restore-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
dependencies:
onetime "^5.1.0"
signal-exit "^3.0.2"
ret@~0.1.10:
version "0.1.15"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
@ -11367,6 +11647,11 @@ rsvp@^4.8.4:
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
run-async@^2.4.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
@ -11381,6 +11666,13 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies:
aproba "^1.1.1"
rxjs@^7.5.5:
version "7.5.5"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f"
integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==
dependencies:
tslib "^2.1.0"
safe-buffer@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
@ -11597,6 +11889,11 @@ set-blocking@^2.0.0:
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
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.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
@ -11950,6 +12247,11 @@ static-extend@^0.1.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
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.13.1"
resolved "https://registry.yarnpkg.com/store2/-/store2-2.13.1.tgz#fae7b5bb9d35fc53dc61cd262df3abb2f6e59022"
@ -11987,6 +12289,13 @@ stream-shift@^1.0.0:
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
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-length@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
@ -12343,6 +12652,11 @@ through2@^2.0.0:
readable-stream "~2.3.6"
xtend "~4.0.1"
through@^2.3.6:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
thunky@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d"
@ -12365,6 +12679,13 @@ tinycolor2@^1.4.1:
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
dependencies:
os-tmpdir "~1.0.2"
tmpl@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
@ -12536,7 +12857,7 @@ tslib@^1.10.0, tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
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.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
@ -12592,6 +12913,11 @@ type-fest@^0.8.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
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.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@ -12841,6 +13167,13 @@ use-latest@^1.0.0:
dependencies:
use-isomorphic-layout-effect "^1.0.0"
use-subscription@^1.3.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1"
integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA==
dependencies:
object-assign "^4.1.1"
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
@ -13019,6 +13352,13 @@ wbuf@^1.1.0, wbuf@^1.7.3:
dependencies:
minimalistic-assert "^1.0.0"
wcwidth@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=
dependencies:
defaults "^1.0.3"
web-namespaces@^1.0.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
@ -13432,6 +13772,11 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xstate@^4.29.0, xstate@^4.30.6:
version "4.30.6"
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.30.6.tgz#62b6dea37a500e0e1c0ff7c553a801eea5119554"
integrity sha512-V7liK1cjkZRh6R/MSneG8S5VLGRatpOUcnNieiYJX4LbwKi9eUVUH5V04ugJYVcJ+2oKDKvEFvzk0VnSC7lTag==
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
@ -13467,6 +13812,11 @@ yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.7:
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
yargs-parser@^21.0.0:
version "21.0.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35"
integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==
yargs@^16.2.0:
version "16.2.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
@ -13480,6 +13830,19 @@ yargs@^16.2.0:
y18n "^5.0.5"
yargs-parser "^20.2.2"
yargs@^17.3.1:
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.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"