Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
30 changes: 21 additions & 9 deletions apps/webapp/app/components/ImpersonationBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import { TertiaryA } from "./primitives/Buttons";
import { Form } from "@remix-run/react";
import { TertiaryA, TertiaryButton } from "./primitives/Buttons";
import { Body } from "./primitives/text/Body";

export function ImpersonationBanner() {
export function ImpersonationBanner({
impersonationId,
}: {
impersonationId: string;
}) {
return (
<div className="grid grid-cols-3 items-center bg-gradient-to-r from-amber-500 via-amber-400 to-amber-500 text-center px-4">
<div className="grid grid-cols-3 items-center bg-gradient-to-r from-amber-500 via-amber-400 to-amber-500 px-4 text-center">
<span></span>
<Body size="small" className="text-slate-800">
You are impersonating{" "}
<span className="font-semibold">James Ritchie</span>
<span className="font-semibold">{impersonationId}</span>
</Body>
<TertiaryA
href="/api/auth/logout"
className="justify-self-end text-slate-700 hover:!text-slate-900"
<Form
action="/resources/impersonation"
method="delete"
reloadDocument
className="justify-self-end"
>
Stop impersonating
</TertiaryA>
<TertiaryButton
type="submit"
className="text-slate-700 hover:!text-slate-900"
>
Stop impersonating
</TertiaryButton>
</Form>
</div>
);
}
23 changes: 17 additions & 6 deletions apps/webapp/app/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import classNames from "classnames";
import { ImpersonationBanner } from "../ImpersonationBanner";

export function AppLayout({ children }: { children: React.ReactNode }) {
export function AppLayout({
children,
impersonationId,
}: {
children: React.ReactNode;
impersonationId?: string;
}) {
if (impersonationId) {
return (
<div className="grid h-full w-full grid-rows-[2rem_3rem_auto_2rem]">
<ImpersonationBanner impersonationId={impersonationId} />
{children}
</div>
);
}

return (
<div className="grid grid-rows-[3rem_auto_2rem] w-full h-full">
<div className="grid h-full w-full grid-rows-[3rem_auto_2rem]">
{children}
</div>
// <div className="grid grid-rows-[2rem_3rem_auto_2rem] w-full h-full">
// <ImpersonationBanner />
// {children}
// </div>
);
}

Expand Down
9 changes: 9 additions & 0 deletions apps/webapp/app/models/admin.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { prisma } from "~/db.server";

export async function adminGetUsers() {
return await prisma.user.findMany({
orderBy: {
createdAt: "desc",
},
});
}
9 changes: 6 additions & 3 deletions apps/webapp/app/routes/__app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Outlet } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson } from "remix-typedjson";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { Footer } from "~/components/layout/Footer";
import { AppBody, AppLayout } from "~/components/layout/AppLayout";
import { getOrganizations } from "~/models/organization.server";
Expand All @@ -10,15 +10,17 @@ import { Header } from "~/components/layout/Header";
import { NoMobileOverlay } from "~/components/NoMobileOverlay";
import { IntercomProvider, useIntercom } from "react-use-intercom";
import { useEffect } from "react";

import { getImpersonationId } from "~/services/impersonation.server";

