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 813351e328e69b91eacf6cbde667c864f0d43cde
Author: Yang Luo <[email protected]>
AuthorDate: Fri Sep 4 21:15:04 2026 +0800

    fix(web): tell the vendor-page link apart from the install button
---
 web/src/components/ToolInstallRow.tsx           | 226 +++++++++++++-----------
 web/src/components/ToolUpgradeConfirmDialog.tsx |  18 +-
 2 files changed, 134 insertions(+), 110 deletions(-)

diff --git a/web/src/components/ToolInstallRow.tsx 
b/web/src/components/ToolInstallRow.tsx
index 6301db2..d21a342 100644
--- a/web/src/components/ToolInstallRow.tsx
+++ b/web/src/components/ToolInstallRow.tsx
@@ -1,100 +1,126 @@
-// 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 {Bot, ExternalLink} from "lucide-react";
-import i18next from "i18next";
-
-import {AgentIcon} from "@/components/AgentIcon";
-import {InstallJobProgress} from "@/components/AgentInstallJob";
-import {AgentInstallButton} from "@/components/ToolUpgradeConfirmDialog";
-import {Badge} from "@/components/ui/badge";
-import {SimpleTooltip} from "@/components/ui/tooltip";
-import type {AgentCatalogEntry, AgentInstallJob} from "@/types";
-
-/**
- * One agent this machine does not have, with the click that installs it. The
- * install runs on the host with the package manager that is already there, so
- * the row shows which one it will use and what that manager is printing; an
- * agent no manager here publishes keeps the link to its vendor, which is then
- * all there is.
- */
-export function ToolInstallRow({
-  entry,
-  job,
-  busy,
-  onInstall,
-}: {
-  entry: AgentCatalogEntry;
-  /** The running or last install of this agent, when there was one. */
-  job?: AgentInstallJob;
-  busy: boolean;
-  onInstall: (agentId: string) => void;
-}) {
-  const plan = entry.install;
-  const failed = job !== undefined && !job.running && !job.ok;
-  const fallbackUrl = !plan.available ? entry.installUrl : undefined;
-
-  return (
-    <div className="bg-card hover:border-foreground/25 space-y-2 rounded-xl 
border p-3 shadow-xs transition-colors">
-      <div className="flex items-center gap-2.5">
-        {/* The icons are keyed by vendor name, which every entry has. */}
-        <AgentIcon
-          agent={entry.name}
-          size={20}
-          fallback={<Bot className="text-muted-foreground size-5" />}
-        />
-        <span className="min-w-0 flex-1 truncate text-sm 
font-medium">{entry.name}</span>
-
-        {entry.installUrl && !fallbackUrl ? (
-          <SimpleTooltip title={i18next.t("agent:Install page")}>
-            <a
-              href={entry.installUrl}
-              target="_blank"
-              rel="noreferrer"
-              className="text-muted-foreground hover:text-primary shrink-0"
-              aria-label={i18next.t("agent:Install page")}
-            >
-              <ExternalLink className="size-4" />
-            </a>
-          </SimpleTooltip>
-        ) : null}
-
-        <span className="shrink-0">
-          <AgentInstallButton
-            name={entry.name}
-            plan={plan}
-            installUrl={entry.installUrl}
-            job={job}
-            busy={busy}
-            label={failed ? i18next.t("agent:Retry") : undefined}
-            onInstall={() => onInstall(entry.agentId)}
-          />
-        </span>
-      </div>
-
-      <div className="flex flex-wrap items-center gap-2">
-        {plan.available ? (
-          <Badge variant="muted" className="text-[11px]">{plan.manager}</Badge>
-        ) : (
-          <span className="text-muted-foreground 
text-[11px]">{plan.detail}</span>
-        )}
-        {plan.available ? (
-          <code className="text-muted-foreground/80 min-w-0 truncate 
text-[11px]">{plan.command}</code>
-        ) : null}
-      </div>
-
-      <InstallJobProgress job={job} />
-    </div>
-  );
-}
+// 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 {Bot, ExternalLink, FolderSearch} from "lucide-react";
+import i18next from "i18next";
+
+import {AgentIcon} from "@/components/AgentIcon";
+import {AgentPathDialog} from "@/components/AgentPathDialog";
+import {InstallJobProgress} from "@/components/AgentInstallJob";
+import {AgentInstallButton} from "@/components/ToolUpgradeConfirmDialog";
+import {Badge} from "@/components/ui/badge";
+import {Button} from "@/components/ui/button";
+import {SimpleTooltip} from "@/components/ui/tooltip";
+import type {AgentCatalogEntry, AgentInstallJob} from "@/types";
+
+/**
+ * One agent this machine does not have, with the click that installs it. The
+ * install runs on the host with the package manager that is already there, so
+ * the row shows which one it will use and what that manager is printing; an
+ * agent no manager here publishes keeps the link to its vendor, which is then
+ * all there is.
+ *
+ * That link is a link and comes last in every card: an install runs a command
+ * here, the vendor's page leaves for a website, and an orange button each told
+ * them apart by nothing but an icon.
+ *
+ * An agent put somewhere no layout describes lands here too, so the card also
+ * offers the picker that points Gateway at its program.
+ */
+export function ToolInstallRow({
+  entry,
+  job,
+  busy,
+  onInstall,
+  onLocated,
+}: {
+  entry: AgentCatalogEntry;
+  /** The running or last install of this agent, when there was one. */
+  job?: AgentInstallJob;
+  busy: boolean;
+  onInstall: (agentId: string) => void;
+  /** Called once a program is picked, which is when a rescan will list it. */
+  onLocated?: () => void;
+}) {
+  const plan = entry.install;
+  const failed = job !== undefined && !job.running && !job.ok;
+
+  return (
+    <div className="bg-card hover:border-foreground/25 space-y-2 rounded-xl 
border p-3 shadow-xs transition-colors">
+      <div className="flex items-center gap-2.5">
+        {/* The icons are keyed by vendor name, which every entry has. */}
+        <AgentIcon
+          agent={entry.name}
+          size={20}
+          fallback={<Bot className="text-muted-foreground size-5" />}
+        />
+        <span className="min-w-0 flex-1 truncate text-sm 
font-medium">{entry.name}</span>
+
+        {plan.available ? (
+          <span className="shrink-0">
+            <AgentInstallButton
+              name={entry.name}
+              plan={plan}
+              job={job}
+              busy={busy}
+              label={failed ? i18next.t("agent:Retry") : undefined}
+              onInstall={() => onInstall(entry.agentId)}
+            />
+          </span>
+        ) : null}
+
+        {entry.installUrl ? (
+          <SimpleTooltip title={i18next.t("agent:Install page")}>
+            <a
+              href={entry.installUrl}
+              target="_blank"
+              rel="noreferrer"
+              className="text-muted-foreground hover:text-primary shrink-0"
+              aria-label={i18next.t("agent:Install page")}
+            >
+              <ExternalLink className="size-4" />
+            </a>
+          </SimpleTooltip>
+        ) : null}
+      </div>
+
+      <div className="flex flex-wrap items-center gap-2">
+        {plan.available ? (
+          <Badge variant="muted" className="text-[11px]">{plan.manager}</Badge>
+        ) : (
+          <span className="text-muted-foreground 
text-[11px]">{plan.detail}</span>
+        )}
+        {plan.available ? (
+          <code className="text-muted-foreground/80 min-w-0 truncate 
text-[11px]">{plan.command}</code>
+        ) : null}
+
+        <span className="ml-auto shrink-0">
+          <AgentPathDialog
+            agentId={entry.agentId}
+            name={entry.name}
+            onAdded={onLocated}
+            trigger={
+              <Button size="xs" variant="ghost" 
className="text-muted-foreground">
+                <FolderSearch />
+                {i18next.t("agent:Locate")}
+              </Button>
+            }
+          />
+        </span>
+      </div>
+
+      <InstallJobProgress job={job} />
+    </div>
+  );
+}
diff --git a/web/src/components/ToolUpgradeConfirmDialog.tsx 
b/web/src/components/ToolUpgradeConfirmDialog.tsx
index 62a07c2..9fa8c62 100644
--- a/web/src/components/ToolUpgradeConfirmDialog.tsx
+++ b/web/src/components/ToolUpgradeConfirmDialog.tsx
@@ -30,9 +30,9 @@ export {InstallJobProgress, InstallLogDialog, InstallOutput} 
from "@/components/
  * updater, or the vendor's install command - the command is spelled out first
  * and confirmed before it runs.
  *
- * An agent nothing here can drive keeps a click of its own: the vendor's page,
- * which is where that install has to be done by hand. A button that only greys
- * out tells a person nothing they can act on.
+ * An agent nothing here can drive keeps a way out: the vendor's page, where
+ * that install has to be done by hand. It is a link, not a button - it leaves
+ * for a website instead of doing the thing the button offered.
  */
 export function AgentActionButton({
   title,
@@ -74,13 +74,11 @@ export function AgentActionButton({
   if (!plan?.available && fallbackUrl) {
     return (
       <SimpleTooltip title={plan?.detail}>
-        <Button
-          size="sm"
-          variant={variant}
-          onClick={() => window.open(fallbackUrl, "_blank", "noreferrer")}
-        >
-          <ExternalLink />
-          {fallbackLabel ?? i18next.t("agent:Vendor page")}
+        <Button size="sm" variant="link" asChild>
+          <a href={fallbackUrl} target="_blank" rel="noreferrer">
+            {fallbackLabel ?? i18next.t("agent:Vendor page")}
+            <ExternalLink className="size-3.5" />
+          </a>
         </Button>
       </SimpleTooltip>
     );


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to