Просмотр исходного кода

Merge branch 'gitlab-integration' of github.com:porter-dev/porter into dev

jnfrati 4 лет назад
Родитель
Сommit
940f407929

+ 24 - 0
dashboard/package-lock.json

@@ -1670,6 +1670,30 @@
         "@types/node": "*"
         "@types/node": "*"
       }
       }
     },
     },
+    "@types/color": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz",
+      "integrity": "sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==",
+      "dev": true,
+      "requires": {
+        "@types/color-convert": "*"
+      }
+    },
+    "@types/color-convert": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz",
+      "integrity": "sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==",
+      "dev": true,
+      "requires": {
+        "@types/color-name": "*"
+      }
+    },
+    "@types/color-name": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
+      "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
+      "dev": true
+    },
     "@types/connect": {
     "@types/connect": {
       "version": "3.4.35",
       "version": "3.4.35",
       "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
       "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",

+ 85 - 43
dashboard/src/components/repo-selector/RepoList.tsx

@@ -1,4 +1,4 @@
-import React, { useContext, useEffect, useState } from "react";
+import React, { useContext, useEffect, useRef, useState } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import github from "assets/github.png";
 import github from "assets/github.png";
 
 
@@ -9,6 +9,7 @@ import { Context } from "shared/Context";
 import Loading from "../Loading";
 import Loading from "../Loading";
 import SearchBar from "../SearchBar";
 import SearchBar from "../SearchBar";
 import DynamicLink from "components/DynamicLink";
 import DynamicLink from "components/DynamicLink";
+import { useOutsideAlerter } from "shared/hooks/useOutsideAlerter";
 
 
 type Props = {
 type Props = {
   actionConfig: ActionConfigType | null;
   actionConfig: ActionConfigType | null;
@@ -173,13 +174,13 @@ const RepoList: React.FC<Props> = ({
       if (currentProvider.provider === "gitlab") {
       if (currentProvider.provider === "gitlab") {
         return (
         return (
           <LoadingWrapper>
           <LoadingWrapper>
-            We couldn't reach gitlab to get your repos. You can
+            GitLab could not be reached.
             <A
             <A
               to={`${window.location.origin}/api/projects/${currentProject.id}/oauth/gitlab?integration_id=${currentProvider.integration_id}`}
               to={`${window.location.origin}/api/projects/${currentProject.id}/oauth/gitlab?integration_id=${currentProvider.integration_id}`}
             >
             >
-              Connect your gitlab account to porter
+              Connect your GitLab account to Porter
             </A>
             </A>
-            or select another git provider.
+            or select another Git provider.
           </LoadingWrapper>
           </LoadingWrapper>
         );
         );
       } else {
       } else {
@@ -242,7 +243,7 @@ const RepoList: React.FC<Props> = ({
     } else {
     } else {
       return (
       return (
         <>
         <>
-          <div style={{ display: "flex" }}>
+          <div style={{ display: "flex", marginBottom: "10px" }}>
             <ProviderSelector
             <ProviderSelector
               values={providers}
               values={providers}
               currentValue={currentProvider}
               currentValue={currentProvider}
@@ -251,7 +252,7 @@ const RepoList: React.FC<Props> = ({
             <SearchBar
             <SearchBar
               setSearchFilter={setSearchFilter}
               setSearchFilter={setSearchFilter}
               disabled={repoError || repoLoading}
               disabled={repoError || repoLoading}
-              prompt={"Search repos..."}
+              prompt={"Search repos . . ."}
               fullWidth
               fullWidth
             />
             />
           </div>
           </div>
@@ -277,18 +278,16 @@ const RepoList: React.FC<Props> = ({
                   justifyContent: "center",
                   justifyContent: "center",
                 }}
                 }}
               >
               >
-                <div>We couldn't find any git provider to connect with.</div>
+                <div>A connected Git provider wasn't found.</div>
                 <div>
                 <div>
                   You can
                   You can
                   <A
                   <A
                     to={`${window.location.origin}/api/integrations/github-app/install`}
                     to={`${window.location.origin}/api/integrations/github-app/install`}
                   >
                   >
-                    Add Github repositories
+                    connect a GitHub repo
                   </A>
                   </A>
                   or
                   or
-                  <A to={"/integrations"}>
-                    Add a Gitlab instance to your project
-                  </A>
+                  <A to={"/integrations"}>add a GitLab instance</A>
                 </div>
                 </div>
               </div>
               </div>
             </LoadingWrapper>
             </LoadingWrapper>
@@ -308,73 +307,116 @@ const ProviderSelector = (props: {
   currentValue: any;
   currentValue: any;
   onChange: (provider: any) => void;
   onChange: (provider: any) => void;
 }) => {
 }) => {
+  const wrapperRef = useRef();
   const { values, currentValue, onChange } = props;
   const { values, currentValue, onChange } = props;
   const [isOpen, setIsOpen] = useState(false);
   const [isOpen, setIsOpen] = useState(false);
   const icon = `devicon-${currentValue?.provider}-plain colored`;
   const icon = `devicon-${currentValue?.provider}-plain colored`;
+  useOutsideAlerter(wrapperRef, () => {
+    setIsOpen(false);
+  });
   return (
   return (
-    <ProviderSelectorStyles.Wrapper>
+    <ProviderSelectorStyles.Wrapper ref={wrapperRef} isOpen={isOpen}>
+      <ProviderSelectorStyles.Icon className={icon} />
       <ProviderSelectorStyles.Button onClick={() => setIsOpen((prev) => !prev)}>
       <ProviderSelectorStyles.Button onClick={() => setIsOpen((prev) => !prev)}>
-        <ProviderSelectorStyles.Icon className={icon} />
         {currentValue?.name || currentValue?.instance_url}
         {currentValue?.name || currentValue?.instance_url}
       </ProviderSelectorStyles.Button>
       </ProviderSelectorStyles.Button>
+      <i className="material-icons">arrow_drop_down</i>
       {isOpen ? (
       {isOpen ? (
-        <ProviderSelectorStyles.OptionWrapper>
-          {values.map((provider) => {
-            return (
-              <ProviderSelectorStyles.Option onClick={() => onChange(provider)}>
-                <ProviderSelectorStyles.Icon
-                  className={`devicon-${provider?.provider}-plain colored`}
-                />
-                {provider?.name || provider?.instance_url}
-              </ProviderSelectorStyles.Option>
-            );
-          })}
-        </ProviderSelectorStyles.OptionWrapper>
+        <>
+          <ProviderSelectorStyles.OptionWrapper>
+            {values.map((provider) => {
+              return (
+                <ProviderSelectorStyles.Option
+                  onClick={() => {
+                    setIsOpen(false);
+                    onChange(provider);
+                  }}
+                >
+                  <ProviderSelectorStyles.Icon
+                    className={`devicon-${provider?.provider}-plain colored`}
+                  />
+                  <ProviderSelectorStyles.Text>
+                    {provider?.name || provider?.instance_url}
+                  </ProviderSelectorStyles.Text>
+                </ProviderSelectorStyles.Option>
+              );
+            })}
+          </ProviderSelectorStyles.OptionWrapper>
+        </>
       ) : null}
       ) : null}
     </ProviderSelectorStyles.Wrapper>
     </ProviderSelectorStyles.Wrapper>
   );
   );
 };
 };
 
 
 const ProviderSelectorStyles = {
 const ProviderSelectorStyles = {
-  Wrapper: styled.div`
+  Wrapper: styled.div<{ isOpen?: boolean }>`
     position: relative;
     position: relative;
     margin-bottom: 10px;
     margin-bottom: 10px;
-    margin-right: 5px;
+    height: 40px;
+    display: flex;
+    min-width: 50%;
+    cursor: pointer;
+    margin-right: 10px;
+    margin-left: 2px;
+    align-items: center;
+
+    > i {
+      margin-left: -26px;
+      margin-right: 10px;
+      z-index: 0;
+      transform: ${(props) => (props.isOpen ? "rotate(180deg)" : "")};
+    }
   `,
   `,
   Button: styled.div`
   Button: styled.div`
-    border-radius: 5px;
-    border: 1px solid white;
     height: 100%;
     height: 100%;
+    font-weight: bold;
+    font-size: 14px;
     border-bottom: 0;
     border-bottom: 0;
-    border: 1px solid #ffffff55;
-    border-radius: 3px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
+    z-index: 999;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
     padding: 6px 15px;
     padding: 6px 15px;
+    padding-left: 40px;
+    padding-right: 28px;
+    border-bottom: 2px solid #ffffff;
+    padding-top: 11px;
   `,
   `,
   OptionWrapper: styled.div`
   OptionWrapper: styled.div`
+    top: 40px;
     position: absolute;
     position: absolute;
-    background-color: #202227;
+    background: #37393f;
+    border-radius: 3px;
+    width: calc(100% - 4px);
+    box-shadow: 0 8px 20px 0px #00000088;
   `,
   `,
   Option: styled.div`
   Option: styled.div`
-    white-space: nowrap;
-    padding: 8px 10px;
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
-    :not(:last-child) {
-      border-bottom: 1px solid black;
-      border-bottom-width: 90%;
-    }
+
     :hover {
     :hover {
-      background-color: #363940;
+      background-color: #ffffff22;
     }
     }
   `,
   `,
   Icon: styled.span`
   Icon: styled.span`
     font-size: 24px;
     font-size: 24px;
-    margin-right: 15px;
+    margin-left: 9px;
+    margin-right: -29px;
     color: white;
     color: white;
   `,
   `,
+  Text: styled.div`
+    font-weight: bold;
+    font-size: 14px;
+    margin-left: 40px;
+    height: 45px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    padding: 8px 10px;
+    width: 100%;
+    padding-top: 14px;
+    padding-left: 0;
+  `,
 };
 };
 
 
 const RepoListWrapper = styled.div`
 const RepoListWrapper = styled.div`

+ 1 - 1
dashboard/src/main/home/integrations/GitlabIntegrationList.tsx

@@ -42,7 +42,7 @@ const GitlabIntegrationList: React.FC<Props> = (props) => {
             );
             );
           })
           })
         ) : (
         ) : (
-          <Placeholder>No GitLab instances setted up yet.</Placeholder>
+          <Placeholder>No GitLab instances found</Placeholder>
         )}
         )}
       </StyledIntegrationList>
       </StyledIntegrationList>
     </>
     </>

+ 5 - 3
dashboard/src/main/home/integrations/create-integration/GitlabForm.tsx

@@ -91,7 +91,7 @@ const GitlabForm: React.FC<Props> = () => {
     <>
     <>
       <StyledForm>
       <StyledForm>
         <CredentialWrapper>
         <CredentialWrapper>
-          <Heading>Gitlab instance settings</Heading>
+          <Heading>GitLab Instance Settings</Heading>
 
 
           <InputRow
           <InputRow
             type="string"
             type="string"
@@ -104,7 +104,7 @@ const GitlabForm: React.FC<Props> = () => {
           />
           />
           <InputRow
           <InputRow
             type="string"
             type="string"
-            label="Client ID"
+            label="Client Application ID"
             value={clientId}
             value={clientId}
             setValue={(val: string) => setClientId(val)}
             setValue={(val: string) => setClientId(val)}
             isRequired
             isRequired
@@ -123,8 +123,10 @@ const GitlabForm: React.FC<Props> = () => {
         </CredentialWrapper>
         </CredentialWrapper>
         <SaveButton
         <SaveButton
           onClick={submit}
           onClick={submit}
-          text="Save new Gitlab instance"
+          makeFlush={true}
+          text="Save Gitlab Settings"
           status={buttonStatus || error?.message}
           status={buttonStatus || error?.message}
+          
         />
         />
       </StyledForm>
       </StyledForm>
     </>
     </>

+ 27 - 2
dashboard/src/main/home/project-settings/ProjectSettings.tsx

@@ -43,7 +43,7 @@ class ProjectSettings extends Component<PropsType, StateType> {
       !this.state.tabOptions.find((t) => t.value === "billing")
       !this.state.tabOptions.find((t) => t.value === "billing")
     ) {
     ) {
       const tabOptions = this.state.tabOptions;
       const tabOptions = this.state.tabOptions;
-      tabOptions.splice(1, 0, { value: "billing", label: "Billing" });
+      // tabOptions.splice(1, 0, { value: "billing", label: "Billing" });
       this.setState({ tabOptions });
       this.setState({ tabOptions });
       return;
       return;
     }
     }
@@ -56,7 +56,7 @@ class ProjectSettings extends Component<PropsType, StateType> {
       const billingIndex = this.state.tabOptions.findIndex(
       const billingIndex = this.state.tabOptions.findIndex(
         (t) => t.value === "billing"
         (t) => t.value === "billing"
       );
       );
-      tabOptions.splice(billingIndex, 1);
+      // tabOptions.splice(billingIndex, 1);
     }
     }
   }
   }
 
 
