diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..82961d7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,15 @@ +# Lines starting with '#' are comments. +# Each line is a file pattern followed by one or more owners. + +# More details are here: https://help.github.com/articles/about-codeowners/ + +# The '*' pattern is global owners. + +# Order is important. The last matching pattern has the most precedence. +# The folders are ordered as follows: + +# In each subsection folders are ordered first by depth, then alphabetically. +# This should make it easy to add new rules without breaking existing ones. + +# Global rule: +* @abe21412 @rodrig67 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..938f8ca --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 The Home Depot + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/spin-observatory-plugin-deck/src/components/PluginContainer.tsx b/spin-observatory-plugin-deck/src/components/PluginContainer.tsx index 95a5d22..3358ae9 100644 --- a/spin-observatory-plugin-deck/src/components/PluginContainer.tsx +++ b/spin-observatory-plugin-deck/src/components/PluginContainer.tsx @@ -4,9 +4,11 @@ import React, { useEffect, useState } from 'react'; import type { Application, IPipeline } from '@spinnaker/core'; import { ReactSelectInput, useDataSource } from '@spinnaker/core'; +import type { IDateRange } from './date-picker/date-picker'; import { DatePicker } from './date-picker/date-picker'; import { ParameterSelect } from './parameters'; -import { PipelineExecutions, STATUSES } from './pipelines'; +import { MAX_DATE_RANGE, PipelineExecutions } from './pipelines'; +import { StatusSelect } from './status'; interface IPluginContainerProps { app: Application; @@ -17,6 +19,9 @@ export function PluginContainer({ app }: IPluginContainerProps) { const { data: pipelines } = useDataSource(dataSource); const [selectedPipeline, setSelectedPipeline] = useState(); const [selectedParams, setSelectedParams] = useState([]); + const [selectedDateRange, setSelectedDateRange] = useState({ start: 0, end: MAX_DATE_RANGE }); + const [selectedStatus, setSelectedStatus] = useState([]); + const [statusCount, setStatusCount] = useState>(new Map()); useEffect(() => { dataSource.activate(); @@ -25,12 +30,17 @@ export function PluginContainer({ app }: IPluginContainerProps) { const onPipelineSelect = async (e: ChangeEvent) => { const pipelineConfig = pipelines.find((p) => p.name === e.target.value); setSelectedParams([]); + setSelectedStatus([]); + setStatusCount(new Map()); setSelectedPipeline(pipelineConfig); }; - const handleDateFilterChange = ({ start, end }: { start: number, end: number }) => { - // eslint-disable-next-line no-console - console.log("filter executions ", { start, end }); + const handleDateFilterChange = ({ start, end }: { start: number; end: number }) => { + setSelectedDateRange({ start, end }); + }; + + const handleStatusCountChange = (statusCount: Map) => { + setStatusCount(statusCount); }; return ( @@ -46,37 +56,40 @@ export function PluginContainer({ app }: IPluginContainerProps) { options={pipelines.map((p) => ({ label: p.name, value: p.name }))} /> -
-
+
+
- + +
+
- - - + {!selectedPipeline ? ( +

Please select a pipeline to view executions.

+ ) : ( + + )}
); diff --git a/spin-observatory-plugin-deck/src/components/date-picker/date-picker.tsx b/spin-observatory-plugin-deck/src/components/date-picker/date-picker.tsx index 81abedc..59ef00e 100644 --- a/spin-observatory-plugin-deck/src/components/date-picker/date-picker.tsx +++ b/spin-observatory-plugin-deck/src/components/date-picker/date-picker.tsx @@ -1,18 +1,13 @@ -import React, { useEffect, useState } from "react"; -import { - Button, - TextField, - MenuItem, - Menu, - ListSubheader, - PopoverProps -} from "@material-ui/core"; +import DateFnsUtils from '@date-io/date-fns'; +import type { PopoverProps } from '@material-ui/core'; +import { Button, ListSubheader, Menu, MenuItem, TextField } from '@material-ui/core'; +import { KeyboardDateTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'; +import React, { useEffect, useState } from 'react'; -import { - MuiPickersUtilsProvider, - KeyboardDateTimePicker -} from "@material-ui/pickers"; -import DateFnsUtils from "@date-io/date-fns"; +export interface IDateRange { + start: number; + end: number; +} const START_AND_END_BY_HOURS = ({ hours = 1 }) => { const now = new Date(); @@ -22,35 +17,35 @@ const START_AND_END_BY_HOURS = ({ hours = 1 }) => { const PREMADE_SELECTIONS = [ { - value: "hour", - text: "Last Hour", - calculation: () => START_AND_END_BY_HOURS({ hours: 1 }) + value: 'hour', + text: 'Last Hour', + calculation: () => START_AND_END_BY_HOURS({ hours: 1 }), }, { - value: "day", - text: "Last 24 Hours", - calculation: () => START_AND_END_BY_HOURS({ hours: 24 }) + value: 'day', + text: 'Last 24 Hours', + calculation: () => START_AND_END_BY_HOURS({ hours: 24 }), }, { - value: "week", - text: "Last 7 Days", - calculation: () => START_AND_END_BY_HOURS({ hours: 24 * 7 }) + value: 'week', + text: 'Last 7 Days', + calculation: () => START_AND_END_BY_HOURS({ hours: 24 * 7 }), }, { - value: "month", - text: "Last 30 Days", - calculation: () => START_AND_END_BY_HOURS({ hours: 24 * 30 }) - } + value: 'month', + text: 'Last 30 Days', + calculation: () => START_AND_END_BY_HOURS({ hours: 24 * 30 }), + }, ]; type DatePickerProps = { - disabled?: boolean, - onChange: ({ start, end }: { start?: number, end?: number }) => void + disabled?: boolean; + onChange: ({ start, end }: { start?: number; end?: number }) => void; }; -export const DatePicker = ({ disabled, onChange }:DatePickerProps) => { +export const DatePicker = ({ disabled, onChange }: DatePickerProps) => { const [anchorEl, setAnchorEl] = useState(null); - const [value, setValue] = useState(""); + const [value, setValue] = useState(''); const [selectedCustomStart, setSelectedCustomStart] = useState(new Date()); const [selectedCustomEnd, setSelectedCustomEnd] = useState(new Date()); @@ -72,7 +67,7 @@ export const DatePicker = ({ disabled, onChange }:DatePickerProps) => { }; const handleMenuItemClick = (newValue: string) => { - const item = PREMADE_SELECTIONS.find(i => i.value === newValue); + const item = PREMADE_SELECTIONS.find((i) => i.value === newValue); setValue(item.text); onChange(item.calculation()); handleClose(); @@ -87,9 +82,7 @@ export const DatePicker = ({ disabled, onChange }:DatePickerProps) => { }; const updateValueWithCustomDateRange = () => { - setValue( - `${selectedCustomStart.toLocaleDateString()} - ${selectedCustomEnd.toLocaleDateString()}` - ); + setValue(`${selectedCustomStart.toLocaleDateString()} - ${selectedCustomEnd.toLocaleDateString()}`); handleClose(); }; @@ -100,35 +93,24 @@ export const DatePicker = ({ disabled, onChange }:DatePickerProps) => { disabled={disabled} select fullWidth - style={disabled ? {} : { backgroundColor: "var(--color-white)" }} + style={disabled ? {} : { backgroundColor: 'var(--color-white)' }} size="small" label="Filter Date Range" variant="outlined" value={value} onClick={handleClick} InputProps={{ - readOnly: true + readOnly: true, }} > {PREMADE_SELECTIONS.map((option) => { return {option.text}; })} - {PREMADE_SELECTIONS.findIndex((p) => p.value === value) === -1 && ( - {value} - )} + {PREMADE_SELECTIONS.findIndex((p) => p.value === value) === -1 && {value}} - + {PREMADE_SELECTIONS.map((option) => { - return ( - handleMenuItemClick(option.value)}> - {option.text} - - ); + return handleMenuItemClick(option.value)}>{option.text}; })} Custom @@ -138,7 +120,7 @@ export const DatePicker = ({ disabled, onChange }:DatePickerProps) => { value={selectedCustomStart} onChange={handleStartDateChange} KeyboardButtonProps={{ - "aria-label": "change start date" + 'aria-label': 'change start date', }} /> { value={selectedCustomEnd} onChange={handleEndDateChange} KeyboardButtonProps={{ - "aria-label": "change end date" + 'aria-label': 'change end date', }} /> @@ -156,4 +138,4 @@ export const DatePicker = ({ disabled, onChange }:DatePickerProps) => { ); -} +}; diff --git a/spin-observatory-plugin-deck/src/components/pipelines/ExecutionRow.tsx b/spin-observatory-plugin-deck/src/components/pipelines/ExecutionRow.tsx index 826b5b9..33b9d26 100644 --- a/spin-observatory-plugin-deck/src/components/pipelines/ExecutionRow.tsx +++ b/spin-observatory-plugin-deck/src/components/pipelines/ExecutionRow.tsx @@ -1,7 +1,10 @@ -import { TableCell, TableRow, Typography, Checkbox } from '@material-ui/core'; -import React, { MouseEventHandler } from 'react'; -import { IExecution, ReactInjector } from '@spinnaker/core'; +import { Checkbox, TableCell, TableRow, Typography } from '@material-ui/core'; import { makeStyles } from '@material-ui/core'; +import type { MouseEventHandler } from 'react'; +import React from 'react'; + +import type { IExecution } from '@spinnaker/core'; +import { ReactInjector } from '@spinnaker/core'; const useStyles = makeStyles({ tableRow: { @@ -17,7 +20,6 @@ interface IExecutionRowProps { parameters: string[]; onSelectOne: MouseEventHandler; isSelected: boolean; - inProgress: boolean; } const goToExecutionDetails = (executionId: string) => () => { @@ -27,6 +29,10 @@ const goToExecutionDetails = (executionId: string) => () => { }; const convertTimestamp = (ts: number) => { + if (!ts) { + return ''; + } + return new Intl.DateTimeFormat('en-US', { year: '2-digit', month: 'numeric', @@ -37,7 +43,7 @@ const convertTimestamp = (ts: number) => { }).format(ts); }; -export const ExecutionRow = ({ execution, parameters, onSelectOne, isSelected, inProgress }: IExecutionRowProps) => { +export const ExecutionRow = ({ execution, parameters, onSelectOne, isSelected }: IExecutionRowProps) => { const styles = useStyles(); return ( {convertTimestamp(execution.startTime)} - {!inProgress && ( - - {convertTimestamp(execution.endTime)} - - )} + + {convertTimestamp(execution.endTime)} + {parameters.map((p) => ( {execution.trigger.parameters![p]} diff --git a/spin-observatory-plugin-deck/src/components/pipelines/ExecutionsContainer.tsx b/spin-observatory-plugin-deck/src/components/pipelines/ExecutionsContainer.tsx deleted file mode 100644 index 33730fa..0000000 --- a/spin-observatory-plugin-deck/src/components/pipelines/ExecutionsContainer.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React, { useState, ReactNode } from 'react'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import { Accordion, AccordionSummary, Typography, AccordionDetails } from '@material-ui/core'; -import Skeleton from '@material-ui/lab/Skeleton'; -import { makeStyles } from '@material-ui/core'; - -const useStyles = makeStyles({ - skeleton: { padding: '3rem', marginLeft: '2rem', marginRight: '2rem' }, - accordion: { marginBottom: '1rem' }, -}); - -interface IExecutionContainerProps { - loading: boolean; - heading: string; - children: NonNullable; -} - -export const ExecutionsContainer = ({ loading, heading, children }: IExecutionContainerProps) => { - const [expanded, setExpanded] = useState(false); - const styles = useStyles(); - - const onAccordionClick = () => { - setExpanded(!expanded); - }; - - return ( - - } onClick={onAccordionClick}> - {heading} - - {loading ? ( - [...Array(4).keys()].map((key) => ( - - )) - ) : ( - {children} - )} - - ); -}; diff --git a/spin-observatory-plugin-deck/src/components/pipelines/ExecutionsTable.tsx b/spin-observatory-plugin-deck/src/components/pipelines/ExecutionsTable.tsx index 5c715f4..5ccb433 100644 --- a/spin-observatory-plugin-deck/src/components/pipelines/ExecutionsTable.tsx +++ b/spin-observatory-plugin-deck/src/components/pipelines/ExecutionsTable.tsx @@ -19,8 +19,7 @@ import { ExecutionRow } from './ExecutionRow'; import { PaginationActions } from './PaginationActions'; import { TableHeaders } from './TableHeaders'; import { ActionButtonsContainer, PauseResumeButton, RetriggerButton } from '../actions'; -import type { IStatus } from './constants'; -import { DEFAULT_ROWS_PER_PAGE, STATUSES } from './constants'; +import { DEFAULT_ROWS_PER_PAGE } from './constants'; const useStyles = makeStyles({ tableContainer: { borderRadius: 'inherit' }, @@ -30,20 +29,16 @@ const useStyles = makeStyles({ interface IExecutionsTableProps { executions: IExecution[]; parameters: string[]; - status: IStatus; refreshExecutions: () => void; } -export const ExecutionsTable = ({ executions, parameters, status, refreshExecutions }: IExecutionsTableProps) => { +export const ExecutionsTable = ({ executions, parameters, refreshExecutions }: IExecutionsTableProps) => { const [selectedExecutionIds, setSelectedExecutionIds] = useState([]); const [currentPage, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(DEFAULT_ROWS_PER_PAGE); const styles = useStyles(); - const headers = - status === STATUSES.TRIGGERED - ? ['ID', 'Status', 'Start Time', ...parameters] - : ['ID', 'Status', 'Start Time', 'End Time', ...parameters]; + const headers = ['ID', 'Status', 'Start Time', 'End Time', ...parameters]; const handlePageChange = (_: any, newPage: number) => { setPage(newPage); @@ -93,7 +88,6 @@ export const ExecutionsTable = ({ executions, parameters, status, refreshExecuti isSelected={isSelected(e.id)} execution={e} parameters={parameters} - inProgress={status === STATUSES.TRIGGERED} onSelectOne={handleSelectOne(e.id)} /> ))} @@ -102,9 +96,7 @@ export const ExecutionsTable = ({ executions, parameters, status, refreshExecuti - {status === STATUSES.TRIGGERED && ( - - )} + diff --git a/spin-observatory-plugin-deck/src/components/pipelines/PipelineExecutions.tsx b/spin-observatory-plugin-deck/src/components/pipelines/PipelineExecutions.tsx index 06d9deb..442bf4d 100644 --- a/spin-observatory-plugin-deck/src/components/pipelines/PipelineExecutions.tsx +++ b/spin-observatory-plugin-deck/src/components/pipelines/PipelineExecutions.tsx @@ -1,53 +1,135 @@ +import { makeStyles } from '@material-ui/core'; +import Skeleton from '@material-ui/lab/Skeleton'; import React, { useEffect, useState } from 'react'; -import { IExecution, useInterval } from '@spinnaker/core'; -import { IStatus, POLL_DELAY_MS, REQUEST_PAGE_SIZE } from './constants'; -import { getExecutions } from '../../services/gateService'; -import { ExecutionsContainer } from './ExecutionsContainer'; + +import type { IExecution, IPipeline } from '@spinnaker/core'; +import { useInterval } from '@spinnaker/core'; + import { ExecutionsTable } from './ExecutionsTable'; +import { POLL_DELAY_MS, REQUEST_PAGE_SIZE } from './constants'; +import type { IDateRange } from '../date-picker/date-picker'; +import { getExecutions } from '../../services/gateService'; +import { STATUSES } from '../status'; + +const useStyles = makeStyles({ + skeleton: { padding: '3rem', marginLeft: '2rem', marginRight: '2rem' }, +}); interface IPipelineExecutionsProps { appName: string; - pipelineName: string; + pipeline?: IPipeline; parameters: string[]; - status: IStatus; + statuses: string[]; + dateRange: IDateRange; + onStatusChange: (statusCount: Map) => void; } -export const PipelineExecutions = ({ appName, pipelineName, parameters, status }: IPipelineExecutionsProps) => { +export const PipelineExecutions = ({ + appName, + pipeline, + parameters, + statuses, + dateRange, + onStatusChange, +}: IPipelineExecutionsProps) => { const [executions, setExecutions] = useState([]); + const [filteredExecutions, setFilteredExecutions] = useState([]); + const [statusCount, setStatusCount] = useState>(new Map()); + const [isLoading, setIsLoading] = useState(true); + const styles = useStyles(); const getExecutionsParams = { - pipelineName, + pipelineName: pipeline.name, pageSize: REQUEST_PAGE_SIZE, - statuses: status.values, + startDate: dateRange.start, + endDate: dateRange.end, }; const refreshExecutions = () => { - console.log('refreshing executions'); getExecutions(appName, getExecutionsParams).then((resp) => setExecutions(resp)); }; useEffect(() => { - if (!pipelineName) { + if (!pipeline.name) { setExecutions([]); + setFilteredExecutions([]); + setStatusCount(new Map()); + setIsLoading(false); return; } - refreshExecutions(); - }, [pipelineName]); + const requestParams = { + pipelineName: pipeline.name, + pageSize: REQUEST_PAGE_SIZE, + startDate: dateRange.start, + endDate: dateRange.end, + }; + + getExecutions(appName, requestParams).then((resp) => { + setExecutions(resp); + setFilteredExecutions(filterExecutions(resp)); + setStatusCount(getStatusCount(resp)); + setIsLoading(false); + }); + }, [pipeline]); + + useEffect(() => { + onStatusChange(statusCount); + }, [statusCount]); + + useEffect(() => { + setFilteredExecutions(filterExecutions(executions)); + }, [statuses]); + + useInterval(async () => { + if (!pipeline) return; + const resp = await getExecutions(appName, { + pipelineName: pipeline.name, + pageSize: REQUEST_PAGE_SIZE, + startDate: dateRange.start, + endDate: dateRange.end, + }); - useInterval(() => { - if (!pipelineName) return; - refreshExecutions(); + setExecutions(resp); + setFilteredExecutions(filterExecutions(resp)); + setStatusCount(getStatusCount(resp)); + setIsLoading(false); }, POLL_DELAY_MS); + const filterExecutions = (ex: IExecution[]) => { + const statusArr = statuses.length === 0 ? STATUSES : statuses; + + return ex.filter((e) => statusArr.includes(e.status)); + }; + + const getStatusCount = (ex: IExecution[]) => { + const statusCount = new Map(); + for (const e of ex) { + if (!statusCount.has(e.status)) { + statusCount.set(e.status, 1); + } else { + statusCount.set(e.status, statusCount.get(e.status) + 1); + } + } + + return statusCount; + }; + + if (isLoading) { + return ( +
+ {[...Array(3).keys()].map((key) => ( + + ))} +
+ ); + } + + if (executions.length == 0) { + return

No pipeline executions found.

; + } + return ( - - - + ); }; diff --git a/spin-observatory-plugin-deck/src/components/pipelines/constants.ts b/spin-observatory-plugin-deck/src/components/pipelines/constants.ts index 1d63a80..ded7c25 100644 --- a/spin-observatory-plugin-deck/src/components/pipelines/constants.ts +++ b/spin-observatory-plugin-deck/src/components/pipelines/constants.ts @@ -1,25 +1,7 @@ -export interface IStatus { - text: string; - values: string[]; -} - -export const STATUSES = { - SUCCESSFUL: { - text: 'Successful', - values: ['SUCCEEDED'], - }, - FAILED: { - text: 'Failed', - values: ['FAILED_CONTINUE', 'TERMINAL', 'CANCELED'], - }, - TRIGGERED: { - text: 'Triggered', - values: ['NOT_STARTED', 'RUNNING', 'PAUSED', 'SUSPENDED', 'BUFFERED', 'STOPPED', 'SKIPPED'], - }, -}; - export const REQUEST_PAGE_SIZE = 5000; export const DEFAULT_ROWS_PER_PAGE = 10; export const POLL_DELAY_MS = 10000; + +export const MAX_DATE_RANGE = 9007199254740991; diff --git a/spin-observatory-plugin-deck/src/components/status/StatusSelect.tsx b/spin-observatory-plugin-deck/src/components/status/StatusSelect.tsx new file mode 100644 index 0000000..2df8c66 --- /dev/null +++ b/spin-observatory-plugin-deck/src/components/status/StatusSelect.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import type { Option } from 'react-select'; +import Select from 'react-select'; + +import type { IPipeline } from '@spinnaker/core'; + +export const STATUSES = [ + 'SUCCEEDED', + 'FAILED_CONTINUE', + 'TERMINAL', + 'CANCELED', + 'NOT_STARTED', + 'RUNNING', + 'PAUSED', + 'SUSPENDED', + 'BUFFERED', + 'STOPPED', + 'SKIPPED', + 'REDIRECT', +]; + +interface IStatusSelectProps { + className?: string; + pipeline?: IPipeline; + selectedStatus: string[]; + setSelectedStatus(params: string[]): void; + statusCount: Map; +} + +export const StatusSelect = ({ + className, + pipeline, + selectedStatus, + setSelectedStatus, + statusCount, +}: IStatusSelectProps) => { + const onStatusSelect = (options: Array>) => { + setSelectedStatus(options.map((o) => o.value)); + }; + + const extractStatus = (statusCount: Map): Array> => { + const options: Array> = []; + statusCount.forEach((value, key) => { + options.push({ label: `${key} (${value})`, value: key }); + }); + + return options; + }; + + return ( +