1
0
Fork 0
freesewing/sites/shared/components/support/status.mjs

335 lines
12 KiB
JavaScript
Raw Normal View History

2023-11-06 21:09:34 +01:00
import { useState, useEffect } from 'react'
import { siteConfig } from 'site/site.config.mjs'
2023-11-07 11:51:15 +01:00
import { nsMerge } from 'shared/utils.mjs'
import {
UserIcon,
FlagIcon,
ChatIcon,
BoolYesIcon,
WarningIcon,
DownIcon,
} from 'shared/components/icons.mjs'
import { TimeAgo, ns as timeAgoNs } from 'shared/components/timeago/index.mjs'
2023-11-06 21:09:34 +01:00
import { Mdx } from 'shared/components/mdx/dynamic.mjs'
2023-11-07 11:51:15 +01:00
import { WebLink } from 'shared/components/link.mjs'
import { useTranslation } from 'next-i18next'
export const ns = nsMerge('support', timeAgoNs)
2023-11-06 21:09:34 +01:00
/*
* GitHub GraphQL queries must be properly quoted and can't handle newlines
*/
const query = {
open:
'query { ' +
'repository(owner: "freesewing", name: "freesewing") { ' +
' issues(states: OPEN, labels: ["statusReported", "statusConfirmed"], first: 20) { ' +
' nodes { ' +
' title body createdAt url number updatedAt ' +
2023-11-07 11:51:15 +01:00
' author { login url } ' +
2023-11-06 21:09:34 +01:00
' labels (first: 5) { edges { node { name } } } ' +
2023-11-07 11:51:15 +01:00
' comments(last: 3) { edges { node { body createdAt url author { login url } } } } ' +
' timelineItems(last: 15, itemTypes:[ ISSUE_COMMENT, CLOSED_EVENT,ASSIGNED_EVENT,REOPENED_EVENT, REFERENCED_EVENT]) { edges { node { ' +
' __typename ' +
' ... on ClosedEvent { createdAt actor { url login } } ' +
' ... on ReopenedEvent { createdAt actor { url login } } ' +
' ... on ReferencedEvent { createdAt actor { url login } commit { url oid message } } ' +
' ... on IssueComment { createdAt body url author { url login } } ' +
' ... on AssignedEvent { createdAt actor { url login } assignee { ... on User { login url } } } ' +
' } } } ' +
2023-11-06 21:09:34 +01:00
' } } } } ',
closed:
'query { ' +
'repository(owner: "freesewing", name: "freesewing") { ' +
2023-11-08 20:49:29 +01:00
' issues(states: CLOSED, labels: ["statusResolved"], first: 20) { ' +
2023-11-06 21:09:34 +01:00
' nodes { ' +
' title body url number createdAt closedAt ' +
2023-11-07 11:51:15 +01:00
' author { login url } ' +
' comments(last: 3) { edges { node { body createdAt url author { login url } } } } ' +
' timelineItems(last: 15, itemTypes:[ ISSUE_COMMENT, CLOSED_EVENT,ASSIGNED_EVENT,REOPENED_EVENT, REFERENCED_EVENT]) { edges { node { ' +
' __typename ' +
' ... on ClosedEvent { createdAt actor { url login } } ' +
' ... on ReopenedEvent { createdAt actor { url login } } ' +
' ... on ReferencedEvent { createdAt actor { url login } commit { url oid message } } ' +
' ... on IssueComment { createdAt body url author { url login } } ' +
' ... on AssignedEvent { createdAt actor { url login } assignee { ... on User { login url } } } ' +
' } } } ' +
2023-11-06 21:09:34 +01:00
' } } } } ',
}
/*
* Helper method
* Runs a GraphQL query and returns the result as JSON
*/
const runQuery = async (query) => {
let result
try {
result = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${siteConfig.issueToken}`,
},
body: JSON.stringify({ query }),
})
} catch (err) {
console.log(err)
return false
}
const data = await result.json()
return data
}
/*
* Helper method to filter out GraphQL nodes based on a label set on them
*/
const filterOnLabel = (nodes, label) =>
nodes.filter((node) =>
node.labels.edges.filter((edge) => edge.node.name === label).length > 0 ? true : false
)
/*
* Method that load status issues from GitHub and
* sets the result with the setter method passed to it.
*
* If issues are found, this will create and object
* with reported, confirmed, and resolved as keys and
* the list of issues as value of those keys.
*/
const loadStatusIssues = async (setIssues) => {
const open = await runQuery(query.open)
const closed = await runQuery(query.closed)
2023-11-07 11:51:15 +01:00
const now = Date.now()
2023-11-06 21:09:34 +01:00
setIssues({
reported: filterOnLabel(open.data.repository.issues.nodes, 'statusReported'),
confirmed: filterOnLabel(open.data.repository.issues.nodes, 'statusConfirmed'),
2023-11-07 11:51:15 +01:00
resolved: closed.data.repository.issues.nodes.filter((node) => {
const closed = new Date(node.closedAt).valueOf()
// Only show what was closed in the last 36 hours
return now - closed < 36 * 60 * 60 * 1000
}),
2023-11-06 21:09:34 +01:00
})
}
2023-11-07 11:51:15 +01:00
const AssignedEvent = ({ evt, t }) => (
<div className="my-2 pb-2 rounded shadow p-2">
<div className="text-sm opacity-70 italic flex flex-row items-center gap-1">
<UserIcon className="w-6 h-6 text-primary" />
<span className="pl-2">{t('support:issueAssigned')}</span>
<TimeAgo date={evt.node.createdAt} />
<span> {t('support:to')}</span>
<WebLink href={evt.node.assignee.url} txt={evt.node.assignee.login} />
<span>
(<span>{t('support:by')} </span>
<WebLink href={evt.node.actor.url} txt={evt.node.actor.login} />)
</span>
</div>
</div>
)
const ClosedEvent = ({ evt, t }) => (
<div className="my-2 pb-2 rounded shadow p-2">
<div className="text-sm opacity-70 italic flex flex-row items-center gap-1">
<BoolYesIcon />
<span className="pl-2">{t('support:issueClosed')} </span>
<TimeAgo date={evt.node.createdAt} />
<span> {t('support:by')} </span>
<WebLink href={evt.node.actor.url} txt={evt.node.actor.login} />
</div>
</div>
)
const ReopenedEvent = ({ evt, t }) => (
<div className="my-2 pb-2 rounded shadow p-2">
<div className="text-sm opacity-70 italic flex flex-row items-center gap-1">
<WarningIcon className="h-6 w-6 text-warning" />
<span className="pl-2">{t('support:issueReopened')} </span>
<TimeAgo date={evt.node.createdAt} />
<span> {t('support:by')} </span>
<WebLink href={evt.node.actor.url} txt={evt.node.actor.login} />
</div>
</div>
)
const IssueComment = ({ evt, t }) => (
<div className="my-2 pb-2 rounded shadow p-2">
<div className="text-sm opacity-70 italic flex flex-row items-center gap-1">
<ChatIcon className="h-6 w-6 text-secondary" />
<span>{t('support:commentAdded')}</span>
<WebLink href={evt.node.url} txt={<TimeAgo date={evt.node.createdAt} />} />
<span> {t('support:by')}</span>
<WebLink href={evt.node.author.url} txt={evt.node.author.login} />
</div>
<Mdx md={evt.node.body} />
</div>
)
const ReferencedEvent = ({ evt, t }) => (
<div className="my-2 pb-2 rounded shadow p-2">
<div className="text-sm opacity-70 italic flex flex-row items-center gap-1">
<FlagIcon className="h-6 w-6 text-accent" />
<span>{t('support:issueReferenced')} </span>
<WebLink href={evt.node.url} txt={<TimeAgo date={evt.node.createdAt} />} />
<span> {t('support:by')} </span>
<WebLink href={evt.node.actor.url} txt={evt.node.actor.login} />
<span> {t('support:in')} </span>
<WebLink href={evt.node.commit.url} txt={evt.node.commit.oid.slice(0, 8)} />
</div>
<Mdx md={evt.node.commit.message} />
</div>
)
const events = {
IssueComment,
AssignedEvent,
ReferencedEvent,
ReopenedEvent,
ClosedEvent,
}
const Null = () => null
const Event = (props) => {
if (!props.evt.node) return <p>{props.evt.node.__typeName}</p> //null
const Component = events[props.evt.node.__typename] || Null
return <Component {...props} />
return <pre>{JSON.stringify(props.evt.node, null, 2)}</pre>
}
const Issue = ({ issue, type, t }) => {
2023-11-06 21:09:34 +01:00
const [detail, setDetail] = useState(false)
const btnClasses =
2023-11-07 11:51:15 +01:00
'w-full my-1 rounded hover:bg-opacity-10 hover:bg-secondary text-left text-base-content p-1 px-2 flex flex-row items-center justify-between'
2023-11-06 21:09:34 +01:00
if (!detail)
return (
2023-11-07 11:51:15 +01:00
<button onClick={() => setDetail(true)} className={`${btnClasses} border-l-4`}>
<div>
{issue.title}
<small className="pl-2 opacity-70">[{issue.number}]</small>
{issue.timelineItems.edges.length > 0 ? (
<div className="badge badge-accent ml-2">
{issue.timelineItems.edges.length} {t('support:updates')}
</div>
) : null}
</div>
2023-11-06 21:09:34 +01:00
<DownIcon />
</button>
)
return (
2023-11-07 11:51:15 +01:00
<div className="shadow rounded mb-2 pb-2">
2023-11-06 21:09:34 +01:00
<button
onClick={() => setDetail(false)}
2023-11-07 11:51:15 +01:00
className={`${btnClasses} border-b-2 bg-opacity-10 bg-secondary rounded-b-none`}
2023-11-06 21:09:34 +01:00
>
2023-11-07 11:51:15 +01:00
<h5>{issue.title}</h5>
<DownIcon className="w-8 h-8 text-secondary rotate-180" stroke={3} />
2023-11-06 21:09:34 +01:00
</button>
2023-11-07 11:51:15 +01:00
<div className="px-4">
<div className="text-sm opacity-70 italic">
{t('support:reported')}{' '}
<WebLink href={issue.url} txt={<TimeAgo date={issue.createdAt} />} />
<span> {t('support:by')} </span>
<WebLink href={issue.author.url} txt={issue.author.login} />
</div>
<Mdx md={issue.body} />
{issue.timelineItems.edges.length > 0 ? (
<>
<h5>
{t('support:updates')}
<div className="badge badge-accent badge-sm -translate-y-2">
{issue.timelineItems.edges.length}
</div>
</h5>
{issue.timelineItems.edges.map((evt, i) => (
<Event evt={evt} t={t} key={i} />
))}
</>
) : null}
2023-11-06 21:09:34 +01:00
</div>
</div>
)
}
2023-11-07 11:51:15 +01:00
const Issues = ({ issues, t }) => (
<div className="lg:ml-8">
2023-11-06 21:09:34 +01:00
{issues.map((issue) => (
2023-11-07 11:51:15 +01:00
<Issue key={issue.url} issue={issue} t={t} />
2023-11-06 21:09:34 +01:00
))}
</div>
)
export const Status = () => {
2023-11-07 11:51:15 +01:00
const { t } = useTranslation(ns)
2023-11-06 21:09:34 +01:00
/*
* null: We are (still) loading issues
* false: No issues, everything is ok
* Object: Object with 'reported', 'confirmed' and 'resolved' keys each holding an array of issues
*/
const [issues, setIssues] = useState(null)
useEffect(() => {
if (issues === null) loadStatusIssues(setIssues)
}, [issues])
return (
<>
{issues === null ? (
<p>Loading...</p>
) : issues === false ? (
2023-11-07 11:51:15 +01:00
<>
<span className="opacity-80 font-light text-sm pl-1">{t('support:status')}</span>
<h6 className="flex flex-row gap-2 items-center bg-success p-2 px-4 rounded-lg bg-opacity-30 border border-success">
<BoolYesIcon className="w-6 h-6 text-warning" />
{t('support:allOk')}
</h6>
</>
2023-11-06 21:09:34 +01:00
) : (
<>
2023-11-07 11:51:15 +01:00
<h2>{t('support:status')}</h2>
2023-11-06 21:09:34 +01:00
{issues.reported.length > 0 ? (
<>
2023-11-07 11:51:15 +01:00
<h6 className="flex flex-row gap-2 items-center">
<BoolYesIcon className="w-6 h-6 text-warning" />
{t('support:reportedIssues')}
</h6>
<Issues issues={issues.reported} t={t} />
2023-11-06 21:09:34 +01:00
</>
) : (
2023-11-07 11:51:15 +01:00
<h6 className="flex flex-row gap-2 items-center opacity-50">
<BoolYesIcon className="w-6 h-6 text-success" /> {t('support:noReportedIssues')}
2023-11-06 21:09:34 +01:00
</h6>
)}
{issues.confirmed.length > 0 ? (
<>
2023-11-07 11:51:15 +01:00
<h6 className="flex flex-row gap-2 items-center">
<WarningIcon className="w-6 h-6 text-warning" />
{t('support:confirmedIssues')}
</h6>
<Issues issues={issues.confirmed} t={t} />
2023-11-06 21:09:34 +01:00
</>
) : (
2023-11-07 11:51:15 +01:00
<h6 className="flex flex-row gap-2 items-center opacity-50">
<BoolYesIcon className="w-6 h-6 text-success" /> {t('support:noConfirmedIssues')}
2023-11-06 21:09:34 +01:00
</h6>
)}
{issues.resolved.length > 0 ? (
<>
2023-11-07 11:51:15 +01:00
<h6 className="flex flex-row gap-2 items-center">
<BoolYesIcon className="w-6 h-6 text-success" />
{t('support:recentlyResolvedIssues')}
</h6>
<Issues issues={issues.resolved} t={t} />
2023-11-06 21:09:34 +01:00
</>
) : null}
</>
)}
</>
)
}