Compare commits
43 Commits
@nhost/apo
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf5423dac6 | ||
|
|
a2efeed36f | ||
|
|
533b74d82d | ||
|
|
42cf86c8f1 | ||
|
|
70e74f2f3d | ||
|
|
a01985466e | ||
|
|
8ea4210582 | ||
|
|
58919ba763 | ||
|
|
86f3f8d505 | ||
|
|
201abb89fd | ||
|
|
cf6b712b20 | ||
|
|
b51986289d | ||
|
|
c640c50c70 | ||
|
|
b3ff6adcc2 | ||
|
|
dbadf59092 | ||
|
|
bedbb82cd7 | ||
|
|
79ce7cae2f | ||
|
|
9c9137f813 | ||
|
|
502abadbae | ||
|
|
b6b67773d1 | ||
|
|
33ce95536d | ||
|
|
11f9ed7507 | ||
|
|
ac6d1b6e01 | ||
|
|
77fba27d12 | ||
|
|
7163854767 | ||
|
|
66bd4504d7 | ||
|
|
a03fb2cf82 | ||
|
|
87a37cfc08 | ||
|
|
6fb0cc27aa | ||
|
|
2c33051f83 | ||
|
|
a9413af6e0 | ||
|
|
f4f0353f2e | ||
|
|
defffd8bc4 | ||
|
|
614c20cbbf | ||
|
|
aef4a0a4fc | ||
|
|
d0c9f4cd17 | ||
|
|
e2646cab55 | ||
|
|
d5077c7ca4 | ||
|
|
c6d5c5cc8c | ||
|
|
f1d9b472d1 | ||
|
|
c6dc7f44df | ||
|
|
3cea460c36 | ||
|
|
4c351714f5 |
@@ -12,7 +12,7 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.10.5
|
||||
run_install: false
|
||||
|
||||
@@ -1,5 +1,75 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 1.21.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- a2efeed: fix: improve project health error handling, add unknown state and polling interval for health state
|
||||
|
||||
## 1.20.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 8ea4210: fix: error toasts can be closed individually, instead of dismissing all toasts at once
|
||||
- 58919ba: chore: add blink animation when project health service is updating
|
||||
|
||||
## 1.19.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- b519862: fix: get configuration in configuration editor using local development environment
|
||||
|
||||
## 1.18.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 502abad: feat: add services health checks indicators to the overview page
|
||||
- b3ff6ad: chore: update title text on service status modal
|
||||
- dbadf59: feat: add project configuration TOML editor to the settings page
|
||||
|
||||
## 1.17.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 77fba27: fix: postgres version validation when activating ai in ai settings page
|
||||
- ac6d1b6: feat: use name instead of awsName
|
||||
|
||||
## 1.16.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 87a37cf: fix: remove unnecessary isPlatform check from verify button disable logic on custom domains
|
||||
- @nhost/react-apollo@12.0.2
|
||||
- @nhost/nextjs@2.1.16
|
||||
|
||||
## 1.16.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a9413af: fix: update `GetAllWorkspacesAndProjects` query polling to use exponential backoff
|
||||
- @nhost/react-apollo@12.0.1
|
||||
- @nhost/nextjs@2.1.15
|
||||
|
||||
## 1.16.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@12.0.0
|
||||
- @nhost/nextjs@2.1.14
|
||||
|
||||
## 1.16.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- c6d5c5c: feat: add toggle switch to enable/disable public access in the database settings
|
||||
|
||||
## 1.15.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@11.0.4
|
||||
- @nhost/nextjs@2.1.13
|
||||
|
||||
## 1.15.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "1.15.1",
|
||||
"version": "1.21.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -21,17 +21,20 @@
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.9.9",
|
||||
"@codemirror/lang-sql": "^6.6.2",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
"@codemirror/legacy-modes": "^6.4.0",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@fontsource/inter": "^5.0.17",
|
||||
"@fontsource/roboto-mono": "^5.0.17",
|
||||
"@graphiql/react": "^0.20.3",
|
||||
"@graphiql/react": "^0.22.3",
|
||||
"@graphiql/toolkit": "^0.9.1",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@mui/base": "5.0.0-beta.31",
|
||||
"@mui/material": "^5.15.14",
|
||||
"@mui/system": "^5.15.14",
|
||||
@@ -45,6 +48,8 @@
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.15.3",
|
||||
"@tanstack/react-virtual": "^3.2.0",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@uiw/codemirror-theme-bbedit": "^4.22.2",
|
||||
"@uiw/codemirror-theme-github": "^4.21.25",
|
||||
"@uiw/react-codemirror": "^4.21.25",
|
||||
"analytics-node": "^6.2.0",
|
||||
@@ -53,7 +58,7 @@
|
||||
"date-fns": "^2.30.0",
|
||||
"framer-motion": "^10.18.0",
|
||||
"generate-password": "^1.7.1",
|
||||
"graphiql": "^3.1.1",
|
||||
"graphiql": "^3.3.1",
|
||||
"graphql": "16.8.1",
|
||||
"graphql-request": "^6.1.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function SettingsLayout({
|
||||
|
||||
<Box
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
className="flex w-full flex-auto flex-col overflow-scroll overflow-x-hidden"
|
||||
className="flex w-full flex-auto flex-col overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
<RetryableErrorBoundary>
|
||||
<div className="flex flex-col space-y-2">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Backdrop } from '@/components/ui/v2/Backdrop';
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { SlidersIcon } from '@/components/ui/v2/icons/SlidersIcon';
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import type { ListItemButtonProps } from '@/components/ui/v2/ListItem';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
@@ -27,12 +28,17 @@ interface SettingsNavLinkProps extends ListItemButtonProps {
|
||||
* @default true
|
||||
*/
|
||||
exact?: boolean;
|
||||
/**
|
||||
* Class name passed to the text element.
|
||||
*/
|
||||
textClassName?: string;
|
||||
}
|
||||
|
||||
function SettingsNavLink({
|
||||
exact = true,
|
||||
href,
|
||||
children,
|
||||
textClassName,
|
||||
...props
|
||||
}: SettingsNavLinkProps) {
|
||||
const router = useRouter();
|
||||
@@ -52,7 +58,7 @@ function SettingsNavLink({
|
||||
selected={active}
|
||||
{...props}
|
||||
>
|
||||
<ListItem.Text>{children}</ListItem.Text>
|
||||
<ListItem.Text className={textClassName}>{children}</ListItem.Text>
|
||||
</ListItem.Button>
|
||||
</ListItem.Root>
|
||||
);
|
||||
@@ -114,13 +120,13 @@ export default function SettingsSidebar({
|
||||
<Box
|
||||
component="aside"
|
||||
className={twMerge(
|
||||
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 px-2 pb-17 pt-2 motion-safe:transition-transform md:relative md:z-0 md:h-full md:py-2.5 md:transition-none',
|
||||
'absolute top-0 z-[35] flex h-full w-full flex-col justify-between overflow-auto border-r-1 pb-17 pt-2 motion-safe:transition-transform md:relative md:z-0 md:h-full md:pb-0 md:pt-2.5 md:transition-none',
|
||||
expanded ? 'translate-x-0' : '-translate-x-full md:translate-x-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<nav aria-label="Settings navigation">
|
||||
<nav aria-label="Settings navigation" className="px-2">
|
||||
<List className="grid gap-2">
|
||||
<SettingsNavLink
|
||||
href="/general"
|
||||
@@ -220,6 +226,20 @@ export default function SettingsSidebar({
|
||||
</SettingsNavLink>
|
||||
</List>
|
||||
</nav>
|
||||
<Box className="border-t">
|
||||
<SettingsNavLink
|
||||
href="/editor"
|
||||
exact={false}
|
||||
onClick={handleSelect}
|
||||
className="flex w-full border group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9"
|
||||
textClassName="flex w-full justify-center"
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-center space-x-4 py-2.5">
|
||||
<SlidersIcon />
|
||||
<span className="flex">Configuration Editor</span>
|
||||
</div>
|
||||
</SettingsNavLink>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<IconButton
|
||||
|
||||
132
dashboard/src/components/presentational/CodeBlock/CodeBlock.tsx
Normal file
132
dashboard/src/components/presentational/CodeBlock/CodeBlock.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { clsx } from 'clsx';
|
||||
import {
|
||||
forwardRef,
|
||||
type ComponentPropsWithoutRef,
|
||||
type ForwardedRef,
|
||||
type ReactElement,
|
||||
} from 'react';
|
||||
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { CopyToClipboardButton as CopyToClipboardButtonOriginal } from './CopyToClipboardButton';
|
||||
import { getNodeText } from './getNodeText';
|
||||
|
||||
export interface CodeBlockPropsBase {
|
||||
filename?: string;
|
||||
/**
|
||||
* Color of the filename text and the border underneath it when content is being shown.
|
||||
*/
|
||||
filenameColor?: string;
|
||||
/**
|
||||
* Text of the toast that appears when the code is copied to the clipboard.
|
||||
*/
|
||||
copyToClipboardToastTitle?: string;
|
||||
}
|
||||
|
||||
export type CodeBlockProps = CodeBlockPropsBase &
|
||||
Omit<ComponentPropsWithoutRef<'div'>, keyof CodeBlockPropsBase>;
|
||||
|
||||
/**
|
||||
* Different from CodeGroup because we cannot use Headless UI's Tab component outside a Tab.Group
|
||||
* Styling should look the same though.
|
||||
*/
|
||||
function CodeTabBar({
|
||||
filename,
|
||||
filenameColor,
|
||||
children,
|
||||
}: {
|
||||
filename: string;
|
||||
filenameColor?: string;
|
||||
children?: ReactElement;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex text-xs leading-6 text-slate-400">
|
||||
<div
|
||||
className="flex flex-none items-center border-b border-t border-t-transparent px-4 py-1"
|
||||
style={{ color: filenameColor, borderBottomColor: filenameColor }}
|
||||
>
|
||||
{filename}
|
||||
</div>
|
||||
<div className="bg-codeblock-tabs flex flex-auto items-center rounded-t border border-slate-500/30">
|
||||
{children && (
|
||||
<div className="flex flex-auto items-center justify-end space-x-4 px-4">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CopyToClipboardButtonProps
|
||||
extends Partial<
|
||||
ComponentPropsWithoutRef<typeof CopyToClipboardButtonOriginal>
|
||||
> {
|
||||
filenameColor?: string;
|
||||
tooltipColor?: string;
|
||||
toastTitle?: string;
|
||||
}
|
||||
|
||||
function CopyToClipboardButton({
|
||||
tooltipColor,
|
||||
filenameColor,
|
||||
textToCopy,
|
||||
toastTitle,
|
||||
...props
|
||||
}: CopyToClipboardButtonProps) {
|
||||
return (
|
||||
<CopyToClipboardButtonOriginal
|
||||
textToCopy={textToCopy}
|
||||
title={toastTitle}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const CodeBlock = forwardRef(
|
||||
(
|
||||
{
|
||||
filename,
|
||||
filenameColor,
|
||||
children,
|
||||
className,
|
||||
copyToClipboardToastTitle,
|
||||
...props
|
||||
}: CodeBlockProps,
|
||||
ref: ForwardedRef<HTMLDivElement>,
|
||||
) => (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.200',
|
||||
}}
|
||||
className={clsx(
|
||||
'not-prose relative mt-5 px-2',
|
||||
filename && 'pt-2',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{filename ? (
|
||||
<CodeTabBar filename={filename} filenameColor={filenameColor}>
|
||||
<CopyToClipboardButton
|
||||
filenameColor={filenameColor}
|
||||
textToCopy={getNodeText(children)}
|
||||
toastTitle={copyToClipboardToastTitle}
|
||||
className="relative"
|
||||
/>
|
||||
</CodeTabBar>
|
||||
) : (
|
||||
<CopyToClipboardButton
|
||||
filenameColor={filenameColor}
|
||||
textToCopy={getNodeText(children)}
|
||||
toastTitle={copyToClipboardToastTitle}
|
||||
className="absolute right-3 top-0"
|
||||
/>
|
||||
)}
|
||||
<pre className="overflow-x-auto">
|
||||
<code className="font-mono">{children}</code>
|
||||
</pre>
|
||||
</Box>
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,55 @@
|
||||
import { clsx } from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
IconButton,
|
||||
type IconButtonProps,
|
||||
} from '@/components/ui/v2/IconButton';
|
||||
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { copy } from '@/utils/copy';
|
||||
|
||||
export function CopyToClipboardButton({
|
||||
textToCopy,
|
||||
className,
|
||||
title,
|
||||
...props
|
||||
}: {
|
||||
textToCopy: string;
|
||||
title: string;
|
||||
} & IconButtonProps) {
|
||||
const [disabled, setDisabled] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Hide copy button if the browser does not support it
|
||||
if (typeof window !== 'undefined' && !navigator?.clipboard) {
|
||||
console.error(
|
||||
"The browser's Clipboard API is unavailable. The Clipboard API is only available on HTTPS.",
|
||||
);
|
||||
setDisabled(true);
|
||||
} else {
|
||||
setDisabled(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Hide copy button if you would copy an empty string
|
||||
if (!textToCopy || disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className={clsx('group', className)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
copy(textToCopy, title);
|
||||
}}
|
||||
aria-label={textToCopy}
|
||||
{...props}
|
||||
>
|
||||
<CopyIcon className="top-5 h-4 w-4" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Gets the text from a component as if you selected it with a mouse and copied it.
|
||||
export const getNodeText = (node: any): string => {
|
||||
if (['string', 'number'].includes(typeof node)) {
|
||||
// Convert number into string
|
||||
return node.toString();
|
||||
}
|
||||
|
||||
if (node instanceof Array) {
|
||||
return node.map(getNodeText).join('');
|
||||
}
|
||||
|
||||
if (typeof node === 'object' && node?.props?.children) {
|
||||
return getNodeText(node.props.children);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import {
|
||||
CodeBlock,
|
||||
type CodeBlockProps,
|
||||
type CodeBlockPropsBase,
|
||||
} from './CodeBlock';
|
||||
|
||||
export type { CodeBlockPropsBase, CodeBlockProps };
|
||||
export { CodeBlock };
|
||||
28
dashboard/src/components/ui/v2/Accordion/Accordion.tsx
Normal file
28
dashboard/src/components/ui/v2/Accordion/Accordion.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { styled } from '@mui/material';
|
||||
import type { AccordionProps as MaterialAccordionProps } from '@mui/material/Accordion';
|
||||
|
||||
import MaterialAccordion, { accordionClasses } from '@mui/material/Accordion';
|
||||
|
||||
export interface AccordionProps extends MaterialAccordionProps {}
|
||||
|
||||
const Accordion = styled(MaterialAccordion)<AccordionProps>(({ theme }) => ({
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
fontSize: theme.typography.pxToRem(12),
|
||||
lineHeight: theme.typography.pxToRem(16),
|
||||
borderBottom: `transparent solid 0px`,
|
||||
boxShadow: `none`,
|
||||
[`&.${accordionClasses.disabled}`]: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
[`&.${accordionClasses.root}`]: {
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
[`&.${accordionClasses.expanded}`]: {
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
Accordion.displayName = 'NhostAccordion';
|
||||
|
||||
export default Accordion;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { styled } from '@mui/material';
|
||||
import type { AccordionActionsProps as MaterialAccordionActionsProps } from '@mui/material/AccordionActions';
|
||||
|
||||
import MaterialAccordionActions from '@mui/material/AccordionActions';
|
||||
|
||||
export interface AccordionActionsProps extends MaterialAccordionActionsProps {}
|
||||
|
||||
const AccordionActions = styled(
|
||||
MaterialAccordionActions,
|
||||
)<AccordionActionsProps>(({ theme }) => ({
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
fontSize: theme.typography.pxToRem(12),
|
||||
lineHeight: theme.typography.pxToRem(16),
|
||||
}));
|
||||
|
||||
AccordionActions.displayName = 'NhostAccordionActions';
|
||||
|
||||
export default AccordionActions;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { styled } from '@mui/material';
|
||||
import type { AccordionDetailsProps as MaterialAccordionDetailsProps } from '@mui/material/AccordionDetails';
|
||||
|
||||
import MaterialAccordionDetails from '@mui/material/AccordionDetails';
|
||||
|
||||
export interface AccordionDetailsProps extends MaterialAccordionDetailsProps {}
|
||||
|
||||
const AccordionDetails = styled(
|
||||
MaterialAccordionDetails,
|
||||
)<AccordionDetailsProps>(({ theme }) => ({
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
fontSize: theme.typography.pxToRem(12),
|
||||
lineHeight: theme.typography.pxToRem(16),
|
||||
backgroundColor: theme.palette.grey[200],
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
}));
|
||||
|
||||
AccordionDetails.displayName = 'NhostAccordionDetails';
|
||||
|
||||
export default AccordionDetails;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { styled } from '@mui/material';
|
||||
import type { AccordionSummaryProps as MaterialAccordionSummaryProps } from '@mui/material/AccordionSummary';
|
||||
|
||||
import MaterialAccordionSummary, {
|
||||
accordionSummaryClasses,
|
||||
} from '@mui/material/AccordionSummary';
|
||||
|
||||
export interface AccordionSummaryProps extends MaterialAccordionSummaryProps {}
|
||||
|
||||
const AccordionSummary = styled(
|
||||
MaterialAccordionSummary,
|
||||
)<AccordionSummaryProps>(({ theme }) => ({
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
fontSize: theme.typography.pxToRem(12),
|
||||
lineHeight: theme.typography.pxToRem(16),
|
||||
[`&.${accordionSummaryClasses.disabled}`]: {
|
||||
backgroundColor: 'transparent',
|
||||
opacity: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
AccordionSummary.displayName = 'NhostAccordionSummary';
|
||||
|
||||
export default AccordionSummary;
|
||||
19
dashboard/src/components/ui/v2/Accordion/index.ts
Normal file
19
dashboard/src/components/ui/v2/Accordion/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import AccordionRoot from './Accordion';
|
||||
import AccordionActions from './AccordionActions';
|
||||
import AccordionDetails from './AccordionDetails';
|
||||
import AccordionSummary from './AccordionSummary';
|
||||
|
||||
export { default as BaseAccordion } from './Accordion';
|
||||
export * from './AccordionActions';
|
||||
export { default as AccordionActions } from './AccordionActions';
|
||||
export * from './AccordionDetails';
|
||||
export { default as AccordionDetails } from './AccordionDetails';
|
||||
export * from './AccordionSummary';
|
||||
export { default as AccordionSummary } from './AccordionSummary';
|
||||
|
||||
export const Accordion = {
|
||||
Root: AccordionRoot,
|
||||
Details: AccordionDetails,
|
||||
Summary: AccordionSummary,
|
||||
Actions: AccordionActions,
|
||||
};
|
||||
47
dashboard/src/components/ui/v2/Badge/Badge.tsx
Normal file
47
dashboard/src/components/ui/v2/Badge/Badge.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { styled } from '@mui/material';
|
||||
import type { BadgeProps as MaterialBadgeProps } from '@mui/material/Badge';
|
||||
import MaterialBadge from '@mui/material/Badge';
|
||||
import type { ElementType } from 'react';
|
||||
|
||||
export interface BadgeProps extends MaterialBadgeProps {
|
||||
/**
|
||||
* Custom component for the root node.
|
||||
*/
|
||||
component?: ElementType;
|
||||
}
|
||||
|
||||
const Badge = styled(MaterialBadge)<BadgeProps>(({ theme }) => ({
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
fontSize: theme.typography.pxToRem(12),
|
||||
lineHeight: theme.typography.pxToRem(16),
|
||||
fontWeight: 500,
|
||||
padding: 0,
|
||||
'& .MuiBadge-dot': {
|
||||
minWidth: '0.625rem',
|
||||
minHeight: '0.625rem',
|
||||
borderRadius: '50%',
|
||||
},
|
||||
'& .MuiBadge-standard': {
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
minWidth: '0.625rem',
|
||||
height: '0.625rem',
|
||||
borderRadius: '50%',
|
||||
},
|
||||
'& .MuiBadge-colorError': {
|
||||
backgroundColor: theme.palette.error.main,
|
||||
},
|
||||
'& .MuiBadge-colorWarning': {
|
||||
backgroundColor: theme.palette.warning.main,
|
||||
},
|
||||
'& .MuiBadge-colorSuccess': {
|
||||
backgroundColor: theme.palette.success.dark,
|
||||
},
|
||||
'& .MuiBadge-colorSecondary': {
|
||||
backgroundColor: theme.palette.grey[500],
|
||||
},
|
||||
}));
|
||||
|
||||
Badge.displayName = 'NhostBadge';
|
||||
|
||||
export default Badge;
|
||||
2
dashboard/src/components/ui/v2/Badge/index.ts
Normal file
2
dashboard/src/components/ui/v2/Badge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './Badge';
|
||||
export { default as Badge } from './Badge';
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Backdrop } from '@/components/ui/v2/Backdrop';
|
||||
import type { ButtonProps } from '@/components/ui/v2/Button';
|
||||
import type { DialogTitleProps } from '@/components/ui/v2/Dialog';
|
||||
import { styled } from '@mui/material';
|
||||
import type { DialogProps as MaterialDialogProps } from '@mui/material/Dialog';
|
||||
import MaterialDialog from '@mui/material/Dialog';
|
||||
import type { DialogActionsProps } from '@mui/material/DialogActions';
|
||||
import type { DialogContentProps } from '@mui/material/DialogContent';
|
||||
import type { DialogContentTextProps } from '@mui/material/DialogContentText';
|
||||
import type { DialogTitleProps } from '@mui/material/DialogTitle';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { render, screen } from '@/tests/testUtils';
|
||||
import { test } from 'vitest';
|
||||
import ErrorToast from './ErrorToast';
|
||||
|
||||
const oneMemberByWorkspaceError = {
|
||||
name: 'ApolloError',
|
||||
graphQLErrors: [
|
||||
{
|
||||
message: 'database query error',
|
||||
extensions: {
|
||||
path: '$.selectionSet.insertApp.args.object',
|
||||
code: 'unexpected',
|
||||
internal: {
|
||||
arguments: [],
|
||||
error: {
|
||||
description: null,
|
||||
exec_status: 'FatalError',
|
||||
hint: null,
|
||||
message:
|
||||
'Only one workspace member is allowed for individual plans',
|
||||
status_code: 'P0001',
|
||||
},
|
||||
prepared: false,
|
||||
statement: '.....',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
protocolErrors: [],
|
||||
clientErrors: [],
|
||||
networkError: null,
|
||||
message: 'database query error',
|
||||
};
|
||||
|
||||
const changeNodeInvalidVersionError = {
|
||||
name: 'ApolloError',
|
||||
graphQLErrors: [
|
||||
{
|
||||
message:
|
||||
'failed to resolve config: failed to validate config: config is not valid: #Config.functions.node.version: 2 errors in empty disjunction: (and 2 more errors)',
|
||||
path: ['replaceConfigRawJSON'],
|
||||
},
|
||||
],
|
||||
protocolErrors: [],
|
||||
clientErrors: [],
|
||||
networkError: null,
|
||||
message:
|
||||
'failed to resolve config: failed to validate config: config is not valid: #Config.functions.node.version: 2 errors in empty disjunction: (and 2 more errors)',
|
||||
};
|
||||
|
||||
test('should render the error message when creating a project with an individual plan in a workspace with multiple users', () => {
|
||||
const errorMessage =
|
||||
'An error occurred while creating the project. Please try again.';
|
||||
render(
|
||||
<ErrorToast
|
||||
isVisible
|
||||
errorMessage={errorMessage}
|
||||
error={oneMemberByWorkspaceError}
|
||||
close={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Only one workspace member is allowed for individual plans/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the error message when changing the node version to an invalid value in configuration editor', () => {
|
||||
const errorMessage =
|
||||
'An error occurred while saving configuration. Please try again.';
|
||||
render(
|
||||
<ErrorToast
|
||||
isVisible
|
||||
errorMessage={errorMessage}
|
||||
error={changeNodeInvalidVersionError}
|
||||
close={() => {}}
|
||||
/>,
|
||||
);
|
||||
const regex =
|
||||
/failed to resolve config: failed to validate config: config is not valid: #Config\.functions\.node\.version: 2 errors in empty disjunction: \(and 2 more errors\)/i;
|
||||
|
||||
expect(screen.getByText(regex)).toBeInTheDocument();
|
||||
});
|
||||
@@ -32,7 +32,7 @@ const getInternalErrorMessage = (
|
||||
const graphqlError = error.graphQLErrors?.[0];
|
||||
const graphqlExtensionsError = graphqlError?.extensions?.internal
|
||||
?.error as { message: string };
|
||||
return graphqlError.message || graphqlExtensionsError?.message || null;
|
||||
return graphqlExtensionsError?.message || graphqlError.message || null;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
function ExclamationFilledIcon(
|
||||
props: IconProps,
|
||||
ref: ForwardedRef<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<SvgIcon
|
||||
width="7"
|
||||
height="7"
|
||||
viewBox="0 0 7 7"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="Exclamation mark"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7 3.5C7 5.433 5.433 7 3.5 7C1.567 7 0 5.433 0 3.5C0 1.567 1.567 0 3.5 0C5.433 0 7 1.567 7 3.5ZM3.96667 5.36667C3.96667 5.6244 3.75773 5.83333 3.5 5.83333C3.24227 5.83333 3.03333 5.6244 3.03333 5.36667C3.03333 5.10893 3.24227 4.9 3.5 4.9C3.75773 4.9 3.96667 5.10893 3.96667 5.36667ZM3.5 1.16667C3.20564 1.16667 2.97296 1.41615 2.99345 1.70979L3.16724 4.20075C3.17943 4.37554 3.32478 4.51111 3.5 4.51111C3.67522 4.51111 3.82057 4.37554 3.83276 4.20075L4.00655 1.70979C4.02704 1.41615 3.79436 1.16667 3.5 1.16667Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
ExclamationFilledIcon.displayName = 'NhostExclamationFilledIcon';
|
||||
|
||||
export default forwardRef(ExclamationFilledIcon);
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ExclamationFilledIcon } from './ExclamationFilledIcon';
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
|
||||
function QuestionMarkIcon(props: IconProps) {
|
||||
return (
|
||||
<SvgIcon
|
||||
width="320"
|
||||
height="512"
|
||||
aria-label="Question mark"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 320 512"
|
||||
{...props}
|
||||
>
|
||||
<path d="M80 160c0-35.3 28.7-64 64-64h32c35.3 0 64 28.7 64 64v3.6c0 21.8-11.1 42.1-29.4 53.8l-42.2 27.1c-25.2 16.2-40.4 44.1-40.4 74V320c0 17.7 14.3 32 32 32s32-14.3 32-32v-1.4c0-8.2 4.2-15.8 11-20.2l42.2-27.1c36.6-23.6 58.8-64.1 58.8-107.7V160c0-70.7-57.3-128-128-128H144C73.3 32 16 89.3 16 160c0 17.7 14.3 32 32 32s32-14.3 32-32zm80 320a40 40 0 1 0 0-80 40 40 0 1 0 0 80z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
QuestionMarkIcon.displayName = 'NhostQuestionMarkIcon';
|
||||
|
||||
export default QuestionMarkIcon;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as QuestionMarkIcon } from './QuestionMarkIcon';
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
|
||||
function ServicesOutlinedIcon(props: IconProps) {
|
||||
return (
|
||||
<SvgIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="Services"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.8521 7.06294C15.8143 7.03192 15.485 6.77435 14.7986 6.73709C14.7878 6.7365 14.7769 6.73597 14.7659 6.73549C14.7223 6.7336 14.6772 6.7326 14.6307 6.7326C14.458 6.73338 14.2857 6.74604 14.1147 6.77049C14.0763 6.77598 14.0379 6.78208 13.9996 6.78876C13.9966 6.76748 13.9934 6.74642 13.9899 6.72556C13.9367 6.41145 13.8247 6.14509 13.6926 5.92522C13.5869 5.74941 13.4683 5.60332 13.3565 5.48631C13.1377 5.25722 12.9451 5.13955 12.9269 5.12843L12.7118 5L12.5703 5.21132C12.4532 5.39906 12.3569 5.59952 12.2832 5.80884C12.2455 5.916 12.2137 6.02549 12.188 6.13679C12.1785 6.17826 12.17 6.21957 12.1626 6.26069C12.0784 6.72436 12.1218 7.16514 12.2887 7.56346C12.3311 7.66471 12.3815 7.76323 12.4399 7.85868C12.0678 8.07332 11.471 8.12616 11.3503 8.13083H1.46954C1.21146 8.1312 1.00204 8.34712 1.00063 8.6143C0.989105 9.51047 1.13576 10.4013 1.43337 11.2429C1.77374 12.1671 2.28012 12.8478 2.93893 13.2644C3.67717 13.7325 4.87658 14 6.23616 14C6.85036 14.0019 7.4634 13.9444 8.06725 13.8281C8.90666 13.6685 9.71439 13.3648 10.457 12.9294C11.0689 12.5625 11.6196 12.0958 12.0879 11.5472C12.8644 10.6371 13.3296 9.62426 13.6755 8.72167C13.6783 8.7144 13.6811 8.70714 13.6839 8.69989H13.8221C14.6792 8.69989 15.2062 8.3448 15.4969 8.04724C15.69 7.85746 15.8408 7.62628 15.9386 7.36986L16 7.18397L15.8521 7.06294ZM11.9029 9.4592C11.6843 9.49512 11.4987 9.51062 11.3978 9.51453L11.374 9.51544H2.35702C2.41226 9.93468 2.51073 10.3477 2.65136 10.7474C2.90985 11.445 3.24967 11.8492 3.60646 12.0748L3.60778 12.0757C4.05853 12.3615 4.98755 12.6153 6.23616 12.6153H6.24016C6.77497 12.6171 7.30873 12.567 7.83441 12.4657L7.83733 12.4652C8.53499 12.3325 9.20535 12.0806 9.82092 11.7205C10.31 11.4265 10.7488 11.0538 11.1211 10.6177M11.9029 9.4592C11.6864 9.86254 11.4322 10.2531 11.1211 10.6177L11.9029 9.4592Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M2.38514 7.54505H3.7092C3.77306 7.54505 3.8248 7.49328 3.8248 7.42944V6.25005C3.82516 6.18619 3.77369 6.13415 3.70985 6.13379C3.70964 6.13379 3.70941 6.13379 3.7092 6.13379H2.38514C2.32128 6.13379 2.26953 6.18556 2.26953 6.24939V6.25005V7.42942C2.26953 7.49328 2.32128 7.54505 2.38514 7.54505Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M4.21003 7.54505H5.53409C5.59794 7.54505 5.64969 7.49328 5.64969 7.42944V6.25005C5.65005 6.18619 5.59857 6.13415 5.53472 6.13379C5.53451 6.13379 5.53427 6.13379 5.53407 6.13379H4.21001C4.14579 6.13379 4.09375 6.18583 4.09375 6.25005V7.42942C4.09413 7.49339 4.14606 7.54505 4.21003 7.54505Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M6.06192 7.54505H7.38597C7.44983 7.54505 7.50157 7.49328 7.50157 7.42944V6.25005C7.50193 6.18619 7.45046 6.13415 7.3866 6.13379C7.38639 6.13379 7.38616 6.13379 7.38595 6.13379H6.06189C5.99804 6.13379 5.94629 6.18556 5.94629 6.24939V6.25005V7.42942C5.94631 7.49328 5.99808 7.54505 6.06192 7.54505Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7.89295 7.54505H9.21701C9.28097 7.54505 9.33291 7.49341 9.33326 7.42944V6.25005C9.33326 6.18583 9.28122 6.13379 9.21701 6.13379H7.89295C7.82909 6.13379 7.77734 6.18556 7.77734 6.24939V6.25005V7.42942C7.77734 7.49328 7.82911 7.54505 7.89295 7.54505Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M4.21001 5.84874H5.53406C5.59801 5.84839 5.64967 5.79643 5.64967 5.73249V4.55311C5.64967 4.48925 5.5979 4.4375 5.53406 4.4375H4.21001C4.14606 4.4375 4.09411 4.48914 4.09375 4.55311V5.73249C4.09411 5.79656 4.14594 5.84839 4.21001 5.84874Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M6.06189 5.84874H7.38595C7.4499 5.84839 7.50156 5.79643 7.50156 5.73249V4.55311C7.50156 4.48925 7.44979 4.4375 7.38595 4.4375H6.06189C5.99804 4.4375 5.94629 4.48927 5.94629 4.55311V5.73249C5.94629 5.79643 5.99795 5.84839 6.06189 5.84874Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7.89295 5.84874H9.21701C9.28108 5.84839 9.33291 5.79656 9.33326 5.73249V4.55311C9.33291 4.48914 9.28097 4.4375 9.21701 4.4375H7.89295C7.82909 4.4375 7.77734 4.48927 7.77734 4.55311V5.73249C7.77734 5.79643 7.82901 5.84839 7.89295 5.84874Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7.89295 4.1515H9.21701C9.28097 4.1515 9.33291 4.09983 9.33326 4.03589V2.85584C9.33291 2.79188 9.28097 2.74023 9.21701 2.74023H7.89295C7.82909 2.74023 7.77734 2.79198 7.77734 2.85584V4.03587C7.77734 4.09973 7.82911 4.1515 7.89295 4.1515Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M9.73963 7.54505H11.0637C11.1277 7.54505 11.1796 7.49341 11.1799 7.42944V6.25005C11.1799 6.18583 11.1279 6.13379 11.0637 6.13379H9.73963C9.67579 6.13379 9.62402 6.18556 9.62402 6.24939V6.25005V7.42942C9.62402 7.49328 9.67579 7.54505 9.73963 7.54505Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
ServicesOutlinedIcon.displayName = 'NhostServicesIcon';
|
||||
|
||||
export default ServicesOutlinedIcon;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ServicesOutlinedIcon } from './ServicesOutlinedIcon';
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
|
||||
function SlidersIcon(props: IconProps) {
|
||||
return (
|
||||
<SvgIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="Sliders"
|
||||
{...props}
|
||||
>
|
||||
<line x1="21" x2="14" y1="4" y2="4" />
|
||||
<line x1="10" x2="3" y1="4" y2="4" />
|
||||
<line x1="21" x2="12" y1="12" y2="12" />
|
||||
<line x1="8" x2="3" y1="12" y2="12" />
|
||||
<line x1="21" x2="16" y1="20" y2="20" />
|
||||
<line x1="12" x2="3" y1="20" y2="20" />
|
||||
<line x1="14" x2="14" y1="2" y2="6" />
|
||||
<line x1="8" x2="8" y1="10" y2="14" />
|
||||
<line x1="16" x2="16" y1="18" y2="22" />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
SlidersIcon.displayName = 'NhostSlidersIcon';
|
||||
|
||||
export default SlidersIcon;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SlidersIcon } from './SlidersIcon';
|
||||
@@ -32,8 +32,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
import { DisableAIServiceConfirmationDialog } from './DisableAIServiceConfirmationDialog';
|
||||
|
||||
const MIN_POSTGRES_VERSION_SUPPORTING_AI = '14.6-20231018-1';
|
||||
import { isPostgresVersionValidForAI } from '@/features/ai/settings/utils/isPostgresVersionValidForAI';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
version: Yup.object({
|
||||
@@ -165,7 +164,7 @@ export default function AISettings() {
|
||||
]);
|
||||
|
||||
const toggleAIService = async (enabled: boolean) => {
|
||||
if (postgresVersion < MIN_POSTGRES_VERSION_SUPPORTING_AI) {
|
||||
if (!isPostgresVersionValidForAI(postgresVersion)) {
|
||||
toast.error(
|
||||
'In order to enable the AI service you need to update your database version to 14.6-20231018-1 or newer.',
|
||||
{
|
||||
@@ -495,3 +494,4 @@ export default function AISettings() {
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as isPostgresVersionValidForAI } from './isPostgresVersionValidForAI';
|
||||
@@ -0,0 +1,22 @@
|
||||
import { test, vi } from 'vitest';
|
||||
|
||||
import isPostgresVersionValidForAI from './isPostgresVersionValidForAI';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
test('greater than minimum version, minor version with two digits, should be valid', () => {
|
||||
const postgresVersion = '14.11-20240515-1';
|
||||
expect(isPostgresVersionValidForAI(postgresVersion)).toBe(true);
|
||||
});
|
||||
|
||||
test('less than minimum version, should be invalid', () => {
|
||||
const postgresVersion = '14.6-20221110-1';
|
||||
expect(isPostgresVersionValidForAI(postgresVersion)).toBe(false);
|
||||
});
|
||||
|
||||
test('equal to minimum version, should be valid', () => {
|
||||
const postgresVersion = '14.6-20231018-1';
|
||||
expect(isPostgresVersionValidForAI(postgresVersion)).toBe(true);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Check if the given postgres version is valid for enabling AI in the project
|
||||
*
|
||||
* @param postgresVersion - Postgres version used in the project.
|
||||
* @returns Whether is valid for enabling AI.
|
||||
*/
|
||||
export default function isPostgresVersionValidForAI(
|
||||
postgresVersion: string,
|
||||
): boolean {
|
||||
const MIN_POSTGRES_VERSION_SUPPORTING_AI = '14.6-20231018-1';
|
||||
|
||||
if (/^14\.6-/.test(postgresVersion)) {
|
||||
return postgresVersion >= MIN_POSTGRES_VERSION_SUPPORTING_AI;
|
||||
}
|
||||
|
||||
// Note: No need to account for versions less than 14.6
|
||||
return true;
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
@@ -7,11 +11,32 @@ import type { InputProps } from '@/components/ui/v2/Input';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { useGetPostgresSettingsQuery } from '@/utils/__generated__/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
useGetPostgresSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const databasePublicAccessValidationSchema = Yup.object({
|
||||
enablePublicAccess: Yup.bool(),
|
||||
});
|
||||
|
||||
type DatabasePublicAccessFormValues = Yup.InferType<
|
||||
typeof databasePublicAccessValidationSchema
|
||||
>;
|
||||
|
||||
export default function DatabaseConnectionInfo() {
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const { data, loading, error } = useGetPostgresSettingsQuery({
|
||||
@@ -19,6 +44,61 @@ export default function DatabaseConnectionInfo() {
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const enablePublicAccess =
|
||||
!!data?.config?.postgres?.resources?.enablePublicAccess;
|
||||
|
||||
const form = useForm<DatabasePublicAccessFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enablePublicAccess,
|
||||
},
|
||||
resolver: yupResolver(databasePublicAccessValidationSchema),
|
||||
});
|
||||
|
||||
async function handleSubmit(formValues: DatabasePublicAccessFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
postgres: {
|
||||
resources: {
|
||||
enablePublicAccess: formValues.enablePublicAccess,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
form.reset(formValues);
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
title: 'Apply your changes',
|
||||
component: <ApplyLocalSettingsDialog />,
|
||||
props: {
|
||||
PaperProps: {
|
||||
className: 'max-w-2xl',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Database settings are being updated...',
|
||||
successMessage: 'Database settings have been updated successfully.',
|
||||
errorMessage:
|
||||
"An error occurred while trying to update the project's database settings.",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
@@ -76,49 +156,72 @@ export default function DatabaseConnectionInfo() {
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsContainer
|
||||
title="Connection Info"
|
||||
description="Connect directly to the Postgres database with this information."
|
||||
slotProps={{ footer: { className: 'hidden' } }}
|
||||
className="grid grid-cols-6 gap-4 pb-2"
|
||||
>
|
||||
{settingsDatabaseCustomInputs.map(
|
||||
({ name, label, className, value: inputValue }) => (
|
||||
<Input
|
||||
key={name}
|
||||
label={label}
|
||||
required
|
||||
disabled
|
||||
value={inputValue}
|
||||
className={className}
|
||||
slotProps={{ inputRoot: { className: '!pr-8 truncate' } }}
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
endAdornment={
|
||||
<InputAdornment position="end" className="absolute right-2">
|
||||
<Button
|
||||
sx={{ minWidth: 0, padding: 0 }}
|
||||
color="secondary"
|
||||
variant="borderless"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copy(inputValue as string, `${label}`);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
const { formState } = form;
|
||||
|
||||
<Alert severity="info" className="col-span-6 text-left">
|
||||
To connect to the Postgres database directly, generate a new password,
|
||||
securely save it, and then modify your connection string with the newly
|
||||
created password.
|
||||
</Alert>
|
||||
</SettingsContainer>
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Public access"
|
||||
description={
|
||||
enablePublicAccess
|
||||
? 'Connect directly to the Postgres database with this information.'
|
||||
: 'Enable public access to your Postgres database.'
|
||||
}
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !formState.isDirty || maintenanceActive,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
className="grid grid-cols-6 gap-4 pb-2"
|
||||
switchId="enablePublicAccess"
|
||||
showSwitch
|
||||
>
|
||||
{enablePublicAccess && (
|
||||
<>
|
||||
{settingsDatabaseCustomInputs.map(
|
||||
({ name, label, className, value: inputValue }) => (
|
||||
<Input
|
||||
key={name}
|
||||
label={label}
|
||||
required
|
||||
disabled
|
||||
value={inputValue}
|
||||
className={className}
|
||||
slotProps={{ inputRoot: { className: '!pr-8 truncate' } }}
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
endAdornment={
|
||||
<InputAdornment
|
||||
position="end"
|
||||
className="absolute right-2"
|
||||
>
|
||||
<Button
|
||||
sx={{ minWidth: 0, padding: 0 }}
|
||||
color="secondary"
|
||||
variant="borderless"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copy(inputValue as string, `${label}`);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<Alert severity="info" className="col-span-6 text-left">
|
||||
To connect to the Postgres database directly, generate a new
|
||||
password, securely save it, and then modify your connection
|
||||
string with the newly created password.
|
||||
</Alert>
|
||||
</>
|
||||
)}
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ query GetPostgresSettings($appId: uuid!) {
|
||||
storage {
|
||||
capacity
|
||||
}
|
||||
enablePublicAccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import { useGetAnnouncementsQuery } from '@/utils/__generated__/graphql';
|
||||
import formatDistance from 'date-fns/formatDistance';
|
||||
|
||||
export default function Announcements() {
|
||||
const { data, loading, error } = useGetAnnouncementsQuery();
|
||||
const { data, loading, error } = useGetAnnouncementsQuery({
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
const announcements = data?.announcements || [];
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import { OverviewDeployments } from '@/features/projects/overview/components/OverviewDeployments';
|
||||
import { OverviewDocumentation } from '@/features/projects/overview/components/OverviewDocumentation';
|
||||
import { OverviewMetrics } from '@/features/projects/overview/components/OverviewMetrics';
|
||||
import { OverviewProjectHealth } from '@/features/projects/overview/components/OverviewProjectHealth';
|
||||
import { OverviewProjectInfo } from '@/features/projects/overview/components/OverviewProjectInfo';
|
||||
import { OverviewRepository } from '@/features/projects/overview/components/OverviewRepository';
|
||||
import { OverviewTopBar } from '@/features/projects/overview/components/OverviewTopBar';
|
||||
@@ -92,6 +93,8 @@ export default function ApplicationLive({
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row content-start gap-8 lg:col-span-1 lg:gap-12">
|
||||
<OverviewProjectHealth />
|
||||
<Divider />
|
||||
<OverviewProjectInfo />
|
||||
<Divider />
|
||||
<OverviewRepository />
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
useGetConfigRawJsonQuery,
|
||||
useReplaceConfigRawJsonMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { StreamLanguage } from '@codemirror/language';
|
||||
import { toml } from '@codemirror/legacy-modes/mode/toml';
|
||||
import * as TOML from '@iarna/toml';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { bbedit } from '@uiw/codemirror-theme-bbedit';
|
||||
import { githubDark } from '@uiw/codemirror-theme-github';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function TOMLEditor() {
|
||||
const theme = useTheme();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const [tomlCode, setTOMLCode] = useState('');
|
||||
const [previousTOMLCode, setPreviousTOMLCode] = useState(''); // used to revert changes
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false); // used to show loading spinner on save
|
||||
|
||||
const { openDialog } = useDialog();
|
||||
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
// fetch the initial TOML code from the server
|
||||
const { data, loading } = useGetConfigRawJsonQuery({
|
||||
variables: {
|
||||
appID: currentProject?.id,
|
||||
},
|
||||
skip: !currentProject,
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const [saveConfigMutation] = useReplaceConfigRawJsonMutation({
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const removeTOMLIndentation = (tomlStr: string) => {
|
||||
const trimmedLines = tomlStr.split('\n').map((line) => line.trimStart());
|
||||
return trimmedLines.join('\n');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Load TOML code from the server on initial load
|
||||
if (!loading && data) {
|
||||
const jsonData = JSON.parse(data?.configRawJSON);
|
||||
const tomlStr = TOML.stringify(jsonData);
|
||||
const unindentedTOMLConfig = removeTOMLIndentation(tomlStr);
|
||||
setTOMLCode(unindentedTOMLConfig);
|
||||
setPreviousTOMLCode(unindentedTOMLConfig);
|
||||
}
|
||||
}, [loading, data]);
|
||||
|
||||
const onChange = useCallback((value: string) => {
|
||||
setTOMLCode(value);
|
||||
setIsDirty(true);
|
||||
}, []);
|
||||
|
||||
const handleRevert = () => {
|
||||
setTOMLCode(previousTOMLCode);
|
||||
setIsDirty(false);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
let jsonEditedConfig;
|
||||
try {
|
||||
jsonEditedConfig = TOML.parse(tomlCode);
|
||||
} catch (error) {
|
||||
const toastStyle = getToastStyleProps();
|
||||
const { line, col } = error;
|
||||
let message = `An error occurred while parsing the TOML file. Please check the syntax.`;
|
||||
if (line !== undefined && col !== undefined) {
|
||||
message = `An error occurred while parsing the TOML file. Please check the syntax at line ${line}, column ${col}.`;
|
||||
}
|
||||
toast.error(message, {
|
||||
style: toastStyle.style,
|
||||
...toastStyle.error,
|
||||
});
|
||||
setIsSaving(false);
|
||||
return;
|
||||
}
|
||||
const rawJSONString = JSON.stringify(jsonEditedConfig);
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
const {
|
||||
data: { replaceConfigRawJSON: updatedConfig },
|
||||
} = await saveConfigMutation({
|
||||
variables: {
|
||||
appID: currentProject?.id,
|
||||
rawJSON: rawJSONString,
|
||||
},
|
||||
});
|
||||
|
||||
if (updatedConfig) {
|
||||
const jsonUpdatedConfig = JSON.parse(updatedConfig);
|
||||
const updatedTOMLConfig = TOML.stringify(jsonUpdatedConfig);
|
||||
const unindentedTOMLConfig = removeTOMLIndentation(updatedTOMLConfig);
|
||||
setTOMLCode(unindentedTOMLConfig);
|
||||
setPreviousTOMLCode(unindentedTOMLConfig);
|
||||
}
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
title: 'Apply your changes',
|
||||
component: <ApplyLocalSettingsDialog />,
|
||||
props: {
|
||||
PaperProps: {
|
||||
className: 'max-w-2xl',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
setIsDirty(false);
|
||||
setIsSaving(false);
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Saving configuration...',
|
||||
successMessage: 'Configuration has been saved successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while saving configuration. Please try again.',
|
||||
onError: () => {
|
||||
setIsSaving(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="flex h-full flex-col">
|
||||
<Box className="flex w-full flex-col space-y-2 border-b p-4">
|
||||
<Text className="font-semibold">Configuration Editor</Text>
|
||||
</Box>
|
||||
<Box className="h-full overflow-auto">
|
||||
{loading ? (
|
||||
<Box
|
||||
className="h-full w-full animate-pulse"
|
||||
sx={{ backgroundColor: 'grey.200' }}
|
||||
/>
|
||||
) : (
|
||||
<CodeMirror
|
||||
value={tomlCode}
|
||||
height="100%"
|
||||
width="100%"
|
||||
theme={theme.palette.mode === 'light' ? bbedit : githubDark}
|
||||
basicSetup={{
|
||||
searchKeymap: false,
|
||||
}}
|
||||
extensions={[StreamLanguage.define(toml)]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box className="grid w-full grid-flow-col justify-end gap-3 place-self-end border-t-1 px-4 py-3 md:justify-between">
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={loading || !isDirty}
|
||||
onClick={handleRevert}
|
||||
color="secondary"
|
||||
>
|
||||
Revert changes
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading || !isDirty}
|
||||
loading={isSaving}
|
||||
className="justify-self-end"
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as TOMLEditor } from './TOMLEditor';
|
||||
@@ -108,7 +108,7 @@ export default function useCurrentWorkspaceAndProject(): UseCurrentWorkspaceAndP
|
||||
id: null,
|
||||
countryCode: null,
|
||||
city: null,
|
||||
awsName: null,
|
||||
name: null,
|
||||
domain: null,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useServiceStatus } from './useServiceStatus';
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import type { ServiceHealthInfo } from '@/features/projects/overview/health';
|
||||
import {
|
||||
useGetProjectServicesHealthQuery,
|
||||
type GetProjectServicesHealthQuery,
|
||||
type GetProjectServicesHealthQueryVariables,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import type { QueryHookOptions } from '@apollo/client';
|
||||
import { useVisibilityChange } from '@uidotdev/usehooks';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export interface UseServiceStatusOptions
|
||||
extends QueryHookOptions<
|
||||
GetProjectServicesHealthQuery,
|
||||
GetProjectServicesHealthQueryVariables
|
||||
> {
|
||||
shouldPoll?: boolean;
|
||||
}
|
||||
|
||||
export default function useServiceStatus(
|
||||
options: UseServiceStatusOptions = {},
|
||||
) {
|
||||
const isPlatform = useIsPlatform();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const isVisible = useVisibilityChange();
|
||||
|
||||
const { data, loading, refetch, startPolling, stopPolling } =
|
||||
useGetProjectServicesHealthQuery({
|
||||
...options,
|
||||
variables: { ...options.variables, appId: currentProject?.id },
|
||||
skip: !isPlatform || !currentProject,
|
||||
skipPollAttempt: () => !isVisible,
|
||||
});
|
||||
|
||||
// Fetch when mounted
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (options.shouldPoll) {
|
||||
startPolling(options.pollInterval || 10000);
|
||||
}
|
||||
|
||||
return () => stopPolling();
|
||||
}, [stopPolling, startPolling, options.shouldPoll, options.pollInterval]);
|
||||
|
||||
const serviceMap: { [key: string]: ServiceHealthInfo | undefined } = {};
|
||||
data?.getProjectStatus?.services.forEach((service) => {
|
||||
serviceMap[service.name] = service;
|
||||
});
|
||||
const {
|
||||
'hasura-auth': auth,
|
||||
'hasura-storage': storage,
|
||||
postgres,
|
||||
hasura,
|
||||
ai,
|
||||
...run
|
||||
} = serviceMap;
|
||||
|
||||
return {
|
||||
loading,
|
||||
auth,
|
||||
storage,
|
||||
postgres,
|
||||
hasura,
|
||||
ai,
|
||||
run,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useSoftwareVersionsInfo } from './useSoftwareVersionsInfo';
|
||||
@@ -0,0 +1,156 @@
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import {
|
||||
Software_Type_Enum,
|
||||
useGetConfiguredVersionsQuery,
|
||||
useGetRecommendedSoftwareVersionsQuery,
|
||||
type GetConfiguredVersionsQuery,
|
||||
type GetConfiguredVersionsQueryVariables,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import type { QueryHookOptions } from '@apollo/client';
|
||||
import { useVisibilityChange } from '@uidotdev/usehooks';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export interface UseSoftwareVersionsInfoOptions
|
||||
extends QueryHookOptions<
|
||||
GetConfiguredVersionsQuery,
|
||||
GetConfiguredVersionsQueryVariables
|
||||
> {}
|
||||
|
||||
type ServiceVersionInfo = {
|
||||
configuredVersion: string | undefined;
|
||||
recommendedVersions: string[];
|
||||
isVersionMismatch: boolean;
|
||||
};
|
||||
|
||||
export default function useSoftwareVersionsInfo(
|
||||
options: UseSoftwareVersionsInfoOptions = {},
|
||||
): {
|
||||
loading: boolean;
|
||||
auth: ServiceVersionInfo;
|
||||
storage: ServiceVersionInfo;
|
||||
postgres: ServiceVersionInfo;
|
||||
hasura: ServiceVersionInfo;
|
||||
ai: ServiceVersionInfo;
|
||||
isAIEnabled: boolean;
|
||||
} {
|
||||
const isPlatform = useIsPlatform();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const isVisible = useVisibilityChange();
|
||||
|
||||
// Recommended software versions are not polled by default
|
||||
const { data: recommendedVersionsData, loading: loadingRecommendedVersions } =
|
||||
useGetRecommendedSoftwareVersionsQuery({
|
||||
skip: !isPlatform || !currentProject,
|
||||
});
|
||||
|
||||
const {
|
||||
data: configuredVersionsData,
|
||||
loading: loadingConfiguredVersions,
|
||||
refetch: refetchConfiguredVersions,
|
||||
stopPolling,
|
||||
} = useGetConfiguredVersionsQuery({
|
||||
...options,
|
||||
variables: { ...options.variables, appId: currentProject?.id },
|
||||
skip: !isPlatform || !currentProject,
|
||||
skipPollAttempt: () => !isVisible,
|
||||
pollInterval: options.pollInterval || 10000,
|
||||
});
|
||||
|
||||
// fetch when mounted
|
||||
useEffect(() => {
|
||||
refetchConfiguredVersions();
|
||||
return () => stopPolling();
|
||||
}, [refetchConfiguredVersions, stopPolling]);
|
||||
|
||||
const recommendedVersions = {
|
||||
'hasura-auth': [],
|
||||
'hasura-storage': [],
|
||||
postgres: [],
|
||||
hasura: [],
|
||||
ai: [],
|
||||
};
|
||||
|
||||
recommendedVersionsData?.softwareVersions.forEach(({ software, version }) => {
|
||||
switch (software) {
|
||||
case Software_Type_Enum.Auth:
|
||||
recommendedVersions['hasura-auth'].push(version);
|
||||
break;
|
||||
case Software_Type_Enum.Storage:
|
||||
recommendedVersions['hasura-storage'].push(version);
|
||||
break;
|
||||
case Software_Type_Enum.PostgreSql:
|
||||
recommendedVersions.postgres.push(version);
|
||||
break;
|
||||
case Software_Type_Enum.Hasura:
|
||||
recommendedVersions.hasura.push(version);
|
||||
break;
|
||||
case Software_Type_Enum.Graphite:
|
||||
recommendedVersions.ai.push(version);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const isVersionMismatch = (
|
||||
service: string,
|
||||
configuredVersion: string | undefined,
|
||||
) =>
|
||||
!recommendedVersions[service].some(
|
||||
(version) => version === configuredVersion,
|
||||
);
|
||||
|
||||
// Check if configured version can't be found in recommended versions
|
||||
const isAuthVersionMismatch = isVersionMismatch(
|
||||
'hasura-auth',
|
||||
configuredVersionsData?.config?.auth?.version,
|
||||
);
|
||||
const isStorageVersionMismatch = isVersionMismatch(
|
||||
'hasura-storage',
|
||||
configuredVersionsData?.config?.storage?.version,
|
||||
);
|
||||
const isPostgresVersionMismatch = isVersionMismatch(
|
||||
'postgres',
|
||||
configuredVersionsData?.config?.postgres?.version,
|
||||
);
|
||||
const isHasuraVersionMismatch = isVersionMismatch(
|
||||
'hasura',
|
||||
configuredVersionsData?.config?.hasura?.version,
|
||||
);
|
||||
const isAIVersionMismatch = isVersionMismatch(
|
||||
'ai',
|
||||
configuredVersionsData?.config?.ai?.version,
|
||||
);
|
||||
|
||||
return {
|
||||
loading: loadingConfiguredVersions || loadingRecommendedVersions,
|
||||
auth: {
|
||||
configuredVersion: configuredVersionsData?.config?.auth?.version,
|
||||
recommendedVersions: recommendedVersions['hasura-auth'],
|
||||
isVersionMismatch: isAuthVersionMismatch,
|
||||
},
|
||||
storage: {
|
||||
configuredVersion: configuredVersionsData?.config?.storage?.version,
|
||||
recommendedVersions: recommendedVersions['hasura-storage'],
|
||||
isVersionMismatch: isStorageVersionMismatch,
|
||||
},
|
||||
postgres: {
|
||||
configuredVersion: configuredVersionsData?.config?.postgres?.version,
|
||||
recommendedVersions: recommendedVersions.postgres,
|
||||
isVersionMismatch: isPostgresVersionMismatch,
|
||||
},
|
||||
hasura: {
|
||||
configuredVersion: configuredVersionsData?.config?.hasura?.version,
|
||||
recommendedVersions: recommendedVersions.hasura,
|
||||
isVersionMismatch: isHasuraVersionMismatch,
|
||||
},
|
||||
ai: {
|
||||
configuredVersion: configuredVersionsData?.config?.ai?.version,
|
||||
recommendedVersions: recommendedVersions.ai,
|
||||
isVersionMismatch: isAIVersionMismatch,
|
||||
},
|
||||
isAIEnabled: Boolean(configuredVersionsData?.config?.ai),
|
||||
};
|
||||
}
|
||||
@@ -31,7 +31,7 @@ afterEach(() => {
|
||||
|
||||
const region: ProjectFragment['region'] = {
|
||||
id: '1',
|
||||
awsName: 'eu-west-1',
|
||||
name: 'eu-west-1',
|
||||
domain: 'nhost.run',
|
||||
city: 'Dublin',
|
||||
countryCode: 'IE',
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function generateAppServiceUrl(
|
||||
const constructedDomain = [
|
||||
subdomain,
|
||||
service,
|
||||
region?.awsName,
|
||||
region?.name,
|
||||
region?.domain || 'nhost.run',
|
||||
]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -165,7 +165,7 @@ export default function AuthDomain() {
|
||||
<VerifyDomain
|
||||
recordType="CNAME"
|
||||
hostname={auth_fqdn}
|
||||
value={`lb.${currentProject.region.awsName}.${currentProject.region.domain}.`}
|
||||
value={`lb.${currentProject.region.name}.${currentProject.region.domain}.`}
|
||||
onHostNameVerified={() => setIsVerified(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -167,7 +167,7 @@ export default function HasuraDomain() {
|
||||
<VerifyDomain
|
||||
recordType="CNAME"
|
||||
hostname={hasura_fqdn}
|
||||
value={`lb.${currentProject.region.awsName}.${currentProject.region.domain}.`}
|
||||
value={`lb.${currentProject.region.name}.${currentProject.region.domain}.`}
|
||||
onHostNameVerified={() => setIsVerified(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -162,7 +162,7 @@ export default function RunServicePortDomain({
|
||||
<VerifyDomain
|
||||
recordType="CNAME"
|
||||
hostname={runServicePortFQDN}
|
||||
value={`lb.${currentProject.region.awsName}.${currentProject.region.domain}.`}
|
||||
value={`lb.${currentProject.region.name}.${currentProject.region.domain}.`}
|
||||
onHostNameVerified={() => setIsVerified(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -169,7 +169,7 @@ export default function ServerlessFunctionsDomain() {
|
||||
<VerifyDomain
|
||||
recordType="CNAME"
|
||||
hostname={functions_fqdn}
|
||||
value={`lb.${currentProject.region.awsName}.${currentProject.region.domain}.`}
|
||||
value={`lb.${currentProject.region.name}.${currentProject.region.domain}.`}
|
||||
onHostNameVerified={() => setIsVerified(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +131,7 @@ export default function VerifyDomain({
|
||||
</div>
|
||||
{isPlatform ? (
|
||||
<Button
|
||||
disabled={loading || !hostname || isPlatform}
|
||||
disabled={loading || !hostname}
|
||||
onClick={handleVerifyDomain}
|
||||
className="mt-4 sm:absolute sm:bottom-0 sm:right-0 sm:mt-0"
|
||||
>
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
|
||||
const systemEnvironmentVariables = [
|
||||
{ key: 'NHOST_SUBDOMAIN', value: currentProject.subdomain },
|
||||
{ key: 'NHOST_REGION', value: currentProject.region.awsName },
|
||||
{ key: 'NHOST_REGION', value: currentProject.region.name },
|
||||
{
|
||||
key: 'NHOST_HASURA_URL',
|
||||
value:
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { CheckIcon } from '@/components/ui/v2/icons/CheckIcon';
|
||||
import { QuestionMarkIcon } from '@/components/ui/v2/icons/QuestionMarkIcon';
|
||||
import { serviceStateToThemeColor } from '@/features/projects/overview/health';
|
||||
import { ServiceState } from '@/utils/__generated__/graphql';
|
||||
|
||||
interface AccordionHealthBadgeProps {
|
||||
serviceState?: ServiceState;
|
||||
unknownState?: boolean;
|
||||
/*
|
||||
* Blinking animation to indicate that the service is updating.
|
||||
*/
|
||||
blink?: boolean;
|
||||
}
|
||||
|
||||
export default function AccordionHealthBadge({
|
||||
serviceState,
|
||||
unknownState,
|
||||
blink,
|
||||
}: AccordionHealthBadgeProps) {
|
||||
if (unknownState) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: serviceStateToThemeColor.get(serviceState),
|
||||
}}
|
||||
className="flex h-2.5 w-2.5 items-center justify-center rounded-full"
|
||||
>
|
||||
<QuestionMarkIcon
|
||||
sx={{
|
||||
color: (theme) =>
|
||||
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.100',
|
||||
}}
|
||||
className="h-3/4 w-3/4 stroke-2"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (serviceState === ServiceState.Running) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: serviceStateToThemeColor.get(serviceState),
|
||||
}}
|
||||
className="flex h-2.5 w-2.5 items-center justify-center rounded-full"
|
||||
>
|
||||
<CheckIcon
|
||||
sx={{
|
||||
color: (theme) =>
|
||||
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.100',
|
||||
}}
|
||||
className="h-3/4 w-3/4 stroke-2"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: serviceStateToThemeColor.get(serviceState),
|
||||
}}
|
||||
className={`h-2.5 w-2.5 rounded-full ${blink ? 'animate-pulse' : ''}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as AccordionHealthBadge } from './AccordionHealthBadge';
|
||||
@@ -0,0 +1,213 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { AIIcon } from '@/components/ui/v2/icons/AIIcon';
|
||||
import { DatabaseIcon } from '@/components/ui/v2/icons/DatabaseIcon';
|
||||
import { HasuraIcon } from '@/components/ui/v2/icons/HasuraIcon';
|
||||
import { ServicesOutlinedIcon } from '@/components/ui/v2/icons/ServicesOutlinedIcon';
|
||||
import { StorageIcon } from '@/components/ui/v2/icons/StorageIcon';
|
||||
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useServiceStatus } from '@/features/projects/common/hooks/useServiceStatus';
|
||||
import { useSoftwareVersionsInfo } from '@/features/projects/common/hooks/useSoftwareVersionsInfo';
|
||||
import { OverviewProjectHealthModal } from '@/features/projects/overview/components/OverviewProjectHealthModal';
|
||||
import { ProjectHealthCard } from '@/features/projects/overview/components/ProjectHealthCard';
|
||||
import { RunStatusTooltip } from '@/features/projects/overview/components/RunStatusTooltip';
|
||||
import { ServiceVersionTooltip } from '@/features/projects/overview/components/ServiceVersionTooltip';
|
||||
import {
|
||||
baseServices,
|
||||
findHighestImportanceState,
|
||||
} from '@/features/projects/overview/health';
|
||||
|
||||
export default function OverviewProjectHealth() {
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
|
||||
const {
|
||||
loading: loadingVersions,
|
||||
auth: authVersionInfo,
|
||||
storage: storageVersionInfo,
|
||||
postgres: postgresVersionInfo,
|
||||
hasura: hasuraVersionInfo,
|
||||
ai: aiVersionInfo,
|
||||
isAIEnabled,
|
||||
} = useSoftwareVersionsInfo();
|
||||
|
||||
const {
|
||||
loading: loadingProjectServicesHealth,
|
||||
auth: authStatus,
|
||||
storage: storageStatus,
|
||||
postgres: postgresStatus,
|
||||
hasura: hasuraStatus,
|
||||
ai: aiStatus,
|
||||
run: runStatus,
|
||||
} = useServiceStatus({
|
||||
shouldPoll: true,
|
||||
});
|
||||
|
||||
if (loadingVersions || loadingProjectServicesHealth) {
|
||||
return (
|
||||
<div className="grid grid-flow-row content-start gap-6">
|
||||
<Text variant="h3">Project Health</Text>
|
||||
<div className="flex flex-row flex-wrap items-center justify-start gap-2 lg:gap-2">
|
||||
<ProjectHealthCard
|
||||
isLoading
|
||||
icon={<UserIcon className="m-1 h-6 w-6" />}
|
||||
/>
|
||||
<ProjectHealthCard
|
||||
isLoading
|
||||
icon={<DatabaseIcon className="m-1 h-6 w-6" />}
|
||||
/>
|
||||
<ProjectHealthCard
|
||||
isLoading
|
||||
icon={<StorageIcon className="m-1 h-6 w-6" />}
|
||||
/>
|
||||
<ProjectHealthCard
|
||||
isLoading
|
||||
icon={<HasuraIcon className="m-1 h-6 w-6" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const openHealthModal = async (
|
||||
defaultExpanded: keyof typeof baseServices | 'run',
|
||||
) => {
|
||||
openDialog({
|
||||
component: (
|
||||
<OverviewProjectHealthModal defaultExpanded={defaultExpanded} />
|
||||
),
|
||||
props: {
|
||||
PaperProps: { className: 'p-0 max-w-2xl w-full' },
|
||||
titleProps: {
|
||||
onClose: closeDialog,
|
||||
},
|
||||
},
|
||||
title: 'Service State',
|
||||
});
|
||||
};
|
||||
|
||||
const authTooltipElem = (
|
||||
<ServiceVersionTooltip
|
||||
serviceName={baseServices['hasura-auth'].displayName}
|
||||
serviceKey="hasura-auth"
|
||||
usedVersion={authVersionInfo?.configuredVersion ?? ''}
|
||||
recommendedVersionMismatch={authVersionInfo?.isVersionMismatch}
|
||||
recommendedVersions={authVersionInfo?.recommendedVersions}
|
||||
openHealthModal={openHealthModal}
|
||||
state={authStatus?.state}
|
||||
/>
|
||||
);
|
||||
|
||||
const hasuraTooltipElem = (
|
||||
<ServiceVersionTooltip
|
||||
serviceName={baseServices.hasura.displayName}
|
||||
serviceKey="hasura"
|
||||
usedVersion={hasuraVersionInfo?.configuredVersion ?? ''}
|
||||
recommendedVersionMismatch={hasuraVersionInfo?.isVersionMismatch}
|
||||
recommendedVersions={hasuraVersionInfo?.recommendedVersions}
|
||||
openHealthModal={openHealthModal}
|
||||
state={hasuraStatus?.state}
|
||||
/>
|
||||
);
|
||||
|
||||
const postgresTooltipElem = (
|
||||
<ServiceVersionTooltip
|
||||
serviceName={baseServices.postgres.displayName}
|
||||
serviceKey="postgres"
|
||||
usedVersion={postgresVersionInfo?.configuredVersion ?? ''}
|
||||
recommendedVersionMismatch={postgresVersionInfo?.isVersionMismatch}
|
||||
recommendedVersions={postgresVersionInfo?.recommendedVersions}
|
||||
openHealthModal={openHealthModal}
|
||||
state={postgresStatus?.state}
|
||||
/>
|
||||
);
|
||||
|
||||
const storageTooltipElem = (
|
||||
<ServiceVersionTooltip
|
||||
serviceName={baseServices['hasura-storage'].displayName}
|
||||
serviceKey="hasura-storage"
|
||||
usedVersion={storageVersionInfo?.configuredVersion ?? ''}
|
||||
recommendedVersionMismatch={storageVersionInfo?.isVersionMismatch}
|
||||
recommendedVersions={storageVersionInfo?.recommendedVersions}
|
||||
openHealthModal={openHealthModal}
|
||||
state={storageStatus?.state}
|
||||
/>
|
||||
);
|
||||
|
||||
const aiTooltipElem = (
|
||||
<ServiceVersionTooltip
|
||||
serviceName={baseServices.ai.displayName}
|
||||
serviceKey="ai"
|
||||
usedVersion={aiVersionInfo?.configuredVersion ?? ''}
|
||||
recommendedVersionMismatch={aiVersionInfo?.isVersionMismatch}
|
||||
recommendedVersions={aiVersionInfo?.recommendedVersions}
|
||||
openHealthModal={openHealthModal}
|
||||
state={aiStatus?.state}
|
||||
/>
|
||||
);
|
||||
|
||||
const runServices = Object.values(runStatus).filter((service) =>
|
||||
service?.name?.startsWith('run-'),
|
||||
);
|
||||
|
||||
const runServicesStates = runServices.map((service) => service.state);
|
||||
|
||||
const runServicesState = findHighestImportanceState(runServicesStates);
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row content-start gap-6">
|
||||
<Text variant="h3">Project Health</Text>
|
||||
|
||||
{currentProject && (
|
||||
<div className="flex flex-row flex-wrap items-center justify-start gap-2 lg:gap-2">
|
||||
<ProjectHealthCard
|
||||
icon={<UserIcon className="m-1 h-6 w-6" />}
|
||||
tooltip={authTooltipElem}
|
||||
isVersionMismatch={authVersionInfo?.isVersionMismatch}
|
||||
state={authStatus?.state}
|
||||
/>
|
||||
<ProjectHealthCard
|
||||
icon={<DatabaseIcon className="m-1 h-6 w-6" />}
|
||||
tooltip={postgresTooltipElem}
|
||||
isVersionMismatch={postgresVersionInfo?.isVersionMismatch}
|
||||
state={postgresStatus?.state}
|
||||
/>
|
||||
<ProjectHealthCard
|
||||
icon={<StorageIcon className="m-1 h-6 w-6" />}
|
||||
tooltip={storageTooltipElem}
|
||||
isVersionMismatch={storageVersionInfo?.isVersionMismatch}
|
||||
state={storageStatus?.state}
|
||||
/>
|
||||
<ProjectHealthCard
|
||||
icon={<HasuraIcon className="m-1 h-6 w-6" />}
|
||||
tooltip={hasuraTooltipElem}
|
||||
isVersionMismatch={hasuraVersionInfo?.isVersionMismatch}
|
||||
state={hasuraStatus?.state}
|
||||
/>
|
||||
{isAIEnabled && (
|
||||
<ProjectHealthCard
|
||||
icon={<AIIcon className="m-1 h-6 w-6" />}
|
||||
tooltip={aiTooltipElem}
|
||||
isVersionMismatch={aiVersionInfo?.isVersionMismatch}
|
||||
state={aiStatus?.state}
|
||||
/>
|
||||
)}
|
||||
{Object.values(runServices).length > 0 && (
|
||||
<ProjectHealthCard
|
||||
icon={<ServicesOutlinedIcon className="m-1 h-6 w-6" />}
|
||||
tooltip={
|
||||
<RunStatusTooltip
|
||||
servicesStatusInfo={Object.values(runServices)}
|
||||
openHealthModal={openHealthModal}
|
||||
/>
|
||||
}
|
||||
state={runServicesState}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as OverviewProjectHealth } from './OverviewProjectHealth';
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { AIIcon } from '@/components/ui/v2/icons/AIIcon';
|
||||
import { DatabaseIcon } from '@/components/ui/v2/icons/DatabaseIcon';
|
||||
import { HasuraIcon } from '@/components/ui/v2/icons/HasuraIcon';
|
||||
import { ServicesOutlinedIcon } from '@/components/ui/v2/icons/ServicesOutlinedIcon';
|
||||
import { StorageIcon } from '@/components/ui/v2/icons/StorageIcon';
|
||||
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
|
||||
import { useServiceStatus } from '@/features/projects/common/hooks/useServiceStatus';
|
||||
import { ServiceAccordion } from '@/features/projects/overview/components/ServiceAccordion';
|
||||
import {
|
||||
findHighestImportanceState,
|
||||
type baseServices,
|
||||
} from '@/features/projects/overview/health';
|
||||
import { removeTypename } from '@/utils/helpers';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface OverviewProjectHealthModalProps {
|
||||
defaultExpanded?: keyof typeof baseServices | 'run';
|
||||
}
|
||||
|
||||
export default function OverviewProjectHealthModal({
|
||||
defaultExpanded,
|
||||
}: OverviewProjectHealthModalProps) {
|
||||
const { auth, storage, postgres, hasura, ai, run } = useServiceStatus({
|
||||
fetchPolicy: 'cache-only',
|
||||
shouldPoll: false,
|
||||
});
|
||||
|
||||
const runServices = Object.values(run).filter((service) =>
|
||||
service.name.startsWith('run-'),
|
||||
);
|
||||
|
||||
const isAuthExpandedByDefault = defaultExpanded === 'hasura-auth';
|
||||
const isPostgresExpandedByDefault = defaultExpanded === 'postgres';
|
||||
const isStorageExpandedByDefault = defaultExpanded === 'hasura-storage';
|
||||
const isHasuraExpandedByDefault = defaultExpanded === 'hasura';
|
||||
const isAIExpandedByDefault = defaultExpanded === 'ai';
|
||||
const isRunExpandedByDefault = defaultExpanded === 'run';
|
||||
|
||||
const getServiceInfo = (service) => {
|
||||
const info = removeTypename(service);
|
||||
return JSON.stringify(info, null, 2);
|
||||
};
|
||||
|
||||
const serviceInfo = {
|
||||
auth: getServiceInfo(auth),
|
||||
storage: getServiceInfo(storage),
|
||||
postgres: getServiceInfo(postgres),
|
||||
hasura: getServiceInfo(hasura),
|
||||
ai: getServiceInfo(ai),
|
||||
run: getServiceInfo(Object.values(runServices)),
|
||||
};
|
||||
|
||||
const runServicesState = findHighestImportanceState(
|
||||
Object.values(runServices).map((service) => service.state),
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className={twMerge('w-full rounded-lg pt-2 text-left')}>
|
||||
<Box
|
||||
sx={{
|
||||
borderColor: 'text.dark',
|
||||
}}
|
||||
className="grid grid-flow-row"
|
||||
>
|
||||
<Divider />
|
||||
<ServiceAccordion
|
||||
icon={<UserIcon className="h-4 w-4" />}
|
||||
serviceName="Auth"
|
||||
serviceInfo={serviceInfo.auth}
|
||||
replicaCount={auth?.replicas?.length}
|
||||
serviceState={auth?.state}
|
||||
defaultExpanded={isAuthExpandedByDefault}
|
||||
/>
|
||||
<Divider />
|
||||
<ServiceAccordion
|
||||
icon={<DatabaseIcon className="h-4 w-4" />}
|
||||
serviceName="Postgres"
|
||||
serviceInfo={serviceInfo.postgres}
|
||||
replicaCount={postgres?.replicas?.length}
|
||||
serviceState={postgres?.state}
|
||||
defaultExpanded={isPostgresExpandedByDefault}
|
||||
/>
|
||||
<Divider />
|
||||
<ServiceAccordion
|
||||
icon={<StorageIcon className="h-4 w-4" />}
|
||||
serviceName="Storage"
|
||||
serviceInfo={serviceInfo.storage}
|
||||
replicaCount={storage?.replicas?.length}
|
||||
serviceState={storage?.state}
|
||||
defaultExpanded={isStorageExpandedByDefault}
|
||||
/>
|
||||
<Divider />
|
||||
<ServiceAccordion
|
||||
icon={<HasuraIcon className="h-4 w-4" />}
|
||||
serviceName="Hasura"
|
||||
serviceInfo={serviceInfo.hasura}
|
||||
replicaCount={hasura?.replicas?.length}
|
||||
serviceState={hasura?.state}
|
||||
defaultExpanded={isHasuraExpandedByDefault}
|
||||
/>
|
||||
{ai ? (
|
||||
<>
|
||||
<Divider />
|
||||
<ServiceAccordion
|
||||
icon={<AIIcon className="h-4 w-4" />}
|
||||
serviceName="AI"
|
||||
serviceInfo={serviceInfo.ai}
|
||||
replicaCount={ai?.replicas?.length}
|
||||
serviceState={ai?.state}
|
||||
defaultExpanded={isAIExpandedByDefault}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{runServices && Object.values(runServices).length > 0 ? (
|
||||
<>
|
||||
<Divider />
|
||||
<ServiceAccordion
|
||||
icon={<ServicesOutlinedIcon className="h-4 w-4" />}
|
||||
serviceName="Run"
|
||||
serviceInfo={serviceInfo.run}
|
||||
replicaCount={0}
|
||||
serviceState={runServicesState}
|
||||
defaultExpanded={isRunExpandedByDefault}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as OverviewProjectHealthModal } from './OverviewProjectHealthModal';
|
||||
@@ -7,7 +7,7 @@ export default function OverviewProjectInfo() {
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const { region, subdomain } = currentProject || {};
|
||||
const isRegionAvailable =
|
||||
region?.awsName && region?.countryCode && region?.city;
|
||||
region?.name && region?.countryCode && region?.city;
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row content-start gap-6">
|
||||
@@ -17,7 +17,7 @@ export default function OverviewProjectInfo() {
|
||||
<div className="grid grid-flow-row gap-3">
|
||||
<InfoCard
|
||||
title="Region"
|
||||
value={region?.awsName}
|
||||
value={region?.name}
|
||||
customValue={
|
||||
region?.countryCode &&
|
||||
region?.city && (
|
||||
@@ -30,7 +30,7 @@ export default function OverviewProjectInfo() {
|
||||
/>
|
||||
|
||||
<Text className="truncate text-sm font-medium">
|
||||
{region.city} ({region.awsName})
|
||||
{region.city} ({region.name})
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Badge, type BadgeProps } from '@/components/ui/v2/Badge';
|
||||
import { CheckIcon } from '@/components/ui/v2/icons/CheckIcon';
|
||||
import { ExclamationFilledIcon } from '@/components/ui/v2/icons/ExclamationFilledIcon';
|
||||
import { QuestionMarkIcon } from '@/components/ui/v2/icons/QuestionMarkIcon';
|
||||
|
||||
export interface ProjectHealthBadgeProps extends BadgeProps {
|
||||
badgeVariant?: 'standard' | 'dot';
|
||||
badgeColor?: 'success' | 'error' | 'warning' | 'secondary';
|
||||
unknownState?: boolean;
|
||||
showExclamation?: boolean;
|
||||
showCheckIcon?: boolean;
|
||||
isLoading?: boolean;
|
||||
blink?: boolean;
|
||||
}
|
||||
|
||||
export default function ProjectHealthBadge({
|
||||
badgeColor,
|
||||
badgeVariant,
|
||||
showExclamation,
|
||||
showCheckIcon,
|
||||
unknownState,
|
||||
blink,
|
||||
children,
|
||||
...props
|
||||
}: ProjectHealthBadgeProps) {
|
||||
let innerBadgeContent = null;
|
||||
if (unknownState) {
|
||||
innerBadgeContent = (
|
||||
<QuestionMarkIcon
|
||||
sx={{
|
||||
color: (theme) =>
|
||||
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.100',
|
||||
}}
|
||||
className="h-2 w-2 stroke-2"
|
||||
/>
|
||||
);
|
||||
} else if (showCheckIcon) {
|
||||
innerBadgeContent = (
|
||||
<CheckIcon
|
||||
sx={{
|
||||
color: (theme) =>
|
||||
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.100',
|
||||
}}
|
||||
className="h-2 w-2 stroke-2"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!badgeColor) {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
if (showExclamation) {
|
||||
return (
|
||||
<Badge
|
||||
variant="standard"
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
badgeContent={
|
||||
<ExclamationFilledIcon
|
||||
sx={{
|
||||
color: (theme) =>
|
||||
theme.palette.mode === 'dark' ? 'grey.900' : 'grey.600',
|
||||
}}
|
||||
className="h-2.5 w-2.5"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Badge
|
||||
color={badgeColor}
|
||||
variant={badgeVariant}
|
||||
badgeContent={innerBadgeContent}
|
||||
sx={{
|
||||
color: (theme) =>
|
||||
theme.palette.mode === 'dark' ? 'grey.900' : 'text.primary',
|
||||
}}
|
||||
componentsProps={{
|
||||
badge: {
|
||||
className: blink ? 'animate-pulse' : '',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Badge>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge
|
||||
color={badgeColor}
|
||||
variant={badgeVariant}
|
||||
badgeContent={innerBadgeContent}
|
||||
componentsProps={{
|
||||
badge: {
|
||||
className: blink ? 'animate-pulse' : '',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ProjectHealthBadge } from './ProjectHealthBadge';
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Tooltip, tooltipClasses } from '@/components/ui/v2/Tooltip';
|
||||
import { ProjectHealthBadge } from '@/features/projects/overview/components/ProjectHealthBadge';
|
||||
import { serviceStateToBadgeColor } from '@/features/projects/overview/health';
|
||||
import { ServiceState } from '@/utils/__generated__/graphql';
|
||||
import type { ImageProps } from 'next/image';
|
||||
import Image from 'next/image';
|
||||
import type { ReactElement } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface ProjectHealthCardProps extends BoxProps {
|
||||
/**
|
||||
* Label of the card icon.
|
||||
*/
|
||||
alt?: string;
|
||||
/**
|
||||
* Tooltip of the card.
|
||||
*/
|
||||
tooltip?: ReactElement | null;
|
||||
/**
|
||||
* Icon to display on the card.
|
||||
*/
|
||||
icon: string | ReactElement;
|
||||
/**
|
||||
* Light version of the icon. This is used for the dark mode.
|
||||
*/
|
||||
lightIcon?: string | ReactElement;
|
||||
/**
|
||||
* Determines whether the icon should have a background.
|
||||
* @default false
|
||||
*/
|
||||
disableIconBackground?: boolean;
|
||||
/**
|
||||
* Determines whether the icon is a react component.
|
||||
* @default true
|
||||
*/
|
||||
iconIsComponent?: boolean;
|
||||
/**
|
||||
* Props to be passed to the internal components.
|
||||
*/
|
||||
slotProps?: {
|
||||
imgIcon?: Partial<ImageProps>;
|
||||
};
|
||||
/**
|
||||
* State of the service.
|
||||
*/
|
||||
state?: ServiceState;
|
||||
|
||||
/**
|
||||
* Determines whether the version is mismatched with recommended version.
|
||||
*/
|
||||
isVersionMismatch?: boolean;
|
||||
|
||||
/**
|
||||
* Determines whether the card is loading.
|
||||
*/
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export default function ProjectHealthCard({
|
||||
alt,
|
||||
tooltip,
|
||||
icon,
|
||||
iconIsComponent = true,
|
||||
className,
|
||||
slotProps = {},
|
||||
isVersionMismatch = false,
|
||||
isLoading = false,
|
||||
state,
|
||||
...props
|
||||
}: ProjectHealthCardProps) {
|
||||
const badgeColor = serviceStateToBadgeColor.get(state);
|
||||
const unknownState = state === undefined;
|
||||
let badgeVariant: 'dot' | 'standard' = 'dot';
|
||||
if (state === ServiceState.Running || unknownState) {
|
||||
badgeVariant = 'standard';
|
||||
}
|
||||
const showCheckIcon = state === ServiceState.Running;
|
||||
const shouldBlink = state === ServiceState.Updating;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={tooltip}
|
||||
slotProps={{
|
||||
popper: {
|
||||
sx: {
|
||||
[`&.${tooltipClasses.popper} .${tooltipClasses.tooltip}`]: {
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === 'dark' ? 'grey.100' : 'grey.200',
|
||||
minWidth: '18rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className={twMerge(
|
||||
'grid aspect-square min-w-12 max-w-14 grid-flow-row gap-0 rounded-md p-0',
|
||||
className,
|
||||
)}
|
||||
sx={{ backgroundColor: 'grey.200' }}
|
||||
{...props}
|
||||
>
|
||||
<div className="grid grid-flow-col items-center justify-center">
|
||||
<ProjectHealthBadge
|
||||
badgeColor={!isLoading ? badgeColor : undefined}
|
||||
badgeVariant={badgeVariant}
|
||||
showCheckIcon={showCheckIcon}
|
||||
showExclamation={isVersionMismatch}
|
||||
unknownState={unknownState}
|
||||
blink={shouldBlink}
|
||||
>
|
||||
{iconIsComponent
|
||||
? icon
|
||||
: typeof icon === 'string' && (
|
||||
<Image
|
||||
src={icon}
|
||||
alt={alt}
|
||||
width={slotProps.imgIcon?.width}
|
||||
height={slotProps.imgIcon?.height}
|
||||
{...slotProps.imgIcon}
|
||||
/>
|
||||
)}
|
||||
</ProjectHealthBadge>
|
||||
</div>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ProjectHealthCard } from './ProjectHealthCard';
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import {
|
||||
serviceStateToThemeColor,
|
||||
type baseServices,
|
||||
type ServiceHealthInfo,
|
||||
} from '@/features/projects/overview/health';
|
||||
import { ServiceState } from '@/generated/graphql';
|
||||
|
||||
export interface RunStatusTooltipProps {
|
||||
servicesStatusInfo?: Array<ServiceHealthInfo>;
|
||||
openHealthModal?: (
|
||||
defaultExpanded?: keyof typeof baseServices | 'run',
|
||||
) => void;
|
||||
}
|
||||
|
||||
export default function RunStatusTooltip({
|
||||
servicesStatusInfo,
|
||||
openHealthModal,
|
||||
}: RunStatusTooltipProps) {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3 px-2 py-3">
|
||||
<ol className="m-0 flex flex-col gap-3">
|
||||
{servicesStatusInfo.map((service) => (
|
||||
<li
|
||||
key={service.name}
|
||||
className="flex flex-row items-center gap-4 text-ellipsis text-nowrap leading-5"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: serviceStateToThemeColor.get(service.state),
|
||||
}}
|
||||
className={`h-3 w-3 flex-shrink-0 rounded-full ${
|
||||
service.state === ServiceState.Updating ? 'animate-pulse' : ''
|
||||
}`}
|
||||
/>
|
||||
<Text
|
||||
sx={{
|
||||
color: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? 'text.primary'
|
||||
: 'text.primary',
|
||||
}}
|
||||
className="font-semibold"
|
||||
>
|
||||
{service.name}
|
||||
</Text>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<Button variant="outlined" onClick={() => openHealthModal('run')}>
|
||||
View state
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as RunStatusTooltip } from './RunStatusTooltip';
|
||||
@@ -0,0 +1,95 @@
|
||||
import { CodeBlock } from '@/components/presentational/CodeBlock';
|
||||
import { Accordion } from '@/components/ui/v2/Accordion';
|
||||
import { ChevronDownIcon } from '@/components/ui/v2/icons/ChevronDownIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { AccordionHealthBadge } from '@/features/projects/overview/components/AccordionHealthBadge';
|
||||
import { ServiceState } from '@/utils/__generated__/graphql';
|
||||
import Image from 'next/image';
|
||||
import { type ReactElement } from 'react';
|
||||
|
||||
export interface ServiceAccordionProps {
|
||||
serviceName: string;
|
||||
serviceInfo: string;
|
||||
replicaCount: number;
|
||||
serviceState: ServiceState;
|
||||
/**
|
||||
* Icon to display on the accordion.
|
||||
*/
|
||||
icon?: string | ReactElement;
|
||||
/**
|
||||
* Label of the icon.
|
||||
*/
|
||||
alt?: string;
|
||||
iconIsComponent?: boolean;
|
||||
defaultExpanded?: boolean;
|
||||
}
|
||||
|
||||
export default function ServiceAccordion({
|
||||
serviceName,
|
||||
serviceInfo,
|
||||
replicaCount,
|
||||
serviceState,
|
||||
icon,
|
||||
iconIsComponent = true,
|
||||
alt,
|
||||
defaultExpanded = false,
|
||||
}: ServiceAccordionProps) {
|
||||
const unknownState = serviceState === undefined;
|
||||
|
||||
const replicasLabel = replicaCount === 1 ? 'replica' : 'replicas';
|
||||
|
||||
const blink = serviceState === ServiceState.Updating;
|
||||
|
||||
return (
|
||||
<Accordion.Root defaultExpanded={defaultExpanded}>
|
||||
<Accordion.Summary
|
||||
expandIcon={
|
||||
<ChevronDownIcon
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
aria-controls="panel1-content"
|
||||
id="panel1-header"
|
||||
className="px-6"
|
||||
>
|
||||
<div className="flex flex-row justify-between gap-2 py-2">
|
||||
<div className="flex items-center gap-3">
|
||||
{iconIsComponent
|
||||
? icon
|
||||
: typeof icon === 'string' && <Image src={icon} alt={alt} />}
|
||||
<Text
|
||||
sx={{ color: 'text.primary' }}
|
||||
variant="h4"
|
||||
className="font-semibold"
|
||||
>
|
||||
{serviceName}{' '}
|
||||
{!unknownState && replicaCount && replicasLabel ? (
|
||||
<Text
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
component="span"
|
||||
className="font-semibold"
|
||||
>
|
||||
({replicaCount} {replicasLabel})
|
||||
</Text>
|
||||
) : null}
|
||||
</Text>
|
||||
<AccordionHealthBadge
|
||||
serviceState={serviceState}
|
||||
unknownState={unknownState}
|
||||
blink={blink}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Summary>
|
||||
<Accordion.Details>
|
||||
<CodeBlock copyToClipboardToastTitle={`${serviceName} status`}>
|
||||
{serviceInfo}
|
||||
</CodeBlock>
|
||||
</Accordion.Details>
|
||||
</Accordion.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ServiceAccordion } from './ServiceAccordion';
|
||||
@@ -0,0 +1,161 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import type { baseServices } from '@/features/projects/overview/health';
|
||||
import { ServiceState } from '@/utils/__generated__/graphql';
|
||||
import { useTheme } from '@mui/material';
|
||||
|
||||
interface ServiceVersionTooltipProps {
|
||||
serviceName?: string;
|
||||
serviceKey?: keyof typeof baseServices;
|
||||
usedVersion?: string;
|
||||
recommendedVersionMismatch?: boolean;
|
||||
recommendedVersions?: string[];
|
||||
children?: React.ReactNode;
|
||||
openHealthModal?: (
|
||||
defaultExpanded?: keyof typeof baseServices | 'run',
|
||||
) => void;
|
||||
state?: ServiceState;
|
||||
}
|
||||
|
||||
function ServiceVersionTooltip({
|
||||
serviceName,
|
||||
usedVersion,
|
||||
recommendedVersionMismatch,
|
||||
recommendedVersions,
|
||||
children,
|
||||
openHealthModal,
|
||||
state,
|
||||
serviceKey,
|
||||
}: ServiceVersionTooltipProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
let errorMessage = '';
|
||||
if (state === ServiceState.UpdateError) {
|
||||
errorMessage =
|
||||
'degraded or failed to update, click on view state for further details';
|
||||
} else if (state === ServiceState.Error || state === ServiceState.None) {
|
||||
errorMessage =
|
||||
'is offline due to errors, click on view state for further details';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 px-2 py-3">
|
||||
<div className="flex flex-row justify-between gap-6">
|
||||
<Text
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === 'dark'
|
||||
? 'text.secondary'
|
||||
: 'text.secondary',
|
||||
}}
|
||||
variant="h4"
|
||||
component="p"
|
||||
className="text-sm+"
|
||||
>
|
||||
service
|
||||
</Text>
|
||||
<Text
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === 'dark' ? 'text.primary' : 'text.primary',
|
||||
}}
|
||||
variant="h4"
|
||||
component="p"
|
||||
className="text-sm+ font-semibold"
|
||||
>
|
||||
{serviceName}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between gap-6">
|
||||
<Text
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === 'dark'
|
||||
? 'text.secondary'
|
||||
: 'text.secondary',
|
||||
}}
|
||||
variant="h4"
|
||||
component="p"
|
||||
className="text-sm+"
|
||||
>
|
||||
version
|
||||
</Text>
|
||||
<Text
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === 'dark' ? 'text.primary' : 'text.primary',
|
||||
}}
|
||||
variant="h4"
|
||||
component="p"
|
||||
className="text-sm+ font-bold"
|
||||
>
|
||||
{usedVersion}
|
||||
</Text>
|
||||
</div>
|
||||
{recommendedVersionMismatch && (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.300',
|
||||
}}
|
||||
className="rounded-md p-2"
|
||||
>
|
||||
<Text
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === 'dark' ? 'text.primary' : 'text.primary',
|
||||
}}
|
||||
variant="body1"
|
||||
component="p"
|
||||
className="text-sm+"
|
||||
>
|
||||
{serviceName} is not using a recommended version. Recommended
|
||||
version(s):
|
||||
</Text>
|
||||
<ul className="list-disc text-sm+">
|
||||
{recommendedVersions.map((version) => (
|
||||
<li className="ml-6 list-item" key={version}>
|
||||
<Text
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === 'dark'
|
||||
? 'text.primary'
|
||||
: 'text.primary',
|
||||
}}
|
||||
variant="body1"
|
||||
component="p"
|
||||
>
|
||||
{version}
|
||||
</Text>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Box>
|
||||
)}
|
||||
{errorMessage ? (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark' ? 'error.dark' : 'error.main',
|
||||
}}
|
||||
className="rounded-md p-2"
|
||||
>
|
||||
<Text
|
||||
variant="body1"
|
||||
component="p"
|
||||
className="text-sm+ font-semibold text-white"
|
||||
>
|
||||
{serviceName} {errorMessage}
|
||||
</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
<Button variant="outlined" onClick={() => openHealthModal(serviceKey)}>
|
||||
View state
|
||||
</Button>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServiceVersionTooltip;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ServiceVersionTooltip } from './ServiceVersionTooltip';
|
||||
80
dashboard/src/features/projects/overview/health/health.ts
Normal file
80
dashboard/src/features/projects/overview/health/health.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
ServiceState,
|
||||
type GetProjectServicesHealthQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
|
||||
export type ServiceHealthInfo =
|
||||
GetProjectServicesHealthQuery['getProjectStatus']['services'][number];
|
||||
|
||||
export const baseServices = {
|
||||
'hasura-auth': {
|
||||
displayName: 'Auth',
|
||||
softwareVersionsName: 'Auth',
|
||||
},
|
||||
hasura: {
|
||||
displayName: 'Hasura',
|
||||
softwareVersionsName: 'Hasura',
|
||||
},
|
||||
postgres: {
|
||||
displayName: 'Postgres',
|
||||
softwareVersionsName: 'PostgreSQL',
|
||||
},
|
||||
'hasura-storage': {
|
||||
displayName: 'Storage',
|
||||
softwareVersionsName: 'Storage',
|
||||
},
|
||||
ai: {
|
||||
displayName: 'Graphite',
|
||||
softwareVersionsName: 'Graphite',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const serviceStateToThemeColor = new Map<ServiceState, string>([
|
||||
[ServiceState.Running, 'success.dark'],
|
||||
[ServiceState.Error, 'error.main'],
|
||||
[ServiceState.UpdateError, 'error.main'],
|
||||
[ServiceState.Updating, 'warning.dark'],
|
||||
[ServiceState.None, 'error.main'],
|
||||
[undefined, 'grey.500'],
|
||||
]);
|
||||
|
||||
export const serviceStateToBadgeColor = new Map<
|
||||
ServiceState,
|
||||
'success' | 'error' | 'warning' | 'secondary'
|
||||
>([
|
||||
[ServiceState.Running, 'success'],
|
||||
[ServiceState.Error, 'error'],
|
||||
[ServiceState.UpdateError, 'error'],
|
||||
[ServiceState.Updating, 'warning'],
|
||||
[ServiceState.None, 'error'],
|
||||
[undefined, 'secondary'], // secondary is used for unknown states
|
||||
]);
|
||||
|
||||
/**
|
||||
* Returns the highest importance state from a list of service states
|
||||
* Example: [Running, Running, Error] => Error
|
||||
*/
|
||||
export const findHighestImportanceState = (
|
||||
servicesStates: ServiceState[],
|
||||
): ServiceState => {
|
||||
const serviceStateToImportance = new Map([
|
||||
[ServiceState.Running, 0],
|
||||
[ServiceState.Updating, 1],
|
||||
[ServiceState.UpdateError, 2],
|
||||
[ServiceState.Error, 3],
|
||||
[ServiceState.None, 4],
|
||||
]);
|
||||
|
||||
if (servicesStates.length === 0) {
|
||||
return ServiceState.None;
|
||||
}
|
||||
|
||||
return servicesStates.reduce((acc, state) => {
|
||||
if (
|
||||
serviceStateToImportance.get(state) > serviceStateToImportance.get(acc)
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
return acc;
|
||||
}, ServiceState.Running);
|
||||
};
|
||||
1
dashboard/src/features/projects/overview/health/index.ts
Normal file
1
dashboard/src/features/projects/overview/health/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './health';
|
||||
@@ -190,7 +190,7 @@ export default function ServiceForm({
|
||||
image:
|
||||
values.image.length > 0
|
||||
? values.image
|
||||
: `registry.${currentProject.region.awsName}.${currentProject.region.domain}/${newServiceID}`,
|
||||
: `registry.${currentProject.region.name}.${currentProject.region.domain}/${newServiceID}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -361,7 +361,7 @@ export default function ServiceForm({
|
||||
{isPlatform && serviceID && serviceImage && (
|
||||
<InfoCard
|
||||
title="Private registry"
|
||||
value={`registry.${currentProject.region.awsName}.${currentProject.region.domain}/${serviceID}`}
|
||||
value={`registry.${currentProject.region.name}.${currentProject.region.domain}/${serviceID}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function PortsFormSection() {
|
||||
const getPortURL = (_port: string | number, subdomain: string) => {
|
||||
const port = Number(_port) > 0 ? Number(_port) : '[port]';
|
||||
|
||||
return `https://${subdomain}-${port}.svc.${currentProject?.region.awsName}.${currentProject?.region.domain}`;
|
||||
return `https://${subdomain}-${port}.svc.${currentProject?.region.name}.${currentProject?.region.domain}`;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function ServiceDetailsDialog({
|
||||
const getPortURL = (_port: string | number) => {
|
||||
const port = Number(_port) > 0 ? Number(_port) : '[port]';
|
||||
|
||||
return `https://${subdomain}-${port}.svc.${currentProject?.region.awsName}.${currentProject?.region.domain}`;
|
||||
return `https://${subdomain}-${port}.svc.${currentProject?.region.name}.${currentProject?.region.domain}`;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -44,7 +44,7 @@ export default function ServiceDetailsDialog({
|
||||
<Text color="secondary">Private registry</Text>
|
||||
<InfoCard
|
||||
title=""
|
||||
value={`registry.${currentProject.region.awsName}.${currentProject.region.domain}/${serviceID}`}
|
||||
value={`registry.${currentProject.region.name}.${currentProject.region.domain}/${serviceID}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
19
dashboard/src/gql/app/getConfiguredVersions.graphql
Normal file
19
dashboard/src/gql/app/getConfiguredVersions.graphql
Normal file
@@ -0,0 +1,19 @@
|
||||
query getConfiguredVersions($appId: uuid!) {
|
||||
config(appID: $appId, resolve: true) {
|
||||
auth {
|
||||
version
|
||||
}
|
||||
postgres {
|
||||
version
|
||||
}
|
||||
hasura {
|
||||
version
|
||||
}
|
||||
ai {
|
||||
version
|
||||
}
|
||||
storage {
|
||||
version
|
||||
}
|
||||
}
|
||||
}
|
||||
20
dashboard/src/gql/app/getProjectServicesHealth.gql
Normal file
20
dashboard/src/gql/app/getProjectServicesHealth.gql
Normal file
@@ -0,0 +1,20 @@
|
||||
query getProjectServicesHealth($appId: String!) {
|
||||
getProjectStatus(appID: $appId) {
|
||||
services {
|
||||
name
|
||||
state
|
||||
replicas {
|
||||
ready
|
||||
date
|
||||
errors {
|
||||
name
|
||||
lastError {
|
||||
reason
|
||||
exitCode
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
dashboard/src/gql/app/settings/getConfigRawJSON.graphql
Normal file
3
dashboard/src/gql/app/settings/getConfigRawJSON.graphql
Normal file
@@ -0,0 +1,3 @@
|
||||
query getConfigRawJSON ($appID: uuid!) {
|
||||
configRawJSON(appID: $appID, resolve: false)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation ReplaceConfigRawJSON($appID: uuid!, $rawJSON: String!) {
|
||||
replaceConfigRawJSON(appID: $appID, rawJSON: $rawJSON)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ mutation UpdateConfig($appId: uuid!, $config: ConfigConfigUpdateInput!) {
|
||||
storage {
|
||||
capacity
|
||||
}
|
||||
enablePublicAccess
|
||||
}
|
||||
}
|
||||
ai {
|
||||
|
||||
@@ -39,7 +39,7 @@ fragment Project on apps {
|
||||
region {
|
||||
id
|
||||
countryCode
|
||||
awsName
|
||||
name
|
||||
domain
|
||||
city
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
query getRecommendedSoftwareVersions {
|
||||
softwareVersions {
|
||||
software
|
||||
version
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { SettingsLayout } from '@/components/layout/SettingsLayout';
|
||||
import { TOMLEditor } from '@/features/projects/common/components/settings/TOMLEditor';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
export default function TOMLEditorPage() {
|
||||
return <TOMLEditor />;
|
||||
}
|
||||
|
||||
TOMLEditorPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <SettingsLayout>{page}</SettingsLayout>;
|
||||
};
|
||||
@@ -7,41 +7,55 @@ import { Button } from '@/components/ui/v2/Button';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { WorkspaceAndProjectList } from '@/features/projects/common/components/WorkspaceAndProjectList';
|
||||
import { WorkspaceSidebar } from '@/features/projects/common/components/WorkspaceSidebar';
|
||||
import { useGetAllWorkspacesAndProjectsQuery } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
useGetAllWorkspacesAndProjectsQuery,
|
||||
type GetAllWorkspacesAndProjectsQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { NetworkStatus } from '@apollo/client';
|
||||
import { darken } from '@mui/system';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import NavLink from 'next/link';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useState, type ReactElement } from 'react';
|
||||
|
||||
export default function IndexPage() {
|
||||
const user = useUserData();
|
||||
const { data, loading, startPolling, stopPolling } =
|
||||
const [, setPollInterval] = useState(1_000);
|
||||
|
||||
// keep polling for workspaces until there is a workspace available.
|
||||
// We do this because when a user signs up a workspace is created automatically
|
||||
// and the serverless function can take some time to complete.
|
||||
const { data, startPolling, stopPolling, networkStatus } =
|
||||
useGetAllWorkspacesAndProjectsQuery({
|
||||
skip: !user,
|
||||
notifyOnNetworkStatusChange: true,
|
||||
onError: () => {
|
||||
// When there's an error (graphql, network error) apply an exponential backoff strategy
|
||||
setPollInterval((prevInterval) => {
|
||||
const newInterval = Math.min(60_000, prevInterval * 2);
|
||||
startPolling(newInterval);
|
||||
return newInterval;
|
||||
});
|
||||
},
|
||||
onCompleted: (queryData: GetAllWorkspacesAndProjectsQuery) => {
|
||||
if (!queryData?.workspaces.length) {
|
||||
setPollInterval(1000);
|
||||
startPolling(1000);
|
||||
} else {
|
||||
setPollInterval(0);
|
||||
stopPolling();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// keep showing loading indicator while polling
|
||||
const loading = networkStatus === NetworkStatus.loading;
|
||||
|
||||
const numberOfProjects = data?.workspaces.reduce(
|
||||
(projectCount, currentWorkspace) =>
|
||||
projectCount + currentWorkspace.projects.length,
|
||||
0,
|
||||
);
|
||||
|
||||
// keep polling for workspaces until there is a workspace available.
|
||||
// We do this because when a user signs up a workspace is created automatically
|
||||
// and the serverless function can take some time to complete.
|
||||
useEffect(() => {
|
||||
startPolling(1000);
|
||||
}, [startPolling]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data?.workspaces.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopPolling();
|
||||
}, [data?.workspaces, stopPolling]);
|
||||
|
||||
if ((!data && loading) || !user) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export const mockApplication: Project = {
|
||||
appStates: [],
|
||||
subdomain: '',
|
||||
region: {
|
||||
awsName: 'us-east-1',
|
||||
name: 'us-east-1',
|
||||
city: 'New York',
|
||||
countryCode: 'US',
|
||||
id: '1',
|
||||
|
||||
425
dashboard/src/utils/__generated__/graphql.ts
generated
425
dashboard/src/utils/__generated__/graphql.ts
generated
@@ -1037,6 +1037,26 @@ export type ConfigAuthsessionaccessTokenCustomClaimsUpdateInput = {
|
||||
value?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigAutoscaler = {
|
||||
__typename?: 'ConfigAutoscaler';
|
||||
maxReplicas: Scalars['ConfigUint8'];
|
||||
};
|
||||
|
||||
export type ConfigAutoscalerComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigAutoscalerComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigAutoscalerComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigAutoscalerComparisonExp>>;
|
||||
maxReplicas?: InputMaybe<ConfigUint8ComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigAutoscalerInsertInput = {
|
||||
maxReplicas: Scalars['ConfigUint8'];
|
||||
};
|
||||
|
||||
export type ConfigAutoscalerUpdateInput = {
|
||||
maxReplicas?: InputMaybe<Scalars['ConfigUint8']>;
|
||||
};
|
||||
|
||||
export type ConfigBooleanComparisonExp = {
|
||||
_eq?: InputMaybe<Scalars['Boolean']>;
|
||||
_in?: InputMaybe<Array<Scalars['Boolean']>>;
|
||||
@@ -1803,7 +1823,9 @@ export type ConfigPostgresInsertInput = {
|
||||
/** Resources for the service */
|
||||
export type ConfigPostgresResources = {
|
||||
__typename?: 'ConfigPostgresResources';
|
||||
autoscaler?: Maybe<ConfigAutoscaler>;
|
||||
compute?: Maybe<ConfigResourcesCompute>;
|
||||
enablePublicAccess?: Maybe<Scalars['Boolean']>;
|
||||
networking?: Maybe<ConfigNetworking>;
|
||||
/** Number of replicas for a service */
|
||||
replicas?: Maybe<Scalars['ConfigUint8']>;
|
||||
@@ -1814,21 +1836,27 @@ export type ConfigPostgresResourcesComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigPostgresResourcesComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigPostgresResourcesComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigPostgresResourcesComparisonExp>>;
|
||||
autoscaler?: InputMaybe<ConfigAutoscalerComparisonExp>;
|
||||
compute?: InputMaybe<ConfigResourcesComputeComparisonExp>;
|
||||
enablePublicAccess?: InputMaybe<ConfigBooleanComparisonExp>;
|
||||
networking?: InputMaybe<ConfigNetworkingComparisonExp>;
|
||||
replicas?: InputMaybe<ConfigUint8ComparisonExp>;
|
||||
storage?: InputMaybe<ConfigPostgresStorageComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigPostgresResourcesInsertInput = {
|
||||
autoscaler?: InputMaybe<ConfigAutoscalerInsertInput>;
|
||||
compute?: InputMaybe<ConfigResourcesComputeInsertInput>;
|
||||
enablePublicAccess?: InputMaybe<Scalars['Boolean']>;
|
||||
networking?: InputMaybe<ConfigNetworkingInsertInput>;
|
||||
replicas?: InputMaybe<Scalars['ConfigUint8']>;
|
||||
storage?: InputMaybe<ConfigPostgresStorageInsertInput>;
|
||||
};
|
||||
|
||||
export type ConfigPostgresResourcesUpdateInput = {
|
||||
autoscaler?: InputMaybe<ConfigAutoscalerUpdateInput>;
|
||||
compute?: InputMaybe<ConfigResourcesComputeUpdateInput>;
|
||||
enablePublicAccess?: InputMaybe<Scalars['Boolean']>;
|
||||
networking?: InputMaybe<ConfigNetworkingUpdateInput>;
|
||||
replicas?: InputMaybe<Scalars['ConfigUint8']>;
|
||||
storage?: InputMaybe<ConfigPostgresStorageUpdateInput>;
|
||||
@@ -1988,6 +2016,7 @@ export type ConfigProviderUpdateInput = {
|
||||
/** Resource configuration for a service */
|
||||
export type ConfigResources = {
|
||||
__typename?: 'ConfigResources';
|
||||
autoscaler?: Maybe<ConfigAutoscaler>;
|
||||
compute?: Maybe<ConfigResourcesCompute>;
|
||||
networking?: Maybe<ConfigNetworking>;
|
||||
/** Number of replicas for a service */
|
||||
@@ -1998,6 +2027,7 @@ export type ConfigResourcesComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigResourcesComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigResourcesComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigResourcesComparisonExp>>;
|
||||
autoscaler?: InputMaybe<ConfigAutoscalerComparisonExp>;
|
||||
compute?: InputMaybe<ConfigResourcesComputeComparisonExp>;
|
||||
networking?: InputMaybe<ConfigNetworkingComparisonExp>;
|
||||
replicas?: InputMaybe<ConfigUint8ComparisonExp>;
|
||||
@@ -2030,12 +2060,14 @@ export type ConfigResourcesComputeUpdateInput = {
|
||||
};
|
||||
|
||||
export type ConfigResourcesInsertInput = {
|
||||
autoscaler?: InputMaybe<ConfigAutoscalerInsertInput>;
|
||||
compute?: InputMaybe<ConfigResourcesComputeInsertInput>;
|
||||
networking?: InputMaybe<ConfigNetworkingInsertInput>;
|
||||
replicas?: InputMaybe<Scalars['ConfigUint8']>;
|
||||
};
|
||||
|
||||
export type ConfigResourcesUpdateInput = {
|
||||
autoscaler?: InputMaybe<ConfigAutoscalerUpdateInput>;
|
||||
compute?: InputMaybe<ConfigResourcesComputeUpdateInput>;
|
||||
networking?: InputMaybe<ConfigNetworkingUpdateInput>;
|
||||
replicas?: InputMaybe<Scalars['ConfigUint8']>;
|
||||
@@ -2153,6 +2185,7 @@ export type ConfigRunServicePortUpdateInput = {
|
||||
/** Resource configuration for a service */
|
||||
export type ConfigRunServiceResources = {
|
||||
__typename?: 'ConfigRunServiceResources';
|
||||
autoscaler?: Maybe<ConfigAutoscaler>;
|
||||
compute: ConfigComputeResources;
|
||||
/** Number of replicas for a service */
|
||||
replicas: Scalars['ConfigUint8'];
|
||||
@@ -2163,12 +2196,14 @@ export type ConfigRunServiceResourcesComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigRunServiceResourcesComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigRunServiceResourcesComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigRunServiceResourcesComparisonExp>>;
|
||||
autoscaler?: InputMaybe<ConfigAutoscalerComparisonExp>;
|
||||
compute?: InputMaybe<ConfigComputeResourcesComparisonExp>;
|
||||
replicas?: InputMaybe<ConfigUint8ComparisonExp>;
|
||||
storage?: InputMaybe<ConfigRunServiceResourcesStorageComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigRunServiceResourcesInsertInput = {
|
||||
autoscaler?: InputMaybe<ConfigAutoscalerInsertInput>;
|
||||
compute: ConfigComputeResourcesInsertInput;
|
||||
replicas: Scalars['ConfigUint8'];
|
||||
storage?: InputMaybe<Array<ConfigRunServiceResourcesStorageInsertInput>>;
|
||||
@@ -2205,6 +2240,7 @@ export type ConfigRunServiceResourcesStorageUpdateInput = {
|
||||
};
|
||||
|
||||
export type ConfigRunServiceResourcesUpdateInput = {
|
||||
autoscaler?: InputMaybe<ConfigAutoscalerUpdateInput>;
|
||||
compute?: InputMaybe<ConfigComputeResourcesUpdateInput>;
|
||||
replicas?: InputMaybe<Scalars['ConfigUint8']>;
|
||||
storage?: InputMaybe<Array<ConfigRunServiceResourcesStorageUpdateInput>>;
|
||||
@@ -2526,6 +2562,7 @@ export type ConfigSystemConfigPostgres = {
|
||||
database: Scalars['String'];
|
||||
disk?: Maybe<ConfigSystemConfigPostgresDisk>;
|
||||
enabled?: Maybe<Scalars['Boolean']>;
|
||||
majorVersion?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigSystemConfigPostgresComparisonExp = {
|
||||
@@ -2536,6 +2573,7 @@ export type ConfigSystemConfigPostgresComparisonExp = {
|
||||
database?: InputMaybe<ConfigStringComparisonExp>;
|
||||
disk?: InputMaybe<ConfigSystemConfigPostgresDiskComparisonExp>;
|
||||
enabled?: InputMaybe<ConfigBooleanComparisonExp>;
|
||||
majorVersion?: InputMaybe<ConfigStringComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigSystemConfigPostgresConnectionString = {
|
||||
@@ -2599,6 +2637,7 @@ export type ConfigSystemConfigPostgresInsertInput = {
|
||||
database: Scalars['String'];
|
||||
disk?: InputMaybe<ConfigSystemConfigPostgresDiskInsertInput>;
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
majorVersion?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigSystemConfigPostgresUpdateInput = {
|
||||
@@ -2606,6 +2645,7 @@ export type ConfigSystemConfigPostgresUpdateInput = {
|
||||
database?: InputMaybe<Scalars['String']>;
|
||||
disk?: InputMaybe<ConfigSystemConfigPostgresDiskUpdateInput>;
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
majorVersion?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigSystemConfigUpdateInput = {
|
||||
@@ -2649,6 +2689,12 @@ export type ConfigUserRoleComparisonExp = {
|
||||
_nin?: InputMaybe<Array<Scalars['ConfigUserRole']>>;
|
||||
};
|
||||
|
||||
export type ContainerError = {
|
||||
__typename?: 'ContainerError';
|
||||
lastError: LastError;
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Boolean expression to compare columns of type "Int". All fields are combined with logical 'AND'. */
|
||||
export type Int_Comparison_Exp = {
|
||||
_eq?: InputMaybe<Scalars['Int']>;
|
||||
@@ -2675,6 +2721,13 @@ export type InvoiceSummary = {
|
||||
items: Array<InvoiceItem>;
|
||||
};
|
||||
|
||||
export type LastError = {
|
||||
__typename?: 'LastError';
|
||||
exitCode: Scalars['Int'];
|
||||
message: Scalars['String'];
|
||||
reason: Scalars['String'];
|
||||
};
|
||||
|
||||
export type Log = {
|
||||
__typename?: 'Log';
|
||||
log: Scalars['String'];
|
||||
@@ -2687,12 +2740,31 @@ export type Metrics = {
|
||||
value: Scalars['float64'];
|
||||
};
|
||||
|
||||
export type StatsDailyLiveFreeApps = {
|
||||
__typename?: 'StatsDailyLiveFreeApps';
|
||||
avg: Scalars['Int'];
|
||||
max: Scalars['Int'];
|
||||
min: Scalars['Int'];
|
||||
raw: Array<Scalars['Int']>;
|
||||
export type ProjectStatusResponse = {
|
||||
__typename?: 'ProjectStatusResponse';
|
||||
services: Array<ServiceStatus>;
|
||||
};
|
||||
|
||||
export type ReplicaStatus = {
|
||||
__typename?: 'ReplicaStatus';
|
||||
date: Scalars['Timestamp'];
|
||||
errors: Array<ContainerError>;
|
||||
ready: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export enum ServiceState {
|
||||
Error = 'Error',
|
||||
None = 'None',
|
||||
Running = 'Running',
|
||||
UpdateError = 'UpdateError',
|
||||
Updating = 'Updating'
|
||||
}
|
||||
|
||||
export type ServiceStatus = {
|
||||
__typename?: 'ServiceStatus';
|
||||
name: Scalars['String'];
|
||||
replicas: Array<ReplicaStatus>;
|
||||
state: ServiceState;
|
||||
};
|
||||
|
||||
export type StatsLiveApps = {
|
||||
@@ -11826,6 +11898,7 @@ export type Mutation_Root = {
|
||||
billingUpdatePersistentVolume: Scalars['Boolean'];
|
||||
billingUpdateReports: Scalars['Boolean'];
|
||||
billingUploadReports: Scalars['Boolean'];
|
||||
changeDatabaseVersion: Scalars['Boolean'];
|
||||
/** delete single row from the table: "apps" */
|
||||
deleteApp?: Maybe<Apps>;
|
||||
/** delete single row from the table: "app_states" */
|
||||
@@ -12171,6 +12244,7 @@ export type Mutation_Root = {
|
||||
pauseAppsExceedUsage: Array<Scalars['String']>;
|
||||
pauseInactiveApps: Array<Scalars['String']>;
|
||||
replaceConfig: ConfigConfig;
|
||||
replaceConfigRawJSON: Scalars['String'];
|
||||
replaceRunServiceConfig: ConfigRunServiceConfig;
|
||||
resetPostgresPassword: Scalars['Boolean'];
|
||||
restoreApplicationDatabase: Scalars['Boolean'];
|
||||
@@ -12511,6 +12585,14 @@ export type Mutation_RootBillingUpdateReportsArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootChangeDatabaseVersionArgs = {
|
||||
appID: Scalars['uuid'];
|
||||
force?: InputMaybe<Scalars['Boolean']>;
|
||||
version: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootDeleteAppArgs = {
|
||||
id: Scalars['uuid'];
|
||||
@@ -13654,6 +13736,13 @@ export type Mutation_RootReplaceConfigArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootReplaceConfigRawJsonArgs = {
|
||||
appID: Scalars['uuid'];
|
||||
rawJSON: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootReplaceRunServiceConfigArgs = {
|
||||
appID: Scalars['uuid'];
|
||||
@@ -15160,6 +15249,7 @@ export type Plans = {
|
||||
deprecated: Scalars['Boolean'];
|
||||
featureAdvancedGraphql: Scalars['Boolean'];
|
||||
featureBackupEnabled: Scalars['Boolean'];
|
||||
featureBackupRetentionDays: Scalars['Int'];
|
||||
featureCustomDomainsEnabled: Scalars['Boolean'];
|
||||
featureCustomEmailTemplatesEnabled: Scalars['Boolean'];
|
||||
featureCustomResources: Scalars['Boolean'];
|
||||
@@ -15242,6 +15332,7 @@ export type Plans_Aggregate_FieldsCountArgs = {
|
||||
/** aggregate avg on columns */
|
||||
export type Plans_Avg_Fields = {
|
||||
__typename?: 'plans_avg_fields';
|
||||
featureBackupRetentionDays?: Maybe<Scalars['Float']>;
|
||||
/** Function execution timeout in seconds */
|
||||
featureFunctionExecutionTimeout?: Maybe<Scalars['Float']>;
|
||||
featureMaxDbSize?: Maybe<Scalars['Float']>;
|
||||
@@ -15263,6 +15354,7 @@ export type Plans_Bool_Exp = {
|
||||
deprecated?: InputMaybe<Boolean_Comparison_Exp>;
|
||||
featureAdvancedGraphql?: InputMaybe<Boolean_Comparison_Exp>;
|
||||
featureBackupEnabled?: InputMaybe<Boolean_Comparison_Exp>;
|
||||
featureBackupRetentionDays?: InputMaybe<Int_Comparison_Exp>;
|
||||
featureCustomDomainsEnabled?: InputMaybe<Boolean_Comparison_Exp>;
|
||||
featureCustomEmailTemplatesEnabled?: InputMaybe<Boolean_Comparison_Exp>;
|
||||
featureCustomResources?: InputMaybe<Boolean_Comparison_Exp>;
|
||||
@@ -15297,6 +15389,7 @@ export enum Plans_Constraint {
|
||||
|
||||
/** input type for incrementing numeric columns in table "plans" */
|
||||
export type Plans_Inc_Input = {
|
||||
featureBackupRetentionDays?: InputMaybe<Scalars['Int']>;
|
||||
/** Function execution timeout in seconds */
|
||||
featureFunctionExecutionTimeout?: InputMaybe<Scalars['Int']>;
|
||||
featureMaxDbSize?: InputMaybe<Scalars['Int']>;
|
||||
@@ -15314,6 +15407,7 @@ export type Plans_Insert_Input = {
|
||||
deprecated?: InputMaybe<Scalars['Boolean']>;
|
||||
featureAdvancedGraphql?: InputMaybe<Scalars['Boolean']>;
|
||||
featureBackupEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
featureBackupRetentionDays?: InputMaybe<Scalars['Int']>;
|
||||
featureCustomDomainsEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
featureCustomEmailTemplatesEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
featureCustomResources?: InputMaybe<Scalars['Boolean']>;
|
||||
@@ -15347,6 +15441,7 @@ export type Plans_Insert_Input = {
|
||||
export type Plans_Max_Fields = {
|
||||
__typename?: 'plans_max_fields';
|
||||
createdAt?: Maybe<Scalars['timestamptz']>;
|
||||
featureBackupRetentionDays?: Maybe<Scalars['Int']>;
|
||||
/** Function execution timeout in seconds */
|
||||
featureFunctionExecutionTimeout?: Maybe<Scalars['Int']>;
|
||||
featureMaxDbSize?: Maybe<Scalars['Int']>;
|
||||
@@ -15371,6 +15466,7 @@ export type Plans_Max_Fields = {
|
||||
export type Plans_Min_Fields = {
|
||||
__typename?: 'plans_min_fields';
|
||||
createdAt?: Maybe<Scalars['timestamptz']>;
|
||||
featureBackupRetentionDays?: Maybe<Scalars['Int']>;
|
||||
/** Function execution timeout in seconds */
|
||||
featureFunctionExecutionTimeout?: Maybe<Scalars['Int']>;
|
||||
featureMaxDbSize?: Maybe<Scalars['Int']>;
|
||||
@@ -15421,6 +15517,7 @@ export type Plans_Order_By = {
|
||||
deprecated?: InputMaybe<Order_By>;
|
||||
featureAdvancedGraphql?: InputMaybe<Order_By>;
|
||||
featureBackupEnabled?: InputMaybe<Order_By>;
|
||||
featureBackupRetentionDays?: InputMaybe<Order_By>;
|
||||
featureCustomDomainsEnabled?: InputMaybe<Order_By>;
|
||||
featureCustomEmailTemplatesEnabled?: InputMaybe<Order_By>;
|
||||
featureCustomResources?: InputMaybe<Order_By>;
|
||||
@@ -15463,6 +15560,8 @@ export enum Plans_Select_Column {
|
||||
/** column name */
|
||||
FeatureBackupEnabled = 'featureBackupEnabled',
|
||||
/** column name */
|
||||
FeatureBackupRetentionDays = 'featureBackupRetentionDays',
|
||||
/** column name */
|
||||
FeatureCustomDomainsEnabled = 'featureCustomDomainsEnabled',
|
||||
/** column name */
|
||||
FeatureCustomEmailTemplatesEnabled = 'featureCustomEmailTemplatesEnabled',
|
||||
@@ -15518,6 +15617,7 @@ export type Plans_Set_Input = {
|
||||
deprecated?: InputMaybe<Scalars['Boolean']>;
|
||||
featureAdvancedGraphql?: InputMaybe<Scalars['Boolean']>;
|
||||
featureBackupEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
featureBackupRetentionDays?: InputMaybe<Scalars['Int']>;
|
||||
featureCustomDomainsEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
featureCustomEmailTemplatesEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
featureCustomResources?: InputMaybe<Scalars['Boolean']>;
|
||||
@@ -15550,6 +15650,7 @@ export type Plans_Set_Input = {
|
||||
/** aggregate stddev on columns */
|
||||
export type Plans_Stddev_Fields = {
|
||||
__typename?: 'plans_stddev_fields';
|
||||
featureBackupRetentionDays?: Maybe<Scalars['Float']>;
|
||||
/** Function execution timeout in seconds */
|
||||
featureFunctionExecutionTimeout?: Maybe<Scalars['Float']>;
|
||||
featureMaxDbSize?: Maybe<Scalars['Float']>;
|
||||
@@ -15563,6 +15664,7 @@ export type Plans_Stddev_Fields = {
|
||||
/** aggregate stddev_pop on columns */
|
||||
export type Plans_Stddev_Pop_Fields = {
|
||||
__typename?: 'plans_stddev_pop_fields';
|
||||
featureBackupRetentionDays?: Maybe<Scalars['Float']>;
|
||||
/** Function execution timeout in seconds */
|
||||
featureFunctionExecutionTimeout?: Maybe<Scalars['Float']>;
|
||||
featureMaxDbSize?: Maybe<Scalars['Float']>;
|
||||
@@ -15576,6 +15678,7 @@ export type Plans_Stddev_Pop_Fields = {
|
||||
/** aggregate stddev_samp on columns */
|
||||
export type Plans_Stddev_Samp_Fields = {
|
||||
__typename?: 'plans_stddev_samp_fields';
|
||||
featureBackupRetentionDays?: Maybe<Scalars['Float']>;
|
||||
/** Function execution timeout in seconds */
|
||||
featureFunctionExecutionTimeout?: Maybe<Scalars['Float']>;
|
||||
featureMaxDbSize?: Maybe<Scalars['Float']>;
|
||||
@@ -15600,6 +15703,7 @@ export type Plans_Stream_Cursor_Value_Input = {
|
||||
deprecated?: InputMaybe<Scalars['Boolean']>;
|
||||
featureAdvancedGraphql?: InputMaybe<Scalars['Boolean']>;
|
||||
featureBackupEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
featureBackupRetentionDays?: InputMaybe<Scalars['Int']>;
|
||||
featureCustomDomainsEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
featureCustomEmailTemplatesEnabled?: InputMaybe<Scalars['Boolean']>;
|
||||
featureCustomResources?: InputMaybe<Scalars['Boolean']>;
|
||||
@@ -15632,6 +15736,7 @@ export type Plans_Stream_Cursor_Value_Input = {
|
||||
/** aggregate sum on columns */
|
||||
export type Plans_Sum_Fields = {
|
||||
__typename?: 'plans_sum_fields';
|
||||
featureBackupRetentionDays?: Maybe<Scalars['Int']>;
|
||||
/** Function execution timeout in seconds */
|
||||
featureFunctionExecutionTimeout?: Maybe<Scalars['Int']>;
|
||||
featureMaxDbSize?: Maybe<Scalars['Int']>;
|
||||
@@ -15653,6 +15758,8 @@ export enum Plans_Update_Column {
|
||||
/** column name */
|
||||
FeatureBackupEnabled = 'featureBackupEnabled',
|
||||
/** column name */
|
||||
FeatureBackupRetentionDays = 'featureBackupRetentionDays',
|
||||
/** column name */
|
||||
FeatureCustomDomainsEnabled = 'featureCustomDomainsEnabled',
|
||||
/** column name */
|
||||
FeatureCustomEmailTemplatesEnabled = 'featureCustomEmailTemplatesEnabled',
|
||||
@@ -15714,6 +15821,7 @@ export type Plans_Updates = {
|
||||
/** aggregate var_pop on columns */
|
||||
export type Plans_Var_Pop_Fields = {
|
||||
__typename?: 'plans_var_pop_fields';
|
||||
featureBackupRetentionDays?: Maybe<Scalars['Float']>;
|
||||
/** Function execution timeout in seconds */
|
||||
featureFunctionExecutionTimeout?: Maybe<Scalars['Float']>;
|
||||
featureMaxDbSize?: Maybe<Scalars['Float']>;
|
||||
@@ -15727,6 +15835,7 @@ export type Plans_Var_Pop_Fields = {
|
||||
/** aggregate var_samp on columns */
|
||||
export type Plans_Var_Samp_Fields = {
|
||||
__typename?: 'plans_var_samp_fields';
|
||||
featureBackupRetentionDays?: Maybe<Scalars['Float']>;
|
||||
/** Function execution timeout in seconds */
|
||||
featureFunctionExecutionTimeout?: Maybe<Scalars['Float']>;
|
||||
featureMaxDbSize?: Maybe<Scalars['Float']>;
|
||||
@@ -15740,6 +15849,7 @@ export type Plans_Var_Samp_Fields = {
|
||||
/** aggregate variance on columns */
|
||||
export type Plans_Variance_Fields = {
|
||||
__typename?: 'plans_variance_fields';
|
||||
featureBackupRetentionDays?: Maybe<Scalars['Float']>;
|
||||
/** Function execution timeout in seconds */
|
||||
featureFunctionExecutionTimeout?: Maybe<Scalars['Float']>;
|
||||
featureMaxDbSize?: Maybe<Scalars['Float']>;
|
||||
@@ -15939,6 +16049,7 @@ export type Query_Root = {
|
||||
getLogsVolume: Metrics;
|
||||
getPostgresVolumeCapacity: Metrics;
|
||||
getPostgresVolumeUsage: Metrics;
|
||||
getProjectStatus: ProjectStatusResponse;
|
||||
/**
|
||||
* Returns list of label values for a given label within a range of time.
|
||||
*
|
||||
@@ -16016,12 +16127,6 @@ export type Query_Root = {
|
||||
softwareVersions: Array<Software_Versions>;
|
||||
/** fetch aggregated fields from the table: "software_versions" */
|
||||
softwareVersionsAggregate: Software_Versions_Aggregate;
|
||||
/**
|
||||
* Returns the per-day number of free live apps in the given time range, as well as the min, max and avg.
|
||||
*
|
||||
* Requests that returned a 4xx or 5xx status code are not counted as live traffic.
|
||||
*/
|
||||
statsDailyLiveFreeApps: StatsDailyLiveFreeApps;
|
||||
/**
|
||||
* Returns lists of apps that have some live traffic in the give time range.
|
||||
* From defaults to 24 hours ago and to defaults to now.
|
||||
@@ -16031,6 +16136,8 @@ export type Query_Root = {
|
||||
statsLiveApps: StatsLiveApps;
|
||||
systemConfig?: Maybe<ConfigSystemConfig>;
|
||||
systemConfigs: Array<ConfigAppSystemConfig>;
|
||||
/** Returns system logs for a given application */
|
||||
systemLogs: Array<Log>;
|
||||
/** fetch data from the table: "auth.users" using primary key columns */
|
||||
user?: Maybe<Users>;
|
||||
/** fetch data from the table: "auth.users" */
|
||||
@@ -16796,6 +16903,11 @@ export type Query_RootGetPostgresVolumeUsageArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootGetProjectStatusArgs = {
|
||||
appID: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootGetServiceLabelValuesArgs = {
|
||||
appID: Scalars['String'];
|
||||
};
|
||||
@@ -17079,12 +17191,6 @@ export type Query_RootSoftwareVersionsAggregateArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootStatsDailyLiveFreeAppsArgs = {
|
||||
from?: InputMaybe<Scalars['Timestamp']>;
|
||||
to?: InputMaybe<Scalars['Timestamp']>;
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootStatsLiveAppsArgs = {
|
||||
from?: InputMaybe<Scalars['Timestamp']>;
|
||||
to?: InputMaybe<Scalars['Timestamp']>;
|
||||
@@ -17101,6 +17207,14 @@ export type Query_RootSystemConfigsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootSystemLogsArgs = {
|
||||
action: Scalars['String'];
|
||||
appID: Scalars['String'];
|
||||
from?: InputMaybe<Scalars['Timestamp']>;
|
||||
to?: InputMaybe<Scalars['Timestamp']>;
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootUserArgs = {
|
||||
id: Scalars['uuid'];
|
||||
};
|
||||
@@ -17420,6 +17534,7 @@ export type Regions = {
|
||||
domain: Scalars['String'];
|
||||
id: Scalars['uuid'];
|
||||
isGdprCompliant: Scalars['Boolean'];
|
||||
name: Scalars['String'];
|
||||
/** An object relationship */
|
||||
region_type: Region_Type;
|
||||
/** An array relationship */
|
||||
@@ -17797,6 +17912,7 @@ export type Regions_Bool_Exp = {
|
||||
domain?: InputMaybe<String_Comparison_Exp>;
|
||||
id?: InputMaybe<Uuid_Comparison_Exp>;
|
||||
isGdprCompliant?: InputMaybe<Boolean_Comparison_Exp>;
|
||||
name?: InputMaybe<String_Comparison_Exp>;
|
||||
region_type?: InputMaybe<Region_Type_Bool_Exp>;
|
||||
regions_allowed_workspaces?: InputMaybe<Regions_Allowed_Workspace_Bool_Exp>;
|
||||
regions_allowed_workspaces_aggregate?: InputMaybe<Regions_Allowed_Workspace_Aggregate_Bool_Exp>;
|
||||
@@ -17824,6 +17940,7 @@ export type Regions_Insert_Input = {
|
||||
domain?: InputMaybe<Scalars['String']>;
|
||||
id?: InputMaybe<Scalars['uuid']>;
|
||||
isGdprCompliant?: InputMaybe<Scalars['Boolean']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
region_type?: InputMaybe<Region_Type_Obj_Rel_Insert_Input>;
|
||||
regions_allowed_workspaces?: InputMaybe<Regions_Allowed_Workspace_Arr_Rel_Insert_Input>;
|
||||
type?: InputMaybe<Region_Type_Enum>;
|
||||
@@ -17840,6 +17957,7 @@ export type Regions_Max_Fields = {
|
||||
description?: Maybe<Scalars['String']>;
|
||||
domain?: Maybe<Scalars['String']>;
|
||||
id?: Maybe<Scalars['uuid']>;
|
||||
name?: Maybe<Scalars['String']>;
|
||||
updatedAt?: Maybe<Scalars['timestamptz']>;
|
||||
};
|
||||
|
||||
@@ -17852,6 +17970,7 @@ export type Regions_Max_Order_By = {
|
||||
description?: InputMaybe<Order_By>;
|
||||
domain?: InputMaybe<Order_By>;
|
||||
id?: InputMaybe<Order_By>;
|
||||
name?: InputMaybe<Order_By>;
|
||||
updatedAt?: InputMaybe<Order_By>;
|
||||
};
|
||||
|
||||
@@ -17865,6 +17984,7 @@ export type Regions_Min_Fields = {
|
||||
description?: Maybe<Scalars['String']>;
|
||||
domain?: Maybe<Scalars['String']>;
|
||||
id?: Maybe<Scalars['uuid']>;
|
||||
name?: Maybe<Scalars['String']>;
|
||||
updatedAt?: Maybe<Scalars['timestamptz']>;
|
||||
};
|
||||
|
||||
@@ -17877,6 +17997,7 @@ export type Regions_Min_Order_By = {
|
||||
description?: InputMaybe<Order_By>;
|
||||
domain?: InputMaybe<Order_By>;
|
||||
id?: InputMaybe<Order_By>;
|
||||
name?: InputMaybe<Order_By>;
|
||||
updatedAt?: InputMaybe<Order_By>;
|
||||
};
|
||||
|
||||
@@ -17917,6 +18038,7 @@ export type Regions_Order_By = {
|
||||
domain?: InputMaybe<Order_By>;
|
||||
id?: InputMaybe<Order_By>;
|
||||
isGdprCompliant?: InputMaybe<Order_By>;
|
||||
name?: InputMaybe<Order_By>;
|
||||
region_type?: InputMaybe<Region_Type_Order_By>;
|
||||
regions_allowed_workspaces_aggregate?: InputMaybe<Regions_Allowed_Workspace_Aggregate_Order_By>;
|
||||
type?: InputMaybe<Order_By>;
|
||||
@@ -17949,6 +18071,8 @@ export enum Regions_Select_Column {
|
||||
/** column name */
|
||||
IsGdprCompliant = 'isGdprCompliant',
|
||||
/** column name */
|
||||
Name = 'name',
|
||||
/** column name */
|
||||
Type = 'type',
|
||||
/** column name */
|
||||
UpdatedAt = 'updatedAt'
|
||||
@@ -17981,6 +18105,7 @@ export type Regions_Set_Input = {
|
||||
domain?: InputMaybe<Scalars['String']>;
|
||||
id?: InputMaybe<Scalars['uuid']>;
|
||||
isGdprCompliant?: InputMaybe<Scalars['Boolean']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
type?: InputMaybe<Region_Type_Enum>;
|
||||
updatedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
};
|
||||
@@ -18004,6 +18129,7 @@ export type Regions_Stream_Cursor_Value_Input = {
|
||||
domain?: InputMaybe<Scalars['String']>;
|
||||
id?: InputMaybe<Scalars['uuid']>;
|
||||
isGdprCompliant?: InputMaybe<Scalars['Boolean']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
type?: InputMaybe<Region_Type_Enum>;
|
||||
updatedAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
};
|
||||
@@ -18029,6 +18155,8 @@ export enum Regions_Update_Column {
|
||||
/** column name */
|
||||
IsGdprCompliant = 'isGdprCompliant',
|
||||
/** column name */
|
||||
Name = 'name',
|
||||
/** column name */
|
||||
Type = 'type',
|
||||
/** column name */
|
||||
UpdatedAt = 'updatedAt'
|
||||
@@ -22690,7 +22818,7 @@ export type GetPostgresSettingsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetPostgresSettingsQuery = { __typename?: 'query_root', systemConfig?: { __typename?: 'ConfigSystemConfig', postgres: { __typename?: 'ConfigSystemConfigPostgres', database: string } } | null, config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', postgres?: { __typename?: 'ConfigPostgres', version?: string | null, resources?: { __typename?: 'ConfigPostgresResources', storage?: { __typename?: 'ConfigPostgresStorage', capacity: any } | null } | null } | null } | null };
|
||||
export type GetPostgresSettingsQuery = { __typename?: 'query_root', systemConfig?: { __typename?: 'ConfigSystemConfig', postgres: { __typename?: 'ConfigSystemConfigPostgres', database: string } } | null, config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', postgres?: { __typename?: 'ConfigPostgres', version?: string | null, resources?: { __typename?: 'ConfigPostgresResources', enablePublicAccess?: boolean | null, storage?: { __typename?: 'ConfigPostgresStorage', capacity: any } | null } | null } | null } | null };
|
||||
|
||||
export type ResetDatabasePasswordMutationVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
@@ -22758,7 +22886,7 @@ export type DeleteApplicationMutation = { __typename?: 'mutation_root', deleteAp
|
||||
export type GetAllWorkspacesAndProjectsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetAllWorkspacesAndProjectsQuery = { __typename?: 'query_root', workspaces: Array<{ __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> }> };
|
||||
export type GetAllWorkspacesAndProjectsQuery = { __typename?: 'query_root', workspaces: Array<{ __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, name: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> }> };
|
||||
|
||||
export type GetAppPlanAndGlobalPlansAppFragment = { __typename?: 'apps', id: any, subdomain: string, workspace: { __typename?: 'workspaces', id: any, paymentMethods: Array<{ __typename?: 'paymentMethods', id: any }> }, plan: { __typename?: 'plans', id: any, name: string } };
|
||||
|
||||
@@ -22787,6 +22915,13 @@ export type GetApplicationStateQueryVariables = Exact<{
|
||||
|
||||
export type GetApplicationStateQuery = { __typename?: 'query_root', app?: { __typename?: 'apps', id: any, name: string, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }> } | null };
|
||||
|
||||
export type GetConfiguredVersionsQueryVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
}>;
|
||||
|
||||
|
||||
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 GetProjectLocalesQueryVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
}>;
|
||||
@@ -22804,6 +22939,13 @@ export type GetProjectMetricsQueryVariables = Exact<{
|
||||
|
||||
export type GetProjectMetricsQuery = { __typename?: 'query_root', logsVolume: { __typename?: 'Metrics', value: any }, cpuSecondsUsage: { __typename?: 'Metrics', value: any }, functionInvocations: { __typename?: 'Metrics', value: any }, functionsDuration: { __typename?: 'Metrics', value: any }, postgresVolumeCapacity: { __typename?: 'Metrics', value: any }, postgresVolumeUsage: { __typename?: 'Metrics', value: any }, totalRequests: { __typename?: 'Metrics', value: any }, egressVolume: { __typename?: 'Metrics', value: any } };
|
||||
|
||||
export type GetProjectServicesHealthQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetProjectServicesHealthQuery = { __typename?: 'query_root', getProjectStatus: { __typename?: 'ProjectStatusResponse', services: Array<{ __typename?: 'ServiceStatus', name: string, state: ServiceState, replicas: Array<{ __typename?: 'ReplicaStatus', ready: boolean, date: any, errors: Array<{ __typename?: 'ContainerError', name: string, lastError: { __typename?: 'LastError', reason: string, exitCode: number, message: string } }> }> }> } };
|
||||
|
||||
export type GetRemoteAppRolesQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
@@ -22815,7 +22957,7 @@ export type GetWorkspaceAndProjectQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetWorkspaceAndProjectQuery = { __typename?: 'query_root', workspaces: Array<{ __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> }> };
|
||||
export type GetWorkspaceAndProjectQuery = { __typename?: 'query_root', workspaces: Array<{ __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, name: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> }> };
|
||||
|
||||
export type InsertApplicationMutationVariables = Exact<{
|
||||
app: Apps_Insert_Input;
|
||||
@@ -22860,6 +23002,21 @@ export type GetEnvironmentVariablesQueryVariables = Exact<{
|
||||
|
||||
export type GetEnvironmentVariablesQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', global?: { __typename?: 'ConfigGlobal', environment?: Array<{ __typename?: 'ConfigGlobalEnvironmentVariable', name: string, value: string, id: string }> | null } | null, hasura: { __typename?: 'ConfigHasura', adminSecret: string, webhookSecret: string, jwtSecrets?: Array<{ __typename?: 'ConfigJWTSecret', issuer?: string | null, key?: string | null, type?: string | null, jwk_url?: any | null, header?: string | null, claims_namespace_path?: string | null, claims_namespace?: string | null, claims_format?: string | null, audience?: string | null, allowed_skew?: any | null }> | null } } | null };
|
||||
|
||||
export type GetConfigRawJsonQueryVariables = Exact<{
|
||||
appID: Scalars['uuid'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetConfigRawJsonQuery = { __typename?: 'query_root', configRawJSON: string };
|
||||
|
||||
export type ReplaceConfigRawJsonMutationVariables = Exact<{
|
||||
appID: Scalars['uuid'];
|
||||
rawJSON: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ReplaceConfigRawJsonMutation = { __typename?: 'mutation_root', replaceConfigRawJSON: string };
|
||||
|
||||
export type PermissionVariableFragment = { __typename?: 'ConfigAuthsessionaccessTokenCustomClaims', key: string, value: string, id: string };
|
||||
|
||||
export type GetRolesPermissionsQueryVariables = Exact<{
|
||||
@@ -22922,7 +23079,7 @@ export type UpdateConfigMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateConfigMutation = { __typename?: 'mutation_root', updateConfig: { __typename?: 'ConfigConfig', id: 'ConfigConfig', postgres?: { __typename?: 'ConfigPostgres', resources?: { __typename?: 'ConfigPostgresResources', storage?: { __typename?: 'ConfigPostgresStorage', capacity: any } | null } | null } | null, ai?: { __typename?: 'ConfigAI', version?: string | null, webhookSecret: string, autoEmbeddings?: { __typename?: 'ConfigAIAutoEmbeddings', synchPeriodMinutes?: any | null } | null, openai: { __typename?: 'ConfigAIOpenai', organization?: string | null, apiKey: string }, resources: { __typename?: 'ConfigAIResources', compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any } } } | null } };
|
||||
export type UpdateConfigMutation = { __typename?: 'mutation_root', updateConfig: { __typename?: 'ConfigConfig', id: 'ConfigConfig', postgres?: { __typename?: 'ConfigPostgres', resources?: { __typename?: 'ConfigPostgresResources', enablePublicAccess?: boolean | null, storage?: { __typename?: 'ConfigPostgresStorage', capacity: any } | null } | null } | null, ai?: { __typename?: 'ConfigAI', version?: string | null, webhookSecret: string, autoEmbeddings?: { __typename?: 'ConfigAIAutoEmbeddings', synchPeriodMinutes?: any | null } | null, openai: { __typename?: 'ConfigAIOpenai', organization?: string | null, apiKey: string }, resources: { __typename?: 'ConfigAIResources', compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any } } } | null } };
|
||||
|
||||
export type UnpauseApplicationMutationVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
@@ -23007,9 +23164,9 @@ export type GetFilesAggregateQuery = { __typename?: 'query_root', filesAggregate
|
||||
|
||||
export type AppStateHistoryFragment = { __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any };
|
||||
|
||||
export type ProjectFragment = { __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null };
|
||||
export type ProjectFragment = { __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, name: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null };
|
||||
|
||||
export type WorkspaceFragment = { __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> };
|
||||
export type WorkspaceFragment = { __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, name: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> };
|
||||
|
||||
export type GithubRepositoryFragment = { __typename?: 'githubRepositories', id: any, name: string, fullName: string, private: boolean, githubAppInstallation: { __typename?: 'githubAppInstallations', id: any, accountLogin?: string | null, accountType?: string | null, accountAvatarUrl?: string | null } };
|
||||
|
||||
@@ -23092,6 +23249,11 @@ export type GetPlansQueryVariables = Exact<{
|
||||
|
||||
export type GetPlansQuery = { __typename?: 'query_root', plans: Array<{ __typename?: 'plans', id: any, name: string, isFree: boolean, price: number }> };
|
||||
|
||||
export type GetRecommendedSoftwareVersionsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetRecommendedSoftwareVersionsQuery = { __typename?: 'query_root', softwareVersions: Array<{ __typename?: 'software_versions', software: Software_Type_Enum, version: string }> };
|
||||
|
||||
export type GetSoftwareVersionsQueryVariables = Exact<{
|
||||
software: Software_Type_Enum;
|
||||
}>;
|
||||
@@ -23552,7 +23714,7 @@ export const ProjectFragmentDoc = gql`
|
||||
region {
|
||||
id
|
||||
countryCode
|
||||
awsName
|
||||
name
|
||||
domain
|
||||
city
|
||||
}
|
||||
@@ -24065,6 +24227,7 @@ export const GetPostgresSettingsDocument = gql`
|
||||
storage {
|
||||
capacity
|
||||
}
|
||||
enablePublicAccess
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24611,6 +24774,58 @@ export type GetApplicationStateQueryResult = Apollo.QueryResult<GetApplicationSt
|
||||
export function refetchGetApplicationStateQuery(variables: GetApplicationStateQueryVariables) {
|
||||
return { query: GetApplicationStateDocument, variables: variables }
|
||||
}
|
||||
export const GetConfiguredVersionsDocument = gql`
|
||||
query getConfiguredVersions($appId: uuid!) {
|
||||
config(appID: $appId, resolve: true) {
|
||||
auth {
|
||||
version
|
||||
}
|
||||
postgres {
|
||||
version
|
||||
}
|
||||
hasura {
|
||||
version
|
||||
}
|
||||
ai {
|
||||
version
|
||||
}
|
||||
storage {
|
||||
version
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetConfiguredVersionsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetConfiguredVersionsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetConfiguredVersionsQuery` 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 } = useGetConfiguredVersionsQuery({
|
||||
* variables: {
|
||||
* appId: // value for 'appId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetConfiguredVersionsQuery(baseOptions: Apollo.QueryHookOptions<GetConfiguredVersionsQuery, GetConfiguredVersionsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetConfiguredVersionsQuery, GetConfiguredVersionsQueryVariables>(GetConfiguredVersionsDocument, options);
|
||||
}
|
||||
export function useGetConfiguredVersionsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetConfiguredVersionsQuery, GetConfiguredVersionsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetConfiguredVersionsQuery, GetConfiguredVersionsQueryVariables>(GetConfiguredVersionsDocument, options);
|
||||
}
|
||||
export type GetConfiguredVersionsQueryHookResult = ReturnType<typeof useGetConfiguredVersionsQuery>;
|
||||
export type GetConfiguredVersionsLazyQueryHookResult = ReturnType<typeof useGetConfiguredVersionsLazyQuery>;
|
||||
export type GetConfiguredVersionsQueryResult = Apollo.QueryResult<GetConfiguredVersionsQuery, GetConfiguredVersionsQueryVariables>;
|
||||
export function refetchGetConfiguredVersionsQuery(variables: GetConfiguredVersionsQueryVariables) {
|
||||
return { query: GetConfiguredVersionsDocument, variables: variables }
|
||||
}
|
||||
export const GetProjectLocalesDocument = gql`
|
||||
query getProjectLocales($appId: uuid!) {
|
||||
config(appID: $appId, resolve: false) {
|
||||
@@ -24727,6 +24942,59 @@ export type GetProjectMetricsQueryResult = Apollo.QueryResult<GetProjectMetricsQ
|
||||
export function refetchGetProjectMetricsQuery(variables: GetProjectMetricsQueryVariables) {
|
||||
return { query: GetProjectMetricsDocument, variables: variables }
|
||||
}
|
||||
export const GetProjectServicesHealthDocument = gql`
|
||||
query getProjectServicesHealth($appId: String!) {
|
||||
getProjectStatus(appID: $appId) {
|
||||
services {
|
||||
name
|
||||
state
|
||||
replicas {
|
||||
ready
|
||||
date
|
||||
errors {
|
||||
name
|
||||
lastError {
|
||||
reason
|
||||
exitCode
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetProjectServicesHealthQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetProjectServicesHealthQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetProjectServicesHealthQuery` 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 } = useGetProjectServicesHealthQuery({
|
||||
* variables: {
|
||||
* appId: // value for 'appId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetProjectServicesHealthQuery(baseOptions: Apollo.QueryHookOptions<GetProjectServicesHealthQuery, GetProjectServicesHealthQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetProjectServicesHealthQuery, GetProjectServicesHealthQueryVariables>(GetProjectServicesHealthDocument, options);
|
||||
}
|
||||
export function useGetProjectServicesHealthLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetProjectServicesHealthQuery, GetProjectServicesHealthQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetProjectServicesHealthQuery, GetProjectServicesHealthQueryVariables>(GetProjectServicesHealthDocument, options);
|
||||
}
|
||||
export type GetProjectServicesHealthQueryHookResult = ReturnType<typeof useGetProjectServicesHealthQuery>;
|
||||
export type GetProjectServicesHealthLazyQueryHookResult = ReturnType<typeof useGetProjectServicesHealthLazyQuery>;
|
||||
export type GetProjectServicesHealthQueryResult = Apollo.QueryResult<GetProjectServicesHealthQuery, GetProjectServicesHealthQueryVariables>;
|
||||
export function refetchGetProjectServicesHealthQuery(variables: GetProjectServicesHealthQueryVariables) {
|
||||
return { query: GetProjectServicesHealthDocument, variables: variables }
|
||||
}
|
||||
export const GetRemoteAppRolesDocument = gql`
|
||||
query getRemoteAppRoles {
|
||||
authRoles {
|
||||
@@ -25009,6 +25277,74 @@ export type GetEnvironmentVariablesQueryResult = Apollo.QueryResult<GetEnvironme
|
||||
export function refetchGetEnvironmentVariablesQuery(variables: GetEnvironmentVariablesQueryVariables) {
|
||||
return { query: GetEnvironmentVariablesDocument, variables: variables }
|
||||
}
|
||||
export const GetConfigRawJsonDocument = gql`
|
||||
query getConfigRawJSON($appID: uuid!) {
|
||||
configRawJSON(appID: $appID, resolve: false)
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetConfigRawJsonQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetConfigRawJsonQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetConfigRawJsonQuery` 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 } = useGetConfigRawJsonQuery({
|
||||
* variables: {
|
||||
* appID: // value for 'appID'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetConfigRawJsonQuery(baseOptions: Apollo.QueryHookOptions<GetConfigRawJsonQuery, GetConfigRawJsonQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetConfigRawJsonQuery, GetConfigRawJsonQueryVariables>(GetConfigRawJsonDocument, options);
|
||||
}
|
||||
export function useGetConfigRawJsonLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetConfigRawJsonQuery, GetConfigRawJsonQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetConfigRawJsonQuery, GetConfigRawJsonQueryVariables>(GetConfigRawJsonDocument, options);
|
||||
}
|
||||
export type GetConfigRawJsonQueryHookResult = ReturnType<typeof useGetConfigRawJsonQuery>;
|
||||
export type GetConfigRawJsonLazyQueryHookResult = ReturnType<typeof useGetConfigRawJsonLazyQuery>;
|
||||
export type GetConfigRawJsonQueryResult = Apollo.QueryResult<GetConfigRawJsonQuery, GetConfigRawJsonQueryVariables>;
|
||||
export function refetchGetConfigRawJsonQuery(variables: GetConfigRawJsonQueryVariables) {
|
||||
return { query: GetConfigRawJsonDocument, variables: variables }
|
||||
}
|
||||
export const ReplaceConfigRawJsonDocument = gql`
|
||||
mutation ReplaceConfigRawJSON($appID: uuid!, $rawJSON: String!) {
|
||||
replaceConfigRawJSON(appID: $appID, rawJSON: $rawJSON)
|
||||
}
|
||||
`;
|
||||
export type ReplaceConfigRawJsonMutationFn = Apollo.MutationFunction<ReplaceConfigRawJsonMutation, ReplaceConfigRawJsonMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useReplaceConfigRawJsonMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useReplaceConfigRawJsonMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useReplaceConfigRawJsonMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [replaceConfigRawJsonMutation, { data, loading, error }] = useReplaceConfigRawJsonMutation({
|
||||
* variables: {
|
||||
* appID: // value for 'appID'
|
||||
* rawJSON: // value for 'rawJSON'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useReplaceConfigRawJsonMutation(baseOptions?: Apollo.MutationHookOptions<ReplaceConfigRawJsonMutation, ReplaceConfigRawJsonMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<ReplaceConfigRawJsonMutation, ReplaceConfigRawJsonMutationVariables>(ReplaceConfigRawJsonDocument, options);
|
||||
}
|
||||
export type ReplaceConfigRawJsonMutationHookResult = ReturnType<typeof useReplaceConfigRawJsonMutation>;
|
||||
export type ReplaceConfigRawJsonMutationResult = Apollo.MutationResult<ReplaceConfigRawJsonMutation>;
|
||||
export type ReplaceConfigRawJsonMutationOptions = Apollo.BaseMutationOptions<ReplaceConfigRawJsonMutation, ReplaceConfigRawJsonMutationVariables>;
|
||||
export const GetRolesPermissionsDocument = gql`
|
||||
query GetRolesPermissions($appId: uuid!) {
|
||||
config(appID: $appId, resolve: false) {
|
||||
@@ -25428,6 +25764,7 @@ export const UpdateConfigDocument = gql`
|
||||
storage {
|
||||
capacity
|
||||
}
|
||||
enablePublicAccess
|
||||
}
|
||||
}
|
||||
ai {
|
||||
@@ -26307,6 +26644,44 @@ export type GetPlansQueryResult = Apollo.QueryResult<GetPlansQuery, GetPlansQuer
|
||||
export function refetchGetPlansQuery(variables?: GetPlansQueryVariables) {
|
||||
return { query: GetPlansDocument, variables: variables }
|
||||
}
|
||||
export const GetRecommendedSoftwareVersionsDocument = gql`
|
||||
query getRecommendedSoftwareVersions {
|
||||
softwareVersions {
|
||||
software
|
||||
version
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetRecommendedSoftwareVersionsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetRecommendedSoftwareVersionsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetRecommendedSoftwareVersionsQuery` 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 } = useGetRecommendedSoftwareVersionsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetRecommendedSoftwareVersionsQuery(baseOptions?: Apollo.QueryHookOptions<GetRecommendedSoftwareVersionsQuery, GetRecommendedSoftwareVersionsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetRecommendedSoftwareVersionsQuery, GetRecommendedSoftwareVersionsQueryVariables>(GetRecommendedSoftwareVersionsDocument, options);
|
||||
}
|
||||
export function useGetRecommendedSoftwareVersionsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetRecommendedSoftwareVersionsQuery, GetRecommendedSoftwareVersionsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetRecommendedSoftwareVersionsQuery, GetRecommendedSoftwareVersionsQueryVariables>(GetRecommendedSoftwareVersionsDocument, options);
|
||||
}
|
||||
export type GetRecommendedSoftwareVersionsQueryHookResult = ReturnType<typeof useGetRecommendedSoftwareVersionsQuery>;
|
||||
export type GetRecommendedSoftwareVersionsLazyQueryHookResult = ReturnType<typeof useGetRecommendedSoftwareVersionsLazyQuery>;
|
||||
export type GetRecommendedSoftwareVersionsQueryResult = Apollo.QueryResult<GetRecommendedSoftwareVersionsQuery, GetRecommendedSoftwareVersionsQueryVariables>;
|
||||
export function refetchGetRecommendedSoftwareVersionsQuery(variables?: GetRecommendedSoftwareVersionsQueryVariables) {
|
||||
return { query: GetRecommendedSoftwareVersionsDocument, variables: variables }
|
||||
}
|
||||
export const GetSoftwareVersionsDocument = gql`
|
||||
query getSoftwareVersions($software: software_type_enum!) {
|
||||
softwareVersions(where: {software: {_eq: $software}}, order_by: {version: desc}) {
|
||||
|
||||
@@ -43,13 +43,13 @@ export default async function execPromiseWithErrorToast(
|
||||
|
||||
onError?.(error);
|
||||
|
||||
toast.custom(
|
||||
const errorToastId = toast.custom(
|
||||
(t) => (
|
||||
<ErrorToast
|
||||
isVisible={t.visible}
|
||||
errorMessage={errorMessage}
|
||||
error={error}
|
||||
close={() => toast.dismiss()}
|
||||
close={() => toast.dismiss(errorToastId)}
|
||||
/>
|
||||
),
|
||||
{
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
# @nhost/docs
|
||||
|
||||
## 2.14.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 86f3f8d: chore: fix broken link in react-native reference
|
||||
|
||||
## 2.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cf6b712: chore: add pnpm to list of supported packagers for functions
|
||||
|
||||
## 2.14.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 79ce7ca: feat: add react-native quickstart guide
|
||||
- bedbb82: feat: functions: added runtime/pkg manager information
|
||||
|
||||
## 2.13.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 6fb0cc2: fix: minor improvements to compute resources' docs
|
||||
- 66bd450: chore: various improvements
|
||||
|
||||
## 2.12.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- d5077c7: feat: added docs about how to connect to postgres
|
||||
|
||||
## 2.11.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- c6dc7f4: chore: docs: add Nhost client reference
|
||||
|
||||
## 2.10.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
176
docs/guides/auth/custom-jwts.mdx
Normal file
176
docs/guides/auth/custom-jwts.mdx
Normal file
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: Custom JWTs
|
||||
description: Creating custom JWTs
|
||||
icon: ghost
|
||||
---
|
||||
|
||||
In some cases it is necessary to act on behalf of a user. While the Auth service doesn't allow that it is not difficult to implement such functionality as a serverless function. Below you can find an example of a function that can generate a valid access token for your application with customized values. For details read the docstring in the function itself.
|
||||
|
||||
Feel free to adapt to your needs.
|
||||
|
||||
### Dependencies
|
||||
|
||||
```
|
||||
npm install jsonwebtoken
|
||||
```
|
||||
|
||||
### Function
|
||||
|
||||
Create a file under `functions` (for instance `/functions/custom-jwts.ts`), with the following contents:
|
||||
|
||||
```js
|
||||
import { Request, Response } from 'express'
|
||||
import process from 'process'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
// function to extract jwt from the request
|
||||
const getJwt = (req: Request): string | null => {
|
||||
const authHeader = req.headers.authorization
|
||||
if (!authHeader) {
|
||||
return null
|
||||
}
|
||||
|
||||
const parts = authHeader.split(' ')
|
||||
if (parts.length !== 2) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [scheme, token] = parts
|
||||
if (!/^Bearer$/i.test(scheme)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
// validate jwt token is valid and caller is either an admin or an operator
|
||||
const jwtIsAuthorized = (req: Request, key: string): string => {
|
||||
const token = getJwt(req)
|
||||
if (!token) {
|
||||
return ""
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, key)
|
||||
|
||||
const claims = decoded['https://hasura.io/jwt/claims']
|
||||
if ( !claims || !claims['x-hasura-allowed-roles'] ) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (
|
||||
claims['x-hasura-allowed-roles'].includes('admin') ||
|
||||
claims['x-hasura-allowed-roles'].includes('operator')
|
||||
) {
|
||||
return decoded.sub
|
||||
}
|
||||
|
||||
return ""
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
This is a sample function that generates a JWT token to impersonate users.
|
||||
|
||||
Authorization:
|
||||
|
||||
- send a valid JWT token with the request. The token should have the `admin` or `operator` role.
|
||||
- send the `x-hasura-admin-secret` header with the request
|
||||
|
||||
Body:
|
||||
|
||||
A json object with the following keys:
|
||||
|
||||
- `userId` (string): the user id for which the token is generated
|
||||
- `defaultRole` (string): the default role for the userId
|
||||
- `allowedRoles` (array of strings): the roles that the userId can assume
|
||||
|
||||
Returns:
|
||||
|
||||
A json object with the following keys:
|
||||
|
||||
- `accessToken` (string) - The generated access token
|
||||
|
||||
|
||||
In addition to the typical JWT claims generated by Nhost, the token generated by this function will have the following claims:
|
||||
|
||||
- `x-hasura-on-behalf-of`: the user id of the caller or `admin` if the caller used the `x-hasura-admin-secret` header
|
||||
|
||||
You are free to modify this function to suit your needs.
|
||||
*/
|
||||
export default (req: Request, res: Response) => {
|
||||
let authorizedCaller = ""
|
||||
if (req.headers['x-hasura-admin-secret'] === process.env.HASURA_GRAPHQL_ADMIN_SECRET) {
|
||||
authorizedCaller = "admin"
|
||||
}
|
||||
|
||||
const jwtSecret = JSON.parse(process.env.NHOST_JWT_SECRET)
|
||||
if (!authorizedCaller) {
|
||||
authorizedCaller = jwtIsAuthorized(req, jwtSecret.key)
|
||||
}
|
||||
|
||||
if (!authorizedCaller) {
|
||||
return res.status(401).json({ message: 'Unauthorized' })
|
||||
}
|
||||
|
||||
// extract from json in the body
|
||||
const {userId, defaultRole, allowedRoles} = req.body
|
||||
if (!userId || !defaultRole || !allowedRoles) {
|
||||
return res.status(400).json({ message: 'Bad request' })
|
||||
}
|
||||
|
||||
let token = jwt.sign({
|
||||
"exp": Math.floor(Date.now() / 1000) + 60 * 60, // 1 hour
|
||||
"https://hasura.io/jwt/claims": {
|
||||
"x-hasura-allowed-roles": allowedRoles,
|
||||
"x-hasura-default-role": defaultRole,
|
||||
"x-hasura-user-id": userId,
|
||||
"x-hasura-user-is-anonymous": "false",
|
||||
"x-hasura-on-behalf-of": authorizedCaller
|
||||
},
|
||||
"iat": Math.floor(Date.now() / 1000),
|
||||
"iss": "custom-lambda",
|
||||
"sub": userId,
|
||||
}, jwtSecret.key);
|
||||
|
||||
res.status(200).json(
|
||||
{
|
||||
accessToken: token,
|
||||
},
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Now you can call it like:
|
||||
|
||||
```
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-hasura-admin-secret: nhost-admin-secret" \
|
||||
-d '{"userId": "FFAB5354-C5EB-42C1-8BC3-AD21D2297883", "defaultRole": "user", "allowedRoles": ["user", "me"]}' \
|
||||
https://local.functions.nhost.run/v1/custom-jwt
|
||||
{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTcxNDIyMTMsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9jbGFpbXMiOnsieC1oYXN1cmEtYWxsb3dlZC1yb2xlcyI6WyJ1c2VyIiwibWUiXSwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoidXNlciIsIngtaGFzdXJhLXVzZXItaWQiOiJGRkFCNTM1NC1DNUVCLTQyQzEtOEJDMy1BRDIxRDIyOTc4ODMiLCJ4LWhhc3VyYS11c2VyLWlzLWFub255bW91cyI6ImZhbHNlIiwieC1oYXN1cmEtb24tYmVoYWxmLW9mIjoiYWRtaW4ifSwiaWF0IjoxNzE3MTM4NjEzLCJpc3MiOiJjdXN0b20tbGFtYmRhIiwic3ViIjoiRkZBQjUzNTQtQzVFQi00MkMxLThCQzMtQUQyMUQyMjk3ODgzIn0.bRhzJvXMdkQA8aXPH95uMT17WHED2rSRq3gE21Vp3Ak"}
|
||||
```
|
||||
|
||||
The new token should be a valid token for your application with the custom values requested:
|
||||
|
||||
```json
|
||||
{
|
||||
"exp": 1717141288,
|
||||
"https://hasura.io/jwt/claims": {
|
||||
"x-hasura-allowed-roles": [
|
||||
"a",
|
||||
"b"
|
||||
],
|
||||
"x-hasura-default-role": "user",
|
||||
"x-hasura-user-id": "FFAB5354-C5EB-42C1-8BC3-AD21D2297883",
|
||||
"x-hasura-user-is-anonymous": "false",
|
||||
"x-hasura-on-behalf-of": "admin"
|
||||
},
|
||||
"iat": 1717137688,
|
||||
"iss": "custom-lambda",
|
||||
"sub": "FFAB5354-C5EB-42C1-8BC3-AD21D2297883"
|
||||
}
|
||||
```
|
||||
@@ -93,3 +93,7 @@ nhost.auth.signIn({
|
||||
provider: 'apple'
|
||||
})
|
||||
```
|
||||
|
||||
<Note>
|
||||
To use your own domain for the callback URL refer to the [custom domains](/platform/custom-domains) documentation.
|
||||
</Note>
|
||||
|
||||
@@ -41,3 +41,7 @@ nhost.auth.signIn({
|
||||
provider: 'discord'
|
||||
})
|
||||
```
|
||||
|
||||
<Note>
|
||||
To use your own domain for the callback URL refer to the [custom domains](/platform/custom-domains) documentation.
|
||||
</Note>
|
||||
|
||||
@@ -64,3 +64,7 @@ nhost.auth.signIn({
|
||||
provider: 'facebook'
|
||||
})
|
||||
```
|
||||
|
||||
<Note>
|
||||
To use your own domain for the callback URL refer to the [custom domains](/platform/custom-domains) documentation.
|
||||
</Note>
|
||||
|
||||
@@ -49,3 +49,7 @@ nhost.auth.signIn({
|
||||
provider: "github",
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
To use your own domain for the callback URL refer to the [custom domains](/platform/custom-domains) documentation.
|
||||
</Note>
|
||||
|
||||
@@ -73,3 +73,7 @@ nhost.auth.signIn({
|
||||
provider: 'google'
|
||||
})
|
||||
```
|
||||
|
||||
<Note>
|
||||
To use your own domain for the callback URL refer to the [custom domains](/platform/custom-domains) documentation.
|
||||
</Note>
|
||||
|
||||
@@ -64,3 +64,7 @@ nhost.auth.signIn({
|
||||
provider: 'linkedin'
|
||||
})
|
||||
```
|
||||
|
||||
<Note>
|
||||
To use your own domain for the callback URL refer to the [custom domains](/platform/custom-domains) documentation.
|
||||
</Note>
|
||||
|
||||
@@ -49,3 +49,7 @@ nhost.auth.signIn({
|
||||
provider: 'spotify'
|
||||
})
|
||||
```
|
||||
|
||||
<Note>
|
||||
To use your own domain for the callback URL refer to the [custom domains](/platform/custom-domains) documentation.
|
||||
</Note>
|
||||
|
||||
@@ -43,3 +43,7 @@ nhost.auth.signIn({
|
||||
provider: 'twitch'
|
||||
})
|
||||
```
|
||||
|
||||
<Note>
|
||||
To use your own domain for the callback URL refer to the [custom domains](/platform/custom-domains) documentation.
|
||||
</Note>
|
||||
|
||||
@@ -63,3 +63,7 @@ nhost.auth.signIn({
|
||||
provider: 'workos'
|
||||
})
|
||||
```
|
||||
|
||||
<Note>
|
||||
To use your own domain for the callback URL refer to the [custom domains](/platform/custom-domains) documentation.
|
||||
</Note>
|
||||
|
||||
121
docs/guides/cli/connect-devices-to-local-nhost-project.mdx
Normal file
121
docs/guides/cli/connect-devices-to-local-nhost-project.mdx
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
title: Connect Devices to Local Nhost Project
|
||||
description: Configuring dnsmasq for network device connectivity to a local Nhost project
|
||||
icon: ethernet
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
If you want to connect to your local environment from other devices on the same network, such as Android emulators
|
||||
or iPhone devices, you can use **dnsmasq**. Follow this guide for the necessary configuration steps to enable this
|
||||
functionality for your local Nhost project running on your machine.
|
||||
|
||||
<Note>
|
||||
Make sure to install **dnsmasq**. If you're using another OS, please refer to the [dnsmasq website](https://thekelleys.org.uk/dnsmasq/doc.html).
|
||||
|
||||
<Tabs>
|
||||
<Tab title="macOS">
|
||||
```shell Terminal
|
||||
brew install dnsmasq
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Debian">
|
||||
```shell Terminal
|
||||
apt-get install dnsmasq
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Nix">
|
||||
```shell Terminal
|
||||
nix-env -iA nixpkgs.dnsmasq
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Note>
|
||||
|
||||
# Configure dnsmasq for Android
|
||||
<Warning>These steps are necessary when running on both an **Android emulator** or **physical Android device**</Warning>
|
||||
<Steps>
|
||||
<Step title="Configure dnsmasq">
|
||||
Configure `dnsmasq` to resolve nhost service urls to your machine's special [loopback address](https://developer.android.com/studio/run/emulator-networking) `10.0.2.2`
|
||||
```shell Terminal
|
||||
sudo dnsmasq -d \
|
||||
--address=/local.auth.nhost.run/10.0.2.2 \
|
||||
--address=/local.graphql.nhost.run/10.0.2.2 \
|
||||
--address=/local.storage.nhost.run/10.0.2.2 \
|
||||
--address=/local.functions.nhost.run/10.0.2.2
|
||||
```
|
||||
</Step>
|
||||
<Step title="Restart dnsmasq">
|
||||
If you're using another OS, please refer to the [dnsmasq website](https://thekelleys.org.uk/dnsmasq/doc.html).
|
||||
<Tabs>
|
||||
<Tab title="macOS">
|
||||
```shell Terminal
|
||||
sudo brew services restart dnsmasq
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Debian">
|
||||
```shell Terminal
|
||||
sudo systemctl restart dnsmasq
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step title="Configure the android device/emulator's DNS settings">
|
||||
1. Edit your network settings: Settings > Network & Internet > Internet > AndroidWifi
|
||||
2. set `IP settings` to `Static`
|
||||
3. set `DNS 1` and `DNS 2` to `10.0.2.2`
|
||||
4. Save
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
# Configure dnsmasq for iOS
|
||||
<Warning>These steps are only necessary when running on physical iOS device, the iOS simulator uses the host machine's network, so no additional configuration is typically needed.</Warning>
|
||||
<Steps>
|
||||
<Step title="Inspect your machine's IP address on your network">
|
||||
<Tabs>
|
||||
<Tab title="macOS">
|
||||
```shell Terminal
|
||||
ipconfig getifaddr en0
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Debian">
|
||||
```shell Terminal
|
||||
ip addr show dev en0 | grep 'inet ' | awk '{print $2}' | cut -d/ -f1
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step title="Configure dnsmasq">
|
||||
Configure `dnsmasq` to resolve nhost service urls to your machine's ip address.
|
||||
<Warning>Make sure to replace every occurrence of **[your-machine-s-up-address]** with the address printed in Step `1`</Warning>
|
||||
|
||||
```shell Terminal
|
||||
sudo dnsmasq -d \
|
||||
--address=/local.auth.nhost.run/[your-machine-s-up-address] \
|
||||
--address=/local.graphql.nhost.run/[your-machine-s-up-address] \
|
||||
--address=/local.storage.nhost.run/[your-machine-s-up-address] \
|
||||
--address=/local.functions.nhost.run/[your-machine-s-up-address]
|
||||
```
|
||||
</Step>
|
||||
<Step title="Restart dnsmasq">
|
||||
If you're using another OS, please refer to the [dnsmasq website](https://thekelleys.org.uk/dnsmasq/doc.html).
|
||||
<Tabs>
|
||||
<Tab title="macOS">
|
||||
```shell Terminal
|
||||
sudo brew services restart dnsmasq
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Debian">
|
||||
```shell Terminal
|
||||
sudo systemctl restart dnsmasq
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step title="Configure the iPhone's DNS settings">
|
||||
1. Select the wifi you're connected and select `Configure DNS`
|
||||
2. Select `Manual`
|
||||
3. Click on `Add server` and type your local machine's IP address printed in Step `1`
|
||||
4. Save
|
||||
</Step>
|
||||
</Steps>
|
||||
43
docs/guides/database/access.mdx
Normal file
43
docs/guides/database/access.mdx
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: Accessing the Database
|
||||
description: How to access the database directly using the connection string
|
||||
icon: key
|
||||
---
|
||||
|
||||
In most cases you will not need to access the database directly, choosing to interact with the data via the Graphql API, however, if you need direct access to postgres you can access it via the connection string.
|
||||
|
||||
|
||||
# Nhost Run
|
||||
|
||||
You can find details on how to connect to the database from an [Nhost Run](/product/run) service [here](/guides/run/networking#connecting-to-the-nhost-stack). If you don't know the password you can set a new password in the dashboard:
|
||||
|
||||
**Project Dashboard -> Settings -> Database**
|
||||
|
||||

|
||||
|
||||
# Public Access
|
||||
|
||||
For security reasons, by default your database won't be accessible online. If you need to access it directly from the Internet, first you will need to enable public access (enabling public access will also show the connection details):
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Dashboard">
|
||||
**Project Dashboard -> Settings -> Database**
|
||||
|
||||

|
||||
|
||||
</Tab>
|
||||
<Tab title="Config">
|
||||
```toml
|
||||
[postgres.resources]
|
||||
enablePublicAccess = true
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Note>
|
||||
Public access to your database utilizes [pgbouncer](http://www.pgbouncer.org). As this pooler is shared infrastructure the pooler will still be available even if your database has no public access configured. The pooler will simply not have access to your database.
|
||||
</Note>
|
||||
|
||||
# Functions
|
||||
|
||||
[Functions](/product/functions) run on a separate network, which means in order to access the database you will first need to [make it public](#public-access).
|
||||
@@ -16,7 +16,7 @@ In case your Postgres service is not meeting your performance expectations, you
|
||||
|
||||
4. Evaluate the usage of indexes in your database. Identify queries that could benefit from additional indexes and strategically add them to improve query performance.
|
||||
|
||||
5. Increase the disk size to increase [disk performance](/platform/compute-resources#disk-performance). Keep in mind increasing the disk size isn't reversible and increasing the memory of the service may yield better results. This is mostly useful when your data is very volatile and the postgres cache can't work effectively. Only attempt to increase disk for performance reasons if your reads and writes are very high and increasing memory isn't effective.
|
||||
5. If the problem is related to IOPS, consider increasing [disk performance](/platform/compute-resources#disk-performance).
|
||||
|
||||
By implementing these steps, you can effectively address performance concerns and enhance the overall performance of your Postgres service.
|
||||
|
||||
|
||||
43
docs/guides/functions/runtimes.mdx
Normal file
43
docs/guides/functions/runtimes.mdx
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: Runtimes and Dependencies
|
||||
description: Supported execution runtimes and package managers
|
||||
icon: person-running
|
||||
---
|
||||
|
||||
# Runtimes
|
||||
|
||||
The following runtimes are supported:
|
||||
|
||||
- [Node.js 18](https://nodejs.org)
|
||||
- [Node.js 20](https://nodejs.org)
|
||||
|
||||
To select your preferred runtime ensure the following configuration is present in your `nhost.toml` file:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Node.js 18">
|
||||
|
||||
```toml
|
||||
[functions.node]
|
||||
version = 18
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Node.js 20">
|
||||
|
||||
```toml
|
||||
[functions.node]
|
||||
version = 20
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Package Managers
|
||||
|
||||
For Node.js, the following package managers are supported:
|
||||
|
||||
1. [npm](https://www.npmjs.com)
|
||||
2. [yarn](https://yarnpkg.com)
|
||||
3. [pnpm](https://pnpm.io)
|
||||
|
||||
To use one package manager or another, add the relevant lockfile (either `package-lock.json` for npm, `yarn.lock` for yarn or `pnpm-lock.yaml` for `pnpm`) to the functions folder or a parent folder. If multiple lockfiles are present, preference order is `npm` > `pnpm` > `yarn`.
|
||||
457
docs/guides/quickstarts/react-native.mdx
Normal file
457
docs/guides/quickstarts/react-native.mdx
Normal file
@@ -0,0 +1,457 @@
|
||||
---
|
||||
title: Get up and running with Nhost and React Native
|
||||
sidebarTitle: React Native
|
||||
description: In this quickstart guide, we'll demonstrate how to build a simple To-Do feature using Nhost and React Native.
|
||||
icon: mobile-notch
|
||||
---
|
||||
|
||||
<Card>
|
||||
Throughout this guide, we'll utilize the **@nhost/react-native-template**, which comes pre-configured with
|
||||
authentication and storage capabilities provided by Nhost.
|
||||
</Card>
|
||||
|
||||
<br />
|
||||
|
||||
<Note>
|
||||
Before starting this quickstart, ensure that your environment is set up to work with React Native.
|
||||
Follow the [setup guide](https://reactnative.dev/docs/next/set-up-your-environment) available on
|
||||
the official React Native website.
|
||||
</Note>
|
||||
|
||||
<Steps>
|
||||
<Step title="Create Nhost Project">
|
||||
Create your project through the [Nhost Dashboard](https://app.nhost.io/new).
|
||||
</Step>
|
||||
|
||||
<Step title="Setup Database">
|
||||
Navigate to the **SQL Editor** of the database and run the following SQL to create a new table `todos`.
|
||||
|
||||
<Warning>Make sure the option `Track this` is enabled</Warning>
|
||||
|
||||
```sql SQL Editor
|
||||
CREATE TABLE todos (
|
||||
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
user_id uuid NOT NULL,
|
||||
contents text NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
```
|
||||
|
||||
<Frame caption="Create Todos Table">
|
||||
<img src="/images/guides/quickstarts/react-native/create-table-todos.png" />
|
||||
</Frame>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Configure the todos table permissions">
|
||||
To set permissions for the new `todos` table, select the table, click on the `...` to open the actions dialog,
|
||||
then click on **Edit Permissions**. Set the following permissions for the `user` role:
|
||||
|
||||
1. `Insert`
|
||||
- Set `Row insert permissions` to `Without any checks`
|
||||
- Select all columns except `user_id` on `Column insert permissions`
|
||||
- Add a new `Column preset` and set `Column Name` to `user_id` and `Column Value` to `X-Hasura-User-Id`
|
||||
- Save
|
||||
|
||||
<Frame caption="Insert Permissions">
|
||||
<img src="/images/guides/quickstarts/react-native/todos-insert-permissions.png" />
|
||||
</Frame>
|
||||
|
||||
2. `Select`
|
||||
- Set `Row select permissions` to `With custom check` and fill in the following rule:
|
||||
- Set `Where` to `todos.user_id`
|
||||
- Set the operator to `_eq`
|
||||
- Set the value to `X-Hasura-User-Id`
|
||||
- Select all columns except `user_id` on `Column select permissions`
|
||||
- Save
|
||||
|
||||
<Frame caption="Select Permissions">
|
||||
<img src="/images/guides/quickstarts/react-native/todos-select-permissions.png" />
|
||||
</Frame>
|
||||
|
||||
3. `Update`
|
||||
- Set `Row update permissions` to `With custom check` and fill in the following rule:
|
||||
- Set `Where` to `todos.user_id`
|
||||
- Set the operator to `_eq`
|
||||
- Set the value to `X-Hasura-User-Id`
|
||||
- Select all columns except `user_id` on `Column select permissions`
|
||||
- Save
|
||||
|
||||
<Frame caption="Update permissions">
|
||||
<img src="/images/guides/quickstarts/react-native/todos-update-permissions.png" />
|
||||
</Frame>
|
||||
|
||||
4. `Delete`
|
||||
- Set `Row delete permissions` to `With custom check` and fill in the following rule:
|
||||
- Set `Where` to `todos.user_id`
|
||||
- Set the operator to `_eq`
|
||||
- Set the value to `X-Hasura-User-Id`
|
||||
- Save
|
||||
|
||||
<Frame caption="Delete permissions">
|
||||
<img src="/images/guides/quickstarts/react-native/todos-delete-permissions.png" />
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Configure permissions to enable user file uploads">
|
||||
To enable file uploads by users, set the permissions as follows:
|
||||
|
||||
1. Edit the **files** table permissions
|
||||
1. Navigate to the files table within the [Database tab](https://app.nhost.io/_/_/database/browser/default/storage/files)
|
||||
2. Click on the three dots (...) next to the files table
|
||||
3. Click on **Edit Permissions**
|
||||
|
||||
2. Modify the `Insert` permission for the `user` role:
|
||||
1. Set `Row insert permissions` to `Without any checks`
|
||||
2. Select all columns on `Column insert permissions`
|
||||
4. Save
|
||||
|
||||
<Frame caption="Insert Permissions">
|
||||
<img src="/images/guides/quickstarts/react-native/files-insert-permissions.png" />
|
||||
</Frame>
|
||||
|
||||
3. `Select`
|
||||
- Set `Row select permissions` to `With custom check` and fill in the following rule:
|
||||
- Set `Where` to `files.uploaded_by_user_id`
|
||||
- Set the operator to `_eq`
|
||||
- Set the value to `X-Hasura-User-Id`
|
||||
- Select all columns on `Column select permissions`
|
||||
- Save
|
||||
|
||||
<Frame caption="Select permissions">
|
||||
<img src="/images/guides/quickstarts/react-native/files-select-permissions.png" />
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Bootstrap your React Native app">
|
||||
Intialize a new React Native project using the template `@nhost/react-native-template`
|
||||
|
||||
```bash Terminal
|
||||
npx react-native init myapp --template @nhost/react-native-template
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Connect your React Native app to the Nhost project">
|
||||
Copy your project's `<subdomain>` and `<region>` values available on the dashboard overview
|
||||
|
||||
```tsx src/root.tsx
|
||||
const nhost = new NhostClient({
|
||||
subdomain: "<subdomain>", // replace the subdomain value e.g. "hjcuuqweqwezolpolrep"
|
||||
region: "<region>", // replace the region value e.g. "eu-central-1"
|
||||
clientStorageType: 'react-native',
|
||||
clientStorage: AsyncStorage,
|
||||
});
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Add the GraphQL queries">
|
||||
Create a new file `src/graphql/todos.ts` that will expose the graphql queries needed to `list`, `add` and `delete` To-Do's.
|
||||
|
||||
```ts src/graphql/todos.ts
|
||||
import {gql} from '@apollo/client';
|
||||
|
||||
export const GET_TODOS = gql`
|
||||
query listTodos {
|
||||
todos(order_by: { created_at: desc }) {
|
||||
id
|
||||
contents
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ADD_TODO = gql`
|
||||
mutation addTodo($contents: String!) {
|
||||
insert_todos_one(object: { contents: $contents }) {
|
||||
id
|
||||
contents
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_TODO = gql`
|
||||
mutation deleteTodo($id: uuid!) {
|
||||
delete_todos_by_pk(id: $id) {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`;
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Add a form to insert a To-Do">
|
||||
```tsx src/components/AddTodoForm.tsx
|
||||
import React from 'react';
|
||||
import {useMutation} from '@apollo/client';
|
||||
import Button from '@components/Button';
|
||||
import ControlledInput from '@components/ControlledInput';
|
||||
import {ADD_TODO, GET_TODOS} from '@graphql/todos';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
interface AddTodoFormValues {
|
||||
contents: string;
|
||||
}
|
||||
|
||||
export default function AddTodoForm() {
|
||||
const {control, handleSubmit, reset} = useForm<AddTodoFormValues>();
|
||||
|
||||
const [addTodo, {loading}] = useMutation(ADD_TODO, {
|
||||
refetchQueries: [{query: GET_TODOS}],
|
||||
});
|
||||
|
||||
const onSubmit = async (values: AddTodoFormValues) => {
|
||||
const {contents} = values;
|
||||
await addTodo({variables: {contents}});
|
||||
reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<View style={styles.inputWrapper}>
|
||||
<ControlledInput
|
||||
control={control}
|
||||
name="contents"
|
||||
placeholder="New To-Do"
|
||||
autoCapitalize="none"
|
||||
rules={{
|
||||
required: true,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<Button
|
||||
label="Add"
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
gap: 12,
|
||||
padding: 12,
|
||||
flexDirection: 'row',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
inputWrapper: {
|
||||
flex: 3,
|
||||
},
|
||||
buttonWrapper: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Add the Todo component and the screen to list all the todos">
|
||||
<CodeGroup>
|
||||
```tsx src/components/Todo.tsx
|
||||
import React from 'react';
|
||||
import {useMutation} from '@apollo/client';
|
||||
import {DELETE_TODO, GET_TODOS} from '@graphql/todos';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import Button from './Button';
|
||||
|
||||
export interface TodoItem {
|
||||
id: string;
|
||||
contents: string;
|
||||
}
|
||||
|
||||
export default function Todo({todo: {id, contents}}: {todo: TodoItem}) {
|
||||
const [deleteTodo] = useMutation(DELETE_TODO, {
|
||||
variables: {id},
|
||||
refetchQueries: [{query: GET_TODOS}],
|
||||
});
|
||||
|
||||
const handleDeleteTodo = async () => {
|
||||
await deleteTodo();
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<View style={styles.todoContentWrapper}>
|
||||
<Icon name="check" size={25} />
|
||||
<Text style={styles.todoContent}>{contents}</Text>
|
||||
</View>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<Button
|
||||
label={<Icon name="trash-can-outline" size={20} />}
|
||||
color="#f1f1f1"
|
||||
onPress={handleDeleteTodo}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
padding: 14,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
todoContentWrapper: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 20,
|
||||
},
|
||||
todoContent: {flex: 1},
|
||||
buttonWrapper: {
|
||||
width: 50,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```tsx src/screens/Todos.tsx
|
||||
import React from 'react';
|
||||
import {useQuery} from '@apollo/client';
|
||||
import AddTodoForm from '@components/AddTodoForm';
|
||||
import Todo, {type TodoItem} from '@components/Todo';
|
||||
import {GET_TODOS} from '@graphql/todos';
|
||||
import {useEffect} from 'react';
|
||||
import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native';
|
||||
|
||||
export default function Todos() {
|
||||
const {loading, data, client} = useQuery<{todos: TodoItem[]}>(GET_TODOS);
|
||||
|
||||
const todos = data?.todos || [];
|
||||
|
||||
useEffect(() => {
|
||||
return () => client.stop();
|
||||
}, [client]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={styles.loadingViewWrapper}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const renderTodo = ({item}: {item: TodoItem}) => <Todo todo={item} />;
|
||||
const itemSeperator = () => <View style={styles.separator} />;
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<AddTodoForm />
|
||||
<FlatList
|
||||
data={todos}
|
||||
keyExtractor={item => item.id}
|
||||
renderItem={renderTodo}
|
||||
ItemSeparatorComponent={itemSeperator}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loadingViewWrapper: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
backgroundColor: '#f1f1f1',
|
||||
},
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
</Step>
|
||||
|
||||
<Step title="Reference the new Todos components in the Drawer Navigator">
|
||||
```tsx src/screens/Main.tsx
|
||||
function DrawerNavigator() {
|
||||
return (
|
||||
<Drawer.Navigator
|
||||
screenOptions={screenOptions}
|
||||
drawerContent={drawerContent}>
|
||||
<Drawer.Screen name="Profile" component={Profile} />
|
||||
{/* Add the Todos component here */}
|
||||
<Drawer.Screen name="Todos" component={Todos} />
|
||||
<Drawer.Screen name="Storage" component={Storage} />
|
||||
</Drawer.Navigator>
|
||||
);
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Run the app on the emulator">
|
||||
<Tabs>
|
||||
<Tab title="Android">
|
||||
1. Open a terminal and start the metro bundler
|
||||
```bash Terminal
|
||||
cd myapp
|
||||
npm start
|
||||
```
|
||||
2. Open a new terminal and run the app on Android
|
||||
```bash Terminal
|
||||
cd myapp
|
||||
npm run android
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="iOS">
|
||||
1. Make sure the iOS project cocopods are installed
|
||||
```bash Terminal
|
||||
cd ios
|
||||
pod install
|
||||
```
|
||||
1. Install the `ios-deploy` CLI
|
||||
```bash Terminal
|
||||
npm install -g ios-deploy
|
||||
```
|
||||
2. Start the metro bundler
|
||||
```bash Terminal
|
||||
cd myapp
|
||||
npm start
|
||||
```
|
||||
3. Open a new terminal and run the app on Android
|
||||
```bash Terminal
|
||||
cd myapp
|
||||
npm run ios --interactive
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
<Step title="Demo">
|
||||
<iframe
|
||||
width="486"
|
||||
height="864"
|
||||
src="https://www.youtube.com/embed/gfzksbce2G4"
|
||||
title="demo react native"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen
|
||||
/>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
<Note>
|
||||
### Next Steps: enabling Google and Apple Sign-In
|
||||
|
||||
The template is preconfigured to allow users to sign in with Google and Apple. To enable this feature, follow these steps:
|
||||
|
||||
1. Navigate to your Nhost project's [Sign-In Methods settings](https://app.nhost.io/_/_/settings/sign-in-methods).
|
||||
2. Enable Google and/or Apple sign-in.
|
||||
3. Fill in the necessary credentials.
|
||||
|
||||
For detailed instructions on generating the required credentials, refer to the following guides:
|
||||
- [Google Sign-In Guide](https://docs.nhost.io/guides/auth/social/sign-in-google)
|
||||
- [Apple Sign-In Guide](https://docs.nhost.io/guides/auth/social/sign-in-apple)
|
||||
</Note>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user