@@ -65,6 +65,10 @@ class ProjectSettings extends Component<PropsType, StateType> {
     this.setState({ projectName: currentProject.name });
     this.setState({ projectName: currentProject.name });
     const tabOptions = [];
     const tabOptions = [];
     tabOptions.push({ value: "manage-access", label: "Manage Access" });
     tabOptions.push({ value: "manage-access", label: "Manage Access" });
+    tabOptions.push({
+      value: "billing",
+      label: "Billing",
+    });
 
 
     if (this.props.isAuthorized("settings", "", ["get", "delete"])) {
     if (this.props.isAuthorized("settings", "", ["get", "delete"])) {
       if (this.context?.hasBillingEnabled) {
       if (this.context?.hasBillingEnabled) {
@@ -111,6 +115,14 @@ class ProjectSettings extends Component<PropsType, StateType> {
       return <InvitePage />;
       return <InvitePage />;
     } else if (this.state.currentTab === "api-tokens") {
     } else if (this.state.currentTab === "api-tokens") {
       return <APITokensSection />;
       return <APITokensSection />;
+    } else if (this.state.currentTab === "billing") {
+      return (
+        <Placeholder>
+          <Helper>
+          Please contact <a href="mailto:support@porter.run">support@porter.run</a> to upgrade your project's usage limits.
+          </Helper>
+        </Placeholder>
+      );
     } else {
     } else {
       return (
       return (
         <>
         <>
@@ -173,6 +185,19 @@ ProjectSettings.contextType = Context;
 
 
 export default withRouter(withAuth(ProjectSettings));
 export default withRouter(withAuth(ProjectSettings));
 
 
+const Placeholder = styled.div`
+  width: 100%;
+  height: 200px;
+  background: #ffffff11;
+  border-radius: 3px;
+  display: flex;
+  align-items: center;
+  text-align: center;
+  padding: 0 30px;
+  justify-content: center;
+  padding-bottom: 10px;
+`;
+
 const Warning = styled.div`
 const Warning = styled.div`
   font-size: 13px;
   font-size: 13px;
   color: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
   color: ${(props: { highlight: boolean; makeFlush?: boolean }) =>

+ 2 - 2
dashboard/src/shared/common.tsx

@@ -107,8 +107,8 @@ export const integrationList: any = {
   },
   },
   gitlab: {
   gitlab: {
     icon: "https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png",
     icon: "https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png",
-    label: "Gitlab",
-    buttonText: "Add instance",
+    label: "GitLab",
+    buttonText: "Add an Instance",
   },
   },
   rds: {
   rds: {
     icon:
     icon:

+ 30 - 0
dashboard/src/shared/hooks/useOutsideAlerter.ts

@@ -0,0 +1,30 @@
+import React, { MutableRefObject, RefObject, useEffect, useRef } from "react";
+
+/**
+ * Hook that alerts clicks outside of the passed ref
+ */
+export function useOutsideAlerter(
+  ref: React.RefObject<HTMLDivElement>,
+  callback: () => void
+) {
+  useEffect(() => {
+    /**
+     * Alert if clicked on outside of element
+     */
+    function handleClickOutside(event: any) {
+      if (
+        ref.current &&
+        !ref.current.contains(event.target) &&
+        typeof callback === "function"
+      ) {
+        callback();
+      }
+    }
+    // Bind the event listener
+    document.addEventListener("mousedown", handleClickOutside);
+    return () => {
+      // Unbind the event listener on clean up
+      document.removeEventListener("mousedown", handleClickOutside);
+    };
+  }, [ref]);
+}

+ 6 - 5
go.mod

@@ -45,7 +45,7 @@ require (
 	github.com/spf13/cobra v1.4.0
 	github.com/spf13/cobra v1.4.0
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.10.0
 	github.com/spf13/viper v1.10.0
-	github.com/stretchr/testify v1.7.0
+	github.com/stretchr/testify v1.7.1
 	golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
 	golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
 	golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2
 	golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2
 	golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
 	golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
@@ -79,6 +79,7 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.5.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.5.0 // indirect
 	github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect
 	github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect
+	github.com/cosmtrek/air v1.30.0 // indirect
 	github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
 	github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
@@ -131,7 +132,7 @@ require (
 	github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
 	github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
 	github.com/evanphx/json-patch v4.12.0+incompatible // indirect
 	github.com/evanphx/json-patch v4.12.0+incompatible // indirect
 	github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
 	github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
-	github.com/fsnotify/fsnotify v1.5.1 // indirect
+	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/fvbommel/sortorder v1.0.1 // indirect
 	github.com/fvbommel/sortorder v1.0.1 // indirect
 	github.com/gdamore/encoding v1.0.0 // indirect
 	github.com/gdamore/encoding v1.0.0 // indirect
 	github.com/gdamore/tcell/v2 v2.5.1 // indirect
 	github.com/gdamore/tcell/v2 v2.5.1 // indirect
@@ -162,7 +163,7 @@ require (
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/heroku/color v0.0.6 // indirect
 	github.com/heroku/color v0.0.6 // indirect
 	github.com/huandu/xstrings v1.3.2 // indirect
 	github.com/huandu/xstrings v1.3.2 // indirect
-	github.com/imdario/mergo v0.3.12 // indirect
+	github.com/imdario/mergo v0.3.13 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/itchyny/astgen-go v0.0.0-20210113000433-0da0671862a3 // indirect
 	github.com/itchyny/astgen-go v0.0.0-20210113000433-0da0671862a3 // indirect
 	github.com/itchyny/timefmt-go v0.1.1 // indirect
 	github.com/itchyny/timefmt-go v0.1.1 // indirect
@@ -249,7 +250,7 @@ require (
 	go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
 	go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
 	golang.org/x/mod v0.5.1 // indirect
 	golang.org/x/mod v0.5.1 // indirect
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
-	golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
+	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
 	golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
 	golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
 	golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
@@ -260,7 +261,7 @@ require (
 	gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect
 	gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect
 	gopkg.in/src-d/go-git.v4 v4.13.1 // indirect
 	gopkg.in/src-d/go-git.v4 v4.13.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	gopkg.in/yaml.v3 v3.0.0 // indirect
 	k8s.io/apiextensions-apiserver v0.23.1 // indirect
 	k8s.io/apiextensions-apiserver v0.23.1 // indirect
 	k8s.io/apiserver v0.23.1 // indirect
 	k8s.io/apiserver v0.23.1 // indirect
 	k8s.io/component-base v0.23.1 // indirect
 	k8s.io/component-base v0.23.1 // indirect

+ 13 - 0
go.sum

@@ -457,6 +457,8 @@ github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cosmtrek/air v1.30.0 h1:Ge27Ye0ZXIcxslU+zy7WKPZAsSz+ws5L+BUGmIRvTLg=
+github.com/cosmtrek/air v1.30.0/go.mod h1:eNJLyoFnfPbr6F/h6FVFyL2Y0ia7A61ULjtk+EB5uaY=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@@ -586,6 +588,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
 github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
 github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
 github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
+github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
+github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
 github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
 github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
 github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE=
 github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE=
 github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
 github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
@@ -942,6 +946,8 @@ github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
+github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
 github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
 github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@@ -1553,6 +1559,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
@@ -2037,8 +2045,11 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
 golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
 golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -2391,6 +2402,8 @@ gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gorm.io/driver/postgres v1.2.3 h1:f4t0TmNMy9gh3TU2PX+EppoA6YsgFnyq8Ojtddb42To=
 gorm.io/driver/postgres v1.2.3 h1:f4t0TmNMy9gh3TU2PX+EppoA6YsgFnyq8Ojtddb42To=
 gorm.io/driver/postgres v1.2.3/go.mod h1:pJV6RgYQPG47aM1f0QeOzFH9HxQc8JcmAgjRCgS0wjs=
 gorm.io/driver/postgres v1.2.3/go.mod h1:pJV6RgYQPG47aM1f0QeOzFH9HxQc8JcmAgjRCgS0wjs=
 gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
 gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=