Repository: vmware-tanzu/octant
Pull request #653 (2020-02-10 20:45:24)

added more detail to contributing.md
Signed-off-by: Wayne Witzel III <wayne@riotousliving.com>


**What this PR does / why we need it**:
Adds some more detail to the contributing documentation.



Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d6522c6e..5c4c8f39 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,33 @@
 # Contributing
 
-## CHANGELOG 
+## Communication
+
+We prefer asynchronous to synchronous communication when ever possible. This ensures that all interested parties are able
+to find relevant information even when ideas and information are being exchanged across many timezones. When ever
+synchronous communication is happening, we encourage folks to make best efforts to transfer that information to an
+issue if relevant or send it to the project-octant group. This could be as plain text notes or a link to a recording
+or markdown document for example.
+
+* [project-octant](https://groups.google.com/forum/#!forum/project-octant)
+* [#octant Slack](https://kubernetes.slack.com/app_redirect?channel=CM37M9FCG)
+
+### Weekly Community Meeting
+We have a weekly Octant community meeting that is held live (recordings uploaded later). We highly encourage folks
+who are interested in contributing to Octant to attend these meetings. More details on the
+ [Octant community page](https://octant.dev/community/).
+
+## Tools
+
+The Octant project uses GitHub issues and pull requests as the primary method for communicating about what work needs
+to be done, what work is currently being done, who work is assigned to, and the current state of that work.
+With GitHub issues we use the ZenHub tool. ZenHub gives us another layer of metadata on top of GitHub issues that
+allow for visualizing work as being in various states and displaying columns for each state. We highly recommend that
+folks who intend to work on Octant familiarize themselves with ZenHub and GitHub issues.
+
+A very helpful browser extension that will create a ZenHub tab on GitHub for you.
+* [ZenHub Browser Extension](https://zenhub.com/extension)
+
+## CHANGELOG
 
 Authors are expected to include a changelog file with their pull requests. The changelog file
 should be a new file created in the `changelogs/unreleased` folder. The file should follow the
@@ -10,6 +37,21 @@ changelog.
     octant/changelogs/unreleased   <- folder
         000-username               <- file
 
+## Proposals
+
+If you wish to add a larger feature or make a major refactor to Octant we encourage folks to write up a proposal document.
+Though our process is not formal, the convention is to create a PR against Octant with your proposal as markdown in the
+proposals folder. Proposal reviews and feedback will happen on the the PR with the proposal.
+
+    octant/proposals       <- folder
+        YYYYMMDD-title.md  <- file
+
+In your proposal it is good to consider and include some of the following:
+* Goals
+* Non-Goals
+* Does this break any public APIs?
+* Does this fix/solve any outstanding issues?
+
 ## DCO Sign off
 
 All authors to the project retain copyright to their work. However, to ensure
@@ -66,4 +108,4 @@ By making a contribution to this project, I certify that:
     personal information I submit with it, including my sign-off) is
     maintained indefinitely and may be redistributed consistent with
     this project or the open source license(s) involved.
-```
\ No newline at end of file
+```
diff --git a/changelogs/unreleased/580-wwitzel3 b/changelogs/unreleased/580-wwitzel3
deleted file mode 100644
index e2798aae..00000000
--- a/changelogs/unreleased/580-wwitzel3
+++ /dev/null
@@ -1,1 +0,0 @@
-Fixed issue where plugins would not load on Windows.
diff --git a/changelogs/unreleased/631-bryanl b/changelogs/unreleased/631-bryanl
deleted file mode 100644
index 62e88659..00000000
--- a/changelogs/unreleased/631-bryanl
+++ /dev/null
@@ -1,2 +0,0 @@
-Remove extra actions from poller and websocket client
-
diff --git a/changelogs/unreleased/644-GuessWhoSamFoo b/changelogs/unreleased/644-GuessWhoSamFoo
deleted file mode 100644
index 95f7b359..00000000
--- a/changelogs/unreleased/644-GuessWhoSamFoo
+++ /dev/null
@@ -1,1 +0,0 @@
-Changed labels and selectors to use clarity labels
diff --git a/internal/api/content_manager.go b/internal/api/content_manager.go
index 4b99fc59..210dcd1f 100644
--- a/internal/api/content_manager.go
+++ b/internal/api/content_manager.go
@@ -31,18 +31,7 @@ type ContentManagerOption func(manager *ContentManager)
 
 // ContentGenerateFunc is a function that generates content. It returns `rerun=true`
 // if the action should be be immediately rerun.
-type ContentGenerateFunc func(ctx context.Context, state octant.State) (Content, bool, error)
-
-type Content struct {
-	Response component.ContentResponse
-	Path     string
-}
-
-var (
-	emptyContent = Content{
-		Response: component.EmptyContentResponse,
-	}
-)
+type ContentGenerateFunc func(ctx context.Context, state octant.State) (component.ContentResponse, bool, error)
 
 // WithContentGenerator configures the content generate function.
 func WithContentGenerator(fn ContentGenerateFunc) ContentManagerOption {
@@ -88,11 +77,7 @@ var _ StateManager = (*ContentManager)(nil)
 
 // Start starts the manager.
 func (cm *ContentManager) Start(ctx context.Context, state octant.State, s OctantClient) {
-	logger := log.From(ctx)
-	logger.Debugf("starting content manager")
-
 	defer func() {
-		logger.Debugf("stopping content manager")
 		close(cm.updateContentCh)
 	}()
 
@@ -111,7 +96,7 @@ func (cm *ContentManager) runUpdate(state octant.State, s OctantClient) PollerFu
 			return false
 		}
 
-		content, _, err := cm.contentGenerateFunc(ctx, state)
+		contentResponse, _, err := cm.contentGenerateFunc(ctx, state)
 		if err != nil {
 			var ae *oerrors.AccessError
 			if errors.As(err, &ae) {
@@ -127,17 +112,14 @@ func (cm *ContentManager) runUpdate(state octant.State, s OctantClient) PollerFu
 		}
 
 		if ctx.Err() == nil {
-			if content.Path == state.GetContentPath() {
-				s.Send(CreateContentEvent(content.Response, state.GetNamespace(), contentPath, state.GetQueryParams()))
-			}
-
+			s.Send(CreateContentEvent(contentResponse, state.GetNamespace(), contentPath, state.GetQueryParams()))
 		}
 
 		return false
 	}
 }
 
-func (cm *ContentManager) generateContent(ctx context.Context, state octant.State) (Content, bool, error) {
+func (cm *ContentManager) generateContent(ctx context.Context, state octant.State) (component.ContentResponse, bool, error) {
 	contentPath := state.GetContentPath()
 	logger := cm.logger.With("contentPath", contentPath)
 
@@ -148,7 +130,7 @@ 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)
+		return component.EmptyContentResponse, false, fmt.Errorf("unable to find module for content path %q", contentPath)
 	}
 	modulePath := strings.TrimPrefix(contentPath, m.Name())
 	options := module.ContentOptions{
@@ -159,17 +141,13 @@ func (cm *ContentManager) generateContent(ctx context.Context, state octant.Stat
 		if nfe, ok := err.(notFound); ok && nfe.NotFound() {
 			logger.Debugf("path not found, redirecting to parent")
 			state.SetContentPath(notFoundRedirectPath(contentPath))
-			return emptyContent, true, nil
+			return component.EmptyContentResponse, true, nil
 		} else {
-			return emptyContent, false, fmt.Errorf("generate content: %w", err)
+			return component.EmptyContentResponse, false, fmt.Errorf("generate content: %w", err)
 		}
 	}
 
-	content := Content{
-		Response: contentResponse,
-		Path:     contentPath,
-	}
-	return content, false, nil
+	return contentResponse, false, nil
 }
 
 // Handlers returns a slice of client request handlers.
@@ -222,8 +200,6 @@ func (cm *ContentManager) SetContentPath(state octant.State, payload action.Payl
 		return fmt.Errorf("extract query params from payload: %w", err)
 	}
 
-	cm.logger.With("content-path", contentPath).Debugf("setting content path")
-
 	state.SetContentPath(contentPath)
 	return nil
 }
diff --git a/internal/api/poller.go b/internal/api/poller.go
index d6145bc7..6d788f3c 100644
--- a/internal/api/poller.go
+++ b/internal/api/poller.go
@@ -7,7 +7,7 @@ package api
 
 import (
 	"context"
-	"fmt"
+	"sync"
 	"time"
 
 	"github.com/google/uuid"
@@ -24,8 +24,6 @@ type PollerFunc func(context.Context) bool
 
 // Poller is a poller. It runs an action.
 type Poller interface {
-	// Run runs `action` and delays `resetDuration` before starting again. If a message
-	// is sent to `ch`, it will cancel current work and restart `action`.
 	Run(ctx context.Context, ch <-chan struct{}, action PollerFunc, resetDuration time.Duration)
 }
 
@@ -40,7 +38,7 @@ func NewSingleRunPoller() *SingleRunPoller {
 }
 
 // Run runs the poller.
-func (a SingleRunPoller) Run(ctx context.Context, _ <-chan struct{}, action PollerFunc, resetDuration time.Duration) {
+func (a SingleRunPoller) Run(ctx context.Context, ch <-chan struct{}, action PollerFunc, resetDuration time.Duration) {
 	action(ctx)
 }
 
@@ -53,45 +51,129 @@ var _ Poller = (*InterruptiblePoller)(nil)
 
 // NewInterruptiblePoller creates an instance of InterruptiblePoller.
 func NewInterruptiblePoller(name string) *InterruptiblePoller {
-	return &InterruptiblePoller{
-		name: name,
-	}
+	return &InterruptiblePoller{name: name}
 }
 
 // Run runs the poller.
-func (a *InterruptiblePoller) Run(ctx context.Context, ch <-chan struct{}, action PollerFunc, resetDuration time.Duration) {
-	logger := log.From(ctx).With(
-		"poller-name", a.name,
-		"poller-instance", uuid.New().String())
-	logger.Debugf("starting poller")
-
-	timer := time.NewTimer(0)
-	done := false
-	for !done {
-		func() {
-			cur, cancel := context.WithCancel(log.WithLoggerContext(ctx, logger))
-			defer cancel()
-			canceled := false
+func (ip *InterruptiblePoller) Run(ctx context.Context, ch <-chan struct{}, action PollerFunc, resetDuration time.Duration) {
+	logger := log.From(ctx).With("poller-name", ip.name)
+	ctx = log.WithLoggerContext(ctx, logger)
+
+	jt := initJobTracker(ctx, action)
+	defer jt.clear()
+
+	pollerQueue := make(chan job, 10)
 
+	worker := func() {
+		for j := range pollerQueue {
 			select {
-			case <-ctx.Done():
-				logger.Debugf("poller has been canceled")
-				done = true
-			case <-ch:
-				canceled = true
-				logger.Debugf("poller was interrupted")
-			case <-timer.C:
-				logger.Debugf("poller is running action")
-				now := time.Now()
-				action(cur)
-				logger.With("elapsed", fmt.Sprintf("%s", time.Since(now))).
-					Debugf("poller ran action")
+			case <-j.ctx.Done():
+				// Job's context was canceled. Nothing else to do here.
+			case <-j.run():
+				if j.ctx.Err() == nil {
+					<-time.After(resetDuration)
+					pollerQueue <- jt.create()
+				}
 			}
+		}
+	}
 
-			if !canceled {
-				timer.Reset(resetDuration)
-			}
+	for i := 0; i < pollerWorkerCount; i++ {
+		go worker()
+	}
+
+	go func() {
+		for range ch {
+			// Cancel all existing jobs before creating a new job.
+			jt.clear()
+			pollerQueue <- jt.create()
+		}
+	}()
+
+	pollerQueue <- jt.create()
+
+	<-ctx.Done()
+}
+
+type job struct {
+	id         uuid.UUID
+	ctx        context.Context
+	cancelFunc context.CancelFunc
+	action     PollerFunc
+}
+
+func (j *job) run() <-chan bool {
+	ch := make(chan bool, 1)
+
+	done := make(chan bool, 1)
+
+	go func() {
+		j.action(j.ctx)
+		done <- true
+	}()
+
+	go func() {
+		select {
+		case <-j.ctx.Done():
+			ch <- true
+		case <-done:
+			ch <- true
+		}
+		defer close(ch)
+	}()
+
+	return ch
+}
+
+func createJob(ctx context.Context, action PollerFunc) job {
+	ctx, cancel := context.WithCancel(ctx)
+
+	return job{
+		id:         uuid.New(),
+		cancelFunc: cancel,
+		ctx:        ctx,
+		action:     action,
+	}
+}
+
+func (j *job) cancel() {
+	j.cancelFunc()
+}
+
+type jobTracker struct {
+	jobs   map[uuid.UUID]job
+	action PollerFunc
+	mu     sync.Mutex
+	ctx    context.Context
+	logger log.Logger
+}
+
+func initJobTracker(ctx context.Context, action PollerFunc) *jobTracker {
+	return &jobTracker{
+		ctx:    ctx,
+		action: action,
+		jobs:   make(map[uuid.UUID]job),
+		mu:     sync.Mutex{},
+		logger: log.From(ctx),
+	}
+}
+
+func (jt *jobTracker) create() job {
+	jt.mu.Lock()
+	defer jt.mu.Unlock()
+
+	j := createJob(jt.ctx, jt.action)
+	jt.jobs[j.id] = j
+
+	return j
+}
+
+func (jt *jobTracker) clear() {
+	jt.mu.Lock()
+	defer jt.mu.Unlock()
 
-		}()
+	for id, j := range jt.jobs {
+		j.cancel()
+		delete(jt.jobs, id)
 	}
 }
diff --git a/internal/api/websocket_client.go b/internal/api/websocket_client.go
index 4bdc3518..8f816891 100644
--- a/internal/api/websocket_client.go
+++ b/internal/api/websocket_client.go
@@ -57,7 +57,6 @@ var _ OctantClient = (*WebsocketClient)(nil)
 // NewWebsocketClient creates an instance of WebsocketClient.
 func NewWebsocketClient(ctx context.Context, conn *websocket.Conn, manager *WebsocketClientManager, dashConfig config.Dash, actionDispatcher ActionDispatcher, id uuid.UUID) *WebsocketClient {
 	logger := dashConfig.Logger().With("component", "websocket-client", "client-id", id.String())
-	ctx = log.WithLoggerContext(ctx, logger)
 
 	ctx, cancel := context.WithCancel(ctx)
 
@@ -85,8 +84,6 @@ func NewWebsocketClient(ctx context.Context, conn *websocket.Conn, manager *Webs
 		client.RegisterHandler(handler)
 	}
 
-	logger.Debugf("created websocket client")
-
 	return client
 }
 
@@ -98,7 +95,6 @@ func (c *WebsocketClient) ID() string {
 func (c *WebsocketClient) readPump() {
 	defer func() {
 		c.isOpen = false
-		c.logger.Debugf("closing read pump")
 	}()
 
 	go func() {
@@ -184,7 +180,6 @@ func (c *WebsocketClient) writePump() {
 	ticker := time.NewTicker(pingPeriod)
 	defer func() {
 		ticker.Stop()
-		c.logger.Debugf("closing write pump")
 	}()
 
 	done := false
diff --git a/pkg/plugin/loader.go b/pkg/plugin/loader.go
index a8c2546d..1a0de292 100644
--- a/pkg/plugin/loader.go
+++ b/pkg/plugin/loader.go
@@ -27,8 +27,6 @@ type Config interface {
 	Home() string
 	// Fs is the afero filesystem
 	Fs() afero.Fs
-	// OS is the opertating system
-	OS() string
 }
 
 type defaultConfig struct {
@@ -44,11 +42,6 @@ var (
 
 var _ Config = (*defaultConfig)(nil)
 
-// OS returns the plugin config operating system
-func (c *defaultConfig) OS() string {
-	return c.os
-}
-
 // PluginDirs returns the plugin directories. Current only works on macOS and Linux
 // and not in a container.
 func (c *defaultConfig) PluginDirs(home string) ([]string, error) {
@@ -113,6 +106,7 @@ func AvailablePlugins(config Config) ([]string, error) {
 	}
 
 	var list []string
+
 	for _, dir := range dirs {
 		_, err = config.Fs().Stat(dir)
 		if err != nil {
@@ -130,8 +124,8 @@ func AvailablePlugins(config Config) ([]string, error) {
 
 		for _, fi := range fis {
 			mode := fi.Mode()
-			// Windows does not have unix style executable bits.
-			if mode|64 == mode || config.OS() == "windows" {
+
+			if mode|64 == mode {
 				pluginPath := filepath.Join(dir, fi.Name())
 				list = append(list, pluginPath)
 			}
diff --git a/pkg/view/component/button.go b/pkg/view/component/button.go
index 133a579b..516fd408 100644
--- a/pkg/view/component/button.go
+++ b/pkg/view/component/button.go
@@ -2,6 +2,7 @@ package component
 
 import (
 	"encoding/json"
+
 	"github.com/vmware-tanzu/octant/pkg/action"
 )
 
@@ -17,10 +18,12 @@ type ButtonOption func(button *Button)
 // WithButtonConfirmation configured a button with a confirmation.
 func WithButtonConfirmation(title, body string) ButtonOption {
 	return func(button *Button) {
-		button.Confirmation = &Confirmation{
+		confirmation := Confirmation{
 			Title: title,
 			Body:  body,
 		}
+
+		button.Confirmation = &confirmation
 	}
 }
 
diff --git a/pkg/view/component/flexlayout.go b/pkg/view/component/flexlayout.go
index 36a8d59e..25921fa5 100644
--- a/pkg/view/component/flexlayout.go
+++ b/pkg/view/component/flexlayout.go
@@ -5,10 +5,7 @@ SPDX-License-Identifier: Apache-2.0
 
 package component
 
-import (
-	"encoding/json"
-	"errors"
-)
+import "encoding/json"
 
 const (
 	// WidthFull is a full width section.
@@ -58,34 +55,6 @@ type FlexLayoutConfig struct {
 	ButtonGroup *ButtonGroup        `json:"buttonGroup,omitempty"`
 }
 
-func (f *FlexLayoutConfig) UnmarshalJSON(data []byte) error {
-	x := struct {
-		Sections    []FlexLayoutSection `json:"sections,omitempty"`
-		ButtonGroup *TypedObject        `json:"buttonGroup,omitempty"`
-	}{}
-
-	if err := json.Unmarshal(data, &x); err != nil {
-		return err
-	}
-
-	if x.ButtonGroup != nil {
-		component, err := x.ButtonGroup.ToComponent()
-		if err != nil {
-			return err
-		}
-
-		buttonGroup, ok := component.(*ButtonGroup)
-		if !ok {
-			return errors.New("item was not a buttonGroup")
-		}
-		f.ButtonGroup = buttonGroup
-	}
-
-	f.Sections = x.Sections
-
-	return nil
-}
-
 // FlexLayout is a flex layout view.
 type FlexLayout struct {
 	base
diff --git a/pkg/view/component/unmarshal.go b/pkg/view/component/unmarshal.go
index 4c19dfce..dfea1f08 100644
--- a/pkg/view/component/unmarshal.go
+++ b/pkg/view/component/unmarshal.go
@@ -7,6 +7,7 @@ package component
 
 import (
 	"encoding/json"
+
 	"github.com/pkg/errors"
 )
 
@@ -20,11 +21,6 @@ func unmarshal(to TypedObject) (Component, error) {
 		err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
 			"unmarshal annotations config")
 		o = t
-	case typeButtonGroup:
-		t := &ButtonGroup{base: base{Metadata: to.Metadata}}
-		err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
-			"unmarshal buttonGroup config")
-		o = t
 	case typeCard:
 		t := &Card{base: base{Metadata: to.Metadata}}
 		err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
diff --git a/web/src/app/app-routing.module.ts b/web/src/app/app-routing.module.ts
index 7b0d6af5..9ac9df63 100644
--- a/web/src/app/app-routing.module.ts
+++ b/web/src/app/app-routing.module.ts
@@ -5,14 +5,20 @@
 import { NgModule } from '@angular/core';
 import { RouterModule, Routes } from '@angular/router';
 
+const denaliModule = () =>
+  import('./modules/denali/denali.module').then(m => m.DenaliModule);
+
+const sugarloafModule = () =>
+  import('./modules/sugarloaf/sugarloaf.module').then(m => m.SugarloafModule);
+
 export const appRoutes: Routes = [
   {
     path: 'denali',
-    loadChildren: './modules/denali/denali.module#DenaliModule',
+    loadChildren: denaliModule,
   },
   {
     path: '**',
-    loadChildren: './modules/sugarloaf/sugarloaf.module#SugarloafModule',
+    loadChildren: sugarloafModule,
   },
 ];
 
diff --git a/web/src/app/modules/shared/components/presentation/content-filter/content-filter.component.ts b/web/src/app/modules/shared/components/presentation/content-filter/content-filter.component.ts
index ed7e46c0..a89e1225 100644
--- a/web/src/app/modules/shared/components/presentation/content-filter/content-filter.component.ts
+++ b/web/src/app/modules/shared/components/presentation/content-filter/content-filter.component.ts
@@ -43,7 +43,7 @@ export class ContentFilterComponent
       return false;
     }
 
-    const view = row[this.column] as TextView;
+    const view = row.Phase as TextView;
     return selected.includes(view.config.value);
   }
 
diff --git a/web/src/app/modules/shared/components/presentation/label-selector/label-selector.component.html b/web/src/app/modules/shared/components/presentation/label-selector/label-selector.component.html
index e7bfc66e..9802b4d1 100644
--- a/web/src/app/modules/shared/components/presentation/label-selector/label-selector.component.html
+++ b/web/src/app/modules/shared/components/presentation/label-selector/label-selector.component.html
@@ -1,1 +1,1 @@
-<span class="label label-orange">{{ key }}:{{ value }}</span>
+<div class="selector--label">{{ key }}:{{ value }}</div>
diff --git a/web/src/app/modules/shared/components/presentation/label-selector/label-selector.component.scss b/web/src/app/modules/shared/components/presentation/label-selector/label-selector.component.scss
index 916ffc68..3543160d 100644
--- a/web/src/app/modules/shared/components/presentation/label-selector/label-selector.component.scss
+++ b/web/src/app/modules/shared/components/presentation/label-selector/label-selector.component.scss
@@ -1,4 +1,9 @@
 /* Copyright (c) 2019 the Octant contributors. All Rights Reserved.
  * SPDX-License-Identifier: Apache-2.0
  */
- 
\ No newline at end of file
+
+@import '../../../../../../styles';
+
+.selector--label {
+  @include pill(var(--selector-color));
+}
diff --git a/web/src/app/modules/shared/components/presentation/labels/labels.component.html b/web/src/app/modules/shared/components/presentation/labels/labels.component.html
index 5710f815..1d8e81b6 100644
--- a/web/src/app/modules/shared/components/presentation/labels/labels.component.html
+++ b/web/src/app/modules/shared/components/presentation/labels/labels.component.html
@@ -2,17 +2,17 @@
   <div class="label-with-title clr-col-md-12">
     <h3>{{ title }}</h3>
     <div class="view-labels">
-      <span *ngFor="let key of labelKeys; trackBy: trackByIdentity" class="label label-light-blue clickable" (click)="click(key, labels[key])">
+      <div *ngFor="let key of labelKeys; trackBy: trackByIdentity" class="view-label" (click)="click(key, labels[key])">
         {{ key }}:{{ labels[key] }}
-      </span>
+      </div>
     </div>
   </div>
 </ng-template>
 <ng-template #noTitle>
   <div class="view-labels">
-    <span *ngFor="let key of labelKeys; trackBy: trackByIdentity" class="label label-light-blue clickable" (click)="click(key, labels[key])">
+    <div *ngFor="let key of labelKeys; trackBy: trackByIdentity" class="view-label" (click)="click(key, labels[key])">
       {{ key }}:{{ labels[key] }}
-    </span>
+    </div>
   </div>
 </ng-template>
 
diff --git a/web/src/app/modules/shared/components/presentation/labels/labels.component.scss b/web/src/app/modules/shared/components/presentation/labels/labels.component.scss
index 66d1b46e..042246b6 100644
--- a/web/src/app/modules/shared/components/presentation/labels/labels.component.scss
+++ b/web/src/app/modules/shared/components/presentation/labels/labels.component.scss
@@ -8,10 +8,11 @@
   display: flex;
   flex-direction: row;
   flex-wrap: wrap;
-  align-items: center;
+  cursor: pointer;
 
-  & > span {
-    cursor: pointer;
+  .view-label {
+    position: relative;
+    @include pill(var(--label-color));
   }
 }
 
diff --git a/web/src/app/modules/shared/services/websocket/websocket.service.ts b/web/src/app/modules/shared/services/websocket/websocket.service.ts
index f7976092..ca40aad7 100644
--- a/web/src/app/modules/shared/services/websocket/websocket.service.ts
+++ b/web/src/app/modules/shared/services/websocket/websocket.service.ts
@@ -11,7 +11,7 @@ import {
   NotifierSignalType,
 } from '../../notifier/notifier.service';
 import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
-import { delay, retryWhen, take, tap } from 'rxjs/operators';
+import { delay, retryWhen, tap } from 'rxjs/operators';
 
 interface WebsocketPayload {
   type: string;
@@ -38,13 +38,13 @@ interface Alert {
   providedIn: 'root',
 })
 export class WebsocketService implements BackendService {
+  ws: WebSocket;
   handlers: { [key: string]: ({}) => void } = {};
   reconnected = new Subject<Event>();
 
   private notifierSession: NotifierSession;
   private subject: WebSocketSubject<unknown>;
   private connectSignalID = new BehaviorSubject<string>('');
-  private opened = false;
 
   constructor(notifierService: NotifierService) {
     this.notifierSession = notifierService.createSession();
@@ -76,9 +76,6 @@ export class WebsocketService implements BackendService {
   }
 
   open() {
-    if (this.opened) {
-      return;
-    }
     this.createWebSocket()
       .pipe(
         retryWhen(errors =>
@@ -96,28 +93,22 @@ export class WebsocketService implements BackendService {
       )
       .subscribe(
         data => {
-          this.connectSignalID.pipe(take(1)).subscribe(id => {
-            if (id !== '') {
-              this.notifierSession.removeAllSignals();
-              this.connectSignalID.next('');
-            }
-          });
+          this.connectSignalID
+            .subscribe(id => {
+              if (id !== '') {
+                this.notifierSession.removeAllSignals();
+                this.connectSignalID.next('');
+              }
+            })
+            .unsubscribe();
 
           this.parseWebsocketMessage(data);
         },
-        err => console.error(err),
-        () => {
-          console.log('web socket is closing');
-        }
+        err => console.error(err)
       );
-
-    this.opened = true;
   }
 
   close() {
-    if (!this.opened) {
-      this.opened = false;
-    }
     this.subject.unsubscribe();
   }
 
diff --git a/web/src/app/modules/sugarloaf/components/smart/container/container.component.ts b/web/src/app/modules/sugarloaf/components/smart/container/container.component.ts
index ebb078e8..c2f118f3 100644
--- a/web/src/app/modules/sugarloaf/components/smart/container/container.component.ts
+++ b/web/src/app/modules/sugarloaf/components/smart/container/container.component.ts
@@ -55,5 +55,11 @@ export class ContainerComponent implements OnInit, OnDestroy {
     this.websocketService.open();
   }
 
-  ngOnDestroy(): void {}
+  closeSocket() {
+    this.websocketService.close();
+  }
+
+  ngOnDestroy(): void {
+    this.closeSocket();
+  }
 }
diff --git a/web/src/app/modules/sugarloaf/components/smart/input-filter/input-filter.component.html b/web/src/app/modules/sugarloaf/components/smart/input-filter/input-filter.component.html
index 4135ef08..c7413317 100644
--- a/web/src/app/modules/sugarloaf/components/smart/input-filter/input-filter.component.html
+++ b/web/src/app/modules/sugarloaf/components/smart/input-filter/input-filter.component.html
@@ -17,8 +17,10 @@
       </div>
       <ng-container *ngIf="filters?.length > 0">
         <div class="input-filter-tags-list">
-          <div class="label label-light-blue" *ngFor="let filter of filters; trackBy: identifyFilter">
-            {{ filter.key }}:{{ filter.value }}
+          <div class="input-filter-tag" *ngFor="let filter of filters; trackBy: identifyFilter">
+            <div class="input-filter-tag-text">
+              {{ filter.key }}:{{ filter.value }}
+            </div>
             <clr-icon class="input-filter-tag-remove" shape="times" (click)="remove(filter)"></clr-icon>
           </div>
         </div>
diff --git a/web/src/app/modules/sugarloaf/components/smart/input-filter/input-filter.component.scss b/web/src/app/modules/sugarloaf/components/smart/input-filter/input-filter.component.scss
index 34f53e93..9ac243f6 100644
--- a/web/src/app/modules/sugarloaf/components/smart/input-filter/input-filter.component.scss
+++ b/web/src/app/modules/sugarloaf/components/smart/input-filter/input-filter.component.scss
@@ -64,10 +64,25 @@
       flex-direction: row;
       flex-wrap: wrap;
 
-      .input-filter-tag-remove {
-        cursor: pointer;
-        height: 23px;
-        width: 12px;
+      .input-filter-tag {
+        @include pill(var(--label-color));
+        text-align: center;
+        margin: 0 4px 4px 0;
+        padding: 0 8px;
+        max-width: 100%;
+        display: flex;
+
+        &-text {
+          flex: 1;
+          overflow-y: auto;
+          margin-right: 4px;
+        }
+
+        &-remove {
+          cursor: pointer;
+          height: 23px;
+          width: 12px;
+        }
       }
     }
 
diff --git a/web/src/app/modules/sugarloaf/components/smart/input-filter/input-filter.component.spec.ts b/web/src/app/modules/sugarloaf/components/smart/input-filter/input-filter.component.spec.ts
index d4c7d0e8..745f968c 100644
--- a/web/src/app/modules/sugarloaf/components/smart/input-filter/input-filter.component.spec.ts
+++ b/web/src/app/modules/sugarloaf/components/smart/input-filter/input-filter.component.spec.ts
@@ -84,7 +84,7 @@ describe('InputFilterComponent', () => {
     component.showTagList = true;
     fixture.detectChanges();
     const tagDebugElements: DebugElement[] = fixture.debugElement.queryAll(
-      By.css('.label-light-blue')
+      By.css('.input-filter-tag')
     );
     expect(tagDebugElements.length).toBe(3);
     expect(tagDebugElements[0].nativeElement.textContent).toMatch(


Test patch:
diff --git a/internal/api/content_manager_test.go b/internal/api/content_manager_test.go
index 10b90d28..81e029e1 100644
--- a/internal/api/content_manager_test.go
+++ b/internal/api/content_manager_test.go
@@ -46,7 +46,9 @@ func TestContentManager_GenerateContent(t *testing.T) {
 	moduleManager := moduleFake.NewMockManagerInterface(controller)
 	state := octantFake.NewMockState(controller)
 
-	state.EXPECT().GetContentPath().Return("/path").AnyTimes()
+	state.EXPECT().GetContentPath().Return("/path")
+	state.EXPECT().GetNamespace().Return("default")
+	state.EXPECT().GetQueryParams().Return(params)
 	state.EXPECT().OnContentPathUpdate(gomock.Any()).DoAndReturn(func(fn octant.ContentPathUpdateFunc) octant.UpdateCancelFunc {
 		fn("foo")
 		return func() {}
@@ -63,8 +65,8 @@ func TestContentManager_GenerateContent(t *testing.T) {
 
 	poller := api.NewSingleRunPoller()
 
-	contentGenerator := func(ctx context.Context, state octant.State) (api.Content, bool, error) {
-		return api.Content{Response: contentResponse}, false, nil
+	contentGenerator := func(ctx context.Context, state octant.State) (component.ContentResponse, bool, error) {
+		return contentResponse, false, nil
 	}
 	manager := api.NewContentManager(moduleManager, logger,
 		api.WithContentGenerator(contentGenerator),
diff --git a/pkg/plugin/loader_test.go b/pkg/plugin/loader_test.go
index dcc4bc92..e245453d 100644
--- a/pkg/plugin/loader_test.go
+++ b/pkg/plugin/loader_test.go
@@ -86,10 +86,6 @@ func Test_AvailablePlugins(t *testing.T) {
 			homePath: filepath.Join("/home", "xdg_config_path"),
 			key:      "xdg-config-home",
 		},
-		{
-			homePath: filepath.Join("/windows", "test"),
-			key:      "windows-test",
-		},
 	}
 
 	for _, test := range tests {
@@ -108,30 +104,6 @@ func Test_AvailablePlugins(t *testing.T) {
 		}
 
 		switch test.key {
-		case "windows-test":
-			c.os = "windows"
-			configPath := filepath.Join(test.homePath, configDir, "plugins")
-			err := fs.MkdirAll(configPath, 0700)
-			require.NoError(t, err, "unable to create test home directory")
-
-			stagePlugin := func(t *testing.T, path string, name string, mode os.FileMode) {
-				p := filepath.Join(path, name)
-				err = afero.WriteFile(fs, p, []byte("guts"), mode)
-				require.NoError(t, err)
-			}
-
-			// Non-executable UNIX permissions but OS is Windows
-			stagePlugin(t, configPath, "a-plugin", 0600)
-			stagePlugin(t, configPath, "e-plugin", 0755)
-
-			got, err := AvailablePlugins(c)
-			require.NoError(t, err)
-
-			expected := []string{
-				filepath.Join(configPath, "a-plugin"),
-				filepath.Join(configPath, "e-plugin"),
-			}
-			assert.Equal(t, expected, got)
 		case "plugin-path":
 			c.os = "unix"
 			customPath := filepath.Join("/example", "test")
diff --git a/pkg/view/component/testdata/config_flexlayout.json b/pkg/view/component/testdata/config_flexlayout.json
index ff87f1d0..59b43314 100644
--- a/pkg/view/component/testdata/config_flexlayout.json
+++ b/pkg/view/component/testdata/config_flexlayout.json
@@ -1,27 +1,17 @@
 {
-  "sections": [
+  "sections":[
     [
       {
-        "width": 24,
-        "view": {
-          "metadata": {
-            "type": "text"
+        "width":24,
+        "view":{
+          "metadata":{
+            "type":"text"
           },
-          "config": {
-            "value": "text"
+          "config":{
+            "value":"text"
           }
         }
       }
     ]
-  ],
-  "buttonGroup": {
-    "metadata": {
-      "type": "buttonGroup"
-    },
-    "config": {
-      "buttons": [{
-        "name": "test"
-      }]
-    }
-  }
+  ]
 }
diff --git a/pkg/view/component/unmarshal_test.go b/pkg/view/component/unmarshal_test.go
index 3f0f0469..21047e6c 100644
--- a/pkg/view/component/unmarshal_test.go
+++ b/pkg/view/component/unmarshal_test.go
@@ -94,14 +94,6 @@ func Test_unmarshal(t *testing.T) {
 							},
 						},
 					},
-					ButtonGroup: &ButtonGroup{
-						base: base{},
-						Config: ButtonGroupConfig{
-							Buttons: []Button{{
-								Name: "test",
-							}},
-						},
-					},
 				},
 				base: newBase(typeFlexLayout, nil),
 			},