-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #364 from Pixilib/metadata-tags
Metadata tags
- Loading branch information
Showing
33 changed files
with
966 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import { useMemo, useState } from "react"; | ||
import { Badge, Input, Spinner } from "../../ui"; | ||
import { instanceHeader, instanceTags } from "../../services/instances"; | ||
import { getInstancesOfSeries } from "../../services/orthanc"; | ||
import { Metadata, Tag as TagType } from "../../utils/types"; | ||
import { useCustomQuery } from "../../utils"; | ||
import TagSequence from "./metadata/TagSequence"; | ||
import TagItem from "./metadata/TagItem"; | ||
|
||
type TagsProps = { | ||
seriesId: string; | ||
}; | ||
|
||
const TagsTree = ({ seriesId }: TagsProps) => { | ||
const [instanceNumber, setInstanceNumber] = useState<number>(1); | ||
const [searchTerm, setSearchTerm] = useState<string>(""); | ||
|
||
const { data: instances } = useCustomQuery( | ||
["series", seriesId, "instances"], | ||
() => getInstancesOfSeries(seriesId) | ||
); | ||
|
||
const currentInstanceId = useMemo(() => { | ||
return instanceNumber != null && instances != null | ||
? instances[instanceNumber - 1]?.id | ||
: null; | ||
}, [instanceNumber, instances]); | ||
|
||
const { data: header } = useCustomQuery<Metadata>( | ||
["instances", currentInstanceId, "metadata"], | ||
() => instanceHeader(currentInstanceId), | ||
{ | ||
enabled: currentInstanceId !== null, | ||
} | ||
); | ||
|
||
const { data: tags } = useCustomQuery<Metadata>( | ||
["instances", currentInstanceId, "tags"], | ||
() => instanceTags(currentInstanceId), | ||
{ | ||
enabled: currentInstanceId !== null, | ||
} | ||
); | ||
|
||
const metadata = useMemo(() => { | ||
if (!header || !tags) return {}; | ||
return { | ||
...header, | ||
...tags, | ||
}; | ||
}, [header, tags]); | ||
|
||
const filteredMetadata = useMemo(() => { | ||
if (!searchTerm) return metadata; | ||
const lowerSearchTerm = searchTerm.toLowerCase(); | ||
|
||
return Object.entries(metadata).reduce((filtered, [tagAddress, tag]) => { | ||
const tagValue = String(tag.Value).toLowerCase(); | ||
const tagName = tag.Name?.toLowerCase(); | ||
|
||
if ( | ||
tagAddress.toLowerCase().includes(lowerSearchTerm) || | ||
tagName?.includes(lowerSearchTerm) || | ||
tagValue.includes(lowerSearchTerm) | ||
) { | ||
filtered[tagAddress] = tag; | ||
} | ||
|
||
return filtered; | ||
}, {} as Metadata); | ||
}, [metadata, searchTerm]); | ||
|
||
const getComponent = (tagAddress: string, tag: TagType) => { | ||
if (Array.isArray(tag.Value)) { | ||
return ( | ||
<li key={tagAddress} className="ml-4 list-none "> | ||
<TagSequence tag={tag}> | ||
{tag.Value.map((metadata, index) => ( | ||
<li key={`${tagAddress}-${index}`} > | ||
<ul className="list-disc"> | ||
{Object.entries(metadata).map(([addressTag, tag]) => | ||
getComponent(addressTag, tag) | ||
)} | ||
</ul> | ||
</li> | ||
))} | ||
</TagSequence> | ||
</li> | ||
); | ||
} else { | ||
return ( | ||
<li className="ml-4 list-none odd:bg-white even:bg-light-gray" key={tagAddress}> | ||
<TagItem address={tagAddress} tag={tag} /> | ||
</li> | ||
); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="space-y-3"> | ||
<Input | ||
label={"Instance Number " + (instanceNumber ?? 1)} | ||
type="range" | ||
min={1} | ||
max={instances?.length ?? 0} | ||
value={instanceNumber ?? 1} | ||
onChange={(event) => setInstanceNumber(Number(event.target?.value))} | ||
/> | ||
<Input | ||
label="Search Metadata" | ||
value={searchTerm} | ||
onChange={(event) => setSearchTerm(event.target?.value)} | ||
placeholder="Search by tag address, name, or value" | ||
/> | ||
<div className="overflow-auto min-h-[800px] max-h-[800px] p-3 pl-0"> | ||
<ul className="list-disc"> | ||
{Object.entries(filteredMetadata) | ||
.sort() | ||
.map(([tagAddress, tag]) => getComponent(tagAddress, tag))} | ||
</ul> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default TagsTree; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { Tag as TagType } from "../../../utils/types"; | ||
|
||
type TagProps = { | ||
address: string; | ||
tag: TagType; | ||
children?: React.ReactNode; | ||
}; | ||
const TagItem = ({ address, tag }: TagProps) => { | ||
return ( | ||
<div className="w-full flex justify-between"> | ||
<span>{address} - {tag.Name}</span> | ||
<span>{tag.Value as string}</span> | ||
</div> | ||
); | ||
}; | ||
|
||
export default TagItem; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import React, { useState } from "react"; | ||
import { Tag as TagType } from "../../../utils/types"; | ||
import { Button } from "../../../ui"; | ||
import { Colors } from "../../../utils"; | ||
|
||
type TagProps = { | ||
tag: TagType; | ||
children?: React.ReactNode; | ||
}; | ||
const TagSequence = ({ tag, children }: TagProps) => { | ||
const [isExpanded, setIsExpanded] = useState(false); | ||
|
||
return ( | ||
<ul className="list-none ml-4 m-3 space-y-3"> | ||
<Button | ||
color={Colors.primary} | ||
onClick={() => setIsExpanded((expanded) => !expanded)} | ||
> | ||
{tag.Name + (isExpanded ? "▲" : "▼")} | ||
</Button> | ||
{isExpanded && children} | ||
</ul> | ||
); | ||
}; | ||
|
||
export default TagSequence; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { fn } from '@storybook/test'; | ||
|
||
import { Button } from './Button'; | ||
|
||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export | ||
const meta = { | ||
title: 'Example/Button', | ||
component: Button, | ||
parameters: { | ||
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout | ||
layout: 'centered', | ||
}, | ||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs | ||
tags: ['autodocs'], | ||
// More on argTypes: https://storybook.js.org/docs/api/argtypes | ||
argTypes: { | ||
backgroundColor: { control: 'color' }, | ||
}, | ||
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args | ||
args: { onClick: fn() }, | ||
} satisfies Meta<typeof Button>; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof meta>; | ||
|
||
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | ||
export const Primary: Story = { | ||
args: { | ||
primary: true, | ||
label: 'Button', | ||
}, | ||
}; | ||
|
||
export const Secondary: Story = { | ||
args: { | ||
label: 'Button', | ||
}, | ||
}; | ||
|
||
export const Large: Story = { | ||
args: { | ||
size: 'large', | ||
label: 'Button', | ||
}, | ||
}; | ||
|
||
export const Small: Story = { | ||
args: { | ||
size: 'small', | ||
label: 'Button', | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import React from 'react'; | ||
|
||
import './button.css'; | ||
|
||
export interface ButtonProps { | ||
/** Is this the principal call to action on the page? */ | ||
primary?: boolean; | ||
/** What background color to use */ | ||
backgroundColor?: string; | ||
/** How large should the button be? */ | ||
size?: 'small' | 'medium' | 'large'; | ||
/** Button contents */ | ||
label: string; | ||
/** Optional click handler */ | ||
onClick?: () => void; | ||
} | ||
|
||
/** Primary UI component for user interaction */ | ||
export const Button = ({ | ||
primary = false, | ||
size = 'medium', | ||
backgroundColor, | ||
label, | ||
...props | ||
}: ButtonProps) => { | ||
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; | ||
return ( | ||
<button | ||
type="button" | ||
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')} | ||
style={{ backgroundColor }} | ||
{...props} | ||
> | ||
{label} | ||
</button> | ||
); | ||
}; |
Oops, something went wrong.