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 #2130 from GuessWhoSamFoo/timeline
Browse files Browse the repository at this point in the history
Add timeline component
  • Loading branch information
Sam Foo authored Mar 11, 2021
2 parents ec84d18 + f53928f commit 0cc85d6
Show file tree
Hide file tree
Showing 18 changed files with 497 additions and 1 deletion.
1 change: 1 addition & 0 deletions changelogs/unreleased/2130-GuessWhoSamFoo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added timeline component
2 changes: 2 additions & 0 deletions pkg/view/component/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ const (
TypeTerminal = "terminal"
// TypeText is a text component.
TypeText = "text"
// TypeTimeline is a timeline component.
TypeTimeline = "timeline"
// TypeTimestamp is a timestamp component.
TypeTimestamp = "timestamp"
// TypeYAML is a YAML component.
Expand Down
11 changes: 11 additions & 0 deletions pkg/view/component/testdata/config_timeline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"steps": [
{
"state": "current",
"header": "Header",
"title": "Title",
"description": "Description"
}
],
"vertical": true
}
40 changes: 40 additions & 0 deletions pkg/view/component/testdata/timeline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"metadata": {
"type": "timeline"
},
"config": {
"steps": [
{
"state": "success",
"header": "success header",
"title": "Step 1",
"description": "this is a success"
},
{
"state": "error",
"header": "error header",
"title": "Step 2",
"description": "this is an error"
},
{
"state": "current",
"header": "current header",
"title": "Step 3",
"description": "this is a current step"
},
{
"state": "processing",
"header": "processing header",
"title": "Step 4",
"description": "this is processing"
},
{
"state": "not-started",
"header": "not started header",
"title": "Step 5",
"description": "this has not started"
}
],
"vertical": false
}
}
109 changes: 109 additions & 0 deletions pkg/view/component/timeline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
Copyright (c) 2021 the Octant contributors. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package component

import (
"sync"

"github.com/pkg/errors"
)

// Timeline is a component for timeline
// +octant:component
type Timeline struct {
Base
Config TimelineConfig `json:"config"`

mu sync.Mutex
}

// TimelineConfig is the contents of Timeline
type TimelineConfig struct {
Steps []TimelineStep `json:"steps"`
Vertical bool `json:"vertical"`
}

// TimelineStep is the data for each timeline step
type TimelineStep struct {
State TimelineState `json:"state"`
Header string `json:"header"`
Title string `json:"title"`
Description string `json:"description"`
ButtonGroup *ButtonGroup `json:"buttonGroup,omitempty"`
}

func (t *TimelineStep) UnmarshalJSON(data []byte) error {
x := struct {
State TimelineState `json:"state"`
Header string `json:"header"`
Title string `json:"title"`
Description string `json:"description"`
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")
}
t.ButtonGroup = buttonGroup
}
t.State = x.State
t.Title = x.Title
t.Header = x.Header
t.Description = x.Description

return nil
}

// TimelineState is the state of a timeline step
type TimelineState string

const (
TimelineStepNotStarted TimelineState = "not-started"
TimelineStepCurrent TimelineState = "current"
TimelineStepProcessing TimelineState = "processing"
TimelineStepSuccess TimelineState = "success"
TimelineStepError TimelineState = "error"
)

// NewTimeline creates a timeline component
func NewTimeline(steps []TimelineStep, vertical bool) *Timeline {
return &Timeline{
Base: newBase(TypeTimeline, nil),
Config: TimelineConfig{
Steps: steps,
Vertical: vertical,
},
}
}

// Add adds an additional step to the timeline
func (t *Timeline) Add(steps ...TimelineStep) {
t.mu.Lock()
defer t.mu.Unlock()
t.Config.Steps = append(t.Config.Steps, steps...)
}

type timelineMarshal Timeline

func (t *Timeline) MarshalJSON() ([]byte, error) {
t.mu.Lock()
defer t.mu.Unlock()

m := timelineMarshal{
Base: t.Base,
Config: t.Config,
}
m.Metadata.Type = TypeTimeline
return json.Marshal(&m)
}
95 changes: 95 additions & 0 deletions pkg/view/component/timeline_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
Copyright (c) 2021 the Octant contributors. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package component

