소스 검색

ExpandedChart additional tabs and preview casing frontend

jusrhee 5 년 전
부모
커밋
a6402438b9

+ 4 - 0
dashboard/src/assets/launch.svg

@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path opacity="0.4" d="M22 12.0048C22 17.5137 17.5116 22 12 22C6.48842 22 2 17.5137 2 12.0048C2 6.48625 6.48842 2 12 2C17.5116 2 22 6.48625 22 12.0048Z" fill="white"/>
+<path d="M16 12.0049C16 12.2576 15.9205 12.5113 15.7614 12.7145C15.7315 12.7543 15.5923 12.9186 15.483 13.0255L15.4233 13.0838C14.5881 13.9694 12.5099 15.3011 11.456 15.7278C11.456 15.7375 10.8295 15.9913 10.5312 16H10.4915C10.0341 16 9.60653 15.7482 9.38778 15.34C9.26847 15.1154 9.15909 14.4642 9.14915 14.4554C9.05966 13.8712 9 12.9769 9 11.9951C9 10.9657 9.05966 10.0316 9.16903 9.45808C9.16903 9.44836 9.27841 8.92345 9.34801 8.74848C9.45739 8.49672 9.65625 8.2819 9.90483 8.14581C10.1037 8.04957 10.3125 8 10.5312 8C10.7599 8.01069 11.1875 8.15553 11.3565 8.22357C12.4702 8.65128 14.598 10.051 15.4134 10.9064C15.5526 11.0425 15.7017 11.2087 15.7415 11.2467C15.9105 11.4605 16 11.723 16 12.0049Z" fill="white"/>
+</svg>

+ 8 - 21
dashboard/src/components/TabSelector.tsx

