Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Neon frontend #4624

Merged
merged 10 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading