sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #06068
[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