diff --git a/packages/server/api/src/app/analytics/platform-analytics-report.service.ts b/packages/server/api/src/app/analytics/platform-analytics-report.service.ts index db7e4304b11..7363602fada 100644 --- a/packages/server/api/src/app/analytics/platform-analytics-report.service.ts +++ b/packages/server/api/src/app/analytics/platform-analytics-report.service.ts @@ -1,4 +1,4 @@ -import { AnalyticsFlowReportItem, AnalyticsRunsUsageItem, AnalyticsTimePeriod, apId, FlowVersionState, isNil, PlatformAnalyticsReport, PlatformId, ProjectLeaderboardItem, RunEnvironment, UserLeaderboardItem, UserWithMetaInformation } from '@activepieces/shared' +import { AnalyticsFlowReportItem, AnalyticsRunsUsageItem, AnalyticsTimePeriod, apId, FlowStatus, FlowVersionState, isNil, PlatformAnalyticsReport, PlatformId, ProjectLeaderboardItem, RunEnvironment, UserLeaderboardItem, UserWithMetaInformation } from '@activepieces/shared' import dayjs from 'dayjs' import { FastifyBaseLogger } from 'fastify' import { IsNull } from 'typeorm' @@ -138,6 +138,7 @@ async function listFlows(platformId: PlatformId, log: FastifyBaseLogger): Promis cursorRequest: null, versionState: FlowVersionState.DRAFT, includeTriggerSource: false, + status: [FlowStatus.ENABLED], }) const projects = await listProjects(platformId) diff --git a/packages/web/src/app/routes/impact/components/project-select.tsx b/packages/web/src/app/routes/impact/components/project-select.tsx index 06b5da05218..7a8170de2bf 100644 --- a/packages/web/src/app/routes/impact/components/project-select.tsx +++ b/packages/web/src/app/routes/impact/components/project-select.tsx @@ -59,7 +59,7 @@ export function ProjectSelect({ variant="outline" role="combobox" aria-expanded={open} - className="w-auto gap-2 font-normal" + className="w-auto gap-2 font-normal h-8" > {selectedProject?.type === ProjectType.TEAM ? ( void; onApply: () => void; - onClear?: () => void; }; export function TimeSavedFilterContent({ @@ -29,7 +28,6 @@ export function TimeSavedFilterContent({ unitMax, onCycleUnitMax, onApply, - onClear, }: TimeSavedFilterContentProps) { return (
@@ -63,31 +61,23 @@ export function TimeSavedFilterContent({ placeholder="∞" value={draftMax} onChange={(e) => onMaxChange(e.target.value)} - className="pr-12" + className={draftMax ? 'pr-12' : ''} /> - + {draftMax && ( + + )}
- - {onClear && ( - - )} ); } diff --git a/packages/web/src/app/routes/impact/details/edit-time-saved-popover.tsx b/packages/web/src/app/routes/impact/details/edit-time-saved-popover.tsx index 947446cf539..f8c2d99bb69 100644 --- a/packages/web/src/app/routes/impact/details/edit-time-saved-popover.tsx +++ b/packages/web/src/app/routes/impact/details/edit-time-saved-popover.tsx @@ -34,6 +34,8 @@ export function EditTimeSavedPopover({ const [isOpen, setIsOpen] = useState(false); const [hms, setHms] = useState({ hours: '', mins: '', secs: '' }); + const minsRef = useRef(null); + const secsRef = useRef(null); const handleOpenChange = (open: boolean) => { if (open) { @@ -81,18 +83,58 @@ export function EditTimeSavedPopover({ else if (e.key === 'Escape') setIsOpen(false); }; - const handleInputChange = ( + const handleTimeInput = ( value: string, field: keyof typeof hms, max: number, + nextRef: React.RefObject | null, ) => { const numericValue = value.replace(/\D/g, ''); - const num = parseInt(numericValue, 10); - if (numericValue === '' || (num >= 0 && num <= max)) { + + if (field === 'hours') { + const num = parseInt(numericValue, 10); + if (numericValue === '' || (num >= 0 && num <= max)) { + setHms((prev) => ({ ...prev, hours: numericValue })); + } + return; + } + + if (numericValue === '') { + setHms((prev) => ({ ...prev, [field]: '' })); + return; + } + + if (numericValue.length === 1) { + const digit = parseInt(numericValue, 10); + if (digit >= 6) { + setHms((prev) => ({ ...prev, [field]: '0' + numericValue })); + nextRef?.current?.focus(); + nextRef?.current?.select(); + return; + } setHms((prev) => ({ ...prev, [field]: numericValue })); + return; + } + + const clamped = numericValue.slice(0, 2); + const num = parseInt(clamped, 10); + if (num >= 0 && num <= max) { + setHms((prev) => ({ ...prev, [field]: clamped })); + nextRef?.current?.focus(); + nextRef?.current?.select(); } }; + const padOnBlur = (field: 'mins' | 'secs') => { + setHms((prev) => { + const val = prev[field]; + if (val.length === 1) { + return { ...prev, [field]: '0' + val }; + } + return prev; + }); + }; + return ( {children} @@ -100,54 +142,55 @@ export function EditTimeSavedPopover({
{t('Time Saved Per Run')}
-
+
handleInputChange(e.target.value, 'hours', 99)} + onChange={(e) => + handleTimeInput(e.target.value, 'hours', 1000, minsRef) + } onKeyDown={handleKeyDown} className="w-full text-center text-sm bg-transparent outline-none placeholder:text-muted-foreground/50" - maxLength={2} + maxLength={4} autoFocus /> - - {t('hh')} -
- : + :
handleInputChange(e.target.value, 'mins', 59)} + onChange={(e) => + handleTimeInput(e.target.value, 'mins', 59, secsRef) + } + onBlur={() => padOnBlur('mins')} onKeyDown={handleKeyDown} className="w-full text-center text-sm bg-transparent outline-none placeholder:text-muted-foreground/50" maxLength={2} /> - - {t('mm')} -
- : + :
handleInputChange(e.target.value, 'secs', 59)} + onChange={(e) => + handleTimeInput(e.target.value, 'secs', 59, null) + } + onBlur={() => padOnBlur('secs')} onKeyDown={handleKeyDown} className="w-full text-center text-sm bg-transparent outline-none placeholder:text-muted-foreground/50" maxLength={2} /> - - {t('ss')} -
diff --git a/packages/web/src/app/routes/impact/details/index.tsx b/packages/web/src/app/routes/impact/details/index.tsx index 04eb94d4e38..9bdd20cffb3 100644 --- a/packages/web/src/app/routes/impact/details/index.tsx +++ b/packages/web/src/app/routes/impact/details/index.tsx @@ -139,11 +139,12 @@ export function FlowsDetails({
- {displayValue ?? t('Not set')} + + {t('Add Estimated Time')}
- {t("You don't have permission to edit this flow")} + {t("You don't have permission to add")}
); @@ -158,10 +159,10 @@ export function FlowsDetails({ flowId={row.original.flowId} currentValue={timeSavedPerRun} > - +
@@ -231,12 +232,7 @@ export function FlowsDetails({ if (userHasAccess) { return ( -
- window.open(`/projects/${row.original.projectId}`, '_blank') - } - > +
{projectAvatar} {projectName}
@@ -283,16 +279,6 @@ export function FlowsDetails({ - {filters.hasActiveFilters && ( - - )} -
@@ -385,7 +371,6 @@ function TimeSavedFilter({ filters }: { filters: FiltersReturn }) { unitMax={filters.draftTimeSaved.unitMax} onCycleUnitMax={filters.cycleDraftTimeUnitMax} onApply={filters.applyTimeSavedFilter} - onClear={filters.clearTimeSavedFilter} /> diff --git a/packages/web/src/app/routes/impact/index.tsx b/packages/web/src/app/routes/impact/index.tsx index 40c2d18effb..77724b39ab1 100644 --- a/packages/web/src/app/routes/impact/index.tsx +++ b/packages/web/src/app/routes/impact/index.tsx @@ -106,14 +106,14 @@ export default function ImpactPage() { - {t('View impact analytics and metrics')} + {t('View impact analytics and metrics for the active flows.')}
-
+
{t('Updated')} {dayjs(data?.updated).format('MMM DD, hh:mm A')} —{' '} {t('Refreshes daily')} @@ -147,7 +147,7 @@ export default function ImpactPage() { value={selectedTimePeriod} onValueChange={handleTimePeriodChange} > - + diff --git a/packages/web/src/app/routes/impact/lib/impact-utils.ts b/packages/web/src/app/routes/impact/lib/impact-utils.ts index 34a4bd9b3a5..e6be5b278d9 100644 --- a/packages/web/src/app/routes/impact/lib/impact-utils.ts +++ b/packages/web/src/app/routes/impact/lib/impact-utils.ts @@ -29,8 +29,8 @@ export const secondsToHMS = ( const s = totalSeconds % 60; return { hours: h > 0 ? h.toString() : '', - mins: m > 0 || h > 0 ? m.toString() : '', - secs: s > 0 || m > 0 || h > 0 ? s.toString() : '', + mins: m > 0 || h > 0 ? m.toString().padStart(2, '0') : '', + secs: s > 0 || m > 0 || h > 0 ? s.toString().padStart(2, '0') : '', }; }; diff --git a/packages/web/src/app/routes/impact/lib/use-details-filters.ts b/packages/web/src/app/routes/impact/lib/use-details-filters.ts index 25a2ce1175a..81b39d5609a 100644 --- a/packages/web/src/app/routes/impact/lib/use-details-filters.ts +++ b/packages/web/src/app/routes/impact/lib/use-details-filters.ts @@ -58,16 +58,26 @@ export function useDetailsFilters( const cycleDraftTimeUnitMin = () => { const idx = TIME_UNITS.indexOf(draftTimeSaved.unitMin); - updateDraftTimeSaved({ - unitMin: TIME_UNITS[(idx + 1) % TIME_UNITS.length], - }); + const newMinUnit = TIME_UNITS[(idx + 1) % TIME_UNITS.length]; + const newMinIdx = TIME_UNITS.indexOf(newMinUnit); + const maxIdx = TIME_UNITS.indexOf(draftTimeSaved.unitMax); + if (newMinIdx > maxIdx) { + updateDraftTimeSaved({ unitMin: newMinUnit, unitMax: newMinUnit }); + } else { + updateDraftTimeSaved({ unitMin: newMinUnit }); + } }; const cycleDraftTimeUnitMax = () => { const idx = TIME_UNITS.indexOf(draftTimeSaved.unitMax); - updateDraftTimeSaved({ - unitMax: TIME_UNITS[(idx + 1) % TIME_UNITS.length], - }); + const newMaxUnit = TIME_UNITS[(idx + 1) % TIME_UNITS.length]; + const newMaxIdx = TIME_UNITS.indexOf(newMaxUnit); + const minIdx = TIME_UNITS.indexOf(draftTimeSaved.unitMin); + if (newMaxIdx < minIdx) { + updateDraftTimeSaved({ unitMax: newMaxUnit, unitMin: newMaxUnit }); + } else { + updateDraftTimeSaved({ unitMax: newMaxUnit }); + } }; const handleTimeSavedPopoverOpen = (open: boolean) => { diff --git a/packages/web/src/app/routes/leaderboard/index.tsx b/packages/web/src/app/routes/leaderboard/index.tsx index c738f044219..3418586359a 100644 --- a/packages/web/src/app/routes/leaderboard/index.tsx +++ b/packages/web/src/app/routes/leaderboard/index.tsx @@ -201,6 +201,14 @@ export default function LeaderboardPage() { setTimeSavedPopoverOpen(false); }; + const hasActiveFilters = + searchQuery !== '' || appliedFilter.min !== '' || appliedFilter.max !== ''; + + const clearAllFilters = () => { + setSearchQuery(''); + setAppliedFilter(emptyFilter); + }; + const timeSavedLabel = useMemo(() => { if (!appliedFilter.min && !appliedFilter.max) return null; const min = appliedFilter.min @@ -219,21 +227,24 @@ export default function LeaderboardPage() { const userMap = new Map(analyticsData.users.map((user) => [user.id, user])); - return usersLeaderboardData.reduce((acc, item) => { - const user = userMap.get(item.userId); - if (!user) return acc; - - acc.push({ - id: item.userId, - visibleId: item.userId, - userName: `${user.firstName} ${user.lastName}`.trim() || user.email, - userEmail: user.email, - flowCount: item.flowCount ?? 0, - minutesSaved: item.minutesSaved ?? 0, - badges: badgesMap.get(item.userId), - }); - return acc; - }, []); + return usersLeaderboardData + .reduce[]>((acc, item) => { + const user = userMap.get(item.userId); + if (!user) return acc; + + acc.push({ + id: item.userId, + visibleId: item.userId, + userName: `${user.firstName} ${user.lastName}`.trim() || user.email, + userEmail: user.email, + flowCount: item.flowCount ?? 0, + minutesSaved: item.minutesSaved ?? 0, + badges: badgesMap.get(item.userId), + }); + return acc; + }, []) + .sort((a, b) => b.minutesSaved - a.minutesSaved) + .map((item, index) => ({ ...item, rank: index + 1 })); }, [analyticsData?.users, usersLeaderboardData, isLoading, badgesMap]); const projectsData = useMemo((): ProjectStats[] => { @@ -241,14 +252,17 @@ export default function LeaderboardPage() { return []; } - return projectsLeaderboardData.map((item) => ({ - id: item.projectId, - projectId: item.projectId, - projectName: item.projectName, - flowCount: item.flowCount ?? 0, - minutesSaved: item.minutesSaved ?? 0, - iconColor: projectIconMap.get(item.projectId), - })); + return projectsLeaderboardData + .map((item) => ({ + id: item.projectId, + projectId: item.projectId, + projectName: item.projectName, + flowCount: item.flowCount ?? 0, + minutesSaved: item.minutesSaved ?? 0, + iconColor: projectIconMap.get(item.projectId), + })) + .sort((a, b) => b.minutesSaved - a.minutesSaved) + .map((item, index) => ({ ...item, rank: index + 1 })); }, [projectsLeaderboardData, isLoading, projectIconMap]); const filteredPeopleData = useMemo(() => { @@ -261,9 +275,7 @@ export default function LeaderboardPage() { p.userEmail.toLowerCase().includes(q), ); } - return applyTimeSavedFilter(data, peopleTimeSaved).sort( - (a, b) => b.minutesSaved - a.minutesSaved, - ); + return applyTimeSavedFilter(data, peopleTimeSaved); }, [peopleData, searchQuery, peopleTimeSaved]); const filteredProjectsData = useMemo(() => { @@ -350,7 +362,7 @@ export default function LeaderboardPage() {
-
+
{t('Updated')}{' '} {dayjs(analyticsData?.cachedAt).format('MMM DD, hh:mm A')} @@ -388,7 +400,7 @@ export default function LeaderboardPage() { setTimePeriod(value as AnalyticsTimePeriod) } > - + @@ -474,14 +486,11 @@ export default function LeaderboardPage() { /> - {timeSavedLabel && ( - + )}
diff --git a/packages/web/src/app/routes/leaderboard/projects-leaderboard.tsx b/packages/web/src/app/routes/leaderboard/projects-leaderboard.tsx index 9aa3a2ba31e..dc402c6c6b0 100644 --- a/packages/web/src/app/routes/leaderboard/projects-leaderboard.tsx +++ b/packages/web/src/app/routes/leaderboard/projects-leaderboard.tsx @@ -20,6 +20,7 @@ export type ProjectStats = { flowCount: number; minutesSaved: number; iconColor?: ColorName; + rank: number; }; type ProjectsLeaderboardProps = { @@ -27,23 +28,23 @@ type ProjectsLeaderboardProps = { isLoading?: boolean; }; -export const getRankIcon = (index: number) => { - if (index === 0) return ; - if (index === 1) return ; - if (index === 2) return ; +export const getRankIcon = (rank: number) => { + if (rank === 1) return ; + if (rank === 2) return ; + if (rank === 3) return ; return null; }; -export const getRankText = (index: number) => { - return [0, 1, 2].includes(index) ? null : `#${index + 1}`; +export const getRankText = (rank: number) => { + return rank <= 3 ? null : `#${rank}`; }; -export function RankCell({ sortIndex }: { sortIndex: number }) { - const icon = getRankIcon(sortIndex); +export function RankCell({ rank }: { rank: number }) { + const icon = getRankIcon(rank); return (
{icon &&
{icon}
} - {getRankText(sortIndex)} + {getRankText(rank)}
); } @@ -54,11 +55,7 @@ const createColumns = (): ColumnDef>[] => [ header: ({ column }) => ( ), - cell: ({ row, table }) => { - const sortedRows = table.getSortedRowModel().rows; - const index = sortedRows.findIndex((r) => r.id === row.id); - return ; - }, + cell: ({ row }) => , enableSorting: false, size: 20, }, @@ -113,10 +110,10 @@ const createColumns = (): ColumnDef>[] => [ ]; const getRowClassName = ( - _row: RowDataWithActions, - index: number, + row: RowDataWithActions, + _index: number, ) => { - if (index < 3) return 'bg-primary/5 hover:bg-primary/10'; + if (row.rank <= 3) return 'bg-primary/5 hover:bg-primary/10'; return 'hover:bg-accent'; }; diff --git a/packages/web/src/app/routes/leaderboard/users-leaderboard.tsx b/packages/web/src/app/routes/leaderboard/users-leaderboard.tsx index 82ad5a430fc..13c3ad3fff8 100644 --- a/packages/web/src/app/routes/leaderboard/users-leaderboard.tsx +++ b/packages/web/src/app/routes/leaderboard/users-leaderboard.tsx @@ -25,6 +25,7 @@ export type UserStats = { flowCount: number; minutesSaved: number; badges?: UserWithBadges['badges']; + rank: number; }; type UsersLeaderboardProps = { @@ -54,7 +55,7 @@ const BadgesCell = ({ src={badgeInfo.imageUrl} alt={badgeInfo.title} className={cn( - 'h-8 w-8 object-cover rounded-md transition-opacity', + 'h-7 w-7 object-cover rounded-md transition-opacity', !isTopRank && 'opacity-30 group-hover/leaderrow:opacity-100', )} /> @@ -76,11 +77,7 @@ const createColumns = (): ColumnDef>[] => [ header: ({ column }) => ( ), - cell: ({ row, table }) => { - const sortedRows = table.getSortedRowModel().rows; - const index = sortedRows.findIndex((r) => r.id === row.id); - return ; - }, + cell: ({ row }) => , enableSorting: false, size: 25, }, @@ -110,6 +107,7 @@ const createColumns = (): ColumnDef>[] => [
{row.original.flowCount}
), enableSorting: false, + size: 120, }, { accessorKey: 'minutesSaved', @@ -122,26 +120,28 @@ const createColumns = (): ColumnDef>[] => [
), enableSorting: false, + size: 120, }, { accessorKey: 'badges', header: ({ column }) => ( ), - cell: ({ row, table }) => { - const sortedRows = table.getSortedRowModel().rows; - const index = sortedRows.findIndex((r) => r.id === row.id); - return ; - }, + cell: ({ row }) => ( + + ), enableSorting: false, }, ]; const getRowClassName = ( - _row: RowDataWithActions, - index: number, + row: RowDataWithActions, + _index: number, ) => { - if (index < 3) return 'group/leaderrow bg-primary/5 hover:bg-primary/10'; + if (row.rank <= 3) return 'group/leaderrow bg-primary/5 hover:bg-primary/10'; return 'group/leaderrow hover:bg-accent'; };