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 { ProjectGraphList } from './interfaces';
|
||||
import { GraphPerfReport } from './machines/interfaces';
|
||||
import Dropdown from './ui-components/dropdown';
|
||||
|
||||
export interface DebuggerPanelProps {
|
||||
projectGraphs: ProjectGraphList[];
|
||||
@ -23,20 +24,22 @@ export const DebuggerPanel = memo(function ({
|
||||
<h4 className="dark:text-sidebar-title-dark mr-4 text-lg font-normal">
|
||||
Debugger
|
||||
</h4>
|
||||
<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"
|
||||
<Dropdown
|
||||
data-cy="project-select"
|
||||
onChange={(event) => projectGraphChange(event.target.value)}
|
||||
value={selectedProjectGraph}
|
||||
onChange={(event) => projectGraphChange(event.currentTarget.value)}
|
||||
>
|
||||
{projectGraphs.map((projectGraph) => {
|
||||
return (
|
||||
<option key={projectGraph.id} value={projectGraph.id}>
|
||||
<option
|
||||
key={projectGraph.id}
|
||||
value={projectGraph.id}
|
||||
selected={projectGraph.id === selectedProjectGraph}
|
||||
>
|
||||
{projectGraph.label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</Dropdown>
|
||||
<p className="text-sm">
|
||||
Last render took {lastPerfReport.renderTime}ms:{' '}
|
||||
<b className="text-medium font-mono">{lastPerfReport.numNodes} nodes</b>{' '}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import Tag from './ui-components/tag';
|
||||
|
||||
export interface EdgeNodeTooltipProps {
|
||||
type: 'static' | 'dynamic' | 'implicit';
|
||||
source: string;
|
||||
@ -14,7 +16,7 @@ export function EdgeNodeTooltip({
|
||||
return (
|
||||
<div>
|
||||
<h4 className={type !== 'implicit' ? 'mb-3' : ''}>
|
||||
<span className="tag">{type ?? 'unknown'}</span>
|
||||
<Tag>{type ?? 'unknown'}</Tag>
|
||||
{source} → {target}
|
||||
</h4>
|
||||
{type !== 'implicit' ? (
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
FlagIcon,
|
||||
MapPinIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import Tag from './ui-components/tag';
|
||||
|
||||
export interface ProjectNodeToolTipProps {
|
||||
type: 'app' | 'lib' | 'e2e';
|
||||
@ -49,7 +50,7 @@ export function ProjectNodeToolTip({
|
||||
return (
|
||||
<div>
|
||||
<h4>
|
||||
<span className="tag">{type}</span>
|
||||
<Tag>{type}</Tag>
|
||||
{id}
|
||||
</h4>
|
||||
{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 { useDebounce } from '../hooks/use-debounce';
|
||||
import { BackspaceIcon, FunnelIcon } from '@heroicons/react/24/outline';
|
||||
import DebouncedTextInput from '../ui-components/debounced-text-input';
|
||||
|
||||
export interface TextFilterPanelProps {
|
||||
textFilter: string;
|
||||
@ -18,72 +19,15 @@ export function TextFilterPanel({
|
||||
toggleIncludeLibsInPathChange,
|
||||
includePath,
|
||||
}: 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 (
|
||||
<div>
|
||||
<div className="mt-10 px-4">
|
||||
<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="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>
|
||||
<DebouncedTextInput
|
||||
resetTextFilter={resetTextFilter}
|
||||
updateTextFilter={updateTextFilter}
|
||||
initialText={''}
|
||||
placeholderText={'lib name, other lib name'}
|
||||
></DebouncedTextInput>
|
||||
</div>
|
||||
|
||||
<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 {
|
||||
border-bottom-color: $gray;
|
||||
}
|
||||
|
||||
&[data-placement^='left'] > .tippy-arrow::before {
|
||||
border-left-color: $gray;
|
||||
}
|
||||
|
||||
&[data-placement^='right'] > .tippy-arrow::before {
|
||||
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 {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~='nx'] p {
|
||||
margin: 0.375rem;
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~='nx'] button {
|
||||
background-color: rgba(249, 250, 251, 1);
|
||||
border-color: $gray;
|
||||
@ -124,6 +113,7 @@ canvas {
|
||||
border-color: rgb(71, 85, 105, 1);
|
||||
color: rgb(203, 213, 225, 1);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: rgb(51, 65, 85, 1);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user