Skip to content

Commit 462872b

Browse files
authored
Merge pull request #2686 from bcgov/DESENG-820-footer-version-information
DESENG-820: Add version information to footer
2 parents dbb3828 + 303568a commit 462872b

17 files changed

Lines changed: 408 additions & 79 deletions

File tree

.github/workflows/met-api-cd.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ jobs:
6060
6161
- name: Build image
6262
run: |
63-
docker build . --file Dockerfile --tag image
63+
docker build . --file Dockerfile \
64+
--build-arg MET_BUILD_COMMIT_HASH=${{ github.sha }} \
65+
--build-arg MET_BUILD_DATE=$(date -u +'%b %d, %Y') \
66+
--build-arg MET_BUILD_BRANCH=${{ github.ref_name }} \
67+
--build-arg MET_GITHUB_REPO=https://github.com/${{ github.repository }} \
68+
--tag image
6469
6570
- name: Push image
6671
run: |

.github/workflows/met-web-cd.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ jobs:
7373
docker build . --file Dockerfile \
7474
--secret id=artifactory_username,src=/tmp/artifactory_username \
7575
--secret id=artifactory_password,src=/tmp/artifactory_password \
76+
--build-arg REACT_APP_BUILD_COMMIT_HASH=${{ github.sha }} \
77+
--build-arg REACT_APP_BUILD_DATE=$(date -u +'%b %d, %Y @ %H:%M') \
78+
--build-arg REACT_APP_BUILD_BRANCH=${{ github.ref_name }} \
79+
--build-arg REACT_APP_GITHUB_REPO=https://github.com/${{ github.repository }} \
7680
--tag image
7781
7882
- name: Push image

