From a3fcc65acbd1d1912914c3974db4084ec4e3e4d8 Mon Sep 17 00:00:00 2001 From: aliang <1098486429@qq.com> Date: Sun, 18 Feb 2024 14:41:55 +0800 Subject: [PATCH] feat(ui): implement jobs management (#1463) * feat(ui): job runs table * update: jobs table * feat: jobrun detail page * fix: detail link * update * feat: update api params * [autofix.ci] apply automated fixes * update rich text style * [autofix.ci] apply automated fixes * update * update(ui): layout * [autofix.ci] apply automated fixes * Update ee/tabby-ui/app/(dashboard)/components/sidebar.tsx Co-authored-by: Meng Zhang * Update ee/tabby-ui/app/(dashboard)/(logs)/jobs/detail/page.tsx Co-authored-by: Meng Zhang * Update ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs.tsx Co-authored-by: Meng Zhang * fix sidebar * [autofix.ci] apply automated fixes * update: log - 30height * format * fix log display * update * [autofix.ci] apply automated fixes * update * [autofix.ci] apply automated fixes * Update ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs-table.tsx Co-authored-by: Meng Zhang * Update ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs-table.tsx Co-authored-by: Meng Zhang * update * [autofix.ci] apply automated fixes * exitcode * update * format * update --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Meng Zhang --- .../(logs)/jobs/components/job-detail.tsx | 99 ++++++++++ .../(logs)/jobs/components/job-list.tsx | 61 ++++++ .../(logs)/jobs/components/jobs-table.tsx | 148 +++++++++++++++ .../(logs)/jobs/components/jobs.tsx | 9 + .../(dashboard)/(logs)/jobs/detail/page.tsx | 9 + .../app/(dashboard)/(logs)/jobs/page.tsx | 11 ++ ee/tabby-ui/app/(dashboard)/(logs)/layout.tsx | 7 + .../app/(dashboard)/components/sidebar.tsx | 51 ++--- ee/tabby-ui/app/(dashboard)/layout.tsx | 18 +- .../components/repository-table.tsx | 4 +- ee/tabby-ui/components/ui/icons.tsx | 179 +++++++++++++++++- ee/tabby-ui/components/ui/table.tsx | 12 +- ee/tabby-ui/components/ui/tabs.tsx | 55 ++++++ ee/tabby-ui/lib/tabby/gql.ts | 17 +- ee/tabby-ui/lib/tabby/query.ts | 37 ++++ ee/tabby-ui/package.json | 2 + ee/tabby-ui/yarn.lock | 33 ++++ 17 files changed, 702 insertions(+), 50 deletions(-) create mode 100644 ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-detail.tsx create mode 100644 ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-list.tsx create mode 100644 ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs-table.tsx create mode 100644 ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs.tsx create mode 100644 ee/tabby-ui/app/(dashboard)/(logs)/jobs/detail/page.tsx create mode 100644 ee/tabby-ui/app/(dashboard)/(logs)/jobs/page.tsx create mode 100644 ee/tabby-ui/app/(dashboard)/(logs)/layout.tsx create mode 100644 ee/tabby-ui/components/ui/tabs.tsx diff --git a/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-detail.tsx b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-detail.tsx new file mode 100644 index 000000000000..ba0994cf194f --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-detail.tsx @@ -0,0 +1,99 @@ +'use client' + +import React from 'react' +import { useSearchParams } from 'next/navigation' +import Ansi from '@curvenote/ansi-to-react' +import { useQuery } from 'urql' + +import { listJobRuns } from '@/lib/tabby/query' +import { cn } from '@/lib/utils' +import { IconAlertTriangle, IconTerminalSquare } from '@/components/ui/icons' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { ListSkeleton } from '@/components/skeleton' + +import { JobsTable } from './jobs-table' + +export default function JobRunDetail() { + const searchParams = useSearchParams() + const id = searchParams.get('id') + const [{ data, error, fetching }, reexecuteQuery] = useQuery({ + query: listJobRuns, + variables: { ids: [id as string] }, + pause: !id + }) + + const edges = data?.jobRuns?.edges?.slice(0, 1) + const currentNode = data?.jobRuns?.edges?.[0]?.node + + React.useEffect(() => { + let timer: number + if (currentNode?.createdAt && !currentNode?.finishedAt) { + timer = window.setTimeout(() => { + reexecuteQuery() + }, 5000) + } + + return () => { + if (timer) { + clearInterval(timer) + } + } + }, [currentNode]) + + return ( + <> + {fetching ? ( + + ) : ( +
+ + + + + + stdout + + + + stderr + + +
+ + + + + + +
+
+
+ )} + + ) +} + +function StdoutView({ + children, + className, + value, + ...rest +}: React.HTMLAttributes & { value?: string }) { + return ( +
+ {value ? ( +
+          {value}
+        
+ ) : ( +
No Data
+ )} +
+ ) +} diff --git a/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-list.tsx b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-list.tsx new file mode 100644 index 000000000000..73b2712a026e --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/job-list.tsx @@ -0,0 +1,61 @@ +'use client' + +import React from 'react' +import { useQuery } from 'urql' + +import { DEFAULT_PAGE_SIZE } from '@/lib/constants' +import { ListJobRunsQueryVariables } from '@/lib/gql/generates/graphql' +import { useIsQueryInitialized } from '@/lib/tabby/gql' +import { listJobRuns } from '@/lib/tabby/query' +import { Button } from '@/components/ui/button' +import { IconSpinner } from '@/components/ui/icons' +import { ListSkeleton } from '@/components/skeleton' + +import { JobsTable } from './jobs-table' + +const PAGE_SIZE = DEFAULT_PAGE_SIZE +export function JobRuns() { + const [variables, setVariables] = React.useState({ + last: PAGE_SIZE + }) + const [{ data, error, fetching, stale }] = useQuery({ + query: listJobRuns, + variables + }) + + const [initialized] = useIsQueryInitialized({ data, error, stale }) + + const edges = data?.jobRuns?.edges + const pageInfo = data?.jobRuns?.pageInfo + const hasNextPage = pageInfo?.hasPreviousPage + + const fetchNextPage = () => { + setVariables({ last: PAGE_SIZE, before: pageInfo?.startCursor }) + } + + const displayJobs = React.useMemo(() => { + return edges?.slice().reverse() + }, [edges]) + + return ( + <> + {initialized ? ( + <> + + {hasNextPage && ( +
+ +
+ )} + + ) : ( + + )} + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs-table.tsx b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs-table.tsx new file mode 100644 index 000000000000..1dd6a3a64738 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs-table.tsx @@ -0,0 +1,148 @@ +import React from 'react' +import { useRouter } from 'next/navigation' +import { isNil } from 'lodash-es' +import moment from 'moment' + +import { ListJobRunsQuery } from '@/lib/gql/generates/graphql' +import { cn } from '@/lib/utils' +import { Badge } from '@/components/ui/badge' +import { + IconCheckCircled, + IconCrossCircled, + IconInfoCircled +} from '@/components/ui/icons' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table' + +type TJobRun = ListJobRunsQuery['jobRuns']['edges'][0] +interface JobsTableProps { + jobs: TJobRun[] | undefined + shouldRedirect?: boolean +} + +export const JobsTable: React.FC = ({ + jobs, + shouldRedirect = true +}) => { + const router = useRouter() + return ( + + + + Start Time + Duration + Job + Status + + + + {!jobs?.length ? ( + + + No Data + + + ) : ( + <> + {jobs?.map(x => { + return ( + { + if (shouldRedirect) { + router.push(`/jobs/detail?id=${x.node.id}`) + } + }} + > + + {moment(x.node.createdAt).format('MMMM D, YYYY h:mm a')} + + + {isNil(x.node?.exitCode) + ? 'Running' + : getJobDuration(x.node)} + + + {x.node.job} + + +
+ +
+
+
+ ) + })} + + )} +
+
+ ) +} + +function getJobDuration({ + createdAt, + finishedAt +}: { + createdAt: string + finishedAt?: string +}) { + if (!createdAt || !finishedAt) return undefined + + let duration = moment.duration(moment(finishedAt).diff(createdAt)) + return formatDuration(duration) +} + +function formatDuration(duration: moment.Duration) { + const hours = duration.hours() + const minutes = duration.minutes() + const seconds = duration.seconds() + + let formattedDuration = '' + + if (hours > 0) { + formattedDuration += `${hours}h` + } + + if (minutes > 0) { + if (formattedDuration.length > 0) { + formattedDuration += ' ' + } + + formattedDuration += `${minutes}min` + } + + if (seconds > 0) { + if (formattedDuration.length > 0) { + formattedDuration += ' ' + } + + formattedDuration += `${seconds}s` + } + + return formattedDuration +} + +function JobStatusIcon({ node }: { node: TJobRun }) { + if (!node) return null + const exitCode = node?.node?.exitCode + + if (isNil(exitCode)) { + return + } + if (exitCode === 0) { + return + } + + return +} diff --git a/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs.tsx b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs.tsx new file mode 100644 index 000000000000..5fcec01ce068 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/components/jobs.tsx @@ -0,0 +1,9 @@ +import { JobRuns } from './job-list' + +export default function JobRunsPage() { + return ( + <> + + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/(logs)/jobs/detail/page.tsx b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/detail/page.tsx new file mode 100644 index 000000000000..ddcbbf793479 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/detail/page.tsx @@ -0,0 +1,9 @@ +import JobRunDetail from '../components/job-detail' + +export default function JobDetailPage() { + return ( + <> + + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/(logs)/jobs/page.tsx b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/page.tsx new file mode 100644 index 000000000000..41b38a9bffdb --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/(logs)/jobs/page.tsx @@ -0,0 +1,11 @@ +import { Metadata } from 'next' + +import JobRunsPage from './components/jobs' + +export const metadata: Metadata = { + title: 'Job runs' +} + +export default function IndexPage() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/(logs)/layout.tsx b/ee/tabby-ui/app/(dashboard)/(logs)/layout.tsx new file mode 100644 index 000000000000..78e572debac4 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/(logs)/layout.tsx @@ -0,0 +1,7 @@ +export default function LogsLayout({ + children +}: { + children: React.ReactNode +}) { + return
{children}
+} diff --git a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx index f047e6de11b4..8a8b5da80663 100644 --- a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx +++ b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx @@ -20,11 +20,12 @@ import { IconGear, IconHome, IconLightingBolt, - IconNetwork + IconNetwork, + IconScrollText } from '@/components/ui/icons' export interface SidebarProps { - children: React.ReactNode + children?: React.ReactNode className?: string } @@ -35,25 +36,24 @@ export default function Sidebar({ children, className }: SidebarProps) {
-
-
-
-
-
-
+
-
{children}
) } diff --git a/ee/tabby-ui/app/(dashboard)/layout.tsx b/ee/tabby-ui/app/(dashboard)/layout.tsx index 12be28345be1..28370816aa44 100644 --- a/ee/tabby-ui/app/(dashboard)/layout.tsx +++ b/ee/tabby-ui/app/(dashboard)/layout.tsx @@ -1,6 +1,5 @@ import { Metadata } from 'next' -import { ScrollArea } from '@/components/ui/scroll-area' import { Header } from '@/components/header' import Sidebar from './components/sidebar' @@ -18,15 +17,12 @@ interface DashboardLayoutProps { export default function RootLayout({ children }: DashboardLayoutProps) { return ( - <> -
- - -
-
{children}
- - -
- +
+ +
+
+
{children}
+
+
) } diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/repository/components/repository-table.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/repository/components/repository-table.tsx index c132258fde52..4023f7ffab0c 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/repository/components/repository-table.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/repository/components/repository-table.tsx @@ -40,11 +40,11 @@ const deleteRepositoryMutation = graphql(/* GraphQL */ ` const PAGE_SIZE = DEFAULT_PAGE_SIZE export default function RepositoryTable() { const client = useClient() - const [{ data, error, fetching }] = useQuery({ + const [{ data, error, fetching, stale }] = useQuery({ query: listRepositories, variables: { first: PAGE_SIZE } }) - const [initialized] = useIsQueryInitialized({ data, error }) + const [initialized] = useIsQueryInitialized({ data, error, stale }) const [currentPage, setCurrentPage] = React.useState(1) const edges = data?.repositories?.edges diff --git a/ee/tabby-ui/components/ui/icons.tsx b/ee/tabby-ui/components/ui/icons.tsx index 4376da826995..983232935143 100644 --- a/ee/tabby-ui/components/ui/icons.tsx +++ b/ee/tabby-ui/components/ui/icons.tsx @@ -983,6 +983,175 @@ function IconLightingBolt({ ) } +function IconScrollText({ className, ...props }: React.ComponentProps<'svg'>) { + return ( + + + + + + + ) +} + +function IconTerminalSquare({ + className, + ...props +}: React.ComponentProps<'svg'>) { + return ( + + + + + + ) +} + +function IconAlertTriangle({ + className, + ...props +}: React.ComponentProps<'svg'>) { + return ( + + + + + + ) +} + +function IconFileSearch({ className, ...props }: React.ComponentProps<'svg'>) { + return ( + + + + + + + ) +} + +function IconPieChart({ className, ...props }: React.ComponentProps<'svg'>) { + return ( + + + + + ) +} + +function IconCheckCircled({ + className, + ...props +}: React.ComponentProps<'svg'>) { + return ( + + + + ) +} + +function IconCrossCircled({ + className, + ...props +}: React.ComponentProps<'svg'>) { + return ( + + + + ) +} + +function IconInfoCircled({ className, ...props }: React.ComponentProps<'svg'>) { + return ( + + + + ) +} + export { IconEdit, IconNextChat, @@ -1034,5 +1203,13 @@ export { IconCircle, IconGithub, IconGoogle, - IconLightingBolt + IconLightingBolt, + IconScrollText, + IconTerminalSquare, + IconAlertTriangle, + IconFileSearch, + IconPieChart, + IconCheckCircled, + IconCrossCircled, + IconInfoCircled } diff --git a/ee/tabby-ui/components/ui/table.tsx b/ee/tabby-ui/components/ui/table.tsx index 893974f33426..8bea0aca0f02 100644 --- a/ee/tabby-ui/components/ui/table.tsx +++ b/ee/tabby-ui/components/ui/table.tsx @@ -6,13 +6,11 @@ const Table = React.forwardRef< HTMLTableElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -
- - +
)) Table.displayName = 'Table' diff --git a/ee/tabby-ui/components/ui/tabs.tsx b/ee/tabby-ui/components/ui/tabs.tsx new file mode 100644 index 000000000000..0a9f08634216 --- /dev/null +++ b/ee/tabby-ui/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +'use client' + +import * as React from 'react' +import * as TabsPrimitive from '@radix-ui/react-tabs' + +import { cn } from '@/lib/utils' + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/ee/tabby-ui/lib/tabby/gql.ts b/ee/tabby-ui/lib/tabby/gql.ts index 34dd0f0498a6..dc5702dd5d0d 100644 --- a/ee/tabby-ui/lib/tabby/gql.ts +++ b/ee/tabby-ui/lib/tabby/gql.ts @@ -91,18 +91,24 @@ function makeFormErrorHandler(form: UseFormReturn) { function useIsQueryInitialized({ data, - error + error, + stale }: { data?: any error?: CombinedError + stale?: boolean }): [boolean, React.Dispatch>] { - // todo urql do cache data, considering passing default `initialized` with data & stale - const [initialized, setInitialized] = useState(false) + const isDataExist = (data?: any, error?: CombinedError) => { + return !isNil(data) || !isNil(error) + } + const [initialized, setInitialized] = useState( + isDataExist(data, error) && !!stale + ) useEffect(() => { if (initialized) return - if (!isNil(data) || !isNil(error)) { + if (isDataExist(data, error)) { setInitialized(true) } }, [data, error]) @@ -121,7 +127,8 @@ const client = new Client({ resolvers: { Query: { invitations: relayPagination(), - repositories: relayPagination() + repositories: relayPagination(), + jobRuns: relayPagination() } }, updates: { diff --git a/ee/tabby-ui/lib/tabby/query.ts b/ee/tabby-ui/lib/tabby/query.ts index 0858e6c60140..d1352884cfb6 100644 --- a/ee/tabby-ui/lib/tabby/query.ts +++ b/ee/tabby-ui/lib/tabby/query.ts @@ -47,3 +47,40 @@ export const listRepositories = graphql(/* GraphQL */ ` } } `) + +export const listJobRuns = graphql(/* GraphQL */ ` + query ListJobRuns( + $ids: [ID!] + $after: String + $before: String + $first: Int + $last: Int + ) { + jobRuns( + ids: $ids + after: $after + before: $before + first: $first + last: $last + ) { + edges { + node { + id + job + createdAt + finishedAt + exitCode + stdout + stderr + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } +`) diff --git a/ee/tabby-ui/package.json b/ee/tabby-ui/package.json index 68ca5009d6a8..8b73fe641bb8 100644 --- a/ee/tabby-ui/package.json +++ b/ee/tabby-ui/package.json @@ -23,6 +23,7 @@ "@codemirror/state": "^6.4.0", "@codemirror/theme-one-dark": "^6.1.2", "@codemirror/view": "^6.23.0", + "@curvenote/ansi-to-react": "^7.0.0", "@hookform/resolvers": "^3.3.2", "@radix-ui/react-alert-dialog": "1.0.4", "@radix-ui/react-checkbox": "^1.0.4", @@ -37,6 +38,7 @@ "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.6", "@uiw/codemirror-extensions-langs": "^4.21.21", "@urql/core": "^4.2.3", diff --git a/ee/tabby-ui/yarn.lock b/ee/tabby-ui/yarn.lock index c301faf0c116..e5ec303b094c 100644 --- a/ee/tabby-ui/yarn.lock +++ b/ee/tabby-ui/yarn.lock @@ -918,6 +918,14 @@ style-mod "^4.1.0" w3c-keyname "^2.2.4" +"@curvenote/ansi-to-react@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@curvenote/ansi-to-react/-/ansi-to-react-7.0.0.tgz#c4dd6691bddc2c863c5dffb9c7d366a0815802eb" + integrity sha512-+m4V86QPmaZ7udMp7Yg81A31dLBGO8gglkPGWPzUJNxLCSyOrJ04pQmdZQelNBEA7MSGz8wf+6RHcuEaujdhHw== + dependencies: + anser "^2.1.1" + escape-carriage "^1.3.1" + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -2246,6 +2254,21 @@ "@radix-ui/react-use-previous" "1.0.1" "@radix-ui/react-use-size" "1.0.1" +"@radix-ui/react-tabs@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2" + integrity sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-roving-focus" "1.0.4" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-tooltip@^1.0.6": version "1.0.7" resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz#8f55070f852e7e7450cc1d9210b793d2e5a7686e" @@ -2763,6 +2786,11 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +anser@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/anser/-/anser-2.1.1.tgz#8afae28d345424c82de89cc0e4d1348eb0c5af7c" + integrity sha512-nqLm4HxOTpeLOxcmB3QWmV5TcDFhW9y/fyQ+hivtDFcK4OQ+pQ5fzPnXHM1Mfcm0VkLtvVi1TCPr++Qy0Q/3EQ== + ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -3846,6 +3874,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-carriage@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/escape-carriage/-/escape-carriage-1.3.1.tgz#842658e5422497b1232585e517dc813fc6a86170" + integrity sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw== + escape-html@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"