Skip to content

Commit

Permalink
Introduce HTML templating
Browse files Browse the repository at this point in the history
  • Loading branch information
mikebroberts committed May 6, 2024
1 parent 13f8513 commit e036840
Show file tree
Hide file tree
Showing 25 changed files with 943 additions and 318 deletions.
5 changes: 3 additions & 2 deletions src/app/domain/github/githubUserAuth/oauthCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { redirectResponseWithCookies } from '../../../inboundInterfaces/httpResp
import { APIGatewayProxyResult } from 'aws-lambda/trigger/api-gateway-proxy'
import { getUserByAuthToken } from '../githubUser'
import { cookies } from './cicadaAuthCookies'
import { generatePageViewResultWithoutHtmx } from '../../../web/views/viewResultWrappers'
import { pageViewResultWithoutHtmx } from '../../../web/views/viewResultWrappers'
import { p } from '../../../web/hiccough/hiccoughElements'

export async function oauthCallback(
appState: AppState,
Expand Down Expand Up @@ -64,7 +65,7 @@ async function tryOauthCallback(

function failedToLoginResult(message: string): APIGatewayProxyResult {
return {
...generatePageViewResultWithoutHtmx(`<p>${message}</p>`),
...pageViewResultWithoutHtmx([p(message)]),
statusCode: 400
}
}
29 changes: 16 additions & 13 deletions src/app/domain/github/setup/processGithubSetupRedirect.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Route } from '../../../internalHttpRouter/internalHttpRoute'
import { APIGatewayProxyEvent } from 'aws-lambda'
import { GithubSetupAppState } from './githubSetupAppState'
import { generatePageViewResultWithoutHtmx } from '../../../web/views/viewResultWrappers'
import { pageViewResultWithoutHtmx } from '../../../web/views/viewResultWrappers'
import { Octokit } from '@octokit/rest'
import { ParameterType, PutParameterCommand, SSMClient } from '@aws-sdk/client-ssm'
import {
Expand All @@ -11,6 +11,7 @@ import {
} from '../../../../multipleContexts/ssmParams'
import { logger } from '../../../util/logging'
import { fromRawAccountType, ORGANIZATION_ACCOUNT_TYPE } from '../../types/githubCommonTypes'
import { a, p } from '../../../web/hiccough/hiccoughElements'

export const setupRedirectRoute: Route<APIGatewayProxyEvent, GithubSetupAppState> = {
path: '/github/setup/redirect',
Expand Down Expand Up @@ -40,12 +41,14 @@ async function processRedirect(appState: GithubSetupAppState, event: APIGatewayP
? `https://github.com/organizations/${appDetails.ownerLogin}/settings/apps/${appDetails.appName}/installations`
: `https://github.com/settings/apps/${appDetails.appName}/installations`

return generatePageViewResultWithoutHtmx(
`<p>
Github app ${appDetails.appName} has been successsfully created.
You now need to install it in GitHub <a href="${installationsPath}">here</a>.
Once you've installed the GitHub app Cicada will start loading your GitHub data - <b>this can take a minute or more</b>.
</p>`,
return pageViewResultWithoutHtmx(
[
p(
`Github app ${appDetails.appName} has been successsfully created. You now need to install it in GitHub `,
a(installationsPath, 'here'),
`Once you've installed the GitHub app Cicada will start loading your GitHub data - <b>this can take a minute or more</b>`
)
],
false
)
}
Expand Down Expand Up @@ -125,17 +128,17 @@ async function writeSSMParameter(
)
}

const noCodeResponse = generatePageViewResultWithoutHtmx(
`<p>Unexpected redirect from GitHub - no code on URL</p>`,
const noCodeResponse = pageViewResultWithoutHtmx(
[p('Unexpected redirect from GitHub - no code on URL')],
false
)

const noStateResponse = generatePageViewResultWithoutHtmx(
`<p>Unexpected redirect from GitHub - no state on URL</p>`,
const noStateResponse = pageViewResultWithoutHtmx(
[p('Unexpected redirect from GitHub - no state on URL')],
false
)

const invalidStateResponse = generatePageViewResultWithoutHtmx(
`<p>Unexpected redirect from GitHub - invalid state on URL</p>`,
const invalidStateResponse = pageViewResultWithoutHtmx(
[p('Unexpected redirect from GitHub - invalid state on URL')],
false
)
6 changes: 3 additions & 3 deletions src/app/domain/github/setup/startGithubSetup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GithubSetupAppState } from './githubSetupAppState'
import { generatePageViewResultWithoutHtmx } from '../../../web/views/viewResultWrappers'
import { pageViewResultWithoutHtmx } from '../../../web/views/viewResultWrappers'
import { APIGatewayProxyEvent } from 'aws-lambda'
import { Route } from '../../../internalHttpRouter/internalHttpRoute'

Expand Down Expand Up @@ -49,8 +49,8 @@ GitHub Account. If you're not sure then the official GitHub docs
</form>
<script type="module">
import { modifyControls } from '/js/github-app-setup.js'
modifyControls(document, "${appName}", "${webHostname}", "${webhookCode}")
modifyControls(document, "${appName}", "${webHostname}", "${webhookCode}", "${callbackState}")
</script>`

return generatePageViewResultWithoutHtmx(bodyContents, false)
return pageViewResultWithoutHtmx([bodyContents], false)
}
9 changes: 5 additions & 4 deletions src/app/lambdaFunctions/authenticatedWeb/lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import { logoutResponse } from '../../domain/github/githubUserAuth/githubWebAuth
import { authorizeUserRequest } from '../../domain/webAuth/userAuthorizer'
import { notAuthorizedHTMLResponse } from '../../inboundInterfaces/standardHttpResponses'
import { logger } from '../../util/logging'
import { generateFragmentViewResult } from '../../web/views/viewResultWrappers'
import { fragmentViewResult } from '../../web/views/viewResultWrappers'
import { startSetupRoute } from '../../domain/github/setup/startGithubSetup'
import { isFailure } from '../../util/structuredResult'
import { a, p } from '../../web/hiccough/hiccoughElements'

const router = createRouter([showHelloRoute, showLatestActivityRoute, showRepoRoute, showWorkflowRoute])

Expand All @@ -35,9 +36,9 @@ export const baseHandler: CicadaAPIAuthorizedAPIHandler = async (event: APIGatew
return await handleWebRequest(appState, event)
}

export const setupRequiredResponse = generateFragmentViewResult(`<p>
Cicada GitHub app not ready yet. <a href="${startSetupRoute.path}">Go here to start the setup process</a>.
</p>`)
export const setupRequiredResponse = fragmentViewResult(
p('Cicada GitHub app not ready yet.', a(startSetupRoute.path, 'Go here to start the setup process'))
)

// TOEventually - top level error handler
export async function handleWebRequest(appState: AppState, event: APIGatewayProxyEvent) {
Expand Down
11 changes: 7 additions & 4 deletions src/app/lambdaFunctions/githubAuth/lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { powertoolsMiddlewares } from '../../middleware/standardMiddleware'
import { handleGitHubWebAuthRequest } from '../../domain/github/githubUserAuth/githubWebAuthHandler'
import { logger } from '../../util/logging'
import { isFailure } from '../../util/structuredResult'
import { generatePageViewResultWithoutHtmx } from '../../web/views/viewResultWrappers'
import { pageViewResultWithoutHtmx } from '../../web/views/viewResultWrappers'
import { startSetupRoute } from '../../domain/github/setup/startGithubSetup'

let appState: AppState
Expand All @@ -24,10 +24,13 @@ export const baseHandler: APIGatewayProxyHandler = async (event) => {
return await handleGitHubWebAuthRequest(appState, event)
}

export const setupRequiredResponse = generatePageViewResultWithoutHtmx(
`<p>
export const setupRequiredResponse = pageViewResultWithoutHtmx(
// TODO - use hiccough elements
[
`<p>
Cicada GitHub app not ready yet. <a href="${startSetupRoute.path}">Go here to start the setup process</a>.
</p>`,
</p>`
],
false
)

Expand Down
7 changes: 3 additions & 4 deletions src/app/lambdaFunctions/githubSetup/lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { powertoolsMiddlewares } from '../../middleware/standardMiddleware'
import { handleGithubSetupRequest } from '../../domain/github/setup/appSetupHandler'
import { GithubSetupAppState, githubSetupStartup } from '../../domain/github/setup/githubSetupAppState'
import { githubAppIsReady } from '../../domain/github/setup/githubAppReadyCheck'
import { generatePageViewResultWithoutHtmx } from '../../web/views/viewResultWrappers'
import { pageViewResultWithoutHtmx } from '../../web/views/viewResultWrappers'
import { p } from '../../web/hiccough/hiccoughElements'

let appState: GithubSetupAppState

Expand All @@ -18,9 +19,7 @@ export const baseHandler: APIGatewayProxyHandler = async (event) => {
return await handleGithubSetupRequest(appState, event)
}

const setupAlreadyCompleteResponse = generatePageViewResultWithoutHtmx(`<p>
Cicada is already configured.
</p>`)
const setupAlreadyCompleteResponse = pageViewResultWithoutHtmx([p('Cicada is already configured.')], false)

// Entry point - usage is defined by CDK
// noinspection JSUnusedGlobalSymbols
Expand Down
2 changes: 2 additions & 0 deletions src/app/util/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
export type Mandatory<T, K extends keyof T> = Pick<Required<T>, K> & Omit<T, K>
70 changes: 70 additions & 0 deletions src/app/web/hiccough/hiccoughCore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { HiccoughContent, HiccoughElement, HiccoughOptions } from './hiccoughElement'
import { Mandatory } from '../../util/types'

export function html(input: HiccoughContent | HiccoughContent[], options: HiccoughOptions = {}): string {
const fullOptions: InternalHiccoughOptions = {
newLines: false,
eachIndent: ' ',
indentCount: -1,
...options
}

return renderContentArray(Array.isArray(input) ? input : [input], fullOptions).rendered
}

type InternalHiccoughOptions = Mandatory<HiccoughOptions, 'newLines' | 'indentCount' | 'eachIndent'>

function renderContentArray(content: HiccoughContent[], options: InternalHiccoughOptions) {
const renderedElements = content
.map((x) => renderContent(x, options))
.filter((x) => x !== undefined) as RenderedContent[]

return {
rendered: renderedElements.map(({ rendered }) => rendered).join(`${options.newLines ? '\n' : ''}`),
containsStructure: renderedElements.length > 1 || renderedElements.some(({ hasChildren }) => hasChildren)
}
}

type RenderedContent = { rendered: string; hasChildren: boolean }

function renderContent(
content: HiccoughContent,
options: InternalHiccoughOptions
): RenderedContent | undefined {
if (typeof content === 'undefined') return undefined
if (typeof content === 'string') return { rendered: content, hasChildren: false }
return {
rendered: `${renderElement(content, {
...options,
indentCount: options.indentCount + 1
})}`,
hasChildren: true
}
}

function renderElement(input: HiccoughElement, parentOptions: InternalHiccoughOptions) {
const options = { ...parentOptions, ...input.options }
const childOptions = { ...options, ...{ indentFromParent: parentOptions.indentFromParent } }

const { name, attributes, content } = input,
{ rendered, containsStructure } = renderContentArray(content ?? [], childOptions),
preOpenIndentString =
options.newLines || options.indentFromParent
? Array.from({ length: options.indentCount }, () => options.eachIndent).join('')
: '',
renderedAttributes = attributes
? ' ' +
Object.entries(attributes)
.map(([k, v]) => `${k}="${v}"`)
.join(' ')
: ''

if (rendered.length === 0) {
return `${preOpenIndentString}<${name}${renderedAttributes} />`
}

const newLineString = options.newLines && containsStructure ? '\n' : '',
preCloseIndentString = options.newLines && containsStructure ? preOpenIndentString : ''

return `${preOpenIndentString}<${name}${renderedAttributes}>${newLineString}${rendered}${newLineString}${preCloseIndentString}</${name}>`
}
67 changes: 67 additions & 0 deletions src/app/web/hiccough/hiccoughElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
export type HiccoughAttributes = Record<string, string>

export type HiccoughContent = string | HiccoughElement | undefined

export type HiccoughOptions = {
newLines?: boolean
indentFromParent?: boolean
eachIndent?: string
indentCount?: number
}

export type HiccoughElement = {
isElement: true
name: string
attributes?: HiccoughAttributes
content?: HiccoughContent[]
options?: HiccoughOptions
}

function isHiccoughElement(x: unknown): x is HiccoughElement {
return typeof x === 'object' && (x as HiccoughElement).isElement
}

function isHiccoughContent(x: unknown): x is HiccoughContent {
const xtype = typeof x
return xtype === 'undefined' || xtype === 'string' || isHiccoughElement(x)
}

export type HiccoughElementDefinition =
| HiccoughContent[]
| [HiccoughAttributes | HiccoughContent, ...HiccoughContent[]]

// First element of def can be either attributes or content
export function element(name: string, ...def: HiccoughElementDefinition): HiccoughElement {
const baseElement: HiccoughElement = { isElement: true, name }
const [first, ...rest] = def

return isHiccoughContent(first)
? { ...baseElement, content: def as HiccoughContent[] }
: {
...baseElement,
attributes: first,
content: rest as HiccoughContent[]
}
}

export function withAttributes(attributes: HiccoughAttributes, element: HiccoughElement): HiccoughElement {
return {
...element,
attributes: {
...element.attributes,
...attributes
}
}
}

export function withOptions(options: HiccoughOptions, element: HiccoughElement): HiccoughElement {
return {
...element,
options
}
}

export const inlineChildren: HiccoughOptions = {
indentFromParent: true,
newLines: false
}
77 changes: 77 additions & 0 deletions src/app/web/hiccough/hiccoughElements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { element, HiccoughAttributes, HiccoughElementDefinition, withAttributes } from './hiccoughElement'

export function htmlPage(...def: HiccoughElementDefinition) {
return element('html', ...def)
}

export function head(...def: HiccoughElementDefinition) {
return element('head', ...def)
}

export function meta(...def: HiccoughElementDefinition) {
return element('meta', ...def)
}

export function title(content: string) {
return element('title', content)
}

export function link(rel: string, href: string, attributes?: HiccoughAttributes) {
return element('link', { rel, href, ...attributes })
}

export function body(...def: HiccoughElementDefinition) {
return element('body', ...def)
}

export function h1(...def: HiccoughElementDefinition) {
return element('h1', ...def)
}

export function h2(...def: HiccoughElementDefinition) {
return element('h2', ...def)
}

export function h3(...def: HiccoughElementDefinition) {
return element('h3', ...def)
}

export function h4(...def: HiccoughElementDefinition) {
return element('h4', ...def)
}

export function table(...def: HiccoughElementDefinition) {
return element('table', ...def)
}

export function thead(...def: HiccoughElementDefinition) {
return element('thead', ...def)
}

export function tbody(...def: HiccoughElementDefinition) {
return element('tbody', ...def)
}

export function tr(...def: HiccoughElementDefinition) {
return element('tr', ...def)
}

export function th(...def: HiccoughElementDefinition) {
return element('th', ...def)
}

export function td(...def: HiccoughElementDefinition) {
return element('td', ...def)
}

export function p(...def: HiccoughElementDefinition) {
return element('p', ...def)
}

export function a(href: string, ...def: HiccoughElementDefinition) {
return withAttributes({ href }, element('a', ...def))
}

export function div(...def: HiccoughElementDefinition) {
return element('div', ...def)
}
1 change: 1 addition & 0 deletions src/app/web/hiccough/hiccoughPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DOCTYPE_HTML5 = '<!doctype html>'
Loading

0 comments on commit e036840

Please sign in to comment.