feat: 🎸 add live refreshing to Environments & API key page

This commit is contained in:
SSHari
2023-07-15 18:24:33 -04:00
parent 2d722b94bd
commit 5288ca3840
4 changed files with 192 additions and 2 deletions
@@ -0,0 +1,143 @@
import { PrismaClient, prisma } from "~/db.server";
import { Project } from "~/models/project.server";
import { User } from "~/models/user.server";
import { sse } from "~/utils/sse";
type EnvironmentSignalsMap = {
[x: string]: {
lastUpdatedAt: number;
lastTotalEndpointUpdatedTime: number;
lastTotalIndexingUpdatedTime: number;
};
};
export class EnvironmentsStreamPresenter {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call({
request,
userId,
projectSlug,
}: {
request: Request;
userId: User["id"];
projectSlug: Project["slug"];
}) {
let lastEnvironmentSignals: EnvironmentSignalsMap;
return sse({
request,
run: async (send, stop) => {
const nextEnvironmentSignals = await this.#runForUpdates({
userId,
projectSlug,
});
if (!nextEnvironmentSignals) {
return stop();
}
const lastEnvironmentIds = lastEnvironmentSignals
? Object.keys(lastEnvironmentSignals)
: [];
const nextEnvironmentIds = Object.keys(nextEnvironmentSignals);
if (
//push update if the number of environments is different
nextEnvironmentIds.length !== lastEnvironmentIds.length ||
//push update if the list of ids is different
lastEnvironmentIds.some((id) => !nextEnvironmentSignals[id]) ||
nextEnvironmentIds.some((id) => !lastEnvironmentSignals[id]) ||
//push update if any signals changed
nextEnvironmentIds.some(
(id) =>
nextEnvironmentSignals[id].lastUpdatedAt !==
lastEnvironmentSignals[id].lastUpdatedAt ||
nextEnvironmentSignals[id].lastTotalEndpointUpdatedTime !==
lastEnvironmentSignals[id].lastTotalEndpointUpdatedTime ||
nextEnvironmentSignals[id].lastTotalIndexingUpdatedTime !==
lastEnvironmentSignals[id].lastTotalIndexingUpdatedTime
)
) {
send({ data: new Date().toISOString() });
}
lastEnvironmentSignals = nextEnvironmentSignals;
},
});
}
async #runForUpdates({
userId,
projectSlug,
}: {
userId: User["id"];
projectSlug: Project["slug"];
}) {
const environments = await this.#prismaClient.runtimeEnvironment.findMany({
select: {
id: true,
updatedAt: true,
endpoints: {
select: {
updatedAt: true,
indexings: {
select: {
updatedAt: true,
},
},
},
},
},
where: {
project: {
slug: projectSlug,
},
organization: {
members: {
some: {
userId,
},
},
},
},
});
if (!environments) return null;
const environmentSignalsMap = environments.reduce<EnvironmentSignalsMap>(
(acc, environment) => {
const lastUpdatedAt = environment.updatedAt.getTime();
const lastTotalEndpointUpdatedTime = environment.endpoints.reduce(
(prev, endpoint) => prev + endpoint.updatedAt.getTime(),
0
);
const lastTotalIndexingUpdatedTime = environment.endpoints.reduce(
(prev, endpoint) =>
prev +
endpoint.indexings.reduce(
(prev, indexing) => prev + indexing.updatedAt.getTime(),
0
),
0
);
return {
...acc,
[environment.id]: {
lastUpdatedAt,
lastTotalEndpointUpdatedTime,
lastTotalIndexingUpdatedTime,
},
};
},
{}
);
return environmentSignalsMap;
}
}
@@ -0,0 +1,16 @@
import { LoaderArgs } from "@remix-run/server-runtime";
import { EnvironmentsStreamPresenter } from "~/presenters/EnvironmentsStreamPresenter.server";
import { requireUserId } from "~/services/session.server";
import { ProjectParamSchema } from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
const { projectParam } = ProjectParamSchema.parse(params);
const presenter = new EnvironmentsStreamPresenter();
return await presenter.call({
request,
userId,
projectSlug: projectParam,
});
};
@@ -1,6 +1,8 @@
import { LoaderArgs } from "@remix-run/server-runtime";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useRevalidator } from "@remix-run/react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { useEventSource } from "remix-utils";
import {
EnvironmentLabel,
environmentTitle,
@@ -32,7 +34,10 @@ import {
} from "~/presenters/EnvironmentsPresenter.server";
import { requireUserId } from "~/services/session.server";
import { Handle } from "~/utils/handle";
import { ProjectParamSchema } from "~/utils/pathBuilder";
import {
ProjectParamSchema,
projectEnvironmentsStreamingPath,
} from "~/utils/pathBuilder";
import { requestUrl } from "~/utils/requestUrl.server";
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet";
@@ -43,6 +48,8 @@ import {
HowToUseApiKeysAndEndpoints,
} from "~/components/helpContent/HelpContentText";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -102,6 +109,23 @@ export default function Page() {
};
}, [selected, clients]);
const organization = useOrganization();
const project = useProject();
const revalidator = useRevalidator();
const events = useEventSource(
projectEnvironmentsStreamingPath(organization, project),
{ event: "message" }
);
useEffect(() => {
if (events !== null) {
revalidator.revalidate();
}
// WARNING Don't put the revalidator in the useEffect deps array or bad things will happen
}, [events]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<PageContainer>
<PageHeader>
+7
View File
@@ -143,6 +143,13 @@ export function projectEnvironmentsPath(
return `${projectPath(organization, project)}/environments`;
}
export function projectEnvironmentsStreamingPath(
organization: OrgForPath,
project: ProjectForPath
) {
return `${projectEnvironmentsPath(organization, project)}/stream`;
}
export function endpointStreamingPath(environment: { id: string }) {
return `/resources/environments/${environment.id}/endpoint/stream`;
}