Compare commits
1 Commits
@nhost/has
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18a64555ce |
5
.changeset/nasty-fishes-remain.md
Normal file
5
.changeset/nasty-fishes-remain.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@nhost/dashboard': minor
|
||||
---
|
||||
|
||||
feat: show contact us info and locked reason when project is locked
|
||||
12
dashboard/public/assets/LockedApp.svg
Normal file
12
dashboard/public/assets/LockedApp.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="72" height="73" viewBox="0 0 72 73" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_10501_253)">
|
||||
<path d="M0 8.5C0 4.08172 3.58172 0.5 8 0.5H64C68.4183 0.5 72 4.08172 72 8.5V64.5C72 68.9183 68.4183 72.5 64 72.5H8C3.58172 72.5 0 68.9183 0 64.5V8.5Z" fill="#9C73DF" fill-opacity="0.2"/>
|
||||
<path d="M43.1203 35.5H29.7687C28.7153 35.5 27.8613 36.3954 27.8613 37.5V44.5C27.8613 45.6046 28.7153 46.5 29.7687 46.5H43.1203C44.1737 46.5 45.0276 45.6046 45.0276 44.5V37.5C45.0276 36.3954 44.1737 35.5 43.1203 35.5Z" stroke="#9C73DF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.6758 35.5V31.5C31.6758 30.1739 32.1782 28.9021 33.0724 27.9645C33.9667 27.0268 35.1795 26.5 36.4442 26.5C37.7089 26.5 38.9217 27.0268 39.816 27.9645C40.7102 28.9021 41.2126 30.1739 41.2126 31.5V35.5" stroke="#9C73DF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_10501_253">
|
||||
<rect width="72" height="72" fill="white" transform="translate(0 0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -8,6 +8,17 @@ import { createTheme as createMuiTheme } from '@mui/material/styles';
|
||||
* @param mode - Color mode
|
||||
* @returns Material UI theme
|
||||
*/
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
interface Palette {
|
||||
beige: Palette['primary'];
|
||||
}
|
||||
|
||||
interface PaletteOptions {
|
||||
beige?: PaletteOptions['primary'];
|
||||
}
|
||||
}
|
||||
|
||||
export default function createTheme(mode: PaletteMode) {
|
||||
return createMuiTheme({
|
||||
shape: {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
|
||||
function PowerOffIcon(props: IconProps) {
|
||||
return (
|
||||
<SvgIcon
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
d="M18.36 6.64A9 9 0 0 1 20.77 15M6.16 6.16a9 9 0 1 0 12.68 12.68M12 2v4M2 2l20 20"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
PowerOffIcon.displayName = 'NhostPowerOffIcon';
|
||||
|
||||
export default PowerOffIcon;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as PowerOffIcon } from './PowerOffIcon';
|
||||
@@ -63,6 +63,9 @@ export default function getDesignTokens(mode: PaletteMode): PaletteOptions {
|
||||
paper: '#171d26',
|
||||
},
|
||||
divider: '#2f363d',
|
||||
beige: {
|
||||
main: '#362c22',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,5 +128,8 @@ export default function getDesignTokens(mode: PaletteMode): PaletteOptions {
|
||||
paper: '#ffffff',
|
||||
},
|
||||
divider: '#eaedf0',
|
||||
beige: {
|
||||
main: '#e5d1bf',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface ApplicationLockedReasonProps {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export default function ApplicationLockedReason({
|
||||
reason,
|
||||
}: ApplicationLockedReasonProps) {
|
||||
return (
|
||||
<Alert severity="warning" className="mx-auto max-w-xs gap-2 p-6 ">
|
||||
<Text className="pb-4 text-left">
|
||||
Your project has been temporarily locked due to the following reason:
|
||||
</Text>
|
||||
<Box
|
||||
className="rounded-md p-2"
|
||||
sx={{
|
||||
backgroundColor: 'beige.main',
|
||||
}}
|
||||
>
|
||||
<Text className="px-2 py-1 font-semibold">{reason}</Text>
|
||||
</Box>
|
||||
<Text className="pt-4 text-left">
|
||||
Please{' '}
|
||||
<Link
|
||||
className="font-semibold underline underline-offset-2"
|
||||
href="mailto:support@nhost.io"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
contact our support
|
||||
</Link>{' '}
|
||||
team for assistance.
|
||||
</Text>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ApplicationLockedReason } from './ApplicationLockedReason';
|
||||
@@ -2,25 +2,24 @@ import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { Modal } from '@/components/ui/v1/Modal';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { ApplicationInfo } from '@/features/projects/common/components/ApplicationInfo';
|
||||
import { ApplicationLockedReason } from '@/features/projects/common/components/ApplicationLockedReason';
|
||||
import { ApplicationPausedReason } from '@/features/projects/common/components/ApplicationPausedReason';
|
||||
import { ApplicationPausedSymbol } from '@/features/projects/common/components/ApplicationPausedSymbol';
|
||||
import { ChangePlanModal } from '@/features/projects/common/components/ChangePlanModal';
|
||||
import { RemoveApplicationModal } from '@/features/projects/common/components/RemoveApplicationModal';
|
||||
import { StagingMetadata } from '@/features/projects/common/components/StagingMetadata';
|
||||
import { useAppPausedReason } from '@/features/projects/common/hooks/useAppPausedReason';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
useGetFreeAndActiveProjectsQuery,
|
||||
useUnpauseApplicationMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { MAX_FREE_PROJECTS } from '@/utils/constants/common';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function ApplicationPaused() {
|
||||
@@ -28,7 +27,6 @@ export default function ApplicationPaused() {
|
||||
const { currentProject, refetch: refetchWorkspaceAndProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const isOwner = useIsCurrentUserOwner();
|
||||
const user = useUserData();
|
||||
|
||||
const [showDeletingModal, setShowDeletingModal] = useState(false);
|
||||
const [unpauseApplication, { loading: changingApplicationStateLoading }] =
|
||||
@@ -36,13 +34,8 @@ export default function ApplicationPaused() {
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
});
|
||||
|
||||
const { data, loading } = useGetFreeAndActiveProjectsQuery({
|
||||
variables: { userId: user?.id },
|
||||
skip: !user,
|
||||
});
|
||||
|
||||
const numberOfFreeAndLiveProjects = data?.freeAndActiveProjects.length || 0;
|
||||
const wakeUpDisabled = numberOfFreeAndLiveProjects >= MAX_FREE_PROJECTS;
|
||||
const { isLocked, lockedReason, freeAndLiveProjectsNumberExceeded, loading } =
|
||||
useAppPausedReason();
|
||||
|
||||
async function handleTriggerUnpausing() {
|
||||
await execPromiseWithErrorToast(
|
||||
@@ -77,75 +70,67 @@ export default function ApplicationPaused() {
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Container className="mx-auto mt-20 grid max-w-lg grid-flow-row gap-4 text-center">
|
||||
<Container className="mx-auto mt-20 grid max-w-lg grid-flow-row gap-6 text-center">
|
||||
<div className="mx-auto flex w-centImage flex-col text-center">
|
||||
<Image
|
||||
src="/assets/PausedApp.svg"
|
||||
alt="Closed Eye"
|
||||
width={72}
|
||||
height={72}
|
||||
/>
|
||||
<ApplicationPausedSymbol isLocked={isLocked} />
|
||||
</div>
|
||||
|
||||
<Box className="grid grid-flow-row gap-1">
|
||||
<Box className="grid grid-flow-row gap-6">
|
||||
<Text variant="h3" component="h1">
|
||||
{currentProject.name} is sleeping
|
||||
{currentProject.name} is {isLocked ? 'locked' : 'paused'}
|
||||
</Text>
|
||||
{isLocked ? (
|
||||
<ApplicationLockedReason reason={lockedReason} />
|
||||
) : (
|
||||
<>
|
||||
<ApplicationPausedReason
|
||||
freeAndLiveProjectsNumberExceeded={
|
||||
freeAndLiveProjectsNumberExceeded
|
||||
}
|
||||
/>
|
||||
<div className="grid grid-flow-row gap-4">
|
||||
{isOwner && (
|
||||
<Button
|
||||
className="mx-auto w-full max-w-xs"
|
||||
onClick={() => {
|
||||
openDialog({
|
||||
component: <ChangePlanModal />,
|
||||
props: {
|
||||
PaperProps: { className: 'p-0' },
|
||||
maxWidth: 'lg',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upgrade to Pro
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="borderless"
|
||||
className="mx-auto w-full max-w-xs"
|
||||
loading={changingApplicationStateLoading}
|
||||
disabled={
|
||||
changingApplicationStateLoading ||
|
||||
freeAndLiveProjectsNumberExceeded
|
||||
}
|
||||
onClick={handleTriggerUnpausing}
|
||||
>
|
||||
Wake Up
|
||||
</Button>
|
||||
|
||||
<Text>
|
||||
Starter projects stop responding to API calls after 7 days of
|
||||
inactivity. Upgrade to Pro to avoid autosleep.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box className="grid grid-flow-row gap-2">
|
||||
{isOwner && (
|
||||
<Button
|
||||
className="mx-auto w-full max-w-[280px]"
|
||||
onClick={() => {
|
||||
openDialog({
|
||||
component: <ChangePlanModal />,
|
||||
props: {
|
||||
PaperProps: { className: 'p-0' },
|
||||
maxWidth: 'lg',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upgrade to Pro
|
||||
</Button>
|
||||
{isOwner && (
|
||||
<Button
|
||||
color="error"
|
||||
variant="outlined"
|
||||
className="mx-auto w-full max-w-xs"
|
||||
onClick={() => setShowDeletingModal(true)}
|
||||
>
|
||||
Delete Project
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button
|
||||
variant="borderless"
|
||||
className="mx-auto w-full max-w-[280px]"
|
||||
loading={changingApplicationStateLoading}
|
||||
disabled={changingApplicationStateLoading || wakeUpDisabled}
|
||||
onClick={handleTriggerUnpausing}
|
||||
>
|
||||
Wake Up
|
||||
</Button>
|
||||
|
||||
{wakeUpDisabled && (
|
||||
<Alert severity="warning" className="mx-auto max-w-xs text-left">
|
||||
Note: Only one free project can be active at any given time.
|
||||
Please pause your active free project before unpausing{' '}
|
||||
{currentProject.name}.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isOwner && (
|
||||
<Button
|
||||
color="error"
|
||||
variant="borderless"
|
||||
className="mx-auto w-full max-w-[280px]"
|
||||
onClick={() => setShowDeletingModal(true)}
|
||||
>
|
||||
Delete Project
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<StagingMetadata>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
|
||||
interface ApplicationPausedReasonProps {
|
||||
freeAndLiveProjectsNumberExceeded?: boolean;
|
||||
}
|
||||
|
||||
export default function ApplicationPausedReason({
|
||||
freeAndLiveProjectsNumberExceeded,
|
||||
}: ApplicationPausedReasonProps) {
|
||||
return (
|
||||
<Alert
|
||||
severity="warning"
|
||||
className="mx-auto flex max-w-xs flex-col gap-4 p-6 text-left"
|
||||
>
|
||||
<Text>
|
||||
Starter projects will stop responding to API calls after 7 days of
|
||||
inactivity, so consider
|
||||
<span className="font-semibold"> upgrading to Pro </span>to avoid
|
||||
auto-sleep.
|
||||
</Text>
|
||||
{freeAndLiveProjectsNumberExceeded && (
|
||||
<Text>
|
||||
Additionally, only 1 free project can be active at any given time, so
|
||||
please pause your current active free project before unpausing
|
||||
another.
|
||||
</Text>
|
||||
)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ApplicationPausedReason } from './ApplicationPausedReason';
|
||||
@@ -0,0 +1,23 @@
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function ApplicationPausedSymbol({
|
||||
isLocked,
|
||||
}: {
|
||||
isLocked?: boolean;
|
||||
}) {
|
||||
if (isLocked) {
|
||||
return (
|
||||
<Image src="/assets/LockedApp.svg" alt="Lock" width={72} height={72} />
|
||||
);
|
||||
}
|
||||
|
||||
// paused
|
||||
return (
|
||||
<Image
|
||||
src="/assets/PausedApp.svg"
|
||||
alt="Closed Eye"
|
||||
width={72}
|
||||
height={72}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ApplicationPausedSymbol } from './ApplicationPausedSymbol';
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useAppPausedReason } from './useAppPausedReason';
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { MAX_FREE_PROJECTS } from '@/utils/constants/common';
|
||||
import {
|
||||
useGetFreeAndActiveProjectsQuery,
|
||||
useGetProjectIsLockedQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
|
||||
/**
|
||||
* This hook returns the reason why the application is paused.
|
||||
* It returns the locked reason and if the user has exceeded the number of free and live projects.
|
||||
*/
|
||||
|
||||
export default function useAppPausedReason(): {
|
||||
isLocked: boolean;
|
||||
lockedReason: string | undefined;
|
||||
freeAndLiveProjectsNumberExceeded: boolean;
|
||||
loading: boolean;
|
||||
} {
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const user = useUserData();
|
||||
const { data, loading } = useGetFreeAndActiveProjectsQuery({
|
||||
variables: { userId: user?.id },
|
||||
skip: !user,
|
||||
});
|
||||
|
||||
const { data: isLockedData } = useGetProjectIsLockedQuery({
|
||||
variables: { appId: currentProject.id },
|
||||
skip: !currentProject,
|
||||
});
|
||||
|
||||
const isLocked = isLockedData?.app?.isLocked;
|
||||
const lockedReason = isLockedData?.app?.isLockedReason;
|
||||
|
||||
const numberOfFreeAndLiveProjects = data?.freeAndActiveProjects.length || 0;
|
||||
const freeAndLiveProjectsNumberExceeded =
|
||||
numberOfFreeAndLiveProjects >= MAX_FREE_PROJECTS;
|
||||
|
||||
return {
|
||||
isLocked,
|
||||
lockedReason,
|
||||
freeAndLiveProjectsNumberExceeded,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
6
dashboard/src/gql/app/getProjectIsLocked.graphql
Normal file
6
dashboard/src/gql/app/getProjectIsLocked.graphql
Normal file
@@ -0,0 +1,6 @@
|
||||
query getProjectIsLocked($appId: uuid!) {
|
||||
app(id: $appId) {
|
||||
isLocked
|
||||
isLockedReason
|
||||
}
|
||||
}
|
||||
52
dashboard/src/utils/__generated__/graphql.ts
generated
52
dashboard/src/utils/__generated__/graphql.ts
generated
@@ -12522,12 +12522,6 @@ export type Mutation_Root = {
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootBackupAllApplicationsDatabaseArgs = {
|
||||
expireInDays?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootBackupApplicationDatabaseArgs = {
|
||||
appID: Scalars['String'];
|
||||
@@ -22922,6 +22916,13 @@ export type GetConfiguredVersionsQueryVariables = Exact<{
|
||||
|
||||
export type GetConfiguredVersionsQuery = { __typename?: 'query_root', config?: { __typename?: 'ConfigConfig', auth?: { __typename?: 'ConfigAuth', version?: string | null } | null, postgres?: { __typename?: 'ConfigPostgres', version?: string | null } | null, hasura: { __typename?: 'ConfigHasura', version?: string | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null, storage?: { __typename?: 'ConfigStorage', version?: string | null } | null } | null };
|
||||
|
||||
export type GetProjectIsLockedQueryVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetProjectIsLockedQuery = { __typename?: 'query_root', app?: { __typename?: 'apps', isLocked?: boolean | null, isLockedReason?: string | null } | null };
|
||||
|
||||
export type GetProjectLocalesQueryVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
}>;
|
||||
@@ -24826,6 +24827,45 @@ export type GetConfiguredVersionsQueryResult = Apollo.QueryResult<GetConfiguredV
|
||||
export function refetchGetConfiguredVersionsQuery(variables: GetConfiguredVersionsQueryVariables) {
|
||||
return { query: GetConfiguredVersionsDocument, variables: variables }
|
||||
}
|
||||
export const GetProjectIsLockedDocument = gql`
|
||||
query getProjectIsLocked($appId: uuid!) {
|
||||
app(id: $appId) {
|
||||
isLocked
|
||||
isLockedReason
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetProjectIsLockedQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetProjectIsLockedQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetProjectIsLockedQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetProjectIsLockedQuery({
|
||||
* variables: {
|
||||
* appId: // value for 'appId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetProjectIsLockedQuery(baseOptions: Apollo.QueryHookOptions<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>(GetProjectIsLockedDocument, options);
|
||||
}
|
||||
export function useGetProjectIsLockedLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>(GetProjectIsLockedDocument, options);
|
||||
}
|
||||
export type GetProjectIsLockedQueryHookResult = ReturnType<typeof useGetProjectIsLockedQuery>;
|
||||
export type GetProjectIsLockedLazyQueryHookResult = ReturnType<typeof useGetProjectIsLockedLazyQuery>;
|
||||
export type GetProjectIsLockedQueryResult = Apollo.QueryResult<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>;
|
||||
export function refetchGetProjectIsLockedQuery(variables: GetProjectIsLockedQueryVariables) {
|
||||
return { query: GetProjectIsLockedDocument, variables: variables }
|
||||
}
|
||||
export const GetProjectLocalesDocument = gql`
|
||||
query getProjectLocales($appId: uuid!) {
|
||||
config(appID: $appId, resolve: false) {
|
||||
|
||||
Reference in New Issue
Block a user