chore(graph): add components to support task graph (#12472)
This commit is contained in:
parent
621baaa709
commit
080a8961d8
@ -1,6 +1,7 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { ProjectGraphList } from './interfaces';
|
import { ProjectGraphList } from './interfaces';
|
||||||
import { GraphPerfReport } from './machines/interfaces';
|
import { GraphPerfReport } from './machines/interfaces';
|
||||||
|
import Dropdown from './ui-components/dropdown';
|
||||||
|
|
||||||
export interface DebuggerPanelProps {
|
export interface DebuggerPanelProps {
|
||||||
projectGraphs: ProjectGraphList[];
|
projectGraphs: ProjectGraphList[];
|
||||||
@ -23,20 +24,22 @@ export const DebuggerPanel = memo(function ({
|
|||||||
<h4 className="dark:text-sidebar-title-dark mr-4 text-lg font-normal">
|
<h4 className="dark:text-sidebar-title-dark mr-4 text-lg font-normal">
|
||||||
Debugger
|
Debugger
|
||||||
</h4>
|
</h4>
|
||||||
<select
|
<Dropdown
|
||||||
className="flex w-auto items-center rounded-md rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
|
||||||
data-cy="project-select"
|
data-cy="project-select"
|
||||||
onChange={(event) => projectGraphChange(event.target.value)}
|
onChange={(event) => projectGraphChange(event.currentTarget.value)}
|
||||||
value={selectedProjectGraph}
|
|
||||||
>
|
>
|
||||||
{projectGraphs.map((projectGraph) => {
|
{projectGraphs.map((projectGraph) => {
|
||||||
return (
|
return (
|
||||||
<option key={projectGraph.id} value={projectGraph.id}>
|
<option
|
||||||
|
key={projectGraph.id}
|
||||||
|
value={projectGraph.id}
|
||||||
|
selected={projectGraph.id === selectedProjectGraph}
|
||||||
|
>
|
||||||
{projectGraph.label}
|
{projectGraph.label}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</select>
|
</Dropdown>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
Last render took {lastPerfReport.renderTime}ms:{' '}
|
Last render took {lastPerfReport.renderTime}ms:{' '}
|
||||||
<b className="text-medium font-mono">{lastPerfReport.numNodes} nodes</b>{' '}
|
<b className="text-medium font-mono">{lastPerfReport.numNodes} nodes</b>{' '}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import Tag from './ui-components/tag';
|
||||||
|
|
||||||
export interface EdgeNodeTooltipProps {
|
export interface EdgeNodeTooltipProps {
|
||||||
type: 'static' | 'dynamic' | 'implicit';
|
type: 'static' | 'dynamic' | 'implicit';
|
||||||
source: string;
|
source: string;
|
||||||
@ -14,7 +16,7 @@ export function EdgeNodeTooltip({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h4 className={type !== 'implicit' ? 'mb-3' : ''}>
|
<h4 className={type !== 'implicit' ? 'mb-3' : ''}>
|
||||||
<span className="tag">{type ?? 'unknown'}</span>
|
<Tag>{type ?? 'unknown'}</Tag>
|
||||||
{source} → {target}
|
{source} → {target}
|
||||||
</h4>
|
</h4>
|
||||||
{type !== 'implicit' ? (
|
{type !== 'implicit' ? (
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
FlagIcon,
|
FlagIcon,
|
||||||
MapPinIcon,
|
MapPinIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
|
import Tag from './ui-components/tag';
|
||||||
|
|
||||||
export interface ProjectNodeToolTipProps {
|
export interface ProjectNodeToolTipProps {
|
||||||
type: 'app' | 'lib' | 'e2e';
|
type: 'app' | 'lib' | 'e2e';
|
||||||
@ -49,7 +50,7 @@ export function ProjectNodeToolTip({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h4>
|
<h4>
|
||||||
<span className="tag">{type}</span>
|
<Tag>{type}</Tag>
|
||||||
{id}
|
{id}
|
||||||
</h4>
|
</h4>
|
||||||
{tags.length > 0 ? (
|
{tags.length > 0 ? (
|
||||||
|
|||||||
67
graph/client/src/app/sidebar/task-list.stories.tsx
Normal file
67
graph/client/src/app/sidebar/task-list.stories.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
|
import { TaskList, TaskListProps } from './task-list';
|
||||||
|
|
||||||
|
const Story: ComponentMeta<typeof TaskList> = {
|
||||||
|
component: TaskList,
|
||||||
|
title: 'TaskList',
|
||||||
|
argTypes: {
|
||||||
|
selectTask: {
|
||||||
|
action: 'selectTask',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default Story;
|
||||||
|
|
||||||
|
const Template: ComponentStory<typeof TaskList> = (args) => (
|
||||||
|
<TaskList {...args} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Primary = Template.bind({});
|
||||||
|
const args: Partial<TaskListProps> = {
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'app1',
|
||||||
|
type: 'app',
|
||||||
|
data: {
|
||||||
|
root: 'apps/app1',
|
||||||
|
targets: {
|
||||||
|
build: {
|
||||||
|
configurations: { production: {}, development: {} },
|
||||||
|
defaultConfiguration: 'production',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nested-app',
|
||||||
|
type: 'app',
|
||||||
|
data: {
|
||||||
|
root: 'apps/nested/app',
|
||||||
|
targets: { build: { configurations: { production: {} } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'app1-e2e',
|
||||||
|
type: 'e2e',
|
||||||
|
data: {
|
||||||
|
root: 'apps/app1-e2e',
|
||||||
|
targets: { e2e: { configurations: { production: {} } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lib1',
|
||||||
|
type: 'lib',
|
||||||
|
data: {
|
||||||
|
root: 'libs/lib1',
|
||||||
|
targets: { lint: { configurations: { production: {} } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
workspaceLayout: {
|
||||||
|
appsDir: 'apps',
|
||||||
|
libsDir: 'libs',
|
||||||
|
},
|
||||||
|
selectedTask: 'app1:build:production',
|
||||||
|
};
|
||||||
|
Primary.args = args;
|
||||||
277
graph/client/src/app/sidebar/task-list.tsx
Normal file
277
graph/client/src/app/sidebar/task-list.tsx
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
import { DocumentMagnifyingGlassIcon } from '@heroicons/react/24/solid';
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import type { ProjectGraphNode, Task } from '@nrwl/devkit';
|
||||||
|
import { parseParentDirectoriesFromFilePath } from '../util';
|
||||||
|
import { WorkspaceLayout } from '../interfaces';
|
||||||
|
import Tag from '../ui-components/tag';
|
||||||
|
|
||||||
|
function getProjectsByType(type: string, projects: ProjectGraphNode[]) {
|
||||||
|
return projects
|
||||||
|
.filter((project) => project.type === type)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarTarget {
|
||||||
|
targetName: string;
|
||||||
|
configurations: Array<{
|
||||||
|
name: string;
|
||||||
|
isSelected: boolean;
|
||||||
|
default: boolean;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarProjectWithTargets {
|
||||||
|
projectGraphNode: ProjectGraphNode;
|
||||||
|
targets: SidebarTarget[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupProjectsByDirectory(
|
||||||
|
projects: ProjectGraphNode[],
|
||||||
|
workspaceLayout: { appsDir: string; libsDir: string }
|
||||||
|
): Record<string, ProjectGraphNode[]> {
|
||||||
|
let groups: Record<string, ProjectGraphNode[]> = {};
|
||||||
|
|
||||||
|
projects.forEach((project) => {
|
||||||
|
const workspaceRoot =
|
||||||
|
project.type === 'app' || project.type === 'e2e'
|
||||||
|
? workspaceLayout.appsDir
|
||||||
|
: workspaceLayout.libsDir;
|
||||||
|
const directories = parseParentDirectoriesFromFilePath(
|
||||||
|
project.data.root,
|
||||||
|
workspaceRoot
|
||||||
|
);
|
||||||
|
|
||||||
|
const directory = directories.join('/');
|
||||||
|
|
||||||
|
if (!groups.hasOwnProperty(directory)) {
|
||||||
|
groups[directory] = [];
|
||||||
|
}
|
||||||
|
groups[directory].push(project);
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectListItem({
|
||||||
|
project,
|
||||||
|
selectTask,
|
||||||
|
}: {
|
||||||
|
project: SidebarProjectWithTargets;
|
||||||
|
selectTask: (
|
||||||
|
projectName: string,
|
||||||
|
targetName: string,
|
||||||
|
configurationName: string
|
||||||
|
) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<li className="relative block cursor-default select-none py-1 pl-2 pr-6 text-xs text-slate-600 dark:text-slate-400">
|
||||||
|
<strong>{project.projectGraphNode.name}</strong>
|
||||||
|
<br />
|
||||||
|
{project.targets.map((target) => (
|
||||||
|
<>
|
||||||
|
<strong>{target.targetName}</strong>
|
||||||
|
<br />
|
||||||
|
{target.configurations.map((configuration) => (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
data-cy={`focus-button-${configuration.name}`}
|
||||||
|
type="button"
|
||||||
|
className="mr-1 flex items-center rounded-md border-slate-300 bg-white p-1 font-medium text-slate-500 shadow-sm ring-1 ring-slate-200 transition hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-400 dark:ring-slate-600 hover:dark:bg-slate-700"
|
||||||
|
title="Focus on this library"
|
||||||
|
onClick={() =>
|
||||||
|
selectTask(
|
||||||
|
project.projectGraphNode.name,
|
||||||
|
target.targetName,
|
||||||
|
configuration.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DocumentMagnifyingGlassIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<label
|
||||||
|
className="ml-2 block w-full cursor-pointer truncate rounded-md p-2 font-mono font-normal transition hover:bg-slate-50 hover:dark:bg-slate-700"
|
||||||
|
data-project={configuration.name}
|
||||||
|
title={configuration.name}
|
||||||
|
data-active={configuration.isSelected.toString()}
|
||||||
|
>
|
||||||
|
{configuration.name}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{configuration.default ? <Tag>default</Tag> : null}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubProjectList({
|
||||||
|
headerText = '',
|
||||||
|
projects,
|
||||||
|
selectTask,
|
||||||
|
}: {
|
||||||
|
headerText: string;
|
||||||
|
projects: SidebarProjectWithTargets[];
|
||||||
|
selectTask: (
|
||||||
|
projectName: string,
|
||||||
|
targetName: string,
|
||||||
|
configurationName: string
|
||||||
|
) => void;
|
||||||
|
}) {
|
||||||
|
let sortedProjects = [...projects];
|
||||||
|
sortedProjects.sort((a, b) => {
|
||||||
|
return a.projectGraphNode.name.localeCompare(b.projectGraphNode.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{headerText !== '' ? (
|
||||||
|
<h3 className="mt-4 cursor-text py-2 text-sm font-semibold uppercase tracking-wide text-slate-800 dark:text-slate-200 lg:text-xs">
|
||||||
|
{headerText}
|
||||||
|
</h3>
|
||||||
|
) : null}
|
||||||
|
<ul className="mt-2 -ml-3">
|
||||||
|
{sortedProjects.map((project) => {
|
||||||
|
return (
|
||||||
|
<ProjectListItem
|
||||||
|
key={project.projectGraphNode.name}
|
||||||
|
project={project}
|
||||||
|
selectTask={selectTask}
|
||||||
|
></ProjectListItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapToSidebarProjectWithTasks(
|
||||||
|
project: ProjectGraphNode,
|
||||||
|
selectedTask: string
|
||||||
|
): SidebarProjectWithTargets {
|
||||||
|
const targets: SidebarTarget[] = [];
|
||||||
|
|
||||||
|
for (const targetName in project.data?.targets) {
|
||||||
|
const target: SidebarTarget = {
|
||||||
|
targetName,
|
||||||
|
configurations: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const configuration in project.data?.targets?.[targetName]
|
||||||
|
?.configurations) {
|
||||||
|
target.configurations.push({
|
||||||
|
name: configuration,
|
||||||
|
isSelected: configuration === selectedTask,
|
||||||
|
default:
|
||||||
|
configuration ===
|
||||||
|
project.data?.targets?.[targetName]?.defaultConfiguration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
targets.push(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectGraphNode: project,
|
||||||
|
targets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskListProps {
|
||||||
|
projects: ProjectGraphNode[];
|
||||||
|
taskGraphs: Record<string, Record<string, Record<string, Task>>>;
|
||||||
|
workspaceLayout: WorkspaceLayout;
|
||||||
|
selectedTask: string;
|
||||||
|
selectTask: (
|
||||||
|
projectName: string,
|
||||||
|
targetName: string,
|
||||||
|
configurationName: string
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TaskList({
|
||||||
|
projects,
|
||||||
|
workspaceLayout,
|
||||||
|
selectedTask,
|
||||||
|
selectTask,
|
||||||
|
}: TaskListProps) {
|
||||||
|
const appProjects = getProjectsByType('app', projects);
|
||||||
|
const libProjects = getProjectsByType('lib', projects);
|
||||||
|
const e2eProjects = getProjectsByType('e2e', projects);
|
||||||
|
|
||||||
|
const appDirectoryGroups = groupProjectsByDirectory(
|
||||||
|
appProjects,
|
||||||
|
workspaceLayout
|
||||||
|
);
|
||||||
|
const libDirectoryGroups = groupProjectsByDirectory(
|
||||||
|
libProjects,
|
||||||
|
workspaceLayout
|
||||||
|
);
|
||||||
|
const e2eDirectoryGroups = groupProjectsByDirectory(
|
||||||
|
e2eProjects,
|
||||||
|
workspaceLayout
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedAppDirectories = Object.keys(appDirectoryGroups).sort();
|
||||||
|
const sortedLibDirectories = Object.keys(libDirectoryGroups).sort();
|
||||||
|
const sortedE2EDirectories = Object.keys(e2eDirectoryGroups).sort();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="project-lists" className="mt-8 border-t border-slate-400/10 px-4">
|
||||||
|
<h2 className="mt-8 border-b border-solid border-slate-200/10 text-lg font-light text-slate-400 dark:text-slate-500">
|
||||||
|
app projects
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{sortedAppDirectories.map((directoryName) => {
|
||||||
|
return (
|
||||||
|
<SubProjectList
|
||||||
|
key={'app-' + directoryName}
|
||||||
|
headerText={directoryName}
|
||||||
|
projects={appDirectoryGroups[directoryName].map((project) =>
|
||||||
|
mapToSidebarProjectWithTasks(project, selectedTask)
|
||||||
|
)}
|
||||||
|
selectTask={selectTask}
|
||||||
|
></SubProjectList>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<h2 className="mt-8 border-b border-solid border-slate-200/10 text-lg font-light">
|
||||||
|
e2e projects
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{sortedE2EDirectories.map((directoryName) => {
|
||||||
|
return (
|
||||||
|
<SubProjectList
|
||||||
|
key={'e2e-' + directoryName}
|
||||||
|
headerText={directoryName}
|
||||||
|
projects={e2eDirectoryGroups[directoryName].map((project) =>
|
||||||
|
mapToSidebarProjectWithTasks(project, selectedTask)
|
||||||
|
)}
|
||||||
|
selectTask={selectTask}
|
||||||
|
></SubProjectList>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<h2 className="mt-8 border-b border-solid border-slate-200/10 text-lg font-light">
|
||||||
|
lib projects
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{sortedLibDirectories.map((directoryName) => {
|
||||||
|
return (
|
||||||
|
<SubProjectList
|
||||||
|
key={'lib-' + directoryName}
|
||||||
|
headerText={directoryName}
|
||||||
|
projects={libDirectoryGroups[directoryName].map((project) =>
|
||||||
|
mapToSidebarProjectWithTasks(project, selectedTask)
|
||||||
|
)}
|
||||||
|
selectTask={selectTask}
|
||||||
|
></SubProjectList>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TaskList;
|
||||||
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import type { KeyboardEvent } from 'react';
|
import type { KeyboardEvent } from 'react';
|
||||||
import { useDebounce } from '../hooks/use-debounce';
|
import { useDebounce } from '../hooks/use-debounce';
|
||||||
import { BackspaceIcon, FunnelIcon } from '@heroicons/react/24/outline';
|
import { BackspaceIcon, FunnelIcon } from '@heroicons/react/24/outline';
|
||||||
|
import DebouncedTextInput from '../ui-components/debounced-text-input';
|
||||||
|
|
||||||
export interface TextFilterPanelProps {
|
export interface TextFilterPanelProps {
|
||||||
textFilter: string;
|
textFilter: string;
|
||||||
@ -18,72 +19,15 @@ export function TextFilterPanel({
|
|||||||
toggleIncludeLibsInPathChange,
|
toggleIncludeLibsInPathChange,
|
||||||
includePath,
|
includePath,
|
||||||
}: TextFilterPanelProps) {
|
}: TextFilterPanelProps) {
|
||||||
const [currentTextFilter, setCurrentTextFilter] = useState('');
|
|
||||||
|
|
||||||
const [debouncedValue, setDebouncedValue] = useDebounce(
|
|
||||||
currentTextFilter,
|
|
||||||
500
|
|
||||||
);
|
|
||||||
|
|
||||||
function onTextFilterKeyUp(event: KeyboardEvent<HTMLInputElement>) {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
updateTextFilter(event.currentTarget.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTextInputChange(change: string) {
|
|
||||||
if (change === '') {
|
|
||||||
setCurrentTextFilter('');
|
|
||||||
setDebouncedValue('');
|
|
||||||
|
|
||||||
resetTextFilter();
|
|
||||||
} else {
|
|
||||||
setCurrentTextFilter(change);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetClicked() {
|
|
||||||
setCurrentTextFilter('');
|
|
||||||
setDebouncedValue('');
|
|
||||||
|
|
||||||
resetTextFilter();
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateTextFilter(debouncedValue);
|
|
||||||
}, [debouncedValue, updateTextFilter]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mt-10 px-4">
|
<div className="mt-10 px-4">
|
||||||
<form
|
<DebouncedTextInput
|
||||||
className="group relative flex rounded-md shadow-sm"
|
resetTextFilter={resetTextFilter}
|
||||||
onSubmit={(event) => event.preventDefault()}
|
updateTextFilter={updateTextFilter}
|
||||||
>
|
initialText={''}
|
||||||
<span className="inline-flex items-center rounded-l-md border border-r-0 border-slate-300 bg-slate-50 p-2 dark:border-slate-900 dark:bg-slate-800">
|
placeholderText={'lib name, other lib name'}
|
||||||
<FunnelIcon className="h-4 w-4"></FunnelIcon>
|
></DebouncedTextInput>
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className={`block w-full flex-1 rounded-none rounded-r-md border border-slate-300 bg-white p-1.5 font-light text-slate-400 placeholder:font-light placeholder:text-slate-400 dark:border-slate-900 dark:bg-slate-800 dark:text-white dark:hover:bg-slate-700`}
|
|
||||||
placeholder="lib name, other lib name"
|
|
||||||
data-cy="textFilterInput"
|
|
||||||
name="filter"
|
|
||||||
value={currentTextFilter}
|
|
||||||
onKeyUp={onTextFilterKeyUp}
|
|
||||||
onChange={(event) => onTextInputChange(event.currentTarget.value)}
|
|
||||||
></input>
|
|
||||||
{currentTextFilter.length > 0 ? (
|
|
||||||
<button
|
|
||||||
data-cy="textFilterReset"
|
|
||||||
type="reset"
|
|
||||||
onClick={resetClicked}
|
|
||||||
className="absolute top-1 right-1 inline-block rounded-md bg-slate-50 p-1 text-slate-400 dark:bg-slate-800"
|
|
||||||
>
|
|
||||||
<BackspaceIcon className="h-5 w-5"></BackspaceIcon>
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 px-4">
|
<div className="mt-4 px-4">
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
|
import { DebouncedTextInput } from './debounced-text-input';
|
||||||
|
|
||||||
|
const Story: ComponentMeta<typeof DebouncedTextInput> = {
|
||||||
|
component: DebouncedTextInput,
|
||||||
|
title: 'Shared/DebouncedTextInput',
|
||||||
|
argTypes: {
|
||||||
|
resetTextFilter: {
|
||||||
|
action: 'resetTextFilter',
|
||||||
|
},
|
||||||
|
updateTextFilter: {
|
||||||
|
action: 'updateTextFilter',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default Story;
|
||||||
|
|
||||||
|
const Template: ComponentStory<typeof DebouncedTextInput> = (args) => (
|
||||||
|
<DebouncedTextInput {...args} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Primary = Template.bind({});
|
||||||
|
Primary.args = {
|
||||||
|
currentText: '',
|
||||||
|
placeholderText: '',
|
||||||
|
};
|
||||||
85
graph/client/src/app/ui-components/debounced-text-input.tsx
Normal file
85
graph/client/src/app/ui-components/debounced-text-input.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { KeyboardEvent, useEffect, useState } from 'react';
|
||||||
|
import { useDebounce } from '../hooks/use-debounce';
|
||||||
|
import { BackspaceIcon, FunnelIcon } from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
|
export interface DebouncedTextInputProps {
|
||||||
|
initialText: string;
|
||||||
|
placeholderText: string;
|
||||||
|
resetTextFilter: () => void;
|
||||||
|
updateTextFilter: (textFilter: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DebouncedTextInput({
|
||||||
|
initialText,
|
||||||
|
placeholderText,
|
||||||
|
resetTextFilter,
|
||||||
|
updateTextFilter,
|
||||||
|
}: DebouncedTextInputProps) {
|
||||||
|
const [currentTextFilter, setCurrentTextFilter] = useState(initialText ?? '');
|
||||||
|
|
||||||
|
const [debouncedValue, setDebouncedValue] = useDebounce(
|
||||||
|
currentTextFilter,
|
||||||
|
500
|
||||||
|
);
|
||||||
|
|
||||||
|
function onTextFilterKeyUp(event: KeyboardEvent<HTMLInputElement>) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
updateTextFilter(event.currentTarget.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTextInputChange(change: string) {
|
||||||
|
if (change === '') {
|
||||||
|
setCurrentTextFilter('');
|
||||||
|
setDebouncedValue('');
|
||||||
|
|
||||||
|
resetTextFilter();
|
||||||
|
} else {
|
||||||
|
setCurrentTextFilter(change);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetClicked() {
|
||||||
|
setCurrentTextFilter('');
|
||||||
|
setDebouncedValue('');
|
||||||
|
|
||||||
|
resetTextFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateTextFilter(debouncedValue);
|
||||||
|
}, [debouncedValue, updateTextFilter]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className="group relative flex rounded-md shadow-sm"
|
||||||
|
onSubmit={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
<span className="inline-flex items-center rounded-l-md border border-r-0 border-slate-300 bg-slate-50 p-2 dark:border-slate-900 dark:bg-slate-800">
|
||||||
|
<FunnelIcon className="h-4 w-4"></FunnelIcon>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={`block w-full flex-1 rounded-none rounded-r-md border border-slate-300 bg-white p-1.5 font-light text-slate-400 placeholder:font-light placeholder:text-slate-400 dark:border-slate-900 dark:bg-slate-800 dark:text-white dark:hover:bg-slate-700`}
|
||||||
|
placeholder={placeholderText}
|
||||||
|
data-cy="textFilterInput"
|
||||||
|
name="filter"
|
||||||
|
value={currentTextFilter}
|
||||||
|
onKeyUp={onTextFilterKeyUp}
|
||||||
|
onChange={(event) => onTextInputChange(event.currentTarget.value)}
|
||||||
|
></input>
|
||||||
|
{currentTextFilter.length > 0 ? (
|
||||||
|
<button
|
||||||
|
data-cy="textFilterReset"
|
||||||
|
type="reset"
|
||||||
|
onClick={resetClicked}
|
||||||
|
className="absolute top-1 right-1 inline-block rounded-md bg-slate-50 p-1 text-slate-400 dark:bg-slate-800"
|
||||||
|
>
|
||||||
|
<BackspaceIcon className="h-5 w-5"></BackspaceIcon>
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DebouncedTextInput;
|
||||||
20
graph/client/src/app/ui-components/dropdown.stories.tsx
Normal file
20
graph/client/src/app/ui-components/dropdown.stories.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
|
import { Dropdown } from './dropdown';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
component: Dropdown,
|
||||||
|
title: 'Shared/Dropdown',
|
||||||
|
argTypes: {
|
||||||
|
onChange: { action: 'onChange' },
|
||||||
|
},
|
||||||
|
} as ComponentMeta<typeof Dropdown>;
|
||||||
|
|
||||||
|
const Template: ComponentStory<typeof Dropdown> = (args) => (
|
||||||
|
<Dropdown {...args}>
|
||||||
|
<option value="Option 1">Option 1</option>
|
||||||
|
<option value="Option 2">Option 2</option>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Primary = Template.bind({});
|
||||||
|
Primary.args = {};
|
||||||
20
graph/client/src/app/ui-components/dropdown.tsx
Normal file
20
graph/client/src/app/ui-components/dropdown.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/* eslint-disable-next-line */
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export type DropdownProps = {
|
||||||
|
children: ReactNode[];
|
||||||
|
} & React.HTMLAttributes<HTMLSelectElement>;
|
||||||
|
|
||||||
|
export function Dropdown(props: DropdownProps) {
|
||||||
|
const { className, children, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
className="flex w-auto items-center rounded-md rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dropdown;
|
||||||
15
graph/client/src/app/ui-components/tag.stories.tsx
Normal file
15
graph/client/src/app/ui-components/tag.stories.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
|
import { Tag } from './tag';
|
||||||
|
|
||||||
|
const Story: ComponentMeta<typeof Tag> = {
|
||||||
|
component: Tag,
|
||||||
|
title: 'Shared/Tag',
|
||||||
|
};
|
||||||
|
export default Story;
|
||||||
|
|
||||||
|
const Template: ComponentStory<typeof Tag> = (args) => <Tag>{args.text}</Tag>;
|
||||||
|
|
||||||
|
export const Primary = Template.bind({});
|
||||||
|
Primary.args = {
|
||||||
|
text: 'tag',
|
||||||
|
};
|
||||||
12
graph/client/src/app/ui-components/tag.tsx
Normal file
12
graph/client/src/app/ui-components/tag.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/* eslint-disable-next-line */
|
||||||
|
export interface TagProps {}
|
||||||
|
|
||||||
|
export function Tag(props) {
|
||||||
|
return (
|
||||||
|
<span className="font- mr-3 inline-block rounded-md bg-slate-300 p-2 font-sans text-xs font-semibold uppercase leading-4 tracking-wide text-slate-700">
|
||||||
|
{props.children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Tag;
|
||||||
@ -71,35 +71,24 @@ canvas {
|
|||||||
&[data-placement^='bottom'] > .tippy-arrow::before {
|
&[data-placement^='bottom'] > .tippy-arrow::before {
|
||||||
border-bottom-color: $gray;
|
border-bottom-color: $gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-placement^='left'] > .tippy-arrow::before {
|
&[data-placement^='left'] > .tippy-arrow::before {
|
||||||
border-left-color: $gray;
|
border-left-color: $gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-placement^='right'] > .tippy-arrow::before {
|
&[data-placement^='right'] > .tippy-arrow::before {
|
||||||
border-right-color: $gray;
|
border-right-color: $gray;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
|
||||||
padding: 0.5rem;
|
|
||||||
font-family: system-ui;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1rem;
|
|
||||||
display: inline-block;
|
|
||||||
background-color: hsla(213, 27%, 84%, 1);
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: hsla(215, 25%, 27%, 1);
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.025em;
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tippy-box[data-theme~='nx'] h4 {
|
.tippy-box[data-theme~='nx'] h4 {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-box[data-theme~='nx'] p {
|
.tippy-box[data-theme~='nx'] p {
|
||||||
margin: 0.375rem;
|
margin: 0.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-box[data-theme~='nx'] button {
|
.tippy-box[data-theme~='nx'] button {
|
||||||
background-color: rgba(249, 250, 251, 1);
|
background-color: rgba(249, 250, 251, 1);
|
||||||
border-color: $gray;
|
border-color: $gray;
|
||||||
@ -124,6 +113,7 @@ canvas {
|
|||||||
border-color: rgb(71, 85, 105, 1);
|
border-color: rgb(71, 85, 105, 1);
|
||||||
color: rgb(203, 213, 225, 1);
|
color: rgb(203, 213, 225, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: rgb(51, 65, 85, 1);
|
background-color: rgb(51, 65, 85, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user