Skip to content

Commit

Permalink
Working stub of event stream using u-cloudevents
Browse files Browse the repository at this point in the history
  • Loading branch information
cardil committed Mar 8, 2023
1 parent 3761f06 commit eb89f73
Show file tree
Hide file tree
Showing 24 changed files with 2,108 additions and 339 deletions.
1,948 changes: 1,697 additions & 251 deletions frontend/package-lock.json

Large diffs are not rendered by default.

40 changes: 0 additions & 40 deletions frontend/src/App.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions frontend/src/App.test.tsx → frontend/src/app.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { render, screen, waitFor } from '@testing-library/react'
import App from './App'
import App from './app'

test('renders Loading text', () => {
render(<App />)
Expand All @@ -8,7 +8,7 @@ test('renders Loading text', () => {
})

test('renders App after a while', async () => {
withEnv({ LOCAL_DELAY: '5' }, async () => {
withEnv({ STUB_DELAY: '5' }, async () => {
render(<App />)
await waitFor(() => {
const title = screen.getByText(/Welcome to Serverless/)
Expand Down
53 changes: 53 additions & 0 deletions frontend/src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import './assets/style.css'
import React from 'react'
import { Info } from './index/types'
import { factory as index } from './index/endpoint'
import { factory as events, Stream } from './events/endpoint'
import { CloudEvent } from './events/u-cloudevents'
import Lead from './components/lead'
import LeftColumn from './components/left-column'
import RightColumn from './components/right-column'
import Loader from './components/loader'

interface AppState {
info?: Info
}

class App extends React.Component<any, AppState> {
stream?: Stream<CloudEvent>

constructor(props: any) {
super(props)
this.state = {
info: undefined
}
index().info().then((info: Info) => {
this.setState({ info })
this.stream = events(info.config).stream()
this.stream.onError((error: Error) => {
console.error(error)
this.stream?.reconnect()
})
})
}

render() {
if (!this.state.info) {
return <Loader />
}
return (
<div>
<Lead info={this.state.info} />
<section className="container">
<LeftColumn
info={this.state.info}
stream={this.stream!}
/>
<RightColumn info={this.state.info} />
</section>
</div>
)
}
}

export default App
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import logo from '../assets/rhoss-icon.svg'
import InfoProps from './InfoProps'
import InfoProps from './info-props'

const Lead: React.FC<InfoProps> = (props: InfoProps) : JSX.Element => {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
import React from "react";
import InfoProps from "./InfoProps";
import React from 'react'
import { Stream } from '../events/endpoint'
import { CloudEvent } from '../events/u-cloudevents'
import InfoProps from './info-props'

class LeftColumn extends React.Component<InfoProps> {
interface LeftColumnProps extends InfoProps {
stream: Stream<CloudEvent>
}

interface LeftColumnState {
events: CloudEvent[]
}

class LeftColumn extends React.Component<LeftColumnProps, LeftColumnState> {
engine: string;

constructor(props: InfoProps) {
constructor(props: LeftColumnProps) {
super(props)
this.state = {
events: []
}
this.engine = props.info.project.platform.replace(/^([a-zA-Z0-9]+)\/.+$/, '$1')
props.stream.onData((ce: CloudEvent) => {
this.setState({
events: [ce, ...this.state.events]
})
})
}

render(): React.ReactNode {
Expand All @@ -20,7 +38,9 @@ class LeftColumn extends React.Component<InfoProps> {
</p>

<h4>Collected events:</h4>
<ul id="events-list"></ul>
<ul id="events-list">
{this.renderEvents()}
</ul>

<h4>Powered by:</h4>
<div className="references">
Expand All @@ -35,6 +55,12 @@ class LeftColumn extends React.Component<InfoProps> {
)
}

renderEvents(): React.ReactNode {
return this.state.events.map((ce: CloudEvent, i: number) => {
return (<li key={i}><code>{JSON.stringify(ce.data)}</code></li>)
})
}

renderEngine(): React.ReactNode {
switch (this.engine) {
case 'Express':
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import InfoProps from './InfoProps'
import InfoProps from './info-props'

class RightColumn extends React.Component<InfoProps> {
render(): React.ReactNode {
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/config/backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const nodeenv = process.env.NODE_ENV ?? 'development'

const shouldStubBackend = process.env.REACT_APP_BACKEND === undefined && (
nodeenv === 'development' || nodeenv === 'test'
)

const baseAddress = process.env.BACKEND ?? 'http://localhost:8080'

export {
shouldStubBackend,
baseAddress
}
23 changes: 23 additions & 0 deletions frontend/src/events/endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { shouldStubBackend } from '../config/backend'
import { Config } from '../index/types'
import { CloudEvent } from './u-cloudevents'
import Stub from './stub'
import ServerSentEvents from './sse'

export interface Stream<T> {
close(): void
reconnect(): void
onData(listener: (data: T) => void): void
onError(listener: (error: Error) => void): void
}

export interface Endpoint {
stream(): Stream<CloudEvent>
}

export const factory = (config: Config): Endpoint => {
if (shouldStubBackend) {
return new Stub(config)
}
return new ServerSentEvents()
}
25 changes: 25 additions & 0 deletions frontend/src/events/sse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { CloudEvent } from './u-cloudevents'
import { Endpoint, Stream } from './endpoint'

export default class ServerSentEvents implements Endpoint {
stream(): Stream<CloudEvent> {
return new SseStream()
}
}

class SseStream implements Stream<CloudEvent> {
private listeners: ((ce: CloudEvent) => void)[] = []

close(): void {
throw new Error('Method not implemented.')
}
reconnect(): void {
throw new Error('Method not implemented.')
}
onData(listener: (data: CloudEvent<any>) => void): void {
throw new Error('Method not implemented.')
}
onError(listener: (error: Error) => void): void {
throw new Error('Method not implemented.')
}
}
78 changes: 78 additions & 0 deletions frontend/src/events/stub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { CloudEvent } from './u-cloudevents'
import { Config } from '../index/types'
import { Endpoint, Stream } from './endpoint'

export default class Stub implements Endpoint {
private config: Config
constructor(config: Config) {
this.config = config
}
stream(): Stream<CloudEvent> {
return new StubStream(this.config)
}
}

class StubStream implements Stream<CloudEvent> {
private counter : number
private active = true
private listeners: ((ce: CloudEvent<any>) => void)[] = []
private errs: ((error: Error) => void)[] = []
private config: Config

constructor(config: Config) {
this.config = config
this.counter = 0
setInterval(() => {
this.tick()
}, 25)
}

close(): void {
this.active = false
}
reconnect(): void {
console.log('reconnecting...')
this.active = true
}
onData(listener: (data: CloudEvent<any>) => void): void {
this.listeners.push(listener)
}
onError(listener: (error: Error) => void): void {
this.errs.push(listener)
}

private tick() {
this.counter++
if (!this.active) {
return
}
const chances = {
event: this.counter % 100 === 0,
error: this.counter % 1000 === 0,
}
if (chances.error) {
return this.error()
}
if (chances.event) {
return this.emit()
}
}

private emit() {
const ce = new CloudEvent({
source: '//devdata/js',
type: 'com.redhat.openshift.example.Hello',
datacontenttype: 'application/json',
data: {
greeting: this.config.greet,
number: this.counter,
who: 'Person',
},
})
this.listeners.forEach((lst) => lst(ce))
}

private error() {

}
}
Loading

0 comments on commit eb89f73

Please sign in to comment.