diff --git a/ui/app/controllers/csi/plugins/index.js b/ui/app/controllers/csi/plugins/index.js
index 51f6c5a61fe..ba62315b905 100644
--- a/ui/app/controllers/csi/plugins/index.js
+++ b/ui/app/controllers/csi/plugins/index.js
@@ -1,40 +1,49 @@
 import { inject as service } from '@ember/service';
+import { computed } from '@ember/object';
 import { alias, readOnly } from '@ember/object/computed';
 import Controller, { inject as controller } from '@ember/controller';
 import SortableFactory from 'nomad-ui/mixins/sortable-factory';
+import Searchable from 'nomad-ui/mixins/searchable';
 import { lazyClick } from 'nomad-ui/helpers/lazy-click';
 
-export default Controller.extend(SortableFactory([]), {
-  userSettings: service(),
-  pluginsController: controller('csi/plugins'),
-
-  isForbidden: alias('pluginsController.isForbidden'),
-
-  queryParams: {
-    currentPage: 'page',
-    sortProperty: 'sort',
-    sortDescending: 'desc',
-  },
+export default Controller.extend(
+  SortableFactory([
+    'plainId',
+    'controllersHealthyProportion',
+    'nodesHealthyProportion',
+    'provider',
+  ]),
+  Searchable,
+  {
+    userSettings: service(),
+    pluginsController: controller('csi/plugins'),
+
+    isForbidden: alias('pluginsController.isForbidden'),
+
+    queryParams: {
+      currentPage: 'page',
+      searchTerm: 'search',
+      sortProperty: 'sort',
+      sortDescending: 'desc',
+    },
 
-  currentPage: 1,
-  pageSize: readOnly('userSettings.pageSize'),
+    currentPage: 1,
+    pageSize: readOnly('userSettings.pageSize'),
 
-  sortProperty: 'id',
-  sortDescending: false,
+    searchProps: computed(() => ['id']),
+    fuzzySearchProps: computed(() => ['id']),
 
-  listToSort: alias('model'),
-  sortedPlugins: alias('listSorted'),
+    sortProperty: 'id',
+    sortDescending: false,
 
-  // TODO: Remove once this page gets search capability
-  resetPagination() {
-    if (this.currentPage != null) {
-      this.set('currentPage', 1);
-    }
-  },
+    listToSort: alias('model'),
+    listToSearch: alias('listSorted'),
+    sortedPlugins: alias('listSearched'),
 
-  actions: {
-    gotoPlugin(plugin, event) {
-      lazyClick([() => this.transitionToRoute('csi.plugins.plugin', plugin.plainId), event]);
+    actions: {
+      gotoPlugin(plugin, event) {
+        lazyClick([() => this.transitionToRoute('csi.plugins.plugin', plugin.plainId), event]);
+      },
     },
-  },
-});
+  }
+);
diff --git a/ui/app/controllers/csi/volumes/index.js b/ui/app/controllers/csi/volumes/index.js
index aa63bb51b69..20aad240660 100644
--- a/ui/app/controllers/csi/volumes/index.js
+++ b/ui/app/controllers/csi/volumes/index.js
@@ -3,6 +3,7 @@ import { computed } from '@ember/object';
 import { alias, readOnly } from '@ember/object/computed';
 import Controller, { inject as controller } from '@ember/controller';
 import SortableFactory from 'nomad-ui/mixins/sortable-factory';
+import Searchable from 'nomad-ui/mixins/searchable';
 import { lazyClick } from 'nomad-ui/helpers/lazy-click';
 
 export default Controller.extend(
@@ -13,6 +14,7 @@ export default Controller.extend(
     'nodesHealthyProportion',
     'provider',
   ]),
+  Searchable,
   {
     system: service(),
     userSettings: service(),
@@ -22,6 +24,7 @@ export default Controller.extend(
 
     queryParams: {
       currentPage: 'page',
+      searchTerm: 'search',
       sortProperty: 'sort',
       sortDescending: 'desc',
     },
@@ -32,6 +35,10 @@ export default Controller.extend(
     sortProperty: 'id',
     sortDescending: false,
 
+    searchProps: computed(() => ['name']),
+    fuzzySearchProps: computed(() => ['name']),
+    fuzzySearchEnabled: true,
+
     /**
       Visible volumes are those that match the selected namespace
     */
@@ -49,14 +56,8 @@ export default Controller.extend(
     }),
 
     listToSort: alias('visibleVolumes'),
-    sortedVolumes: alias('listSorted'),
-
-    // TODO: Remove once this page gets search capability
-    resetPagination() {
-      if (this.currentPage != null) {
-        this.set('currentPage', 1);
-      }
-    },
+    listToSearch: alias('listSorted'),
+    sortedVolumes: alias('listSearched'),
 
     actions: {
       gotoVolume(volume, event) {
diff --git a/ui/app/templates/csi/plugins/index.hbs b/ui/app/templates/csi/plugins/index.hbs
index 43f7eb8a72f..7d4f5f1f1f9 100644
--- a/ui/app/templates/csi/plugins/index.hbs
+++ b/ui/app/templates/csi/plugins/index.hbs
@@ -9,6 +9,17 @@
   {{#if isForbidden}}
     {{partial "partials/forbidden-message"}}
   {{else}}
+    <div class="toolbar">
+      <div class="toolbar-item">
+        {{#if model.length}}
+          {{search-box
+            data-test-plugins-search
+            searchTerm=(mut searchTerm)
+            onChange=(action resetPagination)
+            placeholder="Search plugins..."}}
+        {{/if}}
+      </div>
+    </div>
     {{#if sortedPlugins}}
       {{#list-pagination
         source=sortedPlugins
@@ -56,10 +67,17 @@
       {{/list-pagination}}
     {{else}}
       <div data-test-empty-plugins-list class="empty-message">
-        <h3 data-test-empty-plugins-list-headline class="empty-message-headline">No Plugins</h3>
-        <p class="empty-message-body">
-          The cluster currently has no registered CSI Plugins.
-        </p>
+        {{#if (eq model.length 0)}}
+          <h3 data-test-empty-plugins-list-headline class="empty-message-headline">No Plugins</h3>
+          <p class="empty-message-body">
+            The cluster currently has no registered CSI Plugins.
+          </p>
+        {{else if searchTerm}}
+          <h3 data-test-empty-plugins-list-headline class="empty-message-headline">No Matches</h3>
+          <p class="empty-message-body">
+            No plugins match the term <strong>{{searchTerm}}</strong>
+          </p>
+        {{/if}}
       </div>
     {{/if}}
   {{/if}}
diff --git a/ui/app/templates/csi/volumes/index.hbs b/ui/app/templates/csi/volumes/index.hbs
index 6b4b41fafb8..67f5493f751 100644
--- a/ui/app/templates/csi/volumes/index.hbs
+++ b/ui/app/templates/csi/volumes/index.hbs
@@ -9,6 +9,17 @@
   {{#if isForbidden}}
     {{partial "partials/forbidden-message"}}
   {{else}}
+    <div class="toolbar">
+      <div class="toolbar-item">
+        {{#if model.length}}
+          {{search-box
+            data-test-volumes-search
+            searchTerm=(mut searchTerm)
+            onChange=(action resetPagination)
+            placeholder="Search volumes..."}}
+        {{/if}}
+      </div>
+    </div>
     {{#if sortedVolumes}}
       {{#list-pagination
         source=sortedVolumes
@@ -60,10 +71,17 @@
       {{/list-pagination}}
     {{else}}
       <div data-test-empty-volumes-list class="empty-message">
-        <h3 data-test-empty-volumes-list-headline class="empty-message-headline">No Volumes</h3>
-        <p class="empty-message-body">
-          The cluster currently has no CSI Volumes.
-        </p>
+        {{#if (eq model.length 0)}}
+          <h3 data-test-empty-volumes-list-headline class="empty-message-headline">No Volumes</h3>
+          <p class="empty-message-body">
+            The cluster currently has no CSI Volumes.
+          </p>
+        {{else if searchTerm}}
+          <h3 data-test-empty-volumes-list-headline class="empty-message-headline">No Matches</h3>
+          <p class="empty-message-body">
+            No volumes match the term <strong>{{searchTerm}}</strong>
+          </p>
+        {{/if}}
       </div>
     {{/if}}
   {{/if}}
diff --git a/ui/mirage/factories/csi-plugin.js b/ui/mirage/factories/csi-plugin.js
index e57aea40ea7..b0f95354532 100644
--- a/ui/mirage/factories/csi-plugin.js
+++ b/ui/mirage/factories/csi-plugin.js
@@ -30,6 +30,9 @@ export default Factory.extend({
   // When false, the plugin will not make its own volumes
   createVolumes: true,
 
+  // When true, doesn't create any resources, state, or events for associated allocations
+  shallow: false,
+
   afterCreate(plugin, server) {
     let storageNodes;
     let storageControllers;
@@ -37,16 +40,32 @@ export default Factory.extend({
     if (plugin.isMonolith) {
       const pluginJob = server.create('job', { type: 'service', createAllocations: false });
       const count = plugin.nodesExpected;
-      storageNodes = server.createList('storage-node', count, { job: pluginJob });
-      storageControllers = server.createList('storage-controller', count, { job: pluginJob });
+      storageNodes = server.createList('storage-node', count, {
+        job: pluginJob,
+        shallow: plugin.shallow,
+      });
+      storageControllers = server.createList('storage-controller', count, {
+        job: pluginJob,
+        shallow: plugin.shallow,
+      });
     } else {
-      const controllerJob = server.create('job', { type: 'service', createAllocations: false });
-      const nodeJob = server.create('job', { type: 'service', createAllocations: false });
+      const controllerJob = server.create('job', {
+        type: 'service',
+        createAllocations: false,
+        shallow: plugin.shallow,
+      });
+      const nodeJob = server.create('job', {
+        type: 'service',
+        createAllocations: false,
+        shallow: plugin.shallow,
+      });
       storageNodes = server.createList('storage-node', plugin.nodesExpected, {
         job: nodeJob,
+        shallow: plugin.shallow,
       });
       storageControllers = server.createList('storage-controller', plugin.controllersExpected, {
         job: controllerJob,
+        shallow: plugin.shallow,
       });
     }
 
diff --git a/ui/mirage/factories/storage-controller.js b/ui/mirage/factories/storage-controller.js
index 3557116692a..4399e375bf7 100644
--- a/ui/mirage/factories/storage-controller.js
+++ b/ui/mirage/factories/storage-controller.js
@@ -17,6 +17,8 @@ export default Factory.extend({
   requiresControllerPlugin: true,
   requiresTopologies: true,
 
+  shallow: false,
+
   controllerInfo: () => ({
     SupportsReadOnlyAttach: true,
     SupportsAttachDetach: true,
@@ -29,6 +31,7 @@ export default Factory.extend({
       jobId: storageController.job.id,
       forceRunningClientStatus: true,
       modifyTime: storageController.updateTime * 1000000,
+      shallow: storageController.shallow,
     });
 
     storageController.update({
diff --git a/ui/mirage/factories/storage-node.js b/ui/mirage/factories/storage-node.js
index 88a70020d4d..b5224195929 100644
--- a/ui/mirage/factories/storage-node.js
+++ b/ui/mirage/factories/storage-node.js
@@ -17,6 +17,8 @@ export default Factory.extend({
   requiresControllerPlugin: true,
   requiresTopologies: true,
 
+  shallow: false,
+
   nodeInfo: () => ({
     MaxVolumes: 51,
     AccessibleTopology: {
@@ -29,6 +31,7 @@ export default Factory.extend({
     const alloc = server.create('allocation', {
       jobId: storageNode.job.id,
       modifyTime: storageNode.updateTime * 1000000,
+      shallow: storageNode.shallow,
     });
 
     storageNode.update({
diff --git a/ui/tests/acceptance/plugins-list-test.js b/ui/tests/acceptance/plugins-list-test.js
index 3b66a31c0c4..f86e2d66120 100644
--- a/ui/tests/acceptance/plugins-list-test.js
+++ b/ui/tests/acceptance/plugins-list-test.js
@@ -23,7 +23,7 @@ module('Acceptance | plugins list', function(hooks) {
 
   test('/csi/plugins should list the first page of plugins sorted by id', async function(assert) {
     const pluginCount = PluginsList.pageSize + 1;
-    server.createList('csi-plugin', pluginCount);
+    server.createList('csi-plugin', pluginCount, { shallow: true });
 
     await PluginsList.visit();
 
@@ -35,7 +35,7 @@ module('Acceptance | plugins list', function(hooks) {
   });
 
   test('each plugin row should contain information about the plugin', async function(assert) {
-    const plugin = server.create('csi-plugin');
+    const plugin = server.create('csi-plugin', { shallow: true });
 
     await PluginsList.visit();
 
@@ -56,7 +56,7 @@ module('Acceptance | plugins list', function(hooks) {
   });
 
   test('each plugin row should link to the corresponding plugin', async function(assert) {
-    const plugin = server.create('csi-plugin');
+    const plugin = server.create('csi-plugin', { shallow: true });
 
     await PluginsList.visit();
 
@@ -77,6 +77,30 @@ module('Acceptance | plugins list', function(hooks) {
     assert.equal(PluginsList.emptyState.headline, 'No Plugins');
   });
 
+  test('when there are plugins, but no matches for a search, there is an empty message', async function(assert) {
+    server.create('csi-plugin', { id: 'cat 1', shallow: true });
+    server.create('csi-plugin', { id: 'cat 2', shallow: true });
+
+    await PluginsList.visit();
+
+    await PluginsList.search('dog');
+    assert.ok(PluginsList.isEmpty);
+    assert.equal(PluginsList.emptyState.headline, 'No Matches');
+  });
+
+  test('search resets the current page', async function(assert) {
+    server.createList('csi-plugin', PluginsList.pageSize + 1, { shallow: true });
+
+    await PluginsList.visit();
+    await PluginsList.nextPage();
+
+    assert.equal(currentURL(), '/csi/plugins?page=2');
+
+    await PluginsList.search('foobar');
+
+    assert.equal(currentURL(), '/csi/plugins?search=foobar');
+  });
+
   test('when accessing plugins is forbidden, a message is shown with a link to the tokens page', async function(assert) {
     server.pretender.get('/v1/plugins', () => [403, {}, null]);
 
@@ -92,7 +116,7 @@ module('Acceptance | plugins list', function(hooks) {
     pageObject: PluginsList,
     pageObjectList: PluginsList.plugins,
     async setup() {
-      server.createList('csi-plugin', PluginsList.pageSize);
+      server.createList('csi-plugin', PluginsList.pageSize, { shallow: true });
       await PluginsList.visit();
     },
   });
diff --git a/ui/tests/acceptance/volumes-list-test.js b/ui/tests/acceptance/volumes-list-test.js
index 4aff4b92f0f..4c15ff44ac4 100644
--- a/ui/tests/acceptance/volumes-list-test.js
+++ b/ui/tests/acceptance/volumes-list-test.js
@@ -101,6 +101,30 @@ module('Acceptance | volumes list', function(hooks) {
     assert.equal(VolumesList.emptyState.headline, 'No Volumes');
   });
 
+  test('when there are volumes, but no matches for a search, there is an empty message', async function(assert) {
+    server.create('csi-volume', { id: 'cat 1' });
+    server.create('csi-volume', { id: 'cat 2' });
+
+    await VolumesList.visit();
+
+    await VolumesList.search('dog');
+    assert.ok(VolumesList.isEmpty);
+    assert.equal(VolumesList.emptyState.headline, 'No Matches');
+  });
+
+  test('searching resets the current page', async function(assert) {
+    server.createList('csi-volume', VolumesList.pageSize + 1);
+
+    await VolumesList.visit();
+    await VolumesList.nextPage();
+
+    assert.equal(currentURL(), '/csi/volumes?page=2');
+
+    await VolumesList.search('foobar');
+
+    assert.equal(currentURL(), '/csi/volumes?search=foobar');
+  });
+
   test('when the namespace query param is set, only matching volumes are shown and the namespace value is forwarded to app state', async function(assert) {
     server.createList('namespace', 2);
     const volume1 = server.create('csi-volume', { namespaceId: server.db.namespaces[0].id });
diff --git a/ui/tests/pages/storage/plugins/list.js b/ui/tests/pages/storage/plugins/list.js
index e46afa44820..f294baadc3f 100644
--- a/ui/tests/pages/storage/plugins/list.js
+++ b/ui/tests/pages/storage/plugins/list.js
@@ -1,4 +1,12 @@
-import { clickable, collection, create, isPresent, text, visitable } from 'ember-cli-page-object';
+import {
+  clickable,
+  collection,
+  create,
+  fillable,
+  isPresent,
+  text,
+  visitable,
+} from 'ember-cli-page-object';
 
 import error from 'nomad-ui/tests/pages/components/error';
 import pageSizeSelect from 'nomad-ui/tests/pages/components/page-size-select';
@@ -8,6 +16,8 @@ export default create({
 
   visit: visitable('/csi/plugins'),
 
+  search: fillable('[data-test-plugins-search] input'),
+
   plugins: collection('[data-test-plugin-row]', {
     id: text('[data-test-plugin-id]'),
     controllerHealth: text('[data-test-plugin-controller-health]'),
diff --git a/ui/tests/pages/storage/volumes/list.js b/ui/tests/pages/storage/volumes/list.js
index 85687f7b00a..36b17a23933 100644
--- a/ui/tests/pages/storage/volumes/list.js
+++ b/ui/tests/pages/storage/volumes/list.js
@@ -1,4 +1,12 @@
-import { clickable, collection, create, isPresent, text, visitable } from 'ember-cli-page-object';
+import {
+  clickable,
+  collection,
+  create,
+  fillable,
+  isPresent,
+  text,
+  visitable,
+} from 'ember-cli-page-object';
 
 import error from 'nomad-ui/tests/pages/components/error';
 import pageSizeSelect from 'nomad-ui/tests/pages/components/page-size-select';
@@ -8,6 +16,8 @@ export default create({
 
   visit: visitable('/csi/volumes'),
 
+  search: fillable('[data-test-volumes-search] input'),
+
   volumes: collection('[data-test-volume-row]', {
     name: text('[data-test-volume-name]'),
     schedulable: text('[data-test-volume-schedulable]'),