-
Notifications
You must be signed in to change notification settings - Fork 903
feat: Add the template page #1754
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0764df3
b8ff946
02747cf
f63f04c
fac8e3e
792ee73
2822db0
b42f3e9
728c270
c011edb
5eb55cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import React from "react" | ||
|
||
const ReactMarkdown: React.FC = ({ children }) => { | ||
return <div data-testid="markdown">{children}</div> | ||
} | ||
|
||
export default ReactMarkdown |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { makeStyles } from "@material-ui/core/styles" | ||
import Table from "@material-ui/core/Table" | ||
import TableBody from "@material-ui/core/TableBody" | ||
import TableCell from "@material-ui/core/TableCell" | ||
import TableHead from "@material-ui/core/TableHead" | ||
import TableRow from "@material-ui/core/TableRow" | ||
import React from "react" | ||
import { WorkspaceResource } from "../../api/typesGenerated" | ||
import { TableHeaderRow } from "../TableHeaders/TableHeaders" | ||
|
||
const Language = { | ||
resourceLabel: "Resource", | ||
agentLabel: "Agent", | ||
} | ||
|
||
interface TemplateResourcesProps { | ||
resources: WorkspaceResource[] | ||
} | ||
|
||
export const TemplateResourcesTable: React.FC<TemplateResourcesProps> = ({ resources }) => { | ||
const styles = useStyles() | ||
|
||
return ( | ||
<Table className={styles.table}> | ||
<TableHead> | ||
<TableHeaderRow> | ||
<TableCell>{Language.resourceLabel}</TableCell> | ||
<TableCell className={styles.agentColumn}>{Language.agentLabel}</TableCell> | ||
</TableHeaderRow> | ||
</TableHead> | ||
<TableBody> | ||
{resources.map((resource) => { | ||
// We need to initialize the agents to display the resource | ||
const agents = resource.agents ?? [null] | ||
return agents.map((agent, agentIndex) => { | ||
// If there is no agent, just display the resource name | ||
if (!agent) { | ||
return ( | ||
<TableRow> | ||
<TableCell className={styles.resourceNameCell}> | ||
{resource.name} | ||
<span className={styles.resourceType}>{resource.type}</span> | ||
</TableCell> | ||
<TableCell colSpan={3}></TableCell> | ||
</TableRow> | ||
) | ||
} | ||
|
||
return ( | ||
<TableRow key={`${resource.id}-${agent.id}`}> | ||
{/* We only want to display the name in the first row because we are using rowSpan */} | ||
{/* The rowspan should be the same than the number of agents */} | ||
{agentIndex === 0 && ( | ||
<TableCell className={styles.resourceNameCell} rowSpan={agents.length}> | ||
{resource.name} | ||
<span className={styles.resourceType}>{resource.type}</span> | ||
</TableCell> | ||
)} | ||
|
||
<TableCell className={styles.agentColumn}> | ||
{agent.name} | ||
<span className={styles.operatingSystem}>{agent.operating_system}</span> | ||
</TableCell> | ||
</TableRow> | ||
) | ||
}) | ||
})} | ||
</TableBody> | ||
</Table> | ||
) | ||
} | ||
|
||
const useStyles = makeStyles((theme) => ({ | ||
sectionContents: { | ||
margin: 0, | ||
}, | ||
|
||
table: { | ||
border: 0, | ||
}, | ||
|
||
resourceNameCell: { | ||
borderRight: `1px solid ${theme.palette.divider}`, | ||
}, | ||
|
||
resourceType: { | ||
fontSize: 14, | ||
color: theme.palette.text.secondary, | ||
marginTop: theme.spacing(0.5), | ||
display: "block", | ||
}, | ||
|
||
// Adds some left spacing | ||
agentColumn: { | ||
paddingLeft: `${theme.spacing(2)}px !important`, | ||
}, | ||
|
||
operatingSystem: { | ||
fontSize: 14, | ||
color: theme.palette.text.secondary, | ||
marginTop: theme.spacing(0.5), | ||
display: "block", | ||
textTransform: "capitalize", | ||
}, | ||
})) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Story } from "@storybook/react" | ||
import React from "react" | ||
import * as Mocks from "../../testHelpers/renderHelpers" | ||
import { TemplateStats, TemplateStatsProps } from "../TemplateStats/TemplateStats" | ||
|
||
export default { | ||
title: "components/TemplateStats", | ||
component: TemplateStats, | ||
} | ||
|
||
const Template: Story<TemplateStatsProps> = (args) => <TemplateStats {...args} /> | ||
|
||
export const Example = Template.bind({}) | ||
Example.args = { | ||
template: Mocks.MockTemplate, | ||
activeVersion: Mocks.MockTemplateVersion, | ||
} | ||
|
||
export const UsedByMany = Template.bind({}) | ||
UsedByMany.args = { | ||
template: { | ||
...Mocks.MockTemplate, | ||
workspace_owner_count: 15, | ||
}, | ||
activeVersion: Mocks.MockTemplateVersion, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { makeStyles } from "@material-ui/core/styles" | ||
import dayjs from "dayjs" | ||
import relativeTime from "dayjs/plugin/relativeTime" | ||
import React from "react" | ||
import { Template, TemplateVersion } from "../../api/typesGenerated" | ||
import { CardRadius, MONOSPACE_FONT_FAMILY } from "../../theme/constants" | ||
|
||
dayjs.extend(relativeTime) | ||
|
||
const Language = { | ||
usedByLabel: "Used by", | ||
activeVersionLabel: "Active version", | ||
lastUpdateLabel: "Last updated", | ||
userPlural: "users", | ||
userSingular: "user", | ||
} | ||
|
||
export interface TemplateStatsProps { | ||
template: Template | ||
activeVersion: TemplateVersion | ||
} | ||
|
||
export const TemplateStats: React.FC<TemplateStatsProps> = ({ template, activeVersion }) => { | ||
const styles = useStyles() | ||
|
||
return ( | ||
<div className={styles.stats}> | ||
<div className={styles.statItem}> | ||
<span className={styles.statsLabel}>{Language.usedByLabel}</span> | ||
|
||
<span className={styles.statsValue}> | ||
{template.workspace_owner_count}{" "} | ||
{template.workspace_owner_count === 1 ? Language.userSingular : Language.userPlural} | ||
</span> | ||
</div> | ||
<div className={styles.statsDivider} /> | ||
<div className={styles.statItem}> | ||
<span className={styles.statsLabel}>{Language.activeVersionLabel}</span> | ||
<span className={styles.statsValue}>{activeVersion.name}</span> | ||
</div> | ||
<div className={styles.statsDivider} /> | ||
<div className={styles.statItem}> | ||
<span className={styles.statsLabel}>{Language.lastUpdateLabel}</span> | ||
<span className={styles.statsValue} data-chromatic="ignore"> | ||
{dayjs().to(dayjs(template.updated_at))} | ||
</span> | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
const useStyles = makeStyles((theme) => ({ | ||
stats: { | ||
paddingLeft: theme.spacing(2), | ||
paddingRight: theme.spacing(2), | ||
backgroundColor: theme.palette.background.paper, | ||
borderRadius: CardRadius, | ||
display: "flex", | ||
alignItems: "center", | ||
color: theme.palette.text.secondary, | ||
fontFamily: MONOSPACE_FONT_FAMILY, | ||
border: `1px solid ${theme.palette.divider}`, | ||
}, | ||
|
||
statItem: { | ||
minWidth: theme.spacing(20), | ||
padding: theme.spacing(2), | ||
paddingTop: theme.spacing(1.75), | ||
}, | ||
|
||
statsLabel: { | ||
fontSize: 12, | ||
textTransform: "uppercase", | ||
display: "block", | ||
fontWeight: 600, | ||
}, | ||
|
||
statsValue: { | ||
fontSize: 16, | ||
marginTop: theme.spacing(0.25), | ||
display: "inline-block", | ||
}, | ||
|
||
statsDivider: { | ||
width: 1, | ||
height: theme.spacing(5), | ||
backgroundColor: theme.palette.divider, | ||
marginRight: theme.spacing(2), | ||
}, | ||
})) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { useSelector } from "@xstate/react" | ||
import { useContext } from "react" | ||
import { selectOrgId } from "../xServices/auth/authSelectors" | ||
import { XServiceContext } from "../xServices/StateContext" | ||
|
||
export const useOrganizationId = (): string => { | ||
const xServices = useContext(XServiceContext) | ||
const organizationId = useSelector(xServices.authXService, selectOrgId) | ||
|
||
if (!organizationId) { | ||
throw new Error("No organization ID found") | ||
} | ||
|
||
return organizationId | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { screen } from "@testing-library/react" | ||
import React from "react" | ||
import { MockTemplate, MockWorkspaceResource, renderWithAuth } from "../../testHelpers/renderHelpers" | ||
import { TemplatePage } from "./TemplatePage" | ||
|
||
describe("TemplatePage", () => { | ||
it("shows the template name, readme and resources", async () => { | ||
renderWithAuth(<TemplatePage />, { route: `/templates/${MockTemplate.id}`, path: "/templates/:template" }) | ||
await screen.findByText(MockTemplate.name) | ||
screen.getByTestId("markdown") | ||
screen.getByText(MockWorkspaceResource.name) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { useMachine } from "@xstate/react" | ||
import React from "react" | ||
import { useParams } from "react-router-dom" | ||
import { Loader } from "../../components/Loader/Loader" | ||
import { useOrganizationId } from "../../hooks/useOrganizationId" | ||
import { templateMachine } from "../../xServices/template/templateXService" | ||
import { TemplatePageView } from "./TemplatePageView" | ||
|
||
const useTemplateName = () => { | ||
const { template } = useParams() | ||
|
||
if (!template) { | ||
throw new Error("No template found in the URL") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we just redirect? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a "never" state because react-router-dom is handling that for us but I'm doing this here to make TS happy and return the correct type. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Comment could help clarify that |
||
} | ||
|
||
return template | ||
} | ||
|
||
export const TemplatePage: React.FC = () => { | ||
const organizationId = useOrganizationId() | ||
const templateName = useTemplateName() | ||
const [templateState] = useMachine(templateMachine, { | ||
context: { | ||
templateName, | ||
organizationId, | ||
}, | ||
}) | ||
const { template, activeTemplateVersion, templateResources } = templateState.context | ||
const isLoading = !template || !activeTemplateVersion || !templateResources | ||
|
||
if (isLoading) { | ||
return <Loader /> | ||
} | ||
|
||
return ( | ||
<TemplatePageView | ||
template={template} | ||
activeTemplateVersion={activeTemplateVersion} | ||
templateResources={templateResources} | ||
/> | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we try to use a
typography
property here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably yes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since there are more styles involved I will keep it right now.