Repository: vmware-tanzu/octant (go)

Issue:
Text component with status icons does not render
**What steps did you take and what happened:**
```
		tableRow := component.TableRow{
			"Name":    component.NewText(name, func(t *component.Text) {
				t.SetStatus(component.TextStatusOK)
			}),
		}
```

This results in a table with two errors on the browser:
`ERROR NullInjectorError: R3InjectorError(SugarloafModule)[ClrPopoverToggleService -> ClrPopoverToggleService -> ClrPopoverToggleService -> ClrPopoverToggleService]: 
  NullInjectorError: No provider for ClrPopoverToggleService!`

`ERROR Error: ASSERTION ERROR: Reached the max number of directives [Expected=> 1 != 1 <=Actual]`

**What did you expect to happen:**
Setting a status on a text component does not have any details to show so it should not need a popover, but the icons are expected to be visible.

**Anything else you would like to add:**
xref #1360 

- Octant version (use `octant version`): 0.16.2


Pull request:
Fixed status indicator for text and link components
**What this PR does / why we need it**:
Moves status indicators from clarity angular to core. Re-enable plugin authors to set text next to an icon.

**Which issue(s) this PR fixes**
- Fixes #1688 

**Special notes for your reviewer**:
Example usage:
```
text := component.NewText("my-text", func(t *component.Text) {
	t.SetStatus(component.TextStatusOK)
})
```

Same thing for link components.

Signed-off-by: Sam Foo <foos@vmware.com>


Patch:
diff --git a/changelogs/unreleased/1688-GuessWhoSamFoo b/changelogs/unreleased/1688-GuessWhoSamFoo
new file mode 100644
index 00000000..a2761c1d
--- /dev/null
+++ b/changelogs/unreleased/1688-GuessWhoSamFoo
@@ -0,0 +1,1 @@
+Fix status indicator for text and link components
diff --git a/changelogs/unreleased/178-wwitzel3 b/changelogs/unreleased/178-wwitzel3
new file mode 100644
index 00000000..4206f848
--- /dev/null
+++ b/changelogs/unreleased/178-wwitzel3
@@ -0,0 +1,1 @@
+feat(plugins): allow automatic reloading of Go plugins
diff --git a/internal/api/content_manager.go b/internal/api/content_manager.go
index 1f8eb088..0193872c 100644
--- a/internal/api/content_manager.go
+++ b/internal/api/content_manager.go
@@ -188,7 +188,11 @@ func (cm *ContentManager) generateContent(ctx context.Context, state octant.Stat
 
 	m, ok := cm.moduleManager.ModuleForContentPath(contentPath)
 	if !ok {
-		return emptyContent, false, fmt.Errorf("unable to find module for content path %q", contentPath)
+		content := Content{
+			Response: notFoundPage(contentPath),
+			Path:     contentPath,
+		}
+		return content, false, nil
 	}
 	modulePath := strings.TrimPrefix(contentPath, m.Name())
 	options := module.ContentOptions{
diff --git a/pkg/dash/plugin.go b/pkg/dash/plugin.go
index 25e3787f..437fc16d 100644
--- a/pkg/dash/plugin.go
+++ b/pkg/dash/plugin.go
@@ -32,7 +32,7 @@ func initPlugin(moduleManager module.ManagerInterface, actionManager *action.Man
 		if plugin.IsJavaScriptPlugin(pluginPath) {
 			continue
 		}
-		if err := m.Load(pluginPath); err != nil {
+		if _, err := m.Load(pluginPath); err != nil {
 			return nil, fmt.Errorf("initialize plugin %q: %w", pluginPath, err)
 		}
 
diff --git a/pkg/plugin/fake/fakes.go b/pkg/plugin/fake/fakes.go
index ff4488ca..418fcd05 100644
--- a/pkg/plugin/fake/fakes.go
+++ b/pkg/plugin/fake/fakes.go
@@ -137,6 +137,21 @@ func (mr *MockManagerStoreMockRecorder) Clients() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clients", reflect.TypeOf((*MockManagerStore)(nil).Clients))
 }
 
+// Get mocks base method.
+func (m *MockManagerStore) Get(arg0 string) (plugin.Client, bool) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Get", arg0)
+	ret0, _ := ret[0].(plugin.Client)
+	ret1, _ := ret[1].(bool)
+	return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockManagerStoreMockRecorder) Get(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockManagerStore)(nil).Get), arg0)
+}
+
 // GetCommand mocks base method.
 func (m *MockManagerStore) GetCommand(arg0 string) (string, error) {
 	m.ctrl.T.Helper()
@@ -182,6 +197,21 @@ func (mr *MockManagerStoreMockRecorder) GetMetadata(arg0 interface{}) *gomock.Ca
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMetadata", reflect.TypeOf((*MockManagerStore)(nil).GetMetadata), arg0)
 }
 
