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

jedcunningham pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 8c5e1b2ae18 Add plugin menu items to navbar (#47749)
8c5e1b2ae18 is described below

commit 8c5e1b2ae18ef43800a822c1ae88dc0eddbd8ffe
Author: Brent Bovenzi <[email protected]>
AuthorDate: Fri Mar 14 11:31:28 2025 -0400

    Add plugin menu items to navbar (#47749)
---
 airflow/ui/src/layouts/Nav/Nav.tsx         |  2 +
 airflow/ui/src/layouts/Nav/PluginMenus.tsx | 92 ++++++++++++++++++++++++++++++
 2 files changed, 94 insertions(+)

diff --git a/airflow/ui/src/layouts/Nav/Nav.tsx 
b/airflow/ui/src/layouts/Nav/Nav.tsx
index 44315743404..0132a76576c 100644
--- a/airflow/ui/src/layouts/Nav/Nav.tsx
+++ b/airflow/ui/src/layouts/Nav/Nav.tsx
@@ -27,6 +27,7 @@ import { AdminButton } from "./AdminButton";
 import { BrowseButton } from "./BrowseButton";
 import { DocsButton } from "./DocsButton";
 import { NavButton } from "./NavButton";
+import { PluginMenus } from "./PluginMenus";
 import { SecurityButton } from "./SecurityButton";
 import { UserSettingsButton } from "./UserSettingsButton";
 
@@ -56,6 +57,7 @@ export const Nav = () => {
         <BrowseButton />
         <AdminButton />
         <SecurityButton />
+        <PluginMenus />
       </Flex>
       <Flex flexDir="column">
         <DocsButton />
diff --git a/airflow/ui/src/layouts/Nav/PluginMenus.tsx 
b/airflow/ui/src/layouts/Nav/PluginMenus.tsx
new file mode 100644
index 00000000000..c3234336fba
--- /dev/null
+++ b/airflow/ui/src/layouts/Nav/PluginMenus.tsx
@@ -0,0 +1,92 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 "@chakra-ui/react";
+import { FiChevronRight } from "react-icons/fi";
+import { LuPlug } from "react-icons/lu";
+
+import { usePluginServiceGetPlugins } from "openapi/queries";
+import type { AppBuilderMenuItemResponse } from "openapi/requests/types.gen";
+import { Menu } from "src/components/ui";
+
+import { NavButton } from "./NavButton";
+
+export const PluginMenus = () => {
+  const { data } = usePluginServiceGetPlugins();
+
+  const menuPlugins = data?.plugins.filter((plugin) => 
plugin.appbuilder_menu_items.length > 0);
+
+  if (data === undefined || menuPlugins === undefined) {
+    return undefined;
+  }
+
+  const categories: Record<string, Array<AppBuilderMenuItemResponse>> = {};
+  const buttons: Array<AppBuilderMenuItemResponse> = [];
+
+  menuPlugins.forEach((plugin) => {
+    plugin.appbuilder_menu_items.forEach((mi) => {
+      if (mi.category !== null && mi.category !== undefined) {
+        categories[mi.category] = [...(categories[mi.category] ?? []), mi];
+      } else {
+        buttons.push(mi);
+      }
+    });
+  });
+
+  if (!buttons.length && !Object.keys(categories).length) {
+    return undefined;
+  }
+
+  return (
+    <Menu.Root positioning={{ placement: "right" }}>
+      <Menu.Trigger>
+        <NavButton icon={<LuPlug />} title="Plugins" />
+      </Menu.Trigger>
+      <Menu.Content>
+        {buttons.map(({ href, name }) =>
+          href !== null && href !== undefined ? (
+            <Menu.Item asChild key={name} value={name}>
+              <Link aria-label={name} href={href} rel="noopener noreferrer" 
target="_blank">
+                {name}
+              </Link>
+            </Menu.Item>
+          ) : undefined,
+        )}
+        {Object.entries(categories).map(([key, menuButtons]) => (
+          <Menu.Root key={key} positioning={{ placement: "right" }}>
+            <Menu.TriggerItem display="flex" justifyContent="space-between">
+              {key}
+              <FiChevronRight />
+            </Menu.TriggerItem>
+            <Menu.Content>
+              {menuButtons.map(({ href, name }) =>
+                href !== undefined && href !== null ? (
+                  <Menu.Item asChild key={name} value={name}>
+                    <Link aria-label={name} href={href} rel="noopener 
noreferrer" target="_blank">
+                      {name}
+                    </Link>
+                  </Menu.Item>
+                ) : undefined,
+              )}
+            </Menu.Content>
+          </Menu.Root>
+        ))}
+      </Menu.Content>
+    </Menu.Root>
+  );
+};

Reply via email to