CHANGELOG.MD

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## November 14, 2025
2+
3+
- **Feature** Added app and API version info to footer [🎟️ DESENG-820](https://citz-gdx.atlassian.net/browse/DESENG-820)
4+
- Adjusted GitHub Actions workflow to embed version information into webapp and API at build time.
5+
- Version information displays in the footer of the admin view only, for now.
6+
17
## August 25, 2025
28

39
- **Feature** Implement new CI/CD logic for Helm charts [🎟️ DESENG-849](https://citz-gdx.atlassian.net/browse/DESENG-849)

met-api/Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ USER root
66
RUN mkdir /opt/app-root && chmod 755 /opt/app-root
77
WORKDIR /opt/app-root
88

9+
# Accept build arguments for version information
10+
ARG MET_BUILD_COMMIT_HASH=unknown
11+
ARG MET_BUILD_DATE=unknown
12+
ARG MET_BUILD_BRANCH=unknown
13+
ARG MET_GITHUB_REPO=https://github.com/bcgov/met-public
14+
15+
# Set environment variables from build arguments
16+
ENV MET_BUILD_COMMIT_HASH=${MET_BUILD_COMMIT_HASH}
17+
ENV MET_BUILD_DATE=${MET_BUILD_DATE}
18+
ENV MET_BUILD_BRANCH=${MET_BUILD_BRANCH}
19+
ENV MET_GITHUB_REPO=${MET_GITHUB_REPO}
20+
921
# Install the requirements
1022
COPY ./requirements.txt .
1123

met-api/src/met_api/resources/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
from .timeline_event_translation import API as TIMELINE_EVENT_TRANSLATION_API
6767
from .engagement_translation import API as ENGAGEMENT_TRANSLATION_API
6868
from .engagement_content_translation import API as ENGAGEMENT_CONTENT_TRANSLATION_API
69+
from .version import API as VERSION_API
6970

7071
__all__ = ('API_BLUEPRINT',)
7172

@@ -119,3 +120,4 @@
119120
API.add_namespace(TIMELINE_EVENT_TRANSLATION_API, path='/timelines/<int:timeline_id>/translations')
120121
API.add_namespace(ENGAGEMENT_TRANSLATION_API, path='/engagement/<int:engagement_id>/translations')
121122
API.add_namespace(ENGAGEMENT_CONTENT_TRANSLATION_API, path='/engagement_content/<int:content_id>/translations')
123+
API.add_namespace(VERSION_API, path='/version')
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright © 2021 Province of British Columbia
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the 'License');
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an 'AS IS' BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Resource for managing version information."""
15+
import os
16+
from flask import jsonify
17+
from flask_restx import Namespace, Resource
18+
from met_api.utils.util import cors_preflight
19+
20+
21+
API = Namespace('version', description='Version endpoints')
22+
23+
24+
@cors_preflight('GET')
25+
@API.route('/')
26+
class Version(Resource):
27+
"""Resource for returning version information."""
28+
29+
@staticmethod
30+
def get():
31+
"""Return version information including commit hash."""
32+
commit_hash = os.getenv('MET_BUILD_COMMIT_HASH', 'unknown')
33+
build_date = os.getenv('MET_BUILD_DATE', 'unknown')
34+
branch = os.getenv('MET_BUILD_BRANCH', 'unknown')
35+
repo_url = os.getenv('MET_GITHUB_REPO', 'https://github.com/bcgov/met-public')
36+
37+
return jsonify({
38+
'commit': commit_hash,
39+
'build_date': build_date,
40+
'branch': branch,
41+
'version': f'{commit_hash[:7]}' if commit_hash != 'unknown' else 'dev',
42+
'commit_url': f'{repo_url}/commit/{commit_hash}' if commit_hash != 'unknown' else None,
43+
})
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright © 2019 Province of British Columbia
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Tests to verify the Version API end-point.
16+
17+
Test-Suite to ensure that the /version endpoint is working as expected.
18+
"""
19+
import json
20+
from http import HTTPStatus
21+
from unittest.mock import patch
22+
23+
24+
def test_get_version_default(client, session): # pylint:disable=unused-argument
25+
"""Assert that version endpoint returns default values when env vars not set."""
26+
rv = client.get('/api/version/')
27+
assert rv.status_code == HTTPStatus.OK
28+
29+
data = json.loads(rv.data)
30+
assert 'commit' in data
31+
assert 'build_date' in data
32+
assert 'branch' in data
33+
assert 'version' in data
34+
assert 'commit_url' in data
35+
36+
# Default values when env vars not set
37+
assert data['commit'] == 'unknown'
38+
assert data['build_date'] == 'unknown'
39+
assert data['branch'] == 'unknown'
40+
assert data['version'] == 'dev'
41+
assert data['commit_url'] is None
42+
43+
44+
@patch('os.getenv')
45+
def test_get_version_with_env_vars(mock_getenv, client, session): # pylint:disable=unused-argument
46+
"""Assert that version endpoint returns values from environment variables."""
47+
test_commit = 'abc123def456'
48+
test_date = '2025-11-14T12:00:00Z'
49+
test_branch = 'main'
50+
test_repo = 'https://github.com/bcgov/met-public'
51+
52+
def getenv_side_effect(key, default=None):
53+
env_vars = {
54+
'MET_BUILD_COMMIT_HASH': test_commit,
55+
'MET_BUILD_DATE': test_date,
56+
'MET_BUILD_BRANCH': test_branch,
57+
'MET_GITHUB_REPO': test_repo,
58+
}
59+
return env_vars.get(key, default)
60+
61+
mock_getenv.side_effect = getenv_side_effect
62+
63+
rv = client.get('/api/version/')
64+
assert rv.status_code == HTTPStatus.OK
65+
66+
data = json.loads(rv.data)
67+
assert data['commit'] == test_commit
68+
assert data['build_date'] == test_date
69+
assert data['branch'] == test_branch
70+
assert data['version'].startswith(test_commit[:7])
71+
assert data['commit_url'] == f'{test_repo}/commit/{test_commit}'

met-web/Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ RUN npm run build
3131

3232
# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
3333
FROM nginx:1.17 AS production-stage
34+
35+
# Accept build arguments for version information to pass to runtime
36+
ARG REACT_APP_BUILD_COMMIT_HASH=dev
37+
ARG REACT_APP_BUILD_DATE=unknown
38+
ARG REACT_APP_BUILD_BRANCH=unknown
39+
ARG REACT_APP_GITHUB_REPO=https://github.com/bcgov/met-public
40+
41+
# Set as environment variables for runtime access
42+
ENV REACT_APP_BUILD_COMMIT_HASH=${REACT_APP_BUILD_COMMIT_HASH}
43+
ENV REACT_APP_BUILD_DATE=${REACT_APP_BUILD_DATE}
44+
ENV REACT_APP_BUILD_BRANCH=${REACT_APP_BUILD_BRANCH}
45+
ENV REACT_APP_GITHUB_REPO=${REACT_APP_GITHUB_REPO}
46+
3447
RUN mkdir /app
3548

3649
# RUN touch /var/run/nginx.pid && \

met-web/src/apiManager/endpoints/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ const Endpoints = {
208208
GET_TENANT_LANGUAGES: `${AppConfig.apiUrl}/languages/tenant/tenant_id`,
209209
CREATE_OR_DELETE_TENANT_MAPPING: `${AppConfig.apiUrl}/languages/language_id/tenant/tenant_id`,
210210
},
211+
Version: {
212+
GET: `${AppConfig.apiUrl}/version/`,
213+
},
211214
};
212215

