mirror of https://github.com/coder/coder.git
chore: format code with semicolons when using prettier (#9555)
This commit is contained in:
parent
bef38b8413
commit
988c9af015
|
@ -1,9 +1,8 @@
|
||||||
# This config file is used in conjunction with `.editorconfig` to specify
|
# This config file is used in conjunction with `.editorconfig` to specify
|
||||||
# formatting for prettier-supported files. See `.editorconfig` and
|
# formatting for prettier-supported files. See `.editorconfig` and
|
||||||
# `site/.editorconfig`for whitespace formatting options.
|
# `site/.editorconfig` for whitespace formatting options.
|
||||||
printWidth: 80
|
printWidth: 80
|
||||||
proseWrap: always
|
proseWrap: always
|
||||||
semi: false
|
|
||||||
trailingComma: all
|
trailingComma: all
|
||||||
useTabs: false
|
useTabs: false
|
||||||
tabWidth: 2
|
tabWidth: 2
|
||||||
|
|
|
@ -128,9 +128,9 @@ export const getAgentListeningPorts = async (
|
||||||
): Promise<TypesGen.ListeningPortsResponse> => {
|
): Promise<TypesGen.ListeningPortsResponse> => {
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`/api/v2/workspaceagents/${agentID}/listening-ports`,
|
`/api/v2/workspaceagents/${agentID}/listening-ports`,
|
||||||
)
|
);
|
||||||
return response.data
|
return response.data;
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Sometimes, a FE operation can have multiple API calls so it is ok to wrap it as
|
Sometimes, a FE operation can have multiple API calls so it is ok to wrap it as
|
||||||
|
@ -140,9 +140,9 @@ a single function.
|
||||||
export const updateWorkspaceVersion = async (
|
export const updateWorkspaceVersion = async (
|
||||||
workspace: TypesGen.Workspace,
|
workspace: TypesGen.Workspace,
|
||||||
): Promise<TypesGen.WorkspaceBuild> => {
|
): Promise<TypesGen.WorkspaceBuild> => {
|
||||||
const template = await getTemplate(workspace.template_id)
|
const template = await getTemplate(workspace.template_id);
|
||||||
return startWorkspace(workspace.id, template.active_version_id)
|
return startWorkspace(workspace.id, template.active_version_id);
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
If you need more granular errors or control, you may should consider keep them
|
If you need more granular errors or control, you may should consider keep them
|
||||||
|
@ -242,14 +242,14 @@ instead of using `screen.getByRole("button")` directly we could do
|
||||||
slow.
|
slow.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
user.click(screen.getByRole("button"))
|
user.click(screen.getByRole("button"));
|
||||||
```
|
```
|
||||||
|
|
||||||
✅ Better. We can limit the number of elements we are querying.
|
✅ Better. We can limit the number of elements we are querying.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const form = screen.getByTestId("form")
|
const form = screen.getByTestId("form");
|
||||||
user.click(within(form).getByRole("button"))
|
user.click(within(form).getByRole("button"));
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `jest.spyOn` with the API is not working
|
#### `jest.spyOn` with the API is not working
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
trailingSlash: true,
|
trailingSlash: true,
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = nextConfig
|
module.exports = nextConfig;
|
||||||
|
|
|
@ -24,57 +24,57 @@ import {
|
||||||
Tr,
|
Tr,
|
||||||
UnorderedList,
|
UnorderedList,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from "@chakra-ui/react"
|
} from "@chakra-ui/react";
|
||||||
import fm from "front-matter"
|
import fm from "front-matter";
|
||||||
import { readFileSync } from "fs"
|
import { readFileSync } from "fs";
|
||||||
import _ from "lodash"
|
import _ from "lodash";
|
||||||
import { GetStaticPaths, GetStaticProps, NextPage } from "next"
|
import { GetStaticPaths, GetStaticProps, NextPage } from "next";
|
||||||
import Head from "next/head"
|
import Head from "next/head";
|
||||||
import NextLink from "next/link"
|
import NextLink from "next/link";
|
||||||
import { useRouter } from "next/router"
|
import { useRouter } from "next/router";
|
||||||
import path from "path"
|
import path from "path";
|
||||||
import { MdMenu } from "react-icons/md"
|
import { MdMenu } from "react-icons/md";
|
||||||
import ReactMarkdown from "react-markdown"
|
import ReactMarkdown from "react-markdown";
|
||||||
import rehypeRaw from "rehype-raw"
|
import rehypeRaw from "rehype-raw";
|
||||||
import remarkGfm from "remark-gfm"
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
type FilePath = string
|
type FilePath = string;
|
||||||
type UrlPath = string
|
type UrlPath = string;
|
||||||
type Route = {
|
type Route = {
|
||||||
path: FilePath
|
path: FilePath;
|
||||||
title: string
|
title: string;
|
||||||
description?: string
|
description?: string;
|
||||||
children?: Route[]
|
children?: Route[];
|
||||||
}
|
};
|
||||||
type Manifest = { versions: string[]; routes: Route[] }
|
type Manifest = { versions: string[]; routes: Route[] };
|
||||||
type NavItem = { title: string; path: UrlPath; children?: NavItem[] }
|
type NavItem = { title: string; path: UrlPath; children?: NavItem[] };
|
||||||
type Nav = NavItem[]
|
type Nav = NavItem[];
|
||||||
|
|
||||||
const readContentFile = (filePath: string) => {
|
const readContentFile = (filePath: string) => {
|
||||||
const baseDir = process.cwd()
|
const baseDir = process.cwd();
|
||||||
const docsPath = path.join(baseDir, "..", "docs")
|
const docsPath = path.join(baseDir, "..", "docs");
|
||||||
return readFileSync(path.join(docsPath, filePath), { encoding: "utf-8" })
|
return readFileSync(path.join(docsPath, filePath), { encoding: "utf-8" });
|
||||||
}
|
};
|
||||||
|
|
||||||
const removeTrailingSlash = (path: string) => path.replace(/\/+$/, "")
|
const removeTrailingSlash = (path: string) => path.replace(/\/+$/, "");
|
||||||
|
|
||||||
const removeMkdExtension = (path: string) => path.replace(/\.md/g, "")
|
const removeMkdExtension = (path: string) => path.replace(/\.md/g, "");
|
||||||
|
|
||||||
const removeIndexFilename = (path: string) => {
|
const removeIndexFilename = (path: string) => {
|
||||||
if (path.endsWith("index")) {
|
if (path.endsWith("index")) {
|
||||||
path = path.replace("index", "")
|
path = path.replace("index", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return path
|
return path;
|
||||||
}
|
};
|
||||||
|
|
||||||
const removeREADMEName = (path: string) => {
|
const removeREADMEName = (path: string) => {
|
||||||
if (path.startsWith("README")) {
|
if (path.startsWith("README")) {
|
||||||
path = path.replace("README", "")
|
path = path.replace("README", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return path
|
return path;
|
||||||
}
|
};
|
||||||
|
|
||||||
// transformLinkUri converts the links in the markdown file to
|
// transformLinkUri converts the links in the markdown file to
|
||||||
// href html links. All index page routes are the directory name, and all
|
// href html links. All index page routes are the directory name, and all
|
||||||
|
@ -87,142 +87,142 @@ const removeREADMEName = (path: string) => {
|
||||||
// file.md -> ../file-next-to-file = ../file-next-to-file
|
// file.md -> ../file-next-to-file = ../file-next-to-file
|
||||||
const transformLinkUriSource = (sourceFile: string) => {
|
const transformLinkUriSource = (sourceFile: string) => {
|
||||||
return (href = "") => {
|
return (href = "") => {
|
||||||
const isExternal = href.startsWith("http") || href.startsWith("https")
|
const isExternal = href.startsWith("http") || href.startsWith("https");
|
||||||
if (!isExternal) {
|
if (!isExternal) {
|
||||||
// Remove .md form the path
|
// Remove .md form the path
|
||||||
href = removeMkdExtension(href)
|
href = removeMkdExtension(href);
|
||||||
|
|
||||||
// Add the extra '..' if not an index file.
|
// Add the extra '..' if not an index file.
|
||||||
sourceFile = removeMkdExtension(sourceFile)
|
sourceFile = removeMkdExtension(sourceFile);
|
||||||
if (!sourceFile.endsWith("index")) {
|
if (!sourceFile.endsWith("index")) {
|
||||||
href = "../" + href
|
href = "../" + href;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the index path
|
// Remove the index path
|
||||||
href = removeIndexFilename(href)
|
href = removeIndexFilename(href);
|
||||||
href = removeREADMEName(href)
|
href = removeREADMEName(href);
|
||||||
}
|
}
|
||||||
return href
|
return href;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const transformFilePathToUrlPath = (filePath: string) => {
|
const transformFilePathToUrlPath = (filePath: string) => {
|
||||||
// Remove markdown extension
|
// Remove markdown extension
|
||||||
let urlPath = removeMkdExtension(filePath)
|
let urlPath = removeMkdExtension(filePath);
|
||||||
|
|
||||||
// Remove relative path
|
// Remove relative path
|
||||||
if (urlPath.startsWith("./")) {
|
if (urlPath.startsWith("./")) {
|
||||||
urlPath = urlPath.replace("./", "")
|
urlPath = urlPath.replace("./", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove index from the root file
|
// Remove index from the root file
|
||||||
urlPath = removeIndexFilename(urlPath)
|
urlPath = removeIndexFilename(urlPath);
|
||||||
urlPath = removeREADMEName(urlPath)
|
urlPath = removeREADMEName(urlPath);
|
||||||
|
|
||||||
// Remove trailing slash
|
// Remove trailing slash
|
||||||
if (urlPath.endsWith("/")) {
|
if (urlPath.endsWith("/")) {
|
||||||
urlPath = removeTrailingSlash(urlPath)
|
urlPath = removeTrailingSlash(urlPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return urlPath
|
return urlPath;
|
||||||
}
|
};
|
||||||
|
|
||||||
const mapRoutes = (manifest: Manifest): Record<UrlPath, Route> => {
|
const mapRoutes = (manifest: Manifest): Record<UrlPath, Route> => {
|
||||||
const paths: Record<UrlPath, Route> = {}
|
const paths: Record<UrlPath, Route> = {};
|
||||||
|
|
||||||
const addPaths = (routes: Route[]) => {
|
const addPaths = (routes: Route[]) => {
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
paths[transformFilePathToUrlPath(route.path)] = route
|
paths[transformFilePathToUrlPath(route.path)] = route;
|
||||||
|
|
||||||
if (route.children) {
|
if (route.children) {
|
||||||
addPaths(route.children)
|
addPaths(route.children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
addPaths(manifest.routes)
|
addPaths(manifest.routes);
|
||||||
|
|
||||||
return paths
|
return paths;
|
||||||
}
|
};
|
||||||
|
|
||||||
let manifest: Manifest | undefined
|
let manifest: Manifest | undefined;
|
||||||
|
|
||||||
const getManifest = () => {
|
const getManifest = () => {
|
||||||
if (manifest) {
|
if (manifest) {
|
||||||
return manifest
|
return manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
const manifestContent = readContentFile("manifest.json")
|
const manifestContent = readContentFile("manifest.json");
|
||||||
manifest = JSON.parse(manifestContent) as Manifest
|
manifest = JSON.parse(manifestContent) as Manifest;
|
||||||
return manifest
|
return manifest;
|
||||||
}
|
};
|
||||||
|
|
||||||
let navigation: Nav | undefined
|
let navigation: Nav | undefined;
|
||||||
|
|
||||||
const getNavigation = (manifest: Manifest): Nav => {
|
const getNavigation = (manifest: Manifest): Nav => {
|
||||||
if (navigation) {
|
if (navigation) {
|
||||||
return navigation
|
return navigation;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNavItem = (route: Route, parentPath?: UrlPath): NavItem => {
|
const getNavItem = (route: Route, parentPath?: UrlPath): NavItem => {
|
||||||
const path = parentPath
|
const path = parentPath
|
||||||
? `${parentPath}/${transformFilePathToUrlPath(route.path)}`
|
? `${parentPath}/${transformFilePathToUrlPath(route.path)}`
|
||||||
: transformFilePathToUrlPath(route.path)
|
: transformFilePathToUrlPath(route.path);
|
||||||
const navItem: NavItem = {
|
const navItem: NavItem = {
|
||||||
title: route.title,
|
title: route.title,
|
||||||
path,
|
path,
|
||||||
}
|
};
|
||||||
|
|
||||||
if (route.children) {
|
if (route.children) {
|
||||||
navItem.children = []
|
navItem.children = [];
|
||||||
|
|
||||||
for (const childRoute of route.children) {
|
for (const childRoute of route.children) {
|
||||||
navItem.children.push(getNavItem(childRoute))
|
navItem.children.push(getNavItem(childRoute));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return navItem
|
return navItem;
|
||||||
}
|
};
|
||||||
|
|
||||||
navigation = []
|
navigation = [];
|
||||||
|
|
||||||
for (const route of manifest.routes) {
|
for (const route of manifest.routes) {
|
||||||
navigation.push(getNavItem(route))
|
navigation.push(getNavItem(route));
|
||||||
}
|
}
|
||||||
|
|
||||||
return navigation
|
return navigation;
|
||||||
}
|
};
|
||||||
|
|
||||||
const removeHtmlComments = (string: string) => {
|
const removeHtmlComments = (string: string) => {
|
||||||
return string.replace(/<!--[\s\S]*?-->/g, "")
|
return string.replace(/<!--[\s\S]*?-->/g, "");
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = () => {
|
export const getStaticPaths: GetStaticPaths = () => {
|
||||||
const manifest = getManifest()
|
const manifest = getManifest();
|
||||||
const routes = mapRoutes(manifest)
|
const routes = mapRoutes(manifest);
|
||||||
const paths = Object.keys(routes).map((urlPath) => ({
|
const paths = Object.keys(routes).map((urlPath) => ({
|
||||||
params: { slug: urlPath.split("/") },
|
params: { slug: urlPath.split("/") },
|
||||||
}))
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paths,
|
paths,
|
||||||
fallback: false,
|
fallback: false,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = (context) => {
|
export const getStaticProps: GetStaticProps = (context) => {
|
||||||
// When it is home page, the slug is undefined because there is no url path
|
// When it is home page, the slug is undefined because there is no url path
|
||||||
// so we make it an empty string to work good with the mapRoutes
|
// so we make it an empty string to work good with the mapRoutes
|
||||||
const { slug = [""] } = context.params as { slug: string[] }
|
const { slug = [""] } = context.params as { slug: string[] };
|
||||||
const manifest = getManifest()
|
const manifest = getManifest();
|
||||||
const routes = mapRoutes(manifest)
|
const routes = mapRoutes(manifest);
|
||||||
const urlPath = slug.join("/")
|
const urlPath = slug.join("/");
|
||||||
const route = routes[urlPath]
|
const route = routes[urlPath];
|
||||||
const { body } = fm(readContentFile(route.path))
|
const { body } = fm(readContentFile(route.path));
|
||||||
// Serialize MDX to support custom components
|
// Serialize MDX to support custom components
|
||||||
const content = removeHtmlComments(body)
|
const content = removeHtmlComments(body);
|
||||||
const navigation = getNavigation(manifest)
|
const navigation = getNavigation(manifest);
|
||||||
const version = manifest.versions[0]
|
const version = manifest.versions[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
@ -231,25 +231,26 @@ export const getStaticProps: GetStaticProps = (context) => {
|
||||||
route,
|
route,
|
||||||
version,
|
version,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const SidebarNavItem: React.FC<{ item: NavItem; nav: Nav }> = ({
|
const SidebarNavItem: React.FC<{ item: NavItem; nav: Nav }> = ({
|
||||||
item,
|
item,
|
||||||
nav,
|
nav,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
let isActive = router.asPath.startsWith(`/${item.path}`)
|
let isActive = router.asPath.startsWith(`/${item.path}`);
|
||||||
|
|
||||||
// Special case to handle the home path
|
// Special case to handle the home path
|
||||||
if (item.path === "") {
|
if (item.path === "") {
|
||||||
isActive = router.asPath === "/"
|
isActive = router.asPath === "/";
|
||||||
|
|
||||||
// Special case to handle the home path children
|
// Special case to handle the home path children
|
||||||
const homeNav = nav.find((navItem) => navItem.path === "") as NavItem
|
const homeNav = nav.find((navItem) => navItem.path === "") as NavItem;
|
||||||
const homeNavPaths = homeNav.children?.map((item) => `/${item.path}/`) ?? []
|
const homeNavPaths =
|
||||||
|
homeNav.children?.map((item) => `/${item.path}/`) ?? [];
|
||||||
if (homeNavPaths.includes(router.asPath)) {
|
if (homeNavPaths.includes(router.asPath)) {
|
||||||
isActive = true
|
isActive = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,8 +281,8 @@ const SidebarNavItem: React.FC<{ item: NavItem; nav: Nav }> = ({
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const SidebarNav: React.FC<{ nav: Nav; version: string } & GridProps> = ({
|
const SidebarNav: React.FC<{ nav: Nav; version: string } & GridProps> = ({
|
||||||
nav,
|
nav,
|
||||||
|
@ -312,14 +313,14 @@ const SidebarNav: React.FC<{ nav: Nav; version: string } & GridProps> = ({
|
||||||
<SidebarNavItem key={navItem.path} item={navItem} nav={nav} />
|
<SidebarNavItem key={navItem.path} item={navItem} nav={nav} />
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const MobileNavbar: React.FC<{ nav: Nav; version: string }> = ({
|
const MobileNavbar: React.FC<{ nav: Nav; version: string }> = ({
|
||||||
nav,
|
nav,
|
||||||
version,
|
version,
|
||||||
}) => {
|
}) => {
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -347,26 +348,26 @@ const MobileNavbar: React.FC<{ nav: Nav; version: string }> = ({
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const slugifyTitle = (title: string) => {
|
const slugifyTitle = (title: string) => {
|
||||||
return _.kebabCase(title.toLowerCase())
|
return _.kebabCase(title.toLowerCase());
|
||||||
}
|
};
|
||||||
|
|
||||||
const getImageUrl = (src: string | undefined) => {
|
const getImageUrl = (src: string | undefined) => {
|
||||||
if (src === undefined) {
|
if (src === undefined) {
|
||||||
return ""
|
return "";
|
||||||
}
|
}
|
||||||
const assetPath = src.split("images/")[1]
|
const assetPath = src.split("images/")[1];
|
||||||
return `/images/${assetPath}`
|
return `/images/${assetPath}`;
|
||||||
}
|
};
|
||||||
|
|
||||||
const DocsPage: NextPage<{
|
const DocsPage: NextPage<{
|
||||||
content: string
|
content: string;
|
||||||
navigation: Nav
|
navigation: Nav;
|
||||||
route: Route
|
route: Route;
|
||||||
version: string
|
version: string;
|
||||||
}> = ({ content, navigation, route, version }) => {
|
}> = ({ content, navigation, route, version }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -486,7 +487,7 @@ const DocsPage: NextPage<{
|
||||||
),
|
),
|
||||||
a: ({ children, href = "" }) => {
|
a: ({ children, href = "" }) => {
|
||||||
const isExternal =
|
const isExternal =
|
||||||
href.startsWith("http") || href.startsWith("https")
|
href.startsWith("http") || href.startsWith("https");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
@ -497,7 +498,7 @@ const DocsPage: NextPage<{
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
code: ({ node, ...props }) => (
|
code: ({ node, ...props }) => (
|
||||||
<Code {...props} bgColor="gray.100" />
|
<Code {...props} bgColor="gray.100" />
|
||||||
|
@ -538,7 +539,7 @@ const DocsPage: NextPage<{
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default DocsPage
|
export default DocsPage;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ChakraProvider, extendTheme } from "@chakra-ui/react"
|
import { ChakraProvider, extendTheme } from "@chakra-ui/react";
|
||||||
import type { AppProps } from "next/app"
|
import type { AppProps } from "next/app";
|
||||||
import Head from "next/head"
|
import Head from "next/head";
|
||||||
|
|
||||||
const theme = extendTheme({
|
const theme = extendTheme({
|
||||||
styles: {
|
styles: {
|
||||||
|
@ -10,7 +10,7 @@ const theme = extendTheme({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const MyApp: React.FC<AppProps> = ({ Component, pageProps }) => {
|
const MyApp: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -23,7 +23,7 @@ const MyApp: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default MyApp
|
export default MyApp;
|
||||||
|
|
|
@ -2,10 +2,9 @@
|
||||||
|
|
||||||
# This config file is used in conjunction with `.editorconfig` to specify
|
# This config file is used in conjunction with `.editorconfig` to specify
|
||||||
# formatting for prettier-supported files. See `.editorconfig` and
|
# formatting for prettier-supported files. See `.editorconfig` and
|
||||||
# `site/.editorconfig`for whitespace formatting options.
|
# `site/.editorconfig` for whitespace formatting options.
|
||||||
printWidth: 80
|
printWidth: 80
|
||||||
proseWrap: always
|
proseWrap: always
|
||||||
semi: false
|
|
||||||
trailingComma: all
|
trailingComma: all
|
||||||
useTabs: false
|
useTabs: false
|
||||||
tabWidth: 2
|
tabWidth: 2
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import turbosnap from "vite-plugin-turbosnap"
|
import turbosnap from "vite-plugin-turbosnap";
|
||||||
import { mergeConfig } from "vite"
|
import { mergeConfig } from "vite";
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
stories: ["../src/**/*.stories.tsx"],
|
stories: ["../src/**/*.stories.tsx"],
|
||||||
|
@ -15,7 +15,7 @@ module.exports = {
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
async viteFinal(config, { configType }) {
|
async viteFinal(config, { configType }) {
|
||||||
config.plugins = config.plugins || []
|
config.plugins = config.plugins || [];
|
||||||
// return the customized config
|
// return the customized config
|
||||||
if (configType === "PRODUCTION") {
|
if (configType === "PRODUCTION") {
|
||||||
// ignore @ts-ignore because it's not in the vite types yet
|
// ignore @ts-ignore because it's not in the vite types yet
|
||||||
|
@ -23,8 +23,8 @@ module.exports = {
|
||||||
turbosnap({
|
turbosnap({
|
||||||
rootDir: config.root || "",
|
rootDir: config.root || "",
|
||||||
}),
|
}),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return config
|
return config;
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import CssBaseline from "@mui/material/CssBaseline"
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles"
|
import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles";
|
||||||
import { withRouter } from "storybook-addon-react-router-v6"
|
import { withRouter } from "storybook-addon-react-router-v6";
|
||||||
import { HelmetProvider } from "react-helmet-async"
|
import { HelmetProvider } from "react-helmet-async";
|
||||||
import { dark } from "../src/theme"
|
import { dark } from "../src/theme";
|
||||||
import "../src/theme/globalFonts"
|
import "../src/theme/globalFonts";
|
||||||
import "../src/i18n"
|
import "../src/i18n";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
export const decorators = [
|
export const decorators = [
|
||||||
(Story) => (
|
(Story) => (
|
||||||
|
@ -22,16 +22,16 @@ export const decorators = [
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<Story />
|
<Story />
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
(Story) => {
|
(Story) => {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={new QueryClient()}>
|
<QueryClientProvider client={new QueryClient()}>
|
||||||
<Story />
|
<Story />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
export const parameters = {
|
export const parameters = {
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -44,4 +44,4 @@ export const parameters = {
|
||||||
date: /Date$/,
|
date: /Date$/,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
// Default port from the server
|
// Default port from the server
|
||||||
export const defaultPort = 3000
|
export const defaultPort = 3000;
|
||||||
export const prometheusPort = 2114
|
export const prometheusPort = 2114;
|
||||||
export const pprofPort = 6061
|
export const pprofPort = 6061;
|
||||||
|
|
||||||
// Credentials for the first user
|
// Credentials for the first user
|
||||||
export const username = "admin"
|
export const username = "admin";
|
||||||
export const password = "SomeSecurePassword!"
|
export const password = "SomeSecurePassword!";
|
||||||
export const email = "admin@coder.com"
|
export const email = "admin@coder.com";
|
||||||
|
|
||||||
export const gitAuth = {
|
export const gitAuth = {
|
||||||
deviceProvider: "device",
|
deviceProvider: "device",
|
||||||
|
@ -22,4 +22,4 @@ export const gitAuth = {
|
||||||
codePath: "/code",
|
codePath: "/code",
|
||||||
validatePath: "/validate",
|
validatePath: "/validate",
|
||||||
installationsPath: "/installations",
|
installationsPath: "/installations",
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { test, expect } from "@playwright/test"
|
import { test, expect } from "@playwright/test";
|
||||||
import * as constants from "./constants"
|
import * as constants from "./constants";
|
||||||
import { STORAGE_STATE } from "./playwright.config"
|
import { STORAGE_STATE } from "./playwright.config";
|
||||||
import { Language } from "../src/pages/CreateUserPage/CreateUserForm"
|
import { Language } from "../src/pages/CreateUserPage/CreateUserForm";
|
||||||
|
|
||||||
test("create first user", async ({ page }) => {
|
test("create first user", async ({ page }) => {
|
||||||
await page.goto("/", { waitUntil: "domcontentloaded" })
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
|
|
||||||
await page.getByLabel(Language.usernameLabel).fill(constants.username)
|
await page.getByLabel(Language.usernameLabel).fill(constants.username);
|
||||||
await page.getByLabel(Language.emailLabel).fill(constants.email)
|
await page.getByLabel(Language.emailLabel).fill(constants.email);
|
||||||
await page.getByLabel(Language.passwordLabel).fill(constants.password)
|
await page.getByLabel(Language.passwordLabel).fill(constants.password);
|
||||||
await page.getByTestId("trial").click()
|
await page.getByTestId("trial").click();
|
||||||
await page.getByTestId("create").click()
|
await page.getByTestId("create").click();
|
||||||
|
|
||||||
await expect(page).toHaveURL("/workspaces")
|
await expect(page).toHaveURL("/workspaces");
|
||||||
|
|
||||||
await page.context().storageState({ path: STORAGE_STATE })
|
await page.context().storageState({ path: STORAGE_STATE });
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { expect, Page } from "@playwright/test"
|
import { expect, Page } from "@playwright/test";
|
||||||
import { ChildProcess, exec, spawn } from "child_process"
|
import { ChildProcess, exec, spawn } from "child_process";
|
||||||
import { randomUUID } from "crypto"
|
import { randomUUID } from "crypto";
|
||||||
import path from "path"
|
import path from "path";
|
||||||
import express from "express"
|
import express from "express";
|
||||||
import { TarWriter } from "utils/tar"
|
import { TarWriter } from "utils/tar";
|
||||||
import {
|
import {
|
||||||
Agent,
|
Agent,
|
||||||
App,
|
App,
|
||||||
|
@ -14,13 +14,13 @@ import {
|
||||||
ApplyComplete,
|
ApplyComplete,
|
||||||
Resource,
|
Resource,
|
||||||
RichParameter,
|
RichParameter,
|
||||||
} from "./provisionerGenerated"
|
} from "./provisionerGenerated";
|
||||||
import { prometheusPort, pprofPort } from "./constants"
|
import { prometheusPort, pprofPort } from "./constants";
|
||||||
import { port } from "./playwright.config"
|
import { port } from "./playwright.config";
|
||||||
import * as ssh from "ssh2"
|
import * as ssh from "ssh2";
|
||||||
import { Duplex } from "stream"
|
import { Duplex } from "stream";
|
||||||
import { WorkspaceBuildParameter } from "api/typesGenerated"
|
import { WorkspaceBuildParameter } from "api/typesGenerated";
|
||||||
import axios from "axios"
|
import axios from "axios";
|
||||||
|
|
||||||
// createWorkspace creates a workspace for a template.
|
// createWorkspace creates a workspace for a template.
|
||||||
// It does not wait for it to be running, but it does navigate to the page.
|
// It does not wait for it to be running, but it does navigate to the page.
|
||||||
|
@ -32,25 +32,25 @@ export const createWorkspace = async (
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
await page.goto("/templates/" + templateName + "/workspace", {
|
await page.goto("/templates/" + templateName + "/workspace", {
|
||||||
waitUntil: "domcontentloaded",
|
waitUntil: "domcontentloaded",
|
||||||
})
|
});
|
||||||
await expect(page).toHaveURL("/templates/" + templateName + "/workspace")
|
await expect(page).toHaveURL("/templates/" + templateName + "/workspace");
|
||||||
|
|
||||||
const name = randomName()
|
const name = randomName();
|
||||||
await page.getByLabel("name").fill(name)
|
await page.getByLabel("name").fill(name);
|
||||||
|
|
||||||
await fillParameters(page, richParameters, buildParameters)
|
await fillParameters(page, richParameters, buildParameters);
|
||||||
await page.getByTestId("form-submit").click()
|
await page.getByTestId("form-submit").click();
|
||||||
|
|
||||||
await expect(page).toHaveURL("/@admin/" + name)
|
await expect(page).toHaveURL("/@admin/" + name);
|
||||||
|
|
||||||
await page.waitForSelector(
|
await page.waitForSelector(
|
||||||
"span[data-testid='build-status'] >> text=Running",
|
"span[data-testid='build-status'] >> text=Running",
|
||||||
{
|
{
|
||||||
state: "visible",
|
state: "visible",
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
return name
|
return name;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const verifyParameters = async (
|
export const verifyParameters = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
|
@ -60,56 +60,56 @@ export const verifyParameters = async (
|
||||||
) => {
|
) => {
|
||||||
await page.goto("/@admin/" + workspaceName + "/settings/parameters", {
|
await page.goto("/@admin/" + workspaceName + "/settings/parameters", {
|
||||||
waitUntil: "domcontentloaded",
|
waitUntil: "domcontentloaded",
|
||||||
})
|
});
|
||||||
await expect(page).toHaveURL(
|
await expect(page).toHaveURL(
|
||||||
"/@admin/" + workspaceName + "/settings/parameters",
|
"/@admin/" + workspaceName + "/settings/parameters",
|
||||||
)
|
);
|
||||||
|
|
||||||
for (const buildParameter of expectedBuildParameters) {
|
for (const buildParameter of expectedBuildParameters) {
|
||||||
const richParameter = richParameters.find(
|
const richParameter = richParameters.find(
|
||||||
(richParam) => richParam.name === buildParameter.name,
|
(richParam) => richParam.name === buildParameter.name,
|
||||||
)
|
);
|
||||||
if (!richParameter) {
|
if (!richParameter) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"build parameter is expected to be present in rich parameter schema",
|
"build parameter is expected to be present in rich parameter schema",
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parameterLabel = await page.waitForSelector(
|
const parameterLabel = await page.waitForSelector(
|
||||||
"[data-testid='parameter-field-" + richParameter.name + "']",
|
"[data-testid='parameter-field-" + richParameter.name + "']",
|
||||||
{ state: "visible" },
|
{ state: "visible" },
|
||||||
)
|
);
|
||||||
|
|
||||||
const muiDisabled = richParameter.mutable ? "" : ".Mui-disabled"
|
const muiDisabled = richParameter.mutable ? "" : ".Mui-disabled";
|
||||||
|
|
||||||
if (richParameter.type === "bool") {
|
if (richParameter.type === "bool") {
|
||||||
const parameterField = await parameterLabel.waitForSelector(
|
const parameterField = await parameterLabel.waitForSelector(
|
||||||
"[data-testid='parameter-field-bool'] .MuiRadio-root.Mui-checked" +
|
"[data-testid='parameter-field-bool'] .MuiRadio-root.Mui-checked" +
|
||||||
muiDisabled +
|
muiDisabled +
|
||||||
" input",
|
" input",
|
||||||
)
|
);
|
||||||
const value = await parameterField.inputValue()
|
const value = await parameterField.inputValue();
|
||||||
expect(value).toEqual(buildParameter.value)
|
expect(value).toEqual(buildParameter.value);
|
||||||
} else if (richParameter.options.length > 0) {
|
} else if (richParameter.options.length > 0) {
|
||||||
const parameterField = await parameterLabel.waitForSelector(
|
const parameterField = await parameterLabel.waitForSelector(
|
||||||
"[data-testid='parameter-field-options'] .MuiRadio-root.Mui-checked" +
|
"[data-testid='parameter-field-options'] .MuiRadio-root.Mui-checked" +
|
||||||
muiDisabled +
|
muiDisabled +
|
||||||
" input",
|
" input",
|
||||||
)
|
);
|
||||||
const value = await parameterField.inputValue()
|
const value = await parameterField.inputValue();
|
||||||
expect(value).toEqual(buildParameter.value)
|
expect(value).toEqual(buildParameter.value);
|
||||||
} else if (richParameter.type === "list(string)") {
|
} else if (richParameter.type === "list(string)") {
|
||||||
throw new Error("not implemented yet") // FIXME
|
throw new Error("not implemented yet"); // FIXME
|
||||||
} else {
|
} else {
|
||||||
// text or number
|
// text or number
|
||||||
const parameterField = await parameterLabel.waitForSelector(
|
const parameterField = await parameterLabel.waitForSelector(
|
||||||
"[data-testid='parameter-field-text'] input" + muiDisabled,
|
"[data-testid='parameter-field-text'] input" + muiDisabled,
|
||||||
)
|
);
|
||||||
const value = await parameterField.inputValue()
|
const value = await parameterField.inputValue();
|
||||||
expect(value).toEqual(buildParameter.value)
|
expect(value).toEqual(buildParameter.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// createTemplate navigates to the /templates/new page and uploads a template
|
// createTemplate navigates to the /templates/new page and uploads a template
|
||||||
// with the resources provided in the responses argument.
|
// with the resources provided in the responses argument.
|
||||||
|
@ -120,24 +120,24 @@ export const createTemplate = async (
|
||||||
// Required to have templates submit their provisioner type as echo!
|
// Required to have templates submit their provisioner type as echo!
|
||||||
await page.addInitScript({
|
await page.addInitScript({
|
||||||
content: "window.playwright = true",
|
content: "window.playwright = true",
|
||||||
})
|
});
|
||||||
|
|
||||||
await page.goto("/templates/new", { waitUntil: "domcontentloaded" })
|
await page.goto("/templates/new", { waitUntil: "domcontentloaded" });
|
||||||
await expect(page).toHaveURL("/templates/new")
|
await expect(page).toHaveURL("/templates/new");
|
||||||
|
|
||||||
await page.getByTestId("file-upload").setInputFiles({
|
await page.getByTestId("file-upload").setInputFiles({
|
||||||
buffer: await createTemplateVersionTar(responses),
|
buffer: await createTemplateVersionTar(responses),
|
||||||
mimeType: "application/x-tar",
|
mimeType: "application/x-tar",
|
||||||
name: "template.tar",
|
name: "template.tar",
|
||||||
})
|
});
|
||||||
const name = randomName()
|
const name = randomName();
|
||||||
await page.getByLabel("Name *").fill(name)
|
await page.getByLabel("Name *").fill(name);
|
||||||
await page.getByTestId("form-submit").click()
|
await page.getByTestId("form-submit").click();
|
||||||
await expect(page).toHaveURL("/templates/" + name, {
|
await expect(page).toHaveURL("/templates/" + name, {
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
})
|
});
|
||||||
return name
|
return name;
|
||||||
}
|
};
|
||||||
|
|
||||||
// sshIntoWorkspace spawns a Coder SSH process and a client connected to it.
|
// sshIntoWorkspace spawns a Coder SSH process and a client connected to it.
|
||||||
export const sshIntoWorkspace = async (
|
export const sshIntoWorkspace = async (
|
||||||
|
@ -147,9 +147,9 @@ export const sshIntoWorkspace = async (
|
||||||
binaryArgs: string[] = [],
|
binaryArgs: string[] = [],
|
||||||
): Promise<ssh.Client> => {
|
): Promise<ssh.Client> => {
|
||||||
if (binaryPath === "go") {
|
if (binaryPath === "go") {
|
||||||
binaryArgs = ["run", coderMainPath()]
|
binaryArgs = ["run", coderMainPath()];
|
||||||
}
|
}
|
||||||
const sessionToken = await findSessionToken(page)
|
const sessionToken = await findSessionToken(page);
|
||||||
return new Promise<ssh.Client>((resolve, reject) => {
|
return new Promise<ssh.Client>((resolve, reject) => {
|
||||||
const cp = spawn(binaryPath, [...binaryArgs, "ssh", "--stdio", workspace], {
|
const cp = spawn(binaryPath, [...binaryArgs, "ssh", "--stdio", workspace], {
|
||||||
env: {
|
env: {
|
||||||
|
@ -157,49 +157,49 @@ export const sshIntoWorkspace = async (
|
||||||
CODER_SESSION_TOKEN: sessionToken,
|
CODER_SESSION_TOKEN: sessionToken,
|
||||||
CODER_URL: "http://localhost:3000",
|
CODER_URL: "http://localhost:3000",
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
cp.on("error", (err) => reject(err))
|
cp.on("error", (err) => reject(err));
|
||||||
const proxyStream = new Duplex({
|
const proxyStream = new Duplex({
|
||||||
read: (size) => {
|
read: (size) => {
|
||||||
return cp.stdout.read(Math.min(size, cp.stdout.readableLength))
|
return cp.stdout.read(Math.min(size, cp.stdout.readableLength));
|
||||||
},
|
},
|
||||||
write: cp.stdin.write.bind(cp.stdin),
|
write: cp.stdin.write.bind(cp.stdin),
|
||||||
})
|
});
|
||||||
// eslint-disable-next-line no-console -- Helpful for debugging
|
// eslint-disable-next-line no-console -- Helpful for debugging
|
||||||
cp.stderr.on("data", (data) => console.log(data.toString()))
|
cp.stderr.on("data", (data) => console.log(data.toString()));
|
||||||
cp.stdout.on("readable", (...args) => {
|
cp.stdout.on("readable", (...args) => {
|
||||||
proxyStream.emit("readable", ...args)
|
proxyStream.emit("readable", ...args);
|
||||||
if (cp.stdout.readableLength > 0) {
|
if (cp.stdout.readableLength > 0) {
|
||||||
proxyStream.emit("data", cp.stdout.read())
|
proxyStream.emit("data", cp.stdout.read());
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
const client = new ssh.Client()
|
const client = new ssh.Client();
|
||||||
client.connect({
|
client.connect({
|
||||||
sock: proxyStream,
|
sock: proxyStream,
|
||||||
username: "coder",
|
username: "coder",
|
||||||
})
|
});
|
||||||
client.on("error", (err) => reject(err))
|
client.on("error", (err) => reject(err));
|
||||||
client.on("ready", () => {
|
client.on("ready", () => {
|
||||||
resolve(client)
|
resolve(client);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const stopWorkspace = async (page: Page, workspaceName: string) => {
|
export const stopWorkspace = async (page: Page, workspaceName: string) => {
|
||||||
await page.goto("/@admin/" + workspaceName, {
|
await page.goto("/@admin/" + workspaceName, {
|
||||||
waitUntil: "domcontentloaded",
|
waitUntil: "domcontentloaded",
|
||||||
})
|
});
|
||||||
await expect(page).toHaveURL("/@admin/" + workspaceName)
|
await expect(page).toHaveURL("/@admin/" + workspaceName);
|
||||||
|
|
||||||
await page.getByTestId("workspace-stop-button").click()
|
await page.getByTestId("workspace-stop-button").click();
|
||||||
|
|
||||||
await page.waitForSelector(
|
await page.waitForSelector(
|
||||||
"span[data-testid='build-status'] >> text=Stopped",
|
"span[data-testid='build-status'] >> text=Stopped",
|
||||||
{
|
{
|
||||||
state: "visible",
|
state: "visible",
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const buildWorkspaceWithParameters = async (
|
export const buildWorkspaceWithParameters = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
|
@ -210,15 +210,15 @@ export const buildWorkspaceWithParameters = async (
|
||||||
) => {
|
) => {
|
||||||
await page.goto("/@admin/" + workspaceName, {
|
await page.goto("/@admin/" + workspaceName, {
|
||||||
waitUntil: "domcontentloaded",
|
waitUntil: "domcontentloaded",
|
||||||
})
|
});
|
||||||
await expect(page).toHaveURL("/@admin/" + workspaceName)
|
await expect(page).toHaveURL("/@admin/" + workspaceName);
|
||||||
|
|
||||||
await page.getByTestId("build-parameters-button").click()
|
await page.getByTestId("build-parameters-button").click();
|
||||||
|
|
||||||
await fillParameters(page, richParameters, buildParameters)
|
await fillParameters(page, richParameters, buildParameters);
|
||||||
await page.getByTestId("build-parameters-submit").click()
|
await page.getByTestId("build-parameters-submit").click();
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
await page.getByTestId("confirm-button").click()
|
await page.getByTestId("confirm-button").click();
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.waitForSelector(
|
await page.waitForSelector(
|
||||||
|
@ -226,8 +226,8 @@ export const buildWorkspaceWithParameters = async (
|
||||||
{
|
{
|
||||||
state: "visible",
|
state: "visible",
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
// startAgent runs the coder agent with the provided token.
|
// startAgent runs the coder agent with the provided token.
|
||||||
// It awaits the agent to be ready before returning.
|
// It awaits the agent to be ready before returning.
|
||||||
|
@ -235,8 +235,8 @@ export const startAgent = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
token: string,
|
token: string,
|
||||||
): Promise<ChildProcess> => {
|
): Promise<ChildProcess> => {
|
||||||
return startAgentWithCommand(page, token, "go", "run", coderMainPath())
|
return startAgentWithCommand(page, token, "go", "run", coderMainPath());
|
||||||
}
|
};
|
||||||
|
|
||||||
// downloadCoderVersion downloads the version provided into a temporary dir and
|
// downloadCoderVersion downloads the version provided into a temporary dir and
|
||||||
// caches it so subsequent calls are fast.
|
// caches it so subsequent calls are fast.
|
||||||
|
@ -244,23 +244,23 @@ export const downloadCoderVersion = async (
|
||||||
version: string,
|
version: string,
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
if (version.startsWith("v")) {
|
if (version.startsWith("v")) {
|
||||||
version = version.slice(1)
|
version = version.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const binaryName = "coder-e2e-" + version
|
const binaryName = "coder-e2e-" + version;
|
||||||
const tempDir = "/tmp/coder-e2e-cache"
|
const tempDir = "/tmp/coder-e2e-cache";
|
||||||
// The install script adds `./bin` automatically to the path :shrug:
|
// The install script adds `./bin` automatically to the path :shrug:
|
||||||
const binaryPath = path.join(tempDir, "bin", binaryName)
|
const binaryPath = path.join(tempDir, "bin", binaryName);
|
||||||
|
|
||||||
const exists = await new Promise<boolean>((resolve) => {
|
const exists = await new Promise<boolean>((resolve) => {
|
||||||
const cp = spawn(binaryPath, ["version"])
|
const cp = spawn(binaryPath, ["version"]);
|
||||||
cp.on("close", (code) => {
|
cp.on("close", (code) => {
|
||||||
resolve(code === 0)
|
resolve(code === 0);
|
||||||
})
|
});
|
||||||
cp.on("error", () => resolve(false))
|
cp.on("error", () => resolve(false));
|
||||||
})
|
});
|
||||||
if (exists) {
|
if (exists) {
|
||||||
return binaryPath
|
return binaryPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs our public install script using our options to
|
// Runs our public install script using our options to
|
||||||
|
@ -294,19 +294,19 @@ export const downloadCoderVersion = async (
|
||||||
XDG_CACHE_HOME: "/tmp/coder-e2e-cache",
|
XDG_CACHE_HOME: "/tmp/coder-e2e-cache",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
// eslint-disable-next-line no-console -- Needed for debugging
|
// eslint-disable-next-line no-console -- Needed for debugging
|
||||||
cp.stderr.on("data", (data) => console.log(data.toString()))
|
cp.stderr.on("data", (data) => console.log(data.toString()));
|
||||||
cp.on("close", (code) => {
|
cp.on("close", (code) => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve()
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
reject(new Error("curl failed with code " + code))
|
reject(new Error("curl failed with code " + code));
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
return binaryPath
|
return binaryPath;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const startAgentWithCommand = async (
|
export const startAgentWithCommand = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
|
@ -322,23 +322,23 @@ export const startAgentWithCommand = async (
|
||||||
CODER_AGENT_PPROF_ADDRESS: "127.0.0.1:" + pprofPort,
|
CODER_AGENT_PPROF_ADDRESS: "127.0.0.1:" + pprofPort,
|
||||||
CODER_AGENT_PROMETHEUS_ADDRESS: "127.0.0.1:" + prometheusPort,
|
CODER_AGENT_PROMETHEUS_ADDRESS: "127.0.0.1:" + prometheusPort,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
cp.stdout.on("data", (data: Buffer) => {
|
cp.stdout.on("data", (data: Buffer) => {
|
||||||
// eslint-disable-next-line no-console -- Log agent activity
|
// eslint-disable-next-line no-console -- Log agent activity
|
||||||
console.log(
|
console.log(
|
||||||
`[agent] [stdout] [onData] ${data.toString().replace(/\n$/g, "")}`,
|
`[agent] [stdout] [onData] ${data.toString().replace(/\n$/g, "")}`,
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
cp.stderr.on("data", (data: Buffer) => {
|
cp.stderr.on("data", (data: Buffer) => {
|
||||||
// eslint-disable-next-line no-console -- Log agent activity
|
// eslint-disable-next-line no-console -- Log agent activity
|
||||||
console.log(
|
console.log(
|
||||||
`[agent] [stderr] [onData] ${data.toString().replace(/\n$/g, "")}`,
|
`[agent] [stderr] [onData] ${data.toString().replace(/\n$/g, "")}`,
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
await page.getByTestId("agent-status-ready").waitFor({ state: "visible" })
|
await page.getByTestId("agent-status-ready").waitFor({ state: "visible" });
|
||||||
return cp
|
return cp;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const stopAgent = async (cp: ChildProcess, goRun: boolean = true) => {
|
export const stopAgent = async (cp: ChildProcess, goRun: boolean = true) => {
|
||||||
// When the web server is started with `go run`, it spawns a child process with coder server.
|
// When the web server is started with `go run`, it spawns a child process with coder server.
|
||||||
|
@ -346,31 +346,31 @@ export const stopAgent = async (cp: ChildProcess, goRun: boolean = true) => {
|
||||||
// The command `kill` is used to terminate a web server started as a standalone binary.
|
// The command `kill` is used to terminate a web server started as a standalone binary.
|
||||||
exec(goRun ? `pkill -P ${cp.pid}` : `kill ${cp.pid}`, (error) => {
|
exec(goRun ? `pkill -P ${cp.pid}` : `kill ${cp.pid}`, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new Error(`exec error: ${JSON.stringify(error)}`)
|
throw new Error(`exec error: ${JSON.stringify(error)}`);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
await waitUntilUrlIsNotResponding("http://localhost:" + prometheusPort)
|
await waitUntilUrlIsNotResponding("http://localhost:" + prometheusPort);
|
||||||
}
|
};
|
||||||
|
|
||||||
const waitUntilUrlIsNotResponding = async (url: string) => {
|
const waitUntilUrlIsNotResponding = async (url: string) => {
|
||||||
const maxRetries = 30
|
const maxRetries = 30;
|
||||||
const retryIntervalMs = 1000
|
const retryIntervalMs = 1000;
|
||||||
let retries = 0
|
let retries = 0;
|
||||||
|
|
||||||
while (retries < maxRetries) {
|
while (retries < maxRetries) {
|
||||||
try {
|
try {
|
||||||
await axios.get(url)
|
await axios.get(url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
retries++
|
retries++;
|
||||||
await new Promise((resolve) => setTimeout(resolve, retryIntervalMs))
|
await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`URL ${url} is still responding after ${maxRetries * retryIntervalMs}ms`,
|
`URL ${url} is still responding after ${maxRetries * retryIntervalMs}ms`,
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const coderMainPath = (): string => {
|
const coderMainPath = (): string => {
|
||||||
return path.join(
|
return path.join(
|
||||||
|
@ -381,8 +381,8 @@ const coderMainPath = (): string => {
|
||||||
"cmd",
|
"cmd",
|
||||||
"coder",
|
"coder",
|
||||||
"main.go",
|
"main.go",
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Allows users to more easily define properties they want for agents and resources!
|
// Allows users to more easily define properties they want for agents and resources!
|
||||||
type RecursivePartial<T> = {
|
type RecursivePartial<T> = {
|
||||||
|
@ -390,16 +390,16 @@ type RecursivePartial<T> = {
|
||||||
? RecursivePartial<U>[]
|
? RecursivePartial<U>[]
|
||||||
: T[P] extends object | undefined
|
: T[P] extends object | undefined
|
||||||
? RecursivePartial<T[P]>
|
? RecursivePartial<T[P]>
|
||||||
: T[P]
|
: T[P];
|
||||||
}
|
};
|
||||||
|
|
||||||
interface EchoProvisionerResponses {
|
interface EchoProvisionerResponses {
|
||||||
// parse is for observing any Terraform variables
|
// parse is for observing any Terraform variables
|
||||||
parse?: RecursivePartial<Response>[]
|
parse?: RecursivePartial<Response>[];
|
||||||
// plan occurs when the template is imported
|
// plan occurs when the template is imported
|
||||||
plan?: RecursivePartial<Response>[]
|
plan?: RecursivePartial<Response>[];
|
||||||
// apply occurs when the workspace is built
|
// apply occurs when the workspace is built
|
||||||
apply?: RecursivePartial<Response>[]
|
apply?: RecursivePartial<Response>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// createTemplateVersionTar consumes a series of echo provisioner protobufs and
|
// createTemplateVersionTar consumes a series of echo provisioner protobufs and
|
||||||
|
@ -408,26 +408,26 @@ const createTemplateVersionTar = async (
|
||||||
responses?: EchoProvisionerResponses,
|
responses?: EchoProvisionerResponses,
|
||||||
): Promise<Buffer> => {
|
): Promise<Buffer> => {
|
||||||
if (!responses) {
|
if (!responses) {
|
||||||
responses = {}
|
responses = {};
|
||||||
}
|
}
|
||||||
if (!responses.parse) {
|
if (!responses.parse) {
|
||||||
responses.parse = [
|
responses.parse = [
|
||||||
{
|
{
|
||||||
parse: {},
|
parse: {},
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
}
|
}
|
||||||
if (!responses.apply) {
|
if (!responses.apply) {
|
||||||
responses.apply = [
|
responses.apply = [
|
||||||
{
|
{
|
||||||
apply: {},
|
apply: {},
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
}
|
}
|
||||||
if (!responses.plan) {
|
if (!responses.plan) {
|
||||||
responses.plan = responses.apply.map((response) => {
|
responses.plan = responses.apply.map((response) => {
|
||||||
if (response.log) {
|
if (response.log) {
|
||||||
return response
|
return response;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
plan: {
|
plan: {
|
||||||
|
@ -436,23 +436,23 @@ const createTemplateVersionTar = async (
|
||||||
parameters: response.apply?.parameters ?? [],
|
parameters: response.apply?.parameters ?? [],
|
||||||
gitAuthProviders: response.apply?.gitAuthProviders ?? [],
|
gitAuthProviders: response.apply?.gitAuthProviders ?? [],
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const tar = new TarWriter()
|
const tar = new TarWriter();
|
||||||
responses.parse.forEach((response, index) => {
|
responses.parse.forEach((response, index) => {
|
||||||
response.parse = {
|
response.parse = {
|
||||||
templateVariables: [],
|
templateVariables: [],
|
||||||
error: "",
|
error: "",
|
||||||
readme: new Uint8Array(),
|
readme: new Uint8Array(),
|
||||||
...response.parse,
|
...response.parse,
|
||||||
} as ParseComplete
|
} as ParseComplete;
|
||||||
tar.addFile(
|
tar.addFile(
|
||||||
`${index}.parse.protobuf`,
|
`${index}.parse.protobuf`,
|
||||||
Response.encode(response as Response).finish(),
|
Response.encode(response as Response).finish(),
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
const fillResource = (resource: RecursivePartial<Resource>) => {
|
const fillResource = (resource: RecursivePartial<Resource>) => {
|
||||||
if (resource.agents) {
|
if (resource.agents) {
|
||||||
|
@ -470,8 +470,8 @@ const createTemplateVersionTar = async (
|
||||||
subdomain: false,
|
subdomain: false,
|
||||||
url: "",
|
url: "",
|
||||||
...app,
|
...app,
|
||||||
} as App
|
} as App;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
apps: [],
|
apps: [],
|
||||||
|
@ -492,9 +492,9 @@ const createTemplateVersionTar = async (
|
||||||
troubleshootingUrl: "",
|
troubleshootingUrl: "",
|
||||||
token: randomUUID(),
|
token: randomUUID(),
|
||||||
...agent,
|
...agent,
|
||||||
} as Agent
|
} as Agent;
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
agents: [],
|
agents: [],
|
||||||
|
@ -506,8 +506,8 @@ const createTemplateVersionTar = async (
|
||||||
name: "dev",
|
name: "dev",
|
||||||
type: "echo",
|
type: "echo",
|
||||||
...resource,
|
...resource,
|
||||||
} as Resource
|
} as Resource;
|
||||||
}
|
};
|
||||||
|
|
||||||
responses.apply.forEach((response, index) => {
|
responses.apply.forEach((response, index) => {
|
||||||
response.apply = {
|
response.apply = {
|
||||||
|
@ -517,14 +517,14 @@ const createTemplateVersionTar = async (
|
||||||
parameters: [],
|
parameters: [],
|
||||||
gitAuthProviders: [],
|
gitAuthProviders: [],
|
||||||
...response.apply,
|
...response.apply,
|
||||||
} as ApplyComplete
|
} as ApplyComplete;
|
||||||
response.apply.resources = response.apply.resources?.map(fillResource)
|
response.apply.resources = response.apply.resources?.map(fillResource);
|
||||||
|
|
||||||
tar.addFile(
|
tar.addFile(
|
||||||
`${index}.apply.protobuf`,
|
`${index}.apply.protobuf`,
|
||||||
Response.encode(response as Response).finish(),
|
Response.encode(response as Response).finish(),
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
responses.plan.forEach((response, index) => {
|
responses.plan.forEach((response, index) => {
|
||||||
response.plan = {
|
response.plan = {
|
||||||
error: "",
|
error: "",
|
||||||
|
@ -532,63 +532,63 @@ const createTemplateVersionTar = async (
|
||||||
parameters: [],
|
parameters: [],
|
||||||
gitAuthProviders: [],
|
gitAuthProviders: [],
|
||||||
...response.plan,
|
...response.plan,
|
||||||
} as PlanComplete
|
} as PlanComplete;
|
||||||
response.plan.resources = response.plan.resources?.map(fillResource)
|
response.plan.resources = response.plan.resources?.map(fillResource);
|
||||||
|
|
||||||
tar.addFile(
|
tar.addFile(
|
||||||
`${index}.plan.protobuf`,
|
`${index}.plan.protobuf`,
|
||||||
Response.encode(response as Response).finish(),
|
Response.encode(response as Response).finish(),
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
const tarFile = await tar.write()
|
const tarFile = await tar.write();
|
||||||
return Buffer.from(
|
return Buffer.from(
|
||||||
tarFile instanceof Blob ? await tarFile.arrayBuffer() : tarFile,
|
tarFile instanceof Blob ? await tarFile.arrayBuffer() : tarFile,
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const randomName = () => {
|
const randomName = () => {
|
||||||
return randomUUID().slice(0, 8)
|
return randomUUID().slice(0, 8);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Awaiter is a helper that allows you to wait for a callback to be called.
|
// Awaiter is a helper that allows you to wait for a callback to be called.
|
||||||
// It is useful for waiting for events to occur.
|
// It is useful for waiting for events to occur.
|
||||||
export class Awaiter {
|
export class Awaiter {
|
||||||
private promise: Promise<void>
|
private promise: Promise<void>;
|
||||||
private callback?: () => void
|
private callback?: () => void;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.promise = new Promise((r) => (this.callback = r))
|
this.promise = new Promise((r) => (this.callback = r));
|
||||||
}
|
}
|
||||||
|
|
||||||
public done(): void {
|
public done(): void {
|
||||||
if (this.callback) {
|
if (this.callback) {
|
||||||
this.callback()
|
this.callback();
|
||||||
} else {
|
} else {
|
||||||
this.promise = Promise.resolve()
|
this.promise = Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public wait(): Promise<void> {
|
public wait(): Promise<void> {
|
||||||
return this.promise
|
return this.promise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createServer = async (
|
export const createServer = async (
|
||||||
port: number,
|
port: number,
|
||||||
): Promise<ReturnType<typeof express>> => {
|
): Promise<ReturnType<typeof express>> => {
|
||||||
const e = express()
|
const e = express();
|
||||||
await new Promise<void>((r) => e.listen(port, r))
|
await new Promise<void>((r) => e.listen(port, r));
|
||||||
return e
|
return e;
|
||||||
}
|
};
|
||||||
|
|
||||||
const findSessionToken = async (page: Page): Promise<string> => {
|
const findSessionToken = async (page: Page): Promise<string> => {
|
||||||
const cookies = await page.context().cookies()
|
const cookies = await page.context().cookies();
|
||||||
const sessionCookie = cookies.find((c) => c.name === "coder_session_token")
|
const sessionCookie = cookies.find((c) => c.name === "coder_session_token");
|
||||||
if (!sessionCookie) {
|
if (!sessionCookie) {
|
||||||
throw new Error("session token not found")
|
throw new Error("session token not found");
|
||||||
}
|
}
|
||||||
return sessionCookie.value
|
return sessionCookie.value;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const echoResponsesWithParameters = (
|
export const echoResponsesWithParameters = (
|
||||||
richParameters: RichParameter[],
|
richParameters: RichParameter[],
|
||||||
|
@ -617,8 +617,8 @@ export const echoResponsesWithParameters = (
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const fillParameters = async (
|
export const fillParameters = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
|
@ -628,52 +628,52 @@ export const fillParameters = async (
|
||||||
for (const buildParameter of buildParameters) {
|
for (const buildParameter of buildParameters) {
|
||||||
const richParameter = richParameters.find(
|
const richParameter = richParameters.find(
|
||||||
(richParam) => richParam.name === buildParameter.name,
|
(richParam) => richParam.name === buildParameter.name,
|
||||||
)
|
);
|
||||||
if (!richParameter) {
|
if (!richParameter) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"build parameter is expected to be present in rich parameter schema",
|
"build parameter is expected to be present in rich parameter schema",
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parameterLabel = await page.waitForSelector(
|
const parameterLabel = await page.waitForSelector(
|
||||||
"[data-testid='parameter-field-" + richParameter.name + "']",
|
"[data-testid='parameter-field-" + richParameter.name + "']",
|
||||||
{ state: "visible" },
|
{ state: "visible" },
|
||||||
)
|
);
|
||||||
|
|
||||||
if (richParameter.type === "bool") {
|
if (richParameter.type === "bool") {
|
||||||
const parameterField = await parameterLabel.waitForSelector(
|
const parameterField = await parameterLabel.waitForSelector(
|
||||||
"[data-testid='parameter-field-bool'] .MuiRadio-root input[value='" +
|
"[data-testid='parameter-field-bool'] .MuiRadio-root input[value='" +
|
||||||
buildParameter.value +
|
buildParameter.value +
|
||||||
"']",
|
"']",
|
||||||
)
|
);
|
||||||
await parameterField.check()
|
await parameterField.check();
|
||||||
} else if (richParameter.options.length > 0) {
|
} else if (richParameter.options.length > 0) {
|
||||||
const parameterField = await parameterLabel.waitForSelector(
|
const parameterField = await parameterLabel.waitForSelector(
|
||||||
"[data-testid='parameter-field-options'] .MuiRadio-root input[value='" +
|
"[data-testid='parameter-field-options'] .MuiRadio-root input[value='" +
|
||||||
buildParameter.value +
|
buildParameter.value +
|
||||||
"']",
|
"']",
|
||||||
)
|
);
|
||||||
await parameterField.check()
|
await parameterField.check();
|
||||||
} else if (richParameter.type === "list(string)") {
|
} else if (richParameter.type === "list(string)") {
|
||||||
throw new Error("not implemented yet") // FIXME
|
throw new Error("not implemented yet"); // FIXME
|
||||||
} else {
|
} else {
|
||||||
// text or number
|
// text or number
|
||||||
const parameterField = await parameterLabel.waitForSelector(
|
const parameterField = await parameterLabel.waitForSelector(
|
||||||
"[data-testid='parameter-field-text'] input",
|
"[data-testid='parameter-field-text'] input",
|
||||||
)
|
);
|
||||||
await parameterField.fill(buildParameter.value)
|
await parameterField.fill(buildParameter.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const updateTemplate = async (
|
export const updateTemplate = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
templateName: string,
|
templateName: string,
|
||||||
responses?: EchoProvisionerResponses,
|
responses?: EchoProvisionerResponses,
|
||||||
) => {
|
) => {
|
||||||
const tarball = await createTemplateVersionTar(responses)
|
const tarball = await createTemplateVersionTar(responses);
|
||||||
|
|
||||||
const sessionToken = await findSessionToken(page)
|
const sessionToken = await findSessionToken(page);
|
||||||
const child = spawn(
|
const child = spawn(
|
||||||
"go",
|
"go",
|
||||||
[
|
[
|
||||||
|
@ -695,23 +695,23 @@ export const updateTemplate = async (
|
||||||
CODER_URL: "http://localhost:3000",
|
CODER_URL: "http://localhost:3000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
const uploaded = new Awaiter()
|
const uploaded = new Awaiter();
|
||||||
child.on("exit", (code) => {
|
child.on("exit", (code) => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
uploaded.done()
|
uploaded.done();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`coder templates push failed with code ${code}`)
|
throw new Error(`coder templates push failed with code ${code}`);
|
||||||
})
|
});
|
||||||
|
|
||||||
child.stdin.write(tarball)
|
child.stdin.write(tarball);
|
||||||
child.stdin.end()
|
child.stdin.end();
|
||||||
|
|
||||||
await uploaded.wait()
|
await uploaded.wait();
|
||||||
}
|
};
|
||||||
|
|
||||||
export const updateWorkspace = async (
|
export const updateWorkspace = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
|
@ -721,22 +721,22 @@ export const updateWorkspace = async (
|
||||||
) => {
|
) => {
|
||||||
await page.goto("/@admin/" + workspaceName, {
|
await page.goto("/@admin/" + workspaceName, {
|
||||||
waitUntil: "domcontentloaded",
|
waitUntil: "domcontentloaded",
|
||||||
})
|
});
|
||||||
await expect(page).toHaveURL("/@admin/" + workspaceName)
|
await expect(page).toHaveURL("/@admin/" + workspaceName);
|
||||||
|
|
||||||
await page.getByTestId("workspace-update-button").click()
|
await page.getByTestId("workspace-update-button").click();
|
||||||
await page.getByTestId("confirm-button").click()
|
await page.getByTestId("confirm-button").click();
|
||||||
|
|
||||||
await fillParameters(page, richParameters, buildParameters)
|
await fillParameters(page, richParameters, buildParameters);
|
||||||
await page.getByTestId("form-submit").click()
|
await page.getByTestId("form-submit").click();
|
||||||
|
|
||||||
await page.waitForSelector(
|
await page.waitForSelector(
|
||||||
"span[data-testid='build-status'] >> text=Running",
|
"span[data-testid='build-status'] >> text=Running",
|
||||||
{
|
{
|
||||||
state: "visible",
|
state: "visible",
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const updateWorkspaceParameters = async (
|
export const updateWorkspaceParameters = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
|
@ -746,18 +746,18 @@ export const updateWorkspaceParameters = async (
|
||||||
) => {
|
) => {
|
||||||
await page.goto("/@admin/" + workspaceName + "/settings/parameters", {
|
await page.goto("/@admin/" + workspaceName + "/settings/parameters", {
|
||||||
waitUntil: "domcontentloaded",
|
waitUntil: "domcontentloaded",
|
||||||
})
|
});
|
||||||
await expect(page).toHaveURL(
|
await expect(page).toHaveURL(
|
||||||
"/@admin/" + workspaceName + "/settings/parameters",
|
"/@admin/" + workspaceName + "/settings/parameters",
|
||||||
)
|
);
|
||||||
|
|
||||||
await fillParameters(page, richParameters, buildParameters)
|
await fillParameters(page, richParameters, buildParameters);
|
||||||
await page.getByTestId("form-submit").click()
|
await page.getByTestId("form-submit").click();
|
||||||
|
|
||||||
await page.waitForSelector(
|
await page.waitForSelector(
|
||||||
"span[data-testid='build-status'] >> text=Running",
|
"span[data-testid='build-status'] >> text=Running",
|
||||||
{
|
{
|
||||||
state: "visible",
|
state: "visible",
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { Page } from "@playwright/test"
|
import { Page } from "@playwright/test";
|
||||||
|
|
||||||
export const beforeCoderTest = async (page: Page) => {
|
export const beforeCoderTest = async (page: Page) => {
|
||||||
// eslint-disable-next-line no-console -- Show everything that was printed with console.log()
|
// eslint-disable-next-line no-console -- Show everything that was printed with console.log()
|
||||||
page.on("console", (msg) => console.log("[onConsole] " + msg.text()))
|
page.on("console", (msg) => console.log("[onConsole] " + msg.text()));
|
||||||
|
|
||||||
page.on("request", (request) => {
|
page.on("request", (request) => {
|
||||||
if (!isApiCall(request.url())) {
|
if (!isApiCall(request.url())) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-console -- Log HTTP requests for debugging purposes
|
// eslint-disable-next-line no-console -- Log HTTP requests for debugging purposes
|
||||||
|
@ -14,40 +14,40 @@ export const beforeCoderTest = async (page: Page) => {
|
||||||
`[onRequest] method=${request.method()} url=${request.url()} postData=${
|
`[onRequest] method=${request.method()} url=${request.url()} postData=${
|
||||||
request.postData() ? request.postData() : ""
|
request.postData() ? request.postData() : ""
|
||||||
}`,
|
}`,
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
page.on("response", async (response) => {
|
page.on("response", async (response) => {
|
||||||
if (!isApiCall(response.url())) {
|
if (!isApiCall(response.url())) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldLogResponse =
|
const shouldLogResponse =
|
||||||
!response.url().endsWith("/api/v2/deployment/config") &&
|
!response.url().endsWith("/api/v2/deployment/config") &&
|
||||||
!response.url().endsWith("/api/v2/debug/health")
|
!response.url().endsWith("/api/v2/debug/health");
|
||||||
|
|
||||||
let responseText = ""
|
let responseText = "";
|
||||||
try {
|
try {
|
||||||
if (shouldLogResponse) {
|
if (shouldLogResponse) {
|
||||||
const buffer = await response.body()
|
const buffer = await response.body();
|
||||||
responseText = buffer.toString("utf-8")
|
responseText = buffer.toString("utf-8");
|
||||||
responseText = responseText.replace(/\n$/g, "")
|
responseText = responseText.replace(/\n$/g, "");
|
||||||
} else {
|
} else {
|
||||||
responseText = "skipped..."
|
responseText = "skipped...";
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
responseText = "not_available"
|
responseText = "not_available";
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-console -- Log HTTP requests for debugging purposes
|
// eslint-disable-next-line no-console -- Log HTTP requests for debugging purposes
|
||||||
console.log(
|
console.log(
|
||||||
`[onResponse] url=${response.url()} status=${response.status()} body=${responseText}`,
|
`[onResponse] url=${response.url()} status=${response.status()} body=${responseText}`,
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const isApiCall = (urlString: string): boolean => {
|
const isApiCall = (urlString: string): boolean => {
|
||||||
const url = new URL(urlString)
|
const url = new URL(urlString);
|
||||||
const apiPath = "/api/v2"
|
const apiPath = "/api/v2";
|
||||||
|
|
||||||
return url.pathname.startsWith(apiPath)
|
return url.pathname.startsWith(apiPath);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { RichParameter } from "./provisionerGenerated"
|
import { RichParameter } from "./provisionerGenerated";
|
||||||
|
|
||||||
// Rich parameters
|
// Rich parameters
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ const emptyParameter: RichParameter = {
|
||||||
displayName: "",
|
displayName: "",
|
||||||
order: 0,
|
order: 0,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
// firstParameter is mutable string with a default value (parameter value not required).
|
// firstParameter is mutable string with a default value (parameter value not required).
|
||||||
export const firstParameter: RichParameter = {
|
export const firstParameter: RichParameter = {
|
||||||
|
@ -33,7 +33,7 @@ export const firstParameter: RichParameter = {
|
||||||
defaultValue: "123",
|
defaultValue: "123",
|
||||||
mutable: true,
|
mutable: true,
|
||||||
order: 1,
|
order: 1,
|
||||||
}
|
};
|
||||||
|
|
||||||
// secondParameter is immutable string with a default value (parameter value not required).
|
// secondParameter is immutable string with a default value (parameter value not required).
|
||||||
export const secondParameter: RichParameter = {
|
export const secondParameter: RichParameter = {
|
||||||
|
@ -45,7 +45,7 @@ export const secondParameter: RichParameter = {
|
||||||
description: "This is second parameter.",
|
description: "This is second parameter.",
|
||||||
defaultValue: "abc",
|
defaultValue: "abc",
|
||||||
order: 2,
|
order: 2,
|
||||||
}
|
};
|
||||||
|
|
||||||
// thirdParameter is mutable string with an empty default value (parameter value not required).
|
// thirdParameter is mutable string with an empty default value (parameter value not required).
|
||||||
export const thirdParameter: RichParameter = {
|
export const thirdParameter: RichParameter = {
|
||||||
|
@ -57,7 +57,7 @@ export const thirdParameter: RichParameter = {
|
||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
mutable: true,
|
mutable: true,
|
||||||
order: 3,
|
order: 3,
|
||||||
}
|
};
|
||||||
|
|
||||||
// fourthParameter is immutable boolean with a default "true" value (parameter value not required).
|
// fourthParameter is immutable boolean with a default "true" value (parameter value not required).
|
||||||
export const fourthParameter: RichParameter = {
|
export const fourthParameter: RichParameter = {
|
||||||
|
@ -68,7 +68,7 @@ export const fourthParameter: RichParameter = {
|
||||||
description: "This is fourth parameter.",
|
description: "This is fourth parameter.",
|
||||||
defaultValue: "true",
|
defaultValue: "true",
|
||||||
order: 3,
|
order: 3,
|
||||||
}
|
};
|
||||||
|
|
||||||
// fifthParameter is immutable "string with options", with a default option selected (parameter value not required).
|
// fifthParameter is immutable "string with options", with a default option selected (parameter value not required).
|
||||||
export const fifthParameter: RichParameter = {
|
export const fifthParameter: RichParameter = {
|
||||||
|
@ -100,7 +100,7 @@ export const fifthParameter: RichParameter = {
|
||||||
description: "This is fifth parameter.",
|
description: "This is fifth parameter.",
|
||||||
defaultValue: "def",
|
defaultValue: "def",
|
||||||
order: 3,
|
order: 3,
|
||||||
}
|
};
|
||||||
|
|
||||||
// sixthParameter is mutable string without a default value (parameter value is required).
|
// sixthParameter is mutable string without a default value (parameter value is required).
|
||||||
export const sixthParameter: RichParameter = {
|
export const sixthParameter: RichParameter = {
|
||||||
|
@ -114,7 +114,7 @@ export const sixthParameter: RichParameter = {
|
||||||
required: true,
|
required: true,
|
||||||
mutable: true,
|
mutable: true,
|
||||||
order: 1,
|
order: 1,
|
||||||
}
|
};
|
||||||
|
|
||||||
// seventhParameter is immutable string without a default value (parameter value is required).
|
// seventhParameter is immutable string without a default value (parameter value is required).
|
||||||
export const seventhParameter: RichParameter = {
|
export const seventhParameter: RichParameter = {
|
||||||
|
@ -126,7 +126,7 @@ export const seventhParameter: RichParameter = {
|
||||||
description: "This is seventh parameter.",
|
description: "This is seventh parameter.",
|
||||||
required: true,
|
required: true,
|
||||||
order: 1,
|
order: 1,
|
||||||
}
|
};
|
||||||
|
|
||||||
// Build options
|
// Build options
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ export const firstBuildOption: RichParameter = {
|
||||||
defaultValue: "ABCDEF",
|
defaultValue: "ABCDEF",
|
||||||
mutable: true,
|
mutable: true,
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
}
|
};
|
||||||
|
|
||||||
export const secondBuildOption: RichParameter = {
|
export const secondBuildOption: RichParameter = {
|
||||||
...emptyParameter,
|
...emptyParameter,
|
||||||
|
@ -153,4 +153,4 @@ export const secondBuildOption: RichParameter = {
|
||||||
defaultValue: "false",
|
defaultValue: "false",
|
||||||
mutable: true,
|
mutable: true,
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { defineConfig } from "@playwright/test"
|
import { defineConfig } from "@playwright/test";
|
||||||
import path from "path"
|
import path from "path";
|
||||||
import { defaultPort, gitAuth } from "./constants"
|
import { defaultPort, gitAuth } from "./constants";
|
||||||
|
|
||||||
export const port = process.env.CODER_E2E_PORT
|
export const port = process.env.CODER_E2E_PORT
|
||||||
? Number(process.env.CODER_E2E_PORT)
|
? Number(process.env.CODER_E2E_PORT)
|
||||||
: defaultPort
|
: defaultPort;
|
||||||
|
|
||||||
const coderMain = path.join(__dirname, "../../enterprise/cmd/coder/main.go")
|
const coderMain = path.join(__dirname, "../../enterprise/cmd/coder/main.go");
|
||||||
|
|
||||||
export const STORAGE_STATE = path.join(__dirname, ".auth.json")
|
export const STORAGE_STATE = path.join(__dirname, ".auth.json");
|
||||||
|
|
||||||
const localURL = (port: number, path: string): string => {
|
const localURL = (port: number, path: string): string => {
|
||||||
return `http://localhost:${port}${path}`
|
return `http://localhost:${port}${path}`;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
projects: [
|
projects: [
|
||||||
|
@ -92,4 +92,4 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
reuseExistingServer: false,
|
reuseExistingServer: false,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { Page } from "@playwright/test"
|
import { Page } from "@playwright/test";
|
||||||
|
|
||||||
export abstract class BasePom {
|
export abstract class BasePom {
|
||||||
protected readonly baseURL: string | undefined
|
protected readonly baseURL: string | undefined;
|
||||||
protected readonly path: string
|
protected readonly path: string;
|
||||||
protected readonly page: Page
|
protected readonly page: Page;
|
||||||
|
|
||||||
constructor(baseURL: string | undefined, path: string, page: Page) {
|
constructor(baseURL: string | undefined, path: string, page: Page) {
|
||||||
this.baseURL = baseURL
|
this.baseURL = baseURL;
|
||||||
this.path = path
|
this.path = path;
|
||||||
this.page = page
|
this.page = page;
|
||||||
}
|
}
|
||||||
|
|
||||||
get url(): string {
|
get url(): string {
|
||||||
return this.baseURL + this.path
|
return this.baseURL + this.path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { Page } from "@playwright/test"
|
import { Page } from "@playwright/test";
|
||||||
import { BasePom } from "./BasePom"
|
import { BasePom } from "./BasePom";
|
||||||
|
|
||||||
export class SignInPage extends BasePom {
|
export class SignInPage extends BasePom {
|
||||||
constructor(baseURL: string | undefined, page: Page) {
|
constructor(baseURL: string | undefined, page: Page) {
|
||||||
super(baseURL, "/login", page)
|
super(baseURL, "/login", page);
|
||||||
}
|
}
|
||||||
|
|
||||||
async submitBuiltInAuthentication(
|
async submitBuiltInAuthentication(
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.page.fill("text=Email", email)
|
await this.page.fill("text=Email", email);
|
||||||
await this.page.fill("text=Password", password)
|
await this.page.fill("text=Password", password);
|
||||||
await this.page.click('button:has-text("Sign In")')
|
await this.page.click('button:has-text("Sign In")');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Page } from "@playwright/test"
|
import { Page } from "@playwright/test";
|
||||||
import { BasePom } from "./BasePom"
|
import { BasePom } from "./BasePom";
|
||||||
|
|
||||||
export class WorkspacesPage extends BasePom {
|
export class WorkspacesPage extends BasePom {
|
||||||
constructor(baseURL: string | undefined, page: Page, params?: string) {
|
constructor(baseURL: string | undefined, page: Page, params?: string) {
|
||||||
super(baseURL, `/workspaces${params && params}`, page)
|
super(baseURL, `/workspaces${params && params}`, page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
export * from "./SignInPage"
|
export * from "./SignInPage";
|
||||||
export * from "./WorkspacesPage"
|
export * from "./WorkspacesPage";
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,4 @@
|
||||||
import fs from "fs"
|
import fs from "fs";
|
||||||
import type {
|
import type {
|
||||||
FullConfig,
|
FullConfig,
|
||||||
Suite,
|
Suite,
|
||||||
|
@ -6,18 +6,18 @@ import type {
|
||||||
TestResult,
|
TestResult,
|
||||||
FullResult,
|
FullResult,
|
||||||
Reporter,
|
Reporter,
|
||||||
} from "@playwright/test/reporter"
|
} from "@playwright/test/reporter";
|
||||||
import axios from "axios"
|
import axios from "axios";
|
||||||
|
|
||||||
class CoderReporter implements Reporter {
|
class CoderReporter implements Reporter {
|
||||||
onBegin(config: FullConfig, suite: Suite) {
|
onBegin(config: FullConfig, suite: Suite) {
|
||||||
// eslint-disable-next-line no-console -- Helpful for debugging
|
// eslint-disable-next-line no-console -- Helpful for debugging
|
||||||
console.log(`Starting the run with ${suite.allTests().length} tests`)
|
console.log(`Starting the run with ${suite.allTests().length} tests`);
|
||||||
}
|
}
|
||||||
|
|
||||||
onTestBegin(test: TestCase) {
|
onTestBegin(test: TestCase) {
|
||||||
// eslint-disable-next-line no-console -- Helpful for debugging
|
// eslint-disable-next-line no-console -- Helpful for debugging
|
||||||
console.log(`Starting test ${test.title}`)
|
console.log(`Starting test ${test.title}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
onStdOut(chunk: string, test: TestCase, _: TestResult): void {
|
onStdOut(chunk: string, test: TestCase, _: TestResult): void {
|
||||||
|
@ -27,7 +27,7 @@ class CoderReporter implements Reporter {
|
||||||
/\n$/g,
|
/\n$/g,
|
||||||
"",
|
"",
|
||||||
)}`,
|
)}`,
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onStdErr(chunk: string, test: TestCase, _: TestResult): void {
|
onStdErr(chunk: string, test: TestCase, _: TestResult): void {
|
||||||
|
@ -37,50 +37,50 @@ class CoderReporter implements Reporter {
|
||||||
/\n$/g,
|
/\n$/g,
|
||||||
"",
|
"",
|
||||||
)}`,
|
)}`,
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onTestEnd(test: TestCase, result: TestResult) {
|
async onTestEnd(test: TestCase, result: TestResult) {
|
||||||
// eslint-disable-next-line no-console -- Helpful for debugging
|
// eslint-disable-next-line no-console -- Helpful for debugging
|
||||||
console.log(`Finished test ${test.title}: ${result.status}`)
|
console.log(`Finished test ${test.title}: ${result.status}`);
|
||||||
|
|
||||||
if (result.status !== "passed") {
|
if (result.status !== "passed") {
|
||||||
// eslint-disable-next-line no-console -- Helpful for debugging
|
// eslint-disable-next-line no-console -- Helpful for debugging
|
||||||
console.log("errors", result.errors, "attachments", result.attachments)
|
console.log("errors", result.errors, "attachments", result.attachments);
|
||||||
}
|
}
|
||||||
await exportDebugPprof(test.title)
|
await exportDebugPprof(test.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnd(result: FullResult) {
|
onEnd(result: FullResult) {
|
||||||
// eslint-disable-next-line no-console -- Helpful for debugging
|
// eslint-disable-next-line no-console -- Helpful for debugging
|
||||||
console.log(`Finished the run: ${result.status}`)
|
console.log(`Finished the run: ${result.status}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportDebugPprof = async (testName: string) => {
|
const exportDebugPprof = async (testName: string) => {
|
||||||
const url = "http://127.0.0.1:6060/debug/pprof/goroutine?debug=1"
|
const url = "http://127.0.0.1:6060/debug/pprof/goroutine?debug=1";
|
||||||
const outputFile = `test-results/debug-pprof-goroutine-${testName}.txt`
|
const outputFile = `test-results/debug-pprof-goroutine-${testName}.txt`;
|
||||||
|
|
||||||
await axios
|
await axios
|
||||||
.get(url)
|
.get(url)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
throw new Error(`Error: Received status code ${response.status}`)
|
throw new Error(`Error: Received status code ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFile(outputFile, response.data, (err) => {
|
fs.writeFile(outputFile, response.data, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
throw new Error(`Error writing to ${outputFile}: ${err.message}`)
|
throw new Error(`Error writing to ${outputFile}: ${err.message}`);
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line no-console -- Helpful for debugging
|
// eslint-disable-next-line no-console -- Helpful for debugging
|
||||||
console.log(`Data from ${url} has been saved to ${outputFile}`)
|
console.log(`Data from ${url} has been saved to ${outputFile}`);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw new Error(`Error: ${error.message}`)
|
throw new Error(`Error: ${error.message}`);
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars -- Playwright config uses it
|
// eslint-disable-next-line no-unused-vars -- Playwright config uses it
|
||||||
export default CoderReporter
|
export default CoderReporter;
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
import { test } from "@playwright/test"
|
import { test } from "@playwright/test";
|
||||||
import { randomUUID } from "crypto"
|
import { randomUUID } from "crypto";
|
||||||
import * as http from "http"
|
import * as http from "http";
|
||||||
import {
|
import {
|
||||||
createTemplate,
|
createTemplate,
|
||||||
createWorkspace,
|
createWorkspace,
|
||||||
startAgent,
|
startAgent,
|
||||||
stopAgent,
|
stopAgent,
|
||||||
stopWorkspace,
|
stopWorkspace,
|
||||||
} from "../helpers"
|
} from "../helpers";
|
||||||
import { beforeCoderTest } from "../hooks"
|
import { beforeCoderTest } from "../hooks";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page))
|
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||||
|
|
||||||
test("app", async ({ context, page }) => {
|
test("app", async ({ context, page }) => {
|
||||||
const appContent = "Hello World"
|
const appContent = "Hello World";
|
||||||
const token = randomUUID()
|
const token = randomUUID();
|
||||||
const srv = http
|
const srv = http
|
||||||
.createServer((req, res) => {
|
.createServer((req, res) => {
|
||||||
res.writeHead(200, { "Content-Type": "text/plain" })
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
||||||
res.end(appContent)
|
res.end(appContent);
|
||||||
})
|
})
|
||||||
.listen(0)
|
.listen(0);
|
||||||
const addr = srv.address()
|
const addr = srv.address();
|
||||||
if (typeof addr !== "object" || !addr) {
|
if (typeof addr !== "object" || !addr) {
|
||||||
throw new Error("Expected addr to be an object")
|
throw new Error("Expected addr to be an object");
|
||||||
}
|
}
|
||||||
const appName = "test-app"
|
const appName = "test-app";
|
||||||
const template = await createTemplate(page, {
|
const template = await createTemplate(page, {
|
||||||
apply: [
|
apply: [
|
||||||
{
|
{
|
||||||
|
@ -48,17 +48,17 @@ test("app", async ({ context, page }) => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
const workspaceName = await createWorkspace(page, template)
|
const workspaceName = await createWorkspace(page, template);
|
||||||
const agent = await startAgent(page, token)
|
const agent = await startAgent(page, token);
|
||||||
|
|
||||||
// Wait for the web terminal to open in a new tab
|
// Wait for the web terminal to open in a new tab
|
||||||
const pagePromise = context.waitForEvent("page")
|
const pagePromise = context.waitForEvent("page");
|
||||||
await page.getByText(appName).click()
|
await page.getByText(appName).click();
|
||||||
const app = await pagePromise
|
const app = await pagePromise;
|
||||||
await app.waitForLoadState("domcontentloaded")
|
await app.waitForLoadState("domcontentloaded");
|
||||||
await app.getByText(appContent).isVisible()
|
await app.getByText(appContent).isVisible();
|
||||||
|
|
||||||
await stopWorkspace(page, workspaceName)
|
await stopWorkspace(page, workspaceName);
|
||||||
await stopAgent(agent)
|
await stopAgent(agent);
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { test } from "@playwright/test"
|
import { test } from "@playwright/test";
|
||||||
import {
|
import {
|
||||||
createTemplate,
|
createTemplate,
|
||||||
createWorkspace,
|
createWorkspace,
|
||||||
echoResponsesWithParameters,
|
echoResponsesWithParameters,
|
||||||
verifyParameters,
|
verifyParameters,
|
||||||
} from "../helpers"
|
} from "../helpers";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
secondParameter,
|
secondParameter,
|
||||||
|
@ -14,11 +14,11 @@ import {
|
||||||
thirdParameter,
|
thirdParameter,
|
||||||
seventhParameter,
|
seventhParameter,
|
||||||
sixthParameter,
|
sixthParameter,
|
||||||
} from "../parameters"
|
} from "../parameters";
|
||||||
import { RichParameter } from "../provisionerGenerated"
|
import { RichParameter } from "../provisionerGenerated";
|
||||||
import { beforeCoderTest } from "../hooks"
|
import { beforeCoderTest } from "../hooks";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page))
|
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||||
|
|
||||||
test("create workspace", async ({ page }) => {
|
test("create workspace", async ({ page }) => {
|
||||||
const template = await createTemplate(page, {
|
const template = await createTemplate(page, {
|
||||||
|
@ -33,40 +33,40 @@ test("create workspace", async ({ page }) => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
await createWorkspace(page, template)
|
await createWorkspace(page, template);
|
||||||
})
|
});
|
||||||
|
|
||||||
test("create workspace with default immutable parameters", async ({ page }) => {
|
test("create workspace with default immutable parameters", async ({ page }) => {
|
||||||
const richParameters: RichParameter[] = [
|
const richParameters: RichParameter[] = [
|
||||||
secondParameter,
|
secondParameter,
|
||||||
fourthParameter,
|
fourthParameter,
|
||||||
fifthParameter,
|
fifthParameter,
|
||||||
]
|
];
|
||||||
const template = await createTemplate(
|
const template = await createTemplate(
|
||||||
page,
|
page,
|
||||||
echoResponsesWithParameters(richParameters),
|
echoResponsesWithParameters(richParameters),
|
||||||
)
|
);
|
||||||
const workspaceName = await createWorkspace(page, template)
|
const workspaceName = await createWorkspace(page, template);
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
{ name: secondParameter.name, value: secondParameter.defaultValue },
|
{ name: secondParameter.name, value: secondParameter.defaultValue },
|
||||||
{ name: fourthParameter.name, value: fourthParameter.defaultValue },
|
{ name: fourthParameter.name, value: fourthParameter.defaultValue },
|
||||||
{ name: fifthParameter.name, value: fifthParameter.defaultValue },
|
{ name: fifthParameter.name, value: fifthParameter.defaultValue },
|
||||||
])
|
]);
|
||||||
})
|
});
|
||||||
|
|
||||||
test("create workspace with default mutable parameters", async ({ page }) => {
|
test("create workspace with default mutable parameters", async ({ page }) => {
|
||||||
const richParameters: RichParameter[] = [firstParameter, thirdParameter]
|
const richParameters: RichParameter[] = [firstParameter, thirdParameter];
|
||||||
const template = await createTemplate(
|
const template = await createTemplate(
|
||||||
page,
|
page,
|
||||||
echoResponsesWithParameters(richParameters),
|
echoResponsesWithParameters(richParameters),
|
||||||
)
|
);
|
||||||
const workspaceName = await createWorkspace(page, template)
|
const workspaceName = await createWorkspace(page, template);
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
{ name: firstParameter.name, value: firstParameter.defaultValue },
|
{ name: firstParameter.name, value: firstParameter.defaultValue },
|
||||||
{ name: thirdParameter.name, value: thirdParameter.defaultValue },
|
{ name: thirdParameter.name, value: thirdParameter.defaultValue },
|
||||||
])
|
]);
|
||||||
})
|
});
|
||||||
|
|
||||||
test("create workspace with default and required parameters", async ({
|
test("create workspace with default and required parameters", async ({
|
||||||
page,
|
page,
|
||||||
|
@ -76,46 +76,46 @@ test("create workspace with default and required parameters", async ({
|
||||||
fourthParameter,
|
fourthParameter,
|
||||||
sixthParameter,
|
sixthParameter,
|
||||||
seventhParameter,
|
seventhParameter,
|
||||||
]
|
];
|
||||||
const buildParameters = [
|
const buildParameters = [
|
||||||
{ name: sixthParameter.name, value: "12345" },
|
{ name: sixthParameter.name, value: "12345" },
|
||||||
{ name: seventhParameter.name, value: "abcdef" },
|
{ name: seventhParameter.name, value: "abcdef" },
|
||||||
]
|
];
|
||||||
const template = await createTemplate(
|
const template = await createTemplate(
|
||||||
page,
|
page,
|
||||||
echoResponsesWithParameters(richParameters),
|
echoResponsesWithParameters(richParameters),
|
||||||
)
|
);
|
||||||
const workspaceName = await createWorkspace(
|
const workspaceName = await createWorkspace(
|
||||||
page,
|
page,
|
||||||
template,
|
template,
|
||||||
richParameters,
|
richParameters,
|
||||||
buildParameters,
|
buildParameters,
|
||||||
)
|
);
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
// user values:
|
// user values:
|
||||||
...buildParameters,
|
...buildParameters,
|
||||||
// default values:
|
// default values:
|
||||||
{ name: secondParameter.name, value: secondParameter.defaultValue },
|
{ name: secondParameter.name, value: secondParameter.defaultValue },
|
||||||
{ name: fourthParameter.name, value: fourthParameter.defaultValue },
|
{ name: fourthParameter.name, value: fourthParameter.defaultValue },
|
||||||
])
|
]);
|
||||||
})
|
});
|
||||||
|
|
||||||
test("create workspace and overwrite default parameters", async ({ page }) => {
|
test("create workspace and overwrite default parameters", async ({ page }) => {
|
||||||
const richParameters: RichParameter[] = [secondParameter, fourthParameter]
|
const richParameters: RichParameter[] = [secondParameter, fourthParameter];
|
||||||
const buildParameters = [
|
const buildParameters = [
|
||||||
{ name: secondParameter.name, value: "AAAAA" },
|
{ name: secondParameter.name, value: "AAAAA" },
|
||||||
{ name: fourthParameter.name, value: "false" },
|
{ name: fourthParameter.name, value: "false" },
|
||||||
]
|
];
|
||||||
const template = await createTemplate(
|
const template = await createTemplate(
|
||||||
page,
|
page,
|
||||||
echoResponsesWithParameters(richParameters),
|
echoResponsesWithParameters(richParameters),
|
||||||
)
|
);
|
||||||
|
|
||||||
const workspaceName = await createWorkspace(
|
const workspaceName = await createWorkspace(
|
||||||
page,
|
page,
|
||||||
template,
|
template,
|
||||||
richParameters,
|
richParameters,
|
||||||
buildParameters,
|
buildParameters,
|
||||||
)
|
);
|
||||||
await verifyParameters(page, workspaceName, richParameters, buildParameters)
|
await verifyParameters(page, workspaceName, richParameters, buildParameters);
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { test } from "@playwright/test"
|
import { test } from "@playwright/test";
|
||||||
import { gitAuth } from "../constants"
|
import { gitAuth } from "../constants";
|
||||||
import { Endpoints } from "@octokit/types"
|
import { Endpoints } from "@octokit/types";
|
||||||
import { GitAuthDevice } from "api/typesGenerated"
|
import { GitAuthDevice } from "api/typesGenerated";
|
||||||
import { Awaiter, createServer } from "../helpers"
|
import { Awaiter, createServer } from "../helpers";
|
||||||
import { beforeCoderTest } from "../hooks"
|
import { beforeCoderTest } from "../hooks";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page))
|
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||||
|
|
||||||
// Ensures that a Git auth provider with the device flow functions and completes!
|
// Ensures that a Git auth provider with the device flow functions and completes!
|
||||||
test("git auth device", async ({ page }) => {
|
test("git auth device", async ({ page }) => {
|
||||||
|
@ -15,71 +15,71 @@ test("git auth device", async ({ page }) => {
|
||||||
expires_in: 900,
|
expires_in: 900,
|
||||||
interval: 1,
|
interval: 1,
|
||||||
verification_uri: "",
|
verification_uri: "",
|
||||||
}
|
};
|
||||||
|
|
||||||
// Start a server to mock the GitHub API.
|
// Start a server to mock the GitHub API.
|
||||||
const srv = await createServer(gitAuth.devicePort)
|
const srv = await createServer(gitAuth.devicePort);
|
||||||
srv.use(gitAuth.validatePath, (req, res) => {
|
srv.use(gitAuth.validatePath, (req, res) => {
|
||||||
res.write(JSON.stringify(ghUser))
|
res.write(JSON.stringify(ghUser));
|
||||||
res.end()
|
res.end();
|
||||||
})
|
});
|
||||||
srv.use(gitAuth.codePath, (req, res) => {
|
srv.use(gitAuth.codePath, (req, res) => {
|
||||||
res.write(JSON.stringify(device))
|
res.write(JSON.stringify(device));
|
||||||
res.end()
|
res.end();
|
||||||
})
|
});
|
||||||
srv.use(gitAuth.installationsPath, (req, res) => {
|
srv.use(gitAuth.installationsPath, (req, res) => {
|
||||||
res.write(JSON.stringify(ghInstall))
|
res.write(JSON.stringify(ghInstall));
|
||||||
res.end()
|
res.end();
|
||||||
})
|
});
|
||||||
|
|
||||||
const token = {
|
const token = {
|
||||||
access_token: "",
|
access_token: "",
|
||||||
error: "authorization_pending",
|
error: "authorization_pending",
|
||||||
error_description: "",
|
error_description: "",
|
||||||
}
|
};
|
||||||
// First we send a result from the API that the token hasn't been
|
// First we send a result from the API that the token hasn't been
|
||||||
// authorized yet to ensure the UI reacts properly.
|
// authorized yet to ensure the UI reacts properly.
|
||||||
const sentPending = new Awaiter()
|
const sentPending = new Awaiter();
|
||||||
srv.use(gitAuth.tokenPath, (req, res) => {
|
srv.use(gitAuth.tokenPath, (req, res) => {
|
||||||
res.write(JSON.stringify(token))
|
res.write(JSON.stringify(token));
|
||||||
res.end()
|
res.end();
|
||||||
sentPending.done()
|
sentPending.done();
|
||||||
})
|
});
|
||||||
|
|
||||||
await page.goto(`/gitauth/${gitAuth.deviceProvider}`, {
|
await page.goto(`/gitauth/${gitAuth.deviceProvider}`, {
|
||||||
waitUntil: "domcontentloaded",
|
waitUntil: "domcontentloaded",
|
||||||
})
|
});
|
||||||
await page.getByText(device.user_code).isVisible()
|
await page.getByText(device.user_code).isVisible();
|
||||||
await sentPending.wait()
|
await sentPending.wait();
|
||||||
// Update the token to be valid and ensure the UI updates!
|
// Update the token to be valid and ensure the UI updates!
|
||||||
token.error = ""
|
token.error = "";
|
||||||
token.access_token = "hello-world"
|
token.access_token = "hello-world";
|
||||||
await page.waitForSelector("text=1 organization authorized")
|
await page.waitForSelector("text=1 organization authorized");
|
||||||
})
|
});
|
||||||
|
|
||||||
test("git auth web", async ({ baseURL, page }) => {
|
test("git auth web", async ({ baseURL, page }) => {
|
||||||
const srv = await createServer(gitAuth.webPort)
|
const srv = await createServer(gitAuth.webPort);
|
||||||
// The GitHub validate endpoint returns the currently authenticated user!
|
// The GitHub validate endpoint returns the currently authenticated user!
|
||||||
srv.use(gitAuth.validatePath, (req, res) => {
|
srv.use(gitAuth.validatePath, (req, res) => {
|
||||||
res.write(JSON.stringify(ghUser))
|
res.write(JSON.stringify(ghUser));
|
||||||
res.end()
|
res.end();
|
||||||
})
|
});
|
||||||
srv.use(gitAuth.tokenPath, (req, res) => {
|
srv.use(gitAuth.tokenPath, (req, res) => {
|
||||||
res.write(JSON.stringify({ access_token: "hello-world" }))
|
res.write(JSON.stringify({ access_token: "hello-world" }));
|
||||||
res.end()
|
res.end();
|
||||||
})
|
});
|
||||||
srv.use(gitAuth.authPath, (req, res) => {
|
srv.use(gitAuth.authPath, (req, res) => {
|
||||||
res.redirect(
|
res.redirect(
|
||||||
`${baseURL}/gitauth/${gitAuth.webProvider}/callback?code=1234&state=` +
|
`${baseURL}/gitauth/${gitAuth.webProvider}/callback?code=1234&state=` +
|
||||||
req.query.state,
|
req.query.state,
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
await page.goto(`/gitauth/${gitAuth.webProvider}`, {
|
await page.goto(`/gitauth/${gitAuth.webProvider}`, {
|
||||||
waitUntil: "domcontentloaded",
|
waitUntil: "domcontentloaded",
|
||||||
})
|
});
|
||||||
// This endpoint doesn't have the installations URL set intentionally!
|
// This endpoint doesn't have the installations URL set intentionally!
|
||||||
await page.waitForSelector("text=You've authenticated with GitHub!")
|
await page.waitForSelector("text=You've authenticated with GitHub!");
|
||||||
})
|
});
|
||||||
|
|
||||||
const ghUser: Endpoints["GET /user"]["response"]["data"] = {
|
const ghUser: Endpoints["GET /user"]["response"]["data"] = {
|
||||||
login: "kylecarbs",
|
login: "kylecarbs",
|
||||||
|
@ -115,7 +115,7 @@ const ghUser: Endpoints["GET /user"]["response"]["data"] = {
|
||||||
following: 31,
|
following: 31,
|
||||||
created_at: "2014-04-01T02:24:41Z",
|
created_at: "2014-04-01T02:24:41Z",
|
||||||
updated_at: "2023-06-26T13:03:09Z",
|
updated_at: "2023-06-26T13:03:09Z",
|
||||||
}
|
};
|
||||||
|
|
||||||
const ghInstall: Endpoints["GET /user/installations"]["response"]["data"] = {
|
const ghInstall: Endpoints["GET /user/installations"]["response"]["data"] = {
|
||||||
installations: [
|
installations: [
|
||||||
|
@ -140,4 +140,4 @@ const ghInstall: Endpoints["GET /user/installations"]["response"]["data"] = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
total_count: 1,
|
total_count: 1,
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { test, expect } from "@playwright/test"
|
import { test, expect } from "@playwright/test";
|
||||||
import { beforeCoderTest } from "../hooks"
|
import { beforeCoderTest } from "../hooks";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page))
|
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||||
|
|
||||||
test("list templates", async ({ page, baseURL }) => {
|
test("list templates", async ({ page, baseURL }) => {
|
||||||
await page.goto(`${baseURL}/templates`, { waitUntil: "domcontentloaded" })
|
await page.goto(`${baseURL}/templates`, { waitUntil: "domcontentloaded" });
|
||||||
await expect(page).toHaveTitle("Templates - Coder")
|
await expect(page).toHaveTitle("Templates - Coder");
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { test } from "@playwright/test"
|
import { test } from "@playwright/test";
|
||||||
import { randomUUID } from "crypto"
|
import { randomUUID } from "crypto";
|
||||||
import {
|
import {
|
||||||
createTemplate,
|
createTemplate,
|
||||||
createWorkspace,
|
createWorkspace,
|
||||||
|
@ -8,15 +8,15 @@ import {
|
||||||
startAgentWithCommand,
|
startAgentWithCommand,
|
||||||
stopAgent,
|
stopAgent,
|
||||||
stopWorkspace,
|
stopWorkspace,
|
||||||
} from "../helpers"
|
} from "../helpers";
|
||||||
import { beforeCoderTest } from "../hooks"
|
import { beforeCoderTest } from "../hooks";
|
||||||
|
|
||||||
const agentVersion = "v0.14.0"
|
const agentVersion = "v0.14.0";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page))
|
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||||
|
|
||||||
test("ssh with agent " + agentVersion, async ({ page }) => {
|
test("ssh with agent " + agentVersion, async ({ page }) => {
|
||||||
const token = randomUUID()
|
const token = randomUUID();
|
||||||
const template = await createTemplate(page, {
|
const template = await createTemplate(page, {
|
||||||
apply: [
|
apply: [
|
||||||
{
|
{
|
||||||
|
@ -33,28 +33,28 @@ test("ssh with agent " + agentVersion, async ({ page }) => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
const workspaceName = await createWorkspace(page, template)
|
const workspaceName = await createWorkspace(page, template);
|
||||||
const binaryPath = await downloadCoderVersion(agentVersion)
|
const binaryPath = await downloadCoderVersion(agentVersion);
|
||||||
const agent = await startAgentWithCommand(page, token, binaryPath)
|
const agent = await startAgentWithCommand(page, token, binaryPath);
|
||||||
|
|
||||||
const client = await sshIntoWorkspace(page, workspaceName)
|
const client = await sshIntoWorkspace(page, workspaceName);
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
// We just exec a command to be certain the agent is running!
|
// We just exec a command to be certain the agent is running!
|
||||||
client.exec("exit 0", (err, stream) => {
|
client.exec("exit 0", (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return reject(err)
|
return reject(err);
|
||||||
}
|
}
|
||||||
stream.on("exit", (code) => {
|
stream.on("exit", (code) => {
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
return reject(new Error(`Command exited with code ${code}`))
|
return reject(new Error(`Command exited with code ${code}`));
|
||||||
}
|
}
|
||||||
client.end()
|
client.end();
|
||||||
resolve()
|
resolve();
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
await stopWorkspace(page, workspaceName)
|
await stopWorkspace(page, workspaceName);
|
||||||
await stopAgent(agent, false)
|
await stopAgent(agent, false);
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { test } from "@playwright/test"
|
import { test } from "@playwright/test";
|
||||||
import { randomUUID } from "crypto"
|
import { randomUUID } from "crypto";
|
||||||
import {
|
import {
|
||||||
createTemplate,
|
createTemplate,
|
||||||
createWorkspace,
|
createWorkspace,
|
||||||
|
@ -8,15 +8,15 @@ import {
|
||||||
startAgent,
|
startAgent,
|
||||||
stopAgent,
|
stopAgent,
|
||||||
stopWorkspace,
|
stopWorkspace,
|
||||||
} from "../helpers"
|
} from "../helpers";
|
||||||
import { beforeCoderTest } from "../hooks"
|
import { beforeCoderTest } from "../hooks";
|
||||||
|
|
||||||
const clientVersion = "v0.14.0"
|
const clientVersion = "v0.14.0";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page))
|
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||||
|
|
||||||
test("ssh with client " + clientVersion, async ({ page }) => {
|
test("ssh with client " + clientVersion, async ({ page }) => {
|
||||||
const token = randomUUID()
|
const token = randomUUID();
|
||||||
const template = await createTemplate(page, {
|
const template = await createTemplate(page, {
|
||||||
apply: [
|
apply: [
|
||||||
{
|
{
|
||||||
|
@ -33,28 +33,28 @@ test("ssh with client " + clientVersion, async ({ page }) => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
const workspaceName = await createWorkspace(page, template)
|
const workspaceName = await createWorkspace(page, template);
|
||||||
const agent = await startAgent(page, token)
|
const agent = await startAgent(page, token);
|
||||||
const binaryPath = await downloadCoderVersion(clientVersion)
|
const binaryPath = await downloadCoderVersion(clientVersion);
|
||||||
|
|
||||||
const client = await sshIntoWorkspace(page, workspaceName, binaryPath)
|
const client = await sshIntoWorkspace(page, workspaceName, binaryPath);
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
// We just exec a command to be certain the agent is running!
|
// We just exec a command to be certain the agent is running!
|
||||||
client.exec("exit 0", (err, stream) => {
|
client.exec("exit 0", (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return reject(err)
|
return reject(err);
|
||||||
}
|
}
|
||||||
stream.on("exit", (code) => {
|
stream.on("exit", (code) => {
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
return reject(new Error(`Command exited with code ${code}`))
|
return reject(new Error(`Command exited with code ${code}`));
|
||||||
}
|
}
|
||||||
client.end()
|
client.end();
|
||||||
resolve()
|
resolve();
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
await stopWorkspace(page, workspaceName)
|
await stopWorkspace(page, workspaceName);
|
||||||
await stopAgent(agent)
|
await stopAgent(agent);
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,48 +1,48 @@
|
||||||
import { test } from "@playwright/test"
|
import { test } from "@playwright/test";
|
||||||
import {
|
import {
|
||||||
buildWorkspaceWithParameters,
|
buildWorkspaceWithParameters,
|
||||||
createTemplate,
|
createTemplate,
|
||||||
createWorkspace,
|
createWorkspace,
|
||||||
echoResponsesWithParameters,
|
echoResponsesWithParameters,
|
||||||
verifyParameters,
|
verifyParameters,
|
||||||
} from "../helpers"
|
} from "../helpers";
|
||||||
|
|
||||||
import { firstBuildOption, secondBuildOption } from "../parameters"
|
import { firstBuildOption, secondBuildOption } from "../parameters";
|
||||||
import { RichParameter } from "../provisionerGenerated"
|
import { RichParameter } from "../provisionerGenerated";
|
||||||
import { beforeCoderTest } from "../hooks"
|
import { beforeCoderTest } from "../hooks";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page))
|
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||||
|
|
||||||
test("restart workspace with ephemeral parameters", async ({ page }) => {
|
test("restart workspace with ephemeral parameters", async ({ page }) => {
|
||||||
const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption]
|
const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption];
|
||||||
const template = await createTemplate(
|
const template = await createTemplate(
|
||||||
page,
|
page,
|
||||||
echoResponsesWithParameters(richParameters),
|
echoResponsesWithParameters(richParameters),
|
||||||
)
|
);
|
||||||
const workspaceName = await createWorkspace(page, template)
|
const workspaceName = await createWorkspace(page, template);
|
||||||
|
|
||||||
// Verify that build options are default (not selected).
|
// Verify that build options are default (not selected).
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
||||||
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
||||||
])
|
]);
|
||||||
|
|
||||||
// Now, restart the workspace with ephemeral parameters selected.
|
// Now, restart the workspace with ephemeral parameters selected.
|
||||||
const buildParameters = [
|
const buildParameters = [
|
||||||
{ name: firstBuildOption.name, value: "AAAAA" },
|
{ name: firstBuildOption.name, value: "AAAAA" },
|
||||||
{ name: secondBuildOption.name, value: "true" },
|
{ name: secondBuildOption.name, value: "true" },
|
||||||
]
|
];
|
||||||
await buildWorkspaceWithParameters(
|
await buildWorkspaceWithParameters(
|
||||||
page,
|
page,
|
||||||
workspaceName,
|
workspaceName,
|
||||||
richParameters,
|
richParameters,
|
||||||
buildParameters,
|
buildParameters,
|
||||||
true,
|
true,
|
||||||
)
|
);
|
||||||
|
|
||||||
// Verify that build options are default (not selected).
|
// Verify that build options are default (not selected).
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
||||||
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
||||||
])
|
]);
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { test } from "@playwright/test"
|
import { test } from "@playwright/test";
|
||||||
import {
|
import {
|
||||||
buildWorkspaceWithParameters,
|
buildWorkspaceWithParameters,
|
||||||
createTemplate,
|
createTemplate,
|
||||||
|
@ -6,44 +6,44 @@ import {
|
||||||
echoResponsesWithParameters,
|
echoResponsesWithParameters,
|
||||||
stopWorkspace,
|
stopWorkspace,
|
||||||
verifyParameters,
|
verifyParameters,
|
||||||
} from "../helpers"
|
} from "../helpers";
|
||||||
|
|
||||||
import { firstBuildOption, secondBuildOption } from "../parameters"
|
import { firstBuildOption, secondBuildOption } from "../parameters";
|
||||||
import { RichParameter } from "../provisionerGenerated"
|
import { RichParameter } from "../provisionerGenerated";
|
||||||
|
|
||||||
test("start workspace with ephemeral parameters", async ({ page }) => {
|
test("start workspace with ephemeral parameters", async ({ page }) => {
|
||||||
const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption]
|
const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption];
|
||||||
const template = await createTemplate(
|
const template = await createTemplate(
|
||||||
page,
|
page,
|
||||||
echoResponsesWithParameters(richParameters),
|
echoResponsesWithParameters(richParameters),
|
||||||
)
|
);
|
||||||
const workspaceName = await createWorkspace(page, template)
|
const workspaceName = await createWorkspace(page, template);
|
||||||
|
|
||||||
// Verify that build options are default (not selected).
|
// Verify that build options are default (not selected).
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
||||||
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
||||||
])
|
]);
|
||||||
|
|
||||||
// Stop the workspace
|
// Stop the workspace
|
||||||
await stopWorkspace(page, workspaceName)
|
await stopWorkspace(page, workspaceName);
|
||||||
|
|
||||||
// Now, start the workspace with ephemeral parameters selected.
|
// Now, start the workspace with ephemeral parameters selected.
|
||||||
const buildParameters = [
|
const buildParameters = [
|
||||||
{ name: firstBuildOption.name, value: "AAAAA" },
|
{ name: firstBuildOption.name, value: "AAAAA" },
|
||||||
{ name: secondBuildOption.name, value: "true" },
|
{ name: secondBuildOption.name, value: "true" },
|
||||||
]
|
];
|
||||||
|
|
||||||
await buildWorkspaceWithParameters(
|
await buildWorkspaceWithParameters(
|
||||||
page,
|
page,
|
||||||
workspaceName,
|
workspaceName,
|
||||||
richParameters,
|
richParameters,
|
||||||
buildParameters,
|
buildParameters,
|
||||||
)
|
);
|
||||||
|
|
||||||
// Verify that build options are default (not selected).
|
// Verify that build options are default (not selected).
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
||||||
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
||||||
])
|
]);
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { test } from "@playwright/test"
|
import { test } from "@playwright/test";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createTemplate,
|
createTemplate,
|
||||||
|
@ -8,7 +8,7 @@ import {
|
||||||
updateWorkspace,
|
updateWorkspace,
|
||||||
updateWorkspaceParameters,
|
updateWorkspaceParameters,
|
||||||
verifyParameters,
|
verifyParameters,
|
||||||
} from "../helpers"
|
} from "../helpers";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fifthParameter,
|
fifthParameter,
|
||||||
|
@ -16,119 +16,119 @@ import {
|
||||||
secondParameter,
|
secondParameter,
|
||||||
sixthParameter,
|
sixthParameter,
|
||||||
secondBuildOption,
|
secondBuildOption,
|
||||||
} from "../parameters"
|
} from "../parameters";
|
||||||
import { RichParameter } from "../provisionerGenerated"
|
import { RichParameter } from "../provisionerGenerated";
|
||||||
import { beforeCoderTest } from "../hooks"
|
import { beforeCoderTest } from "../hooks";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page))
|
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||||
|
|
||||||
test("update workspace, new optional, immutable parameter added", async ({
|
test("update workspace, new optional, immutable parameter added", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const richParameters: RichParameter[] = [firstParameter, secondParameter]
|
const richParameters: RichParameter[] = [firstParameter, secondParameter];
|
||||||
const template = await createTemplate(
|
const template = await createTemplate(
|
||||||
page,
|
page,
|
||||||
echoResponsesWithParameters(richParameters),
|
echoResponsesWithParameters(richParameters),
|
||||||
)
|
);
|
||||||
|
|
||||||
const workspaceName = await createWorkspace(page, template)
|
const workspaceName = await createWorkspace(page, template);
|
||||||
|
|
||||||
// Verify that parameter values are default.
|
// Verify that parameter values are default.
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
{ name: firstParameter.name, value: firstParameter.defaultValue },
|
{ name: firstParameter.name, value: firstParameter.defaultValue },
|
||||||
{ name: secondParameter.name, value: secondParameter.defaultValue },
|
{ name: secondParameter.name, value: secondParameter.defaultValue },
|
||||||
])
|
]);
|
||||||
|
|
||||||
// Push updated template.
|
// Push updated template.
|
||||||
const updatedRichParameters = [...richParameters, fifthParameter]
|
const updatedRichParameters = [...richParameters, fifthParameter];
|
||||||
await updateTemplate(
|
await updateTemplate(
|
||||||
page,
|
page,
|
||||||
template,
|
template,
|
||||||
echoResponsesWithParameters(updatedRichParameters),
|
echoResponsesWithParameters(updatedRichParameters),
|
||||||
)
|
);
|
||||||
|
|
||||||
// Now, update the workspace, and select the value for immutable parameter.
|
// Now, update the workspace, and select the value for immutable parameter.
|
||||||
await updateWorkspace(page, workspaceName, updatedRichParameters, [
|
await updateWorkspace(page, workspaceName, updatedRichParameters, [
|
||||||
{ name: fifthParameter.name, value: fifthParameter.options[0].value },
|
{ name: fifthParameter.name, value: fifthParameter.options[0].value },
|
||||||
])
|
]);
|
||||||
|
|
||||||
// Verify parameter values.
|
// Verify parameter values.
|
||||||
await verifyParameters(page, workspaceName, updatedRichParameters, [
|
await verifyParameters(page, workspaceName, updatedRichParameters, [
|
||||||
{ name: firstParameter.name, value: firstParameter.defaultValue },
|
{ name: firstParameter.name, value: firstParameter.defaultValue },
|
||||||
{ name: secondParameter.name, value: secondParameter.defaultValue },
|
{ name: secondParameter.name, value: secondParameter.defaultValue },
|
||||||
{ name: fifthParameter.name, value: fifthParameter.options[0].value },
|
{ name: fifthParameter.name, value: fifthParameter.options[0].value },
|
||||||
])
|
]);
|
||||||
})
|
});
|
||||||
|
|
||||||
test("update workspace, new required, mutable parameter added", async ({
|
test("update workspace, new required, mutable parameter added", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const richParameters: RichParameter[] = [firstParameter, secondParameter]
|
const richParameters: RichParameter[] = [firstParameter, secondParameter];
|
||||||
const template = await createTemplate(
|
const template = await createTemplate(
|
||||||
page,
|
page,
|
||||||
echoResponsesWithParameters(richParameters),
|
echoResponsesWithParameters(richParameters),
|
||||||
)
|
);
|
||||||
|
|
||||||
const workspaceName = await createWorkspace(page, template)
|
const workspaceName = await createWorkspace(page, template);
|
||||||
|
|
||||||
// Verify that parameter values are default.
|
// Verify that parameter values are default.
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
{ name: firstParameter.name, value: firstParameter.defaultValue },
|
{ name: firstParameter.name, value: firstParameter.defaultValue },
|
||||||
{ name: secondParameter.name, value: secondParameter.defaultValue },
|
{ name: secondParameter.name, value: secondParameter.defaultValue },
|
||||||
])
|
]);
|
||||||
|
|
||||||
// Push updated template.
|
// Push updated template.
|
||||||
const updatedRichParameters = [...richParameters, sixthParameter]
|
const updatedRichParameters = [...richParameters, sixthParameter];
|
||||||
await updateTemplate(
|
await updateTemplate(
|
||||||
page,
|
page,
|
||||||
template,
|
template,
|
||||||
echoResponsesWithParameters(updatedRichParameters),
|
echoResponsesWithParameters(updatedRichParameters),
|
||||||
)
|
);
|
||||||
|
|
||||||
// Now, update the workspace, and provide the parameter value.
|
// Now, update the workspace, and provide the parameter value.
|
||||||
const buildParameters = [{ name: sixthParameter.name, value: "99" }]
|
const buildParameters = [{ name: sixthParameter.name, value: "99" }];
|
||||||
await updateWorkspace(
|
await updateWorkspace(
|
||||||
page,
|
page,
|
||||||
workspaceName,
|
workspaceName,
|
||||||
updatedRichParameters,
|
updatedRichParameters,
|
||||||
buildParameters,
|
buildParameters,
|
||||||
)
|
);
|
||||||
|
|
||||||
// Verify parameter values.
|
// Verify parameter values.
|
||||||
await verifyParameters(page, workspaceName, updatedRichParameters, [
|
await verifyParameters(page, workspaceName, updatedRichParameters, [
|
||||||
{ name: firstParameter.name, value: firstParameter.defaultValue },
|
{ name: firstParameter.name, value: firstParameter.defaultValue },
|
||||||
{ name: secondParameter.name, value: secondParameter.defaultValue },
|
{ name: secondParameter.name, value: secondParameter.defaultValue },
|
||||||
...buildParameters,
|
...buildParameters,
|
||||||
])
|
]);
|
||||||
})
|
});
|
||||||
|
|
||||||
test("update workspace with ephemeral parameter enabled", async ({ page }) => {
|
test("update workspace with ephemeral parameter enabled", async ({ page }) => {
|
||||||
const richParameters: RichParameter[] = [firstParameter, secondBuildOption]
|
const richParameters: RichParameter[] = [firstParameter, secondBuildOption];
|
||||||
const template = await createTemplate(
|
const template = await createTemplate(
|
||||||
page,
|
page,
|
||||||
echoResponsesWithParameters(richParameters),
|
echoResponsesWithParameters(richParameters),
|
||||||
)
|
);
|
||||||
|
|
||||||
const workspaceName = await createWorkspace(page, template)
|
const workspaceName = await createWorkspace(page, template);
|
||||||
|
|
||||||
// Verify that parameter values are default.
|
// Verify that parameter values are default.
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
{ name: firstParameter.name, value: firstParameter.defaultValue },
|
{ name: firstParameter.name, value: firstParameter.defaultValue },
|
||||||
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
||||||
])
|
]);
|
||||||
|
|
||||||
// Now, update the workspace, and select the value for ephemeral parameter.
|
// Now, update the workspace, and select the value for ephemeral parameter.
|
||||||
const buildParameters = [{ name: secondBuildOption.name, value: "true" }]
|
const buildParameters = [{ name: secondBuildOption.name, value: "true" }];
|
||||||
await updateWorkspaceParameters(
|
await updateWorkspaceParameters(
|
||||||
page,
|
page,
|
||||||
workspaceName,
|
workspaceName,
|
||||||
richParameters,
|
richParameters,
|
||||||
buildParameters,
|
buildParameters,
|
||||||
)
|
);
|
||||||
|
|
||||||
// Verify that parameter values are default.
|
// Verify that parameter values are default.
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
{ name: firstParameter.name, value: firstParameter.defaultValue },
|
{ name: firstParameter.name, value: firstParameter.defaultValue },
|
||||||
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
||||||
])
|
]);
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { test } from "@playwright/test"
|
import { test } from "@playwright/test";
|
||||||
import {
|
import {
|
||||||
createTemplate,
|
createTemplate,
|
||||||
createWorkspace,
|
createWorkspace,
|
||||||
startAgent,
|
startAgent,
|
||||||
stopAgent,
|
stopAgent,
|
||||||
} from "../helpers"
|
} from "../helpers";
|
||||||
import { randomUUID } from "crypto"
|
import { randomUUID } from "crypto";
|
||||||
import { beforeCoderTest } from "../hooks"
|
import { beforeCoderTest } from "../hooks";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => await beforeCoderTest(page))
|
test.beforeEach(async ({ page }) => await beforeCoderTest(page));
|
||||||
|
|
||||||
test("web terminal", async ({ context, page }) => {
|
test("web terminal", async ({ context, page }) => {
|
||||||
const token = randomUUID()
|
const token = randomUUID();
|
||||||
const template = await createTemplate(page, {
|
const template = await createTemplate(page, {
|
||||||
apply: [
|
apply: [
|
||||||
{
|
{
|
||||||
|
@ -31,29 +31,29 @@ test("web terminal", async ({ context, page }) => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
await createWorkspace(page, template)
|
await createWorkspace(page, template);
|
||||||
const agent = await startAgent(page, token)
|
const agent = await startAgent(page, token);
|
||||||
|
|
||||||
// Wait for the web terminal to open in a new tab
|
// Wait for the web terminal to open in a new tab
|
||||||
const pagePromise = context.waitForEvent("page")
|
const pagePromise = context.waitForEvent("page");
|
||||||
await page.getByTestId("terminal").click()
|
await page.getByTestId("terminal").click();
|
||||||
const terminal = await pagePromise
|
const terminal = await pagePromise;
|
||||||
await terminal.waitForLoadState("domcontentloaded")
|
await terminal.waitForLoadState("domcontentloaded");
|
||||||
|
|
||||||
// Ensure that we can type in it
|
// Ensure that we can type in it
|
||||||
await terminal.keyboard.type("echo hello")
|
await terminal.keyboard.type("echo hello");
|
||||||
await terminal.keyboard.press("Enter")
|
await terminal.keyboard.press("Enter");
|
||||||
|
|
||||||
const locator = terminal.locator("text=hello")
|
const locator = terminal.locator("text=hello");
|
||||||
|
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const items = await locator.all()
|
const items = await locator.all();
|
||||||
// Make sure the text came back
|
// Make sure the text came back
|
||||||
if (items.length === 2) {
|
if (items.length === 2) {
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
await new Promise((r) => setTimeout(r, 250))
|
await new Promise((r) => setTimeout(r, 250));
|
||||||
}
|
}
|
||||||
await stopAgent(agent)
|
await stopAgent(agent);
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Toggle eslint --fix by specifying the `FIX` env.
|
// Toggle eslint --fix by specifying the `FIX` env.
|
||||||
const fix = !!process.env.FIX
|
const fix = !!process.env.FIX;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
cliOptions: {
|
cliOptions: {
|
||||||
|
@ -10,4 +10,4 @@ module.exports = {
|
||||||
resolvePluginsRelativeTo: ".",
|
resolvePluginsRelativeTo: ".",
|
||||||
maxWarnings: 0,
|
maxWarnings: 0,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -67,4 +67,4 @@ module.exports = {
|
||||||
"!<rootDir>/out/**/*.*",
|
"!<rootDir>/out/**/*.*",
|
||||||
"!<rootDir>/storybook-static/**/*.*",
|
"!<rootDir>/storybook-static/**/*.*",
|
||||||
],
|
],
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import "@testing-library/jest-dom"
|
import "@testing-library/jest-dom";
|
||||||
import { cleanup } from "@testing-library/react"
|
import { cleanup } from "@testing-library/react";
|
||||||
import crypto from "crypto"
|
import crypto from "crypto";
|
||||||
import { server } from "./src/testHelpers/server"
|
import { server } from "./src/testHelpers/server";
|
||||||
import "jest-location-mock"
|
import "jest-location-mock";
|
||||||
import { TextEncoder, TextDecoder } from "util"
|
import { TextEncoder, TextDecoder } from "util";
|
||||||
import { Blob } from "buffer"
|
import { Blob } from "buffer";
|
||||||
import jestFetchMock from "jest-fetch-mock"
|
import jestFetchMock from "jest-fetch-mock";
|
||||||
import { ProxyLatencyReport } from "contexts/useProxyLatency"
|
import { ProxyLatencyReport } from "contexts/useProxyLatency";
|
||||||
import { Region } from "api/typesGenerated"
|
import { Region } from "api/typesGenerated";
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react";
|
||||||
|
|
||||||
jestFetchMock.enableMocks()
|
jestFetchMock.enableMocks();
|
||||||
|
|
||||||
// useProxyLatency does some http requests to determine latency.
|
// useProxyLatency does some http requests to determine latency.
|
||||||
// This would fail unit testing, or at least make it very slow with
|
// This would fail unit testing, or at least make it very slow with
|
||||||
|
@ -21,7 +21,7 @@ jest.mock("contexts/useProxyLatency", () => ({
|
||||||
// Mocking the hook with a hook.
|
// Mocking the hook with a hook.
|
||||||
const proxyLatencies = useMemo(() => {
|
const proxyLatencies = useMemo(() => {
|
||||||
if (!proxies) {
|
if (!proxies) {
|
||||||
return {} as Record<string, ProxyLatencyReport>
|
return {} as Record<string, ProxyLatencyReport>;
|
||||||
}
|
}
|
||||||
return proxies.reduce(
|
return proxies.reduce(
|
||||||
(acc, proxy) => {
|
(acc, proxy) => {
|
||||||
|
@ -31,49 +31,49 @@ jest.mock("contexts/useProxyLatency", () => ({
|
||||||
// If you make this random it could break stories.
|
// If you make this random it could break stories.
|
||||||
latencyMS: 8,
|
latencyMS: 8,
|
||||||
at: new Date(),
|
at: new Date(),
|
||||||
}
|
};
|
||||||
return acc
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<string, ProxyLatencyReport>,
|
{} as Record<string, ProxyLatencyReport>,
|
||||||
)
|
);
|
||||||
}, [proxies])
|
}, [proxies]);
|
||||||
|
|
||||||
return { proxyLatencies, refetch: jest.fn() }
|
return { proxyLatencies, refetch: jest.fn() };
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
||||||
global.TextEncoder = TextEncoder
|
global.TextEncoder = TextEncoder;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom
|
||||||
global.TextDecoder = TextDecoder as any
|
global.TextDecoder = TextDecoder as any;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom
|
||||||
global.Blob = Blob as any
|
global.Blob = Blob as any;
|
||||||
|
|
||||||
// Polyfill the getRandomValues that is used on utils/random.ts
|
// Polyfill the getRandomValues that is used on utils/random.ts
|
||||||
Object.defineProperty(global.self, "crypto", {
|
Object.defineProperty(global.self, "crypto", {
|
||||||
value: {
|
value: {
|
||||||
getRandomValues: function (buffer: Buffer) {
|
getRandomValues: function (buffer: Buffer) {
|
||||||
return crypto.randomFillSync(buffer)
|
return crypto.randomFillSync(buffer);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
// Establish API mocking before all tests through MSW.
|
// Establish API mocking before all tests through MSW.
|
||||||
beforeAll(() =>
|
beforeAll(() =>
|
||||||
server.listen({
|
server.listen({
|
||||||
onUnhandledRequest: "warn",
|
onUnhandledRequest: "warn",
|
||||||
}),
|
}),
|
||||||
)
|
);
|
||||||
|
|
||||||
// Reset any request handlers that we may add during the tests,
|
// Reset any request handlers that we may add during the tests,
|
||||||
// so they don't affect other tests.
|
// so they don't affect other tests.
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup()
|
cleanup();
|
||||||
server.resetHandlers()
|
server.resetHandlers();
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks();
|
||||||
})
|
});
|
||||||
|
|
||||||
// Clean up after the tests are finished.
|
// Clean up after the tests are finished.
|
||||||
afterAll(() => server.close())
|
afterAll(() => server.close());
|
||||||
|
|
||||||
// This is needed because we are compiling under `--isolatedModules`
|
// This is needed because we are compiling under `--isolatedModules`
|
||||||
export {}
|
export {};
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
declare module "@emoji-mart/react" {
|
declare module "@emoji-mart/react" {
|
||||||
const Picker: React.FC<{
|
const Picker: React.FC<{
|
||||||
theme: "dark" | "light"
|
theme: "dark" | "light";
|
||||||
data: Record<string, unknown>
|
data: Record<string, unknown>;
|
||||||
onEmojiSelect: (emojiData: { unified: string }) => void
|
onEmojiSelect: (emojiData: { unified: string }) => void;
|
||||||
}>
|
}>;
|
||||||
|
|
||||||
export default Picker
|
export default Picker;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
declare module "eventsourcemock"
|
declare module "eventsourcemock";
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import "i18next"
|
import "i18next";
|
||||||
|
|
||||||
// https://github.com/i18next/react-i18next/issues/1543#issuecomment-1528679591
|
// https://github.com/i18next/react-i18next/issues/1543#issuecomment-1528679591
|
||||||
declare module "i18next" {
|
declare module "i18next" {
|
||||||
interface TypeOptions {
|
interface TypeOptions {
|
||||||
returnNull: false
|
returnNull: false;
|
||||||
allowObjectInHTMLChildren: false
|
allowObjectInHTMLChildren: false;
|
||||||
}
|
}
|
||||||
export function t<T>(s: string): T
|
export function t<T>(s: string): T;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { PaletteColor, PaletteColorOptions, Theme } from "@mui/material/styles"
|
import { PaletteColor, PaletteColorOptions, Theme } from "@mui/material/styles";
|
||||||
|
|
||||||
declare module "@mui/styles/defaultTheme" {
|
declare module "@mui/styles/defaultTheme" {
|
||||||
interface DefaultTheme extends Theme {}
|
interface DefaultTheme extends Theme {}
|
||||||
|
@ -6,20 +6,20 @@ declare module "@mui/styles/defaultTheme" {
|
||||||
|
|
||||||
declare module "@mui/material/styles" {
|
declare module "@mui/material/styles" {
|
||||||
interface TypeBackground {
|
interface TypeBackground {
|
||||||
paperLight: string
|
paperLight: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Palette {
|
interface Palette {
|
||||||
neutral: PaletteColor
|
neutral: PaletteColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PaletteOptions {
|
interface PaletteOptions {
|
||||||
neutral?: PaletteColorOptions
|
neutral?: PaletteColorOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "@mui/material/Button" {
|
declare module "@mui/material/Button" {
|
||||||
interface ButtonPropsColorOverrides {
|
interface ButtonPropsColorOverrides {
|
||||||
neutral: true
|
neutral: true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,186 +1,188 @@
|
||||||
import { FullScreenLoader } from "components/Loader/FullScreenLoader"
|
import { FullScreenLoader } from "components/Loader/FullScreenLoader";
|
||||||
import { TemplateLayout } from "components/TemplateLayout/TemplateLayout"
|
import { TemplateLayout } from "components/TemplateLayout/TemplateLayout";
|
||||||
import { UsersLayout } from "components/UsersLayout/UsersLayout"
|
import { UsersLayout } from "components/UsersLayout/UsersLayout";
|
||||||
import IndexPage from "pages"
|
import IndexPage from "pages";
|
||||||
import AuditPage from "pages/AuditPage/AuditPage"
|
import AuditPage from "pages/AuditPage/AuditPage";
|
||||||
import GroupsPage from "pages/GroupsPage/GroupsPage"
|
import GroupsPage from "pages/GroupsPage/GroupsPage";
|
||||||
import LoginPage from "pages/LoginPage/LoginPage"
|
import LoginPage from "pages/LoginPage/LoginPage";
|
||||||
import { SetupPage } from "pages/SetupPage/SetupPage"
|
import { SetupPage } from "pages/SetupPage/SetupPage";
|
||||||
import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage"
|
import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage";
|
||||||
import TemplatesPage from "pages/TemplatesPage/TemplatesPage"
|
import TemplatesPage from "pages/TemplatesPage/TemplatesPage";
|
||||||
import UsersPage from "pages/UsersPage/UsersPage"
|
import UsersPage from "pages/UsersPage/UsersPage";
|
||||||
import WorkspacesPage from "pages/WorkspacesPage/WorkspacesPage"
|
import WorkspacesPage from "pages/WorkspacesPage/WorkspacesPage";
|
||||||
import { FC, lazy, Suspense } from "react"
|
import { FC, lazy, Suspense } from "react";
|
||||||
import { Route, Routes, BrowserRouter as Router } from "react-router-dom"
|
import { Route, Routes, BrowserRouter as Router } from "react-router-dom";
|
||||||
import { DashboardLayout } from "./components/Dashboard/DashboardLayout"
|
import { DashboardLayout } from "./components/Dashboard/DashboardLayout";
|
||||||
import { RequireAuth } from "./components/RequireAuth/RequireAuth"
|
import { RequireAuth } from "./components/RequireAuth/RequireAuth";
|
||||||
import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout"
|
import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout";
|
||||||
import { DeploySettingsLayout } from "components/DeploySettingsLayout/DeploySettingsLayout"
|
import { DeploySettingsLayout } from "components/DeploySettingsLayout/DeploySettingsLayout";
|
||||||
import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout"
|
import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout";
|
||||||
import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"
|
import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout";
|
||||||
|
|
||||||
// Lazy load pages
|
// Lazy load pages
|
||||||
// - Pages that are secondary, not in the main navigation or not usually accessed
|
// - Pages that are secondary, not in the main navigation or not usually accessed
|
||||||
// - Pages that use heavy dependencies like charts or time libraries
|
// - Pages that use heavy dependencies like charts or time libraries
|
||||||
const NotFoundPage = lazy(() => import("./pages/404Page/404Page"))
|
const NotFoundPage = lazy(() => import("./pages/404Page/404Page"));
|
||||||
const CliAuthenticationPage = lazy(
|
const CliAuthenticationPage = lazy(
|
||||||
() => import("./pages/CliAuthPage/CliAuthPage"),
|
() => import("./pages/CliAuthPage/CliAuthPage"),
|
||||||
)
|
);
|
||||||
const AccountPage = lazy(
|
const AccountPage = lazy(
|
||||||
() => import("./pages/UserSettingsPage/AccountPage/AccountPage"),
|
() => import("./pages/UserSettingsPage/AccountPage/AccountPage"),
|
||||||
)
|
);
|
||||||
const SecurityPage = lazy(
|
const SecurityPage = lazy(
|
||||||
() => import("./pages/UserSettingsPage/SecurityPage/SecurityPage"),
|
() => import("./pages/UserSettingsPage/SecurityPage/SecurityPage"),
|
||||||
)
|
);
|
||||||
const SSHKeysPage = lazy(
|
const SSHKeysPage = lazy(
|
||||||
() => import("./pages/UserSettingsPage/SSHKeysPage/SSHKeysPage"),
|
() => import("./pages/UserSettingsPage/SSHKeysPage/SSHKeysPage"),
|
||||||
)
|
);
|
||||||
const TokensPage = lazy(
|
const TokensPage = lazy(
|
||||||
() => import("./pages/UserSettingsPage/TokensPage/TokensPage"),
|
() => import("./pages/UserSettingsPage/TokensPage/TokensPage"),
|
||||||
)
|
);
|
||||||
const WorkspaceProxyPage = lazy(
|
const WorkspaceProxyPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import("./pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyPage"),
|
import("./pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyPage"),
|
||||||
)
|
);
|
||||||
const CreateUserPage = lazy(
|
const CreateUserPage = lazy(
|
||||||
() => import("./pages/CreateUserPage/CreateUserPage"),
|
() => import("./pages/CreateUserPage/CreateUserPage"),
|
||||||
)
|
);
|
||||||
const WorkspaceBuildPage = lazy(
|
const WorkspaceBuildPage = lazy(
|
||||||
() => import("./pages/WorkspaceBuildPage/WorkspaceBuildPage"),
|
() => import("./pages/WorkspaceBuildPage/WorkspaceBuildPage"),
|
||||||
)
|
);
|
||||||
const WorkspacePage = lazy(() => import("./pages/WorkspacePage/WorkspacePage"))
|
const WorkspacePage = lazy(() => import("./pages/WorkspacePage/WorkspacePage"));
|
||||||
const WorkspaceSchedulePage = lazy(
|
const WorkspaceSchedulePage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
"./pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage"
|
"./pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage"
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
const WorkspaceParametersPage = lazy(
|
const WorkspaceParametersPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
"./pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage"
|
"./pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage"
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage"))
|
const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage"));
|
||||||
const TemplatePermissionsPage = lazy(
|
const TemplatePermissionsPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
"./pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage"
|
"./pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPage"
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
const TemplateSummaryPage = lazy(
|
const TemplateSummaryPage = lazy(
|
||||||
() => import("./pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage"),
|
() => import("./pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage"),
|
||||||
)
|
);
|
||||||
const CreateWorkspacePage = lazy(
|
const CreateWorkspacePage = lazy(
|
||||||
() => import("./pages/CreateWorkspacePage/CreateWorkspacePage"),
|
() => import("./pages/CreateWorkspacePage/CreateWorkspacePage"),
|
||||||
)
|
);
|
||||||
const CreateGroupPage = lazy(() => import("./pages/GroupsPage/CreateGroupPage"))
|
const CreateGroupPage = lazy(
|
||||||
const GroupPage = lazy(() => import("./pages/GroupsPage/GroupPage"))
|
() => import("./pages/GroupsPage/CreateGroupPage"),
|
||||||
|
);
|
||||||
|
const GroupPage = lazy(() => import("./pages/GroupsPage/GroupPage"));
|
||||||
const SettingsGroupPage = lazy(
|
const SettingsGroupPage = lazy(
|
||||||
() => import("./pages/GroupsPage/SettingsGroupPage"),
|
() => import("./pages/GroupsPage/SettingsGroupPage"),
|
||||||
)
|
);
|
||||||
const GeneralSettingsPage = lazy(
|
const GeneralSettingsPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
"./pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage"
|
"./pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage"
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
const SecuritySettingsPage = lazy(
|
const SecuritySettingsPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
"./pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage"
|
"./pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage"
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
const AppearanceSettingsPage = lazy(
|
const AppearanceSettingsPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
"./pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage"
|
"./pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage"
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
const UserAuthSettingsPage = lazy(
|
const UserAuthSettingsPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
"./pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPage"
|
"./pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPage"
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
const GitAuthSettingsPage = lazy(
|
const GitAuthSettingsPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
"./pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPage"
|
"./pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPage"
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
const NetworkSettingsPage = lazy(
|
const NetworkSettingsPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
"./pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPage"
|
"./pages/DeploySettingsPage/NetworkSettingsPage/NetworkSettingsPage"
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
const GitAuthPage = lazy(() => import("./pages/GitAuthPage/GitAuthPage"))
|
const GitAuthPage = lazy(() => import("./pages/GitAuthPage/GitAuthPage"));
|
||||||
const TemplateVersionPage = lazy(
|
const TemplateVersionPage = lazy(
|
||||||
() => import("./pages/TemplateVersionPage/TemplateVersionPage"),
|
() => import("./pages/TemplateVersionPage/TemplateVersionPage"),
|
||||||
)
|
);
|
||||||
const TemplateVersionEditorPage = lazy(
|
const TemplateVersionEditorPage = lazy(
|
||||||
() => import("./pages/TemplateVersionEditorPage/TemplateVersionEditorPage"),
|
() => import("./pages/TemplateVersionEditorPage/TemplateVersionEditorPage"),
|
||||||
)
|
);
|
||||||
const StarterTemplatesPage = lazy(
|
const StarterTemplatesPage = lazy(
|
||||||
() => import("./pages/StarterTemplatesPage/StarterTemplatesPage"),
|
() => import("./pages/StarterTemplatesPage/StarterTemplatesPage"),
|
||||||
)
|
);
|
||||||
const StarterTemplatePage = lazy(
|
const StarterTemplatePage = lazy(
|
||||||
() => import("pages/StarterTemplatePage/StarterTemplatePage"),
|
() => import("pages/StarterTemplatePage/StarterTemplatePage"),
|
||||||
)
|
);
|
||||||
const CreateTemplatePage = lazy(
|
const CreateTemplatePage = lazy(
|
||||||
() => import("./pages/CreateTemplatePage/CreateTemplatePage"),
|
() => import("./pages/CreateTemplatePage/CreateTemplatePage"),
|
||||||
)
|
);
|
||||||
const TemplateVariablesPage = lazy(
|
const TemplateVariablesPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
"./pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage"
|
"./pages/TemplateSettingsPage/TemplateVariablesPage/TemplateVariablesPage"
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
const WorkspaceSettingsPage = lazy(
|
const WorkspaceSettingsPage = lazy(
|
||||||
() => import("./pages/WorkspaceSettingsPage/WorkspaceSettingsPage"),
|
() => import("./pages/WorkspaceSettingsPage/WorkspaceSettingsPage"),
|
||||||
)
|
);
|
||||||
const CreateTokenPage = lazy(
|
const CreateTokenPage = lazy(
|
||||||
() => import("./pages/CreateTokenPage/CreateTokenPage"),
|
() => import("./pages/CreateTokenPage/CreateTokenPage"),
|
||||||
)
|
);
|
||||||
|
|
||||||
const TemplateDocsPage = lazy(
|
const TemplateDocsPage = lazy(
|
||||||
() => import("./pages/TemplatePage/TemplateDocsPage/TemplateDocsPage"),
|
() => import("./pages/TemplatePage/TemplateDocsPage/TemplateDocsPage"),
|
||||||
)
|
);
|
||||||
|
|
||||||
const TemplateFilesPage = lazy(
|
const TemplateFilesPage = lazy(
|
||||||
() => import("./pages/TemplatePage/TemplateFilesPage/TemplateFilesPage"),
|
() => import("./pages/TemplatePage/TemplateFilesPage/TemplateFilesPage"),
|
||||||
)
|
);
|
||||||
|
|
||||||
const TemplateVersionsPage = lazy(
|
const TemplateVersionsPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import("./pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage"),
|
import("./pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage"),
|
||||||
)
|
);
|
||||||
const TemplateSchedulePage = lazy(
|
const TemplateSchedulePage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
"./pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage"
|
"./pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage"
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
|
|
||||||
const LicensesSettingsPage = lazy(
|
const LicensesSettingsPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
"./pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage"
|
"./pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage"
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
const AddNewLicensePage = lazy(
|
const AddNewLicensePage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import("./pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage"),
|
import("./pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage"),
|
||||||
)
|
);
|
||||||
const TemplateEmbedPage = lazy(
|
const TemplateEmbedPage = lazy(
|
||||||
() => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"),
|
() => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"),
|
||||||
)
|
);
|
||||||
const TemplateInsightsPage = lazy(
|
const TemplateInsightsPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import("./pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage"),
|
import("./pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage"),
|
||||||
)
|
);
|
||||||
const HealthPage = lazy(() => import("./pages/HealthPage/HealthPage"))
|
const HealthPage = lazy(() => import("./pages/HealthPage/HealthPage"));
|
||||||
|
|
||||||
export const AppRouter: FC = () => {
|
export const AppRouter: FC = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -331,5 +333,5 @@ export const AppRouter: FC = () => {
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { inspect } from "@xstate/inspect"
|
import { inspect } from "@xstate/inspect";
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client";
|
||||||
import { Interpreter } from "xstate"
|
import { Interpreter } from "xstate";
|
||||||
import { App } from "./app"
|
import { App } from "./app";
|
||||||
import "./i18n"
|
import "./i18n";
|
||||||
|
|
||||||
// if this is a development build and the developer wants to inspect
|
// if this is a development build and the developer wants to inspect
|
||||||
// helpful to see realtime changes on the services
|
// helpful to see realtime changes on the services
|
||||||
|
@ -14,9 +14,9 @@ if (
|
||||||
inspect({
|
inspect({
|
||||||
url: "https://stately.ai/viz?inspect",
|
url: "https://stately.ai/viz?inspect",
|
||||||
iframe: false,
|
iframe: false,
|
||||||
})
|
});
|
||||||
// configure all XServices to use the inspector
|
// configure all XServices to use the inspector
|
||||||
Interpreter.defaultOptions.devTools = true
|
Interpreter.defaultOptions.devTools = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is the entry point for the app - where everything start.
|
// This is the entry point for the app - where everything start.
|
||||||
|
@ -28,13 +28,13 @@ const main = () => {
|
||||||
▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀
|
▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀
|
||||||
█▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █
|
█▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █
|
||||||
██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀
|
██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀
|
||||||
`)
|
`);
|
||||||
const element = document.getElementById("root")
|
const element = document.getElementById("root");
|
||||||
if (element === null) {
|
if (element === null) {
|
||||||
throw new Error("root element is null")
|
throw new Error("root element is null");
|
||||||
}
|
}
|
||||||
const root = createRoot(element)
|
const root = createRoot(element);
|
||||||
root.render(<App />)
|
root.render(<App />);
|
||||||
}
|
};
|
||||||
|
|
||||||
main()
|
main();
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export default jest.fn()
|
export default jest.fn();
|
||||||
|
|
|
@ -7,14 +7,14 @@ const editor = {
|
||||||
dispose: () => {
|
dispose: () => {
|
||||||
//
|
//
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
const monaco = {
|
const monaco = {
|
||||||
editor,
|
editor,
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = monaco
|
module.exports = monaco;
|
||||||
|
|
||||||
export {}
|
export {};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FC, PropsWithChildren } from "react"
|
import { FC, PropsWithChildren } from "react";
|
||||||
|
|
||||||
const ReactMarkdown: FC<PropsWithChildren<unknown>> = ({ children }) => {
|
const ReactMarkdown: FC<PropsWithChildren<unknown>> = ({ children }) => {
|
||||||
return <div data-testid="markdown">{children}</div>
|
return <div data-testid="markdown">{children}</div>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ReactMarkdown
|
export default ReactMarkdown;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import axios from "axios"
|
import axios from "axios";
|
||||||
import {
|
import {
|
||||||
MockTemplate,
|
MockTemplate,
|
||||||
MockTemplateVersionParameter1,
|
MockTemplateVersionParameter1,
|
||||||
|
@ -6,9 +6,9 @@ import {
|
||||||
MockWorkspace,
|
MockWorkspace,
|
||||||
MockWorkspaceBuild,
|
MockWorkspaceBuild,
|
||||||
MockWorkspaceBuildParameter1,
|
MockWorkspaceBuildParameter1,
|
||||||
} from "testHelpers/entities"
|
} from "testHelpers/entities";
|
||||||
import * as api from "./api"
|
import * as api from "./api";
|
||||||
import * as TypesGen from "./typesGenerated"
|
import * as TypesGen from "./typesGenerated";
|
||||||
|
|
||||||
describe("api.ts", () => {
|
describe("api.ts", () => {
|
||||||
describe("login", () => {
|
describe("login", () => {
|
||||||
|
@ -16,111 +16,111 @@ describe("api.ts", () => {
|
||||||
// given
|
// given
|
||||||
const loginResponse: TypesGen.LoginWithPasswordResponse = {
|
const loginResponse: TypesGen.LoginWithPasswordResponse = {
|
||||||
session_token: "abc_123_test",
|
session_token: "abc_123_test",
|
||||||
}
|
};
|
||||||
jest.spyOn(axios, "post").mockResolvedValueOnce({ data: loginResponse })
|
jest.spyOn(axios, "post").mockResolvedValueOnce({ data: loginResponse });
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await api.login("test", "123")
|
const result = await api.login("test", "123");
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(axios.post).toHaveBeenCalled()
|
expect(axios.post).toHaveBeenCalled();
|
||||||
expect(result).toStrictEqual(loginResponse)
|
expect(result).toStrictEqual(loginResponse);
|
||||||
})
|
});
|
||||||
|
|
||||||
it("should throw an error on 401", async () => {
|
it("should throw an error on 401", async () => {
|
||||||
// given
|
// given
|
||||||
// ..ensure that we await our expect assertion in async/await test
|
// ..ensure that we await our expect assertion in async/await test
|
||||||
expect.assertions(1)
|
expect.assertions(1);
|
||||||
const expectedError = {
|
const expectedError = {
|
||||||
message: "Validation failed",
|
message: "Validation failed",
|
||||||
errors: [{ field: "email", code: "email" }],
|
errors: [{ field: "email", code: "email" }],
|
||||||
}
|
};
|
||||||
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
|
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
|
||||||
return Promise.reject(expectedError)
|
return Promise.reject(expectedError);
|
||||||
})
|
});
|
||||||
axios.post = axiosMockPost
|
axios.post = axiosMockPost;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.login("test", "123")
|
await api.login("test", "123");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toStrictEqual(expectedError)
|
expect(error).toStrictEqual(expectedError);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
describe("logout", () => {
|
describe("logout", () => {
|
||||||
it("should return without erroring", async () => {
|
it("should return without erroring", async () => {
|
||||||
// given
|
// given
|
||||||
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
|
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
|
||||||
return Promise.resolve()
|
return Promise.resolve();
|
||||||
})
|
});
|
||||||
axios.post = axiosMockPost
|
axios.post = axiosMockPost;
|
||||||
|
|
||||||
// when
|
// when
|
||||||
await api.logout()
|
await api.logout();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(axiosMockPost).toHaveBeenCalled()
|
expect(axiosMockPost).toHaveBeenCalled();
|
||||||
})
|
});
|
||||||
|
|
||||||
it("should throw an error on 500", async () => {
|
it("should throw an error on 500", async () => {
|
||||||
// given
|
// given
|
||||||
// ..ensure that we await our expect assertion in async/await test
|
// ..ensure that we await our expect assertion in async/await test
|
||||||
expect.assertions(1)
|
expect.assertions(1);
|
||||||
const expectedError = {
|
const expectedError = {
|
||||||
message: "Failed to logout.",
|
message: "Failed to logout.",
|
||||||
}
|
};
|
||||||
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
|
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
|
||||||
return Promise.reject(expectedError)
|
return Promise.reject(expectedError);
|
||||||
})
|
});
|
||||||
axios.post = axiosMockPost
|
axios.post = axiosMockPost;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.logout()
|
await api.logout();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toStrictEqual(expectedError)
|
expect(error).toStrictEqual(expectedError);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
describe("getApiKey", () => {
|
describe("getApiKey", () => {
|
||||||
it("should return APIKeyResponse", async () => {
|
it("should return APIKeyResponse", async () => {
|
||||||
// given
|
// given
|
||||||
const apiKeyResponse: TypesGen.GenerateAPIKeyResponse = {
|
const apiKeyResponse: TypesGen.GenerateAPIKeyResponse = {
|
||||||
key: "abc_123_test",
|
key: "abc_123_test",
|
||||||
}
|
};
|
||||||
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
|
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
|
||||||
return Promise.resolve({ data: apiKeyResponse })
|
return Promise.resolve({ data: apiKeyResponse });
|
||||||
})
|
});
|
||||||
axios.post = axiosMockPost
|
axios.post = axiosMockPost;
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await api.getApiKey()
|
const result = await api.getApiKey();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(axiosMockPost).toHaveBeenCalled()
|
expect(axiosMockPost).toHaveBeenCalled();
|
||||||
expect(result).toStrictEqual(apiKeyResponse)
|
expect(result).toStrictEqual(apiKeyResponse);
|
||||||
})
|
});
|
||||||
|
|
||||||
it("should throw an error on 401", async () => {
|
it("should throw an error on 401", async () => {
|
||||||
// given
|
// given
|
||||||
// ..ensure that we await our expect assertion in async/await test
|
// ..ensure that we await our expect assertion in async/await test
|
||||||
expect.assertions(1)
|
expect.assertions(1);
|
||||||
const expectedError = {
|
const expectedError = {
|
||||||
message: "No Cookie!",
|
message: "No Cookie!",
|
||||||
}
|
};
|
||||||
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
|
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
|
||||||
return Promise.reject(expectedError)
|
return Promise.reject(expectedError);
|
||||||
})
|
});
|
||||||
axios.post = axiosMockPost
|
axios.post = axiosMockPost;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.getApiKey()
|
await api.getApiKey();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toStrictEqual(expectedError)
|
expect(error).toStrictEqual(expectedError);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
describe("getURLWithSearchParams - workspaces", () => {
|
describe("getURLWithSearchParams - workspaces", () => {
|
||||||
it.each<[string, TypesGen.WorkspaceFilter | undefined, string]>([
|
it.each<[string, TypesGen.WorkspaceFilter | undefined, string]>([
|
||||||
|
@ -141,10 +141,10 @@ describe("api.ts", () => {
|
||||||
])(
|
])(
|
||||||
`Workspaces - getURLWithSearchParams(%p, %p) returns %p`,
|
`Workspaces - getURLWithSearchParams(%p, %p) returns %p`,
|
||||||
(basePath, filter, expected) => {
|
(basePath, filter, expected) => {
|
||||||
expect(api.getURLWithSearchParams(basePath, filter)).toBe(expected)
|
expect(api.getURLWithSearchParams(basePath, filter)).toBe(expected);
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
describe("getURLWithSearchParams - users", () => {
|
describe("getURLWithSearchParams - users", () => {
|
||||||
it.each<[string, TypesGen.UsersRequest | undefined, string]>([
|
it.each<[string, TypesGen.UsersRequest | undefined, string]>([
|
||||||
|
@ -158,72 +158,72 @@ describe("api.ts", () => {
|
||||||
])(
|
])(
|
||||||
`Users - getURLWithSearchParams(%p, %p) returns %p`,
|
`Users - getURLWithSearchParams(%p, %p) returns %p`,
|
||||||
(basePath, filter, expected) => {
|
(basePath, filter, expected) => {
|
||||||
expect(api.getURLWithSearchParams(basePath, filter)).toBe(expected)
|
expect(api.getURLWithSearchParams(basePath, filter)).toBe(expected);
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
it("creates a build with start and the latest template", async () => {
|
it("creates a build with start and the latest template", async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(api, "postWorkspaceBuild")
|
.spyOn(api, "postWorkspaceBuild")
|
||||||
.mockResolvedValueOnce(MockWorkspaceBuild)
|
.mockResolvedValueOnce(MockWorkspaceBuild);
|
||||||
jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate)
|
jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate);
|
||||||
await api.updateWorkspace(MockWorkspace)
|
await api.updateWorkspace(MockWorkspace);
|
||||||
expect(api.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
|
expect(api.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
|
||||||
transition: "start",
|
transition: "start",
|
||||||
template_version_id: MockTemplate.active_version_id,
|
template_version_id: MockTemplate.active_version_id,
|
||||||
rich_parameter_values: [],
|
rich_parameter_values: [],
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
it("fails when having missing parameters", async () => {
|
it("fails when having missing parameters", async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(api, "postWorkspaceBuild")
|
.spyOn(api, "postWorkspaceBuild")
|
||||||
.mockResolvedValue(MockWorkspaceBuild)
|
.mockResolvedValue(MockWorkspaceBuild);
|
||||||
jest.spyOn(api, "getTemplate").mockResolvedValue(MockTemplate)
|
jest.spyOn(api, "getTemplate").mockResolvedValue(MockTemplate);
|
||||||
jest.spyOn(api, "getWorkspaceBuildParameters").mockResolvedValue([])
|
jest.spyOn(api, "getWorkspaceBuildParameters").mockResolvedValue([]);
|
||||||
jest
|
jest
|
||||||
.spyOn(api, "getTemplateVersionRichParameters")
|
.spyOn(api, "getTemplateVersionRichParameters")
|
||||||
.mockResolvedValue([
|
.mockResolvedValue([
|
||||||
MockTemplateVersionParameter1,
|
MockTemplateVersionParameter1,
|
||||||
{ ...MockTemplateVersionParameter2, mutable: false },
|
{ ...MockTemplateVersionParameter2, mutable: false },
|
||||||
])
|
]);
|
||||||
|
|
||||||
let error = new Error()
|
let error = new Error();
|
||||||
try {
|
try {
|
||||||
await api.updateWorkspace(MockWorkspace)
|
await api.updateWorkspace(MockWorkspace);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e as Error
|
error = e as Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(error).toBeInstanceOf(api.MissingBuildParameters)
|
expect(error).toBeInstanceOf(api.MissingBuildParameters);
|
||||||
// Verify if the correct missing parameters are being passed
|
// Verify if the correct missing parameters are being passed
|
||||||
expect((error as api.MissingBuildParameters).parameters).toEqual([
|
expect((error as api.MissingBuildParameters).parameters).toEqual([
|
||||||
MockTemplateVersionParameter1,
|
MockTemplateVersionParameter1,
|
||||||
{ ...MockTemplateVersionParameter2, mutable: false },
|
{ ...MockTemplateVersionParameter2, mutable: false },
|
||||||
])
|
]);
|
||||||
})
|
});
|
||||||
|
|
||||||
it("creates a build with the no parameters if it is already filled", async () => {
|
it("creates a build with the no parameters if it is already filled", async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(api, "postWorkspaceBuild")
|
.spyOn(api, "postWorkspaceBuild")
|
||||||
.mockResolvedValueOnce(MockWorkspaceBuild)
|
.mockResolvedValueOnce(MockWorkspaceBuild);
|
||||||
jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate)
|
jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate);
|
||||||
jest
|
jest
|
||||||
.spyOn(api, "getWorkspaceBuildParameters")
|
.spyOn(api, "getWorkspaceBuildParameters")
|
||||||
.mockResolvedValue([MockWorkspaceBuildParameter1])
|
.mockResolvedValue([MockWorkspaceBuildParameter1]);
|
||||||
jest
|
jest
|
||||||
.spyOn(api, "getTemplateVersionRichParameters")
|
.spyOn(api, "getTemplateVersionRichParameters")
|
||||||
.mockResolvedValue([
|
.mockResolvedValue([
|
||||||
{ ...MockTemplateVersionParameter1, required: true, mutable: false },
|
{ ...MockTemplateVersionParameter1, required: true, mutable: false },
|
||||||
])
|
]);
|
||||||
await api.updateWorkspace(MockWorkspace)
|
await api.updateWorkspace(MockWorkspace);
|
||||||
expect(api.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
|
expect(api.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
|
||||||
transition: "start",
|
transition: "start",
|
||||||
template_version_id: MockTemplate.active_version_id,
|
template_version_id: MockTemplate.active_version_id,
|
||||||
rich_parameter_values: [],
|
rich_parameter_values: [],
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,9 +1,9 @@
|
||||||
import { mockApiError } from "testHelpers/entities"
|
import { mockApiError } from "testHelpers/entities";
|
||||||
import {
|
import {
|
||||||
getValidationErrorMessage,
|
getValidationErrorMessage,
|
||||||
isApiError,
|
isApiError,
|
||||||
mapApiErrorToFieldErrors,
|
mapApiErrorToFieldErrors,
|
||||||
} from "./errors"
|
} from "./errors";
|
||||||
|
|
||||||
describe("isApiError", () => {
|
describe("isApiError", () => {
|
||||||
it("returns true when the object is an API Error", () => {
|
it("returns true when the object is an API Error", () => {
|
||||||
|
@ -16,17 +16,17 @@ describe("isApiError", () => {
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toBe(true)
|
).toBe(true);
|
||||||
})
|
});
|
||||||
|
|
||||||
it("returns false when the object is Error", () => {
|
it("returns false when the object is Error", () => {
|
||||||
expect(isApiError(new Error())).toBe(false)
|
expect(isApiError(new Error())).toBe(false);
|
||||||
})
|
});
|
||||||
|
|
||||||
it("returns false when the object is undefined", () => {
|
it("returns false when the object is undefined", () => {
|
||||||
expect(isApiError(undefined)).toBe(false)
|
expect(isApiError(undefined)).toBe(false);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
describe("mapApiErrorToFieldErrors", () => {
|
describe("mapApiErrorToFieldErrors", () => {
|
||||||
it("returns correct field errors", () => {
|
it("returns correct field errors", () => {
|
||||||
|
@ -39,9 +39,9 @@ describe("mapApiErrorToFieldErrors", () => {
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
username: "Username is already in use",
|
username: "Username is already in use",
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
describe("getValidationErrorMessage", () => {
|
describe("getValidationErrorMessage", () => {
|
||||||
it("returns multiple validation messages", () => {
|
it("returns multiple validation messages", () => {
|
||||||
|
@ -63,14 +63,14 @@ describe("getValidationErrorMessage", () => {
|
||||||
),
|
),
|
||||||
).toEqual(
|
).toEqual(
|
||||||
`Query param "status" has invalid value: "inactive" is not a valid user status\nQuery element "role:a:e" can only contain 1 ':'`,
|
`Query param "status" has invalid value: "inactive" is not a valid user status\nQuery element "role:a:e" can only contain 1 ':'`,
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
it("non-API error returns empty validation message", () => {
|
it("non-API error returns empty validation message", () => {
|
||||||
expect(
|
expect(
|
||||||
getValidationErrorMessage(new Error("Invalid user search query.")),
|
getValidationErrorMessage(new Error("Invalid user search query.")),
|
||||||
).toEqual("")
|
).toEqual("");
|
||||||
})
|
});
|
||||||
|
|
||||||
it("no validations field returns empty validation message", () => {
|
it("no validations field returns empty validation message", () => {
|
||||||
expect(
|
expect(
|
||||||
|
@ -80,6 +80,6 @@ describe("getValidationErrorMessage", () => {
|
||||||
detail: `Query element "role:a:e" can only contain 1 ':'`,
|
detail: `Query element "role:a:e" can only contain 1 ':'`,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toEqual("")
|
).toEqual("");
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,56 +1,56 @@
|
||||||
import axios, { AxiosError, AxiosResponse } from "axios"
|
import axios, { AxiosError, AxiosResponse } from "axios";
|
||||||
|
|
||||||
const Language = {
|
const Language = {
|
||||||
errorsByCode: {
|
errorsByCode: {
|
||||||
defaultErrorCode: "Invalid value",
|
defaultErrorCode: "Invalid value",
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface FieldError {
|
export interface FieldError {
|
||||||
field: string
|
field: string;
|
||||||
detail: string
|
detail: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FieldErrors = Record<FieldError["field"], FieldError["detail"]>
|
export type FieldErrors = Record<FieldError["field"], FieldError["detail"]>;
|
||||||
|
|
||||||
export interface ApiErrorResponse {
|
export interface ApiErrorResponse {
|
||||||
message: string
|
message: string;
|
||||||
detail?: string
|
detail?: string;
|
||||||
validations?: FieldError[]
|
validations?: FieldError[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApiError = AxiosError<ApiErrorResponse> & {
|
export type ApiError = AxiosError<ApiErrorResponse> & {
|
||||||
response: AxiosResponse<ApiErrorResponse>
|
response: AxiosResponse<ApiErrorResponse>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const isApiError = (err: unknown): err is ApiError => {
|
export const isApiError = (err: unknown): err is ApiError => {
|
||||||
return axios.isAxiosError(err) && err.response !== undefined
|
return axios.isAxiosError(err) && err.response !== undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const hasApiFieldErrors = (error: ApiError): boolean =>
|
export const hasApiFieldErrors = (error: ApiError): boolean =>
|
||||||
Array.isArray(error.response.data.validations)
|
Array.isArray(error.response.data.validations);
|
||||||
|
|
||||||
export const isApiValidationError = (error: unknown): error is ApiError => {
|
export const isApiValidationError = (error: unknown): error is ApiError => {
|
||||||
return isApiError(error) && hasApiFieldErrors(error)
|
return isApiError(error) && hasApiFieldErrors(error);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const hasError = (error: unknown) =>
|
export const hasError = (error: unknown) =>
|
||||||
error !== undefined && error !== null
|
error !== undefined && error !== null;
|
||||||
|
|
||||||
export const mapApiErrorToFieldErrors = (
|
export const mapApiErrorToFieldErrors = (
|
||||||
apiErrorResponse: ApiErrorResponse,
|
apiErrorResponse: ApiErrorResponse,
|
||||||
): FieldErrors => {
|
): FieldErrors => {
|
||||||
const result: FieldErrors = {}
|
const result: FieldErrors = {};
|
||||||
|
|
||||||
if (apiErrorResponse.validations) {
|
if (apiErrorResponse.validations) {
|
||||||
for (const error of apiErrorResponse.validations) {
|
for (const error of apiErrorResponse.validations) {
|
||||||
result[error.field] =
|
result[error.field] =
|
||||||
error.detail || Language.errorsByCode.defaultErrorCode
|
error.detail || Language.errorsByCode.defaultErrorCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -66,7 +66,7 @@ export const getErrorMessage = (
|
||||||
? error.response.data.message
|
? error.response.data.message
|
||||||
: error instanceof Error
|
: error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: defaultMessage
|
: defaultMessage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -78,13 +78,13 @@ export const getValidationErrorMessage = (error: unknown): string => {
|
||||||
const validationErrors =
|
const validationErrors =
|
||||||
isApiError(error) && error.response.data.validations
|
isApiError(error) && error.response.data.validations
|
||||||
? error.response.data.validations
|
? error.response.data.validations
|
||||||
: []
|
: [];
|
||||||
return validationErrors.map((error) => error.detail).join("\n")
|
return validationErrors.map((error) => error.detail).join("\n");
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getErrorDetail = (error: unknown): string | undefined | null =>
|
export const getErrorDetail = (error: unknown): string | undefined | null =>
|
||||||
isApiError(error)
|
isApiError(error)
|
||||||
? error.response.data.detail
|
? error.response.data.detail
|
||||||
: error instanceof Error
|
: error instanceof Error
|
||||||
? `Please check the developer console for more details.`
|
? `Please check the developer console for more details.`
|
||||||
: null
|
: null;
|
||||||
|
|
|
@ -1,40 +1,40 @@
|
||||||
import { DeploymentValues } from "./typesGenerated"
|
import { DeploymentValues } from "./typesGenerated";
|
||||||
|
|
||||||
export interface UserAgent {
|
export interface UserAgent {
|
||||||
readonly browser: string
|
readonly browser: string;
|
||||||
readonly device: string
|
readonly device: string;
|
||||||
readonly ip_address: string
|
readonly ip_address: string;
|
||||||
readonly os: string
|
readonly os: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReconnectingPTYRequest {
|
export interface ReconnectingPTYRequest {
|
||||||
readonly data?: string
|
readonly data?: string;
|
||||||
readonly height?: number
|
readonly height?: number;
|
||||||
readonly width?: number
|
readonly width?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkspaceBuildTransition = "start" | "stop" | "delete"
|
export type WorkspaceBuildTransition = "start" | "stop" | "delete";
|
||||||
|
|
||||||
export type Message = { message: string }
|
export type Message = { message: string };
|
||||||
|
|
||||||
export interface DeploymentGroup {
|
export interface DeploymentGroup {
|
||||||
readonly name: string
|
readonly name: string;
|
||||||
readonly parent?: DeploymentGroup
|
readonly parent?: DeploymentGroup;
|
||||||
readonly description: string
|
readonly description: string;
|
||||||
readonly children: DeploymentGroup[]
|
readonly children: DeploymentGroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeploymentOption {
|
export interface DeploymentOption {
|
||||||
readonly name: string
|
readonly name: string;
|
||||||
readonly description: string
|
readonly description: string;
|
||||||
readonly flag: string
|
readonly flag: string;
|
||||||
readonly flag_shorthand: string
|
readonly flag_shorthand: string;
|
||||||
readonly value: unknown
|
readonly value: unknown;
|
||||||
readonly hidden: boolean
|
readonly hidden: boolean;
|
||||||
readonly group?: DeploymentGroup
|
readonly group?: DeploymentGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DeploymentConfig = {
|
export type DeploymentConfig = {
|
||||||
readonly config: DeploymentValues
|
readonly config: DeploymentValues;
|
||||||
readonly options: DeploymentOption[]
|
readonly options: DeploymentOption[];
|
||||||
}
|
};
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,14 +1,14 @@
|
||||||
import CssBaseline from "@mui/material/CssBaseline"
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { AuthProvider } from "components/AuthProvider/AuthProvider"
|
import { AuthProvider } from "components/AuthProvider/AuthProvider";
|
||||||
import { FC, PropsWithChildren } from "react"
|
import { FC, PropsWithChildren } from "react";
|
||||||
import { HelmetProvider } from "react-helmet-async"
|
import { HelmetProvider } from "react-helmet-async";
|
||||||
import { AppRouter } from "./AppRouter"
|
import { AppRouter } from "./AppRouter";
|
||||||
import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary"
|
import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary";
|
||||||
import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar"
|
import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar";
|
||||||
import { dark } from "./theme"
|
import { dark } from "./theme";
|
||||||
import "./theme/globalFonts"
|
import "./theme/globalFonts";
|
||||||
import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles"
|
import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
|
@ -19,7 +19,7 @@ const queryClient = new QueryClient({
|
||||||
networkMode: "offlineFirst",
|
networkMode: "offlineFirst",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
export const AppProviders: FC<PropsWithChildren> = ({ children }) => {
|
export const AppProviders: FC<PropsWithChildren> = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -38,13 +38,13 @@ export const AppProviders: FC<PropsWithChildren> = ({ children }) => {
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</StyledEngineProvider>
|
</StyledEngineProvider>
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const App: FC = () => {
|
export const App: FC = () => {
|
||||||
return (
|
return (
|
||||||
<AppProviders>
|
<AppProviders>
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</AppProviders>
|
</AppProviders>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
import { Alert } from "./Alert"
|
import { Alert } from "./Alert";
|
||||||
import Button from "@mui/material/Button"
|
import Button from "@mui/material/Button";
|
||||||
import Link from "@mui/material/Link"
|
import Link from "@mui/material/Link";
|
||||||
import type { Meta, StoryObj } from "@storybook/react"
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
const meta: Meta<typeof Alert> = {
|
const meta: Meta<typeof Alert> = {
|
||||||
title: "components/Alert",
|
title: "components/Alert",
|
||||||
component: Alert,
|
component: Alert,
|
||||||
}
|
};
|
||||||
|
|
||||||
export default meta
|
export default meta;
|
||||||
type Story = StoryObj<typeof Alert>
|
type Story = StoryObj<typeof Alert>;
|
||||||
|
|
||||||
const ExampleAction = (
|
const ExampleAction = (
|
||||||
<Button onClick={() => null} size="small" variant="text">
|
<Button onClick={() => null} size="small" variant="text">
|
||||||
Button
|
Button
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
|
|
||||||
export const Success: Story = {
|
export const Success: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
@ -23,14 +23,14 @@ export const Success: Story = {
|
||||||
severity: "success",
|
severity: "success",
|
||||||
onRetry: undefined,
|
onRetry: undefined,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Warning: Story = {
|
export const Warning: Story = {
|
||||||
args: {
|
args: {
|
||||||
children: "This is a warning",
|
children: "This is a warning",
|
||||||
severity: "warning",
|
severity: "warning",
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const WarningWithDismiss: Story = {
|
export const WarningWithDismiss: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
@ -38,7 +38,7 @@ export const WarningWithDismiss: Story = {
|
||||||
dismissible: true,
|
dismissible: true,
|
||||||
severity: "warning",
|
severity: "warning",
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const WarningWithAction: Story = {
|
export const WarningWithAction: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
@ -46,7 +46,7 @@ export const WarningWithAction: Story = {
|
||||||
actions: [ExampleAction],
|
actions: [ExampleAction],
|
||||||
severity: "warning",
|
severity: "warning",
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const WarningWithActionAndDismiss: Story = {
|
export const WarningWithActionAndDismiss: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
@ -55,7 +55,7 @@ export const WarningWithActionAndDismiss: Story = {
|
||||||
dismissible: true,
|
dismissible: true,
|
||||||
severity: "warning",
|
severity: "warning",
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const WithChildren: Story = {
|
export const WithChildren: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
@ -66,4 +66,4 @@ export const WithChildren: Story = {
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { useState, FC, ReactNode } from "react"
|
import { useState, FC, ReactNode } from "react";
|
||||||
import Collapse from "@mui/material/Collapse"
|
import Collapse from "@mui/material/Collapse";
|
||||||
// eslint-disable-next-line no-restricted-imports -- It is the base component
|
// eslint-disable-next-line no-restricted-imports -- It is the base component
|
||||||
import MuiAlert, { AlertProps as MuiAlertProps } from "@mui/material/Alert"
|
import MuiAlert, { AlertProps as MuiAlertProps } from "@mui/material/Alert";
|
||||||
import Button from "@mui/material/Button"
|
import Button from "@mui/material/Button";
|
||||||
import Box from "@mui/material/Box"
|
import Box from "@mui/material/Box";
|
||||||
|
|
||||||
export type AlertProps = MuiAlertProps & {
|
export type AlertProps = MuiAlertProps & {
|
||||||
actions?: ReactNode
|
actions?: ReactNode;
|
||||||
dismissible?: boolean
|
dismissible?: boolean;
|
||||||
onRetry?: () => void
|
onRetry?: () => void;
|
||||||
onDismiss?: () => void
|
onDismiss?: () => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Alert: FC<AlertProps> = ({
|
export const Alert: FC<AlertProps> = ({
|
||||||
children,
|
children,
|
||||||
|
@ -21,7 +21,7 @@ export const Alert: FC<AlertProps> = ({
|
||||||
onDismiss,
|
onDismiss,
|
||||||
...alertProps
|
...alertProps
|
||||||
}) => {
|
}) => {
|
||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapse in={open}>
|
<Collapse in={open}>
|
||||||
|
@ -47,8 +47,8 @@ export const Alert: FC<AlertProps> = ({
|
||||||
variant="text"
|
variant="text"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpen(false)
|
setOpen(false);
|
||||||
onDismiss && onDismiss()
|
onDismiss && onDismiss();
|
||||||
}}
|
}}
|
||||||
data-testid="dismiss-banner-btn"
|
data-testid="dismiss-banner-btn"
|
||||||
>
|
>
|
||||||
|
@ -61,8 +61,8 @@ export const Alert: FC<AlertProps> = ({
|
||||||
{children}
|
{children}
|
||||||
</MuiAlert>
|
</MuiAlert>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const AlertDetail = ({ children }: { children: ReactNode }) => {
|
export const AlertDetail = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -74,5 +74,5 @@ export const AlertDetail = ({ children }: { children: ReactNode }) => {
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import Button from "@mui/material/Button"
|
import Button from "@mui/material/Button";
|
||||||
import { mockApiError } from "testHelpers/entities"
|
import { mockApiError } from "testHelpers/entities";
|
||||||
import type { Meta, StoryObj } from "@storybook/react"
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
import { action } from "@storybook/addon-actions"
|
import { action } from "@storybook/addon-actions";
|
||||||
import { ErrorAlert } from "./ErrorAlert"
|
import { ErrorAlert } from "./ErrorAlert";
|
||||||
|
|
||||||
const mockError = mockApiError({
|
const mockError = mockApiError({
|
||||||
message: "Email or password was invalid",
|
message: "Email or password was invalid",
|
||||||
detail: "Password is invalid",
|
detail: "Password is invalid",
|
||||||
})
|
});
|
||||||
|
|
||||||
const meta: Meta<typeof ErrorAlert> = {
|
const meta: Meta<typeof ErrorAlert> = {
|
||||||
title: "components/ErrorAlert",
|
title: "components/ErrorAlert",
|
||||||
|
@ -17,16 +17,16 @@ const meta: Meta<typeof ErrorAlert> = {
|
||||||
dismissible: false,
|
dismissible: false,
|
||||||
onRetry: undefined,
|
onRetry: undefined,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export default meta
|
export default meta;
|
||||||
type Story = StoryObj<typeof ErrorAlert>
|
type Story = StoryObj<typeof ErrorAlert>;
|
||||||
|
|
||||||
const ExampleAction = (
|
const ExampleAction = (
|
||||||
<Button onClick={() => null} size="small" variant="text">
|
<Button onClick={() => null} size="small" variant="text">
|
||||||
Button
|
Button
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
|
|
||||||
export const WithOnlyMessage: Story = {
|
export const WithOnlyMessage: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
@ -34,33 +34,33 @@ export const WithOnlyMessage: Story = {
|
||||||
message: "Email or password was invalid",
|
message: "Email or password was invalid",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const WithDismiss: Story = {
|
export const WithDismiss: Story = {
|
||||||
args: {
|
args: {
|
||||||
dismissible: true,
|
dismissible: true,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const WithAction: Story = {
|
export const WithAction: Story = {
|
||||||
args: {
|
args: {
|
||||||
actions: [ExampleAction],
|
actions: [ExampleAction],
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const WithActionAndDismiss: Story = {
|
export const WithActionAndDismiss: Story = {
|
||||||
args: {
|
args: {
|
||||||
actions: [ExampleAction],
|
actions: [ExampleAction],
|
||||||
dismissible: true,
|
dismissible: true,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const WithRetry: Story = {
|
export const WithRetry: Story = {
|
||||||
args: {
|
args: {
|
||||||
onRetry: action("retry"),
|
onRetry: action("retry"),
|
||||||
dismissible: true,
|
dismissible: true,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const WithActionRetryAndDismiss: Story = {
|
export const WithActionRetryAndDismiss: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
@ -68,10 +68,10 @@ export const WithActionRetryAndDismiss: Story = {
|
||||||
onRetry: action("retry"),
|
onRetry: action("retry"),
|
||||||
dismissible: true,
|
dismissible: true,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const WithNonApiError: Story = {
|
export const WithNonApiError: Story = {
|
||||||
args: {
|
args: {
|
||||||
error: new Error("Non API error here"),
|
error: new Error("Non API error here"),
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { AlertProps, Alert, AlertDetail } from "./Alert"
|
import { AlertProps, Alert, AlertDetail } from "./Alert";
|
||||||
import AlertTitle from "@mui/material/AlertTitle"
|
import AlertTitle from "@mui/material/AlertTitle";
|
||||||
import { getErrorMessage, getErrorDetail } from "api/errors"
|
import { getErrorMessage, getErrorDetail } from "api/errors";
|
||||||
import { FC } from "react"
|
import { FC } from "react";
|
||||||
|
|
||||||
export const ErrorAlert: FC<
|
export const ErrorAlert: FC<
|
||||||
Omit<AlertProps, "severity" | "children"> & { error: unknown }
|
Omit<AlertProps, "severity" | "children"> & { error: unknown }
|
||||||
> = ({ error, ...alertProps }) => {
|
> = ({ error, ...alertProps }) => {
|
||||||
const message = getErrorMessage(error, "Something went wrong.")
|
const message = getErrorMessage(error, "Something went wrong.");
|
||||||
const detail = getErrorDetail(error)
|
const detail = getErrorDetail(error);
|
||||||
|
|
||||||
// For some reason, the message and detail can be the same on the BE, but does
|
// For some reason, the message and detail can be the same on the BE, but does
|
||||||
// not make sense in the FE to showing them duplicated
|
// not make sense in the FE to showing them duplicated
|
||||||
const shouldDisplayDetail = message !== detail
|
const shouldDisplayDetail = message !== detail;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert severity="error" {...alertProps}>
|
<Alert severity="error" {...alertProps}>
|
||||||
|
@ -24,5 +24,5 @@ export const ErrorAlert: FC<
|
||||||
message
|
message
|
||||||
)}
|
)}
|
||||||
</Alert>
|
</Alert>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,36 +1,36 @@
|
||||||
import { useActor, useInterpret } from "@xstate/react"
|
import { useActor, useInterpret } from "@xstate/react";
|
||||||
import { createContext, FC, PropsWithChildren, useContext } from "react"
|
import { createContext, FC, PropsWithChildren, useContext } from "react";
|
||||||
import { authMachine } from "xServices/auth/authXService"
|
import { authMachine } from "xServices/auth/authXService";
|
||||||
import { ActorRefFrom } from "xstate"
|
import { ActorRefFrom } from "xstate";
|
||||||
|
|
||||||
interface AuthContextValue {
|
interface AuthContextValue {
|
||||||
authService: ActorRefFrom<typeof authMachine>
|
authService: ActorRefFrom<typeof authMachine>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextValue | undefined>(undefined)
|
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
||||||
|
|
||||||
export const AuthProvider: FC<PropsWithChildren> = ({ children }) => {
|
export const AuthProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
const authService = useInterpret(authMachine)
|
const authService = useInterpret(authMachine);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ authService }}>
|
<AuthContext.Provider value={{ authService }}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
type UseAuthReturnType = ReturnType<
|
type UseAuthReturnType = ReturnType<
|
||||||
typeof useActor<AuthContextValue["authService"]>
|
typeof useActor<AuthContextValue["authService"]>
|
||||||
>
|
>;
|
||||||
|
|
||||||
export const useAuth = (): UseAuthReturnType => {
|
export const useAuth = (): UseAuthReturnType => {
|
||||||
const context = useContext(AuthContext)
|
const context = useContext(AuthContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useAuth should be used inside of <AuthProvider />")
|
throw new Error("useAuth should be used inside of <AuthProvider />");
|
||||||
}
|
}
|
||||||
|
|
||||||
const auth = useActor(context.authService)
|
const auth = useActor(context.authService);
|
||||||
|
|
||||||
return auth
|
return auth;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,61 +1,63 @@
|
||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react";
|
||||||
import { Avatar, AvatarIcon, AvatarProps } from "./Avatar"
|
import { Avatar, AvatarIcon, AvatarProps } from "./Avatar";
|
||||||
import PauseIcon from "@mui/icons-material/PauseOutlined"
|
import PauseIcon from "@mui/icons-material/PauseOutlined";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "components/Avatar",
|
title: "components/Avatar",
|
||||||
component: Avatar,
|
component: Avatar,
|
||||||
}
|
};
|
||||||
|
|
||||||
const Template: Story<AvatarProps> = (args: AvatarProps) => <Avatar {...args} />
|
const Template: Story<AvatarProps> = (args: AvatarProps) => (
|
||||||
|
<Avatar {...args} />
|
||||||
|
);
|
||||||
|
|
||||||
export const Letter = Template.bind({})
|
export const Letter = Template.bind({});
|
||||||
Letter.args = {
|
Letter.args = {
|
||||||
children: "Coder",
|
children: "Coder",
|
||||||
}
|
};
|
||||||
|
|
||||||
export const LetterXL = Template.bind({})
|
export const LetterXL = Template.bind({});
|
||||||
LetterXL.args = {
|
LetterXL.args = {
|
||||||
children: "Coder",
|
children: "Coder",
|
||||||
size: "xl",
|
size: "xl",
|
||||||
}
|
};
|
||||||
|
|
||||||
export const LetterDarken = Template.bind({})
|
export const LetterDarken = Template.bind({});
|
||||||
LetterDarken.args = {
|
LetterDarken.args = {
|
||||||
children: "Coder",
|
children: "Coder",
|
||||||
colorScheme: "darken",
|
colorScheme: "darken",
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Image = Template.bind({})
|
export const Image = Template.bind({});
|
||||||
Image.args = {
|
Image.args = {
|
||||||
src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
|
src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ImageXL = Template.bind({})
|
export const ImageXL = Template.bind({});
|
||||||
ImageXL.args = {
|
ImageXL.args = {
|
||||||
src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
|
src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
|
||||||
size: "xl",
|
size: "xl",
|
||||||
}
|
};
|
||||||
|
|
||||||
export const MuiIcon = Template.bind({})
|
export const MuiIcon = Template.bind({});
|
||||||
MuiIcon.args = {
|
MuiIcon.args = {
|
||||||
children: <PauseIcon />,
|
children: <PauseIcon />,
|
||||||
}
|
};
|
||||||
|
|
||||||
export const MuiIconDarken = Template.bind({})
|
export const MuiIconDarken = Template.bind({});
|
||||||
MuiIconDarken.args = {
|
MuiIconDarken.args = {
|
||||||
children: <PauseIcon />,
|
children: <PauseIcon />,
|
||||||
colorScheme: "darken",
|
colorScheme: "darken",
|
||||||
}
|
};
|
||||||
|
|
||||||
export const MuiIconXL = Template.bind({})
|
export const MuiIconXL = Template.bind({});
|
||||||
MuiIconXL.args = {
|
MuiIconXL.args = {
|
||||||
children: <PauseIcon />,
|
children: <PauseIcon />,
|
||||||
size: "xl",
|
size: "xl",
|
||||||
}
|
};
|
||||||
|
|
||||||
export const AvatarIconDarken = Template.bind({})
|
export const AvatarIconDarken = Template.bind({});
|
||||||
AvatarIconDarken.args = {
|
AvatarIconDarken.args = {
|
||||||
children: <AvatarIcon src="/icon/database.svg" />,
|
children: <AvatarIcon src="/icon/database.svg" />,
|
||||||
colorScheme: "darken",
|
colorScheme: "darken",
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
// This is the only place MuiAvatar can be used
|
// This is the only place MuiAvatar can be used
|
||||||
// eslint-disable-next-line no-restricted-imports -- Read above
|
// eslint-disable-next-line no-restricted-imports -- Read above
|
||||||
import MuiAvatar, { AvatarProps as MuiAvatarProps } from "@mui/material/Avatar"
|
import MuiAvatar, { AvatarProps as MuiAvatarProps } from "@mui/material/Avatar";
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
import { FC } from "react"
|
import { FC } from "react";
|
||||||
import { combineClasses } from "utils/combineClasses"
|
import { combineClasses } from "utils/combineClasses";
|
||||||
import { firstLetter } from "./firstLetter"
|
import { firstLetter } from "./firstLetter";
|
||||||
|
|
||||||
export type AvatarProps = MuiAvatarProps & {
|
export type AvatarProps = MuiAvatarProps & {
|
||||||
size?: "sm" | "md" | "xl"
|
size?: "sm" | "md" | "xl";
|
||||||
colorScheme?: "light" | "darken"
|
colorScheme?: "light" | "darken";
|
||||||
fitImage?: boolean
|
fitImage?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Avatar: FC<AvatarProps> = ({
|
export const Avatar: FC<AvatarProps> = ({
|
||||||
size = "md",
|
size = "md",
|
||||||
|
@ -20,7 +20,7 @@ export const Avatar: FC<AvatarProps> = ({
|
||||||
children,
|
children,
|
||||||
...muiProps
|
...muiProps
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MuiAvatar
|
<MuiAvatar
|
||||||
|
@ -35,16 +35,16 @@ export const Avatar: FC<AvatarProps> = ({
|
||||||
{/* If the children is a string, we always want to render the first letter */}
|
{/* If the children is a string, we always want to render the first letter */}
|
||||||
{typeof children === "string" ? firstLetter(children) : children}
|
{typeof children === "string" ? firstLetter(children) : children}
|
||||||
</MuiAvatar>
|
</MuiAvatar>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use it to make an img element behaves like a MaterialUI Icon component
|
* Use it to make an img element behaves like a MaterialUI Icon component
|
||||||
*/
|
*/
|
||||||
export const AvatarIcon: FC<{ src: string }> = ({ src }) => {
|
export const AvatarIcon: FC<{ src: string }> = ({ src }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
return <img src={src} alt="" className={styles.avatarIcon} />
|
return <img src={src} alt="" className={styles.avatarIcon} />;
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
// Size styles
|
// Size styles
|
||||||
|
@ -77,4 +77,4 @@ const useStyles = makeStyles((theme) => ({
|
||||||
objectFit: "contain",
|
objectFit: "contain",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { firstLetter } from "./firstLetter"
|
import { firstLetter } from "./firstLetter";
|
||||||
|
|
||||||
describe("first-letter", () => {
|
describe("first-letter", () => {
|
||||||
it.each<[string, string]>([
|
it.each<[string, string]>([
|
||||||
|
@ -6,6 +6,6 @@ describe("first-letter", () => {
|
||||||
["User", "U"],
|
["User", "U"],
|
||||||
["test", "T"],
|
["test", "T"],
|
||||||
])(`firstLetter(%p) returns %p`, (input, expected) => {
|
])(`firstLetter(%p) returns %p`, (input, expected) => {
|
||||||
expect(firstLetter(input)).toBe(expected)
|
expect(firstLetter(input)).toBe(expected);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
*/
|
*/
|
||||||
export const firstLetter = (str: string): string => {
|
export const firstLetter = (str: string): string => {
|
||||||
if (str.length > 0) {
|
if (str.length > 0) {
|
||||||
return str[0].toLocaleUpperCase()
|
return str[0].toLocaleUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return "";
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react";
|
||||||
import { AvatarData, AvatarDataProps } from "./AvatarData"
|
import { AvatarData, AvatarDataProps } from "./AvatarData";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "components/AvatarData",
|
title: "components/AvatarData",
|
||||||
component: AvatarData,
|
component: AvatarData,
|
||||||
}
|
};
|
||||||
|
|
||||||
const Template: Story<AvatarDataProps> = (args: AvatarDataProps) => (
|
const Template: Story<AvatarDataProps> = (args: AvatarDataProps) => (
|
||||||
<AvatarData {...args} />
|
<AvatarData {...args} />
|
||||||
)
|
);
|
||||||
|
|
||||||
export const Example = Template.bind({})
|
export const Example = Template.bind({});
|
||||||
Example.args = {
|
Example.args = {
|
||||||
title: "coder",
|
title: "coder",
|
||||||
subtitle: "coder@coder.com",
|
subtitle: "coder@coder.com",
|
||||||
}
|
};
|
||||||
|
|
||||||
export const WithImage = Template.bind({})
|
export const WithImage = Template.bind({});
|
||||||
WithImage.args = {
|
WithImage.args = {
|
||||||
title: "coder",
|
title: "coder",
|
||||||
subtitle: "coder@coder.com",
|
subtitle: "coder@coder.com",
|
||||||
src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
|
src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4",
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { Avatar } from "components/Avatar/Avatar"
|
import { Avatar } from "components/Avatar/Avatar";
|
||||||
import { FC, PropsWithChildren } from "react"
|
import { FC, PropsWithChildren } from "react";
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack";
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
|
|
||||||
export interface AvatarDataProps {
|
export interface AvatarDataProps {
|
||||||
title: string | JSX.Element
|
title: string | JSX.Element;
|
||||||
subtitle?: string
|
subtitle?: string;
|
||||||
src?: string
|
src?: string;
|
||||||
avatar?: React.ReactNode
|
avatar?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AvatarData: FC<PropsWithChildren<AvatarDataProps>> = ({
|
export const AvatarData: FC<PropsWithChildren<AvatarDataProps>> = ({
|
||||||
|
@ -16,10 +16,10 @@ export const AvatarData: FC<PropsWithChildren<AvatarDataProps>> = ({
|
||||||
src,
|
src,
|
||||||
avatar,
|
avatar,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
|
|
||||||
if (!avatar) {
|
if (!avatar) {
|
||||||
avatar = <Avatar src={src}>{title}</Avatar>
|
avatar = <Avatar src={src}>{title}</Avatar>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -36,8 +36,8 @@ export const AvatarData: FC<PropsWithChildren<AvatarDataProps>> = ({
|
||||||
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
|
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
|
@ -61,4 +61,4 @@ const useStyles = makeStyles((theme) => ({
|
||||||
lineHeight: "150%",
|
lineHeight: "150%",
|
||||||
maxWidth: 540,
|
maxWidth: 540,
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { FC } from "react"
|
import { FC } from "react";
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack";
|
||||||
import Skeleton from "@mui/material/Skeleton"
|
import Skeleton from "@mui/material/Skeleton";
|
||||||
|
|
||||||
export const AvatarDataSkeleton: FC = () => {
|
export const AvatarDataSkeleton: FC = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -12,5 +12,5 @@ export const AvatarDataSkeleton: FC = () => {
|
||||||
<Skeleton variant="text" width={60} />
|
<Skeleton variant="text" width={60} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import Badge from "@mui/material/Badge"
|
import Badge from "@mui/material/Badge";
|
||||||
import { useTheme, withStyles } from "@mui/styles"
|
import { useTheme, withStyles } from "@mui/styles";
|
||||||
import { FC } from "react"
|
import { FC } from "react";
|
||||||
import { WorkspaceBuild } from "api/typesGenerated"
|
import { WorkspaceBuild } from "api/typesGenerated";
|
||||||
import { getDisplayWorkspaceBuildStatus } from "utils/workspace"
|
import { getDisplayWorkspaceBuildStatus } from "utils/workspace";
|
||||||
import { Avatar, AvatarProps } from "components/Avatar/Avatar"
|
import { Avatar, AvatarProps } from "components/Avatar/Avatar";
|
||||||
import { PaletteIndex } from "theme/theme"
|
import { PaletteIndex } from "theme/theme";
|
||||||
import { Theme } from "@mui/material/styles"
|
import { Theme } from "@mui/material/styles";
|
||||||
import { BuildIcon } from "components/BuildIcon/BuildIcon"
|
import { BuildIcon } from "components/BuildIcon/BuildIcon";
|
||||||
|
|
||||||
interface StylesBadgeProps {
|
interface StylesBadgeProps {
|
||||||
type: PaletteIndex
|
type: PaletteIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledBadge = withStyles((theme) => ({
|
const StyledBadge = withStyles((theme) => ({
|
||||||
|
@ -22,16 +22,16 @@ const StyledBadge = withStyles((theme) => ({
|
||||||
display: "block",
|
display: "block",
|
||||||
padding: 0,
|
padding: 0,
|
||||||
},
|
},
|
||||||
}))(Badge)
|
}))(Badge);
|
||||||
|
|
||||||
export interface BuildAvatarProps {
|
export interface BuildAvatarProps {
|
||||||
build: WorkspaceBuild
|
build: WorkspaceBuild;
|
||||||
size?: AvatarProps["size"]
|
size?: AvatarProps["size"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BuildAvatar: FC<BuildAvatarProps> = ({ build, size }) => {
|
export const BuildAvatar: FC<BuildAvatarProps> = ({ build, size }) => {
|
||||||
const theme = useTheme<Theme>()
|
const theme = useTheme<Theme>();
|
||||||
const displayBuildStatus = getDisplayWorkspaceBuildStatus(theme, build)
|
const displayBuildStatus = getDisplayWorkspaceBuildStatus(theme, build);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBadge
|
<StyledBadge
|
||||||
|
@ -50,5 +50,5 @@ export const BuildAvatar: FC<BuildAvatarProps> = ({ build, size }) => {
|
||||||
<BuildIcon transition={build.transition} />
|
<BuildIcon transition={build.transition} />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</StyledBadge>
|
</StyledBadge>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import PlayArrowOutlined from "@mui/icons-material/PlayArrowOutlined"
|
import PlayArrowOutlined from "@mui/icons-material/PlayArrowOutlined";
|
||||||
import StopOutlined from "@mui/icons-material/StopOutlined"
|
import StopOutlined from "@mui/icons-material/StopOutlined";
|
||||||
import DeleteOutlined from "@mui/icons-material/DeleteOutlined"
|
import DeleteOutlined from "@mui/icons-material/DeleteOutlined";
|
||||||
import { WorkspaceTransition } from "api/typesGenerated"
|
import { WorkspaceTransition } from "api/typesGenerated";
|
||||||
import { ComponentProps } from "react"
|
import { ComponentProps } from "react";
|
||||||
|
|
||||||
type SVGIcon = typeof PlayArrowOutlined
|
type SVGIcon = typeof PlayArrowOutlined;
|
||||||
|
|
||||||
type SVGIconProps = ComponentProps<SVGIcon>
|
type SVGIconProps = ComponentProps<SVGIcon>;
|
||||||
|
|
||||||
const iconByTransition: Record<WorkspaceTransition, SVGIcon> = {
|
const iconByTransition: Record<WorkspaceTransition, SVGIcon> = {
|
||||||
start: PlayArrowOutlined,
|
start: PlayArrowOutlined,
|
||||||
stop: StopOutlined,
|
stop: StopOutlined,
|
||||||
delete: DeleteOutlined,
|
delete: DeleteOutlined,
|
||||||
}
|
};
|
||||||
|
|
||||||
export const BuildIcon = (
|
export const BuildIcon = (
|
||||||
props: SVGIconProps & { transition: WorkspaceTransition },
|
props: SVGIconProps & { transition: WorkspaceTransition },
|
||||||
) => {
|
) => {
|
||||||
const Icon = iconByTransition[props.transition]
|
const Icon = iconByTransition[props.transition];
|
||||||
return <Icon {...props} />
|
return <Icon {...props} />;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react";
|
||||||
import { CodeExample, CodeExampleProps } from "./CodeExample"
|
import { CodeExample, CodeExampleProps } from "./CodeExample";
|
||||||
|
|
||||||
const sampleCode = `echo "Hello, world"`
|
const sampleCode = `echo "Hello, world"`;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "components/CodeExample",
|
title: "components/CodeExample",
|
||||||
|
@ -9,18 +9,18 @@ export default {
|
||||||
argTypes: {
|
argTypes: {
|
||||||
code: { control: "string", defaultValue: sampleCode },
|
code: { control: "string", defaultValue: sampleCode },
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
const Template: Story<CodeExampleProps> = (args: CodeExampleProps) => (
|
const Template: Story<CodeExampleProps> = (args: CodeExampleProps) => (
|
||||||
<CodeExample {...args} />
|
<CodeExample {...args} />
|
||||||
)
|
);
|
||||||
|
|
||||||
export const Example = Template.bind({})
|
export const Example = Template.bind({});
|
||||||
Example.args = {
|
Example.args = {
|
||||||
code: sampleCode,
|
code: sampleCode,
|
||||||
}
|
};
|
||||||
|
|
||||||
export const LongCode = Template.bind({})
|
export const LongCode = Template.bind({});
|
||||||
LongCode.args = {
|
LongCode.args = {
|
||||||
code: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICnKzATuWwmmt5+CKTPuRGN0R1PBemA+6/SStpLiyX+L",
|
code: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICnKzATuWwmmt5+CKTPuRGN0R1PBemA+6/SStpLiyX+L",
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { screen } from "@testing-library/react"
|
import { screen } from "@testing-library/react";
|
||||||
import { render } from "../../testHelpers/renderHelpers"
|
import { render } from "../../testHelpers/renderHelpers";
|
||||||
import { CodeExample } from "./CodeExample"
|
import { CodeExample } from "./CodeExample";
|
||||||
|
|
||||||
describe("CodeExample", () => {
|
describe("CodeExample", () => {
|
||||||
it("renders code", async () => {
|
it("renders code", async () => {
|
||||||
// When
|
// When
|
||||||
render(<CodeExample code="echo hello" />)
|
render(<CodeExample code="echo hello" />);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
// Both lines should be rendered
|
// Both lines should be rendered
|
||||||
await screen.findByText("echo hello")
|
await screen.findByText("echo hello");
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
import { FC } from "react"
|
import { FC } from "react";
|
||||||
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
|
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants";
|
||||||
import { combineClasses } from "../../utils/combineClasses"
|
import { combineClasses } from "../../utils/combineClasses";
|
||||||
import { CopyButton } from "../CopyButton/CopyButton"
|
import { CopyButton } from "../CopyButton/CopyButton";
|
||||||
import { Theme } from "@mui/material/styles"
|
import { Theme } from "@mui/material/styles";
|
||||||
|
|
||||||
export interface CodeExampleProps {
|
export interface CodeExampleProps {
|
||||||
code: string
|
code: string;
|
||||||
className?: string
|
className?: string;
|
||||||
buttonClassName?: string
|
buttonClassName?: string;
|
||||||
tooltipTitle?: string
|
tooltipTitle?: string;
|
||||||
inline?: boolean
|
inline?: boolean;
|
||||||
password?: boolean
|
password?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,7 +24,7 @@ export const CodeExample: FC<React.PropsWithChildren<CodeExampleProps>> = ({
|
||||||
tooltipTitle,
|
tooltipTitle,
|
||||||
inline,
|
inline,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles({ inline: inline })
|
const styles = useStyles({ inline: inline });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={combineClasses([styles.root, className])}>
|
<div className={combineClasses([styles.root, className])}>
|
||||||
|
@ -35,12 +35,12 @@ export const CodeExample: FC<React.PropsWithChildren<CodeExampleProps>> = ({
|
||||||
buttonClassName={buttonClassName}
|
buttonClassName={buttonClassName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface styleProps {
|
interface styleProps {
|
||||||
inline?: boolean
|
inline?: boolean;
|
||||||
password?: boolean
|
password?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles<Theme, styleProps>((theme) => ({
|
const useStyles = makeStyles<Theme, styleProps>((theme) => ({
|
||||||
|
@ -65,4 +65,4 @@ const useStyles = makeStyles<Theme, styleProps>((theme) => ({
|
||||||
wordBreak: "break-all",
|
wordBreak: "break-all",
|
||||||
"-webkit-text-security": (props) => (props.password ? "disc" : undefined),
|
"-webkit-text-security": (props) => (props.password ? "disc" : undefined),
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react";
|
||||||
import { ChooseOne, Cond } from "./ChooseOne"
|
import { ChooseOne, Cond } from "./ChooseOne";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "components/Conditionals/ChooseOne",
|
title: "components/Conditionals/ChooseOne",
|
||||||
component: ChooseOne,
|
component: ChooseOne,
|
||||||
subcomponents: { Cond },
|
subcomponents: { Cond },
|
||||||
}
|
};
|
||||||
|
|
||||||
export const FirstIsTrue: Story = () => (
|
export const FirstIsTrue: Story = () => (
|
||||||
<ChooseOne>
|
<ChooseOne>
|
||||||
|
@ -13,7 +13,7 @@ export const FirstIsTrue: Story = () => (
|
||||||
<Cond condition={false}>The second one does not show.</Cond>
|
<Cond condition={false}>The second one does not show.</Cond>
|
||||||
<Cond>The default does not show.</Cond>
|
<Cond>The default does not show.</Cond>
|
||||||
</ChooseOne>
|
</ChooseOne>
|
||||||
)
|
);
|
||||||
|
|
||||||
export const SecondIsTrue: Story = () => (
|
export const SecondIsTrue: Story = () => (
|
||||||
<ChooseOne>
|
<ChooseOne>
|
||||||
|
@ -21,7 +21,7 @@ export const SecondIsTrue: Story = () => (
|
||||||
<Cond condition>The second one shows.</Cond>
|
<Cond condition>The second one shows.</Cond>
|
||||||
<Cond>The default does not show.</Cond>
|
<Cond>The default does not show.</Cond>
|
||||||
</ChooseOne>
|
</ChooseOne>
|
||||||
)
|
);
|
||||||
|
|
||||||
export const AllAreTrue: Story = () => (
|
export const AllAreTrue: Story = () => (
|
||||||
<ChooseOne>
|
<ChooseOne>
|
||||||
|
@ -29,7 +29,7 @@ export const AllAreTrue: Story = () => (
|
||||||
<Cond condition>The second one does not show.</Cond>
|
<Cond condition>The second one does not show.</Cond>
|
||||||
<Cond>The default does not show.</Cond>
|
<Cond>The default does not show.</Cond>
|
||||||
</ChooseOne>
|
</ChooseOne>
|
||||||
)
|
);
|
||||||
|
|
||||||
export const NoneAreTrue: Story = () => (
|
export const NoneAreTrue: Story = () => (
|
||||||
<ChooseOne>
|
<ChooseOne>
|
||||||
|
@ -37,10 +37,10 @@ export const NoneAreTrue: Story = () => (
|
||||||
<Cond condition={false}>The second one does not show.</Cond>
|
<Cond condition={false}>The second one does not show.</Cond>
|
||||||
<Cond>The default shows.</Cond>
|
<Cond>The default shows.</Cond>
|
||||||
</ChooseOne>
|
</ChooseOne>
|
||||||
)
|
);
|
||||||
|
|
||||||
export const OneCond: Story = () => (
|
export const OneCond: Story = () => (
|
||||||
<ChooseOne>
|
<ChooseOne>
|
||||||
<Cond>An only child renders.</Cond>
|
<Cond>An only child renders.</Cond>
|
||||||
</ChooseOne>
|
</ChooseOne>
|
||||||
)
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Children, PropsWithChildren } from "react"
|
import { Children, PropsWithChildren } from "react";
|
||||||
|
|
||||||
export interface CondProps {
|
export interface CondProps {
|
||||||
condition?: boolean
|
condition?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,8 +14,8 @@ export interface CondProps {
|
||||||
export const Cond = ({
|
export const Cond = ({
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<CondProps>): JSX.Element => {
|
}: PropsWithChildren<CondProps>): JSX.Element => {
|
||||||
return <>{children}</>
|
return <>{children}</>;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper component for rendering exactly one of its children. Wrap each child in Cond to associate it
|
* Wrapper component for rendering exactly one of its children. Wrap each child in Cond to associate it
|
||||||
|
@ -27,22 +27,22 @@ export const Cond = ({
|
||||||
export const ChooseOne = ({
|
export const ChooseOne = ({
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren): JSX.Element | null => {
|
}: PropsWithChildren): JSX.Element | null => {
|
||||||
const childArray = Children.toArray(children) as JSX.Element[]
|
const childArray = Children.toArray(children) as JSX.Element[];
|
||||||
if (childArray.length === 0) {
|
if (childArray.length === 0) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
const conditionedOptions = childArray.slice(0, childArray.length - 1)
|
const conditionedOptions = childArray.slice(0, childArray.length - 1);
|
||||||
const defaultCase = childArray[childArray.length - 1]
|
const defaultCase = childArray[childArray.length - 1];
|
||||||
if (defaultCase.props.condition !== undefined) {
|
if (defaultCase.props.condition !== undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"The last Cond in a ChooseOne was given a condition prop, but it is the default case.",
|
"The last Cond in a ChooseOne was given a condition prop, but it is the default case.",
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
if (conditionedOptions.some((cond) => cond.props.condition === undefined)) {
|
if (conditionedOptions.some((cond) => cond.props.condition === undefined)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"A non-final Cond in a ChooseOne does not have a condition prop or the prop is undefined.",
|
"A non-final Cond in a ChooseOne does not have a condition prop or the prop is undefined.",
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
const chosen = conditionedOptions.find((child) => child.props.condition)
|
const chosen = conditionedOptions.find((child) => child.props.condition);
|
||||||
return chosen ?? defaultCase
|
return chosen ?? defaultCase;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react";
|
||||||
import { Maybe, MaybeProps } from "./Maybe"
|
import { Maybe, MaybeProps } from "./Maybe";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "components/Conditionals/Maybe",
|
title: "components/Conditionals/Maybe",
|
||||||
component: Maybe,
|
component: Maybe,
|
||||||
}
|
};
|
||||||
|
|
||||||
const Template: Story<MaybeProps> = (args: MaybeProps) => (
|
const Template: Story<MaybeProps> = (args: MaybeProps) => (
|
||||||
<Maybe {...args}>Now you see me</Maybe>
|
<Maybe {...args}>Now you see me</Maybe>
|
||||||
)
|
);
|
||||||
|
|
||||||
export const ConditionIsTrue = Template.bind({})
|
export const ConditionIsTrue = Template.bind({});
|
||||||
ConditionIsTrue.args = {
|
ConditionIsTrue.args = {
|
||||||
condition: true,
|
condition: true,
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ConditionIsFalse = Template.bind({})
|
export const ConditionIsFalse = Template.bind({});
|
||||||
ConditionIsFalse.args = {
|
ConditionIsFalse.args = {
|
||||||
condition: false,
|
condition: false,
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { PropsWithChildren } from "react"
|
import { PropsWithChildren } from "react";
|
||||||
|
|
||||||
export interface MaybeProps {
|
export interface MaybeProps {
|
||||||
condition: boolean
|
condition: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,5 +13,5 @@ export const Maybe = ({
|
||||||
children,
|
children,
|
||||||
condition,
|
condition,
|
||||||
}: PropsWithChildren<MaybeProps>): JSX.Element | null => {
|
}: PropsWithChildren<MaybeProps>): JSX.Element | null => {
|
||||||
return condition ? <>{children}</> : null
|
return condition ? <>{children}</> : null;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import IconButton from "@mui/material/Button"
|
import IconButton from "@mui/material/Button";
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
import Tooltip from "@mui/material/Tooltip"
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import Check from "@mui/icons-material/Check"
|
import Check from "@mui/icons-material/Check";
|
||||||
import { useClipboard } from "hooks/useClipboard"
|
import { useClipboard } from "hooks/useClipboard";
|
||||||
import { combineClasses } from "../../utils/combineClasses"
|
import { combineClasses } from "../../utils/combineClasses";
|
||||||
import { FileCopyIcon } from "../Icons/FileCopyIcon"
|
import { FileCopyIcon } from "../Icons/FileCopyIcon";
|
||||||
|
|
||||||
interface CopyButtonProps {
|
interface CopyButtonProps {
|
||||||
text: string
|
text: string;
|
||||||
ctaCopy?: string
|
ctaCopy?: string;
|
||||||
wrapperClassName?: string
|
wrapperClassName?: string;
|
||||||
buttonClassName?: string
|
buttonClassName?: string;
|
||||||
tooltipTitle?: string
|
tooltipTitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
tooltipTitle: "Copy to clipboard",
|
tooltipTitle: "Copy to clipboard",
|
||||||
ariaLabel: "Copy to clipboard",
|
ariaLabel: "Copy to clipboard",
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy button used inside the CodeBlock component internally
|
* Copy button used inside the CodeBlock component internally
|
||||||
|
@ -29,8 +29,8 @@ export const CopyButton: React.FC<React.PropsWithChildren<CopyButtonProps>> = ({
|
||||||
buttonClassName = "",
|
buttonClassName = "",
|
||||||
tooltipTitle = Language.tooltipTitle,
|
tooltipTitle = Language.tooltipTitle,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
const { isCopied, copy: copyToClipboard } = useClipboard(text)
|
const { isCopied, copy: copyToClipboard } = useClipboard(text);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={tooltipTitle} placement="top">
|
<Tooltip title={tooltipTitle} placement="top">
|
||||||
|
@ -53,8 +53,8 @@ export const CopyButton: React.FC<React.PropsWithChildren<CopyButtonProps>> = ({
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
copyButtonWrapper: {
|
copyButtonWrapper: {
|
||||||
|
@ -76,4 +76,4 @@ const useStyles = makeStyles((theme) => ({
|
||||||
buttonCopy: {
|
buttonCopy: {
|
||||||
marginLeft: theme.spacing(1),
|
marginLeft: theme.spacing(1),
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
import Tooltip from "@mui/material/Tooltip"
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import { useClickable } from "hooks/useClickable"
|
import { useClickable } from "hooks/useClickable";
|
||||||
import { useClipboard } from "hooks/useClipboard"
|
import { useClipboard } from "hooks/useClipboard";
|
||||||
import { FC, HTMLProps } from "react"
|
import { FC, HTMLProps } from "react";
|
||||||
import { combineClasses } from "utils/combineClasses"
|
import { combineClasses } from "utils/combineClasses";
|
||||||
|
|
||||||
interface CopyableValueProps extends HTMLProps<HTMLDivElement> {
|
interface CopyableValueProps extends HTMLProps<HTMLDivElement> {
|
||||||
value: string
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CopyableValue: FC<CopyableValueProps> = ({
|
export const CopyableValue: FC<CopyableValueProps> = ({
|
||||||
|
@ -14,9 +14,9 @@ export const CopyableValue: FC<CopyableValueProps> = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { isCopied, copy } = useClipboard(value)
|
const { isCopied, copy } = useClipboard(value);
|
||||||
const clickableProps = useClickable(copy)
|
const clickableProps = useClickable(copy);
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -29,11 +29,11 @@ export const CopyableValue: FC<CopyableValueProps> = ({
|
||||||
className={combineClasses([styles.value, className])}
|
className={combineClasses([styles.value, className])}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
const useStyles = makeStyles(() => ({
|
||||||
value: {
|
value: {
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Box from "@mui/material/Box"
|
import Box from "@mui/material/Box";
|
||||||
import { Theme } from "@mui/material/styles"
|
import { Theme } from "@mui/material/styles";
|
||||||
import useTheme from "@mui/styles/useTheme"
|
import useTheme from "@mui/styles/useTheme";
|
||||||
import * as TypesGen from "api/typesGenerated"
|
import * as TypesGen from "api/typesGenerated";
|
||||||
import {
|
import {
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
|
@ -13,16 +13,16 @@ import {
|
||||||
TimeScale,
|
TimeScale,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "chart.js"
|
} from "chart.js";
|
||||||
import "chartjs-adapter-date-fns"
|
import "chartjs-adapter-date-fns";
|
||||||
import {
|
import {
|
||||||
HelpTooltip,
|
HelpTooltip,
|
||||||
HelpTooltipTitle,
|
HelpTooltipTitle,
|
||||||
HelpTooltipText,
|
HelpTooltipText,
|
||||||
} from "components/HelpTooltip/HelpTooltip"
|
} from "components/HelpTooltip/HelpTooltip";
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs";
|
||||||
import { FC } from "react"
|
import { FC } from "react";
|
||||||
import { Bar } from "react-chartjs-2"
|
import { Bar } from "react-chartjs-2";
|
||||||
|
|
||||||
ChartJS.register(
|
ChartJS.register(
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
|
@ -32,25 +32,25 @@ ChartJS.register(
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Legend,
|
Legend,
|
||||||
)
|
);
|
||||||
|
|
||||||
export interface DAUChartProps {
|
export interface DAUChartProps {
|
||||||
daus: TypesGen.DAUsResponse
|
daus: TypesGen.DAUsResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DAUChart: FC<DAUChartProps> = ({ daus }) => {
|
export const DAUChart: FC<DAUChartProps> = ({ daus }) => {
|
||||||
const theme: Theme = useTheme()
|
const theme: Theme = useTheme();
|
||||||
|
|
||||||
const labels = daus.entries.map((val) => {
|
const labels = daus.entries.map((val) => {
|
||||||
return dayjs(val.date).format("YYYY-MM-DD")
|
return dayjs(val.date).format("YYYY-MM-DD");
|
||||||
})
|
});
|
||||||
|
|
||||||
const data = daus.entries.map((val) => {
|
const data = daus.entries.map((val) => {
|
||||||
return val.amount
|
return val.amount;
|
||||||
})
|
});
|
||||||
|
|
||||||
defaults.font.family = theme.typography.fontFamily as string
|
defaults.font.family = theme.typography.fontFamily as string;
|
||||||
defaults.color = theme.palette.text.secondary
|
defaults.color = theme.palette.text.secondary;
|
||||||
|
|
||||||
const options: ChartOptions<"bar"> = {
|
const options: ChartOptions<"bar"> = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
|
@ -62,8 +62,8 @@ export const DAUChart: FC<DAUChartProps> = ({ daus }) => {
|
||||||
displayColors: false,
|
displayColors: false,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
title: (context) => {
|
title: (context) => {
|
||||||
const date = new Date(context[0].parsed.x)
|
const date = new Date(context[0].parsed.x);
|
||||||
return date.toLocaleDateString()
|
return date.toLocaleDateString();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -86,7 +86,7 @@ export const DAUChart: FC<DAUChartProps> = ({ daus }) => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Bar
|
<Bar
|
||||||
|
@ -108,8 +108,8 @@ export const DAUChart: FC<DAUChartProps> = ({ daus }) => {
|
||||||
}}
|
}}
|
||||||
options={options}
|
options={options}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const DAUTitle = () => {
|
export const DAUTitle = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -125,5 +125,5 @@ export const DAUTitle = () => {
|
||||||
</HelpTooltipText>
|
</HelpTooltipText>
|
||||||
</HelpTooltip>
|
</HelpTooltip>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { renderWithAuth } from "testHelpers/renderHelpers"
|
import { renderWithAuth } from "testHelpers/renderHelpers";
|
||||||
import { DashboardLayout } from "./DashboardLayout"
|
import { DashboardLayout } from "./DashboardLayout";
|
||||||
import * as API from "api/api"
|
import * as API from "api/api";
|
||||||
import { screen } from "@testing-library/react"
|
import { screen } from "@testing-library/react";
|
||||||
|
|
||||||
test("Show the new Coder version notification", async () => {
|
test("Show the new Coder version notification", async () => {
|
||||||
jest.spyOn(API, "getUpdateCheck").mockResolvedValue({
|
jest.spyOn(API, "getUpdateCheck").mockResolvedValue({
|
||||||
current: false,
|
current: false,
|
||||||
version: "v0.12.9",
|
version: "v0.12.9",
|
||||||
url: "https://github.com/coder/coder/releases/tag/v0.12.9",
|
url: "https://github.com/coder/coder/releases/tag/v0.12.9",
|
||||||
})
|
});
|
||||||
renderWithAuth(<DashboardLayout />, {
|
renderWithAuth(<DashboardLayout />, {
|
||||||
children: [{ element: <h1>Test page</h1> }],
|
children: [{ element: <h1>Test page</h1> }],
|
||||||
})
|
});
|
||||||
await screen.findByTestId("update-check-snackbar")
|
await screen.findByTestId("update-check-snackbar");
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
import { useMachine } from "@xstate/react"
|
import { useMachine } from "@xstate/react";
|
||||||
import { DeploymentBanner } from "./DeploymentBanner/DeploymentBanner"
|
import { DeploymentBanner } from "./DeploymentBanner/DeploymentBanner";
|
||||||
import { LicenseBanner } from "components/Dashboard/LicenseBanner/LicenseBanner"
|
import { LicenseBanner } from "components/Dashboard/LicenseBanner/LicenseBanner";
|
||||||
import { Loader } from "components/Loader/Loader"
|
import { Loader } from "components/Loader/Loader";
|
||||||
import { ServiceBanner } from "components/Dashboard/ServiceBanner/ServiceBanner"
|
import { ServiceBanner } from "components/Dashboard/ServiceBanner/ServiceBanner";
|
||||||
import { usePermissions } from "hooks/usePermissions"
|
import { usePermissions } from "hooks/usePermissions";
|
||||||
import { FC, Suspense } from "react"
|
import { FC, Suspense } from "react";
|
||||||
import { Outlet } from "react-router-dom"
|
import { Outlet } from "react-router-dom";
|
||||||
import { dashboardContentBottomPadding } from "theme/constants"
|
import { dashboardContentBottomPadding } from "theme/constants";
|
||||||
import { updateCheckMachine } from "xServices/updateCheck/updateCheckXService"
|
import { updateCheckMachine } from "xServices/updateCheck/updateCheckXService";
|
||||||
import { Navbar } from "./Navbar/Navbar"
|
import { Navbar } from "./Navbar/Navbar";
|
||||||
import Snackbar from "@mui/material/Snackbar"
|
import Snackbar from "@mui/material/Snackbar";
|
||||||
import Link from "@mui/material/Link"
|
import Link from "@mui/material/Link";
|
||||||
import Box, { BoxProps } from "@mui/material/Box"
|
import Box, { BoxProps } from "@mui/material/Box";
|
||||||
import InfoOutlined from "@mui/icons-material/InfoOutlined"
|
import InfoOutlined from "@mui/icons-material/InfoOutlined";
|
||||||
import Button from "@mui/material/Button"
|
import Button from "@mui/material/Button";
|
||||||
import { docs } from "utils/docs"
|
import { docs } from "utils/docs";
|
||||||
import { HealthBanner } from "./HealthBanner"
|
import { HealthBanner } from "./HealthBanner";
|
||||||
|
|
||||||
export const DashboardLayout: FC = () => {
|
export const DashboardLayout: FC = () => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
const permissions = usePermissions()
|
const permissions = usePermissions();
|
||||||
const [updateCheckState, updateCheckSend] = useMachine(updateCheckMachine, {
|
const [updateCheckState, updateCheckSend] = useMachine(updateCheckMachine, {
|
||||||
context: {
|
context: {
|
||||||
permissions,
|
permissions,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
const { updateCheck } = updateCheckState.context
|
const { updateCheck } = updateCheckState.context;
|
||||||
const canViewDeployment = Boolean(permissions.viewDeploymentValues)
|
const canViewDeployment = Boolean(permissions.viewDeploymentValues);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -99,8 +99,8 @@ export const DashboardLayout: FC = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const DashboardFullPage = (props: BoxProps) => {
|
export const DashboardFullPage = (props: BoxProps) => {
|
||||||
return (
|
return (
|
||||||
|
@ -116,8 +116,8 @@ export const DashboardFullPage = (props: BoxProps) => {
|
||||||
minHeight: "100%",
|
minHeight: "100%",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
site: {
|
site: {
|
||||||
|
@ -131,4 +131,4 @@ const useStyles = makeStyles({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,62 +1,62 @@
|
||||||
import { useMachine } from "@xstate/react"
|
import { useMachine } from "@xstate/react";
|
||||||
import {
|
import {
|
||||||
AppearanceConfig,
|
AppearanceConfig,
|
||||||
BuildInfoResponse,
|
BuildInfoResponse,
|
||||||
Entitlements,
|
Entitlements,
|
||||||
Experiments,
|
Experiments,
|
||||||
} from "api/typesGenerated"
|
} from "api/typesGenerated";
|
||||||
import { FullScreenLoader } from "components/Loader/FullScreenLoader"
|
import { FullScreenLoader } from "components/Loader/FullScreenLoader";
|
||||||
import { createContext, FC, PropsWithChildren, useContext } from "react"
|
import { createContext, FC, PropsWithChildren, useContext } from "react";
|
||||||
import { appearanceMachine } from "xServices/appearance/appearanceXService"
|
import { appearanceMachine } from "xServices/appearance/appearanceXService";
|
||||||
import { buildInfoMachine } from "xServices/buildInfo/buildInfoXService"
|
import { buildInfoMachine } from "xServices/buildInfo/buildInfoXService";
|
||||||
import { entitlementsMachine } from "xServices/entitlements/entitlementsXService"
|
import { entitlementsMachine } from "xServices/entitlements/entitlementsXService";
|
||||||
import { experimentsMachine } from "xServices/experiments/experimentsMachine"
|
import { experimentsMachine } from "xServices/experiments/experimentsMachine";
|
||||||
|
|
||||||
interface Appearance {
|
interface Appearance {
|
||||||
config: AppearanceConfig
|
config: AppearanceConfig;
|
||||||
preview: boolean
|
preview: boolean;
|
||||||
setPreview: (config: AppearanceConfig) => void
|
setPreview: (config: AppearanceConfig) => void;
|
||||||
save: (config: AppearanceConfig) => void
|
save: (config: AppearanceConfig) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DashboardProviderValue {
|
interface DashboardProviderValue {
|
||||||
buildInfo: BuildInfoResponse
|
buildInfo: BuildInfoResponse;
|
||||||
entitlements: Entitlements
|
entitlements: Entitlements;
|
||||||
appearance: Appearance
|
appearance: Appearance;
|
||||||
experiments: Experiments
|
experiments: Experiments;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DashboardProviderContext = createContext<
|
export const DashboardProviderContext = createContext<
|
||||||
DashboardProviderValue | undefined
|
DashboardProviderValue | undefined
|
||||||
>(undefined)
|
>(undefined);
|
||||||
|
|
||||||
export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
|
export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
const [buildInfoState] = useMachine(buildInfoMachine)
|
const [buildInfoState] = useMachine(buildInfoMachine);
|
||||||
const [entitlementsState] = useMachine(entitlementsMachine)
|
const [entitlementsState] = useMachine(entitlementsMachine);
|
||||||
const [appearanceState, appearanceSend] = useMachine(appearanceMachine)
|
const [appearanceState, appearanceSend] = useMachine(appearanceMachine);
|
||||||
const [experimentsState] = useMachine(experimentsMachine)
|
const [experimentsState] = useMachine(experimentsMachine);
|
||||||
const { buildInfo } = buildInfoState.context
|
const { buildInfo } = buildInfoState.context;
|
||||||
const { entitlements } = entitlementsState.context
|
const { entitlements } = entitlementsState.context;
|
||||||
const { appearance, preview } = appearanceState.context
|
const { appearance, preview } = appearanceState.context;
|
||||||
const { experiments } = experimentsState.context
|
const { experiments } = experimentsState.context;
|
||||||
const isLoading = !buildInfo || !entitlements || !appearance || !experiments
|
const isLoading = !buildInfo || !entitlements || !appearance || !experiments;
|
||||||
|
|
||||||
const setAppearancePreview = (config: AppearanceConfig) => {
|
const setAppearancePreview = (config: AppearanceConfig) => {
|
||||||
appearanceSend({
|
appearanceSend({
|
||||||
type: "SET_PREVIEW_APPEARANCE",
|
type: "SET_PREVIEW_APPEARANCE",
|
||||||
appearance: config,
|
appearance: config,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const saveAppearance = (config: AppearanceConfig) => {
|
const saveAppearance = (config: AppearanceConfig) => {
|
||||||
appearanceSend({
|
appearanceSend({
|
||||||
type: "SAVE_APPEARANCE",
|
type: "SAVE_APPEARANCE",
|
||||||
appearance: config,
|
appearance: config,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <FullScreenLoader />
|
return <FullScreenLoader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -75,25 +75,27 @@ export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</DashboardProviderContext.Provider>
|
</DashboardProviderContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useDashboard = (): DashboardProviderValue => {
|
export const useDashboard = (): DashboardProviderValue => {
|
||||||
const context = useContext(DashboardProviderContext)
|
const context = useContext(DashboardProviderContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useDashboard only can be used inside of DashboardProvider")
|
throw new Error(
|
||||||
|
"useDashboard only can be used inside of DashboardProvider",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useIsWorkspaceActionsEnabled = (): boolean => {
|
export const useIsWorkspaceActionsEnabled = (): boolean => {
|
||||||
const { entitlements, experiments } = useDashboard()
|
const { entitlements, experiments } = useDashboard();
|
||||||
const allowAdvancedScheduling =
|
const allowAdvancedScheduling =
|
||||||
entitlements.features["advanced_template_scheduling"].enabled
|
entitlements.features["advanced_template_scheduling"].enabled;
|
||||||
// This check can be removed when https://github.com/coder/coder/milestone/19
|
// This check can be removed when https://github.com/coder/coder/milestone/19
|
||||||
// is merged up
|
// is merged up
|
||||||
const allowWorkspaceActions = experiments.includes("workspace_actions")
|
const allowWorkspaceActions = experiments.includes("workspace_actions");
|
||||||
return allowWorkspaceActions && allowAdvancedScheduling
|
return allowWorkspaceActions && allowAdvancedScheduling;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { useMachine } from "@xstate/react"
|
import { useMachine } from "@xstate/react";
|
||||||
import { usePermissions } from "hooks/usePermissions"
|
import { usePermissions } from "hooks/usePermissions";
|
||||||
import { DeploymentBannerView } from "./DeploymentBannerView"
|
import { DeploymentBannerView } from "./DeploymentBannerView";
|
||||||
import { deploymentStatsMachine } from "xServices/deploymentStats/deploymentStatsMachine"
|
import { deploymentStatsMachine } from "xServices/deploymentStats/deploymentStatsMachine";
|
||||||
|
|
||||||
export const DeploymentBanner: React.FC = () => {
|
export const DeploymentBanner: React.FC = () => {
|
||||||
const permissions = usePermissions()
|
const permissions = usePermissions();
|
||||||
const [state, sendEvent] = useMachine(deploymentStatsMachine)
|
const [state, sendEvent] = useMachine(deploymentStatsMachine);
|
||||||
|
|
||||||
if (!permissions.viewDeploymentValues || !state.context.deploymentStats) {
|
if (!permissions.viewDeploymentValues || !state.context.deploymentStats) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -16,5 +16,5 @@ export const DeploymentBanner: React.FC = () => {
|
||||||
stats={state.context.deploymentStats}
|
stats={state.context.deploymentStats}
|
||||||
fetchStats={() => sendEvent("RELOAD")}
|
fetchStats={() => sendEvent("RELOAD")}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react";
|
||||||
import { MockDeploymentStats } from "testHelpers/entities"
|
import { MockDeploymentStats } from "testHelpers/entities";
|
||||||
import {
|
import {
|
||||||
DeploymentBannerView,
|
DeploymentBannerView,
|
||||||
DeploymentBannerViewProps,
|
DeploymentBannerViewProps,
|
||||||
} from "./DeploymentBannerView"
|
} from "./DeploymentBannerView";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "components/DeploymentBannerView",
|
title: "components/DeploymentBannerView",
|
||||||
component: DeploymentBannerView,
|
component: DeploymentBannerView,
|
||||||
}
|
};
|
||||||
|
|
||||||
const Template: Story<DeploymentBannerViewProps> = (args) => (
|
const Template: Story<DeploymentBannerViewProps> = (args) => (
|
||||||
<DeploymentBannerView {...args} />
|
<DeploymentBannerView {...args} />
|
||||||
)
|
);
|
||||||
|
|
||||||
export const Preview = Template.bind({})
|
export const Preview = Template.bind({});
|
||||||
Preview.args = {
|
Preview.args = {
|
||||||
stats: MockDeploymentStats,
|
stats: MockDeploymentStats,
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,83 +1,83 @@
|
||||||
import { DeploymentStats, WorkspaceStatus } from "api/typesGenerated"
|
import { DeploymentStats, WorkspaceStatus } from "api/typesGenerated";
|
||||||
import { FC, useMemo, useEffect, useState } from "react"
|
import { FC, useMemo, useEffect, useState } from "react";
|
||||||
import prettyBytes from "pretty-bytes"
|
import prettyBytes from "pretty-bytes";
|
||||||
import BuildingIcon from "@mui/icons-material/Build"
|
import BuildingIcon from "@mui/icons-material/Build";
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
import { RocketIcon } from "components/Icons/RocketIcon"
|
import { RocketIcon } from "components/Icons/RocketIcon";
|
||||||
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
|
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
|
||||||
import Tooltip from "@mui/material/Tooltip"
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import { Link as RouterLink } from "react-router-dom"
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import Link from "@mui/material/Link"
|
import Link from "@mui/material/Link";
|
||||||
import { VSCodeIcon } from "components/Icons/VSCodeIcon"
|
import { VSCodeIcon } from "components/Icons/VSCodeIcon";
|
||||||
import DownloadIcon from "@mui/icons-material/CloudDownload"
|
import DownloadIcon from "@mui/icons-material/CloudDownload";
|
||||||
import UploadIcon from "@mui/icons-material/CloudUpload"
|
import UploadIcon from "@mui/icons-material/CloudUpload";
|
||||||
import LatencyIcon from "@mui/icons-material/SettingsEthernet"
|
import LatencyIcon from "@mui/icons-material/SettingsEthernet";
|
||||||
import WebTerminalIcon from "@mui/icons-material/WebAsset"
|
import WebTerminalIcon from "@mui/icons-material/WebAsset";
|
||||||
import { TerminalIcon } from "components/Icons/TerminalIcon"
|
import { TerminalIcon } from "components/Icons/TerminalIcon";
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs";
|
||||||
import CollectedIcon from "@mui/icons-material/Compare"
|
import CollectedIcon from "@mui/icons-material/Compare";
|
||||||
import RefreshIcon from "@mui/icons-material/Refresh"
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
import Button from "@mui/material/Button"
|
import Button from "@mui/material/Button";
|
||||||
import { getDisplayWorkspaceStatus } from "utils/workspace"
|
import { getDisplayWorkspaceStatus } from "utils/workspace";
|
||||||
|
|
||||||
export const bannerHeight = 36
|
export const bannerHeight = 36;
|
||||||
|
|
||||||
export interface DeploymentBannerViewProps {
|
export interface DeploymentBannerViewProps {
|
||||||
fetchStats?: () => void
|
fetchStats?: () => void;
|
||||||
stats?: DeploymentStats
|
stats?: DeploymentStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
|
export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
|
||||||
stats,
|
stats,
|
||||||
fetchStats,
|
fetchStats,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
const aggregatedMinutes = useMemo(() => {
|
const aggregatedMinutes = useMemo(() => {
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
return dayjs(stats.collected_at).diff(stats.aggregated_from, "minutes")
|
return dayjs(stats.collected_at).diff(stats.aggregated_from, "minutes");
|
||||||
}, [stats])
|
}, [stats]);
|
||||||
const displayLatency = stats?.workspaces.connection_latency_ms.P50 || -1
|
const displayLatency = stats?.workspaces.connection_latency_ms.P50 || -1;
|
||||||
const [timeUntilRefresh, setTimeUntilRefresh] = useState(0)
|
const [timeUntilRefresh, setTimeUntilRefresh] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!stats || !fetchStats) {
|
if (!stats || !fetchStats) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeUntilRefresh = dayjs(stats.next_update_at).diff(
|
let timeUntilRefresh = dayjs(stats.next_update_at).diff(
|
||||||
stats.collected_at,
|
stats.collected_at,
|
||||||
"seconds",
|
"seconds",
|
||||||
)
|
);
|
||||||
setTimeUntilRefresh(timeUntilRefresh)
|
setTimeUntilRefresh(timeUntilRefresh);
|
||||||
let canceled = false
|
let canceled = false;
|
||||||
const loop = () => {
|
const loop = () => {
|
||||||
if (canceled) {
|
if (canceled) {
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
}
|
||||||
setTimeUntilRefresh(timeUntilRefresh--)
|
setTimeUntilRefresh(timeUntilRefresh--);
|
||||||
if (timeUntilRefresh > 0) {
|
if (timeUntilRefresh > 0) {
|
||||||
return window.setTimeout(loop, 1000)
|
return window.setTimeout(loop, 1000);
|
||||||
}
|
}
|
||||||
fetchStats()
|
fetchStats();
|
||||||
}
|
};
|
||||||
const timeout = setTimeout(loop, 1000)
|
const timeout = setTimeout(loop, 1000);
|
||||||
return () => {
|
return () => {
|
||||||
canceled = true
|
canceled = true;
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout);
|
||||||
}
|
};
|
||||||
}, [fetchStats, stats])
|
}, [fetchStats, stats]);
|
||||||
const lastAggregated = useMemo(() => {
|
const lastAggregated = useMemo(() => {
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (!fetchStats) {
|
if (!fetchStats) {
|
||||||
// Storybook!
|
// Storybook!
|
||||||
return "just now"
|
return "just now";
|
||||||
}
|
}
|
||||||
return dayjs().to(dayjs(stats.collected_at))
|
return dayjs().to(dayjs(stats.collected_at));
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- We want this to periodically update!
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- We want this to periodically update!
|
||||||
}, [timeUntilRefresh, stats])
|
}, [timeUntilRefresh, stats]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
@ -194,7 +194,7 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
|
||||||
className={`${styles.value} ${styles.refreshButton}`}
|
className={`${styles.value} ${styles.refreshButton}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (fetchStats) {
|
if (fetchStats) {
|
||||||
fetchStats()
|
fetchStats();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
variant="text"
|
variant="text"
|
||||||
|
@ -205,25 +205,25 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const ValueSeparator: FC = () => {
|
const ValueSeparator: FC = () => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
return <div className={styles.valueSeparator}>/</div>
|
return <div className={styles.valueSeparator}>/</div>;
|
||||||
}
|
};
|
||||||
|
|
||||||
const WorkspaceBuildValue: FC<{
|
const WorkspaceBuildValue: FC<{
|
||||||
status: WorkspaceStatus
|
status: WorkspaceStatus;
|
||||||
count?: number
|
count?: number;
|
||||||
}> = ({ status, count }) => {
|
}> = ({ status, count }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
const displayStatus = getDisplayWorkspaceStatus(status)
|
const displayStatus = getDisplayWorkspaceStatus(status);
|
||||||
let statusText = displayStatus.text
|
let statusText = displayStatus.text;
|
||||||
let icon = displayStatus.icon
|
let icon = displayStatus.icon;
|
||||||
if (status === "starting") {
|
if (status === "starting") {
|
||||||
icon = <BuildingIcon />
|
icon = <BuildingIcon />;
|
||||||
statusText = "Building"
|
statusText = "Building";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -238,8 +238,8 @@ const WorkspaceBuildValue: FC<{
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
rocket: {
|
rocket: {
|
||||||
|
@ -324,4 +324,4 @@ const useStyles = makeStyles((theme) => ({
|
||||||
marginRight: theme.spacing(0.5),
|
marginRight: theme.spacing(0.5),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { Alert } from "components/Alert/Alert"
|
import { Alert } from "components/Alert/Alert";
|
||||||
import { Link as RouterLink } from "react-router-dom"
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import Link from "@mui/material/Link"
|
import Link from "@mui/material/Link";
|
||||||
import { colors } from "theme/colors"
|
import { colors } from "theme/colors";
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getHealth } from "api/api"
|
import { getHealth } from "api/api";
|
||||||
import { useDashboard } from "./DashboardProvider"
|
import { useDashboard } from "./DashboardProvider";
|
||||||
|
|
||||||
export const HealthBanner = () => {
|
export const HealthBanner = () => {
|
||||||
const { data: healthStatus } = useQuery({
|
const { data: healthStatus } = useQuery({
|
||||||
queryKey: ["health"],
|
queryKey: ["health"],
|
||||||
queryFn: () => getHealth(),
|
queryFn: () => getHealth(),
|
||||||
})
|
});
|
||||||
const dashboard = useDashboard()
|
const dashboard = useDashboard();
|
||||||
const hasHealthIssues = healthStatus && !healthStatus.data.healthy
|
const hasHealthIssues = healthStatus && !healthStatus.data.healthy;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
dashboard.experiments.includes("deployment_health_page") &&
|
dashboard.experiments.includes("deployment_health_page") &&
|
||||||
|
@ -38,8 +38,8 @@ export const HealthBanner = () => {
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
</Alert>
|
</Alert>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { useDashboard } from "components/Dashboard/DashboardProvider"
|
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||||
import { LicenseBannerView } from "./LicenseBannerView"
|
import { LicenseBannerView } from "./LicenseBannerView";
|
||||||
|
|
||||||
export const LicenseBanner: React.FC = () => {
|
export const LicenseBanner: React.FC = () => {
|
||||||
const { entitlements } = useDashboard()
|
const { entitlements } = useDashboard();
|
||||||
const { errors, warnings } = entitlements
|
const { errors, warnings } = entitlements;
|
||||||
|
|
||||||
if (errors.length > 0 || warnings.length > 0) {
|
if (errors.length > 0 || warnings.length > 0) {
|
||||||
return <LicenseBannerView errors={errors} warnings={warnings} />
|
return <LicenseBannerView errors={errors} warnings={warnings} />;
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react";
|
||||||
import { LicenseBannerView, LicenseBannerViewProps } from "./LicenseBannerView"
|
import { LicenseBannerView, LicenseBannerViewProps } from "./LicenseBannerView";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "components/LicenseBannerView",
|
title: "components/LicenseBannerView",
|
||||||
component: LicenseBannerView,
|
component: LicenseBannerView,
|
||||||
}
|
};
|
||||||
|
|
||||||
const Template: Story<LicenseBannerViewProps> = (args) => (
|
const Template: Story<LicenseBannerViewProps> = (args) => (
|
||||||
<LicenseBannerView {...args} />
|
<LicenseBannerView {...args} />
|
||||||
)
|
);
|
||||||
|
|
||||||
export const OneWarning = Template.bind({})
|
export const OneWarning = Template.bind({});
|
||||||
OneWarning.args = {
|
OneWarning.args = {
|
||||||
errors: [],
|
errors: [],
|
||||||
warnings: ["You have exceeded the number of seats in your license."],
|
warnings: ["You have exceeded the number of seats in your license."],
|
||||||
}
|
};
|
||||||
|
|
||||||
export const TwoWarnings = Template.bind({})
|
export const TwoWarnings = Template.bind({});
|
||||||
TwoWarnings.args = {
|
TwoWarnings.args = {
|
||||||
errors: [],
|
errors: [],
|
||||||
warnings: [
|
warnings: [
|
||||||
"You have exceeded the number of seats in your license.",
|
"You have exceeded the number of seats in your license.",
|
||||||
"You are flying too close to the sun.",
|
"You are flying too close to the sun.",
|
||||||
],
|
],
|
||||||
}
|
};
|
||||||
|
|
||||||
export const OneError = Template.bind({})
|
export const OneError = Template.bind({});
|
||||||
OneError.args = {
|
OneError.args = {
|
||||||
errors: [
|
errors: [
|
||||||
"You have multiple replicas but high availability is an Enterprise feature. You will be unable to connect to workspaces.",
|
"You have multiple replicas but high availability is an Enterprise feature. You will be unable to connect to workspaces.",
|
||||||
],
|
],
|
||||||
warnings: [],
|
warnings: [],
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import Link from "@mui/material/Link"
|
import Link from "@mui/material/Link";
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
import { Expander } from "components/Expander/Expander"
|
import { Expander } from "components/Expander/Expander";
|
||||||
import { Pill } from "components/Pill/Pill"
|
import { Pill } from "components/Pill/Pill";
|
||||||
import { useState } from "react"
|
import { useState } from "react";
|
||||||
import { colors } from "theme/colors"
|
import { colors } from "theme/colors";
|
||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
licenseIssue: "License Issue",
|
licenseIssue: "License Issue",
|
||||||
|
@ -12,22 +12,22 @@ export const Language = {
|
||||||
exceeded: "It looks like you've exceeded some limits of your license.",
|
exceeded: "It looks like you've exceeded some limits of your license.",
|
||||||
lessDetails: "Less",
|
lessDetails: "Less",
|
||||||
moreDetails: "More",
|
moreDetails: "More",
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface LicenseBannerViewProps {
|
export interface LicenseBannerViewProps {
|
||||||
errors: string[]
|
errors: string[];
|
||||||
warnings: string[]
|
warnings: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({
|
export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({
|
||||||
errors,
|
errors,
|
||||||
warnings,
|
warnings,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
const [showDetails, setShowDetails] = useState(false)
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
const isError = errors.length > 0
|
const isError = errors.length > 0;
|
||||||
const messages = [...errors, ...warnings]
|
const messages = [...errors, ...warnings];
|
||||||
const type = isError ? "error" : "warning"
|
const type = isError ? "error" : "warning";
|
||||||
|
|
||||||
if (messages.length === 1) {
|
if (messages.length === 1) {
|
||||||
return (
|
return (
|
||||||
|
@ -41,7 +41,7 @@ export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.container} ${type}`}>
|
<div className={`${styles.container} ${type}`}>
|
||||||
|
@ -73,9 +73,9 @@ export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({
|
||||||
</Expander>
|
</Expander>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
container: {
|
container: {
|
||||||
|
@ -103,4 +103,4 @@ const useStyles = makeStyles((theme) => ({
|
||||||
listItem: {
|
listItem: {
|
||||||
margin: theme.spacing(0.5),
|
margin: theme.spacing(0.5),
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { render, screen, waitFor } from "@testing-library/react"
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
import { App } from "app"
|
import { App } from "app";
|
||||||
import { Language } from "./NavbarView"
|
import { Language } from "./NavbarView";
|
||||||
import { rest } from "msw"
|
import { rest } from "msw";
|
||||||
import {
|
import {
|
||||||
MockEntitlementsWithAuditLog,
|
MockEntitlementsWithAuditLog,
|
||||||
MockMemberPermissions,
|
MockMemberPermissions,
|
||||||
} from "testHelpers/entities"
|
} from "testHelpers/entities";
|
||||||
import { server } from "testHelpers/server"
|
import { server } from "testHelpers/server";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The LicenseBanner, mounted above the AppRouter, fetches entitlements. Thus, to test their
|
* The LicenseBanner, mounted above the AppRouter, fetches entitlements. Thus, to test their
|
||||||
|
@ -17,52 +17,52 @@ describe("Navbar", () => {
|
||||||
// set entitlements to allow audit log
|
// set entitlements to allow audit log
|
||||||
server.use(
|
server.use(
|
||||||
rest.get("/api/v2/entitlements", (req, res, ctx) => {
|
rest.get("/api/v2/entitlements", (req, res, ctx) => {
|
||||||
return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog))
|
return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog));
|
||||||
}),
|
}),
|
||||||
)
|
);
|
||||||
render(<App />)
|
render(<App />);
|
||||||
await waitFor(
|
await waitFor(
|
||||||
() => {
|
() => {
|
||||||
const link = screen.getByText(Language.audit)
|
const link = screen.getByText(Language.audit);
|
||||||
expect(link).toBeDefined()
|
expect(link).toBeDefined();
|
||||||
},
|
},
|
||||||
{ timeout: 2000 },
|
{ timeout: 2000 },
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
it("does not show Audit Log link when not entitled", async () => {
|
it("does not show Audit Log link when not entitled", async () => {
|
||||||
// by default, user is an Admin with permission to see the audit log,
|
// by default, user is an Admin with permission to see the audit log,
|
||||||
// but is unlicensed so not entitled to see the audit log
|
// but is unlicensed so not entitled to see the audit log
|
||||||
render(<App />)
|
render(<App />);
|
||||||
await waitFor(
|
await waitFor(
|
||||||
() => {
|
() => {
|
||||||
const link = screen.queryByText(Language.audit)
|
const link = screen.queryByText(Language.audit);
|
||||||
expect(link).toBe(null)
|
expect(link).toBe(null);
|
||||||
},
|
},
|
||||||
{ timeout: 2000 },
|
{ timeout: 2000 },
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
it("does not show Audit Log link when not permitted via role", async () => {
|
it("does not show Audit Log link when not permitted via role", async () => {
|
||||||
// set permissions to Member (can't audit)
|
// set permissions to Member (can't audit)
|
||||||
server.use(
|
server.use(
|
||||||
rest.post("/api/v2/authcheck", async (req, res, ctx) => {
|
rest.post("/api/v2/authcheck", async (req, res, ctx) => {
|
||||||
return res(ctx.status(200), ctx.json(MockMemberPermissions))
|
return res(ctx.status(200), ctx.json(MockMemberPermissions));
|
||||||
}),
|
}),
|
||||||
)
|
);
|
||||||
// set entitlements to allow audit log
|
// set entitlements to allow audit log
|
||||||
server.use(
|
server.use(
|
||||||
rest.get("/api/v2/entitlements", (req, res, ctx) => {
|
rest.get("/api/v2/entitlements", (req, res, ctx) => {
|
||||||
return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog))
|
return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog));
|
||||||
}),
|
}),
|
||||||
)
|
);
|
||||||
render(<App />)
|
render(<App />);
|
||||||
await waitFor(
|
await waitFor(
|
||||||
() => {
|
() => {
|
||||||
const link = screen.queryByText(Language.audit)
|
const link = screen.queryByText(Language.audit);
|
||||||
expect(link).toBe(null)
|
expect(link).toBe(null);
|
||||||
},
|
},
|
||||||
{ timeout: 2000 },
|
{ timeout: 2000 },
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
import { useAuth } from "components/AuthProvider/AuthProvider"
|
import { useAuth } from "components/AuthProvider/AuthProvider";
|
||||||
import { useDashboard } from "components/Dashboard/DashboardProvider"
|
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||||
import { useFeatureVisibility } from "hooks/useFeatureVisibility"
|
import { useFeatureVisibility } from "hooks/useFeatureVisibility";
|
||||||
import { useMe } from "hooks/useMe"
|
import { useMe } from "hooks/useMe";
|
||||||
import { usePermissions } from "hooks/usePermissions"
|
import { usePermissions } from "hooks/usePermissions";
|
||||||
import { FC } from "react"
|
import { FC } from "react";
|
||||||
import { NavbarView } from "./NavbarView"
|
import { NavbarView } from "./NavbarView";
|
||||||
import { useProxy } from "contexts/ProxyContext"
|
import { useProxy } from "contexts/ProxyContext";
|
||||||
|
|
||||||
export const Navbar: FC = () => {
|
export const Navbar: FC = () => {
|
||||||
const { appearance, buildInfo } = useDashboard()
|
const { appearance, buildInfo } = useDashboard();
|
||||||
const [_, authSend] = useAuth()
|
const [_, authSend] = useAuth();
|
||||||
const me = useMe()
|
const me = useMe();
|
||||||
const permissions = usePermissions()
|
const permissions = usePermissions();
|
||||||
const featureVisibility = useFeatureVisibility()
|
const featureVisibility = useFeatureVisibility();
|
||||||
const canViewAuditLog =
|
const canViewAuditLog =
|
||||||
featureVisibility["audit_log"] && Boolean(permissions.viewAuditLog)
|
featureVisibility["audit_log"] && Boolean(permissions.viewAuditLog);
|
||||||
const canViewDeployment = Boolean(permissions.viewDeploymentValues)
|
const canViewDeployment = Boolean(permissions.viewDeploymentValues);
|
||||||
const canViewAllUsers = Boolean(permissions.readAllUsers)
|
const canViewAllUsers = Boolean(permissions.readAllUsers);
|
||||||
const onSignOut = () => authSend("SIGN_OUT")
|
const onSignOut = () => authSend("SIGN_OUT");
|
||||||
const proxyContextValue = useProxy()
|
const proxyContextValue = useProxy();
|
||||||
const dashboard = useDashboard()
|
const dashboard = useDashboard();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavbarView
|
<NavbarView
|
||||||
|
@ -35,5 +35,5 @@ export const Navbar: FC = () => {
|
||||||
dashboard.experiments.includes("moons") ? proxyContextValue : undefined
|
dashboard.experiments.includes("moons") ? proxyContextValue : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react";
|
||||||
import { MockUser, MockUser2 } from "../../../testHelpers/entities"
|
import { MockUser, MockUser2 } from "../../../testHelpers/entities";
|
||||||
import { NavbarView, NavbarViewProps } from "./NavbarView"
|
import { NavbarView, NavbarViewProps } from "./NavbarView";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "components/NavbarView",
|
title: "components/NavbarView",
|
||||||
|
@ -8,38 +8,38 @@ export default {
|
||||||
argTypes: {
|
argTypes: {
|
||||||
onSignOut: { action: "Sign Out" },
|
onSignOut: { action: "Sign Out" },
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
const Template: Story<NavbarViewProps> = (args: NavbarViewProps) => (
|
const Template: Story<NavbarViewProps> = (args: NavbarViewProps) => (
|
||||||
<NavbarView {...args} />
|
<NavbarView {...args} />
|
||||||
)
|
);
|
||||||
|
|
||||||
export const ForAdmin = Template.bind({})
|
export const ForAdmin = Template.bind({});
|
||||||
ForAdmin.args = {
|
ForAdmin.args = {
|
||||||
user: MockUser,
|
user: MockUser,
|
||||||
onSignOut: () => {
|
onSignOut: () => {
|
||||||
return Promise.resolve()
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ForMember = Template.bind({})
|
export const ForMember = Template.bind({});
|
||||||
ForMember.args = {
|
ForMember.args = {
|
||||||
user: MockUser2,
|
user: MockUser2,
|
||||||
onSignOut: () => {
|
onSignOut: () => {
|
||||||
return Promise.resolve()
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const SmallViewport = Template.bind({})
|
export const SmallViewport = Template.bind({});
|
||||||
SmallViewport.args = {
|
SmallViewport.args = {
|
||||||
user: MockUser,
|
user: MockUser,
|
||||||
onSignOut: () => {
|
onSignOut: () => {
|
||||||
return Promise.resolve()
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
SmallViewport.parameters = {
|
SmallViewport.parameters = {
|
||||||
viewport: {
|
viewport: {
|
||||||
defaultViewport: "tablet",
|
defaultViewport: "tablet",
|
||||||
},
|
},
|
||||||
chromatic: { viewports: [420] },
|
chromatic: { viewports: [420] },
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { screen } from "@testing-library/react"
|
import { screen } from "@testing-library/react";
|
||||||
import {
|
import {
|
||||||
MockPrimaryWorkspaceProxy,
|
MockPrimaryWorkspaceProxy,
|
||||||
MockUser,
|
MockUser,
|
||||||
MockUser2,
|
MockUser2,
|
||||||
} from "../../../testHelpers/entities"
|
} from "../../../testHelpers/entities";
|
||||||
import { renderWithAuth } from "../../../testHelpers/renderHelpers"
|
import { renderWithAuth } from "../../../testHelpers/renderHelpers";
|
||||||
import { Language as navLanguage, NavbarView } from "./NavbarView"
|
import { Language as navLanguage, NavbarView } from "./NavbarView";
|
||||||
import { ProxyContextValue } from "contexts/ProxyContext"
|
import { ProxyContextValue } from "contexts/ProxyContext";
|
||||||
import { action } from "@storybook/addon-actions"
|
import { action } from "@storybook/addon-actions";
|
||||||
|
|
||||||
const proxyContextValue: ProxyContextValue = {
|
const proxyContextValue: ProxyContextValue = {
|
||||||
proxy: {
|
proxy: {
|
||||||
|
@ -21,24 +21,24 @@ const proxyContextValue: ProxyContextValue = {
|
||||||
clearProxy: action("clearProxy"),
|
clearProxy: action("clearProxy"),
|
||||||
refetchProxyLatencies: jest.fn(),
|
refetchProxyLatencies: jest.fn(),
|
||||||
proxyLatencies: {},
|
proxyLatencies: {},
|
||||||
}
|
};
|
||||||
|
|
||||||
describe("NavbarView", () => {
|
describe("NavbarView", () => {
|
||||||
const noop = () => {
|
const noop = () => {
|
||||||
return
|
return;
|
||||||
}
|
};
|
||||||
|
|
||||||
const env = process.env
|
const env = process.env;
|
||||||
|
|
||||||
// REMARK: copying process.env so we don't mutate that object or encounter conflicts between tests
|
// REMARK: copying process.env so we don't mutate that object or encounter conflicts between tests
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env = { ...env }
|
process.env = { ...env };
|
||||||
})
|
});
|
||||||
|
|
||||||
// REMARK: restoring process.env
|
// REMARK: restoring process.env
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
process.env = env
|
process.env = env;
|
||||||
})
|
});
|
||||||
|
|
||||||
it("workspaces nav link has the correct href", async () => {
|
it("workspaces nav link has the correct href", async () => {
|
||||||
renderWithAuth(
|
renderWithAuth(
|
||||||
|
@ -50,10 +50,10 @@ describe("NavbarView", () => {
|
||||||
canViewDeployment
|
canViewDeployment
|
||||||
canViewAllUsers
|
canViewAllUsers
|
||||||
/>,
|
/>,
|
||||||
)
|
);
|
||||||
const workspacesLink = await screen.findByText(navLanguage.workspaces)
|
const workspacesLink = await screen.findByText(navLanguage.workspaces);
|
||||||
expect((workspacesLink as HTMLAnchorElement).href).toContain("/workspaces")
|
expect((workspacesLink as HTMLAnchorElement).href).toContain("/workspaces");
|
||||||
})
|
});
|
||||||
|
|
||||||
it("templates nav link has the correct href", async () => {
|
it("templates nav link has the correct href", async () => {
|
||||||
renderWithAuth(
|
renderWithAuth(
|
||||||
|
@ -65,10 +65,10 @@ describe("NavbarView", () => {
|
||||||
canViewDeployment
|
canViewDeployment
|
||||||
canViewAllUsers
|
canViewAllUsers
|
||||||
/>,
|
/>,
|
||||||
)
|
);
|
||||||
const templatesLink = await screen.findByText(navLanguage.templates)
|
const templatesLink = await screen.findByText(navLanguage.templates);
|
||||||
expect((templatesLink as HTMLAnchorElement).href).toContain("/templates")
|
expect((templatesLink as HTMLAnchorElement).href).toContain("/templates");
|
||||||
})
|
});
|
||||||
|
|
||||||
it("users nav link has the correct href", async () => {
|
it("users nav link has the correct href", async () => {
|
||||||
renderWithAuth(
|
renderWithAuth(
|
||||||
|
@ -80,10 +80,10 @@ describe("NavbarView", () => {
|
||||||
canViewDeployment
|
canViewDeployment
|
||||||
canViewAllUsers
|
canViewAllUsers
|
||||||
/>,
|
/>,
|
||||||
)
|
);
|
||||||
const userLink = await screen.findByText(navLanguage.users)
|
const userLink = await screen.findByText(navLanguage.users);
|
||||||
expect((userLink as HTMLAnchorElement).href).toContain("/users")
|
expect((userLink as HTMLAnchorElement).href).toContain("/users");
|
||||||
})
|
});
|
||||||
|
|
||||||
it("renders profile picture for user", async () => {
|
it("renders profile picture for user", async () => {
|
||||||
// Given
|
// Given
|
||||||
|
@ -91,7 +91,7 @@ describe("NavbarView", () => {
|
||||||
...MockUser,
|
...MockUser,
|
||||||
username: "bryan",
|
username: "bryan",
|
||||||
avatar_url: "",
|
avatar_url: "",
|
||||||
}
|
};
|
||||||
|
|
||||||
// When
|
// When
|
||||||
renderWithAuth(
|
renderWithAuth(
|
||||||
|
@ -103,13 +103,13 @@ describe("NavbarView", () => {
|
||||||
canViewDeployment
|
canViewDeployment
|
||||||
canViewAllUsers
|
canViewAllUsers
|
||||||
/>,
|
/>,
|
||||||
)
|
);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
// There should be a 'B' avatar!
|
// There should be a 'B' avatar!
|
||||||
const element = await screen.findByText("B")
|
const element = await screen.findByText("B");
|
||||||
expect(element).toBeDefined()
|
expect(element).toBeDefined();
|
||||||
})
|
});
|
||||||
|
|
||||||
it("audit nav link has the correct href", async () => {
|
it("audit nav link has the correct href", async () => {
|
||||||
renderWithAuth(
|
renderWithAuth(
|
||||||
|
@ -121,10 +121,10 @@ describe("NavbarView", () => {
|
||||||
canViewDeployment
|
canViewDeployment
|
||||||
canViewAllUsers
|
canViewAllUsers
|
||||||
/>,
|
/>,
|
||||||
)
|
);
|
||||||
const auditLink = await screen.findByText(navLanguage.audit)
|
const auditLink = await screen.findByText(navLanguage.audit);
|
||||||
expect((auditLink as HTMLAnchorElement).href).toContain("/audit")
|
expect((auditLink as HTMLAnchorElement).href).toContain("/audit");
|
||||||
})
|
});
|
||||||
|
|
||||||
it("audit nav link is hidden for members", async () => {
|
it("audit nav link is hidden for members", async () => {
|
||||||
renderWithAuth(
|
renderWithAuth(
|
||||||
|
@ -136,10 +136,10 @@ describe("NavbarView", () => {
|
||||||
canViewDeployment
|
canViewDeployment
|
||||||
canViewAllUsers
|
canViewAllUsers
|
||||||
/>,
|
/>,
|
||||||
)
|
);
|
||||||
const auditLink = screen.queryByText(navLanguage.audit)
|
const auditLink = screen.queryByText(navLanguage.audit);
|
||||||
expect(auditLink).not.toBeInTheDocument()
|
expect(auditLink).not.toBeInTheDocument();
|
||||||
})
|
});
|
||||||
|
|
||||||
it("deployment nav link has the correct href", async () => {
|
it("deployment nav link has the correct href", async () => {
|
||||||
renderWithAuth(
|
renderWithAuth(
|
||||||
|
@ -151,12 +151,12 @@ describe("NavbarView", () => {
|
||||||
canViewDeployment
|
canViewDeployment
|
||||||
canViewAllUsers
|
canViewAllUsers
|
||||||
/>,
|
/>,
|
||||||
)
|
);
|
||||||
const auditLink = await screen.findByText(navLanguage.deployment)
|
const auditLink = await screen.findByText(navLanguage.deployment);
|
||||||
expect((auditLink as HTMLAnchorElement).href).toContain(
|
expect((auditLink as HTMLAnchorElement).href).toContain(
|
||||||
"/deployment/general",
|
"/deployment/general",
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
it("deployment nav link is hidden for members", async () => {
|
it("deployment nav link is hidden for members", async () => {
|
||||||
renderWithAuth(
|
renderWithAuth(
|
||||||
|
@ -168,8 +168,8 @@ describe("NavbarView", () => {
|
||||||
canViewDeployment={false}
|
canViewDeployment={false}
|
||||||
canViewAllUsers
|
canViewAllUsers
|
||||||
/>,
|
/>,
|
||||||
)
|
);
|
||||||
const auditLink = screen.queryByText(navLanguage.deployment)
|
const auditLink = screen.queryByText(navLanguage.deployment);
|
||||||
expect(auditLink).not.toBeInTheDocument()
|
expect(auditLink).not.toBeInTheDocument();
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,43 +1,45 @@
|
||||||
import Drawer from "@mui/material/Drawer"
|
import Drawer from "@mui/material/Drawer";
|
||||||
import IconButton from "@mui/material/IconButton"
|
import IconButton from "@mui/material/IconButton";
|
||||||
import List from "@mui/material/List"
|
import List from "@mui/material/List";
|
||||||
import ListItem from "@mui/material/ListItem"
|
import ListItem from "@mui/material/ListItem";
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
import MenuIcon from "@mui/icons-material/Menu"
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import { CoderIcon } from "components/Icons/CoderIcon"
|
import { CoderIcon } from "components/Icons/CoderIcon";
|
||||||
import { FC, useRef, useState } from "react"
|
import { FC, useRef, useState } from "react";
|
||||||
import { NavLink, useLocation, useNavigate } from "react-router-dom"
|
import { NavLink, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { colors } from "theme/colors"
|
import { colors } from "theme/colors";
|
||||||
import * as TypesGen from "../../../api/typesGenerated"
|
import * as TypesGen from "../../../api/typesGenerated";
|
||||||
import { navHeight } from "../../../theme/constants"
|
import { navHeight } from "../../../theme/constants";
|
||||||
import { combineClasses } from "../../../utils/combineClasses"
|
import { combineClasses } from "../../../utils/combineClasses";
|
||||||
import { UserDropdown } from "./UserDropdown/UserDropdown"
|
import { UserDropdown } from "./UserDropdown/UserDropdown";
|
||||||
import Box from "@mui/material/Box"
|
import Box from "@mui/material/Box";
|
||||||
import Menu from "@mui/material/Menu"
|
import Menu from "@mui/material/Menu";
|
||||||
import Button from "@mui/material/Button"
|
import Button from "@mui/material/Button";
|
||||||
import MenuItem from "@mui/material/MenuItem"
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutlined"
|
import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutlined";
|
||||||
import { ProxyContextValue } from "contexts/ProxyContext"
|
import { ProxyContextValue } from "contexts/ProxyContext";
|
||||||
import { displayError } from "components/GlobalSnackbar/utils"
|
import { displayError } from "components/GlobalSnackbar/utils";
|
||||||
import Divider from "@mui/material/Divider"
|
import Divider from "@mui/material/Divider";
|
||||||
import Skeleton from "@mui/material/Skeleton"
|
import Skeleton from "@mui/material/Skeleton";
|
||||||
import { BUTTON_SM_HEIGHT } from "theme/theme"
|
import { BUTTON_SM_HEIGHT } from "theme/theme";
|
||||||
import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency"
|
import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency";
|
||||||
import { usePermissions } from "hooks/usePermissions"
|
import { usePermissions } from "hooks/usePermissions";
|
||||||
import Typography from "@mui/material/Typography"
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
export const USERS_LINK = `/users?filter=${encodeURIComponent("status:active")}`
|
export const USERS_LINK = `/users?filter=${encodeURIComponent(
|
||||||
|
"status:active",
|
||||||
|
)}`;
|
||||||
|
|
||||||
export interface NavbarViewProps {
|
export interface NavbarViewProps {
|
||||||
logo_url?: string
|
logo_url?: string;
|
||||||
user?: TypesGen.User
|
user?: TypesGen.User;
|
||||||
buildInfo?: TypesGen.BuildInfoResponse
|
buildInfo?: TypesGen.BuildInfoResponse;
|
||||||
supportLinks?: TypesGen.LinkConfig[]
|
supportLinks?: TypesGen.LinkConfig[];
|
||||||
onSignOut: () => void
|
onSignOut: () => void;
|
||||||
canViewAuditLog: boolean
|
canViewAuditLog: boolean;
|
||||||
canViewDeployment: boolean
|
canViewDeployment: boolean;
|
||||||
canViewAllUsers: boolean
|
canViewAllUsers: boolean;
|
||||||
proxyContextValue?: ProxyContextValue
|
proxyContextValue?: ProxyContextValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
|
@ -46,18 +48,18 @@ export const Language = {
|
||||||
users: "Users",
|
users: "Users",
|
||||||
audit: "Audit",
|
audit: "Audit",
|
||||||
deployment: "Deployment",
|
deployment: "Deployment",
|
||||||
}
|
};
|
||||||
|
|
||||||
const NavItems: React.FC<
|
const NavItems: React.FC<
|
||||||
React.PropsWithChildren<{
|
React.PropsWithChildren<{
|
||||||
className?: string
|
className?: string;
|
||||||
canViewAuditLog: boolean
|
canViewAuditLog: boolean;
|
||||||
canViewDeployment: boolean
|
canViewDeployment: boolean;
|
||||||
canViewAllUsers: boolean
|
canViewAllUsers: boolean;
|
||||||
}>
|
}>
|
||||||
> = ({ className, canViewAuditLog, canViewDeployment, canViewAllUsers }) => {
|
> = ({ className, canViewAuditLog, canViewDeployment, canViewAllUsers }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
const location = useLocation()
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List className={combineClasses([styles.navItems, className])}>
|
<List className={combineClasses([styles.navItems, className])}>
|
||||||
|
@ -99,8 +101,8 @@ const NavItems: React.FC<
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export const NavbarView: FC<NavbarViewProps> = ({
|
export const NavbarView: FC<NavbarViewProps> = ({
|
||||||
user,
|
user,
|
||||||
logo_url,
|
logo_url,
|
||||||
|
@ -112,8 +114,8 @@ export const NavbarView: FC<NavbarViewProps> = ({
|
||||||
canViewAllUsers,
|
canViewAllUsers,
|
||||||
proxyContextValue,
|
proxyContextValue,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={styles.root}>
|
<nav className={styles.root}>
|
||||||
|
@ -122,7 +124,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
|
||||||
aria-label="Open menu"
|
aria-label="Open menu"
|
||||||
className={styles.mobileMenuButton}
|
className={styles.mobileMenuButton}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDrawerOpen(true)
|
setIsDrawerOpen(true);
|
||||||
}}
|
}}
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
|
@ -188,30 +190,30 @@ export const NavbarView: FC<NavbarViewProps> = ({
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
|
const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
|
||||||
proxyContextValue,
|
proxyContextValue,
|
||||||
}) => {
|
}) => {
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [refetchDate, setRefetchDate] = useState<Date>()
|
const [refetchDate, setRefetchDate] = useState<Date>();
|
||||||
const selectedProxy = proxyContextValue.proxy.proxy
|
const selectedProxy = proxyContextValue.proxy.proxy;
|
||||||
const refreshLatencies = proxyContextValue.refetchProxyLatencies
|
const refreshLatencies = proxyContextValue.refetchProxyLatencies;
|
||||||
const closeMenu = () => setIsOpen(false)
|
const closeMenu = () => setIsOpen(false);
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
const latencies = proxyContextValue.proxyLatencies
|
const latencies = proxyContextValue.proxyLatencies;
|
||||||
const isLoadingLatencies = Object.keys(latencies).length === 0
|
const isLoadingLatencies = Object.keys(latencies).length === 0;
|
||||||
const isLoading = proxyContextValue.isLoading || isLoadingLatencies
|
const isLoading = proxyContextValue.isLoading || isLoadingLatencies;
|
||||||
const permissions = usePermissions()
|
const permissions = usePermissions();
|
||||||
const proxyLatencyLoading = (proxy: TypesGen.Region): boolean => {
|
const proxyLatencyLoading = (proxy: TypesGen.Region): boolean => {
|
||||||
if (!refetchDate) {
|
if (!refetchDate) {
|
||||||
// Only show loading if the user manually requested a refetch
|
// Only show loading if the user manually requested a refetch
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const latency = latencies?.[proxy.id]
|
const latency = latencies?.[proxy.id];
|
||||||
// Only show a loading spinner if:
|
// Only show a loading spinner if:
|
||||||
// - A latency exists. This means the latency was fetched at some point, so the
|
// - A latency exists. This means the latency was fetched at some point, so the
|
||||||
// loader *should* be resolved.
|
// loader *should* be resolved.
|
||||||
|
@ -220,11 +222,11 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
|
||||||
// is stale and we should show a loading spinner until the new latency is
|
// is stale and we should show a loading spinner until the new latency is
|
||||||
// fetched.
|
// fetched.
|
||||||
if (proxy.healthy && latency && latency.at < refetchDate) {
|
if (proxy.healthy && latency && latency.at < refetchDate) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
@ -233,7 +235,7 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
|
||||||
height={BUTTON_SM_HEIGHT}
|
height={BUTTON_SM_HEIGHT}
|
||||||
sx={{ borderRadius: "4px", transform: "none" }}
|
sx={{ borderRadius: "4px", transform: "none" }}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -315,21 +317,21 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
|
||||||
<Divider sx={{ borderColor: (theme) => theme.palette.divider }} />
|
<Divider sx={{ borderColor: (theme) => theme.palette.divider }} />
|
||||||
{proxyContextValue.proxies
|
{proxyContextValue.proxies
|
||||||
?.sort((a, b) => {
|
?.sort((a, b) => {
|
||||||
const latencyA = latencies?.[a.id]?.latencyMS ?? Infinity
|
const latencyA = latencies?.[a.id]?.latencyMS ?? Infinity;
|
||||||
const latencyB = latencies?.[b.id]?.latencyMS ?? Infinity
|
const latencyB = latencies?.[b.id]?.latencyMS ?? Infinity;
|
||||||
return latencyA - latencyB
|
return latencyA - latencyB;
|
||||||
})
|
})
|
||||||
.map((proxy) => (
|
.map((proxy) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!proxy.healthy) {
|
if (!proxy.healthy) {
|
||||||
displayError("Please select a healthy workspace proxy.")
|
displayError("Please select a healthy workspace proxy.");
|
||||||
closeMenu()
|
closeMenu();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyContextValue.setProxy(proxy)
|
proxyContextValue.setProxy(proxy);
|
||||||
closeMenu()
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
key={proxy.id}
|
key={proxy.id}
|
||||||
selected={proxy.id === selectedProxy?.id}
|
selected={proxy.id === selectedProxy?.id}
|
||||||
|
@ -361,7 +363,7 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
|
||||||
<MenuItem
|
<MenuItem
|
||||||
sx={{ fontSize: 14 }}
|
sx={{ fontSize: 14 }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("deployment/workspace-proxies")
|
navigate("deployment/workspace-proxies");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Proxy settings
|
Proxy settings
|
||||||
|
@ -371,18 +373,18 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
|
||||||
sx={{ fontSize: 14 }}
|
sx={{ fontSize: 14 }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
// Stop the menu from closing
|
// Stop the menu from closing
|
||||||
e.stopPropagation()
|
e.stopPropagation();
|
||||||
// Refresh the latencies.
|
// Refresh the latencies.
|
||||||
const refetchDate = refreshLatencies()
|
const refetchDate = refreshLatencies();
|
||||||
setRefetchDate(refetchDate)
|
setRefetchDate(refetchDate);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Refresh Latencies
|
Refresh Latencies
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
displayInitial: {
|
displayInitial: {
|
||||||
|
@ -472,4 +474,4 @@ const useStyles = makeStyles((theme) => ({
|
||||||
padding: `0 ${theme.spacing(3)}`,
|
padding: `0 ${theme.spacing(3)}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import Popover, { PopoverProps } from "@mui/material/Popover"
|
import Popover, { PopoverProps } from "@mui/material/Popover";
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
import { FC, PropsWithChildren } from "react"
|
import { FC, PropsWithChildren } from "react";
|
||||||
|
|
||||||
type BorderedMenuVariant = "user-dropdown"
|
type BorderedMenuVariant = "user-dropdown";
|
||||||
|
|
||||||
export type BorderedMenuProps = Omit<PopoverProps, "variant"> & {
|
export type BorderedMenuProps = Omit<PopoverProps, "variant"> & {
|
||||||
variant?: BorderedMenuVariant
|
variant?: BorderedMenuVariant;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const BorderedMenu: FC<PropsWithChildren<BorderedMenuProps>> = ({
|
export const BorderedMenu: FC<PropsWithChildren<BorderedMenuProps>> = ({
|
||||||
children,
|
children,
|
||||||
variant,
|
variant,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
|
@ -23,8 +23,8 @@ export const BorderedMenu: FC<PropsWithChildren<BorderedMenuProps>> = ({
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
paperRoot: {
|
paperRoot: {
|
||||||
|
@ -32,4 +32,4 @@ const useStyles = makeStyles((theme) => ({
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
boxShadow: theme.shadows[6],
|
boxShadow: theme.shadows[6],
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
import ListItem from "@mui/material/ListItem"
|
import ListItem from "@mui/material/ListItem";
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
import CheckIcon from "@mui/icons-material/Check"
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
import { FC } from "react"
|
import { FC } from "react";
|
||||||
import { NavLink } from "react-router-dom"
|
import { NavLink } from "react-router-dom";
|
||||||
import { ellipsizeText } from "../../../../../utils/ellipsizeText"
|
import { ellipsizeText } from "../../../../../utils/ellipsizeText";
|
||||||
import { Typography } from "../../../../Typography/Typography"
|
import { Typography } from "../../../../Typography/Typography";
|
||||||
|
|
||||||
type BorderedMenuRowVariant = "narrow" | "wide"
|
type BorderedMenuRowVariant = "narrow" | "wide";
|
||||||
|
|
||||||
interface BorderedMenuRowProps {
|
interface BorderedMenuRowProps {
|
||||||
/** `true` indicates this row is currently selected */
|
/** `true` indicates this row is currently selected */
|
||||||
active?: boolean
|
active?: boolean;
|
||||||
/** Optional description that appears beneath the title */
|
/** Optional description that appears beneath the title */
|
||||||
description?: string
|
description?: string;
|
||||||
/** URL path */
|
/** URL path */
|
||||||
path: string
|
path: string;
|
||||||
/** Required title of this row */
|
/** Required title of this row */
|
||||||
title: string
|
title: string;
|
||||||
/** Defaults to `"wide"` */
|
/** Defaults to `"wide"` */
|
||||||
variant?: BorderedMenuRowVariant
|
variant?: BorderedMenuRowVariant;
|
||||||
/** Callback fired when this row is clicked */
|
/** Callback fired when this row is clicked */
|
||||||
onClick?: () => void
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BorderedMenuRow: FC<
|
export const BorderedMenuRow: FC<
|
||||||
React.PropsWithChildren<BorderedMenuRowProps>
|
React.PropsWithChildren<BorderedMenuRowProps>
|
||||||
> = ({ active, description, path, title, variant, onClick }) => {
|
> = ({ active, description, path, title, variant, onClick }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavLink className={styles.link} to={path}>
|
<NavLink className={styles.link} to={path}>
|
||||||
|
@ -54,10 +54,10 @@ export const BorderedMenuRow: FC<
|
||||||
</div>
|
</div>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const iconSize = 20
|
const iconSize = 20;
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
|
@ -127,4 +127,4 @@ const useStyles = makeStyles((theme) => ({
|
||||||
marginLeft: theme.spacing(4.5),
|
marginLeft: theme.spacing(4.5),
|
||||||
marginTop: theme.spacing(0.5),
|
marginTop: theme.spacing(0.5),
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Box from "@mui/material/Box"
|
import Box from "@mui/material/Box";
|
||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react";
|
||||||
import { MockUser } from "../../../../testHelpers/entities"
|
import { MockUser } from "../../../../testHelpers/entities";
|
||||||
import { UserDropdown, UserDropdownProps } from "./UserDropdown"
|
import { UserDropdown, UserDropdownProps } from "./UserDropdown";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "components/UserDropdown",
|
title: "components/UserDropdown",
|
||||||
|
@ -9,18 +9,18 @@ export default {
|
||||||
argTypes: {
|
argTypes: {
|
||||||
onSignOut: { action: "Sign Out" },
|
onSignOut: { action: "Sign Out" },
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
const Template: Story<UserDropdownProps> = (args: UserDropdownProps) => (
|
const Template: Story<UserDropdownProps> = (args: UserDropdownProps) => (
|
||||||
<Box style={{ backgroundColor: "#000", width: 88 }}>
|
<Box style={{ backgroundColor: "#000", width: 88 }}>
|
||||||
<UserDropdown {...args} />
|
<UserDropdown {...args} />
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
|
|
||||||
export const Example = Template.bind({})
|
export const Example = Template.bind({});
|
||||||
Example.args = {
|
Example.args = {
|
||||||
user: MockUser,
|
user: MockUser,
|
||||||
onSignOut: () => {
|
onSignOut: () => {
|
||||||
return Promise.resolve()
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { fireEvent, screen } from "@testing-library/react"
|
import { fireEvent, screen } from "@testing-library/react";
|
||||||
import { MockSupportLinks, MockUser } from "../../../../testHelpers/entities"
|
import { MockSupportLinks, MockUser } from "../../../../testHelpers/entities";
|
||||||
import { render } from "../../../../testHelpers/renderHelpers"
|
import { render } from "../../../../testHelpers/renderHelpers";
|
||||||
import { Language } from "./UserDropdownContent/UserDropdownContent"
|
import { Language } from "./UserDropdownContent/UserDropdownContent";
|
||||||
import { UserDropdown, UserDropdownProps } from "./UserDropdown"
|
import { UserDropdown, UserDropdownProps } from "./UserDropdown";
|
||||||
|
|
||||||
const renderAndClick = async (props: Partial<UserDropdownProps> = {}) => {
|
const renderAndClick = async (props: Partial<UserDropdownProps> = {}) => {
|
||||||
render(
|
render(
|
||||||
|
@ -11,20 +11,20 @@ const renderAndClick = async (props: Partial<UserDropdownProps> = {}) => {
|
||||||
supportLinks={MockSupportLinks}
|
supportLinks={MockSupportLinks}
|
||||||
onSignOut={props.onSignOut ?? jest.fn()}
|
onSignOut={props.onSignOut ?? jest.fn()}
|
||||||
/>,
|
/>,
|
||||||
)
|
);
|
||||||
const trigger = await screen.findByTestId("user-dropdown-trigger")
|
const trigger = await screen.findByTestId("user-dropdown-trigger");
|
||||||
fireEvent.click(trigger)
|
fireEvent.click(trigger);
|
||||||
}
|
};
|
||||||
|
|
||||||
describe("UserDropdown", () => {
|
describe("UserDropdown", () => {
|
||||||
describe("when the trigger is clicked", () => {
|
describe("when the trigger is clicked", () => {
|
||||||
it("opens the menu", async () => {
|
it("opens the menu", async () => {
|
||||||
await renderAndClick()
|
await renderAndClick();
|
||||||
expect(screen.getByText(Language.accountLabel)).toBeDefined()
|
expect(screen.getByText(Language.accountLabel)).toBeDefined();
|
||||||
expect(screen.getByText(MockSupportLinks[0].name)).toBeDefined()
|
expect(screen.getByText(MockSupportLinks[0].name)).toBeDefined();
|
||||||
expect(screen.getByText(MockSupportLinks[1].name)).toBeDefined()
|
expect(screen.getByText(MockSupportLinks[1].name)).toBeDefined();
|
||||||
expect(screen.getByText(MockSupportLinks[2].name)).toBeDefined()
|
expect(screen.getByText(MockSupportLinks[2].name)).toBeDefined();
|
||||||
expect(screen.getByText(Language.signOutLabel)).toBeDefined()
|
expect(screen.getByText(Language.signOutLabel)).toBeDefined();
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import Badge from "@mui/material/Badge"
|
import Badge from "@mui/material/Badge";
|
||||||
import MenuItem from "@mui/material/MenuItem"
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
import { useState, FC, PropsWithChildren, MouseEvent } from "react"
|
import { useState, FC, PropsWithChildren, MouseEvent } from "react";
|
||||||
import { colors } from "theme/colors"
|
import { colors } from "theme/colors";
|
||||||
import * as TypesGen from "../../../../api/typesGenerated"
|
import * as TypesGen from "../../../../api/typesGenerated";
|
||||||
import { navHeight } from "../../../../theme/constants"
|
import { navHeight } from "../../../../theme/constants";
|
||||||
import { BorderedMenu } from "./BorderedMenu/BorderedMenu"
|
import { BorderedMenu } from "./BorderedMenu/BorderedMenu";
|
||||||
import {
|
import {
|
||||||
CloseDropdown,
|
CloseDropdown,
|
||||||
OpenDropdown,
|
OpenDropdown,
|
||||||
} from "../../../DropdownArrows/DropdownArrows"
|
} from "../../../DropdownArrows/DropdownArrows";
|
||||||
import { UserAvatar } from "../../../UserAvatar/UserAvatar"
|
import { UserAvatar } from "../../../UserAvatar/UserAvatar";
|
||||||
import { UserDropdownContent } from "./UserDropdownContent/UserDropdownContent"
|
import { UserDropdownContent } from "./UserDropdownContent/UserDropdownContent";
|
||||||
import { BUTTON_SM_HEIGHT } from "theme/theme"
|
import { BUTTON_SM_HEIGHT } from "theme/theme";
|
||||||
|
|
||||||
export interface UserDropdownProps {
|
export interface UserDropdownProps {
|
||||||
user: TypesGen.User
|
user: TypesGen.User;
|
||||||
buildInfo?: TypesGen.BuildInfoResponse
|
buildInfo?: TypesGen.BuildInfoResponse;
|
||||||
supportLinks?: TypesGen.LinkConfig[]
|
supportLinks?: TypesGen.LinkConfig[];
|
||||||
onSignOut: () => void
|
onSignOut: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserDropdown: FC<PropsWithChildren<UserDropdownProps>> = ({
|
export const UserDropdown: FC<PropsWithChildren<UserDropdownProps>> = ({
|
||||||
|
@ -27,15 +27,15 @@ export const UserDropdown: FC<PropsWithChildren<UserDropdownProps>> = ({
|
||||||
supportLinks,
|
supportLinks,
|
||||||
onSignOut,
|
onSignOut,
|
||||||
}: UserDropdownProps) => {
|
}: UserDropdownProps) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | undefined>()
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | undefined>();
|
||||||
|
|
||||||
const handleDropdownClick = (ev: MouseEvent<HTMLLIElement>): void => {
|
const handleDropdownClick = (ev: MouseEvent<HTMLLIElement>): void => {
|
||||||
setAnchorEl(ev.currentTarget)
|
setAnchorEl(ev.currentTarget);
|
||||||
}
|
};
|
||||||
const onPopoverClose = () => {
|
const onPopoverClose = () => {
|
||||||
setAnchorEl(undefined)
|
setAnchorEl(undefined);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -88,8 +88,8 @@ export const UserDropdown: FC<PropsWithChildren<UserDropdownProps>> = ({
|
||||||
/>
|
/>
|
||||||
</BorderedMenu>
|
</BorderedMenu>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useStyles = makeStyles((theme) => ({
|
export const useStyles = makeStyles((theme) => ({
|
||||||
divider: {
|
divider: {
|
||||||
|
@ -110,4 +110,4 @@ export const useStyles = makeStyles((theme) => ({
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,36 +1,36 @@
|
||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react";
|
||||||
import { MockUser } from "../../../../../testHelpers/entities"
|
import { MockUser } from "../../../../../testHelpers/entities";
|
||||||
import {
|
import {
|
||||||
UserDropdownContent,
|
UserDropdownContent,
|
||||||
UserDropdownContentProps,
|
UserDropdownContentProps,
|
||||||
} from "./UserDropdownContent"
|
} from "./UserDropdownContent";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "components/UserDropdownContent",
|
title: "components/UserDropdownContent",
|
||||||
component: UserDropdownContent,
|
component: UserDropdownContent,
|
||||||
}
|
};
|
||||||
|
|
||||||
const Template: Story<UserDropdownContentProps> = (args) => (
|
const Template: Story<UserDropdownContentProps> = (args) => (
|
||||||
<UserDropdownContent {...args} />
|
<UserDropdownContent {...args} />
|
||||||
)
|
);
|
||||||
|
|
||||||
export const ExampleNoRoles = Template.bind({})
|
export const ExampleNoRoles = Template.bind({});
|
||||||
ExampleNoRoles.args = {
|
ExampleNoRoles.args = {
|
||||||
user: {
|
user: {
|
||||||
...MockUser,
|
...MockUser,
|
||||||
roles: [],
|
roles: [],
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ExampleOneRole = Template.bind({})
|
export const ExampleOneRole = Template.bind({});
|
||||||
ExampleOneRole.args = {
|
ExampleOneRole.args = {
|
||||||
user: {
|
user: {
|
||||||
...MockUser,
|
...MockUser,
|
||||||
roles: [{ name: "member", display_name: "Member" }],
|
roles: [{ name: "member", display_name: "Member" }],
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ExampleThreeRoles = Template.bind({})
|
export const ExampleThreeRoles = Template.bind({});
|
||||||
ExampleThreeRoles.args = {
|
ExampleThreeRoles.args = {
|
||||||
user: {
|
user: {
|
||||||
...MockUser,
|
...MockUser,
|
||||||
|
@ -40,4 +40,4 @@ ExampleThreeRoles.args = {
|
||||||
{ name: "auditor", display_name: "Auditor" },
|
{ name: "auditor", display_name: "Auditor" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import { screen } from "@testing-library/react"
|
import { screen } from "@testing-library/react";
|
||||||
import {
|
import {
|
||||||
MockBuildInfo,
|
MockBuildInfo,
|
||||||
MockSupportLinks,
|
MockSupportLinks,
|
||||||
MockUser,
|
MockUser,
|
||||||
} from "../../../../../testHelpers/entities"
|
} from "../../../../../testHelpers/entities";
|
||||||
import { render } from "../../../../../testHelpers/renderHelpers"
|
import { render } from "../../../../../testHelpers/renderHelpers";
|
||||||
import { Language, UserDropdownContent } from "./UserDropdownContent"
|
import { Language, UserDropdownContent } from "./UserDropdownContent";
|
||||||
|
|
||||||
describe("UserDropdownContent", () => {
|
describe("UserDropdownContent", () => {
|
||||||
const env = process.env
|
const env = process.env;
|
||||||
|
|
||||||
// REMARK: copying process.env so we don't mutate that object or encounter conflicts between tests
|
// REMARK: copying process.env so we don't mutate that object or encounter conflicts between tests
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env = { ...env }
|
process.env = { ...env };
|
||||||
})
|
});
|
||||||
|
|
||||||
// REMARK: restoring process.env
|
// REMARK: restoring process.env
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
process.env = env
|
process.env = env;
|
||||||
})
|
});
|
||||||
|
|
||||||
it("displays the menu items", () => {
|
it("displays the menu items", () => {
|
||||||
render(
|
render(
|
||||||
|
@ -29,21 +29,21 @@ describe("UserDropdownContent", () => {
|
||||||
onSignOut={jest.fn()}
|
onSignOut={jest.fn()}
|
||||||
onPopoverClose={jest.fn()}
|
onPopoverClose={jest.fn()}
|
||||||
/>,
|
/>,
|
||||||
)
|
);
|
||||||
expect(screen.getByText(Language.accountLabel)).toBeDefined()
|
expect(screen.getByText(Language.accountLabel)).toBeDefined();
|
||||||
expect(screen.getByText(Language.signOutLabel)).toBeDefined()
|
expect(screen.getByText(Language.signOutLabel)).toBeDefined();
|
||||||
expect(screen.getByText(Language.copyrightText)).toBeDefined()
|
expect(screen.getByText(Language.copyrightText)).toBeDefined();
|
||||||
expect(screen.getByText(MockSupportLinks[0].name)).toBeDefined()
|
expect(screen.getByText(MockSupportLinks[0].name)).toBeDefined();
|
||||||
expect(screen.getByText(MockSupportLinks[1].name)).toBeDefined()
|
expect(screen.getByText(MockSupportLinks[1].name)).toBeDefined();
|
||||||
expect(screen.getByText(MockSupportLinks[2].name)).toBeDefined()
|
expect(screen.getByText(MockSupportLinks[2].name)).toBeDefined();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(MockSupportLinks[2].name).closest("a"),
|
screen.getByText(MockSupportLinks[2].name).closest("a"),
|
||||||
).toHaveAttribute(
|
).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
"https://github.com/coder/coder/issues/new?labels=needs+grooming&body=Version%3A%20%5B%60v99.999.9999%2Bc9cdf14%60%5D(file%3A%2F%2F%2Fmock-url)",
|
"https://github.com/coder/coder/issues/new?labels=needs+grooming&body=Version%3A%20%5B%60v99.999.9999%2Bc9cdf14%60%5D(file%3A%2F%2F%2Fmock-url)",
|
||||||
)
|
);
|
||||||
expect(screen.getByText(MockBuildInfo.version)).toBeDefined()
|
expect(screen.getByText(MockBuildInfo.version)).toBeDefined();
|
||||||
})
|
});
|
||||||
|
|
||||||
it("has the correct link for the account item", () => {
|
it("has the correct link for the account item", () => {
|
||||||
render(
|
render(
|
||||||
|
@ -52,26 +52,26 @@ describe("UserDropdownContent", () => {
|
||||||
onSignOut={jest.fn()}
|
onSignOut={jest.fn()}
|
||||||
onPopoverClose={jest.fn()}
|
onPopoverClose={jest.fn()}
|
||||||
/>,
|
/>,
|
||||||
)
|
);
|
||||||
|
|
||||||
const link = screen.getByText(Language.accountLabel).closest("a")
|
const link = screen.getByText(Language.accountLabel).closest("a");
|
||||||
if (!link) {
|
if (!link) {
|
||||||
throw new Error("Anchor tag not found for the account menu item")
|
throw new Error("Anchor tag not found for the account menu item");
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(link.getAttribute("href")).toBe("/settings/account")
|
expect(link.getAttribute("href")).toBe("/settings/account");
|
||||||
})
|
});
|
||||||
|
|
||||||
it("calls the onSignOut function", () => {
|
it("calls the onSignOut function", () => {
|
||||||
const onSignOut = jest.fn()
|
const onSignOut = jest.fn();
|
||||||
render(
|
render(
|
||||||
<UserDropdownContent
|
<UserDropdownContent
|
||||||
user={MockUser}
|
user={MockUser}
|
||||||
onSignOut={onSignOut}
|
onSignOut={onSignOut}
|
||||||
onPopoverClose={jest.fn()}
|
onPopoverClose={jest.fn()}
|
||||||
/>,
|
/>,
|
||||||
)
|
);
|
||||||
screen.getByText(Language.signOutLabel).click()
|
screen.getByText(Language.signOutLabel).click();
|
||||||
expect(onSignOut).toBeCalledTimes(1)
|
expect(onSignOut).toBeCalledTimes(1);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
import Divider from "@mui/material/Divider"
|
import Divider from "@mui/material/Divider";
|
||||||
import MenuItem from "@mui/material/MenuItem"
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
import AccountIcon from "@mui/icons-material/AccountCircleOutlined"
|
import AccountIcon from "@mui/icons-material/AccountCircleOutlined";
|
||||||
import BugIcon from "@mui/icons-material/BugReportOutlined"
|
import BugIcon from "@mui/icons-material/BugReportOutlined";
|
||||||
import ChatIcon from "@mui/icons-material/ChatOutlined"
|
import ChatIcon from "@mui/icons-material/ChatOutlined";
|
||||||
import LaunchIcon from "@mui/icons-material/LaunchOutlined"
|
import LaunchIcon from "@mui/icons-material/LaunchOutlined";
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack";
|
||||||
import { FC } from "react"
|
import { FC } from "react";
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom";
|
||||||
import * as TypesGen from "../../../../../api/typesGenerated"
|
import * as TypesGen from "../../../../../api/typesGenerated";
|
||||||
import DocsIcon from "@mui/icons-material/MenuBook"
|
import DocsIcon from "@mui/icons-material/MenuBook";
|
||||||
import LogoutIcon from "@mui/icons-material/ExitToAppOutlined"
|
import LogoutIcon from "@mui/icons-material/ExitToAppOutlined";
|
||||||
import { combineClasses } from "utils/combineClasses"
|
import { combineClasses } from "utils/combineClasses";
|
||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
accountLabel: "Account",
|
accountLabel: "Account",
|
||||||
signOutLabel: "Sign Out",
|
signOutLabel: "Sign Out",
|
||||||
copyrightText: `\u00a9 ${new Date().getFullYear()} Coder Technologies, Inc.`,
|
copyrightText: `\u00a9 ${new Date().getFullYear()} Coder Technologies, Inc.`,
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface UserDropdownContentProps {
|
export interface UserDropdownContentProps {
|
||||||
user: TypesGen.User
|
user: TypesGen.User;
|
||||||
buildInfo?: TypesGen.BuildInfoResponse
|
buildInfo?: TypesGen.BuildInfoResponse;
|
||||||
supportLinks?: TypesGen.LinkConfig[]
|
supportLinks?: TypesGen.LinkConfig[];
|
||||||
onPopoverClose: () => void
|
onPopoverClose: () => void;
|
||||||
onSignOut: () => void
|
onSignOut: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserDropdownContent: FC<UserDropdownContentProps> = ({
|
export const UserDropdownContent: FC<UserDropdownContentProps> = ({
|
||||||
|
@ -34,7 +34,7 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
|
||||||
onPopoverClose,
|
onPopoverClose,
|
||||||
onSignOut,
|
onSignOut,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -101,8 +101,8 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
|
||||||
<div className={styles.footerText}>{Language.copyrightText}</div>
|
<div className={styles.footerText}>{Language.copyrightText}</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
info: {
|
info: {
|
||||||
|
@ -166,7 +166,7 @@ const useStyles = makeStyles((theme) => ({
|
||||||
buildInfo: {
|
buildInfo: {
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
||||||
const includeBuildInfo = (
|
const includeBuildInfo = (
|
||||||
href: string,
|
href: string,
|
||||||
|
@ -177,5 +177,5 @@ const includeBuildInfo = (
|
||||||
`${encodeURIComponent(
|
`${encodeURIComponent(
|
||||||
`Version: [\`${buildInfo?.version}\`](${buildInfo?.external_url})`,
|
`Version: [\`${buildInfo?.version}\`](${buildInfo?.external_url})`,
|
||||||
)}`,
|
)}`,
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { useDashboard } from "components/Dashboard/DashboardProvider"
|
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||||
import { ServiceBannerView } from "./ServiceBannerView"
|
import { ServiceBannerView } from "./ServiceBannerView";
|
||||||
|
|
||||||
export const ServiceBanner: React.FC = () => {
|
export const ServiceBanner: React.FC = () => {
|
||||||
const { appearance } = useDashboard()
|
const { appearance } = useDashboard();
|
||||||
const { message, background_color, enabled } =
|
const { message, background_color, enabled } =
|
||||||
appearance.config.service_banner
|
appearance.config.service_banner;
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message !== undefined && background_color !== undefined) {
|
if (message !== undefined && background_color !== undefined) {
|
||||||
|
@ -17,8 +17,8 @@ export const ServiceBanner: React.FC = () => {
|
||||||
backgroundColor={background_color}
|
backgroundColor={background_color}
|
||||||
preview={appearance.preview}
|
preview={appearance.preview}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react";
|
||||||
import { ServiceBannerView, ServiceBannerViewProps } from "./ServiceBannerView"
|
import { ServiceBannerView, ServiceBannerViewProps } from "./ServiceBannerView";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "components/ServiceBannerView",
|
title: "components/ServiceBannerView",
|
||||||
component: ServiceBannerView,
|
component: ServiceBannerView,
|
||||||
}
|
};
|
||||||
|
|
||||||
const Template: Story<ServiceBannerViewProps> = (args) => (
|
const Template: Story<ServiceBannerViewProps> = (args) => (
|
||||||
<ServiceBannerView {...args} />
|
<ServiceBannerView {...args} />
|
||||||
)
|
);
|
||||||
|
|
||||||
export const Production = Template.bind({})
|
export const Production = Template.bind({});
|
||||||
Production.args = {
|
Production.args = {
|
||||||
message: "weeeee",
|
message: "weeeee",
|
||||||
backgroundColor: "#FFFFFF",
|
backgroundColor: "#FFFFFF",
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Preview = Template.bind({})
|
export const Preview = Template.bind({});
|
||||||
Preview.args = {
|
Preview.args = {
|
||||||
message: "weeeee",
|
message: "weeeee",
|
||||||
backgroundColor: "#000000",
|
backgroundColor: "#000000",
|
||||||
preview: true,
|
preview: true,
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
import { Pill } from "components/Pill/Pill"
|
import { Pill } from "components/Pill/Pill";
|
||||||
import ReactMarkdown from "react-markdown"
|
import ReactMarkdown from "react-markdown";
|
||||||
import { colors } from "theme/colors"
|
import { colors } from "theme/colors";
|
||||||
import { hex } from "color-convert"
|
import { hex } from "color-convert";
|
||||||
|
|
||||||
export interface ServiceBannerViewProps {
|
export interface ServiceBannerViewProps {
|
||||||
message: string
|
message: string;
|
||||||
backgroundColor: string
|
backgroundColor: string;
|
||||||
preview: boolean
|
preview: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServiceBannerView: React.FC<ServiceBannerViewProps> = ({
|
export const ServiceBannerView: React.FC<ServiceBannerViewProps> = ({
|
||||||
|
@ -15,7 +15,7 @@ export const ServiceBannerView: React.FC<ServiceBannerViewProps> = ({
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
preview,
|
preview,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
// We don't want anything funky like an image or a heading in the service
|
// We don't want anything funky like an image or a heading in the service
|
||||||
// banner.
|
// banner.
|
||||||
const markdownElementsAllowed = [
|
const markdownElementsAllowed = [
|
||||||
|
@ -28,7 +28,7 @@ export const ServiceBannerView: React.FC<ServiceBannerViewProps> = ({
|
||||||
"italic",
|
"italic",
|
||||||
"link",
|
"link",
|
||||||
"em",
|
"em",
|
||||||
]
|
];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.container}
|
className={styles.container}
|
||||||
|
@ -50,8 +50,8 @@ export const ServiceBannerView: React.FC<ServiceBannerViewProps> = ({
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
container: {
|
container: {
|
||||||
|
@ -74,14 +74,14 @@ const useStyles = makeStyles((theme) => ({
|
||||||
color: "inherit",
|
color: "inherit",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
||||||
const readableForegroundColor = (backgroundColor: string): string => {
|
const readableForegroundColor = (backgroundColor: string): string => {
|
||||||
const rgb = hex.rgb(backgroundColor)
|
const rgb = hex.rgb(backgroundColor);
|
||||||
|
|
||||||
// Logic taken from here:
|
// Logic taken from here:
|
||||||
// https://github.com/casesandberg/react-color/blob/bc9a0e1dc5d11b06c511a8e02a95bd85c7129f4b/src/helpers/color.js#L56
|
// https://github.com/casesandberg/react-color/blob/bc9a0e1dc5d11b06c511a8e02a95bd85c7129f4b/src/helpers/color.js#L56
|
||||||
// to be consistent with the color-picker label.
|
// to be consistent with the color-picker label.
|
||||||
const yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000
|
const yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;
|
||||||
return yiq >= 128 ? "#000" : "#fff"
|
return yiq >= 128 ? "#000" : "#fff";
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,92 +1,92 @@
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles";
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack";
|
||||||
import { PropsWithChildren, FC } from "react"
|
import { PropsWithChildren, FC } from "react";
|
||||||
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
|
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
|
||||||
import { combineClasses } from "utils/combineClasses"
|
import { combineClasses } from "utils/combineClasses";
|
||||||
import Tooltip from "@mui/material/Tooltip"
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
|
||||||
export const EnabledBadge: FC = () => {
|
export const EnabledBadge: FC = () => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
return (
|
return (
|
||||||
<span className={combineClasses([styles.badge, styles.enabledBadge])}>
|
<span className={combineClasses([styles.badge, styles.enabledBadge])}>
|
||||||
Enabled
|
Enabled
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const EntitledBadge: FC = () => {
|
export const EntitledBadge: FC = () => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
return (
|
return (
|
||||||
<span className={combineClasses([styles.badge, styles.enabledBadge])}>
|
<span className={combineClasses([styles.badge, styles.enabledBadge])}>
|
||||||
Entitled
|
Entitled
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const HealthyBadge: FC<{ derpOnly: boolean }> = ({ derpOnly }) => {
|
export const HealthyBadge: FC<{ derpOnly: boolean }> = ({ derpOnly }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
let text = "Healthy"
|
let text = "Healthy";
|
||||||
if (derpOnly) {
|
if (derpOnly) {
|
||||||
text = "Healthy (DERP Only)"
|
text = "Healthy (DERP Only)";
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className={combineClasses([styles.badge, styles.enabledBadge])}>
|
<span className={combineClasses([styles.badge, styles.enabledBadge])}>
|
||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const NotHealthyBadge: FC = () => {
|
export const NotHealthyBadge: FC = () => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
return (
|
return (
|
||||||
<span className={combineClasses([styles.badge, styles.errorBadge])}>
|
<span className={combineClasses([styles.badge, styles.errorBadge])}>
|
||||||
Unhealthy
|
Unhealthy
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const NotRegisteredBadge: FC = () => {
|
export const NotRegisteredBadge: FC = () => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
return (
|
return (
|
||||||
<Tooltip title="Workspace Proxy has never come online and needs to be started.">
|
<Tooltip title="Workspace Proxy has never come online and needs to be started.">
|
||||||
<span className={combineClasses([styles.badge, styles.warnBadge])}>
|
<span className={combineClasses([styles.badge, styles.warnBadge])}>
|
||||||
Never Seen
|
Never Seen
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const NotReachableBadge: FC = () => {
|
export const NotReachableBadge: FC = () => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
return (
|
return (
|
||||||
<Tooltip title="Workspace Proxy not responding to http(s) requests.">
|
<Tooltip title="Workspace Proxy not responding to http(s) requests.">
|
||||||
<span className={combineClasses([styles.badge, styles.warnBadge])}>
|
<span className={combineClasses([styles.badge, styles.warnBadge])}>
|
||||||
Not Dialable
|
Not Dialable
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const DisabledBadge: FC = () => {
|
export const DisabledBadge: FC = () => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
return (
|
return (
|
||||||
<span className={combineClasses([styles.badge, styles.disabledBadge])}>
|
<span className={combineClasses([styles.badge, styles.disabledBadge])}>
|
||||||
Disabled
|
Disabled
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const EnterpriseBadge: FC = () => {
|
export const EnterpriseBadge: FC = () => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
return (
|
return (
|
||||||
<span className={combineClasses([styles.badge, styles.enterpriseBadge])}>
|
<span className={combineClasses([styles.badge, styles.enterpriseBadge])}>
|
||||||
Enterprise
|
Enterprise
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Badges: FC<PropsWithChildren> = ({ children }) => {
|
export const Badges: FC<PropsWithChildren> = ({ children }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles();
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
className={styles.badges}
|
className={styles.badges}
|
||||||
|
@ -96,8 +96,8 @@ export const Badges: FC<PropsWithChildren> = ({ children }) => {
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
badges: {
|
badges: {
|
||||||
|
@ -152,4 +152,4 @@ const useStyles = makeStyles((theme) => ({
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue