Skip to content
This repository has been archived by the owner on Jan 19, 2023. It is now read-only.

Commit

Permalink
Merge pull request #2580 from xtreme-vikram-yadav/route-history
Browse files Browse the repository at this point in the history
Show octant route history in the header
  • Loading branch information
Sam Foo committed Jul 29, 2021
2 parents 83f18fc + acc9227 commit f6ef1a0
Show file tree
Hide file tree
Showing 21 changed files with 470 additions and 22 deletions.
1 change: 1 addition & 0 deletions changelogs/unreleased/2580-xtreme-vikram-yadav
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added Octant route history dropdown and updated page title
9 changes: 9 additions & 0 deletions internal/api/breadcrumb.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ func GenerateBreadcrumb(cm *ContentManager, contentPath string, state octant.Sta
parent, title := CreateNavigationBreadcrumb(navs, contentPath)
if title == nil {
return title
} else if parent.Title == "" {
title = append(title, component.NewText(path.Base(contentPath)))
return title
}

if strings.Contains(contentPath, crPath) {
Expand Down Expand Up @@ -76,6 +79,12 @@ func CreateNavigationBreadcrumb(navs []navigation.Navigation, contentPath string
var last LinkDefinition
var title []component.TitleComponent

// When there is a single non-nested navigation entry, then show it as a link.
if len(navs) == 1 && contentPath != navs[0].Path && strings.HasPrefix(contentPath, navs[0].Path) {
title = append(title, component.NewLink("", path.Base(navs[0].Title), path_util.PrefixedPath(navs[0].Path)))
return LinkDefinition{}, title
}

thisPath := contentPath
for {
if thisPath == "." { // done
Expand Down
47 changes: 38 additions & 9 deletions internal/api/breadcrumb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import (
"path/filepath"
"testing"

"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"

"github.com/vmware-tanzu/octant/internal/api"
"github.com/vmware-tanzu/octant/internal/util/json"
"github.com/vmware-tanzu/octant/pkg/navigation"
"github.com/vmware-tanzu/octant/pkg/view/component"
)

func Test_NavigationFromPathNamespace(t *testing.T) {
Expand Down Expand Up @@ -94,8 +94,6 @@ func Test_NavigationFromPathNamespace(t *testing.T) {
expectedItems: 0,
},
}
controller := gomock.NewController(t)
defer controller.Finish()
data, err := ioutil.ReadFile(filepath.Join("testdata", "namespace_navigation.json"))
require.NoError(t, err)
var namespaceNavigation []navigation.Navigation
Expand Down Expand Up @@ -192,8 +190,6 @@ func Test_NavigationFromPathCluster(t *testing.T) {
expectedItems: 0,
},
}
controller := gomock.NewController(t)
defer controller.Finish()
data, err := ioutil.ReadFile(filepath.Join("testdata", "cluster_navigation.json"))
require.NoError(t, err)
var namespaceNavigation []navigation.Navigation
Expand Down Expand Up @@ -287,8 +283,6 @@ func Test_CreateNavigationBreadcrumbNamespace(t *testing.T) {
expectedItems: 0,
},
}
controller := gomock.NewController(t)
defer controller.Finish()
data, err := ioutil.ReadFile(filepath.Join("testdata", "namespace_navigation.json"))
require.NoError(t, err)
var namespaceNavigation []navigation.Navigation
Expand Down Expand Up @@ -377,8 +371,6 @@ func Test_CreateNavigationBreadcrumbCluster(t *testing.T) {
expectedItems: 0,
},
}
controller := gomock.NewController(t)
defer controller.Finish()
data, err := ioutil.ReadFile(filepath.Join("testdata", "cluster_navigation.json"))
require.NoError(t, err)
var namespaceNavigation []navigation.Navigation
Expand All @@ -399,3 +391,40 @@ func Test_CreateNavigationBreadcrumbCluster(t *testing.T) {
})
}
}

func Test_CreateNavigationBreadcrumbApplications(t *testing.T) {
data, err := ioutil.ReadFile(filepath.Join("testdata", "application_navigation.json"))
require.NoError(t, err)
var namespaceNavigation []navigation.Navigation

err = json.Unmarshal([]byte(data), &namespaceNavigation)
require.NoError(t, err)
require.Equal(t, 1, len(namespaceNavigation))

tests := []struct {
name string
path string
lastTitle string
lastUrl string
expectedTitle component.TitleComponent
expectedItems int
}{
{
name: "Applications Detail Breadcumb",
path: "workloads/namespace/milan/detail/simple-app",
expectedTitle: component.NewLink("", "Applications", "/workloads/namespace/milan"),
expectedItems: 1,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
last, title := api.CreateNavigationBreadcrumb(namespaceNavigation, test.path)
require.Equal(t, test.expectedItems, len(title))
require.Equal(t, test.expectedTitle, title[0])
require.NotNil(t, title)
require.Equal(t, "", last.Title)
require.Equal(t, "", last.Url)
})
}
}
11 changes: 10 additions & 1 deletion internal/api/content_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ func (cm *ContentManager) runUpdate(state octant.State, s api.OctantClient) Poll

