mirror of
https://github.com/go-sigma/sigma.git
synced 2026-10-28 15:34:34 +08:00
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:
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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user