fetch job and challenges lists and update JobsPerDepartment data source

This commit is contained in:
jongomez
2023-11-10 22:47:01 +00:00
committed by Jon
parent 8f8c43af08
commit 74fbe344fa
16 changed files with 1437 additions and 201 deletions
+1
View File
@@ -0,0 +1 @@
GITHUB_ACCESS_TOKEN=
@@ -45,6 +45,18 @@ const config = {
routeBasePath: '/',
path: 'docs',
},
generated: {
challenges: {
repoArray: [
{ owner: 'logos-co', repo: 'nomos-node' },
{ owner: 'logos-co', repo: 'nomos-specs' },
],
githubAccessToken: process.env.GITHUB_ACCESS_TOKEN,
},
jobList: {
jobBoard: 'waku',
},
},
}),
],
],
@@ -20,6 +20,7 @@ import {
ShowcaseCard,
JobsPerDepartment,
} from '../components/mdx'
import * as jobData from '/generated/jobs.json'
<Hero size="large">
<HeroInfo>
@@ -353,4 +354,4 @@ import {
Build on Waku
</CallToActionButton>
</Box>
<JobsPerDepartment jobBoard="codex" useDummyData />
<JobsPerDepartment jobData={jobData} />
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,289 @@
{
"departments": [
{
"id": 87842,
"name": "App",
"parent_id": 43806,
"child_ids": [87847, 87852, 87850, 87848, 45530, 87849],
"jobs": []
},
{
"id": 54504,
"name": "Brand Design Studio",
"parent_id": null,
"child_ids": [],
"jobs": []
},
{
"id": 45532,
"name": "Business Development",
"parent_id": null,
"child_ids": [],
"jobs": []
},
{
"id": 87841,
"name": "Codex",
"parent_id": 43806,
"child_ids": [],
"jobs": []
},
{
"id": 84549,
"name": "Communications",
"parent_id": null,
"child_ids": [],
"jobs": []
},
{
"id": 45531,
"name": "Design",
"parent_id": null,
"child_ids": [],
"jobs": []
},
{
"id": 87847,
"name": "Desktop",
"parent_id": 87842,
"child_ids": [],
"jobs": []
},
{
"id": 87852,
"name": "Documentation",
"parent_id": 87842,
"child_ids": [],
"jobs": []
},
{
"id": 45547,
"name": "Engineering ",
"parent_id": null,
"child_ids": [],
"jobs": []
},
{
"id": 49925,
"name": "Finance",
"parent_id": 87845,
"child_ids": [],
"jobs": []
},
{
"id": 87854,
"name": "Infrastructure",
"parent_id": 43806,
"child_ids": [],
"jobs": []
},
{
"id": 87853,
"name": "Insights",
"parent_id": 87845,
"child_ids": [],
"jobs": []
},
{
"id": 87850,
"name": "Keycard",
"parent_id": 87842,
"child_ids": [],
"jobs": []
},
{
"id": 145838,
"name": "Leadership",
"parent_id": null,
"child_ids": [],
"jobs": []
},
{
"id": 74156,
"name": "Legal",
"parent_id": 87845,
"child_ids": [],
"jobs": []
},
{
"id": 91698,
"name": "Logos",
"parent_id": null,
"child_ids": [],
"jobs": []
},
{
"id": 43807,
"name": "Marketing",
"parent_id": null,
"child_ids": [],
"jobs": []
},
{
"id": 87848,
"name": "Mobile",
"parent_id": 87842,
"child_ids": [],
"jobs": []
},
{
"id": 87843,
"name": "Nimbus",
"parent_id": 43806,
"child_ids": [],
"jobs": []
},
{
"id": 144866,
"name": "Nomos",
"parent_id": 43806,
"child_ids": [],
"jobs": []
},
{
"id": 45548,
"name": "People Operations",
"parent_id": 87845,
"child_ids": [],
"jobs": []
},
{
"id": 45530,
"name": "Product Design",
"parent_id": 87842,
"child_ids": [],
"jobs": []
},
{
"id": 90941,
"name": "Program Management",
"parent_id": null,
"child_ids": [],
"jobs": []
},
{
"id": 43806,
"name": "Research & Development",
"parent_id": null,
"child_ids": [
87842, 87841, 87854, 87843, 144866, 87846, 87981, 87847, 87852, 87850,
87848, 45530, 87849
],
"jobs": []
},
{
"id": 87851,
"name": "Security",
"parent_id": 87845,
"child_ids": [],
"jobs": []
},
{
"id": 87845,
"name": "Services",
"parent_id": null,
"child_ids": [49925, 87853, 74156, 45548, 87851],
"jobs": []
},
{
"id": 91697,
"name": "Status App ",
"parent_id": null,
"child_ids": [],
"jobs": []
},
{
"id": 54783,
"name": "Technical Writing ",
"parent_id": null,
"child_ids": [],
"jobs": []
},
{
"id": 87846,
"name": "Vac",
"parent_id": 43806,
"child_ids": [],
"jobs": []
},
{
"id": 87981,
"name": "Waku",
"parent_id": 43806,
"child_ids": [],
"jobs": [
{
"absolute_url": "https://boards.greenhouse.io/waku/jobs/5499678",
"data_compliance": [
{
"type": "gdpr",
"requires_consent": false,
"requires_processing_consent": false,
"requires_retention_consent": false,
"retention_period": null
}
],
"internal_job_id": 2707470,
"location": { "name": "Remote (Worldwide)" },
"metadata": null,
"id": 5499678,
"updated_at": "2023-11-10T10:02:59-05:00",
"requisition_id": "WAK-GL-1",
"title": "Growth Lead"
},
{
"absolute_url": "https://boards.greenhouse.io/waku/jobs/5453098",
"data_compliance": [
{
"type": "gdpr",
"requires_consent": false,
"requires_processing_consent": false,
"requires_retention_consent": false,
"retention_period": null
}
],
"internal_job_id": 1830496,
"location": { "name": "Remote, Worldwide" },
"metadata": null,
"id": 5453098,
"updated_at": "2023-10-20T07:01:13-04:00",
"requisition_id": "PROV-Sec-3",
"title": "Protocol Researcher (Distributed Systems)"
},
{
"absolute_url": "https://boards.greenhouse.io/waku/jobs/5453103",
"data_compliance": [
{
"type": "gdpr",
"requires_consent": false,
"requires_processing_consent": false,
"requires_retention_consent": false,
"retention_period": null
}
],
"internal_job_id": 2694724,
"location": { "name": "Remote (Worldwide)" },
"metadata": null,
"id": 5453103,
"updated_at": "2023-10-20T13:12:19-04:00",
"requisition_id": "WAK-BD-1",
"title": "Technical Business Development Lead "
}
]
},
{
"id": 87849,
"name": "Web",
"parent_id": 87842,
"child_ids": [],
"jobs": []
},
{
"id": 0,
"name": "No Department",
"parent_id": null,
"child_ids": [],
"jobs": []
}
]
}
@@ -29,7 +29,8 @@
"@docusaurus/utils-validation": "^2.4.1",
"docusaurus-plugin-sass": "^0.2.3",
"fs-extra": "^10.1.0",
"lodash": "^4.17.21"
"lodash": "^4.17.21",
"node-fetch": "2"
},
"engines": {
"node": ">=16.14"
@@ -121,7 +121,7 @@ export default function logosPreset(
}
}
if (options.generated) plugins.push([generatedDataPlugin, {}])
if (options.generated) plugins.push([generatedDataPlugin, options.generated])
return {
plugins,
@@ -2,14 +2,18 @@ import { PluginModule } from '@docusaurus/types'
import * as fsp from 'fs/promises'
import * as fs from 'fs'
import path from 'path'
import fetch from 'node-fetch'
import { fetchGithubIssues } from '../utils/github.utils'
type JobListConfig = {
url: string
jobBoard: string
}
type ChallengeConfig = {
repo: string
owner: string
repoArray: {
repo: string
owner: string
}[]
githubAccessToken: string
}
@@ -18,23 +22,35 @@ export type GeneratedDataPluginConfig = {
challenges?: ChallengeConfig
}
const fetchJobList = async ({ url, out }: { out: string } & JobListConfig) => {
const list = []
export type FetchJobListParams = {
jobBoard: string
out: string
}
await fsp.writeFile(out, JSON.stringify(list))
const fetchJobList = async ({ jobBoard, out }: FetchJobListParams) => {
try {
const response = await fetch(
`https://boards-api.greenhouse.io/v1/boards/${jobBoard}/departments`,
)
const jobDepartmentData = await response.json()
await fsp.writeFile(out, JSON.stringify(jobDepartmentData))
} catch (err) {
console.error('\nfetchJobList error:')
console.error(err)
}
}
const fetchChallenges = async ({
owner,
repo,
repoArray,
out,
githubAccessToken,
}: {
out: string
} & ChallengeConfig) => {
const list = []
const issues = await fetchGithubIssues(repoArray, githubAccessToken)
await fsp.writeFile(out, JSON.stringify(list))
await fsp.writeFile(out, JSON.stringify(issues))
}
export const generatedDataPlugin: PluginModule = (context, options) => {
@@ -51,14 +67,13 @@ export const generatedDataPlugin: PluginModule = (context, options) => {
loadContent: async () => {
if (jobList)
await fetchJobList({
url: jobList.url,
jobBoard: jobList.jobBoard,
out: path.join(outDir, 'jobs.json'),
})
if (challenges)
await fetchChallenges({
repo: challenges.repo,
owner: challenges.owner,
repoArray: challenges.repoArray,
githubAccessToken: challenges.githubAccessToken,
out: path.join(outDir, 'challenges.json'),
})
@@ -0,0 +1,45 @@
type GithubProject = {
name: string
}
type GithubUser = {
login: string
avatarUrl: string
}
type GithubComment = {
id: string
author: string
body: string
createdAt: string
}
type GithubAssignee = {
login: string
avatarUrl: string
}
export type GithubIssue = {
id: string
title: string
body?: string
url: string
user: GithubUser
labels: string[]
commentCount: number
comments: GithubComment[]
assignees: GithubAssignee[]
milestone?: string
created_at: string
updated_at: string
projects: GithubProject[]
}
export type GithubIssuesPerRepo = {
[key: string]: GithubIssue[]
}
export type RepoOwnerArray = {
repo: string
owner: string
}[]
@@ -0,0 +1,134 @@
import {
GithubIssue,
GithubIssuesPerRepo,
RepoOwnerArray,
} from '../types/github'
import fetch from 'node-fetch'
const https = require('https')
// Inspired by:
// https://github.com/acid-info/rnd-dynamic-list/blob/master/challenges/fetch-challenges-multiple-private-graphql.js
const sanitizeAlias = (str) => str.replace(/-/g, '_')
const createQueryPart = (owner, repo) => {
const alias = sanitizeAlias(`${owner}_${repo}`)
return `
${alias}: repository(owner: "${owner}", name: "${repo}") {
issues(first: 100, states: OPEN) {
nodes {
id
title
url
author {
login
avatarUrl
}
labels(first: 10) {
nodes {
name
}
}
commentCount: comments {
totalCount
}
commentsDetailed: comments(first: 10) {
nodes {
id
author {
login
}
body
createdAt
}
}
assignees(first: 10) {
nodes {
login
avatarUrl
}
}
milestone {
title
}
createdAt
updatedAt
projectCards(first: 10) {
nodes {
project {
name
url
}
}
}
}
}
}
`
}
export const fetchGithubIssues = async (
repoOwnerArray: RepoOwnerArray,
githubAccessToken: string,
): Promise<GithubIssuesPerRepo[]> => {
let query = '{'
repoOwnerArray.forEach(({ owner, repo }) => {
query += createQueryPart(owner, repo)
})
query += '}'
const response = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
'User-Agent': 'NodeJS-Script',
Authorization: `Bearer ${githubAccessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query }),
})
if (!response.ok) {
throw new Error(`GitHub API responded with status ${response.status}`)
}
const responseData = await response.json()
const parsedIssues: GithubIssuesPerRepo[] = repoOwnerArray.map(
({ owner, repo }) => {
const alias = sanitizeAlias(`${owner}_${repo}`)
const rawIssues = responseData.data[alias]?.issues.nodes || []
return {
[`${owner}/${repo}`]: rawIssues.map(
(issue): GithubIssue => ({
id: issue.id,
title: issue.title,
url: issue.url,
user: {
login: issue.author.login,
avatarUrl: issue.author.avatarUrl,
},
labels: issue.labels.nodes.map((label) => label.name),
commentCount: issue.commentCount.totalCount,
comments: issue.commentsDetailed.nodes.map((comment) => ({
id: comment.id,
author: comment.author.login,
body: comment.body,
createdAt: comment.createdAt,
})),
assignees: issue.assignees.nodes,
milestone: issue.milestone ? issue.milestone.title : null,
created_at: issue.createdAt,
updated_at: issue.updatedAt,
projects: issue.projectCards.nodes.map((card) => ({
name: card.project.name,
url: card.project.url,
})),
}),
),
}
},
)
return parsedIssues
}
@@ -21,11 +21,17 @@ const schema = Joi.object<PluginOptions>({
generated: Joi.object<GeneratedDataPluginConfig>({
challenges: Joi.object<GeneratedDataPluginConfig['challenges']>({
githubAccessToken: Joi.string().required(),
owner: Joi.string().required(),
repo: Joi.string().required(),
repoArray: Joi.array()
.items(
Joi.object({
repo: Joi.string().required(),
owner: Joi.string().required(),
}),
)
.required(),
}).optional(),
jobList: Joi.object<GeneratedDataPluginConfig['jobList']>({
url: Joi.string().uri().required(),
jobBoard: Joi.string().uri().required(),
}).optional(),
}).optional(),
})
@@ -1,9 +1,38 @@
import { Typography } from '@acid-info/lsd-react'
import { SingleDepartmentJobs } from './SingleDepartmentJobs'
import useFetchJobs, { JobDepartmentArray } from './useFetchJobs'
import React from 'react'
import { jobsPerDepartmentDummyData } from './jobsPerDepartmentDummyData'
export type JobDepartmentData = {
id: number
name: string
parent_id: number | null
child_ids: number[]
jobs: Job[]
}
type Job = {
absolute_url: string
data_compliance?: {
type: string
requires_consent: boolean
requires_processing_consent: boolean
requires_retention_consent: boolean
retention_period: null | string
}[]
internal_job_id: number
location: {
name: string
}
metadata?: any
id: number
updated_at: string
requisition_id: string
title: string
}
export type JobDepartmentArray = JobDepartmentData[] | null
type JobsPerDepartmentHeaderProps = {
message?: string
}
@@ -34,42 +63,39 @@ const hasJobs = (jobsPerDepartment: JobDepartmentArray): boolean => {
}
type JobsPerDepartmentProps = React.HTMLAttributes<HTMLDivElement> & {
jobBoard: string
jobData: { departments?: JobDepartmentArray }
titleFilter?: string
fetchAll?: boolean
useDummyData?: boolean
}
export const JobsPerDepartment: React.FC<JobsPerDepartmentProps> = ({
jobBoard,
jobData,
titleFilter = '',
fetchAll = false,
useDummyData = false,
...props
}) => {
const { data, error, isLoading } = useFetchJobs(
jobBoard,
titleFilter,
useDummyData ? jobsPerDepartmentDummyData : null,
)
let jobArray: JobDepartmentData[] | undefined | null = useDummyData
? jobsPerDepartmentDummyData
: jobData?.departments
if (isLoading) {
// Skipping loading state for now, as per the designer's request.
return <JobsPerDepartmentHeader />
}
if (error) {
return <JobsPerDepartmentHeader message="Error fetching data" />
}
if (!data || !hasJobs(data)) {
if (!jobArray || !hasJobs(jobArray)) {
return <JobsPerDepartmentHeader message="No job openings to show" />
}
// Filter jobs in each department based on titleFilter
if (jobArray && titleFilter) {
jobArray = jobArray.map((department) => ({
...department,
jobs: department.jobs.filter((job) => job.title.includes(titleFilter)),
}))
}
return (
<div {...props}>
<JobsPerDepartmentHeader />
{data.map((department) => {
{jobArray.map((department) => {
return (
<SingleDepartmentJobs key={department.name} department={department} />
)
@@ -1,9 +1,9 @@
import { Typography } from '@acid-info/lsd-react'
import { JobDepartmentData } from './useFetchJobs'
import React from 'react'
import { IconExternalLink } from '@logos-theme/components/Icon'
import './JobsPerDepartment.scss'
import Link from '@docusaurus/Link'
import { JobDepartmentData } from '.'
type SingleDepartmentJobsProps = {
department: JobDepartmentData
@@ -1,4 +1,4 @@
import { JobDepartmentArray } from './useFetchJobs'
import { JobDepartmentArray } from '.'
// Only for dev purposes - prevents making multiple requests to the API while developing.
export const jobsPerDepartmentDummyData: JobDepartmentArray = [
@@ -1,161 +0,0 @@
import { useEffect, useRef, useState } from 'react'
/*
// Here's an array with some possible job boards:
const allJobBoards = [
'testacidinfo', // Acid Test job board
'logos',
'nimbus',
'codex',
'status72', // Note: this job board has all the jobs from the other job boards
'nomos',
'thestatusnetwork',
'instituteoffreetechnologies', // aka IFT
'vac',
'waku',
] as const
*/
export type JobDepartmentName = string
export type JobDepartmentData = {
id: number
name: string
parent_id: number | null
child_ids: number[]
jobs: Job[]
}
export type Job = {
absolute_url: string
data_compliance?: {
type: string
requires_consent: boolean
requires_processing_consent: boolean
requires_retention_consent: boolean
retention_period: null | string
}[]
internal_job_id: number
location: {
name: string
}
metadata?: any
id: number
updated_at: string
requisition_id: string
title: string
}
export type JobDepartmentArray = JobDepartmentData[] | null
type UseFetchJobsReturn = {
data: JobDepartmentArray
error: Error | null
isLoading: boolean
}
const parseJobDepartments = (
rawJobDepartmentData: any,
titleFilter?: string,
): JobDepartmentArray => {
const jobDepartmentArray = rawJobDepartmentData?.departments
if (!jobDepartmentArray) {
throw Error('No departments found in rawJobDepartmentData')
}
if (!Array.isArray(jobDepartmentArray)) {
throw Error('The received jobDepartmentArray is not an array')
}
// Map through each department and filter its jobs based on titleFilter
const processedJobDepartmentArray: JobDepartmentData[] =
jobDepartmentArray.map((department: JobDepartmentData) => {
let filteredJobs = department.jobs
if (titleFilter) {
filteredJobs = department.jobs.filter((job) =>
job.title.includes(titleFilter),
)
}
return {
...department,
jobs: filteredJobs,
}
})
return processedJobDepartmentArray
}
// Fetches jobs from greenhouse via their department endpoint:
// https://developers.greenhouse.io/job-board.html#list-departments
export const useFetchJobs = (
jobBoardToFetch: string,
titleFilter: string = '',
dummyResponse: JobDepartmentArray = null,
): UseFetchJobsReturn => {
const [data, setData] = useState<JobDepartmentArray>(null)
const [error, setError] = useState<Error | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(false)
const isMounted = useRef(true)
const fetchJobs = async (): Promise<{
jobsPerDepartment: JobDepartmentArray
errorData: Error | null
}> => {
try {
if (dummyResponse) {
return { jobsPerDepartment: dummyResponse, errorData: null }
}
const response = await fetch(
`https://boards-api.greenhouse.io/v1/boards/${jobBoardToFetch}/departments`,
)
const jobDepartmentData = await response.json()
const jobsPerDepartment: JobDepartmentArray = parseJobDepartments(
jobDepartmentData,
titleFilter,
)
return {
jobsPerDepartment,
errorData: null,
}
} catch (err) {
const errorData =
err instanceof Error
? err
: typeof err === 'string'
? new Error(err)
: new Error('Unknown error')
console.error(err)
return { jobsPerDepartment: null, errorData }
}
}
useEffect(() => {
if (isLoading) return
setIsLoading(true)
fetchJobs().then(({ jobsPerDepartment, errorData }) => {
// We should not update state if the component is unmounted.
if (!isMounted.current) return
if (jobsPerDepartment) setData(jobsPerDepartment)
if (errorData) setError(errorData)
setIsLoading(false)
})
return () => {
isMounted.current = false
}
}, [])
return { data, error, isLoading }
}
export default useFetchJobs
+7
View File
@@ -12461,6 +12461,13 @@ node-emoji@^1.10.0:
dependencies:
lodash "^4.17.21"
node-fetch@2:
version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
dependencies:
whatwg-url "^5.0.0"
node-fetch@2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"