Skip to content

Commit

Permalink
[POR-2202] allow users to link apps to their datastore from the dashb…
Browse files Browse the repository at this point in the history
…oard (#4170)
  • Loading branch information
Feroze Mohideen authored Jan 19, 2024
1 parent 1b97469 commit 1b897d3
Show file tree
Hide file tree
Showing 19 changed files with 553 additions and 348 deletions.
2 changes: 1 addition & 1 deletion api/server/router/porter_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,7 @@ func getPorterAppRoutes(
Router: r,
})

// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/revisions -> porter_app.NewCurrentAppRevisionHandler
// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/revisions -> porter_app.NewLatestAppRevisionsHandler
latestAppRevisionsEndpoint := factory.NewAPIEndpoint(
&types.APIRequestMetadata{
Verb: types.APIVerbGet,
Expand Down
3 changes: 3 additions & 0 deletions dashboard/src/assets/connect.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 47 additions & 2 deletions dashboard/src/lib/hooks/useDatabaseMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ import { Context } from "shared/Context";
type DatabaseHook = {
create: (values: DbFormData) => Promise<void>;
deleteDatastore: (name: string) => Promise<void>;
attachDatastoreToAppInstances: ({
name,
appInstanceIds,
clusterId,
}: {
name: string;
appInstanceIds: string[];
clusterId: number;
}) => Promise<void>;
};
type CreateDatastoreInput = {
name: string;
Expand Down Expand Up @@ -76,7 +85,7 @@ const clientDbToCreateInput = (values: DbFormData): CreateDatastoreInput => {
};

export const useDatabaseMethods = (): DatabaseHook => {
const { currentProject } = useContext(Context);
const { currentProject, currentCluster } = useContext(Context);

const queryClient = useQueryClient();

Expand Down Expand Up @@ -126,5 +135,41 @@ export const useDatabaseMethods = (): DatabaseHook => {
[currentProject]
);

return { create, deleteDatastore };
const attachDatastoreToAppInstances = useCallback(
async ({
name,
appInstanceIds,
}: {
name: string;
appInstanceIds: string[];
}): Promise<void> => {
if (
!currentProject?.id ||
currentProject.id === -1 ||
!currentCluster?.id ||
currentCluster.id === -1
) {
return;
}

await api.attachEnvGroup(
"<token>",
{
app_instance_ids: appInstanceIds,
env_group_name: name,
},
{
project_id: currentProject.id,
// NB: this endpoint does not actually use the cluster id, because the app instance id is used
// to deploy in its correct deployment target.
cluster_id: currentCluster.id,
}
);

await queryClient.invalidateQueries({ queryKey: ["getDatastore"] });
},
[currentProject, currentCluster]
);

return { create, deleteDatastore, attachDatastoreToAppInstances };
};
59 changes: 59 additions & 0 deletions dashboard/src/lib/hooks/useLatestAppRevisions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useQuery } from "@tanstack/react-query";
import { z } from "zod";

import {
appRevisionWithSourceValidator,
type AppRevisionWithSource,
} from "main/home/app-dashboard/apps/types";

import api from "shared/api";

// use this hook to get the latest revision of every app in the project/cluster
export const useLatestAppRevisions = ({
projectId,
clusterId,
}: {
projectId: number;
clusterId: number;
}): {
revisions: AppRevisionWithSource[];
} => {
const { data: apps = [] } = useQuery(
[
"getLatestAppRevisions",
{
cluster_id: clusterId,
project_id: projectId,
},
],
async () => {
if (clusterId === -1 || projectId === -1) {
return;
}

const res = await api.getLatestAppRevisions(
"<token>",
{
deployment_target_id: undefined,
ignore_preview_apps: true,
},
{ cluster_id: clusterId, project_id: projectId }
);

const apps = await z
.object({
app_revisions: z.array(appRevisionWithSourceValidator),
})
.parseAsync(res.data);

return apps.app_revisions;
},
{
refetchOnWindowFocus: false,
enabled: clusterId !== 0 && projectId !== 0,
}
);
return {
revisions: apps,
};
};
1 change: 1 addition & 0 deletions dashboard/src/lib/revisions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const appRevisionValidator = z.object({
id: z.string(),
created_at: z.string(),
updated_at: z.string(),
app_instance_id: z.string(),
});

export type AppRevision = z.infer<typeof appRevisionValidator>;
122 changes: 122 additions & 0 deletions dashboard/src/main/home/app-dashboard/apps/SelectableAppList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React, { useMemo } from "react";
import { PorterApp } from "@porter-dev/api-contracts";
import styled, { css } from "styled-components";

import Container from "components/porter/Container";
import Icon from "components/porter/Icon";
import Spacer from "components/porter/Spacer";
import Text from "components/porter/Text";
import { AppIcon, AppSource } from "main/home/app-dashboard/apps/AppMeta";

import healthy from "assets/status-healthy.png";

import { type AppRevisionWithSource } from "./types";

type SelectableAppRowProps = {
app: AppRevisionWithSource;
onSelect?: () => void;
onDeselect?: () => void;
selected?: boolean;
};

const SelectableAppRow: React.FC<SelectableAppRowProps> = ({
app,
selected,
onSelect,
onDeselect,
}) => {
const proto = useMemo(() => {
return PorterApp.fromJsonString(atob(app.app_revision.b64_app_proto), {
ignoreUnknownFields: true,
});
}, [app.app_revision.b64_app_proto]);

return (
<ResourceOption
selected={selected}
onClick={() => {
if (selected) {
onDeselect?.();
} else {
onSelect?.();
}
}}
isHoverable={onSelect != null || onDeselect != null}
>
<div>
<Container row>
<Spacer inline width="1px" />
<AppIcon buildpacks={proto.build?.buildpacks ?? []} />
<Spacer inline width="12px" />
<Text size={14}>{proto.name}</Text>
<Spacer inline x={1} />
</Container>
<Spacer height="15px" />
<Container row>
<AppSource source={app.source} />
<Spacer inline x={1} />
</Container>
</div>
{selected && <Icon height="18px" src={healthy} />}
</ResourceOption>
);
};

type AppListProps = {
appListItems: Array<{
app: AppRevisionWithSource;
key: string;
onSelect?: () => void;
onDeselect?: () => void;
isSelected?: boolean;
}>;
};

const SelectableAppList: React.FC<AppListProps> = ({ appListItems }) => {
return (
<StyledSelectableAppList>
{appListItems.map((ali) => {
return (
<SelectableAppRow
key={ali.key}
app={ali.app}
selected={ali.isSelected}
onSelect={ali.onSelect}
onDeselect={ali.onDeselect}
/>
);
})}
</StyledSelectableAppList>
);
};

export default SelectableAppList;

const StyledSelectableAppList = styled.div`
display: flex;
row-gap: 10px;
flex-direction: column;
max-height: 400px;
overflow-y: scroll;
`;

const ResourceOption = styled.div<{ selected?: boolean; isHoverable: boolean }>`
background: ${(props) => props.theme.clickable.bg};
border: 1px solid
${(props) => (props.selected ? "#ffffff" : props.theme.border)};
width: 100%;
padding: 10px 15px;
border-radius: 5px;
display: flex;
justify-content: space-between;
align-items: center;
${(props) => props.isHoverable && "cursor: pointer;"}
${(props) =>
props.isHoverable &&
!props.selected &&
css`
&:hover {
border: 1px solid #7a7b80;
}
`}
`;
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import React, { useContext } from "react";
import { useQuery } from "@tanstack/react-query";
import { useHistory } from "react-router";
import styled from "styled-components";
import { z } from "zod";

import Loading from "components/Loading";
import Button from "components/porter/Button";
import Fieldset from "components/porter/Fieldset";
import Spacer from "components/porter/Spacer";
import Text from "components/porter/Text";
import { appRevisionWithSourceValidator } from "main/home/app-dashboard/apps/types";
import { useLatestAppRevisions } from "lib/hooks/useLatestAppRevisions";

import api from "shared/api";
import { Context } from "shared/Context";

import { ConfigurableAppRow } from "./ConfigurableAppRow";
Expand All @@ -22,46 +19,10 @@ export const ConfigurableAppList: React.FC = () => {

const { currentProject, currentCluster } = useContext(Context);

const { data: apps = [], status } = useQuery(
[
"getLatestAppRevisions",
{
cluster_id: currentCluster?.id,
project_id: currentProject?.id,
},
],
async () => {
if (
!currentCluster ||
!currentProject ||
currentCluster.id === -1 ||
currentProject.id === -1
) {
return;
}

const res = await api.getLatestAppRevisions(
"<token>",
{
deployment_target_id: undefined,
ignore_preview_apps: true,
},
{ cluster_id: currentCluster.id, project_id: currentProject.id }
);

const apps = await z
.object({
app_revisions: z.array(appRevisionWithSourceValidator),
})
.parseAsync(res.data);

return apps.app_revisions;
},
{
refetchOnWindowFocus: false,
enabled: !!currentCluster && !!currentProject,
}
);
const { revisions: apps } = useLatestAppRevisions({
projectId: currentProject?.id ?? 0,
clusterId: currentCluster?.id ?? 0,
});

if (status === "loading") {
return <Loading offset="-150px" />;
Expand Down
Loading

0 comments on commit 1b897d3

Please sign in to comment.