+// GetModuleService mocks base method.
+func (m *MockManagerStore) GetModuleService(arg0 string) (plugin.ModuleService, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetModuleService", arg0)
+	ret0, _ := ret[0].(plugin.ModuleService)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetModuleService indicates an expected call of GetModuleService.
+func (mr *MockManagerStoreMockRecorder) GetModuleService(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetModuleService", reflect.TypeOf((*MockManagerStore)(nil).GetModuleService), arg0)
+}
+
 // GetService mocks base method.
 func (m *MockManagerStore) GetService(arg0 string) (plugin.Service, error) {
 	m.ctrl.T.Helper()
@@ -211,6 +241,18 @@ func (mr *MockManagerStoreMockRecorder) NamesJS() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NamesJS", reflect.TypeOf((*MockManagerStore)(nil).NamesJS))
 }
 
+// Remove mocks base method.
+func (m *MockManagerStore) Remove(arg0 string) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Remove", arg0)
+}
+
+// Remove indicates an expected call of Remove.
+func (mr *MockManagerStoreMockRecorder) Remove(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockManagerStore)(nil).Remove), arg0)
+}
+
 // RemoveJS mocks base method.
 func (m *MockManagerStore) RemoveJS(arg0 string) {
 	m.ctrl.T.Helper()
diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go
index 68e8babf..3c98f5c1 100644
--- a/pkg/plugin/manager.go
+++ b/pkg/plugin/manager.go
@@ -82,10 +82,13 @@ type Client interface {
 type ManagerStore interface {
 	Store(name string, client Client, metadata *Metadata, cmd string) error
 	StoreJS(name string, jspc JSPlugin) error
+	Get(name string) (Client, bool)
 	GetJS(name string) (JSPlugin, bool)
+	Remove(name string)
 	RemoveJS(name string)
 	NamesJS() []string
 	GetMetadata(name string) (*Metadata, error)
+	GetModuleService(name string) (ModuleService, error)
 	GetService(name string) (Service, error)
 	GetCommand(name string) (string, error)
 	Clients() map[string]Client
@@ -130,6 +133,11 @@ func (s *DefaultStore) StoreJS(name string, plugin JSPlugin) error {
 	return nil
 }
 
+func (s *DefaultStore) Get(name string) (Client, bool) {
+	client, ok := s.clients[name]
+	return client, ok
+}
+
 func (s *DefaultStore) GetJS(name string) (JSPlugin, bool) {
 	voidStar, ok := s.jsPlugins.Load(name)
 	if !ok {
@@ -139,6 +147,12 @@ func (s *DefaultStore) GetJS(name string) (JSPlugin, bool) {
 	return jspc, ok
 }
 
+func (s *DefaultStore) Remove(name string) {
+	delete(s.commands, name)
+	delete(s.clients, name)
+	delete(s.metadata, name)
+}
+
 func (s *DefaultStore) RemoveJS(name string) {
 	s.jsPlugins.Delete(name)
 }
@@ -156,8 +170,8 @@ func (s *DefaultStore) Store(name string, client Client, metadata *Metadata, cmd
 	return nil
 }
 
-// GetService gets the service for a plugin.
-func (s *DefaultStore) GetService(name string) (Service, error) {
+// GetModuleService gets the moduleService for a plugin
+func (s *DefaultStore) GetModuleService(name string) (ModuleService, error) {
 	client, ok := s.clients[name]
 	if !ok {
 		return nil, errors.Errorf("plugin %q doesn't have a client", name)
@@ -173,11 +187,24 @@ func (s *DefaultStore) GetService(name string) (Service, error) {
 		return nil, errors.Wrapf(err, "dispensing plugin for %q", name)
 	}
 
-	service, ok := raw.(Service)
+	moduleService, ok := raw.(ModuleService)
 	if !ok {
 		return nil, errors.Errorf("unknown type for plugin %q: %T", name, raw)
 	}
 
+	return moduleService, nil
+}
+
+// GetService gets the service for a plugin.
+func (s *DefaultStore) GetService(name string) (Service, error) {
+	moduleService, err := s.GetModuleService(name)
+	if err != nil {
+		return nil, err
+	}
+	service, ok := moduleService.(Service)
+	if !ok {
+		return nil, fmt.Errorf("failed to GetService, unable to cast ModuleService to Service")
+	}
 	return service, nil
 }
 
@@ -217,9 +244,9 @@ func (s *DefaultStore) ClientNames() []string {
 	return list
 }
 
-type config struct {
-	cmd  string
-	name string
+type PluginConfig struct {
+	Cmd  string
+	Name string
 }
 
 // ClusterClient defines the cluster client plugin manager has access to.
@@ -277,7 +304,7 @@ type Manager struct {
 	Runners Runners
 
 	octantClient javascript.OctantClient
-	configs      []config
+	configs      []PluginConfig
 	store        ManagerStore
 
 	lock sync.Mutex
@@ -322,40 +349,48 @@ func (m *Manager) SetStore(store ManagerStore) {
 }
 
 // Load loads a plugin.
-func (m *Manager) Load(cmd string) error {
+func (m *Manager) Load(cmd string) (PluginConfig, error) {
 	m.lock.Lock()
 	defer m.lock.Unlock()
 
 	name := filepath.Base(cmd)
 
 	for _, c := range m.configs {
-		if name == c.name {
-			return errors.Errorf("tried to load plugin %q more than once", name)
+		if name == c.Name {
+			return PluginConfig{}, fmt.Errorf("tried to load plugin %q more than once", name)
 		}
 	}
 
-	c := config{
-		name: name,
-		cmd:  cmd,
+	c := PluginConfig{
+		Name: name,
+		Cmd:  cmd,
 	}
 
 	m.configs = append(m.configs, c)
 
-	return nil
+	return c, nil
 }
 
-func (m *Manager) watchJS(ctx context.Context) {
+func (m *Manager) Unload(ctx context.Context, cmd string) {
+	if IsJavaScriptPlugin(cmd) {
+		m.unregisterJSPlugin(ctx, cmd)
+	} else {
+		m.unregisterGoPlugin(ctx, cmd)
+	}
+}
+
+func (m *Manager) watchPluginFiles(ctx context.Context) {
 	logger := log.From(ctx)
 
 	dirs, err := DefaultConfig.PluginDirs(DefaultConfig.Home())
 	if err != nil {
-		logger.Errorf("unable to get plugin dirs for JavaScript plugins: %w", err)
+		logger.Errorf("unable to get plugin directories: %w", err)
 		return
 	}
 
 	watcher, err := fsnotify.NewWatcher()
 	if err != nil {
-		logger.Errorf("initializing JavaScript plugin watcher: %w", err)
+		logger.Errorf("initializing plugin watcher: %w", err)
 		return
 	}
 	defer func() {
@@ -367,57 +402,53 @@ func (m *Manager) watchJS(ctx context.Context) {
 	watchedDirs := []string{}
 	for _, dir := range dirs {
 		if err := watcher.Add(dir); err != nil {
-			logger.Warnf("Unable to add %s to JavaScript plugin watcher. Error: %s\n", dir, err)
+			logger.Warnf("Unable to add %s to the plugin watcher. Error: %s\n", dir, err)
 		} else {
 			watchedDirs = append(watchedDirs, dir)
 		}
 	}
 
 	if len(watchedDirs) > 0 {
-		logger.Infof("Watching for new JavaScript plugins in %q\n", dirs)
+		logger.Infof("Watching for plugin changes %q\n", dirs)
 	}
 
 	writeEvents := make(map[string]bool)
 	updatePlugin := func(name string) {
-		jsPlugin, ok := m.store.GetJS(name)
-		if ok {
-			if err := m.unregisterJSPlugin(ctx, jsPlugin); err != nil {
-				logger.Errorf("unregistering: %w", err)
+		m.Unload(ctx, name)
+		if IsJavaScriptPlugin(name) {
+			logger.Infof("reloading: JavaScript plugin: %s", name)
+			if err := m.registerJSPlugin(ctx, name); err != nil {
+				logger.Errorf("reloading: JavaScript plugin: %w", err)
+			}
+		} else {
+			logger.Infof("reloading: Go plugin: %s", name)
+			config, err := m.Load(name)
+			if err != nil {
+				logger.Errorf("reloading: Go plugin: %w", err)
+			} else {
+				m.start(ctx, config)
 			}
-			m.store.RemoveJS(name)
-		}
-		logger.Infof("reloading: JavaScript plugin: %s", name)
-		if err := m.registerJSPlugin(ctx, name); err != nil {
-			logger.Errorf("reloading: JavaScript plugin watcher: %w", err)
 		}
 	}
 
 	for {
 		select {
 		case <-ctx.Done():
-			logger.Infof("context cancelled shutting down JavaScript plugin watcher.")
+			logger.Infof("context cancelled shutting down plugin watcher.")
 			return
 		case ev, ok := <-watcher.Events:
 			if !ok {
-				logger.Errorf("bad event returned from JavaScript plugin watcher")
+				logger.Errorf("bad event returned from plugin watcher")
 				return
 			}
 			if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
 				continue
 			}
-			if IsJavaScriptPlugin(ev.Name) {
-				if ev.Op&fsnotify.Remove == fsnotify.Remove {
-					jsPlugin, ok := m.store.GetJS(ev.Name)
-					if ok {
-						if err := m.unregisterJSPlugin(ctx, jsPlugin); err != nil {
-							logger.Errorf("unregistering: %w", err)
-						}
-						m.store.RemoveJS(ev.Name)
-						logger.Infof("removing: JavaScript plugin: %s", ev.Name)
-					}
-				} else if ev.Op&fsnotify.Write == fsnotify.Write {
-					writeEvents[ev.Name] = true
-				}
+			if ev.Op&(fsnotify.Remove|fsnotify.Rename) == fsnotify.Remove || ev.Op&fsnotify.Rename == fsnotify.Rename {
+				logger.Infof("removing plugin: %s", ev.Name)
+				m.Unload(ctx, ev.Name)
+			} else if ev.Op&(fsnotify.Write|ev.Op&fsnotify.Rename|ev.Op&fsnotify.Create) == fsnotify.Create {
+				writeEvents[ev.Name] = true
 			}
 		case err, ok := <-watcher.Errors:
 			if !ok {
@@ -435,12 +466,63 @@ func (m *Manager) watchJS(ctx context.Context) {
 	}
 }
 
-func (m *Manager) unregisterJSPlugin(_ context.Context, p JSPlugin) error {
-	p.Close()
+func (m *Manager) unregisterGoPlugin(ctx context.Context, cmd string) {
+	m.lock.Lock()
+	defer m.lock.Unlock()
+
+	logger := log.From(ctx)
+
+	name := filepath.Base(cmd)
+
+	var match bool
+	var matchIdx int
+
+	for i, c := range m.configs {
+		if name == c.Name {
+			match = true
+			matchIdx = i
+			break
+		}
+	}
+
+	if match {
+		client, ok := m.store.Get(name)
+		if ok {
+			client.Kill()
+		}
+		if metadata, err := m.store.GetMetadata(name); err == nil {
+			moduleService, err := m.store.GetModuleService(name)
+			if err != nil {
+				logger.Errorf("failed unregister service (go): %w", err)
+			}
+			if err := m.unregisterMetadata(ctx, cmd, metadata, moduleService); err != nil {
+				logger.Errorf("failed unregister metadata (go): %w", err)
+			}
+		}
+
+		m.configs[matchIdx] = m.configs[len(m.configs)-1]
+		m.configs = m.configs[:len(m.configs)-1]
+
+		m.store.Remove(name)
+	}
+}
+
+func (m *Manager) unregisterJSPlugin(ctx context.Context, cmd string) error {
+	jsPlugin, ok := m.store.GetJS(cmd)
+	if ok {
+		m.store.RemoveJS(cmd)
+		jsPlugin.Close()
+		if err := m.unregisterMetadata(ctx, jsPlugin.PluginPath(), jsPlugin.Metadata(), jsPlugin); err != nil {
+			logger := log.From(ctx)
+			logger.Errorf("failed unregister metadata (js): %w", err)
+		}
+	}
+	return nil
+}
 
-	metadata := p.Metadata()
+func (m *Manager) unregisterMetadata(ctx context.Context, path string, metadata *Metadata, moduleService ModuleService) error {
 	if metadata.Capabilities.IsModule {
-		mp, err := NewModuleProxy(metadata.Name, metadata, p)
+		mp, err := NewModuleProxy(metadata.Name, metadata, moduleService)
 		if err != nil {
 			return fmt.Errorf("unregister: creating module proxy: %w", err)
 		}
@@ -449,7 +531,7 @@ func (m *Manager) unregisterJSPlugin(_ context.Context, p JSPlugin) error {
 
 	for _, actionName := range metadata.Capabilities.ActionNames {
 		actionPath := actionName
-		m.ActionRegistrar.Unregister(actionPath, p.PluginPath())
+		m.ActionRegistrar.Unregister(actionPath, path)
 	}
 	return nil
 }
@@ -544,8 +626,6 @@ func (m *Manager) Start(ctx context.Context) error {
 		}
 	}
 
-	go m.watchJS(ctx)
-
 	for i := range m.configs {
 		c := m.configs[i]
 
@@ -554,12 +634,14 @@ func (m *Manager) Start(ctx context.Context) error {
 		}
 	}
 
-	go m.watchPlugins(ctx)
+	go m.watchPluginFiles(ctx)
+	go m.goPluginPingPong(ctx)
 
 	return nil
 }
 
-func (m *Manager) watchPlugins(ctx context.Context) {
+// goPluginPingPong will attempt to restart a Go plugin process if the Ping method for a client returns a non-nil error.
+func (m *Manager) goPluginPingPong(ctx context.Context) {
 	logger := log.From(ctx)
 
 	timer := time.NewTimer(5 * time.Second)
@@ -570,7 +652,7 @@ func (m *Manager) watchPlugins(ctx context.Context) {
 		case <-ctx.Done():
 			logger.Infof("shutting down plugin watcher")
 			running = false
-			break
+			return
 		case <-timer.C:
 			for clientName, client := range m.store.Clients() {
 				rpcClient, err := client.Client()
@@ -587,9 +669,11 @@ func (m *Manager) watchPlugins(ctx context.Context) {
 						continue
 					}
 
-					c := config{
-						name: clientName,
-						cmd:  cmd,
+					m.Unload(ctx, cmd)
+
+					c := PluginConfig{
+						Name: clientName,
+						Cmd:  cmd,
 					}
 
 					if err := m.start(ctx, c); err != nil {
@@ -605,39 +689,39 @@ func (m *Manager) watchPlugins(ctx context.Context) {
 
 }
 
-func (m *Manager) start(ctx context.Context, c config) error {
-	client := m.ClientFactory.Init(ctx, c.cmd)
+func (m *Manager) start(ctx context.Context, c PluginConfig) error {
+	client := m.ClientFactory.Init(ctx, c.Cmd)
 
 	rpcClient, err := client.Client()
 	if err != nil {
-		return errors.Wrapf(err, "get rpc client for %q", c.name)
+		return errors.Wrapf(err, "get rpc client for %q", c.Name)
 	}
 
-	pluginLogger := log.From(ctx).With("plugin-name", c.name)
+	pluginLogger := log.From(ctx).With("plugin-name", c.Name)
 
 	raw, err := rpcClient.Dispense("plugin")
 	if err != nil {
-		return errors.Wrapf(err, "dispensing plugin for %q", c.name)
+		return errors.Wrapf(err, "dispensing plugin for %q", c.Name)
 	}
 
 	service, ok := raw.(Service)
 	if !ok {
-		return errors.Errorf("unknown type for plugin %q: %T", c.name, raw)
+		return errors.Errorf("unknown type for plugin %q: %T", c.Name, raw)
 	}
 
 	metadata, err := service.Register(ctx, m.API.Addr())
 	if err != nil {
-		return errors.Wrapf(err, "register plugin %q", c.name)
+		return errors.Wrapf(err, "register plugin %q", c.Name)
 	}
 
-	if err := m.store.Store(c.name, client, &metadata, c.cmd); err != nil {
+	if err := m.store.Store(c.Name, client, &metadata, c.Cmd); err != nil {
 		return errors.Wrapf(err, "storing plugin")
 	}
 
 	for _, actionName := range metadata.Capabilities.ActionNames {
 		actionPath := actionName
 		pluginLogger.With("action-path", actionPath).Infof("registering plugin action")
-		err := m.ActionRegistrar.Register(actionPath, c.name, func(ctx context.Context, alerter action.Alerter, payload action.Payload) error {
+		err := m.ActionRegistrar.Register(actionPath, c.Name, func(ctx context.Context, alerter action.Alerter, payload action.Payload) error {
 			return service.HandleAction(ctx, actionPath, payload)
 		})
 
@@ -647,7 +731,7 @@ func (m *Manager) start(ctx context.Context, c config) error {
 	}
 
 	pluginLogger.With(
-		"cmd", c.cmd,
+		"cmd", c.Cmd,
 		"metadata", metadata,
 	).Infof("registered plugin %q", metadata.Name)
 
@@ -659,7 +743,7 @@ func (m *Manager) start(ctx context.Context, c config) error {
 
 		pluginLogger.Infof("plugin supports navigation")
 
-		mp, err := NewModuleProxy(c.name, &metadata, service)
+		mp, err := NewModuleProxy(c.Name, &metadata, service)
 		if err != nil {
 			return errors.Wrap(err, "creating module proxy")
 		}
diff --git a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.html b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.html
index 5eb7dfb6..aca56557 100644
--- a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.html
+++ b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.html
@@ -1,18 +1,14 @@
-<ng-container *ngIf="currentStatus">
-  <ng-container *ngIf="detail !== undefined; else showStatus">
+<ng-container *ngIf="status && iconShape">
+  <ng-container *ngIf="!detail; else signpost">
+    <cds-icon shape="{{iconShape}}" status="{{currentStatus}}" size="16" solid></cds-icon>
+  </ng-container>
+  <ng-template #signpost>
     <clr-signpost>
-
-      <clr-icon *ngIf="currentStatus === 'error'" shape="exclamation-circle" class="is-error is-solid" size="16" clrSignpostTrigger></clr-icon>
-      <clr-icon *ngIf="currentStatus === 'warning'" shape="info-circle" class="is-warning is-solid" size="16" clrSignpostTrigger></clr-icon>
-      <clr-icon *ngIf="currentStatus === 'success'" shape="check-circle" class="is-success is-solid" size="16" clrSignpostTrigger></clr-icon>
+      <cds-icon shape="{{iconShape}}" status="{{currentStatus}}" size="16" clrSignpostTrigger solid></cds-icon>
       <span style="margin-right: 4px;"></span>
       <clr-signpost-content *clrIfOpen>
         <app-view-container [view]="detail"></app-view-container>
       </clr-signpost-content>
     </clr-signpost>
-  </ng-container>
+  </ng-template>
 </ng-container>
-
-<ng-template #showStatus>
-  <div clrSignpostTrigger></div>
-</ng-template>
diff --git a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.spec.ts b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.spec.ts
index 8ea1942e..fa149428 100644
--- a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.spec.ts
+++ b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.spec.ts
@@ -3,7 +3,9 @@ import {
   IndicatorComponent,
   Status,
   statusLookup,
+  iconLookup,
 } from './indicator.component';
+import { CdsModule } from '@cds/angular';
 import { Component } from '@angular/core';
 import { View } from '../../../models/content';
 
@@ -26,6 +28,7 @@ describe('IndicatorComponent', () => {
     waitForAsync(() => {
       TestBed.configureTestingModule({
         declarations: [WrapperComponent, IndicatorComponent],
+        imports: [CdsModule],
       }).compileComponents();
     })
   );
@@ -52,9 +55,8 @@ describe('IndicatorComponent', () => {
       });
 
       it(`shows ${name} indicator`, () => {
-        expect(
-          element.querySelector(`app-indicator clr-icon.is-${name}`)
-        ).toBeTruthy();
+        const e = element.querySelector(`app-indicator cds-icon`);
+        expect(e.getAttribute('ng-reflect-shape')).toBe(iconLookup[v]);
       });
     });
   });
@@ -67,7 +69,7 @@ describe('IndicatorComponent', () => {
     });
 
     it('does not show an indicator', () => {
-      expect(element.querySelector('app-indicator div.indicator')).toBeNull();
+      expect(element.querySelector('app-indicator cds-icon')).toBeNull();
     });
   });
 });
diff --git a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.ts b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.ts
index c4aa1c86..b0c89334 100644
--- a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.ts
+++ b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.ts
@@ -1,5 +1,12 @@
 import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 import { View } from '../../../models/content';
+import '@cds/core/icon/register.js';
+import {
+  ClarityIcons,
+  checkCircleIcon,
+  exclamationCircleIcon,
+  infoCircleIcon,
+} from '@cds/core/icon';
 
 /**
  * Status are statuses known to indicator.
@@ -13,7 +20,13 @@ export const Status = {
 export const statusLookup = {
   [Status.Ok]: 'success',
   [Status.Warning]: 'warning',
-  [Status.Error]: 'error',
+  [Status.Error]: 'danger',
+};
+
+export const iconLookup = {
+  [Status.Ok]: 'check-circle',
+  [Status.Warning]: 'info-circle',
+  [Status.Error]: 'exclamation-circle',
 };
 
 @Component({
@@ -28,12 +41,20 @@ export class IndicatorComponent implements OnChanges {
   detail: View;
 
   currentStatus: string;
+  iconShape: string;
 
-  constructor() {}
+  constructor() {
+    ClarityIcons.addIcons(
+      checkCircleIcon,
+      exclamationCircleIcon,
+      infoCircleIcon
+    );
+  }
 
   ngOnChanges(changes: SimpleChanges): void {
     if (changes.status) {
       this.currentStatus = statusLookup[changes.status.currentValue];
+      this.iconShape = iconLookup[changes.status.currentValue];
     }
   }
 
diff --git a/web/src/app/modules/shared/components/presentation/text/text.component.html b/web/src/app/modules/shared/components/presentation/text/text.component.html
index 5453b09d..2a84220c 100644
--- a/web/src/app/modules/shared/components/presentation/text/text.component.html
+++ b/web/src/app/modules/shared/components/presentation/text/text.component.html
@@ -4,7 +4,7 @@
   </ng-container>
   {{ value }}
   <div *ngIf="clipboardValue" class="button-container">
-    <div cds-layout="horizonal gap:sm">
+    <div cds-layout="horizontal gap:sm">
       <cds-icon-button action="flat" ariaLabel="copy-button" (click)="copyToClipboard()">
         <cds-icon shape="clipboard"></cds-icon>
         <ng-container *ngIf="copied">Copied</ng-container>
diff --git a/web/src/app/modules/shared/shared.module.ts b/web/src/app/modules/shared/shared.module.ts
index 8c062cf5..a46f6f89 100644
--- a/web/src/app/modules/shared/shared.module.ts
+++ b/web/src/app/modules/shared/shared.module.ts
@@ -210,7 +210,6 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
     IconComponent,
     IFrameComponent,
     IndicatorComponent,
-    IconComponent,
     LabelsComponent,
     LabelSelectorComponent,
     LinkComponent,
@@ -296,6 +295,7 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
     HeptagonLabelComponent,
     IconComponent,
     IFrameComponent,
+    IndicatorComponent,
     JSONEditorComponent,
     LabelsComponent,
     LabelSelectorComponent,
diff --git a/web/src/styles.scss b/web/src/styles.scss
index a0440601..b28a4b00 100644
--- a/web/src/styles.scss
+++ b/web/src/styles.scss
@@ -46,7 +46,7 @@ cds-modal-content app-view-text {
   color: var(--cds-global-typography-color-500);
 }
 
-cds-icon {
+app-helper cds-icon {
   color: var(--clr-header-font-color);
 }
 


Test patch:
diff --git a/pkg/plugin/manager_test.go b/pkg/plugin/manager_test.go
index bc7a0165..3c3c4330 100644
--- a/pkg/plugin/manager_test.go
+++ b/pkg/plugin/manager_test.go
@@ -83,8 +83,9 @@ func TestManager(t *testing.T) {
 
 	manager.SetStore(store)
 
-	err := manager.Load(name)
+	cfg, err := manager.Load(name)
 	require.NoError(t, err)
+	assert.Equal(t, name, cfg.Name)
 
 	ctx := context.Background()
 	err = manager.Start(ctx)
@@ -205,14 +206,14 @@ func TestManager_Tabs(t *testing.T) {
 
 type fakePluginClient struct {
 	clientProtocol *fake.MockClientProtocol
-	service        *fake.MockService
+	service        *fake.MockModuleService
 	name           string
 }
 
 var _ dashPlugin.Client = (*fakePluginClient)(nil)
 
 func newFakePluginClient(name string, controller *gomock.Controller) *fakePluginClient {
-	service := fake.NewMockService(controller)
+	service := fake.NewMockModuleService(controller)
 	metadata := dashPlugin.Metadata{
 		Name: name,
 	}