Unverified Commit 788c184f authored by Milan Klanjsek's avatar Milan Klanjsek Committed by GitHub
Browse files

Merge pull request #2482 from ftovaro/issue-2304

Tabs Component
parents 0eb782f9 44de7c1f
Showing with 695 additions and 2 deletions
+695 -2
Added tabs as components
......@@ -10,7 +10,7 @@ const (
TypeAccordion = "accordion"
// TypeAnnotations is an annotations component.
TypeAnnotations = "annotations"
// ButtonGroup is a button group component.
// TypeButtonGroup is a button group component.
TypeButtonGroup = "buttonGroup"
// TypeCard is a card component.
TypeCard = "card"
......@@ -96,6 +96,8 @@ const (
TypeSignpost = "signpost"
// TypeButton is a Button component.
TypeButton = "button"
// TypeTabsView is a Tab component.
TypeTabsView = "tabsView"
)
// Base is an abstract base for components..
......
......@@ -57,7 +57,7 @@ func (bg *ButtonGroupConfig) UnmarshalJSON(data []byte) error {
button, ok := component.(*Button)
if !ok {
return fmt.Errorf("item was not a card")
return fmt.Errorf("item was not a button group")
}
bg.Buttons = append(bg.Buttons, *button)
......
/*
Copyright (c) 2021 the Octant contributors. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package component
import (
"fmt"
"github.com/vmware-tanzu/octant/internal/util/json"
)
type TabsView struct {
Base
Config TabsViewConfig `json:"config"`
}
// TabsOrientation is the direction of the Tabs
type TabsOrientation string
const (
// VerticalTabs are tabs organized vertically
VerticalTabs TabsOrientation = "vertical"
// HorizontalTabs are tabs organized horizontally
HorizontalTabs TabsOrientation = "horizontal"
)
type TabsViewConfig struct {
// Tabs are an array of Tab structs
Tabs []SingleTab `json:"tabs"`
// Orientation is the direction of the tabs
Orientation TabsOrientation `json:"orientation,omitempty"`
}
func NewTabs(orientation TabsOrientation, tabs []SingleTab) *TabsView {
return &TabsView{
Base: newBase(TypeTabsView, nil),
Config: TabsViewConfig{
Tabs: tabs,
Orientation: orientation,
},
}
}
type tabsMarshal TabsView
// MarshalJSON marshals a button group.
func (t *TabsView) MarshalJSON() ([]byte, error) {
m := tabsMarshal(*t)
m.Metadata.Type = TypeTabsView
return json.Marshal(&m)
}
var _ Component = (*TabsView)(nil)
type SingleTab struct {
Name string `json:"name"`
Contents FlexLayout `json:"contents"`
}
func (t *TabsViewConfig) UnmarshalJSON(data []byte) error {
x := struct {
Orientation TabsOrientation `json:"orientation,omitempty"`
Tabs []struct {
Name string `json:"name"`
Contents TypedObject `json:"contents"`
} `json:"tabs"`
}{}
if err := json.Unmarshal(data, &x); err != nil {
return err
}
for _, tab := range x.Tabs {
c, err := tab.Contents.ToComponent()
if err != nil {
return err
}
fl, ok := c.(*FlexLayout)
if !ok {
return fmt.Errorf("item was not a FlexLayout")
}
st := SingleTab{
Name: tab.Name,
Contents: *fl,
}
t.Tabs = append(t.Tabs, st)
}
t.Orientation = x.Orientation
return nil
}
/*
Copyright (c) 2021 the Octant contributors. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package component
import (
"io/ioutil"
"path"
"testing"
"github.com/vmware-tanzu/octant/pkg/action"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vmware-tanzu/octant/internal/util/json"
)
func Test_Tabs_Marshal(t *testing.T) {
fl := NewFlexLayout("title")
button := NewButton("test", action.Payload{"foo": "bar"})
section := FlexLayoutSection{
{
Width: WidthFull,
View: button,
},
}
fl.AddSections(section)
test := []struct {
name string
input Component
expectedPath string
isErr bool
}{
{
name: "in general",
input: &TabsView{
Base: newBase(TypeTabsView, nil),
Config: TabsViewConfig{
Tabs: []SingleTab{
{
Name: "title",
Contents: *fl,
},
{
Name: "title 2",
Contents: *fl,
},
},
},
},
expectedPath: "tabs.json",
isErr: false,
},
}
for _, tc := range test {
t.Run(tc.name, func(t *testing.T) {
actual, err := json.Marshal(tc.input)
isErr := err != nil
if isErr != tc.isErr {
t.Fatalf("UnExpected error: %v", err)
}
expected, err := ioutil.ReadFile(path.Join("testdata", tc.expectedPath))
require.NoError(t, err)
assert.JSONEq(t, string(expected), string(actual))
})
}
}
{
"tabs":
[
{
"name":"Tab 1",
"contents":{
"config":{
"sections":
[
[
{
"width": 24,
"view":{
"config":{
"name":"test",
"payload":{
"foo":"bar"
}
},
"metadata":{
"type":"button"
}
}
}
]
]
},
"metadata":{
"type":"flexlayout"
}
}
}
]
}
{
"metadata":{"type":"tabsView"},
"config":{
"tabs":
[
{
"name":"title",
"contents":{
"config":{
"buttonGroup":{
"config":{
"buttons":null
},
"metadata":{
"type":"buttonGroup"
}
},
"sections":
[
[
{
"width": 24,
"view":{
"config":{
"name":"test",
"payload":{
"foo":"bar"
}
},
"metadata":{
"type":"button"
}
}
}
]
]
},
"metadata":{
"title":
[
{
"config":{
"value":"title"
},
"metadata":{
"type":"text"
}
}
],
"type":"flexlayout"
}
}
},
{
"name":"title 2",
"contents":{
"config":{
"buttonGroup":{
"config":{
"buttons":null
},
"metadata":{
"type":"buttonGroup"
}
},
"sections":
[
[
{
"width": 24,
"view":{
"config":{
"name":"test",
"payload":{
"foo":"bar"
}
},
"metadata":{
"type":"button"
}
}
}
]
]
},
"metadata":{
"title":
[
{
"config":{
"value":"title"
},
"metadata":{
"type":"text"
}
}
],
"type":"flexlayout"
}
}
}
]
}
}
......@@ -211,6 +211,11 @@ func unmarshal(to TypedObject) (Component, error) {
err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
"unmarshal signpost config")
o = t
case TypeTabsView:
t := &TabsView{Base: Base{Metadata: to.Metadata}}
err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
"unmarshal tabs config")
o = t
default:
return nil, errors.Errorf("unknown view component %q", to.Metadata.Type)
}
......
......@@ -503,6 +503,29 @@ func Test_unmarshal(t *testing.T) {
Base: newBase(TypeStepper, nil),
},
},
{
name: "tabsView",
configFile: "config_tabs.json",
objectType: "tabsView",
expected: &TabsView{
Config: TabsViewConfig{
Tabs: []SingleTab{{
Name: "Tab 1",
Contents: FlexLayout{
Base: newBase(TypeFlexLayout, nil),
Config: FlexLayoutConfig{
Sections: []FlexLayoutSection{{{
Width: WidthFull,
View: NewButton("test", action.Payload{"foo": "bar"}),
}}},
ButtonGroup: nil,
},
},
}},
},
Base: newBase(TypeTabsView, nil),
},
},
{
name: "summary",
configFile: "config_summary.json",
......
<clr-tabs *ngIf="tabs.length > 0" clrLayout="{{ orientation }}">
<clr-tab *ngFor="let tab of tabs; trackBy: identifyTab">
<button clrTabLink class="tab-button" (click)="clickTab(tab.name)">
{{ tab.name }}
</button>
<ng-template [clrIfActive]="activeTab === tab.name">
<clr-tab-content>
<div [ngClass]="{'tabs-container': orientation === 'horizontal'}">
<app-view-container [view]="tab.contents"></app-view-container>
</div>
</clr-tab-content>
</ng-template>
</clr-tab>
</clr-tabs>
/* Copyright (c) 2021 the Octant contributors. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
.tab-button {
outline: none;
}
.tabs-container {
margin-top: 10px;
}
// Copyright (c) 2019 the Octant contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
//
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { TabsViewComponent } from './tabs-view.component';
import { SharedModule } from '../../../shared.module';
import { windowProvider, WindowToken } from '../../../../../window';
import { OctantTooltipComponent } from '../octant-tooltip/octant-tooltip';
describe('TabsComponent', () => {
let component: TabsViewComponent;
let fixture: ComponentFixture<TabsViewComponent>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OctantTooltipComponent],
imports: [SharedModule],
providers: [{ provide: WindowToken, useFactory: windowProvider }],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(TabsViewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
// Copyright (c) 2021 the Octant contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
//
import { Component, EventEmitter, Output } from '@angular/core';
import { TabsView, View } from '../../../models/content';
import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
interface Tab {
name: string;
view: View;
accessor: string;
}
@Component({
selector: 'app-tabs-view',
templateUrl: './tabs-view.component.html',
styleUrls: ['./tabs-view.component.scss'],
})
export class TabsViewComponent extends AbstractViewComponent<TabsView> {
activeTab: string;
tabs: View[] = [];
orientation: string;
constructor() {
super();
}
update() {
this.tabs = this.v.config.tabs;
this.orientation = this.v.config?.orientation || 'horizontal';
}
clickTab(tabAccessor: string) {
if (this.activeTab === tabAccessor) {
return;
}
this.activeTab = tabAccessor;
}
identifyTab(index: number, item: Tab): string {
return item.name;
}
}
......@@ -46,6 +46,7 @@ import { TextComponent } from './components/presentation/text/text.component';
import { TimelineComponent } from './components/presentation/timeline/timeline.component';
import { TimestampComponent } from './components/presentation/timestamp/timestamp.component';
import { YamlComponent } from './components/presentation/yaml/yaml.component';
import { TabsViewComponent } from './components/presentation/tabs-view/tabs-view.component';
export interface ComponentMapping {
[key: string]: Type<any>;
......@@ -94,6 +95,7 @@ const DynamicComponentMapping: ComponentMapping = {
timestamp: TimestampComponent,
yaml: YamlComponent,
signpost: SignpostComponent,
tabsView: TabsViewComponent,
};
export const DYNAMIC_COMPONENTS_MAPPING = new InjectionToken<ComponentMapping>(
......
......@@ -652,3 +652,10 @@ export interface SelectFileView extends View {
action?: string;
};
}
export interface TabsView extends View {
config: {
tabs: View[];
orientation?: string;
};
}
......@@ -16,6 +16,7 @@ import { LabelsComponent } from './components/presentation/labels/labels.compone
import { LinkComponent } from './components/presentation/link/link.component';
import { ListComponent } from './components/presentation/list/list.component';
import { TabsComponent } from './components/presentation/tabs/tabs.component';
import { TabsViewComponent } from './components/presentation/tabs-view/tabs-view.component';
import { ContainersComponent } from './components/presentation/containers/containers.component';
import { DatagridComponent } from './components/presentation/datagrid/datagrid.component';
import { DonutChartComponent } from './components/presentation/donut-chart/donut-chart.component';
......@@ -156,6 +157,7 @@ import { JoinPipe } from './pipes/join/join.pipe';
SummaryComponent,
TableComponent,
TabsComponent,
TabsViewComponent,
TerminalComponent,
TextComponent,
TimelineComponent,
......@@ -228,6 +230,7 @@ import { JoinPipe } from './pipes/join/join.pipe';
SummaryComponent,
TableComponent,
TabsComponent,
TabsViewComponent,
TerminalComponent,
TextComponent,
TimelineComponent,
......@@ -314,6 +317,7 @@ import { JoinPipe } from './pipes/join/join.pipe';
SummaryComponent,
TableComponent,
TabsComponent,
TabsViewComponent,
TerminalComponent,
TextComponent,
TimelineComponent,
......
import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { argTypesView } from "./helpers/helpers";
export const tabsDocs= { source: { code: `
tabs := component.NewTabs(component.HorizontalTabs, []component.SingleTab{
{
Name: "Tab 1",
Contents: component.FlexLayout{
Base: component.Base{Metadata: component.Metadata{
Type: component.TypeFlexLayout,
}},
Config: component.FlexLayoutConfig{
Sections: []component.FlexLayoutSection{{
{
Width: component.WidthFull,
View: component.NewButton("test", action.Payload{"foo": "bar"}),
},
}},
ButtonGroup: nil,
},
},
},
{
Name: "Tab 2",
Contents: component.FlexLayout{
Base: component.Base{Metadata: component.Metadata{
Type: component.TypeFlexLayout,
}},
Config: component.FlexLayoutConfig{
Sections: []component.FlexLayoutSection{{
{
Width: component.WidthFull,
View: component.NewText("My Text"),
},
}},
ButtonGroup: nil,
},
},
},
})
`}}
export const tabsVerticalDocs= { source: { code: `
tabs := component.NewTabs(component.VerticalTabs, []component.SingleTab{
{
Name: "Tab 1",
Contents: component.FlexLayout{
Base: component.Base{Metadata: component.Metadata{
Type: component.TypeFlexLayout,
}},
Config: component.FlexLayoutConfig{
Sections: []component.FlexLayoutSection{{
{
Width: component.WidthFull,
View: component.NewButton("test", action.Payload{"foo": "bar"}),
},
}},
ButtonGroup: nil,
},
},
},
{
Name: "Tab 2",
Contents: component.FlexLayout{
Base: component.Base{Metadata: component.Metadata{
Type: component.TypeFlexLayout,
}},
Config: component.FlexLayoutConfig{
Sections: []component.FlexLayoutSection{{
{
Width: component.WidthFull,
View: component.NewText("My Text"),
},
}},
ButtonGroup: nil,
},
},
},
})
`}}
export const tabsView = {
config: {
tabs: [
{
name:"Tab 1",
contents:{
config:{
sections:
[[
{
width: 24,
view:{
config:{
name:"test",
payload:{
foo:"bar"
}
},
metadata:{type:"button"}
}
}
]]
},
metadata:{
type:"flexlayout"
}
}
},
{
name:"Tab 2",
contents:{
config:{
sections:
[[
{
width: 24,
view:{
config:{
value:"My Text"
},
metadata:{
type:"text"
}
}
}
]]
},
metadata:{
type:"flexlayout"
}
}
}
]
},
metadata: {
type: 'tabsView',
},
}
export const tabsVerticalView = {
config: {
orientation: "vertical",
tabs: [
{
name:"Tab 1",
contents:{
config:{
sections:
[[
{
width: 24,
view:{
config:{
name:"test",
payload:{
foo:"bar"
}
},
metadata:{type:"button"}
}
}
]]
},
metadata:{
type:"flexlayout"
}
}
},
{
name:"Tab 2",
contents:{
config:{
sections:
[[
{
width: 24,
view:{
config:{
value:"My Text"
},
metadata:{
type:"text"
}
}
}
]]
},
metadata:{
type:"flexlayout"
}
}
}
]
},
metadata: {
type: 'tabsView',
},
}
export const TabsStoryTemplate = args => ({
template: `<app-tabs-view [view]= "view"></app-tabs-view>`,
argTypes: argTypesView,
props: {view: args.view},
});
<h1>Tabs component</h1>
<h2>Description</h2>
<p>The Tab component can have any other component inside, you can use to organize your views.</p>
<h2>Example</h2>
<Meta title="Components/Tabs" argTypes = { argTypesView } />
<Canvas withToolbar>
<Story name="Horizontal Tabs component"
parameters={{ docs: tabsDocs }}
args= {{ view: tabsView }}>
{ TabsStoryTemplate.bind({}) }
</Story>
</Canvas>
<h2>Props</h2>
<ArgsTable story = "Horizontal Tabs component" />
<h2>Vertical Tabs</h2>
<Canvas withToolbar>
<Story name="Vertical Tabs component"
parameters={{ docs: tabsVerticalDocs }}
args= {{ view: tabsVerticalView }}>
{ TabsStoryTemplate.bind({}) }
</Story>
</Canvas>
<h2>Props</h2>
<ArgsTable story = "Vertical Tabs component" />
......@@ -91,6 +91,8 @@
"src//app/modules/shared/components/presentation/datagrid/datagrid.component.ts",
"src//app/modules/shared/components/presentation/tabs/tabs.component.spec.ts",
"src//app/modules/shared/components/presentation/tabs/tabs.component.ts",
"src//app/modules/shared/components/presentation/tabs-view/tabs-view.component.spec.ts",
"src//app/modules/shared/components/presentation/tabs-view/tabs-view.component.ts",
"src//app/modules/shared/components/presentation/label-selector/label-selector.component.ts",
"src//app/modules/shared/components/presentation/label-selector/label-selector.component.spec.ts",
"src//app/modules/shared/components/presentation/preferences/preferences.component.ts",
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment