Ver Fonte

Merge pull request #227 from porter-dev/beta.3.integration-frontend

Beta.3.integration frontend
jusrhee há 5 anos atrás
pai
commit
e5a719393c

+ 0 - 8
dashboard/src/components/values-form/Base64InputRow.tsx

@@ -28,14 +28,6 @@ export default class InputRow extends Component<PropsType, StateType> {
   
   render() {
     let { label, value, type, unit, placeholder, width } = this.props;
-    if (type === 'b64') {
-      type = 'string-input';
-    } else if (type === 'b64-pass') {
-      type = 'password';
-    }
-    if (value === undefined) {
-        value = '';
-    }
     value = value.toString();
     value = atob(value);
     return (

+ 2 - 2
dashboard/src/components/values-form/ValuesForm.tsx

@@ -155,7 +155,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
             <Base64InputRow
               key={i}
               isRequired={item.required}
-              type='b64'
+              type='text'
               value={this.getInputValue(item)}
               setValue={(x: string) => {
                 if (item.settings && item.settings.unit && x !== '') {
@@ -172,7 +172,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
             <Base64InputRow
               key={i}
               isRequired={item.required}
-              type='b64-pass'
+              type='password'
               value={this.getInputValue(item)}
               setValue={(x: string) => {
                 if (item.settings && item.settings.unit && x !== '') {

+ 4 - 0
dashboard/src/components/values-form/ValuesWrapper.tsx

@@ -53,6 +53,10 @@ export default class ValuesWrapper extends Component<PropsType, StateType> {
               case 'select':
                 metaState[key] = def ? def : item.settings.options[0].value;
                 break;
+              case 'base-64':
+                metaState[key] = def ? def : '';
+              case 'base-64-password':
+                metaState[key] = def ? def : '';
               default:
             }
           });

+ 1 - 7
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -26,16 +26,10 @@ type StateType = {
 export default class ClusterDashboard extends Component<PropsType, StateType> {
   state = {
     namespace: 'default',
-    sortType: 'Newest',
+    sortType: (localStorage.getItem("SortType") ? localStorage.getItem('SortType') : 'Newest'),
     currentChart: null as (ChartType | null)
   }
 
-  componentDidMount() {
-    if (localStorage.getItem("SortType")) {
-      this.setState({ sortType: localStorage.getItem("SortType") });
-    }
-  }
-
   componentDidUpdate(prevProps: PropsType) {
     localStorage.setItem("SortType", this.state.sortType);
     // Reset namespace filter and close expanded chart on cluster change

+ 9 - 3
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -234,16 +234,22 @@ export default class ChartList extends Component<PropsType, StateType> {
 ChartList.contextType = Context;
 
 const Placeholder = styled.div`
-  padding-top: 100px;
   width: 100%;
   display: flex;
   justify-content: center;
   align-items: center;
   color: #ffffff44;
-  font-size: 14px;
+  background: #26282f;
+  border-radius: 5px;
+  height: 320px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ffffff44;
+  font-size: 13px;
 
   > i {
-    font-size: 18px;
+    font-size: 16px;
     margin-right: 12px;
   }
 `;

+ 81 - 10
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -18,7 +18,8 @@ import StatusSection from './status/StatusSection';
 import ValuesWrapper from '../../../../components/values-form/ValuesWrapper';
 import ValuesForm from '../../../../components/values-form/ValuesForm';
 import SettingsSection from './SettingsSection';
-import { format } from 'util';
+import ConfirmOverlay from '../../../../components/ConfirmOverlay';
+import Loading from '../../../../components/Loading';
 
 type PropsType = {
   namespace: string,
@@ -44,6 +45,8 @@ type StateType = {
   controllers: Record<string, Record<string, any>>,
   websockets: Record<string, any>,
   url: string | null,
+  showDeleteOverlay: boolean,
+  deleting: boolean,
 };
 
 export default class ExpandedChart extends Component<PropsType, StateType> {
@@ -62,6 +65,8 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     controllers: {} as Record<string, Record<string, any>>,
     websockets : {} as Record<string, any>,
     url: null as string | null,
+    showDeleteOverlay: false,
+    deleting: false,
   }
 
   // Retrieve full chart data (includes form and values)
@@ -250,6 +255,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
             currentChart={chart}
             refreshChart={this.refreshChart}
             setCurrentView={setCurrentView}
+            setShowDeleteOverlay={(x: boolean) => this.setState({ showDeleteOverlay: x })}
           /> 
         );
       case 'graph': 
@@ -333,7 +339,6 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     // Append universal tabs
     tabOptions.push(
       { label: 'Status', value: 'status' },
-      { label: 'Deploy', value: 'settings' },
       { label: 'Chart Overview', value: 'graph' },
     );
 
@@ -344,6 +349,9 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       );
     }
 
+    // Settings tab is always last
+    tabOptions.push({ label: 'Settings', value: 'settings' });
+
     // Filter tabs if previewing an old revision
     if (this.state.isPreview) {
       let liveTabs = ['status', 'settings', 'deploy'];
@@ -452,7 +460,6 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
         console.log(err)
       } else {
         this.setState({ components: res.data.Objects });
-        console.log(res.data.Objects)
       }
     });
 
@@ -467,12 +474,13 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
         console.log(err);
         return
       }
-      console.log(res.data)
       
       if (res.data?.status?.loadBalancer?.ingress) {
         this.setState({url: `http://${res.data?.status?.loadBalancer?.ingress[0]?.hostname}` })
       }
-    })
+    });
+
+    this.updateTabs();
   }
 
   componentDidUpdate(prevProps: PropsType) {
@@ -485,8 +493,8 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
   componentWillUnmount() {
     if (this.state.websockets) {
       this.state.websockets.forEach((ws: WebSocket) => {
-        ws.close()
-      })
+        ws.close();
+      });
     }
   }
 