if ctx.Err() == nil {
if content.Path == state.GetContentPath() {
s.Send(CreateContentEvent(content.Response, state.GetNamespace(), contentPath, state.GetQueryParams()))
s.Send(CreateContentEvent(content.Response, getNamespace(state, contentPath, cm), contentPath, state.GetQueryParams()))
}

}
Expand Down Expand Up @@ -346,3 +346,12 @@ func moduloIndex(key string, options [][]string) []string {
i := int(h.Sum32()) % len(options)
return options[i]
}

func getNamespace(state octant.State, contentPath string, cm *ContentManager) string {
m, ok := cm.moduleManager.ModuleForContentPath(contentPath)
if ok && m.Name() == "cluster-overview" {
return ""
}

return state.GetNamespace()
}
49 changes: 48 additions & 1 deletion internal/api/content_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestContentManager_GenerateContent(t *testing.T) {
octantClient.EXPECT().Send(contentEvent).AnyTimes()
octantClient.EXPECT().StopCh().Return(stopCh).AnyTimes()

moduleManager.EXPECT().ModuleForContentPath(gomock.Any()).Return(fakeModule, true)
moduleManager.EXPECT().ModuleForContentPath(gomock.Any()).Return(fakeModule, true).Times(2)
moduleManager.EXPECT().Navigation(gomock.Any(), "foo-namespace", "foo-module").Return([]navigation.Navigation{}, nil)
fakeModule.EXPECT().Name().Return("foo-module").AnyTimes()
fakeModule.EXPECT().Content(gomock.Any(), ".", gomock.Any()).
Expand All @@ -101,6 +101,53 @@ func TestContentManager_GenerateContent(t *testing.T) {
manager.Start(ctx, state, octantClient)
}

func TestContentManager_GenerateContent_ClusterOverviewNamespace(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()

params := map[string][]string{}
filters := []octant.Filter{{Key: "foo", Value: "bar"}}

dashConfig := configFake.NewMockDash(controller)
moduleManager := moduleFake.NewMockManagerInterface(controller)
fakeModule := moduleFake.NewMockModule(controller)
state := octantFake.NewMockState(controller)

dashConfig.EXPECT().CurrentContext().Return("foo-context")
state.EXPECT().GetClientID().Return("foo-client")
state.EXPECT().GetFilters().Return(filters).AnyTimes()
state.EXPECT().GetNamespace().Return("foo-namespace").AnyTimes()
state.EXPECT().GetQueryParams().Return(params).AnyTimes()
state.EXPECT().GetContentPath().Return(".").AnyTimes()
state.EXPECT().OnContentPathUpdate(gomock.Any()).DoAndReturn(func(fn octant.ContentPathUpdateFunc) octant.UpdateCancelFunc {
fn("foo")
return func() {}
})
octantClient := fake.NewMockOctantClient(controller)

stopCh := make(chan struct{}, 1)

contentResponse := component.ContentResponse{}
contentEvent := api.CreateContentEvent(contentResponse, "", ".", params)
octantClient.EXPECT().Send(contentEvent).AnyTimes()
octantClient.EXPECT().StopCh().Return(stopCh).AnyTimes()

moduleManager.EXPECT().ModuleForContentPath(gomock.Any()).Return(fakeModule, true).Times(2)
moduleManager.EXPECT().Navigation(gomock.Any(), "foo-namespace", "cluster-overview").Return([]navigation.Navigation{}, nil)
fakeModule.EXPECT().Name().Return("cluster-overview").AnyTimes()
fakeModule.EXPECT().Content(gomock.Any(), ".", gomock.Any()).Return(contentResponse, nil)

logger := log.NopLogger()

poller := api.NewSingleRunPoller()

manager := api.NewContentManager(moduleManager, dashConfig, logger,
api.WithContentGeneratorPoller(poller))

ctx := context.Background()
manager.Start(ctx, state, octantClient)
}

func TestContentManager_SetContentPath(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
Expand Down
9 changes: 9 additions & 0 deletions internal/api/testdata/application_navigation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"title": "Applications",
"path": "workloads/namespace/milan",
"children": [],
"iconName": "application",
"isLoading": false
}
]
3 changes: 1 addition & 2 deletions internal/modules/workloads/detail_describer.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,8 @@ func (d *DetailDescriber) Describe(ctx context.Context, namespace string, option
}