@@ -10,7 +10,8 @@ type PropsType = {
   currentTab: string,
   options: selectOption[],
   setCurrentTab: (value: string) => void,
-  addendum?: any
+  addendum?: any,
+  color?: string
 };
 
 type StateType = {
@@ -22,6 +23,7 @@ export default class TabSelector extends Component<PropsType, StateType> {
   }
 
   renderTabList = () => {
+    let color = this.props.color || '#949effcc';
     return (
       this.props.options.map((option: selectOption, i: number) => {
         return (
@@ -29,7 +31,7 @@ export default class TabSelector extends Component<PropsType, StateType> {
             key={i}
             onClick={() => this.handleTabClick(option.value)}
             lastItem={i === this.props.options.length - 1}
-            highlight={option.value === this.props.currentTab}
+            highlight={option.value === this.props.currentTab ? color : null}
           >
             {option.label}
           </Tab>
@@ -48,29 +50,14 @@ export default class TabSelector extends Component<PropsType, StateType> {
   }
 }
 
-const Highlight = styled.div`
-  width: 80%;
-  height: 1px;
-  margin-top: 5px;
-  background: #949EFFcc00;
-
-  opacity: 0;
-  animation: lineEnter 0.5s 0s;
-  animation-fill-mode: forwards;
-  @keyframes lineEnter {
-    from { width: 0%; opacity: 0; }
-    to   { width: 80%; opacity: 1; }
-  }
-`; 
-
 const Tab = styled.div`
   height: 30px;
-  margin-right: ${(props: { lastItem: boolean, highlight: boolean }) => props.lastItem ? '' : '30px'};
+  margin-right: ${(props: { lastItem: boolean, highlight: string }) => props.lastItem ? '' : '30px'};
   display: flex;
   font-family: 'Work Sans', sans-serif;
   font-size: 13px;
   user-select: none;
-  color: ${(props: { lastItem: boolean, highlight: boolean }) => props.highlight ? '#949effcc' : '#aaaabb55'};
+  color: ${(props: { lastItem: boolean, highlight: string }) => props.highlight ? props.highlight : '#aaaabb55'};
   flex-direction: column;
   padding-top: 7px;
   padding-bottom: 2px;
@@ -78,9 +65,9 @@ const Tab = styled.div`
   align-items: center;
   cursor: pointer;
   white-space: nowrap;
-  border-bottom: 1px solid ${(props: { lastItem: boolean, highlight: boolean }) => props.highlight ? '#949effcc' : 'none'};
+  border-bottom: 1px solid ${(props: { lastItem: boolean, highlight: string }) => props.highlight ? props.highlight : 'none'};
   :hover {
-    color: ${(props: { lastItem: boolean, highlight: boolean }) => props.highlight ? '' : '#aaaabb'};
+    color: ${(props: { lastItem: boolean, highlight: string }) => props.highlight ? '' : '#aaaabb'};
   }
 `;
 

+ 1 - 1
dashboard/src/main/home/Home.tsx

@@ -24,7 +24,7 @@ export default class Home extends Component<PropsType, StateType> {
   state = {
     forceSidebar: true,
     showWelcome: false,
-    currentView: 'templates'
+    currentView: 'dashboard'
   }
 
   renderDashboard = () => {

+ 43 - 14
dashboard/src/main/home/dashboard/expanded-chart/ExpandedChart.tsx

@@ -33,13 +33,17 @@ const tabOptions = [
   { label: 'Chart Overview', value: 'graph' },
   { label: 'Search Chart', value: 'list' },
   { label: 'Raw Values', value: 'values' },
-  { label: 'Logs', value: 'logs' },
+  { label: 'Detailed Logs', value: 'detailed-logs' },
+  { label: 'Deploy', value: 'deploy' },
+  { label: 'Settings', value: 'settings' },
 ];
 
 const basicOptions = [
-  { label: 'Update Values', value: 'values-form' },
+  { label: 'Values', value: 'values-form' },
   { label: 'Environment', value: 'environment' },
   { label: 'Logs', value: 'logs' },
+  { label: 'Deploy', value: 'deploy' },
+  { label: 'Settings', value: 'settings' },
 ];
 
 // TODO: consolidate revisionPreview and currentChart (currentChart can just be the initial state)
@@ -83,17 +87,26 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     let { currentCluster } = this.context;
     this.setState({ revisionPreview: oldChart });
 
-    api.getChartComponents('<token>', {
-      namespace: oldChart.namespace,
-      context: currentCluster,
-      storage: StorageType.Secret
-    }, { name: oldChart.name, revision: oldChart.version }, (err: any, res: any) => {
-      if (err) {
-        console.log(err)
-      } else {
-        this.setState({ components: res.data });
+    if (oldChart) {
+      api.getChartComponents('<token>', {
+        namespace: oldChart.namespace,
+        context: currentCluster,
+        storage: StorageType.Secret
+      }, { name: oldChart.name, revision: oldChart.version }, (err: any, res: any) => {
+        if (err) {
+          console.log(err)
+        } else {
+          this.setState({ components: res.data });
+        }
+      });
+
+      // Handle preview old chart while logs tab is open
+      if (this.state.currentTab === 'logs') {
+        this.setState({ currentTab: 'values-form' });
+      } else if (this.state.currentTab === 'detailed-logs') {
+        this.setState({ currentTab: 'graph' });
       }
-    });
+    }
   }
 
   toggleDevOpsMode = () => {
@@ -121,6 +134,21 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     return `${time} on ${date}`;
   }
 
+  // Hide certain tabs when previewing old charts
+  getTabOptions = () => {
+    let options = basicOptions.slice();
+    if (this.state.devOpsMode) {
+      options = tabOptions.slice();
+    }
+
+    if (this.state.revisionPreview) {
+      options.pop();
+      options.pop();
+      options.pop();
+    }
+    return options;
+  }
+
   renderTabContents = () => {
     let { currentChart, refreshChart, setSidebar } = this.props;
     let chart = currentChart;
@@ -199,7 +227,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
 
               <TagWrapper>
                 Namespace
-              <NamespaceTag>
+                <NamespaceTag>
                   {chart.namespace}
                 </NamespaceTag>
               </TagWrapper>
@@ -219,7 +247,8 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
 
             <TabSelectorWrapper>
               <TabSelector
-                options={this.state.devOpsMode ? tabOptions : basicOptions}
+                options={this.getTabOptions()}
+                color={this.state.revisionPreview ? '#f5cb42' : null}
                 currentTab={this.state.currentTab}
                 setCurrentTab={(value: string) => this.setState({ currentTab: value })}
                 addendum={

+ 16 - 5
dashboard/src/main/home/dashboard/expanded-chart/RevisionSection.tsx

@@ -21,6 +21,7 @@ type StateType = {
   maxVersion: number
 };
 
+// TODO: handle refresh when new revision is generated from an old revision
 export default class RevisionSection extends Component<PropsType, StateType> {
   state = {
     revisions: [] as ChartType[],
@@ -89,13 +90,22 @@ export default class RevisionSection extends Component<PropsType, StateType> {
     });
   }
 
+  handleClickRevision = (revision: ChartType) => {
+    let isCurrent = revision.version === this.state.maxVersion;
+    if (isCurrent) {
+      this.props.setRevisionPreview(null);
+    } else {
+      this.props.setRevisionPreview(revision);
+    }
+  }
+
   renderRevisionList = () => {
     return this.state.revisions.map((revision: ChartType, i: number) => {
       let isCurrent = revision.version === this.state.maxVersion;
       return (
         <Tr
           key={i}
-          onClick={() => this.props.setRevisionPreview(revision)}
+          onClick={() => this.handleClickRevision(revision)}
           selected={this.props.chart.version === revision.version}
         >
           <Td>{revision.version}</Td>
@@ -172,6 +182,7 @@ export default class RevisionSection extends Component<PropsType, StateType> {
       <div>
         <RevisionHeader
           showRevisions={this.props.showRevisions}
+          isCurrent={isCurrent}
           onClick={this.props.toggleShowRevisions}
         >
           {isCurrent ? `Current Revision` : `Previewing Revision (Not Deployed)`} - <Revision>No. {this.props.chart.version}</Revision>
@@ -343,7 +354,7 @@ const Revision = styled.div`
 `;
 
 const RevisionHeader = styled.div`
-  color: #ffffff66;
+  color: ${(props: { showRevisions: boolean, isCurrent: boolean }) => props.isCurrent ? '#ffffff66' : '#f5cb42'};
   display: flex;
   align-items: center;
   height: 40px;
@@ -351,7 +362,7 @@ const RevisionHeader = styled.div`
   width: 100%;
   padding-left: 15px;
   cursor: pointer;
-  background: ${(props: { showRevisions: boolean }) => props.showRevisions ? '#ffffff11' : ''};
+  background: ${(props: { showRevisions: boolean, isCurrent: boolean }) => props.showRevisions ? '#ffffff11' : ''};
   :hover {
     background: #ffffff18;
     > i {
@@ -364,8 +375,8 @@ const RevisionHeader = styled.div`
     font-size: 20px;
     cursor: pointer;
     border-radius: 20px;
-    background: ${(props: { showRevisions: boolean }) => props.showRevisions ? '#ffffff18' : ''};
-    transform: ${(props: { showRevisions: boolean }) => props.showRevisions ? 'rotate(180deg)' : ''};
+    background: ${(props: { showRevisions: boolean, isCurrent: boolean }) => props.showRevisions ? '#ffffff18' : ''};
+    transform: ${(props: { showRevisions: boolean, isCurrent: boolean }) => props.showRevisions ? 'rotate(180deg)' : ''};
   }
 `;
 

+ 95 - 37
dashboard/src/main/home/templates/Templates.tsx

@@ -3,9 +3,11 @@ import styled from 'styled-components';
 
 import { Context } from '../../../shared/Context';
 import api from '../../../shared/api';
+import { PorterChart } from '../../../shared/types';
 
 import TabSelector from '../../../components/TabSelector';
-import { AnyNaptrRecord } from 'dns';
+import ExpandedTemplate from './expanded-template/ExpandedTemplate';
+import Loading from '../../../components/Loading';
 
 const tabOptions = [
   { label: 'Community Templates', value: 'community' }
@@ -15,60 +17,94 @@ type PropsType = {
 };
 
 type StateType = {
+  currentChart: PorterChart | null,
   currentTab: string,
-  porterCharts: any[]
+  porterCharts: PorterChart[],
+  loading: boolean,
+  error: boolean
 };
 
 export default class Templates extends Component<PropsType, StateType> {
   state = {
+    currentChart: null as (PorterChart | null),
     currentTab: 'community',
-    porterCharts: [] as any[]
+    porterCharts: [] as PorterChart[],
+    loading: false,
+    error: false,
   }
 
   componentDidMount() {
+
     // Get templates
+    this.setState({ loading: true });
     api.getTemplates('<token>', {}, {}, (err: any, res: any) => {
       if (err) {
-        console.log(err);
+        this.setState({ loading: false, error: true });
       } else {
-        this.setState({ porterCharts: res.data });
+        this.setState({ porterCharts: res.data, loading: false, error: false });
       }
     });
   }
 
   renderIcon = (icon: string) => {
     if (icon) {
-      return <Icon src={icon} />
+      return <Icon src={icon} />;
     }
 
     return (
-        <Polymer><i className="material-icons">layers</i></Polymer>
-    )
+      <Polymer><i className="material-icons">layers</i></Polymer>
+    );
   }
 
-  renderStackList = () => {
-    return this.state.porterCharts.map((template, i) => {
-      console.log(template)
+  renderTemplateList = () => {
+    let { loading, error, porterCharts } = this.state;
+
+    if (loading) {
+      return <LoadingWrapper><Loading /></LoadingWrapper>
+    } else if (error) {
       return (
-        <TemplateBlock key={i}>
-          {this.renderIcon(template.Icon)}
+        <Placeholder>
+          <i className="material-icons">error</i> Error retrieving templates.
+        </Placeholder>
+      );
+    } else if (porterCharts.length === 0) {
+      return (
+        <Placeholder>
+          <i className="material-icons">category</i> No templates found.
+        </Placeholder>
+      );
+    }
+
+    return this.state.porterCharts.map((template: PorterChart, i: number) => {
+      let { Name, Icon, Description } = template.Form;
+      return (
+        <TemplateBlock key={i} onClick={() => this.setState({ currentChart: template })}>
+          {Icon ? this.renderIcon(Icon) : this.renderIcon(template.Icon)}
           <TemplateTitle>
-            {template.Form.Name ? template.Form.Name : template.Name}
+            {Name ? Name : template.Name}
           </TemplateTitle>
           <TemplateDescription>
-            {template.Form.Description ? template.Form.Description : template.Description}
+            {Description ? Description : template.Description}
           </TemplateDescription>
         </TemplateBlock>
       )
-    })
+    });
   }
-  
-  render() {
-    return ( 
-      <StyledTemplates>
-        <TemplatesWrapper>
+
+  renderContents = () => {
+    if (this.state.currentChart) {
+      return (
+        <ExpandedTemplate
+          currentChart={this.state.currentChart}
+          setCurrentChart={(currentChart: PorterChart) => this.setState({ currentChart })}
+        />
+      );
+    }
+
+    return (
+      <TemplatesWrapper>
         <TitleSection>
-          <Title>Template Manager</Title>
+          <Title>Template Explorer</Title>
         </TitleSection>
         <TabSelector
           options={tabOptions}
@@ -76,9 +112,16 @@ export default class Templates extends Component<PropsType, StateType> {
           setCurrentTab={(value: string) => this.setState({ currentTab: value })}
         />
         <TemplateList>
-          {this.renderStackList()}
+          {this.renderTemplateList()}
         </TemplateList>
-        </TemplatesWrapper>
+      </TemplatesWrapper>
+    );
+  }
+  
+  render() {
+    return ( 
+      <StyledTemplates>
+        {this.renderContents()}
       </StyledTemplates>
     );
   }
@@ -86,11 +129,29 @@ export default class Templates extends Component<PropsType, StateType> {
 
 Templates.contextType = Context;
 
+const Placeholder = styled.div`
+  padding-top: 100px;
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #ffffff44;
+  font-size: 14px;
+
+  > i {
+    font-size: 18px;
+    margin-right: 12px;
+  }
+`;
+
+const LoadingWrapper = styled.div`
+  padding-top: 300px;
+`;
 
 const Icon = styled.img`
-  height: 50px;
-  margin-top: 28px;
-  margin-bottom: 15px;
+  height: 42px;
+  margin-top: 35px;
+  margin-bottom: 13px;
 `;
 
 const Polymer = styled.div`
@@ -125,14 +186,6 @@ const TemplateTitle = styled.div`
   text-overflow: ellipsis;
 `;
 
-const CenterWrap = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex: 1;
-  padding-bottom: 15px;
-`;
-
 const TemplateBlock = styled.div`
   background: none;
   border: 1px solid #ffffff44;
@@ -152,9 +205,14 @@ const TemplateBlock = styled.div`
   cursor: pointer;
   color: #ffffff;
   position: relative;
-
   :hover {
-    background: #ffffff08;
+    background: #ffffff11;
+  }
+
+  animation: fadeIn 0.3s 0s;
+  @keyframes fadeIn {
+    from { opacity: 0 }
+    to { opacity: 1 }
   }
 `;
 

+ 147 - 0
dashboard/src/main/home/templates/expanded-template/ExpandedTemplate.tsx

@@ -0,0 +1,147 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+import launch from '../../../../assets/launch.svg';
+
+import { PorterChart } from '../../../../shared/types';
+
+type PropsType = {
+  currentChart: PorterChart,
+  setCurrentChart: (x: PorterChart) => void
+};
+
+type StateType = {
+};
+
+export default class ExpandedTemplate extends Component<PropsType, StateType> {
+  state = {
+  }
+
+  renderIcon = (icon: string) => {
+    if (icon) {
+      return <Icon src={icon} />
+    }
+
+    return (
+      <Polymer><i className="material-icons">layers</i></Polymer>
+    );
+  }
+
+  render() {
+    let { Name, Icon, Description } = this.props.currentChart.Form;
+    let { currentChart } = this.props;
+
+    return (
+      <StyledExpandedTemplate>
+        <TitleSection>
+          <Flex>
+            <i className="material-icons" onClick={() => this.props.setCurrentChart(null)}>
+              keyboard_backspace
+            </i>
+            {Icon ? this.renderIcon(Icon) : this.renderIcon(currentChart.Icon)}
+            <Title>{Name ? Name : currentChart.Name}</Title>
+          </Flex>
+          <Button>
+            <img src={launch} />
+            Launch Template
+          </Button>
+        </TitleSection>
+      </StyledExpandedTemplate>
+    );
+  }
+}
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+
+  > i {
+    margin-left: 10px;
+    cursor: pointer;
+    font-size 24px;
+    color: #969Fbbaa;
+    padding: 3px;
+    border-radius: 100px;
+    :hover {
+      background: #ffffff11;
+    }
+  }
+`;
+
+const Button = styled.div`
+  height: 100%;
+  background: #616FEEcc;
+  :hover {
+    background: #505edddd;
+  }
+  color: white;
+  font-weight: 500;
+  font-size: 13px;
+  padding: 10px 15px;
+  border-radius: 3px;
+  cursor: pointer;
+  box-shadow: 0 5px 8px 0px #00000010;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+
+  > img {
+    width: 20px;
+    height: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 10px;
+    justify-content: center;
+  }
+`;
+
+const Icon = styled.img`
+  width: 27px;
+  margin-left: 14px;
+  margin-right: 4px;
+  margin-bottom: -1px;
+`;
+
+
+const Polymer = styled.div`
+  margin-bottom: -3px;
+
+  > i {
+    color: ${props => props.theme.containerIcon};
+    font-size: 24px;
+    margin-left: 12px;
+    margin-right: 3px;
+  }
+`;
+
+const Description = styled.div`
+  font-size: 14px;
+  font-family: 'Work Sans', sans-serif;
+  margin-left: 30px;
+  width: calc(100% - 60px);
+  height: 4em;
+  border-radius: 2px;
+  color: #aaaabb;
+  padding: 5px 10px;
+`;
+
+const Title = styled.div`
+  font-size: 24px;
+  font-weight: 600;
+  font-family: 'Work Sans', sans-serif;
+  margin-left: 10px;
+  border-radius: 2px;
+  color: #ffffff;
+`;
+
+const TitleSection = styled.div`
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  width: 100%;
+  align-items: center;
+`;
+
+const StyledExpandedTemplate = styled.div`
+  width: calc(90% - 10px);
+  padding-top: 20px;
+`;

+ 31 - 1
dashboard/src/shared/types.tsx

@@ -57,9 +57,39 @@ export interface EdgeType {
   target: number
 }
 
-
 export enum StorageType {
   Secret = 'secret',
   ConfigMap = 'configmap',
   Memory = 'memory'
 }
+
+// PorterChart represents a bundled Porter template
+export interface PorterChart {
+	Name: string,
+	Description: string,
+	Icon: string,
+	Form: FormYAML
+}
+
+// FormYAML represents a chart's values.yaml form abstraction
+export interface FormYAML {
+	Name: string,  
+	Icon: string,   
+	Description: string,   
+	Tags: string[],
+  Sections: {
+    Name: string,
+    Contents: FormElement[]
+  }[]
+}
+
+// FormElement represents a form element
+export interface FormElement {
+  Type: string,
+  Label: string,
+  Name: string,
+  Variable: string,
+  Settings: {
+    Default: number
+  }
+}

+ 2 - 0
server/api/template_handler.go

@@ -49,6 +49,7 @@ type PorterChart struct {
 // FormYAML represents a chart's values.yaml form abstraction
 type FormYAML struct {
 	Name        string   `yaml:"name"`
+	Icon        string   `yaml:"icon"`
 	Description string   `yaml:"description"`
 	Tags        []string `yaml:"tags"`
 	Sections    []struct {
@@ -66,6 +67,7 @@ type FormYAML struct {
 }
 
 // HandleListTemplates retrieves a list of Porter templates
+// TODO: test and reduce fragility (handle untar/parse error for individual charts)
 func (app *App) HandleListTemplates(w http.ResponseWriter, r *http.Request) {
 	resp, err := http.Get(baseURL + "index.yaml")
 	if err != nil {