[website] Connect burnup charts to API (#425)
* feat: add queries to get epics links and repos and epic burnup data * feat: add codegen and api fetcher util to create named custom hook query functions * feat: add more queries with fetcher and infinite scroll * feat: add some improvements on existing components * feat: add more functionalities to table issues * feat: add available fitlers and search to table issues * feat: add more missing features to the burnup chart * feat: fix some issues with scroll * fix: color epic overview and fixe repos card icons * feat: add overview page data with filters * fix: changes from review * fix: component removed from pages folder * fix import * fix: several changes to improve performance and handles some edge cases * fix: infinite scroll epics overview page --------- Co-authored-by: marcelines <marcio@significa.co> Co-authored-by: Pavel Prichodko <14926950+prichodko@users.noreply.github.com>
This commit is contained in:
parent
0d5a0fe21e
commit
e52b72f731
|
@ -0,0 +1,12 @@
|
|||
type ApiError = {
|
||||
extensions: { code?: string; [key: string]: string }
|
||||
locations: { column: number; line: number }[]
|
||||
message: string
|
||||
path: string[]
|
||||
}
|
||||
|
||||
declare interface GraphqlApiError {
|
||||
response?: {
|
||||
errors?: ApiError[]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
schema:
|
||||
- https://hasura.infra.status.im/v1/graphql
|
||||
|
||||
documents:
|
||||
- ./src/**/*.tsx
|
||||
- ./src/**/*.ts
|
||||
|
||||
overwrite: true
|
||||
|
||||
hooks:
|
||||
afterAllFileWrite:
|
||||
- prettier --write
|
||||
- eslint --fix
|
||||
|
||||
generates:
|
||||
./src/lib/graphql/generated/schemas.ts:
|
||||
plugins:
|
||||
- typescript
|
||||
|
||||
./src/lib/graphql/generated/operations.ts:
|
||||
preset: import-types
|
||||
presetConfig:
|
||||
typesPath: ./schemas
|
||||
plugins:
|
||||
- typescript-operations
|
||||
|
||||
./src/lib/graphql/generated/hooks.ts:
|
||||
preset: import-types
|
||||
presetConfig:
|
||||
typesPath: ./operations
|
||||
plugins:
|
||||
- typescript-react-query
|
||||
config:
|
||||
fetcher: '../api#createFetcher'
|
||||
exposeQueryKeys: true
|
||||
errorType: GraphqlApiError
|
|
@ -9,7 +9,9 @@
|
|||
"lint": "next lint",
|
||||
"typecheck": "contentlayer build && tsc",
|
||||
"clean": "rimraf .next .tamagui .turbo .vercel/output node_modules",
|
||||
"preview": "next start --port 8151"
|
||||
"preview": "next start --port 8151",
|
||||
"codegen": "graphql-codegen -r dotenv/config --config codegen.yml",
|
||||
"codegen:watch": "yarn run codegen -- --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^2.3.0",
|
||||
|
@ -48,6 +50,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@achingbrain/ssdp": "^4.0.1",
|
||||
"@graphql-codegen/cli": "^4.0.1",
|
||||
"@graphql-codegen/import-types-preset": "^2.2.6",
|
||||
"@graphql-codegen/typescript": "^4.0.1",
|
||||
"@graphql-codegen/typescript-operations": "^4.0.1",
|
||||
"@graphql-codegen/typescript-react-query": "^4.1.0",
|
||||
"@status-im/eslint-config": "*",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@tamagui/next-plugin": "1.36.4",
|
||||
"@types/d3-array": "^3.0.4",
|
||||
|
@ -76,7 +84,6 @@
|
|||
"typescript": "^5.0.3",
|
||||
"unist-util-visit": "^4.1.2",
|
||||
"@types/tryghost__content-api": "^1.3.11",
|
||||
"@status-im/eslint-config": "*",
|
||||
"rehype-parse": "^8.0.4",
|
||||
"rehype-react": "^7.2.0",
|
||||
"rehype-stringify": "^9.0.3",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Stack } from '@tamagui/core'
|
||||
import { ParentSize } from '@visx/responsive'
|
||||
|
||||
import { ChartComponent, Empty, Loading } from './components'
|
||||
import { ChartComponent, Loading } from './components'
|
||||
|
||||
type DayType = {
|
||||
date: string
|
||||
|
@ -26,14 +26,6 @@ const Chart = (props: Props) => {
|
|||
)
|
||||
}
|
||||
|
||||
if (!data.length) {
|
||||
return (
|
||||
<Stack width="100%" height={rest.height}>
|
||||
<Empty />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ParentSize style={{ maxHeight: 326 }}>
|
||||
{({ width }) => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { animated } from '@react-spring/web'
|
||||
import { curveMonotoneX } from '@visx/curve'
|
||||
import { curveBasis } from '@visx/curve'
|
||||
import { LinearGradient } from '@visx/gradient'
|
||||
import { AreaClosed } from '@visx/shape'
|
||||
|
||||
|
@ -71,7 +71,7 @@ const Areas = (props: Props) => {
|
|||
}}
|
||||
yScale={yScale}
|
||||
fill="url(#gradient)"
|
||||
curve={curveMonotoneX}
|
||||
curve={curveBasis}
|
||||
style={{ ...clipPathAnimation, zIndex: 10 }}
|
||||
/>
|
||||
|
||||
|
@ -88,7 +88,7 @@ const Areas = (props: Props) => {
|
|||
}}
|
||||
yScale={yScale}
|
||||
fill="url(#gradient-open)"
|
||||
curve={curveMonotoneX}
|
||||
curve={curveBasis}
|
||||
style={{ ...clipPathAnimation, zIndex: 10 }}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -63,7 +63,10 @@ const ChartComponent = (props: Props): JSX.Element => {
|
|||
|
||||
const yScale = scaleLinear({
|
||||
domain: [0, max(totalIssues) || 0],
|
||||
range: [innerHeight, 0],
|
||||
range: [
|
||||
innerHeight,
|
||||
totalIssues.every(issue => issue === 0) ? innerHeight : 0,
|
||||
], // Adjusted the range to start from innerHeight instead of 0
|
||||
nice: true,
|
||||
})
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ const ChartTooltip = (props: Props) => {
|
|||
<DoneIcon size={16} color="$neutral-40" />
|
||||
<Stack px={4}>
|
||||
<Text size={13} weight="medium">
|
||||
{tooltipData.closedIssues} closes
|
||||
{tooltipData.closedIssues} closed
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
|
|
|
@ -7,7 +7,7 @@ const Empty = () => {
|
|||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
<div className="relative z-10 flex flex-col items-center justify-center">
|
||||
<Image src={'/assets/chart/empty.png'} width={80} height={80} />
|
||||
<Image src="/assets/chart/empty.png" width={80} height={80} />
|
||||
<div className="pb-3" />
|
||||
<Text size={15} weight="semibold">
|
||||
No results found
|
||||
|
@ -17,7 +17,7 @@ const Empty = () => {
|
|||
Try adjusting your search or filter to find what you’re looking for.
|
||||
</Text>
|
||||
</div>
|
||||
<div className="left-50 absolute top-16 w-full opacity-60">
|
||||
<div className="absolute left-[50%] top-16 w-full translate-x-[-50%] opacity-60">
|
||||
<LineA />
|
||||
</div>
|
||||
<div className="absolute left-0 top-28 w-full opacity-60">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { animated } from '@react-spring/web'
|
||||
import { curveMonotoneX } from '@visx/curve'
|
||||
import { curveBasis } from '@visx/curve'
|
||||
import { LinePath } from '@visx/shape'
|
||||
|
||||
import { colors } from './chart-component'
|
||||
|
@ -47,7 +47,7 @@ const Lines = (props: Props) => {
|
|||
}}
|
||||
stroke={colors.total}
|
||||
strokeWidth={2}
|
||||
curve={curveMonotoneX}
|
||||
curve={curveBasis}
|
||||
style={drawingLineStyle}
|
||||
/>
|
||||
|
||||
|
@ -64,7 +64,7 @@ const Lines = (props: Props) => {
|
|||
}}
|
||||
stroke={colors.closed}
|
||||
strokeWidth={2}
|
||||
curve={curveMonotoneX}
|
||||
curve={curveBasis}
|
||||
style={drawingLineStyle}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -98,9 +98,10 @@ const useChartTooltip = (props: Props) => {
|
|||
const totalIssues = getTotalIssues(d)
|
||||
const openIssues = totalIssues - closedIssues
|
||||
|
||||
const completedIssuesPercentage = getPercentage(closedIssues, totalIssues)
|
||||
const completedIssuesPercentage =
|
||||
getPercentage(closedIssues, totalIssues) || 0
|
||||
|
||||
const openIssuesPercentage = getPercentage(openIssues, totalIssues)
|
||||
const openIssuesPercentage = getPercentage(openIssues, totalIssues) || 0
|
||||
|
||||
showTooltip({
|
||||
tooltipData: {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Calendar } from '@status-im/components/src/calendar/calendar'
|
||||
import { Popover } from '@status-im/components/src/popover'
|
||||
import { EditIcon } from '@status-im/icons'
|
||||
import { addDays } from 'date-fns'
|
||||
|
||||
import { formatDate } from '../chart/utils/format-time'
|
||||
|
||||
|
@ -35,6 +36,7 @@ const DatePicker = (props: Props) => {
|
|||
selected={selected}
|
||||
onSelect={onSelect}
|
||||
fixedWeeks
|
||||
disabled={{ from: addDays(new Date(), 1) }}
|
||||
/>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
|
|
|
@ -1,112 +1,101 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { Tag, Text } from '@status-im/components'
|
||||
import { OpenIcon } from '@status-im/icons'
|
||||
|
||||
import { Chart } from './chart/chart'
|
||||
import { DatePicker } from './datepicker/datepicker'
|
||||
|
||||
const DATA = [
|
||||
{
|
||||
date: '2022-01-25',
|
||||
open_issues: 100,
|
||||
closed_issues: 2,
|
||||
},
|
||||
{
|
||||
date: '2022-01-26',
|
||||
open_issues: 100,
|
||||
closed_issues: 10,
|
||||
},
|
||||
{
|
||||
date: '2022-01-27',
|
||||
open_issues: 100,
|
||||
closed_issues: 20,
|
||||
},
|
||||
{
|
||||
date: '2022-01-28',
|
||||
open_issues: 90,
|
||||
closed_issues: 30,
|
||||
},
|
||||
{
|
||||
date: '2022-01-29',
|
||||
open_issues: 80,
|
||||
closed_issues: 40,
|
||||
},
|
||||
{
|
||||
date: '2022-01-30',
|
||||
open_issues: 40,
|
||||
closed_issues: 80,
|
||||
},
|
||||
{
|
||||
date: '2022-01-31',
|
||||
open_issues: 30,
|
||||
closed_issues: 90,
|
||||
},
|
||||
{
|
||||
date: '2022-02-01',
|
||||
open_issues: 25,
|
||||
closed_issues: 95,
|
||||
},
|
||||
{
|
||||
date: '2022-02-02',
|
||||
open_issues: 20,
|
||||
closed_issues: 98,
|
||||
},
|
||||
{
|
||||
date: '2022-02-03',
|
||||
open_issues: 10,
|
||||
closed_issues: 130,
|
||||
},
|
||||
]
|
||||
import type { GetBurnupQuery } from '@/lib/graphql/generated/operations'
|
||||
import type { DateRange } from '@status-im/components/src/calendar/calendar'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
description: string
|
||||
description?: string
|
||||
color?: `#${string}`
|
||||
fullscreen?: boolean
|
||||
isLoading?: boolean
|
||||
burnup?: GetBurnupQuery['gh_burnup']
|
||||
selectedDates?: DateRange
|
||||
setSelectedDates: (date?: DateRange) => void
|
||||
showPicker?: boolean
|
||||
}
|
||||
|
||||
export const EpicOverview = (props: Props) => {
|
||||
const { title, description, fullscreen } = props
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
color,
|
||||
fullscreen,
|
||||
isLoading,
|
||||
burnup,
|
||||
selectedDates,
|
||||
setSelectedDates,
|
||||
showPicker = true,
|
||||
} = props
|
||||
|
||||
// Simulating loading state
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
}, 2000)
|
||||
const filteredData = burnup?.reduce(
|
||||
(
|
||||
accumulator: {
|
||||
date: string
|
||||
open_issues: number
|
||||
closed_issues: number
|
||||
}[],
|
||||
current: GetBurnupQuery['gh_burnup'][0]
|
||||
) => {
|
||||
const existingItem = accumulator.find(
|
||||
item => item.date === current.date_field
|
||||
)
|
||||
if (!existingItem) {
|
||||
accumulator.push({
|
||||
date: current?.date_field,
|
||||
open_issues:
|
||||
current?.total_opened_issues - current?.total_closed_issues,
|
||||
closed_issues: current?.total_closed_issues,
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [])
|
||||
return accumulator
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div className="flex items-center gap-1">
|
||||
<Text size={fullscreen ? 27 : 19} weight="semibold">
|
||||
{title}
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<Text size={fullscreen ? 27 : 19} weight="semibold">
|
||||
{title}
|
||||
</Text>
|
||||
<OpenIcon size={20} />
|
||||
</div>
|
||||
{showPicker && (
|
||||
<DatePicker selected={selectedDates} onSelect={setSelectedDates} />
|
||||
)}
|
||||
</div>
|
||||
{Boolean(description) && (
|
||||
<Text size={fullscreen ? 19 : 15} color="$neutral-50">
|
||||
{description}
|
||||
</Text>
|
||||
<OpenIcon size={20} />
|
||||
</div>
|
||||
<Text size={fullscreen ? 19 : 15} color="$neutral-50">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<div className="flex py-3">
|
||||
<Tag size={24} label="E:CommunitiesProtocol" color="$blue-50" />
|
||||
<Tag size={24} label={title} color={color} />
|
||||
</div>
|
||||
|
||||
<Chart data={DATA} height={300} isLoading={isLoading} />
|
||||
<Chart data={filteredData || []} height={300} isLoading={isLoading} />
|
||||
|
||||
<div className="flex justify-between pt-3">
|
||||
{/* TODO - Add theses when we have milestones and/or labels */}
|
||||
{/* <div className="flex justify-between pt-3">
|
||||
<div className="flex gap-1">
|
||||
<Tag size={24} label="Communities" color="#FF7D46" icon="🧙♂️" />
|
||||
<Tag size={24} label="Wallet" color="#7140FD" icon="🎎" />
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex gap-1">
|
||||
<Tag size={24} label="M:0.11.0" color="$danger-50" />
|
||||
<Tag size={24} label="M:0.12.0" color="$success-50" />
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
export { Breadcrumbs } from './breadcrumbs'
|
||||
export { EpicOverview } from './epic-overview'
|
||||
export { SidebarMenu } from './sidebar-menu'
|
||||
export { TableIssues } from './table-issues/table-issues'
|
||||
|
|
|
@ -25,11 +25,10 @@ const FloatingMenu = (): JSX.Element => {
|
|||
useLockScroll(open)
|
||||
|
||||
useScroll({
|
||||
onChange: ({ value: { scrollYProgress } }) => {
|
||||
onChange: ({ value: { scrollY } }) => {
|
||||
const isMenuOpen = openRef.current
|
||||
const isScrollingUp = scrollYProgress < scrollYRef.current
|
||||
const detectionPoint = scrollYProgress > 0.005
|
||||
|
||||
const isScrollingUp = scrollY < scrollYRef.current
|
||||
const detectionPoint = scrollY > 0.005
|
||||
if (detectionPoint && isScrollingUp) {
|
||||
if (!visibleRef.current) {
|
||||
setVisible(true)
|
||||
|
@ -39,7 +38,7 @@ const FloatingMenu = (): JSX.Element => {
|
|||
setVisible(false)
|
||||
}
|
||||
}
|
||||
scrollYRef.current = scrollYProgress
|
||||
scrollYRef.current = scrollY
|
||||
},
|
||||
default: {
|
||||
immediate: true,
|
||||
|
@ -58,7 +57,7 @@ const FloatingMenu = (): JSX.Element => {
|
|||
'rounded-2xl border border-neutral-80/5 bg-blur-neutral-80/80 backdrop-blur-md',
|
||||
' w-[calc(100%-24px)]',
|
||||
' opacity-0 transition-opacity data-[visible=true]:opacity-100',
|
||||
'z-10',
|
||||
'z-20',
|
||||
])}
|
||||
>
|
||||
<FloatingMobile open={open} setOpen={setOpen} />
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { Shadow, Skeleton } from '@status-im/components'
|
||||
|
||||
const LoadingSkeleton = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-5 p-10">
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<Shadow key={index}>
|
||||
<div className="flex h-[124px] flex-col justify-between rounded-2xl border border-neutral-10 px-4 py-3 ">
|
||||
<Skeleton height={12} width={120} borderRadius="$6" />
|
||||
<Skeleton height={12} width={220} borderRadius="$6" />
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={40}
|
||||
borderRadius="$6"
|
||||
variant="secondary"
|
||||
/>
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={40}
|
||||
borderRadius="$6"
|
||||
variant="secondary"
|
||||
/>
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={40}
|
||||
borderRadius="$6"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Shadow>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { LoadingSkeleton }
|
|
@ -5,8 +5,11 @@ import { useRouter } from 'next/router'
|
|||
|
||||
import { NavLink } from './nav-link'
|
||||
import { NavNestedLinks } from './nav-nested-links'
|
||||
import { SkeletonPlaceholder } from './skeleton-placeholder'
|
||||
import { decodeUriComponent } from './utils'
|
||||
|
||||
type Props = {
|
||||
isLoading?: boolean
|
||||
items: {
|
||||
label: string
|
||||
href?: string
|
||||
|
@ -22,7 +25,7 @@ type Props = {
|
|||
}
|
||||
|
||||
const SidebarMenu = (props: Props) => {
|
||||
const { items } = props
|
||||
const { items, isLoading } = props
|
||||
|
||||
const [label, setLabel] = useState<string>('')
|
||||
|
||||
|
@ -30,7 +33,8 @@ const SidebarMenu = (props: Props) => {
|
|||
|
||||
const defaultLabel = items?.find(
|
||||
item =>
|
||||
item.href === asPath || item.links?.find(link => link.href === asPath)
|
||||
item.href === decodeUriComponent(asPath) ||
|
||||
item.links?.find(link => link.href === decodeUriComponent(asPath))
|
||||
)?.label
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -40,40 +44,40 @@ const SidebarMenu = (props: Props) => {
|
|||
return (
|
||||
<div className="border-r border-neutral-10 p-5">
|
||||
<aside className=" sticky top-5 min-w-[320px]">
|
||||
<Accordion.Root
|
||||
type="single"
|
||||
collapsible
|
||||
value={label}
|
||||
onValueChange={value => setLabel(value)}
|
||||
className="accordion-root flex flex-col gap-3"
|
||||
>
|
||||
{items?.map((item, index) => {
|
||||
if (item.links && item.links.length > 0) {
|
||||
return (
|
||||
<NavNestedLinks
|
||||
key={index}
|
||||
label={item.label}
|
||||
links={item.links}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{isLoading ? (
|
||||
<SkeletonPlaceholder />
|
||||
) : (
|
||||
<Accordion.Root
|
||||
type="single"
|
||||
collapsible
|
||||
value={label}
|
||||
onValueChange={value => setLabel(value)}
|
||||
className="accordion-root flex flex-col gap-3"
|
||||
>
|
||||
{items?.map((item, index) => {
|
||||
if (item.links && item.links.length > 0) {
|
||||
return (
|
||||
<NavNestedLinks
|
||||
key={index}
|
||||
label={item.label}
|
||||
links={item.links}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion.Item
|
||||
key={item.label}
|
||||
value={item.label}
|
||||
className="accordion-item"
|
||||
>
|
||||
<Accordion.Trigger
|
||||
className="accordion-trigger"
|
||||
onClick={() => setLabel(item.label)}
|
||||
>
|
||||
<NavLink href={item.href || ''}>{item.label}</NavLink>
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Item>
|
||||
)
|
||||
})}
|
||||
</Accordion.Root>
|
||||
return (
|
||||
<Accordion.Item key={item.label} value={item.label}>
|
||||
<Accordion.Trigger
|
||||
className="accordion-trigger"
|
||||
onClick={() => setLabel(item.label)}
|
||||
>
|
||||
<NavLink href={item.href || ''}>{item.label}</NavLink>
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Item>
|
||||
)
|
||||
})}
|
||||
</Accordion.Root>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@ import { ChevronRightIcon } from '@status-im/icons'
|
|||
import { useRouter } from 'next/router'
|
||||
|
||||
import { Link } from '../link'
|
||||
import { decodeUriComponent } from './utils'
|
||||
|
||||
import type { Url } from 'next/dist/shared/lib/router/router'
|
||||
|
||||
|
@ -26,7 +27,7 @@ const NavNestedLinks = (props: NavLinkProps) => {
|
|||
const { asPath } = useRouter()
|
||||
|
||||
return (
|
||||
<Accordion.Item value={label} className="accordion-item">
|
||||
<Accordion.Item value={label}>
|
||||
<div>
|
||||
<Accordion.Trigger className="accordion-trigger">
|
||||
<div className="accordion-chevron inline-flex h-5 w-5">
|
||||
|
@ -44,7 +45,7 @@ const NavNestedLinks = (props: NavLinkProps) => {
|
|||
}}
|
||||
>
|
||||
{links.map((link, index) => {
|
||||
const active = asPath === link.href
|
||||
const active = decodeUriComponent(asPath) === link.href
|
||||
|
||||
const paddingClassName = index === 0 ? 'pt-5' : 'pt-2'
|
||||
const paddingLastChild = index === links.length - 1 ? 'pb-5' : ''
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
import { Skeleton } from '@status-im/components'
|
||||
import { Stack } from '@tamagui/core'
|
||||
|
||||
const SkeletonPlaceholder = () => {
|
||||
return (
|
||||
<Stack height="100%">
|
||||
<Stack
|
||||
paddingBottom={16}
|
||||
paddingTop={16}
|
||||
backgroundColor="$background"
|
||||
zIndex={10}
|
||||
>
|
||||
<Stack paddingBottom={16}>
|
||||
<Stack mb={27}>
|
||||
<Skeleton height={12} width={120} borderRadius="$6" mb={19} />
|
||||
<Stack flexDirection="row" alignItems="center" mb={16}>
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={200}
|
||||
borderRadius="$6"
|
||||
variant="secondary"
|
||||
ml={12}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack flexDirection="row" alignItems="center" mb={16}>
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={100}
|
||||
borderRadius="$6"
|
||||
variant="secondary"
|
||||
ml={12}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack flexDirection="row" alignItems="center" mb={16}>
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={130}
|
||||
borderRadius="$6"
|
||||
variant="secondary"
|
||||
ml={12}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack flexDirection="row" alignItems="center">
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={90}
|
||||
borderRadius="$6"
|
||||
variant="secondary"
|
||||
ml={12}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Skeleton height={12} width={50} borderRadius={5} mb={19} />
|
||||
<Stack flexDirection="row" alignItems="center" mb={16}>
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={120}
|
||||
borderRadius="$6"
|
||||
variant="secondary"
|
||||
ml={12}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack flexDirection="row" alignItems="center" mb={16}>
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={100}
|
||||
borderRadius="$6"
|
||||
variant="secondary"
|
||||
ml={12}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack flexDirection="row" alignItems="center" mb={16}>
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={200}
|
||||
borderRadius="$6"
|
||||
variant="secondary"
|
||||
ml={12}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export { SkeletonPlaceholder }
|
|
@ -0,0 +1,3 @@
|
|||
export function decodeUriComponent(str: string): string {
|
||||
return decodeURIComponent(str.replace(/\+/g, '%20'))
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
import { Avatar, Skeleton, Tag, Text } from '@status-im/components'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import Link from 'next/link'
|
||||
|
||||
import type {
|
||||
GetIssuesByEpicQuery,
|
||||
GetOrphansQuery,
|
||||
} from '@/lib/graphql/generated/operations'
|
||||
|
||||
type Props = {
|
||||
isLoading?: boolean
|
||||
data?: GetOrphansQuery['gh_orphans'] | GetIssuesByEpicQuery['gh_epic_issues']
|
||||
count?: {
|
||||
total: number
|
||||
closed: number
|
||||
open: number
|
||||
}
|
||||
}
|
||||
|
||||
// function isOrphans(
|
||||
// data: GetOrphansQuery['gh_orphans'] | GetIssuesByEpicQuery['gh_epic_issues']
|
||||
// ): data is GetOrphansQuery['gh_orphans'] {
|
||||
// return 'labels' in data[0]
|
||||
// }
|
||||
|
||||
function isIssues(
|
||||
data: GetOrphansQuery['gh_orphans'] | GetIssuesByEpicQuery['gh_epic_issues']
|
||||
): data is GetIssuesByEpicQuery['gh_epic_issues'] {
|
||||
return 'assignee' in data[0]
|
||||
}
|
||||
|
||||
export const TableIssues = (props: Props) => {
|
||||
const { data, count, isLoading } = props
|
||||
|
||||
const issues = data || []
|
||||
|
||||
return (
|
||||
<div className="mb-8 overflow-hidden rounded-2xl border border-neutral-10 transition-opacity">
|
||||
<div className="border-b border-neutral-10 bg-neutral-5 p-3">
|
||||
<Text size={15} weight="medium">
|
||||
{count?.open || 0} Open
|
||||
</Text>
|
||||
|
||||
<Text size={15} weight="medium">
|
||||
{count?.closed || 0} Closed
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-neutral-10">
|
||||
{issues.length !== 0 &&
|
||||
isIssues(issues) &&
|
||||
issues.map(issue => (
|
||||
<Link
|
||||
key={issue.issue_number}
|
||||
href={`https://github.com/status-im/${issue.repository}/issues/${issue.issue_number}`}
|
||||
className="flex items-center justify-between px-4 py-3 transition-colors duration-200 hover:bg-neutral-5"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<Text size={15} weight="medium">
|
||||
{issue.title}
|
||||
</Text>
|
||||
<Text size={13} color="$neutral-50">
|
||||
#{issue.issue_number} •{' '}
|
||||
{formatDistanceToNow(new Date(issue.created_at), {
|
||||
addSuffix: true,
|
||||
})}{' '}
|
||||
by {issue.author}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-1">
|
||||
<Tag
|
||||
size={24}
|
||||
label={issue.epic_name || ''}
|
||||
color={`#${issue.epic_color}` || '$primary'}
|
||||
/>
|
||||
</div>
|
||||
<Avatar type="user" size={24} name={issue.author || ''} />
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<div className="flex h-10 grow flex-col justify-between">
|
||||
<Skeleton width={340} height={18} />
|
||||
<Skeleton width={200} height={12} />
|
||||
</div>
|
||||
<div className="flex flex-auto flex-row justify-end gap-2">
|
||||
<Skeleton width={85} height={24} />
|
||||
<Skeleton width={24} height={24} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { Image, Text } from '@status-im/components'
|
||||
|
||||
const Empty = () => {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
<div className="relative z-10 flex flex-col items-center justify-center">
|
||||
<Image src="/assets/chart/empty.png" width={80} height={80} />
|
||||
<div className="pb-3" />
|
||||
<Text size={15} weight="semibold">
|
||||
No results found
|
||||
</Text>
|
||||
<div className="pb-1" />
|
||||
<Text size={13} color="$neutral-50">
|
||||
Try adjusting your search or filter to find what you’re looking for.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute left-0 top-0 h-24 w-full"
|
||||
style={{
|
||||
background: `linear-gradient(180deg, white, transparent)`,
|
||||
}}
|
||||
/>
|
||||
<div className="absolute flex h-full w-full justify-between opacity-80">
|
||||
{Array.from({ length: 12 }).map((_, i) => (
|
||||
<div
|
||||
className="h-full w-1"
|
||||
style={{
|
||||
borderLeft: '1px dashed #F0F2F5',
|
||||
}}
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { Empty }
|
|
@ -1,4 +1,4 @@
|
|||
import { cloneElement, useState } from 'react'
|
||||
import { cloneElement, useMemo, useState } from 'react'
|
||||
|
||||
import { Avatar, Button, Input, Text } from '@status-im/components'
|
||||
import { DropdownMenu } from '@status-im/components/src/dropdown-menu'
|
||||
|
@ -12,7 +12,7 @@ import { ColorCircle } from './components/color-circle'
|
|||
import type { ColorTokens } from '@tamagui/core'
|
||||
|
||||
type Data = {
|
||||
id: number
|
||||
id: string
|
||||
name: string
|
||||
avatar?: string | React.ReactElement
|
||||
color?: ColorTokens | `#${string}`
|
||||
|
@ -22,6 +22,8 @@ type Props = {
|
|||
data: Data[]
|
||||
label: string
|
||||
placeholder?: string
|
||||
selectedValues: string[]
|
||||
onSelectedValuesChange: (values: string[]) => void
|
||||
}
|
||||
|
||||
const isAvatar = (value: unknown): value is string => {
|
||||
|
@ -45,23 +47,26 @@ const RenderIcon = (props: Data) => {
|
|||
}
|
||||
|
||||
const DropdownFilter = (props: Props) => {
|
||||
const { data, label, placeholder } = props
|
||||
const { data, label, placeholder, selectedValues, onSelectedValuesChange } =
|
||||
props
|
||||
|
||||
const [filterText, setFilterText] = useState('')
|
||||
|
||||
// TODO - this will be improved by having a debounced search and use memoization
|
||||
const filteredData = data.filter(label =>
|
||||
label.name.toLowerCase().includes(filterText.toLowerCase())
|
||||
const filteredData = useMemo(
|
||||
() =>
|
||||
data.filter(label =>
|
||||
label.name.toLowerCase().includes(filterText.toLowerCase())
|
||||
),
|
||||
[data, filterText]
|
||||
)
|
||||
|
||||
const [selectedValues, setSelectedValues] = useState<number[]>([])
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const currentBreakpoint = useCurrentBreakpoint()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DropdownMenu onOpenChange={() => setIsOpen(!isOpen)}>
|
||||
<DropdownMenu onOpenChange={() => setIsOpen(!isOpen)} modal={false}>
|
||||
<Button
|
||||
size={32}
|
||||
variant="outline"
|
||||
|
@ -77,6 +82,7 @@ const DropdownFilter = (props: Props) => {
|
|||
>
|
||||
{label}
|
||||
</Button>
|
||||
|
||||
<DropdownMenu.Content
|
||||
sideOffset={10}
|
||||
align={currentBreakpoint === '2xl' ? 'end' : 'start'}
|
||||
|
@ -99,11 +105,11 @@ const DropdownFilter = (props: Props) => {
|
|||
checked={selectedValues.includes(filtered.id)}
|
||||
onSelect={() => {
|
||||
if (selectedValues.includes(filtered.id)) {
|
||||
setSelectedValues(
|
||||
onSelectedValuesChange(
|
||||
selectedValues.filter(id => id !== filtered.id)
|
||||
)
|
||||
} else {
|
||||
setSelectedValues([...selectedValues, filtered.id])
|
||||
onSelectedValuesChange([...selectedValues, filtered.id])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
import { IconButton, Text } from '@status-im/components'
|
||||
import { DropdownMenu } from '@status-im/components/src/dropdown-menu'
|
||||
import { SortIcon } from '@status-im/icons'
|
||||
import Image from 'next/image'
|
||||
|
||||
import type { Order_By } from '@/lib/graphql/generated/schemas'
|
||||
|
||||
type Data = {
|
||||
id: number
|
||||
id: Order_By
|
||||
name: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
data: Data[]
|
||||
orderByValue: Order_By
|
||||
onOrderByValueChange: (value: Order_By) => void
|
||||
}
|
||||
|
||||
const DropdownSort = (props: Props) => {
|
||||
const { data } = props
|
||||
|
||||
const [selectedValue, setSelectedValue] = useState<number>()
|
||||
const { data, orderByValue, onOrderByValueChange } = props
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -37,31 +36,12 @@ const DropdownSort = (props: Props) => {
|
|||
key={option.id}
|
||||
label={option.name}
|
||||
onSelect={() => {
|
||||
setSelectedValue(option.id)
|
||||
onOrderByValueChange(option.id)
|
||||
}}
|
||||
selected={selectedValue === option.id}
|
||||
selected={orderByValue === option.id}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{data.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center p-2 py-1">
|
||||
<Image
|
||||
className="pb-3 invert"
|
||||
alt="No results"
|
||||
src={'/assets/filters/empty.png'}
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
<div className="pb-[2px]">
|
||||
<Text size={15} weight="semibold">
|
||||
No options found
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<Text size={13}>We didn't find any results</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
import { DoneIcon, OpenIcon } from '@status-im/icons'
|
||||
|
||||
const Tabs = (): JSX.Element => {
|
||||
const [activeTab, setActiveTab] = useState<'open' | 'closed'>('open')
|
||||
import { ActiveMembersIcon, OpenIcon } from '@status-im/icons'
|
||||
|
||||
type Props = {
|
||||
count: {
|
||||
open?: number
|
||||
closed?: number
|
||||
}
|
||||
activeTab: 'open' | 'closed'
|
||||
onTabChange: (tab: 'open' | 'closed') => void
|
||||
}
|
||||
const Tabs = (props: Props): JSX.Element => {
|
||||
const { count, activeTab, onTabChange } = props
|
||||
const isOpen = activeTab === 'open'
|
||||
|
||||
return (
|
||||
|
@ -14,10 +19,12 @@ const Tabs = (): JSX.Element => {
|
|||
className={`flex cursor-pointer flex-row items-center transition-colors ${
|
||||
isOpen ? 'text-neutral-100' : 'text-neutral-50'
|
||||
}`}
|
||||
onClick={() => setActiveTab('open')}
|
||||
onClick={() => onTabChange('open')}
|
||||
>
|
||||
<OpenIcon size={20} color={isOpen ? '$neutral-100' : '$neutral-50'} />
|
||||
<span className="pl-1 text-[15px]">784 Open</span>
|
||||
<span className="pl-1 text-[15px]">
|
||||
{typeof count.open === 'number' ? count.open : '-'} Open
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center pr-3">
|
||||
|
@ -25,13 +32,15 @@ const Tabs = (): JSX.Element => {
|
|||
className={`flex cursor-pointer flex-row items-center transition-colors ${
|
||||
!isOpen ? 'text-neutral-100' : 'text-neutral-50'
|
||||
}`}
|
||||
onClick={() => setActiveTab('closed')}
|
||||
onClick={() => onTabChange('closed')}
|
||||
>
|
||||
<DoneIcon
|
||||
<ActiveMembersIcon
|
||||
size={20}
|
||||
color={!isOpen ? '$neutral-100' : '$neutral-50'}
|
||||
/>
|
||||
<span className="pl-1 text-[15px]">1012 Closed</span>
|
||||
<span className="pl-1 text-[15px]">
|
||||
{typeof count.closed === 'number' ? count.closed : '-'} Closed
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,273 +1,155 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
import { Avatar, Button, Input, Tag, Text } from '@status-im/components'
|
||||
import { ProfileIcon, SearchIcon } from '@status-im/icons'
|
||||
import { Avatar, Input, Skeleton, Tag, Text } from '@status-im/components'
|
||||
import { ActiveMembersIcon, OpenIcon, SearchIcon } from '@status-im/icons'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useCurrentBreakpoint } from '@/hooks/use-current-breakpoint'
|
||||
import { Order_By } from '@/lib/graphql/generated/schemas'
|
||||
|
||||
import { Empty } from './empty'
|
||||
import { DropdownFilter, DropdownSort, Tabs } from './filters'
|
||||
|
||||
import type { DropdownFilterProps } from './filters/dropdown-filter'
|
||||
import type { DropdownSortProps } from './filters/dropdown-sort'
|
||||
|
||||
const issues = [
|
||||
{
|
||||
id: 5154,
|
||||
title: 'Add support for encrypted communities',
|
||||
status: 'open',
|
||||
},
|
||||
{
|
||||
id: 5155,
|
||||
title: 'Add support for encrypted communities',
|
||||
status: 'open',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Add support for encrypted communities',
|
||||
status: 'open',
|
||||
},
|
||||
{
|
||||
id: 4324,
|
||||
title: 'Add support for encrypted communities',
|
||||
status: 'open',
|
||||
},
|
||||
{
|
||||
id: 134,
|
||||
title: 'Add support for encrypted communities',
|
||||
status: 'closed',
|
||||
},
|
||||
{
|
||||
id: 999,
|
||||
title: 'Add support for encrypted communities',
|
||||
status: 'closed',
|
||||
},
|
||||
{
|
||||
id: 873,
|
||||
title: 'Add support for encrypted communities',
|
||||
status: 'open',
|
||||
},
|
||||
{
|
||||
id: 123,
|
||||
title: 'Add support for encrypted communities',
|
||||
status: 'open',
|
||||
},
|
||||
]
|
||||
|
||||
const authors: DropdownFilterProps['data'] = [
|
||||
{
|
||||
id: 4,
|
||||
name: 'marcelines',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/29401404?v=4',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'prichodko',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/14926950?v=4',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'felicio',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/13265126?v=4',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'jkbktl',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/520927?v=4',
|
||||
},
|
||||
]
|
||||
|
||||
const epics: DropdownFilterProps['data'] = [
|
||||
{
|
||||
id: 4,
|
||||
name: 'E:ActivityCenter',
|
||||
color: '$orange-60',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'E:Keycard',
|
||||
color: '$purple-60',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'E:Wallet',
|
||||
color: '$pink-60',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'E:Chat',
|
||||
color: '$beige-60',
|
||||
},
|
||||
]
|
||||
|
||||
const labels: DropdownFilterProps['data'] = [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Mobile',
|
||||
color: '$blue-60',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Frontend',
|
||||
color: '$brown-50',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Backend',
|
||||
color: '$red-60',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Desktop',
|
||||
color: '$green-60',
|
||||
},
|
||||
]
|
||||
|
||||
const assignees: DropdownFilterProps['data'] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Unassigned',
|
||||
avatar: <ProfileIcon size={16} />,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'marcelines',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/29401404?v=4',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'prichodko',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/14926950?v=4',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'felicio',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/13265126?v=4',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'jkbktl',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/520927?v=4',
|
||||
},
|
||||
]
|
||||
|
||||
const repositories: DropdownFilterProps['data'] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'status-mobile',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'status-desktop',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'status-web',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'status-go',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'nwaku',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'go-waku',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'js-waku',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'nimbus-eth2',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'help.status.im',
|
||||
},
|
||||
]
|
||||
import type {
|
||||
GetFiltersForOrphansQuery,
|
||||
GetFiltersWithEpicQuery,
|
||||
GetIssuesByEpicQuery,
|
||||
GetOrphansQuery,
|
||||
} from '@/lib/graphql/generated/operations'
|
||||
|
||||
const sortOptions: DropdownSortProps['data'] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
id: Order_By.Asc,
|
||||
name: 'Ascending',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Alphabetical',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Creation date',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Updated',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Completion',
|
||||
id: Order_By.Desc,
|
||||
name: 'Descending',
|
||||
},
|
||||
]
|
||||
|
||||
const TableIssues = () => {
|
||||
const [issuesSearchText, setIssuesSearchText] = useState('')
|
||||
type Props = {
|
||||
isLoading?: boolean
|
||||
data?: GetOrphansQuery['gh_orphans'] | GetIssuesByEpicQuery['gh_epic_issues']
|
||||
filters?: GetFiltersWithEpicQuery | GetFiltersForOrphansQuery
|
||||
count?: {
|
||||
total?: number
|
||||
closed?: number
|
||||
open?: number
|
||||
}
|
||||
activeTab: 'open' | 'closed'
|
||||
handleTabChange: (tab: 'open' | 'closed') => void
|
||||
selectedAuthors: string[]
|
||||
handleSelectedAuthors: (values: string[]) => void
|
||||
selectedAssignees: string[]
|
||||
handleSelectedAssignees: (values: string[]) => void
|
||||
selectedRepos: string[]
|
||||
handleSelectedRepos: (values: string[]) => void
|
||||
orderByValue: Order_By
|
||||
handleOrderByValue: (value: Order_By) => void
|
||||
handleSearchFilter: (value: string) => void
|
||||
searchFilterValue: string
|
||||
}
|
||||
|
||||
const TableIssues = (props: Props) => {
|
||||
const currentBreakpoint = useCurrentBreakpoint()
|
||||
|
||||
const {
|
||||
data = [],
|
||||
count,
|
||||
isLoading,
|
||||
filters,
|
||||
handleTabChange,
|
||||
activeTab,
|
||||
selectedAuthors,
|
||||
handleSelectedAuthors,
|
||||
selectedAssignees,
|
||||
handleSelectedAssignees,
|
||||
selectedRepos,
|
||||
handleSelectedRepos,
|
||||
orderByValue,
|
||||
handleOrderByValue,
|
||||
handleSearchFilter,
|
||||
searchFilterValue,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-2xl border border-neutral-10">
|
||||
<div className="flex border-b border-neutral-10 bg-neutral-5 p-3">
|
||||
<div className="overflow-hidden rounded-2xl border border-neutral-20 shadow-1">
|
||||
<div className="flex border-b border-neutral-20 bg-neutral-5 p-3">
|
||||
<div className="flex w-full flex-col 2xl:flex-row 2xl:justify-between">
|
||||
<Tabs />
|
||||
<Tabs
|
||||
onTabChange={handleTabChange}
|
||||
activeTab={activeTab}
|
||||
count={{
|
||||
closed: count?.closed,
|
||||
open: count?.open,
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center 2xl:justify-end">
|
||||
<div className="flex w-full justify-between pt-4 2xl:justify-end 2xl:pt-0">
|
||||
<div className="flex gap-2">
|
||||
<div className="transition-all">
|
||||
<Input
|
||||
placeholder="Find Author"
|
||||
placeholder="Find issue..."
|
||||
icon={<SearchIcon size={20} />}
|
||||
size={32}
|
||||
value={issuesSearchText}
|
||||
onChangeText={setIssuesSearchText}
|
||||
value={searchFilterValue}
|
||||
onChangeText={handleSearchFilter}
|
||||
variant="retractable"
|
||||
direction={currentBreakpoint === '2xl' ? 'rtl' : 'ltr'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DropdownFilter
|
||||
data={authors}
|
||||
onSelectedValuesChange={handleSelectedAuthors}
|
||||
selectedValues={selectedAuthors}
|
||||
data={
|
||||
filters?.authors.map(author => {
|
||||
return {
|
||||
id: author.author || '',
|
||||
name: author.author || '',
|
||||
}
|
||||
}) || []
|
||||
}
|
||||
label="Author"
|
||||
placeholder="Find author"
|
||||
placeholder="Find author "
|
||||
/>
|
||||
<DropdownFilter
|
||||
data={epics}
|
||||
label="Epics"
|
||||
placeholder="Find epic"
|
||||
/>
|
||||
<DropdownFilter
|
||||
data={labels}
|
||||
label="Labels"
|
||||
placeholder="Find label"
|
||||
/>
|
||||
<DropdownFilter
|
||||
data={assignees}
|
||||
onSelectedValuesChange={handleSelectedAssignees}
|
||||
selectedValues={selectedAssignees}
|
||||
data={
|
||||
filters?.assignees.map(assignee => {
|
||||
return {
|
||||
id: assignee.assignee || '',
|
||||
name: assignee.assignee || '',
|
||||
}
|
||||
}) || []
|
||||
}
|
||||
label="Assignee"
|
||||
placeholder="Find assignee"
|
||||
/>
|
||||
<DropdownFilter
|
||||
data={repositories}
|
||||
onSelectedValuesChange={handleSelectedRepos}
|
||||
selectedValues={selectedRepos}
|
||||
data={
|
||||
filters?.repos.map(repo => {
|
||||
return {
|
||||
id: repo.repository || '',
|
||||
name: repo.repository || '',
|
||||
}
|
||||
}) || []
|
||||
}
|
||||
label="Repos"
|
||||
placeholder="Find repo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pl-2">
|
||||
<DropdownSort data={sortOptions} />
|
||||
<DropdownSort
|
||||
data={sortOptions}
|
||||
onOrderByValueChange={handleOrderByValue}
|
||||
orderByValue={orderByValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -275,47 +157,78 @@ const TableIssues = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-neutral-10">
|
||||
{issues.map(issue => (
|
||||
<Link
|
||||
key={issue.id}
|
||||
href={`https://github.com/status-im/status-react/issues/${issue.id}`}
|
||||
className="flex items-center justify-between px-4 py-3 transition-colors duration-200 hover:bg-neutral-5"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<Text size={15} weight="medium">
|
||||
{issue.title}
|
||||
</Text>
|
||||
<Text size={13} color="$neutral-50">
|
||||
#9667 • Opened 2 days ago by slaedjenic
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-1">
|
||||
<Tag size={24} label="E:Syncing" color="$orange-50" />
|
||||
<Tag size={24} label="E:Wallet" color="$green-50" />
|
||||
<Tag size={24} label="Feature" color="$pink-50" />
|
||||
<Tag size={24} label="Web" color="$purple-50" />
|
||||
<div className="relative divide-y divide-neutral-10">
|
||||
{data.length !== 0 &&
|
||||
data.map(issue => (
|
||||
<Link
|
||||
key={issue.issue_number}
|
||||
href={issue.issue_url || ''}
|
||||
className="flex items-center justify-between px-4 py-3 transition-colors duration-200 hover:bg-neutral-5"
|
||||
>
|
||||
<div className="flex flex-row items-start gap-2 ">
|
||||
<div className="pt-1">
|
||||
{issue.stage === 'open' ? (
|
||||
<OpenIcon size={20} color="$neutral-50" />
|
||||
) : (
|
||||
<ActiveMembersIcon size={20} color="$neutral-50" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex max-w-lg flex-col">
|
||||
<Text size={15} weight="medium" truncate>
|
||||
{issue.title}
|
||||
</Text>
|
||||
<Text size={13} color="$neutral-50">
|
||||
#{issue.issue_number} •{' '}
|
||||
{formatDistanceToNow(new Date(issue.created_at), {
|
||||
addSuffix: true,
|
||||
})}{' '}
|
||||
by {issue.author}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tag size={24} label="9435" />
|
||||
<div className="flex gap-3">
|
||||
{'epic_name' in issue && issue.epic_name && (
|
||||
<Tag
|
||||
size={24}
|
||||
label={issue.epic_name || ''}
|
||||
color={`#${issue.epic_color}` || '$primary'}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Avatar
|
||||
type="user"
|
||||
size={24}
|
||||
name="jkbktl"
|
||||
src="https://images.unsplash.com/photo-1552058544-f2b08422138a?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1299&q=80"
|
||||
/>
|
||||
{'labels' in issue &&
|
||||
issue.labels &&
|
||||
JSON.parse(issue.labels).map(
|
||||
(label: { id: string; name: string; color: string }) => (
|
||||
<Tag
|
||||
key={label.id}
|
||||
size={24}
|
||||
label={label.name}
|
||||
color={`#${label.color}`}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<Avatar type="user" size={24} name={issue.author || ''} />
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
{data.length === 0 && !isLoading && (
|
||||
<div className="py-11">
|
||||
<Empty />
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<div className="flex h-10 grow flex-col justify-between">
|
||||
<Skeleton width={340} height={18} />
|
||||
<Skeleton width={200} height={12} />
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-3">
|
||||
<Button size={40} variant="outline">
|
||||
Show more 10
|
||||
</Button>
|
||||
<div className="flex flex-auto flex-row justify-end gap-2">
|
||||
<Skeleton width={85} height={24} />
|
||||
<Skeleton width={24} height={24} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function useDebounce<T>(value: T, delay?: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedValue(value), delay || 300)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import type { RefObject } from 'react'
|
||||
|
||||
interface Args extends IntersectionObserverInit {
|
||||
freezeOnceVisible?: boolean
|
||||
}
|
||||
|
||||
export function useIntersectionObserver(
|
||||
elementRef: RefObject<Element>,
|
||||
{
|
||||
threshold = 0,
|
||||
root = null,
|
||||
rootMargin = '0%',
|
||||
freezeOnceVisible = false,
|
||||
}: Args
|
||||
): IntersectionObserverEntry | undefined {
|
||||
const [entry, setEntry] = useState<IntersectionObserverEntry>()
|
||||
|
||||
const frozen = entry?.isIntersecting && freezeOnceVisible
|
||||
|
||||
const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
|
||||
setEntry(entry)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const node = elementRef?.current // DOM Ref
|
||||
const hasIOSupport = !!window.IntersectionObserver
|
||||
|
||||
if (!hasIOSupport || frozen || !node) return
|
||||
|
||||
const observerParams = { threshold, root, rootMargin }
|
||||
const observer = new IntersectionObserver(updateEntry, observerParams)
|
||||
|
||||
observer.observe(node)
|
||||
|
||||
return () => observer.disconnect()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [elementRef?.current, JSON.stringify(threshold), root, rootMargin, frozen])
|
||||
|
||||
return entry
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
const TIMEOUT = 600
|
||||
|
||||
type Props = {
|
||||
initialVisible?: boolean
|
||||
defaultHeight?: number
|
||||
visibleOffset?: number
|
||||
disabled?: boolean
|
||||
root?: HTMLElement | null
|
||||
rootElement?: string
|
||||
rootElementClass?: string
|
||||
children: React.ReactNode
|
||||
placeholderComponent?: React.ReactNode
|
||||
}
|
||||
|
||||
export const RenderIfVisible = ({
|
||||
initialVisible = false,
|
||||
defaultHeight = 300,
|
||||
visibleOffset = 1000,
|
||||
disabled = false,
|
||||
root = null,
|
||||
rootElement = 'div',
|
||||
rootElementClass = '',
|
||||
placeholderComponent: PlaceholderComponent,
|
||||
children,
|
||||
}: Props) => {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(initialVisible)
|
||||
const wasVisible = useRef<boolean>(initialVisible)
|
||||
const placeholderHeight = useRef<number>(defaultHeight)
|
||||
const intersectionRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (intersectionRef.current) {
|
||||
const localRef = intersectionRef.current
|
||||
const observer = new IntersectionObserver(
|
||||
entries => {
|
||||
if (!entries[0].isIntersecting) {
|
||||
placeholderHeight.current = localRef!.offsetHeight
|
||||
}
|
||||
if (typeof window !== undefined && window.requestIdleCallback) {
|
||||
window.requestIdleCallback(
|
||||
() => setIsVisible(entries[0].isIntersecting),
|
||||
{
|
||||
timeout: TIMEOUT,
|
||||
}
|
||||
)
|
||||
} else {
|
||||
setIsVisible(entries[0].isIntersecting)
|
||||
}
|
||||
},
|
||||
{ root, rootMargin: `${visibleOffset}px 0px ${visibleOffset}px 0px` }
|
||||
)
|
||||
|
||||
observer.observe(localRef)
|
||||
return () => {
|
||||
if (localRef) {
|
||||
observer.unobserve(localRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [root, visibleOffset])
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
wasVisible.current = true
|
||||
}
|
||||
}, [isVisible])
|
||||
|
||||
const rootClasses = useMemo(
|
||||
() => `renderIfVisible ${rootElementClass}`,
|
||||
[rootElementClass]
|
||||
)
|
||||
|
||||
return React.createElement(
|
||||
rootElement,
|
||||
{
|
||||
ref: intersectionRef,
|
||||
className: rootClasses,
|
||||
},
|
||||
isVisible || (disabled && wasVisible.current)
|
||||
? children
|
||||
: PlaceholderComponent || null
|
||||
)
|
||||
}
|
|
@ -1,74 +1,7 @@
|
|||
import { SidebarMenu } from '../components/sidebar-menu'
|
||||
import { SidebarMenu } from '../components'
|
||||
import { AppLayout } from './app-layout'
|
||||
|
||||
// Eventually this will be fetched from the API, at least the nested links
|
||||
const MENU_LINKS = [
|
||||
{
|
||||
label: 'Epics',
|
||||
links: [
|
||||
{
|
||||
label: 'Overview',
|
||||
href: '/insights/epics',
|
||||
},
|
||||
{
|
||||
label: 'Community Protocol',
|
||||
href: '/insights/epics/1',
|
||||
},
|
||||
{
|
||||
label: 'Keycard',
|
||||
href: '/insights/epics/keycard',
|
||||
},
|
||||
{
|
||||
label: 'Notifications Settings',
|
||||
href: '/insights/epics/notifications-settings',
|
||||
},
|
||||
{
|
||||
label: 'Wallet',
|
||||
href: '/insights/epics/wallet',
|
||||
},
|
||||
{
|
||||
label: 'Communities',
|
||||
href: '/insights/epics/communities',
|
||||
},
|
||||
{
|
||||
label: 'Acitivity Center',
|
||||
href: '/insights/epics/activity-center',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Workstreams',
|
||||
links: [
|
||||
{
|
||||
label: 'Overview',
|
||||
href: '/insights/workstreams',
|
||||
},
|
||||
{
|
||||
label: 'Community Protocol 2',
|
||||
href: '/insights/workstreams/community-protocol-2',
|
||||
},
|
||||
{
|
||||
label: 'Keycard 2',
|
||||
href: '/insights/workstreams/keycard-2',
|
||||
},
|
||||
{
|
||||
label: 'Notifications Settings 2',
|
||||
href: '/insights/workstreams/notifications-settings-2',
|
||||
},
|
||||
{
|
||||
label: 'Wallet 2',
|
||||
href: '/insights/workstreams/wallet-2',
|
||||
},
|
||||
{
|
||||
label: 'Communities 2',
|
||||
href: '/insights/workstreams/communities-2',
|
||||
},
|
||||
{
|
||||
label: 'Acitivity Center 2',
|
||||
href: '/insights/workstreams/activity-center-2',
|
||||
},
|
||||
],
|
||||
},
|
||||
const STATIC_LINKS = [
|
||||
{
|
||||
label: 'Orphans',
|
||||
href: '/insights/orphans',
|
||||
|
@ -81,14 +14,40 @@ const MENU_LINKS = [
|
|||
|
||||
interface InsightsLayoutProps {
|
||||
children: React.ReactNode
|
||||
links: string[]
|
||||
}
|
||||
|
||||
export const InsightsLayout: React.FC<InsightsLayoutProps> = ({ children }) => {
|
||||
export const InsightsLayout: React.FC<InsightsLayoutProps> = ({
|
||||
children,
|
||||
links: linksFromProps,
|
||||
}) => {
|
||||
const epicLinks =
|
||||
linksFromProps?.map(epic => {
|
||||
return {
|
||||
label: epic || '',
|
||||
href: `/insights/epics/${epic}`,
|
||||
}
|
||||
}) || []
|
||||
|
||||
const links = [
|
||||
{
|
||||
label: 'Epics',
|
||||
links: [
|
||||
{
|
||||
label: 'Overview',
|
||||
href: '/insights/epics',
|
||||
},
|
||||
...epicLinks,
|
||||
],
|
||||
},
|
||||
...STATIC_LINKS,
|
||||
]
|
||||
|
||||
return (
|
||||
<AppLayout hasPreFooter={false}>
|
||||
<div className="relative mx-1 flex min-h-[calc(100vh-56px-4px)] w-full rounded-3xl bg-white-100">
|
||||
<SidebarMenu items={MENU_LINKS} />
|
||||
<main className="flex-1">{children}</main>
|
||||
<div className="relative flex min-h-[calc(100vh-56px-4px)] w-full rounded-3xl bg-white-100">
|
||||
{<SidebarMenu items={links} />}
|
||||
<main className="flex-1 pb-8">{children}</main>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
export const GET_BURNUP = /* GraphQL */ `
|
||||
query getBurnup($epicNames: [String!], $from: timestamptz, $to: timestamptz) {
|
||||
gh_burnup(
|
||||
where: {
|
||||
epic_name: { _in: $epicNames }
|
||||
_or: [
|
||||
{
|
||||
_and: [
|
||||
{ date_field: { _gte: $from } }
|
||||
{ date_field: { _lt: $to } }
|
||||
]
|
||||
}
|
||||
{
|
||||
_and: [
|
||||
{ date_field: { _gt: $from } }
|
||||
{ date_field: { _lte: $to } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
order_by: { date_field: asc }
|
||||
) {
|
||||
epic_name
|
||||
total_closed_issues
|
||||
total_opened_issues
|
||||
date_field
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_ISSUES_BY_EPIC = /* GraphQL */ `
|
||||
query getIssuesByEpic(
|
||||
$where: gh_epic_issues_bool_exp!
|
||||
$limit: Int!
|
||||
$offset: Int!
|
||||
$orderBy: order_by
|
||||
) {
|
||||
gh_epic_issues(
|
||||
where: $where
|
||||
order_by: { created_at: $orderBy }
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
) {
|
||||
assignee
|
||||
author
|
||||
closed_at
|
||||
created_at
|
||||
epic_color
|
||||
epic_name
|
||||
repository
|
||||
stage
|
||||
title
|
||||
issue_number
|
||||
issue_url
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_EPIC_ISSUES_COUNT = /* GraphQL */ `
|
||||
query getEpicIssuesCount($where: gh_epic_issues_bool_exp!) {
|
||||
gh_epic_issues(where: $where) {
|
||||
closed_at
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_FILTERS_WITH_EPIC = /* GraphQL */ `
|
||||
query getFiltersWithEpic($epicName: String!) {
|
||||
authors: gh_epic_issues(
|
||||
where: { epic_name: { _eq: $epicName }, author: { _is_null: false } }
|
||||
distinct_on: author
|
||||
) {
|
||||
author
|
||||
}
|
||||
assignees: gh_epic_issues(
|
||||
where: { epic_name: { _eq: $epicName }, assignee: { _is_null: false } }
|
||||
distinct_on: assignee
|
||||
) {
|
||||
assignee
|
||||
}
|
||||
repos: gh_epic_issues(
|
||||
where: { epic_name: { _eq: $epicName } }
|
||||
distinct_on: repository
|
||||
) {
|
||||
repository
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_EPIC_LINKS = /* GraphQL */ `
|
||||
query getEpicMenuLinks(
|
||||
$where: gh_epics_bool_exp
|
||||
$orderBy: [gh_epics_order_by!]
|
||||
$limit: Int
|
||||
$offset: Int
|
||||
) {
|
||||
gh_epics(
|
||||
where: $where
|
||||
order_by: $orderBy
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
distinct_on: epic_name
|
||||
) {
|
||||
epic_name
|
||||
epic_color
|
||||
epic_description
|
||||
status
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_REPOS = /* GraphQL */ `
|
||||
query getRepositories {
|
||||
gh_repositories {
|
||||
description
|
||||
full_name
|
||||
name
|
||||
open_issues_count
|
||||
stargazers_count
|
||||
visibility
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_ORPHANS = /* GraphQL */ `
|
||||
query getOrphans(
|
||||
$where: gh_orphans_bool_exp!
|
||||
$limit: Int!
|
||||
$offset: Int!
|
||||
$orderBy: order_by
|
||||
) {
|
||||
gh_orphans(
|
||||
where: $where
|
||||
order_by: { created_at: $orderBy }
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
) {
|
||||
labels
|
||||
assignee
|
||||
author
|
||||
issue_number
|
||||
issue_url
|
||||
created_at
|
||||
closed_at
|
||||
repository
|
||||
stage
|
||||
title
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_ORPHANS_COUNT = /* GraphQL */ `
|
||||
query getOrphansCount($where: gh_orphans_bool_exp!) {
|
||||
gh_orphans(where: $where) {
|
||||
closed_at
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GET_FILTERS_FOR_ORPHANS = /* GraphQL */ `
|
||||
query getFiltersForOrphans {
|
||||
authors: gh_orphans(
|
||||
where: { author: { _is_null: false } }
|
||||
distinct_on: author
|
||||
) {
|
||||
author
|
||||
}
|
||||
assignees: gh_orphans(
|
||||
where: { assignee: { _is_null: false } }
|
||||
distinct_on: assignee
|
||||
) {
|
||||
assignee
|
||||
}
|
||||
repos: gh_orphans(distinct_on: repository) {
|
||||
repository
|
||||
}
|
||||
}
|
||||
`
|
|
@ -0,0 +1,27 @@
|
|||
import { GraphQLClient } from 'graphql-request'
|
||||
|
||||
import type { RequestDocument, Variables } from 'graphql-request'
|
||||
|
||||
export const GRAPHQL_ENDPOINT = `https://hasura.infra.status.im/v1/graphql`
|
||||
|
||||
export const api = <T, V extends Variables = Variables>(
|
||||
operation: RequestDocument,
|
||||
variables?: V,
|
||||
headers?: Record<string, string>
|
||||
): Promise<T> => {
|
||||
const client = new GraphQLClient(GRAPHQL_ENDPOINT, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
})
|
||||
|
||||
return client.request<T>(operation, variables as Variables)
|
||||
}
|
||||
|
||||
export const createFetcher = <T, V extends Variables = Variables>(
|
||||
operation: string,
|
||||
variables?: V
|
||||
) => {
|
||||
return () => api<T, V>(operation, variables)
|
||||
}
|
|
@ -0,0 +1,327 @@
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { createFetcher } from '../api'
|
||||
|
||||
import type * as Types from './operations'
|
||||
import type { UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
export const GetBurnupDocument = `
|
||||
query getBurnup($epicNames: [String!], $from: timestamptz, $to: timestamptz) {
|
||||
gh_burnup(
|
||||
where: {epic_name: {_in: $epicNames}, _or: [{_and: [{date_field: {_gte: $from}}, {date_field: {_lt: $to}}]}, {_and: [{date_field: {_gt: $from}}, {date_field: {_lte: $to}}]}]}
|
||||
order_by: {date_field: asc}
|
||||
) {
|
||||
epic_name
|
||||
total_closed_issues
|
||||
total_opened_issues
|
||||
date_field
|
||||
}
|
||||
}
|
||||
`
|
||||
export const useGetBurnupQuery = <
|
||||
TData = Types.GetBurnupQuery,
|
||||
TError = GraphqlApiError
|
||||
>(
|
||||
variables?: Types.GetBurnupQueryVariables,
|
||||
options?: UseQueryOptions<Types.GetBurnupQuery, TError, TData>
|
||||
) =>
|
||||
useQuery<Types.GetBurnupQuery, TError, TData>(
|
||||
variables === undefined ? ['getBurnup'] : ['getBurnup', variables],
|
||||
createFetcher<Types.GetBurnupQuery, Types.GetBurnupQueryVariables>(
|
||||
GetBurnupDocument,
|
||||
variables
|
||||
),
|
||||
options
|
||||
)
|
||||
|
||||
useGetBurnupQuery.getKey = (variables?: Types.GetBurnupQueryVariables) =>
|
||||
variables === undefined ? ['getBurnup'] : ['getBurnup', variables]
|
||||
export const GetIssuesByEpicDocument = `
|
||||
query getIssuesByEpic($where: gh_epic_issues_bool_exp!, $limit: Int!, $offset: Int!, $orderBy: order_by) {
|
||||
gh_epic_issues(
|
||||
where: $where
|
||||
order_by: {created_at: $orderBy}
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
) {
|
||||
assignee
|
||||
author
|
||||
closed_at
|
||||
created_at
|
||||
epic_color
|
||||
epic_name
|
||||
repository
|
||||
stage
|
||||
title
|
||||
issue_number
|
||||
issue_url
|
||||
}
|
||||
}
|
||||
`
|
||||
export const useGetIssuesByEpicQuery = <
|
||||
TData = Types.GetIssuesByEpicQuery,
|
||||
TError = GraphqlApiError
|
||||
>(
|
||||
variables: Types.GetIssuesByEpicQueryVariables,
|
||||
options?: UseQueryOptions<Types.GetIssuesByEpicQuery, TError, TData>
|
||||
) =>
|
||||
useQuery<Types.GetIssuesByEpicQuery, TError, TData>(
|
||||
['getIssuesByEpic', variables],
|
||||
createFetcher<
|
||||
Types.GetIssuesByEpicQuery,
|
||||
Types.GetIssuesByEpicQueryVariables
|
||||
>(GetIssuesByEpicDocument, variables),
|
||||
options
|
||||
)
|
||||
|
||||
useGetIssuesByEpicQuery.getKey = (
|
||||
variables: Types.GetIssuesByEpicQueryVariables
|
||||
) => ['getIssuesByEpic', variables]
|
||||
export const GetEpicIssuesCountDocument = `
|
||||
query getEpicIssuesCount($where: gh_epic_issues_bool_exp!) {
|
||||
gh_epic_issues(where: $where) {
|
||||
closed_at
|
||||
}
|
||||
}
|
||||
`
|
||||
export const useGetEpicIssuesCountQuery = <
|
||||
TData = Types.GetEpicIssuesCountQuery,
|
||||
TError = GraphqlApiError
|
||||
>(
|
||||
variables: Types.GetEpicIssuesCountQueryVariables,
|
||||
options?: UseQueryOptions<Types.GetEpicIssuesCountQuery, TError, TData>
|
||||
) =>
|
||||
useQuery<Types.GetEpicIssuesCountQuery, TError, TData>(
|
||||
['getEpicIssuesCount', variables],
|
||||
createFetcher<
|
||||
Types.GetEpicIssuesCountQuery,
|
||||
Types.GetEpicIssuesCountQueryVariables
|
||||
>(GetEpicIssuesCountDocument, variables),
|
||||
options
|
||||
)
|
||||
|
||||
useGetEpicIssuesCountQuery.getKey = (
|
||||
variables: Types.GetEpicIssuesCountQueryVariables
|
||||
) => ['getEpicIssuesCount', variables]
|
||||
export const GetFiltersWithEpicDocument = `
|
||||
query getFiltersWithEpic($epicName: String!) {
|
||||
authors: gh_epic_issues(
|
||||
where: {epic_name: {_eq: $epicName}, author: {_is_null: false}}
|
||||
distinct_on: author
|
||||
) {
|
||||
author
|
||||
}
|
||||
assignees: gh_epic_issues(
|
||||
where: {epic_name: {_eq: $epicName}, assignee: {_is_null: false}}
|
||||
distinct_on: assignee
|
||||
) {
|
||||
assignee
|
||||
}
|
||||
repos: gh_epic_issues(
|
||||
where: {epic_name: {_eq: $epicName}}
|
||||
distinct_on: repository
|
||||
) {
|
||||
repository
|
||||
}
|
||||
}
|
||||
`
|
||||
export const useGetFiltersWithEpicQuery = <
|
||||
TData = Types.GetFiltersWithEpicQuery,
|
||||
TError = GraphqlApiError
|
||||
>(
|
||||
variables: Types.GetFiltersWithEpicQueryVariables,
|
||||
options?: UseQueryOptions<Types.GetFiltersWithEpicQuery, TError, TData>
|
||||
) =>
|
||||
useQuery<Types.GetFiltersWithEpicQuery, TError, TData>(
|
||||
['getFiltersWithEpic', variables],
|
||||
createFetcher<
|
||||
Types.GetFiltersWithEpicQuery,
|
||||
Types.GetFiltersWithEpicQueryVariables
|
||||
>(GetFiltersWithEpicDocument, variables),
|
||||
options
|
||||
)
|
||||
|
||||
useGetFiltersWithEpicQuery.getKey = (
|
||||
variables: Types.GetFiltersWithEpicQueryVariables
|
||||
) => ['getFiltersWithEpic', variables]
|
||||
export const GetEpicMenuLinksDocument = `
|
||||
query getEpicMenuLinks($where: gh_epics_bool_exp, $orderBy: [gh_epics_order_by!], $limit: Int, $offset: Int) {
|
||||
gh_epics(
|
||||
where: $where
|
||||
order_by: $orderBy
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
distinct_on: epic_name
|
||||
) {
|
||||
epic_name
|
||||
epic_color
|
||||
epic_description
|
||||
status
|
||||
}
|
||||
}
|
||||
`
|
||||
export const useGetEpicMenuLinksQuery = <
|
||||
TData = Types.GetEpicMenuLinksQuery,
|
||||
TError = GraphqlApiError
|
||||
>(
|
||||
variables?: Types.GetEpicMenuLinksQueryVariables,
|
||||
options?: UseQueryOptions<Types.GetEpicMenuLinksQuery, TError, TData>
|
||||
) =>
|
||||
useQuery<Types.GetEpicMenuLinksQuery, TError, TData>(
|
||||
variables === undefined
|
||||
? ['getEpicMenuLinks']
|
||||
: ['getEpicMenuLinks', variables],
|
||||
createFetcher<
|
||||
Types.GetEpicMenuLinksQuery,
|
||||
Types.GetEpicMenuLinksQueryVariables
|
||||
>(GetEpicMenuLinksDocument, variables),
|
||||
options
|
||||
)
|
||||
|
||||
useGetEpicMenuLinksQuery.getKey = (
|
||||
variables?: Types.GetEpicMenuLinksQueryVariables
|
||||
) =>
|
||||
variables === undefined
|
||||
? ['getEpicMenuLinks']
|
||||
: ['getEpicMenuLinks', variables]
|
||||
export const GetRepositoriesDocument = `
|
||||
query getRepositories {
|
||||
gh_repositories {
|
||||
description
|
||||
full_name
|
||||
name
|
||||
open_issues_count
|
||||
stargazers_count
|
||||
visibility
|
||||
}
|
||||
}
|
||||
`
|
||||
export const useGetRepositoriesQuery = <
|
||||
TData = Types.GetRepositoriesQuery,
|
||||
TError = GraphqlApiError
|
||||
>(
|
||||
variables?: Types.GetRepositoriesQueryVariables,
|
||||
options?: UseQueryOptions<Types.GetRepositoriesQuery, TError, TData>
|
||||
) =>
|
||||
useQuery<Types.GetRepositoriesQuery, TError, TData>(
|
||||
variables === undefined
|
||||
? ['getRepositories']
|
||||
: ['getRepositories', variables],
|
||||
createFetcher<
|
||||
Types.GetRepositoriesQuery,
|
||||
Types.GetRepositoriesQueryVariables
|
||||
>(GetRepositoriesDocument, variables),
|
||||
options
|
||||
)
|
||||
|
||||
useGetRepositoriesQuery.getKey = (
|
||||
variables?: Types.GetRepositoriesQueryVariables
|
||||
) =>
|
||||
variables === undefined ? ['getRepositories'] : ['getRepositories', variables]
|
||||
export const GetOrphansDocument = `
|
||||
query getOrphans($where: gh_orphans_bool_exp!, $limit: Int!, $offset: Int!, $orderBy: order_by) {
|
||||
gh_orphans(
|
||||
where: $where
|
||||
order_by: {created_at: $orderBy}
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
) {
|
||||
labels
|
||||
assignee
|
||||
author
|
||||
issue_number
|
||||
issue_url
|
||||
created_at
|
||||
closed_at
|
||||
repository
|
||||
stage
|
||||
title
|
||||
}
|
||||
}
|
||||
`
|
||||
export const useGetOrphansQuery = <
|
||||
TData = Types.GetOrphansQuery,
|
||||
TError = GraphqlApiError
|
||||
>(
|
||||
variables: Types.GetOrphansQueryVariables,
|
||||
options?: UseQueryOptions<Types.GetOrphansQuery, TError, TData>
|
||||
) =>
|
||||
useQuery<Types.GetOrphansQuery, TError, TData>(
|
||||
['getOrphans', variables],
|
||||
createFetcher<Types.GetOrphansQuery, Types.GetOrphansQueryVariables>(
|
||||
GetOrphansDocument,
|
||||
variables
|
||||
),
|
||||
options
|
||||
)
|
||||
|
||||
useGetOrphansQuery.getKey = (variables: Types.GetOrphansQueryVariables) => [
|
||||
'getOrphans',
|
||||
variables,
|
||||
]
|
||||
export const GetOrphansCountDocument = `
|
||||
query getOrphansCount($where: gh_orphans_bool_exp!) {
|
||||
gh_orphans(where: $where) {
|
||||
closed_at
|
||||
}
|
||||
}
|
||||
`
|
||||
export const useGetOrphansCountQuery = <
|
||||
TData = Types.GetOrphansCountQuery,
|
||||
TError = GraphqlApiError
|
||||
>(
|
||||
variables: Types.GetOrphansCountQueryVariables,
|
||||
options?: UseQueryOptions<Types.GetOrphansCountQuery, TError, TData>
|
||||
) =>
|
||||
useQuery<Types.GetOrphansCountQuery, TError, TData>(
|
||||
['getOrphansCount', variables],
|
||||
createFetcher<
|
||||
Types.GetOrphansCountQuery,
|
||||
Types.GetOrphansCountQueryVariables
|
||||
>(GetOrphansCountDocument, variables),
|
||||
options
|
||||
)
|
||||
|
||||
useGetOrphansCountQuery.getKey = (
|
||||
variables: Types.GetOrphansCountQueryVariables
|
||||
) => ['getOrphansCount', variables]
|
||||
export const GetFiltersForOrphansDocument = `
|
||||
query getFiltersForOrphans {
|
||||
authors: gh_orphans(where: {author: {_is_null: false}}, distinct_on: author) {
|
||||
author
|
||||
}
|
||||
assignees: gh_orphans(
|
||||
where: {assignee: {_is_null: false}}
|
||||
distinct_on: assignee
|
||||
) {
|
||||
assignee
|
||||
}
|
||||
repos: gh_orphans(distinct_on: repository) {
|
||||
repository
|
||||
}
|
||||
}
|
||||
`
|
||||
export const useGetFiltersForOrphansQuery = <
|
||||
TData = Types.GetFiltersForOrphansQuery,
|
||||
TError = GraphqlApiError
|
||||
>(
|
||||
variables?: Types.GetFiltersForOrphansQueryVariables,
|
||||
options?: UseQueryOptions<Types.GetFiltersForOrphansQuery, TError, TData>
|
||||
) =>
|
||||
useQuery<Types.GetFiltersForOrphansQuery, TError, TData>(
|
||||
variables === undefined
|
||||
? ['getFiltersForOrphans']
|
||||
: ['getFiltersForOrphans', variables],
|
||||
createFetcher<
|
||||
Types.GetFiltersForOrphansQuery,
|
||||
Types.GetFiltersForOrphansQueryVariables
|
||||
>(GetFiltersForOrphansDocument, variables),
|
||||
options
|
||||
)
|
||||
|
||||
useGetFiltersForOrphansQuery.getKey = (
|
||||
variables?: Types.GetFiltersForOrphansQueryVariables
|
||||
) =>
|
||||
variables === undefined
|
||||
? ['getFiltersForOrphans']
|
||||
: ['getFiltersForOrphans', variables]
|
|
@ -0,0 +1,149 @@
|
|||
import type * as Types from './schemas'
|
||||
|
||||
export type GetBurnupQueryVariables = Types.Exact<{
|
||||
epicNames?: Types.InputMaybe<
|
||||
Array<Types.Scalars['String']['input']> | Types.Scalars['String']['input']
|
||||
>
|
||||
from?: Types.InputMaybe<Types.Scalars['timestamptz']['input']>
|
||||
to?: Types.InputMaybe<Types.Scalars['timestamptz']['input']>
|
||||
}>
|
||||
|
||||
export type GetBurnupQuery = {
|
||||
__typename?: 'query_root'
|
||||
gh_burnup: Array<{
|
||||
__typename?: 'gh_burnup'
|
||||
epic_name?: string | null
|
||||
total_closed_issues?: any | null
|
||||
total_opened_issues?: any | null
|
||||
date_field?: any | null
|
||||
}>
|
||||
}
|
||||
|
||||
export type GetIssuesByEpicQueryVariables = Types.Exact<{
|
||||
where: Types.Gh_Epic_Issues_Bool_Exp
|
||||
limit: Types.Scalars['Int']['input']
|
||||
offset: Types.Scalars['Int']['input']
|
||||
orderBy?: Types.InputMaybe<Types.Order_By>
|
||||
}>
|
||||
|
||||
export type GetIssuesByEpicQuery = {
|
||||
__typename?: 'query_root'
|
||||
gh_epic_issues: Array<{
|
||||
__typename?: 'gh_epic_issues'
|
||||
assignee?: string | null
|
||||
author?: string | null
|
||||
closed_at?: any | null
|
||||
created_at?: any | null
|
||||
epic_color?: string | null
|
||||
epic_name?: string | null
|
||||
repository?: string | null
|
||||
stage?: string | null
|
||||
title?: string | null
|
||||
issue_number?: any | null
|
||||
issue_url?: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
export type GetEpicIssuesCountQueryVariables = Types.Exact<{
|
||||
where: Types.Gh_Epic_Issues_Bool_Exp
|
||||
}>
|
||||
|
||||
export type GetEpicIssuesCountQuery = {
|
||||
__typename?: 'query_root'
|
||||
gh_epic_issues: Array<{
|
||||
__typename?: 'gh_epic_issues'
|
||||
closed_at?: any | null
|
||||
}>
|
||||
}
|
||||
|
||||
export type GetFiltersWithEpicQueryVariables = Types.Exact<{
|
||||
epicName: Types.Scalars['String']['input']
|
||||
}>
|
||||
|
||||
export type GetFiltersWithEpicQuery = {
|
||||
__typename?: 'query_root'
|
||||
authors: Array<{ __typename?: 'gh_epic_issues'; author?: string | null }>
|
||||
assignees: Array<{ __typename?: 'gh_epic_issues'; assignee?: string | null }>
|
||||
repos: Array<{ __typename?: 'gh_epic_issues'; repository?: string | null }>
|
||||
}
|
||||
|
||||
export type GetEpicMenuLinksQueryVariables = Types.Exact<{
|
||||
where?: Types.InputMaybe<Types.Gh_Epics_Bool_Exp>
|
||||
orderBy?: Types.InputMaybe<
|
||||
Array<Types.Gh_Epics_Order_By> | Types.Gh_Epics_Order_By
|
||||
>
|
||||
limit?: Types.InputMaybe<Types.Scalars['Int']['input']>
|
||||
offset?: Types.InputMaybe<Types.Scalars['Int']['input']>
|
||||
}>
|
||||
|
||||
export type GetEpicMenuLinksQuery = {
|
||||
__typename?: 'query_root'
|
||||
gh_epics: Array<{
|
||||
__typename?: 'gh_epics'
|
||||
epic_name?: string | null
|
||||
epic_color?: string | null
|
||||
epic_description?: string | null
|
||||
status?: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
export type GetRepositoriesQueryVariables = Types.Exact<{
|
||||
[key: string]: never
|
||||
}>
|
||||
|
||||
export type GetRepositoriesQuery = {
|
||||
__typename?: 'query_root'
|
||||
gh_repositories: Array<{
|
||||
__typename?: 'gh_repositories'
|
||||
description?: string | null
|
||||
full_name?: string | null
|
||||
name?: string | null
|
||||
open_issues_count?: any | null
|
||||
stargazers_count?: any | null
|
||||
visibility?: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
export type GetOrphansQueryVariables = Types.Exact<{
|
||||
where: Types.Gh_Orphans_Bool_Exp
|
||||
limit: Types.Scalars['Int']['input']
|
||||
offset: Types.Scalars['Int']['input']
|
||||
orderBy?: Types.InputMaybe<Types.Order_By>
|
||||
}>
|
||||
|
||||
export type GetOrphansQuery = {
|
||||
__typename?: 'query_root'
|
||||
gh_orphans: Array<{
|
||||
__typename?: 'gh_orphans'
|
||||
labels?: string | null
|
||||
assignee?: string | null
|
||||
author?: string | null
|
||||
issue_number?: any | null
|
||||
issue_url?: string | null
|
||||
created_at?: any | null
|
||||
closed_at?: any | null
|
||||
repository?: string | null
|
||||
stage?: string | null
|
||||
title?: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
export type GetOrphansCountQueryVariables = Types.Exact<{
|
||||
where: Types.Gh_Orphans_Bool_Exp
|
||||
}>
|
||||
|
||||
export type GetOrphansCountQuery = {
|
||||
__typename?: 'query_root'
|
||||
gh_orphans: Array<{ __typename?: 'gh_orphans'; closed_at?: any | null }>
|
||||
}
|
||||
|
||||
export type GetFiltersForOrphansQueryVariables = Types.Exact<{
|
||||
[key: string]: never
|
||||
}>
|
||||
|
||||
export type GetFiltersForOrphansQuery = {
|
||||
__typename?: 'query_root'
|
||||
authors: Array<{ __typename?: 'gh_orphans'; author?: string | null }>
|
||||
assignees: Array<{ __typename?: 'gh_orphans'; assignee?: string | null }>
|
||||
repos: Array<{ __typename?: 'gh_orphans'; repository?: string | null }>
|
||||
}
|
|
@ -0,0 +1,775 @@
|
|||
export type Maybe<T> = T | null
|
||||
export type InputMaybe<T> = Maybe<T>
|
||||
export type Exact<T extends { [key: string]: unknown }> = {
|
||||
[K in keyof T]: T[K]
|
||||
}
|
||||
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & {
|
||||
[SubKey in K]?: Maybe<T[SubKey]>
|
||||
}
|
||||
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & {
|
||||
[SubKey in K]: Maybe<T[SubKey]>
|
||||
}
|
||||
export type MakeEmpty<
|
||||
T extends { [key: string]: unknown },
|
||||
K extends keyof T
|
||||
> = { [_ in K]?: never }
|
||||
export type Incremental<T> =
|
||||
| T
|
||||
| {
|
||||
[P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never
|
||||
}
|
||||
/** All built-in and custom scalars, mapped to their actual values */
|
||||
export type Scalars = {
|
||||
ID: { input: string; output: string }
|
||||
String: { input: string; output: string }
|
||||
Boolean: { input: boolean; output: boolean }
|
||||
Int: { input: number; output: number }
|
||||
Float: { input: number; output: number }
|
||||
bigint: { input: any; output: any }
|
||||
timestamptz: { input: any; output: any }
|
||||
}
|
||||
|
||||
/** Boolean expression to compare columns of type "String". All fields are combined with logical 'AND'. */
|
||||
export type String_Comparison_Exp = {
|
||||
_eq?: InputMaybe<Scalars['String']['input']>
|
||||
_gt?: InputMaybe<Scalars['String']['input']>
|
||||
_gte?: InputMaybe<Scalars['String']['input']>
|
||||
/** does the column match the given case-insensitive pattern */
|
||||
_ilike?: InputMaybe<Scalars['String']['input']>
|
||||
_in?: InputMaybe<Array<Scalars['String']['input']>>
|
||||
/** does the column match the given POSIX regular expression, case insensitive */
|
||||
_iregex?: InputMaybe<Scalars['String']['input']>
|
||||
_is_null?: InputMaybe<Scalars['Boolean']['input']>
|
||||
/** does the column match the given pattern */
|
||||
_like?: InputMaybe<Scalars['String']['input']>
|
||||
_lt?: InputMaybe<Scalars['String']['input']>
|
||||
_lte?: InputMaybe<Scalars['String']['input']>
|
||||
_neq?: InputMaybe<Scalars['String']['input']>
|
||||
/** does the column NOT match the given case-insensitive pattern */
|
||||
_nilike?: InputMaybe<Scalars['String']['input']>
|
||||
_nin?: InputMaybe<Array<Scalars['String']['input']>>
|
||||
/** does the column NOT match the given POSIX regular expression, case insensitive */
|
||||
_niregex?: InputMaybe<Scalars['String']['input']>
|
||||
/** does the column NOT match the given pattern */
|
||||
_nlike?: InputMaybe<Scalars['String']['input']>
|
||||
/** does the column NOT match the given POSIX regular expression, case sensitive */
|
||||
_nregex?: InputMaybe<Scalars['String']['input']>
|
||||
/** does the column NOT match the given SQL regular expression */
|
||||
_nsimilar?: InputMaybe<Scalars['String']['input']>
|
||||
/** does the column match the given POSIX regular expression, case sensitive */
|
||||
_regex?: InputMaybe<Scalars['String']['input']>
|
||||
/** does the column match the given SQL regular expression */
|
||||
_similar?: InputMaybe<Scalars['String']['input']>
|
||||
}
|
||||
|
||||
/** Boolean expression to compare columns of type "bigint". All fields are combined with logical 'AND'. */
|
||||
export type Bigint_Comparison_Exp = {
|
||||
_eq?: InputMaybe<Scalars['bigint']['input']>
|
||||
_gt?: InputMaybe<Scalars['bigint']['input']>
|
||||
_gte?: InputMaybe<Scalars['bigint']['input']>
|
||||
_in?: InputMaybe<Array<Scalars['bigint']['input']>>
|
||||
_is_null?: InputMaybe<Scalars['Boolean']['input']>
|
||||
_lt?: InputMaybe<Scalars['bigint']['input']>
|
||||
_lte?: InputMaybe<Scalars['bigint']['input']>
|
||||
_neq?: InputMaybe<Scalars['bigint']['input']>
|
||||
_nin?: InputMaybe<Array<Scalars['bigint']['input']>>
|
||||
}
|
||||
|
||||
/** ordering argument of a cursor */
|
||||
export enum Cursor_Ordering {
|
||||
/** ascending ordering of the cursor */
|
||||
Asc = 'ASC',
|
||||
/** descending ordering of the cursor */
|
||||
Desc = 'DESC',
|
||||
}
|
||||
|
||||
/** columns and relationships of "gh_burnup" */
|
||||
export type Gh_Burnup = {
|
||||
__typename?: 'gh_burnup'
|
||||
date_field?: Maybe<Scalars['timestamptz']['output']>
|
||||
epic_name?: Maybe<Scalars['String']['output']>
|
||||
total_closed_issues?: Maybe<Scalars['bigint']['output']>
|
||||
total_opened_issues?: Maybe<Scalars['bigint']['output']>
|
||||
}
|
||||
|
||||
/** Boolean expression to filter rows from the table "gh_burnup". All fields are combined with a logical 'AND'. */
|
||||
export type Gh_Burnup_Bool_Exp = {
|
||||
_and?: InputMaybe<Array<Gh_Burnup_Bool_Exp>>
|
||||
_not?: InputMaybe<Gh_Burnup_Bool_Exp>
|
||||
_or?: InputMaybe<Array<Gh_Burnup_Bool_Exp>>
|
||||
date_field?: InputMaybe<Timestamptz_Comparison_Exp>
|
||||
epic_name?: InputMaybe<String_Comparison_Exp>
|
||||
total_closed_issues?: InputMaybe<Bigint_Comparison_Exp>
|
||||
total_opened_issues?: InputMaybe<Bigint_Comparison_Exp>
|
||||
}
|
||||
|
||||
/** Ordering options when selecting data from "gh_burnup". */
|
||||
export type Gh_Burnup_Order_By = {
|
||||
date_field?: InputMaybe<Order_By>
|
||||
epic_name?: InputMaybe<Order_By>
|
||||
total_closed_issues?: InputMaybe<Order_By>
|
||||
total_opened_issues?: InputMaybe<Order_By>
|
||||
}
|
||||
|
||||
/** select columns of table "gh_burnup" */
|
||||
export enum Gh_Burnup_Select_Column {
|
||||
/** column name */
|
||||
DateField = 'date_field',
|
||||
/** column name */
|
||||
EpicName = 'epic_name',
|
||||
/** column name */
|
||||
TotalClosedIssues = 'total_closed_issues',
|
||||
/** column name */
|
||||
TotalOpenedIssues = 'total_opened_issues',
|
||||
}
|
||||
|
||||
/** Streaming cursor of the table "gh_burnup" */
|
||||
export type Gh_Burnup_Stream_Cursor_Input = {
|
||||
/** Stream column input with initial value */
|
||||
initial_value: Gh_Burnup_Stream_Cursor_Value_Input
|
||||
/** cursor ordering */
|
||||
ordering?: InputMaybe<Cursor_Ordering>
|
||||
}
|
||||
|
||||
/** Initial value of the column from where the streaming should start */
|
||||
export type Gh_Burnup_Stream_Cursor_Value_Input = {
|
||||
date_field?: InputMaybe<Scalars['timestamptz']['input']>
|
||||
epic_name?: InputMaybe<Scalars['String']['input']>
|
||||
total_closed_issues?: InputMaybe<Scalars['bigint']['input']>
|
||||
total_opened_issues?: InputMaybe<Scalars['bigint']['input']>
|
||||
}
|
||||
|
||||
/** columns and relationships of "gh_epic_issues" */
|
||||
export type Gh_Epic_Issues = {
|
||||
__typename?: 'gh_epic_issues'
|
||||
assignee?: Maybe<Scalars['String']['output']>
|
||||
author?: Maybe<Scalars['String']['output']>
|
||||
closed_at?: Maybe<Scalars['timestamptz']['output']>
|
||||
created_at?: Maybe<Scalars['timestamptz']['output']>
|
||||
epic_color?: Maybe<Scalars['String']['output']>
|
||||
epic_name?: Maybe<Scalars['String']['output']>
|
||||
issue_number?: Maybe<Scalars['bigint']['output']>
|
||||
issue_url?: Maybe<Scalars['String']['output']>
|
||||
labels?: Maybe<Scalars['String']['output']>
|
||||
repository?: Maybe<Scalars['String']['output']>
|
||||
stage?: Maybe<Scalars['String']['output']>
|
||||
title?: Maybe<Scalars['String']['output']>
|
||||
}
|
||||
|
||||
/** Boolean expression to filter rows from the table "gh_epic_issues". All fields are combined with a logical 'AND'. */
|
||||
export type Gh_Epic_Issues_Bool_Exp = {
|
||||
_and?: InputMaybe<Array<Gh_Epic_Issues_Bool_Exp>>
|
||||
_not?: InputMaybe<Gh_Epic_Issues_Bool_Exp>
|
||||
_or?: InputMaybe<Array<Gh_Epic_Issues_Bool_Exp>>
|
||||
assignee?: InputMaybe<String_Comparison_Exp>
|
||||
author?: InputMaybe<String_Comparison_Exp>
|
||||
closed_at?: InputMaybe<Timestamptz_Comparison_Exp>
|
||||
created_at?: InputMaybe<Timestamptz_Comparison_Exp>
|
||||
epic_color?: InputMaybe<String_Comparison_Exp>
|
||||
epic_name?: InputMaybe<String_Comparison_Exp>
|
||||
issue_number?: InputMaybe<Bigint_Comparison_Exp>
|
||||
issue_url?: InputMaybe<String_Comparison_Exp>
|
||||
labels?: InputMaybe<String_Comparison_Exp>
|
||||
repository?: InputMaybe<String_Comparison_Exp>
|
||||
stage?: InputMaybe<String_Comparison_Exp>
|
||||
title?: InputMaybe<String_Comparison_Exp>
|
||||
}
|
||||
|
||||
/** Ordering options when selecting data from "gh_epic_issues". */
|
||||
export type Gh_Epic_Issues_Order_By = {
|
||||
assignee?: InputMaybe<Order_By>
|
||||
author?: InputMaybe<Order_By>
|
||||
closed_at?: InputMaybe<Order_By>
|
||||
created_at?: InputMaybe<Order_By>
|
||||
epic_color?: InputMaybe<Order_By>
|
||||
epic_name?: InputMaybe<Order_By>
|
||||
issue_number?: InputMaybe<Order_By>
|
||||
issue_url?: InputMaybe<Order_By>
|
||||
labels?: InputMaybe<Order_By>
|
||||
repository?: InputMaybe<Order_By>
|
||||
stage?: InputMaybe<Order_By>
|
||||
title?: InputMaybe<Order_By>
|
||||
}
|
||||
|
||||
/** select columns of table "gh_epic_issues" */
|
||||
export enum Gh_Epic_Issues_Select_Column {
|
||||
/** column name */
|
||||
Assignee = 'assignee',
|
||||
/** column name */
|
||||
Author = 'author',
|
||||
/** column name */
|
||||
ClosedAt = 'closed_at',
|
||||
/** column name */
|
||||
CreatedAt = 'created_at',
|
||||
/** column name */
|
||||
EpicColor = 'epic_color',
|
||||
/** column name */
|
||||
EpicName = 'epic_name',
|
||||
/** column name */
|
||||
IssueNumber = 'issue_number',
|
||||
/** column name */
|
||||
IssueUrl = 'issue_url',
|
||||
/** column name */
|
||||
Labels = 'labels',
|
||||
/** column name */
|
||||
Repository = 'repository',
|
||||
/** column name */
|
||||
Stage = 'stage',
|
||||
/** column name */
|
||||
Title = 'title',
|
||||
}
|
||||
|
||||
/** Streaming cursor of the table "gh_epic_issues" */
|
||||
export type Gh_Epic_Issues_Stream_Cursor_Input = {
|
||||
/** Stream column input with initial value */
|
||||
initial_value: Gh_Epic_Issues_Stream_Cursor_Value_Input
|
||||
/** cursor ordering */
|
||||
ordering?: InputMaybe<Cursor_Ordering>
|
||||
}
|
||||
|
||||
/** Initial value of the column from where the streaming should start */
|
||||
export type Gh_Epic_Issues_Stream_Cursor_Value_Input = {
|
||||
assignee?: InputMaybe<Scalars['String']['input']>
|
||||
author?: InputMaybe<Scalars['String']['input']>
|
||||
closed_at?: InputMaybe<Scalars['timestamptz']['input']>
|
||||
created_at?: InputMaybe<Scalars['timestamptz']['input']>
|
||||
epic_color?: InputMaybe<Scalars['String']['input']>
|
||||
epic_name?: InputMaybe<Scalars['String']['input']>
|
||||
issue_number?: InputMaybe<Scalars['bigint']['input']>
|
||||
issue_url?: InputMaybe<Scalars['String']['input']>
|
||||
labels?: InputMaybe<Scalars['String']['input']>
|
||||
repository?: InputMaybe<Scalars['String']['input']>
|
||||
stage?: InputMaybe<Scalars['String']['input']>
|
||||
title?: InputMaybe<Scalars['String']['input']>
|
||||
}
|
||||
|
||||
/** columns and relationships of "gh_epics" */
|
||||
export type Gh_Epics = {
|
||||
__typename?: 'gh_epics'
|
||||
closed_count?: Maybe<Scalars['bigint']['output']>
|
||||
epic_color?: Maybe<Scalars['String']['output']>
|
||||
epic_description?: Maybe<Scalars['String']['output']>
|
||||
epic_name?: Maybe<Scalars['String']['output']>
|
||||
opened_count?: Maybe<Scalars['bigint']['output']>
|
||||
status?: Maybe<Scalars['String']['output']>
|
||||
total_count?: Maybe<Scalars['bigint']['output']>
|
||||
}
|
||||
|
||||
/** Boolean expression to filter rows from the table "gh_epics". All fields are combined with a logical 'AND'. */
|
||||
export type Gh_Epics_Bool_Exp = {
|
||||
_and?: InputMaybe<Array<Gh_Epics_Bool_Exp>>
|
||||
_not?: InputMaybe<Gh_Epics_Bool_Exp>
|
||||
_or?: InputMaybe<Array<Gh_Epics_Bool_Exp>>
|
||||
closed_count?: InputMaybe<Bigint_Comparison_Exp>
|
||||
epic_color?: InputMaybe<String_Comparison_Exp>
|
||||
epic_description?: InputMaybe<String_Comparison_Exp>
|
||||
epic_name?: InputMaybe<String_Comparison_Exp>
|
||||
opened_count?: InputMaybe<Bigint_Comparison_Exp>
|
||||
status?: InputMaybe<String_Comparison_Exp>
|
||||
total_count?: InputMaybe<Bigint_Comparison_Exp>
|
||||
}
|
||||
|
||||
/** Ordering options when selecting data from "gh_epics". */
|
||||
export type Gh_Epics_Order_By = {
|
||||
closed_count?: InputMaybe<Order_By>
|
||||
epic_color?: InputMaybe<Order_By>
|
||||
epic_description?: InputMaybe<Order_By>
|
||||
epic_name?: InputMaybe<Order_By>
|
||||
opened_count?: InputMaybe<Order_By>
|
||||
status?: InputMaybe<Order_By>
|
||||
total_count?: InputMaybe<Order_By>
|
||||
}
|
||||
|
||||
/** select columns of table "gh_epics" */
|
||||
export enum Gh_Epics_Select_Column {
|
||||
/** column name */
|
||||
ClosedCount = 'closed_count',
|
||||
/** column name */
|
||||
EpicColor = 'epic_color',
|
||||
/** column name */
|
||||
EpicDescription = 'epic_description',
|
||||
/** column name */
|
||||
EpicName = 'epic_name',
|
||||
/** column name */
|
||||
OpenedCount = 'opened_count',
|
||||
/** column name */
|
||||
Status = 'status',
|
||||
/** column name */
|
||||
TotalCount = 'total_count',
|
||||
}
|
||||
|
||||
/** Streaming cursor of the table "gh_epics" */
|
||||
export type Gh_Epics_Stream_Cursor_Input = {
|
||||
/** Stream column input with initial value */
|
||||
initial_value: Gh_Epics_Stream_Cursor_Value_Input
|
||||
/** cursor ordering */
|
||||
ordering?: InputMaybe<Cursor_Ordering>
|
||||
}
|
||||
|
||||
/** Initial value of the column from where the streaming should start */
|
||||
export type Gh_Epics_Stream_Cursor_Value_Input = {
|
||||
closed_count?: InputMaybe<Scalars['bigint']['input']>
|
||||
epic_color?: InputMaybe<Scalars['String']['input']>
|
||||
epic_description?: InputMaybe<Scalars['String']['input']>
|
||||
epic_name?: InputMaybe<Scalars['String']['input']>
|
||||
opened_count?: InputMaybe<Scalars['bigint']['input']>
|
||||
status?: InputMaybe<Scalars['String']['input']>
|
||||
total_count?: InputMaybe<Scalars['bigint']['input']>
|
||||
}
|
||||
|
||||
/** columns and relationships of "gh_issues" */
|
||||
export type Gh_Issues = {
|
||||
__typename?: 'gh_issues'
|
||||
assignee?: Maybe<Scalars['String']['output']>
|
||||
author?: Maybe<Scalars['String']['output']>
|
||||
closed_at?: Maybe<Scalars['timestamptz']['output']>
|
||||
created_at?: Maybe<Scalars['timestamptz']['output']>
|
||||
issue_number?: Maybe<Scalars['bigint']['output']>
|
||||
issue_url?: Maybe<Scalars['String']['output']>
|
||||
labels?: Maybe<Scalars['String']['output']>
|
||||
repository?: Maybe<Scalars['String']['output']>
|
||||
stage?: Maybe<Scalars['String']['output']>
|
||||
title?: Maybe<Scalars['String']['output']>
|
||||
}
|
||||
|
||||
/** Boolean expression to filter rows from the table "gh_issues". All fields are combined with a logical 'AND'. */
|
||||
export type Gh_Issues_Bool_Exp = {
|
||||
_and?: InputMaybe<Array<Gh_Issues_Bool_Exp>>
|
||||
_not?: InputMaybe<Gh_Issues_Bool_Exp>
|
||||
_or?: InputMaybe<Array<Gh_Issues_Bool_Exp>>
|
||||
assignee?: InputMaybe<String_Comparison_Exp>
|
||||
author?: InputMaybe<String_Comparison_Exp>
|
||||
closed_at?: InputMaybe<Timestamptz_Comparison_Exp>
|
||||
created_at?: InputMaybe<Timestamptz_Comparison_Exp>
|
||||
issue_number?: InputMaybe<Bigint_Comparison_Exp>
|
||||
issue_url?: InputMaybe<String_Comparison_Exp>
|
||||
labels?: InputMaybe<String_Comparison_Exp>
|
||||
repository?: InputMaybe<String_Comparison_Exp>
|
||||
stage?: InputMaybe<String_Comparison_Exp>
|
||||
title?: InputMaybe<String_Comparison_Exp>
|
||||
}
|
||||
|
||||
/** Ordering options when selecting data from "gh_issues". */
|
||||
export type Gh_Issues_Order_By = {
|
||||
assignee?: InputMaybe<Order_By>
|
||||
author?: InputMaybe<Order_By>
|
||||
closed_at?: InputMaybe<Order_By>
|
||||
created_at?: InputMaybe<Order_By>
|
||||
issue_number?: InputMaybe<Order_By>
|
||||
issue_url?: InputMaybe<Order_By>
|
||||
labels?: InputMaybe<Order_By>
|
||||
repository?: InputMaybe<Order_By>
|
||||
stage?: InputMaybe<Order_By>
|
||||
title?: InputMaybe<Order_By>
|
||||
}
|
||||
|
||||
/** select columns of table "gh_issues" */
|
||||
export enum Gh_Issues_Select_Column {
|
||||
/** column name */
|
||||
Assignee = 'assignee',
|
||||
/** column name */
|
||||
Author = 'author',
|
||||
/** column name */
|
||||
ClosedAt = 'closed_at',
|
||||
/** column name */
|
||||
CreatedAt = 'created_at',
|
||||
/** column name */
|
||||
IssueNumber = 'issue_number',
|
||||
/** column name */
|
||||
IssueUrl = 'issue_url',
|
||||
/** column name */
|
||||
Labels = 'labels',
|
||||
/** column name */
|
||||
Repository = 'repository',
|
||||
/** column name */
|
||||
Stage = 'stage',
|
||||
/** column name */
|
||||
Title = 'title',
|
||||
}
|
||||
|
||||
/** Streaming cursor of the table "gh_issues" */
|
||||
export type Gh_Issues_Stream_Cursor_Input = {
|
||||
/** Stream column input with initial value */
|
||||
initial_value: Gh_Issues_Stream_Cursor_Value_Input
|
||||
/** cursor ordering */
|
||||
ordering?: InputMaybe<Cursor_Ordering>
|
||||
}
|
||||
|
||||
/** Initial value of the column from where the streaming should start */
|
||||
export type Gh_Issues_Stream_Cursor_Value_Input = {
|
||||
assignee?: InputMaybe<Scalars['String']['input']>
|
||||
author?: InputMaybe<Scalars['String']['input']>
|
||||
closed_at?: InputMaybe<Scalars['timestamptz']['input']>
|
||||
created_at?: InputMaybe<Scalars['timestamptz']['input']>
|
||||
issue_number?: InputMaybe<Scalars['bigint']['input']>
|
||||
issue_url?: InputMaybe<Scalars['String']['input']>
|
||||
labels?: InputMaybe<Scalars['String']['input']>
|
||||
repository?: InputMaybe<Scalars['String']['input']>
|
||||
stage?: InputMaybe<Scalars['String']['input']>
|
||||
title?: InputMaybe<Scalars['String']['input']>
|
||||
}
|
||||
|
||||
/** columns and relationships of "gh_orphans" */
|
||||
export type Gh_Orphans = {
|
||||
__typename?: 'gh_orphans'
|
||||
assignee?: Maybe<Scalars['String']['output']>
|
||||
author?: Maybe<Scalars['String']['output']>
|
||||
closed_at?: Maybe<Scalars['timestamptz']['output']>
|
||||
created_at?: Maybe<Scalars['timestamptz']['output']>
|
||||
issue_number?: Maybe<Scalars['bigint']['output']>
|
||||
issue_url?: Maybe<Scalars['String']['output']>
|
||||
labels?: Maybe<Scalars['String']['output']>
|
||||
repository?: Maybe<Scalars['String']['output']>
|
||||
stage?: Maybe<Scalars['String']['output']>
|
||||
title?: Maybe<Scalars['String']['output']>
|
||||
}
|
||||
|
||||
/** Boolean expression to filter rows from the table "gh_orphans". All fields are combined with a logical 'AND'. */
|
||||
export type Gh_Orphans_Bool_Exp = {
|
||||
_and?: InputMaybe<Array<Gh_Orphans_Bool_Exp>>
|
||||
_not?: InputMaybe<Gh_Orphans_Bool_Exp>
|
||||
_or?: InputMaybe<Array<Gh_Orphans_Bool_Exp>>
|
||||
assignee?: InputMaybe<String_Comparison_Exp>
|
||||
author?: InputMaybe<String_Comparison_Exp>
|
||||
closed_at?: InputMaybe<Timestamptz_Comparison_Exp>
|
||||
created_at?: InputMaybe<Timestamptz_Comparison_Exp>
|
||||
issue_number?: InputMaybe<Bigint_Comparison_Exp>
|
||||
issue_url?: InputMaybe<String_Comparison_Exp>
|
||||
labels?: InputMaybe<String_Comparison_Exp>
|
||||
repository?: InputMaybe<String_Comparison_Exp>
|
||||
stage?: InputMaybe<String_Comparison_Exp>
|
||||
title?: InputMaybe<String_Comparison_Exp>
|
||||
}
|
||||
|
||||
/** Ordering options when selecting data from "gh_orphans". */
|
||||
export type Gh_Orphans_Order_By = {
|
||||
assignee?: InputMaybe<Order_By>
|
||||
author?: InputMaybe<Order_By>
|
||||
closed_at?: InputMaybe<Order_By>
|
||||
created_at?: InputMaybe<Order_By>
|
||||
issue_number?: InputMaybe<Order_By>
|
||||
issue_url?: InputMaybe<Order_By>
|
||||
labels?: InputMaybe<Order_By>
|
||||
repository?: InputMaybe<Order_By>
|
||||
stage?: InputMaybe<Order_By>
|
||||
title?: InputMaybe<Order_By>
|
||||
}
|
||||
|
||||
/** select columns of table "gh_orphans" */
|
||||
export enum Gh_Orphans_Select_Column {
|
||||
/** column name */
|
||||
Assignee = 'assignee',
|
||||
/** column name */
|
||||
Author = 'author',
|
||||
/** column name */
|
||||
ClosedAt = 'closed_at',
|
||||
/** column name */
|
||||
CreatedAt = 'created_at',
|
||||
/** column name */
|
||||
IssueNumber = 'issue_number',
|
||||
/** column name */
|
||||
IssueUrl = 'issue_url',
|
||||
/** column name */
|
||||
Labels = 'labels',
|
||||
/** column name */
|
||||
Repository = 'repository',
|
||||
/** column name */
|
||||
Stage = 'stage',
|
||||
/** column name */
|
||||
Title = 'title',
|
||||
}
|
||||
|
||||
/** Streaming cursor of the table "gh_orphans" */
|
||||
export type Gh_Orphans_Stream_Cursor_Input = {
|
||||
/** Stream column input with initial value */
|
||||
initial_value: Gh_Orphans_Stream_Cursor_Value_Input
|
||||
/** cursor ordering */
|
||||
ordering?: InputMaybe<Cursor_Ordering>
|
||||
}
|
||||
|
||||
/** Initial value of the column from where the streaming should start */
|
||||
export type Gh_Orphans_Stream_Cursor_Value_Input = {
|
||||
assignee?: InputMaybe<Scalars['String']['input']>
|
||||
author?: InputMaybe<Scalars['String']['input']>
|
||||
closed_at?: InputMaybe<Scalars['timestamptz']['input']>
|
||||
created_at?: InputMaybe<Scalars['timestamptz']['input']>
|
||||
issue_number?: InputMaybe<Scalars['bigint']['input']>
|
||||
issue_url?: InputMaybe<Scalars['String']['input']>
|
||||
labels?: InputMaybe<Scalars['String']['input']>
|
||||
repository?: InputMaybe<Scalars['String']['input']>
|
||||
stage?: InputMaybe<Scalars['String']['input']>
|
||||
title?: InputMaybe<Scalars['String']['input']>
|
||||
}
|
||||
|
||||
/** columns and relationships of "gh_repositories" */
|
||||
export type Gh_Repositories = {
|
||||
__typename?: 'gh_repositories'
|
||||
description?: Maybe<Scalars['String']['output']>
|
||||
full_name?: Maybe<Scalars['String']['output']>
|
||||
name?: Maybe<Scalars['String']['output']>
|
||||
open_issues_count?: Maybe<Scalars['bigint']['output']>
|
||||
stargazers_count?: Maybe<Scalars['bigint']['output']>
|
||||
visibility?: Maybe<Scalars['String']['output']>
|
||||
}
|
||||
|
||||
/** Boolean expression to filter rows from the table "gh_repositories". All fields are combined with a logical 'AND'. */
|
||||
export type Gh_Repositories_Bool_Exp = {
|
||||
_and?: InputMaybe<Array<Gh_Repositories_Bool_Exp>>
|
||||
_not?: InputMaybe<Gh_Repositories_Bool_Exp>
|
||||
_or?: InputMaybe<Array<Gh_Repositories_Bool_Exp>>
|
||||
description?: InputMaybe<String_Comparison_Exp>
|
||||
full_name?: InputMaybe<String_Comparison_Exp>
|
||||
name?: InputMaybe<String_Comparison_Exp>
|
||||
open_issues_count?: InputMaybe<Bigint_Comparison_Exp>
|
||||
stargazers_count?: InputMaybe<Bigint_Comparison_Exp>
|
||||
visibility?: InputMaybe<String_Comparison_Exp>
|
||||
}
|
||||
|
||||
/** Ordering options when selecting data from "gh_repositories". */
|
||||
export type Gh_Repositories_Order_By = {
|
||||
description?: InputMaybe<Order_By>
|
||||
full_name?: InputMaybe<Order_By>
|
||||
name?: InputMaybe<Order_By>
|
||||
open_issues_count?: InputMaybe<Order_By>
|
||||
stargazers_count?: InputMaybe<Order_By>
|
||||
visibility?: InputMaybe<Order_By>
|
||||
}
|
||||
|
||||
/** select columns of table "gh_repositories" */
|
||||
export enum Gh_Repositories_Select_Column {
|
||||
/** column name */
|
||||
Description = 'description',
|
||||
/** column name */
|
||||
FullName = 'full_name',
|
||||
/** column name */
|
||||
Name = 'name',
|
||||
/** column name */
|
||||
OpenIssuesCount = 'open_issues_count',
|
||||
/** column name */
|
||||
StargazersCount = 'stargazers_count',
|
||||
/** column name */
|
||||
Visibility = 'visibility',
|
||||
}
|
||||
|
||||
/** Streaming cursor of the table "gh_repositories" */
|
||||
export type Gh_Repositories_Stream_Cursor_Input = {
|
||||
/** Stream column input with initial value */
|
||||
initial_value: Gh_Repositories_Stream_Cursor_Value_Input
|
||||
/** cursor ordering */
|
||||
ordering?: InputMaybe<Cursor_Ordering>
|
||||
}
|
||||
|
||||
/** Initial value of the column from where the streaming should start */
|
||||
export type Gh_Repositories_Stream_Cursor_Value_Input = {
|
||||
description?: InputMaybe<Scalars['String']['input']>
|
||||
full_name?: InputMaybe<Scalars['String']['input']>
|
||||
name?: InputMaybe<Scalars['String']['input']>
|
||||
open_issues_count?: InputMaybe<Scalars['bigint']['input']>
|
||||
stargazers_count?: InputMaybe<Scalars['bigint']['input']>
|
||||
visibility?: InputMaybe<Scalars['String']['input']>
|
||||
}
|
||||
|
||||
/** column ordering options */
|
||||
export enum Order_By {
|
||||
/** in ascending order, nulls last */
|
||||
Asc = 'asc',
|
||||
/** in ascending order, nulls first */
|
||||
AscNullsFirst = 'asc_nulls_first',
|
||||
/** in ascending order, nulls last */
|
||||
AscNullsLast = 'asc_nulls_last',
|
||||
/** in descending order, nulls first */
|
||||
Desc = 'desc',
|
||||
/** in descending order, nulls first */
|
||||
DescNullsFirst = 'desc_nulls_first',
|
||||
/** in descending order, nulls last */
|
||||
DescNullsLast = 'desc_nulls_last',
|
||||
}
|
||||
|
||||
export type Query_Root = {
|
||||
__typename?: 'query_root'
|
||||
/** fetch data from the table: "gh_burnup" */
|
||||
gh_burnup: Array<Gh_Burnup>
|
||||
/** fetch data from the table: "gh_epic_issues" */
|
||||
gh_epic_issues: Array<Gh_Epic_Issues>
|
||||
/** fetch data from the table: "gh_epics" */
|
||||
gh_epics: Array<Gh_Epics>
|
||||
/** fetch data from the table: "gh_issues" */
|
||||
gh_issues: Array<Gh_Issues>
|
||||
/** fetch data from the table: "gh_orphans" */
|
||||
gh_orphans: Array<Gh_Orphans>
|
||||
/** fetch data from the table: "gh_repositories" */
|
||||
gh_repositories: Array<Gh_Repositories>
|
||||
}
|
||||
|
||||
export type Query_RootGh_BurnupArgs = {
|
||||
distinct_on?: InputMaybe<Array<Gh_Burnup_Select_Column>>
|
||||
limit?: InputMaybe<Scalars['Int']['input']>
|
||||
offset?: InputMaybe<Scalars['Int']['input']>
|
||||
order_by?: InputMaybe<Array<Gh_Burnup_Order_By>>
|
||||
where?: InputMaybe<Gh_Burnup_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Query_RootGh_Epic_IssuesArgs = {
|
||||
distinct_on?: InputMaybe<Array<Gh_Epic_Issues_Select_Column>>
|
||||
limit?: InputMaybe<Scalars['Int']['input']>
|
||||
offset?: InputMaybe<Scalars['Int']['input']>
|
||||
order_by?: InputMaybe<Array<Gh_Epic_Issues_Order_By>>
|
||||
where?: InputMaybe<Gh_Epic_Issues_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Query_RootGh_EpicsArgs = {
|
||||
distinct_on?: InputMaybe<Array<Gh_Epics_Select_Column>>
|
||||
limit?: InputMaybe<Scalars['Int']['input']>
|
||||
offset?: InputMaybe<Scalars['Int']['input']>
|
||||
order_by?: InputMaybe<Array<Gh_Epics_Order_By>>
|
||||
where?: InputMaybe<Gh_Epics_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Query_RootGh_IssuesArgs = {
|
||||
distinct_on?: InputMaybe<Array<Gh_Issues_Select_Column>>
|
||||
limit?: InputMaybe<Scalars['Int']['input']>
|
||||
offset?: InputMaybe<Scalars['Int']['input']>
|
||||
order_by?: InputMaybe<Array<Gh_Issues_Order_By>>
|
||||
where?: InputMaybe<Gh_Issues_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Query_RootGh_OrphansArgs = {
|
||||
distinct_on?: InputMaybe<Array<Gh_Orphans_Select_Column>>
|
||||
limit?: InputMaybe<Scalars['Int']['input']>
|
||||
offset?: InputMaybe<Scalars['Int']['input']>
|
||||
order_by?: InputMaybe<Array<Gh_Orphans_Order_By>>
|
||||
where?: InputMaybe<Gh_Orphans_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Query_RootGh_RepositoriesArgs = {
|
||||
distinct_on?: InputMaybe<Array<Gh_Repositories_Select_Column>>
|
||||
limit?: InputMaybe<Scalars['Int']['input']>
|
||||
offset?: InputMaybe<Scalars['Int']['input']>
|
||||
order_by?: InputMaybe<Array<Gh_Repositories_Order_By>>
|
||||
where?: InputMaybe<Gh_Repositories_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Subscription_Root = {
|
||||
__typename?: 'subscription_root'
|
||||
/** fetch data from the table: "gh_burnup" */
|
||||
gh_burnup: Array<Gh_Burnup>
|
||||
/** fetch data from the table in a streaming manner: "gh_burnup" */
|
||||
gh_burnup_stream: Array<Gh_Burnup>
|
||||
/** fetch data from the table: "gh_epic_issues" */
|
||||
gh_epic_issues: Array<Gh_Epic_Issues>
|
||||
/** fetch data from the table in a streaming manner: "gh_epic_issues" */
|
||||
gh_epic_issues_stream: Array<Gh_Epic_Issues>
|
||||
/** fetch data from the table: "gh_epics" */
|
||||
gh_epics: Array<Gh_Epics>
|
||||
/** fetch data from the table in a streaming manner: "gh_epics" */
|
||||
gh_epics_stream: Array<Gh_Epics>
|
||||
/** fetch data from the table: "gh_issues" */
|
||||
gh_issues: Array<Gh_Issues>
|
||||
/** fetch data from the table in a streaming manner: "gh_issues" */
|
||||
gh_issues_stream: Array<Gh_Issues>
|
||||
/** fetch data from the table: "gh_orphans" */
|
||||
gh_orphans: Array<Gh_Orphans>
|
||||
/** fetch data from the table in a streaming manner: "gh_orphans" */
|
||||
gh_orphans_stream: Array<Gh_Orphans>
|
||||
/** fetch data from the table: "gh_repositories" */
|
||||
gh_repositories: Array<Gh_Repositories>
|
||||
/** fetch data from the table in a streaming manner: "gh_repositories" */
|
||||
gh_repositories_stream: Array<Gh_Repositories>
|
||||
}
|
||||
|
||||
export type Subscription_RootGh_BurnupArgs = {
|
||||
distinct_on?: InputMaybe<Array<Gh_Burnup_Select_Column>>
|
||||
limit?: InputMaybe<Scalars['Int']['input']>
|
||||
offset?: InputMaybe<Scalars['Int']['input']>
|
||||
order_by?: InputMaybe<Array<Gh_Burnup_Order_By>>
|
||||
where?: InputMaybe<Gh_Burnup_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Subscription_RootGh_Burnup_StreamArgs = {
|
||||
batch_size: Scalars['Int']['input']
|
||||
cursor: Array<InputMaybe<Gh_Burnup_Stream_Cursor_Input>>
|
||||
where?: InputMaybe<Gh_Burnup_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Subscription_RootGh_Epic_IssuesArgs = {
|
||||
distinct_on?: InputMaybe<Array<Gh_Epic_Issues_Select_Column>>
|
||||
limit?: InputMaybe<Scalars['Int']['input']>
|
||||
offset?: InputMaybe<Scalars['Int']['input']>
|
||||
order_by?: InputMaybe<Array<Gh_Epic_Issues_Order_By>>
|
||||
where?: InputMaybe<Gh_Epic_Issues_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Subscription_RootGh_Epic_Issues_StreamArgs = {
|
||||
batch_size: Scalars['Int']['input']
|
||||
cursor: Array<InputMaybe<Gh_Epic_Issues_Stream_Cursor_Input>>
|
||||
where?: InputMaybe<Gh_Epic_Issues_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Subscription_RootGh_EpicsArgs = {
|
||||
distinct_on?: InputMaybe<Array<Gh_Epics_Select_Column>>
|
||||
limit?: InputMaybe<Scalars['Int']['input']>
|
||||
offset?: InputMaybe<Scalars['Int']['input']>
|
||||
order_by?: InputMaybe<Array<Gh_Epics_Order_By>>
|
||||
where?: InputMaybe<Gh_Epics_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Subscription_RootGh_Epics_StreamArgs = {
|
||||
batch_size: Scalars['Int']['input']
|
||||
cursor: Array<InputMaybe<Gh_Epics_Stream_Cursor_Input>>
|
||||
where?: InputMaybe<Gh_Epics_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Subscription_RootGh_IssuesArgs = {
|
||||
distinct_on?: InputMaybe<Array<Gh_Issues_Select_Column>>
|
||||
limit?: InputMaybe<Scalars['Int']['input']>
|
||||
offset?: InputMaybe<Scalars['Int']['input']>
|
||||
order_by?: InputMaybe<Array<Gh_Issues_Order_By>>
|
||||
where?: InputMaybe<Gh_Issues_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Subscription_RootGh_Issues_StreamArgs = {
|
||||
batch_size: Scalars['Int']['input']
|
||||
cursor: Array<InputMaybe<Gh_Issues_Stream_Cursor_Input>>
|
||||
where?: InputMaybe<Gh_Issues_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Subscription_RootGh_OrphansArgs = {
|
||||
distinct_on?: InputMaybe<Array<Gh_Orphans_Select_Column>>
|
||||
limit?: InputMaybe<Scalars['Int']['input']>
|
||||
offset?: InputMaybe<Scalars['Int']['input']>
|
||||
order_by?: InputMaybe<Array<Gh_Orphans_Order_By>>
|
||||
where?: InputMaybe<Gh_Orphans_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Subscription_RootGh_Orphans_StreamArgs = {
|
||||
batch_size: Scalars['Int']['input']
|
||||
cursor: Array<InputMaybe<Gh_Orphans_Stream_Cursor_Input>>
|
||||
where?: InputMaybe<Gh_Orphans_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Subscription_RootGh_RepositoriesArgs = {
|
||||
distinct_on?: InputMaybe<Array<Gh_Repositories_Select_Column>>
|
||||
limit?: InputMaybe<Scalars['Int']['input']>
|
||||
offset?: InputMaybe<Scalars['Int']['input']>
|
||||
order_by?: InputMaybe<Array<Gh_Repositories_Order_By>>
|
||||
where?: InputMaybe<Gh_Repositories_Bool_Exp>
|
||||
}
|
||||
|
||||
export type Subscription_RootGh_Repositories_StreamArgs = {
|
||||
batch_size: Scalars['Int']['input']
|
||||
cursor: Array<InputMaybe<Gh_Repositories_Stream_Cursor_Input>>
|
||||
where?: InputMaybe<Gh_Repositories_Bool_Exp>
|
||||
}
|
||||
|
||||
/** Boolean expression to compare columns of type "timestamptz". All fields are combined with logical 'AND'. */
|
||||
export type Timestamptz_Comparison_Exp = {
|
||||
_eq?: InputMaybe<Scalars['timestamptz']['input']>
|
||||
_gt?: InputMaybe<Scalars['timestamptz']['input']>
|
||||
_gte?: InputMaybe<Scalars['timestamptz']['input']>
|
||||
_in?: InputMaybe<Array<Scalars['timestamptz']['input']>>
|
||||
_is_null?: InputMaybe<Scalars['Boolean']['input']>
|
||||
_lt?: InputMaybe<Scalars['timestamptz']['input']>
|
||||
_lte?: InputMaybe<Scalars['timestamptz']['input']>
|
||||
_neq?: InputMaybe<Scalars['timestamptz']['input']>
|
||||
_nin?: InputMaybe<Array<Scalars['timestamptz']['input']>>
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { api, GRAPHQL_ENDPOINT } from './api'
|
|
@ -0,0 +1,14 @@
|
|||
export const catchApiError = (
|
||||
error: GraphqlApiError,
|
||||
callback: (codes: string[]) => void
|
||||
) => {
|
||||
const codes: string[] = []
|
||||
|
||||
error.response?.errors?.forEach(error => {
|
||||
if (error.extensions?.code) {
|
||||
codes.push(error.extensions?.code)
|
||||
}
|
||||
})
|
||||
|
||||
return callback(codes)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
export async function fetchQueryFromHasura(query: string) {
|
||||
const response = await fetch('https://hasura.infra.status.im/v1/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch data from Hasura.')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
|
@ -1,82 +1,352 @@
|
|||
import { Breadcrumbs } from '@/components/breadcrumbs'
|
||||
import { EpicOverview } from '@/components/epic-overview'
|
||||
import { TableIssues } from '@/components/table-issues'
|
||||
import { InsightsLayout } from '@/layouts/insights-layout'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { epics } from '.'
|
||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { differenceInCalendarDays, format } from 'date-fns'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { Breadcrumbs, EpicOverview, TableIssues } from '@/components'
|
||||
import { useDebounce } from '@/hooks/use-debounce'
|
||||
import { useIntersectionObserver } from '@/hooks/use-intersection-observer'
|
||||
import { InsightsLayout } from '@/layouts/insights-layout'
|
||||
import {
|
||||
GET_EPIC_ISSUES_COUNT,
|
||||
GET_EPIC_LINKS,
|
||||
GET_FILTERS_WITH_EPIC,
|
||||
GET_ISSUES_BY_EPIC,
|
||||
} from '@/lib/burnup'
|
||||
import { api } from '@/lib/graphql'
|
||||
import {
|
||||
useGetBurnupQuery,
|
||||
useGetEpicIssuesCountQuery,
|
||||
useGetFiltersWithEpicQuery,
|
||||
} from '@/lib/graphql/generated/hooks'
|
||||
import { Order_By } from '@/lib/graphql/generated/schemas'
|
||||
|
||||
import type { BreadcrumbsProps } from '@/components/breadcrumbs'
|
||||
import type { GetStaticPaths, GetStaticProps, Page } from 'next'
|
||||
import type {
|
||||
GetBurnupQuery,
|
||||
GetEpicIssuesCountQuery,
|
||||
GetEpicIssuesCountQueryVariables,
|
||||
GetEpicMenuLinksQuery,
|
||||
GetEpicMenuLinksQueryVariables,
|
||||
GetFiltersWithEpicQuery,
|
||||
GetFiltersWithEpicQueryVariables,
|
||||
GetIssuesByEpicQuery,
|
||||
GetIssuesByEpicQueryVariables,
|
||||
} from '@/lib/graphql/generated/operations'
|
||||
import type { DateRange } from '@status-im/components/src/calendar/calendar'
|
||||
import type { GetServerSidePropsContext, Page } from 'next'
|
||||
|
||||
type Params = { epic: string }
|
||||
|
||||
type Epic = (typeof epics)[number]
|
||||
|
||||
export const getStaticPaths: GetStaticPaths<Params> = async () => {
|
||||
const paths = epics.map(epic => ({
|
||||
params: { epic: epic.id },
|
||||
}))
|
||||
|
||||
return { paths, fallback: false }
|
||||
type Epic = {
|
||||
title: string
|
||||
color: `#${string}`
|
||||
description: string
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<Props, Params> = async context => {
|
||||
const epic = epics.find(epic => epic.id === context.params!.epic)!
|
||||
type Props = {
|
||||
links: string[]
|
||||
epic: Epic
|
||||
breadcrumbs: BreadcrumbsProps['items']
|
||||
count: GetEpicIssuesCountQuery
|
||||
filters: GetFiltersWithEpicQuery
|
||||
initialDates: {
|
||||
from: string
|
||||
to: string
|
||||
}
|
||||
}
|
||||
|
||||
if (!epic) {
|
||||
const LIMIT = 10
|
||||
|
||||
const EpicsDetailPage: Page<Props> = props => {
|
||||
const router = useRouter()
|
||||
|
||||
const { epic: epicName } = router.query
|
||||
|
||||
const { epic, breadcrumbs, links, initialDates } = props
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'open' | 'closed'>('open')
|
||||
const [selectedAuthors, setSelectedAuthors] = useState<string[]>([])
|
||||
const [selectedAssignees, setSelectedAssignees] = useState<string[]>([])
|
||||
const [selectedRepos, setSelectedRepos] = useState<string[]>([])
|
||||
const [orderByValue, setOrderByValue] = useState<Order_By>(Order_By.Desc)
|
||||
|
||||
const [searchFilter, setSearchFilter] = useState<string>('')
|
||||
const debouncedSearchFilter = useDebounce<string>(searchFilter)
|
||||
|
||||
const [selectedDates, setSelectedDates] = useState<DateRange>()
|
||||
const [burnupData, setBurnupData] = useState<GetBurnupQuery['gh_burnup']>()
|
||||
|
||||
const { isFetching: isLoadingBurnup } = useGetBurnupQuery(
|
||||
{
|
||||
epicNames: epicName,
|
||||
from: selectedDates?.from || initialDates.from,
|
||||
to: selectedDates?.to || initialDates.to,
|
||||
},
|
||||
{
|
||||
// Prevent animation if we go out of the page
|
||||
refetchOnWindowFocus: false,
|
||||
onSuccess: data => {
|
||||
const differenceBetweenSelectedDates = differenceInCalendarDays(
|
||||
selectedDates?.to || new Date(),
|
||||
selectedDates?.from || new Date()
|
||||
)
|
||||
|
||||
const rate = 50 // 1 sample per 50 days
|
||||
|
||||
let samplingRate = rate // Use the default rate as the initial value
|
||||
|
||||
if (differenceBetweenSelectedDates > 0) {
|
||||
// Calculate the total number of data points within the selected date range
|
||||
const totalDataPoints = data?.gh_burnup.length || 0
|
||||
|
||||
// Calculate the desired number of data points based on the sampling rate
|
||||
const desiredDataPoints = Math.ceil(totalDataPoints / rate)
|
||||
|
||||
// Calculate the actual sampling rate based on the desired number of data points
|
||||
samplingRate = Math.max(
|
||||
1,
|
||||
Math.floor(totalDataPoints / desiredDataPoints)
|
||||
)
|
||||
}
|
||||
|
||||
// Downsampling the burnup data
|
||||
const downsampledData: GetBurnupQuery['gh_burnup'] = []
|
||||
|
||||
if (data?.gh_burnup.length > 0) {
|
||||
data?.gh_burnup.forEach((dataPoint, index) => {
|
||||
if (index % samplingRate === 0) {
|
||||
downsampledData.push(dataPoint)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
selectedDates?.from &&
|
||||
selectedDates?.to &&
|
||||
data?.gh_burnup.length === 0
|
||||
) {
|
||||
downsampledData.push({
|
||||
date_field: selectedDates.from,
|
||||
total_closed_issues: 0,
|
||||
total_opened_issues: 0,
|
||||
})
|
||||
downsampledData.push({
|
||||
date_field: selectedDates.to,
|
||||
total_closed_issues: 0,
|
||||
total_opened_issues: 0,
|
||||
})
|
||||
}
|
||||
|
||||
setBurnupData(downsampledData)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const { data: dataCounter } = useGetEpicIssuesCountQuery({
|
||||
where: {
|
||||
epic_name: { _eq: epicName as string },
|
||||
...(selectedAuthors.length > 0 && {
|
||||
author: { _in: selectedAuthors },
|
||||
}),
|
||||
...(selectedAssignees.length > 0 && {
|
||||
assignee: { _in: selectedAssignees },
|
||||
}),
|
||||
...(selectedRepos.length > 0 && {
|
||||
repository: { _in: selectedRepos },
|
||||
}),
|
||||
title: { _ilike: `%${debouncedSearchFilter}%` },
|
||||
},
|
||||
})
|
||||
|
||||
const count = {
|
||||
total: dataCounter?.gh_epic_issues.length,
|
||||
closed: dataCounter?.gh_epic_issues.filter(issue => issue.closed_at).length,
|
||||
open: dataCounter?.gh_epic_issues.filter(issue => !issue.closed_at).length,
|
||||
}
|
||||
|
||||
const { data, isFetching, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
useInfiniteQuery(
|
||||
[
|
||||
'getIssuesByEpic',
|
||||
epicName,
|
||||
activeTab,
|
||||
selectedAssignees,
|
||||
selectedRepos,
|
||||
selectedAuthors,
|
||||
orderByValue,
|
||||
debouncedSearchFilter,
|
||||
],
|
||||
async ({ pageParam = 0 }) => {
|
||||
const result = await api<
|
||||
GetIssuesByEpicQuery,
|
||||
GetIssuesByEpicQueryVariables
|
||||
>(GET_ISSUES_BY_EPIC, {
|
||||
where: {
|
||||
epic_name: { _eq: epicName as string },
|
||||
stage: { _eq: activeTab },
|
||||
...(selectedAuthors.length > 0 && {
|
||||
author: { _in: selectedAuthors },
|
||||
}),
|
||||
...(selectedAssignees.length > 0 && {
|
||||
assignee: { _in: selectedAssignees },
|
||||
}),
|
||||
...(selectedRepos.length > 0 && {
|
||||
repository: { _in: selectedRepos },
|
||||
}),
|
||||
title: { _ilike: `%${debouncedSearchFilter}%` },
|
||||
},
|
||||
limit: LIMIT,
|
||||
offset: pageParam,
|
||||
orderBy: orderByValue,
|
||||
})
|
||||
|
||||
return result?.gh_epic_issues || []
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage, pages) => {
|
||||
if (lastPage.length < LIMIT) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return pages.length * LIMIT
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const { data: filters } = useGetFiltersWithEpicQuery(
|
||||
{
|
||||
epicName: epicName as string,
|
||||
},
|
||||
{
|
||||
initialData: props.filters,
|
||||
}
|
||||
)
|
||||
|
||||
const issues = useMemo(
|
||||
() => data?.pages.flatMap(page => page) || [],
|
||||
[data?.pages]
|
||||
)
|
||||
|
||||
const endOfPageRef = useRef<HTMLDivElement | null>(null)
|
||||
const entry = useIntersectionObserver(endOfPageRef, {
|
||||
rootMargin: '800px',
|
||||
})
|
||||
const isVisible = !!entry?.isIntersecting
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible && !isFetchingNextPage && hasNextPage) {
|
||||
fetchNextPage()
|
||||
}
|
||||
}, [fetchNextPage, hasNextPage, isFetchingNextPage, isVisible])
|
||||
|
||||
return (
|
||||
<InsightsLayout links={links}>
|
||||
<Breadcrumbs items={breadcrumbs} />
|
||||
<div className="px-10 py-6">
|
||||
<EpicOverview
|
||||
title={epic.title}
|
||||
description={epic.description}
|
||||
color={epic.color}
|
||||
fullscreen
|
||||
burnup={burnupData}
|
||||
isLoading={isLoadingBurnup}
|
||||
selectedDates={selectedDates}
|
||||
setSelectedDates={setSelectedDates}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-neutral-10 px-10 py-6">
|
||||
<TableIssues
|
||||
data={issues}
|
||||
count={count}
|
||||
isLoading={isFetchingNextPage || isFetching || hasNextPage}
|
||||
filters={filters}
|
||||
handleTabChange={setActiveTab}
|
||||
activeTab={activeTab}
|
||||
selectedAuthors={selectedAuthors}
|
||||
handleSelectedAuthors={setSelectedAuthors}
|
||||
selectedAssignees={selectedAssignees}
|
||||
handleSelectedAssignees={setSelectedAssignees}
|
||||
selectedRepos={selectedRepos}
|
||||
handleSelectedRepos={setSelectedRepos}
|
||||
orderByValue={orderByValue}
|
||||
handleOrderByValue={setOrderByValue}
|
||||
searchFilterValue={searchFilter}
|
||||
handleSearchFilter={setSearchFilter}
|
||||
/>
|
||||
<div ref={endOfPageRef} />
|
||||
</div>
|
||||
</InsightsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default EpicsDetailPage
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { epic } = context.query
|
||||
|
||||
const links = await api<
|
||||
GetEpicMenuLinksQuery,
|
||||
GetEpicMenuLinksQueryVariables
|
||||
>(GET_EPIC_LINKS)
|
||||
|
||||
const epicLinkExists = links?.gh_epics.find(link => link.epic_name === epic)
|
||||
|
||||
if (!epicLinkExists) {
|
||||
return {
|
||||
// notFound: true,
|
||||
redirect: { destination: '/insights/epics', permanent: false },
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: get initial date based on the epic when available
|
||||
const initialDates = {
|
||||
from: '2017-01-01',
|
||||
to: format(new Date(), 'yyyy-MM-dd'),
|
||||
}
|
||||
|
||||
const [resultIssuesCount, resultFilters] = await Promise.all([
|
||||
api<GetEpicIssuesCountQuery, GetEpicIssuesCountQueryVariables>(
|
||||
GET_EPIC_ISSUES_COUNT,
|
||||
{
|
||||
where: {
|
||||
epic_name: { _eq: String(epic) },
|
||||
},
|
||||
}
|
||||
),
|
||||
api<GetFiltersWithEpicQuery, GetFiltersWithEpicQueryVariables>(
|
||||
GET_FILTERS_WITH_EPIC,
|
||||
{
|
||||
epicName: String(epic),
|
||||
}
|
||||
),
|
||||
])
|
||||
|
||||
return {
|
||||
props: {
|
||||
epic,
|
||||
links:
|
||||
links?.gh_epics
|
||||
.filter(epic => epic.status === 'In Progress')
|
||||
.map(epic => epic.epic_name) || [],
|
||||
count: resultIssuesCount?.gh_epic_issues,
|
||||
filters: resultFilters || [],
|
||||
initialDates,
|
||||
epic: {
|
||||
title: String(epic),
|
||||
description: epicLinkExists?.epic_description || '',
|
||||
color: epicLinkExists.epic_color
|
||||
? `#${epicLinkExists.epic_color}`
|
||||
: '#4360df',
|
||||
},
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: 'Epics',
|
||||
href: '/insights/epics',
|
||||
},
|
||||
{
|
||||
label: epic.title,
|
||||
href: `/insights/epics/${epic.id}`,
|
||||
label: epic,
|
||||
href: `/insights/epics/${epic}`,
|
||||
},
|
||||
],
|
||||
key: epic,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Props = {
|
||||
epic: Epic
|
||||
breadcrumbs: BreadcrumbsProps['items']
|
||||
}
|
||||
|
||||
const EpicsDetailPage: Page<Props> = props => {
|
||||
const { epic, breadcrumbs } = props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Breadcrumbs items={breadcrumbs} />
|
||||
|
||||
<div className="px-10 py-6">
|
||||
<EpicOverview
|
||||
title={epic.title}
|
||||
description={epic.description}
|
||||
fullscreen
|
||||
/>
|
||||
</div>
|
||||
<div className="border-b border-neutral-10 px-10 py-6">
|
||||
<div role="separator" className="-mx-6 my-6 h-px bg-neutral-10" />
|
||||
|
||||
<TableIssues />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
EpicsDetailPage.getLayout = function getLayout(page) {
|
||||
return <InsightsLayout>{page}</InsightsLayout>
|
||||
}
|
||||
|
||||
export default EpicsDetailPage
|
||||
|
|
|
@ -1,71 +1,310 @@
|
|||
import { useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { IconButton, Shadow, Tag, Text } from '@status-im/components'
|
||||
import {
|
||||
DoneIcon,
|
||||
NotStartedIcon,
|
||||
OpenIcon,
|
||||
SearchIcon,
|
||||
SortIcon,
|
||||
} from '@status-im/icons'
|
||||
import { Input, Shadow, Tag, Text } from '@status-im/components'
|
||||
import { DoneIcon, OpenIcon, SearchIcon } from '@status-im/icons'
|
||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { differenceInCalendarDays, format } from 'date-fns'
|
||||
|
||||
import { Loading } from '@/components/chart/components'
|
||||
import { DatePicker } from '@/components/datepicker/datepicker'
|
||||
import { EpicOverview } from '@/components/epic-overview'
|
||||
import { Empty } from '@/components/table-issues/empty'
|
||||
import { DropdownSort } from '@/components/table-issues/filters'
|
||||
import { useDebounce } from '@/hooks/use-debounce'
|
||||
import { useIntersectionObserver } from '@/hooks/use-intersection-observer'
|
||||
import { RenderIfVisible } from '@/hooks/use-render-if-visible'
|
||||
import { InsightsLayout } from '@/layouts/insights-layout'
|
||||
import { GET_BURNUP, GET_EPIC_LINKS } from '@/lib/burnup'
|
||||
import { api } from '@/lib/graphql'
|
||||
import { Order_By } from '@/lib/graphql/generated/schemas'
|
||||
|
||||
import type { DropdownSortProps } from '@/components/table-issues/filters/dropdown-sort'
|
||||
import type {
|
||||
GetBurnupQuery,
|
||||
GetBurnupQueryVariables,
|
||||
GetEpicMenuLinksQuery,
|
||||
GetEpicMenuLinksQueryVariables,
|
||||
} from '@/lib/graphql/generated/operations'
|
||||
import type { DateRange } from '@status-im/components/src/calendar/calendar'
|
||||
import type { Page } from 'next'
|
||||
|
||||
export const epics = [
|
||||
type Props = {
|
||||
links: string[]
|
||||
}
|
||||
|
||||
const LIMIT = 3
|
||||
|
||||
const sortOptions: DropdownSortProps['data'] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Communities protocol',
|
||||
description: 'Support Encrypted Communities',
|
||||
id: Order_By.Asc,
|
||||
name: 'Ascending',
|
||||
},
|
||||
{
|
||||
id: '5155',
|
||||
title: 'Keycard',
|
||||
description:
|
||||
'Detecting keycard reader removal for the beginning of each flow',
|
||||
id: Order_By.Desc,
|
||||
name: 'Descending',
|
||||
},
|
||||
]
|
||||
|
||||
const EpicsPage: Page = () => {
|
||||
const EpicsPage: Page<Props> = props => {
|
||||
const { links } = props
|
||||
const [selectedFilters, setSelectedFilters] = useState<string[]>([
|
||||
'In Progress',
|
||||
])
|
||||
|
||||
const [orderByValue, setOrderByValue] = useState<Order_By>(Order_By.Desc)
|
||||
|
||||
const [searchFilter, setSearchFilter] = useState<string>('')
|
||||
const debouncedSearchFilter = useDebounce<string>(searchFilter)
|
||||
|
||||
const [selectedDates, setSelectedDates] = useState<DateRange>()
|
||||
|
||||
const handleFilter = (filter: string) => {
|
||||
if (selectedFilters.includes(filter)) {
|
||||
setSelectedFilters(selectedFilters.filter(f => f !== filter))
|
||||
} else {
|
||||
setSelectedFilters([...selectedFilters, filter])
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
data,
|
||||
isFetchedAfterMount,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
} = useInfiniteQuery(
|
||||
[
|
||||
'getEpicsOverview',
|
||||
orderByValue,
|
||||
debouncedSearchFilter,
|
||||
selectedDates,
|
||||
selectedFilters,
|
||||
],
|
||||
async ({ pageParam = 0 }) => {
|
||||
const result = await api<
|
||||
GetEpicMenuLinksQuery,
|
||||
GetEpicMenuLinksQueryVariables
|
||||
>(GET_EPIC_LINKS, {
|
||||
where: {
|
||||
status: {
|
||||
_in:
|
||||
selectedFilters.length > 0
|
||||
? selectedFilters
|
||||
: ['In Progress', 'Closed'],
|
||||
},
|
||||
epic_name: { _ilike: `%${debouncedSearchFilter}%` },
|
||||
},
|
||||
limit: LIMIT,
|
||||
offset: pageParam,
|
||||
orderBy: {
|
||||
epic_name: orderByValue || Order_By.Asc,
|
||||
},
|
||||
})
|
||||
|
||||
const burnup = await api<GetBurnupQuery, GetBurnupQueryVariables>(
|
||||
GET_BURNUP,
|
||||
{
|
||||
epicNames: result?.gh_epics.map(epic => epic.epic_name || '') || [],
|
||||
from: selectedDates?.from || '2018-05-01',
|
||||
to: selectedDates?.to || format(new Date(), 'yyyy-MM-dd'),
|
||||
}
|
||||
)
|
||||
|
||||
const differenceBetweenSelectedDates = differenceInCalendarDays(
|
||||
selectedDates?.to || new Date(),
|
||||
selectedDates?.from || new Date()
|
||||
)
|
||||
|
||||
const rate = 50 // 1 sample per 50 days
|
||||
|
||||
let samplingRate = rate // Use the default rate as the initial value
|
||||
|
||||
if (differenceBetweenSelectedDates > 0) {
|
||||
// Calculate the ratio between the difference in days and the desired sampling rate
|
||||
const ratio = differenceBetweenSelectedDates / rate
|
||||
|
||||
// Calculate the sampling rate based on the ratio
|
||||
samplingRate = Math.ceil(1 / ratio)
|
||||
}
|
||||
|
||||
// Downsampling the burnup data
|
||||
const downsampledData: GetBurnupQuery['gh_burnup'] = []
|
||||
|
||||
burnup?.gh_burnup.forEach((dataPoint, index) => {
|
||||
if (index % samplingRate === 0) {
|
||||
downsampledData.push(dataPoint)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
result?.gh_epics.map(epic => {
|
||||
const burnupData = downsampledData?.filter(
|
||||
b => b.epic_name === epic.epic_name
|
||||
)
|
||||
|
||||
return {
|
||||
title: epic.epic_name,
|
||||
description: epic.epic_description,
|
||||
color: epic.epic_color ? `#${epic.epic_color}` : '#4360df',
|
||||
burnup:
|
||||
burnupData.length > 0
|
||||
? burnupData
|
||||
: [
|
||||
{
|
||||
date: selectedDates?.from,
|
||||
total_closed_issues: 0,
|
||||
total_open_issues: 0,
|
||||
},
|
||||
{
|
||||
date: selectedDates?.to,
|
||||
total_closed_issues: 0,
|
||||
total_open_issues: 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}) || []
|
||||
)
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage, pages) => {
|
||||
if (lastPage.length < LIMIT) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return pages.length * LIMIT
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const epics = useMemo(() => {
|
||||
return data?.pages.flatMap(page => page) || []
|
||||
}, [data])
|
||||
|
||||
const endOfPageRef = useRef<HTMLDivElement | null>(null)
|
||||
const entry = useIntersectionObserver(endOfPageRef, {})
|
||||
const isVisible = !!entry?.isIntersecting
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible && !isFetchingNextPage && hasNextPage) {
|
||||
fetchNextPage()
|
||||
}
|
||||
}, [fetchNextPage, hasNextPage, isFetchingNextPage, isVisible])
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-10">
|
||||
<Text size={27} weight="semibold">
|
||||
Epics
|
||||
</Text>
|
||||
<InsightsLayout links={links}>
|
||||
<div className="flex h-full flex-1 flex-col justify-between">
|
||||
<div className="space-y-4 p-10">
|
||||
<Text size={27} weight="semibold">
|
||||
Epics
|
||||
</Text>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Tag size={32} label="In Progress" icon={OpenIcon} selected />
|
||||
<Tag size={32} label="Closed" icon={DoneIcon} />
|
||||
<Tag size={32} label="Not Started" icon={NotStartedIcon} />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<IconButton variant="outline" icon={<SearchIcon size={20} />} />
|
||||
<IconButton variant="outline" icon={<SortIcon size={20} />} />
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Tag
|
||||
size={32}
|
||||
label="In Progress"
|
||||
icon={OpenIcon}
|
||||
selected={selectedFilters.includes('In Progress')}
|
||||
onPress={() => handleFilter('In Progress')}
|
||||
/>
|
||||
<Tag
|
||||
size={32}
|
||||
label="Closed"
|
||||
icon={DoneIcon}
|
||||
selected={selectedFilters.includes('Closed')}
|
||||
onPress={() => handleFilter('Closed')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
direction="rtl"
|
||||
variant="retractable"
|
||||
placeholder="Search"
|
||||
icon={<SearchIcon size={20} />}
|
||||
size={32}
|
||||
value={searchFilter}
|
||||
onChangeText={setSearchFilter}
|
||||
/>
|
||||
<DropdownSort
|
||||
data={sortOptions}
|
||||
onOrderByValueChange={setOrderByValue}
|
||||
orderByValue={orderByValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{epics.map(epic => (
|
||||
<RenderIfVisible
|
||||
key={epic.title}
|
||||
defaultHeight={398}
|
||||
placeholderComponent={
|
||||
<Shadow
|
||||
variant="$2"
|
||||
className="h-[398px] rounded-2xl px-4 py-3"
|
||||
>
|
||||
<div className="flex h-full flex-col p-5">
|
||||
<Loading />
|
||||
</div>
|
||||
</Shadow>
|
||||
}
|
||||
>
|
||||
<Shadow variant="$2" className="rounded-2xl px-4 py-3">
|
||||
<EpicOverview
|
||||
title={epic.title || ''}
|
||||
description={epic.description || ''}
|
||||
selectedDates={selectedDates}
|
||||
setSelectedDates={setSelectedDates}
|
||||
showPicker={false}
|
||||
color={epic.color as `#${string}`}
|
||||
burnup={epic.burnup}
|
||||
isLoading={!isFetchedAfterMount}
|
||||
/>
|
||||
</Shadow>
|
||||
</RenderIfVisible>
|
||||
))}
|
||||
<div ref={endOfPageRef} />
|
||||
|
||||
{(isFetching || isFetchingNextPage || hasNextPage) && (
|
||||
<Shadow
|
||||
variant="$2"
|
||||
className="mt-[-1rem] h-[398px] rounded-2xl px-4 py-3"
|
||||
>
|
||||
<div className="flex h-full flex-col p-5">
|
||||
<Loading />
|
||||
</div>
|
||||
</Shadow>
|
||||
)}
|
||||
|
||||
{!isFetching && !isFetchingNextPage && epics.length === 0 && (
|
||||
<div className="pt-4">
|
||||
<Empty />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DatePicker selected={selectedDates} onSelect={setSelectedDates} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{epics.map(epic => (
|
||||
<Shadow key={epic.id} variant="$2" className="rounded-2xl px-4 py-3">
|
||||
<EpicOverview title={epic.title} description={epic.description} />
|
||||
</Shadow>
|
||||
))}
|
||||
</div>
|
||||
<DatePicker selected={selectedDates} onSelect={setSelectedDates} />
|
||||
</div>
|
||||
</InsightsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
EpicsPage.getLayout = function getLayout(page) {
|
||||
return <InsightsLayout>{page}</InsightsLayout>
|
||||
export async function getServerSideProps() {
|
||||
const epics = await api<
|
||||
GetEpicMenuLinksQuery,
|
||||
GetEpicMenuLinksQueryVariables
|
||||
>(GET_EPIC_LINKS)
|
||||
|
||||
return {
|
||||
props: {
|
||||
links:
|
||||
epics?.gh_epics
|
||||
.filter(epic => epic.status === 'In Progress')
|
||||
.map(epic => epic.epic_name) || [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default EpicsPage
|
||||
|
|
|
@ -1,24 +1,205 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { Text } from '@status-im/components'
|
||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||
|
||||
import { TableIssues } from '@/components'
|
||||
import { useDebounce } from '@/hooks/use-debounce'
|
||||
import { useIntersectionObserver } from '@/hooks/use-intersection-observer'
|
||||
import { InsightsLayout } from '@/layouts/insights-layout'
|
||||
import {
|
||||
GET_EPIC_LINKS,
|
||||
GET_FILTERS_FOR_ORPHANS,
|
||||
GET_ORPHANS,
|
||||
GET_ORPHANS_COUNT,
|
||||
} from '@/lib/burnup'
|
||||
import { api } from '@/lib/graphql'
|
||||
import { useGetOrphansCountQuery } from '@/lib/graphql/generated/hooks'
|
||||
import { Order_By } from '@/lib/graphql/generated/schemas'
|
||||
|
||||
import type {
|
||||
GetEpicMenuLinksQuery,
|
||||
GetEpicMenuLinksQueryVariables,
|
||||
GetFiltersForOrphansQuery,
|
||||
GetFiltersForOrphansQueryVariables,
|
||||
GetOrphansCountQuery,
|
||||
GetOrphansCountQueryVariables,
|
||||
GetOrphansQuery,
|
||||
GetOrphansQueryVariables,
|
||||
} from '@/lib/graphql/generated/operations'
|
||||
import type { Page } from 'next'
|
||||
|
||||
const OrphansPage: Page = () => {
|
||||
return (
|
||||
<div className="space-y-6 p-10">
|
||||
<Text size={27} weight="semibold">
|
||||
Orphans
|
||||
</Text>
|
||||
type Props = {
|
||||
orphans: GetOrphansQuery
|
||||
filters: GetFiltersForOrphansQuery
|
||||
links: string[]
|
||||
}
|
||||
|
||||
<TableIssues />
|
||||
</div>
|
||||
const LIMIT = 50
|
||||
|
||||
const OrphansPage: Page<Props> = props => {
|
||||
const { links } = props
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'open' | 'closed'>('open')
|
||||
const [selectedAuthors, setSelectedAuthors] = useState<string[]>([])
|
||||
const [selectedAssignees, setSelectedAssignees] = useState<string[]>([])
|
||||
const [selectedRepos, setSelectedRepos] = useState<string[]>([])
|
||||
const [orderByValue, setOrderByValue] = useState<Order_By>(Order_By.Desc)
|
||||
|
||||
const [searchFilter, setSearchFilter] = useState<string>('')
|
||||
const debouncedSearchFilter = useDebounce<string>(searchFilter)
|
||||
|
||||
const { data, isFetching, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
useInfiniteQuery(
|
||||
[
|
||||
'getOrphans',
|
||||
activeTab,
|
||||
selectedAssignees,
|
||||
selectedRepos,
|
||||
selectedAuthors,
|
||||
orderByValue,
|
||||
debouncedSearchFilter,
|
||||
],
|
||||
async ({ pageParam = 0 }) => {
|
||||
const result = await api<GetOrphansQuery, GetOrphansQueryVariables>(
|
||||
GET_ORPHANS,
|
||||
{
|
||||
where: {
|
||||
stage: { _eq: activeTab },
|
||||
...(selectedAuthors.length > 0 && {
|
||||
author: { _in: selectedAuthors },
|
||||
}),
|
||||
...(selectedAssignees.length > 0 && {
|
||||
assignee: { _in: selectedAssignees },
|
||||
}),
|
||||
...(selectedRepos.length > 0 && {
|
||||
repository: { _in: selectedRepos },
|
||||
}),
|
||||
title: { _ilike: `%${debouncedSearchFilter}%` },
|
||||
},
|
||||
limit: LIMIT,
|
||||
offset: pageParam,
|
||||
orderBy: orderByValue,
|
||||
}
|
||||
)
|
||||
|
||||
return result?.gh_orphans || []
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage, pages) => {
|
||||
if (lastPage.length < LIMIT) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return pages.length * LIMIT
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const { data: dataCounter } = useGetOrphansCountQuery({
|
||||
where: {
|
||||
...(selectedAuthors.length > 0 && {
|
||||
author: { _in: selectedAuthors },
|
||||
}),
|
||||
...(selectedAssignees.length > 0 && {
|
||||
assignee: { _in: selectedAssignees },
|
||||
}),
|
||||
...(selectedRepos.length > 0 && {
|
||||
repository: { _in: selectedRepos },
|
||||
}),
|
||||
title: { _ilike: `%${debouncedSearchFilter}%` },
|
||||
},
|
||||
})
|
||||
|
||||
const count = {
|
||||
total: dataCounter?.gh_orphans.length,
|
||||
closed: dataCounter?.gh_orphans.filter(issue => issue.closed_at).length,
|
||||
open: dataCounter?.gh_orphans.filter(issue => !issue.closed_at).length,
|
||||
}
|
||||
|
||||
const orphans = useMemo(
|
||||
() => data?.pages.flatMap(page => page) || [],
|
||||
[data?.pages]
|
||||
)
|
||||
|
||||
const endOfPageRef = useRef<HTMLDivElement | null>(null)
|
||||
const entry = useIntersectionObserver(endOfPageRef, {
|
||||
rootMargin: '800px',
|
||||
threshold: 0,
|
||||
})
|
||||
const isVisible = !!entry?.isIntersecting
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible && !isFetchingNextPage && hasNextPage) {
|
||||
fetchNextPage()
|
||||
}
|
||||
}, [fetchNextPage, hasNextPage, isFetchingNextPage, isVisible])
|
||||
|
||||
return (
|
||||
<InsightsLayout links={links}>
|
||||
<div className="space-y-6 scroll-smooth p-10">
|
||||
<Text size={27} weight="semibold">
|
||||
Orphans
|
||||
</Text>
|
||||
<TableIssues
|
||||
data={orphans}
|
||||
count={count}
|
||||
isLoading={isFetchingNextPage || isFetching || hasNextPage}
|
||||
filters={props.filters}
|
||||
handleTabChange={setActiveTab}
|
||||
activeTab={activeTab}
|
||||
selectedAuthors={selectedAuthors}
|
||||
handleSelectedAuthors={setSelectedAuthors}
|
||||
selectedAssignees={selectedAssignees}
|
||||
handleSelectedAssignees={setSelectedAssignees}
|
||||
selectedRepos={selectedRepos}
|
||||
handleSelectedRepos={setSelectedRepos}
|
||||
orderByValue={orderByValue}
|
||||
handleOrderByValue={setOrderByValue}
|
||||
searchFilterValue={searchFilter}
|
||||
handleSearchFilter={setSearchFilter}
|
||||
/>
|
||||
<div ref={endOfPageRef} />
|
||||
</div>
|
||||
</InsightsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
OrphansPage.getLayout = function getLayout(page) {
|
||||
return <InsightsLayout>{page}</InsightsLayout>
|
||||
export async function getServerSideProps() {
|
||||
const [links, repos, filters, resultIssuesCount] = await Promise.all([
|
||||
api<GetEpicMenuLinksQuery, GetEpicMenuLinksQueryVariables>(GET_EPIC_LINKS),
|
||||
api<GetOrphansQuery, GetOrphansQueryVariables>(GET_ORPHANS, {
|
||||
where: {
|
||||
stage: { _eq: 'open' },
|
||||
},
|
||||
limit: LIMIT,
|
||||
offset: 0,
|
||||
orderBy: Order_By.Desc,
|
||||
}),
|
||||
api<GetFiltersForOrphansQuery, GetFiltersForOrphansQueryVariables>(
|
||||
GET_FILTERS_FOR_ORPHANS
|
||||
),
|
||||
api<GetOrphansCountQuery, GetOrphansCountQueryVariables>(
|
||||
GET_ORPHANS_COUNT,
|
||||
{
|
||||
where: {
|
||||
stage: { _eq: 'open' },
|
||||
},
|
||||
}
|
||||
),
|
||||
])
|
||||
|
||||
return {
|
||||
props: {
|
||||
links:
|
||||
links?.gh_epics
|
||||
.filter(epic => epic.status === 'In Progress')
|
||||
.map(epic => epic.epic_name) || [],
|
||||
repos: repos.gh_orphans || [],
|
||||
filters,
|
||||
count: resultIssuesCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default OrphansPage
|
||||
|
|
|
@ -1,129 +0,0 @@
|
|||
import { Shadow, Text } from '@status-im/components'
|
||||
import { OpenIcon, UnlockedIcon } from '@status-im/icons'
|
||||
|
||||
import { Link } from '@/components/link'
|
||||
import { InsightsLayout } from '@/layouts/insights-layout'
|
||||
|
||||
import type { Page } from 'next'
|
||||
|
||||
const repos = [
|
||||
{
|
||||
name: 'status-web',
|
||||
description: 'a free (libre) open source, mobile OS for Ethereum.',
|
||||
issues: 10,
|
||||
stars: 5,
|
||||
},
|
||||
{
|
||||
name: 'status-mobile',
|
||||
description: 'a free (libre) open source, mobile OS for Ethereum.',
|
||||
issues: 10,
|
||||
stars: 5,
|
||||
},
|
||||
{
|
||||
name: 'status-desktop',
|
||||
description: 'a free (libre) open source, mobile OS for Ethereum.',
|
||||
issues: 10,
|
||||
stars: 5,
|
||||
},
|
||||
{
|
||||
name: 'status-go',
|
||||
description: 'a free (libre) open source, mobile OS for Ethereum.',
|
||||
issues: 10,
|
||||
stars: 5,
|
||||
},
|
||||
{
|
||||
name: 'nim-waku',
|
||||
description: 'a free (libre) open source, mobile OS for Ethereum.',
|
||||
issues: 10,
|
||||
stars: 5,
|
||||
},
|
||||
{
|
||||
name: 'go-waku',
|
||||
description: 'a free (libre) open source, mobile OS for Ethereum.',
|
||||
issues: 10,
|
||||
stars: 5,
|
||||
},
|
||||
{
|
||||
name: 'js-waku',
|
||||
description: 'a free (libre) open source, mobile OS for Ethereum.',
|
||||
issues: 10,
|
||||
stars: 5,
|
||||
},
|
||||
{
|
||||
name: 'nimbus-eth2',
|
||||
description: 'a free (libre) open source, mobile OS for Ethereum.',
|
||||
issues: 10,
|
||||
stars: 5,
|
||||
},
|
||||
{
|
||||
name: 'help.status.im',
|
||||
description: 'help.status.im',
|
||||
issues: 10,
|
||||
stars: 5,
|
||||
},
|
||||
]
|
||||
|
||||
const ReposPage: Page = () => {
|
||||
return (
|
||||
<div className="p-10">
|
||||
<div className="mb-6">
|
||||
<Text size={27} weight="semibold">
|
||||
Repos
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-5">
|
||||
{repos.map(repo => (
|
||||
<Shadow key={repo.name}>
|
||||
<Link
|
||||
href={`https://github.com/status-im/${repo.name}`}
|
||||
className="flex h-[124px] flex-col justify-between rounded-2xl border border-neutral-10 px-4 py-3 transition-colors duration-200 hover:border-neutral-40"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<Text size={15} weight="semibold">
|
||||
{repo.name}
|
||||
</Text>
|
||||
<Text size={15} color="$neutral-50">
|
||||
{repo.description}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<div className="flex items-center">
|
||||
<div className="pr-1">
|
||||
<UnlockedIcon size={12} color="$neutral-50" />
|
||||
</div>
|
||||
<Text size={13} weight="medium" color="$neutral-100">
|
||||
Public
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="pr-1">
|
||||
<OpenIcon size={12} color="$neutral-50" />
|
||||
</div>
|
||||
<Text size={13} weight="medium" color="$neutral-100">
|
||||
42 issues
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="pr-1">
|
||||
<OpenIcon size={12} color="$neutral-50" />
|
||||
</div>
|
||||
<Text size={13} weight="medium" color="$neutral-100">
|
||||
32
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Shadow>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ReposPage.getLayout = function getLayout(page) {
|
||||
return <InsightsLayout>{page}</InsightsLayout>
|
||||
}
|
||||
|
||||
export default ReposPage
|
|
@ -0,0 +1,139 @@
|
|||
import { Shadow, Text } from '@status-im/components'
|
||||
import { OpenIcon, UnlockedIcon } from '@status-im/icons'
|
||||
|
||||
import { Link } from '@/components/link'
|
||||
import { LoadingSkeleton } from '@/components/repos/loading-skeleton'
|
||||
import { InsightsLayout } from '@/layouts/insights-layout'
|
||||
import { GET_EPIC_LINKS, GET_REPOS } from '@/lib/burnup'
|
||||
import { api } from '@/lib/graphql'
|
||||
import { useGetRepositoriesQuery } from '@/lib/graphql/generated/hooks'
|
||||
|
||||
import type {
|
||||
GetEpicMenuLinksQuery,
|
||||
GetEpicMenuLinksQueryVariables,
|
||||
GetRepositoriesQuery,
|
||||
GetRepositoriesQueryVariables,
|
||||
} from '@/lib/graphql/generated/operations'
|
||||
import type { Page } from 'next'
|
||||
|
||||
type Props = {
|
||||
repos: GetRepositoriesQuery
|
||||
links: string[]
|
||||
}
|
||||
|
||||
const capitalizeString = (word: string) =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
|
||||
const ReposPage: Page<Props> = props => {
|
||||
const { data, isLoading } = useGetRepositoriesQuery(undefined, {
|
||||
initialData: props.repos,
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSkeleton />
|
||||
}
|
||||
|
||||
const repos = data?.gh_repositories || []
|
||||
|
||||
return (
|
||||
<InsightsLayout links={props.links}>
|
||||
<div className="p-10">
|
||||
<div className="mb-6">
|
||||
<Text size={27} weight="semibold">
|
||||
Repos
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-5">
|
||||
{repos.map(repo => (
|
||||
<Shadow key={repo.name}>
|
||||
<Link
|
||||
href={`https://github.com/status-im/${repo.name}`}
|
||||
className="flex h-[124px] flex-col justify-between rounded-2xl border border-neutral-10 px-4 py-3 transition-colors duration-200 hover:border-neutral-40"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<Text size={15} weight="semibold">
|
||||
{repo.name}
|
||||
</Text>
|
||||
<Text size={15} color="$neutral-50" truncate>
|
||||
{repo.description}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<div className="flex items-center">
|
||||
<div className="pr-1">
|
||||
<UnlockedIcon size={12} color="$neutral-50" />
|
||||
</div>
|
||||
<Text size={13} weight="medium" color="$neutral-100">
|
||||
{repo.visibility && capitalizeString(repo.visibility)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="pr-1">
|
||||
<OpenIcon size={12} color="$neutral-50" />
|
||||
</div>
|
||||
<Text size={13} weight="medium" color="$neutral-100">
|
||||
{repo.open_issues_count} Issues
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="pr-1">
|
||||
{/* TODO Change the correct star icon when available */}
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_3507_951)">
|
||||
<path
|
||||
d="M6.00004 1C6.33333 1 7.66667 4.32285 7.66667 4.32285C7.66667 4.32285 11 4.32285 11 4.7382C11 5.15356 8.08333 7.23033 8.08333 7.23033C8.08333 7.23033 9.66667 10.6363 9.33333 10.9685C9 11.3008 6.00004 8.89176 6.00004 8.89176C6.00004 8.89176 3 11.3008 2.66667 10.9685C2.33333 10.6363 3.91667 7.23033 3.91667 7.23033C3.91667 7.23033 1 5.15356 1 4.7382C1 4.32285 4.33333 4.32285 4.33333 4.32285C4.33333 4.32285 5.66674 1 6.00004 1Z"
|
||||
stroke="#647084"
|
||||
stroke-width="1.1"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3507_951">
|
||||
<rect width="12" height="12" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<Text size={13} weight="medium" color="$neutral-100">
|
||||
{repo.stargazers_count} Stars
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Shadow>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</InsightsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps() {
|
||||
const result = await api<GetRepositoriesQuery, GetRepositoriesQueryVariables>(
|
||||
GET_REPOS,
|
||||
undefined
|
||||
)
|
||||
|
||||
const links = await api<
|
||||
GetEpicMenuLinksQuery,
|
||||
GetEpicMenuLinksQueryVariables
|
||||
>(GET_EPIC_LINKS)
|
||||
|
||||
return {
|
||||
props: {
|
||||
links:
|
||||
links?.gh_epics
|
||||
.filter(epic => epic.status === 'In Progress')
|
||||
.map(epic => epic.epic_name) || [],
|
||||
repos: result.gh_repositories || [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default ReposPage
|
Loading…
Reference in New Issue