Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
65b324b
keycloak init
Oct 9, 2023
9a1ee61
update with KeyCloak
Oct 13, 2023
eea834f
wip add appList
Oct 16, 2023
5691792
wip integrate DORA metrics
Oct 17, 2023
ba5481d
wip 4 DORA working
Oct 18, 2023
33bab6e
wip not working
Oct 18, 2023
e39842f
Working
Oct 18, 2023
c1b71e7
Refactor to have useEffect retrigger dashboard components
Oct 19, 2023
76fad6f
Merge pull request #1 from etsauer/useeffect-refactor
cgruver Oct 19, 2023
196b614
chart for LTFC
Oct 19, 2023
9ecd599
sort dates in LTFC
Oct 19, 2023
6912369
wip for ltfc table
Oct 19, 2023
e1d7b45
add table to LTFC
Oct 20, 2023
13dcf45
Add percent change badges for ltfc and df
Oct 20, 2023
daba0dc
Merge pull request #2 from etsauer/percent-change-badges
cgruver Oct 20, 2023
1cfae5e
fix useState inits, and fix missing key in table
Oct 20, 2023
1852c18
Add a github action to build the app into a NodeJS Image
Oct 20, 2023
717cd31
Update trigger
Oct 20, 2023
4b47c80
trigger on pr
Oct 20, 2023
eec8339
Change namespace
Oct 20, 2023
4b2d081
Try vars instead of env
Oct 20, 2023
17c9f09
Add dev tag
Oct 20, 2023
d7b9c0c
wip add openshift oauth
Oct 20, 2023
535b556
wip OpenShift OAuth - hardcoded URLs
Oct 23, 2023
a7c25f1
externalize OpenShift OAuth Config
Oct 23, 2023
8228fb3
remove comments from options.js
Oct 23, 2023
7eb29d5
Merge pull request #3 from etsauer/github-action-image-build
cgruver Oct 23, 2023
54fbf3e
update scope to user:info
Oct 24, 2023
0844111
Updated measure tabs to recalculate on date selection change
Oct 25, 2023
cb64dc9
For now, go to 5 decimal places
etsauer Feb 7, 2024
16fe876
Update to pass a timestamp along with a date range to the api call
etsauer Feb 7, 2024
3b7fdf9
updated to pass query param per new api updates. Date picker now works
etsauer Feb 8, 2024
dd1eab8
Finally got the bug fixed, using a custom event handler
etsauer Mar 5, 2024
4a5ae75
OK, date picker minimally works now
etsauer Mar 5, 2024
0477e3c
Remove unused code
etsauer Mar 8, 2024
16a18ab
Refactor to store the selected date in the browser cache so it surviv…
etsauer Mar 8, 2024
6f65e68
Add a few helpful comments
etsauer Mar 8, 2024
a24d8b7
Merge pull request #4 from etsauer/date-picker
cgruver Mar 8, 2024
c7a1db7
Get chart and table populating
etsauer Mar 7, 2024
a7b2d2a
Further refactors
etsauer Mar 8, 2024
c01797a
fix issue with locality in the chart rendering
etsauer Mar 8, 2024
3976fa7
Merge pull request #5 from etsauer/finish-deployment-frequency
etsauer Mar 14, 2024
990c036
Code changes for enabling the MTTR chart and table, plus icon improve…
etsauer Mar 13, 2024
82fe01c
Enable chart and table for change failure rate
etsauer Mar 14, 2024
84c6a84
Merge pull request #6 from etsauer/finish-cfr
etsauer Mar 14, 2024
2989a1f
Get change sine list badges working for mttr and cfr
etsauer Mar 14, 2024
dbb6dbb
Merge pull request #7 from etsauer/change-since-last
etsauer Mar 14, 2024
c31c918
Modify the 'to' date to the end of the day (i.e. 23:59:59) instead of…
etsauer Mar 15, 2024
0755cf8
Refactor to gather all the API calls into one place (#9)
etsauer Mar 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/build-image.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---

name: CD

on: [push, pull_request]


jobs:

build_image:

# The type of runner that the job will run on
runs-on: ubuntu-latest

# This workflow builds a container image of a java
# application using the source to image build strategy,
# and pushes the image to quay.io.
env:
IMAGE_NAME: pelorus-dashboard
TAGS: dev ${{ github.sha }}

steps:

- name: Checkout
uses: actions/checkout@v2

# Setup S2i and Build container image
- name: Setup and Build
id: build_image
uses: redhat-actions/s2i-build@v2
with:
path_context: '.'
# Builder image for a java project
builder_image: 'registry.access.redhat.com/ubi9/nodejs-18:1'
image: ${{ env.IMAGE_NAME }}
tags: ${{ env.TAGS }}

# Push Image to Quay registry
- name: Push To Quay Action
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build_image.outputs.image }}
tags: ${{ steps.build_image.outputs.tags }}
registry: quay.io/${{ vars.QUAY_NAMESPACE }}
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/node_modules
/.pnp
.pnp.js
package-lock.json

# testing
/coverage
Expand Down
25 changes: 25 additions & 0 deletions app/api/auth/[...nextauth]/options.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
import NextAuth from "next-auth"
import GithubProvider from "next-auth/providers/github"
import GoogleProvider from "next-auth/providers/google"
import KeycloakProvider from "next-auth/providers/keycloak";

export const options = {
providers: [
{
id: "openshift",
name: "OpenShift",
type: "oauth",
issuer: process.env.OPENSHIFT_OAUTH_ISSUER_URL,
authorization: {
url: process.env.OPENSHIFT_OAUTH_AUTH_URL,
params: { scope: "user:info"}
},
token: process.env.OPENSHIFT_OAUTH_TOKEN_URL,
userinfo: process.env.OPENSHIFT_OAUTH_USERINFO_URL,
async profile(profile) {
return {
id: profile.metadata.name,
name: profile.metadata.name
}},
clientId: process.env.OPENSHIFT_ID,
clientSecret: process.env.OPENSHIFT_SECRET
},
KeycloakProvider({
clientId: process.env.KEYCLOAK_ID,
clientSecret: process.env.KEYCLOAK_SECRET,
issuer: process.env.KEYCLOAK_ISSUER,
}),
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
Expand Down
15 changes: 15 additions & 0 deletions app/api/old-api.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export async function getData() {
const response = await fetch('https://api.npoint.io/ee46484033b5d4d4658e')
if (!response.ok) {
throw new Error('Failed to fetch data')
}
return response.json()
}

export async function getApps() {
const response = await fetch(`${process.env.NEXT_PUBLIC_PELORUS_API_URL}/sdp/apps?range=1w`)
if (!response.ok) {
throw new Error('Failed to fetch list of Apps from Pelorus')
}
return response.json()
}
65 changes: 65 additions & 0 deletions app/api/pelorus-api.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use client'
import { useState, useEffect } from "react"

export async function getApps() {
const response = await fetch(`${process.env.NEXT_PUBLIC_PELORUS_API_URL}/sdp/apps?range=1w`)
if (!response.ok) {
throw new Error('Failed to fetch list of Apps from Pelorus')
}
return response.json()
}

export function getLTFC(appName) {

// const [response, setResponse] = useState('')
// useEffect(() => {
// fetch(`${process.env.NEXT_PUBLIC_PELORUS_API_URL}/sdp/lead_time_for_change/${appName}?range=1w`).then((test) => test.json()).then((data) => {setResponse(data)}).catch((error) => { console.log(error) })
// }, []);
// return response
const response = fetch(`${process.env.NEXT_PUBLIC_PELORUS_API_URL}/sdp/lead_time_for_change/${appName}?range=1w`).then((test) => test.json()).catch((error) => { console.log(error) })
return response
}

export function getDF(appName) {

// const [response, setResponse] = useState('')
// useEffect(() => {
// fetch(`${process.env.NEXT_PUBLIC_PELORUS_API_URL}/sdp/deployment_frequency/${appName}?range=1w`).then((test) => test.json()).then((data) => {setResponse(data)}).catch((error) => { console.log(error) })
// }, []);
// return response
const response = fetch(`${process.env.NEXT_PUBLIC_PELORUS_API_URL}/sdp/deployment_frequency/${appName}?range=1w`).then((test) => test.json()).catch((error) => { console.log(error) })
return response
}

export function getMTTR(appName) {
// const [response, setResponse] = useState('')
// useEffect(() => {
// fetch(`${process.env.NEXT_PUBLIC_PELORUS_API_URL}/sdp/mean_time_to_restore/${appName}?range=1w`).then((test) => test.json()).then((data) => {setResponse(data)}).catch((error) => { console.log(error) })
// }, []);
const response = fetch(`${process.env.NEXT_PUBLIC_PELORUS_API_URL}/sdp/mean_time_to_restore/${appName}?range=1w`).then((test) => test.json()).catch((error) => { console.log(error) })
return response
}

export function getCFR(appName) {
// const [response, setResponse] = useState('')
// useEffect(() => {
// fetch(`${process.env.NEXT_PUBLIC_PELORUS_API_URL}/sdp/change_failure_rate/${appName}?range=1w`).then((test) => test.json()).then((data) => {setResponse(data)}).catch((error) => { console.log(error) })
// }, []);

const response = fetch(`${process.env.NEXT_PUBLIC_PELORUS_API_URL}/sdp/change_failure_rate/${appName}?range=1w`).then((test) => test.json()).catch((error) => { console.log(error) })
return response
}

export function getDORA(appName) {

const dora = {mttr: 0, cfr: 0, df: 0, ltfc: 0}
const cfr = getCFR(appName)
dora.cfr = cfr.cfr
const df = getDF(appName)
dora.df = df.df
const mttr = getMTTR(appName)
dora.mttr = mttr.mttr
const ltfc = getLTFC(appName)
dora.ltfc = ltfc.ltfc
return dora
}
52 changes: 0 additions & 52 deletions app/dashboard-old/page.js

This file was deleted.

16 changes: 4 additions & 12 deletions app/dashboard/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,20 @@ import { getServerSession } from 'next-auth'
import { options } from '../api/auth/[...nextauth]/options'
import { redirect } from 'next/navigation'
import { AppIcon } from '@/components/app-icon'
import { Dashboard } from '@/components/dashboard-alternate'
import { Dashboard } from '@/components/dashboard'
import { Cog, FileText, LayoutGrid, Users, Search, Bell, GitBranch } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { getApps, getData } from '../api/old-api'

export const metadata = {
title: 'Dashboard - Red Hat Developer Intelligence'
}

async function getData() {
const response = await fetch('https://api.npoint.io/ee46484033b5d4d4658e')

if (!response.ok) {
throw new Error('Failed to fetch data')
}

return response.json()
}

export default async function Page() {

const session = await getServerSession(options)
const data = await getData()
const appList = await getApps()

if (!session) {
redirect('/')
Expand Down Expand Up @@ -62,7 +54,7 @@ export default async function Page() {
</div>
</header>
<div className="flex flex-col h-full ml-20">
<Dashboard data={data} />
<Dashboard data={data} appList={appList} />
</div>
</>
)
Expand Down
4 changes: 4 additions & 0 deletions app/page.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Link from 'next/link'
import ButtonSignInGithub from '@/components/button-signin-github'
import ButtonSignInGoogle from '@/components/button-signin-google'
import ButtonSignInKeyCloak from '@/components/button-signin-keycloak'
import ButtonSignInOpenShift from '@/components/button-signin-openshift'
import SignInPhoto from '@/public/images/image--sign-in.jpg'
import BlurImage from '@/components/blur-image'
import { AppLogo } from '@/components/app-logo'
Expand Down Expand Up @@ -68,6 +70,8 @@ export default async function AuthenticationPage() {
</div>
</div>
<ButtonSignInGoogle />
<ButtonSignInKeyCloak />
<ButtonSignInOpenShift />
<p className="px-8 text-center text-sm text-muted-foreground">By signing in, you agree to our <Link href="https://www.redhat.com/en/about/terms-use">terms of service</Link> and <Link href="https://www.redhat.com/en/about/privacy-policy">privacy policy</Link>.</p>
</>
)}
Expand Down
51 changes: 51 additions & 0 deletions components/app-selector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client"

import { useState, useContext } from 'react'
import { Check, ChevronsUpDown, Github, Gitlab, Box } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, } from "@/components/ui/command"
import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"

export function AppSelector({ appList, activeApp, setActiveApp }) {

const [open, setOpen] = useState(false)

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" aria-expanded={open} className="w-80 justify-between rounded-full">
<div className="flex items-center">
<Box className="mr-2 w-4 h-4" />
{activeApp && appList.find((application) => application.app === activeApp)?.app}
{!activeApp && 'Select application...'}
</div>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent align={'left'} className="w-[320px] p-0 rounded-2xl overflow-hidden">
<Command>
<CommandInput placeholder="Search application..." />
<CommandEmpty>No application found.</CommandEmpty>
<CommandGroup>
{appList.map((application) => (
<CommandItem className="flex items-center justify-between" key={application.app} onSelect={(currentValue) => {
setActiveApp(currentValue)
setOpen(false)
}}>
<div className="flex items-center">
<Box className={cn("mr-2 w-4 h-4", application.app === activeApp ? "opacity-100" : "opacity-40")} />
<span className={`${activeApp === application.app ? 'font-semibold' : 'font-base'}`}>{application.app}</span>
</div>
<Check className={cn("mr-2 h-4 w-4", activeApp === application.app ? "opacity-100" : "opacity-0")} />
</CommandItem>
))}
</CommandGroup>
</Command>
<div className="p-2">
<Button variant="secondary" className="w-full rounded-full" disabled>Customize</Button>
</div>
</PopoverContent>
</Popover>
)
}
2 changes: 1 addition & 1 deletion components/application-selector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,4 @@ export function ApplicationSelector({ applications, activeApplication, changeAct
</PopoverContent>
</Popover>
)
}
}
31 changes: 31 additions & 0 deletions components/button-signin-keycloak.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client'

import { useState } from 'react'
import { signIn } from 'next-auth/react'
import { Button } from '@/components/ui/button'
import LogoKeyCloak from '@/public/logos/keycloak_icon_512px.svg'
import { Loader2 } from "lucide-react"

export default function ButtonSignInKeyCloak() {

const [loading, setLoading] = useState(false)

function handleClick() {
setLoading(loading => !loading)
signIn('keycloak', { callbackUrl: '/dashboard' })
}

return (
<Button className="w-full" variant="outline" size="lg" type="button" onClick={handleClick}>
{loading && <Loader2 className="mr-2 h-6 w-6 animate-spin" />}
{!loading && (
<>
<span className="mr-2 w-4 h-4">
<LogoKeyCloak />
</span>
<span>Sign in with KeyCloak</span>
</>
)}
</Button>
)
}
Loading