← Back to team overview

sts-sponsors team mailing list archive

[Merge] ~petermakowski/maas-site-manager:feat-token-generation-form-MAASENG-1476 into maas-site-manager:main

 

Peter Makowski has proposed merging ~petermakowski/maas-site-manager:feat-token-generation-form-MAASENG-1476 into maas-site-manager:main.

Commit message:
add token generation form MAASENG-1476

Requested reviews:
  MAAS Committers (maas-committers)

For more details, see:
https://code.launchpad.net/~petermakowski/maas-site-manager/+git/site-manager/+merge/439061

add the following dependencies:
formik, yup, human-interval

The form is displayed inline and not as a side panel which will be implemented as part of: https://warthogs.atlassian.net/browse/MAASENG-1487

QA
- Go to `/tokens` page
- Click on "Generate tokens"
- Set the number of tokens to generate
- Enter a time duration in a natural language, e.g. "1 week"
- Click the submit button
- Ensure the network request have been made with the correct values (body should contain the amount as a number and duration in ISO duration format).
-- 
Your team MAAS Committers is requested to review the proposed merge of ~petermakowski/maas-site-manager:feat-token-generation-form-MAASENG-1476 into maas-site-manager:main.
diff --git a/frontend/package.json b/frontend/package.json
index add2a15..b0aa455 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -19,13 +19,16 @@
     "classnames": "2.3.2",
     "date-fns": "2.29.3",
     "date-fns-tz": "2.0.0",
+    "formik": "2.2.9",
+    "human-interval": "2.0.1",
     "lodash": "4.17.21",
     "pluralize": "8.0.0",
     "react": "18.2.0",
     "react-dom": "18.2.0",
     "react-router-dom": "6.8.1",
     "use-local-storage-state": "18.1.2",