@@ -518,15 +526,50 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     }
   }
 
+  handleUninstallChart = () => {
+    let { currentProject, currentCluster } = this.context;
+    let { currentChart } = this.props;
+    this.setState({ deleting: true });
+    api.uninstallTemplate('<token>', {
+    }, {
+      namespace: currentChart.namespace,
+      storage: StorageType.Secret,
+      name: currentChart.name,
+      id: currentProject.id,
+      cluster_id: currentCluster.id,
+    }, (err: any, res: any) => {
+      if (err) {
+        console.log(err)
+      } else {
+        this.setState({ showDeleteOverlay: false });
+        this.props.setCurrentChart(null);
+      }
+    });
+  }
+
+  renderDeleteOverlay = () => {
+    if (this.state.deleting) {
+      return <DeleteOverlay><Loading /></DeleteOverlay>;
+    }
+  }
+
   render() {
     let { currentChart, setCurrentChart } = this.props;
     let chart = currentChart;
     let status = this.getChartStatus(chart.info.status);
 
     return ( 
-      <div>
-        <CloseOverlay onClick={() => setCurrentChart(null)}/>
+      <>
+        <CloseOverlay onClick={() => setCurrentChart(null)} />
         <StyledExpandedChart>
+          <ConfirmOverlay
+            show={this.state.showDeleteOverlay}
+            message={`Are you sure you want to delete ${currentChart.name}?`}
+            onYes={this.handleUninstallChart}
+            onNo={() => this.setState({ showDeleteOverlay: false })}
+          />
+          {this.renderDeleteOverlay()}
+          
           <HeaderWrapper>
             <TitleSection>
               <Title>
@@ -580,13 +623,41 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
             {this.renderTabContents()}
           </TabRegion>
         </StyledExpandedChart>
-      </div>
+      </>
     );
   }
 }
 
 ExpandedChart.contextType = Context;
 
+const DeleteOverlay = styled.div`
+  position: absolute;
+  top: 0px;
+  opacity: 100%;
+  left: 0px;
+  width: 100%;
+  height: 100%;
+  z-index: 999;
+  display: flex;
+  padding-bottom: 30px;
+  align-items: center;
+  justify-content: center;
+  font-family: 'Work Sans', sans-serif;
+  font-size: 18px;
+  font-weight: 500;
+  color: white;
+  flex-direction: column;
+  background: rgb(0,0,0,0.73);
+  opacity: 0;
+  animation: lindEnter 0.2s;
+  animation-fill-mode: forwards;
+
+  @keyframes lindEnter {
+    from { opacity: 0; }
+    to   { opacity: 1; }
+  }
+`;
+
 const Bolded = styled.div`
   font-weight: 500;
   color: #ffffff44;

+ 29 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -17,6 +17,7 @@ type PropsType = {
   currentChart: ChartType,
   refreshChart: () => void,
   setCurrentView: (x: string) => void,
+  setShowDeleteOverlay: (x: boolean) => void,
 };
 
 type StateType = {
@@ -156,7 +157,7 @@ export default class SettingsSection extends Component<PropsType, StateType> {
   }
 
   renderWebhookSection = () => {
-    if (this.state.webhookToken) {
+    if (true || this.state.webhookToken) {
       let webhookText = `curl -X POST 'https://dashboard.getporter.dev/api/webhooks/deploy/${this.state.webhookToken}?commit=???&repository=???'`;
       return (
         <>
@@ -184,9 +185,13 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     return (
       <Wrapper>
         <StyledSettingsSection>
-          <Heading>Connected source</Heading>
+          <Heading>Connected Source</Heading>
           {this.renderSourceSection()}
           {this.renderWebhookSection()}
+          <Heading>Additional Settings</Heading>
+          <Button color='#b91133' onClick={() => this.props.setShowDeleteOverlay(true)}>
+            Delete {this.props.currentChart.name}
+          </Button>
         </StyledSettingsSection>
         <SaveButton
           text='Save Settings'
@@ -202,6 +207,27 @@ export default class SettingsSection extends Component<PropsType, StateType> {
 
 SettingsSection.contextType = Context;
 
+const Button = styled.button`
+  height: 40px;
+  font-size: 13px;
+  margin-top: 20px;
+  font-weight: 500;
+  font-family: 'Work Sans', sans-serif;
+  color: white;
+  padding: 6px 20px 7px 20px;
+  text-align: left;
+  border: 0;
+  border-radius: 5px;
+  background: ${(props) => (!props.disabled ? props.color : '#aaaabb')};
+  box-shadow: ${(props) => (!props.disabled ? '0 2px 5px 0 #00000030' : 'none')};
+  cursor: ${(props) => (!props.disabled ? 'pointer' : 'default')};
+  user-select: none;
+  :focus { outline: 0 }
+  :hover {
+    filter: ${(props) => (!props.disabled ? 'brightness(120%)' : '')};
+  }
+`;
+
 const Webhook = styled.div`
   width: 100%;
   border: 1px solid #ffffff55;
