You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@casbin.apache.org by hs...@apache.org on 2026/09/04 05:57:05 UTC

(casbin-gateway) branch master updated (194a7be -> d06bfbb)

This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git


    from 194a7be  style(header): improve language and account menus
     new feaf78c  fix(agent): return a string row key for agent records
     new d06bfbb  feat(audit): link authenticity tiles to their provider card

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 web/src/components/AuthenticityOverview.tsx | 351 ++++++++++++++--------------
 web/src/components/ProviderAuditPanel.tsx   |  62 +++--
 web/src/pages/AgentDetailPage.tsx           |   2 +-
 web/src/pages/AuthenticityPage.tsx          |   4 +-
 4 files changed, 229 insertions(+), 190 deletions(-)


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@casbin.apache.org
For additional commands, e-mail: commits-help@casbin.apache.org


(casbin-gateway) 02/02: feat(audit): link authenticity tiles to their provider card

Posted by hs...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git

commit d06bfbbeb713e41dc399dc592bf7ca8fcb0128ea
Author: Yang Luo <hs...@gmail.com>
AuthorDate: Fri Sep 4 13:56:57 2026 +0800

    feat(audit): link authenticity tiles to their provider card
---
 web/src/components/AuthenticityOverview.tsx | 351 ++++++++++++++--------------
 web/src/components/ProviderAuditPanel.tsx   |  62 +++--
 web/src/pages/AuthenticityPage.tsx          |   4 +-
 3 files changed, 228 insertions(+), 189 deletions(-)

