diff --git a/web/src/App.tsx b/web/src/App.tsx index cdd1e80..61725bd 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -14,6 +14,7 @@ import DataLoadingError from "./components/DataLoadingError"; import Filters from "./components/Filters"; import { Filter, FilterX } from "lucide-react"; import { WithTooltip } from "./components/ui/Tooltip"; +import { getDescription } from "./utils"; const SORT_ORDER = [ "monitored_images", @@ -32,6 +33,8 @@ function App() { const [showFilters, setShowFilters] = useState(false); const [filters, setFilters] = useState({ onlyInUse: false, + registries: [], + statuses: [], }); const [searchQuery, setSearchQuery] = useState(""); @@ -39,7 +42,7 @@ function App() { if (isError || !data) return ; const toggleShowFilters = () => { if (showFilters) { - setFilters({ onlyInUse: false }); + setFilters({ onlyInUse: false, registries: [], statuses: [] }); } setShowFilters(!showFilters); }; @@ -95,7 +98,13 @@ function App() { {showFilters && ( - + image.parts.registry)), + ]} + /> )}
    {Object.entries( @@ -116,6 +125,16 @@ function App() { .filter((image) => filters.onlyInUse ? !!image.in_use : true, ) + .filter((image) => + filters.registries.length == 0 + ? true + : filters.registries.includes(image.parts.registry), + ) + .filter((image) => + filters.statuses.length == 0 + ? true + : filters.statuses.includes(getDescription(image)), + ) .filter((image) => image.reference.includes(searchQuery)) .map((image) => ( diff --git a/web/src/components/Filters.tsx b/web/src/components/Filters.tsx index f1f4da6..bf20163 100644 --- a/web/src/components/Filters.tsx +++ b/web/src/components/Filters.tsx @@ -1,33 +1,81 @@ +import { useState } from "react"; import { theme } from "../theme"; import { Filters as FiltersType } from "../types"; import { Checkbox } from "./ui/Checkbox"; +import Select from "./ui/Select"; +import { Server } from "lucide-react"; interface Props { filters: FiltersType; setFilters: (filters: FiltersType) => void; + registries: string[]; } -export default function Filters({ filters, setFilters }: Props) { +const STATUSES = [ + "Major update", + "Minor update", + "Patch update", + "Digest update", + "Up to date", + "Unknown", +]; + +export default function Filters({ filters, setFilters, registries }: Props) { + const [selectedRegistries, setSelectedRegistries] = useState< + FiltersType["registries"] + >([]); + const [selectedStatuses, setSelectedStatuses] = useState< + FiltersType["statuses"] + >([]); + const handleSelectRegistries = (registries: string[]) => { + setSelectedRegistries(registries); + setFilters({ + ...filters, + registries, + }); + }; + const handleSelectStatuses = (statuses: string[]) => { + if (statuses.every((status) => STATUSES.includes(status))) { + setSelectedStatuses(statuses as FiltersType["statuses"]); + setFilters({ + ...filters, + statuses: statuses as FiltersType["statuses"], + }); + } + }; return ( -
    -
    - { - setFilters({ - ...filters, - onlyInUse: value === "indeterminate" ? false : value, - }); - }} - /> - -
    +
    +
    + { + setFilters({ + ...filters, + onlyInUse: value === "indeterminate" ? false : value, + }); + }} + /> +
    + +
    ); } diff --git a/web/src/components/Image.tsx b/web/src/components/Image.tsx index df0b2b9..7cccaf5 100644 --- a/web/src/components/Image.tsx +++ b/web/src/components/Image.tsx @@ -19,6 +19,7 @@ import { X, } from "lucide-react"; import Badge from "./Badge"; +import { getDescription } from "../utils"; const clickable_registries = [ "registry-1.docker.io", @@ -39,7 +40,7 @@ export default function Image({ data }: { data: Image }) { data.result.info?.type == "version" ? data.reference.split(":")[0] + ":" + data.result.info.new_tag : data.reference; - const info = getInfo(data)!; + const info = getInfo(data); let url: string | null = null; if (data.url) { url = data.url; @@ -182,54 +183,49 @@ export default function Image({ data }: { data: Image }) { ); } -function getInfo(data: Image): - | { - color: string; - icon: typeof HelpCircle; - description: string; - } - | undefined { - switch (data.result.has_update) { - case null: +function getInfo(data: Image): { + color: string; + icon: typeof HelpCircle; + description: string; +} { + const description = getDescription(data); + switch (description) { + case "Unknown": return { color: "text-gray-500", icon: HelpCircle, - description: "Unknown", + description, }; - case false: + case "Up to date": return { color: "text-green-500", icon: CircleCheck, - description: "Up to date", + description, + }; + + case "Major update": + return { + color: "text-red-500", + icon: CircleArrowUp, + description, + }; + case "Minor update": + return { + color: "text-yellow-500", + icon: CircleArrowUp, + description, + }; + case "Patch update": + return { + color: "text-blue-500", + icon: CircleArrowUp, + description, + }; + case "Digest update": + return { + color: "text-blue-500", + icon: CircleArrowUp, + description, }; - case true: - if (data.result.info?.type === "version") { - switch (data.result.info.version_update_type) { - case "major": - return { - color: "text-red-500", - icon: CircleArrowUp, - description: "Major update", - }; - case "minor": - return { - color: "text-yellow-500", - icon: CircleArrowUp, - description: "Minor update", - }; - case "patch": - return { - color: "text-blue-500", - icon: CircleArrowUp, - description: "Patch update", - }; - } - } else if (data.result.info?.type === "digest") { - return { - color: "text-blue-500", - icon: CircleArrowUp, - description: "Update available", - }; - } } } diff --git a/web/src/components/ui/Select.tsx b/web/src/components/ui/Select.tsx new file mode 100644 index 0000000..1fe2de7 --- /dev/null +++ b/web/src/components/ui/Select.tsx @@ -0,0 +1,84 @@ +import { + Listbox, + ListboxButton, + ListboxOptions, + ListboxOption, +} from "@headlessui/react"; +import { ChevronDown, Check } from "lucide-react"; +import { theme } from "../../theme"; +import { cn } from "../../utils"; +import { Server } from "lucide-react"; + +export default function Select({ + items, + Icon, + placeholder, + selectedItems, + setSelectedItems, +}: { + items: string[]; + Icon?: typeof Server; + placeholder: string; + selectedItems: string[]; + setSelectedItems: (items: string[]) => void; +}) { + return ( + +
    + + {Icon && ( + + )} + + {selectedItems.length == 0 + ? placeholder + : selectedItems.length == 1 + ? selectedItems[0] + : `${selectedItems[0]} +${(selectedItems.length - 1).toString()} more`} + + + + {items.map((item) => ( + + {item} + + + + ))} + +
    +
    + ); +} diff --git a/web/src/types.ts b/web/src/types.ts index c1086dc..b634d81 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -47,4 +47,13 @@ interface DigestInfo { export interface Filters { onlyInUse: boolean; + registries: string[]; + statuses: ( + | "Major update" + | "Minor update" + | "Patch update" + | "Digest update" + | "Up to date" + | "Unknown" + )[]; } diff --git a/web/src/utils.ts b/web/src/utils.ts index a5ef193..f728e91 100644 --- a/web/src/utils.ts +++ b/web/src/utils.ts @@ -1,6 +1,30 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; +import type { Image } from "./types"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export function getDescription(image: Image) { + switch (image.result.has_update) { + case null: + return "Unknown"; + case false: + return "Up to date"; + case true: + if (image.result.info?.type === "version") { + switch (image.result.info.version_update_type) { + case "major": + return "Major update"; + case "minor": + return "Minor update"; + case "patch": + return "Patch update"; + } + } else if (image.result.info?.type === "digest") { + return "Digest update"; + } + return "Unknown"; + } +} diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 557bc79..95749d5 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -32,14 +32,14 @@ export default { }, { pattern: /text-(gray|neutral)-600/, - variants: ["*", "dark", "hover", "placeholder"], + variants: ["*", "dark", "hover", "placeholder", "data-[placeholder]"], }, { pattern: /text-(gray|neutral)-400/, - variants: ["*:dark", "dark", "dark:hover", "placeholder:dark"], + variants: ["*:dark", "dark", "dark:hover", "placeholder:dark", "data-[placeholder]:dark"], }, { - pattern: /text-(gray|neutral)-700/, + pattern: /text-(gray|neutral)-(500|700)/, }, { pattern: /text-(gray|neutral)-950/,