Skip to content

fix(use-data-query): correctly infer type from apollo client result for masked result #9353

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .bitmap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ types/*
#distrubution
distribution/

artifacts/
bit-*.tar.gz
tmp/
bin/node
Expand Down
Original file line number Diff line number Diff line change
@@ -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(() => {});
}
};
75 changes: 75 additions & 0 deletions components/ui/artifacts/artifacts-tree/artifacts-tree.module.scss
Original file line number Diff line number Diff line change
@@ -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);
}
}
189 changes: 189 additions & 0 deletions components/ui/artifacts/artifacts-tree/artifacts-tree.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>;

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<string, { open?: boolean }>())) ||
new Map<string, { open?: boolean }>();

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 (
<DrawerUI
isOpen={drawerOpen}
onToggle={onToggleDrawer}
name={drawerName}
contentClass={styles.artifactsPanelCodeDrawerContent}
className={classNames(styles.artifactsPanelCodeTabDrawer, drawerOpen && styles.openDrawer)}
>
{loading && <ComponentTreeLoader />}
{loading || (
<FileTree
getIcon={getIcon}
getHref={getHref}
files={artifactFilesTree}
widgets={widgets}
payloadMap={payloadMap}
TreeNode={fileTreeNodeWithArtifactFiles(artifactFiles)}
selected={selected}
onTreeNodeSelected={(id: string, e) => {
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 });
}
}}
/>
)}
</DrawerUI>
);
}

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<any>) {
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 (
<div className={styles.artifactWidgets}>
<div className={classNames(styles.size, isSelected && styles.selected)}>{prettyBytes(artifactFile.size)}</div>
{!isBinary && artifactFile.size <= FILE_SIZE_THRESHOLD && (
<Icon className={styles.icon} of="open-tab" onClick={(e) => fileNodeClicked(files, 'new tab')(e, node)} />
)}
<Icon
className={styles.icon}
of="download"
onClick={(e) => {
fileNodeClicked(files, 'download')(e, node);
}}
/>
</div>
);
}
return null;
};
}

function fileTreeNodeWithArtifactFiles(artifactFiles: Array<ArtifactFile & { id: string }>) {
return function FileTreeNode(props: TreeNodeProps<any>) {
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 (
<Node
{...props}
className={classNames(styles.node, isLink && styles.link)}
onClick={onSelect && ((e) => onSelect(node.id, e))}
href={href}
isActive={node?.id === selected}
icon={icon}
widgets={widgets}
/>
);
}
return <FolderTreeNode {...props} />;
};
}
2 changes: 2 additions & 0 deletions components/ui/artifacts/artifacts-tree/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ArtifactsTree, ArtifactsTreeProps } from './artifacts-tree';
export const FILE_SIZE_THRESHOLD = 10000;
Original file line number Diff line number Diff line change
@@ -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<ArtifactFile>;
};

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<Artifact>,
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,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export {
mapToArtifacts,
ArtifactFile,
Artifact,
ComponentArtifactsGQLResponse,
getArtifactFileDetailsFromUrl,
} from './component-artifacts.model';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useComponentArtifacts, useComponentArtifactFileContent } from './use-component-artifacts';
Loading