NIFI-14320 buckets listing and management, (manage bucket policies is… (#10346)

* NIFI-14320 buckets listing and management, (manage bucket policies is still TODO)

* address review feedback

* remove unused imports, fix broken unit test, update delete UX to use Yes/No dialog from shared, add test coverage for droplets and buckets effects

* fix legacy routes

* align menu option, remove unused legacy redirect

* remove empty state

This closes #10346
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
index 3b71ab7..a9f8e24 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/jest.config.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/jest.config.ts
@@ -17,9 +17,17 @@
 
 export default {
     displayName: 'NiFi Registry',
-    preset: '../../jest.preset.js',
-    setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
+    clearMocks: true,
     coverageDirectory: '../../coverage/apps/nifi-registry',
+    extensionsToTreatAsEsm: ['.ts'],
+
+    preset: '../../jest.preset.js',
+
+    // The test environment that will be used for testing
+    testEnvironment: '@happy-dom/jest-environment',
+
+    setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
+
     transform: {
         '^.+\\.(ts|mjs|js|html)$': [
             'jest-preset-angular',
@@ -29,10 +37,5 @@
             }
         ]
     },
-    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'
-    ]
+    transformIgnorePatterns: []
 };
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/project.json b/nifi-frontend/src/main/frontend/apps/nifi-registry/project.json
index 0dcc095..562c83d 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/project.json
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/project.json
@@ -25,6 +25,11 @@
                         "glob": "**/*.svg",
                         "input": "libs/shared/src/assets/",
                         "output": "./assets"
+                    },
+                    {
+                        "glob": "**/*.png",
+                        "input": "libs/shared/src/assets/",
+                        "output": "./assets"
                     }
                 ],
                 "styles": ["apps/nifi-registry/src/styles.scss"],
@@ -63,6 +68,11 @@
                             "glob": "**/*.svg",
                             "input": "libs/shared/src/assets/",
                             "output": "./assets"
+                        },
+                        {
+                            "glob": "**/*.png",
+                            "input": "libs/shared/src/assets/",
+                            "output": "./assets"
                         }
                     ],
                     "fileReplacements": [
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app-routing.module.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app-routing.module.ts
index 1895108..06d8061 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app-routing.module.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app-routing.module.ts
@@ -28,13 +28,25 @@
         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'
+        path: 'buckets',
+        loadChildren: () => import('./pages/buckets/feature/buckets.module').then((m) => m.BucketsModule)
+    },
+    {
+        path: 'administration',
+        children: [
+            {
+                path: 'workflow',
+                children: [
+                    {
+                        path: '',
+                        redirectTo: '/buckets',
+                        pathMatch: 'full'
+                    }
+                ]
+            }
+        ]
     }
-    // TODO: buckets
     // TODO: Users/groups
     // TODO: Page not found
 ];
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
new file mode 100644
index 0000000..c1152fc
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { RouterModule, Routes } from '@angular/router';
+import { BucketsComponent } from './buckets.component';
+
+const routes: Routes = [
+    {
+        path: '',
+        component: BucketsComponent,
+        children: [
+            {
+                path: ':id',
+                component: BucketsComponent
+            }
+        ]
+    }
+];
+
+@NgModule({
+    imports: [RouterModule.forChild(routes)],
+    exports: [RouterModule]
+})
+export class BucketsRoutingModule {}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.html
new file mode 100644
index 0000000..2061b76
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.html
@@ -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.
+-->
+
+<div class="pb-5 px-5 flex flex-col h-full">
+    <div class="h-full flex flex-col">
+        <h3 class="primary-color">Buckets</h3>
+        <context-error-banner [context]="ErrorContextKey.GLOBAL"></context-error-banner>
+        <div class="flex justify-between items-baseline">
+            <bucket-table-filter
+                [filterTerm]="filterTerm"
+                [filterColumn]="filterColumn"
+                [filterableColumns]="filterableColumns"
+                [filteredCount]="dataSource.filteredData.length"
+                [totalCount]="dataSource.data.length"
+                (filterChanged)="applyFilter($event)"></bucket-table-filter>
+            <div class="flex justify-end">
+                <button mat-icon-button class="primary-icon-button" (click)="openCreateBucketDialog()">
+                    <i class="fa fa-plus"></i>
+                </button>
+            </div>
+        </div>
+        @if (buckets$ | async; as buckets) {
+            <bucket-table
+                [dataSource]="dataSource"
+                [selectedId]="selectedBucketId$ | async"
+                (selectBucket)="selectBucket($event)"
+                class="h-full flex"></bucket-table>
+        }
+
+        @if (bucketsState$ | async; as bucketsState) {
+            <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)="refreshBucketsListing()">
+                        <i class="fa fa-refresh" [class.fa-spin]="bucketsState.status === 'loading'"></i>
+                    </button>
+                </div>
+            </div>
+        }
+    </div>
+</div>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.scss
similarity index 92%
rename from nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
rename to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.scss
index 2677b47..bdd8410 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.scss
@@ -17,6 +17,6 @@
 
 @use '@angular/material' as mat;
 