-    "vanilla-framework": "3.11.0"
+    "vanilla-framework": "3.11.0",
+    "yup": "1.0.2"
   },
   "devDependencies": {
     "@playwright/test": "1.31.1",
diff --git a/frontend/src/api/handlers.ts b/frontend/src/api/handlers.ts
index a1769fe..c5ad882 100644
--- a/frontend/src/api/handlers.ts
+++ b/frontend/src/api/handlers.ts
@@ -25,12 +25,12 @@ export const getSites = async (params: GetSitesQueryParams, queryText?: string) 
 
 export type PostTokensData = {
   amount: number;
-  name: string;
+  name?: string;
   expires: string; // <ISO 8601 date string>,
 };
 
 export const postTokens = async (data: PostTokensData) => {
-  if (!data?.amount || !data?.name || !data?.expires) {
+  if (!data?.amount || !data?.expires) {
     throw Error("Missing required fields");
   }
   try {
diff --git a/frontend/src/components/TokensCreate/TokensCreate.test.tsx b/frontend/src/components/TokensCreate/TokensCreate.test.tsx
index a3b8abd..b2a82f7 100644
--- a/frontend/src/components/TokensCreate/TokensCreate.test.tsx
+++ b/frontend/src/components/TokensCreate/TokensCreate.test.tsx
@@ -1,10 +1,71 @@
+import { rest } from "msw";
+import { setupServer } from "msw/node";
+import { vi } from "vitest";
+
 import TokensCreate from "./TokensCreate";
 
-import { render, screen } from "@/test-utils";
+import urls from "@/api/urls";
+import { render, screen, userEvent } from "@/test-utils";
+
+const postTokensEndpointMock = vi.fn();
+const mockServer = setupServer(
+  rest.post(urls.tokens, async (req) => {
+    postTokensEndpointMock(await req.json());
+  }),
+);
+
+beforeAll(() => {
+  mockServer.listen();
+});
+
+afterEach(() => {
+  mockServer.resetHandlers();
+});
+
+afterAll(() => {
+  mockServer.close();
+});
 
 describe("TokensCreate", () => {
   it("renders the form", async () => {
     render(<TokensCreate />);
     expect(screen.getByRole("form", { name: /Generate new enrollment tokens/i })).toBeInTheDocument();
   });
+
+  it("if not all required fields have been entered the submit button is disabled", async () => {
+    render(<TokensCreate />);
+    const amount = screen.getByLabelText(/Amount of tokens to generate/i);
+    const expires = screen.getByLabelText(/Expiration time/i);
+    expect(screen.getByRole("button", { name: /Generate tokens/i })).toBeDisabled();
+    await userEvent.type(amount, "1");
+    await userEvent.type(expires, "1 month");
+    expect(screen.getByRole("button", { name: /Generate tokens/i })).toBeEnabled();
+  });
+
+  it("displays an error for invalid expiration value", async () => {
+    render(<TokensCreate />);
+    const expires = screen.getByLabelText(/Expiration time/i);
+    await userEvent.type(expires, "2");
+    await userEvent.tab();
+    expect(expires).toHaveErrorMessage(
+      /Time unit must be a `string` type with a value of weeks, days, hours, and\/or minutes./i,
+    );
+  });
+
+  it("can generate enrolment tokens", async () => {
+    render(<TokensCreate />);
+    const amount = screen.getByLabelText(/Amount of tokens to generate/i);
+    const expires = screen.getByLabelText(/Expiration time/i);
+    expect(screen.getByRole("button", { name: /Generate tokens/i })).toBeDisabled();
+    // can specify the number of tokens to generate
+    await userEvent.type(amount, "1");
+    // can specify the token expiration time (e.g. 1 week)
+    await userEvent.type(expires, "1 week");
+    await userEvent.click(screen.getByRole("button", { name: /Generate tokens/i }));
+    expect(postTokensEndpointMock).toHaveBeenCalledTimes(1);
+    expect(postTokensEndpointMock).toHaveBeenCalledWith({
+      amount: 1,
+      expires: "P0Y0M7DT0H0M0S",
+    });
+  });
 });
diff --git a/frontend/src/components/TokensCreate/TokensCreate.tsx b/frontend/src/components/TokensCreate/TokensCreate.tsx
index b2d13df..241e2b9 100644
--- a/frontend/src/components/TokensCreate/TokensCreate.tsx
+++ b/frontend/src/components/TokensCreate/TokensCreate.tsx
@@ -1,24 +1,97 @@
 import { useId } from "react";
 
-import { Form, Input, Button } from "@canonical/react-components";
+import { Button, Input, Label, Notification } from "@canonical/react-components";
+import { useMutation } from "@tanstack/react-query";
+import { formatISODuration, intervalToDuration } from "date-fns";
+import { Field, Formik, Form } from "formik";
+import humanInterval from "human-interval";
+import * as Yup from "yup";
+
+import { humanIntervalToISODuration } from "./utils";
+
+import { postTokens } from "@/api/handlers";
+
+const initialValues = {
+  amount: "",
+  expires: "",
+};
+
+type TokensCreateFormValues = typeof initialValues;
+
+const TokensCreateSchema = Yup.object().shape({
+  amount: Yup.number().positive().required("Please enter a valid number"),
+  expires: Yup.string()
+    .matches(
+      /^((\d)+ ?(minute|hour|day|week|month|year)(s)? ?(and)? ?)+$/,
+      "Time unit must be a `string` type with a value of weeks, days, hours, and/or minutes.",
+    )
+    .test("Please enter a valid time unit", function (value) {
+      if (!value) {
+        return false;
+      }
+      try {
+        return !!humanIntervalToISODuration(value);
+      } catch (error) {
+        return false;
+      }
+    })
+    .required("Please enter a valid time unit"),
+});
 
 const TokensCreate = () => {
-  const id = useId();
+  const headingId = useId();
+  const expiresId = useId();
+  const amountId = useId();
+  const mutation = useMutation(postTokens);
+  const handleSubmit = async (
+    { amount, expires }: TokensCreateFormValues,
+    { setSubmitting }: { setSubmitting: (isSubmitting: boolean) => void },
+  ) => {
+    await mutation.mutateAsync({ amount: Number(amount), expires: humanIntervalToISODuration(expires) as string });
+    // TODO: update the tokens list
+    setSubmitting(false);
+  };
+
   return (
     <div>
-      <Form aria-labelledby={id}>
-        <h3 id={id}>Generate new enrollment tokens</h3>
-        <Input label="Amount of tokens to generate" type="number" />
-        <Input label="Expiration time" type="text" />
-        <p className="u-text--muted">
-          Use this token once to request an enrolment in the specified timeframe. <br />
-          Allowed time units are seconds, minutes, hours, days and weeks.
-        </p>
-        <Button type="button">Cancel</Button>
-        <Button appearance="positive" type="submit">
-          Generate token
-        </Button>
-      </Form>
+      <h3 id={headingId}>Generate new enrollment tokens</h3>
+      {mutation.isError && <Notification severity="negative">There was an error generating the token(s).</Notification>}
+      <Formik initialValues={initialValues} onSubmit={handleSubmit} validationSchema={TokensCreateSchema}>
+        {({ isSubmitting, errors, touched, isValid, dirty }) => (
+          <Form aria-labelledby={headingId} noValidate>
+            <Label htmlFor={amountId}>Amount of tokens to generate</Label>
+            <Field
+              as={Input}
+              error={touched.amount && errors.amount}
+              id={amountId}
+              name="amount"
+              required
+              type="text"
+            />
+            <Label htmlFor={expiresId}>Expiration time</Label>
+            <Field
+              as={Input}
+              error={touched.expires && errors.expires}
+              id={expiresId}
+              name="expires"
+              required
+              type="text"
+            />
+            <p className="u-text--muted">
+              Use this token once to request an enrolment in the specified timeframe. <br />
+              Allowed time units are seconds, minutes, hours, days and weeks.
+            </p>
+            <Button type="button">Cancel</Button>
+            <Button
+              appearance="positive"
+              disabled={!dirty || !isValid || mutation.isLoading || isSubmitting}
+              type="submit"
+            >
+              Generate tokens
+            </Button>
+          </Form>
+        )}
+      </Formik>
     </div>
   );
 };
diff --git a/frontend/src/components/TokensCreate/utils.test.ts b/frontend/src/components/TokensCreate/utils.test.ts
new file mode 100644
index 0000000..0271d0c
--- /dev/null
+++ b/frontend/src/components/TokensCreate/utils.test.ts
@@ -0,0 +1,7 @@
+import { humanIntervalToISODuration } from "./utils";
+
+describe("humanIntervalToISODuration", () => {
+  it("returns a valid ISO duration string for hours and seconds", () => {
+    expect(humanIntervalToISODuration("1 week 1 days 3 hours 30 seconds")).toEqual("P0Y0M8DT3H0M30S");
+  });
+});
diff --git a/frontend/src/components/TokensCreate/utils.ts b/frontend/src/components/TokensCreate/utils.ts
new file mode 100644
index 0000000..718d876
--- /dev/null
+++ b/frontend/src/components/TokensCreate/utils.ts
@@ -0,0 +1,9 @@
+import { formatISODuration, intervalToDuration } from "date-fns";
+import humanInterval from "human-interval";
+
+export const humanIntervalToISODuration = (intervalString: string) => {
+  const intervalNumber = humanInterval(intervalString);
+  if (intervalNumber) {
+    return formatISODuration(intervalToDuration({ start: 0, end: intervalNumber }));
+  }
+};
diff --git a/frontend/src/mocks/resolvers.test.ts b/frontend/src/mocks/resolvers.test.ts
index f09733e..d8f2cad 100644
--- a/frontend/src/mocks/resolvers.test.ts
+++ b/frontend/src/mocks/resolvers.test.ts
@@ -20,9 +20,9 @@ afterAll(() => {
 describe("mock post tokens server", () => {
   it("returns list of tokens based on the request data", async () => {
     const amount = 1;
-    const { name, expires } = tokenFactory.build({ name: "test", expires: "2021-01-01" });
-    const result = await axios.post(urls.tokens, { name, expires, amount });
+    const { expires } = tokenFactory.build({ name: "test", expires: "2021-01-01" });
+    const result = await axios.post(urls.tokens, { expires, amount });
     expect(result.data.items).toHaveLength(amount);
-    expect(result.data.items[0]).toEqual(expect.objectContaining({ name, expires }));
+    expect(result.data.items[0]).toEqual(expect.objectContaining({ expires }));
   });
 });
diff --git a/frontend/src/mocks/resolvers.ts b/frontend/src/mocks/resolvers.ts
index bbbdad6..9350a9a 100644
--- a/frontend/src/mocks/resolvers.ts
+++ b/frontend/src/mocks/resolvers.ts
@@ -1,10 +1,10 @@
+import type { GetSitesQueryParams, PostTokensData } from "api/handlers";
 import { rest } from "msw";
 import type { RestRequest, restContext, ResponseResolver } from "msw";
 
 import { siteFactory, tokenFactory } from "./factories";
 
 import urls from "@/api/urls";
-import type { GetSitesQueryParams, PostTokensData } from "api/handlers";
 
 export const sitesList = siteFactory.buildList(155);
 
@@ -28,9 +28,11 @@ export const createMockSitesResolver =
 type TokensResponseResolver = ResponseResolver<RestRequest<PostTokensData>, typeof restContext>;
 export const createMockTokensResolver = (): TokensResponseResolver => async (req, res, ctx) => {
   let items;
-  const { amount, name, expires } = await req.json();
-  if (amount && name && expires) {
-    items = Array(amount).fill(tokenFactory.build({ name, expires }));
+  const { amount, expires } = await req.json();
+  if (amount && expires) {
+    items = Array(amount).fill(tokenFactory.build({ expires }));
+  } else {
+    return res(ctx.status(400));
   }
   const response = {
     items,
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
index 32c13da..1d31aae 100644
--- a/frontend/vitest.config.ts
+++ b/frontend/vitest.config.ts
@@ -10,5 +10,6 @@ export default defineConfig({
     environment: "jsdom",
     setupFiles: ["./setupTests.ts"],
     exclude: [...configDefaults.exclude, "**/tests/**"],
+    clearMocks: true,
   },
 });
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 567d215..fc6dc28 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -2834,6 +2834,11 @@ deep-is@^0.1.3, deep-is@~0.1.3:
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831";
   integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
 
+deepmerge@^2.1.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170";
+  integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
+
 defaults@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a";
@@ -3556,6 +3561,19 @@ form-data@^4.0.0:
     combined-stream "^1.0.8"
     mime-types "^2.1.12"
 
+formik@2.2.9:
+  version "2.2.9"
+  resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0";
+  integrity sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==
+  dependencies:
+    deepmerge "^2.1.1"
+    hoist-non-react-statics "^3.3.0"
+    lodash "^4.17.21"
+    lodash-es "^4.17.21"
+    react-fast-compare "^2.0.1"
+    tiny-warning "^1.0.2"
+    tslib "^1.10.0"
+
 fraction.js@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950";
@@ -3821,6 +3839,13 @@ headers-polyfill@^3.1.0:
   resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-3.1.2.tgz#9a4dcb545c5b95d9569592ef7ec0708aab763fbe";
   integrity sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA==
 
+hoist-non-react-statics@^3.3.0:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45";
+  integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
+  dependencies:
+    react-is "^16.7.0"
+
 hosted-git-info@^2.1.4:
   version "2.8.9"
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9";
@@ -3857,6 +3882,13 @@ https-proxy-agent@^5.0.1:
     agent-base "6"
     debug "4"
 
+human-interval@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/human-interval/-/human-interval-2.0.1.tgz#655baf606c7067bb26042dcae14ec777b099af15";
+  integrity sha512-r4Aotzf+OtKIGQCB3odUowy4GfUDTy3aTWTfLd7ZF2gBCy3XW3v/dJLRefZnOFFnjqs5B1TypvS8WarpBkYUNQ==
+  dependencies:
+    numbered "^1.1.0"
+
 i18n-iso-countries@7.5.0:
   version "7.5.0"
   resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-7.5.0.tgz#74fedd72619526a195cfb2e768fe1d82eed2123f";
@@ -4471,6 +4503,11 @@ locate-path@^6.0.0:
   dependencies:
     p-locate "^5.0.0"
 
+lodash-es@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee";
+  integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
 lodash.debounce@^4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af";
@@ -4780,6 +4817,11 @@ npm-package-json-lint@6.4.0:
     type-fest "^3.2.0"
     validate-npm-package-name "^5.0.0"
 
+numbered@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/numbered/-/numbered-1.1.0.tgz#9fcd79564c73a84b9574e8370c3d8e58fe3c133c";
+  integrity sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==
+
 nwsapi@^2.2.2:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0";
@@ -5206,6 +5248,11 @@ prop-types@15.8.1, prop-types@^15.8.1:
     object-assign "^4.1.1"
     react-is "^16.13.1"
 
+property-expr@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4";
+  integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==
+
 proxy-from-env@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2";
@@ -5251,7 +5298,12 @@ react-error-boundary@^3.1.0:
   dependencies:
     "@babel/runtime" "^7.12.5"
 
-react-is@^16.13.1:
+react-fast-compare@^2.0.1:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9";
+  integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
+
+react-is@^16.13.1, react-is@^16.7.0:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4";
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -5900,6 +5952,11 @@ timezone-mock@1.3.6:
   resolved "https://registry.yarnpkg.com/timezone-mock/-/timezone-mock-1.3.6.tgz#44e4c5aeb57e6c07ae630a05c528fc4d9aab86f4";
   integrity sha512-YcloWmZfLD9Li5m2VcobkCDNVaLMx8ohAb/97l/wYS3m+0TIEK5PFNMZZfRcusc6sFjIfxu8qcJT0CNnOdpqmg==
 
+tiny-case@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03";
+  integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==
+
 tiny-glob@^0.2.9:
   version "0.2.9"
   resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2";
@@ -5908,6 +5965,11 @@ tiny-glob@^0.2.9:
     globalyzer "0.1.0"
     globrex "^0.1.2"
 
+tiny-warning@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754";
+  integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+
 tinybench@^2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.3.1.tgz#14f64e6b77d7ef0b1f6ab850c7a808c6760b414d";
@@ -5942,6 +6004,11 @@ to-regex-range@^5.0.1:
   dependencies:
     is-number "^7.0.0"
 
+toposort@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330";
+  integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==
+
 tough-cookie@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874";
@@ -5979,7 +6046,7 @@ tsconfig-paths@^3.14.1:
     minimist "^1.2.6"
     strip-bom "^3.0.0"
 
-tslib@^1.8.1:
+tslib@^1.10.0, tslib@^1.8.1:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00";
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
@@ -6542,3 +6609,13 @@ yocto-queue@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251";
   integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
+
+yup@1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/yup/-/yup-1.0.2.tgz#1cf485f407f77e0407b450311f2981a1e66f7c58";
+  integrity sha512-Lpi8nITFKjWtCoK3yQP8MUk78LJmHWqbFd0OOMXTar+yjejlQ4OIIoZgnTW1bnEUKDw6dZBcy3/IdXnt2KDUow==
+  dependencies:
+    property-expr "^2.0.5"
+    tiny-case "^1.0.3"
+    toposort "^2.0.2"
+    type-fest "^2.19.0"

Follow ups