From ca0d17ad2f2d8017a53c231bbe318242c6372f31 Mon Sep 17 00:00:00 2001 From: Luv Kapur Date: Thu, 5 Dec 2024 11:26:44 -0500 Subject: [PATCH] fix(use-data-query): correctly infer type from apollo client result for masked result (#9353) This PR updates the `@teambit/ui-foundation.ui.hooks.use-data-query` package which fixes the build issue with apollo client incorrectly inferring result type because of the introduction of MaskedType as part of their latest `3.12.0` releases. --- .bitmap | 7 - .gitignore | 1 - .../artifact-file-node-clicked.ts | 27 +++ .../artifacts-tree/artifacts-tree.module.scss | 75 +++++++ .../artifacts-tree/artifacts-tree.tsx | 189 ++++++++++++++++++ .../ui/artifacts/artifacts-tree/index.ts | 2 + .../component-artifacts.model.ts | 55 +++++ .../models/component-artifacts-model/index.ts | 7 + .../queries/use-component-artifacts/index.ts | 1 + .../use-component-artifacts.ts | 107 ++++++++++ .../api-reference/hooks/use-schema/index.ts | 1 - .../hooks/use-schema/use-schema.ts | 36 ---- workspace.jsonc | 2 +- 13 files changed, 464 insertions(+), 46 deletions(-) create mode 100644 components/ui/artifacts/artifacts-tree/artifact-file-node-clicked.ts create mode 100644 components/ui/artifacts/artifacts-tree/artifacts-tree.module.scss create mode 100644 components/ui/artifacts/artifacts-tree/artifacts-tree.tsx create mode 100644 components/ui/artifacts/artifacts-tree/index.ts create mode 100644 components/ui/artifacts/models/component-artifacts-model/component-artifacts.model.ts create mode 100644 components/ui/artifacts/models/component-artifacts-model/index.ts create mode 100644 components/ui/artifacts/queries/use-component-artifacts/index.ts create mode 100644 components/ui/artifacts/queries/use-component-artifacts/use-component-artifacts.ts delete mode 100644 scopes/api-reference/hooks/use-schema/index.ts delete mode 100644 scopes/api-reference/hooks/use-schema/use-schema.ts diff --git a/.bitmap b/.bitmap index 382d584e05e6..851d7180131d 100644 --- a/.bitmap +++ b/.bitmap @@ -779,13 +779,6 @@ "mainFile": "index.ts", "rootDir": "scopes/cloud/hooks/use-logout" }, - "hooks/use-schema": { - "name": "hooks/use-schema", - "scope": "teambit.api-reference", - "version": "0.0.35", - "mainFile": "index.ts", - "rootDir": "scopes/api-reference/hooks/use-schema" - }, "hooks/use-viewed-lane-from-url": { "name": "hooks/use-viewed-lane-from-url", "scope": "teambit.lanes", diff --git a/.gitignore b/.gitignore index fd0055a76507..d18e1a475104 100644 --- a/.gitignore +++ b/.gitignore @@ -64,7 +64,6 @@ types/* #distrubution distribution/ -artifacts/ bit-*.tar.gz tmp/ bin/node diff --git a/components/ui/artifacts/artifacts-tree/artifact-file-node-clicked.ts b/components/ui/artifacts/artifacts-tree/artifact-file-node-clicked.ts new file mode 100644 index 000000000000..ee75f85e7752 --- /dev/null +++ b/components/ui/artifacts/artifacts-tree/artifact-file-node-clicked.ts @@ -0,0 +1,27 @@ +import { ArtifactFile } from '@teambit/component.ui.artifacts.models.component-artifacts-model'; + +export const fileNodeClicked = + (files: (ArtifactFile & { id: string })[], opts: 'download' | 'new tab') => (e, node) => { + const { id } = node; + const artifactFile = files.find((file) => file.id === id); + + if (artifactFile?.downloadUrl) { + fetch(artifactFile.downloadUrl, { method: 'GET' }) + .then((res) => res.blob()) + .then((blob) => { + // create blob link to download + const url = window.URL.createObjectURL(new Blob([blob])); + const link = document.createElement('a'); + link.href = url; + if (opts === 'download') link.setAttribute('download', artifactFile.path); + if (opts === 'new tab') link.setAttribute('target', '_blank'); + // append to html page + document.body.appendChild(link); + // force download + link.click(); + // clean up and remove the link + link.parentNode?.removeChild(link); + }) + .catch(() => {}); + } + }; diff --git a/components/ui/artifacts/artifacts-tree/artifacts-tree.module.scss b/components/ui/artifacts/artifacts-tree/artifacts-tree.module.scss new file mode 100644 index 000000000000..46176663067f --- /dev/null +++ b/components/ui/artifacts/artifacts-tree/artifacts-tree.module.scss @@ -0,0 +1,75 @@ +.artifactsPanel { + display: flex; + flex-direction: column; + overflow-y: auto; + height: 100%; + border-right: 2px solid #ededed; + background: #fafafa; + font-size: var(--bit-p-xs); +} + +.artifactsPanelCodeTabDrawer { + // drawer name + > div:first-child { + border: none; + border-bottom: 1px solid var(--bit-border-color-lightest, #ededed); + } +} +.openDrawer { + flex: 1; + height: 100%; +} +.artifactsPanelCodeDrawerContent { + overflow-y: auto; +} + +.drawerIcon { + margin-right: 8px; + font-size: var(--bit-p-xs); +} + +.label { + font-size: var(--bit-p-xxs); + font-weight: unset; + padding: 4px; +} + +.icon { + font-size: var(--bit-p-xxs); + padding: 4px; + color: var(--bit-text-color-heavy, #2b2b2b); + pointer-events: auto; +} + +.artifactIconLink { + text-decoration: none; +} + +.link { + color: var(--bit-accent-color, #6c5ce7); +} + +.node { + pointer-events: none; + > div { + pointer-events: auto; + } +} + +.artifactWidgets { + display: flex; +} + +.size { + margin-right: 8px; + padding: 4px; + font-size: var(--bit-p-xxs, '12px'); + line-height: 1em; + border-radius: 4px; + background-color: var(--bit-accent-bg, #edebfc); + color: var(--bit-accent-text, #6c5ce7); + + &.selected { + border: 1px solid var(--bit-accent-text, #6c5ce7); + } +} diff --git a/components/ui/artifacts/artifacts-tree/artifacts-tree.tsx b/components/ui/artifacts/artifacts-tree/artifacts-tree.tsx new file mode 100644 index 000000000000..04e5db470435 --- /dev/null +++ b/components/ui/artifacts/artifacts-tree/artifacts-tree.tsx @@ -0,0 +1,189 @@ +import React, { HTMLAttributes, useMemo, useContext, useCallback } from 'react'; +import classNames from 'classnames'; +import { Icon } from '@teambit/evangelist.elements.icon'; +import { WidgetProps, TreeNode as Node } from '@teambit/ui-foundation.ui.tree.tree-node'; +import { DrawerUI } from '@teambit/ui-foundation.ui.tree.drawer'; +import { FileTree, useFileTreeContext } from '@teambit/ui-foundation.ui.tree.file-tree'; +import { + ArtifactFile, + getArtifactFileDetailsFromUrl, +} from '@teambit/component.ui.artifacts.models.component-artifacts-model'; +import { TreeNode, TreeNodeProps } from '@teambit/design.ui.tree'; +import { TreeContext } from '@teambit/base-ui.graph.tree.tree-context'; +import { ComponentTreeLoader } from '@teambit/design.ui.skeletons.sidebar-loader'; +import isBinaryPath from 'is-binary-path'; +import { FolderTreeNode } from '@teambit/ui-foundation.ui.tree.folder-tree-node'; +import { useCodeParams } from '@teambit/code.ui.hooks.use-code-params'; +import { affix } from '@teambit/base-ui.utils.string.affix'; +import { ComponentContext } from '@teambit/component'; +import { useComponentArtifacts } from '@teambit/component.ui.artifacts.queries.use-component-artifacts'; +import prettyBytes from 'pretty-bytes'; +import { fileNodeClicked } from './artifact-file-node-clicked'; +import { FILE_SIZE_THRESHOLD } from '.'; + +import styles from './artifacts-tree.module.scss'; + +export type ArtifactsTreeProps = { + getIcon?: (node: TreeNode) => string | undefined; + drawerOpen: boolean; + onToggleDrawer: () => void; + drawerName: string; + host: string; +} & HTMLAttributes; + +export function ArtifactsTree({ getIcon, drawerName, drawerOpen, onToggleDrawer, host }: ArtifactsTreeProps) { + const urlParams = useCodeParams(); + const component = useContext(ComponentContext); + + const { data: artifacts = [], loading } = useComponentArtifacts(host, component.id.toString()); + + const [artifactFiles, artifactFilesTree] = useMemo(() => { + const files = + (artifacts.length > 0 && + artifacts.flatMap((artifact) => + artifact.files.map((file) => ({ ...file, id: `${artifact.taskName}/${artifact.name}/${file.path}` })) + )) || + []; + + const _artifactFilesTree = files.map((file) => file.id); + return [files, _artifactFilesTree]; + }, [loading]); + + const hasArtifacts = artifacts.length > 0; + const artifactDetailsFromUrl = getArtifactFileDetailsFromUrl(artifacts, urlParams.file); + const selected = + artifactDetailsFromUrl && + `${artifactDetailsFromUrl.taskName}/${artifactDetailsFromUrl.artifactName}/${artifactDetailsFromUrl.artifactFile.path}`; + + const payloadMap = useMemo(() => { + const _payloadMap = + (hasArtifacts && + artifacts.reduce((accum, next) => { + if (!accum.has(next.taskName)) accum.set(`${next.taskName}/`, { open: false }); + return accum; + }, new Map())) || + new Map(); + + const { taskName, artifactName, artifactFile } = { + taskName: artifactDetailsFromUrl?.taskName, + artifactName: artifactDetailsFromUrl?.artifactName, + artifactFile: artifactDetailsFromUrl?.artifactFile, + }; + + if (taskName && artifactName && artifactFile) { + _payloadMap.set(`${taskName}/`, { open: true }); + _payloadMap.set(`${taskName}/${artifactName}/`, { open: true }); + _payloadMap.set(`${taskName}/${artifactName}/${artifactFile.path}`, { open: true }); + } + + return _payloadMap; + }, [loading, selected]); + + const getHref = useCallback( + (node) => { + return `~artifact/${node.id}${affix('?version=', urlParams.version)}`; + }, + [loading] + ); + + const widgets = useMemo(() => [generateWidget(artifactFiles || [], selected)], [loading]); + + if (!hasArtifacts) return null; + + return ( + + {loading && } + {loading || ( + { + const matchingArtifactFile = artifactFiles.find((artifactFile) => artifactFile.id === id); + if (!matchingArtifactFile) return; + const fileName = getFileNameFromNode(id); + if (isBinaryPath(fileName) || matchingArtifactFile.size > FILE_SIZE_THRESHOLD) { + fileNodeClicked(artifactFiles, 'download')(e, { id }); + } + }} + /> + )} + + ); +} + +function getFileNameFromNode(node: string) { + const lastIndex = node.lastIndexOf('/'); + return node.slice(lastIndex + 1); +} + +function generateWidget(files: (ArtifactFile & { id: string })[], selected?: string) { + return function Widget({ node }: WidgetProps) { + const id = node.id; + const artifactFile = files.find((file) => file.id === id); + const path = getFileNameFromNode(id); + const isBinary = isBinaryPath(path); + const isSelected = selected === id; + + if (artifactFile) { + return ( +
+
{prettyBytes(artifactFile.size)}
+ {!isBinary && artifactFile.size <= FILE_SIZE_THRESHOLD && ( + fileNodeClicked(files, 'new tab')(e, node)} /> + )} + { + fileNodeClicked(files, 'download')(e, node); + }} + /> +
+ ); + } + return null; + }; +} + +function fileTreeNodeWithArtifactFiles(artifactFiles: Array) { + return function FileTreeNode(props: TreeNodeProps) { + const { node } = props; + const { id } = node; + const fileTreeContext = useFileTreeContext(); + const { selected, onSelect } = useContext(TreeContext); + + const href = fileTreeContext?.getHref?.(node); + const widgets = fileTreeContext?.widgets; + const icon = fileTreeContext?.getIcon?.(node); + const path = getFileNameFromNode(id); + const isBinary = isBinaryPath(path); + const matchingArtifactFile = artifactFiles.find((artifactFile) => artifactFile.id === node.id); + const isLink = isBinary || (matchingArtifactFile?.size ?? 0) > FILE_SIZE_THRESHOLD; + + if (!node?.children) { + return ( + onSelect(node.id, e))} + href={href} + isActive={node?.id === selected} + icon={icon} + widgets={widgets} + /> + ); + } + return ; + }; +} diff --git a/components/ui/artifacts/artifacts-tree/index.ts b/components/ui/artifacts/artifacts-tree/index.ts new file mode 100644 index 000000000000..7c382ce0d393 --- /dev/null +++ b/components/ui/artifacts/artifacts-tree/index.ts @@ -0,0 +1,2 @@ +export { ArtifactsTree, ArtifactsTreeProps } from './artifacts-tree'; +export const FILE_SIZE_THRESHOLD = 10000; diff --git a/components/ui/artifacts/models/component-artifacts-model/component-artifacts.model.ts b/components/ui/artifacts/models/component-artifacts-model/component-artifacts.model.ts new file mode 100644 index 000000000000..5bb59d1a90b6 --- /dev/null +++ b/components/ui/artifacts/models/component-artifacts-model/component-artifacts.model.ts @@ -0,0 +1,55 @@ +export type ArtifactFile = { + name: string; + path: string; + content?: string; + downloadUrl?: string; + size: number; +}; + +export type Artifact = { + name: string; + taskId: string; + taskName: string; + description?: string; + files: Array; +}; + +export type ComponentArtifactsGQLResponse = Array<{ + taskId: string; + taskName: string; + artifact: Artifact; +}>; + +export function mapToArtifacts(gqlResponse: ComponentArtifactsGQLResponse): Artifact[] { + return gqlResponse + .filter((task) => task.artifact) + .map((task) => ({ + ...task.artifact, + taskId: task.taskId, + taskName: task.taskName, + })); +} + +export function getArtifactFileDetailsFromUrl( + artifacts: Array, + fileFromUrl?: string +): { taskName: string; artifactName: string; artifactFile: ArtifactFile; taskId: string } | undefined { + if (!fileFromUrl || !fileFromUrl.startsWith('~artifact/')) return undefined; + const [, fileFromUrlParsed] = fileFromUrl.split('~artifact/'); + const [taskName, ...artifactNameAndPath] = fileFromUrlParsed.split('/'); + const [artifactName, ...path] = artifactNameAndPath; + const filePath = path.join('/'); + const matchingArtifact = artifacts.find( + (artifact) => artifact.taskName === taskName && artifact.name === artifactName + ); + const matchingArtifactFile = matchingArtifact?.files.find((artifactFile) => artifactFile.path === filePath); + + if (!matchingArtifact || !matchingArtifactFile) return undefined; + + return { + taskName, + artifactName, + artifactFile: { ...matchingArtifactFile }, + taskId: matchingArtifact.taskId, + }; +} diff --git a/components/ui/artifacts/models/component-artifacts-model/index.ts b/components/ui/artifacts/models/component-artifacts-model/index.ts new file mode 100644 index 000000000000..febcf2123435 --- /dev/null +++ b/components/ui/artifacts/models/component-artifacts-model/index.ts @@ -0,0 +1,7 @@ +export { + mapToArtifacts, + ArtifactFile, + Artifact, + ComponentArtifactsGQLResponse, + getArtifactFileDetailsFromUrl, +} from './component-artifacts.model'; diff --git a/components/ui/artifacts/queries/use-component-artifacts/index.ts b/components/ui/artifacts/queries/use-component-artifacts/index.ts new file mode 100644 index 000000000000..89e0e915269f --- /dev/null +++ b/components/ui/artifacts/queries/use-component-artifacts/index.ts @@ -0,0 +1 @@ +export { useComponentArtifacts, useComponentArtifactFileContent } from './use-component-artifacts'; diff --git a/components/ui/artifacts/queries/use-component-artifacts/use-component-artifacts.ts b/components/ui/artifacts/queries/use-component-artifacts/use-component-artifacts.ts new file mode 100644 index 000000000000..d1f49cec05d6 --- /dev/null +++ b/components/ui/artifacts/queries/use-component-artifacts/use-component-artifacts.ts @@ -0,0 +1,107 @@ +import { gql } from '@apollo/client'; +import { useDataQuery } from '@teambit/ui-foundation.ui.hooks.use-data-query'; +import { mapToArtifacts, Artifact } from '@teambit/component.ui.artifacts.models.component-artifacts-model'; + +const ARTIFACTS_QUERY = gql` + query ComponentArtifacts($id: String!, $extensionId: String!) { + getHost(id: $extensionId) { + id # used for GQL caching + get(id: $id) { + id { + name + version + scope + } + pipelineReport { + id + taskId + taskName + artifact { + id + name + description + files { + id + name + path + downloadUrl + size + } + } + } + } + } + } +`; + +export function useComponentArtifacts( + host: string, + componentId: string, + skip?: boolean +): { data: Artifact[]; loading?: boolean } { + const { data, loading } = useDataQuery(ARTIFACTS_QUERY, { + variables: { id: componentId, extensionId: host }, + skip, + fetchPolicy: 'no-cache', + }); + + const artifacts = mapToArtifacts(data?.getHost?.get?.pipelineReport || []); + + return { + loading, + data: artifacts, + }; +} + +const ARTIFACTS_QUERY_WITH_FILE_CONTENT = gql` + query ComponentArtifactsWithFileContent($id: String!, $extensionId: String!, $taskId: String, $path: String) { + getHost(id: $extensionId) { + id # used for GQL caching + get(id: $id) { + id { + name + version + scope + } + pipelineReport(taskId: $taskId) { + id + taskId + taskName + artifact(path: $path) { + id + name + description + files { + id + name + path + content + downloadUrl + size + } + } + } + } + } + } +`; + +export function useComponentArtifactFileContent( + host: string, + options: { componentId: string; taskId?: string; filePath?: string }, + skip?: boolean +): { data: Artifact[]; loading?: boolean } { + const { componentId, taskId, filePath } = options; + const { data, ...rest } = useDataQuery(ARTIFACTS_QUERY_WITH_FILE_CONTENT, { + variables: { id: componentId, extensionId: host, taskId, path: filePath }, + skip, + fetchPolicy: 'no-cache', + }); + + const artifacts = mapToArtifacts(data?.getHost?.get?.pipelineReport || []); + + return { + ...rest, + data: artifacts, + }; +} diff --git a/scopes/api-reference/hooks/use-schema/index.ts b/scopes/api-reference/hooks/use-schema/index.ts deleted file mode 100644 index a88bb7af81ae..000000000000 --- a/scopes/api-reference/hooks/use-schema/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useSchema } from './use-schema'; diff --git a/scopes/api-reference/hooks/use-schema/use-schema.ts b/scopes/api-reference/hooks/use-schema/use-schema.ts deleted file mode 100644 index 3b0cf00c09c0..000000000000 --- a/scopes/api-reference/hooks/use-schema/use-schema.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { DataQueryResult, useDataQuery } from '@teambit/ui-foundation.ui.hooks.use-data-query'; -import { gql } from '@apollo/client'; -import { SchemaQueryResult, APIReferenceModel } from '@teambit/api-reference.models.api-reference-model'; -import { APINodeRenderer } from '@teambit/api-reference.models.api-node-renderer'; - -const GET_SCHEMA = gql` - query Schema($extensionId: String!, $componentId: String!) { - getHost(id: $extensionId) { - id # used for GQL caching - getSchema(id: $componentId) - } - } -`; - -export function useSchema( - host: string, - componentId: string, - apiNodeRenderers: APINodeRenderer[] -): { apiModel?: APIReferenceModel } & Omit< - DataQueryResult, - 'data' -> { - const { data, ...rest } = useDataQuery(GET_SCHEMA, { - variables: { - extensionId: host, - componentId, - }, - }); - - const apiModel = data?.getHost?.getSchema ? APIReferenceModel.from(data, apiNodeRenderers) : undefined; - - return { - ...rest, - apiModel, - }; -} diff --git a/workspace.jsonc b/workspace.jsonc index d5f603773691..9854e7752769 100644 --- a/workspace.jsonc +++ b/workspace.jsonc @@ -290,7 +290,7 @@ "@teambit/ui-foundation.ui.get-icon-from-file-name": "^0.0.500", "@teambit/ui-foundation.ui.global-loader": "^0.0.502", "@teambit/ui-foundation.ui.hooks.use-bind-key": "^0.0.500", - "@teambit/ui-foundation.ui.hooks.use-data-query": "^0.0.505", + "@teambit/ui-foundation.ui.hooks.use-data-query": "^0.0.506", "@teambit/ui-foundation.ui.hooks.use-in-out-transition": "^0.0.500", "@teambit/ui-foundation.ui.hooks.use-is-mobile": "^0.0.198", "@teambit/ui-foundation.ui.hooks.use-user-agent": "^0.0.197",