-.delete-droplet-dialog {
-    @include mat.button-density(-1);
+:host {
+    height: 100%;
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.spec.ts
new file mode 100644
index 0000000..e0cf59d
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.spec.ts
@@ -0,0 +1,80 @@
+/*
+ * 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 { BucketsComponent } from './buckets.component';
+import { provideMockStore } from '@ngrx/store/testing';
+import { NiFiCommon } from '@nifi/shared';
+import { BucketTableComponent } from './ui/bucket-table/bucket-table.component';
+import { BucketTableFilterComponent } from './ui/bucket-table-filter/bucket-table-filter.component';
+import { ContextErrorBanner } from '../../../ui/common/context-error-banner/context-error-banner.component';
+import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
+import { Router } from '@angular/router';
+
+describe('BucketsComponent', () => {
+    let component: BucketsComponent;
+    let fixture: ComponentFixture<BucketsComponent>;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            declarations: [BucketsComponent],
+            imports: [
+                BucketTableComponent,
+                BucketTableFilterComponent,
+                ContextErrorBanner,
+                MatButtonModule,
+                MatIconModule
+            ],
+            providers: [
+                provideMockStore({
+                    initialState: {
+                        resources: {
+                            buckets: {
+                                buckets: [],
+                                status: 'pending'
+                            }
+                        },
+                        error: {
+                            bannerErrors: {}
+                        }
+                    }
+                }),
+                NiFiCommon,
+                { provide: Router, useValue: { navigate: jest.fn() } }
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(BucketsComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should initialize with correct default values', () => {
+        expect(component.displayedColumns).toEqual(['name', 'description', 'identifier', 'actions']);
+        expect(component.sort).toEqual({
+            active: 'name',
+            direction: 'asc'
+        });
+        expect(component.filterTerm).toBe('');
+        expect(component.filterColumn).toBe('name');
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.ts
new file mode 100644
index 0000000..626f44a
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.ts
@@ -0,0 +1,161 @@
+/*
+ * 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, OnDestroy, OnInit, inject } from '@angular/core';
+import { selectBucketIdFromRoute, selectBuckets, selectBucketsState } from '../../../state/buckets/buckets.selectors';
+import { loadBuckets, openCreateBucketDialog } from '../../../state/buckets/buckets.actions';
+import { Bucket } from '../../../state/buckets';
+import { Store } from '@ngrx/store';
+import { MatTableDataSource } from '@angular/material/table';
+import { Observable, Subject } from 'rxjs';
+import { Sort } from '@angular/material/sort';
+import { NiFiCommon } from '@nifi/shared';
+import {
+    BucketTableFilterColumn,
+    BucketTableFilterContext
+} from './ui/bucket-table-filter/bucket-table-filter.component';
+import { ErrorContextKey } from '../../../state/error';
+import { Router } from '@angular/router';
+
+@Component({
+    selector: 'buckets',
+    templateUrl: './buckets.component.html',
+    styleUrl: './buckets.component.scss',
+    standalone: false
+})
+export class BucketsComponent implements OnInit, OnDestroy {
+    private store = inject(Store);
+    private nifiCommon = inject(NiFiCommon);
+    private router = inject(Router);
+
+    buckets$: Observable<Bucket[]> = this.store.select(selectBuckets);
+    selectedBucketId$ = this.store.select(selectBucketIdFromRoute);
+    dataSource: MatTableDataSource<Bucket> = new MatTableDataSource<Bucket>();
+    displayedColumns: string[] = ['name', 'description', 'identifier', 'actions'];
+
+    filterableColumns: BucketTableFilterColumn[] = [
+        { key: 'name', label: 'Name' },
+        { key: 'description', label: 'Description' },
+        { key: 'identifier', label: 'Bucket ID' }
+    ];
+    sort: Sort = {
+        active: 'name',
+        direction: 'asc'
+    };
+    filterTerm = '';
+    filterColumn = 'name';
+    bucketsState$ = this.store.select(selectBucketsState);
+
+    private destroy$ = new Subject<void>();
+
+    ngOnInit(): void {
+        this.store.dispatch(loadBuckets());
+        this.buckets$.subscribe((buckets) => {
+            this.dataSource.data = [...buckets];
+            this.sortData(this.sort);
+        });
+
+        this.dataSource.filterPredicate = (data: Bucket, filter: string) => {
+            if (!filter) {
+                return true;
+            }
+
+            const { filterTerm, filterColumn } = JSON.parse(filter);
+
+            if (!filterTerm) {
+                return true;
+            }
+
+            const value = filterColumn ? (data as any)[filterColumn] : undefined;
+            if (typeof value === 'number') {
+                return value.toString().includes(filterTerm);
+            }
+            if (value) {
+                return this.nifiCommon.stringContains(value, filterTerm, true);
+            }
+            // fall back to checking all string fields when column isn't set
+            return Object.keys(data).some((key) => {
+                const fieldValue = (data as any)[key];
+                if (typeof fieldValue === 'string') {
+                    return this.nifiCommon.stringContains(fieldValue, filterTerm, true);
+                }
+                if (typeof fieldValue === 'number') {
+                    return fieldValue.toString().includes(filterTerm);
+                }
+                return false;
+            });
+        };
+
+        this.dataSource.filter = JSON.stringify({ filterTerm: '', filterColumn: this.filterColumn });
+    }
+
+    sortData(sort: Sort) {
+        this.sort = sort;
+        this.dataSource.data = this.sortBuckets(this.dataSource.data, sort);
+    }
+
+    sortBuckets(data: Bucket[], sort: Sort): Bucket[] {
+        if (!data) {
+            return [];
+        }
+        return data.slice().sort((a, b) => {
+            const isAsc = sort.direction === 'asc';
+            let retVal = 0;
+            switch (sort.active) {
+                case 'name':
+                    retVal = this.nifiCommon.compareString(a.name, b.name);
+                    break;
+                case 'description':
+                    retVal = this.nifiCommon.compareString(a.description, b.description);
+                    break;
+                case 'identifier':
+                    retVal = this.nifiCommon.compareString(a.identifier, b.identifier);
+                    break;
+            }
+            return retVal * (isAsc ? 1 : -1);
+        });
+    }
+
+    openCreateBucketDialog() {
+        this.store.dispatch(openCreateBucketDialog());
+    }
+
+    applyFilter(filter: BucketTableFilterContext) {
+        if (!filter || !this.dataSource) {
+            return;
+        }
+
+        this.filterTerm = filter.filterTerm;
+        this.filterColumn = filter.filterColumn;
+        this.dataSource.filter = JSON.stringify(filter);
+    }
+
+    refreshBucketsListing() {
+        this.store.dispatch(loadBuckets());
+    }
+
+    selectBucket(bucket: Bucket): void {
+        this.router.navigate(['/buckets', bucket.identifier]);
+    }
+
+    protected readonly ErrorContextKey = ErrorContextKey;
+
+    ngOnDestroy(): void {
+        this.destroy$.next();
+        this.destroy$.complete();
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.module.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.module.ts
new file mode 100644
index 0000000..92777f8
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.module.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 { ReactiveFormsModule, FormsModule } from '@angular/forms';
+import { StoreModule } from '@ngrx/store';
+import { EffectsModule } from '@ngrx/effects';
+import { BucketsRoutingModule } from './buckets-routing.module';
+import { BucketsComponent } from './buckets.component';
+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 { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatSelectModule } from '@angular/material/select';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatDialogModule } from '@angular/material/dialog';
+import { BucketTableFilterComponent } from './ui/bucket-table-filter/bucket-table-filter.component';
+import { BucketTableComponent } from './ui/bucket-table/bucket-table.component';
+import { CreateBucketDialogComponent } from './ui/create-bucket-dialog/create-bucket-dialog.component';
+import { EditBucketDialogComponent } from './ui/edit-bucket-dialog/edit-bucket-dialog.component';
+import { ManageBucketPoliciesDialogComponent } from './ui/manage-bucket-policies-dialog/manage-bucket-policies-dialog.component';
+import { ContextErrorBanner } from '../../../ui/common/context-error-banner/context-error-banner.component';
+
+@NgModule({
+    declarations: [BucketsComponent],
+    exports: [BucketsComponent],
+    imports: [
+        BucketTableFilterComponent,
+        BucketTableComponent,
+        CreateBucketDialogComponent,
+        EditBucketDialogComponent,
+        ManageBucketPoliciesDialogComponent,
+        ContextErrorBanner,
+        CommonModule,
+        ReactiveFormsModule,
+        FormsModule,
+        MatTableModule,
+        MatSortModule,
+        MatMenuModule,
+        MatButtonModule,
+        MatIconModule,
+        MatFormFieldModule,
+        MatInputModule,
+        MatSelectModule,
+        MatCheckboxModule,
+        MatDialogModule,
+        BucketsRoutingModule,
+        StoreModule.forFeature(resourcesFeatureKey, reducers),
+        EffectsModule.forFeature([BucketsEffects])
+    ]
+})
+export class BucketsModule {}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table-filter/bucket-table-filter.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table-filter/bucket-table-filter.component.html
new file mode 100644
index 0000000..beee60e
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table-filter/bucket-table-filter.component.html
@@ -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.
+-->
+
+<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>
+        </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-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table-filter/bucket-table-filter.component.scss
similarity index 88%
copy from nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table-filter/bucket-table-filter.component.scss
index 2677b47..2944f98 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table-filter/bucket-table-filter.component.scss
@@ -14,9 +14,3 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-@use '@angular/material' as mat;
-
-.delete-droplet-dialog {
-    @include mat.button-density(-1);
-}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table-filter/bucket-table-filter.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table-filter/bucket-table-filter.component.spec.ts
new file mode 100644
index 0000000..0bbba19
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table-filter/bucket-table-filter.component.spec.ts
@@ -0,0 +1,79 @@
+/*
+ * 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, fakeAsync, tick } from '@angular/core/testing';
+import { BucketTableFilterComponent, BucketTableFilterColumn } from './bucket-table-filter.component';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatSelectModule } from '@angular/material/select';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { ReactiveFormsModule } from '@angular/forms';
+
+const columns: BucketTableFilterColumn[] = [
+    { key: 'name', label: 'Name' },
+    { key: 'description', label: 'Description' }
+];
+
+describe('BucketTableFilterComponent', () => {
+    let component: BucketTableFilterComponent;
+    let fixture: ComponentFixture<BucketTableFilterComponent>;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [
+                BucketTableFilterComponent,
+                MatFormFieldModule,
+                MatInputModule,
+                MatSelectModule,
+                NoopAnimationsModule,
+                ReactiveFormsModule
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(BucketTableFilterComponent);
+        component = fixture.componentInstance;
+        component.filterableColumns = columns;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should emit filter changes when term changes', fakeAsync(() => {
+        jest.spyOn(component.filterChanged, 'emit');
+        component.filterForm.get('filterTerm')?.setValue('test');
+        fixture.detectChanges();
+        tick(500); // Wait for debounceTime
+        expect(component.filterChanged.emit).toHaveBeenCalledWith({
+            filterTerm: 'test',
+            filterColumn: 'name',
+            changedField: 'filterTerm'
+        });
+    }));
+
+    it('should emit filter changes when column changes', fakeAsync(() => {
+        jest.spyOn(component.filterChanged, 'emit');
+        component.filterForm.get('filterColumn')?.setValue('description');
+        fixture.detectChanges();
+        expect(component.filterChanged.emit).toHaveBeenCalledWith({
+            filterTerm: '',
+            filterColumn: 'description',
+            changedField: 'filterColumn'
+        });
+    }));
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table-filter/bucket-table-filter.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table-filter/bucket-table-filter.component.ts
new file mode 100644
index 0000000..2d916aa
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table-filter/bucket-table-filter.component.ts
@@ -0,0 +1,104 @@
+/*
+ * 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, DestroyRef, EventEmitter, Input, Output, inject } from '@angular/core';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatSelectModule } from '@angular/material/select';
+import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { debounceTime } from 'rxjs';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+
+export interface BucketTableFilterColumn {
+    key: string;
+    label: string;
+}
+
+export interface BucketTableFilterContext {
+    filterTerm: string;
+    filterColumn: string;
+    changedField: string;
+}
+
+@Component({
+    selector: 'bucket-table-filter',
+    templateUrl: './bucket-table-filter.component.html',
+    styleUrl: './bucket-table-filter.component.scss',
+    standalone: true,
+    imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatSelectModule]
+})
+export class BucketTableFilterComponent {
+    private formBuilder = inject(FormBuilder);
+    private destroyRef = inject(DestroyRef);
+
+    filterForm: FormGroup = this.formBuilder.group({
+        filterTerm: '',
+        filterColumn: ''
+    });
+
+    private _filterableColumns: BucketTableFilterColumn[] = [];
+
+    @Input() set filterableColumns(columns: BucketTableFilterColumn[]) {
+        this._filterableColumns = columns ?? [];
+        if (this._filterableColumns.length > 0) {
+            const current = this.filterForm.get('filterColumn')?.value;
+            const valid = this._filterableColumns.some((column) => column.key === current);
+            const valueToApply = valid ? current : this._filterableColumns[0].key;
+            this.filterForm.get('filterColumn')?.setValue(valueToApply, { emitEvent: false });
+        }
+    }
+    get filterableColumns(): BucketTableFilterColumn[] {
+        return this._filterableColumns;
+    }
+
+    @Input() set filterTerm(term: string) {
+        this.filterForm.get('filterTerm')?.setValue(term ?? '', { emitEvent: false });
+    }
+
+    @Input() set filterColumn(column: string) {
+        if (column) {
+            this.filterForm.get('filterColumn')?.setValue(column, { emitEvent: false });
+        } else if (this._filterableColumns.length > 0) {
+            this.filterForm.get('filterColumn')?.setValue(this._filterableColumns[0].key, { emitEvent: false });
+        }
+    }
+
+    @Input() filteredCount = 0;
+    @Input() totalCount = 0;
+
+    @Output() filterChanged: EventEmitter<BucketTableFilterContext> = new EventEmitter<BucketTableFilterContext>();
+
+    constructor() {
+        this.filterForm
+            .get('filterTerm')
+            ?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef))
+            .subscribe((term) => this.applyFilter(term, this.filterForm.get('filterColumn')?.value, 'filterTerm'));
+
+        this.filterForm
+            .get('filterColumn')
+            ?.valueChanges.pipe(takeUntilDestroyed())
+            .subscribe((column) => this.applyFilter(this.filterForm.get('filterTerm')?.value, column, 'filterColumn'));
+    }
+
+    private applyFilter(filterTerm: string, filterColumn: string, changedField: string) {
+        this.filterChanged.emit({
+            filterTerm: filterTerm ?? '',
+            filterColumn: filterColumn ?? '',
+            changedField
+        });
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table/bucket-table.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table/bucket-table.component.html
new file mode 100644
index 0000000..8a3c8ff
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table/bucket-table.component.html
@@ -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.
+-->
+
+<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>
+                    <!-- Description Column -->
+                    <ng-container matColumnDef="description">
+                        <th mat-header-cell *matHeaderCellDef mat-sort-header>Description</th>
+                        <td mat-cell *matCellDef="let item">
+                            <div class="overflow-ellipsis overflow-hidden whitespace-nowrap" [title]="item.description">
+                                {{ item.description }}
+                            </div>
+                        </td>
+                    </ng-container>
+                    <!-- Identifier Column -->
+                    <ng-container matColumnDef="identifier">
+                        <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.identifier">
+                                {{ item.identifier }}
+                            </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)="openEditBucketDialog(item)">Edit</button>
+                                    <button mat-menu-item (click)="openManageBucketPoliciesDialog(item)">
+                                        Manage Policies
+                                    </button>
+                                    <button mat-menu-item (click)="openDeleteBucketDialog(item)">Delete</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>
+        </div>
+    </div>
+</div>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table/bucket-table.component.scss
similarity index 88%
copy from nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table/bucket-table.component.scss
index 2677b47..2944f98 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table/bucket-table.component.scss
@@ -14,9 +14,3 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-@use '@angular/material' as mat;
-
-.delete-droplet-dialog {
-    @include mat.button-density(-1);
-}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table/bucket-table.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table/bucket-table.component.spec.ts
new file mode 100644
index 0000000..4524732
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table/bucket-table.component.spec.ts
@@ -0,0 +1,126 @@
+/*
+ * 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 { BucketTableComponent } from './bucket-table.component';
+import { MatTableDataSource } from '@angular/material/table';
+import { Bucket } from '../../../../../state/buckets';
+import { provideMockStore } from '@ngrx/store/testing';
+import { NiFiCommon } from '@nifi/shared';
+import {
+    openDeleteBucketDialog,
+    openEditBucketDialog,
+    openManageBucketPoliciesDialog
+} from '../../../../../state/buckets/buckets.actions';
+import { Store } from '@ngrx/store';
+import { MatMenuModule } from '@angular/material/menu';
+import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { MatSortModule } from '@angular/material/sort';
+import { MatTableModule } from '@angular/material/table';
+
+const createBucket = (overrides: Partial<Bucket> = {}): Bucket => ({
+    allowBundleRedeploy: false,
+    allowPublicRead: false,
+    createdTimestamp: Date.now(),
+    description: 'Test bucket',
+    identifier: 'bucket-1',
+    link: {
+        href: '',
+        params: { rel: '' }
+    },
+    name: 'A Bucket',
+    permissions: {
+        canRead: true,
+        canWrite: true
+    },
+    revision: {
+        version: 0
+    },
+    ...overrides
+});
+
+describe('BucketTableComponent', () => {
+    let component: BucketTableComponent;
+    let fixture: ComponentFixture<BucketTableComponent>;
+    let store: Store;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [
+                BucketTableComponent,
+                MatTableModule,
+                MatSortModule,
+                MatMenuModule,
+                MatButtonModule,
+                MatIconModule,
+                NoopAnimationsModule
+            ],
+            providers: [provideMockStore(), NiFiCommon]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(BucketTableComponent);
+        component = fixture.componentInstance;
+        store = TestBed.inject(Store);
+        jest.spyOn(store, 'dispatch');
+
+        const buckets = [
+            createBucket({ identifier: 'bucket-1', name: 'Alpha' }),
+            createBucket({ identifier: 'bucket-2', name: 'Bravo' })
+        ];
+        component.dataSource = new MatTableDataSource<Bucket>(buckets);
+
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should emit when a bucket row is selected', () => {
+        jest.spyOn(component.selectBucket, 'next');
+        component.select(component.dataSource.data[0]);
+        expect(component.selectBucket.next).toHaveBeenCalledWith(component.dataSource.data[0]);
+    });
+
+    it('should dispatch edit dialog action', () => {
+        component.openEditBucketDialog(component.dataSource.data[0]);
+        expect(store.dispatch).toHaveBeenCalledWith(
+            openEditBucketDialog({ request: { bucket: component.dataSource.data[0] } })
+        );
+    });
+
+    it('should dispatch delete dialog action', () => {
+        component.openDeleteBucketDialog(component.dataSource.data[0]);
+        expect(store.dispatch).toHaveBeenCalledWith(
+            openDeleteBucketDialog({ request: { bucket: component.dataSource.data[0] } })
+        );
+    });
+
+    it('should dispatch manage policies dialog action', () => {
+        component.openManageBucketPoliciesDialog(component.dataSource.data[0]);
+        expect(store.dispatch).toHaveBeenCalledWith(
+            openManageBucketPoliciesDialog({ request: { bucket: component.dataSource.data[0] } })
+        );
+    });
+
+    it('should sort data on init', () => {
+        component.sortData({ active: 'name', direction: 'desc' });
+        expect(component.dataSource.data[0].name).toBe('Bravo');
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table/bucket-table.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table/bucket-table.component.ts
new file mode 100644
index 0000000..1801093
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/bucket-table/bucket-table.component.ts
@@ -0,0 +1,113 @@
+/*
+ * 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, inject } from '@angular/core';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { Bucket } from 'apps/nifi-registry/src/app/state/buckets';
+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 { Store } from '@ngrx/store';
+import {
+    openDeleteBucketDialog,
+    openEditBucketDialog,
+    openManageBucketPoliciesDialog
+} from 'apps/nifi-registry/src/app/state/buckets/buckets.actions';
+import { MatIconModule } from '@angular/material/icon';
+
+@Component({
+    selector: 'bucket-table',
+    standalone: true,
+    imports: [MatTableModule, MatSortModule, MatMenuModule, MatButtonModule, MatIconModule],
+    templateUrl: './bucket-table.component.html',
+    styleUrl: './bucket-table.component.scss'
+})
+export class BucketTableComponent implements OnInit {
+    private nifiCommon = inject(NiFiCommon);
+    private store = inject(Store);
+
+    @Input() dataSource: MatTableDataSource<Bucket> = new MatTableDataSource<Bucket>();
+    @Input() selectedId: string | null = null;
+
+    @Output() selectBucket: EventEmitter<Bucket> = new EventEmitter<Bucket>();
+
+    displayedColumns: string[] = ['name', 'description', 'identifier', 'actions'];
+    sort: Sort = {
+        active: 'name',
+        direction: 'asc'
+    };
+
+    ngOnInit(): void {
+        this.sortData(this.sort);
+    }
+
+    sortData(sort: Sort) {
+        this.sort = sort;
+        this.dataSource.data = this.sortBuckets(this.dataSource.data, sort);
+    }
+
+    sortBuckets(data: Bucket[], sort: Sort): Bucket[] {
+        if (!data) {
+            return [];
+        }
+        return data.slice().sort((a, b) => {
+            const isAsc = sort.direction === 'asc';
+            let retVal = 0;
+            switch (sort.active) {
+                case 'name':
+                    retVal = this.nifiCommon.compareString(a.name, b.name);
+                    break;
+                case 'description':
+                    retVal = this.nifiCommon.compareString(a.description, b.description);
+                    break;
+                case 'identifier':
+                    retVal = this.nifiCommon.compareString(a.identifier, b.identifier);
+                    break;
+                    break;
+            }
+            return retVal * (isAsc ? 1 : -1);
+        });
+    }
+
+    select(bucket: Bucket) {
+        this.selectBucket.next(bucket);
+    }
+
+    isSelected(bucket: Bucket): boolean {
+        if (this.selectedId) {
+            return this.selectedId === bucket.identifier;
+        }
+        return false;
+    }
+
+    openEditBucketDialog(bucket: Bucket) {
+        this.store.dispatch(
+            openEditBucketDialog({
+                request: { bucket }
+            })
+        );
+    }
+
+    openDeleteBucketDialog(bucket: Bucket) {
+        this.store.dispatch(openDeleteBucketDialog({ request: { bucket } }));
+    }
+
+    openManageBucketPoliciesDialog(bucket: Bucket) {
+        this.store.dispatch(openManageBucketPoliciesDialog({ request: { bucket } }));
+    }
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/create-bucket-dialog/create-bucket-dialog.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/create-bucket-dialog/create-bucket-dialog.component.html
new file mode 100644
index 0000000..4e9ea20
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/create-bucket-dialog/create-bucket-dialog.component.html
@@ -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.
+-->
+
+<h2 mat-dialog-title>New Bucket</h2>
+<div class="create-bucket-dialog">
+    <form class="create-bucket-form" [formGroup]="bucketForm" (ngSubmit)="onSubmit()">
+        <mat-dialog-content>
+            <context-error-banner [context]="ErrorContextKey.CREATE_BUCKET"></context-error-banner>
+            <div class="flex flex-col gap-y-4">
+                <mat-form-field appearance="outline">
+                    <mat-label>Bucket Name</mat-label>
+                    <input matInput formControlName="name" placeholder="Enter a unique bucket name" />
+                    @if (bucketForm.get('name')?.hasError('required')) {
+                        <mat-error>Name is required</mat-error>
+                    }
+                    @if (bucketForm.get('name')?.hasError('maxlength')) {
+                        <mat-error>Name cannot exceed 255 characters</mat-error>
+                    }
+                </mat-form-field>
+                <mat-form-field appearance="outline">
+                    <mat-label>Description</mat-label>
+                    <textarea
+                        matInput
+                        formControlName="description"
+                        placeholder="Describe the purpose of this bucket (optional)"
+                        rows="3"></textarea>
+                    @if (bucketForm.get('description')?.hasError('maxlength')) {
+                        <mat-error>Description cannot exceed 1000 characters</mat-error>
+                    }
+                </mat-form-field>
+                <mat-checkbox class="flex items-center" formControlName="allowPublicRead">
+                    <mat-label
+                        >Allow Public Read Access
+                        <i
+                            class="fa fa-info-circle"
+                            [class.primary-color]="supportsPublicRead"
+                            matTooltip="Allows unauthenticated users to read the items in this bucket. Overrides policies granting read access."
+                            aria-label="Public read access information">
+                        </i>
+                    </mat-label>
+                </mat-checkbox>
+                <mat-checkbox class="flex items-center" formControlName="keepDialogOpen">
+                    Keep this dialog open after creating bucket
+                </mat-checkbox>
+            </div>
+        </mat-dialog-content>
+
+        <mat-dialog-actions class="mt-2" align="end">
+            <button mat-button type="button" mat-dialog-close>Cancel</button>
+            <button mat-flat-button color="primary" type="submit" [disabled]="bucketForm.invalid">Create</button>
+        </mat-dialog-actions>
+    </form>
+</div>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/create-bucket-dialog/create-bucket-dialog.component.scss
similarity index 88%
copy from nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/create-bucket-dialog/create-bucket-dialog.component.scss
index 2677b47..2944f98 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/create-bucket-dialog/create-bucket-dialog.component.scss
@@ -14,9 +14,3 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-@use '@angular/material' as mat;
-
-.delete-droplet-dialog {
-    @include mat.button-density(-1);
-}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/create-bucket-dialog/create-bucket-dialog.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/create-bucket-dialog/create-bucket-dialog.component.spec.ts
new file mode 100644
index 0000000..f031be7
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/create-bucket-dialog/create-bucket-dialog.component.spec.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 { CreateBucketDialogComponent } from './create-bucket-dialog.component';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { FormBuilder } from '@angular/forms';
+import { provideMockStore } from '@ngrx/store/testing';
+import { MatDialogRef } from '@angular/material/dialog';
+import { createBucket } from '../../../../../state/buckets/buckets.actions';
+import { Store } from '@ngrx/store';
+
+describe('CreateBucketDialogComponent', () => {
+    let component: CreateBucketDialogComponent;
+    let fixture: ComponentFixture<CreateBucketDialogComponent>;
+    let store: Store;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [CreateBucketDialogComponent, NoopAnimationsModule],
+            providers: [
+                FormBuilder,
+                provideMockStore({
+                    initialState: {
+                        error: {
+                            bannerErrors: {}
+                        }
+                    }
+                }),
+                { provide: MatDialogRef, useValue: { close: jest.fn() } }
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(CreateBucketDialogComponent);
+        component = fixture.componentInstance;
+        store = TestBed.inject(Store);
+        jest.spyOn(store, 'dispatch');
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should dispatch create bucket action when form valid', () => {
+        component.bucketForm.patchValue({
+            name: 'Test Bucket',
+            description: 'description',
+            allowPublicRead: true,
+            keepDialogOpen: true
+        });
+
+        component.onSubmit();
+
+        expect(store.dispatch).toHaveBeenCalledWith(
+            createBucket({
+                request: {
+                    name: 'Test Bucket',
+                    description: 'description',
+                    allowPublicRead: true
+                },
+                keepDialogOpen: true
+            })
+        );
+    });
+
+    it('should not dispatch when form invalid', () => {
+        component.bucketForm.patchValue({ name: '', description: '' });
+        component.onSubmit();
+        expect(store.dispatch).not.toHaveBeenCalled();
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/create-bucket-dialog/create-bucket-dialog.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/create-bucket-dialog/create-bucket-dialog.component.ts
new file mode 100644
index 0000000..836c06e
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/create-bucket-dialog/create-bucket-dialog.component.ts
@@ -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.
+ */
+
+import { Component, inject } from '@angular/core';
+import { MatDialogModule } from '@angular/material/dialog';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatIconModule } from '@angular/material/icon';
+import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
+import { createBucket, CreateBucketRequest } from '../../../../../state/buckets/buckets.actions';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { Store } from '@ngrx/store';
+import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
+import { ErrorContextKey } from '../../../../../state/error';
+
+@Component({
+    selector: 'create-bucket-dialog',
+    templateUrl: './create-bucket-dialog.component.html',
+    styleUrl: './create-bucket-dialog.component.scss',
+    standalone: true,
+    imports: [
+        MatDialogModule,
+        MatFormFieldModule,
+        MatInputModule,
+        MatButtonModule,
+        MatCheckboxModule,
+        MatIconModule,
+        ReactiveFormsModule,
+        MatTooltipModule,
+        ContextErrorBanner
+    ]
+})
+export class CreateBucketDialogComponent {
+    private formBuilder = inject(FormBuilder);
+    bucketForm: FormGroup;
+    readonly supportsPublicRead = window?.location?.protocol === 'https:';
+    private store = inject(Store);
+
+    constructor() {
+        this.bucketForm = this.formBuilder.group({
+            name: ['', [Validators.required, Validators.maxLength(255)]],
+            description: ['', [Validators.maxLength(1000)]],
+            allowPublicRead: [{ value: false, disabled: !this.supportsPublicRead }],
+            keepDialogOpen: [false]
+        });
+    }
+
+    onSubmit(): void {
+        if (this.bucketForm.valid) {
+            const rawValue = this.bucketForm.getRawValue();
+            const { name, description, allowPublicRead, keepDialogOpen } = rawValue;
+
+            const request: CreateBucketRequest = {
+                name,
+                description,
+                allowPublicRead
+            };
+
+            this.store.dispatch(
+                createBucket({
+                    request: request,
+                    keepDialogOpen
+                })
+            );
+        }
+    }
+
+    protected readonly ErrorContextKey = ErrorContextKey;
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/edit-bucket-dialog/edit-bucket-dialog.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/edit-bucket-dialog/edit-bucket-dialog.component.html
new file mode 100644
index 0000000..a447079
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/edit-bucket-dialog/edit-bucket-dialog.component.html
@@ -0,0 +1,78 @@
+<!--
+~  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>Edit Bucket</h2>
+<form [formGroup]="bucketForm" (ngSubmit)="onSaveBucket()">
+    <mat-dialog-content>
+        <context-error-banner [context]="ErrorContextKey.UPDATE_BUCKET"></context-error-banner>
+        <div class="flex flex-col gap-y-4">
+            <div class="flex flex-col mb-5">
+                <div>Id</div>
+                <div [copy]="data.bucket.identifier" class="tertiary-color font-medium">
+                    {{ data.bucket.identifier }}
+                </div>
+            </div>
+            <mat-form-field appearance="outline">
+                <mat-label>Name</mat-label>
+                <input matInput formControlName="name" placeholder="Enter bucket name" />
+                @if (bucketForm.get('name')?.hasError('required')) {
+                    <mat-error>Name is required</mat-error>
+                }
+                @if (bucketForm.get('name')?.hasError('maxlength')) {
+                    <mat-error>Name cannot exceed 255 characters</mat-error>
+                }
+            </mat-form-field>
+
+            <mat-form-field appearance="outline">
+                <mat-label>Description</mat-label>
+                <textarea
+                    matInput
+                    formControlName="description"
+                    placeholder="Enter bucket description (optional)"
+                    rows="3"></textarea>
+                @if (bucketForm.get('description')?.hasError('maxlength')) {
+                    <mat-error>Description cannot exceed 1000 characters</mat-error>
+                }
+            </mat-form-field>
+
+            <mat-checkbox formControlName="allowPublicRead">
+                <mat-label
+                    >Make publicly visible
+                    <i
+                        class="fa fa-info-circle primary-color"
+                        matTooltip="Allows read access to items in this bucket by unauthenticated users. Overrides any specific policies granting read access.">
+                    </i>
+                </mat-label>
+            </mat-checkbox>
+
+            <mat-checkbox formControlName="allowBundleRedeploy">
+                <mat-label
+                    >Allow bundle overwrite
+                    <i
+                        class="fa fa-info-circle primary-color"
+                        matTooltip="Allows released bundles in this bucket to be overwritten.">
+                    </i>
+                </mat-label>
+            </mat-checkbox>
+        </div>
+    </mat-dialog-content>
+
+    <mat-dialog-actions align="end">
+        <button mat-button type="button" mat-dialog-close>Cancel</button>
+        <button mat-flat-button color="primary" type="submit" [disabled]="bucketForm.invalid">Apply</button>
+    </mat-dialog-actions>
+</form>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/edit-bucket-dialog/edit-bucket-dialog.component.scss
similarity index 88%
copy from nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/edit-bucket-dialog/edit-bucket-dialog.component.scss
index 2677b47..2944f98 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/edit-bucket-dialog/edit-bucket-dialog.component.scss
@@ -14,9 +14,3 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-@use '@angular/material' as mat;
-
-.delete-droplet-dialog {
-    @include mat.button-density(-1);
-}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/edit-bucket-dialog/edit-bucket-dialog.component.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/edit-bucket-dialog/edit-bucket-dialog.component.spec.ts
new file mode 100644
index 0000000..f26d621
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/edit-bucket-dialog/edit-bucket-dialog.component.spec.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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { EditBucketDialogComponent, EditBucketDialogData } from './edit-bucket-dialog.component';
+import { MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { Bucket } from '../../../../../state/buckets';
+import { provideMockStore } from '@ngrx/store/testing';
+import { updateBucket } from '../../../../../state/buckets/buckets.actions';
+import { Store } from '@ngrx/store';
+import { FormBuilder } from '@angular/forms';
+
+const bucket: Bucket = {
+    allowBundleRedeploy: false,
+    allowPublicRead: false,
+    createdTimestamp: Date.now(),
+    description: 'desc',
+    identifier: 'bucket-1',
+    link: {
+        href: '',
+        params: { rel: '' }
+    },
+    name: 'Bucket 1',
+    permissions: {
+        canRead: true,
+        canWrite: true
+    },
+    revision: {
+        version: 1
+    }
+};
+
+describe('EditBucketDialogComponent', () => {
+    let component: EditBucketDialogComponent;
+    let fixture: ComponentFixture<EditBucketDialogComponent>;
+    let store: Store;
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [EditBucketDialogComponent, NoopAnimationsModule],
+            providers: [
+                FormBuilder,
+                provideMockStore({
+                    initialState: {
+                        error: {
+                            bannerErrors: {}
+                        }
+                    }
+                }),
+                {
+                    provide: MAT_DIALOG_DATA,
+                    useValue: { bucket } as EditBucketDialogData
+                }
+            ]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(EditBucketDialogComponent);
+        component = fixture.componentInstance;
+        store = TestBed.inject(Store);
+        jest.spyOn(store, 'dispatch');
+
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('should dispatch update action when form valid', () => {
+        component.bucketForm.patchValue({
+            name: 'Updated',
+            allowPublicRead: true
+        });
+        component.onSaveBucket();
+
+        expect(store.dispatch).toHaveBeenCalledWith(
+            updateBucket({
+                request: {
+                    bucket: {
+                        ...bucket,
+                        name: 'Updated',
+                        description: bucket.description,
+                        allowPublicRead: true,
+                        allowBundleRedeploy: bucket.allowBundleRedeploy
+                    }
+                }
+            })
+        );
+    });
+
+    it('should not dispatch when form invalid', () => {
+        component.bucketForm.patchValue({ name: '' });
+        component.onSaveBucket();
+        expect(store.dispatch).not.toHaveBeenCalled();
+    });
+});
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/edit-bucket-dialog/edit-bucket-dialog.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/edit-bucket-dialog/edit-bucket-dialog.component.ts
new file mode 100644
index 0000000..7526338
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/edit-bucket-dialog/edit-bucket-dialog.component.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 { Component, inject } from '@angular/core';
+import { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
+import { Bucket } from 'apps/nifi-registry/src/app/state/buckets';
+import { CopyDirective } from '@nifi/shared';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { Store } from '@ngrx/store';
+import { updateBucket } from '../../../../../state/buckets/buckets.actions';
+import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
+import { ErrorContextKey } from '../../../../../state/error';
+
+export interface EditBucketDialogData {
+    bucket: Bucket;
+}
+
+@Component({
+    selector: 'edit-bucket-dialog',
+    templateUrl: './edit-bucket-dialog.component.html',
+    styleUrl: './edit-bucket-dialog.component.scss',
+    standalone: true,
+    imports: [
+        MatDialogModule,
+        MatFormFieldModule,
+        MatInputModule,
+        MatButtonModule,
+        MatCheckboxModule,
+        ReactiveFormsModule,
+        CopyDirective,
+        MatTooltipModule,
+        ContextErrorBanner
+    ]
+})
+export class EditBucketDialogComponent {
+    bucketForm: FormGroup;
+    protected data = inject<EditBucketDialogData>(MAT_DIALOG_DATA);
+    protected formBuilder = inject(FormBuilder);
+    private store = inject(Store);
+
+    constructor() {
+        this.bucketForm = this.formBuilder.group({
+            name: [this.data.bucket.name, [Validators.required, Validators.maxLength(255)]],
+            description: [this.data.bucket.description || '', [Validators.maxLength(1000)]],
+            allowPublicRead: [this.data.bucket.allowPublicRead],
+            allowBundleRedeploy: [this.data.bucket.allowBundleRedeploy]
+        });
+    }
+
+    onSaveBucket(): void {
+        if (this.data.bucket && this.bucketForm.valid) {
+            const bucket: Bucket = {
+                ...this.data.bucket,
+                name: this.bucketForm.value.name,
+                description: this.bucketForm.value.description,
+                allowPublicRead: this.bucketForm.value.allowPublicRead,
+                allowBundleRedeploy: this.bucketForm.value.allowBundleRedeploy
+            };
+
+            this.store.dispatch(
+                updateBucket({
+                    request: {
+                        bucket
+                    }
+                })
+            );
+        }
+    }
+
+    protected readonly ErrorContextKey = ErrorContextKey;
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/manage-bucket-policies-dialog/manage-bucket-policies-dialog.component.html b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/manage-bucket-policies-dialog/manage-bucket-policies-dialog.component.html
new file mode 100644
index 0000000..7cf225b
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/manage-bucket-policies-dialog/manage-bucket-policies-dialog.component.html
@@ -0,0 +1,30 @@
+<!--
+~  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>Manage Bucket Policies</h2>
+<mat-dialog-content>
+    <div class="flex flex-col gap-y-4">
+        <p>
+            TODO: Manage policies for bucket: <strong>{{ data.bucket.name }}</strong>
+        </p>
+        <!--        TODO: Manage policies for bucket         -->
+    </div>
+</mat-dialog-content>
+
+<mat-dialog-actions align="end">
+    <button mat-flat-button mat-dialog-close>Close</button>
+</mat-dialog-actions>
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/manage-bucket-policies-dialog/manage-bucket-policies-dialog.component.scss
similarity index 88%
copy from nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
copy to nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/manage-bucket-policies-dialog/manage-bucket-policies-dialog.component.scss
index 2677b47..2944f98 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/manage-bucket-policies-dialog/manage-bucket-policies-dialog.component.scss
@@ -14,9 +14,3 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-@use '@angular/material' as mat;
-
-.delete-droplet-dialog {
-    @include mat.button-density(-1);
-}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/manage-bucket-policies-dialog/manage-bucket-policies-dialog.component.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/manage-bucket-policies-dialog/manage-bucket-policies-dialog.component.ts
new file mode 100644
index 0000000..1593ce0
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/ui/manage-bucket-policies-dialog/manage-bucket-policies-dialog.component.ts
@@ -0,0 +1,36 @@
+/*
+ * 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 { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { MatButtonModule } from '@angular/material/button';
+import { Bucket } from 'apps/nifi-registry/src/app/state/buckets';
+
+export interface ManageBucketPoliciesDialogData {
+    bucket: Bucket;
+}
+
+@Component({
+    selector: 'manage-bucket-policies-dialog',
+    templateUrl: './manage-bucket-policies-dialog.component.html',
+    styleUrl: './manage-bucket-policies-dialog.component.scss',
+    standalone: true,
+    imports: [MatDialogModule, MatButtonModule]
+})
+export class ManageBucketPoliciesDialogComponent {
+    protected data = inject<ManageBucketPoliciesDialogData>(MAT_DIALOG_DATA);
+}
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
index 252eb51..7574886 100644
--- 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
@@ -24,6 +24,11 @@
 import { resourcesFeatureKey } from '../../../state';
 import { dropletsFeatureKey } from '../../../state/droplets';
 import { bucketsFeatureKey } from '../../../state/buckets';
+import { DropletTableComponent } from './ui/droplet-table/droplet-table.component';
+import { DropletTableFilterComponent } from './ui/droplet-table-filter/droplet-table-filter.component';
+import { ContextErrorBanner } from '../../../ui/common/context-error-banner/context-error-banner.component';
+import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
 
 describe('Resources', () => {
     let component: ResourcesComponent;
@@ -32,13 +37,23 @@
     beforeEach(() => {
         TestBed.configureTestingModule({
             declarations: [ResourcesComponent],
-            imports: [RouterModule],
+            imports: [
+                RouterModule,
+                DropletTableComponent,
+                DropletTableFilterComponent,
+                ContextErrorBanner,
+                MatButtonModule,
+                MatIconModule
+            ],
             providers: [
                 provideMockStore({
                     initialState: {
                         [resourcesFeatureKey]: {
                             [dropletsFeatureKey]: initialState,
                             [bucketsFeatureKey]: initialBucketState
+                        },
+                        error: {
+                            bannerErrors: {}
                         }
                     }
                 })
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
index a6688f3..fe40532 100644
--- 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
@@ -21,7 +21,7 @@
     selectDroplets,
     selectDropletState
 } from '../../../state/droplets/droplets.selectors';
-import { loadDroplets, openImportNewDropletDialog, selectDroplet } from '../../../state/droplets/droplets.actions';
+import { loadDroplets, openImportNewDropletDialog } from '../../../state/droplets/droplets.actions';
 import { Droplet } from '../../../state/droplets';
 import { Store } from '@ngrx/store';
 import { MatTableDataSource } from '@angular/material/table';
@@ -37,6 +37,7 @@
 } from './ui/droplet-table-filter/droplet-table-filter.component';
 import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
 import { ErrorContextKey } from '../../../state/error';
+import { Router } from '@angular/router';
 
 @Component({
     selector: 'resources',
@@ -47,6 +48,7 @@
 export class ResourcesComponent implements OnInit {
     private store = inject(Store);
     private nifiCommon = inject(NiFiCommon);
+    private router = inject(Router);
 
     droplets$: Observable<Droplet[]> = this.store.select(selectDroplets).pipe(takeUntilDestroyed());
     buckets$: Observable<Bucket[]> = this.store.select(selectBuckets).pipe(takeUntilDestroyed());
@@ -171,13 +173,7 @@
     }
 
     selectDroplet(droplet: Droplet): void {
-        this.store.dispatch(
-            selectDroplet({
-                request: {
-                    id: droplet.identifier
-                }
-            })
-        );
+        this.router.navigate(['/explorer', droplet.identifier]);
     }
 
     protected readonly ErrorContextKey = ErrorContextKey;
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
deleted file mode 100644
index 8ce39f2..0000000
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.html
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-~  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-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
deleted file mode 100644
index 15df3b0..0000000
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.spec.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 9260916..0000000
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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 { 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: [MatDialogModule, MatButtonModule, ContextErrorBanner],
-    templateUrl: './delete-droplet-dialog.component.html',
-    styleUrl: './delete-droplet-dialog.component.scss'
-})
-export class DeleteDropletDialogComponent extends CloseOnEscapeDialog {
-    data = inject<DeleteDropletDialogData>(MAT_DIALOG_DATA);
-    private store = inject(Store);
-
-    protected readonly ErrorContextKey = ErrorContextKey;
-    droplet: Droplet;
-
-    constructor() {
-        super();
-        const data = this.data;
-
-        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-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
index 95aba2e..d4f9e56 100644
--- 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
@@ -75,7 +75,13 @@
                         keydownEvents: () => new Subject<KeyboardEvent>()
                     }
                 },
-                provideMockStore({})
+                provideMockStore({
+                    initialState: {
+                        error: {
+                            bannerErrors: {}
+                        }
+                    }
+                })
             ]
         }).compileComponents();
 
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
index 7b8f295..dbfe86c 100644
--- 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
@@ -56,7 +56,13 @@
                         keydownEvents: () => new Subject<KeyboardEvent>()
                     }
                 },
-                provideMockStore({})
+                provideMockStore({
+                    initialState: {
+                        error: {
+                            bannerErrors: {}
+                        }
+                    }
+                })
             ]
         }).compileComponents();
 
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
index 48bfbd3..eb3d34c 100644
--- 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
@@ -102,7 +102,13 @@
                         keydownEvents: () => new Subject<KeyboardEvent>()
                     }
                 },
-                provideMockStore({})
+                provideMockStore({
+                    initialState: {
+                        error: {
+                            bannerErrors: {}
+                        }
+                    }
+                })
             ]
         }).compileComponents();
 
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
index c255d01..30a004f 100644
--- 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
@@ -68,7 +68,13 @@
                         keydownEvents: () => new Subject<KeyboardEvent>()
                     }
                 },
-                provideMockStore({})
+                provideMockStore({
+                    initialState: {
+                        error: {
+                            bannerErrors: {}
+                        }
+                    }
+                })
             ]
         }).compileComponents();
 
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
index 1ac5412..cea1232 100644
--- 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
@@ -18,6 +18,8 @@
 import { Injectable, inject } from '@angular/core';
 import { Observable } from 'rxjs';
 import { HttpClient } from '@angular/common/http';
+import { Bucket } from '../state/buckets';
+import { CreateBucketRequest, DeleteBucketRequest } from '../state/buckets/buckets.actions';
 
 @Injectable({ providedIn: 'root' })
 export class BucketsService {
@@ -25,7 +27,7 @@
 
     private static readonly API: string = '../nifi-registry-api';
 
-    getBuckets(): Observable<any> {
+    getBuckets(): Observable<Bucket[]> {
         // const mockError: HttpErrorResponse = new HttpErrorResponse({
         //     status: 404,
         //     statusText: 'Bad Gateway',
@@ -37,6 +39,61 @@
         // });
         // return throwError(() => mockError);
 
-        return this.httpClient.get(`${BucketsService.API}/buckets`);
+        return this.httpClient.get<Bucket[]>(`${BucketsService.API}/buckets`);
+    }
+
+    createBucket(request: CreateBucketRequest): Observable<Bucket> {
+        // const mockError: HttpErrorResponse = new HttpErrorResponse({
+        //     status: 404,
+        //     statusText: 'Bad Gateway',
+        //     url: `${BucketsService.API}/buckets`,
+        //     error: {
+        //         message: 'Mock error: unable to create bucket.',
+        //         timestamp: new Date().toISOString()
+        //     }
+        // });
+        // return throwError(() => mockError);
+
+        return this.httpClient.post<Bucket>(`${BucketsService.API}/buckets`, {
+            ...request,
+            revision: {
+                version: 0
+            }
+        });
+    }
+
+    updateBucket(request: { bucket: Bucket }): Observable<Bucket> {
+        // const mockError: HttpErrorResponse = new HttpErrorResponse({
+        //     status: 404,
+        //     statusText: 'Bad Gateway',
+        //     url: `${BucketsService.API}/buckets`,
+        //     error: {
+        //         message: 'Mock error: unable to update bucket.',
+        //         timestamp: new Date().toISOString()
+        //     }
+        // });
+        // return throwError(() => mockError);
+
+        return this.httpClient.put<Bucket>(
+            `${BucketsService.API}/buckets/${request.bucket.identifier}`,
+            request.bucket
+        );
+    }
+
+    deleteBucket(request: DeleteBucketRequest): Observable<Bucket> {
+        // const mockError: HttpErrorResponse = new HttpErrorResponse({
+        //     status: 404,
+        //     statusText: 'Bad Gateway',
+        //     url: `${BucketsService.API}/buckets`,
+        //     error: {
+        //         message: 'Mock error: unable to delete bucket.',
+        //         timestamp: new Date().toISOString()
+        //     }
+        // });
+        // return throwError(() => mockError);
+
+        return this.httpClient.delete<Bucket>(
+            `${BucketsService.API}/buckets/${request.bucket.identifier}?version=${request.version}`
+        );
     }
 }
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.actions.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.actions.ts
index 46a3df6..c612dc3 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.actions.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.actions.ts
@@ -16,7 +16,18 @@
  */
 
 import { createAction, props } from '@ngrx/store';
-import { LoadBucketsResponse } from '.';
+import { Bucket, LoadBucketsResponse } from '.';
+
+export interface CreateBucketRequest {
+    name: string;
+    description: string;
+    allowPublicRead: boolean;
+}
+
+export interface DeleteBucketRequest {
+    bucket: Bucket;
+    version: number;
+}
 
 export const loadBuckets = createAction('[Buckets] Load Buckets');
 
@@ -24,3 +35,46 @@
     '[Buckets] Load Buckets Success',
     props<{ response: LoadBucketsResponse }>()
 );
+
+export const openCreateBucketDialog = createAction('[Buckets] Open Create Bucket Dialog');
+
+export const createBucket = createAction(
+    '[Buckets] Create Bucket',
+    props<{ request: CreateBucketRequest; keepDialogOpen: boolean }>()
+);
+
+export const createBucketSuccess = createAction(
+    '[Buckets] Create Bucket Success',
+    props<{ response: Bucket; keepDialogOpen: boolean }>()
+);
+
+export const createBucketFailure = createAction('[Buckets] Create Bucket Failure');
+
+export const openEditBucketDialog = createAction(
+    '[Buckets] Open Edit Bucket Dialog',
+    props<{ request: { bucket: Bucket } }>()
+);
+
+export const updateBucket = createAction('[Buckets] Update Bucket', props<{ request: { bucket: Bucket } }>());
+
+export const updateBucketSuccess = createAction('[Buckets] Update Bucket Success', props<{ response: Bucket }>());
+
+export const updateBucketFailure = createAction('[Buckets] Update Bucket Failure');
+
+export const openDeleteBucketDialog = createAction(
+    '[Buckets] Open Delete Bucket Dialog',
+    props<{ request: { bucket: Bucket } }>()
+);
+
+export const deleteBucket = createAction('[Buckets] Delete Bucket', props<{ request: DeleteBucketRequest }>());
+
+export const deleteBucketSuccess = createAction('[Buckets] Delete Bucket Success', props<{ response: Bucket }>());
+
+export const deleteBucketFailure = createAction('[Buckets] Delete Bucket Failure');
+
+export const openManageBucketPoliciesDialog = createAction(
+    '[Buckets] Open Manage Bucket Policies Dialog',
+    props<{ request: { bucket: Bucket } }>()
+);
+
+export const bucketNoOp = createAction('[Buckets] No Op');
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.effects.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.effects.spec.ts
new file mode 100644
index 0000000..107b1be
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.effects.spec.ts
@@ -0,0 +1,389 @@
+/*
+ * 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 { TestBed } from '@angular/core/testing';
+import { provideMockActions } from '@ngrx/effects/testing';
+import { Action } from '@ngrx/store';
+import { Observable, of, throwError } from 'rxjs';
+import { BucketsEffects } from './buckets.effects';
+import { BucketsService } from '../../service/buckets.service';
+import { ErrorHelper } from '../../service/error-helper.service';
+import { MatDialog } from '@angular/material/dialog';
+import * as BucketsActions from './buckets.actions';
+import * as ErrorActions from '../error/error.actions';
+import { HttpErrorResponse } from '@angular/common/http';
+import { ErrorContextKey } from '../error';
+import { CreateBucketDialogComponent } from '../../pages/buckets/feature/ui/create-bucket-dialog/create-bucket-dialog.component';
+import { ManageBucketPoliciesDialogComponent } from '../../pages/buckets/feature/ui/manage-bucket-policies-dialog/manage-bucket-policies-dialog.component';
+import { YesNoDialog } from '@nifi/shared';
+import { Store } from '@ngrx/store';
+import { provideMockStore } from '@ngrx/store/testing';
+import { Bucket } from './index';
+
+const createBucket = (overrides = {}): Bucket => ({
+    identifier: 'bucket-1',
+    name: 'Test Bucket',
+    description: 'Test Description',
+    allowBundleRedeploy: false,
+    allowPublicRead: false,
+    createdTimestamp: 1632924000000,
+    revision: {
+        version: 1
+    },
+    permissions: {
+        canRead: true,
+        canWrite: true
+    },
+    link: {
+        href: '/nifi-registry-api/buckets/bucket-1',
+        params: {
+            rel: 'self'
+        }
+    },
+    ...overrides
+});
+
+describe('BucketsEffects', () => {
+    let actions$: Observable<Action>;
+    let effects: BucketsEffects;
+    let bucketsService: jest.Mocked<BucketsService>;
+    let errorHelper: jest.Mocked<ErrorHelper>;
+    let dialog: jest.Mocked<MatDialog>;
+    let store: Store;
+
+    beforeEach(() => {
+        const mockBucketsService = {
+            getBuckets: jest.fn(),
+            createBucket: jest.fn(),
+            updateBucket: jest.fn(),
+            deleteBucket: jest.fn()
+        };
+
+        const mockErrorHelper = {
+            getErrorString: jest.fn()
+        };
+
+        const mockDialog = {
+            open: jest.fn(),
+            closeAll: jest.fn()
+        };
+
+        TestBed.configureTestingModule({
+            providers: [
+                BucketsEffects,
+                provideMockActions(() => actions$),
+                provideMockStore(),
+                { provide: BucketsService, useValue: mockBucketsService },
+                { provide: ErrorHelper, useValue: mockErrorHelper },
+                { provide: MatDialog, useValue: mockDialog }
+            ]
+        });
+
+        effects = TestBed.inject(BucketsEffects);
+        bucketsService = TestBed.inject(BucketsService) as jest.Mocked<BucketsService>;
+        errorHelper = TestBed.inject(ErrorHelper) as jest.Mocked<ErrorHelper>;
+        dialog = TestBed.inject(MatDialog) as jest.Mocked<MatDialog>;
+        store = TestBed.inject(Store);
+        jest.spyOn(store, 'dispatch');
+    });
+
+    describe('loadBuckets$', () => {
+        it('should return loadBucketsSuccess with buckets on success', (done) => {
+            const buckets = [createBucket(), createBucket({ identifier: 'bucket-2' })];
+            bucketsService.getBuckets.mockReturnValue(of(buckets));
+
+            actions$ = of(BucketsActions.loadBuckets());
+
+            effects.loadBuckets$.subscribe((action) => {
+                expect(action).toEqual(
+                    BucketsActions.loadBucketsSuccess({
+                        response: { buckets }
+                    })
+                );
+                done();
+            });
+        });
+
+        it('should return error action on failure', (done) => {
+            const error = new HttpErrorResponse({ status: 404, statusText: 'Not Found' });
+            bucketsService.getBuckets.mockReturnValue(throwError(() => error));
+            errorHelper.getErrorString.mockReturnValue('Error loading buckets');
+
+            actions$ = of(BucketsActions.loadBuckets());
+
+            effects.loadBuckets$.subscribe((action) => {
+                expect(action).toEqual(
+                    ErrorActions.addBannerError({
+                        errorContext: {
+                            errors: ['Error loading buckets'],
+                            context: ErrorContextKey.GLOBAL
+                        }
+                    })
+                );
+                done();
+            });
+        });
+    });
+
+    describe('openCreateBucketDialog$', () => {
+        it('should open create bucket dialog', (done) => {
+            actions$ = of(BucketsActions.openCreateBucketDialog());
+
+            effects.openCreateBucketDialog$.subscribe(() => {
+                expect(dialog.open).toHaveBeenCalledWith(
+                    CreateBucketDialogComponent,
+                    expect.objectContaining({
+                        autoFocus: false
+                    })
+                );
+                done();
+            });
+        });
+    });
+
+    describe('createBucket$', () => {
+        const bucket = createBucket();
+        const request = {
+            name: bucket.name,
+            description: bucket.description,
+            allowPublicRead: bucket.allowPublicRead
+        };
+
+        it('should return createBucketSuccess on success', (done) => {
+            bucketsService.createBucket.mockReturnValue(of(bucket));
+
+            actions$ = of(BucketsActions.createBucket({ request, keepDialogOpen: false }));
+
+            effects.createBucket$.subscribe((action) => {
+                expect(action).toEqual(
+                    BucketsActions.createBucketSuccess({
+                        response: bucket,
+                        keepDialogOpen: false
+                    })
+                );
+                done();
+            });
+        });
+
+        it('should return error actions on failure', (done) => {
+            const error = new HttpErrorResponse({ status: 400, statusText: 'Bad Request' });
+            bucketsService.createBucket.mockReturnValue(throwError(() => error));
+            errorHelper.getErrorString.mockReturnValue('Error creating bucket');
+
+            actions$ = of(BucketsActions.createBucket({ request, keepDialogOpen: false }));
+
+            let actionCount = 0;
+            effects.createBucket$.subscribe((action) => {
+                if (actionCount === 0) {
+                    expect(action).toEqual(BucketsActions.createBucketFailure());
+                } else {
+                    expect(action).toEqual(
+                        ErrorActions.addBannerError({
+                            errorContext: {
+                                errors: ['Error creating bucket'],
+                                context: ErrorContextKey.CREATE_BUCKET
+                            }
+                        })
+                    );
+                    done();
+                }
+                actionCount++;
+            });
+        });
+    });
+
+    describe('updateBucket$', () => {
+        const bucket = createBucket();
+        const request = { bucket };
+
+        it('should return updateBucketSuccess on success', (done) => {
+            bucketsService.updateBucket.mockReturnValue(of(bucket));
+
+            actions$ = of(BucketsActions.updateBucket({ request }));
+
+            effects.updateBucket$.subscribe((action) => {
+                expect(action).toEqual(
+                    BucketsActions.updateBucketSuccess({
+                        response: bucket
+                    })
+                );
+                done();
+            });
+        });
+
+        it('should return error actions on failure', (done) => {
+            const error = new HttpErrorResponse({ status: 400, statusText: 'Bad Request' });
+            bucketsService.updateBucket.mockReturnValue(throwError(() => error));
+            errorHelper.getErrorString.mockReturnValue('Error updating bucket');
+
+            actions$ = of(BucketsActions.updateBucket({ request }));
+
+            let actionCount = 0;
+            effects.updateBucket$.subscribe((action) => {
+                if (actionCount === 0) {
+                    expect(action).toEqual(BucketsActions.updateBucketFailure());
+                } else {
+                    expect(action).toEqual(
+                        ErrorActions.addBannerError({
+                            errorContext: {
+                                errors: ['Error updating bucket'],
+                                context: ErrorContextKey.UPDATE_BUCKET
+                            }
+                        })
+                    );
+                    done();
+                }
+                actionCount++;
+            });
+        });
+    });
+
+    describe('deleteBucket$', () => {
+        const bucket = createBucket();
+        const request = { bucket, version: bucket.revision.version };
+
+        it('should return deleteBucketSuccess on success', (done) => {
+            bucketsService.deleteBucket.mockReturnValue(of(bucket));
+
+            actions$ = of(BucketsActions.deleteBucket({ request }));
+
+            effects.deleteBucket$.subscribe((action) => {
+                expect(action).toEqual(
+                    BucketsActions.deleteBucketSuccess({
+                        response: bucket
+                    })
+                );
+                done();
+            });
+        });
+
+        it('should return error actions on failure', (done) => {
+            const error = new HttpErrorResponse({ status: 400, statusText: 'Bad Request' });
+            bucketsService.deleteBucket.mockReturnValue(throwError(() => error));
+            errorHelper.getErrorString.mockReturnValue('Error deleting bucket');
+
+            actions$ = of(BucketsActions.deleteBucket({ request }));
+
+            let actionCount = 0;
+            effects.deleteBucket$.subscribe((action) => {
+                if (actionCount === 0) {
+                    expect(action).toEqual(BucketsActions.deleteBucketFailure());
+                } else {
+                    expect(action).toEqual(
+                        ErrorActions.snackBarError({
+                            error: 'Error deleting bucket'
+                        })
+                    );
+                    done();
+                }
+                actionCount++;
+            });
+        });
+    });
+
+    describe('openDeleteBucketDialog$', () => {
+        it('should open delete confirmation dialog', (done) => {
+            const bucket = createBucket();
+            const mockDialogRef = {
+                componentInstance: {
+                    yes: of(true)
+                }
+            };
+            dialog.open.mockReturnValue(mockDialogRef as any);
+
+            actions$ = of(BucketsActions.openDeleteBucketDialog({ request: { bucket } }));
+
+            effects.openDeleteBucketDialog$.subscribe(() => {
+                expect(dialog.open).toHaveBeenCalledWith(
+                    YesNoDialog,
+                    expect.objectContaining({
+                        data: {
+                            title: 'Delete Bucket',
+                            message: 'All items stored in this bucket will be deleted as well.'
+                        }
+                    })
+                );
+                expect(store.dispatch).toHaveBeenCalledWith(
+                    BucketsActions.deleteBucket({
+                        request: {
+                            bucket,
+                            version: bucket.revision.version
+                        }
+                    })
+                );
+                done();
+            });
+        });
+    });
+
+    describe('openManageBucketPoliciesDialog$', () => {
+        it('should open manage bucket policies dialog', (done) => {
+            const bucket = createBucket();
+
+            actions$ = of(BucketsActions.openManageBucketPoliciesDialog({ request: { bucket } }));
+
+            effects.openManageBucketPoliciesDialog$.subscribe(() => {
+                expect(dialog.open).toHaveBeenCalledWith(
+                    ManageBucketPoliciesDialogComponent,
+                    expect.objectContaining({
+                        autoFocus: false,
+                        data: { bucket }
+                    })
+                );
+                done();
+            });
+        });
+    });
+
+    describe('dialog closing effects', () => {
+        it('should close dialogs on createBucketSuccess when keepDialogOpen is false', (done) => {
+            actions$ = of(BucketsActions.createBucketSuccess({ response: createBucket(), keepDialogOpen: false }));
+
+            effects.createBucketSuccess$.subscribe(() => {
+                expect(dialog.closeAll).toHaveBeenCalled();
+                done();
+            });
+        });
+
+        it('should not close dialogs on createBucketSuccess when keepDialogOpen is true', (done) => {
+            actions$ = of(BucketsActions.createBucketSuccess({ response: createBucket(), keepDialogOpen: true }));
+
+            effects.createBucketSuccess$.subscribe(() => {
+                expect(dialog.closeAll).not.toHaveBeenCalled();
+                done();
+            });
+        });
+
+        it('should close dialogs on updateBucketSuccess', (done) => {
+            actions$ = of(BucketsActions.updateBucketSuccess({ response: createBucket() }));
+
+            effects.updateBucketSuccess$.subscribe(() => {
+                expect(dialog.closeAll).toHaveBeenCalled();
+                done();
+            });
+        });
+
+        it('should close dialogs on deleteBucketSuccess', (done) => {
+            actions$ = of(BucketsActions.deleteBucketSuccess({ response: createBucket() }));
+
+            effects.deleteBucketSuccess$.subscribe(() => {
+                expect(dialog.closeAll).toHaveBeenCalled();
+                done();
+            });
+        });
+    });
+});
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
index 070ad3a..39bedcb 100644
--- 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
@@ -16,44 +16,205 @@
  */
 
 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 { MatDialog } from '@angular/material/dialog';
+import { Actions, createEffect, ofType } from '@ngrx/effects';
+import { from, of, take } from 'rxjs';
+import { catchError, map, switchMap, tap } from 'rxjs/operators';
 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';
+import * as ErrorActions from '../error/error.actions';
+import { CreateBucketDialogComponent } from '../../pages/buckets/feature/ui/create-bucket-dialog/create-bucket-dialog.component';
+import { EditBucketDialogComponent } from '../../pages/buckets/feature/ui/edit-bucket-dialog/edit-bucket-dialog.component';
+import { ManageBucketPoliciesDialogComponent } from '../../pages/buckets/feature/ui/manage-bucket-policies-dialog/manage-bucket-policies-dialog.component';
+import { LARGE_DIALOG, MEDIUM_DIALOG, SMALL_DIALOG, YesNoDialog } from '@nifi/shared';
+import { deleteBucket } from './buckets.actions';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../../../nifi/src/app/state';
 
 @Injectable()
 export class BucketsEffects {
     private bucketsService = inject(BucketsService);
     private errorHelper = inject(ErrorHelper);
-
-    actions$ = inject(Actions);
+    private dialog = inject(MatDialog);
+    private actions$ = inject(Actions);
+    private store = inject<Store<NiFiState>>(Store);
 
     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)))
-                    )
-                );
-            })
+            switchMap(() =>
+                from(this.bucketsService.getBuckets()).pipe(
+                    map((response) =>
+                        BucketsActions.loadBucketsSuccess({
+                            response: {
+                                buckets: response
+                            }
+                        })
+                    ),
+                    catchError((errorResponse: HttpErrorResponse) => of(this.bannerError(errorResponse)))
+                )
+            )
         )
     );
 
