Skip to content

Commit

Permalink
Show octant route history in the header
Browse files Browse the repository at this point in the history
- Show descriptive page title
- Update octant content response to return empty namespace for non-namespaced resources
[vmware-archive#2472, vmware-archive#2640]

Signed-off-by: Vikram Yadav <yvikram@vmware.com>
  • Loading branch information
Vikram Yadav committed Jul 15, 2021
1 parent a723e6e commit c22c6c2
Show file tree
Hide file tree
Showing 18 changed files with 556 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
8 changes: 8 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,11 @@ func CreateNavigationBreadcrumb(navs []navigation.Navigation, contentPath string
var last LinkDefinition
var title []component.TitleComponent

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).AnyTimes()
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).AnyTimes()
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
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,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
14 changes: 13 additions & 1 deletion 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 All @@ -16,10 +22,16 @@ export interface Content {

export interface Metadata {
type: string;
title?: View[];
title?: View[] | ValueConfig[];
accessor?: string;
}

export interface ValueConfig extends View {
config?: {
value: string;
};
}

export interface View {
metadata: Metadata;
totalItems?: number;
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 Expand Up @@ -71,6 +74,68 @@ describe('ContentService', () => {
})
);
});

it('updates the title with namespace', () => {
const router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
router.navigate.and.callFake(() => {
return new Promise(resolve => resolve(true));
});
const backendService = TestBed.inject(WebsocketService);
let newUpdate = Object.assign(
update,
{ contentPath: '/newPath' },
{
content: {
viewComponents: [],
title: [{ config: { value: 'foo-title' } }],
},
}
);
backendService.triggerHandler(ContentUpdateMessage, newUpdate);

const titleServiceSpy = TestBed.inject(Title) as jasmine.SpyObj<Title>;
expect(titleServiceSpy.setTitle).toHaveBeenCalledOnceWith(
'Octant | foo-title | default'
);
service.title.subscribe(title => {
expect(title).toEqual({
namespace: 'default',
title: 'foo-title',
path: '/newPath',
});
});
});

it('updates the title without namespace', () => {
const router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
router.navigate.and.callFake(() => {
return new Promise(resolve => resolve(true));
});
const backendService = TestBed.inject(WebsocketService);
let newUpdate = Object.assign(
update,
{ contentPath: '/new-path', namespace: '' },
{
content: {
viewComponents: [],
title: [{ config: { value: 'foo-title' } }],
},
}
);
backendService.triggerHandler(ContentUpdateMessage, newUpdate);

const titleServiceSpy = TestBed.inject(Title) as jasmine.SpyObj<Title>;
expect(titleServiceSpy.setTitle).toHaveBeenCalledOnceWith(
'Octant | foo-title'
);
service.title.subscribe(title => {
expect(title).toEqual({
namespace: '',
title: 'foo-title',
path: '/new-path',
});
});
});
});

describe('label filters updated', () => {
Expand Down
Loading

0 comments on commit c22c6c2

Please sign in to comment.