export const loader = async ({ request }: LoaderArgs) => {
const userId = await requireUserId(request);
const organizations = await getOrganizations({ userId });
const impersonationId = await getImpersonationId(request);

return typedjson(
{
organizations,
impersonationId,
},
{
headers: {
Expand All @@ -31,10 +33,11 @@ export const loader = async ({ request }: LoaderArgs) => {
const INTERCOM_APP_ID = "pfbctmiv";

export default function App() {
const { impersonationId } = useTypedLoaderData<typeof loader>();

return (
<IntercomProvider appId={INTERCOM_APP_ID}>
<AppLayout>
<AppLayout impersonationId={impersonationId}>
<NoMobileOverlay />
<Header />
<AppBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,7 @@ export const action = async ({ request, params }: ActionArgs) => {

const maxWidth = "flex max-w-4xl";
const subTitle = "text-slate-200 font-semibold mb-3";
const carousel =
"-ml-[26px] overflow-hidden overflow-x-auto pl-[1.5rem] scrollbar-hide";
const carousel = "-ml-[26px] overflow-hidden overflow-x-auto pl-[1.5rem]";

export default function NewWorkflowPage() {
const environment = useCurrentEnvironment();
Expand Down
212 changes: 212 additions & 0 deletions apps/webapp/app/routes/admin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/* This example requires Tailwind CSS v2.0+ */
import { Dialog, Transition } from "@headlessui/react";
import { HomeIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { Outlet } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { Fragment, useState } from "react";
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
import { getUser, requireUserId } from "~/services/session.server";

const navigation = [{ name: "Home", href: "/admin", icon: HomeIcon }];

export async function loader({ request }: LoaderArgs) {
await requireUserId(request);
const user = await getUser(request);
if (user == null) {
return redirect("/");
}

if (!user.admin) {
return redirect("/");
}

return typedjson({ user });
}

export default function Page() {
const data = useTypedLoaderData<typeof loader>();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);

return (
<>
<div className="flex h-full">
<Transition.Root show={mobileMenuOpen} as={Fragment}>
<Dialog
as="div"
className="relative z-40 lg:hidden"
onClose={setMobileMenuOpen}
>
<Transition.Child
as={Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" />
</Transition.Child>

<div className="fixed inset-0 z-40 flex">
<Transition.Child
as={Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="-translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full"
>
<Dialog.Panel className="relative flex w-full max-w-xs flex-1 flex-col bg-white focus:outline-none">
<Transition.Child
as={Fragment}
enter="ease-in-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute top-0 right-0 -mr-12 pt-4">
<button
type="button"
className="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
onClick={() => setMobileMenuOpen(false)}
>
<span className="sr-only">Close sidebar</span>
<XMarkIcon
className="h-6 w-6 text-white"
aria-hidden="true"
/>
</button>
</div>
</Transition.Child>
<div className="pt-5 pb-4">
<div className="flex flex-shrink-0 items-center px-4">
<img
className="h-8 w-auto"
src="https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600"
alt="Workflow"
/>
</div>
<nav aria-label="Sidebar" className="mt-5">
<div className="space-y-1 px-2">
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
className="group flex items-center rounded-md p-2 text-base font-medium text-gray-600 hover:bg-gray-50 hover:text-gray-900"
>
<item.icon
className="mr-4 h-6 w-6 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
{item.name}
</a>
))}
</div>
</nav>
</div>
<div className="flex flex-shrink-0 border-t border-gray-200 p-4">
<button className="group block flex-shrink-0">
<div className="flex items-center">
<div>
<UserProfilePhoto
user={data.user}
className="h-10 w-10"
/>
</div>
<div className="ml-3">
<p className="text-base font-medium text-gray-700 group-hover:text-gray-900">
{data.user.displayName}
</p>
<p className="text-sm font-medium text-gray-500 group-hover:text-gray-700">
Account Settings
</p>
</div>
</div>
</button>
</div>
</Dialog.Panel>
</Transition.Child>
<div className="w-14 flex-shrink-0" aria-hidden="true">
{/* Force sidebar to shrink to fit close icon */}
</div>
</div>
</Dialog>
</Transition.Root>

{/* Static sidebar for desktop */}
<div className="hidden lg:flex lg:flex-shrink-0">
<div className="flex w-20 flex-col">
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto bg-indigo-600">
<div className="flex-1">
<div className="flex items-center justify-center bg-indigo-700 py-4">
<img
className="h-8 w-auto"
src="https://tailwindui.com/img/logos/workflow-mark.svg?color=white"
alt="Workflow"
/>
</div>
<nav
aria-label="Sidebar"
className="flex flex-col items-center space-y-3 py-6"
>
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
className="flex items-center rounded-lg p-4 text-indigo-200 hover:bg-indigo-700"
>
<item.icon className="h-6 w-6" aria-hidden="true" />
<span className="sr-only">{item.name}</span>
</a>
))}
</nav>
</div>
<div className="flex flex-shrink-0 pb-5">
<button className="flex w-full flex-shrink-0 flex-grow justify-center">
<UserProfilePhoto
user={data.user}
className="block h-10 w-10"
/>
<div className="sr-only">
<p>{data.user.displayName}</p>
<p>Account settings</p>
</div>
</button>
</div>
</div>
</div>
</div>

<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
<Outlet />
</div>
</div>
</>
);
}

import { UserCircleIcon } from "@heroicons/react/24/solid";
import classNames from "classnames";
import type { User } from "~/models/user.server";

function UserProfilePhoto({
user,
className,
}: {
user: User;
className?: string;
}) {
return user.avatarUrl ? (
<img
className={classNames("rounded-full", className)}
src={user.avatarUrl}
alt={user.name ?? user.displayName ?? "User"}
/>
) : (
<UserCircleIcon className={classNames("text-gray-400", className)} />
);
}
Loading