+    openCreateBucketDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(BucketsActions.openCreateBucketDialog),
+                tap(() => {
+                    this.dialog.open(CreateBucketDialogComponent, {
+                        ...MEDIUM_DIALOG,
+                        autoFocus: false
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+
+    createBucket$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(BucketsActions.createBucket),
+            switchMap(({ request, keepDialogOpen }) =>
+                from(this.bucketsService.createBucket(request)).pipe(
+                    map((bucket) => BucketsActions.createBucketSuccess({ response: bucket, keepDialogOpen })),
+                    catchError((errorResponse: HttpErrorResponse) =>
+                        of(
+                            BucketsActions.createBucketFailure(),
+                            this.bannerError(errorResponse, ErrorContextKey.CREATE_BUCKET)
+                        )
+                    )
+                )
+            )
+        )
+    );
+
+    createBucketSuccess$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(BucketsActions.createBucketSuccess),
+                tap(({ keepDialogOpen }) => {
+                    if (!keepDialogOpen) {
+                        this.dialog.closeAll();
+                    }
+                })
+            ),
+        { dispatch: false }
+    );
+
+    openEditBucketDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(BucketsActions.openEditBucketDialog),
+                tap(({ request }) => {
+                    this.dialog.open(EditBucketDialogComponent, {
+                        ...MEDIUM_DIALOG,
+                        autoFocus: false,
+                        data: { bucket: request.bucket }
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+
+    updateBucket$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(BucketsActions.updateBucket),
+            switchMap(({ request }) =>
+                from(this.bucketsService.updateBucket(request)).pipe(
+                    map((bucket) => BucketsActions.updateBucketSuccess({ response: bucket })),
+                    catchError((errorResponse: HttpErrorResponse) =>
+                        of(
+                            BucketsActions.updateBucketFailure(),
+                            this.bannerError(errorResponse, ErrorContextKey.UPDATE_BUCKET)
+                        )
+                    )
+                )
+            )
+        )
+    );
+
+    updateBucketSuccess$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(BucketsActions.updateBucketSuccess),
+                tap(() => this.dialog.closeAll())
+            ),
+        { dispatch: false }
+    );
+
+    openDeleteBucketDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(BucketsActions.openDeleteBucketDialog),
+                tap(({ request }) => {
+                    const dialogRef = this.dialog.open(YesNoDialog, {
+                        ...SMALL_DIALOG,
+                        data: {
+                            title: 'Delete Bucket',
+                            message: `All items stored in this bucket will be deleted as well.`
+                        }
+                    });
+                    dialogRef.componentInstance.yes.pipe(take(1)).subscribe(() => {
+                        this.store.dispatch(
+                            deleteBucket({
+                                request: {
+                                    bucket: request.bucket,
+                                    version: request.bucket.revision.version
+                                }
+                            })
+                        );
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+
+    deleteBucket$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(BucketsActions.deleteBucket),
+            switchMap(({ request }) =>
+                from(this.bucketsService.deleteBucket(request)).pipe(
+                    map((bucket) => BucketsActions.deleteBucketSuccess({ response: bucket })),
+                    catchError((errorResponse: HttpErrorResponse) =>
+                        of(
+                            BucketsActions.deleteBucketFailure(),
+                            ErrorActions.snackBarError({ error: this.errorHelper.getErrorString(errorResponse) })
+                        )
+                    )
+                )
+            )
+        )
+    );
+
+    deleteBucketSuccess$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(BucketsActions.deleteBucketSuccess),
+                tap(() => this.dialog.closeAll())
+            ),
+        { dispatch: false }
+    );
+
+    openManageBucketPoliciesDialog$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(BucketsActions.openManageBucketPoliciesDialog),
+                tap(({ request }) => {
+                    this.dialog.open(ManageBucketPoliciesDialogComponent, {
+                        ...LARGE_DIALOG,
+                        autoFocus: false,
+                        data: { bucket: request.bucket }
+                    });
+                })
+            ),
+        { dispatch: false }
+    );
+
     private bannerError(errorResponse: HttpErrorResponse, context: ErrorContextKey = ErrorContextKey.GLOBAL) {
-        return DropletsActions.dropletsBannerError({
+        return ErrorActions.addBannerError({
             errorContext: {
                 errors: [this.errorHelper.getErrorString(errorResponse)],
                 context
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.reducer.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.reducer.ts
index 657c1f5..d609e5a 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.reducer.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.reducer.ts
@@ -16,8 +16,15 @@
  */
 
 import { createReducer, on } from '@ngrx/store';
-import { loadBuckets, loadBucketsSuccess } from './buckets.actions';
-import { BucketsState } from '.';
+import {
+    createBucketSuccess,
+    deleteBucketSuccess,
+    loadBuckets,
+    loadBucketsSuccess,
+    updateBucketSuccess
+} from './buckets.actions';
+import { Bucket, BucketsState } from '.';
+import { produce } from 'immer';
 
 export const initialState: BucketsState = {
     buckets: [],
@@ -34,5 +41,35 @@
         ...state,
         buckets: response.buckets,
         status: 'success' as const
-    }))
+    })),
+    on(createBucketSuccess, (state, { response }) => {
+        return produce(state, (draftState) => {
+            const componentIndex: number = draftState.buckets.findIndex(
+                (f: Bucket) => response.identifier === f.identifier
+            );
+            if (componentIndex === -1) {
+                draftState.buckets.push(response);
+            }
+        });
+    }),
+    on(deleteBucketSuccess, (state, { response }) => {
+        return produce(state, (draftState) => {
+            const componentIndex: number = draftState.buckets.findIndex(
+                (f: Bucket) => response.identifier === f.identifier
+            );
+            if (componentIndex > -1) {
+                draftState.buckets.splice(componentIndex, 1);
+            }
+        });
+    }),
+    on(updateBucketSuccess, (state, { response }) => {
+        return produce(state, (draftState) => {
+            const componentIndex: number = draftState.buckets.findIndex(
+                (f: Bucket) => response.identifier === f.identifier
+            );
+            if (componentIndex > -1) {
+                draftState.buckets[componentIndex] = response;
+            }
+        });
+    })
 );
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.selectors.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.selectors.ts
index 7384865..9ee3476 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.selectors.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/buckets/buckets.selectors.ts
@@ -19,9 +19,19 @@
 import { bucketsFeatureKey, BucketsState } from './index';
 
 import { resourcesFeatureKey, ResourcesState } from '..';