213216
export default Endpoints;

met-web/src/components/common/Input/Button.tsx

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
import React, { useState } from 'react';
1+
import React from 'react';
22
import {
33
Button as MuiButton,
44
ButtonProps as MuiButtonProps,
5-
Stack,
65
IconButton as MuiIconButton,
76
useTheme,
87
} from '@mui/material';
98
import { globalFocusShadow, colors, elevations } from '../../common';
109
import { isDarkColor } from 'utils';
11-
import { IconProp } from '@fortawesome/fontawesome-svg-core';
10+
import { IconParams, IconProp } from '@fortawesome/fontawesome-svg-core';
1211
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
13-
import { styled } from '@mui/system';
1412
import { RouterLinkRenderer } from '../Navigation/Link';
1513

1614
const buttonStyles = {
@@ -213,7 +211,7 @@ const TertiaryButton = ({
213211
}: ButtonProps) => {
214212
const height: string = sizeMap[size];
215213
const customColor = colors.notification[color as keyof typeof colors.notification]?.shade ?? color;
216-
const activeColor = color !== 'default' ? `color-mix(in srgb, ${customColor}, white 90%)` : colors.surface.blue[10];
214+
const activeColor = color == 'default' ? colors.surface.blue[10] : `color-mix(in srgb, ${customColor}, white 90%)`;
217215

218216
return (
219217
<MuiButton
@@ -284,58 +282,77 @@ type IconButtonProps = {
284282
icon: IconProp;
285283
onClick?: () => void;
286284
title?: string;
287-
sx?: React.CSSProperties; // Add sx prop
285+
sx?: React.CSSProperties;
286+
size?: string;
287+
iconSize?: string;
288+
color?: string;
288289
backgroundColor?: string;
290+
hoverBackgroundColor?: string;
291+
disabled?: boolean;
292+
iconProps?: IconParams;
289293
};
290294

291-
const StyledIconButton = styled(MuiIconButton)(() => ({
292-
color: '#494949',
293-
borderRadius: '50%', // Make it round
294-
width: '40px', // Set width to maintain circle shape
295-
height: '40px', // Set height to maintain circle shape
296-
}));
297-
298295
/**
299296
* IconButton component that renders a circular button with an icon.
300-
* It supports focus styles and hover effects.
297+
* Provides full customization of size, colors, and positioning while maintaining
298+
* consistent focus and hover behavior.
299+
*
301300
* @param {IconButtonProps} props - The properties for the icon button.
302301
* @param {IconProp} props.icon - The icon to display in the button.
303302
* @param {() => void} props.onClick - The function to call when the button is clicked.
304303
* @param {string} props.title - The title for the button, used for accessibility.
305304
* @param {React.CSSProperties} props.sx - Additional styles to apply to the button.
306-
* @param {string} props.backgroundColor - Optional background color for the button.
305+
* @param {string} props.size - Button diameter (e.g., '40px', '3rem'). Default is '40px'.
306+
* @param {string} props.iconSize - Icon font size (e.g., '20px', '1.5rem'). Default is '20px'.
307+
* @param {string} props.color - Icon color. Default is '#494949'.
308+
* @param {string} props.backgroundColor - Button background color. Default is colors.focus.regular.inner.
309+
* @param {string} props.hoverBackgroundColor - Background color on hover/focus. Default is '#F2F2F2'.
310+
* @param {boolean} props.disabled - Whether the button is disabled.
311+
* @param {IconParams} props.iconProps - Additional properties for the FontAwesomeIcon.
307312
* @returns A styled icon button component.
308313
*/
309-
export const IconButton: React.FC<IconButtonProps> = ({ icon, onClick, title, sx, backgroundColor }) => {
310-
const [focused, setFocused] = useState(false);
311-
314+
export const IconButton: React.FC<IconButtonProps> = ({
315+
icon,
316+
onClick,
317+
title,
318+
sx,
319+
size = '40px',
320+
iconSize = '20px',
321+
color = '#494949',
322+
backgroundColor = colors.focus.regular.inner,
323+
hoverBackgroundColor = '#F2F2F2',
324+
disabled = false,
325+
iconProps,
326+
}) => {
312327
return (
313-
<Stack
314-
direction="row"
315-
justifyContent="center"
316-
alignItems="center"
328+
<MuiIconButton
329+
onClick={onClick}
330+
title={title}
331+
aria-label={title}
332+
disabled={disabled}
317333
sx={{
318-
backgroundColor: backgroundColor ?? `${colors.focus.regular.inner}`,
319-
cursor: 'pointer',
334+
width: size,
335+
height: size,
320336
borderRadius: '50%',
321-
boxShadow: focused ? `0 0 0 2px white, 0 0 0 4px ${colors.focus.regular.outer}` : 'none',
337+
backgroundColor: backgroundColor,
338+
color: color,
322339
'&:hover': {
323-
backgroundColor: '#F2F2F2',
340+
backgroundColor: hoverBackgroundColor,
324341
},
325342
'&:focus-visible': {
326-
backgroundColor: '#F2F2F2',
327-
outline: 'white 2px dashed', // Remove default outline
343+
backgroundColor: hoverBackgroundColor,
344+
outline: `2px solid ${colors.focus.regular.outer}`,
328345
outlineOffset: '2px',
346+
boxShadow: globalFocusShadow,
347+
},
348+
'&:disabled': {
349+
backgroundColor: colors.surface.gray[40],
350+
color: colors.type.regular.disabled,
329351
},
330352
...sx,
331353
}}
332-
onFocus={() => setFocused(true)}
333-
onBlur={() => setFocused(false)}
334-
tabIndex={focused ? 0 : -1} // Set tabIndex to -1 when not focused
335354
>
336-
<StyledIconButton onClick={onClick} title={title} aria-label={title}>
337-
<FontAwesomeIcon icon={icon} style={{ fontSize: '20px', color: '#494949' }} />
338-
</StyledIconButton>
339-
</Stack>
355+
<FontAwesomeIcon icon={icon} style={{ fontSize: iconSize }} {...iconProps} />
356+
</MuiIconButton>
340357
);
341358
};

0 commit comments

Comments
 (0)