[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",
|
"lint": "next lint",
|
||||||
"typecheck": "contentlayer build && tsc",
|
"typecheck": "contentlayer build && tsc",
|
||||||
"clean": "rimraf .next .tamagui .turbo .vercel/output node_modules",
|
"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": {
|
"dependencies": {
|
||||||
"@mdx-js/react": "^2.3.0",
|
"@mdx-js/react": "^2.3.0",
|
||||||
|
@ -48,6 +50,12 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@achingbrain/ssdp": "^4.0.1",
|
"@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",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@tamagui/next-plugin": "1.36.4",
|
"@tamagui/next-plugin": "1.36.4",
|
||||||
"@types/d3-array": "^3.0.4",
|
"@types/d3-array": "^3.0.4",
|
||||||
|
@ -76,7 +84,6 @@
|
||||||
"typescript": "^5.0.3",
|
"typescript": "^5.0.3",
|
||||||
"unist-util-visit": "^4.1.2",
|
"unist-util-visit": "^4.1.2",
|
||||||
"@types/tryghost__content-api": "^1.3.11",
|
"@types/tryghost__content-api": "^1.3.11",
|
||||||
"@status-im/eslint-config": "*",
|
|
||||||
"rehype-parse": "^8.0.4",
|
"rehype-parse": "^8.0.4",
|
||||||
"rehype-react": "^7.2.0",
|
"rehype-react": "^7.2.0",
|
||||||
"rehype-stringify": "^9.0.3",
|
"rehype-stringify": "^9.0.3",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Stack } from '@tamagui/core'
|
import { Stack } from '@tamagui/core'
|
||||||
import { ParentSize } from '@visx/responsive'
|
import { ParentSize } from '@visx/responsive'
|
||||||
|
|
||||||
import { ChartComponent, Empty, Loading } from './components'
|
import { ChartComponent, Loading } from './components'
|
||||||
|
|
||||||
type DayType = {
|
type DayType = {
|
||||||
date: string
|
date: string
|
||||||
|
@ -26,14 +26,6 @@ const Chart = (props: Props) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.length) {
|
|
||||||
return (
|
|
||||||
<Stack width="100%" height={rest.height}>
|
|
||||||
<Empty />
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ParentSize style={{ maxHeight: 326 }}>
|
<ParentSize style={{ maxHeight: 326 }}>
|
||||||
{({ width }) => {
|
{({ width }) => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { animated } from '@react-spring/web'
|
import { animated } from '@react-spring/web'
|
||||||
import { curveMonotoneX } from '@visx/curve'
|
import { curveBasis } from '@visx/curve'
|
||||||
import { LinearGradient } from '@visx/gradient'
|
import { LinearGradient } from '@visx/gradient'
|
||||||
import { AreaClosed } from '@visx/shape'
|
import { AreaClosed } from '@visx/shape'
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ const Areas = (props: Props) => {
|
||||||
}}
|
}}
|
||||||
yScale={yScale}
|
yScale={yScale}
|
||||||
fill="url(#gradient)"
|
fill="url(#gradient)"
|
||||||
curve={curveMonotoneX}
|
curve={curveBasis}
|
||||||
style={{ ...clipPathAnimation, zIndex: 10 }}
|
style={{ ...clipPathAnimation, zIndex: 10 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ const Areas = (props: Props) => {
|
||||||
}}
|
}}
|
||||||
yScale={yScale}
|
yScale={yScale}
|
||||||
fill="url(#gradient-open)"
|
fill="url(#gradient-open)"
|
||||||
curve={curveMonotoneX}
|
curve={curveBasis}
|
||||||
style={{ ...clipPathAnimation, zIndex: 10 }}
|
style={{ ...clipPathAnimation, zIndex: 10 }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -63,7 +63,10 @@ const ChartComponent = (props: Props): JSX.Element => {
|
||||||
|
|
||||||
const yScale = scaleLinear({
|
const yScale = scaleLinear({
|
||||||
domain: [0, max(totalIssues) || 0],
|
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,
|
nice: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -91,7 +91,7 @@ const ChartTooltip = (props: Props) => {
|
||||||
<DoneIcon size={16} color="$neutral-40" />
|
<DoneIcon size={16} color="$neutral-40" />
|
||||||
<Stack px={4}>
|
<Stack px={4}>
|
||||||
<Text size={13} weight="medium">
|
<Text size={13} weight="medium">
|
||||||
{tooltipData.closedIssues} closes
|
{tooltipData.closedIssues} closed
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack
|
<Stack
|
||||||
|
|
|
@ -7,7 +7,7 @@ const Empty = () => {
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
<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 flex h-full w-full items-center justify-center">
|
||||||
<div className="relative z-10 flex flex-col 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" />
|
<div className="pb-3" />
|
||||||
<Text size={15} weight="semibold">
|
<Text size={15} weight="semibold">
|
||||||
No results found
|
No results found
|
||||||
|
@ -17,7 +17,7 @@ const Empty = () => {
|
||||||
Try adjusting your search or filter to find what you’re looking for.
|
Try adjusting your search or filter to find what you’re looking for.
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</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 />
|
<LineA />
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute left-0 top-28 w-full opacity-60">
|
<div className="absolute left-0 top-28 w-full opacity-60">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { animated } from '@react-spring/web'
|
import { animated } from '@react-spring/web'
|
||||||
import { curveMonotoneX } from '@visx/curve'
|
import { curveBasis } from '@visx/curve'
|
||||||
import { LinePath } from '@visx/shape'
|
import { LinePath } from '@visx/shape'
|
||||||
|
|
||||||
import { colors } from './chart-component'
|
import { colors } from './chart-component'
|
||||||
|
@ -47,7 +47,7 @@ const Lines = (props: Props) => {
|
||||||
}}
|
}}
|
||||||
stroke={colors.total}
|
stroke={colors.total}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
curve={curveMonotoneX}
|
curve={curveBasis}
|
||||||
style={drawingLineStyle}
|
style={drawingLineStyle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ const Lines = (props: Props) => {
|
||||||
}}
|
}}
|
||||||
stroke={colors.closed}
|
stroke={colors.closed}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
curve={curveMonotoneX}
|
curve={curveBasis}
|
||||||
style={drawingLineStyle}
|
style={drawingLineStyle}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -98,9 +98,10 @@ const useChartTooltip = (props: Props) => {
|
||||||
const totalIssues = getTotalIssues(d)
|
const totalIssues = getTotalIssues(d)
|
||||||
const openIssues = totalIssues - closedIssues
|
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({
|
showTooltip({
|
||||||
tooltipData: {
|
tooltipData: {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Calendar } from '@status-im/components/src/calendar/calendar'
|
import { Calendar } from '@status-im/components/src/calendar/calendar'
|
||||||
import { Popover } from '@status-im/components/src/popover'
|
import { Popover } from '@status-im/components/src/popover'
|
||||||
import { EditIcon } from '@status-im/icons'
|
import { EditIcon } from '@status-im/icons'
|
||||||
|
import { addDays } from 'date-fns'
|
||||||
|
|
||||||
import { formatDate } from '../chart/utils/format-time'
|
import { formatDate } from '../chart/utils/format-time'
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ const DatePicker = (props: Props) => {
|
||||||
selected={selected}
|
selected={selected}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
fixedWeeks
|
fixedWeeks
|
||||||
|
disabled={{ from: addDays(new Date(), 1) }}
|
||||||
/>
|
/>
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
|
@ -1,112 +1,101 @@
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import { Tag, Text } from '@status-im/components'
|
import { Tag, Text } from '@status-im/components'
|
||||||
import { OpenIcon } from '@status-im/icons'
|
import { OpenIcon } from '@status-im/icons'
|
||||||
|
|
||||||
import { Chart } from './chart/chart'
|
import { Chart } from './chart/chart'
|
||||||
|
import { DatePicker } from './datepicker/datepicker'
|
||||||
|
|
||||||
const DATA = [
|
import type { GetBurnupQuery } from '@/lib/graphql/generated/operations'
|
||||||
{
|
import type { DateRange } from '@status-im/components/src/calendar/calendar'
|
||||||
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,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description?: string
|
||||||
|
color?: `#${string}`
|
||||||
fullscreen?: boolean
|
fullscreen?: boolean
|
||||||
|
isLoading?: boolean
|
||||||
|
burnup?: GetBurnupQuery['gh_burnup']
|
||||||
|
selectedDates?: DateRange
|
||||||
|
setSelectedDates: (date?: DateRange) => void
|
||||||
|
showPicker?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EpicOverview = (props: Props) => {
|
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 filteredData = burnup?.reduce(
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
(
|
||||||
useEffect(() => {
|
accumulator: {
|
||||||
const timeout = setTimeout(() => {
|
date: string
|
||||||
setIsLoading(false)
|
open_issues: number
|
||||||
}, 2000)
|
closed_issues: number
|
||||||
|
}[],
|
||||||
return () => {
|
current: GetBurnupQuery['gh_burnup'][0]
|
||||||
clearTimeout(timeout)
|
) => {
|
||||||
|
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 accumulator
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
|
<div className="flex justify-between">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Text size={fullscreen ? 27 : 19} weight="semibold">
|
<Text size={fullscreen ? 27 : 19} weight="semibold">
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<OpenIcon size={20} />
|
<OpenIcon size={20} />
|
||||||
</div>
|
</div>
|
||||||
|
{showPicker && (
|
||||||
|
<DatePicker selected={selectedDates} onSelect={setSelectedDates} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{Boolean(description) && (
|
||||||
<Text size={fullscreen ? 19 : 15} color="$neutral-50">
|
<Text size={fullscreen ? 19 : 15} color="$neutral-50">
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex py-3">
|
<div className="flex py-3">
|
||||||
<Tag size={24} label="E:CommunitiesProtocol" color="$blue-50" />
|
<Tag size={24} label={title} color={color} />
|
||||||
</div>
|
</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">
|
<div className="flex gap-1">
|
||||||
<Tag size={24} label="Communities" color="#FF7D46" icon="🧙♂️" />
|
<Tag size={24} label="Communities" color="#FF7D46" icon="🧙♂️" />
|
||||||
<Tag size={24} label="Wallet" color="#7140FD" icon="🎎" />
|
<Tag size={24} label="Wallet" color="#7140FD" icon="🎎" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Tag size={24} label="M:0.11.0" color="$danger-50" />
|
<Tag size={24} label="M:0.11.0" color="$danger-50" />
|
||||||
<Tag size={24} label="M:0.12.0" color="$success-50" />
|
<Tag size={24} label="M:0.12.0" color="$success-50" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
|
export { Breadcrumbs } from './breadcrumbs'
|
||||||
export { EpicOverview } from './epic-overview'
|
export { EpicOverview } from './epic-overview'
|
||||||
|
export { SidebarMenu } from './sidebar-menu'
|
||||||
export { TableIssues } from './table-issues/table-issues'
|
export { TableIssues } from './table-issues/table-issues'
|
||||||
|
|
|
@ -25,11 +25,10 @@ const FloatingMenu = (): JSX.Element => {
|
||||||
useLockScroll(open)
|
useLockScroll(open)
|
||||||
|
|
||||||
useScroll({
|
useScroll({
|
||||||
onChange: ({ value: { scrollYProgress } }) => {
|
onChange: ({ value: { scrollY } }) => {
|
||||||
const isMenuOpen = openRef.current
|
const isMenuOpen = openRef.current
|
||||||
const isScrollingUp = scrollYProgress < scrollYRef.current
|
const isScrollingUp = scrollY < scrollYRef.current
|
||||||
const detectionPoint = scrollYProgress > 0.005
|
const detectionPoint = scrollY > 0.005
|
||||||
|
|
||||||
if (detectionPoint && isScrollingUp) {
|
if (detectionPoint && isScrollingUp) {
|
||||||
if (!visibleRef.current) {
|
if (!visibleRef.current) {
|
||||||
setVisible(true)
|
setVisible(true)
|
||||||
|
@ -39,7 +38,7 @@ const FloatingMenu = (): JSX.Element => {
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scrollYRef.current = scrollYProgress
|
scrollYRef.current = scrollY
|
||||||
},
|
},
|
||||||
default: {
|
default: {
|
||||||
immediate: true,
|
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',
|
'rounded-2xl border border-neutral-80/5 bg-blur-neutral-80/80 backdrop-blur-md',
|
||||||
' w-[calc(100%-24px)]',
|
' w-[calc(100%-24px)]',
|
||||||
' opacity-0 transition-opacity data-[visible=true]:opacity-100',
|
' opacity-0 transition-opacity data-[visible=true]:opacity-100',
|
||||||
'z-10',
|
'z-20',
|
||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
<FloatingMobile open={open} setOpen={setOpen} />
|
<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 { NavLink } from './nav-link'
|
||||||
import { NavNestedLinks } from './nav-nested-links'
|
import { NavNestedLinks } from './nav-nested-links'
|
||||||
|
import { SkeletonPlaceholder } from './skeleton-placeholder'
|
||||||
|
import { decodeUriComponent } from './utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
isLoading?: boolean
|
||||||
items: {
|
items: {
|
||||||
label: string
|
label: string
|
||||||
href?: string
|
href?: string
|
||||||
|
@ -22,7 +25,7 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const SidebarMenu = (props: Props) => {
|
const SidebarMenu = (props: Props) => {
|
||||||
const { items } = props
|
const { items, isLoading } = props
|
||||||
|
|
||||||
const [label, setLabel] = useState<string>('')
|
const [label, setLabel] = useState<string>('')
|
||||||
|
|
||||||
|
@ -30,7 +33,8 @@ const SidebarMenu = (props: Props) => {
|
||||||
|
|
||||||
const defaultLabel = items?.find(
|
const defaultLabel = items?.find(
|
||||||
item =>
|
item =>
|
||||||
item.href === asPath || item.links?.find(link => link.href === asPath)
|
item.href === decodeUriComponent(asPath) ||
|
||||||
|
item.links?.find(link => link.href === decodeUriComponent(asPath))
|
||||||
)?.label
|
)?.label
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -40,6 +44,9 @@ const SidebarMenu = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<div className="border-r border-neutral-10 p-5">
|
<div className="border-r border-neutral-10 p-5">
|
||||||
<aside className=" sticky top-5 min-w-[320px]">
|
<aside className=" sticky top-5 min-w-[320px]">
|
||||||
|
{isLoading ? (
|
||||||
|
<SkeletonPlaceholder />
|
||||||
|
) : (
|
||||||
<Accordion.Root
|
<Accordion.Root
|
||||||
type="single"
|
type="single"
|
||||||
collapsible
|
collapsible
|
||||||
|
@ -59,11 +66,7 @@ const SidebarMenu = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion.Item
|
<Accordion.Item key={item.label} value={item.label}>
|
||||||
key={item.label}
|
|
||||||
value={item.label}
|
|
||||||
className="accordion-item"
|
|
||||||
>
|
|
||||||
<Accordion.Trigger
|
<Accordion.Trigger
|
||||||
className="accordion-trigger"
|
className="accordion-trigger"
|
||||||
onClick={() => setLabel(item.label)}
|
onClick={() => setLabel(item.label)}
|
||||||
|
@ -74,6 +77,7 @@ const SidebarMenu = (props: Props) => {
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Accordion.Root>
|
</Accordion.Root>
|
||||||
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ChevronRightIcon } from '@status-im/icons'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
import { Link } from '../link'
|
import { Link } from '../link'
|
||||||
|
import { decodeUriComponent } from './utils'
|
||||||
|
|
||||||
import type { Url } from 'next/dist/shared/lib/router/router'
|
import type { Url } from 'next/dist/shared/lib/router/router'
|
||||||
|
|
||||||
|
@ -26,7 +27,7 @@ const NavNestedLinks = (props: NavLinkProps) => {
|
||||||
const { asPath } = useRouter()
|
const { asPath } = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion.Item value={label} className="accordion-item">
|
<Accordion.Item value={label}>
|
||||||
<div>
|
<div>
|
||||||
<Accordion.Trigger className="accordion-trigger">
|
<Accordion.Trigger className="accordion-trigger">
|
||||||
<div className="accordion-chevron inline-flex h-5 w-5">
|
<div className="accordion-chevron inline-flex h-5 w-5">
|
||||||
|
@ -44,7 +45,7 @@ const NavNestedLinks = (props: NavLinkProps) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{links.map((link, index) => {
|
{links.map((link, index) => {
|
||||||
const active = asPath === link.href
|
const active = decodeUriComponent(asPath) === link.href
|
||||||
|
|
||||||
const paddingClassName = index === 0 ? 'pt-5' : 'pt-2'
|
const paddingClassName = index === 0 ? 'pt-5' : 'pt-2'
|
||||||
const paddingLastChild = index === links.length - 1 ? 'pb-5' : ''
|
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 { Avatar, Button, Input, Text } from '@status-im/components'
|
||||||
import { DropdownMenu } from '@status-im/components/src/dropdown-menu'
|
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'
|
import type { ColorTokens } from '@tamagui/core'
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
id: number
|
id: string
|
||||||
name: string
|
name: string
|
||||||
avatar?: string | React.ReactElement
|
avatar?: string | React.ReactElement
|
||||||
color?: ColorTokens | `#${string}`
|
color?: ColorTokens | `#${string}`
|
||||||
|
@ -22,6 +22,8 @@ type Props = {
|
||||||
data: Data[]
|
data: Data[]
|
||||||
label: string
|
label: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
|
selectedValues: string[]
|
||||||
|
onSelectedValuesChange: (values: string[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAvatar = (value: unknown): value is string => {
|
const isAvatar = (value: unknown): value is string => {
|
||||||
|
@ -45,23 +47,26 @@ const RenderIcon = (props: Data) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownFilter = (props: Props) => {
|
const DropdownFilter = (props: Props) => {
|
||||||
const { data, label, placeholder } = props
|
const { data, label, placeholder, selectedValues, onSelectedValuesChange } =
|
||||||
|
props
|
||||||
|
|
||||||
const [filterText, setFilterText] = useState('')
|
const [filterText, setFilterText] = useState('')
|
||||||
|
|
||||||
// TODO - this will be improved by having a debounced search and use memoization
|
const filteredData = useMemo(
|
||||||
const filteredData = data.filter(label =>
|
() =>
|
||||||
|
data.filter(label =>
|
||||||
label.name.toLowerCase().includes(filterText.toLowerCase())
|
label.name.toLowerCase().includes(filterText.toLowerCase())
|
||||||
|
),
|
||||||
|
[data, filterText]
|
||||||
)
|
)
|
||||||
|
|
||||||
const [selectedValues, setSelectedValues] = useState<number[]>([])
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
const currentBreakpoint = useCurrentBreakpoint()
|
const currentBreakpoint = useCurrentBreakpoint()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<DropdownMenu onOpenChange={() => setIsOpen(!isOpen)}>
|
<DropdownMenu onOpenChange={() => setIsOpen(!isOpen)} modal={false}>
|
||||||
<Button
|
<Button
|
||||||
size={32}
|
size={32}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
@ -77,6 +82,7 @@ const DropdownFilter = (props: Props) => {
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
sideOffset={10}
|
sideOffset={10}
|
||||||
align={currentBreakpoint === '2xl' ? 'end' : 'start'}
|
align={currentBreakpoint === '2xl' ? 'end' : 'start'}
|
||||||
|
@ -99,11 +105,11 @@ const DropdownFilter = (props: Props) => {
|
||||||
checked={selectedValues.includes(filtered.id)}
|
checked={selectedValues.includes(filtered.id)}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
if (selectedValues.includes(filtered.id)) {
|
if (selectedValues.includes(filtered.id)) {
|
||||||
setSelectedValues(
|
onSelectedValuesChange(
|
||||||
selectedValues.filter(id => id !== filtered.id)
|
selectedValues.filter(id => id !== filtered.id)
|
||||||
)
|
)
|
||||||
} else {
|
} 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 { IconButton, Text } from '@status-im/components'
|
||||||
import { DropdownMenu } from '@status-im/components/src/dropdown-menu'
|
import { DropdownMenu } from '@status-im/components/src/dropdown-menu'
|
||||||
import { SortIcon } from '@status-im/icons'
|
import { SortIcon } from '@status-im/icons'
|
||||||
import Image from 'next/image'
|
|
||||||
|
import type { Order_By } from '@/lib/graphql/generated/schemas'
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
id: number
|
id: Order_By
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: Data[]
|
data: Data[]
|
||||||
|
orderByValue: Order_By
|
||||||
|
onOrderByValueChange: (value: Order_By) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownSort = (props: Props) => {
|
const DropdownSort = (props: Props) => {
|
||||||
const { data } = props
|
const { data, orderByValue, onOrderByValueChange } = props
|
||||||
|
|
||||||
const [selectedValue, setSelectedValue] = useState<number>()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -37,31 +36,12 @@ const DropdownSort = (props: Props) => {
|
||||||
key={option.id}
|
key={option.id}
|
||||||
label={option.name}
|
label={option.name}
|
||||||
onSelect={() => {
|
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.Content>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import { useState } from 'react'
|
import { ActiveMembersIcon, OpenIcon } from '@status-im/icons'
|
||||||
|
|
||||||
import { DoneIcon, OpenIcon } from '@status-im/icons'
|
|
||||||
|
|
||||||
const Tabs = (): JSX.Element => {
|
|
||||||
const [activeTab, setActiveTab] = useState<'open' | 'closed'>('open')
|
|
||||||
|
|
||||||
|
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'
|
const isOpen = activeTab === 'open'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -14,10 +19,12 @@ const Tabs = (): JSX.Element => {
|
||||||
className={`flex cursor-pointer flex-row items-center transition-colors ${
|
className={`flex cursor-pointer flex-row items-center transition-colors ${
|
||||||
isOpen ? 'text-neutral-100' : 'text-neutral-50'
|
isOpen ? 'text-neutral-100' : 'text-neutral-50'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setActiveTab('open')}
|
onClick={() => onTabChange('open')}
|
||||||
>
|
>
|
||||||
<OpenIcon size={20} color={isOpen ? '$neutral-100' : '$neutral-50'} />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center pr-3">
|
<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 ${
|
className={`flex cursor-pointer flex-row items-center transition-colors ${
|
||||||
!isOpen ? 'text-neutral-100' : 'text-neutral-50'
|
!isOpen ? 'text-neutral-100' : 'text-neutral-50'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setActiveTab('closed')}
|
onClick={() => onTabChange('closed')}
|
||||||
>
|
>
|
||||||
<DoneIcon
|
<ActiveMembersIcon
|
||||||
size={20}
|
size={20}
|
||||||
color={!isOpen ? '$neutral-100' : '$neutral-50'}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,273 +1,155 @@
|
||||||
import { useState } from 'react'
|
import { Avatar, Input, Skeleton, Tag, Text } from '@status-im/components'
|
||||||
|
import { ActiveMembersIcon, OpenIcon, SearchIcon } from '@status-im/icons'
|
||||||
import { Avatar, Button, Input, Tag, Text } from '@status-im/components'
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
import { ProfileIcon, SearchIcon } from '@status-im/icons'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
import { useCurrentBreakpoint } from '@/hooks/use-current-breakpoint'
|
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 { DropdownFilter, DropdownSort, Tabs } from './filters'
|
||||||
|
|
||||||
import type { DropdownFilterProps } from './filters/dropdown-filter'
|
|
||||||
import type { DropdownSortProps } from './filters/dropdown-sort'
|
import type { DropdownSortProps } from './filters/dropdown-sort'
|
||||||
|
import type {
|
||||||
const issues = [
|
GetFiltersForOrphansQuery,
|
||||||
{
|
GetFiltersWithEpicQuery,
|
||||||
id: 5154,
|
GetIssuesByEpicQuery,
|
||||||
title: 'Add support for encrypted communities',
|
GetOrphansQuery,
|
||||||
status: 'open',
|
} from '@/lib/graphql/generated/operations'
|
||||||
},
|
|
||||||
{
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const sortOptions: DropdownSortProps['data'] = [
|
const sortOptions: DropdownSortProps['data'] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: Order_By.Asc,
|
||||||
name: 'Default',
|
name: 'Ascending',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: Order_By.Desc,
|
||||||
name: 'Alphabetical',
|
name: 'Descending',
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Creation date',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Updated',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: 'Completion',
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const TableIssues = () => {
|
type Props = {
|
||||||
const [issuesSearchText, setIssuesSearchText] = useState('')
|
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 currentBreakpoint = useCurrentBreakpoint()
|
||||||
|
|
||||||
|
const {
|
||||||
|
data = [],
|
||||||
|
count,
|
||||||
|
isLoading,
|
||||||
|
filters,
|
||||||
|
handleTabChange,
|
||||||
|
activeTab,
|
||||||
|
selectedAuthors,
|
||||||
|
handleSelectedAuthors,
|
||||||
|
selectedAssignees,
|
||||||
|
handleSelectedAssignees,
|
||||||
|
selectedRepos,
|
||||||
|
handleSelectedRepos,
|
||||||
|
orderByValue,
|
||||||
|
handleOrderByValue,
|
||||||
|
handleSearchFilter,
|
||||||
|
searchFilterValue,
|
||||||
|
} = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-2xl border border-neutral-10">
|
<div className="overflow-hidden rounded-2xl border border-neutral-20 shadow-1">
|
||||||
<div className="flex border-b border-neutral-10 bg-neutral-5 p-3">
|
<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">
|
<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-1">
|
||||||
<div className="flex items-center 2xl:justify-end">
|
<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 w-full justify-between pt-4 2xl:justify-end 2xl:pt-0">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="transition-all">
|
<div className="transition-all">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Find Author"
|
placeholder="Find issue..."
|
||||||
icon={<SearchIcon size={20} />}
|
icon={<SearchIcon size={20} />}
|
||||||
size={32}
|
size={32}
|
||||||
value={issuesSearchText}
|
value={searchFilterValue}
|
||||||
onChangeText={setIssuesSearchText}
|
onChangeText={handleSearchFilter}
|
||||||
variant="retractable"
|
variant="retractable"
|
||||||
direction={currentBreakpoint === '2xl' ? 'rtl' : 'ltr'}
|
direction={currentBreakpoint === '2xl' ? 'rtl' : 'ltr'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DropdownFilter
|
<DropdownFilter
|
||||||
data={authors}
|
onSelectedValuesChange={handleSelectedAuthors}
|
||||||
|
selectedValues={selectedAuthors}
|
||||||
|
data={
|
||||||
|
filters?.authors.map(author => {
|
||||||
|
return {
|
||||||
|
id: author.author || '',
|
||||||
|
name: author.author || '',
|
||||||
|
}
|
||||||
|
}) || []
|
||||||
|
}
|
||||||
label="Author"
|
label="Author"
|
||||||
placeholder="Find author"
|
placeholder="Find author "
|
||||||
/>
|
/>
|
||||||
<DropdownFilter
|
<DropdownFilter
|
||||||
data={epics}
|
onSelectedValuesChange={handleSelectedAssignees}
|
||||||
label="Epics"
|
selectedValues={selectedAssignees}
|
||||||
placeholder="Find epic"
|
data={
|
||||||
/>
|
filters?.assignees.map(assignee => {
|
||||||
<DropdownFilter
|
return {
|
||||||
data={labels}
|
id: assignee.assignee || '',
|
||||||
label="Labels"
|
name: assignee.assignee || '',
|
||||||
placeholder="Find label"
|
}
|
||||||
/>
|
}) || []
|
||||||
<DropdownFilter
|
}
|
||||||
data={assignees}
|
|
||||||
label="Assignee"
|
label="Assignee"
|
||||||
placeholder="Find assignee"
|
placeholder="Find assignee"
|
||||||
/>
|
/>
|
||||||
<DropdownFilter
|
<DropdownFilter
|
||||||
data={repositories}
|
onSelectedValuesChange={handleSelectedRepos}
|
||||||
|
selectedValues={selectedRepos}
|
||||||
|
data={
|
||||||
|
filters?.repos.map(repo => {
|
||||||
|
return {
|
||||||
|
id: repo.repository || '',
|
||||||
|
name: repo.repository || '',
|
||||||
|
}
|
||||||
|
}) || []
|
||||||
|
}
|
||||||
label="Repos"
|
label="Repos"
|
||||||
placeholder="Find repo"
|
placeholder="Find repo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pl-2">
|
<div className="pl-2">
|
||||||
<DropdownSort data={sortOptions} />
|
<DropdownSort
|
||||||
|
data={sortOptions}
|
||||||
|
onOrderByValueChange={handleOrderByValue}
|
||||||
|
orderByValue={orderByValue}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -275,47 +157,78 @@ const TableIssues = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divide-y divide-neutral-10">
|
<div className="relative divide-y divide-neutral-10">
|
||||||
{issues.map(issue => (
|
{data.length !== 0 &&
|
||||||
|
data.map(issue => (
|
||||||
<Link
|
<Link
|
||||||
key={issue.id}
|
key={issue.issue_number}
|
||||||
href={`https://github.com/status-im/status-react/issues/${issue.id}`}
|
href={issue.issue_url || ''}
|
||||||
className="flex items-center justify-between px-4 py-3 transition-colors duration-200 hover:bg-neutral-5"
|
className="flex items-center justify-between px-4 py-3 transition-colors duration-200 hover:bg-neutral-5"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-row items-start gap-2 ">
|
||||||
<Text size={15} weight="medium">
|
<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}
|
{issue.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size={13} color="$neutral-50">
|
<Text size={13} color="$neutral-50">
|
||||||
#9667 • Opened 2 days ago by slaedjenic
|
#{issue.issue_number} •{' '}
|
||||||
|
{formatDistanceToNow(new Date(issue.created_at), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}{' '}
|
||||||
|
by {issue.author}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<div className="flex gap-1">
|
{'epic_name' in issue && issue.epic_name && (
|
||||||
<Tag size={24} label="E:Syncing" color="$orange-50" />
|
<Tag
|
||||||
<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>
|
|
||||||
|
|
||||||
<Tag size={24} label="9435" />
|
|
||||||
|
|
||||||
<Avatar
|
|
||||||
type="user"
|
|
||||||
size={24}
|
size={24}
|
||||||
name="jkbktl"
|
label={issue.epic_name || ''}
|
||||||
src="https://images.unsplash.com/photo-1552058544-f2b08422138a?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1299&q=80"
|
color={`#${issue.epic_color}` || '$primary'}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{'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>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
{data.length === 0 && !isLoading && (
|
||||||
|
<div className="py-11">
|
||||||
|
<Empty />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="p-3">
|
{isLoading && (
|
||||||
<Button size={40} variant="outline">
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
Show more 10
|
<div className="flex h-10 grow flex-col justify-between">
|
||||||
</Button>
|
<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>
|
||||||
</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'
|
import { AppLayout } from './app-layout'
|
||||||
|
|
||||||
// Eventually this will be fetched from the API, at least the nested links
|
const STATIC_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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Orphans',
|
label: 'Orphans',
|
||||||
href: '/insights/orphans',
|
href: '/insights/orphans',
|
||||||
|
@ -81,14 +14,40 @@ const MENU_LINKS = [
|
||||||
|
|
||||||
interface InsightsLayoutProps {
|
interface InsightsLayoutProps {
|
||||||
children: React.ReactNode
|
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 (
|
return (
|
||||||
<AppLayout hasPreFooter={false}>
|
<AppLayout hasPreFooter={false}>
|
||||||
<div className="relative mx-1 flex min-h-[calc(100vh-56px-4px)] w-full rounded-3xl bg-white-100">
|
<div className="relative flex min-h-[calc(100vh-56px-4px)] w-full rounded-3xl bg-white-100">
|
||||||
<SidebarMenu items={MENU_LINKS} />
|
{<SidebarMenu items={links} />}
|
||||||
<main className="flex-1">{children}</main>
|
<main className="flex-1 pb-8">{children}</main>
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</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 { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { EpicOverview } from '@/components/epic-overview'
|
|
||||||
import { TableIssues } from '@/components/table-issues'
|
|
||||||
import { InsightsLayout } from '@/layouts/insights-layout'
|
|
||||||
|
|
||||||
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 { 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 = {
|
||||||
|
title: string
|
||||||
type Epic = (typeof epics)[number]
|
color: `#${string}`
|
||||||
|
description: string
|
||||||
export const getStaticPaths: GetStaticPaths<Params> = async () => {
|
|
||||||
const paths = epics.map(epic => ({
|
|
||||||
params: { epic: epic.id },
|
|
||||||
}))
|
|
||||||
|
|
||||||
return { paths, fallback: false }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<Props, Params> = async context => {
|
type Props = {
|
||||||
const epic = epics.find(epic => epic.id === context.params!.epic)!
|
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 {
|
return {
|
||||||
// notFound: true,
|
|
||||||
redirect: { destination: '/insights/epics', permanent: false },
|
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 {
|
return {
|
||||||
props: {
|
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: [
|
breadcrumbs: [
|
||||||
{
|
{
|
||||||
label: 'Epics',
|
label: 'Epics',
|
||||||
href: '/insights/epics',
|
href: '/insights/epics',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: epic.title,
|
label: epic,
|
||||||
href: `/insights/epics/${epic.id}`,
|
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,39 +1,199 @@
|
||||||
import { useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { IconButton, Shadow, Tag, Text } from '@status-im/components'
|
import { Input, Shadow, Tag, Text } from '@status-im/components'
|
||||||
import {
|
import { DoneIcon, OpenIcon, SearchIcon } from '@status-im/icons'
|
||||||
DoneIcon,
|
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||||
NotStartedIcon,
|
import { differenceInCalendarDays, format } from 'date-fns'
|
||||||
OpenIcon,
|
|
||||||
SearchIcon,
|
|
||||||
SortIcon,
|
|
||||||
} from '@status-im/icons'
|
|
||||||
|
|
||||||
|
import { Loading } from '@/components/chart/components'
|
||||||
import { DatePicker } from '@/components/datepicker/datepicker'
|
import { DatePicker } from '@/components/datepicker/datepicker'
|
||||||
import { EpicOverview } from '@/components/epic-overview'
|
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 { 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 { DateRange } from '@status-im/components/src/calendar/calendar'
|
||||||
import type { Page } from 'next'
|
import type { Page } from 'next'
|
||||||
|
|
||||||
export const epics = [
|
type Props = {
|
||||||
|
links: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const LIMIT = 3
|
||||||
|
|
||||||
|
const sortOptions: DropdownSortProps['data'] = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: Order_By.Asc,
|
||||||
title: 'Communities protocol',
|
name: 'Ascending',
|
||||||
description: 'Support Encrypted Communities',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '5155',
|
id: Order_By.Desc,
|
||||||
title: 'Keycard',
|
name: 'Descending',
|
||||||
description:
|
|
||||||
'Detecting keycard reader removal for the beginning of each flow',
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
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 [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 (
|
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 (
|
||||||
|
<InsightsLayout links={links}>
|
||||||
|
<div className="flex h-full flex-1 flex-col justify-between">
|
||||||
<div className="space-y-4 p-10">
|
<div className="space-y-4 p-10">
|
||||||
<Text size={27} weight="semibold">
|
<Text size={27} weight="semibold">
|
||||||
Epics
|
Epics
|
||||||
|
@ -41,31 +201,110 @@ const EpicsPage: Page = () => {
|
||||||
|
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Tag size={32} label="In Progress" icon={OpenIcon} selected />
|
<Tag
|
||||||
<Tag size={32} label="Closed" icon={DoneIcon} />
|
size={32}
|
||||||
<Tag size={32} label="Not Started" icon={NotStartedIcon} />
|
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>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<IconButton variant="outline" icon={<SearchIcon size={20} />} />
|
<Input
|
||||||
<IconButton variant="outline" icon={<SortIcon size={20} />} />
|
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>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{epics.map(epic => (
|
{epics.map(epic => (
|
||||||
<Shadow key={epic.id} variant="$2" className="rounded-2xl px-4 py-3">
|
<RenderIfVisible
|
||||||
<EpicOverview title={epic.title} description={epic.description} />
|
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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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>
|
</div>
|
||||||
<DatePicker selected={selectedDates} onSelect={setSelectedDates} />
|
<DatePicker selected={selectedDates} onSelect={setSelectedDates} />
|
||||||
</div>
|
</div>
|
||||||
|
</InsightsLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
EpicsPage.getLayout = function getLayout(page) {
|
export async function getServerSideProps() {
|
||||||
return <InsightsLayout>{page}</InsightsLayout>
|
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
|
export default EpicsPage
|
||||||
|
|
|
@ -1,24 +1,205 @@
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { Text } from '@status-im/components'
|
import { Text } from '@status-im/components'
|
||||||
|
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||||
|
|
||||||
import { TableIssues } from '@/components'
|
import { TableIssues } from '@/components'
|
||||||
|
import { useDebounce } from '@/hooks/use-debounce'
|
||||||
|
import { useIntersectionObserver } from '@/hooks/use-intersection-observer'
|
||||||
import { InsightsLayout } from '@/layouts/insights-layout'
|
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'
|
import type { Page } from 'next'
|
||||||
|
|
||||||
const OrphansPage: Page = () => {
|
type Props = {
|
||||||
|
orphans: GetOrphansQuery
|
||||||
|
filters: GetFiltersForOrphansQuery
|
||||||
|
links: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="space-y-6 p-10">
|
<InsightsLayout links={links}>
|
||||||
|
<div className="space-y-6 scroll-smooth p-10">
|
||||||
<Text size={27} weight="semibold">
|
<Text size={27} weight="semibold">
|
||||||
Orphans
|
Orphans
|
||||||
</Text>
|
</Text>
|
||||||
|
<TableIssues
|
||||||
<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>
|
</div>
|
||||||
|
</InsightsLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
OrphansPage.getLayout = function getLayout(page) {
|
export async function getServerSideProps() {
|
||||||
return <InsightsLayout>{page}</InsightsLayout>
|
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
|
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