Skip to content

Commit

Permalink
feat(core/nav): Components for new navigation categories (#8150)
Browse files Browse the repository at this point in the history
* feat(core/nav): Introduce new  nav item component

* feat(core/nav): UI router wrapper for nav category

* feat(core/nav): css

* feat(core/nav): Upgrade @spninaker/mocks

* feat(core/nav): Upgrade @spninaker/mocks

* feat(core/nav): Write unit tests

* feat(core/nav): Sync import format

* feat(core/nav): Refector NavRoute to function copmponent

* feat(core/nav): useDataSrouce hook and rebase

* fix tests

* Update tests

* update imports

* feat(core/nav): Propogate active styles

* Update test

* Use uirouter hook

* Move css to styleguide classes

* Move css to styleguide classes
  • Loading branch information
caseyhebebrand committed Apr 23, 2020
1 parent 895272c commit 21488bc
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 0 deletions.
138 changes: 138 additions & 0 deletions app/scripts/modules/core/src/application/nav/NavCategory.spec.tsx
@@ -0,0 +1,138 @@
import React from 'react';
import { mount } from 'enzyme';
import { BehaviorSubject } from 'rxjs';

import { mockEntityTags, mockServerGroupDataSourceConfig, mockPipelineDataSourceConfig } from '@spinnaker/mocks';
import { Application, ApplicationModelBuilder } from '../../application';
import { ApplicationDataSource, IDataSourceConfig } from '../service/applicationDataSource';
import { IEntityTags, IServerGroup, IPipeline } from '../../domain';
import { NavCategory } from './NavCategory';

describe('NavCategory', () => {
const buildApp = <T,>(config: IDataSourceConfig<T>): Application =>
ApplicationModelBuilder.createApplicationForTests('testapp', config);

it('should render a datasources icon', () => {
const app = buildApp<IServerGroup>(mockServerGroupDataSourceConfig);
const category = app.getDataSource('serverGroups');
category.iconName = 'spMenuClusters';

const wrapper = mount(<NavCategory app={app} category={category} isActive={false} />);
const nodes = wrapper.children();
const icon = nodes.childAt(1).children();
expect(icon.find('svg').length).toEqual(1);
});

it('should render a placeholder when there is icon', () => {
const app = buildApp<IServerGroup>(mockServerGroupDataSourceConfig);
const category = app.getDataSource('serverGroups');

const wrapper = mount(<NavCategory app={app} category={category} isActive={false} />);
const nodes = wrapper.children();
const icon = nodes.childAt(1).children();
expect(icon.find('svg').length).toEqual(0);
});

it('should render running tasks badge', () => {
const app = buildApp<IPipeline>(mockPipelineDataSourceConfig);
const category = app.getDataSource('executions');
app.dataSources.push({ ...category, key: 'runningExecutions' } as ApplicationDataSource<IPipeline>);
app.getDataSource(category.badge).status$ = new BehaviorSubject({
status: 'FETCHED',
loaded: true,
lastRefresh: 0,
error: null,
data: [mockPipelineDataSourceConfig, mockPipelineDataSourceConfig],
});

const wrapper = mount(<NavCategory app={app} category={category} isActive={false} />);
const nodes = wrapper.children();
expect(nodes.find('.badge-running-count').length).toBe(1);
expect(nodes.find('.badge-none').length).toBe(0);

const text = nodes.childAt(0).getDOMNode();
expect(text.textContent).toBe('2');
});

it('should not render running tasks badge if there are none', () => {
const app = buildApp<IPipeline>(mockPipelineDataSourceConfig);
const category = app.getDataSource('executions');
app.dataSources.push({ ...category, key: 'runningExecutions' } as ApplicationDataSource<IPipeline>);

const wrapper = mount(<NavCategory app={app} category={category} isActive={false} />);
const nodes = wrapper.children();
expect(nodes.find('.badge-running-count').length).toBe(0);
expect(nodes.find('.badge-none').length).toBe(1);

const text = nodes.childAt(0).getDOMNode();
expect(text.textContent).toBe('');
});

it('subscribes to runningCount updates', () => {
const app = buildApp<IPipeline>(mockPipelineDataSourceConfig);
const category = app.getDataSource('executions');
app.dataSources.push({ ...category, key: 'runningExecutions' } as ApplicationDataSource<IPipeline>);

const wrapper = mount(<NavCategory app={app} category={category} isActive={false} />);
const nodes = wrapper.children();
expect(nodes.find('.badge-running-count').length).toBe(0);
expect(nodes.find('.badge-none').length).toBe(1);

const text = nodes.childAt(0).getDOMNode();
expect(text.textContent).toBe('');

const updatedApp = buildApp<IPipeline>(mockPipelineDataSourceConfig);
updatedApp.dataSources.push({
...category,
key: 'runningExecutions',
} as ApplicationDataSource<IPipeline>);
updatedApp.getDataSource(category.badge).status$ = new BehaviorSubject({
status: 'FETCHED',
loaded: true,
lastRefresh: 0,
error: null,
data: [mockPipelineDataSourceConfig, mockPipelineDataSourceConfig],
});

wrapper.setProps({
app: updatedApp,
category,
isActive: false,
});
wrapper.update();

const newNodes = wrapper.children();
expect(newNodes.find('.badge-running-count').length).toBe(1);
expect(newNodes.find('.badge-none').length).toBe(0);

const newText = nodes.childAt(0).getDOMNode();
expect(newText.textContent).toBe('2');
});

it('should subscribe to alert updates', () => {
const app = buildApp<IServerGroup>(mockServerGroupDataSourceConfig);
const category = app.getDataSource('serverGroups');
const wrapper = mount(<NavCategory app={app} category={category} isActive={false} />);
const nodes = wrapper.children();
const tags: IEntityTags[] = nodes.find('DataSourceNotifications').prop('tags');
expect(tags.length).toEqual(0);

const newCategory = {
...category,
alerts: [mockEntityTags],
entityTags: [mockEntityTags],
};

wrapper.setProps({
app,
category: newCategory,
isActive: false,
});

const newTags: IEntityTags[] = wrapper
.children()
.find('DataSourceNotifications')
.prop('tags');
expect(newTags.length).toEqual(1);
});
});
40 changes: 40 additions & 0 deletions app/scripts/modules/core/src/application/nav/NavCategory.tsx
@@ -0,0 +1,40 @@
import React from 'react';

import { DataSourceNotifications } from 'core/entityTag/notifications/DataSourceNotifications';
import { Icon, useDataSource } from '../../presentation';

import { ApplicationDataSource } from '../service/applicationDataSource';
import { Application } from '../application.model';
import { IEntityTags } from '../../domain';

export interface INavCategoryProps {
category: ApplicationDataSource;
isActive: boolean;
app: Application;
}

export const NavCategory = ({ app, category, isActive }: INavCategoryProps) => {
const { alerts, badge, iconName, key, label } = category;

const { data: badgeData } = useDataSource(app.getDataSource(badge || key));
const runningCount = badge ? badgeData.length : 0;

// useDataSource is enough to update alerts when needed
useDataSource(category);
const tags: IEntityTags[] = alerts || [];

const badgeClassNames = runningCount ? 'badge-running-count' : 'badge-none';

return (
<div className="nav-category flex-container-h middle sp-padding-s-yaxis'">
<div className={badgeClassNames}>{runningCount > 0 ? runningCount : ''}</div>
<div className="nav-item">
{iconName && (
<Icon className="nav-icon" name={iconName} size="extraSmall" color={isActive ? 'primary' : 'accent'} />
)}
</div>
<div className="nav-item">{' ' + category.label}</div>
<DataSourceNotifications tags={tags} application={app} tabName={label} />
</div>
);
};
21 changes: 21 additions & 0 deletions app/scripts/modules/core/src/application/nav/NavRoute.tsx
@@ -0,0 +1,21 @@
import React from 'react';
import { useSrefActive } from '@uirouter/react';

import { NavCategory } from './NavCategory';
import { ApplicationDataSource } from '../service/applicationDataSource';
import { Application } from '../../application';

export interface INavRouteProps {
category: ApplicationDataSource;
isActive: boolean;
app: Application;
}

export const NavRoute = ({ app, category, isActive }: INavRouteProps) => {
const sref = useSrefActive(category.sref, null, 'active');
return (
<a {...sref}>
<NavCategory app={app} category={category} isActive={isActive} />
</a>
);
};
44 changes: 44 additions & 0 deletions app/scripts/modules/core/src/application/nav/verticalNav.less
@@ -0,0 +1,44 @@
.nav-category {
color: var(--color-accent);
padding-left: 40px;

.badge-none {
min-width: 18px;
margin-right: 8px;
}

.badge-running-count {
min-width: 18px;
margin-right: 8px;
border-radius: 3px;
color: var(--color-white);
background-color: var(--color-primary);
padding: 3px;
line-height: 14px;
text-align: center;
@media (max-width: 1400px) {
font-size: 10px;
}
}

.nav-item {
min-width: 16px;
margin-right: 8px;
line-height: 10px;
}
}

a {
text-decoration: none;
}

.active {
.nav-category {
background: var(--color-accessory-light);
color: var(--color-primary);
}

.nav-icon {
color: var(--color-primary);
}
}

0 comments on commit 21488bc

Please sign in to comment.