Skip to content

Commit

Permalink
Archive app (#1338)
Browse files Browse the repository at this point in the history
* Add app archive support

* gql

* Tweak copy

* Delete function archive button

* AlertModal

* Fix

* Bring back function archive button

* Simplify code

* Make alert modal more similar to modal

---------

Co-authored-by: Ana <anafilipadealmeida@gmail.com>
  • Loading branch information
goodoldneon and anafilipadealmeida committed May 13, 2024
1 parent 04e72bd commit ca0c540
Show file tree
Hide file tree
Showing 15 changed files with 298 additions and 133 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useState } from 'react';
import { Button } from '@inngest/components/Button';

import { ArchiveModal } from './ArchiveModal';

type Props = {
appID: string;
disabled?: boolean;
isArchived: boolean;
};

export function ArchiveButton({ appID, disabled = false, isArchived }: Props) {
const [isModalVisible, setIsModalVisible] = useState(false);

let label = 'Archive';
if (isArchived) {
label = 'Unarchive';
}

return (
<>
<Button
appearance="outlined"
btnAction={() => setIsModalVisible(true)}
disabled={disabled}
kind="danger"
label={label}
/>

<ArchiveModal
appID={appID}
isArchived={isArchived}
isOpen={isModalVisible}
onClose={() => setIsModalVisible(false)}
/>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { useState } from 'react';
import { Alert } from '@inngest/components/Alert';
import { AlertModal } from '@inngest/components/Modal';
import { toast } from 'sonner';
import { useMutation } from 'urql';

import { graphql } from '@/gql';

type Props = {
appID: string;
isArchived: boolean;
isOpen: boolean;
onClose: () => void;
};

export function ArchiveModal({ appID, isArchived, isOpen, onClose }: Props) {
const [error, setError] = useState<Error>();
const [isLoading, setIsLoading] = useState(false);
const [, archiveApp] = useMutation(ArchiveAppDocument);
const [, unarchiveApp] = useMutation(UnarchiveAppDocument);

async function onConfirm() {
setIsLoading(true);
try {
let error;
let message: string;
if (isArchived) {
error = (await unarchiveApp({ appID })).error;
message = 'Unarchived app';
} else {
error = (await archiveApp({ appID })).error;
message = 'Archived app';
}
if (error) {
throw error;
}
setError(undefined);
toast.success(message);
onClose();
} catch (error) {
if (error instanceof Error) {
setError(error);
} else {
setError(new Error('unknown error'));
}
} finally {
setIsLoading(false);
}
}

return (
<AlertModal
isLoading={isLoading}
isOpen={isOpen}
onClose={onClose}
onSubmit={onConfirm}
title={`Are you sure you want to ${isArchived ? 'unarchive' : 'archive'} this app?`}
className="w-[600px]"
>
<ul className="list-inside list-disc p-6 pb-0">
{isArchived && (
<>
<li>New function runs can trigger.</li>
<li>You may re-archive at any time.</li>
</>
)}
{!isArchived && (
<>
<li>New function runs will not trigger.</li>
<li>Existing function runs will continue until completion.</li>
<li>Functions will still be visible, including their run history.</li>
<li>You may unarchive at any time.</li>
</>
)}
</ul>

{error && (
<Alert className="mt-4" severity="error">
{error.message}
</Alert>
)}
</AlertModal>
);
}

const ArchiveAppDocument = graphql(`
mutation AchiveApp($appID: UUID!) {
archiveApp(id: $appID) {
id
}
}
`);

const UnarchiveAppDocument = graphql(`
mutation UnachiveApp($appID: UUID!) {
unarchiveApp(id: $appID) {
id
}
}
`);
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@ import ResyncModal from './ResyncModal';

type Props = {
appExternalID: string;
disabled?: boolean;
latestSyncUrl: string;
platform: string | null;
};

export function ResyncButton({ appExternalID, latestSyncUrl, platform }: Props) {
export function ResyncButton({ appExternalID, disabled = false, latestSyncUrl, platform }: Props) {
const [isModalVisible, setIsModalVisible] = useState(false);

return (
<>
<Button btnAction={() => setIsModalVisible(true)} kind="primary" label="Resync" />
<Button
btnAction={() => setIsModalVisible(true)}
disabled={disabled}
kind="primary"
label="Resync"
/>

<ResyncModal
appExternalID={appExternalID}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { Alert } from '@inngest/components/Alert';
import { IconApp } from '@inngest/components/icons/App';

import { useEnvironment } from '@/app/(organization-active)/(dashboard)/env/[environmentSlug]/environment-context';
import { ArchivedAppBanner } from '@/components/ArchivedAppBanner';
import Header, { type HeaderLink } from '@/components/Header/Header';
import { ArchiveButton } from './ArchiveButton';
import { ResyncButton } from './ResyncButton';
import { useNavData } from './useNavData';

Expand Down Expand Up @@ -49,21 +51,31 @@ export default function Layout({ children, params: { externalID } }: Props) {
},
];

let action;
if (res.data.latestSync?.url && !env.isArchived) {
action = (
const actions = [];
if (res.data.latestSync?.url) {
actions.push(
<ResyncButton
appExternalID={externalID}
disabled={res.data.isArchived}
platform={res.data.latestSync.platform}
latestSyncUrl={res.data.latestSync.url}
/>
);
}

actions.push(
<ArchiveButton
appID={res.data.id}
disabled={res.data.isParentArchived}
isArchived={res.data.isArchived}
/>
);

return (
<>
{<ArchivedAppBanner externalAppID={externalID} />}
<Header
action={action}
action={<div className="flex gap-4">{actions}</div>}
icon={<IconApp className="h-5 w-5 text-white" />}
links={navLinks}
title={res.data.name}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ const query = graphql(`
query AppNavData($envID: ID!, $externalAppID: String!) {
environment: workspace(id: $envID) {
app: appByExternalID(externalID: $externalAppID) {
id
isArchived
isParentArchived
latestSync {
platform
url
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const query = graphql(`
id
externalID
functionCount
isArchived
name
latestSync {
error
Expand Down Expand Up @@ -54,7 +55,7 @@ export function useApps({ envID, isArchived }: { envID: string; isArchived: bool
// This is a hack to get around the fact that app archival is not a
// first-class feature yet. We'll infer that an app is archived if all
// of its functions are archived.
isArchived: app.functionCount === 0,
isArchived: app.isArchived || app.functionCount === 0,
};
})
.filter((app) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,13 @@
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@inngest/components/Button';
import { AlertModal } from '@inngest/components/Modal';
import * as Tooltip from '@radix-ui/react-tooltip';
import { RiArchive2Line, RiInboxUnarchiveLine } from '@remixicon/react';
import { toast } from 'sonner';
import { useMutation, useQuery } from 'urql';
import { useQuery } from 'urql';

import { useEnvironment } from '@/app/(organization-active)/(dashboard)/env/[environmentSlug]/environment-context';
import { graphql } from '@/gql';

const ArchiveFunctionDocument = graphql(`
mutation ArchiveFunction($input: ArchiveWorkflowInput!) {
archiveWorkflow(input: $input) {
workflow {
id
}
}
}
`);

const GetFunctionArchivalDocument = graphql(`
query GetFunctionArchival($slug: String!, $environmentID: ID!) {
workspace(id: $environmentID) {
Expand All @@ -34,84 +20,17 @@ const GetFunctionArchivalDocument = graphql(`
}
`);

type ArchiveFunctionModalProps = {
functionID: string | undefined;
functionName: string;
isArchived: boolean;
isOpen: boolean;
onClose: () => void;
};

function ArchiveFunctionModal({
functionID,
functionName,
isArchived,
isOpen,
onClose,
}: ArchiveFunctionModalProps) {
const [, archiveFunctionMutation] = useMutation(ArchiveFunctionDocument);
const router = useRouter();

function handleArchive() {
if (functionID) {
archiveFunctionMutation({
input: {
workflowID: functionID,
archive: !isArchived,
},
}).then((result) => {
if (result.error) {
toast.error(
`${functionName} could not be ${isArchived ? 'resumed' : 'archived'}: ${
result.error.message
}`
);
} else {
toast.success(`${functionName} was successfully ${isArchived ? 'resumed' : 'archived'}`);
router.refresh();
}
});
onClose();
}
}

return (
<AlertModal
className="w-1/3"
isOpen={isOpen}
onClose={onClose}
onSubmit={handleArchive}
title={`Are you sure you want to ${isArchived ? 'unarchive' : 'archive'} this function?`}
>
{isArchived && (
<p className="pt-4">
Reactivate this function. This function will resume normal functionality and will be
invoked as new events are received. Events received while archived will not be replayed.
</p>
)}
{!isArchived && (
<ul className="list-disc p-4 pb-0 leading-8">
<li>Existing runs will continue to run to completion.</li>
<li>No new runs will be queued or invoked.</li>
<li>Events will continue to be received, but they will not trigger new runs.</li>
<li>Archived functions and their logs can be viewed at any time.</li>
<li>Archived functions will be unarchived when you resync your app.</li>
<li>Functions can be unarchived at any time.</li>
</ul>
)}
</AlertModal>
);
}

type ArchiveFunctionProps = {
functionSlug: string;
};

/**
* @deprecated Delete this component any time after 2024-05-17
*/
export default function ArchiveFunctionButton({ functionSlug }: ArchiveFunctionProps) {
const [isArchivedFunctionModalVisible, setIsArchivedFunctionModalVisible] = useState(false);
const environment = useEnvironment();

const [{ data: version, fetching: isFetchingVersions }] = useQuery({
const [{ data: version }] = useQuery({
query: GetFunctionArchivalDocument,
variables: {
environmentID: environment.id,
Expand Down Expand Up @@ -141,27 +60,20 @@ export default function ArchiveFunctionButton({ functionSlug }: ArchiveFunctionP
<RiArchive2Line className=" text-slate-300" />
)
}
btnAction={() => setIsArchivedFunctionModalVisible(true)}
disabled={isFetchingVersions}
btnAction={() =>
console.error('manual function archival has been replaced with app archival')
}
disabled
label={isArchived ? 'Unarchive' : 'Archive'}
/>
</span>
</Tooltip.Trigger>
<Tooltip.Content className="align-center rounded-md bg-slate-800 px-2 text-xs text-slate-300">
{isArchived
? 'Reactivate function'
: 'Deactivate this function and archive for historic purposes'}
Manual function archival has been replaced with app archival
<Tooltip.Arrow className="fill-slate-800" />
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
<ArchiveFunctionModal
functionID={fn.id}
functionName={fn.name || 'This function'}
isOpen={isArchivedFunctionModalVisible}
onClose={() => setIsArchivedFunctionModalVisible(false)}
isArchived={isArchived}
/>
</>
);
}

0 comments on commit ca0c540

Please sign in to comment.