@@ -262,6 +288,7 @@ const StyledSettingsSection = styled.div`
   height: calc(100% - 60px);
   background: #ffffff11;
   padding: 0 35px;
+  padding-bottom: 50px;
   position: relative;
   border-radius: 5px;
   overflow: auto;

+ 5 - 1
dashboard/src/main/home/templates/Templates.tsx

@@ -45,7 +45,11 @@ export default class Templates extends Component<PropsType, StateType> {
       if (err) {
         this.setState({ loading: false, error: true });
       } else {
-        this.setState({ porterTemplates: res.data, loading: false, error: false });
+        this.setState({ porterTemplates: res.data, error: false }, () => {
+          this.state.porterTemplates.sort((a, b) => (a.name > b.name) ? 1 : -1);
+          this.state.porterTemplates.sort((a,b) => (a.name === 'docker') ? -1 : (b.name === 'docker') ? 1 : 0);
+          this.setState({ loading: false });
+        });
       }
     });
   }

+ 9 - 0
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -55,9 +55,16 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
     let { currentCluster, currentProject } = this.context;
     let name = randomWords({ exactly: 3, join: '-' });
     this.setState({ saveValuesStatus: 'loading' });
+
+    let values = {};
+    for (let key in wildcard) {
+      _.set(values, key, wildcard[key]);
+    }
+
     api.deployTemplate('<token>', {
       templateName: this.props.currentTemplate.name,
       storage: StorageType.Secret,
+      formValues: values,
       namespace: this.state.selectedNamespace,
       name,
     }, {
@@ -92,6 +99,8 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
       let splits = this.state.selectedImageUrl.split(':');
       imageUrl = splits[0];
       tag = splits[1];
+    } else if (!tag) {
+      tag = 'latest';
     }
 
     _.set(values, "image.repository", imageUrl)

+ 13 - 0
dashboard/src/shared/api.tsx

@@ -190,6 +190,18 @@ const deployTemplate = baseApi<{
   return `/api/projects/${id}/deploy/${name}/${version}?cluster_id=${cluster_id}`;
 });
 
+const uninstallTemplate = baseApi<{
+}, {
+  id: number,
+  name: string, 
+  cluster_id: number,
+  namespace: string,
+  storage: StorageType,
+}>('POST', pathParams => {
+  let { id, name, cluster_id, storage, namespace } = pathParams;
+  return `/api/projects/${id}/deploy/${name}?cluster_id=${cluster_id}&namespace=${namespace}&storage=${storage}`;
+});
+
 const getClusterIntegrations = baseApi('GET', '/api/integrations/cluster');
 
 const getRegistryIntegrations = baseApi('GET', '/api/integrations/registry');
@@ -322,6 +334,7 @@ const createGKE = baseApi<{
 
 // Bundle export to allow default api import (api.<method> is more readable)
 export default {
+  uninstallTemplate,
   createGCR,
   createGKE,
   createGCPIntegration,

+ 3 - 0
go.sum

@@ -1454,8 +1454,11 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
 gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
+helm.sh/helm v1.2.1 h1:Jrn7kKQqQ/hnFWZEX+9pMFvYqFexkzrBnGqYBmIph7c=
+helm.sh/helm v2.17.0+incompatible h1:cSe3FaQOpRWLDXvTObQNj0P7WI98IG5yloU6tQVls2k=
 helm.sh/helm/v3 v3.3.4 h1:tbad6WQVMxEw1HlVBvI2rQqOblmI5lgXOrWAMwJ198M=
 helm.sh/helm/v3 v3.3.4/go.mod h1:CyCGQa53/k1JFxXvXveGwtfJ4cuB9zkaBSGa5rnAiHU=
+helm.sh/helm/v3 v3.5.0 h1:uqIT3Bh4hVEyZRThyTPik8FkiABj3VJIY+POvDFT3a4=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

+ 1 - 1
internal/config/config.go

@@ -29,7 +29,7 @@ type ServerConf struct {
 	IsLocal        bool          `env:"IS_LOCAL,default=false"`
 	IsTesting      bool          `env:"IS_TESTING,default=false"`
 
-	DefaultHelmRepoURL string `env:"HELM_REPO_URL,default=https://s2011r2593.github.io/test-porter-chart-repo/"`
+	DefaultHelmRepoURL string `env:"HELM_REPO_URL,default=https://porter-dev.github.io/chart-repo/"`
 
 	GithubClientID     string `env:"GITHUB_CLIENT_ID"`
 	GithubClientSecret string `env:"GITHUB_CLIENT_SECRET"`

+ 5 - 1
internal/forms/release.go

@@ -1,6 +1,7 @@
 package forms
 
 import (
+	"fmt"
 	"net/url"
 	"strconv"
 
@@ -19,6 +20,7 @@ func (rf *ReleaseForm) PopulateHelmOptionsFromQueryParams(
 	vals url.Values,
 	repo repository.ClusterRepository,
 ) error {
+	fmt.Println(vals)
 	if clusterID, ok := vals["cluster_id"]; ok && len(clusterID) == 1 {
 		id, err := strconv.ParseUint(clusterID[0], 10, 64)
 
@@ -31,15 +33,17 @@ func (rf *ReleaseForm) PopulateHelmOptionsFromQueryParams(
 		if err != nil {
 			return err
 		}
-
+		fmt.Println("setting cluster")
 		rf.Cluster = cluster
 	}
 
 	if namespace, ok := vals["namespace"]; ok && len(namespace) == 1 {
+		fmt.Println("setting namespace")
 		rf.Namespace = namespace[0]
 	}
 
 	if storage, ok := vals["storage"]; ok && len(storage) == 1 {
+		fmt.Println("setting storage")
 		rf.Storage = storage[0]
 	}
 

+ 8 - 0
internal/helm/agent.go

@@ -193,6 +193,14 @@ func (a *Agent) InstallChart(
 	return cmd.Run(conf.Chart, conf.Values)
 }
 
+// UninstallChart uninstalls a chart
+func (a *Agent) UninstallChart(
+	name string,
+) (*release.UninstallReleaseResponse, error) {
+	cmd := action.NewUninstall(a.ActionConfig)
+	return cmd.Run(name)
+}
+
 // RollbackRelease rolls a release back to a specified revision/version
 func (a *Agent) RollbackRelease(
 	name string,

+ 34 - 0
server/api/deploy_handler.go

@@ -140,3 +140,37 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 
 	w.WriteHeader(http.StatusOK)
 }
+
+// HandleUninstallTemplate triggers a chart deployment from a template
+func (app *App) HandleUninstallTemplate(w http.ResponseWriter, r *http.Request) {
+	name := chi.URLParam(r, "name")
+
+	form := &forms.GetReleaseForm{
+		ReleaseForm: &forms.ReleaseForm{
+			Form: &helm.Form{
+				Repo: app.Repo,
+			},
+		},
+		Name: name,
+	}
+
+	agent, err := app.getAgentFromQueryParams(
+		w,
+		r,
+		form.ReleaseForm,
+		form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
+	)
+
+	// errors are handled in app.getAgentFromQueryParams
+	if err != nil {
+		return
+	}
+
+	_, err = agent.UninstallChart(name)
+	if err != nil {
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+	return
+}

+ 15 - 0
server/router/router.go

@@ -908,6 +908,21 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
+		// /api/projects/{project_id}/deploy routes
+		r.Method(
+			"POST",
+			"/projects/{project_id}/deploy/{name}",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveClusterAccess(
+					requestlog.NewHandler(a.HandleUninstallTemplate, l),
+					mw.URLParam,
+					mw.QueryParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
 		// /api/projects/{project_id}/k8s routes
 		r.Method(
 			"GET",