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'
|
2023-11-12 12:39:30 +01:00
|
|
|
import { Spinner } from 'shared/components/spinner.mjs'
|
2023-11-07 11:51:15 +01:00
|
|
|
|
|
|
|
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) => {
|
2023-11-20 15:56:53 +01:00
|
|
|
if (!props.evt.node) return null
|
2023-11-07 11:51:15 +01:00
|
|
|
|
|
|
|
const Component = events[props.evt.node.__typename] || Null
|
|
|
|
|
|
|
|
return <Component {...props} />
|
|
|
|
}
|
|
|
|
|
2023-11-12 12:56:11 +01:00
|
|
|
const Issue = ({ issue, 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
|
|
|
|
* 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 ? (
|
2023-11-12 12:39:30 +01:00
|
|
|
<Spinner />
|
|
|
|
) : [...issues.reported, ...issues.confirmed].length < 1 ? (
|
2023-11-07 11:51:15 +01:00
|
|
|
<>
|
|
|
|
<span className="opacity-80 font-light text-sm pl-1">{t('support:status')}</span>
|
2023-11-12 12:39:30 +01:00
|
|
|
<h6 className="flex flex-row gap-2 items-center bg-success p-2 px-4 rounded-lg bg-opacity-30 border border-success mb-4">
|
2023-11-07 11:51:15 +01:00
|
|
|
<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>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
2023-11-12 12:54:13 +01:00
|
|
|
{issues && issues.resolved.length > 0 ? (
|
2023-11-12 12:39:30 +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} />
|
|
|
|
</>
|
|
|
|
) : null}
|
2023-11-06 21:09:34 +01:00
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|