+import { selectCurrentRoute } from '@nifi/shared';
 
 export const selectResourcesState = createFeatureSelector<ResourcesState>(resourcesFeatureKey);
 
 export const selectBucketState = createSelector(selectResourcesState, (state) => state[bucketsFeatureKey]);
 
 export const selectBuckets = createSelector(selectBucketState, (state: BucketsState) => state.buckets);
+
+export const selectBucketsState = createSelector(selectBucketState, (state: BucketsState) => state);
+
+export const selectBucketIdFromRoute = createSelector(selectCurrentRoute, (route) => {
+    if (route) {
+        return route.params['id'] ?? null;
+    }
+    return null;
+});
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
index 9ddecaa..2760876 100644
--- 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
@@ -42,6 +42,8 @@
 
 export const deleteDropletSuccess = createAction('[Droplets] Delete Droplet Success', props<{ response: Droplet }>());
 
+export const deleteDropletFailure = createAction('[Droplets] Delete Droplet Failure');
+
 export const openImportNewDropletDialog = createAction(
     '[Droplets] Open Import New Droplet Dialog',
     props<{ request: ImportDropletDialog }>()
@@ -92,8 +94,6 @@
     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(
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.effects.spec.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.effects.spec.ts
new file mode 100644
index 0000000..4280e8c
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.effects.spec.ts
@@ -0,0 +1,496 @@
+/*
+ * 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 { TestBed } from '@angular/core/testing';
+import { provideMockActions } from '@ngrx/effects/testing';
+import { Action } from '@ngrx/store';
+import { Observable, of, throwError } from 'rxjs';
+import { DropletsEffects } from './droplets.effects';
+import { DropletsService } from '../../service/droplets.service';
+import { ErrorHelper } from '../../service/error-helper.service';
+import { MatDialog } from '@angular/material/dialog';
+import * as DropletsActions from './droplets.actions';
+import * as ErrorActions from '../../state/error/error.actions';
+import { HttpErrorResponse, HttpHeaders, HttpResponse } from '@angular/common/http';
+import { ErrorContextKey } from '../error';
+import { ImportNewDropletDialogComponent } from '../../pages/resources/feature/ui/import-new-droplet-dialog/import-new-droplet-dialog.component';
+import { DropletVersionsDialogComponent } from '../../pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component';
+import { YesNoDialog } from '@nifi/shared';
+import { Store } from '@ngrx/store';
+import { provideMockStore } from '@ngrx/store/testing';
+import { Bucket } from '../buckets';
+
+const createDroplet = (overrides = {}) => ({
+    identifier: 'droplet-1',
+    name: 'Test Droplet',
+    description: 'Test Description',
+    bucketIdentifier: 'bucket-1',
+    bucketName: 'Test Bucket',
+    createdTimestamp: 1632924000000,
+    modifiedTimestamp: 1632924000000,
+    type: 'FLOW',
+    permissions: {
+        canRead: true,
+        canWrite: true,
+        canDelete: true
+    },
+    revision: {
+        version: 1
+    },
+    link: {
+        href: '/nifi-registry-api/buckets/bucket-1/flows/droplet-1',
+        params: {
+            rel: 'self'
+        }
+    },
+    versionCount: 1,
+    ...overrides
+});
+
+const createBucket = (overrides = {}): Bucket => ({
+    identifier: 'bucket-1',
+    name: 'Test Bucket',
+    description: 'Test Description',
+    createdTimestamp: 1632924000000,
+    allowBundleRedeploy: false,
+    allowPublicRead: false,
+    permissions: {
+        canRead: true,
+        canWrite: true
+    },
+    revision: {
+        version: 1
+    },
+    link: {
+        href: '/nifi-registry-api/buckets/bucket-1',
+        params: {
+            rel: 'self'
+        }
+    },
+    ...overrides
+});
+
+describe('DropletsEffects', () => {
+    let actions$: Observable<Action>;
+    let effects: DropletsEffects;
+    let dropletsService: jest.Mocked<DropletsService>;
+    let errorHelper: jest.Mocked<ErrorHelper>;
+    let dialog: jest.Mocked<MatDialog>;
+    let store: Store;
+
+    beforeEach(() => {
+        const mockDropletsService = {
+            getDroplets: jest.fn(),
+            deleteDroplet: jest.fn(),
+            createNewDroplet: jest.fn(),
+            uploadDroplet: jest.fn(),
+            exportDropletVersionedSnapshot: jest.fn(),
+            getDropletSnapshotMetadata: jest.fn()
+        };
+
+        const mockErrorHelper = {
+            getErrorString: jest.fn()
+        };
+
+        const mockDialog = {
+            open: jest.fn(),
+            closeAll: jest.fn()
+        };
+
+        TestBed.configureTestingModule({
+            providers: [
+                DropletsEffects,
+                provideMockActions(() => actions$),
+                provideMockStore(),
+                { provide: DropletsService, useValue: mockDropletsService },
+                { provide: ErrorHelper, useValue: mockErrorHelper },
+                { provide: MatDialog, useValue: mockDialog }
+            ]
+        });
+
+        effects = TestBed.inject(DropletsEffects);
+        dropletsService = TestBed.inject(DropletsService) as jest.Mocked<DropletsService>;
+        errorHelper = TestBed.inject(ErrorHelper) as jest.Mocked<ErrorHelper>;
+        dialog = TestBed.inject(MatDialog) as jest.Mocked<MatDialog>;
+        store = TestBed.inject(Store);
+        jest.spyOn(store, 'dispatch');
+    });
+
+    describe('loadDroplets$', () => {
+        it('should return loadDropletsSuccess with droplets on success', (done) => {
+            const droplets = [createDroplet(), createDroplet({ identifier: 'droplet-2' })];
+            dropletsService.getDroplets.mockReturnValue(of(droplets));
+
+            actions$ = of(DropletsActions.loadDroplets());
+
+            effects.loadDroplets$.subscribe((action) => {
+                expect(action).toEqual(
+                    DropletsActions.loadDropletsSuccess({
+                        response: { droplets }
+                    })
+                );
+                done();
+            });
+        });
+
+        it('should return error action on failure', (done) => {
+            const error = new HttpErrorResponse({ status: 404, statusText: 'Not Found' });
+            dropletsService.getDroplets.mockReturnValue(throwError(() => error));
+            errorHelper.getErrorString.mockReturnValue('Error loading droplets');
+
+            actions$ = of(DropletsActions.loadDroplets());
+
+            effects.loadDroplets$.subscribe((action) => {
+                expect(action).toEqual(
+                    DropletsActions.dropletsBannerError({
+                        errorContext: {
+                            errors: ['Error loading droplets'],
+                            context: ErrorContextKey.GLOBAL
+                        }
+                    })
+                );
+                done();
+            });
+        });
+    });
+
+    describe('openDeleteDropletDialog$', () => {
+        it('should open delete confirmation dialog and dispatch delete action on confirmation', (done) => {
+            const droplet = createDroplet();
+            const mockDialogRef = {
+                componentInstance: {
+                    yes: of(true)
+                }
+            };
+            dialog.open.mockReturnValue(mockDialogRef as any);
+
+            actions$ = of(DropletsActions.openDeleteDropletDialog({ request: { droplet } }));
+
+            effects.openDeleteDropletDialog$.subscribe(() => {
+                expect(dialog.open).toHaveBeenCalledWith(
+                    YesNoDialog,
+                    expect.objectContaining({
+                        data: {
+                            title: 'Delete resource',
+                            message: `This action will delete all versions of ${droplet.name}`
+                        }
+                    })
+                );
+                expect(store.dispatch).toHaveBeenCalledWith(
+                    DropletsActions.deleteDroplet({
+                        request: { droplet }
+                    })
+                );
+                done();
+            });
+        });
+    });
+
+    describe('deleteDroplet$', () => {
+        const droplet = createDroplet();
+
+        it('should return deleteDropletSuccess on success', (done) => {
+            dropletsService.deleteDroplet.mockReturnValue(of(droplet));
+
+            actions$ = of(DropletsActions.deleteDroplet({ request: { droplet } }));
+
+            effects.deleteDroplet$.subscribe((action) => {
+                expect(action).toEqual(
+                    DropletsActions.deleteDropletSuccess({
+                        response: droplet
+                    })
+                );
+                done();
+            });
+        });
+
+        it('should return error actions on failure', (done) => {
+            const error = new HttpErrorResponse({ status: 400, statusText: 'Bad Request' });
+            dropletsService.deleteDroplet.mockReturnValue(throwError(() => error));
+            errorHelper.getErrorString.mockReturnValue('Error deleting droplet');
+
+            actions$ = of(DropletsActions.deleteDroplet({ request: { droplet } }));
+
+            let actionCount = 0;
+            effects.deleteDroplet$.subscribe((action) => {
+                if (actionCount === 0) {
+                    expect(action).toEqual(DropletsActions.deleteDropletFailure());
+                } else {
+                    expect(action).toEqual(
+                        ErrorActions.snackBarError({
+                            error: 'Error deleting droplet'
+                        })
+                    );
+                    done();
+                }
+                actionCount++;
+            });
+        });
+    });
+
+    describe('openImportNewDropletDialog$', () => {
+        it('should open import new droplet dialog', (done) => {
+            const buckets = [createBucket(), createBucket({ identifier: 'bucket-2' })];
+
+            actions$ = of(DropletsActions.openImportNewDropletDialog({ request: { buckets } }));
+
+            effects.openImportNewDropletDialog$.subscribe(() => {
+                expect(dialog.open).toHaveBeenCalledWith(
+                    ImportNewDropletDialogComponent,
+                    expect.objectContaining({
+                        autoFocus: false,
+                        data: { buckets }
+                    })
+                );
+                done();
+            });
+        });
+    });
+
+    describe('createNewDroplet$', () => {
+        const bucket = createBucket();
+        const request = {
+            bucket,
+            name: 'New Droplet',
+            description: 'New Description',
+            file: new File([], 'test.json')
+        };
+
+        it('should return createNewDropletSuccess and trigger version import on success', (done) => {
+            const newDroplet = createDroplet({ name: request.name, description: request.description });
+            dropletsService.createNewDroplet.mockReturnValue(of(newDroplet));
+
+            actions$ = of(DropletsActions.createNewDroplet({ request }));
+
+            effects.createNewDroplet$.subscribe((action) => {
+                expect(action).toEqual(
+                    DropletsActions.createNewDropletSuccess({
+                        response: newDroplet,
+                        request: {
+                            href: newDroplet.link.href,
+                            file: request.file,
+                            description: request.description
+                        }
+                    })
+                );
+                done();
+            });
+        });
+
+        it('should return error action on failure', (done) => {
+            const error = new HttpErrorResponse({ status: 400, statusText: 'Bad Request' });
+            dropletsService.createNewDroplet.mockReturnValue(throwError(() => error));
+            errorHelper.getErrorString.mockReturnValue('Error creating droplet');
+
+            actions$ = of(DropletsActions.createNewDroplet({ request }));
+
+            effects.createNewDroplet$.subscribe((action) => {
+                expect(action).toEqual(
+                    DropletsActions.dropletsBannerError({
+                        errorContext: {
+                            errors: ['Error creating droplet'],
+                            context: ErrorContextKey.CREATE_DROPLET
+                        }
+                    })
+                );
+                done();
+            });
+        });
+    });
+
+    describe('importNewDropletVersion$', () => {
+        const droplet = createDroplet();
+        const request = {
+            href: droplet.link.href,
+            file: new File([], 'test.json'),
+            description: 'New Version Description'
+        };
+
+        it('should return importNewDropletVersionSuccess on success', (done) => {
+            dropletsService.uploadDroplet.mockReturnValue(of(droplet));
+
+            actions$ = of(DropletsActions.importNewDropletVersion({ request }));
+
+            effects.importNewDroplet$.subscribe((action) => {
+                expect(action).toEqual(
+                    DropletsActions.importNewDropletVersionSuccess({
+                        response: droplet
+                    })
+                );
+                done();
+            });
+        });
+
+        it('should return error action on failure', (done) => {
+            const error = new HttpErrorResponse({ status: 400, statusText: 'Bad Request' });
+            dropletsService.uploadDroplet.mockReturnValue(throwError(() => error));
+            errorHelper.getErrorString.mockReturnValue('Error importing version');
+
+            actions$ = of(DropletsActions.importNewDropletVersion({ request }));
+
+            effects.importNewDroplet$.subscribe((action) => {
+                expect(action).toEqual(
+                    DropletsActions.dropletsBannerError({
+                        errorContext: {
+                            errors: ['Error importing version'],
+                            context: ErrorContextKey.IMPORT_DROPLET_VERSION
+                        }
+                    })
+                );
+                done();
+            });
+        });
+    });
+
+    describe('exportDropletVersion$', () => {
+        const droplet = createDroplet();
+        const request = {
+            droplet,
+            version: 1
+        };
+
+        it('should return exportDropletVersionSuccess and trigger download on success', (done) => {
+            const headers = new HttpHeaders().set('Filename', 'test.json');
+            const mockResponse = new HttpResponse({
+                body: JSON.stringify({ content: 'test' }),
+                headers
+            });
+            dropletsService.exportDropletVersionedSnapshot.mockReturnValue(of(mockResponse));
+
+            // Mock document methods
+            const mockAnchor = {
+                href: '',
+                download: '',
+                setAttribute: jest.fn(),
+                click: jest.fn()
+            };
+            jest.spyOn(document, 'createElement').mockReturnValue(mockAnchor as any);
+            jest.spyOn(document.body, 'appendChild').mockImplementation();
+            jest.spyOn(document.body, 'removeChild').mockImplementation();
+
+            actions$ = of(DropletsActions.exportDropletVersion({ request }));
+
+            effects.exportDropletVersion$.subscribe((action) => {
+                expect(action).toEqual(
+                    DropletsActions.exportDropletVersionSuccess({
+                        response: mockResponse
+                    })
+                );
+                expect(document.createElement).toHaveBeenCalledWith('a');
+                expect(mockAnchor.click).toHaveBeenCalled();
+                done();
+            });
+        });
+
+        it('should return error action on failure', (done) => {
+            const error = new HttpErrorResponse({ status: 400, statusText: 'Bad Request' });
+            dropletsService.exportDropletVersionedSnapshot.mockReturnValue(throwError(() => error));
+            errorHelper.getErrorString.mockReturnValue('Error exporting version');
+
+            actions$ = of(DropletsActions.exportDropletVersion({ request }));
+
+            effects.exportDropletVersion$.subscribe((action) => {
+                expect(action).toEqual(
+                    DropletsActions.dropletsBannerError({
+                        errorContext: {
+                            errors: ['Error exporting version'],
+                            context: ErrorContextKey.EXPORT_DROPLET_VERSION
+                        }
+                    })
+                );
+                done();
+            });
+        });
+    });
+
+    describe('openDropletVersionsDialog$', () => {
+        it('should open versions dialog with metadata', (done) => {
+            const droplet = createDroplet();
+            const versions = [{ version: 1, userIdentity: 'user1', timestamp: Date.now() }];
+            dropletsService.getDropletSnapshotMetadata.mockReturnValue(of(versions));
+
+            actions$ = of(DropletsActions.openDropletVersionsDialog({ request: { droplet } }));
+
+            effects.openDropletVersionsDialog$.subscribe((action) => {
+                expect(dialog.open).toHaveBeenCalledWith(
+                    DropletVersionsDialogComponent,
+                    expect.objectContaining({
+                        autoFocus: false,
+                        data: { droplet, versions }
+                    })
+                );
+                expect(action).toEqual(DropletsActions.noOp());
+                done();
+            });
+        });
+
+        it('should return error action on metadata fetch failure', (done) => {
+            const droplet = createDroplet();
+            const error = new HttpErrorResponse({ status: 400, statusText: 'Bad Request' });
+            dropletsService.getDropletSnapshotMetadata.mockReturnValue(throwError(() => error));
+            errorHelper.getErrorString.mockReturnValue('Error fetching versions');
+
+            actions$ = of(DropletsActions.openDropletVersionsDialog({ request: { droplet } }));
+
+            effects.openDropletVersionsDialog$.subscribe((action) => {
+                expect(action).toEqual(
+                    DropletsActions.dropletsBannerError({
+                        errorContext: {
+                            errors: ['Error fetching versions'],
+                            context: ErrorContextKey.GLOBAL
+                        }
+                    })
+                );
+                done();
+            });
+        });
+    });
+
+    describe('dialog closing effects', () => {
+        it('should close dialogs on deleteDropletSuccess', (done) => {
+            actions$ = of(DropletsActions.deleteDropletSuccess({ response: createDroplet() }));
+
+            effects.deleteDropletSuccess$.subscribe(() => {
+                expect(dialog.closeAll).toHaveBeenCalled();
+                done();
+            });
+        });
+
+        it('should close dialogs on importNewDropletVersionSuccess', (done) => {
+            actions$ = of(DropletsActions.importNewDropletVersionSuccess({ response: createDroplet() }));
+
+            effects.importNewDropletSuccess$.subscribe(() => {
+                expect(dialog.closeAll).toHaveBeenCalled();
+                done();
+            });
+        });
+
+        it('should close dialogs on exportDropletVersionSuccess', (done) => {
+            const headers = new HttpHeaders().set('Filename', 'test.json');
+            const mockResponse = new HttpResponse({
+                body: JSON.stringify({ content: 'test' }),
+                headers
+            });
+            actions$ = of(DropletsActions.exportDropletVersionSuccess({ response: mockResponse }));
+
+            effects.exportDropletVersionSuccess$.subscribe(() => {
+                expect(dialog.closeAll).toHaveBeenCalled();
+                done();
+            });
+        });
+    });
+});
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
index 8e9ef9d..d54846d 100644
--- 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
@@ -19,11 +19,10 @@
 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 { catchError, from, map, of, switchMap, take, tap } from 'rxjs';
+import { MEDIUM_DIALOG, SMALL_DIALOG, XL_DIALOG, YesNoDialog } 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
@@ -38,16 +37,18 @@
 } 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';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../../../nifi/src/app/state';
+import { deleteDroplet } from './droplets.actions';
 
 @Injectable()
 export class DropletsEffects {
     private dropletsService = inject(DropletsService);
     private dialog = inject(MatDialog);
     private errorHelper = inject(ErrorHelper);
-    private router = inject(Router);
+    private store = inject<Store<NiFiState>>(Store);
 
     actions$ = inject(Actions);
 
@@ -76,14 +77,22 @@
             this.actions$.pipe(
                 ofType(DropletsActions.openDeleteDropletDialog),
                 tap(({ request }) => {
-                    this.dialog.open<DeleteDropletDialogComponent, ExportFlowVersionDialogData>(
-                        DeleteDropletDialogComponent,
-                        {
-                            ...SMALL_DIALOG,
-                            autoFocus: false,
-                            data: request
+                    const dialogRef = this.dialog.open(YesNoDialog, {
+                        ...SMALL_DIALOG,
+                        data: {
+                            title: 'Delete resource',
+                            message: `This action will delete all versions of ${request.droplet.name}`
                         }
-                    );
+                    });
+                    dialogRef.componentInstance.yes.pipe(take(1)).subscribe(() => {
+                        this.store.dispatch(
+                            deleteDroplet({
+                                request: {
+                                    droplet: request.droplet
+                                }
+                            })
+                        );
+                    });
                 })
             ),
         { dispatch: false }
@@ -97,7 +106,10 @@
                 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))
+                        of(
+                            DropletsActions.deleteDropletFailure(),
+                            ErrorActions.snackBarError({ error: this.errorHelper.getErrorString(errorResponse) })
+                        )
                     )
                 )
             )
@@ -346,18 +358,6 @@
         )
     );
 
-    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),
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.actions.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.actions.ts
index 1c36c77..6976ad8 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.actions.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.actions.ts
@@ -18,6 +18,8 @@
 import { createAction, props } from '@ngrx/store';
 import { ErrorContext, ErrorContextKey } from './index';
 
+export const snackBarError = createAction('[Error] Snackbar Error', props<{ error: string }>());
+
 export const addBannerError = createAction('[Error] Add Banner Error', props<{ errorContext: ErrorContext }>());
 
 export const clearBannerErrors = createAction('[Error] Clear Banner Errors', props<{ context: ErrorContextKey }>());
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.effects.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.effects.ts
index fa3f2f8..17cbc18 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.effects.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/error.effects.ts
@@ -15,7 +15,26 @@
  * limitations under the License.
  */
 
-import { Injectable } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
+import { Actions, createEffect, ofType } from '@ngrx/effects';
+import * as ErrorActions from '../../../../../nifi/src/app/state/error/error.actions';
+import { map, tap } from 'rxjs';
+import { MatSnackBar } from '@angular/material/snack-bar';
 
 @Injectable()
-export class ErrorEffects {}
+export class ErrorEffects {
+    private actions$ = inject(Actions);
+    private snackBar = inject(MatSnackBar);
+
+    snackBarError$ = createEffect(
+        () =>
+            this.actions$.pipe(
+                ofType(ErrorActions.snackBarError),
+                map((action) => action.error),
+                tap((error) => {
+                    this.snackBar.open(error, 'Dismiss', { duration: 30000 });
+                })
+            ),
+        { dispatch: false }
+    );
+}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/index.ts b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/index.ts
index 621c666..8e270e0 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/index.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/error/index.ts
@@ -24,9 +24,10 @@
 
 export enum ErrorContextKey {
     EXPORT_DROPLET_VERSION = 'droplet listing',
-    DELETE_DROPLET = 'delete droplet',
     CREATE_DROPLET = 'create droplet',
     IMPORT_DROPLET_VERSION = 'import droplet version',
+    CREATE_BUCKET = 'create bucket',
+    UPDATE_BUCKET = 'update bucket',
     GLOBAL = 'global'
 }
 
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
index 4acdb7f..f49d392 100644
--- 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
@@ -25,7 +25,7 @@
                         priority
                         alt="nifi registry logo"
                         class="pointer p-3"
-                        (click)="goHome()" />
+                        (click)="navigateToResources()" />
                 </div>
                 <div class="h-[64px] w-[112px] mr-6 relative">
                     <img ngSrc="assets/icons/nifi-logo.svg" fill priority alt="NiFi Logo" />
@@ -39,7 +39,12 @@
                     <i class="fa fa-navicon text-[32px]"></i>
                 </button>
                 <mat-menu #globalMenu="matMenu" xPosition="before">
-                    <button mat-menu-item class="global-menu-item"><i class="fa mr-2"></i>Buckets</button>
+                    <button mat-menu-item class="global-menu-item" (click)="navigateToResources()">
+                        <i class="fa mr-2"></i>Resources
+                    </button>
+                    <button mat-menu-item class="global-menu-item" (click)="navigateToWorkflow()">
+                        <i class="fa mr-2"></i>Buckets
+                    </button>
                     <button mat-menu-item class="global-menu-item"><i class="fa mr-2"></i>Users/Groups</button>
                     <button mat-menu-item class="global-menu-item">
                         <i class="fa fa-info-circle primary-color mr-2"></i>
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
index ba4d813..de407ac 100644
--- 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
@@ -57,10 +57,6 @@
         }
     }
 
-    goHome() {
-        this.router.navigateByUrl('/resources');
-    }
-
     toggleTheme(theme: string) {
         this.theme = theme;
         this.storage.setItem('theme', theme);
@@ -72,4 +68,12 @@
         this.storage.setItem('disable-animations', this.disableAnimations.toString());
         window.location.reload();
     }
+
+    navigateToResources() {
+        this.router.navigateByUrl('/explorer');
+    }
+
+    navigateToWorkflow() {
+        this.router.navigateByUrl('/buckets');
+    }
 }
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
index 933cfcf..2d3d086 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/index.html
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/index.html
@@ -21,7 +21,7 @@
         <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" />
+        <link rel="icon" type="image/png" href="assets/icons/registry-favicon.png" />
     </head>
     <body class="mat-app-background mat-typography text-base">
         <nifi-registry-app-root></nifi-registry-app-root>