workloadName := fmt.Sprintf(`
### %s
_%s_
`, cur.Name, cur.Owner.GroupVersionKind())
`, cur.Owner.GroupVersionKind())

headerSection := component.FlexLayoutSection{
{
Expand Down
8 changes: 8 additions & 0 deletions pkg/view/component/dropdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type DropdownConfig struct {
Action string `json:"action,omitempty"`
Selection string `json:"selection,omitempty"`
UseSelection bool `json:"useSelection"`
ShowToggleIcon bool `json:"showToggleIcon"`
Items []DropdownItemConfig `json:"items"`
}

Expand Down Expand Up @@ -114,6 +115,11 @@ func (t *Dropdown) SetDropdownUseSelection(sel bool) {
t.Config.UseSelection = sel
}

// SetDropdownShowToggleIcon defines if dropdown toggle caret icon is shown.
func (t *Dropdown) SetDropdownShowToggleIcon(sti bool) {
t.Config.ShowToggleIcon = sti
}

// SupportsTitle designates this is a TextComponent.
func (t *Dropdown) SupportsTitle() {}

Expand All @@ -138,6 +144,7 @@ func (t *DropdownConfig) UnmarshalJSON(data []byte) error {
Action string `json:"action,omitempty"`
Selection string `json:"selection,omitempty"`
UseSelection bool `json:"useSelection,omitempty"`
ShowToggleIcon bool `json:"showToggleIcon,omitempty"`
Items []DropdownItemConfig `json:"items"`
}{}

Expand All @@ -150,6 +157,7 @@ func (t *DropdownConfig) UnmarshalJSON(data []byte) error {
t.Action = x.Action
t.Selection = x.Selection
t.UseSelection = x.UseSelection
t.ShowToggleIcon = x.ShowToggleIcon
t.Items = x.Items
return nil
}
1 change: 1 addition & 0 deletions pkg/view/component/testdata/dropdown.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"type": "button",
"action": "action.octant.dev/dropdownTest",
"useSelection": false,
"showToggleIcon": false,
"items": [
{
"name": "first",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</div>
<div *ngIf="type == 'icon'" class="source" clrDropdownTrigger>
<clr-icon [attr.shape]="title"></clr-icon>
<clr-icon shape="caret down"></clr-icon>
<clr-icon *ngIf="showToggleIcon" shape="caret down"></clr-icon>
</div>
<div *ngIf="type == 'label'" class="source" clrDropdownTrigger>
{{title}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('DropdownComponent', () => {
type: 'button',
action: 'action',
useSelection: false,
showToggleIcon: false,
items: [
{
name: 'item-header',
Expand Down Expand Up @@ -119,6 +120,7 @@ describe('DropdownComponent', () => {
type: 'button',
action: 'action',
useSelection: false,
showToggleIcon: false,
items: [
{
name: 'first',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { WebsocketService } from '../../../../../data/services/websocket/websock
export class DropdownComponent extends AbstractViewComponent<DropdownView> {
readonly defaultItemLimit = 10;
useSelection = false;
showToggleIcon = true;
selectedItem = '';
url: string;
position: string;
Expand Down Expand Up @@ -58,6 +59,7 @@ export class DropdownComponent extends AbstractViewComponent<DropdownView> {
this.type = view.config.type;
this.action = view.config.action;
this.items = view.config.items;
this.showToggleIcon = view.config.showToggleIcon;

this.items.forEach(item => {
if (item.name === view.config.selection) {
Expand Down Expand Up @@ -100,7 +102,7 @@ export class DropdownComponent extends AbstractViewComponent<DropdownView> {
});
}

if (item.url && this.type === 'link') {
if (item.url && item.type === 'link') {
setTimeout(() => {
this.router.navigateByUrl(item.url);
}, 0);
Expand Down
7 changes: 7 additions & 0 deletions web/src/app/modules/shared/models/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export interface ContentResponse {
currentPath: string;
}

export interface NamespacedTitle {
namespace: string;
title: string;
path: string;
}

export interface Content {
extensionComponent: ExtensionView;
viewComponents: View[];
Expand Down Expand Up @@ -106,6 +112,7 @@ export interface DropdownView extends View {
action: string;
selection?: string;
useSelection: boolean;
showToggleIcon: boolean;
items: DropdownItem[];
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@ import {
Filter,
LabelFilterService,
} from '../label-filter/label-filter.service';
import { Title } from '@angular/platform-browser';

describe('ContentService', () => {
let service: ContentService;
const mockRouter = {
navigate: jasmine.createSpy('navigate'),
};
const mockRouter = jasmine.createSpyObj('Router', ['navigate']);

beforeEach(() => {
TestBed.configureTestingModule({
Expand All @@ -40,6 +39,10 @@ describe('ContentService', () => {
provide: Router,
useValue: mockRouter,
},
{
provide: Title,
useValue: jasmine.createSpyObj('Title', ['getTitle', 'setTitle']),
},
],
});

Expand Down
Loading

0 comments on commit f6ef1a0

Please sign in to comment.