@@ -8,7 +8,7 @@ import type { OrganizationMember } from "@gitpod/public-api/lib/gitpod/v1/organi
88import { LoadingState } from "@podkit/loading/LoadingState" ;
99import { Heading2 , Subheading } from "@podkit/typography/Headings" ;
1010import classNames from "classnames" ;
11- import { useMemo } from "react" ;
11+ import { useCallback , useMemo , useState } from "react" ;
1212import { Accordion } from "./components/accordion/Accordion" ;
1313import Alert from "./components/Alert" ;
1414import Header from "./components/Header" ;
@@ -17,19 +17,71 @@ import { useWorkspaceSessions } from "./data/insights/list-workspace-sessions-qu
1717import { useListOrganizationMembers } from "./data/organizations/members-query" ;
1818import { WorkspaceSessionGroup } from "./insights/WorkspaceSessionGroup" ;
1919import { gitpodHostUrl } from "./service/service" ;
20+ import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from "@podkit/select/Select" ;
21+ import dayjs from "dayjs" ;
22+ import { Timestamp } from "@bufbuild/protobuf" ;
23+ import { LoadingButton } from "@podkit/buttons/LoadingButton" ;
24+ import { TextMuted } from "@podkit/typography/TextMuted" ;
25+ import { DownloadInsightsToast } from "./insights/download/DownloadInsights" ;
26+ import { useCurrentOrg } from "./data/organizations/orgs-query" ;
27+ import { useToast } from "./components/toasts/Toasts" ;
28+ import { useTemporaryState } from "./hooks/use-temporary-value" ;
29+ import { DownloadIcon } from "lucide-react" ;
30+ import { Button } from "@podkit/buttons/Button" ;
2031
2132export const Insights = ( ) => {
22- const { data, error : errorMessage , isLoading } = useWorkspaceSessions ( ) ;
33+ const [ prebuildsFilter , setPrebuildsFilter ] = useState < "week" | "month" | "year" > ( "week" ) ;
34+ const [ upperBound , lowerBound ] = useMemo ( ( ) => {
35+ const from = dayjs ( ) . subtract ( 1 , prebuildsFilter ) ;
36+
37+ const fromTimestamp = Timestamp . fromDate ( from . toDate ( ) ) ;
38+ const toTimestamp = Timestamp . fromDate ( new Date ( ) ) ;
39+ return [ fromTimestamp , toTimestamp ] ;
40+ } , [ prebuildsFilter ] ) ;
41+ const {
42+ data,
43+ error : errorMessage ,
44+ isLoading,
45+ isFetchingNextPage,
46+ hasNextPage,
47+ fetchNextPage,
48+ } = useWorkspaceSessions ( {
49+ from : upperBound ,
50+ to : lowerBound ,
51+ } ) ;
2352 const membersQuery = useListOrganizationMembers ( ) ;
2453 const members : OrganizationMember [ ] = useMemo ( ( ) => membersQuery . data || [ ] , [ membersQuery . data ] ) ;
2554
26- const sessions = data ?? [ ] ;
55+ const hasMoreThanOnePage = ( data ?. pages . length ?? 0 ) > 1 ;
56+ const sessions = useMemo ( ( ) => data ?. pages . flatMap ( ( p ) => p ) ?? [ ] , [ data ] ) ;
2757 const grouped = Object . groupBy ( sessions , ( ws ) => ws . workspace ?. id ?? "unknown" ) ;
58+ const [ page , setPage ] = useState ( 0 ) ;
2859
2960 return (
3061 < >
3162 < Header title = "Insights" subtitle = "Insights into workspace sessions in your organization" />
3263 < div className = "app-container pt-5" >
64+ < div
65+ className = { classNames (
66+ "flex flex-col items-start space-y-3 justify-between" ,
67+ "md:flex-row md:items-center md:space-x-4 md:space-y-0" ,
68+ ) }
69+ >
70+ < Select value = { prebuildsFilter } onValueChange = { ( v ) => setPrebuildsFilter ( v as any ) } >
71+ < SelectTrigger className = "w-[180px]" >
72+ < SelectValue placeholder = "Select time range" />
73+ </ SelectTrigger >
74+ < SelectContent >
75+ < SelectItem value = "day" > Last 24 hours</ SelectItem > { " " }
76+ { /* here for debugging, probably not useful */ }
77+ < SelectItem value = "week" > Last 7 days</ SelectItem >
78+ < SelectItem value = "month" > Last 30 days</ SelectItem >
79+ < SelectItem value = "year" > Last 365 days</ SelectItem >
80+ </ SelectContent >
81+ </ Select >
82+ < DownloadUsage from = { upperBound } to = { lowerBound } />
83+ </ div >
84+
3385 < div
3486 className = { classNames (
3587 "flex flex-col items-start space-y-3 justify-between px-3" ,
@@ -100,9 +152,63 @@ export const Insights = () => {
100152 ) }
101153 </ ItemsList >
102154 </ div >
155+
156+ < div className = "mt-4 mb-8 flex flex-row justify-center" >
157+ { hasNextPage ? (
158+ < LoadingButton
159+ variant = "secondary"
160+ onClick = { ( ) => {
161+ setPage ( page + 1 ) ;
162+ fetchNextPage ( ) ;
163+ } }
164+ loading = { isFetchingNextPage }
165+ >
166+ Load more
167+ </ LoadingButton >
168+ ) : (
169+ hasMoreThanOnePage && < TextMuted > All workspace sessions are loaded</ TextMuted >
170+ ) }
171+ </ div >
103172 </ div >
104173 </ >
105174 ) ;
106175} ;
107176
177+ type DownloadUsageProps = {
178+ from : Timestamp ;
179+ to : Timestamp ;
180+ } ;
181+ export const DownloadUsage = ( { from, to } : DownloadUsageProps ) => {
182+ const { data : org } = useCurrentOrg ( ) ;
183+ const { toast } = useToast ( ) ;
184+ // When we start the download, we disable the button for a short time
185+ const [ downloadDisabled , setDownloadDisabled ] = useTemporaryState ( false , 1000 ) ;
186+
187+ const handleDownload = useCallback ( async ( ) => {
188+ if ( ! org ) {
189+ return ;
190+ }
191+
192+ setDownloadDisabled ( true ) ;
193+ toast (
194+ < DownloadInsightsToast
195+ organizationName = { org ?. slug ?? org ?. id }
196+ organizationId = { org . id }
197+ from = { from }
198+ to = { to }
199+ /> ,
200+ {
201+ autoHide : false ,
202+ } ,
203+ ) ;
204+ } , [ org , setDownloadDisabled , toast , from , to ] ) ;
205+
206+ return (
207+ < Button variant = "secondary" onClick = { handleDownload } className = "gap-1" disabled = { downloadDisabled } >
208+ < DownloadIcon />
209+ < span > Export as CSV</ span >
210+ </ Button >
211+ ) ;
212+ } ;
213+
108214export default Insights ;
0 commit comments