You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by sc...@apache.org on 2025/09/23 14:49:36 UTC

(nifi) branch main updated: NIFI-14319: Create initial project structure for new NiFi Registry UI (#9855)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new 1c995f4876 NIFI-14319: Create initial project structure for new NiFi Registry UI (#9855)
1c995f4876 is described below

commit 1c995f487670db43a4cd33afc13eacccd865c0ff
Author: Shane Ardell <sa...@users.noreply.github.com>
AuthorDate: Tue Sep 23 09:49:29 2025 -0500

    NIFI-14319: Create initial project structure for new NiFi Registry UI (#9855)
    
    * NIFI-14319: init new registry app
    
    * NIFI-14319: add unit test coverage
    
    * NIFI-14319: update redirect value
    
    * Remove commented code in droplet filter
    
    * NIFI-14319: remove commented code from current user effects
    
    * NIFI-14319: remove commented code from bucket reducer
    
    * NIFI-14319: remove commented code from index.ts
    
    * NIFI-14319: address initial feedback
    
    * check if window.matchMedia has value before calling method
    * add empty states to explorer table
    * destroy direct store subscriptions
    * add filtered count to droplet table filter
    
    * NIFI-14319: add wrongly removed favicon back to old project
    
    * NIFI-14319: fix CI build failures
    
    * Linux solution: https://github.com/evanw/esbuild/issues/1819\#issuecomment-2426254293
    * Windows solution: https://github.com/nrwl/nx/issues/29551
    
    * NIFI-14319: fix alt value used for registry logo
    
    * NIFI-14319: review feedback
    
    * fix error service
    
    * NIFI-14319: address naming and formatting feedback
    
    * NIFI-14319: address review feedback
    
    * fix multiple namings
    * template cleanup
    * configuration changes to align better with NiFi
    * style fixes
    
    * NIFI-14319: refactor droplet table into standalone component
    
    * NIFI-14319: align banner error handling with NiFi UX
    
    * NIFI-14319: align file upload UX with NiFi UI
    
    * NIFI-14319: additional dialog header and button style tweaks
    
    * NIFI-14319: update logo styling
    
    * NIFI-14319: error handling improvements
    
    * NIFI-14319: use NiFi file pick UX in import new flow dialog
    
    * NIFI-14319: remove unused directory
    
    * NIFI-14319: cleanup action types and fix missing import description
    
    * NIFI-14319: address feedback
    
    * use px units instead of calc for logo container
    * use unique contexts for dialog errors
    * update explorer page title
    * margin and sizing class updates
    * run prettier to fix formatting issues
    
    * NIFI-14319: move context error into shared lib
    
    * NIFI-14319: remove current user code
    
    * NIFI-14319: remove old context error code from registry
    
    * NIFI-14319: update explorer naming to resources
    
    * NIFI-14319: remove circular dependency
    
    * NIFI-14319: remove error abstractions from shared folder
    
    * NIFI-14319: update out-of-sync lock file
    
    * NIFI-14319: a few minor changes
    
    * remove rej file
    * remove unused deps
    * add standalone to components missing that configuration
    * hadle legacy routes
    
    * NIFI-14319: restore ignore path for favicon in old ui gitignore
    
    * NIFI-14319: update lock file
    
    * NIFI-14319: fix build issues
---
 .../frontend/apps/nifi-registry/.eslintrc.json     |  46 +++
 .../frontend/apps/nifi-registry/jest.config.ts     |  38 +++
 .../main/frontend/apps/nifi-registry/project.json  | 134 ++++++++
 .../frontend/apps/nifi-registry/proxy.config.mjs   |  47 +++
 .../src/app/app-routing.module.ts}                 |  52 +--
 .../apps/nifi-registry/src/app/app.component.html  |  23 ++
 .../src/app/app.component.scss}                    |  15 -
 .../src/app/app.component.spec.ts}                 |  19 +-
 .../apps/nifi-registry/src/app/app.component.ts    |  50 +++
 .../apps/nifi-registry/src/app/app.module.ts       |  90 +++++
 .../resources/feature/resources-routing.module.ts  |  65 ++++
 .../resources/feature/resources.component.html     |  57 ++++
 .../resources/feature/resources.component.scss}    |  13 +-
 .../resources/feature/resources.component.spec.ts  |  55 +++
 .../pages/resources/feature/resources.component.ts | 186 ++++++++++
 .../pages/resources/feature/resources.module.ts    |  53 +++
 .../delete-droplet-dialog.component.html           |  28 ++
 .../delete-droplet-dialog.component.scss}          |  11 +-
 .../delete-droplet-dialog.component.spec.ts        |  81 +++++
 .../delete-droplet-dialog.component.ts             |  54 +++
 .../droplet-table-filter.component.html            |  53 +++
 .../droplet-table-filter.component.scss}           |  15 -
 .../droplet-table-filter.component.spec.ts}        |  18 +-
 .../droplet-table-filter.component.ts              | 142 ++++++++
 .../ui/droplet-table/droplet-table.component.html  | 135 ++++++++
 .../ui/droplet-table/droplet-table.component.scss} |  13 -
 .../droplet-table/droplet-table.component.spec.ts  | 134 ++++++++
 .../ui/droplet-table/droplet-table.component.ts    | 137 ++++++++
 .../droplet-versions-dialog.component.html         |  94 +++++
 .../droplet-versions-dialog.component.scss}        |  11 +-
 .../droplet-versions-dialog.component.spec.ts      |  99 ++++++
 .../droplet-versions-dialog.component.ts           | 133 ++++++++
 .../export-droplet-version-dialog.component.html   |  49 +++
 .../export-droplet-version-dialog.component.scss}  |  11 +-
 ...export-droplet-version-dialog.component.spec.ts |  82 +++++
 .../export-droplet-version-dialog.component.ts     |  70 ++++
 .../import-new-droplet-dialog.component.html       | 100 ++++++
 .../import-new-droplet-dialog.component.scss}      |  11 +-
 .../import-new-droplet-dialog.component.spec.ts    | 143 ++++++++
 .../import-new-droplet-dialog.component.ts         | 136 ++++++++
 ...mport-new-droplet-version-dialog.component.html |  85 +++++
 ...port-new-droplet-version-dialog.component.scss} |  11 +-
 ...rt-new-droplet-version-dialog.component.spec.ts |  94 +++++
 .../import-new-droplet-version-dialog.component.ts | 115 +++++++
 .../src/app/service/buckets.service.ts             |  42 +++
 .../src/app/service/droplets.service.ts            | 136 ++++++++
 .../src/app/service/error-helper.service.ts        |  45 +++
 .../src/app/state/buckets/buckets.actions.ts}      |  19 +-
 .../src/app/state/buckets/buckets.effects.ts       |  65 ++++
 .../src/app/state/buckets/buckets.reducer.ts}      |  31 +-
 .../src/app/state/buckets/buckets.selectors.ts}    |  18 +-
 .../src/app/state/buckets/index.ts}                |  31 +-
 .../src/app/state/droplets/droplets.actions.ts     | 111 ++++++
 .../src/app/state/droplets/droplets.effects.ts     | 379 +++++++++++++++++++++
 .../src/app/state/droplets/droplets.reducer.ts     | 114 +++++++
 .../src/app/state/droplets/droplets.selectors.ts}  |  24 +-
 .../nifi-registry/src/app/state/droplets/index.ts  |  67 ++++
 .../src/app/state/error/error.actions.ts}          |  16 +-
 .../src/app/state/error/error.effects.ts}          |  16 +-
 .../src/app/state/error/error.reducer.ts           |  51 +++
 .../src/app/state/error/error.selectors.ts}        |  17 +-
 .../src/app/state/error/index.ts}                  |  34 +-
 .../apps/nifi-registry/src/app/state/index.ts      |  67 ++++
 .../context-error-banner.component.html            |  20 ++
 .../context-error-banner.component.scss            |  18 +
 .../context-error-banner.component.spec.ts         |  50 +++
 .../context-error-banner.component.ts              |   6 +-
 .../app/ui/header/_header.component-theme.scss}    |  32 +-
 .../src/app/ui/header/header.component.html        | 115 +++++++
 .../src/app/ui/header/header.component.scss}       |  38 ++-
 .../src/app/ui/header/header.component.spec.ts}    |  14 +-
 .../src/app/ui/header/header.component.ts          |  73 ++++
 .../src/environments/environment.development.ts}   |  17 +-
 .../src/environments/environment.ts}               |  17 +-
 .../frontend/apps/nifi-registry/src/index.html     |  29 ++
 .../src/main.ts}                                   |  17 +-
 .../src/styles.scss}                               |  32 +-
 .../src/test-setup.ts}                             |  15 +-
 .../tailwind.config.js}                            |  26 +-
 .../frontend/apps/nifi-registry/tsconfig.app.json  |   9 +
 .../main/frontend/apps/nifi-registry/tsconfig.json |  19 ++
 .../frontend/apps/nifi-registry/tsconfig.spec.json |  11 +
 .../main/frontend/apps/nifi/src/app/app.module.ts  |  20 +-
 .../component-access-policies.module.ts            |  10 +-
 .../global-access-policies.module.ts               |   8 +-
 .../app/pages/cluster/feature/cluster.module.ts    |   4 +-
 .../service/canvas-context-menu.service.ts         |   4 +-
 .../flow-designer/state/flow/flow.effects.spec.ts  |   4 -
 .../change-version-progress-dialog.ts              |   2 +-
 .../context-error-banner.component.ts              |   2 +-
 .../src/assets/icons/registry-background-logo.svg  |  17 +
 .../shared/src/assets/icons/registry-favicon.png   | Bin 0 -> 388 bytes
 .../src/assets/icons/registry-logo-web-app.svg     |  17 +
 .../error-banner/error-banner.component.html       |   2 +-
 .../error-banner/error-banner.component.scss       |   0
 .../error-banner/error-banner.component.spec.ts    |   0
 .../error-banner/error-banner.component.ts         |   3 +-
 .../frontend/libs/shared/src/components/index.ts   |   1 +
 98 files changed, 4579 insertions(+), 364 deletions(-)

diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/.eslintrc.json b/nifi-frontend/src/main/frontend/apps/nifi-registry/.eslintrc.json
new file mode 100644
index 0000000000..1312176600
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/.eslintrc.json
@@ -0,0 +1,46 @@
+{
+    "root": true,
+    "ignorePatterns": ["!**/*"],
+    "parserOptions": {
+        "ecmaVersion": "latest"
+    },
+    "overrides": [
+        {
+            "files": ["*.ts"],
+            "extends": [
+                "plugin:@nx/angular",
+                "eslint:recommended",
+                "plugin:@typescript-eslint/recommended",
+                "plugin:@angular-eslint/recommended",
+                "plugin:@angular-eslint/template/process-inline-templates",
+                "plugin:prettier/recommended"
+            ],
+            "rules": {
+                "@angular-eslint/directive-selector": [
+                    "error",
+                    {
+                        "type": "attribute",
+                        "prefix": "",
+                        "style": "camelCase"
+                    }
+                ],
+                "@angular-eslint/component-selector": [
+                    "error",
+                    {
+                        "type": "element",
+                        "prefix": "",
+                        "style": "kebab-case"
+                    }
+                ],
+                "@angular-eslint/component-class-suffix": "off",
+                "@typescript-eslint/no-explicit-any": "off",
+                "@typescript-eslint/ban-ts-comment": "off",
+                "@typescript-eslint/no-non-null-assertion": "off",
+                "@typescript-eslint/no-this-alias": "warn",
+                "@typescript-eslint/no-unused-vars": "warn",
+                "no-useless-escape": "off",
+                "@angular-eslint/prefer-standalone": "off"
+            }
+        }
+    ]
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/jest.config.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/jest.config.ts
new file mode 100644
index 0000000000..3b71ab7bfb
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/jest.config.ts
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+export default {
+    displayName: 'NiFi Registry',
+    preset: '../../jest.preset.js',
+    setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
+    coverageDirectory: '../../coverage/apps/nifi-registry',
+    transform: {
+        '^.+\\.(ts|mjs|js|html)$': [
+            'jest-preset-angular',
+            {
+                tsconfig: '<rootDir>/tsconfig.spec.json',
+                stringifyContentPathRegex: '\\.(html|svg)$'
+            }
+        ]
+    },
+    transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+    snapshotSerializers: [
+        'jest-preset-angular/build/serializers/no-ng-attributes',
+        'jest-preset-angular/build/serializers/ng-snapshot',
+        'jest-preset-angular/build/serializers/html-comment'
+    ]
+};
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/project.json b/nifi-frontend/src/main/frontend/apps/nifi-registry/project.json
new file mode 100644
index 0000000000..0dcc095b82
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/project.json
@@ -0,0 +1,134 @@
+{
+    "name": "nifi-registry",
+    "$schema": "../../node_modules/nx/schemas/project-schema.json",
+    "projectType": "application",
+    "generators": {
+        "@schematics/angular:component": {
+            "style": "scss"
+        }
+    },
+    "prefix": "app",
+    "sourceRoot": "apps/nifi-registry/src",
+    "targets": {
+        "build": {
+            "executor": "@nx/angular:application",
+            "outputs": ["{options.outputPath}"],
+            "options": {
+                "outputPath": "dist/nifi-registry",
+                "index": "apps/nifi-registry/src/index.html",
+                "browser": "apps/nifi-registry/src/main.ts",
+                "polyfills": ["zone.js"],
+                "tsConfig": "apps/nifi-registry/tsconfig.app.json",
+                "inlineStyleLanguage": "scss",
+                "assets": [
+                    {
+                        "glob": "**/*.svg",
+                        "input": "libs/shared/src/assets/",
+                        "output": "./assets"
+                    }
+                ],
+                "styles": ["apps/nifi-registry/src/styles.scss"],
+                "stylePreprocessorOptions": {
+                    "includePaths": [""]
+                },
+                "scripts": [],
+                "preserveSymlinks": true
+            },
+            "configurations": {
+                "production": {
+                    "budgets": [
+                        {
+                            "type": "initial",
+                            "maximumWarning": "500kb",
+                            "maximumError": "1mb"
+                        },
+                        {
+                            "type": "anyComponentStyle",
+                            "maximumWarning": "2kb",
+                            "maximumError": "4kb"
+                        }
+                    ],
+                    "outputHashing": "all",
+                    "optimization": true
+                },
+                "development": {
+                    "optimization": false,
+                    "extractLicenses": false,
+                    "sourceMap": true,
+                    "preserveSymlinks": true,
+                    "namedChunks": true,
+                    "outputHashing": "none",
+                    "assets": [
+                        {
+                            "glob": "**/*.svg",
+                            "input": "libs/shared/src/assets/",
+                            "output": "./assets"
+                        }
+                    ],
+                    "fileReplacements": [
+                        {
+                            "replace": "apps/nifi-registry/src/environments/environment.ts",
+                            "with": "apps/nifi-registry/src/environments/environment.development.ts"
+                        }
+                    ]
+                }
+            },
+            "defaultConfiguration": "production"
+        },
+        "serve": {
+            "executor": "@nx/angular:dev-server",
+            "configurations": {
+                "production": {
+                    "buildTarget": "nifi-registry:build:production"
+                },
+                "development": {
+                    "buildTarget": "nifi-registry:build:development",
+                    "servePath": "/nifi-registry"
+                }
+            },
+            "defaultConfiguration": "development",
+            "options": {
+                "proxyConfig": "apps/nifi-registry/proxy.config.mjs",
+                "buildTarget": "nifi-registry:build",
+                "port": 4204
+            },
+            "continuous": true
+        },
+        "extract-i18n": {
+            "executor": "@angular-devkit/build-angular:extract-i18n",
+            "options": {
+                "buildTarget": "nifi-registry:build"
+            }
+        },
+        "lint": {
+            "executor": "@nx/eslint:lint",
+            "outputs": ["{options.outputFile}"]
+        },
+        "lint:fix": {
+            "executor": "@nx/eslint:lint",
+            "options": {
+                "fix": true
+            },
+            "outputs": ["{options.outputFile}"]
+        },
+        "prettier": {
+            "executor": "nx:run-commands",
+            "options": {
+                "command": "npm run prettier"
+            }
+        },
+        "prettier:format": {
+            "executor": "nx:run-commands",
+            "options": {
+                "command": "npm run prettier-format"
+            }
+        },
+        "test": {
+            "executor": "@nx/jest:jest",
+            "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+            "options": {
+                "jestConfig": "apps/nifi-registry/jest.config.ts"
+            }
+        }
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/proxy.config.mjs b/nifi-frontend/src/main/frontend/apps/nifi-registry/proxy.config.mjs
new file mode 100644
index 0000000000..19f9c3956b
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/proxy.config.mjs
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+const target = {
+    target: 'http://localhost:18080',
+    secure: false,
+    logLevel: 'debug',
+    changeOrigin: true,
+    headers: {
+        'X-ProxyScheme': 'http',
+        'X-ProxyPort': 4204
+    },
+    configure: (proxy, _options) => {
+        proxy.on('error', (err, _req, _res) => {
+            console.log('proxy error', err);
+        });
+        proxy.on('proxyReq', (proxyReq, req, _res) => {
+            console.log('Sending Request to the Target:', req.method, req.url);
+        });
+        proxy.on('proxyRes', (proxyRes, req, _res) => {
+            console.log('Received Response from the Target:', proxyRes.statusCode, req.url);
+        });
+    },
+    bypass: function (req) {
+        if (req.url.startsWith('/nifi-registry/')) {
+            return req.url;
+        }
+    }
+};
+
+export default {
+    '/**': target
+};
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app-routing.module.ts
similarity index 50%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.ts
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app-routing.module.ts
index 9681c6c647..1895108bbe 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app-routing.module.ts
@@ -15,25 +15,37 @@
  * limitations under the License.
  */
 
-import { Component, EventEmitter, Input, Output } from '@angular/core';
-import { NgClass } from '@angular/common';
-import { MatButtonModule } from '@angular/material/button';
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
 
-@Component({
-    selector: 'error-banner',
-    imports: [MatButtonModule, NgClass],
-    templateUrl: './error-banner.component.html',
-    styleUrls: ['./error-banner.component.scss']
-})
-export class ErrorBanner {
-    @Input() messages: string[] | null = null;
-    @Input() showErrorIcon = true;
-    @Input() showBorder = true;
-    @Input() allowDismiss = true;
-
-    @Output() dismiss: EventEmitter<void> = new EventEmitter<void>();
-
-    dismissClicked(): void {
-        this.dismiss.next();
+const routes: Routes = [
+    {
+        path: '',
+        redirectTo: 'explorer',
+        pathMatch: 'full'
+    },
+    {
+        path: 'explorer',
+        loadChildren: () => import('./pages/resources/feature/resources.module').then((m) => m.ResourcesModule)
+    },
+    // Backward compatibility: old app's default route
+    {
+        path: 'nifi-registry',
+        redirectTo: 'explorer',
+        pathMatch: 'full'
     }
-}
+    // TODO: buckets
+    // TODO: Users/groups
+    // TODO: Page not found
+];
+
+@NgModule({
+    imports: [
+        RouterModule.forRoot(routes, {
+            paramsInheritanceStrategy: 'always',
+            useHash: true
+        })
+    ],
+    exports: [RouterModule]
+})
+export class AppRoutingModule {}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.component.html
new file mode 100644
index 0000000000..5ea966d92c
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.component.html
@@ -0,0 +1,23 @@
+<!--
+  ~ 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.
+  -->
+
+<div class="flex flex-col h-screen">
+    <header class="mb-5 nifi-registry-header">
+        <app-header></app-header>
+    </header>
+    <router-outlet></router-outlet>
+</div>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.component.scss
similarity index 76%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.component.scss
index 71ad5e3cd6..2944f98194 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.component.scss
@@ -14,18 +14,3 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-@use '@angular/material' as mat;
-
-.banner-container {
-    @include mat.button-density(-1);
-
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
-}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.component.spec.ts
similarity index 72%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.spec.ts
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.component.spec.ts
index a1d0d010e5..dd833f9d9b 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.spec.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.component.spec.ts
@@ -16,18 +16,19 @@
  */
 
 import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { AppComponent } from './app.component';
+import { RouterModule } from '@angular/router';
 
-import { ErrorBanner } from './error-banner.component';
+describe('AppComponent', () => {
+    let component: AppComponent;
+    let fixture: ComponentFixture<AppComponent>;
 
-describe('ErrorBanner', () => {
-    let component: ErrorBanner;
-    let fixture: ComponentFixture<ErrorBanner>;
-
-    beforeEach(() => {
-        TestBed.configureTestingModule({
-            imports: [ErrorBanner]
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [RouterModule.forRoot([])]
         });
-        fixture = TestBed.createComponent(ErrorBanner);
+
+        fixture = TestBed.createComponent(AppComponent);
         component = fixture.componentInstance;
         fixture.detectChanges();
     });
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.component.ts
new file mode 100644
index 0000000000..2ebb7236f7
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.component.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 { Component } from '@angular/core';
+import { ThemingService, Storage } from '@nifi/shared';
+
+@Component({
+    selector: 'nifi-registry-app-root',
+    templateUrl: './app.component.html',
+    styleUrl: './app.component.scss',
+    standalone: false
+})
+export class AppComponent {
+    title = 'nifi-registry';
+
+    constructor(
+        private storage: Storage,
+        private themingService: ThemingService
+    ) {
+        let theme = this.storage.getItem('theme');
+
+        // Initially check if dark mode is enabled on system
+        const darkModeOn = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
+
+        // If dark mode is enabled then directly switch to the dark-theme
+        this.themingService.toggleTheme(darkModeOn, theme);
+
+        if (window.matchMedia) {
+            // Watch for changes of the preference
+            window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
+                theme = this.storage.getItem('theme');
+                this.themingService.toggleTheme(e.matches, theme);
+            });
+        }
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.module.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.module.ts
new file mode 100644
index 0000000000..982661189e
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.module.ts
@@ -0,0 +1,90 @@
+/*
+ * 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 { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { AppRoutingModule } from './app-routing.module';
+import { AppComponent } from './app.component';
+import {
+    BrowserAnimationsModule,
+    provideAnimations,
+    provideNoopAnimations
+} from '@angular/platform-browser/animations';
+import { StoreModule } from '@ngrx/store';
+import { StoreDevtoolsModule } from '@ngrx/store-devtools';
+import { provideHttpClient, withInterceptors, withXsrfConfiguration } from '@angular/common/http';
+import { NavigationActionTiming, RouterState, StoreRouterConnectingModule } from '@ngrx/router-store';
+import { EffectsModule } from '@ngrx/effects';
+import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
+import { HeaderComponent } from './ui/header/header.component';
+import { rootReducers } from './state';
+import { ErrorEffects } from './state/error/error.effects';
+import { environment } from '../environments/environment';
+
+const entry = localStorage.getItem('disable-animations');
+let disableAnimations = '';
+
+try {
+    disableAnimations = entry !== null ? JSON.parse(entry).item : '';
+} catch (error) {
+    /* empty */
+}
+
+// honor OS settings if user has not explicitly disabled animations for the application
+try {
+    if (disableAnimations !== 'true' && disableAnimations !== 'false') {
+        disableAnimations = window.matchMedia('(prefers-reduced-motion: reduce)').matches.toString();
+    }
+} catch (error) {
+    /* empty */
+}
+
+@NgModule({
+    declarations: [AppComponent],
+    bootstrap: [AppComponent],
+    imports: [
+        BrowserModule,
+        AppRoutingModule,
+        BrowserAnimationsModule,
+        HeaderComponent,
+        StoreModule.forRoot(rootReducers),
+        StoreRouterConnectingModule.forRoot({
+            routerState: RouterState.Minimal,
+            navigationActionTiming: NavigationActionTiming.PostActivation
+        }),
+        EffectsModule.forRoot(ErrorEffects),
+        StoreDevtoolsModule.instrument({
+            maxAge: 25,
+            logOnly: environment.production,
+            autoPause: true,
+            name: 'NiFi Registry',
+            trace: !environment.production
+        })
+    ],
+    providers: [
+        disableAnimations === 'true' ? provideNoopAnimations() : provideAnimations(),
+        { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' } },
+        provideHttpClient(
+            withInterceptors([]),
+            withXsrfConfiguration({
+                cookieName: '__Secure-Request-Token',
+                headerName: 'Request-Token'
+            })
+        )
+    ]
+})
+export class AppModule {}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources-routing.module.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources-routing.module.ts
new file mode 100644
index 0000000000..bc01e8e2b1
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources-routing.module.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 { RouterModule, Routes } from '@angular/router';
+import { NgModule } from '@angular/core';
+import { ResourcesComponent } from './resources.component';
+
+const routes: Routes = [
+    {
+        path: '',
+        component: ResourcesComponent,
+        children: [
+            {
+                path: ':id',
+                component: ResourcesComponent
+            }
+        ]
+    },
+    // Backward compatibility routes for old NiFi Registry deep links
+    {
+        path: 'grid-list',
+        redirectTo: '',
+        pathMatch: 'full'
+    },
+    {
+        path: 'grid-list/buckets/:bucketId',
+        redirectTo: '',
+        pathMatch: 'full'
+    },
+    {
+        path: 'grid-list/buckets/:bucketId/:dropletType/:dropletId',
+        redirectTo: '',
+        pathMatch: 'full'
+    },
+    {
+        path: 'grid-list/buckets',
+        redirectTo: '',
+        pathMatch: 'full'
+    },
+    // Catch-all for any other grid-list sub-routes
+    {
+        path: 'grid-list/**',
+        redirectTo: ''
+    }
+];
+
+@NgModule({
+    imports: [RouterModule.forChild(routes)],
+    exports: [RouterModule]
+})
+export class ResourcesRoutingModule {}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.html
new file mode 100644
index 0000000000..36dd7f7e3b
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.html
@@ -0,0 +1,57 @@
+<!--
+~  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.
+-->
+
+<div class="pb-5 px-5 flex flex-col h-full">
+    <div class="h-full flex flex-col">
+        <h3 class="primary-color">Resources</h3>
+        <context-error-banner [context]="ErrorContextKey.GLOBAL"></context-error-banner>
+        <div class="flex justify-between items-baseline">
+            <droplet-table-filter
+                [buckets]="buckets"
+                [filterTerm]="filterTerm"
+                [filterBucket]="filterBucket"
+                [filterColumn]="filterColumn"
+                [filterableColumns]="filterableColumns"
+                [filteredCount]="dataSource.filteredData.length"
+                [totalCount]="dataSource.data.length"
+                (filterChanged)="applyFilter($event)"></droplet-table-filter>
+            <div class="flex justify-end">
+                <button mat-icon-button class="primary-icon-button" (click)="openImportNewDropletDialog()">
+                    <i class="fa fa-plus"></i>
+                </button>
+            </div>
+        </div>
+        @if (droplets$ | async; as droplets) {
+            <droplet-table
+                [buckets]="buckets"
+                [dataSource]="dataSource"
+                [selectedId]="selectedDropletId$ | async"
+                (selectDroplet)="selectDroplet($event)"
+                class="h-full flex"></droplet-table>
+        }
+
+        @if (dropletsState$ | async; as dropletsState) {
+            <div class="flex justify-between mt-2">
+                <div class="text-sm flex items-center gap-x-2">
+                    <button mat-icon-button class="primary-icon-button" (click)="refreshDropletsListing()">
+                        <i class="fa fa-refresh" [class.fa-spin]="dropletsState.status === 'loading'"></i>
+                    </button>
+                </div>
+            </div>
+        }
+    </div>
+</div>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.scss
similarity index 80%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.scss
index 71ad5e3cd6..bdd8410601 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.scss
@@ -17,15 +17,6 @@
 
 @use '@angular/material' as mat;
 
-.banner-container {
-    @include mat.button-density(-1);
-
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
+:host {
+    height: 100%;
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.spec.ts
new file mode 100644
index 0000000000..252eb51088
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.spec.ts
@@ -0,0 +1,55 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterModule } from '@angular/router';
+import { provideMockStore } from '@ngrx/store/testing';
+import { ResourcesComponent } from './resources.component';
+import { initialState } from '../../../state/droplets/droplets.reducer';
+import { initialState as initialBucketState } from '../../../state/buckets/buckets.reducer';
+import { resourcesFeatureKey } from '../../../state';
+import { dropletsFeatureKey } from '../../../state/droplets';
+import { bucketsFeatureKey } from '../../../state/buckets';
+
+describe('Resources', () => {
+    let component: ResourcesComponent;
+    let fixture: ComponentFixture<ResourcesComponent>;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            declarations: [ResourcesComponent],
+            imports: [RouterModule],
+            providers: [
+                provideMockStore({
+                    initialState: {
+                        [resourcesFeatureKey]: {
+                            [dropletsFeatureKey]: initialState,
+                            [bucketsFeatureKey]: initialBucketState
+                        }
+                    }
+                })
+            ]
+        });
+        fixture = TestBed.createComponent(ResourcesComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.ts
new file mode 100644
index 0000000000..8fd2868a5b
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.ts
@@ -0,0 +1,186 @@
+/*
+ * 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 { Component, OnInit } from '@angular/core';
+import {
+    selectDropletIdFromRoute,
+    selectDroplets,
+    selectDropletState
+} from '../../../state/droplets/droplets.selectors';
+import { loadDroplets, openImportNewDropletDialog, selectDroplet } from '../../../state/droplets/droplets.actions';
+import { Droplet } from '../../../state/droplets';
+import { Store } from '@ngrx/store';
+import { MatTableDataSource } from '@angular/material/table';
+import { Observable } from 'rxjs';
+import { Sort } from '@angular/material/sort';
+import { NiFiCommon } from '@nifi/shared';
+import { loadBuckets } from '../../../state/buckets/buckets.actions';
+import { Bucket } from '../../../state/buckets';
+import { selectBuckets } from '../../../state/buckets/buckets.selectors';
+import {
+    DropletTableFilterColumn,
+    DropletTableFilterContext
+} from './ui/droplet-table-filter/droplet-table-filter.component';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { ErrorContextKey } from '../../../state/error';
+
+@Component({
+    selector: 'resources',
+    templateUrl: './resources.component.html',
+    styleUrl: './resources.component.scss',
+    standalone: false
+})
+export class ResourcesComponent implements OnInit {
+    droplets$: Observable<Droplet[]> = this.store.select(selectDroplets).pipe(takeUntilDestroyed());
+    buckets$: Observable<Bucket[]> = this.store.select(selectBuckets).pipe(takeUntilDestroyed());
+    buckets: Bucket[] = [];
+    dataSource: MatTableDataSource<Droplet> = new MatTableDataSource<Droplet>();
+    displayedColumns: string[] = [
+        'name',
+        'type',
+        'bucketName',
+        'bucketIdentifier',
+        'identifier',
+        'versions',
+        'actions'
+    ];
+
+    filterableColumns: DropletTableFilterColumn[] = [
+        { key: 'name', label: 'Name' },
+        { key: 'type', label: 'Type' },
+        { key: 'bucketName', label: 'Bucket' },
+        { key: 'bucketIdentifier', label: 'Bucket ID' },
+        { key: 'identifier', label: 'Resource ID' },
+        { key: 'versionCount', label: 'Versions' }
+    ];
+    sort: Sort = {
+        active: 'name',
+        direction: 'asc'
+    };
+    filterTerm = '';
+    filterBucket = 'All';
+    filterColumn = '';
+    dropletsState$ = this.store.select(selectDropletState);
+    selectedDropletId$ = this.store.select(selectDropletIdFromRoute);
+
+    constructor(
+        private store: Store,
+        private nifiCommon: NiFiCommon
+    ) {}
+
+    ngOnInit(): void {
+        this.store.dispatch(loadDroplets());
+        this.store.dispatch(loadBuckets());
+        this.droplets$.subscribe((droplets) => {
+            this.dataSource.data = [...droplets];
+            this.sortData(this.sort);
+        });
+        this.buckets$.subscribe((buckets) => {
+            this.buckets = [...buckets];
+        });
+
+        this.dataSource.filterPredicate = (data: Droplet, filter: string) => {
+            const { filterTerm, filterColumn, filterBucket } = JSON.parse(filter);
+            const matchBucket: boolean = filterBucket !== 'All';
+
+            if (!filterTerm && !filterBucket) {
+                return true;
+            }
+
+            if (matchBucket) {
+                if (data.bucketIdentifier !== filterBucket) {
+                    return false;
+                }
+            }
+
+            if (filterTerm === '') {
+                return true;
+            }
+
+            const value = (data as any)[filterColumn];
+            if (typeof value === 'number') {
+                return value.toString().includes(filterTerm);
+            }
+            return this.nifiCommon.stringContains(value, filterTerm, true);
+        };
+    }
+
+    sortData(sort: Sort) {
+        this.sort = sort;
+        this.dataSource.data = this.sortVersions(this.dataSource.data, sort);
+    }
+
+    sortVersions(data: Droplet[], sort: Sort): Droplet[] {
+        if (!data) {
+            return [];
+        }
+        return data.slice().sort((a, b) => {
+            const isAsc = sort.direction === 'asc';
+            let retVal = 0;
+            switch (sort.active) {
+                case 'versions':
+                    retVal = this.nifiCommon.compareNumber(a.versionCount, b.versionCount);
+                    break;
+                case 'name':
+                    retVal = this.nifiCommon.compareString(a.name, b.name);
+                    break;
+                case 'bucketName':
+                    retVal = this.nifiCommon.compareString(a.bucketName, b.bucketName);
+                    break;
+                case 'bucketIdentifier':
+                    retVal = this.nifiCommon.compareString(a.bucketIdentifier, b.bucketIdentifier);
+                    break;
+                case 'identifier':
+                    retVal = this.nifiCommon.compareString(a.identifier, b.identifier);
+                    break;
+                case 'type':
+                    retVal = this.nifiCommon.compareString(a.type, b.type);
+                    break;
+            }
+            return retVal * (isAsc ? 1 : -1);
+        });
+    }
+
+    openImportNewDropletDialog() {
+        this.store.dispatch(openImportNewDropletDialog({ request: { buckets: this.buckets } }));
+    }
+
+    applyFilter(filter: DropletTableFilterContext) {
+        if (!filter || !this.dataSource) {
+            return;
+        }
+
+        this.dataSource.filter = JSON.stringify(filter);
+    }
+
+    refreshDropletsListing() {
+        this.store.dispatch(loadDroplets());
+        this.store.dispatch(loadBuckets());
+    }
+
+    selectDroplet(droplet: Droplet): void {
+        this.store.dispatch(
+            selectDroplet({
+                request: {
+                    id: droplet.identifier
+                }
+            })
+        );
+    }
+
+    protected readonly ErrorContextKey = ErrorContextKey;
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.module.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.module.ts
new file mode 100644
index 0000000000..d5da391bc2
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.module.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { StoreModule } from '@ngrx/store';
+import { EffectsModule } from '@ngrx/effects';
+import { ResourcesRoutingModule } from './resources-routing.module';
+import { ResourcesComponent } from './resources.component';
+import { DropletsEffects } from '../../../state/droplets/droplets.effects';
+import { BucketsEffects } from '../../../state/buckets/buckets.effects';
+import { reducers, resourcesFeatureKey } from '../../../state';
+import { MatTableModule } from '@angular/material/table';
+import { MatSortModule } from '@angular/material/sort';
+import { MatMenuModule } from '@angular/material/menu';
+import { DropletTableFilterComponent } from './ui/droplet-table-filter/droplet-table-filter.component';
+import { MatButtonModule } from '@angular/material/button';
+import { DropletTableComponent } from './ui/droplet-table/droplet-table.component';
+import { ContextErrorBanner } from '../../../ui/common/context-error-banner/context-error-banner.component';
+
+@NgModule({
+    declarations: [ResourcesComponent],
+    exports: [ResourcesComponent],
+    imports: [
+        CommonModule,
+        MatTableModule,
+        MatSortModule,
+        MatMenuModule,
+        DropletTableFilterComponent,
+        MatButtonModule,
+        MatButtonModule,
+        DropletTableComponent,
+        ContextErrorBanner,
+        ResourcesRoutingModule,
+        StoreModule.forFeature(resourcesFeatureKey, reducers),
+        EffectsModule.forFeature([DropletsEffects, BucketsEffects])
+    ]
+})
+export class ResourcesModule {}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.html
new file mode 100644
index 0000000000..8ce39f26a1
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.html
@@ -0,0 +1,28 @@
+<!--
+~  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.
+-->
+
+<h2 mat-dialog-title>Delete resource?</h2>
+<div class="delete-droplet-dialog">
+    <context-error-banner [context]="ErrorContextKey.DELETE_DROPLET"></context-error-banner>
+    <mat-dialog-content>
+        <p class="test">This action will delete all versions of {{ droplet.name }}</p>
+    </mat-dialog-content>
+    <mat-dialog-actions align="end">
+        <button mat-button mat-dialog-close>No</button>
+        <button mat-flat-button (click)="deleteDroplet(droplet)" class="ml-2" data-test-id="delete-btn">Yes</button>
+    </mat-dialog-actions>
+</div>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
similarity index 83%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
index 71ad5e3cd6..2677b47e02 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
@@ -17,15 +17,6 @@
 
 @use '@angular/material' as mat;
 
-.banner-container {
+.delete-droplet-dialog {
     @include mat.button-density(-1);
-
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.spec.ts
new file mode 100644
index 0000000000..15df3b091d
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.spec.ts
@@ -0,0 +1,81 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { DeleteDropletDialogComponent } from './delete-droplet-dialog.component';
+import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
+import { MatButtonModule } from '@angular/material/button';
+import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+import { Subject } from 'rxjs';
+import { deleteDroplet } from 'apps/nifi-registry/src/app/state/droplets/droplets.actions';
+
+describe('DeleteDropletDialogComponent', () => {
+    let component: DeleteDropletDialogComponent;
+    let fixture: ComponentFixture<DeleteDropletDialogComponent>;
+    let debug: DebugElement;
+    let store: MockStore;
+    const mockData = {
+        bucketIdentifier: '1234',
+        bucketName: 'testBucket',
+        createdTimestamp: 123456789,
+        description: 'testDescription',
+        identifier: '1234',
+        link: { href: 'testHref', params: { rel: 'testRel' } },
+        modifiedTimestamp: 123456789,
+        name: 'testName',
+        permissions: { canRead: true, canWrite: true },
+        revision: { version: 1 },
+        type: 'FLOW',
+        versionCount: 2
+    };
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            imports: [DeleteDropletDialogComponent, MatDialogModule, MatButtonModule],
+            providers: [
+                { provide: MAT_DIALOG_DATA, useValue: { droplet: mockData } },
+                {
+                    provide: MatDialogRef,
+                    useValue: {
+                        close: () => null,
+                        keydownEvents: () => new Subject<KeyboardEvent>()
+                    }
+                },
+                provideMockStore({})
+            ]
+        }).compileComponents();
+
+        store = TestBed.inject(MockStore);
+        fixture = TestBed.createComponent(DeleteDropletDialogComponent);
+        component = fixture.componentInstance;
+        debug = fixture.debugElement;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should delete droplet', () => {
+        const deleteDropletSpy = jest.spyOn(store, 'dispatch');
+        const deleteBtn = debug.query(By.css('[data-test-id=delete-btn]')).nativeElement;
+        deleteBtn.click();
+        expect(deleteDropletSpy).toHaveBeenCalledWith(deleteDroplet({ request: { droplet: mockData } }));
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.ts
new file mode 100644
index 0000000000..c2251d7854
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 { Component, Inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { CloseOnEscapeDialog } from '@nifi/shared';
+import { Store } from '@ngrx/store';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { deleteDroplet } from 'apps/nifi-registry/src/app/state/droplets/droplets.actions';
+import { Droplet } from 'apps/nifi-registry/src/app/state/droplets';
+import { MatButtonModule } from '@angular/material/button';
+import { ErrorContextKey } from 'apps/nifi-registry/src/app/state/error';
+import { ContextErrorBanner } from 'apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component';
+
+interface DeleteDropletDialogData {
+    droplet: Droplet;
+}
+
+@Component({
+    selector: 'app-delete-droplet-dialog',
+    imports: [CommonModule, MatDialogModule, MatButtonModule, ContextErrorBanner],
+    templateUrl: './delete-droplet-dialog.component.html',
+    styleUrl: './delete-droplet-dialog.component.scss'
+})
+export class DeleteDropletDialogComponent extends CloseOnEscapeDialog {
+    protected readonly ErrorContextKey = ErrorContextKey;
+    droplet: Droplet;
+
+    constructor(
+        @Inject(MAT_DIALOG_DATA) public data: DeleteDropletDialogData,
+        private store: Store
+    ) {
+        super();
+        this.droplet = data.droplet;
+    }
+
+    deleteDroplet(droplet: Droplet) {
+        this.store.dispatch(deleteDroplet({ request: { droplet } }));
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table-filter/droplet-table-filter.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table-filter/droplet-table-filter.component.html
new file mode 100644
index 0000000000..ad7ac1ad7c
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table-filter/droplet-table-filter.component.html
@@ -0,0 +1,53 @@
+<!--
+~  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.
+-->
+
+<div class="summary-table-filter-container">
+    <div>
+        <form [formGroup]="filterForm" class="my-2">
+            <div class="flex mt-2 gap-1 items-center">
+                <div>
+                    <mat-form-field subscriptSizing="dynamic">
+                        <mat-label>Filter</mat-label>
+                        <input matInput type="text" class="small" formControlName="filterTerm" />
+                    </mat-form-field>
+                </div>
+                <div>
+                    <mat-form-field subscriptSizing="dynamic">
+                        <mat-label>Filter By</mat-label>
+                        <mat-select formControlName="filterColumn">
+                            @for (option of filterableColumns; track option) {
+                                <mat-option [value]="option.key"> {{ option.label }}</mat-option>
+                            }
+                        </mat-select>
+                    </mat-form-field>
+                </div>
+                <div>
+                    <mat-form-field subscriptSizing="dynamic">
+                        <mat-label>Bucket</mat-label>
+                        <mat-select formControlName="filterBucket">
+                            <mat-option value="All"> All Buckets</mat-option>
+                            @for (bucket of buckets; track bucket) {
+                                <mat-option [value]="bucket.identifier"> {{ bucket.name }}</mat-option>
+                            }
+                        </mat-select>
+                    </mat-form-field>
+                </div>
+            </div>
+        </form>
+        <div class="my-2 tertiary-color font-medium">Filter matched {{ filteredCount }} of {{ totalCount }}</div>
+    </div>
+</div>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table-filter/droplet-table-filter.component.scss
similarity index 76%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table-filter/droplet-table-filter.component.scss
index 71ad5e3cd6..2944f98194 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table-filter/droplet-table-filter.component.scss
@@ -14,18 +14,3 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-@use '@angular/material' as mat;
-
-.banner-container {
-    @include mat.button-density(-1);
-
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
-}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table-filter/droplet-table-filter.component.spec.ts
similarity index 68%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.spec.ts
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table-filter/droplet-table-filter.component.spec.ts
index a1d0d010e5..38ce3882b4 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.spec.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table-filter/droplet-table-filter.component.spec.ts
@@ -16,18 +16,18 @@
  */
 
 import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { DropletTableFilterComponent } from './droplet-table-filter.component';
 
-import { ErrorBanner } from './error-banner.component';
+describe('DropletTableFilterComponent', () => {
+    let component: DropletTableFilterComponent;
+    let fixture: ComponentFixture<DropletTableFilterComponent>;
 
-describe('ErrorBanner', () => {
-    let component: ErrorBanner;
-    let fixture: ComponentFixture<ErrorBanner>;
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [DropletTableFilterComponent]
+        }).compileComponents();
 
-    beforeEach(() => {
-        TestBed.configureTestingModule({
-            imports: [ErrorBanner]
-        });
-        fixture = TestBed.createComponent(ErrorBanner);
+        fixture = TestBed.createComponent(DropletTableFilterComponent);
         component = fixture.componentInstance;
         fixture.detectChanges();
     });
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table-filter/droplet-table-filter.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table-filter/droplet-table-filter.component.ts
new file mode 100644
index 0000000000..8813c5f255
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table-filter/droplet-table-filter.component.ts
@@ -0,0 +1,142 @@
+/*
+ * 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 { AfterViewInit, Component, DestroyRef, EventEmitter, inject, Input, Output } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatSelectModule } from '@angular/material/select';
+import { MatInputModule } from '@angular/material/input';
+import { Bucket } from 'apps/nifi-registry/src/app/state/buckets';
+import { debounceTime } from 'rxjs';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { MatButtonModule } from '@angular/material/button';
+
+export interface DropletTableFilterColumn {
+    key: string;
+    label: string;
+}
+
+export interface DropletTableFilterArgs {
+    filterTerm: string;
+    filterColumn: string;
+    filterBucket?: string;
+}
+
+export interface DropletTableFilterContext extends DropletTableFilterArgs {
+    changedField: string;
+}
+
+@Component({
+    selector: 'droplet-table-filter',
+    imports: [CommonModule, ReactiveFormsModule, MatFormFieldModule, MatSelectModule, MatInputModule, MatButtonModule],
+    templateUrl: './droplet-table-filter.component.html',
+    styleUrl: './droplet-table-filter.component.scss'
+})
+export class DropletTableFilterComponent implements AfterViewInit {
+    filterForm: FormGroup;
+    private _initialFilterColumn = 'name';
+    private _filterableColumns: DropletTableFilterColumn[] = [];
+    private _buckets: Bucket[] = [];
+    private destroyRef: DestroyRef = inject(DestroyRef);
+
+    @Input() set filterableColumns(filterableColumns: DropletTableFilterColumn[]) {
+        this._filterableColumns = filterableColumns;
+    }
+    get filterableColumns(): DropletTableFilterColumn[] {
+        return this._filterableColumns;
+    }
+
+    @Input() set buckets(filterableColumns: Bucket[]) {
+        this._buckets = filterableColumns;
+    }
+    get buckets(): Bucket[] {
+        return this._buckets;
+    }
+
+    @Input() set filterTerm(term: string) {
+        this.filterForm.controls['filterTerm']?.setValue(term);
+    }
+
+    @Input() set filterColumn(column: string) {
+        this._initialFilterColumn = column;
+        if (this.filterableColumns?.length > 0) {
+            if (this.filterableColumns.findIndex((col) => col.key === column) >= 0) {
+                this.filterForm.get('filterColumn')?.setValue(column);
+            } else {
+                this.filterForm.get('filterColumn')?.setValue(this.filterableColumns[0].key);
+            }
+        } else {
+            this.filterForm.get('filterColumn')?.setValue(this._initialFilterColumn);
+        }
+    }
+
+    @Input() set filterBucket(term: string) {
+        this.filterForm.controls['filterBucket']?.setValue(term);
+    }
+
+    @Input() totalCount = 0;
+    @Input() filteredCount = 0;
+
+    @Output() filterChanged: EventEmitter<DropletTableFilterContext> = new EventEmitter<DropletTableFilterContext>();
+
+    constructor(private formBuilder: FormBuilder) {
+        this.filterForm = this.formBuilder.group({
+            filterTerm: '',
+            filterColumn: this._initialFilterColumn || 'name',
+            filterBucket: 'All'
+        });
+    }
+
+    ngAfterViewInit() {
+        this.filterForm
+            .get('filterTerm')
+            ?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef))
+            .subscribe((filterTerm: string) => {
+                const filterColumn = this.filterForm.get('filterColumn')?.value;
+                const filterBucket = this.filterForm.get('filterBucket')?.value;
+                this.applyFilter(filterTerm, filterColumn, filterBucket, 'filterTerm');
+            });
+
+        this.filterForm
+            .get('filterColumn')
+            ?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef))
+            .subscribe((filterColumn: string) => {
+                const filterTerm = this.filterForm.get('filterTerm')?.value;
+                const filterBucket = this.filterForm.get('filterBucket')?.value;
+                this.applyFilter(filterTerm, filterColumn, filterBucket, 'filterColumn');
+            });
+
+        this.filterForm
+            .get('filterBucket')
+            ?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef))
+            .subscribe((filterBucket: string) => {
+                const filterColumn = this.filterForm.get('filterColumn')?.value;
+                const filterTerm = this.filterForm.get('filterTerm')?.value;
+                this.applyFilter(filterTerm, filterColumn, filterBucket, 'filterBucket');
+            });
+    }
+
+    applyFilter(filterTerm: string, filterColumn: string, filterBucket: string, changedField: string) {
+        this.filterChanged.next({
+            filterColumn,
+            filterTerm,
+            filterBucket,
+            changedField
+        });
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table/droplet-table.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table/droplet-table.component.html
new file mode 100644
index 0000000000..6d78cc578e
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table/droplet-table.component.html
@@ -0,0 +1,135 @@
+<!--
+~  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.
+-->
+
+<div class="flex flex-col flex-1 h-full gap-y-2">
+    <div class="listing-table select-none flex flex-1 h-full">
+        <div class="flex-1 relative">
+            <div class="absolute inset-0 overflow-y-auto overflow-x-hidden">
+                <table
+                    mat-table
+                    [dataSource]="dataSource"
+                    matSort
+                    matSortDisableClear
+                    (matSortChange)="sortData($event)"
+                    [matSortActive]="sort.active"
+                    [matSortDirection]="sort.direction">
+                    <!-- Name Column -->
+                    <ng-container matColumnDef="name">
+                        <th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
+                        <td mat-cell *matCellDef="let item">
+                            <div class="overflow-ellipsis overflow-hidden whitespace-nowrap" [title]="item.name">
+                                {{ item.name }}
+                            </div>
+                        </td>
+                    </ng-container>
+                    <!-- Bucket Column -->
+                    <ng-container matColumnDef="bucketName">
+                        <th mat-header-cell *matHeaderCellDef mat-sort-header>Bucket</th>
+                        <td mat-cell *matCellDef="let item">
+                            <div class="overflow-ellipsis overflow-hidden whitespace-nowrap" [title]="item.bucketName">
+                                {{ item.bucketName }}
+                            </div>
+                        </td>
+                    </ng-container>
+                    <!-- Bucket ID Column -->
+                    <ng-container matColumnDef="bucketIdentifier">
+                        <th mat-header-cell *matHeaderCellDef mat-sort-header>Bucket ID</th>
+                        <td mat-cell *matCellDef="let item">
+                            <div
+                                class="overflow-ellipsis overflow-hidden whitespace-nowrap"
+                                [title]="item.bucketIdentifier">
+                                {{ item.bucketIdentifier }}
+                            </div>
+                        </td>
+                    </ng-container>
+                    <!-- Resource ID Column -->
+                    <ng-container matColumnDef="identifier">
+                        <th mat-header-cell *matHeaderCellDef mat-sort-header>Resource ID</th>
+                        <td mat-cell *matCellDef="let item">
+                            <div class="overflow-ellipsis overflow-hidden whitespace-nowrap" [title]="item.identifier">
+                                {{ item.identifier }}
+                            </div>
+                        </td>
+                    </ng-container>
+                    <!-- Type Column -->
+                    <ng-container matColumnDef="type">
+                        <th mat-header-cell *matHeaderCellDef mat-sort-header>Type</th>
+                        <td mat-cell *matCellDef="let item">
+                            <div class="overflow-ellipsis overflow-hidden whitespace-nowrap" [title]="item.type">
+                                {{ item.type }}
+                            </div>
+                        </td>
+                    </ng-container>
+                    <!-- Versions Column -->
+                    <ng-container matColumnDef="versions">
+                        <th mat-header-cell *matHeaderCellDef mat-sort-header>Versions</th>
+                        <td mat-cell *matCellDef="let item">
+                            <div
+                                class="overflow-ellipsis overflow-hidden whitespace-nowrap"
+                                [title]="item.versionCount">
+                                {{ item.versionCount }}
+                            </div>
+                        </td>
+                    </ng-container>
+                    <!-- Actions Column -->
+                    <ng-container matColumnDef="actions">
+                        <th mat-header-cell *matHeaderCellDef></th>
+                        <td mat-cell *matCellDef="let item">
+                            <div class="flex items-center justify-end gap-x-2">
+                                <button
+                                    mat-icon-button
+                                    type="button"
+                                    [matMenuTriggerFor]="actionMenu"
+                                    class="h-16 w-16 flex items-center justify-center icon global-menu">
+                                    <i class="fa fa-ellipsis-v"></i>
+                                </button>
+                                <mat-menu #actionMenu="matMenu" xPosition="before">
+                                    <button mat-menu-item (click)="openDropletVersionsDialog(item)">
+                                        <i class="fa fa-list primary-color mr-2"></i>
+                                        See Versions
+                                    </button>
+                                    <button mat-menu-item (click)="openImportNewDropletVersionDialog(item)">
+                                        <i class="fa fa-upload primary-color mr-2"></i>
+                                        Import new version
+                                    </button>
+                                    <button mat-menu-item (click)="openExportDropletVersionDialog(item)">
+                                        <i class="fa fa-download primary-color mr-2"></i>
+                                        Export version
+                                    </button>
+                                    <button mat-menu-item (click)="openDeleteDialog(item)">
+                                        <i class="fa fa-trash primary-color mr-2"></i>
+                                        Delete resource
+                                    </button>
+                                </mat-menu>
+                            </div>
+                        </td>
+                    </ng-container>
+                    <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
+                    <tr
+                        mat-row
+                        *matRowDef="let row; let even = even; columns: displayedColumns"
+                        [class.even]="even"
+                        (click)="select(row)"
+                        [class.selected]="isSelected(row)"></tr>
+                </table>
+                <div class="py-5" *ngIf="dataSource.data.length === 0 && buckets.length !== 0">
+                    <p class="text-center">There are no resources to display.</p>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table/droplet-table.component.scss
similarity index 79%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table/droplet-table.component.scss
index 71ad5e3cd6..d2e1d81533 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table/droplet-table.component.scss
@@ -16,16 +16,3 @@
  */
 
 @use '@angular/material' as mat;
-
-.banner-container {
-    @include mat.button-density(-1);
-
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
-}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table/droplet-table.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table/droplet-table.component.spec.ts
new file mode 100644
index 0000000000..c1dac9172d
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table/droplet-table.component.spec.ts
@@ -0,0 +1,134 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { DropletTableComponent } from './droplet-table.component';
+import { provideMockStore } from '@ngrx/store/testing';
+import { CommonModule } from '@angular/common';
+import { MatButtonModule } from '@angular/material/button';
+import { MatMenuModule } from '@angular/material/menu';
+import { MatSortModule } from '@angular/material/sort';
+import { MatTableModule } from '@angular/material/table';
+
+describe('DropletTableComponent', () => {
+    let component: DropletTableComponent;
+    let fixture: ComponentFixture<DropletTableComponent>;
+    const droplets = [
+        {
+            bucketIdentifier: '39a248d9-4ac6-42b2-8288-4b9ee6b2e3ed',
+            bucketName: 'Bucket 2',
+            createdTimestamp: 1747167193176,
+            description: 'This is a test description',
+            identifier: 'a48c0576-9075-45e5-af4d-5a6e76682f42',
+            link: {
+                href: 'buckets/39a248d9-4ac6-42b2-8288-4b9ee6b2e3ed/flows/a48c0576-9075-45e5-af4d-5a6e76682f42',
+                params: {
+                    rel: 'self'
+                }
+            },
+            modifiedTimestamp: 1747167193204,
+            name: 'Test Flow 1',
+            permissions: {
+                canDelete: true,
+                canRead: true,
+                canWrite: true
+            },
+            revision: {
+                version: 0
+            },
+            type: 'Flow',
+            versionCount: 1
+        }
+    ];
+
+    const buckets = [
+        {
+            allowBundleRedeploy: false,
+            allowPublicRead: false,
+            createdTimestamp: 1747167167303,
+            description: '',
+            identifier: '39a248d9-4ac6-42b2-8288-4b9ee6b2e3ed',
+            link: {
+                href: 'buckets/39a248d9-4ac6-42b2-8288-4b9ee6b2e3ed',
+                params: {
+                    rel: 'self'
+                }
+            },
+            name: 'Bucket 2',
+            permissions: {
+                canDelete: true,
+                canRead: true,
+                canWrite: true
+            },
+            revision: {
+                version: 0
+            }
+        }
+    ];
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [
+                DropletTableComponent,
+                CommonModule,
+                MatTableModule,
+                MatSortModule,
+                MatMenuModule,
+                MatButtonModule,
+                MatButtonModule
+            ],
+            providers: [provideMockStore({})]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(DropletTableComponent);
+        component = fixture.componentInstance;
+        component.buckets = buckets;
+        component.dataSource.data = [...droplets];
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should render the droplet table with the correct headings', () => {
+        const rows = fixture.nativeElement.querySelectorAll('tr');
+
+        // Header row
+        const headerRow = rows[0];
+        expect(headerRow.cells[0].innerHTML).toContain('Name');
+        expect(headerRow.cells[1].innerHTML).toContain('Type');
+        expect(headerRow.cells[2].innerHTML).toContain('Bucket');
+        expect(headerRow.cells[3].innerHTML).toContain('Bucket ID');
+        expect(headerRow.cells[4].innerHTML).toContain('Resource ID');
+        expect(headerRow.cells[5].innerHTML).toContain('Versions');
+    });
+
+    it('should render the droplet table with the correct row values', () => {
+        const rows = fixture.nativeElement.querySelectorAll('tr');
+        expect(rows.length).toBe(2);
+
+        // Droplet row
+        const row1 = rows[1];
+        expect(row1.cells[0].innerHTML).toContain(droplets[0].name);
+        expect(row1.cells[1].innerHTML).toContain(droplets[0].type);
+        expect(row1.cells[2].innerHTML).toContain(droplets[0].bucketName);
+        expect(row1.cells[3].innerHTML).toContain(droplets[0].bucketIdentifier);
+        expect(row1.cells[4].innerHTML).toContain(droplets[0].identifier);
+        expect(row1.cells[5].innerHTML).toContain(droplets[0].versionCount.toString());
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table/droplet-table.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table/droplet-table.component.ts
new file mode 100644
index 0000000000..6c0a14725d
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-table/droplet-table.component.ts
@@ -0,0 +1,137 @@
+/*
+ * 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 { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { Droplet } from 'apps/nifi-registry/src/app/state/droplets';
+import { MatSortModule, Sort } from '@angular/material/sort';
+import { NiFiCommon } from '@nifi/shared';
+import { MatMenuModule } from '@angular/material/menu';
+import { MatButtonModule } from '@angular/material/button';
+import { Bucket } from 'apps/nifi-registry/src/app/state/buckets';
+import { Store } from '@ngrx/store';
+import {
+    openDeleteDropletDialog,
+    openExportDropletVersionDialog,
+    openDropletVersionsDialog,
+    openImportNewDropletVersionDialog
+} from 'apps/nifi-registry/src/app/state/droplets/droplets.actions';
+
+@Component({
+    selector: 'droplet-table',
+    imports: [CommonModule, MatTableModule, MatSortModule, MatMenuModule, MatButtonModule, MatButtonModule],
+    templateUrl: './droplet-table.component.html',
+    styleUrl: './droplet-table.component.scss'
+})
+export class DropletTableComponent implements OnInit {
+    @Input() buckets: Bucket[] = [];
+    @Input() dataSource: MatTableDataSource<Droplet> = new MatTableDataSource<Droplet>();
+    @Input() selectedId: string | null = null;
+
+    @Output() selectDroplet: EventEmitter<Droplet> = new EventEmitter<Droplet>();
+
+    displayedColumns: string[] = [
+        'name',
+        'type',
+        'bucketName',
+        'bucketIdentifier',
+        'identifier',
+        'versions',
+        'actions'
+    ];
+    sort: Sort = {
+        active: 'name',
+        direction: 'asc'
+    };
+
+    constructor(
+        private nifiCommon: NiFiCommon,
+        private store: Store
+    ) {}
+
+    ngOnInit(): void {
+        this.sortData(this.sort);
+    }
+
+    sortData(sort: Sort) {
+        this.sort = sort;
+        this.dataSource.data = this.sortVersions(this.dataSource.data, sort);
+    }
+
+    sortVersions(data: Droplet[], sort: Sort): Droplet[] {
+        if (!data) {
+            return [];
+        }
+        return data.slice().sort((a, b) => {
+            const isAsc = sort.direction === 'asc';
+            let retVal = 0;
+            switch (sort.active) {
+                case 'versions':
+                    retVal = this.nifiCommon.compareNumber(a.versionCount, b.versionCount);
+                    break;
+                case 'name':
+                    retVal = this.nifiCommon.compareString(a.name, b.name);
+                    break;
+                case 'bucketName':
+                    retVal = this.nifiCommon.compareString(a.bucketName, b.bucketName);
+                    break;
+                case 'bucketIdentifier':
+                    retVal = this.nifiCommon.compareString(a.bucketIdentifier, b.bucketIdentifier);
+                    break;
+                case 'identifier':
+                    retVal = this.nifiCommon.compareString(a.identifier, b.identifier);
+                    break;
+                case 'type':
+                    retVal = this.nifiCommon.compareString(a.type, b.type);
+                    break;
+            }
+            return retVal * (isAsc ? 1 : -1);
+        });
+    }
+
+    select(droplet: Droplet) {
+        this.selectDroplet.next(droplet);
+    }
+
+    isSelected(droplet: Droplet): boolean {
+        if (this.selectedId) {
+            return this.selectedId === droplet.identifier;
+        }
+        return false;
+    }
+
+    openImportNewDropletVersionDialog(droplet: Droplet) {
+        this.store.dispatch(
+            openImportNewDropletVersionDialog({
+                request: { droplet }
+            })
+        );
+    }
+
+    openExportDropletVersionDialog(droplet: Droplet) {
+        this.store.dispatch(openExportDropletVersionDialog({ request: { droplet } }));
+    }
+
+    openDeleteDialog(droplet: Droplet) {
+        this.store.dispatch(openDeleteDropletDialog({ request: { droplet } }));
+    }
+
+    openDropletVersionsDialog(droplet: Droplet) {
+        this.store.dispatch(openDropletVersionsDialog({ request: { droplet } }));
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.html
new file mode 100644
index 0000000000..5dafc3236f
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.html
@@ -0,0 +1,94 @@
+<!--
+~  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.
+-->
+
+<h2 mat-dialog-title>Resource Versions</h2>
+<div class="flow-versions-dialog">
+    <mat-dialog-content>
+        <context-error-banner [context]="ErrorContextKey.EXPORT_DROPLET_VERSION"></context-error-banner>
+        <div class="listing-table flex-1 relative dialog-tab-content">
+            <div class="absolute inset-0 overflow-y-auto overflow-x-hidden">
+                <table
+                    mat-table
+                    [dataSource]="dataSource"
+                    matSort
+                    matSortDisableClear
+                    (matSortChange)="sortData($event)"
+                    [matSortActive]="sort.active"
+                    [matSortDirection]="sort.direction">
+                    <!-- Version Column -->
+                    <ng-container matColumnDef="version">
+                        <th mat-header-cell *matHeaderCellDef mat-sort-header>Version</th>
+                        <td mat-cell *matCellDef="let item">
+                            <div class="overflow-ellipsis overflow-hidden whitespace-nowrap" [title]="item.version">
+                                {{ item.version }}
+                            </div>
+                        </td>
+                    </ng-container>
+                    <!-- Create Column -->
+                    <ng-container matColumnDef="created">
+                        <th mat-header-cell *matHeaderCellDef mat-sort-header>Created</th>
+                        <td mat-cell *matCellDef="let item">
+                            {{ formatTimestamp(item) }}
+                        </td>
+                    </ng-container>
+                    <!-- Comments Column -->
+                    <ng-container matColumnDef="comments">
+                        <th mat-header-cell *matHeaderCellDef mat-sort-header>Comments</th>
+                        <td mat-cell *matCellDef="let item">
+                            <div class="overflow-ellipsis overflow-hidden whitespace-nowrap" [title]="item.comments">
+                                {{ item.comments }}
+                            </div>
+                        </td>
+                    </ng-container>
+                    <!-- Actions Column -->
+                    <ng-container matColumnDef="actions">
+                        <th mat-header-cell *matHeaderCellDef></th>
+                        <td mat-cell *matCellDef="let item">
+                            <div class="flex items-center justify-end gap-x-2">
+                                <button
+                                    data-test-id="exportMenuBtn"
+                                    mat-icon-button
+                                    type="button"
+                                    [matMenuTriggerFor]="actionMenu"
+                                    class="h-16 w-16 flex items-center justify-center icon global-menu">
+                                    <i class="fa fa-ellipsis-v"></i>
+                                </button>
+                                <mat-menu #actionMenu="matMenu" xPosition="before">
+                                    <button
+                                        mat-menu-item
+                                        (click)="exportVersion(item.version)"
+                                        data-test-id="exportBtn">
+                                        <i class="fa fa-download primary-color mr-2"></i>
+                                        Export resource version
+                                    </button>
+                                </mat-menu>
+                            </div>
+                        </td>
+                    </ng-container>
+                    <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
+                    <tr
+                        mat-row
+                        *matRowDef="let row; let even = even; columns: displayedColumns"
+                        [class.even]="even"></tr>
+                </table>
+            </div>
+        </div>
+    </mat-dialog-content>
+    <mat-dialog-actions align="end">
+        <button mat-button mat-dialog-close>Close</button>
+    </mat-dialog-actions>
+</div>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.scss
similarity index 83%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.scss
index 71ad5e3cd6..3316222cc3 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.scss
@@ -17,15 +17,6 @@
 
 @use '@angular/material' as mat;
 
-.banner-container {
+.flow-versions-dialog {
     @include mat.button-density(-1);
-
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.spec.ts
new file mode 100644
index 0000000000..95aba2e8a5
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.spec.ts
@@ -0,0 +1,99 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { DropletVersionsDialogComponent } from './droplet-versions-dialog.component';
+import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { Subject } from 'rxjs';
+import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
+import { MatTableModule } from '@angular/material/table';
+import { MatSortModule } from '@angular/material/sort';
+import { MatMenuModule } from '@angular/material/menu';
+import { MatButtonModule } from '@angular/material/button';
+import { exportDropletVersion } from 'apps/nifi-registry/src/app/state/droplets/droplets.actions';
+
+describe('DropletVersionsDialogComponent', () => {
+    let component: DropletVersionsDialogComponent;
+    let fixture: ComponentFixture<DropletVersionsDialogComponent>;
+    let store: MockStore;
+    const mockDroplet = {
+        bucketIdentifier: '1234',
+        bucketName: 'testBucket',
+        createdTimestamp: 123456789,
+        description: 'testDescription',
+        identifier: '1234',
+        link: { href: 'testHref', params: { rel: 'testRel' } },
+        modifiedTimestamp: 123456789,
+        name: 'testName',
+        permissions: { canRead: true, canWrite: true },
+        revision: { version: 1 },
+        type: 'FLOW',
+        versionCount: 1
+    };
+    const mockVersions = {
+        author: 'anonymous',
+        bucketIdentifier: '311c6295-d8b6-47bd-806e-b5ee27dd8187',
+        flowIdentifier: 'ed9f3f48-751b-4237-a8a7-a4a2020f1efe',
+        link: {
+            href: 'buckets/311c6295-d8b6-47bd-806e-b5ee27dd8187/flows/ed9f3f48-751b-4237-a8a7-a4a2020f1efe/versions/1',
+            params: { rel: 'content' }
+        },
+        timestamp: 1741788614197,
+        version: 1
+    };
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [
+                DropletVersionsDialogComponent,
+                MatTableModule,
+                MatSortModule,
+                MatDialogModule,
+                MatMenuModule,
+                MatButtonModule
+            ],
+            providers: [
+                { provide: MAT_DIALOG_DATA, useValue: { droplet: mockDroplet, versions: mockVersions } },
+                {
+                    provide: MatDialogRef,
+                    useValue: {
+                        close: () => null,
+                        keydownEvents: () => new Subject<KeyboardEvent>()
+                    }
+                },
+                provideMockStore({})
+            ]
+        }).compileComponents();
+
+        store = TestBed.inject(MockStore);
+        fixture = TestBed.createComponent(DropletVersionsDialogComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should export flow', () => {
+        const dispatchSpy = jest.spyOn(store, 'dispatch');
+        component.exportVersion(2);
+        expect(dispatchSpy).toHaveBeenCalledWith(
+            exportDropletVersion({ request: { droplet: mockDroplet, version: 2 } })
+        );
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.ts
new file mode 100644
index 0000000000..96a8ae335c
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.ts
@@ -0,0 +1,133 @@
+/*
+ * 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 { Component, Inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Droplet } from 'apps/nifi-registry/src/app/state/droplets';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { Store } from '@ngrx/store';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { MatSortModule, Sort } from '@angular/material/sort';
+import { CloseOnEscapeDialog, NiFiCommon } from '@nifi/shared';
+import { MatMenuModule } from '@angular/material/menu';
+import { exportDropletVersion } from 'apps/nifi-registry/src/app/state/droplets/droplets.actions';
+import { MatButtonModule } from '@angular/material/button';
+import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
+import { ErrorContextKey } from '../../../../../state/error';
+
+interface Data {
+    droplet: Droplet;
+    versions: VersionedFlowSnapshotMetadata[];
+}
+
+interface VersionedFlowSnapshotMetadata {
+    bucketIdentifier: string;
+    flowIdentifier: string;
+    version: string;
+    timestamp: number;
+    author: string;
+    comments: string;
+    branch?: string;
+}
+
+@Component({
+    selector: 'app-flow-versions-dialog',
+    imports: [
+        CommonModule,
+        MatTableModule,
+        MatSortModule,
+        MatDialogModule,
+        MatMenuModule,
+        MatButtonModule,
+        ContextErrorBanner
+    ],
+    templateUrl: './droplet-versions-dialog.component.html',
+    styleUrl: './droplet-versions-dialog.component.scss'
+})
+export class DropletVersionsDialogComponent extends CloseOnEscapeDialog {
+    dataSource: MatTableDataSource<VersionedFlowSnapshotMetadata> =
+        new MatTableDataSource<VersionedFlowSnapshotMetadata>();
+
+    sort: Sort = {
+        active: 'created',
+        direction: 'desc'
+    };
+    displayedColumns: string[] = ['version', 'created', 'comments', 'actions'];
+    timeOffset = 0;
+
+    constructor(
+        @Inject(MAT_DIALOG_DATA) public data: Data,
+        private nifiCommon: NiFiCommon,
+        private store: Store
+    ) {
+        super();
+        this.dataSource.data = data.versions;
+    }
+
+    sortData(sort: Sort) {
+        this.sort = sort;
+        this.dataSource.data = this.sortVersions(this.dataSource.data, sort);
+    }
+
+    sortVersions(data: VersionedFlowSnapshotMetadata[], sort: Sort): VersionedFlowSnapshotMetadata[] {
+        if (!data) {
+            return [];
+        }
+        return data.slice().sort((a, b) => {
+            const isAsc = sort.direction === 'asc';
+            let retVal = 0;
+            switch (sort.active) {
+                case 'version':
+                    retVal = this.compareVersion(a.version, b.version);
+                    break;
+                case 'created':
+                    retVal = this.nifiCommon.compareNumber(a.timestamp, b.timestamp);
+                    break;
+                case 'comments':
+                    retVal = this.nifiCommon.compareString(a.comments, b.comments);
+                    break;
+            }
+            return retVal * (isAsc ? 1 : -1);
+        });
+    }
+
+    formatTimestamp(flowVersion: VersionedFlowSnapshotMetadata) {
+        // get the current user time to properly convert the server time
+        const now: Date = new Date();
+
+        // convert the user offset to millis
+        const userTimeOffset: number = now.getTimezoneOffset() * 60 * 1000;
+
+        // create the proper date by adjusting by the offsets
+        const date: Date = new Date(flowVersion.timestamp + userTimeOffset + this.timeOffset);
+        return this.nifiCommon.formatDateTime(date);
+    }
+
+    exportVersion(version: number) {
+        this.store.dispatch(exportDropletVersion({ request: { droplet: this.data.droplet, version } }));
+    }
+
+    private compareVersion(a: string, b: string): number {
+        if (this.nifiCommon.isNumber(a) && this.nifiCommon.isNumber(b)) {
+            return this.nifiCommon.compareNumber(parseInt(a, 10), parseInt(b, 10));
+        } else {
+            return this.nifiCommon.compareString(a, b);
+        }
+    }
+
+    protected readonly ErrorContextKey = ErrorContextKey;
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.html
new file mode 100644
index 0000000000..358d529620
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.html
@@ -0,0 +1,49 @@
+<!--
+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.
+-->
+
+<h2 mat-dialog-title>Export Version</h2>
+<div class="export-version-dialog">
+    <mat-dialog-content class="pb-2 flex flex-col justify-between start">
+        <context-error-banner [context]="ErrorContextKey.EXPORT_DROPLET_VERSION"></context-error-banner>
+        <div class="pb-2 label-name">
+            <label>Choose Version</label>
+        </div>
+        <div class="w-full bucket-dropdown-field">
+            <mat-form-field class="flex w-full">
+                <mat-select [(value)]="selectedVersion">
+                    <mat-option
+                        *ngFor="let snapshotMeta of [].constructor(droplet.versionCount); let i = index"
+                        [value]="i + 1">
+                        <span *ngIf="snapshotMeta === i">Latest (Version {{ i + 1 }})</span>
+                        <span *ngIf="snapshotMeta !== i">Version {{ i + 1 }}</span>
+                    </mat-option>
+                </mat-select>
+            </mat-form-field>
+        </div>
+    </mat-dialog-content>
+    <mat-dialog-actions align="end">
+        <button mat-button mat-dialog-close type="button">Cancel</button>
+        <button
+            mat-flat-button
+            class="pl-2"
+            data-automation-id="export-versioned-flow-button"
+            (click)="exportVersion()"
+            data-test-id="export-btn">
+            Export
+        </button>
+    </mat-dialog-actions>
+</div>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.scss
similarity index 83%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.scss
index 71ad5e3cd6..df15c3f913 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.scss
@@ -17,15 +17,6 @@
 
 @use '@angular/material' as mat;
 
-.banner-container {
+.export-version-dialog {
     @include mat.button-density(-1);
-
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.spec.ts
new file mode 100644
index 0000000000..7b8f295ec2
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.spec.ts
@@ -0,0 +1,82 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ExportDropletVersionDialogComponent } from './export-droplet-version-dialog.component';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { DebugElement } from '@angular/core';
+import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { Subject } from 'rxjs';
+import { By } from '@angular/platform-browser';
+import { exportDropletVersion } from 'apps/nifi-registry/src/app/state/droplets/droplets.actions';
+
+describe('ExportDropletVersionDialogComponent', () => {
+    let component: ExportDropletVersionDialogComponent;
+    let fixture: ComponentFixture<ExportDropletVersionDialogComponent>;
+    let debug: DebugElement;
+    let store: MockStore;
+    const mockData = {
+        bucketIdentifier: '1234',
+        bucketName: 'testBucket',
+        createdTimestamp: 123456789,
+        description: 'testDescription',
+        identifier: '1234',
+        link: { href: 'testHref', params: { rel: 'testRel' } },
+        modifiedTimestamp: 123456789,
+        name: 'testName',
+        permissions: { canRead: true, canWrite: true },
+        revision: { version: 1 },
+        type: 'FLOW',
+        versionCount: 2
+    };
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [ExportDropletVersionDialogComponent],
+            providers: [
+                { provide: MAT_DIALOG_DATA, useValue: { droplet: mockData } },
+                {
+                    provide: MatDialogRef,
+                    useValue: {
+                        close: () => null,
+                        keydownEvents: () => new Subject<KeyboardEvent>()
+                    }
+                },
+                provideMockStore({})
+            ]
+        }).compileComponents();
+
+        store = TestBed.inject(MockStore);
+        fixture = TestBed.createComponent(ExportDropletVersionDialogComponent);
+        component = fixture.componentInstance;
+        debug = fixture.debugElement;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should export flow', () => {
+        const exportFlowSpy = jest.spyOn(store, 'dispatch');
+        const exportBtn = debug.query(By.css('[data-test-id=export-btn]')).nativeElement;
+        exportBtn.click();
+        expect(exportFlowSpy).toHaveBeenCalledWith(
+            exportDropletVersion({ request: { droplet: mockData, version: 2 } })
+        );
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.ts
new file mode 100644
index 0000000000..602720fee8
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 { Component, Inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { MatFormField } from '@angular/material/form-field';
+import { MatSelectModule } from '@angular/material/select';
+import { MatInputModule } from '@angular/material/input';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { Droplet } from 'apps/nifi-registry/src/app/state/droplets';
+import { Store } from '@ngrx/store';
+import { exportDropletVersion } from 'apps/nifi-registry/src/app/state/droplets/droplets.actions';
+import { CloseOnEscapeDialog } from '@nifi/shared';
+import { MatButtonModule } from '@angular/material/button';
+import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
+import { ErrorContextKey } from '../../../../../state/error';
+
+export interface ExportFlowVersionDialogData {
+    droplet: Droplet;
+}
+
+@Component({
+    selector: 'app-export-flow-version-dialog',
+    imports: [
+        CommonModule,
+        MatFormField,
+        MatSelectModule,
+        MatInputModule,
+        MatDialogModule,
+        MatButtonModule,
+        ContextErrorBanner
+    ],
+    templateUrl: './export-droplet-version-dialog.component.html',
+    styleUrl: './export-droplet-version-dialog.component.scss'
+})
+export class ExportDropletVersionDialogComponent extends CloseOnEscapeDialog {
+    droplet: Droplet;
+    selectedVersion: number;
+
+    constructor(
+        @Inject(MAT_DIALOG_DATA) public data: ExportFlowVersionDialogData,
+        private store: Store
+    ) {
+        super();
+        this.droplet = data.droplet;
+        this.selectedVersion = this.droplet.versionCount;
+    }
+
+    exportVersion() {
+        this.store.dispatch(
+            exportDropletVersion({ request: { droplet: this.droplet, version: this.selectedVersion } })
+        );
+    }
+
+    protected readonly ErrorContextKey = ErrorContextKey;
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-dialog/import-new-droplet-dialog.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-dialog/import-new-droplet-dialog.component.html
new file mode 100644
index 0000000000..4c0983b5db
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-dialog/import-new-droplet-dialog.component.html
@@ -0,0 +1,100 @@
+<!--
+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.
+-->
+
+<h2 mat-dialog-title>Import New Resource</h2>
+<div class="import-new-flow-dialog">
+    <form class="import-new-flow-form" [formGroup]="importNewFlowForm">
+        <mat-dialog-content>
+            <context-error-banner [context]="ErrorContextKey.CREATE_DROPLET"></context-error-banner>
+            <div class="flex justify-between items-center">
+                <div class="w-full flex flex-1 items-center">
+                    <mat-form-field class="w-full">
+                        <mat-label>Name</mat-label>
+                        <input matInput type="text" placeholder="Create a unique name" formControlName="name" />
+                    </mat-form-field>
+                    <div class="version-count ml-2">v.1</div>
+                </div>
+            </div>
+            <mat-label class="flex mb-2">Resource</mat-label>
+            <mat-form-field class="w-full">
+                <input
+                    class="pointer"
+                    (click)="flowUploadControl.click()"
+                    matInput
+                    formControlName="definition"
+                    type="text"
+                    placeholder="Choose a file"
+                    [readonly]="true" />
+                <button
+                    matSuffix
+                    type="button"
+                    class="upload-flow-definition tertiary-color icon icon-template-import mr-2.5 pointer"
+                    title="Browse"
+                    (click)="flowUploadControl.click()">
+                    <input type="file" #flowUploadControl class="hidden" (change)="attachFlowDefinition($event)" />
+                </button>
+            </mat-form-field>
+
+            @if (fileToUpload) {
+                <div>
+                    <div class="flex flex-col gap-y-3 mb-3">
+                        <div class="flex flex-col">
+                            <div>File to upload</div>
+                            <div class="flex justify-between items-center">
+                                <div class="tertiary-color font-medium">{{ fileNameAttached }}</div>
+                                <button
+                                    class="pointer"
+                                    aria-label="Remove attached resource"
+                                    (click)="removeAttachedFlowDefinition()">
+                                    X
+                                </button>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            }
+            <div class="w-full flex flex-col">
+                <mat-label class="mb-2">Bucket</mat-label>
+                <mat-form-field class="flex">
+                    <mat-select
+                        #bucketSelect
+                        panelClass="bucket-dropdown-select"
+                        placeholder="Choose a location"
+                        formControlName="bucket">
+                        <mat-option *ngFor="let bucket of writableBuckets" [value]="bucket.identifier">
+                            {{ bucket.name }}
+                        </mat-option>
+                    </mat-select>
+                </mat-form-field>
+            </div>
+            <mat-form-field class="w-full">
+                <mat-label class="mb-2">Description</mat-label>
+                <textarea matInput class="mb-2 w-full" formControlName="description"></textarea>
+            </mat-form-field>
+        </mat-dialog-content>
+    </form>
+    <mat-dialog-actions align="end">
+        <button mat-button mat-dialog-close type="button">Cancel</button>
+        <button
+            [disabled]="!importNewFlowForm.valid"
+            class="ml-2"
+            data-automation-id="import-new-versioned-flow-button"
+            (click)="importNewFlow()"
+            type="button"
+            mat-flat-button>
+            Import
+        </button>
+    </mat-dialog-actions>
+</div>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-dialog/import-new-droplet-dialog.component.scss
similarity index 83%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-dialog/import-new-droplet-dialog.component.scss
index 71ad5e3cd6..48b091cd26 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-dialog/import-new-droplet-dialog.component.scss
@@ -17,15 +17,6 @@
 
 @use '@angular/material' as mat;
 
-.banner-container {
+.import-new-flow-dialog {
     @include mat.button-density(-1);
-
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-dialog/import-new-droplet-dialog.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-dialog/import-new-droplet-dialog.component.spec.ts
new file mode 100644
index 0000000000..48bfbd3492
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-dialog/import-new-droplet-dialog.component.spec.ts
@@ -0,0 +1,143 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ImportNewDropletDialogComponent } from './import-new-droplet-dialog.component';
+import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
+import { Subject } from 'rxjs';
+import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { CommonModule } from '@angular/common';
+import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatSelectModule } from '@angular/material/select';
+import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+import { createNewDroplet } from 'apps/nifi-registry/src/app/state/droplets/droplets.actions';
+
+describe('ImportNewDropletDialogComponent', () => {
+    let component: ImportNewDropletDialogComponent;
+    let fixture: ComponentFixture<ImportNewDropletDialogComponent>;
+    let store: MockStore;
+    let form: FormGroup;
+    const buckets = [
+        {
+            allowBundleRedeploy: false,
+            allowPublicRead: false,
+            createdTimestamp: 1736800325035,
+            description: '',
+            identifier: '311c6295-d8b6-47bd-806e-b5ee27dd8187',
+            link: {
+                href: 'buckets/311c6295-d8b6-47bd-806e-b5ee27dd8187',
+                params: {
+                    rel: 'self'
+                }
+            },
+            name: 'Test Bucket',
+            permissions: {
+                canDelete: true,
+                canRead: true,
+                canWrite: true
+            },
+            revision: {
+                version: 0
+            }
+        },
+        {
+            allowBundleRedeploy: false,
+            allowPublicRead: false,
+            createdTimestamp: 1738008373583,
+            description: '',
+            identifier: '6c2f8d9a-2d72-4637-86d7-3e125296db85',
+            link: {
+                href: 'buckets/6c2f8d9a-2d72-4637-86d7-3e125296db85',
+                params: {
+                    rel: 'self'
+                }
+            },
+            name: 'Test Bucket #2',
+            permissions: {
+                canDelete: true,
+                canRead: true,
+                canWrite: false
+            },
+            revision: {
+                version: 0
+            }
+        }
+    ];
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [
+                ImportNewDropletDialogComponent,
+                CommonModule,
+                MatDialogModule,
+                FormsModule,
+                ReactiveFormsModule,
+                MatFormFieldModule,
+                MatSelectModule,
+                MatInputModule,
+                MatButtonModule
+            ],
+            providers: [
+                { provide: MAT_DIALOG_DATA, useValue: { buckets } },
+                {
+                    provide: MatDialogRef,
+                    useValue: {
+                        close: () => null,
+                        keydownEvents: () => new Subject<KeyboardEvent>()
+                    }
+                },
+                provideMockStore({})
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ImportNewDropletDialogComponent);
+        store = TestBed.inject(MockStore);
+        component = fixture.componentInstance;
+        form = component.importNewFlowForm;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should filter writable buckets', () => {
+        expect(component.writableBuckets.length).toBe(1);
+    });
+
+    it('should import a new flow', () => {
+        const dispatchSpy = jest.spyOn(store, 'dispatch');
+        const file = new File([''], 'testFile');
+        form.get('name')?.setValue('Flow Import 1');
+        form.get('description')?.setValue('test description');
+        form.get('bucket')?.setValue('311c6295-d8b6-47bd-806e-b5ee27dd8187');
+        component.fileToUpload = file;
+        component.importNewFlow();
+        expect(dispatchSpy).toHaveBeenCalledWith(
+            createNewDroplet({
+                request: {
+                    bucket: buckets[0],
+                    file: file,
+                    name: 'Flow Import 1',
+                    description: 'test description'
+                }
+            })
+        );
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-dialog/import-new-droplet-dialog.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-dialog/import-new-droplet-dialog.component.ts
new file mode 100644
index 0000000000..303b3c581e
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-dialog/import-new-droplet-dialog.component.ts
@@ -0,0 +1,136 @@
+/*
+ * 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 { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { CloseOnEscapeDialog, NiFiCommon } from '@nifi/shared';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatSelectModule } from '@angular/material/select';
+import { Bucket } from 'apps/nifi-registry/src/app/state/buckets';
+import { MatInputModule } from '@angular/material/input';
+import { Store } from '@ngrx/store';
+import { createNewDroplet } from 'apps/nifi-registry/src/app/state/droplets/droplets.actions';
+import { MatButtonModule } from '@angular/material/button';
+import { ErrorContextKey } from 'apps/nifi-registry/src/app/state/error';
+import { ContextErrorBanner } from 'apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component';
+
+export interface ImportNewFlowDialogData {
+    buckets: Bucket[];
+}
+
+@Component({
+    selector: 'app-import-new-flow-dialog',
+    imports: [
+        CommonModule,
+        MatDialogModule,
+        FormsModule,
+        ReactiveFormsModule,
+        MatFormFieldModule,
+        MatSelectModule,
+        MatInputModule,
+        MatButtonModule,
+        ContextErrorBanner
+    ],
+    templateUrl: './import-new-droplet-dialog.component.html',
+    styleUrl: './import-new-droplet-dialog.component.scss'
+})
+export class ImportNewDropletDialogComponent extends CloseOnEscapeDialog implements OnInit {
+    @ViewChild('flowUploadControl') flowUploadControl!: ElementRef;
+
+    protected readonly ErrorContextKey = ErrorContextKey;
+
+    fileToUpload: File | null = null;
+    writableBuckets: Bucket[] = [];
+    buckets: Bucket[] = [];
+    importNewFlowForm: FormGroup;
+    fileNameAttached: string | null = '';
+
+    constructor(
+        @Inject(MAT_DIALOG_DATA) public data: ImportNewFlowDialogData,
+        private store: Store,
+        private formBuilder: FormBuilder,
+        private nifiCommon: NiFiCommon
+    ) {
+        super();
+        this.buckets = data.buckets;
+        this.importNewFlowForm = this.formBuilder.group({
+            name: new FormControl('', Validators.required),
+            description: new FormControl(null),
+            bucket: new FormControl('', Validators.required),
+            definition: new FormControl('', Validators.required)
+        });
+    }
+
+    ngOnInit() {
+        this.writableBuckets = this.filterWritableBuckets(this.buckets);
+
+        // if there's only 1 writable bucket, always set as the initial value in the bucket dropdown
+        if (this.writableBuckets.length === 1) {
+            const autoSelectedBucket = this.buckets.find((b) => b.identifier === this.writableBuckets[0].identifier);
+            this.importNewFlowForm.get('bucket')?.setValue(autoSelectedBucket?.identifier);
+        }
+    }
+
+    filterWritableBuckets(buckets: Bucket[]) {
+        const filteredWritableBuckets: Bucket[] = [];
+        buckets.forEach(function (b: Bucket) {
+            if (b.permissions.canWrite) {
+                filteredWritableBuckets.push(b);
+            }
+        });
+        return filteredWritableBuckets;
+    }
+
+    importNewFlow() {
+        const selectedBucket: Bucket = this.writableBuckets.find((b) => {
+            return b.identifier === this.importNewFlowForm.get('bucket')?.value;
+        })!;
+
+        this.store.dispatch(
+            createNewDroplet({
+                request: {
+                    bucket: selectedBucket,
+                    file: this.fileToUpload!,
+                    name: this.importNewFlowForm.get('name')?.value,
+                    description: this.importNewFlowForm.get('description')?.value || null
+                }
+            })
+        );
+    }
+
+    attachFlowDefinition(event: Event): void {
+        const target = event.target as HTMLInputElement;
+        const files = target.files as FileList;
+        const file = files.item(0);
+        if (file) {
+            this.importNewFlowForm.get('definition')?.setValue(this.nifiCommon.substringBeforeLast(file.name, '.'));
+            this.importNewFlowForm.get('definition')?.markAsDirty();
+            this.importNewFlowForm.get('description')?.setValue(null);
+            this.fileNameAttached = file.name;
+            this.fileToUpload = file;
+        }
+    }
+
+    removeAttachedFlowDefinition() {
+        this.importNewFlowForm.get('definition')?.setValue('');
+        this.flowUploadControl.nativeElement.value = '';
+        this.fileNameAttached = null;
+        this.fileToUpload = null;
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-version-dialog/import-new-droplet-version-dialog.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-version-dialog/import-new-droplet-version-dialog.component.html
new file mode 100644
index 0000000000..9dc2c2fc42
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-version-dialog/import-new-droplet-version-dialog.component.html
@@ -0,0 +1,85 @@
+<!--
+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.
+-->
+
+<h2 mat-dialog-title>Import New Resource Version</h2>
+<div class="import-new-flow-version-dialog">
+    <form class="import-new-flow-version-form" [formGroup]="importNewFlowVersionForm">
+        <context-error-banner [context]="ErrorContextKey.IMPORT_DROPLET_VERSION"></context-error-banner>
+        <mat-dialog-content>
+            <div>
+                <mat-label class="mb-2">Name</mat-label>
+                <div class="flex justify-between items-center mb-2">
+                    <div class="w-full tertiary-color font-medium">
+                        {{ droplet.name }} v.{{ droplet.versionCount + 1 }}
+                    </div>
+                </div>
+            </div>
+            <div>
+                <mat-label class="flex mb-2">Resource</mat-label>
+                <mat-form-field class="w-full">
+                    <input
+                        class="pointer"
+                        (click)="flowUploadControl.click()"
+                        matInput
+                        formControlName="newFlowVersionDefinition"
+                        type="text"
+                        placeholder="Choose a file"
+                        [readonly]="true" />
+                    <button
+                        matSuffix
+                        type="button"
+                        class="upload-flow-definition tertiary-color icon icon-template-import mr-2.5 pointer"
+                        title="Browse"
+                        (click)="flowUploadControl.click()">
+                        <input type="file" #flowUploadControl class="hidden" (change)="attachFlowDefinition($event)" />
+                    </button>
+                </mat-form-field>
+            </div>
+            @if (fileToUpload) {
+                <div>
+                    <div class="flex flex-col gap-y-3 mb-3">
+                        <div class="flex flex-col">
+                            <div>File to upload</div>
+                            <div class="flex justify-between items-center">
+                                <div class="tertiary-color font-medium">{{ fileNameAttached }}</div>
+                                <button aria-label="Remove attached resource" (click)="removeAttachedFlowDefinition()">
+                                    X
+                                </button>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            }
+            <mat-form-field class="w-full">
+                <mat-label class="mb-2">Version Comments</mat-label>
+                <textarea matInput class="mb-2 w-full" formControlName="newFlowVersionComments"></textarea>
+            </mat-form-field>
+        </mat-dialog-content>
+    </form>
+    <mat-dialog-actions align="end">
+        <button mat-button mat-dialog-close type="button">Cancel</button>
+        <button
+            [disabled]="!fileToUpload"
+            class="ml-2"
+            data-automation-id="import-new-versioned-flow-button"
+            (click)="importNewFlowVersion()"
+            type="submit"
+            mat-flat-button>
+            Import
+        </button>
+    </mat-dialog-actions>
+</div>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-version-dialog/import-new-droplet-version-dialog.component.scss
similarity index 83%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-version-dialog/import-new-droplet-version-dialog.component.scss
index 71ad5e3cd6..615a87bd7f 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-version-dialog/import-new-droplet-version-dialog.component.scss
@@ -17,15 +17,6 @@
 
 @use '@angular/material' as mat;
 
-.banner-container {
+.import-new-flow-version-dialog {
     @include mat.button-density(-1);
-
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-version-dialog/import-new-droplet-version-dialog.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-version-dialog/import-new-droplet-version-dialog.component.spec.ts
new file mode 100644
index 0000000000..c255d01073
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-version-dialog/import-new-droplet-version-dialog.component.spec.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ImportNewDropletVersionDialogComponent } from './import-new-droplet-version-dialog.component';
+import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
+import { Subject } from 'rxjs';
+import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatSelectModule } from '@angular/material/select';
+import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+
+describe('ImportNewDropletVersionDialogComponent', () => {
+    let component: ImportNewDropletVersionDialogComponent;
+    let fixture: ComponentFixture<ImportNewDropletVersionDialogComponent>;
+    let store: MockStore;
+    const mockDroplet = {
+        bucketIdentifier: '1234',
+        bucketName: 'testBucket',
+        createdTimestamp: 123456789,
+        description: 'testDescription',
+        identifier: '1234',
+        link: { href: 'testHref', params: { rel: 'testRel' } },
+        modifiedTimestamp: 123456789,
+        name: 'testName',
+        permissions: { canRead: true, canWrite: true },
+        revision: { version: 1 },
+        type: 'FLOW',
+        versionCount: 2
+    };
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [
+                ImportNewDropletVersionDialogComponent,
+                CommonModule,
+                MatDialogModule,
+                FormsModule,
+                ReactiveFormsModule,
+                MatFormFieldModule,
+                MatSelectModule,
+                MatInputModule,
+                MatButtonModule
+            ],
+            providers: [
+                { provide: MAT_DIALOG_DATA, useValue: { droplet: mockDroplet } },
+                {
+                    provide: MatDialogRef,
+                    useValue: {
+                        close: () => null,
+                        keydownEvents: () => new Subject<KeyboardEvent>()
+                    }
+                },
+                provideMockStore({})
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ImportNewDropletVersionDialogComponent);
+        store = TestBed.inject(MockStore);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should import a new flow version to the correct bucket', () => {
+        const dispatchSpy = jest.spyOn(store, 'dispatch');
+        const file = new File([''], 'testFile');
+        component.fileToUpload = file;
+        component.name = 'testName';
+        component.description = 'testDescription';
+        component.importNewFlowVersion();
+        expect(dispatchSpy).toHaveBeenCalled();
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-version-dialog/import-new-droplet-version-dialog.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-version-dialog/import-new-droplet-version-dialog.component.ts
new file mode 100644
index 0000000000..778bfd0f48
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/import-new-droplet-version-dialog/import-new-droplet-version-dialog.component.ts
@@ -0,0 +1,115 @@
+/*
+ * 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 { Component, ElementRef, Inject, ViewChild } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { CloseOnEscapeDialog, NiFiCommon } from '@nifi/shared';
+import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
+import { Store } from '@ngrx/store';
+import { importNewDropletVersion } from 'apps/nifi-registry/src/app/state/droplets/droplets.actions';
+import { Droplet } from 'apps/nifi-registry/src/app/state/droplets';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatSelectModule } from '@angular/material/select';
+import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+import { ErrorContextKey } from 'apps/nifi-registry/src/app/state/error';
+import { ContextErrorBanner } from 'apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component';
+
+export interface ImportNewFlowVersionDialogData {
+    droplet: Droplet;
+}
+
+@Component({
+    selector: 'app-import-new-flow-version-dialog',
+    imports: [
+        CommonModule,
+        MatDialogModule,
+        FormsModule,
+        ReactiveFormsModule,
+        MatFormFieldModule,
+        MatSelectModule,
+        MatInputModule,
+        MatButtonModule,
+        ContextErrorBanner
+    ],
+    templateUrl: './import-new-droplet-version-dialog.component.html',
+    styleUrl: './import-new-droplet-version-dialog.component.scss'
+})
+export class ImportNewDropletVersionDialogComponent extends CloseOnEscapeDialog {
+    @ViewChild('flowUploadControl') flowUploadControl!: ElementRef;
+
+    protected readonly ErrorContextKey = ErrorContextKey;
+    extensions = 'application/json';
+    fileName: string | null = null;
+    fileToUpload: File | null = null;
+    name = '';
+    description = '';
+    droplet: Droplet;
+    comments = '';
+    importNewFlowVersionForm: FormGroup;
+    fileNameAttached: string | null = '';
+
+    constructor(
+        @Inject(MAT_DIALOG_DATA) public data: ImportNewFlowVersionDialogData,
+        private formBuilder: FormBuilder,
+        private store: Store,
+        private nifiCommon: NiFiCommon
+    ) {
+        super();
+        this.droplet = data.droplet;
+
+        this.importNewFlowVersionForm = this.formBuilder.group({
+            newFlowVersionDefinition: new FormControl('', Validators.required),
+            newFlowVersionComments: new FormControl(null)
+        });
+    }
+
+    attachFlowDefinition(event: Event): void {
+        const target = event.target as HTMLInputElement;
+        const files = target.files as FileList;
+        const file = files.item(0);
+        if (file) {
+            this.importNewFlowVersionForm
+                .get('newFlowVersionDefinition')
+                ?.setValue(this.nifiCommon.substringBeforeLast(file.name, '.'));
+            this.importNewFlowVersionForm.get('newFlowVersionDefinition')?.markAsDirty();
+            this.importNewFlowVersionForm.get('newFlowVersionComments')?.setValue(null);
+            this.fileNameAttached = file.name;
+            this.fileToUpload = file;
+        }
+    }
+
+    removeAttachedFlowDefinition() {
+        this.importNewFlowVersionForm.get('newFlowVersionDefinition')?.setValue('');
+        this.flowUploadControl.nativeElement.value = '';
+        this.fileNameAttached = null;
+        this.fileToUpload = null;
+    }
+
+    importNewFlowVersion() {
+        this.store.dispatch(
+            importNewDropletVersion({
+                request: {
+                    href: this.droplet.link.href,
+                    file: this.fileToUpload!,
+                    description: this.importNewFlowVersionForm.get('newFlowVersionComments')?.value || null
+                }
+            })
+        );
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/buckets.service.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/buckets.service.ts
new file mode 100644
index 0000000000..fa4c5d3ec3
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/buckets.service.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { HttpClient } from '@angular/common/http';
+
+@Injectable({ providedIn: 'root' })
+export class BucketsService {
+    private static readonly API: string = '../nifi-registry-api';
+
+    constructor(private httpClient: HttpClient) {}
+
+    getBuckets(): Observable<any> {
+        // const mockError: HttpErrorResponse = new HttpErrorResponse({
+        //     status: 404,
+        //     statusText: 'Bad Gateway',
+        //     url: `${BucketsService.API}/buckets`,
+        //     error: {
+        //         message: 'Mock error: unable to GET buckets.',
+        //         timestamp: new Date().toISOString()
+        //     }
+        // });
+        // return throwError(() => mockError);
+
+        return this.httpClient.get(`${BucketsService.API}/buckets`);
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/droplets.service.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/droplets.service.ts
new file mode 100644
index 0000000000..e4194a176d
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/droplets.service.ts
@@ -0,0 +1,136 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+
+@Injectable({ providedIn: 'root' })
+export class DropletsService {
+    private static readonly API: string = '../nifi-registry-api';
+
+    constructor(private httpClient: HttpClient) {}
+
+    getDroplets(bucketId?: string): Observable<any> {
+        // const mockError: HttpErrorResponse = new HttpErrorResponse({
+        //     status: 404,
+        //     statusText: 'Service Unavailable',
+        //     url: bucketId
+        //         ? `${DropletsService.API}/items/${bucketId}`
+        //         : `${DropletsService.API}/items`,
+        //     error: {
+        //         message: 'Mock error: unable to GET droplets.',
+        //         timestamp: new Date().toISOString()
+        //     }
+        // });
+        //
+        // return throwError(() => mockError);
+
+        if (bucketId) {
+            return this.httpClient.get(`${DropletsService.API}/items/${bucketId}`);
+        }
+        return this.httpClient.get(`${DropletsService.API}/items`);
+    }
+
+    deleteDroplet(href: string): Observable<any> {
+        // const mockError: HttpErrorResponse = new HttpErrorResponse({
+        //     status: 403,
+        //     statusText: 'Forbidden',
+        //     url: `${DropletsService.API}/${href}?version=0`,
+        //     error: {
+        //         message: 'Mock error: unable to delete droplet.',
+        //         timestamp: new Date().toISOString()
+        //     }
+        // });
+        // return throwError(() => mockError);
+
+        return this.httpClient.delete(`${DropletsService.API}/${href}?version=0`);
+    }
+
+    createNewDroplet(bucketUri: string, name: string, description: string): Observable<any> {
+        // const mockError: HttpErrorResponse = new HttpErrorResponse({
+        //     status: 400,
+        //     statusText: 'Bad Request',
+        //     url: `${DropletsService.API}/${bucketUri}/flows`,
+        //     error: {
+        //         message: 'Mock error: unable to import resource.',
+        //         details: { name, description },
+        //         timestamp: new Date().toISOString()
+        //     }
+        // });
+        // return throwError(() => mockError);
+
+        return this.httpClient.post(`${DropletsService.API}/${bucketUri}/flows`, {
+            name: name,
+            description: description
+        });
+    }
+
+    uploadDroplet(flowUri: string, file: File, description: string): Observable<any> {
+        // const mockError: HttpErrorResponse = new HttpErrorResponse({
+        //     status: 413,
+        //     statusText: 'Payload Too Large',
+        //     url: `${DropletsService.API}/${flowUri}/versions/import`,
+        //     error: {
+        //         message: 'Mock error: uploaded file exceeds maximum size.',
+        //         fileName: file?.name,
+        //         timestamp: new Date().toISOString()
+        //     }
+        // });
+        // return throwError(() => mockError);
+
+        return this.httpClient.post(`${DropletsService.API}/${flowUri}/versions/import`, file, {
+            headers: { Comments: description || '' } // using a short-circuit here because the backend does not accept null
+        });
+    }
+
+    exportDropletVersionedSnapshot(dropletUri: string, versionNumber: number): Observable<any> {
+        // const mockError: HttpErrorResponse = new HttpErrorResponse({
+        //     status: 404,
+        //     statusText: 'Not Found',
+        //     url: `${DropletsService.API}/${dropletUri}/versions/${versionNumber}/export`,
+        //     error: {
+        //         message: 'Mock error: error exporting resource.',
+        //         version: versionNumber,
+        //         timestamp: new Date().toISOString()
+        //     }
+        // });
+        // return throwError(() => mockError);
+
+        const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
+        return this.httpClient.get(`${DropletsService.API}/${dropletUri}/versions/${versionNumber}/export`, {
+            headers,
+            observe: 'response',
+            responseType: 'text'
+        });
+    }
+
+    getDropletSnapshotMetadata(dropletUri: string): Observable<any> {
+        // const mockError: HttpErrorResponse = new HttpErrorResponse({
+        //     status: 500,
+        //     statusText: 'Internal Server Error',
+        //     url: `${DropletsService.API}/${dropletUri}/versions`,
+        //     error: {
+        //         message: 'Mock error: failed to retrieve snapshot metadata.',
+        //         timestamp: new Date().toISOString()
+        //     }
+        // });
+        // return throwError(() => mockError);
+
+        return this.httpClient.get(`${DropletsService.API}/${dropletUri}/versions`);
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/error-helper.service.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/error-helper.service.ts
new file mode 100644
index 0000000000..bffd7c42b4
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/error-helper.service.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { HttpErrorResponse } from '@angular/common/http';
+
+@Injectable({ providedIn: 'root' })
+export class ErrorHelper {
+    getErrorString(errorResponse: HttpErrorResponse, prefix?: string): string {
+        // TODO: Update to handle actual error response structure from NiFi Registry REST API
+        let errorMessage = 'An unspecified error occurred.';
+        if (errorResponse) {
+            const err = errorResponse.error;
+            if (err) {
+                if (typeof err === 'string') {
+                    errorMessage = err;
+                } else if (typeof err === 'object') {
+                    // Prefer nested message fields from our mock errors
+                    errorMessage = err.message || JSON.stringify(err);
+                } else {
+                    errorMessage = `${err}`;
+                }
+            } else if (errorResponse.message) {
+                errorMessage = errorResponse.message;
+            } else if (errorResponse.status) {
+                errorMessage = `${errorResponse.status} ${errorResponse.statusText || ''}`.trim();
+            }
+        }
+        return prefix ? `${prefix} - [${errorMessage}]` : errorMessage;
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.actions.ts
similarity index 73%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.actions.ts
index 71ad5e3cd6..46a3df69d8 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.actions.ts
@@ -15,17 +15,12 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
+import { createAction, props } from '@ngrx/store';
+import { LoadBucketsResponse } from '.';
 
-.banner-container {
-    @include mat.button-density(-1);
+export const loadBuckets = createAction('[Buckets] Load Buckets');
 
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
-}
+export const loadBucketsSuccess = createAction(
+    '[Buckets] Load Buckets Success',
+    props<{ response: LoadBucketsResponse }>()
+);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.effects.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.effects.ts
new file mode 100644
index 0000000000..256ffe2a36
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.effects.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 { inject, Injectable } from '@angular/core';
+import { Actions, createEffect, ofType } from '@ngrx/effects';
+import { catchError, from, map, of, switchMap } from 'rxjs';
+import { HttpErrorResponse } from '@angular/common/http';
+import * as BucketsActions from './buckets.actions';
+import { BucketsService } from '../../service/buckets.service';
+import { ErrorHelper } from '../../service/error-helper.service';
+import { ErrorContextKey } from '../error';
+import * as DropletsActions from '../droplets/droplets.actions';
+
+@Injectable()
+export class BucketsEffects {
+    constructor(
+        private bucketsService: BucketsService,
+        private errorHelper: ErrorHelper
+    ) {}
+
+    actions$ = inject(Actions);
+
+    loadBuckets$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(BucketsActions.loadBuckets),
+            switchMap(() => {
+                return from(
+                    this.bucketsService.getBuckets().pipe(
+                        map((response) =>
+                            BucketsActions.loadBucketsSuccess({
+                                response: {
+                                    buckets: response
+                                }
+                            })
+                        ),
+                        catchError((errorResponse: HttpErrorResponse) => of(this.bannerError(errorResponse)))
+                    )
+                );
+            })
+        )
+    );
+
+    private bannerError(errorResponse: HttpErrorResponse, context: ErrorContextKey = ErrorContextKey.GLOBAL) {
+        return DropletsActions.dropletsBannerError({
+            errorContext: {
+                errors: [this.errorHelper.getErrorString(errorResponse)],
+                context
+            }
+        });
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.reducer.ts
similarity index 59%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.reducer.ts
index 71ad5e3cd6..657c1f583c 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.reducer.ts
@@ -15,17 +15,24 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
+import { createReducer, on } from '@ngrx/store';
+import { loadBuckets, loadBucketsSuccess } from './buckets.actions';
+import { BucketsState } from '.';
 
-.banner-container {
-    @include mat.button-density(-1);
+export const initialState: BucketsState = {
+    buckets: [],
+    status: 'pending'
+};
 
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
-}
+export const bucketsReducer = createReducer(
+    initialState,
+    on(loadBuckets, (state) => ({
+        ...state,
+        status: 'loading' as const
+    })),
+    on(loadBucketsSuccess, (state, { response }) => ({
+        ...state,
+        buckets: response.buckets,
+        status: 'success' as const
+    }))
+);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.selectors.ts
similarity index 61%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.selectors.ts
index 71ad5e3cd6..7384865a9d 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.selectors.ts
@@ -15,17 +15,13 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
+import { createFeatureSelector, createSelector } from '@ngrx/store';
+import { bucketsFeatureKey, BucketsState } from './index';
 
-.banner-container {
-    @include mat.button-density(-1);
+import { resourcesFeatureKey, ResourcesState } from '..';
 
-    .error-banner-message {
-        word-break: break-word;
-    }
+export const selectResourcesState = createFeatureSelector<ResourcesState>(resourcesFeatureKey);
 
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
-}
+export const selectBucketState = createSelector(selectResourcesState, (state) => state[bucketsFeatureKey]);
+
+export const selectBuckets = createSelector(selectBucketState, (state: BucketsState) => state.buckets);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/index.ts
similarity index 59%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/index.ts
index 71ad5e3cd6..771d43da4e 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/index.ts
@@ -15,17 +15,28 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
+import { Permissions } from '@nifi/shared';
+import { Link, Revision } from '../index';
 
-.banner-container {
-    @include mat.button-density(-1);
+export const bucketsFeatureKey = 'buckets';
 
-    .error-banner-message {
-        word-break: break-word;
-    }
+export interface LoadBucketsResponse {
+    buckets: Bucket[];
+}
+
+export interface Bucket {
+    allowBundleRedeploy: boolean;
+    allowPublicRead: boolean;
+    createdTimestamp: number;
+    description: string;
+    identifier: string;
+    link: Link;
+    name: string;
+    permissions: Permissions;
+    revision: Revision;
+}
 
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
+export interface BucketsState {
+    buckets: Bucket[];
+    status: 'pending' | 'loading' | 'success';
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.actions.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.actions.ts
new file mode 100644
index 0000000000..9ddecaaaf7
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.actions.ts
@@ -0,0 +1,111 @@
+/*
+ * 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 { createAction, props } from '@ngrx/store';
+import {
+    DeleteDropletRequest,
+    Droplet,
+    ImportDropletDialog,
+    ImportDropletRequest,
+    ImportDropletVersionRequest,
+    LoadDropletsResponse
+} from '.';
+import { ErrorContext } from '../error';
+
+export const loadDroplets = createAction('[Droplets] Load Droplets');
+
+export const loadDropletsSuccess = createAction(
+    '[Droplets] Load Droplets User Success',
+    props<{ response: LoadDropletsResponse }>()
+);
+
+export const openDeleteDropletDialog = createAction(
+    '[Droplets] Open Delete Droplet Dialog',
+    props<{ request: DeleteDropletRequest }>()
+);
+
+export const deleteDroplet = createAction('[Droplets] Delete Droplet', props<{ request: DeleteDropletRequest }>());
+
+export const deleteDropletSuccess = createAction('[Droplets] Delete Droplet Success', props<{ response: Droplet }>());
+
+export const openImportNewDropletDialog = createAction(
+    '[Droplets] Open Import New Droplet Dialog',
+    props<{ request: ImportDropletDialog }>()
+);
+
+export const createNewDroplet = createAction(
+    '[Droplets] Create New Droplet',
+    props<{ request: ImportDropletRequest }>()
+);
+
+export const importNewDropletVersion = createAction(
+    '[Droplets] Import New Droplet Version',
+    props<{ request: ImportDropletVersionRequest }>()
+);
+
+export const createNewDropletSuccess = createAction(
+    `[Droplets] Create New Droplet Success`,
+    props<{ response: Droplet; request: ImportDropletVersionRequest }>()
+);
+
+export const importNewDropletVersionSuccess = createAction(
+    '[Droplets] Import New Droplet Version Success',
+    props<{ response: any }>()
+);
+
+export const openImportNewDropletVersionDialog = createAction(
+    '[Droplets] Open Import New Droplet Version Dialog',
+    props<{ request: { droplet: Droplet } }>()
+);
+
+export const openExportDropletVersionDialog = createAction(
+    `[Droplets] Open Export Droplet Version Dialog`,
+    props<{ request: { droplet: Droplet } }>()
+);
+
+export const exportDropletVersion = createAction(
+    `[Droplets] Export Droplet Version`,
+    props<{ request: { droplet: Droplet; version: number } }>()
+);
+
+export const exportDropletVersionSuccess = createAction(
+    `[Droplets] Export Droplet Version Success`,
+    props<{ response: any }>()
+);
+
+export const openDropletVersionsDialog = createAction(
+    `[Droplets] Open Droplet Versions Dialog`,
+    props<{ request: { droplet: Droplet } }>()
+);
+
+export const selectDroplet = createAction(`[Droplets] Select Droplet`, props<{ request: { id: string } }>());
+
+export const dropletsBannerError = createAction(`[Droplets] Banner Error`, props<{ errorContext: ErrorContext }>());
+
+export const importNewDropletVersionError = createAction(
+    '[Droplets] Import New Droplet Version Error',
+    props<{ errorContext: ErrorContext; createdDroplet: Droplet }>()
+);
+
+export const importVersionForNewDroplet = createAction(
+    '[Droplets] Import Version For New Droplet',
+    props<{ request: ImportDropletVersionRequest; createdDroplet: Droplet }>()
+);
+
+// Utility no-op action for effects that need to emit a valid action on success. This allows dialogs to add global
+// context errors when the dialog is unable to async GET the data it needs in order to open.
+export const noOp = createAction('[Droplets] No Op');
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.effects.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.effects.ts
new file mode 100644
index 0000000000..0729382ed4
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.effects.ts
@@ -0,0 +1,379 @@
+/*
+ * 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 { inject, Injectable } from '@angular/core';
+import { HttpErrorResponse } from '@angular/common/http';
+import { MatDialog } from '@angular/material/dialog';
+import { Actions, createEffect, ofType } from '@ngrx/effects';
+import { catchError, from, map, of, switchMap, tap } from 'rxjs';
+import { MEDIUM_DIALOG, SMALL_DIALOG, XL_DIALOG } from '@nifi/shared';
+import { DropletsService } from '../../service/droplets.service';
+import * as DropletsActions from './droplets.actions';
+import { DeleteDropletDialogComponent } from '../../pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component';
+import {
+    ImportNewDropletDialogComponent,
+    ImportNewFlowDialogData
+} from '../../pages/resources/feature/ui/import-new-droplet-dialog/import-new-droplet-dialog.component';
+import {
+    ImportNewDropletVersionDialogComponent,
+    ImportNewFlowVersionDialogData
+} from '../../pages/resources/feature/ui/import-new-droplet-version-dialog/import-new-droplet-version-dialog.component';
+import {
+    ExportDropletVersionDialogComponent,
+    ExportFlowVersionDialogData
+} from '../../pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component';
+import { DropletVersionsDialogComponent } from '../../pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component';
+import { ErrorHelper } from '../../service/error-helper.service';
+import { Router } from '@angular/router';
+import { ErrorContextKey } from '../error';
+import * as ErrorActions from '../../state/error/error.actions';
+
+@Injectable()
+export class DropletsEffects {
+    constructor(
+        private dropletsService: DropletsService,
+        private dialog: MatDialog,
+        private errorHelper: ErrorHelper,
+        private router: Router
+    ) {}
+
+    actions$ = inject(Actions);
+
+    loadDroplets$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(DropletsActions.loadDroplets),
+            switchMap(() => {
+                return from(
+                    this.dropletsService.getDroplets().pipe(
+                        map((response) =>
+                            DropletsActions.loadDropletsSuccess({
+                                response: {
+                                    droplets: response
+                                }
+                            })
+                        ),
+                        catchError((errorResponse: HttpErrorResponse) => of(this.bannerError(errorResponse)))
+                    )
+                );
+            })
+        )
+    );
+
+    openDeleteDropletDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(DropletsActions.openDeleteDropletDialog),
+                tap(({ request }) => {
+                    this.dialog.open<DeleteDropletDialogComponent, ExportFlowVersionDialogData>(
+                        DeleteDropletDialogComponent,
+                        {
+                            ...SMALL_DIALOG,
+                            autoFocus: false,
+                            data: request
+                        }
+                    );
+                })
+            ),
+        { dispatch: false }
+    );
+
+    deleteDroplet$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(DropletsActions.deleteDroplet),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(this.dropletsService.deleteDroplet(request.droplet.link.href)).pipe(
+                    map((res) => DropletsActions.deleteDropletSuccess({ response: res })),
+                    catchError((errorResponse: HttpErrorResponse) =>
+                        of(this.bannerError(errorResponse, ErrorContextKey.DELETE_DROPLET))
+                    )
+                )
+            )
+        )
+    );
+
+    deleteDropletSuccess$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(DropletsActions.deleteDropletSuccess),
+                tap(() => this.dialog.closeAll())
+            ),
+        { dispatch: false }
+    );
+
+    openImportNewDropletDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(DropletsActions.openImportNewDropletDialog),
+                map((action) => action.request),
+                tap((request) => {
+                    this.dialog.open<ImportNewDropletDialogComponent, ImportNewFlowDialogData>(
+                        ImportNewDropletDialogComponent,
+                        {
+                            ...MEDIUM_DIALOG,
+                            autoFocus: false,
+                            data: {
+                                buckets: request.buckets
+                            }
+                        }
+                    );
+                })
+            ),
+        { dispatch: false }
+    );
+
+    createNewDroplet$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(DropletsActions.createNewDroplet),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(
+                    this.dropletsService.createNewDroplet(request.bucket.link.href, request.name, request.description)
+                ).pipe(
+                    map((res) =>
+                        DropletsActions.createNewDropletSuccess({
+                            response: res,
+                            request: {
+                                href: res.link.href,
+                                file: request.file,
+                                description: request.description
+                            }
+                        })
+                    ),
+                    catchError((errorResponse: HttpErrorResponse) =>
+                        of(this.bannerError(errorResponse, ErrorContextKey.CREATE_DROPLET))
+                    )
+                )
+            )
+        )
+    );
+
+    createNewDropletSuccess$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(DropletsActions.createNewDropletSuccess),
+            tap(() => this.dialog.closeAll()),
+            map((action) =>
+                DropletsActions.importVersionForNewDroplet({
+                    request: action.request,
+                    createdDroplet: action.response
+                })
+            )
+        )
+    );
+
+    importNewDroplet$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(DropletsActions.importNewDropletVersion),
+            map((action) => action.request),
+            switchMap(({ href, file, description }) =>
+                from(this.dropletsService.uploadDroplet(href, file, description)).pipe(
+                    map((res) => DropletsActions.importNewDropletVersionSuccess({ response: res })),
+                    catchError((errorResponse: HttpErrorResponse) =>
+                        of(this.bannerError(errorResponse, ErrorContextKey.IMPORT_DROPLET_VERSION))
+                    )
+                )
+            )
+        )
+    );
+
+    importVersionForNewDroplet$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(DropletsActions.importVersionForNewDroplet),
+            switchMap((action) =>
+                from(
+                    this.dropletsService.uploadDroplet(
+                        action.request.href,
+                        action.request.file,
+                        action.request.description
+                    )
+                ).pipe(
+                    map((res) => DropletsActions.importNewDropletVersionSuccess({ response: res })),
+                    catchError((errorResponse: HttpErrorResponse) =>
+                        of(
+                            DropletsActions.importNewDropletVersionError({
+                                errorContext: {
+                                    errors: [this.errorHelper.getErrorString(errorResponse)],
+                                    context: ErrorContextKey.IMPORT_DROPLET_VERSION
+                                },
+                                createdDroplet: action.createdDroplet
+                            })
+                        )
+                    )
+                )
+            )
+        )
+    );
+
+    importNewDropletSuccess$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(DropletsActions.importNewDropletVersionSuccess),
+                tap(() => this.dialog.closeAll())
+            ),
+        { dispatch: false }
+    );
+
+    importNewDropletVersionError$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(DropletsActions.importNewDropletVersionError),
+            switchMap((action) =>
+                from(this.dropletsService.deleteDroplet(action.createdDroplet.link.href + '?version=0')).pipe(
+                    switchMap(() => [DropletsActions.dropletsBannerError({ errorContext: action.errorContext })]),
+                    catchError(() => [
+                        // If deletion also fails, still show the original import error
+                        DropletsActions.dropletsBannerError({ errorContext: action.errorContext })
+                    ])
+                )
+            )
+        )
+    );
+
+    openImportNewDropletVersionDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(DropletsActions.openImportNewDropletVersionDialog),
+                map((action) => action.request),
+                tap((request) => {
+                    this.dialog.open<ImportNewDropletVersionDialogComponent, ImportNewFlowVersionDialogData>(
+                        ImportNewDropletVersionDialogComponent,
+                        {
+                            ...MEDIUM_DIALOG,
+                            autoFocus: false,
+                            data: {
+                                droplet: request.droplet
+                            }
+                        }
+                    );
+                })
+            ),
+        { dispatch: false }
+    );
+
+    openExportDropletVersionDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(DropletsActions.openExportDropletVersionDialog),
+                map((action) => action.request),
+                tap((request) => {
+                    this.dialog.open<ExportDropletVersionDialogComponent, ExportFlowVersionDialogData>(
+                        ExportDropletVersionDialogComponent,
+                        {
+                            ...MEDIUM_DIALOG,
+                            autoFocus: false,
+                            data: {
+                                droplet: request.droplet
+                            }
+                        }
+                    );
+                })
+            ),
+        { dispatch: false }
+    );
+
+    exportDropletVersion$ = createEffect(() => {
+        return this.actions$.pipe(
+            ofType(DropletsActions.exportDropletVersion),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(
+                    this.dropletsService.exportDropletVersionedSnapshot(request.droplet.link.href, request.version)
+                ).pipe(
+                    tap((res) => {
+                        const stringSnapshot = encodeURIComponent(res.body);
+                        const filename = res.headers.get('Filename');
+
+                        const anchorElement = document.createElement('a');
+                        anchorElement.href = 'data:application/json;charset=utf-8,' + stringSnapshot;
+                        anchorElement.download = filename;
+                        anchorElement.setAttribute('style', 'display: none;');
+
+                        document.body.appendChild(anchorElement);
+                        anchorElement.click();
+                        document.body.removeChild(anchorElement);
+
+                        return res;
+                    }),
+                    map((res) => DropletsActions.exportDropletVersionSuccess({ response: res })),
+                    catchError((errorResponse: HttpErrorResponse) => {
+                        return of(this.bannerError(errorResponse, ErrorContextKey.EXPORT_DROPLET_VERSION));
+                    })
+                )
+            )
+        );
+    });
+
+    exportDropletVersionSuccess$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(DropletsActions.exportDropletVersionSuccess),
+                tap(() => this.dialog.closeAll())
+            ),
+        { dispatch: false }
+    );
+
+    openDropletVersionsDialog$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(DropletsActions.openDropletVersionsDialog),
+            map((action) => action.request),
+            switchMap((request) =>
+                from(this.dropletsService.getDropletSnapshotMetadata(request.droplet.link.href)).pipe(
+                    map((res) => {
+                        this.dialog.open(DropletVersionsDialogComponent, {
+                            ...XL_DIALOG,
+                            autoFocus: false,
+                            data: {
+                                droplet: request.droplet,
+                                versions: res
+                            }
+                        });
+                        return DropletsActions.noOp();
+                    }),
+                    catchError((errorResponse: HttpErrorResponse) => of(this.bannerError(errorResponse)))
+                )
+            )
+        )
+    );
+
+    selectDroplet$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(DropletsActions.selectDroplet),
+                map((action) => action.request),
+                tap((request) => {
+                    this.router.navigate(['/explorer', request.id]);
+                })
+            ),
+        { dispatch: false }
+    );
+
+    dropletsBannerError$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(DropletsActions.dropletsBannerError),
+            map((action) => action.errorContext),
+            switchMap((errorContext) => of(ErrorActions.addBannerError({ errorContext })))
+        )
+    );
+
+    private bannerError(errorResponse: HttpErrorResponse, context: ErrorContextKey = ErrorContextKey.GLOBAL) {
+        return DropletsActions.dropletsBannerError({
+            errorContext: {
+                errors: [this.errorHelper.getErrorString(errorResponse)],
+                context
+            }
+        });
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.reducer.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.reducer.ts
new file mode 100644
index 0000000000..aa0408b040
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.reducer.ts
@@ -0,0 +1,114 @@
+/*
+ * 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 { createReducer, on } from '@ngrx/store';
+import {
+    deleteDroplet,
+    deleteDropletSuccess,
+    importNewDropletVersion,
+    importNewDropletVersionSuccess,
+    createNewDropletSuccess,
+    loadDroplets,
+    loadDropletsSuccess,
+    dropletsBannerError,
+    importVersionForNewDroplet,
+    importNewDropletVersionError
+} from './droplets.actions';
+import { Droplet, DropletsState } from '.';
+import { produce } from 'immer';
+
+export const initialState: DropletsState = {
+    droplets: [],
+    status: 'pending',
+    saving: false
+};
+
+export const dropletsReducer = createReducer(
+    initialState,
+    on(loadDroplets, (state) => ({
+        ...state,
+        status: 'loading' as const
+    })),
+    on(loadDropletsSuccess, (state, { response }) => ({
+        ...state,
+        droplets: response.droplets,
+        status: 'success' as const
+    })),
+    on(deleteDroplet, (state) => ({
+        ...state,
+        saving: true
+    })),
+    on(deleteDropletSuccess, (state, { response }) => {
+        return produce(state, (draftState) => {
+            const componentIndex: number = draftState.droplets.findIndex(
+                (f: Droplet) => response.identifier === f.identifier
+            );
+            if (componentIndex > -1) {
+                draftState.droplets.splice(componentIndex, 1);
+            }
+            draftState.saving = false;
+        });
+    }),
+    on(importNewDropletVersion, (state) => ({
+        ...state,
+        saving: true
+    })),
+    on(createNewDropletSuccess, (state, { response }) => {
+        return produce(state, (draftState) => {
+            const componentIndex: number = draftState.droplets.findIndex(
+                (f: Droplet) => response.identifier === f.identifier
+            );
+            if (componentIndex === -1) {
+                draftState.droplets.push(response);
+            }
+            draftState.saving = false;
+        });
+    }),
+    on(importNewDropletVersionSuccess, (state, { response }) => {
+        return produce(state, (draftState) => {
+            const componentIndex: number = draftState.droplets.findIndex(
+                (f: Droplet) => response.flow.identifier === f.identifier
+            );
+            if (componentIndex === -1) {
+                draftState.droplets.push(response.flow);
+            } else {
+                draftState.droplets[componentIndex].versionCount++;
+            }
+            draftState.saving = false;
+        });
+    }),
+    on(dropletsBannerError, (state) => ({
+        ...state,
+        saving: false
+    })),
+    on(importVersionForNewDroplet, (state) => ({
+        ...state,
+        saving: true
+    })),
+    on(importNewDropletVersionError, (state, { createdDroplet }) => {
+        return produce(state, (draftState) => {
+            // Remove the created flow from state since it will be deleted
+            const componentIndex: number = draftState.droplets.findIndex(
+                (f: Droplet) => createdDroplet.identifier === f.identifier
+            );
+            if (componentIndex > -1) {
+                draftState.droplets.splice(componentIndex, 1);
+            }
+            draftState.saving = false;
+        });
+    })
+);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.selectors.ts
similarity index 53%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.selectors.ts
index 71ad5e3cd6..38db6cd29e 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.selectors.ts
@@ -15,17 +15,21 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
+import { createFeatureSelector, createSelector } from '@ngrx/store';
+import { dropletsFeatureKey, DropletsState } from './index';
+import { selectCurrentRoute } from '@nifi/shared';
 
-.banner-container {
-    @include mat.button-density(-1);
+import { resourcesFeatureKey, ResourcesState } from '..';
 
-    .error-banner-message {
-        word-break: break-word;
-    }
+export const selectResourcesState = createFeatureSelector<ResourcesState>(resourcesFeatureKey);
+
+export const selectDropletState = createSelector(selectResourcesState, (state) => state[dropletsFeatureKey]);
+
+export const selectDroplets = createSelector(selectDropletState, (state: DropletsState) => state.droplets);
 
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
+export const selectDropletIdFromRoute = createSelector(selectCurrentRoute, (route) => {
+    if (route) {
+        return route.params.id;
     }
-}
+    return null;
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/index.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/index.ts
new file mode 100644
index 0000000000..7ae03247ee
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/index.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 { Bucket } from '../../state/buckets';
+import { Link, Revision, Permissions } from '../index';
+
+export const dropletsFeatureKey = 'droplets';
+
+export interface LoadDropletsResponse {
+    droplets: Droplet[];
+}
+
+export interface Droplet {
+    bucketIdentifier: string;
+    bucketName: string;
+    createdTimestamp: number;
+    description: string;
+    identifier: string;
+    link: Link;
+    modifiedTimestamp: number;
+    name: string;
+    permissions: Permissions;
+    revision: Revision;
+    type: string;
+    versionCount: number;
+}
+
+export interface DropletsState {
+    droplets: Droplet[];
+    status: 'pending' | 'loading' | 'success';
+    saving: boolean;
+}
+
+export interface DeleteDropletRequest {
+    droplet: Droplet;
+}
+
+export interface ImportDropletDialog {
+    buckets: Bucket[];
+}
+
+export interface ImportDropletRequest {
+    bucket: Bucket;
+    description: string;
+    file: File;
+    name: string;
+}
+
+export interface ImportDropletVersionRequest {
+    href: string;
+    file: File;
+    description: string;
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.actions.ts
similarity index 66%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.actions.ts
index 71ad5e3cd6..1c36c77d46 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.actions.ts
@@ -15,17 +15,11 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
+import { createAction, props } from '@ngrx/store';
+import { ErrorContext, ErrorContextKey } from './index';
 
-.banner-container {
-    @include mat.button-density(-1);
+export const addBannerError = createAction('[Error] Add Banner Error', props<{ errorContext: ErrorContext }>());
 
-    .error-banner-message {
-        word-break: break-word;
-    }
+export const clearBannerErrors = createAction('[Error] Clear Banner Errors', props<{ context: ErrorContextKey }>());
 
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
-}
+export const resetErrorState = createAction('[Error] Reset Error State');
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.effects.ts
similarity index 76%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.effects.ts
index 71ad5e3cd6..fa3f2f8568 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.effects.ts
@@ -15,17 +15,7 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
+import { Injectable } from '@angular/core';
 
-.banner-container {
-    @include mat.button-density(-1);
-
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
-}
+@Injectable()
+export class ErrorEffects {}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.reducer.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.reducer.ts
new file mode 100644
index 0000000000..c8f3f4c070
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.reducer.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 { createReducer, on } from '@ngrx/store';
+import { ErrorState } from './index';
+import { resetErrorState, addBannerError, clearBannerErrors } from './error.actions';
+import { produce } from 'immer';
+
+export const initialState: ErrorState = {
+    bannerErrors: {}
+};
+
+export const errorReducer = createReducer(
+    initialState,
+    on(addBannerError, (state, { errorContext }) => {
+        return produce(state, (draftState) => {
+            if (draftState.bannerErrors === null) {
+                draftState.bannerErrors = {};
+            }
+            if (!draftState.bannerErrors[errorContext.context]) {
+                draftState.bannerErrors[errorContext.context] = [];
+            }
+
+            draftState.bannerErrors[errorContext.context].push(...errorContext.errors);
+        });
+    }),
+    on(clearBannerErrors, (state, { context }) => {
+        return produce(state, (draftState) => {
+            if (draftState.bannerErrors && draftState.bannerErrors[context]) {
+                delete draftState.bannerErrors[context];
+            }
+        });
+    }),
+    on(resetErrorState, () => ({
+        ...initialState
+    }))
+);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.selectors.ts
similarity index 61%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.selectors.ts
index 71ad5e3cd6..5a2c8cf731 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.selectors.ts
@@ -15,17 +15,12 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
+import { createFeatureSelector, createSelector } from '@ngrx/store';
+import { BannerErrors, errorFeatureKey, ErrorState } from './index';
 
-.banner-container {
-    @include mat.button-density(-1);
+export const selectErrorState = createFeatureSelector<ErrorState>(errorFeatureKey);
 
-    .error-banner-message {
-        word-break: break-word;
-    }
+export const selectAllBannerErrors = createSelector(selectErrorState, (state: ErrorState) => state.bannerErrors);
 
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
-}
+export const selectBannerErrors = (context: string) =>
+    createSelector(selectAllBannerErrors, (bannerErrors: BannerErrors) => bannerErrors[context] || []);
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/index.ts
similarity index 56%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/index.ts
index 71ad5e3cd6..621c666253 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/index.ts
@@ -15,17 +15,31 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
+export const errorFeatureKey = 'error';
 
-.banner-container {
-    @include mat.button-density(-1);
+export interface ErrorDetail {
+    title: string;
+    message: string;
+}
+
+export enum ErrorContextKey {
+    EXPORT_DROPLET_VERSION = 'droplet listing',
+    DELETE_DROPLET = 'delete droplet',
+    CREATE_DROPLET = 'create droplet',
+    IMPORT_DROPLET_VERSION = 'import droplet version',
+    GLOBAL = 'global'
+}
 
-    .error-banner-message {
-        word-break: break-word;
-    }
+export interface ErrorContext {
+    context: ErrorContextKey;
+    errors: string[];
+}
+
+export interface BannerErrors {
+    // key should be the ErrorContextKey of the banner error
+    [key: string]: string[];
+}
 
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
+export interface ErrorState {
+    bannerErrors: BannerErrors;
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/index.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/index.ts
new file mode 100644
index 0000000000..ffb913e079
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/index.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 { routerReducer, RouterReducerState, DEFAULT_ROUTER_FEATURENAME } from '@ngrx/router-store';
+import { Action, ActionReducerMap, combineReducers } from '@ngrx/store';
+import { dropletsFeatureKey, DropletsState } from './droplets';
+import { dropletsReducer } from './droplets/droplets.reducer';
+import { bucketsFeatureKey, BucketsState } from './buckets';
+import { bucketsReducer } from './buckets/buckets.reducer';
+import { errorReducer } from './error/error.reducer';
+import { errorFeatureKey, ErrorState } from './error';
+
+export const resourcesFeatureKey = 'resources';
+
+export interface Params {
+    rel: string;
+}
+
+export interface Link {
+    href: string;
+    params: Params;
+}
+
+export interface Revision {
+    version: number;
+}
+
+export interface Permissions {
+    canRead: boolean;
+    canWrite: boolean;
+}
+
+export interface ResourcesState {
+    [dropletsFeatureKey]: DropletsState;
+    [bucketsFeatureKey]: BucketsState;
+}
+
+export function reducers(state: ResourcesState | undefined, action: Action) {
+    return combineReducers({
+        [dropletsFeatureKey]: dropletsReducer,
+        [bucketsFeatureKey]: bucketsReducer
+    })(state, action);
+}
+
+export interface NiFiRegistryState {
+    [DEFAULT_ROUTER_FEATURENAME]: RouterReducerState;
+    [errorFeatureKey]: ErrorState;
+}
+
+export const rootReducers: ActionReducerMap<NiFiRegistryState> = {
+    [DEFAULT_ROUTER_FEATURENAME]: routerReducer,
+    [errorFeatureKey]: errorReducer
+};
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component.html
new file mode 100644
index 0000000000..20e35b964c
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component.html
@@ -0,0 +1,20 @@
+<!--
+  ~ 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.
+  -->
+
+<error-banner [messages]="messages$ | async" (dismiss)="dismiss()"></error-banner>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component.scss
new file mode 100644
index 0000000000..3d56d22bb3
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component.scss
@@ -0,0 +1,18 @@
+/*!
+ * 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.
+ */
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component.spec.ts
new file mode 100644
index 0000000000..6d73d99aa5
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component.spec.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ContextErrorBanner } from './context-error-banner.component';
+import { errorFeatureKey } from '../../../state/error';
+import { provideMockStore } from '@ngrx/store/testing';
+import { initialState as initialErrorState } from '../../../state/error/error.reducer';
+
+describe('ContextErrorBanner', () => {
+    let component: ContextErrorBanner;
+    let fixture: ComponentFixture<ContextErrorBanner>;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [ContextErrorBanner],
+            providers: [
+                provideMockStore({
+                    initialState: {
+                        [errorFeatureKey]: initialErrorState
+                    }
+                })
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ContextErrorBanner);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/context-error-banner/context-error-banner.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component.ts
similarity index 92%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/context-error-banner/context-error-banner.component.ts
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component.ts
index 5d54cfdc5e..c05221f5a6 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/context-error-banner/context-error-banner.component.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/common/context-error-banner/context-error-banner.component.ts
@@ -19,13 +19,13 @@
 
 import { Component, DestroyRef, inject, Input, OnDestroy } from '@angular/core';
 import { CommonModule } from '@angular/common';
-import { NiFiState } from '../../../state';
+import { NiFiRegistryState } from '../../../state';
 import { Store } from '@ngrx/store';
 import { clearBannerErrors } from '../../../state/error/error.actions';
 import { Observable } from 'rxjs';
 import { selectBannerErrors } from '../../../state/error/error.selectors';
 import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { ErrorBanner } from '../error-banner/error-banner.component';
+import { ErrorBanner } from '@nifi/shared';
 import { ErrorContextKey } from '../../../state/error';
 
 @Component({
@@ -48,7 +48,7 @@ export class ContextErrorBanner implements OnDestroy {
     messages$: Observable<string[]> | null = null;
     private destroyRef: DestroyRef = inject(DestroyRef);
 
-    constructor(private store: Store<NiFiState>) {}
+    constructor(private store: Store<NiFiRegistryState>) {}
 
     ngOnDestroy(): void {
         this.store.dispatch(clearBannerErrors({ context: this.context }));
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/_header.component-theme.scss
similarity index 57%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/_header.component-theme.scss
index 71ad5e3cd6..a8dd6f0751 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/_header.component-theme.scss
@@ -15,17 +15,33 @@
  * limitations under the License.
  */
 
+@use 'sass:map';
 @use '@angular/material' as mat;
 
-.banner-container {
-    @include mat.button-density(-1);
+@mixin generate-theme() {
+    .nifi-registry-navigation {
+        background-color: var(--mat-sys-secondary);
 
-    .error-banner-message {
-        word-break: break-word;
-    }
+        .global-menu-icon.global-menu {
+            @extend .secondary-contrast;
+        }
+
+        .global-menu-icon.global-menu:disabled {
+            @extend .secondary-contrast;
+        }
+
+        .current-user {
+            @extend .secondary-contrast;
+            font-family: var(--mat-sys-body-medium-font);
+        }
+
+        a {
+            @extend .secondary-contrast;
+            text-decoration-color: var(--mat-sys-on-secondary);
+        }
 
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
+        a:hover {
+            opacity: 0.6;
+        }
     }
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.html
new file mode 100644
index 0000000000..d991de643d
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.html
@@ -0,0 +1,115 @@
+<!--
+~  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.
+-->
+<div class="flex flex-col">
+    <nav class="nifi-registry-navigation select-none">
+        <div class="flex justify-between items-center h-[64px] pl-4">
+            <div class="flex items-center">
+                <div class="ml-2 mr-2 logo-container relative rounded-md">
+                    <img
+                        ngSrc="assets/icons/registry-logo-web-app.svg"
+                        fill
+                        priority
+                        alt="nifi registry logo"
+                        class="pointer p-3"
+                        (click)="goHome()" />
+                </div>
+                <div class="h-[64px] w-[112px] mr-6 relative">
+                    <img ngSrc="assets/icons/nifi-logo.svg" fill priority alt="NiFi Logo" />
+                </div>
+            </div>
+            <div class="flex justify-between items-center gap-x-1">
+                <div class="flex flex-col justify-between items-end gap-y-1">
+                    <!-- TODO: current user/anonymous user                       -->
+                </div>
+                <button mat-button [matMenuTriggerFor]="globalMenu" class="global-menu-icon global-menu">
+                    <i class="fa fa-navicon text-[32px]"></i>
+                </button>
+                <mat-menu #globalMenu="matMenu" xPosition="before">
+                    <button mat-menu-item class="global-menu-item">Buckets</button>
+                    <button mat-menu-item class="global-menu-item">Users/Groups</button>
+                    <button mat-menu-item class="global-menu-item">
+                        <i class="fa fa-info-circle primary-color mr-2"></i>
+                        About
+                    </button>
+                    <button mat-menu-item [matMenuTriggerFor]="theming">
+                        <i class="fa mr-2"></i>
+                        Appearance
+                    </button>
+                    <button mat-menu-item [matMenuTriggerFor]="animations">
+                        <i class="fa mr-2"></i>
+                        Animations
+                    </button>
+                </mat-menu>
+                <mat-menu #theming="matMenu" xPosition="before">
+                    <button mat-menu-item class="global-menu-item" (click)="toggleTheme(LIGHT_THEME)">
+                        @if (theme === LIGHT_THEME) {
+                            <i class="fa fa-check primary-color mr-2"></i>
+                        }
+                        @if (theme !== LIGHT_THEME) {
+                            <i class="fa mr-2"></i>
+                        }
+                        Light
+                    </button>
+                    <button mat-menu-item class="global-menu-item" (click)="toggleTheme(DARK_THEME)">
+                        @if (theme === DARK_THEME) {
+                            <i class="fa fa-check primary-color mr-2"></i>
+                        }
+                        @if (theme !== DARK_THEME) {
+                            <i class="fa mr-2"></i>
+                        }
+                        Dark
+                    </button>
+                    <button mat-menu-item class="global-menu-item" (click)="toggleTheme(OS_SETTING)">
+                        @if (theme === OS_SETTING || theme === null) {
+                            <i class="fa fa-check primary-color mr-2"></i>
+                        }
+                        @if (theme !== OS_SETTING && theme !== null) {
+                            <i class="fa mr-2"></i>
+                        }
+                        Device
+                    </button>
+                </mat-menu>
+                <mat-menu #animations="matMenu" xPosition="before">
+                    <button mat-menu-item class="global-menu-item" (click)="toggleAnimations('false')">
+                        @if (disableAnimations === 'false') {
+                            <i class="fa fa-check primary-color mr-2"></i>
+                        } @else {
+                            <i class="fa mr-2"></i>
+                        }
+                        Enabled
+                    </button>
+                    <button mat-menu-item class="global-menu-item" (click)="toggleAnimations('true')">
+                        @if (disableAnimations === 'true') {
+                            <i class="fa fa-check primary-color mr-2"></i>
+                        } @else {
+                            <i class="fa mr-2"></i>
+                        }
+                        Disabled
+                    </button>
+                    <button mat-menu-item class="global-menu-item" (click)="toggleAnimations()">
+                        @if (disableAnimations === null) {
+                            <i class="fa fa-check primary-color mr-2"></i>
+                        } @else {
+                            <i class="fa mr-2"></i>
+                        }
+                        Device
+                    </button>
+                </mat-menu>
+            </div>
+        </div>
+    </nav>
+</div>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.scss
similarity index 56%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.scss
index 71ad5e3cd6..5bcf14cfd2 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.scss
@@ -15,17 +15,39 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
+.nifi-registry-navigation {
+    .icon {
+        font-size: 32px;
+    }
+
+    .logo-container {
+        background-color: #2c3e44; // legacy registry header background color not part of theme
+        height: 52px;
+        width: 52px;
+    }
 
-.banner-container {
-    @include mat.button-density(-1);
+    .current-user {
+        max-width: 250px;
+        text-overflow: ellipsis;
+        line-height: normal;
+        overflow: hidden;
+        white-space: nowrap;
+    }
 
-    .error-banner-message {
-        word-break: break-word;
+    a {
+        text-transform: uppercase;
     }
 
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
+    .global-menu {
+        height: 64px;
+        width: 64px;
+        border-radius: 0;
+    }
+}
+
+button.global-menu-item {
+    .fa,
+    .icon {
+        text-align: start;
     }
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.spec.ts
similarity index 80%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.spec.ts
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.spec.ts
index a1d0d010e5..f55e94b453 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.spec.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.spec.ts
@@ -16,18 +16,18 @@
  */
 
 import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { HeaderComponent } from './header.component';
 
-import { ErrorBanner } from './error-banner.component';
-
-describe('ErrorBanner', () => {
-    let component: ErrorBanner;
-    let fixture: ComponentFixture<ErrorBanner>;
+describe('HeaderComponent', () => {
+    let component: HeaderComponent;
+    let fixture: ComponentFixture<HeaderComponent>;
 
     beforeEach(() => {
         TestBed.configureTestingModule({
-            imports: [ErrorBanner]
+            imports: [HeaderComponent]
         });
-        fixture = TestBed.createComponent(ErrorBanner);
+
+        fixture = TestBed.createComponent(HeaderComponent);
         component = fixture.componentInstance;
         fixture.detectChanges();
     });
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.ts
new file mode 100644
index 0000000000..43a41a460d
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.ts
@@ -0,0 +1,73 @@
+/*
+ * 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 { Component } from '@angular/core';
+import { CommonModule, NgOptimizedImage } from '@angular/common';
+import { Router, RouterModule } from '@angular/router';
+import { MatButton } from '@angular/material/button';
+import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
+import { DARK_THEME, LIGHT_THEME, OS_SETTING, Storage, ThemingService } from '@nifi/shared';
+
+@Component({
+    selector: 'app-header',
+    standalone: true,
+    imports: [CommonModule, NgOptimizedImage, RouterModule, MatButton, MatMenu, MatMenuItem, MatMenuTrigger],
+    templateUrl: './header.component.html',
+    styleUrl: './header.component.scss'
+})
+export class HeaderComponent {
+    theme: any | undefined;
+    darkModeOn: boolean | undefined;
+    LIGHT_THEME: string = LIGHT_THEME;
+    DARK_THEME: string = DARK_THEME;
+    OS_SETTING: string = OS_SETTING;
+    disableAnimations: string | null;
+
+    constructor(
+        private storage: Storage,
+        private themingService: ThemingService,
+        private router: Router
+    ) {
+        this.darkModeOn = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
+        this.theme = this.storage.getItem('theme');
+        this.disableAnimations = this.storage.getItem('disable-animations');
+
+        if (window.matchMedia) {
+            // Watch for changes of the preference
+            window.matchMedia('(prefers-color-scheme: dark)').addListener((e) => {
+                this.darkModeOn = e.matches;
+                this.theme = this.storage.getItem('theme');
+            });
+        }
+    }
+
+    goHome() {
+        this.router.navigateByUrl('/resources');
+    }
+
+    toggleTheme(theme: string) {
+        this.theme = theme;
+        this.storage.setItem('theme', theme);
+        this.themingService.toggleTheme(!!this.darkModeOn, theme);
+    }
+
+    toggleAnimations(disableAnimations: string = '') {
+        this.disableAnimations = disableAnimations;
+        this.storage.setItem('disable-animations', this.disableAnimations.toString());
+        window.location.reload();
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/environments/environment.development.ts
similarity index 76%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/environments/environment.development.ts
index 71ad5e3cd6..584182e1d9 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/environments/environment.development.ts
@@ -15,17 +15,6 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
-
-.banner-container {
-    @include mat.button-density(-1);
-
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
-}
+export const environment = {
+    production: false
+};
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/environments/environment.ts
similarity index 76%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/environments/environment.ts
index 71ad5e3cd6..29d7aa9df3 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/environments/environment.ts
@@ -15,17 +15,6 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
-
-.banner-container {
-    @include mat.button-density(-1);
-
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
-}
+export const environment = {
+    production: true
+};
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/index.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/index.html
new file mode 100644
index 0000000000..933cfcf13d
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/index.html
@@ -0,0 +1,29 @@
+<!--
+  ~ 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.
+  -->
+
+<!doctype html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <title>NiFi Registry</title>
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <link rel="icon" type="image/x-icon" href="assets/icons/registry-favicon.png" />
+    </head>
+    <body class="mat-app-background mat-typography text-base">
+        <nifi-registry-app-root></nifi-registry-app-root>
+    </body>
+</html>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/main.ts
similarity index 76%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/main.ts
index 71ad5e3cd6..b6eaeef9ed 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/main.ts
@@ -15,17 +15,10 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
 
-.banner-container {
-    @include mat.button-density(-1);
+import { AppModule } from './app/app.module';
 
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
-}
+platformBrowserDynamic()
+    .bootstrapModule(AppModule)
+    .catch((err) => console.error(err));
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/styles.scss
similarity index 52%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/styles.scss
index 71ad5e3cd6..49961a0512 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/styles.scss
@@ -16,16 +16,32 @@
  */
 
 @use '@angular/material' as mat;
+@use 'sass:map';
 
-.banner-container {
-    @include mat.button-density(-1);
+@use 'libs/shared/src/assets/styles/app' as app;
+@use 'app/ui/header/header.component-theme' as header;
 
-    .error-banner-message {
-        word-break: break-word;
-    }
+@use 'font-awesome';
+@use 'libs/shared/src/assets/fonts/flowfont/flowfont.css';
+@use 'libs/shared/src/assets/themes/material';
+@use 'libs/shared/src/assets/styles/listing-table' as listing-table;
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+// only include these once (not needed for dark mode)
+@include app.styles();
+@include listing-table.styles();
+
+html {
+    @include app.generate-material-theme();
+    @include listing-table.generate-theme();
+    @include header.generate-theme();
 
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
+    .dark-theme {
+        @include app.generate-material-theme();
+        @include listing-table.generate-theme();
+        @include header.generate-theme();
     }
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/test-setup.ts
similarity index 76%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/test-setup.ts
index 71ad5e3cd6..d0be6ebc08 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/test-setup.ts
@@ -15,17 +15,6 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
+import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
 
-.banner-container {
-    @include mat.button-density(-1);
-
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
-}
+setupZoneTestEnv();
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/tailwind.config.js
similarity index 61%
copy from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/tailwind.config.js
index 71ad5e3cd6..52c0148d7e 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/tailwind.config.js
@@ -6,7 +6,7 @@
  * (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
+ *    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,
@@ -15,17 +15,15 @@
  * limitations under the License.
  */
 
-@use '@angular/material' as mat;
+const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind');
+const { join } = require('path');
+const baseTheme = require('../../libs/shared/tailwind.theme.json');
 
-.banner-container {
-    @include mat.button-density(-1);
-
-    .error-banner-message {
-        word-break: break-word;
-    }
-
-    ul {
-        list-style-type: disc;
-        list-style-position: inside;
-    }
-}
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+    content: [join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'), ...createGlobPatternsForDependencies(__dirname)],
+    theme: {
+        ...baseTheme
+    },
+    plugins: []
+};
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/tsconfig.app.json b/nifi-frontend/src/main/frontend/apps/nifi-registry/tsconfig.app.json
new file mode 100644
index 0000000000..994d3618ce
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/tsconfig.app.json
@@ -0,0 +1,9 @@
+{
+    "extends": "./tsconfig.json",
+    "compilerOptions": {
+        "outDir": "../../out-tsc/app",
+        "types": []
+    },
+    "files": ["src/main.ts"],
+    "include": ["src/**/*.d.ts"]
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/tsconfig.json b/nifi-frontend/src/main/frontend/apps/nifi-registry/tsconfig.json
new file mode 100644
index 0000000000..29abb3698f
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/tsconfig.json
@@ -0,0 +1,19 @@
+{
+    "compilerOptions": {
+        "outDir": "../../dist/out-tsc",
+        "forceConsistentCasingInFileNames": true,
+        "strict": true,
+        "noImplicitOverride": true,
+        "noPropertyAccessFromIndexSignature": true,
+        "noImplicitReturns": true,
+        "noFallthroughCasesInSwitch": true,
+        "useDefineForClassFields": false
+    },
+    "extends": "../../tsconfig.base.json",
+    "angularCompilerOptions": {
+        "enableI18nLegacyMessageIdFormat": false,
+        "strictInjectionParameters": true,
+        "strictInputAccessModifiers": true,
+        "strictTemplates": true
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/tsconfig.spec.json b/nifi-frontend/src/main/frontend/apps/nifi-registry/tsconfig.spec.json
new file mode 100644
index 0000000000..ad652198e3
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/tsconfig.spec.json
@@ -0,0 +1,11 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+    "extends": "./tsconfig.json",
+    "compilerOptions": {
+        "outDir": "../../out-tsc/spec",
+        "types": ["jest"],
+        "module": "ES2022",
+        "esModuleInterop": true
+    },
+    "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/app.module.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/app.module.ts
index da7e4f503c..4adf2ed4de 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/app.module.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/app.module.ts
@@ -59,11 +59,21 @@ import { CLIPBOARD_OPTIONS, provideMarkdown } from 'ngx-markdown';
 import { CopyEffects } from './state/copy/copy.effects';
 
 const entry = localStorage.getItem('disable-animations');
-let disableAnimations: string = entry !== null ? JSON.parse(entry).item : '';
+let disableAnimations: string = '';
+
+try {
+    disableAnimations = entry !== null ? JSON.parse(entry).item : '';
+} catch (error) {
+    /* empty */
+}
 
 // honor OS settings if user has not explicitly disabled animations for the application
-if (disableAnimations !== 'true' && disableAnimations !== 'false') {
-    disableAnimations = window.matchMedia('(prefers-reduced-motion: reduce)').matches.toString();
+try {
+    if (disableAnimations !== 'true' && disableAnimations !== 'false') {
+        disableAnimations = window.matchMedia('(prefers-reduced-motion: reduce)').matches.toString();
+    }
+} catch (error) {
+    /* empty */
 }
 
 export const customTooltipDefaults: MatTooltipDefaultOptions = {
@@ -104,7 +114,9 @@ export const customTooltipDefaults: MatTooltipDefaultOptions = {
         StoreDevtoolsModule.instrument({
             maxAge: 25,
             logOnly: environment.production,
-            autoPause: true
+            autoPause: true,
+            name: 'NiFi',
+            trace: !environment.production
         }),
         MatProgressSpinnerModule,
         MatNativeDateModule,
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.module.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.module.ts
index 025d3757d3..7b2f406308 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.module.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/access-policies/ui/component-access-policies/component-access-policies.module.ts
@@ -17,18 +17,19 @@
 
 import { NgModule } from '@angular/core';
 import { ComponentAccessPolicies } from './component-access-policies.component';
-import { CommonModule } from '@angular/common';
+import { CommonModule, AsyncPipe } from '@angular/common';
 import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
 import { MatTableModule } from '@angular/material/table';
 import { MatSortModule } from '@angular/material/sort';
 import { MatInputModule } from '@angular/material/input';
+import { MatFormFieldModule } from '@angular/material/form-field';
 import { ReactiveFormsModule } from '@angular/forms';
 import { MatSelectModule } from '@angular/material/select';
 import { ComponentAccessPoliciesRoutingModule } from './component-access-policies-routing.module';
+import { RouterModule } from '@angular/router';
 import { PolicyTable } from '../common/policy-table/policy-table.component';
 import { MatButtonModule } from '@angular/material/button';
-import { ErrorBanner } from '../../../../ui/common/error-banner/error-banner.component';
-import { ComponentContext, NifiTooltipDirective } from '@nifi/shared';
+import { ComponentContext, NifiTooltipDirective, ErrorBanner } from '@nifi/shared';
 import { ContextErrorBanner } from '../../../../ui/common/context-error-banner/context-error-banner.component';
 
 @NgModule({
@@ -36,13 +37,16 @@ import { ContextErrorBanner } from '../../../../ui/common/context-error-banner/c
     exports: [ComponentAccessPolicies],
     imports: [
         CommonModule,
+        AsyncPipe,
         ComponentAccessPoliciesRoutingModule,
         NgxSkeletonLoaderModule,
         MatTableModule,
         MatSortModule,
         MatInputModule,
+        MatFormFieldModule,
         ReactiveFormsModule,
         MatSelectModule,
+        RouterModule,
         NifiTooltipDirective,
         PolicyTable,
         MatButtonModule,
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.module.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.module.ts
index 68b97e1876..68162008e2 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.module.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/access-policies/ui/global-access-policies/global-access-policies.module.ts
@@ -17,18 +17,18 @@
 
 import { NgModule } from '@angular/core';
 import { GlobalAccessPolicies } from './global-access-policies.component';
-import { CommonModule } from '@angular/common';
+import { CommonModule, AsyncPipe } from '@angular/common';
 import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
 import { MatTableModule } from '@angular/material/table';
 import { MatSortModule } from '@angular/material/sort';
 import { MatInputModule } from '@angular/material/input';
+import { MatFormFieldModule } from '@angular/material/form-field';
 import { ReactiveFormsModule } from '@angular/forms';
 import { MatSelectModule } from '@angular/material/select';
 import { GlobalAccessPoliciesRoutingModule } from './global-access-policies-routing.module';
-import { NifiTooltipDirective } from '@nifi/shared';
+import { NifiTooltipDirective, ErrorBanner } from '@nifi/shared';
 import { PolicyTable } from '../common/policy-table/policy-table.component';
 import { MatButtonModule } from '@angular/material/button';
-import { ErrorBanner } from '../../../../ui/common/error-banner/error-banner.component';
 import { ContextErrorBanner } from '../../../../ui/common/context-error-banner/context-error-banner.component';
 
 @NgModule({
@@ -36,11 +36,13 @@ import { ContextErrorBanner } from '../../../../ui/common/context-error-banner/c
     exports: [GlobalAccessPolicies],
     imports: [
         CommonModule,
+        AsyncPipe,
         GlobalAccessPoliciesRoutingModule,
         NgxSkeletonLoaderModule,
         MatTableModule,
         MatSortModule,
         MatInputModule,
+        MatFormFieldModule,
         ReactiveFormsModule,
         MatSelectModule,
         NifiTooltipDirective,
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/cluster/feature/cluster.module.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/cluster/feature/cluster.module.ts
index 5e83cb1db9..49073179eb 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/cluster/feature/cluster.module.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/cluster/feature/cluster.module.ts
@@ -24,9 +24,10 @@ import { clusterFeatureKey, reducers } from '../state';
 import { EffectsModule } from '@ngrx/effects';
 import { ClusterListingEffects } from '../state/cluster-listing/cluster-listing.effects';
 import { ClusterRoutingModule } from './cluster-routing.module';
+import { RouterModule } from '@angular/router';
 import { MatTabsModule } from '@angular/material/tabs';
 import { MatIconButton } from '@angular/material/button';
-import { ErrorBanner } from '../../../ui/common/error-banner/error-banner.component';
+import { ErrorBanner } from '@nifi/shared';
 import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
 import { ContextErrorBanner } from '../../../ui/common/context-error-banner/context-error-banner.component';
 
@@ -41,6 +42,7 @@ import { ContextErrorBanner } from '../../../ui/common/context-error-banner/cont
         EffectsModule.forFeature(ClusterListingEffects),
         MatTabsModule,
         MatIconButton,
+        RouterModule,
         ErrorBanner,
         BannerText,
         ContextErrorBanner
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
index 353433539a..13ae7a098a 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
@@ -279,7 +279,7 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
         id: 'upstream-downstream',
         menuItems: [
             {
-                condition: (selection: any) => {
+                condition: () => {
                     // TODO - hasUpstream
                     return false;
                 },
@@ -290,7 +290,7 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
                 }
             },
             {
-                condition: (selection: any) => {
+                condition: () => {
                     // TODO - hasDownstream
                     return false;
                 },
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts
index 91898dbddc..7c264d95c6 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.spec.ts
@@ -67,8 +67,6 @@ describe('FlowEffects', () => {
     let propertyTableHelperService: PropertyTableHelperService;
     let dialog: MatDialog;
     let store: MockStore;
-    let copyPasteService: CopyPasteService;
-    let canvasView: CanvasView;
     let verify: EventEmitter<VerifyPropertiesRequestContext>;
     let editProcessor: EventEmitter<UpdateProcessorRequest>;
     let startRequest: EventEmitter<StartComponentRequest>;
@@ -875,8 +873,6 @@ describe('FlowEffects', () => {
         action$ = new ReplaySubject<Action>();
         flowService = TestBed.inject(FlowService);
         propertyTableHelperService = TestBed.inject(PropertyTableHelperService);
-        copyPasteService = TestBed.inject(CopyPasteService);
-        canvasView = TestBed.inject(CanvasView);
         dialog = TestBed.inject(MatDialog);
         store = TestBed.inject(MockStore);
         verify = new EventEmitter<VerifyPropertiesRequestContext>();
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/change-version-progress-dialog/change-version-progress-dialog.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/change-version-progress-dialog/change-version-progress-dialog.ts
index 05356944ae..0da3259cce 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/change-version-progress-dialog/change-version-progress-dialog.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/change-version-progress-dialog/change-version-progress-dialog.ts
@@ -22,7 +22,7 @@ import { FlowUpdateRequestEntity } from '../../../../../state/flow';
 import { Observable, of } from 'rxjs';
 import { AsyncPipe } from '@angular/common';
 import { MatProgressBar } from '@angular/material/progress-bar';
-import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
+import { ErrorBanner } from '@nifi/shared';
 
 @Component({
     selector: 'change-version-progress-dialog',
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/context-error-banner/context-error-banner.component.ts b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/context-error-banner/context-error-banner.component.ts
index 5d54cfdc5e..6dd7d0f7a7 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/context-error-banner/context-error-banner.component.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/context-error-banner/context-error-banner.component.ts
@@ -25,7 +25,7 @@ import { clearBannerErrors } from '../../../state/error/error.actions';
 import { Observable } from 'rxjs';
 import { selectBannerErrors } from '../../../state/error/error.selectors';
 import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { ErrorBanner } from '../error-banner/error-banner.component';
+import { ErrorBanner } from '@nifi/shared';
 import { ErrorContextKey } from '../../../state/error';
 
 @Component({
diff --git a/nifi-frontend/src/main/frontend/libs/shared/src/assets/icons/registry-background-logo.svg b/nifi-frontend/src/main/frontend/libs/shared/src/assets/icons/registry-background-logo.svg
new file mode 100644
index 0000000000..bd7281516b
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/libs/shared/src/assets/icons/registry-background-logo.svg
@@ -0,0 +1,17 @@
+<!--
+    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.
+-->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 142.71 182.73"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:#6b8791;}.cls-3{fill:#8da4aa;}.cls-4{fill:#b0c1c6;}</style><linearGradient id="linear-gradient" x1="79.26" y1="128.13" x2="79.26" y2="78.82" gradientUnits="userSpaceOnUse"><stop offset="0.1" stop-color="#e2e6e8"/><stop offset="1" stop-color="#e2e6e8" stop-opacity="0"/></linearGradient></defs><title>Asset 5</title><g id= [...]
\ No newline at end of file
diff --git a/nifi-frontend/src/main/frontend/libs/shared/src/assets/icons/registry-favicon.png b/nifi-frontend/src/main/frontend/libs/shared/src/assets/icons/registry-favicon.png
new file mode 100644
index 0000000000..87dc8c96b1
Binary files /dev/null and b/nifi-frontend/src/main/frontend/libs/shared/src/assets/icons/registry-favicon.png differ
diff --git a/nifi-frontend/src/main/frontend/libs/shared/src/assets/icons/registry-logo-web-app.svg b/nifi-frontend/src/main/frontend/libs/shared/src/assets/icons/registry-logo-web-app.svg
new file mode 100644
index 0000000000..25cf2ae2c0
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/libs/shared/src/assets/icons/registry-logo-web-app.svg
@@ -0,0 +1,17 @@
+<!--
+    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.
+-->
+<svg id="registryl-logo-mini" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 37.49 48"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:#acbbbf;}.cls-3{fill:#899ea3;}.cls-4{fill:#678289;}</style><linearGradient id="linear-gradient" x1="20.82" y1="33.66" x2="20.82" y2="20.7" gradientUnits="userSpaceOnUse"><stop offset="0.3" stop-color="#678289" stop-opacity="0.9"/><stop offset="1" stop-color="#678289" stop-opacity="0"/></linearGradien [...]
\ No newline at end of file
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.html b/nifi-frontend/src/main/frontend/libs/shared/src/components/error-banner/error-banner.component.html
similarity index 96%
rename from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.html
rename to nifi-frontend/src/main/frontend/libs/shared/src/components/error-banner/error-banner.component.html
index 85b96e53a7..f2a35e7df9 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.html
+++ b/nifi-frontend/src/main/frontend/libs/shared/src/components/error-banner/error-banner.component.html
@@ -29,7 +29,7 @@
                 <div class="error-banner-message">{{ messages[0] }}</div>
             } @else {
                 <ul>
-                    @for (message of messages; track $index) {
+                    @for (message of messages; track message) {
                         <li class="error-banner-message">{{ message }}</li>
                     }
                 </ul>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss b/nifi-frontend/src/main/frontend/libs/shared/src/components/error-banner/error-banner.component.scss
similarity index 100%
rename from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.scss
rename to nifi-frontend/src/main/frontend/libs/shared/src/components/error-banner/error-banner.component.scss
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.spec.ts b/nifi-frontend/src/main/frontend/libs/shared/src/components/error-banner/error-banner.component.spec.ts
similarity index 100%
rename from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.spec.ts
rename to nifi-frontend/src/main/frontend/libs/shared/src/components/error-banner/error-banner.component.spec.ts
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.ts b/nifi-frontend/src/main/frontend/libs/shared/src/components/error-banner/error-banner.component.ts
similarity index 95%
rename from nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.ts
rename to nifi-frontend/src/main/frontend/libs/shared/src/components/error-banner/error-banner.component.ts
index 9681c6c647..831a907d28 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/error-banner/error-banner.component.ts
+++ b/nifi-frontend/src/main/frontend/libs/shared/src/components/error-banner/error-banner.component.ts
@@ -23,7 +23,8 @@ import { MatButtonModule } from '@angular/material/button';
     selector: 'error-banner',
     imports: [MatButtonModule, NgClass],
     templateUrl: './error-banner.component.html',
-    styleUrls: ['./error-banner.component.scss']
+    styleUrls: ['./error-banner.component.scss'],
+    standalone: true
 })
 export class ErrorBanner {
     @Input() messages: string[] | null = null;
diff --git a/nifi-frontend/src/main/frontend/libs/shared/src/components/index.ts b/nifi-frontend/src/main/frontend/libs/shared/src/components/index.ts
index c3fd5c10fd..dd6735156e 100644
--- a/nifi-frontend/src/main/frontend/libs/shared/src/components/index.ts
+++ b/nifi-frontend/src/main/frontend/libs/shared/src/components/index.ts
@@ -28,3 +28,4 @@ export * from './codemirror/themes/baseTheme';
 export * from './yes-no-dialog/yes-no-dialog.component';
 export * from './tooltips/property-hint-tip/property-hint-tip.component';
 export * from './tooltips/text-tip/text-tip.component';
+export * from './error-banner/error-banner.component';