feat: 🎸 add live refreshing to Environments & API key page
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
+16
@@ -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,
|
||||
});
|
||||
};
|
||||
+26
-2
@@ -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>
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user