diff --git a/assets/src/components/cd/clusters/Clusters.tsx b/assets/src/components/cd/clusters/Clusters.tsx index 680bffb7a..4105ba35c 100644 --- a/assets/src/components/cd/clusters/Clusters.tsx +++ b/assets/src/components/cd/clusters/Clusters.tsx @@ -45,6 +45,7 @@ import { useCDEnabled } from '../utils/useCDEnabled' import { ClusterStatusTabKey, ClustersFilters, + UpgradeableFilterKey, } from '../services/ClustersFilters' import { TagsFilter } from '../services/ClusterTagsFilter' @@ -109,6 +110,8 @@ export default function Clusters() { const tabStateRef = useRef(null) const [statusFilter, setStatusFilter] = useState('ALL') const [selectedTagKeys, setSelectedTagKeys] = useState(new Set()) + const [upgradeableFilter, setUpgradeableFilter] = + useState('ALL') const [searchString, setSearchString] = useState() const debouncedSearchString = useDebounce(searchString, 100) @@ -145,6 +148,10 @@ export default function Clusters() { ...(!isEmpty(searchTags) ? { tagQuery: { op: tagOp, tags: searchTags } } : {}), + upgradeable: + upgradeableFilter === 'ALL' + ? undefined + : upgradeableFilter === 'UPGRADEABLE', } ) @@ -194,9 +201,11 @@ export default function Clusters() { useSetBreadcrumbs(CD_CLUSTERS_BASE_CRUMBS) const clusterEdges = data?.clusters?.edges - const isDemo = statusCounts.ALL === 0 || !cdIsEnabled + const hasStatFilters = !!debouncedSearchString || !!projectId + const isDemo = (statusCounts.ALL === 0 && !hasStatFilters) || !cdIsEnabled const tableData = isDemo ? DEMO_CLUSTERS : clusterEdges - const showGettingStarted = isDemo || (statusCounts.ALL ?? 0) < 2 + const showGettingStarted = + isDemo || ((statusCounts.ALL ?? 0) < 2 && !hasStatFilters) useSetPageScrollable(showGettingStarted || isDemo) @@ -229,6 +238,9 @@ export default function Clusters() { setTagOp={ setTagOp as ComponentProps['setSearchOp'] } + upgradeableFilter={upgradeableFilter} + setUpgradeableFilter={setUpgradeableFilter} + upgradeStats={data.upgradeStatistics} /> { - const timestamp = val ?? customTimestamp + const setValsFromTimestamp = (val?: string) => { + const timestamp = handleUnixTS(val ?? customTimestamp) if (!isValidDateTime(timestamp)) { setTimestampError(true) return } setIsEnteringTimestamp(false) - const date = formatDateTime(timestamp, DATE_FORMAT) - const time = formatDateTime(timestamp, TIME_FORMAT) + const date = formatDateTime(timestamp, DATE_FORMAT, true) + const time = formatDateTime(timestamp, TIME_FORMAT, true) runAfterLayout(() => { dateInputRef.current?.setValue(date) timeInputRef.current?.setValue(time) @@ -130,6 +132,7 @@ export function DateTimeFormInput({ ) } {...props} + {...focusCallbacks} > {isEnteringTimestamp ? ( MM/DD/YYYY} @@ -173,7 +176,7 @@ export function DateTimeFormInput({ /> UTC} @@ -220,3 +223,11 @@ const CaptionTextBtnSC = styled.span<{ opacity: $disabled ? 0.4 : 1, '&:hover': { textDecoration: $disabled ? 'none' : 'underline' }, })) + +const handleUnixTS = (val: string) => { + // parse as a unix timestamp if it's a valid number + // otherwise keep it as is + const valNum = Number(val) + if (!isNaN(valNum)) return val.length === 10 ? valNum * 1000 : valNum + return val +} diff --git a/assets/src/components/cd/logs/LogsFilters.tsx b/assets/src/components/cd/logs/LogsFilters.tsx index b8556e767..44f928119 100644 --- a/assets/src/components/cd/logs/LogsFilters.tsx +++ b/assets/src/components/cd/logs/LogsFilters.tsx @@ -9,6 +9,7 @@ import { ListBoxItem, SearchIcon, Select, + Toast, } from '@pluralsh/design-system' import { useUpdateState } from 'components/hooks/useUpdateState' import { LogFacetInput } from 'generated/graphql' @@ -114,8 +115,10 @@ function FiltersForm({ onSubmit: (form: LogsFlyoverFiltersT) => void setLive: (live: boolean) => void }) { + const { spacing } = useTheme() const [hasDTErrors, setHasDTErrors] = useState(false) const clearDTFormRef = useRef<() => void>(null) + const [showSuccessToast, setShowSuccessToast] = useState(false) const { state, update, initialState, hasUpdates } = useUpdateState(initialForm) @@ -125,6 +128,7 @@ function FiltersForm({ onSubmit(state) // by default, logs should be live if no specific date is set, and vice versa setLive(!state.date) + setShowSuccessToast(true) } const resetToDefault = () => { @@ -202,6 +206,14 @@ function FiltersForm({ + setShowSuccessToast(false)} + margin={spacing.xlarge} + > + Filters applied + ) } diff --git a/assets/src/components/cd/services/ClustersFilters.tsx b/assets/src/components/cd/services/ClustersFilters.tsx index fa042aa7f..8e4b12be3 100644 --- a/assets/src/components/cd/services/ClustersFilters.tsx +++ b/assets/src/components/cd/services/ClustersFilters.tsx @@ -1,21 +1,33 @@ -import { type ComponentProps, useDeferredValue, useState } from 'react' import { Chip, Input, + ListBoxItem, SearchIcon, + Select, SubTab, TabList, } from '@pluralsh/design-system' -import styled from 'styled-components' -import { Dispatch, MutableRefObject, SetStateAction, useEffect } from 'react' import isNil from 'lodash/isNil' +import { + type ComponentProps, + Dispatch, + RefObject, + SetStateAction, + useDeferredValue, + useEffect, + useState, +} from 'react' +import styled from 'styled-components' import { useDebounce } from '@react-hooks-library/core' -import { serviceStatusToSeverity } from './ServiceStatusChip' +import { UpgradeStatistics } from 'generated/graphql' +import { isNumber } from 'lodash' import { TagsFilter } from './ClusterTagsFilter' +import { serviceStatusToSeverity } from './ServiceStatusChip' export type ClusterStatusTabKey = 'HEALTHY' | 'UNHEALTHY' | 'ALL' +export type UpgradeableFilterKey = 'ALL' | 'UPGRADEABLE' | 'NON-UPGRADEABLE' export const statusTabs = Object.entries({ ALL: { label: 'All' }, HEALTHY: { @@ -46,20 +58,28 @@ export function ClustersFilters({ setSelectedTagKeys, tagOp, setTagOp, + upgradeableFilter, + setUpgradeableFilter, + upgradeStats, }: { setQueryStatusFilter: Dispatch> setQueryString: (string) => void - tabStateRef: MutableRefObject + tabStateRef: RefObject statusCounts: Record selectedTagKeys: ComponentProps['selectedTagKeys'] setSelectedTagKeys: ComponentProps['setSelectedTagKeys'] tagOp: ComponentProps['searchOp'] setTagOp: ComponentProps['setSearchOp'] + upgradeableFilter: UpgradeableFilterKey + setUpgradeableFilter: (val: UpgradeableFilterKey) => void + upgradeStats: Nullable> }) { const [searchString, setSearchString] = useState('') const debouncedSearchString = useDebounce(searchString, 400) const [statusFilter, setStatusFilter] = useState('ALL') const deferredStatusFilter = useDeferredValue(statusFilter) + const { count, upgradeable } = upgradeStats ?? {} + const hasUpgradeStats = isNumber(count) && isNumber(upgradeable) useEffect(() => { setQueryString(debouncedSearchString) @@ -79,7 +99,7 @@ export function ClustersFilters({ setSearchOp={setTagOp} /> -
+
} @@ -118,6 +138,35 @@ export function ClustersFilters({ ))} +
+ +
) } diff --git a/assets/src/generated/graphql.ts b/assets/src/generated/graphql.ts index 3bf311705..dc1a73c9e 100644 --- a/assets/src/generated/graphql.ts +++ b/assets/src/generated/graphql.ts @@ -10189,10 +10189,11 @@ export type ClustersQueryVariables = Exact<{ healthy?: InputMaybe; tagQuery?: InputMaybe; projectId?: InputMaybe; + upgradeable?: InputMaybe; }>; -export type ClustersQuery = { __typename?: 'RootQueryType', tags?: Array | null, clusters?: { __typename?: 'ClusterConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'ClusterEdge', node?: { __typename?: 'Cluster', currentVersion?: string | null, id: string, self?: boolean | null, healthy?: boolean | null, protect?: boolean | null, name: string, handle?: string | null, distro?: ClusterDistro | null, installed?: boolean | null, pingedAt?: string | null, deletedAt?: string | null, version?: string | null, kubeletVersion?: string | null, virtual?: boolean | null, metricsSummary?: { __typename?: 'ClusterMetricsSummary', cpuUsed?: number | null, cpuAvailable?: number | null, cpuTotal?: number | null, memoryUsed?: number | null, memoryAvailable?: number | null, memoryTotal?: number | null } | null, provider?: { __typename?: 'ClusterProvider', id: string, cloud: string, name: string, namespace: string, supportedVersions?: Array | null } | null, prAutomations?: Array<{ __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, repository?: { __typename?: 'GitRepository', url: string, refs?: Array | null } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null> | null, service?: { __typename?: 'ServiceDeployment', id: string, repository?: { __typename?: 'GitRepository', url: string } | null } | null, tags?: Array<{ __typename?: 'Tag', name: string, value: string } | null> | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, insight?: { __typename?: 'AiInsight', id: string, summary?: string | null, freshness?: InsightFreshness | null, insertedAt?: string | null, updatedAt?: string | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null } | null } | null> | null } | null, clusterStatuses?: Array<{ __typename?: 'ClusterStatusInfo', count?: number | null, healthy?: boolean | null } | null> | null }; +export type ClustersQuery = { __typename?: 'RootQueryType', tags?: Array | null, clusters?: { __typename?: 'ClusterConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'ClusterEdge', node?: { __typename?: 'Cluster', currentVersion?: string | null, id: string, self?: boolean | null, healthy?: boolean | null, protect?: boolean | null, name: string, handle?: string | null, distro?: ClusterDistro | null, installed?: boolean | null, pingedAt?: string | null, deletedAt?: string | null, version?: string | null, kubeletVersion?: string | null, virtual?: boolean | null, metricsSummary?: { __typename?: 'ClusterMetricsSummary', cpuUsed?: number | null, cpuAvailable?: number | null, cpuTotal?: number | null, memoryUsed?: number | null, memoryAvailable?: number | null, memoryTotal?: number | null } | null, provider?: { __typename?: 'ClusterProvider', id: string, cloud: string, name: string, namespace: string, supportedVersions?: Array | null } | null, prAutomations?: Array<{ __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, repository?: { __typename?: 'GitRepository', url: string, refs?: Array | null } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null> | null, service?: { __typename?: 'ServiceDeployment', id: string, repository?: { __typename?: 'GitRepository', url: string } | null } | null, tags?: Array<{ __typename?: 'Tag', name: string, value: string } | null> | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, insight?: { __typename?: 'AiInsight', id: string, summary?: string | null, freshness?: InsightFreshness | null, insertedAt?: string | null, updatedAt?: string | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null } | null } | null> | null } | null, clusterStatuses?: Array<{ __typename?: 'ClusterStatusInfo', count?: number | null, healthy?: boolean | null } | null> | null, upgradeStatistics?: { __typename?: 'UpgradeStatistics', upgradeable?: number | null, count?: number | null } | null }; export type ClusterMinimalFragment = { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null }; @@ -17687,7 +17688,7 @@ export type DeleteCatalogMutationHookResult = ReturnType; export type DeleteCatalogMutationOptions = Apollo.BaseMutationOptions; export const ClustersDocument = gql` - query Clusters($first: Int, $after: String, $q: String, $healthy: Boolean, $tagQuery: TagQuery, $projectId: ID) { + query Clusters($first: Int, $after: String, $q: String, $healthy: Boolean, $tagQuery: TagQuery, $projectId: ID, $upgradeable: Boolean) { clusters( first: $first after: $after @@ -17695,6 +17696,7 @@ export const ClustersDocument = gql` healthy: $healthy tagQuery: $tagQuery projectId: $projectId + upgradeable: $upgradeable ) { pageInfo { ...PageInfo @@ -17705,9 +17707,13 @@ export const ClustersDocument = gql` } } } - clusterStatuses { + clusterStatuses(q: $q, projectId: $projectId) { ...ClusterStatusInfo } + upgradeStatistics(q: $q, projectId: $projectId) { + upgradeable + count + } tags } ${PageInfoFragmentDoc} @@ -17732,6 +17738,7 @@ ${ClusterStatusInfoFragmentDoc}`; * healthy: // value for 'healthy' * tagQuery: // value for 'tagQuery' * projectId: // value for 'projectId' + * upgradeable: // value for 'upgradeable' * }, * }); */ diff --git a/assets/src/graph/cdClusters.graphql b/assets/src/graph/cdClusters.graphql index d2fcdc517..1b74034e2 100644 --- a/assets/src/graph/cdClusters.graphql +++ b/assets/src/graph/cdClusters.graphql @@ -361,6 +361,7 @@ query Clusters( $healthy: Boolean $tagQuery: TagQuery $projectId: ID + $upgradeable: Boolean ) { clusters( first: $first @@ -369,6 +370,7 @@ query Clusters( healthy: $healthy tagQuery: $tagQuery projectId: $projectId + upgradeable: $upgradeable ) { pageInfo { ...PageInfo @@ -379,9 +381,13 @@ query Clusters( } } } - clusterStatuses { + clusterStatuses(q: $q, projectId: $projectId) { ...ClusterStatusInfo } + upgradeStatistics(q: $q, projectId: $projectId) { + upgradeable + count + } tags } diff --git a/assets/src/utils/datetime.ts b/assets/src/utils/datetime.ts index 0f959aa3a..8682f2395 100644 --- a/assets/src/utils/datetime.ts +++ b/assets/src/utils/datetime.ts @@ -79,13 +79,18 @@ dayjs.extend(localizedFormat) export { dayjs as dayjsExtended } -export const formatDateTime = (date: DateParam, pattern?: string) => { +export const formatDateTime = ( + date: DateParam, + pattern?: string, + isUtc: boolean = false +) => { if (!date) return '' - if (pattern) return dayjs(date).format(pattern) + const dateObj = isUtc ? dayjs(date).utc(true) : dayjs(date) + if (pattern) return dateObj.format(pattern) - if (isSameDay(date)) return dayjs(date).format('h:mm a') + if (isSameDay(date)) return dateObj.format('h:mm a') - return dayjs(date).format('MMM D, YYYY h:mm a') + return dateObj.format('MMM D, YYYY h:mm a') } export const toISOStringOrUndef = (date: DateParam, isUtc: boolean = false) => {