refactor(web): unify namespace UI and modernize tables, tabs and dialogs

- Add a shared NamespaceTabs component and use underline tabs across the
  namespace summary, repository, member, daemon task and webhook pages.
- Redesign the pagination footer with theme tokens and a page indicator,
  and give previous/next proper pointer and disabled states.
- Align the daemon task and webhook table headers with the namespace list
  style, drop bold text, and use a muted header color.
- Convert the namespace member list to the shadcn Table component and
  match the search/create toolbar height.
- Modernize the webhook create dialog (Input, Label, Switch, two-column
  events) and unify button and input styles.
- Make the namespace summary markdown editor fill the available height.
- Stay silent on 404/empty responses for the namespace summary, member
  and webhook requests instead of showing an error toast.
- Add standard `appearance` declarations and update deprecated Tailwind
  classes to clear editor warnings.

Verification: bunx tsc --noEmit; bunx oxlint; bun run build (web)
This commit is contained in:
Tosone committed 2026-10-02 23:34:29 +08:00
1 parent eca764eda4
commit dbd9601947
12 files changed
+404 -575

No files matched your search

@@ -0,0 +1,69 @@
/**
* Copyright 2023 sigma
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Link } from "react-router-dom";
import { useTranslation } from "../../i18n/useTranslation";
import { MessageKey } from "../../i18n/types";
import { cn } from "@/lib/utils";
export type NamespaceSection = "summary" | "repositories" | "members" | "daemon-tasks" | "webhooks";
const sections: { key: NamespaceSection; path: string; labelKey: MessageKey }[] = [
{ key: "summary", path: "namespace-summary", labelKey: "common.summary" },
{ key: "repositories", path: "repositories", labelKey: "common.repositoryList" },
{ key: "members", path: "members", labelKey: "common.members" },
{ key: "daemon-tasks", path: "daemon-tasks", labelKey: "common.daemonTask" },
{ key: "webhooks", path: "webhooks", labelKey: "common.webhook" },
];
export default function NamespaceTabs({ namespace, namespaceId, active }: {
namespace?: string;
namespaceId?: string;
active: NamespaceSection;
}) {
const { t } = useTranslation();
const query = namespaceId ? `?namespace_id=${namespaceId}` : "";
return (
<nav className="flex h-full items-stretch gap-1 overflow-x-auto" aria-label={t("common.namespaces")}>
{sections.map(section => {
const isActive = section.key === active;
const className = cn(
"relative inline-flex cursor-pointer items-center whitespace-nowrap border-b-2 px-3 text-sm transition-colors",
isActive
? "border-foreground text-foreground"
: "border-transparent text-muted-foreground hover:border-border hover:text-foreground",
);
if (isActive) {
return (
<span key={section.key} className={className} aria-current="page">
{t(section.labelKey)}
</span>
);
}
return (
<Link key={section.key} to={`/namespaces/${namespace}/${section.path}${query}`} className={className}>
{t(section.labelKey)}
</Link>
);
})}
</nav>
);
}
+23 -13
View File
@@ -27,41 +27,51 @@ export default function ({ limit, page, total, setPage }: { limit: number, page:
const { t } = useTranslation();
const start = (page - 1) * limit + 1 > total ? total : (page - 1) * limit + 1;
const end = total > page * limit ? page * limit : total;
const pageCount = Math.max(1, Math.ceil(total / limit));
const hasPrevious = page > 1;
const hasNext = total / limit > page;
return (
<div
className="flex items-center justify-between border-gray-200 px-4 py-3 border-t-0 bg-slate-100 dark:border-gray-800 dark:bg-gray-900"
className="flex flex-wrap items-center justify-between gap-3 border-t border-border bg-muted/40 px-4 py-3"
aria-label="Pagination"
>
<div>
<p className="text-sm text-gray-700 dark:text-gray-200">
{t("pagination.summary", { start, end, total })}
</p>
</div>
<ShadPagination>
<PaginationContent>
<p className="text-sm text-muted-foreground">
{t("pagination.summary", { start, end, total })}
</p>
<ShadPagination className="mx-0 w-auto">
<PaginationContent className="gap-2">
<PaginationItem>
<PaginationPrevious
text={t("common.previous")}
aria-disabled={!hasPrevious}
onClick={(e: React.MouseEvent) => {
e.preventDefault();
if (page > 1) setPage(page - 1);
if (hasPrevious) setPage(page - 1);
}}
className={page <= 1 ? "pointer-events-none opacity-50" : ""}
className={hasPrevious ? "cursor-pointer" : "cursor-not-allowed opacity-50 hover:bg-transparent"}
/>
</PaginationItem>
{total > 0 && (
<PaginationItem>
<span className="min-w-14 text-center text-sm tabular-nums text-muted-foreground">
{page} / {pageCount}
</span>
</PaginationItem>
)}
<PaginationItem>
<PaginationNext
text={t("common.next")}
aria-disabled={!hasNext}
onClick={(e: React.MouseEvent) => {
e.preventDefault();
if (total / limit > page) setPage(page + 1);
if (hasNext) setPage(page + 1);
}}
className={total / limit <= page ? "pointer-events-none opacity-50" : ""}
className={hasNext ? "cursor-pointer" : "cursor-not-allowed opacity-50 hover:bg-transparent"}
/>
</PaginationItem>
</PaginationContent>
</ShadPagination>
</div>
)
}
}
+2 -2
View File
@@ -70,7 +70,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
<th
data-slot="table-head"
className={cn(
"h-10 px-4 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
"h-10 px-4 text-left align-middle font-medium whitespace-nowrap text-muted-foreground has-[[role=checkbox]]:pr-0",
className
)}
{...props}
@@ -83,7 +83,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
<td
data-slot="table-cell"
className={cn(
"px-4 py-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
"px-4 py-2 align-middle whitespace-nowrap has-[[role=checkbox]]:pr-0",
className
)}
{...props}
+26 -59
View File
@@ -21,11 +21,12 @@ import parser from 'cron-parser';
import { EllipsisVerticalIcon } from '@heroicons/react/20/solid';
import { Fragment, useEffect, useState } from "react";
import { Helmet, HelmetProvider } from 'react-helmet-async';
import { Link, useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { Tooltip } from '../../utils';
import Header from "../../components/Header";
import IMenu from "../../components/Menu";
import NamespaceTabs from "../../components/NamespaceTabs";
import Notification from "../../components/Notification";
import { useTranslation } from "../../i18n/useTranslation";
@@ -620,61 +621,27 @@ export default function ({ localServer }: { localServer: string }) {
<Header title={location.pathname.startsWith("/settings") ? t("header.settingDaemonTask") : t("header.namespaceDaemonTask")}
props={
location.pathname.startsWith("/settings") ? null : (
<div className="flex space-x-8">
<Link
to={`/namespaces/${namespace}/namespace-summary?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
{t("common.summary")}
</Link>
<Link
to={`/namespaces/${namespace}/repositories?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
{t("common.repositoryList")}
</Link>
<Link
to={`/namespaces/${namespace}/members?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
{t("common.members")}
</Link>
<Link
to={`/namespaces/${namespace}/daemon-tasks?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-indigo-500 px-1 pt-1 text-sm font-medium text-gray-900 capitalize"
onClick={e => {
e.preventDefault();
}}
>
{t("common.daemonTask")}
</Link>
<Link
to={`/namespaces/${namespace}/webhooks?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
{t("common.webhook")}
</Link>
</div>
<NamespaceTabs namespace={namespace} namespaceId={namespaceId ? namespaceId.toString() : ""} active="daemon-tasks" />
)
} />
<div className="flex flex-1 overflow-visible">
<div className="align-middle inline-block min-w-full border-gray-200">
<table className="min-w-full flex-1 overflow-visible">
<thead>
<tr className="border-gray-200 dark:border-gray-800">
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-left text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300">
<tr className="border-b">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-left text-sm font-normal text-muted-foreground whitespace-nowrap">
<span className="lg:pl-2">{t("common.task")}</span>
</th>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-right text-sm font-normal text-muted-foreground whitespace-nowrap">
<span className="lg:pl-2">{t("common.running")}</span>
</th>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-right text-sm font-normal text-muted-foreground whitespace-nowrap">
<span className="lg:pl-2">{t("daemon.lastTrigger")}</span>
</th>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-right text-sm font-normal text-muted-foreground whitespace-nowrap">
<span className="lg:pl-2">{t("daemon.nextTrigger")}</span>
</th>
<th className="sticky top-0 z-10 pr-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-center text-sm font-normal text-muted-foreground whitespace-nowrap">
<span className="lg:pl-2">{t("common.action")}</span>
</th>
</tr>
@@ -706,7 +673,7 @@ export default function ({ localServer }: { localServer: string }) {
<td className="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">
{gcRepositoryRuleExist && gcRepositoryRule.cron_enabled && gcRepositoryRule.cron_next_trigger != undefined ? gcRepositoryRule.cron_next_trigger : "-"}
</td>
<td className="pr-3 whitespace-nowrap">
<td className="px-6 text-center whitespace-nowrap">
<DropdownMenu>
<DropdownMenuTrigger
render={
@@ -718,7 +685,7 @@ export default function ({ localServer }: { localServer: string }) {
/>
<DropdownMenuContent align="end" className="w-30">
<DropdownMenuItem onClick={() => setGcRepositoryRuleConfigModal(true)}>
{gcRepositoryRuleExist ? "Update" : "Configuration"}
Update
</DropdownMenuItem>
<DropdownMenuItem disabled={!gcRepositoryRuleExist} onClick={() => createGcRepositoryRunner()}>
Run
@@ -753,7 +720,7 @@ export default function ({ localServer }: { localServer: string }) {
<td className="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">
{gcTagRuleExist && gcTagRule.cron_enabled && gcTagRule.cron_next_trigger != undefined ? gcTagRule.cron_next_trigger : "-"}
</td>
<td className="pr-3 whitespace-nowrap">
<td className="px-6 text-center whitespace-nowrap">
<DropdownMenu>
<DropdownMenuTrigger
render={
@@ -765,7 +732,7 @@ export default function ({ localServer }: { localServer: string }) {
/>
<DropdownMenuContent align="end" className="w-30">
<DropdownMenuItem onClick={() => setGcTagRuleConfigModal(true)}>
{gcTagRuleExist ? "Update" : "Configuration"}
Update
</DropdownMenuItem>
<DropdownMenuItem disabled={!gcTagRuleExist} onClick={() => createGcTagRunner()}>
Run
@@ -800,7 +767,7 @@ export default function ({ localServer }: { localServer: string }) {
<td className="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">
{gcArtifactRuleExist && gcArtifactRule.cron_enabled && gcArtifactRule.cron_next_trigger != undefined ? gcArtifactRule.cron_next_trigger : "-"}
</td>
<td className="pr-3 whitespace-nowrap">
<td className="px-6 text-center whitespace-nowrap">
<DropdownMenu>
<DropdownMenuTrigger
render={
@@ -812,7 +779,7 @@ export default function ({ localServer }: { localServer: string }) {
/>
<DropdownMenuContent align="end" className="w-30">
<DropdownMenuItem onClick={() => setGcArtifactRuleConfigModal(true)}>
{gcArtifactRuleExist ? "Update" : "Configuration"}
Update
</DropdownMenuItem>
<DropdownMenuItem disabled={!gcArtifactRuleExist} onClick={() => createGcArtifactRunner()}>
Run
@@ -849,7 +816,7 @@ export default function ({ localServer }: { localServer: string }) {
<td className="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">
{gcBlobRuleExist && gcBlobRule.cron_enabled && gcBlobRule.cron_next_trigger != undefined ? gcBlobRule.cron_next_trigger : "-"}
</td>
<td className="pr-3 whitespace-nowrap">
<td className="px-6 text-center whitespace-nowrap">
<DropdownMenu>
<DropdownMenuTrigger
render={
@@ -861,7 +828,7 @@ export default function ({ localServer }: { localServer: string }) {
/>
<DropdownMenuContent align="end" className="w-30">
<DropdownMenuItem onClick={() => setGcBlobRuleConfigModal(true)}>
{gcBlobRuleExist ? "Update" : "Configuration"}
Update
</DropdownMenuItem>
<DropdownMenuItem disabled={!gcBlobRuleExist} onClick={() => createGcBlobRunner()}>
Run
@@ -971,7 +938,7 @@ export default function ({ localServer }: { localServer: string }) {
id="namespace_count_limit"
name="namespace_count_limit"
placeholder="0 means no limit"
className={(gcBlobRuleRetentionDaysValid ? "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" : "block w-full rounded-md border-0 py-1.5 pr-10 text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset focus:ring-red-500 sm:text-sm sm:leading-6")}
className={(gcBlobRuleRetentionDaysValid ? "h-9 w-full min-w-0 rounded-lg border border-input bg-transparent px-3 py-1 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30" : "h-9 w-full min-w-0 rounded-lg border border-destructive bg-transparent px-3 py-1 pr-10 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-destructive focus-visible:ring-3 focus-visible:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30")}
value={gcBlobRuleRetentionDays}
onChange={e => setGcBlobRuleRetentionDays(Number.isNaN(parseInt(e.target.value)) ? "" : parseInt(e.target.value))}
/>
@@ -1055,7 +1022,7 @@ export default function ({ localServer }: { localServer: string }) {
id="gc_repository_cron_rule"
name="gc_repository_cron_rule"
placeholder="cron rule"
className={(gcBlobRuleCronRuleValid ? "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" : "block w-full rounded-md border-0 py-1.5 pr-10 text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset focus:ring-red-500 sm:text-sm sm:leading-6")}
className={(gcBlobRuleCronRuleValid ? "h-9 w-full min-w-0 rounded-lg border border-input bg-transparent px-3 py-1 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30" : "h-9 w-full min-w-0 rounded-lg border border-destructive bg-transparent px-3 py-1 pr-10 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-destructive focus-visible:ring-3 focus-visible:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30")}
value={gcBlobRuleCronRule}
onChange={e => setGcBlobRuleCronRule(e.target.value)}
/>
@@ -1139,7 +1106,7 @@ export default function ({ localServer }: { localServer: string }) {
id="namespace_count_limit"
name="namespace_count_limit"
placeholder="0 means no limit"
className={(gcRepositoryRuleRetentionDaysValid ? "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" : "block w-full rounded-md border-0 py-1.5 pr-10 text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset focus:ring-red-500 sm:text-sm sm:leading-6")}
className={(gcRepositoryRuleRetentionDaysValid ? "h-9 w-full min-w-0 rounded-lg border border-input bg-transparent px-3 py-1 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30" : "h-9 w-full min-w-0 rounded-lg border border-destructive bg-transparent px-3 py-1 pr-10 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-destructive focus-visible:ring-3 focus-visible:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30")}
value={gcRepositoryRuleRetentionDays}
onChange={e => setGcRepositoryRuleRetentionDays(Number.isNaN(parseInt(e.target.value)) ? "" : parseInt(e.target.value))}
/>
@@ -1223,7 +1190,7 @@ export default function ({ localServer }: { localServer: string }) {
id="gc_repository_cron_rule"
name="gc_repository_cron_rule"
placeholder="cron rule"
className={(gcRepositoryRuleCronRuleValid ? "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" : "block w-full rounded-md border-0 py-1.5 pr-10 text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset focus:ring-red-500 sm:text-sm sm:leading-6")}
className={(gcRepositoryRuleCronRuleValid ? "h-9 w-full min-w-0 rounded-lg border border-input bg-transparent px-3 py-1 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30" : "h-9 w-full min-w-0 rounded-lg border border-destructive bg-transparent px-3 py-1 pr-10 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-destructive focus-visible:ring-3 focus-visible:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30")}
value={gcRepositoryRuleCronRule}
onChange={e => setGcRepositoryRuleCronRule(e.target.value)}
/>
@@ -1308,7 +1275,7 @@ export default function ({ localServer }: { localServer: string }) {
id="namespace_count_limit"
name="namespace_count_limit"
placeholder="0 means no limit"
className={(gcArtifactRuleRetentionDaysValid ? "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" : "block w-full rounded-md border-0 py-1.5 pr-10 text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset focus:ring-red-500 sm:text-sm sm:leading-6")}
className={(gcArtifactRuleRetentionDaysValid ? "h-9 w-full min-w-0 rounded-lg border border-input bg-transparent px-3 py-1 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30" : "h-9 w-full min-w-0 rounded-lg border border-destructive bg-transparent px-3 py-1 pr-10 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-destructive focus-visible:ring-3 focus-visible:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30")}
value={gcArtifactRuleRetentionDays}
onChange={e => setGcArtifactRuleRetentionDays(Number.isNaN(parseInt(e.target.value)) ? "" : parseInt(e.target.value))}
/>
@@ -1391,7 +1358,7 @@ export default function ({ localServer }: { localServer: string }) {
id="gc_repository_cron_rule"
name="gc_repository_cron_rule"
placeholder="cron rule"
className={(gcArtifactRuleCronRuleValid ? "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" : "block w-full rounded-md border-0 py-1.5 pr-10 text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset focus:ring-red-500 sm:text-sm sm:leading-6")}
className={(gcArtifactRuleCronRuleValid ? "h-9 w-full min-w-0 rounded-lg border border-input bg-transparent px-3 py-1 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30" : "h-9 w-full min-w-0 rounded-lg border border-destructive bg-transparent px-3 py-1 pr-10 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-destructive focus-visible:ring-3 focus-visible:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30")}
value={gcArtifactRuleCronRule}
onChange={e => setGcArtifactRuleCronRule(e.target.value)}
/>
@@ -1492,7 +1459,7 @@ export default function ({ localServer }: { localServer: string }) {
id="namespace_count_limit"
name="namespace_count_limit"
placeholder="0 means no limit"
className={(gcTagRuleRetentionAmountValid ? "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" : "block w-full rounded-md border-0 py-1.5 pr-10 text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset focus:ring-red-500 sm:text-sm sm:leading-6")}
className={(gcTagRuleRetentionAmountValid ? "h-9 w-full min-w-0 rounded-lg border border-input bg-transparent px-3 py-1 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30" : "h-9 w-full min-w-0 rounded-lg border border-destructive bg-transparent px-3 py-1 pr-10 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-destructive focus-visible:ring-3 focus-visible:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30")}
value={gcTagRuleRetentionAmount}
onChange={e => setGcTagRuleRetentionAmount(Number.isNaN(parseInt(e.target.value)) ? "" : parseInt(e.target.value))}
/>
@@ -1552,7 +1519,7 @@ export default function ({ localServer }: { localServer: string }) {
id="namespace_count_limit"
name="namespace_count_limit"
placeholder="regexp"
className={(gcTagRuleRetentionPatternValid ? "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" : "block w-full rounded-md border-0 py-1.5 pr-10 text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset focus:ring-red-500 sm:text-sm sm:leading-6")}
className={(gcTagRuleRetentionPatternValid ? "h-9 w-full min-w-0 rounded-lg border border-input bg-transparent px-3 py-1 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30" : "h-9 w-full min-w-0 rounded-lg border border-destructive bg-transparent px-3 py-1 pr-10 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-destructive focus-visible:ring-3 focus-visible:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30")}
value={gcTagRuleRetentionPattern}
onChange={e => setGcTagRuleRetentionPattern(e.target.value)}
/>
@@ -1635,7 +1602,7 @@ export default function ({ localServer }: { localServer: string }) {
id="gc_repository_cron_rule"
name="gc_repository_cron_rule"
placeholder="cron rule"
className={(gcTagRuleCronRuleValid ? "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" : "block w-full rounded-md border-0 py-1.5 pr-10 text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset focus:ring-red-500 sm:text-sm sm:leading-6")}
className={(gcTagRuleCronRuleValid ? "h-9 w-full min-w-0 rounded-lg border border-input bg-transparent px-3 py-1 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30" : "h-9 w-full min-w-0 rounded-lg border border-destructive bg-transparent px-3 py-1 pr-10 text-base text-foreground transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-destructive focus-visible:ring-3 focus-visible:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30")}
value={gcTagRuleCronRule}
onChange={e => setGcTagRuleCronRule(e.target.value)}
/>
+101 -151
View File
@@ -18,10 +18,12 @@ import axios from "axios";
import Toast from 'react-hot-toast';
import { Fragment, useCallback, useEffect, useState } from "react";
import { Helmet, HelmetProvider } from 'react-helmet-async';
import { Link, useParams, useSearchParams } from 'react-router-dom';
import { useParams, useSearchParams } from 'react-router-dom';
import { ChevronUpDownIcon, EllipsisVerticalIcon } from "@heroicons/react/20/solid";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Command,
CommandEmpty,
@@ -50,11 +52,22 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { CornerDownLeft } from "lucide-react";
import Header from "../../components/Header";
import IMenu from "../../components/Menu";
import NamespaceTabs from "../../components/NamespaceTabs";
import Notification from "../../components/Notification";
import Pagination from "../../components/Pagination";
import RelativeTime from "../../components/RelativeTime";
import Settings from "../../Settings";
import { IHTTPError, INamespaceItem, INamespaceRoleItem as INamespaceMemberItem, INamespaceRoleList, IUserItem, IUserList } from "../../interfaces";
@@ -85,13 +98,17 @@ export default function Member({ localServer }: { localServer: string }) {
if (response?.status === 200) {
const namespaceData = response.data as INamespaceItem;
setNamespaceObj(namespaceData);
} else {
} else if (response.status !== 404) {
const errorcode = response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
}
}).catch(error => {
const errorcode = error.response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
// A missing namespace means there are no members, so stay silent.
if (error.response?.status === 404) {
return;
}
const errorcode = error.response?.data as IHTTPError;
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
});
}, [namespaceId, localServer]);
@@ -105,13 +122,17 @@ export default function Member({ localServer }: { localServer: string }) {
const namespaceRoleList = response.data as INamespaceRoleList;
setMemberList(namespaceRoleList);
setTotal(namespaceRoleList.total);
} else {
} else if (response.status !== 404) {
const errorcode = response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
}
}).catch(error => {
const errorcode = error.response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
// An empty member list is not an error, so stay silent.
if (error.response?.status === 404) {
return;
}
const errorcode = error.response?.data as IHTTPError;
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
});
}, [localServer, memberSearch, namespaceId]);
@@ -139,8 +160,8 @@ export default function Member({ localServer }: { localServer: string }) {
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
}
}).catch(error => {
const errorcode = error.response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
const errorcode = error.response?.data as IHTTPError;
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
});
}, [userSearch, localServer]);
@@ -180,113 +201,53 @@ export default function Member({ localServer }: { localServer: string }) {
<main className="relative z-0 focus:outline-none">
<Header title="Namespace - Member"
props={
(
<div className="flex space-x-8">
<Link
to={`/namespaces/${namespace}/namespace-summary?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
Summary
</Link>
<Link
to={`/namespaces/${namespace}/repositories?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
Repository list
</Link>
<Link
to={`/namespaces/${namespace}/members?namespace_id=${namespaceId}`}
className="z-10 inline-flex items-center border-b border-indigo-500 px-1 pt-1 text-sm font-medium text-gray-900 capitalize"
onClick={e => {
e.stopPropagation();
e.preventDefault();
}}
>
Members
</Link>
<Link
to={`/namespaces/${namespace}/daemon-tasks?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
Daemon task
</Link>
<Link
to={`/namespaces/${namespace}/webhooks?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
Webhook
</Link>
</div>
)
<NamespaceTabs namespace={namespace} namespaceId={namespaceId ? namespaceId.toString() : ""} active="members" />
}
/>
<div className="pt-1 pb-1 flex justify-between items-center min-h-15">
<div className="pt-4 pb-4 flex justify-between items-center">
<div className="px-4">
<div className="flex gap-4">
<div className="relative mt-2 flex items-center">
<label
htmlFor="usernameSearch"
className="absolute -top-2 left-2 inline-block bg-white px-1 text-xs font-medium text-gray-900"
>
Username
</label>
<input
type="text"
id="usernameSearch"
placeholder="search username"
value={memberSearch}
onChange={e => { setUsernameSearch(e.target.value); }}
onKeyDown={e => {
if (e.key == "Enter") {
fetchMembers();
}
}}
className="block w-full h-10 rounded-md border-0 py-1.5 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
<div className="absolute inset-y-0 right-0 flex py-1.5 pr-1.5">
<kbd className="inline-flex items-center rounded border border-gray-200 px-1 font-sans text-xs text-gray-400">
enter
</kbd>
</div>
</div>
<div className="relative flex items-center">
<Label
htmlFor="usernameSearch"
className="absolute -top-2 left-2 inline-block bg-background px-1 text-xs font-medium text-foreground z-10"
>
Username
</Label>
<Input
id="usernameSearch"
placeholder="search username"
value={memberSearch}
onChange={e => setUsernameSearch(e.target.value)}
onKeyDown={e => { if (e.key == "Enter") fetchMembers() }}
className="h-10 pr-14"
/>
<kbd className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground" aria-hidden="true">
<CornerDownLeft className="size-3.5" />
</kbd>
</div>
</div>
<div className="px-4 flex flex-col">
<button className="my-auto block px-4 py-2 h-10 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-1 sm:ml-3"
onClick={() => { setCreateUserNamespaceModal(true) }}
>Add</button>
<div className="px-4">
<Button onClick={() => setCreateUserNamespaceModal(true)}>Add</Button>
</div>
</div>
</main>
<div className="flex-1 flex overflow-y-auto">
<div className="align-middle inline-block min-w-full border-b border-gray-200">
<table className="min-w-full flex-1">
<thead>
<tr>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-left text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<span className="lg:pl-2">Username</span>
</th>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<span className="lg:pl-2">Role</span>
</th>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<span className="lg:pl-2">Added</span>
</th>
<th className="sticky top-0 z-10 pr-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<span className="lg:pl-2">Action</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-100 max-h-max">
{
memberList.items?.map((member) => {
return (
<TableItem key={member.id} localServer={localServer} namespace={namespaceObj} userSelectedArg={{ username: member.username, id: member.user_id } as IUserItem} member={member} onChanged={fetchMembers} />
);
})
}
</tbody>
</table>
<div className="w-full">
<Table>
<TableHeader className="[&_th]:font-normal">
<TableRow>
<TableHead>Username</TableHead>
<TableHead className="text-right">Role</TableHead>
<TableHead className="text-right">Added</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{memberList.items?.map((member) => (
<TableItem key={member.id} localServer={localServer} namespace={namespaceObj} userSelectedArg={{ username: member.username, id: member.user_id } as IUserItem} member={member} onChanged={fetchMembers} />
))}
</TableBody>
</Table>
</div>
</div>
<Pagination limit={Settings.PageSize} page={page} setPage={setPage} total={total} />
@@ -439,46 +400,36 @@ function TableItem({ localServer, namespace, userSelectedArg, member, onChanged
}
return (
<tr className="align-middle">
<td className="px-6 py-4 w-5/6 whitespace-nowrap text-sm font-medium text-gray-900 cursor-pointer" >
<div className="items-center space-x-3 lg:pl-2">
<div className="truncate hover:text-gray-600">
<span>
{member.username}
</span>
<>
<TableRow className="align-middle">
<TableCell>{member.username}</TableCell>
<TableCell className="text-right text-muted-foreground">{member.role.substring(9)}</TableCell>
<TableCell className="text-right text-muted-foreground"><RelativeTime time={member.created_at} /></TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end">
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="ghost" size="icon-sm" className="text-muted-foreground">
<span className="sr-only">Open options</span>
<EllipsisVerticalIcon className="size-5" aria-hidden="true" />
</Button>
}
/>
<DropdownMenuContent align="end" className="w-28">
<DropdownMenuItem onClick={() => setUpdateUserNamespaceModal(true)}>
Update
</DropdownMenuItem>
<DropdownMenuItem variant="destructive" onClick={() => deleteMember()}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</td>
<td className="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">
{member.role.substring(9)}
</td>
<td className="hidden md:table-cell px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">
{member.updated_at}
</td>
<td className="pr-3 whitespace-nowrap" onClick={e => {
e.stopPropagation();
}}>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="ghost" size="icon-sm" className="text-gray-500">
<span className="sr-only">Open options</span>
<EllipsisVerticalIcon className="size-5" aria-hidden="true" />
</Button>
}
/>
<DropdownMenuContent align="end" className="w-28">
<DropdownMenuItem onClick={() => setUpdateUserNamespaceModal(true)}>
Update
</DropdownMenuItem>
<DropdownMenuItem variant="destructive" onClick={() => deleteMember()}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
<td className="absolute hidden" onClick={e => { e.preventDefault() }}>
<Dialog open={updateUserNamespaceModal} onOpenChange={setUpdateUserNamespaceModal}>
</TableCell>
</TableRow>
<Dialog open={updateUserNamespaceModal} onOpenChange={setUpdateUserNamespaceModal}>
<DialogContent className="sm:max-w-lg">
<DialogTitle className="border-b pb-4">Update member</DialogTitle>
<div className="flex flex-col gap-0 mt-4">
@@ -578,8 +529,7 @@ function TableItem({ localServer, namespace, userSelectedArg, member, onChanged
<Button onClick={() => updateMember()}>Update</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</td>
</tr>
</Dialog>
</>
)
}
+19 -55
View File
@@ -22,14 +22,17 @@ import axios from "axios";
import gfm from "@bytemd/plugin-gfm";
import { Editor, Viewer } from "@bytemd/react";
import { Helmet, HelmetProvider } from "react-helmet-async";
import { Link, useParams, useSearchParams } from "react-router-dom";
import { useParams, useSearchParams } from "react-router-dom";
import { useEffect, useState } from "react";
import Header from "../../components/Header";
import IMenu from "../../components/Menu";
import NamespaceTabs from "../../components/NamespaceTabs";
import Notification from "../../components/Notification";
import { IHTTPError, INamespaceItem } from "../../interfaces";
import { Button } from "@/components/ui/button";
export default function ({ localServer }: { localServer: string }) {
const { namespace } = useParams<{ namespace: string }>();
const [searchParams] = useSearchParams();
@@ -48,13 +51,17 @@ export default function ({ localServer }: { localServer: string }) {
const r = response.data as INamespaceItem;
setNamespaceObj(r);
setOverview(r.overview);
} else {
} else if (response.status !== 404) {
const errorcode = response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
}
}).catch(error => {
const errorcode = error.response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
// A missing namespace means there is no overview yet, so stay silent.
if (error.response?.status === 404) {
return;
}
const errorcode = error.response?.data as IHTTPError;
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
});
}, [namespace, repositoryId, localServer, namespaceId]);
@@ -94,39 +101,7 @@ export default function ({ localServer }: { localServer: string }) {
<main className="relative z-0 focus:outline-none">
<Header title="Repository"
props={
(
<div className="flex space-x-8">
<span
className="z-10 inline-flex items-center border-b border-indigo-500 px-1 pt-1 text-sm font-medium text-gray-900 capitalize cursor-pointer"
>
Summary
</span>
<Link
to={`/namespaces/${namespace}/repositories?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
Repository list
</Link>
<Link
to={`/namespaces/${namespace}/members?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
Members
</Link>
<Link
to={`/namespaces/${namespace}/daemon-tasks?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
Daemon task
</Link>
<Link
to={`/namespaces/${namespace}/webhooks?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
Webhook
</Link>
</div>
)
<NamespaceTabs namespace={namespace} namespaceId={Number.isNaN(namespaceId) ? "" : namespaceId.toString()} active="summary" />
} />
</main>
<div className="flex flex-1 overflow-y-auto">
@@ -135,9 +110,9 @@ export default function ({ localServer }: { localServer: string }) {
editorState ? (
<span></span>
) : (
<button className="my-auto block px-4 py-2 h-10 border border-transparent shadow-sm text-sm font-medium rounded-md text-gray-700 bg-gray-200 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-1 sm:ml-3 absolute right-4 top-2"
onClick={() => { setEditorState(true) }}
>Edit</button>
<Button variant="outline" className="absolute right-4 top-2"
onClick={() => setEditorState(true)}
>Edit</Button>
)
}
{
@@ -158,20 +133,9 @@ export default function ({ localServer }: { localServer: string }) {
</div>
{
editorState ? (
<div
className="flex flex-2 items-center justify-between border-gray-200 px-4 py-3 sm:px-6 border-t-0 bg-slate-100"
aria-label="Pagination"
>
<div>
</div>
<div className="flex flex-1 justify-between sm:justify-end">
<button className="my-auto block px-4 py-2 h-10 border border-transparent shadow-sm text-sm font-medium rounded-md text-gray-700 bg-gray-200 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-1 sm:ml-3"
onClick={() => setEditorState(false)}
>Cancel</button>
<button className="my-auto block px-4 py-2 h-10 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-1 sm:ml-3"
onClick={() => { updateNamespace() }}
>Update</button>
</div>
<div className="flex items-center justify-end gap-2 border-t border-border bg-muted/40 px-4 py-3 sm:px-6">
<Button variant="outline" onClick={() => setEditorState(false)}>Cancel</Button>
<Button onClick={() => updateNamespace()}>Update</Button>
</div>
) : (
<div></div>
+13
View File
@@ -18,14 +18,24 @@
margin-top: -7rem;
}
.editor > div {
height: 100%;
}
.editor > div > div {
height: 100%;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
/*light*/
@@ -176,6 +186,7 @@ input[type="number"] {
.markdown-body [type="reset"],
.markdown-body [type="submit"] {
-webkit-appearance: button;
appearance: button;
}
.markdown-body [type="checkbox"],
@@ -192,6 +203,7 @@ input[type="number"] {
.markdown-body [type="search"]::-webkit-search-cancel-button,
.markdown-body [type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
appearance: none;
}
.markdown-body ::-webkit-input-placeholder {
@@ -201,6 +213,7 @@ input[type="number"] {
.markdown-body ::-webkit-file-upload-button {
-webkit-appearance: button;
appearance: button;
font: inherit;
}
+2 -2
View File
@@ -196,7 +196,7 @@ export default function Namespace({ localServer }: { localServer: string }) {
<div className="flex-1 flex overflow-y-auto">
<div className="w-full">
<Table>
<TableHeader>
<TableHeader className="[&_th]:font-normal">
<TableRow>
<TableHead>{t("namespace.table.name")}</TableHead>
<TableHead className="text-right"><OrderHeader text={t("namespace.table.size")} orderStatus={sizeOrder} setOrder={e => { resetOrder(); setSizeOrder(e); setSortOrder(e); setSortName("size"); }} /></TableHead>
@@ -339,7 +339,7 @@ function TableItem({ localServer, user, namespace: ns, onChanged }: { localServe
<TableRow className="align-middle">
<TableCell className="cursor-pointer" onClick={() => navigate(`/namespaces/${ns.name}/repositories?namespace_id=${ns.id}`)}>
<div className="truncate">
<span className="font-medium">{ns.name}</span>
<span>{ns.name}</span>
<span className="text-muted-foreground font-normal ml-4">{ns.description}</span>
</div>
</TableCell>
+3
View File
@@ -174,6 +174,7 @@
.markdown-body [type="reset"],
.markdown-body [type="submit"] {
-webkit-appearance: button;
appearance: button;
}
.markdown-body [type="checkbox"],
@@ -190,6 +191,7 @@
.markdown-body [type="search"]::-webkit-search-cancel-button,
.markdown-body [type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
appearance: none;
}
.markdown-body ::-webkit-input-placeholder {
@@ -199,6 +201,7 @@
.markdown-body ::-webkit-file-upload-button {
-webkit-appearance: button;
appearance: button;
font: inherit;
}
+6 -14
View File
@@ -19,10 +19,11 @@ import dayjs from "dayjs";
import { useDebounce } from "react-use";
import { Fragment, useCallback, useEffect, useState } from "react";
import { Helmet, HelmetProvider } from 'react-helmet-async';
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import Header from "../../components/Header";
import IMenu from "../../components/Menu";
import NamespaceTabs from "../../components/NamespaceTabs";
import Notification from "../../components/Notification";
import OrderHeader from "../../components/OrderHeader";
import Pagination from "../../components/Pagination";
@@ -38,7 +39,6 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Dialog,
DialogContent,
@@ -191,18 +191,10 @@ export default function ({ localServer }: { localServer: string }) {
<main className="relative z-0 focus:outline-none">
<Header title={t("header.repository")}
props={
<Tabs value="repositoryList" className="h-full">
<TabsList className="h-full">
<TabsTrigger value="summary" render={<Link to={`/namespaces/${namespace}/namespace-summary?namespace_id=${namespaceId}`} />}>{t("common.summary")}</TabsTrigger>
<TabsTrigger value="repositoryList">{t("common.repositoryList")}</TabsTrigger>
<TabsTrigger value="members" render={<Link to={`/namespaces/${namespace}/members?namespace_id=${namespaceId}`} />}>{t("common.members")}</TabsTrigger>
<TabsTrigger value="daemon" render={<Link to={`/namespaces/${namespace}/daemon-tasks?namespace_id=${namespaceId}`} />}>{t("common.daemonTask")}</TabsTrigger>
<TabsTrigger value="webhook" render={<Link to={`/namespaces/${namespace}/webhooks?namespace_id=${namespaceId}`} />}>{t("common.webhook")}</TabsTrigger>
</TabsList>
</Tabs>
<NamespaceTabs namespace={namespace} namespaceId={namespaceId || ""} active="repositories" />
}
/>
<div className="pt-3 pb-3 flex justify-between items-center min-h-15">
<div className="pt-4 pb-4 flex justify-between items-center">
<div className="px-4">
<div className="relative flex items-center">
<Label htmlFor="repositorySearch" className="absolute -top-2 left-2 inline-block bg-background px-1 text-xs font-medium text-foreground z-10">
@@ -224,7 +216,7 @@ export default function ({ localServer }: { localServer: string }) {
<div className="flex flex-1 overflow-y-auto">
<div className="w-full">
<Table>
<TableHeader>
<TableHeader className="[&_th]:font-normal">
<TableRow>
<TableHead>{t("repository.table.name")}</TableHead>
<TableHead className="text-right"><OrderHeader text={t("repository.table.size")} orderStatus={sizeOrder} setOrder={e => { resetOrder(); setSizeOrder(e); setSortOrder(e); setSortName("size"); }} /></TableHead>
@@ -352,7 +344,7 @@ function TableItem({ localServer, user, namespace: ns, repository, setRefresh }:
<TableRow>
<TableCell className="cursor-pointer" onClick={() => navigate(`/namespaces/${ns.name}/repository/tags?namespace_id=${repository.namespace_id}&repository=${repository.name}&repository_id=${repository.id}`)}>
<div className="truncate">
<span className="font-medium">{repository.name}</span>
<span>{repository.name}</span>
<span className="text-muted-foreground font-normal ml-4">{repository.description}</span>
</div>
</TableCell>
+109 -229
View File
@@ -16,16 +16,20 @@
import axios from "axios";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogTitle } from "@/components/ui/dialog";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Fragment, useCallback, useEffect, useState } from "react";
import { Helmet, HelmetProvider } from 'react-helmet-async';
import { Link, useParams, useSearchParams, useLocation, useNavigate } from 'react-router-dom';
import { useParams, useSearchParams, useLocation, useNavigate } from 'react-router-dom';
import { Tooltip } from '../../utils';
import dayjs from "dayjs";
import Header from "../../components/Header";
import IMenu from "../../components/Menu";
import NamespaceTabs from "../../components/NamespaceTabs";
import Notification from "../../components/Notification";
import Pagination from "../../components/Pagination";
import Settings from "../../Settings";
@@ -54,13 +58,17 @@ export default function ({ localServer }: { localServer: string }) {
if (response.status == 200) {
const namespaceData = response.data as INamespaceItem;
setNamespaceObj(namespaceData);
} else {
} else if (response.status !== 404) {
const errorcode = response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
}
}).catch(error => {
const errorcode = error.response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
// A missing namespace means there are no webhooks, so stay silent.
if (error.response?.status === 404) {
return;
}
const errorcode = error.response?.data as IHTTPError;
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
})
}, [localServer, location.pathname, namespaceId]);
@@ -76,8 +84,8 @@ export default function ({ localServer }: { localServer: string }) {
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
}
}).catch(error => {
const errorcode = error.response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
const errorcode = error.response?.data as IHTTPError;
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
});
}, [localServer]);
@@ -133,13 +141,17 @@ export default function ({ localServer }: { localServer: string }) {
const webhookListData = response.data as IWebhookList;
setWebhookList(webhookListData);
setTotal(webhookListData.total);
} else {
} else if (response.status !== 404) {
const errorcode = response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
}
}).catch(error => {
const errorcode = error.response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
// An empty webhook list is not an error, so stay silent.
if (error.response?.status === 404) {
return;
}
const errorcode = error.response?.data as IHTTPError;
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
});
}, [localServer, page, sortName, sortOrder, namespaceId]);
@@ -226,51 +238,12 @@ export default function ({ localServer }: { localServer: string }) {
<main className="relative z-0 focus:outline-none" tabIndex={0}>
<Header title={t("header.webhook")} props={
location.pathname.startsWith("/settings") ? null : (
<div className="flex space-x-8">
<Link
to={`/namespaces/${namespace}/namespace-summary?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
{t("common.summary")}
</Link>
<Link
to={`/namespaces/${namespace}/repositories?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
{t("common.repositoryList")}
</Link>
<Link
to={`/namespaces/${namespace}/members?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
{t("common.members")}
</Link>
<Link
to={`/namespaces/${namespace}/daemon-tasks?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
{t("common.daemonTask")}
</Link>
<Link
to="#"
className="inline-flex items-center border-b border-indigo-500 px-1 pt-1 text-sm font-medium text-gray-900 capitalize"
>
{t("common.webhook")}
</Link>
</div>
<NamespaceTabs namespace={namespace} namespaceId={namespaceId || ""} active="webhooks" />
)
} />
<div className="pt-1 pb-1 flex justify-between items-center min-h-15">
<div className="pt-4 pb-4 flex justify-end items-center">
<div className="px-4">
<div className="flex gap-4">
<div className="relative mt-2 flex items-center">
</div>
</div>
</div>
<div className="px-4 flex flex-col">
<button className="my-auto block px-4 py-2 h-10 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-1 sm:ml-3"
onClick={() => { setCreateWebhookModal(true) }}
>{t("common.create")}</button>
<Button onClick={() => setCreateWebhookModal(true)}>{t("common.create")}</Button>
</div>
</div>
</main>
@@ -278,17 +251,17 @@ export default function ({ localServer }: { localServer: string }) {
<div className="align-middle inline-block min-w-full border-b border-gray-200">
<table className="min-w-full flex-1">
<thead>
<tr>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-left text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<tr className="border-b">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-left text-sm font-normal text-muted-foreground whitespace-nowrap">
<span className="lg:pl-2">URL</span>
</th>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-right text-sm font-normal text-muted-foreground whitespace-nowrap">
<span className="lg:pl-2">Enable</span>
</th>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-right text-sm font-normal text-muted-foreground whitespace-nowrap">
<span className="lg:pl-2">SSL Verify</span>
</th>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-right text-sm font-normal text-muted-foreground whitespace-nowrap">
<OrderHeader text={"Created at"} orderStatus={createdAtOrder} setOrder={(e) => {
resetOrder();
setCreatedAtOrder(e);
@@ -296,7 +269,7 @@ export default function ({ localServer }: { localServer: string }) {
setSortName("created_at");
}} />
</th>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-right text-sm font-normal text-muted-foreground whitespace-nowrap">
<OrderHeader text={"Updated at"} orderStatus={updatedAtOrder} setOrder={(e) => {
resetOrder();
setUpdatedAtOrder(e);
@@ -304,7 +277,7 @@ export default function ({ localServer }: { localServer: string }) {
setSortName("updated_at");
}} />
</th>
<th className="sticky top-0 z-10 pr-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<th className="sticky top-0 z-10 pr-6 py-3 bg-muted text-right text-sm font-normal text-muted-foreground whitespace-nowrap">
Action
</th>
</tr>
@@ -326,188 +299,95 @@ export default function ({ localServer }: { localServer: string }) {
</div>
<Dialog open={createWebhookModal} onOpenChange={setCreateWebhookModal}>
<DialogContent className="sm:max-w-lg">
<DialogTitle className="border-b pb-4">Create webhook</DialogTitle>
<div className="flex flex-col gap-0 mt-4">
<div className="grid grid-cols-12 gap-4">
<div className="col-span-2 flex flex-row">
<label htmlFor="usernameText" className="block text-sm font-medium leading-6 text-gray-900 my-auto">
<div className="flex">
<span className="text-red-600">*</span>
<span className="leading-6 ">URL</span>
<span>:</span>
</div>
</label>
</div>
<div className="col-span-10">
<input
type="text"
name="description"
placeholder="128 characters"
className={(urlValid ? "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" : "block w-full rounded-md border-0 py-1.5 pr-10 text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset focus:ring-red-500 sm:text-sm sm:leading-6")}
value={url}
onChange={e => setUrl(e.target.value)}
/>
</div>
<DialogHeader>
<DialogTitle>Create webhook</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="webhookUrl"><span className="text-destructive">*</span>URL</Label>
<Input
id="webhookUrl"
placeholder="128 characters"
value={url}
onChange={e => setUrl(e.target.value)}
aria-invalid={!urlValid || undefined}
className="h-10"
/>
{!urlValid && <p className="text-xs text-destructive">Not a valid URL, max 128 characters.</p>}
</div>
<div className="grid grid-cols-12 gap-4 mt-4">
<div className="col-span-2 flex flex-row">
<label htmlFor="usernameText" className="block text-sm font-medium leading-6 text-gray-900 my-auto">
<div className="flex">
<span className="leading-6 ">Secret</span>
<span>:</span>
</div>
</label>
</div>
<div className="col-span-10">
<input
type="text"
name="description"
placeholder="max 63 characters"
className={(secretValid ? "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" : "block w-full rounded-md border-0 py-1.5 pr-10 text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset focus:ring-red-500 sm:text-sm sm:leading-6")}
value={secret}
onChange={e => setSecret(e.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="webhookSecret">Secret</Label>
<Input
id="webhookSecret"
placeholder="max 63 characters"
value={secret ?? ""}
onChange={e => setSecret(e.target.value)}
aria-invalid={!secretValid || undefined}
className="h-10"
/>
{!secretValid && <p className="text-xs text-destructive">Not a valid secret, max 63 characters.</p>}
</div>
{
showSslVerify ? (
<div className="grid grid-cols-12 gap-4 mt-4">
<div className="col-span-2 flex flex-row">
<label htmlFor="usernameText" className="block text-sm font-medium leading-6 text-gray-900 my-auto">
<div className="flex">
<span className="leading-6 ">SSL Verify</span>
<span>:</span>
</div>
</label>
</div>
<div className="col-span-10 flex flex-row">
<label className="inline-flex items-center cursor-pointer">
<input type="checkbox"
checked={sslVerify}
onChange={() => setSslVerify(!sslVerify)}
className="sr-only peer" />
<div className="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-0.5 after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
) : null
}
<div className="grid grid-cols-12 gap-4 mt-4">
<div className="col-span-2 flex flex-row">
<label htmlFor="usernameText" className="block text-sm font-medium leading-6 text-gray-900 my-auto">
<div className="flex">
<span className="leading-6 ">Retry Times</span>
<span>:</span>
</div>
</label>
{showSslVerify && (
<div className="flex items-center justify-between">
<Label htmlFor="webhookSslVerify">SSL Verify</Label>
<Switch id="webhookSslVerify" checked={sslVerify} onCheckedChange={setSslVerify} />
</div>
<div className="col-span-4 flex flex-row">
<input
type="text"
name="description"
)}
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="webhookRetryTimes">Retry Times</Label>
<Input
id="webhookRetryTimes"
placeholder="1 <= times <= 5"
className={(retryTimesValid ? "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" : "block w-full rounded-md border-0 py-1.5 pr-10 text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset focus:ring-red-500 sm:text-sm sm:leading-6")}
value={retryTimes}
onChange={e => setRetryTimes(Number.isNaN(parseInt(e.target.value)) ? "" : parseInt(e.target.value))}
aria-invalid={!retryTimesValid || undefined}
className="h-10"
/>
{!retryTimesValid && <p className="text-xs text-destructive">Retry times should be between 1 and 5.</p>}
</div>
<div className="col-span-2 flex flex-row">
<label htmlFor="usernameText" className="block text-sm font-medium leading-6 text-gray-900 my-auto">
<div className="flex">
<span className="leading-6 ">Retry Duration</span>
<div className="flex flex-row cursor-pointer"
id="gcRepositoryRetentionDaysHelp"
onClick={() => {
let tooltip = new Tooltip(document.getElementById("tooltip-gc-repository-retention-days"),
document.getElementById("gcRepositoryRetentionDaysHelp"), { triggerType: "click" });
tooltip.show();
}}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-4 h-4 block my-auto ml-0.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
</svg>
</div>
<span>:</span>
</div>
</label>
</div>
<div className="col-span-4 flex flex-row">
<input
type="text"
name="description"
<div className="grid gap-2">
<Label htmlFor="webhookRetryDuration">Retry Duration</Label>
<Input
id="webhookRetryDuration"
placeholder="less than 10"
className={(retryDurationValid ? "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" : "block w-full rounded-md border-0 py-1.5 pr-10 text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset focus:ring-red-500 sm:text-sm sm:leading-6")}
value={retryDuration}
onChange={e => setRetryDuration(Number.isNaN(parseInt(e.target.value)) ? "" : parseInt(e.target.value))}
aria-invalid={!retryDurationValid || undefined}
className="h-10"
/>
{!retryDurationValid && <p className="text-xs text-destructive">Retry duration should be between 0 and 10.</p>}
</div>
</div>
<div className="grid grid-cols-12 gap-4 mt-4">
<div className="col-span-2 flex flex-row">
<label htmlFor="usernameText" className="block text-sm font-medium leading-6 text-gray-900 my-auto">
<div className="flex">
<span className="leading-6 ">Enable</span>
<span>:</span>
</div>
</label>
</div>
<div className="col-span-10 flex flex-row">
<label className="inline-flex items-center cursor-pointer">
<input type="checkbox"
checked={enable}
onChange={() => setEnable(!enable)}
className="sr-only peer" />
<div className="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-0.5 after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
<div className="grid gap-2">
<Label>Events</Label>
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
{[
...(location.pathname.startsWith("/settings")
? [{ id: "event-namespace", label: "Namespace Event", checked: eventNamespace, setChecked: setEventNamespace }]
: []),
{ id: "event-member", label: "Member Event", checked: eventMember, setChecked: setEventMember },
{ id: "event-repository", label: "Repository Event", checked: eventRepository, setChecked: setEventRepository },
{ id: "event-tag", label: "Tag Event", checked: eventTag, setChecked: setEventTag },
{ id: "event-artifact", label: "Artifact Event", checked: eventArtifact, setChecked: setEventArtifact },
{ id: "event-daemon-task-gc", label: "GC Event", checked: eventDaemonTaskGc, setChecked: setEventDaemonTaskGc },
].map(({ id, label, checked, setChecked }) => (
<label key={id} htmlFor={id} className="flex items-center gap-2 text-sm text-foreground">
<input
id={id}
type="checkbox"
checked={checked}
onChange={() => setChecked(!checked)}
className="size-4 rounded border-input accent-primary"
/>
{label}
</label>
))}
</div>
</div>
<div className="mt-4 flex flex-row gap-4">
{
location.pathname.startsWith("/settings") ? (
<div className="flex items-center">
<input id="event-namespace" type="checkbox"
checked={eventNamespace}
onChange={() => setEventNamespace(!eventNamespace)}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
<label htmlFor="event-namespace" className="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Namespace Event</label>
</div>
) : null
}
<div className="flex items-center">
<input id="event-member"
checked={eventMember}
onChange={() => setEventMember(!eventMember)}
type="checkbox" className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
<label htmlFor="event-member" className="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Member Event</label>
</div>
<div className="flex items-center">
<input id="event-repository" type="checkbox"
checked={eventRepository}
onChange={() => setEventRepository(!eventRepository)}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
<label htmlFor="event-repository" className="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Repository Event</label>
</div>
<div className="flex items-center">
<input id="event-tag" type="checkbox"
checked={eventTag}
onChange={() => setEventTag(!eventTag)}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
<label htmlFor="event-tag" className="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Tag Event</label>
</div>
<div className="flex items-center">
<input id="event-artifact" type="checkbox"
checked={eventArtifact}
onChange={() => setEventArtifact(!eventArtifact)}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
<label htmlFor="event-artifact" className="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Artifact Event</label>
</div>
<div className="flex items-center">
<input id="event-daemon-task-gc" type="checkbox"
checked={eventDaemonTaskGc}
onChange={() => setEventDaemonTaskGc(!eventDaemonTaskGc)}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
<label htmlFor="event-daemon-task-gc" className="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Gc Event</label>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="webhookEnable">Enable</Label>
<Switch id="webhookEnable" checked={enable} onCheckedChange={setEnable} />
</div>
</div>
<DialogFooter>
@@ -720,7 +600,7 @@ function TableItem({ localServer, userObj, namespaceObj, webhookObj, setRefresh
checked={sslVerify}
onChange={() => setSslVerify(!sslVerify)}
className="sr-only peer" />
<div className="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-0.5 after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
<div className="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:inset-s-0.5 after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
@@ -791,7 +671,7 @@ function TableItem({ localServer, userObj, namespaceObj, webhookObj, setRefresh
checked={enable}
onChange={() => setEnable(!enable)}
className="sr-only peer" />
<div className="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-0.5 after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
<div className="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:inset-s-0.5 after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
+31 -50
View File
@@ -21,10 +21,11 @@ import { Helmet, HelmetProvider } from 'react-helmet-async';
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogTitle } from "@/components/ui/dialog";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Link, useParams, useSearchParams, useLocation } from 'react-router-dom';
import { useParams, useSearchParams, useLocation } from 'react-router-dom';
import Header from "../../components/Header";
import IMenu from "../../components/Menu";
import NamespaceTabs from "../../components/NamespaceTabs";
import Notification from "../../components/Notification";
import Pagination from "../../components/Pagination";
import Settings from "../../Settings";
@@ -55,13 +56,17 @@ export default function ({ localServer }: { localServer: string }) {
if (response.status == 200) {
const namespaceData = response.data as INamespaceItem;
setNamespaceObj(namespaceData);
} else {
} else if (response.status !== 404) {
const errorcode = response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
}
}).catch(error => {
const errorcode = error.response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
// A missing namespace means there are no webhook logs, so stay silent.
if (error.response?.status === 404) {
return;
}
const errorcode = error.response?.data as IHTTPError;
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
})
}, [localServer, location.pathname, namespaceId]);
@@ -77,8 +82,8 @@ export default function ({ localServer }: { localServer: string }) {
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
}
}).catch(error => {
const errorcode = error.response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
const errorcode = error.response?.data as IHTTPError;
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
});
}, [localServer]);
@@ -108,14 +113,21 @@ export default function ({ localServer }: { localServer: string }) {
setTotal(webhookLogListData.total);
setFetchWebhookSuccess(true);
} else {
const errorcode = response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
setFetchWebhookSuccess(false);
if (response.status === 404) {
return;
}
const errorcode = response.data as IHTTPError;
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
}
}).catch(error => {
const errorcode = error.response.data as IHTTPError;
Notification({ level: "warning", title: errorcode.title, message: errorcode.description });
setFetchWebhookSuccess(false);
// An empty webhook log list is not an error, so stay silent.
if (error.response?.status === 404) {
return;
}
const errorcode = error.response?.data as IHTTPError;
Notification({ level: "warning", title: errorcode?.title, message: errorcode?.description });
});
}, [refresh, page, sortOrder, sortName, localServer, webhookId]);
@@ -165,38 +177,7 @@ export default function ({ localServer }: { localServer: string }) {
<main className="relative z-0 focus:outline-none" tabIndex={0}>
<Header title="Webhook" props={
location.pathname.startsWith("/settings") ? null : (
<div className="flex space-x-8">
<Link
to={`/namespaces/${namespace}/namespace-summary?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
Summary
</Link>
<Link
to={`/namespaces/${namespace}/repositories?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
Repository list
</Link>
<Link
to={`/namespaces/${namespace}/members?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
Members
</Link>
<Link
to={`/namespaces/${namespace}/daemon-tasks?namespace_id=${namespaceId}`}
className="inline-flex items-center border-b border-transparent px-1 pt-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 capitalize"
>
Daemon task
</Link>
<Link
to="#"
className="inline-flex items-center border-b border-indigo-500 px-1 pt-1 text-sm font-medium text-gray-900 capitalize"
>
Webhook
</Link>
</div>
<NamespaceTabs namespace={namespace} namespaceId={namespaceId || ""} active="webhooks" />
)
} />
<div className="pt-1 pb-1 flex justify-between items-center min-h-15">
@@ -217,17 +198,17 @@ export default function ({ localServer }: { localServer: string }) {
<div className="align-middle inline-block min-w-full border-b border-gray-200">
<table className="min-w-full flex-1">
<thead>
<tr>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-left text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<tr className="border-b">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-left text-sm font-normal text-muted-foreground whitespace-nowrap">
<span className="lg:pl-2">Event</span>
</th>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-right text-sm font-normal text-muted-foreground whitespace-nowrap">
<span className="lg:pl-2">Action</span>
</th>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-right text-sm font-normal text-muted-foreground whitespace-nowrap">
<span className="lg:pl-2">Status</span>
</th>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-right text-sm font-normal text-muted-foreground whitespace-nowrap">
<OrderHeader text={"Created at"} orderStatus={createdAtOrder} setOrder={(e) => {
resetOrder();
setCreatedAtOrder(e);
@@ -235,7 +216,7 @@ export default function ({ localServer }: { localServer: string }) {
setSortName("created_at");
}} />
</th>
<th className="sticky top-0 z-10 px-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<th className="sticky top-0 z-10 px-6 py-3 bg-muted text-right text-sm font-normal text-muted-foreground whitespace-nowrap">
<OrderHeader text={"Updated at"} orderStatus={updatedAtOrder} setOrder={(e) => {
resetOrder();
setUpdatedAtOrder(e);
@@ -243,7 +224,7 @@ export default function ({ localServer }: { localServer: string }) {
setSortName("updated_at");
}} />
</th>
<th className="sticky top-0 z-10 pr-6 py-3 border-gray-200 bg-gray-100 text-right text-xs font-medium text-gray-500 tracking-wider whitespace-nowrap">
<th className="sticky top-0 z-10 pr-6 py-3 bg-muted text-right text-sm font-normal text-muted-foreground whitespace-nowrap">
Action
</th>
</tr>