From 0c672374e44c4fbd802deabcbe0ed862505c914c Mon Sep 17 00:00:00 2001 From: David Butenhof Date: Fri, 22 Nov 2024 10:25:58 -0500 Subject: [PATCH] Add a metric label template mechanism The API makes an effort to create unique metric labels when comparing data across multiple runs, but without human direction the constructed names aren't necessarily ideal to help a human focus on the comparisons they want to make. This PR adds a UI template mechanism to allow the human to format names that are more useful -- for example, to focus on the differences between software releases or hardware configurations expressed by Crucible tags or params. For example, a template of ` : ` identifies metrics between two runs based on the hardware accelerator type used for each. This introduces a new action to fetch and store the available filters, which exposes the param and tag values currently in use. Usability of the template mechanism will depend on consistent application of specific tags, which we expect to be increasingly supplied automatically by Crucible discovery rather than relying on the (current) ad hoc user definition in endpoint files. --- frontend/src/actions/ilabActions.js | 291 ++++++++++++++--- frontend/src/actions/types.js | 2 + .../templates/ILab/IlabCompareComponent.jsx | 4 + .../components/templates/ILab/MetricTitle.jsx | 293 ++++++++++++++++++ .../src/components/templates/ILab/index.jsx | 2 + frontend/src/reducers/ilabReducer.js | 6 + publish-containers.sh | 17 +- 7 files changed, 563 insertions(+), 52 deletions(-) create mode 100644 frontend/src/components/templates/ILab/MetricTitle.jsx diff --git a/frontend/src/actions/ilabActions.js b/frontend/src/actions/ilabActions.js index 194a19b..9cce0be 100644 --- a/frontend/src/actions/ilabActions.js +++ b/frontend/src/actions/ilabActions.js @@ -1,11 +1,16 @@ import * as API_ROUTES from "@/utils/apiConstants"; import * as TYPES from "./types.js"; +import { showFailureToast } from "@/actions/toastActions"; import API from "@/utils/axiosInstance"; import { appendQueryString } from "@/utils/helper"; import { cloneDeep } from "lodash"; -import { showFailureToast } from "@/actions/toastActions"; +/** + * Fetch and store InstructLab jobs based on configured filters. + * + * @param {boolean} [shouldStartFresh=false] + */ export const fetchILabJobs = (shouldStartFresh = false) => async (dispatch, getState) => { @@ -53,7 +58,14 @@ export const fetchILabJobs = dispatch(showFailureToast()); } dispatch({ type: TYPES.COMPLETED }); - }; + }; + +/** + * Isolate the current page of cached jobs. + * + * @param {number} startIdx + * @param {number} endIdx + */ export const sliceIlabTableRows = (startIdx, endIdx) => (dispatch, getState) => { const results = [...getState().ilab.results]; @@ -63,6 +75,16 @@ export const sliceIlabTableRows = payload: results.slice(startIdx, endIdx), }); }; + + +/** + * Store the start & end date filters in redux and as URL + * query parameters for page reload. + * + * @param {string} start_date + * @param {string} end_date + * @param {React NavigateFunction} navigate + */ export const setIlabDateFilter = (start_date, end_date, navigate) => (dispatch, getState) => { const appliedFilters = getState().ilab.appliedFilters; @@ -78,6 +100,24 @@ export const setIlabDateFilter = appendQueryString({ ...appliedFilters, start_date, end_date }, navigate); }; +/** + * Fetch the set of possible InstructLab run filters and store them. + */ +export const fetchIlabFilters = () => async (dispatch, getState) => { + try { + const response = await API.get(`/api/v1/ilab/runs/filters`); + dispatch({ type: TYPES.SET_ILAB_RUN_FILTERS, payload: response.data }); + } catch (error) { + console.error(error); + dispatch(showFailureToast()); + } +}; + +/** + * Fetch the recorded metrics for a specific run and store them. + * + * @param {string} uid of a run + */ export const fetchMetricsInfo = (uid) => async (dispatch, getState) => { try { if (getState().ilab.metrics?.find((i) => i.uid == uid)) { @@ -104,6 +144,11 @@ export const fetchMetricsInfo = (uid) => async (dispatch, getState) => { dispatch({ type: TYPES.COMPLETED }); }; +/** + * Fetch the recording periods for a specific run and store them. + * + * @param {string} uid of a run + */ export const fetchPeriods = (uid) => async (dispatch, getState) => { try { if (getState().ilab.periods?.find((i) => i.uid == uid)) { @@ -128,57 +173,139 @@ export const fetchPeriods = (uid) => async (dispatch, getState) => { dispatch({ type: TYPES.COMPLETED }); }; -export const fetchSummaryData = (uid) => async (dispatch, getState) => { +/** + * Construct a metric title based on the metric template store. + * + * @param {string} uid of a run + * @returns {string} metric title based on the template + */ +const makeTitle = (run, period, metric, template) => { + if (!template) { + return null; + } try { - const periods = getState().ilab.periods.find((i) => i.uid == uid); - const metrics = getState().ilab.metrics_selected; - const avail_metrics = getState().ilab.metrics; - dispatch({ type: TYPES.SET_ILAB_SUMMARY_LOADING }); - let summaries = []; - periods?.periods?.forEach((p) => { - if (p.is_primary) { - summaries.push({ - run: uid, - metric: p.primary_metric, - periods: [p.id], - }); - } - if (metrics) { - metrics.forEach((metric) => { - if ( - avail_metrics.find((m) => m.uid == uid)?.metrics?.includes(metric) - ) { - summaries.push({ - run: uid, - metric, - aggregate: true, - periods: [p.id], - }); + const chip = /<((?
\w+):)?(?\w+)>/; + const params = run?.iterations?.find( + (i) => i.iteration === period.iteration + )?.params; + let title = ""; + for (const t of template) { + const ctx = chip.exec(t); + if (ctx !== null) { + const section = ctx.groups.section; + const name = ctx.groups.name; + if (!section) { + if (name === "metric") { + title += metric; + } else if (name == "iteration") { + title += period.iteration; + } else if (name === "period") { + title += period.name; } - }); + } else if (section === "run") { + title += run?.[name] || t; + } else if (section === "param") { + title += params?.[name] || t; + } else if (section === "tag") { + title += run.tags?.[name] || t; + } else { + title += ``; + } + } else { + title += t; } - }); - const response = await API.post( - `/api/v1/ilab/runs/multisummary`, - summaries - ); - if (response.status === 200) { - dispatch({ - type: TYPES.SET_ILAB_SUMMARY_DATA, - payload: { uid, data: response.data }, - }); } - } catch (error) { - console.error( - `ERROR (${error?.response?.status}): ${JSON.stringify( - error?.response?.data - )}` - ); - dispatch(showFailureToast()); + return title; + } catch (e) { + console.error(e); + throw e; } - dispatch({ type: TYPES.SET_ILAB_SUMMARY_COMPLETE }); }; +/** + * Fetch and store metric statistics for a run. + * + * This will iterate through all defined recording periods for the run, + * generating for each a request for statistics for each specified metric + * within the period's time range. In addition, each "primary period" will + * include a request for the period's "primary metric". + * + * To maximize store effectiveness, we store data for each run separately, + * and combine data from multiple runs in the comparison table as needed + * rather than combining runs in a single request. + * + * NOTE: we don't try to avoid a duplicate fetch operation for statistics + * because the API call depends on the set of metrics selected and not just + * the selected run. + * + * @param {string} uid of a run + * @param {boolean} [useTemplate=false] + */ +export const fetchSummaryData = + (uid, useTemplate = false) => + async (dispatch, getState) => { + try { + const run = getState().ilab.results.find((i) => i.id == uid); + const template = useTemplate ? getState().ilab.metricTemplate : null; + const periods = getState().ilab.periods.find((i) => i.uid == uid); + const metrics = getState().ilab.metrics_selected; + const avail_metrics = getState().ilab.metrics; + dispatch({ type: TYPES.SET_ILAB_SUMMARY_LOADING }); + let summaries = []; + periods?.periods?.forEach((p) => { + if (p.is_primary) { + summaries.push({ + run: uid, + metric: p.primary_metric, + periods: [p.id], + title: makeTitle(run, p, p.primary_metric, template), + }); + } + if (metrics) { + metrics.forEach((metric) => { + if ( + avail_metrics.find((m) => m.uid == uid)?.metrics?.includes(metric) + ) { + summaries.push({ + run: uid, + metric, + aggregate: true, + periods: [p.id], + title: makeTitle(run, p, metric, template), + }); + } + }); + } + }); + const response = await API.post( + `/api/v1/ilab/runs/multisummary`, + summaries + ); + if (response.status === 200) { + dispatch({ + type: TYPES.SET_ILAB_SUMMARY_DATA, + payload: { uid, data: response.data }, + }); + } + } catch (error) { + console.error( + `ERROR (${error?.response?.status}): ${JSON.stringify( + error?.response?.data + )}` + ); + dispatch(showFailureToast()); + } + dispatch({ type: TYPES.SET_ILAB_SUMMARY_COMPLETE }); + }; + +/** + * A helper to ensure that statistical summary data is available for a + * selected set of runs. In general we should expect that all periods have + * been fetched, but we make sure of that before fetching summary data for + * all specified runs. + * + * @param {Array[string]} run uids + */ export const handleSummaryData = (uids) => async (dispatch, getState) => { try { const periods = getState().ilab.periods; @@ -193,7 +320,7 @@ export const handleSummaryData = (uids) => async (dispatch, getState) => { ); await Promise.all( uids.map(async (uid) => { - await dispatch(fetchSummaryData(uid)); + await dispatch(fetchSummaryData(uid, true)); }) ); } catch (error) { @@ -202,6 +329,17 @@ export const handleSummaryData = (uids) => async (dispatch, getState) => { } }; +/** + * Fetch and store Plotly graph data for a specified run. + * + * This will iterate through all defined recording periods for the run, + * generating for each a graph request for each specified metric + * within the period's time range. In addition, each "primary period" will + * include a graph request for the period's "primary metric". + * + * @param {string} run uid + * @returns {(dispatch: any, getState: any) => any} + */ export const fetchGraphData = (uid) => async (dispatch, getState) => { try { const periods = getState().ilab.periods.find((i) => i.uid == uid); @@ -262,6 +400,13 @@ export const fetchGraphData = (uid) => async (dispatch, getState) => { dispatch({ type: TYPES.GRAPH_COMPLETED }); }; +/** + * A helper to ensure that graph data is available for a selected set of + * runs. In general we should expect that all periods have been fetched, but + * we make sure of that before fetching graph data for all specified runs. + * + * @param {Array[string]} run uids + */ export const handleMultiGraph = (uids) => async (dispatch, getState) => { try { const periods = getState().ilab.periods; @@ -287,6 +432,17 @@ export const handleMultiGraph = (uids) => async (dispatch, getState) => { dispatch(showFailureToast()); } }; + +/** + * Generate a single Plotly graph containing potentially multiple metrics from + * all periods of a set of runs. + * + * For each run, we iterate through the collection periods, generating a graph + * for each selected metric over that period's time range. For each "primary + * period" we also generate a graph for that period's "primary metric". + * + * @param {Array[string]} run uids + */ export const fetchMultiGraphData = (uids) => async (dispatch, getState) => { try { dispatch({ type: TYPES.LOADING }); @@ -294,9 +450,11 @@ export const fetchMultiGraphData = (uids) => async (dispatch, getState) => { const filterPeriods = periods.filter((item) => uids.includes(item.uid)); const get_metrics = getState().ilab.metrics_selected; const avail_metrics = getState().ilab.metrics; + const template = getState().ilab.metricTemplate; let graphs = []; uids.forEach(async (uid) => { + const run = getState().ilab.results.find((i) => i.id === uid); const periods = filterPeriods.find((i) => i.uid == uid); periods?.periods?.forEach((p) => { if (p.is_primary) { @@ -304,6 +462,7 @@ export const fetchMultiGraphData = (uids) => async (dispatch, getState) => { run: uid, metric: p.primary_metric, periods: [p.id], + title: makeTitle(run, p, p.primary_metric, template), }); } if (get_metrics) { @@ -316,6 +475,7 @@ export const fetchMultiGraphData = (uids) => async (dispatch, getState) => { metric, aggregate: true, periods: [p.id], + title: makeTitle(run, p, metric, template), }); } }); @@ -352,16 +512,32 @@ export const fetchMultiGraphData = (uids) => async (dispatch, getState) => { dispatch({ type: TYPES.COMPLETED }); }; +/** + * Store the current page number used to slice cached run data for display. + * + * @param {number} pageNo + */ export const setIlabPage = (pageNo) => ({ type: TYPES.SET_ILAB_PAGE, payload: pageNo, }); +/** + * Store an updated page size and page number. + * + * @param {number} page + * @param {number} perPage + */ export const setIlabPageOptions = (page, perPage) => ({ type: TYPES.SET_ILAB_PAGE_OPTIONS, payload: { page, perPage }, }); +/** + * Fetch and store a page of results if not already in the store. + * + * @param {number} newPage + */ export const checkIlabJobs = (newPage) => (dispatch, getState) => { const results = cloneDeep(getState().ilab.results); const { totalItems, perPage } = getState().ilab; @@ -378,6 +554,12 @@ export const checkIlabJobs = (newPage) => (dispatch, getState) => { } }; +/** + * Add a new metric to the selected list if not present, or remove it if it + * was previously present. + * + * @param {string} metric + */ export const toggleSelectedMetric = (metric) => (dispatch, getState) => { let metrics_selected = getState().ilab.metrics_selected; if (metrics_selected.includes(metric)) { @@ -391,6 +573,9 @@ export const toggleSelectedMetric = (metric) => (dispatch, getState) => { }); }; +/** + * Reconcile pagination and accordion state. + */ export const tableReCalcValues = () => (dispatch, getState) => { const { page, perPage } = getState().ilab; @@ -400,15 +585,27 @@ export const tableReCalcValues = () => (dispatch, getState) => { dispatch(getMetaRowdId()); }; +/** + * Help to manage the set of accordion folds that are open. + */ export const getMetaRowdId = () => (dispatch, getState) => { const tableData = getState().ilab.tableData; const metaId = tableData.map((item) => `metadata-toggle-${item.id}`); dispatch(setMetaRowExpanded(metaId)); }; + +/** + * Toggle the state of the comparison view. + */ export const toggleComparisonSwitch = () => ({ type: TYPES.TOGGLE_COMPARISON_SWITCH, }); +/** + * Store the set of expanded rows. + * + * @param {Array[string]} expandedItems list of currently expanded runs + */ export const setMetaRowExpanded = (expandedItems) => ({ type: TYPES.SET_EXPANDED_METAROW, payload: expandedItems, diff --git a/frontend/src/actions/types.js b/frontend/src/actions/types.js index 70a6787..031e560 100644 --- a/frontend/src/actions/types.js +++ b/frontend/src/actions/types.js @@ -93,5 +93,7 @@ export const SET_ILAB_METRICS = "SET_ILAB_METRICS"; export const SET_ILAB_SELECTED_METRICS = "SET_ILAB_SELECTED_METRICS"; export const SET_ILAB_PERIODS = "SET_ILAB_PERIODS"; export const SET_ILAB_INIT_JOBS = "SET_ILAB_INIT_JOBS"; +export const SET_ILAB_RUN_FILTERS = "SET_ILAB_RUN_FILTERS"; +export const SET_ILAB_METRIC_TEMPLATE = "SET_ILAB_METRIC_TEMPLATE"; export const TOGGLE_COMPARISON_SWITCH = "TOGGLE_COMPARISON_SWITCH"; export const SET_EXPANDED_METAROW = "SET_EXPANDED_METAROW"; diff --git a/frontend/src/components/templates/ILab/IlabCompareComponent.jsx b/frontend/src/components/templates/ILab/IlabCompareComponent.jsx index e81b6f6..4d4cbc6 100644 --- a/frontend/src/components/templates/ILab/IlabCompareComponent.jsx +++ b/frontend/src/components/templates/ILab/IlabCompareComponent.jsx @@ -29,6 +29,7 @@ import { useState } from "react"; import ILabSummary from "./ILabSummary"; import ILabMetadata from "./ILabMetadata"; import MetricsSelect from "./MetricsDropdown"; +import MetricTitle from "./MetricTitle"; const IlabCompareComponent = () => { const { page, perPage, totalItems, tableData } = useSelector( @@ -119,6 +120,9 @@ const IlabCompareComponent = () => { + + + {isSummaryLoading ? (
diff --git a/frontend/src/components/templates/ILab/MetricTitle.jsx b/frontend/src/components/templates/ILab/MetricTitle.jsx new file mode 100644 index 0000000..244065b --- /dev/null +++ b/frontend/src/components/templates/ILab/MetricTitle.jsx @@ -0,0 +1,293 @@ +import React from "react"; +import { + TextInputGroup, + TextInputGroupMain, + TextInputGroupUtilities, + Button, + Menu, + MenuContent, + MenuList, + MenuItem, + Popper, + Divider, + Label, + LabelGroup, +} from "@patternfly/react-core"; +import { useDispatch, useSelector } from "react-redux"; +import { useState } from "react"; +import SearchIcon from "@patternfly/react-icons/dist/esm/icons/search-icon"; +import TimesIcon from "@patternfly/react-icons/dist/esm/icons/times-icon"; +import * as TYPES from "@/actions/types.js"; + +export const MetricTitle = () => { + const dispatch = useDispatch(); + const [inputValue, setInputValue] = useState(""); + const [menuIsOpen, setMenuIsOpen] = useState(false); + const [hint, setHint] = useState(""); + + /** auto-completing suggestion text items to be shown in the menu */ + let suggestionItems = ["", "", ""]; + const { runFilters, metricTemplate } = useSelector((state) => state.ilab); + for (const s of Object.keys(runFilters).sort()) { + if (runFilters[s]) { + for (const n of Object.keys(runFilters[s]).sort()) { + suggestionItems.push(`<${s}:${n}>`); + } + } + } + const [menuItems, setMenuItems] = useState([]); + + /** refs used to detect when clicks occur inside vs outside of the textInputGroup and menu popper */ + const menuRef = React.useRef(); + const textInputGroupRef = React.useRef(); + + /** callback for updating the inputValue state in this component so that the input can be controlled */ + const handleInputChange = (_event, value) => { + setInputValue(value); + }; + + /** callback for removing a chip from the chip selections */ + const deleteChip = (chipToDelete) => { + const newChips = metricTemplate.filter( + (chip) => !Object.is(chip, chipToDelete) + ); + dispatch({ type: TYPES.SET_ILAB_METRIC_TEMPLATE, payload: newChips }); + }; + + /** callback for clearing all selected chips and the text input */ + const clearChipsAndInput = () => { + dispatch({ type: TYPES.SET_ILAB_METRIC_TEMPLATE, payload: [] }); + setInputValue(""); + }; + + React.useEffect(() => { + /** in the menu only show items that include the text in the input */ + const filteredMenuItems = suggestionItems + .filter( + (item) => + !inputValue || + item.toLowerCase().includes(inputValue.toString().toLowerCase()) + ) + .map((currentValue, index) => ( + + {currentValue} + + )); + + /** in the menu show a disabled "no result" when all menu items are filtered out */ + if (filteredMenuItems.length === 0) { + const noResultItem = ( + + No results found + + ); + setMenuItems([noResultItem]); + setHint(""); + return; + } + + /** The hint is set whenever there is only one autocomplete option left. */ + if (filteredMenuItems.length === 1) { + const hint = filteredMenuItems[0].props.children; + if (hint.toLowerCase().indexOf(inputValue.toLowerCase())) { + // the match was found in a place other than the start, so typeahead wouldn't work right + setHint(""); + } else { + // use the input for the first part, otherwise case difference could make things look wrong + setHint(inputValue + hint.substr(inputValue.length)); + } + } else { + setHint(""); + } + + /** add a heading to the menu */ + const headingItem = ( + + Suggestions + + ); + + const divider = ; + + setMenuItems([headingItem, divider, ...filteredMenuItems]); + }, [inputValue]); + + /** add the given string as a chip in the chip group and clear the input */ + const addChip = (newChipText) => { + dispatch({ + type: TYPES.SET_ILAB_METRIC_TEMPLATE, + payload: [...metricTemplate, newChipText], + }); + setInputValue(""); + }; + + /** add the current input value as a chip */ + const handleEnter = () => { + if (inputValue.length) { + addChip(inputValue); + } + }; + + const handleTab = () => { + if (menuItems.length === 3) { + setInputValue(menuItems[2].props.children); + } + setMenuIsOpen(false); + }; + + /** close the menu when escape is hit */ + const handleEscape = () => { + setMenuIsOpen(false); + }; + + /** allow the user to focus on the menu and navigate using the arrow keys */ + const handleArrowKey = () => { + if (menuRef.current) { + const firstElement = menuRef.current.querySelector( + "li > button:not(:disabled)" + ); + firstElement && firstElement.focus(); + } + }; + + /** reopen the menu if it's closed and any un-designated keys are hit */ + const handleDefault = () => { + if (!menuIsOpen) { + setMenuIsOpen(true); + } + }; + + /** enable keyboard only usage while focused on the text input */ + const handleTextInputKeyDown = (event) => { + switch (event.key) { + case "Enter": + handleEnter(); + break; + case "Escape": + handleEscape(); + break; + case "Tab": + handleTab(); + break; + case "ArrowUp": + case "ArrowDown": + handleArrowKey(); + break; + default: + handleDefault(); + } + }; + + /** apply focus to the text input */ + const focusTextInput = () => { + textInputGroupRef.current.querySelector("input").focus(); + }; + + /** add the text of the selected item as a new chip */ + const onSelect = (event, _itemId) => { + const selectedText = event.target.innerText; + addChip(selectedText); + event.stopPropagation(); + focusTextInput(); + }; + + /** close the menu when a click occurs outside of the menu or text input group */ + const handleClick = (event) => { + if ( + menuRef.current && + !menuRef.current.contains(event.target) && + !textInputGroupRef.current.contains(event.target) + ) { + setMenuIsOpen(false); + } + }; + + /** enable keyboard only usage while focused on the menu */ + const handleMenuKeyDown = (event) => { + switch (event.key) { + case "Tab": + case "Escape": + event.preventDefault(); + focusTextInput(); + setMenuIsOpen(false); + break; + case "Enter": + case " ": + setTimeout(() => setMenuIsOpen(false), 0); + break; + } + }; + + /** show the search icon only when there are no chips to prevent the chips from being displayed behind the icon */ + const showSearchIcon = !metricTemplate.length; + + /** only show the clear button when there is something that can be cleared */ + const showClearButton = !!inputValue || !!metricTemplate.length; + + /** render the utilities component only when a component it contains is being rendered */ + const showUtilities = showClearButton; + + const inputGroup = ( +
+ + } + value={inputValue} + hint={hint} + onChange={handleInputChange} + onFocus={() => setMenuIsOpen(true)} + onKeyDown={handleTextInputKeyDown} + placeholder="template" + aria-label="Metric title template" + > + + {metricTemplate.map((currentChip) => ( + + ))} + + + {showUtilities && ( + + {showClearButton && ( +
+ ); + + const menu = ( +
+ + + {menuItems} + + +
+ ); + + return ( + textInputGroupRef.current} + isVisible={menuIsOpen} + onDocumentClick={handleClick} + /> + ); +}; +export default MetricTitle; diff --git a/frontend/src/components/templates/ILab/index.jsx b/frontend/src/components/templates/ILab/index.jsx index c5bfc61..4de6ee8 100644 --- a/frontend/src/components/templates/ILab/index.jsx +++ b/frontend/src/components/templates/ILab/index.jsx @@ -10,6 +10,7 @@ import { Tr, } from "@patternfly/react-table"; import { + fetchIlabFilters, fetchILabJobs, fetchGraphData, fetchMetricsInfo, @@ -64,6 +65,7 @@ const ILab = () => { }; useEffect(() => { + dispatch(fetchIlabFilters()); if (searchParams.size > 0) { // date filter is set apart const startDate = searchParams.get("start_date"); diff --git a/frontend/src/reducers/ilabReducer.js b/frontend/src/reducers/ilabReducer.js index 46d39df..655fae5 100644 --- a/frontend/src/reducers/ilabReducer.js +++ b/frontend/src/reducers/ilabReducer.js @@ -2,6 +2,8 @@ import * as TYPES from "@/actions/types"; const initialState = { results: [], + runFilters: {}, + metricTemplate: [], start_date: "", end_date: "", graphData: [], @@ -76,6 +78,10 @@ const ILabReducer = (state = initialState, action = {}) => { payload, ], }; + case TYPES.SET_ILAB_RUN_FILTERS: + return { ...state, runFilters: payload }; + case TYPES.SET_ILAB_METRIC_TEMPLATE: + return { ...state, metricTemplate: payload }; default: return state; } diff --git a/publish-containers.sh b/publish-containers.sh index ca97f8b..4a2dca9 100755 --- a/publish-containers.sh +++ b/publish-containers.sh @@ -17,23 +17,30 @@ REGISTRY=${REGISTRY:-images.paas.redhat.com} ACCOUNT=${ACCOUNT:-${USER}} REPO=${REPO:-cpt} -TAG=${TAG:-latest} SKIP_FRONTEND=${SKIP_FRONTEND:-0} SKIP_BACKEND=${SKIP_BACKEND:-0} -REPOSITORY=${REGISTRY}/${ACCOUNT}/${REPO} +REPOSITORY="${REGISTRY}/${ACCOUNT}/${REPO}" + +TAG=${TAG:-latest} +BRANCH=$(git symbolic-ref --short HEAD) +SHA1=$(git rev-parse --short=9 HEAD) +REVISION="${BRANCH}-${SHA1}" +TIMESTAMP=$(date +'%Y%m%d-%H%M%S') + # remove current images, if any podman rm -f front back -now=$(date +'%Y%m%d-%H%M%S') if [ "$SKIP_BACKEND" != 1 ] ;then podman build -f backend/backend.containerfile --tag backend podman push backend "${REPOSITORY}/backend:${TAG}" - podman push backend "${REPOSITORY}/backend:${now}" + podman push backend "${REPOSITORY}/backend:${TIMESTAMP}" + podman push backend "${REPOSITORY}/backend:${REVISION}" fi if [ "$SKIP_FRONTEND" != 1 ] ;then podman build -f frontend/frontend.containerfile --tag frontend podman push frontend "${REPOSITORY}/frontend:${TAG}" - podman push frontend "${REPOSITORY}/frontend:${now}" + podman push frontend "${REPOSITORY}/frontend:${TIMESTAMP}" + podman push frontend "${REPOSITORY}/frontend:${REVISION}" fi