import (
"io/ioutil"
"path"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_Timeline_Marshal(t *testing.T) {
tests := []struct {
name string
input Component
expectedPath string
isErr bool
}{
{
name: "in general",
input: &Timeline{
Base: newBase(TypeTimeline, nil),
Config: TimelineConfig{
Steps: []TimelineStep{
{
State: TimelineStepSuccess,
Title: "Step 1",
Header: "success header",
Description: "this is a success",
},
{
State: TimelineStepError,
Title: "Step 2",
Header: "error header",
Description: "this is an error",
},
{
State: TimelineStepCurrent,
Title: "Step 3",
Header: "current header",
Description: "this is a current step",
},
{
State: TimelineStepProcessing,
Title: "Step 4",
Header: "processing header",
Description: "this is processing",
},
{
State: TimelineStepNotStarted,
Title: "Step 5",
Header: "not started header",
Description: "this has not started",
},
},
Vertical: false,
},
},
expectedPath: "timeline.json",
},
}
for _, tc := range tests {
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))
})
}
}

func Test_Timeline_Add(t *testing.T) {
step := TimelineStep{
State: TimelineStepCurrent,
Title: "Title",
Header: "Header",
Description: "Description",
}
timeline := NewTimeline([]TimelineStep{}, true)
timeline.Add(step)

expected := []TimelineStep{
step,
}
assert.Equal(t, expected, timeline.Config.Steps)
}
5 changes: 5 additions & 0 deletions pkg/view/component/unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ func unmarshal(to TypedObject) (Component, error) {
err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
"unmarshal text config")
o = t
case TypeTimeline:
t := &Timeline{Base: Base{Metadata: to.Metadata}}
err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
"unmarshal timeline config")
o = t
case TypeTimestamp:
t := &Timestamp{Base: Base{Metadata: to.Metadata}}
err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
Expand Down
19 changes: 19 additions & 0 deletions pkg/view/component/unmarshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,25 @@ func Test_unmarshal(t *testing.T) {
Base: newBase(TypeTimestamp, nil),
},
},
{
name: "timeline",
configFile: "config_timeline.json",
objectType: "timeline",
expected: &Timeline{
Config: TimelineConfig{
Steps: []TimelineStep{
{
State: TimelineStepCurrent,
Header: "Header",
Title: "Title",
Description: "Description",
},
},
Vertical: true,
},
Base: newBase(TypeTimeline, nil),
},
},
}

for _, tc := range cases {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<clr-timeline [clrLayout]="vertical ? 'vertical':'horizontal'">
<ng-container *ngFor="let step of steps; trackBy: trackByFn">
<clr-timeline-step [clrState]="step.state">
<clr-timeline-step-header>{{ step.header }}</clr-timeline-step-header>
<clr-timeline-step-title>{{ step.title }}</clr-timeline-step-title>
<clr-timeline-step-description>
{{ step.description }}
<ng-container *ngIf="step.buttonGroup">
<br>
<app-button-group [view]="step.buttonGroup"></app-button-group>
</ng-container>
</clr-timeline-step-description>
</clr-timeline-step>
</ng-container>
</clr-timeline>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
::ng-deep .btn-group .btn {
margin-top: 0.3rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) 2021 the Octant contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
//
import { Component } from '@angular/core';
import { TimelineView } from '../../../models/content';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { TimelineComponent } from './timeline.component';
@Component({
template: '<app-view-timeline [view]="view"></app-view-timeline>',
})
class TestWrapperComponent {
view: TimelineView;
}

describe('TimelineComponent', () => {
describe('handle changes', () => {
let component: TestWrapperComponent;
let fixture: ComponentFixture<TestWrapperComponent>;

beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
providers: [],
declarations: [TestWrapperComponent, TimelineComponent],
}).compileComponents();
})
);

beforeEach(() => {
fixture = TestBed.createComponent(TestWrapperComponent);
component = fixture.componentInstance;
});

it('should show step', () => {
const element: HTMLDivElement = fixture.nativeElement;
component.view = {
config: {
steps: [
{
state: 'current',
header: 'header',
title: 'title',
description: 'description',
},
],
vertical: false,
},
metadata: { type: 'timeline', title: [], accessor: 'accessor' },
};
fixture.detectChanges();

expect(element.querySelector('app-view-timeline').innerHTML).toContain(
'description'
);
});
});
});
Loading

0 comments on commit 0cc85d6

Please sign in to comment.