diff --git a/web/src/components/AuthenticityOverview.tsx b/web/src/components/AuthenticityOverview.tsx
index 8d8cabd..3580fc9 100644
--- a/web/src/components/AuthenticityOverview.tsx
+++ b/web/src/components/AuthenticityOverview.tsx
@@ -1,174 +1,177 @@
-// Copyright 2026 The casbin Authors. All Rights Reserved.
-//
-// 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 * as React from "react";
-import {Link} from "react-router-dom";
-import {ShieldCheck} from "lucide-react";
-import i18next from "i18next";
-
-import * as ProviderBackend from "@/backend/ProviderBackend";
-import {ScoreBadge, ScoreDial} from "@/components/AuthenticityScore";
-import {ProviderIcon} from "@/components/ProviderIcon";
-import {Badge} from "@/components/ui/badge";
-import {Button} from "@/components/ui/button";
-import {Card, CardContent} from "@/components/ui/card";
-import {cn} from "@/lib/utils";
-import {gradeStyleOf, probeFindings, summarizeAuthenticity} from "@/lib/authenticity";
-import {providerIdOf} from "@/lib/providers";
-import type {Provider, ProviderProbe, ProviderProbeMode} from "@/types";
-
-/** How often the scores are read again while the page is open. */
-const refreshMs = 20000;
-
-/** Fills {name} placeholders, which is all the interpolation these strings need. */
-function fill(key: string, values: Record<string, string | number>) {
-  return Object.entries(values).reduce(
-    (text, [name, value]) => text.split(`{${name}}`).join(String(value)),
-    i18next.t(key),
-  );
-}
-
-/** One provider's line: who it is, what it scored, and what failed if anything. */
-function ProviderScore({provider, probe}: {provider: Provider; probe?: ProviderProbe}) {
-  const {alerts, warnings} = probeFindings(probe);
-  const name = provider.displayName || provider.name;
-
-  return (
-    <div className="hover:border-foreground/20 flex min-w-0 items-center gap-2 rounded-lg border px-2.5 py-1.5 transition-colors">
-      <ProviderIcon icon={provider.icon} baseUrl={provider.baseUrl} alt={name} size={16} />
-      <span className="truncate text-xs">{name}</span>
-      <div className="ml-auto flex shrink-0 items-center gap-1.5">
-        {alerts.length > 0 ? (
-          <Badge variant="danger">{fill("audit:Failed cases", {count: alerts.length})}</Badge>
-        ) : warnings.length > 0 ? (
-          <Badge variant="warning">{fill("audit:Flagged cases", {count: warnings.length})}</Badge>
-        ) : null}
-        <ScoreBadge probe={probe} showLabel={false} />
-      </div>
-    </div>
-  );
-}
-
-/**
- * Whether the APIs on this machine are what they are sold as, in one card. It
- * is measured without being asked for: every provider is probed when it is
- * added and again once its report goes stale, so this says something before
- * anyone thinks to check.
- */
-export function AuthenticityOverview({
-  providers,
-  showLink = true,
-  className,
-}: {
-  providers: Provider[];
-  /** False on the page this links to, where the link would point at itself. */
-  showLink?: boolean;
-  className?: string;
-}) {
-  const [probes, setProbes] = React.useState<ProviderProbe[]>([]);
-  const [mode, setMode] = React.useState<ProviderProbeMode>("auto");
-  const [loaded, setLoaded] = React.useState(false);
-
-  React.useEffect(() => {
-    const load = () => {
-      ProviderBackend.getProviderProbes()
-        .then(res => {
-          if (res.status !== "ok") {
-            return;
-          }
-          setProbes(res.data ?? []);
-          setMode(res.data2 ?? "auto");
-        })
-        .catch(() => undefined)
-        .then(() => setLoaded(true));
-    };
-
-    load();
-    const interval = setInterval(load, refreshMs);
-    return () => clearInterval(interval);
-  }, []);
-
-  const configured = providers.filter(provider => provider.status !== "disabled");
-  const byProvider = new Map(probes.map(probe => [probe.provider, probe]));
-  const scored = configured
-    .map(provider => ({provider: provider, probe: byProvider.get(providerIdOf(provider))}))
-    .sort((left, right) => {
-      const score = (entry: {probe?: ProviderProbe}) =>
-        entry.probe?.ok && entry.probe.grade !== "unknown" ? entry.probe.score : Number.POSITIVE_INFINITY;
-      return score(left) - score(right);
-    });
-
-  const summary = summarizeAuthenticity(
-    scored.filter(entry => entry.probe).map(entry => entry.probe as ProviderProbe),
-    configured.length,
-  );
-  // The headline grade is the worst one held, not the average: a machine with
-  // four honest providers and one that is not is not four fifths fine.
-  const worst = scored.find(entry => entry.probe?.ok && entry.probe.grade !== "unknown")?.probe;
-  const style = gradeStyleOf(worst?.grade);
-
-  if (configured.length === 0) {
-    return null;
-  }
-
-  return (
-    <Card className={cn("gap-0 py-0 shadow-xs", className)}>
-      <CardContent className="flex flex-col gap-3 p-4">
-        <div className="flex flex-wrap items-center gap-3">
-          <ScoreDial probe={worst} size={56} />
-          <div className="flex min-w-0 flex-col gap-0.5">
-            <div className="flex items-center gap-1.5">
-              <ShieldCheck className="text-muted-foreground size-3.5" />
-              <span className="text-sm font-medium">{i18next.t("audit:Authenticity")}</span>
-            </div>
-            <span className="text-xs">
-              {summary.graded === 0
-                ? i18next.t(mode === "off" ? "audit:Probing is off" : "audit:Measuring now")
-                : i18next.t(style.verdict)}
-            </span>
-            <span className="text-muted-foreground text-[11px]">
-              {fill("audit:Graded providers", {
-                graded: summary.graded,
-                total: configured.length,
-              })}
-              {summary.alerting > 0 ? ` · ${fill("audit:Providers alerting", {count: summary.alerting})}` : ""}
-              {summary.ungraded > 0 ? ` · ${fill("audit:Providers unmeasured", {count: summary.ungraded})}` : ""}
-            </span>
-          </div>
-
-          {showLink ? (
-            <Button asChild variant="outline" size="sm" className="ml-auto">
-              <Link to="/authenticity">
-                <ShieldCheck />
-                {i18next.t("audit:See the evidence")}
-              </Link>
-            </Button>
-          ) : null}
-        </div>
-
-        {loaded ? (
-          <div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
-            {scored.map(entry => (
-              <ProviderScore
-                key={providerIdOf(entry.provider)}
-                provider={entry.provider}
-                probe={entry.probe}
-              />
-            ))}
-          </div>
-        ) : null}
-      </CardContent>
-    </Card>
-  );
-}
+// Copyright 2026 The casbin Authors. All Rights Reserved.
+//
+// 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 * as React from "react";
+import {Link} from "react-router-dom";
+import {ShieldCheck} from "lucide-react";
+import i18next from "i18next";
+
+import * as ProviderBackend from "@/backend/ProviderBackend";
+import {ScoreBadge, ScoreDial} from "@/components/AuthenticityScore";
+import {ProviderIcon} from "@/components/ProviderIcon";
+import {Badge} from "@/components/ui/badge";
+import {Button} from "@/components/ui/button";
+import {Card, CardContent} from "@/components/ui/card";
+import {cn} from "@/lib/utils";
+import {gradeStyleOf, probeFindings, summarizeAuthenticity} from "@/lib/authenticity";
+import {providerIdOf} from "@/lib/providers";
+import type {Provider, ProviderProbe, ProviderProbeMode} from "@/types";
+
+/** How often the scores are read again while the page is open. */
+const refreshMs = 20000;
+
+/** Fills {name} placeholders, which is all the interpolation these strings need. */
+function fill(key: string, values: Record<string, string | number>) {
+  return Object.entries(values).reduce(
+    (text, [name, value]) => text.split(`{${name}}`).join(String(value)),
+    i18next.t(key),
+  );
+}
+
+/** One provider's line: who it is, what it scored, and what failed if anything. */
+function ProviderScore({provider, probe}: {provider: Provider; probe?: ProviderProbe}) {
+  const {alerts, warnings} = probeFindings(probe);
+  const name = provider.displayName || provider.name;
+
+  return (
+    <Link
+      to={`/authenticity?tab=report&provider=${encodeURIComponent(providerIdOf(provider))}`}
+      className="hover:border-foreground/20 hover:bg-accent/40 flex min-w-0 items-center gap-2 rounded-lg border px-2.5 py-1.5 transition-colors"
+    >
+      <ProviderIcon icon={provider.icon} baseUrl={provider.baseUrl} alt={name} size={16} />
+      <span className="truncate text-xs">{name}</span>
+      <div className="ml-auto flex shrink-0 items-center gap-1.5">
+        {alerts.length > 0 ? (
+          <Badge variant="danger">{fill("audit:Failed cases", {count: alerts.length})}</Badge>
+        ) : warnings.length > 0 ? (
+          <Badge variant="warning">{fill("audit:Flagged cases", {count: warnings.length})}</Badge>
+        ) : null}
+        <ScoreBadge probe={probe} showLabel={false} />
+      </div>
+    </Link>
+  );
+}
+
+/**
+ * Whether the APIs on this machine are what they are sold as, in one card. It
+ * is measured without being asked for: every provider is probed when it is
+ * added and again once its report goes stale, so this says something before
+ * anyone thinks to check.
+ */
+export function AuthenticityOverview({
+  providers,
+  showLink = true,
+  className,
+}: {
+  providers: Provider[];
+  /** False on the page this links to, where the link would point at itself. */
+  showLink?: boolean;
+  className?: string;
+}) {
+  const [probes, setProbes] = React.useState<ProviderProbe[]>([]);
+  const [mode, setMode] = React.useState<ProviderProbeMode>("auto");
+  const [loaded, setLoaded] = React.useState(false);
+
+  React.useEffect(() => {
+    const load = () => {
+      ProviderBackend.getProviderProbes()
+        .then(res => {
+          if (res.status !== "ok") {
+            return;
+          }
+          setProbes(res.data ?? []);
+          setMode(res.data2 ?? "auto");
+        })
+        .catch(() => undefined)
+        .then(() => setLoaded(true));
+    };
+
+    load();
+    const interval = setInterval(load, refreshMs);
+    return () => clearInterval(interval);
+  }, []);
+
+  const configured = providers.filter(provider => provider.status !== "disabled");
+  const byProvider = new Map(probes.map(probe => [probe.provider, probe]));
+  const scored = configured
+    .map(provider => ({provider: provider, probe: byProvider.get(providerIdOf(provider))}))
+    .sort((left, right) => {
+      const score = (entry: {probe?: ProviderProbe}) =>
+        entry.probe?.ok && entry.probe.grade !== "unknown" ? entry.probe.score : Number.POSITIVE_INFINITY;
+      return score(left) - score(right);
+    });
+
+  const summary = summarizeAuthenticity(
+    scored.filter(entry => entry.probe).map(entry => entry.probe as ProviderProbe),
+    configured.length,
+  );
+  // The headline grade is the worst one held, not the average: a machine with
+  // four honest providers and one that is not is not four fifths fine.
+  const worst = scored.find(entry => entry.probe?.ok && entry.probe.grade !== "unknown")?.probe;
+  const style = gradeStyleOf(worst?.grade);
+
+  if (configured.length === 0) {
+    return null;
+  }
+
+  return (
+    <Card className={cn("gap-0 py-0 shadow-xs", className)}>
+      <CardContent className="flex flex-col gap-3 p-4">
+        <div className="flex flex-wrap items-center gap-3">
+          <ScoreDial probe={worst} size={56} />
+          <div className="flex min-w-0 flex-col gap-0.5">
+            <div className="flex items-center gap-1.5">
+              <ShieldCheck className="text-muted-foreground size-3.5" />
+              <span className="text-sm font-medium">{i18next.t("audit:Authenticity")}</span>
+            </div>
+            <span className="text-xs">
+              {summary.graded === 0
+                ? i18next.t(mode === "off" ? "audit:Probing is off" : "audit:Measuring now")
+                : i18next.t(style.verdict)}
+            </span>
+            <span className="text-muted-foreground text-[11px]">
+              {fill("audit:Graded providers", {
+                graded: summary.graded,
+                total: configured.length,
+              })}
+              {summary.alerting > 0 ? ` · ${fill("audit:Providers alerting", {count: summary.alerting})}` : ""}
+              {summary.ungraded > 0 ? ` · ${fill("audit:Providers unmeasured", {count: summary.ungraded})}` : ""}
+            </span>
+          </div>
+
+          {showLink ? (
+            <Button asChild variant="outline" size="sm" className="ml-auto">
+              <Link to="/authenticity">
+                <ShieldCheck />
+                {i18next.t("audit:See the evidence")}
+              </Link>
+            </Button>
+          ) : null}
+        </div>
+
+        {loaded ? (
+          <div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
+            {scored.map(entry => (
+              <ProviderScore
+                key={providerIdOf(entry.provider)}
+                provider={entry.provider}
+                probe={entry.probe}
+              />
+            ))}
+          </div>
+        ) : null}
+      </CardContent>
+    </Card>
+  );
+}
diff --git a/web/src/components/ProviderAuditPanel.tsx b/web/src/components/ProviderAuditPanel.tsx
index 1ca9dfb..007abb5 100644
--- a/web/src/components/ProviderAuditPanel.tsx
+++ b/web/src/components/ProviderAuditPanel.tsx
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import * as React from "react";
-import {Link} from "react-router-dom";
+import {Link, useLocation} from "react-router-dom";
 import {Logs, RefreshCw, Stethoscope} from "lucide-react";
 import i18next from "i18next";
 
@@ -28,6 +28,7 @@ import {SimpleSelect} from "@/components/shared/simple-select";
 import {MessageAlert} from "@/components/ui/alert";
 import {Button} from "@/components/ui/button";
 import {Card} from "@/components/ui/card";
+import {cn} from "@/lib/utils";
 import {providerIdOf} from "@/lib/providers";
 import type {
   LlmAuditReport,
@@ -116,7 +117,7 @@ function buildRows(
  * documented, which is the only way to learn something a real request never
  * happened to show — and spends a few cents of that provider's own credit.
  */
-export function ProviderAuditPanel({owner}: {owner: string}) {
+export function ProviderAuditPanel({owner, focus = ""}: {owner: string; focus?: string}) {
   const [hours, setHours] = React.useState(24 * 7);
   const [report, setReport] = React.useState<LlmAuditReport | null>(null);
   // Null until a listing lands. An empty list and "not asked yet" must not
@@ -204,6 +205,31 @@ export function ProviderAuditPanel({owner}: {owner: string}) {
 
   const rows = buildRows(providers ?? [], report?.providers ?? [], probes);
 
+  // A card linked to from the overview is scrolled to and lit for a moment,
+  // otherwise the click lands on a page that looks unchanged.
+  const [lit, setLit] = React.useState("");
+  // Keyed on the navigation, so clicking the same tile twice scrolls again.
+  const navigation = useLocation().key;
+
+  React.useEffect(() => {
+    if (focus === "") {
+      return;
+    }
+    const scroll = (behavior: ScrollBehavior) =>
+      document
+        .querySelector(`[data-audit-provider="${CSS.escape(focus)}"]`)
+        ?.scrollIntoView({behavior: behavior, block: "start"});
+    scroll("smooth");
+    // Landed on exactly, once the panels above have loaded and moved the card.
+    const again = setTimeout(() => scroll("auto"), 600);
+    setLit(focus);
+    const off = setTimeout(() => setLit(""), 2500);
+    return () => {
+      clearTimeout(again);
+      clearTimeout(off);
+    };
+  }, [focus, navigation, rows.length, loading]);
+
   return (
     <div className="space-y-3">
       <div className="flex flex-wrap items-start justify-between gap-2">
@@ -272,19 +298,27 @@ export function ProviderAuditPanel({owner}: {owner: string}) {
         <>
           <div className="space-y-3">
             {rows.map(row => (
-              <ProviderAuditCard
+              <div
                 key={row.id}
-                audit={row.audit}
-                probe={row.probe}
-                cases={cases}
-                provider={row.provider}
-                providersKnown={providers !== null}
-                probeMode={probeMode}
-                probing={probing.includes(row.id)}
-                // A provider the records still name but nothing is configured
-                // for has no endpoint left to ask.
-                onProbe={row.provider && probeMode !== "off" ? () => runProbe(row.id) : undefined}
-              />
+                data-audit-provider={row.id}
+                className={cn(
+                  "scroll-mt-20 rounded-xl transition-shadow",
+                  lit === row.id ? "ring-primary/60 ring-2" : "",
+                )}
+              >
+                <ProviderAuditCard
+                  audit={row.audit}
+                  probe={row.probe}
+                  cases={cases}
+                  provider={row.provider}
+                  providersKnown={providers !== null}
+                  probeMode={probeMode}
+                  probing={probing.includes(row.id)}
+                  // A provider the records still name but nothing is configured
+                  // for has no endpoint left to ask.
+                  onProbe={row.provider && probeMode !== "off" ? () => runProbe(row.id) : undefined}
+                />
+              </div>
             ))}
           </div>
           <p className="text-muted-foreground text-xs">
diff --git a/web/src/pages/AuthenticityPage.tsx b/web/src/pages/AuthenticityPage.tsx
index 1c6b637..6b6121a 100644
--- a/web/src/pages/AuthenticityPage.tsx
+++ b/web/src/pages/AuthenticityPage.tsx
@@ -39,6 +39,8 @@ export default function AuthenticityPage({account}: {account: Account}) {
   // The tab lives in the URL so the rail can link straight to one of them.
   const [searchParams, setSearchParams] = useSearchParams();
   const tab: AuthenticityTab = searchParams.get("tab") === "cases" ? "cases" : "report";
+  // Set when the overview above was clicked, so the report opens at that card.
+  const focus = searchParams.get("provider") ?? "";
   const setTab = (next: AuthenticityTab) =>
     setSearchParams(
       previous => {
@@ -78,7 +80,7 @@ export default function AuthenticityPage({account}: {account: Account}) {
         </TabsList>
       </Tabs>
 
-      {tab === "report" ? <ProviderAuditPanel owner={account.name} /> : <ProbeCaseList />}
+      {tab === "report" ? <ProviderAuditPanel owner={account.name} focus={focus} /> : <ProbeCaseList />}
     </PageContainer>
   );
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@casbin.apache.org
For additional commands, e-mail: commits-help@casbin.apache.org


(casbin-gateway) 01/02: fix(agent): return a string row key for agent records

Posted by hs...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git

commit feaf78c45e2ed05bfb075ffeb6e1ed93d774f2f3
Author: Yang Luo <hs...@gmail.com>
AuthorDate: Fri Sep 4 13:08:41 2026 +0800

    fix(agent): return a string row key for agent records
---
 web/src/pages/AgentDetailPage.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web/src/pages/AgentDetailPage.tsx b/web/src/pages/AgentDetailPage.tsx
index c33803f..5c392cd 100644
--- a/web/src/pages/AgentDetailPage.tsx
+++ b/web/src/pages/AgentDetailPage.tsx
@@ -607,7 +607,7 @@ export default function AgentDetailPage({account}: {account: Account}) {
             <DataTable
               columns={recordColumns}
               dataSource={records}
-              rowKey={record => record.id}
+              rowKey={record => String(record.id)}
               pageSize={0}
               emptyText={i18next.t("agent:Monitoring, no activity yet")}
             />


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@casbin.apache.org
For additional commands, e-mail: commits-help@casbin.apache.org