Skip to content

Commit

Permalink
Neon frontend (#4624)
Browse files Browse the repository at this point in the history
  • Loading branch information
Feroze Mohideen authored May 8, 2024
1 parent 7139832 commit 8e442ec
Show file tree
Hide file tree
Showing 34 changed files with 1,062 additions and 125 deletions.
2 changes: 1 addition & 1 deletion api/server/handlers/addons/tailscale_services.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func (c *TailscaleServicesHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
return
}

var services []TailscaleService
services := make([]TailscaleService, 0)
for _, svc := range svcList.Items {
var port int
if len(svc.Spec.Ports) > 0 {
Expand Down
3 changes: 3 additions & 0 deletions api/server/handlers/datastore/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ func (h *UpdateDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
MasterUserPasswordLiteral: pointer.String(datastoreValues.Config.MasterUserPassword),
},
}
case "NEON":
datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_NEON
datastoreProto.KindValues = &porterv1.ManagedDatastore_NeonKind{}
default:
err = telemetry.Error(ctx, span, nil, "invalid datastore type")
h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
Expand Down
69 changes: 69 additions & 0 deletions api/server/handlers/neon_integration/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package neon_integration

import (
"net/http"
"time"

"github.com/porter-dev/porter/api/server/handlers"
"github.com/porter-dev/porter/api/server/shared"
"github.com/porter-dev/porter/api/server/shared/apierrors"
"github.com/porter-dev/porter/api/server/shared/config"
"github.com/porter-dev/porter/api/types"
"github.com/porter-dev/porter/internal/models"
"github.com/porter-dev/porter/internal/telemetry"
)

// ListNeonIntegrationsHandler is a struct for listing all noen integrations for a given project
type ListNeonIntegrationsHandler struct {
handlers.PorterHandlerReadWriter
}

// NewListNeonIntegrationsHandler constructs a ListNeonIntegrationsHandler
func NewListNeonIntegrationsHandler(
config *config.Config,
decoderValidator shared.RequestDecoderValidator,
writer shared.ResultWriter,
) *ListNeonIntegrationsHandler {
return &ListNeonIntegrationsHandler{
PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
}
}

// NeonIntegration describes a neon integration
type NeonIntegration struct {
CreatedAt time.Time `json:"created_at"`
}

// ListNeonIntegrationsResponse describes the list neon integrations response body
type ListNeonIntegrationsResponse struct {
// Integrations is a list of neon integrations
Integrations []NeonIntegration `json:"integrations"`
}

// ServeHTTP returns a list of neon integrations associated with the specified project
func (h *ListNeonIntegrationsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx, span := telemetry.NewSpan(r.Context(), "serve-list-neon-integrations")
defer span.End()

project, _ := ctx.Value(types.ProjectScope).(*models.Project)

resp := ListNeonIntegrationsResponse{}
integrationList := make([]NeonIntegration, 0)

integrations, err := h.Repo().NeonIntegration().Integrations(ctx, project.ID)
if err != nil {
err := telemetry.Error(ctx, span, err, "error getting datastores")
h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
return
}

for _, int := range integrations {
integrationList = append(integrationList, NeonIntegration{
CreatedAt: int.CreatedAt,
})
}

resp.Integrations = integrationList

h.WriteResult(w, r, resp)
}
1 change: 1 addition & 0 deletions api/server/handlers/oauth_callback/neon.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func (p *OAuthCallbackNeonHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ

oauthInt := integrations.NeonIntegration{
SharedOAuthModel: integrations.SharedOAuthModel{
ClientID: []byte(p.Config().NeonConf.ClientID),
AccessToken: []byte(token.AccessToken),
RefreshToken: []byte(token.RefreshToken),
Expiry: token.Expiry,
Expand Down
28 changes: 28 additions & 0 deletions api/server/router/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/porter-dev/porter/api/server/handlers/cloud_provider"
"github.com/porter-dev/porter/api/server/handlers/neon_integration"

"github.com/porter-dev/porter/api/server/handlers/deployment_target"

Expand Down Expand Up @@ -2021,5 +2022,32 @@ func getProjectRoutes(
Router: r,
})

// GET /api/projects/{project_id}/neon-integrations -> apiContract.NewListNeonIntegrationsHandler
listNeonIntegrationsEndpoint := factory.NewAPIEndpoint(
&types.APIRequestMetadata{
Verb: types.APIVerbGet,
Method: types.HTTPVerbGet,
Path: &types.Path{
Parent: basePath,
RelativePath: relPath + "/neon-integrations",
},
Scopes: []types.PermissionScope{
types.UserScope,
types.ProjectScope,
},
},
)

listNeonIntegrationsHandler := neon_integration.NewListNeonIntegrationsHandler(
config,
factory.GetDecoderValidator(),
factory.GetResultWriter(),
)
routes = append(routes, &router.Route{
Endpoint: listNeonIntegrationsEndpoint,
Handler: listNeonIntegrationsHandler,
Router: r,
})

return routes, newPath
}
20 changes: 20 additions & 0 deletions dashboard/src/assets/neon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions dashboard/src/components/porter/BlockSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ const Block = styled.div<{ selected?: boolean; disabled?: boolean }>`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
align-items: left;
user-select: none;
font-size: 13px;
Expand Down
25 changes: 24 additions & 1 deletion dashboard/src/lib/databases/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const datastoreTypeValidator = z.enum([
"ELASTICACHE",
"MANAGED_REDIS",
"MANAGED_POSTGRES",
"NEON",
]);
const datastoreEngineValidator = z.enum([
"UNKNOWN",
Expand Down Expand Up @@ -109,6 +110,10 @@ export const DATASTORE_TYPE_MANAGED_REDIS: DatastoreType = {
name: "MANAGED_REDIS" as const,
displayName: "Managed Redis",
};
export const DATASTORE_TYPE_NEON: DatastoreType = {
name: "NEON" as const,
displayName: "Neon",
};

export type DatastoreState = {
state: z.infer<typeof datastoreValidator>["status"];
Expand Down Expand Up @@ -159,6 +164,19 @@ export const DATASTORE_STATE_DELETED: DatastoreState = {
displayName: "Wrapping up",
};

export type DatastoreTab = {
name: string;
displayName: string;
component: React.FC;
isOnlyForPorterOperators?: boolean;
};

export const DEFAULT_DATASTORE_TAB = {
name: "configuration",
displayName: "Configuration",
component: () => null,
};

export type DatastoreTemplate = {
highLevelType: DatastoreEngine; // this was created so that rds aurora postgres and rds postgres can be grouped together
type: DatastoreType;
Expand All @@ -170,9 +188,9 @@ export type DatastoreTemplate = {
disabled: boolean;
instanceTiers: ResourceOption[];
supportedEngineVersions: EngineVersion[];
formTitle: string;
creationStateProgression: DatastoreState[];
deletionStateProgression: DatastoreState[];
tabs: DatastoreTab[]; // this what is rendered on the dashboard after the datastore is deployed
};

const instanceTierValidator = z.enum([
Expand Down Expand Up @@ -312,6 +330,10 @@ const managedRedisConfigValidator = z.object({
.default(1),
});

const neonValidator = z.object({
type: z.literal("neon"),
});

export const dbFormValidator = z.object({
name: z
.string()
Expand All @@ -332,6 +354,7 @@ export const dbFormValidator = z.object({
elasticacheRedisConfigValidator,
managedRedisConfigValidator,
managedPostgresConfigValidator,
neonValidator,
]),
clusterId: z.number(),
});
Expand Down
4 changes: 2 additions & 2 deletions dashboard/src/lib/hooks/useAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export const useAddonList = ({
"monitoring",
"porter-agent-system",
"external-secrets",
"infisical"
"infisical",
].includes(a.namespace ?? "");
});
},
Expand Down Expand Up @@ -552,7 +552,7 @@ export const useAddonLogs = ({
projectId?: number;
deploymentTarget: DeploymentTarget;
addon?: ClientAddon;
}): { logs: Log[]; refresh: () => void; isInitializing: boolean } => {
}): { logs: Log[]; refresh: () => Promise<void>; isInitializing: boolean } => {
const [logs, setLogs] = useState<Log[]>([]);
const logsBufferRef = useRef<Log[]>([]);
const { newWebsocket, openWebsocket, closeAllWebsockets } = useWebsockets();
Expand Down
52 changes: 52 additions & 0 deletions dashboard/src/lib/hooks/useAuthWindow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useEffect, useState } from "react";

/**
* Hook to open an authentication window at a given url.
* Once the auth flow redirects back to Porter, the window is closed.
*/
export const useAuthWindow = ({
authUrl,
}: {
authUrl: string;
}): {
openAuthWindow: () => void;
} => {
const [authWindow, setAuthWindow] = useState<Window | null>(null);

const openAuthWindow = (): void => {
const windowObjectReference = window.open(
authUrl,
"porterAuthWindow",
"width=600,height=700,left=200,top=200"
);
setAuthWindow(windowObjectReference);
};

useEffect(() => {
const interval = setInterval(() => {
if (authWindow) {
try {
if (
authWindow.location.hostname.includes("dashboard.getporter.dev") ||
authWindow.location.hostname.includes("localhost")
) {
authWindow.close();
setAuthWindow(null);
clearInterval(interval);
}
} catch (e) {
console.log("Error accessing the authentication window.", e);
}
}
}, 1000);

return () => {
clearInterval(interval);
if (authWindow) {
authWindow.close();
}
};
}, [authWindow]);

return { openAuthWindow };
};
15 changes: 14 additions & 1 deletion dashboard/src/lib/hooks/useDatastore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type DatastoreHook = {
};
type CreateDatastoreInput = {
name: string;
type: "RDS" | "ELASTICACHE" | "MANAGED-POSTGRES" | "MANAGED-REDIS";
type: "RDS" | "ELASTICACHE" | "MANAGED-POSTGRES" | "MANAGED-REDIS" | "NEON";
engine: "POSTGRES" | "AURORA-POSTGRES" | "REDIS";
values: object;
};
Expand Down Expand Up @@ -134,6 +134,19 @@ const clientDbToCreateInput = (values: DbFormData): CreateDatastoreInput => {
};
}
)
.with(
{ config: { type: "neon" } },
(values): CreateDatastoreInput => ({
name: values.name,
values: {
config: {
name: values.name,
},
},
type: "NEON",
engine: "POSTGRES",
})
)
.exhaustive();
};

Expand Down
41 changes: 41 additions & 0 deletions dashboard/src/lib/hooks/useNeon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { z } from "zod";

import {
neonIntegrationValidator,
type ClientNeonIntegration,
} from "lib/neon/types";

import api from "shared/api";

type TUseNeon = {
getNeonIntegrations: ({
projectId,
}: {
projectId: number;
}) => Promise<ClientNeonIntegration[]>;
};
export const useNeon = (): TUseNeon => {
const getNeonIntegrations = async ({
projectId,
}: {
projectId: number;
}): Promise<ClientNeonIntegration[]> => {
const response = await api.getNeonIntegrations(
"<token>",
{},
{
projectId,
}
);

const results = await z
.object({ integrations: z.array(neonIntegrationValidator) })
.parseAsync(response.data);

return results.integrations;
};

return {
getNeonIntegrations,
};
};
6 changes: 6 additions & 0 deletions dashboard/src/lib/neon/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { z } from "zod";

export const neonIntegrationValidator = z.object({
created_at: z.string(),
});
export type ClientNeonIntegration = z.infer<typeof neonIntegrationValidator>;
Loading

0 comments on commit 8e442ec

Please sign in to comment.