diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb7821883..9e594b724 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+
+## [3.4.3] - 2023-12-11: Fix push key handling
+### Fixed
+
+- WP-408: use archiveSystemId set in app definition as default (#917)
+
+
+## [3.4.2] - 2023-12-07: Fix push key handling
+### Fixed
+
+- WP-402: handle 401 unauthorized Tapis error for pushing keys (#915, #916)
+
+## [3.4.1] - 2023-12-05: Fix web hook and impersonation bug
+### Fixed
+
+- WP-400: Fix impersonate url (#912)
+- Bug: Fix websockets via ASGI_APPLICATION setting (#913)
+
+
## [3.4.0] - 2023-11-27: Django upgrade to 4 and bug fixes
### Changed
@@ -1006,7 +1025,10 @@ WP-306: Fix target path regression (#871)
## [1.0.0] - 2020-02-28
v1.0.0 Production release as of Feb 28, 2020.
-[unreleased]: https://github.com/TACC/Core-Portal/compare/v3.4.0...HEAD
+[unreleased]: https://github.com/TACC/Core-Portal/compare/v3.4.3...HEAD
+[3.4.3]: https://github.com/TACC/Core-Portal/releases/tag/v3.4.3
+[3.4.2]: https://github.com/TACC/Core-Portal/releases/tag/v3.4.2
+[3.4.1]: https://github.com/TACC/Core-Portal/releases/tag/v3.4.1
[3.4.0]: https://github.com/TACC/Core-Portal/releases/tag/v3.4.0
[3.3.2]: https://github.com/TACC/Core-Portal/releases/tag/v3.3.2
[3.3.1]: https://github.com/TACC/Core-Portal/releases/tag/v3.3.1
diff --git a/client/src/components/Applications/AppForm/AppForm.jsx b/client/src/components/Applications/AppForm/AppForm.jsx
index 7b95b6fca..02c80555a 100644
--- a/client/src/components/Applications/AppForm/AppForm.jsx
+++ b/client/src/components/Applications/AppForm/AppForm.jsx
@@ -275,7 +275,7 @@ export const AppSchemaForm = ({ app }) => {
coresPerNode: app.definition.jobAttributes.coresPerNode,
maxMinutes: app.definition.jobAttributes.maxMinutes,
archiveSystemId:
- defaultSystem || app.definition.jobAttributes.archiveSystemId,
+ app.definition.jobAttributes.archiveSystemId || defaultSystem,
archiveSystemDir: app.definition.jobAttributes.archiveSystemDir,
archiveOnAppError: true,
appId: app.definition.id,
@@ -787,14 +787,19 @@ export const AppSchemaForm = ({ app }) => {
description="System into which output files are archived after application execution."
name="archiveSystemId"
type="text"
- placeholder={defaultSystem}
+ placeholder={
+ app.definition.archiveSystemId || defaultSystem
+ }
/>
>
) : null}
diff --git a/client/src/components/Applications/AppForm/AppForm.test.js b/client/src/components/Applications/AppForm/AppForm.test.js
index 2a6b119df..a70c63263 100644
--- a/client/src/components/Applications/AppForm/AppForm.test.js
+++ b/client/src/components/Applications/AppForm/AppForm.test.js
@@ -72,11 +72,20 @@ describe('AppSchemaForm', () => {
...initialMockState,
});
- const { getByText } = renderAppSchemaFormComponent(store, {
+ const { getByText, container } = renderAppSchemaFormComponent(store, {
...helloWorldAppFixture,
});
+
+ const archiveSystemId = container.querySelector(
+ 'input[name="archiveSystemId"]'
+ );
await waitFor(() => {
expect(getByText(/TACC-ACI/)).toBeDefined();
+
+ // use app definition default archive system
+ expect(archiveSystemId.value).toBe(
+ helloWorldAppFixture.definition.jobAttributes.archiveSystemId
+ );
});
});
diff --git a/client/src/components/Applications/AppForm/fixtures/AppForm.app.fixture.js b/client/src/components/Applications/AppForm/fixtures/AppForm.app.fixture.js
index 1458448ea..263cb67e2 100644
--- a/client/src/components/Applications/AppForm/fixtures/AppForm.app.fixture.js
+++ b/client/src/components/Applications/AppForm/fixtures/AppForm.app.fixture.js
@@ -26,7 +26,7 @@ export const helloWorldAppFixture = {
execSystemInputDir: '${JobWorkingDir}',
execSystemOutputDir: '${JobWorkingDir}/output',
execSystemLogicalQueue: 'development',
- archiveSystemId: 'cloud.data',
+ archiveSystemId: 'frontera',
archiveSystemDir:
'HOST_EVAL($HOME)/tapis-jobs-archive/${JobCreateDate}/${JobName}-${JobUUID}',
archiveOnAppError: true,
diff --git a/client/src/components/DataFiles/DataFilesListing/DataFilesListing.jsx b/client/src/components/DataFiles/DataFilesListing/DataFilesListing.jsx
index 2abd8e6a0..53d2e2616 100644
--- a/client/src/components/DataFiles/DataFilesListing/DataFilesListing.jsx
+++ b/client/src/components/DataFiles/DataFilesListing/DataFilesListing.jsx
@@ -153,13 +153,32 @@ const DataFilesListing = ({ api, scheme, system, path, isPublic }) => {
const systemDisplayName = useSystemDisplayName({ scheme, system, path });
const homeDir = selectedSystem?.homeDir;
- // Check if the current path is the home directory itself
- const isAtHomeDir = path.replace(/^\/+/, '') === homeDir?.replace(/^\/+/, '');
+ // Check if the current path is the root directory
+ const isRootDir = path === '/' || path === '';
- // Determine the sectionName based on the path (if homeDir, use systemDisplayName--else use current dir)
- const sectionName = isAtHomeDir
- ? systemDisplayName
- : getCurrentDirectory(path);
+ // Adjusted check for home directory
+ const isAtHomeDir =
+ isRootDir || path.replace(/^\/+/, '') === homeDir?.replace(/^\/+/, '');
+
+ // Determine the sectionName with added handling for root directory
+ function determineSectionName(
+ isAtHomeDir,
+ isRootDir,
+ systemDisplayName,
+ path
+ ) {
+ if (isAtHomeDir) {
+ return isRootDir ? 'Root' : systemDisplayName;
+ }
+ return getCurrentDirectory(path);
+ }
+
+ const sectionName = determineSectionName(
+ isAtHomeDir,
+ isRootDir,
+ systemDisplayName,
+ path
+ );
return (
<>
diff --git a/client/src/components/DataFiles/DataFilesTable/DataFilesTable.jsx b/client/src/components/DataFiles/DataFilesTable/DataFilesTable.jsx
index ee09d5567..7b247ce26 100644
--- a/client/src/components/DataFiles/DataFilesTable/DataFilesTable.jsx
+++ b/client/src/components/DataFiles/DataFilesTable/DataFilesTable.jsx
@@ -105,7 +105,7 @@ const DataFilesTablePlaceholder = ({ section, data }) => {
.
>
);
- if (err === '500') {
+ if (err === '500' || err === '401') {
if (downSystems.includes(currSystemHost)) {
return (
diff --git a/client/src/components/Onboarding/OnboardingAdmin.jsx b/client/src/components/Onboarding/OnboardingAdmin.jsx
index f4b91790f..d373d0365 100644
--- a/client/src/components/Onboarding/OnboardingAdmin.jsx
+++ b/client/src/components/Onboarding/OnboardingAdmin.jsx
@@ -237,17 +237,21 @@ const OnboardingAdmin = () => {
const [showIncompleteOnly, setShowIncompleteOnly] = useState(false);
const toggleShowIncomplete = () => {
+ dispatch({
+ type: 'FETCH_ONBOARDING_ADMIN_LIST',
+ payload: {
+ offset: 0,
+ limit,
+ query,
+ showIncompleteOnly: !showIncompleteOnly, // Toggle the parameter
+ },
+ });
setShowIncompleteOnly((prev) => !prev);
};
const { users, offset, limit, total, query, loading, error } = useSelector(
(state) => state.onboarding.admin
);
-
- const filteredUsers = users.filter((user) =>
- showIncompleteOnly ? !user.setupComplete : true
- );
-
const paginationCallback = useCallback(
(page) => {
dispatch({
@@ -256,10 +260,11 @@ const OnboardingAdmin = () => {
offset: (page - 1) * limit,
limit,
query,
+ showIncompleteOnly,
},
});
},
- [offset, limit, query]
+ [offset, limit, query, showIncompleteOnly]
);
const viewLogCallback = useCallback(
@@ -276,7 +281,7 @@ const OnboardingAdmin = () => {
useEffect(() => {
dispatch({
type: 'FETCH_ONBOARDING_ADMIN_LIST',
- payload: { offset, limit, query: null },
+ payload: { offset, limit, query: null, showIncompleteOnly },
});
}, [dispatch]);
@@ -294,6 +299,7 @@ const OnboardingAdmin = () => {
);
}
+
return (
@@ -302,7 +308,7 @@ const OnboardingAdmin = () => {
- {filteredUsers.length === 0 && (
+ {users.length === 0 && (
No users to show.
)}
- {filteredUsers.length > 0 && (
+ {users.length > 0 && (
)}
- {filteredUsers.length > 0 && (
+ {users.length > 0 && (
td:not(.has-wrappable-content):not(.status) {
@include truncate-with-ellipsis;
}
-.user:nth-child(even) > td:not(.staffwait):not(.name) {
- background-color: #c6c6c61a;
+.user:nth-child(4n),
+.user:nth-child(4n-1) > td:not(.staffwait) {
+ background-color: #f4f4f4;
}
.username {
diff --git a/client/src/components/Onboarding/OnboardingAdminSearchbar.module.scss b/client/src/components/Onboarding/OnboardingAdminSearchbar.module.scss
index e59240406..04e2e6eb3 100644
--- a/client/src/components/Onboarding/OnboardingAdminSearchbar.module.scss
+++ b/client/src/components/Onboarding/OnboardingAdminSearchbar.module.scss
@@ -17,7 +17,13 @@
/* WARN: Non-standard un-documented first-party breakpoint */
@media (max-width: 1700px) {
.query-fieldset {
- width: 460px;
+ width: 360px;
+ }
+}
+
+@media (max-width: 768px) {
+ .query-fieldset {
+ width: 260px;
}
}
/* FP-563: Support count in status message */
diff --git a/client/src/components/Onboarding/OnboardingStatus.jsx b/client/src/components/Onboarding/OnboardingStatus.jsx
index 7f2deaeae..9818a1644 100644
--- a/client/src/components/Onboarding/OnboardingStatus.jsx
+++ b/client/src/components/Onboarding/OnboardingStatus.jsx
@@ -22,6 +22,9 @@ const getContents = (step) => {
case 'completed':
type = 'success';
break;
+ case null:
+ type = 'unavailable';
+ break;
default:
type = 'normal';
}
@@ -38,6 +41,8 @@ const getContents = (step) => {
case 'failed':
case 'error':
return Unsuccessful;
+ case null:
+ return Unavailable;
case 'completed':
return Completed;
case 'processing':
diff --git a/client/src/components/PublicData/PublicData.jsx b/client/src/components/PublicData/PublicData.jsx
index 40724764f..fa5e76da3 100644
--- a/client/src/components/PublicData/PublicData.jsx
+++ b/client/src/components/PublicData/PublicData.jsx
@@ -18,6 +18,8 @@ import DataFilesShowPathModal from '../DataFiles/DataFilesModals/DataFilesShowPa
import { ToolbarButton } from '../DataFiles/DataFilesToolbar/DataFilesToolbar';
import styles from './PublicData.module.css';
+import dropdownStyles from '../../styles/components/dropdown-menu.css';
+import CombinedBreadcrumbs from '../DataFiles/CombinedBreadcrumbs/CombinedBreadcrumbs';
const PublicData = () => {
const history = useHistory();
@@ -98,9 +100,9 @@ const PublicDataListing = ({ canDownload, downloadCallback }) => {
{
label="TACC Token"
required
disabled={submitting}
- autocomplete="off"
+ autoComplete="off"
/>
diff --git a/client/src/redux/sagas/datafiles.sagas.js b/client/src/redux/sagas/datafiles.sagas.js
index 2ef66a565..1ccb630af 100644
--- a/client/src/redux/sagas/datafiles.sagas.js
+++ b/client/src/redux/sagas/datafiles.sagas.js
@@ -168,7 +168,7 @@ export function* fetchFiles(action) {
},
});
// If listing returns 500, body should contain a system def for key pushing.
- yield e.status === 500 &&
+ yield (e.status === 500 || e.status === 401) &&
put({
type: 'SET_SYSTEM',
payload: {
diff --git a/client/src/redux/sagas/onboarding.sagas.js b/client/src/redux/sagas/onboarding.sagas.js
index 750edb6fc..0c0d07e10 100644
--- a/client/src/redux/sagas/onboarding.sagas.js
+++ b/client/src/redux/sagas/onboarding.sagas.js
@@ -3,8 +3,13 @@ import { fetchUtil } from 'utils/fetchUtil';
import Cookies from 'js-cookie';
// Admin listing of all users
-export async function fetchOnboardingAdminList(offset, limit, q) {
- const params = { offset, limit };
+export async function fetchOnboardingAdminList(
+ offset,
+ limit,
+ q,
+ showIncompleteOnly
+) {
+ const params = { offset, limit, showIncompleteOnly };
if (q) {
params.q = q;
}
@@ -18,8 +23,14 @@ export async function fetchOnboardingAdminList(offset, limit, q) {
export function* getOnboardingAdminList(action) {
yield put({ type: 'FETCH_ONBOARDING_ADMIN_LIST_PROCESSING' });
try {
- const { offset, limit, query } = action.payload;
- const result = yield call(fetchOnboardingAdminList, offset, limit, query);
+ const { offset, limit, query, showIncompleteOnly } = action.payload;
+ const result = yield call(
+ fetchOnboardingAdminList,
+ offset,
+ limit,
+ query,
+ showIncompleteOnly
+ );
yield put({
type: 'FETCH_ONBOARDING_ADMIN_LIST_SUCCESS',
payload: {
diff --git a/client/src/redux/sagas/onboarding.sagas.test.js b/client/src/redux/sagas/onboarding.sagas.test.js
index 39e669b3f..6da3a8ea0 100644
--- a/client/src/redux/sagas/onboarding.sagas.test.js
+++ b/client/src/redux/sagas/onboarding.sagas.test.js
@@ -23,14 +23,19 @@ jest.mock('cross-fetch');
describe('getOnboardingAdminList Saga', () => {
it('should fetch list of onboarding users and transform state', () =>
expectSaga(getOnboardingAdminList, {
- payload: { offset: 0, limit: 25, query: 'query' },
+ payload: {
+ offset: 0,
+ limit: 25,
+ query: 'query',
+ showIncompleteOnly: true,
+ },
})
.withReducer(onboarding)
.provide([
[matchers.call.fn(fetchOnboardingAdminList), onboardingAdminFixture],
])
.put({ type: 'FETCH_ONBOARDING_ADMIN_LIST_PROCESSING' })
- .call(fetchOnboardingAdminList, 0, 25, 'query')
+ .call(fetchOnboardingAdminList, 0, 25, 'query', true)
.put({
type: 'FETCH_ONBOARDING_ADMIN_LIST_SUCCESS',
payload: {
diff --git a/server/portal/apps/datafiles/views.py b/server/portal/apps/datafiles/views.py
index 06e72e13d..69fa02fdb 100644
--- a/server/portal/apps/datafiles/views.py
+++ b/server/portal/apps/datafiles/views.py
@@ -4,7 +4,7 @@
from django.conf import settings
from django.http import JsonResponse, HttpResponseForbidden
from requests.exceptions import HTTPError
-from tapipy.errors import InternalServerError
+from tapipy.errors import InternalServerError, UnauthorizedError
from portal.views.base import BaseApiView
from portal.libs.agave.utils import service_account
from portal.apps.datafiles.handlers.tapis_handlers import (tapis_get_handler,
@@ -109,11 +109,11 @@ def get(self, request, operation=None, scheme=None, system=None, path='/'):
operation in NOTIFY_ACTIONS and \
notify(request.user.username, operation, 'success', {'response': response})
- except InternalServerError as e:
+ except (InternalServerError, UnauthorizedError) as e:
error_status = e.response.status_code
error_json = e.response.json()
operation in NOTIFY_ACTIONS and notify(request.user.username, operation, 'error', {})
- if error_status == 500:
+ if error_status == 500 or error_status == 401:
logger.info(e)
# In case of 500 determine cause
system = client.systems.getSystem(systemId=system)
diff --git a/server/portal/apps/onboarding/api/views.py b/server/portal/apps/onboarding/api/views.py
index a0759d8b6..73750b82b 100644
--- a/server/portal/apps/onboarding/api/views.py
+++ b/server/portal/apps/onboarding/api/views.py
@@ -264,8 +264,13 @@ def get(self, request):
if q:
query = q_to_model_queries(q)
results = results.filter(query)
+ show_incomplete_only = request.GET.get('showIncompleteOnly', 'False').lower()
+ # Filter users based on the showIncompleteOnly parameter
+ if show_incomplete_only == 'true':
+ results = results.filter(profile__setup_complete=False)
# Get users, with most recently joined users that do not have setup_complete, first
results = results.order_by('-date_joined', 'profile__setup_complete', 'last_name', 'first_name')
+
# Uncomment this line to simulate many user results
# results = list(results) * 105
total = len(results)
diff --git a/server/portal/apps/onboarding/api/views_unit_test.py b/server/portal/apps/onboarding/api/views_unit_test.py
index dd3ebab7c..0d1622ea9 100644
--- a/server/portal/apps/onboarding/api/views_unit_test.py
+++ b/server/portal/apps/onboarding/api/views_unit_test.py
@@ -234,11 +234,19 @@ def test_get(client, authenticated_staff, regular_user, mock_steps):
authenticated_staff.profile.setup_complete = True
authenticated_staff.profile.save()
+ # Make a request without 'showIncompleteOnly' parameter
response = client.get("/api/onboarding/admin/")
result = json.loads(response.content)
users = result["users"]
+ # Make a request with 'showIncompleteOnly' parameter set to true
+ response_incomplete_users = client.get("/api/onboarding/admin/?showIncompleteOnly=true")
+ result_incomplete_users = json.loads(response_incomplete_users.content)
+
+ users_incomplete = result_incomplete_users["users"]
+
+ # Assertions without 'showIncompleteOnly'
# The first result should be the regular_user, since they have not completed setup
assert users[0]["username"] == regular_user.username
@@ -248,6 +256,13 @@ def test_get(client, authenticated_staff, regular_user, mock_steps):
# There should be two users returned
assert len(users) == 2
+ # Assertions with 'showIncompleteOnly=true'
+ assert users_incomplete[0]["username"] == regular_user.username
+ assert users_incomplete[0]['steps'][0]['step'] == "portal.apps.onboarding.steps.test_steps.MockStep"
+
+ # There should be one user since only one user has setup_complete = True
+ assert len(users_incomplete) == 1
+
def test_get_search(client, authenticated_staff, regular_user, mock_steps):
response = client.get("/api/onboarding/admin/?q=Firstname")
diff --git a/server/portal/apps/workspace/api/views.py b/server/portal/apps/workspace/api/views.py
index 9e263e08a..406ad7d96 100644
--- a/server/portal/apps/workspace/api/views.py
+++ b/server/portal/apps/workspace/api/views.py
@@ -11,7 +11,7 @@
from django.urls import reverse
from django.db.models.functions import Coalesce
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
-from tapipy.errors import BaseTapyException, InternalServerError
+from tapipy.errors import BaseTapyException, InternalServerError, UnauthorizedError
from portal.views.base import BaseApiView
from portal.exceptions.api import ApiException
from portal.apps.licenses.models import LICENSE_TYPES, get_license_info
@@ -107,7 +107,7 @@ def get(self, request, *args, **kwargs):
try:
tapis.files.listFiles(systemId=system_id, path="/")
- except InternalServerError:
+ except (InternalServerError, UnauthorizedError):
success = _test_listing_with_existing_keypair(system_def, request.user)
data['systemNeedsKeys'] = not success
data['pushKeysSystem'] = system_def
@@ -300,7 +300,7 @@ def post(self, request, *args, **kwargs):
for system_id in list(set([job_post['archiveSystemId'], job_post['execSystemId']])):
try:
tapis.files.listFiles(systemId=system_id, path="/")
- except InternalServerError:
+ except (InternalServerError, UnauthorizedError):
system_def = tapis.systems.getSystem(systemId=system_id)
success = _test_listing_with_existing_keypair(system_def, request.user)
if not success:
@@ -459,6 +459,7 @@ def getPublicApps(self, user):
categoryResult["apps"].append(app)
+ categoryResult["apps"] = sorted(categoryResult["apps"], key=lambda app: app['label'] or app['appId'])
categories.append(categoryResult)
return categories, html_definitions
diff --git a/server/portal/urls.py b/server/portal/urls.py
index 118071507..f19e91166 100644
--- a/server/portal/urls.py
+++ b/server/portal/urls.py
@@ -27,6 +27,7 @@
from django.views.generic.base import TemplateView
from django.urls import path, re_path, include
from impersonate import views as impersonate_views
+from portal.views.views import health_check
admin.autodiscover()
urlpatterns = [
@@ -112,4 +113,8 @@
# version check.
path('version/', portal_version),
+ # health check
+ path('core/health-check', health_check)
+
+
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/server/portal/views/unit_test.py b/server/portal/views/unit_test.py
index cf3882ef8..f4ffbbf69 100644
--- a/server/portal/views/unit_test.py
+++ b/server/portal/views/unit_test.py
@@ -6,6 +6,7 @@
import requests
import json
+
# route to be used for testing purposes
API_ROUTE = '/api/system-monitor/'
@@ -110,3 +111,9 @@ def test_exception(client, api_method_mock):
response = client.get(API_ROUTE)
assert response.status_code == 500
assert json.loads(response.content) == {'message': 'Something went wrong here...'}
+
+
+def test_health_check(client):
+ response = client.get('/core/health-check')
+ assert response.status_code == 200
+ assert json.loads(response.content) == {'status': 'healthy'}
diff --git a/server/portal/views/views.py b/server/portal/views/views.py
index cc4c2b90a..6c65764a2 100644
--- a/server/portal/views/views.py
+++ b/server/portal/views/views.py
@@ -1,5 +1,5 @@
import logging
-from django.http import HttpResponse
+from django.http import HttpResponse, JsonResponse
logger = logging.getLogger(__name__)
@@ -23,3 +23,8 @@ def project_version(request):
version = 'UNKNOWN'
return HttpResponse(version, content_type='text/plain')
+
+
+def health_check(request):
+ health_status = {'status': 'healthy'}
+ return JsonResponse(health_status)