Add repository column sorting
This commit is contained in:
parent
72c2f89cc8
commit
088502d12f
1 changed files with 114 additions and 11 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { Database, Plus, RotateCcw } from "lucide-react";
|
import { ArrowDown, ArrowUp, ArrowUpDown, Database, Plus, RotateCcw } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { RepositoryIcon } from "~/client/components/repository-icon";
|
import { RepositoryIcon } from "~/client/components/repository-icon";
|
||||||
|
|
@ -12,11 +12,38 @@ import { cn } from "~/client/lib/utils";
|
||||||
import { StatusDot } from "~/client/components/status-dot";
|
import { StatusDot } from "~/client/components/status-dot";
|
||||||
import { EmptyState } from "~/client/components/empty-state";
|
import { EmptyState } from "~/client/components/empty-state";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import type { RepositoryBackend } from "@zerobyte/core/restic";
|
||||||
|
|
||||||
|
type SortColumn = "name" | "backend" | "status" | "compression";
|
||||||
|
type SortDirection = "asc" | "desc";
|
||||||
|
type RepositoryRow = {
|
||||||
|
id: string;
|
||||||
|
shortId: string;
|
||||||
|
name: string;
|
||||||
|
type: RepositoryBackend;
|
||||||
|
status: string | null;
|
||||||
|
compressionMode?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSortValue = (column: SortColumn, repository: RepositoryRow) => {
|
||||||
|
switch (column) {
|
||||||
|
case "name":
|
||||||
|
return repository.name;
|
||||||
|
case "backend":
|
||||||
|
return repository.type;
|
||||||
|
case "status":
|
||||||
|
return repository.status || "";
|
||||||
|
case "compression":
|
||||||
|
return repository.compressionMode || "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export function RepositoriesPage() {
|
export function RepositoriesPage() {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [statusFilter, setStatusFilter] = useState("");
|
const [statusFilter, setStatusFilter] = useState("");
|
||||||
const [backendFilter, setBackendFilter] = useState("");
|
const [backendFilter, setBackendFilter] = useState("");
|
||||||
|
const [sortColumn, setSortColumn] = useState<SortColumn>("name");
|
||||||
|
const [sortDirection, setSortDirection] = useState<SortDirection>("asc");
|
||||||
|
|
||||||
const clearFilters = () => {
|
const clearFilters = () => {
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
|
|
@ -30,15 +57,47 @@ export function RepositoriesPage() {
|
||||||
...listRepositoriesOptions(),
|
...listRepositoriesOptions(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredRepositories = data.filter((repository) => {
|
const repositories = data as RepositoryRow[];
|
||||||
|
|
||||||
|
const toggleSort = (column: SortColumn) => {
|
||||||
|
if (sortColumn === column) {
|
||||||
|
setSortDirection((prev) => (prev === "asc" ? "desc" : "asc"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSortColumn(column);
|
||||||
|
setSortDirection("asc");
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSortIcon = (column: SortColumn) => {
|
||||||
|
if (sortColumn !== column) {
|
||||||
|
return <ArrowUpDown className="ml-2 h-3.5 w-3.5" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortDirection === "asc" ? (
|
||||||
|
<ArrowUp className="ml-2 h-3.5 w-3.5" />
|
||||||
|
) : (
|
||||||
|
<ArrowDown className="ml-2 h-3.5 w-3.5" />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredRepositories = repositories.filter((repository) => {
|
||||||
const matchesSearch = repository.name.toLowerCase().includes(searchQuery.toLowerCase());
|
const matchesSearch = repository.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
const matchesStatus = !statusFilter || repository.status === statusFilter;
|
const matchesStatus = !statusFilter || repository.status === statusFilter;
|
||||||
const matchesBackend = !backendFilter || repository.type === backendFilter;
|
const matchesBackend = !backendFilter || repository.type === backendFilter;
|
||||||
return matchesSearch && matchesStatus && matchesBackend;
|
return matchesSearch && matchesStatus && matchesBackend;
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasNoRepositories = data.length === 0;
|
const sortedFilteredRepositories = [...filteredRepositories].sort((a, b) => {
|
||||||
const hasNoFilteredRepositories = filteredRepositories.length === 0 && !hasNoRepositories;
|
const valueA = getSortValue(sortColumn, a).toLowerCase();
|
||||||
|
const valueB = getSortValue(sortColumn, b).toLowerCase();
|
||||||
|
const result = valueA.localeCompare(valueB);
|
||||||
|
|
||||||
|
return sortDirection === "asc" ? result : -result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasNoRepositories = repositories.length === 0;
|
||||||
|
const hasNoFilteredRepositories = sortedFilteredRepositories.length === 0 && !hasNoRepositories;
|
||||||
|
|
||||||
if (hasNoRepositories) {
|
if (hasNoRepositories) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -103,10 +162,54 @@ export function RepositoriesPage() {
|
||||||
<Table className="border-t">
|
<Table className="border-t">
|
||||||
<TableHeader className="bg-card-header">
|
<TableHeader className="bg-card-header">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-25 uppercase">Name</TableHead>
|
<TableHead className="w-25 uppercase">
|
||||||
<TableHead className="uppercase text-left">Backend</TableHead>
|
<Button
|
||||||
<TableHead className="uppercase hidden sm:table-cell">Compression</TableHead>
|
type="button"
|
||||||
<TableHead className="uppercase text-center">Status</TableHead>
|
variant="ghost"
|
||||||
|
onClick={() => toggleSort("name")}
|
||||||
|
className="h-auto! p-0! font-inherit hover:bg-transparent uppercase group/sort"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
<div className="lg:invisible lg:group-hover/sort:visible">{renderSortIcon("name")}</div>
|
||||||
|
</Button>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="uppercase text-left">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => toggleSort("backend")}
|
||||||
|
className="h-auto! p-0! font-inherit hover:bg-transparent uppercase group/sort"
|
||||||
|
>
|
||||||
|
Backend
|
||||||
|
<div className="lg:invisible lg:group-hover/sort:visible">{renderSortIcon("backend")}</div>
|
||||||
|
</Button>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="uppercase hidden sm:table-cell">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => toggleSort("compression")}
|
||||||
|
className="h-auto! p-0! font-inherit hover:bg-transparent uppercase group/sort"
|
||||||
|
>
|
||||||
|
Compresison
|
||||||
|
<div className="lg:invisible lg:group-hover/sort:visible">{renderSortIcon("compression")}</div>
|
||||||
|
</Button>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="uppercase text-center">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => toggleSort("status")}
|
||||||
|
className="h-auto! w-full! px-0! py-0! font-inherit hover:bg-transparent uppercase group/sort relative"
|
||||||
|
>
|
||||||
|
<span className="relative flex w-full items-center justify-center">
|
||||||
|
Status
|
||||||
|
<span className="absolute -right-6 top-1/2 -translate-y-1/2 lg:invisible lg:group-hover/sort:visible">
|
||||||
|
{renderSortIcon("status")}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|
@ -121,7 +224,7 @@ export function RepositoriesPage() {
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{filteredRepositories.map((repository) => (
|
{sortedFilteredRepositories.map((repository) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={repository.id}
|
key={repository.id}
|
||||||
className="hover:bg-accent/50 hover:cursor-pointer h-12"
|
className="hover:bg-accent/50 hover:cursor-pointer h-12"
|
||||||
|
|
@ -163,8 +266,8 @@ export function RepositoriesPage() {
|
||||||
"No repositories match filters."
|
"No repositories match filters."
|
||||||
) : (
|
) : (
|
||||||
<span>
|
<span>
|
||||||
<span className="text-strong-accent">{filteredRepositories.length}</span> repositor
|
<span className="text-strong-accent">{sortedFilteredRepositories.length}</span> repositor
|
||||||
{filteredRepositories.length === 1 ? "y" : "ies"}
|
{sortedFilteredRepositories.length === 1 ? "y" : "ies"}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue