diff --git a/clients/nodejs/zts/package.json b/clients/nodejs/zts/package.json
index db452ccca63..cc07595d805 100644
--- a/clients/nodejs/zts/package.json
+++ b/clients/nodejs/zts/package.json
@@ -32,7 +32,7 @@
"debug": "^2.6.8",
"lodash.clone": "^4.5.0",
"memory-cache": "^0.2.0",
- "axios": "1.6.0",
+ "axios": "1.7.4",
"winston": "^3.7.2"
},
"devDependencies": {
diff --git a/libs/java/server_common/pom.xml b/libs/java/server_common/pom.xml
index 947691a4ac4..2895c4723e2 100644
--- a/libs/java/server_common/pom.xml
+++ b/libs/java/server_common/pom.xml
@@ -37,9 +37,50 @@
2.0.2
5.1.0
0.3
+ 1.40.0
+
+
+
+ io.opentelemetry
+ opentelemetry-bom
+ ${opentelemetry.version}
+ pom
+ import
+
+
+
+
+
+ io.opentelemetry
+ opentelemetry-sdk-extension-autoconfigure
+
+
+ io.opentelemetry
+ opentelemetry-exporter-otlp
+
+
+ io.opentelemetry
+ opentelemetry-sdk
+
+
+ io.opentelemetry
+ opentelemetry-sdk-common
+
+
+ io.opentelemetry
+ opentelemetry-api
+
+
+ io.opentelemetry
+ opentelemetry-sdk-metrics
+
+
+ io.opentelemetry
+ opentelemetry-exporter-logging
+
org.slf4j
slf4j-api
@@ -94,7 +135,7 @@
com.fasterxml.uuid
java-uuid-generator
${uuid.version}
-
+
${project.groupId}
athenz-auth-core
@@ -283,11 +324,11 @@
athenz-zms-core
${project.parent.version}
-
- com.yahoo.athenz
- athenz-zms-java-client
- ${project.parent.version}
-
+
+ com.yahoo.athenz
+ athenz-zms-java-client
+ ${project.parent.version}
+
org.eclipse.jetty
jetty-server
diff --git a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetric.java b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetric.java
new file mode 100644
index 00000000000..e26a562f765
--- /dev/null
+++ b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetric.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright The Athenz Authors
+ *
+ * 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.
+ */
+package com.yahoo.athenz.common.metrics.impl;
+
+import com.yahoo.athenz.common.metrics.Metric;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.Meter;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Context;
+
+public class OpenTelemetryMetric implements Metric {
+ final Meter meter;
+ final Tracer tracer;
+
+ private static final String REQUEST_DOMAIN_NAME = "requestDomainName";
+ private static final String PRINCIPAL_DOMAIN_NAME = "principalDomainName";
+ private static final String HTTP_METHOD_NAME = "httpMethodName";
+ private static final String HTTP_STATUS = "httpStatus";
+ private static final String API_NAME = "apiName";
+
+ public OpenTelemetryMetric(OpenTelemetry openTelemetry) {
+ meter = openTelemetry.getMeter("meter");
+ tracer = openTelemetry.getTracer("tracer");
+ }
+
+ @Override
+ public void increment(String metric) {
+ LongCounter counter = meter.counterBuilder(metric).build();
+ counter.add(1);
+ }
+
+ @Override
+ public void increment(String metric, String requestDomainName) {
+ increment(metric, requestDomainName, 1);
+ }
+
+ @Override
+ public void increment(String metric, String requestDomainName, int count) {
+ LongCounter counter = meter.counterBuilder(metric).build();
+ Attributes attributes = Attributes.builder()
+ .put(REQUEST_DOMAIN_NAME, requestDomainName)
+ .build();
+ counter.add(count, attributes);
+ }
+
+ @Override
+ public void increment(String metric, String requestDomainName, String principalDomainName) {
+ increment(metric, requestDomainName, principalDomainName, 1);
+ }
+
+ @Override
+ public void increment(String metric, String requestDomainName, String principalDomainName, String httpMethod, int httpStatus, String apiName) {
+ LongCounter counter = meter.counterBuilder(metric).build();
+ Attributes attributes = Attributes.builder()
+ .put(REQUEST_DOMAIN_NAME, requestDomainName)
+ .put(PRINCIPAL_DOMAIN_NAME, principalDomainName)
+ .put(HTTP_METHOD_NAME, httpMethod)
+ .put(HTTP_STATUS, Integer.toString(httpStatus))
+ .put(API_NAME, apiName)
+ .build();
+ counter.add(1, attributes);
+ }
+
+ @Override
+ public void increment(String metric, String requestDomainName, String principalDomainName, int count) {
+ LongCounter counter = meter.counterBuilder(metric).build();
+ Attributes attributes = Attributes.builder()
+ .put(REQUEST_DOMAIN_NAME, requestDomainName)
+ .put(PRINCIPAL_DOMAIN_NAME, principalDomainName)
+ .build();
+ counter.add(count, attributes);
+ }
+
+ @Override
+ public Object startTiming(String metric, String requestDomainName) {
+ Span span = tracer.spanBuilder(metric).startSpan();
+ Context context = Context.current().with(span);
+ return new Timer(context, System.currentTimeMillis(), span);
+ }
+
+ @Override
+ public void stopTiming(Object timerMetric) {
+ //not necessary method
+ }
+
+ @Override
+ public void stopTiming(Object timerMetric, String requestDomainName, String principalDomainName) {
+ stopTiming(timerMetric, requestDomainName, principalDomainName, null, -1, null);
+ }
+
+ @Override
+ public void stopTiming(Object timerMetric, String requestDomainName, String principalDomainName,
+ String httpMethod, int httpStatus, String apiName) {
+ Timer timer = (Timer) timerMetric;
+ long duration = System.currentTimeMillis() - timer.start;
+ Span span = timer.getSpan();
+ span.setAttribute("duration", duration);
+ span.setAttribute(REQUEST_DOMAIN_NAME, requestDomainName);
+ span.setAttribute(PRINCIPAL_DOMAIN_NAME, principalDomainName);
+
+ if (httpMethod != null) {
+ span.setAttribute(HTTP_METHOD_NAME, httpMethod);
+ }
+ if (httpStatus != -1) {
+ span.setAttribute(HTTP_STATUS, Integer.toString(httpStatus));
+ }
+ if (apiName != null) {
+ span.setAttribute(API_NAME, apiName);
+ }
+ span.end();
+ }
+
+ @Override
+ public void flush() {
+ //doesn't require flushing
+ }
+
+ @Override
+ public void quit() {
+ //don't need to quit anything
+ }
+
+ static class Timer {
+ private final Context context;
+ private final long start;
+ private final Span span;
+ public Timer(Context context, long start, Span span) {
+ this.context = context;
+ this.start = start;
+ this.span = span;
+ }
+ public Span getSpan() {
+ return span;
+ }
+ }
+}
diff --git a/libs/java/server_common/src/main/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricFactory.java b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricFactory.java
new file mode 100644
index 00000000000..e9137f529bc
--- /dev/null
+++ b/libs/java/server_common/src/main/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricFactory.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright The Athenz Authors
+ *
+ * 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.
+ */
+
+package com.yahoo.athenz.common.metrics.impl;
+
+import com.yahoo.athenz.common.metrics.Metric;
+import com.yahoo.athenz.common.metrics.MetricFactory;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
+
+/*
+ In order to use the otlp exporters you need to configure the environment variables.
+ You need to set the endpoint (OTEL_EXPORTER_OTLP_ENDPOINT) which is defaulted to
+ "http:://localhost:4317" and the attributes (OTEL_RESOURCE_ATTRIBUTES) which is defaulted
+ to "service.name=my-service." AutoConfiguredOpenTelemetrySdk automatically reads the
+ configuration and sets up the exporter.
+*/
+
+public class OpenTelemetryMetricFactory implements MetricFactory {
+ @Override
+ public Metric create() {
+ OpenTelemetry openTelemetry = initialize();
+ return new OpenTelemetryMetric(openTelemetry);
+ }
+
+ public OpenTelemetry initialize() {
+ return AutoConfiguredOpenTelemetrySdk.initialize().getOpenTelemetrySdk();
+ }
+}
diff --git a/libs/java/server_common/src/test/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricFactoryTest.java b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricFactoryTest.java
new file mode 100644
index 00000000000..c08a3e311e8
--- /dev/null
+++ b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricFactoryTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright The Athenz Authors
+ *
+ * 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.
+ */
+package com.yahoo.athenz.common.metrics.impl;
+
+import static org.testng.Assert.*;
+import com.yahoo.athenz.common.metrics.Metric;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+public class OpenTelemetryMetricFactoryTest {
+ private OpenTelemetryMetricFactory factory;
+
+ @BeforeMethod
+ public void setUp() {
+ factory = new OpenTelemetryMetricFactory();
+ }
+
+ @Test
+ public void testCreate() {
+ Metric metric = factory.create();
+ assertNotNull(metric);
+ assertTrue(metric instanceof OpenTelemetryMetric);
+ }
+}
diff --git a/libs/java/server_common/src/test/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricsTest.java b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricsTest.java
new file mode 100644
index 00000000000..da544a444a7
--- /dev/null
+++ b/libs/java/server_common/src/test/java/com/yahoo/athenz/common/metrics/impl/OpenTelemetryMetricsTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright The Athenz Authors
+ *
+ * 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.
+ */
+package com.yahoo.athenz.common.metrics.impl;
+
+import static org.mockito.Mockito.*;
+import static org.testng.Assert.*;
+
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.LongCounterBuilder;
+import io.opentelemetry.api.metrics.Meter;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanBuilder;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Context;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.mockito.ArgumentCaptor;
+
+public class OpenTelemetryMetricsTest {
+ private Meter meter;
+ private Tracer tracer;
+ private LongCounter counter;
+ private Span span;
+ private OpenTelemetryMetric metric;
+
+ @BeforeMethod
+ public void setUp() {
+ meter = mock(Meter.class);
+ tracer = mock(Tracer.class);
+ counter = mock(LongCounter.class);
+ span = mock(Span.class);
+ OpenTelemetry openTelemetry = mock(OpenTelemetry.class);
+
+ LongCounterBuilder counterBuilder = mock(LongCounterBuilder.class);
+ when(meter.counterBuilder(anyString())).thenReturn(counterBuilder);
+ when(counterBuilder.build()).thenReturn(counter);
+
+ SpanBuilder spanBuilder = mock(SpanBuilder.class);
+ when(tracer.spanBuilder(anyString())).thenReturn(spanBuilder);
+ when(spanBuilder.startSpan()).thenReturn(span);
+
+ when(openTelemetry.getMeter("meter")).thenReturn(meter);
+ when(openTelemetry.getTracer("tracer")).thenReturn(tracer);
+
+ metric = new OpenTelemetryMetric(openTelemetry);
+ }
+
+ @Test
+ public void testIncrementMetric() {
+ metric.increment("testIncrement");
+ verify(counter).add(1L);
+ }
+
+ @Test
+ public void testIncrementMetricRequest() {
+ metric.increment("testMetric", "testRequestDomain");
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class);
+ verify(counter).add(eq(1L), captor.capture());
+ Attributes attributes = captor.getValue();
+ assertEquals(attributes.get(AttributeKey.stringKey("requestDomainName")), "testRequestDomain");
+ }
+
+ @Test
+ public void testIncrementMetricRequestCount() {
+ metric.increment("testMetric", "testRequestDomain", 3);
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class);
+ verify(counter).add(eq(3L), captor.capture());
+ Attributes attributes = captor.getValue();
+ assertEquals(attributes.get(AttributeKey.stringKey("requestDomainName")), "testRequestDomain");
+ }
+
+ @Test
+ public void testIncrementMetricRequestPrincipal() {
+ metric.increment("testMetric", "testRequestDomain", "testPrincipalDomain");
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class);
+ verify(counter).add(eq(1L), captor.capture());
+ Attributes attributes = captor.getValue();
+ assertEquals(attributes.get(AttributeKey.stringKey("requestDomainName")), "testRequestDomain");
+ assertEquals(attributes.get(AttributeKey.stringKey("principalDomainName")), "testPrincipalDomain");
+ }
+
+ @Test
+ public void testIncrementMetricRequestPrincipalCount() {
+ metric.increment("testMetric", "testRequestDomain",
+ "testPrincipalDomain", 5);
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class);
+ verify(counter).add(eq(5L), captor.capture());
+ Attributes attributes = captor.getValue();
+ assertEquals(attributes.get(AttributeKey.stringKey("requestDomainName")), "testRequestDomain");
+ assertEquals(attributes.get(AttributeKey.stringKey("principalDomainName")), "testPrincipalDomain");
+ }
+
+ @Test
+ public void testIncrementAllAttributes() {
+ metric.increment("testMetric", "testRequestDomain",
+ "testPrincipalDomain", "GET", 200, "testAPI");
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class);
+ verify(counter).add(eq(1L), captor.capture());
+ Attributes attributes = captor.getValue();
+ assertEquals(attributes.get(AttributeKey.stringKey("requestDomainName")), "testRequestDomain");
+ assertEquals(attributes.get(AttributeKey.stringKey("principalDomainName")), "testPrincipalDomain");
+ assertEquals(attributes.get(AttributeKey.stringKey("httpMethodName")), "GET");
+ assertEquals(attributes.get(AttributeKey.stringKey("httpStatus")), "200");
+ assertEquals(attributes.get(AttributeKey.stringKey("apiName")), "testAPI");
+ }
+
+ @Test
+ public void testStartTiming() {
+ Object timerMetric = metric.startTiming("testMetric", "testRequestDomain");
+ assertNotNull(timerMetric);
+ assertTrue(timerMetric instanceof OpenTelemetryMetric.Timer);
+ OpenTelemetryMetric.Timer timer = (OpenTelemetryMetric.Timer) timerMetric;
+ assertEquals(span, timer.getSpan());
+ }
+
+ @Test
+ public void testStopTimingTimer() {
+ OpenTelemetryMetric.Timer timer = new OpenTelemetryMetric.Timer(Context.current(),
+ System.currentTimeMillis(), span);
+ metric.stopTiming(timer);
+ verifyNoInteractions(meter, tracer, counter, span);
+ }
+
+ @Test
+ public void testStopTimingTimerRequestPrincipal() {
+ OpenTelemetryMetric.Timer timer = new OpenTelemetryMetric.Timer(Context.current(),
+ System.currentTimeMillis(), span);
+ metric.stopTiming(timer, "testRequestDomain", "testPrincipalDomain");
+ verify(span).setAttribute("requestDomainName", "testRequestDomain");
+ verify(span).setAttribute("principalDomainName", "testPrincipalDomain");
+ verify(span).setAttribute(eq("duration"), anyLong());
+ verify(span).end();
+ }
+
+ @Test
+ public void testStopTimingAllAttributes() {
+ OpenTelemetryMetric.Timer timer = new OpenTelemetryMetric.Timer(Context.current(),
+ System.currentTimeMillis(), span);
+ metric.stopTiming(timer, "testRequestDomain",
+ "testPrincipalDomain", "GET", 200, "testAPI");
+ verify(span).setAttribute("requestDomainName", "testRequestDomain");
+ verify(span).setAttribute("principalDomainName", "testPrincipalDomain");
+ verify(span).setAttribute("httpMethodName", "GET");
+ verify(span).setAttribute("httpStatus", "200");
+ verify(span).setAttribute("apiName", "testAPI");
+ verify(span).setAttribute(eq("duration"), anyLong());
+ verify(span).end();
+ }
+
+
+ @Test
+ public void testFlush() {
+ metric.flush();
+ verifyNoInteractions(meter, tracer, counter, span);
+ }
+
+ @Test
+ public void testQuit() {
+ metric.quit();
+ verifyNoInteractions(meter, tracer, counter, span);
+ }
+}
diff --git a/ui/src/__tests__/components/role/RoleRow.test.js b/ui/src/__tests__/components/role/RoleRow.test.js
index 66b746379c5..dc8b5476912 100644
--- a/ui/src/__tests__/components/role/RoleRow.test.js
+++ b/ui/src/__tests__/components/role/RoleRow.test.js
@@ -14,15 +14,19 @@
* limitations under the License.
*/
import React from 'react';
-import RoleRow from '../../../components/role/RoleRow';
+import RoleRow, { getSmallestExpiryOrReview, isReviewRequired } from '../../../components/role/RoleRow';
+import { _ } from 'lodash';
import { colors } from '../../../components/denali/styles';
import { renderWithRedux } from '../../../tests_utils/ComponentsTestUtils';
-import { fireEvent, screen, waitFor } from '@testing-library/react';
-import { configure } from '@testing-library/dom';
-import { act } from 'react-dom/test-utils';
-import { USER_DOMAIN } from '../../../components/constants/constants';
+import { fireEvent, screen } from '@testing-library/react';
+import moment from 'moment';
+
+describe('RoleRow', (object, method) => {
+
+ afterAll(() => {
+ jest.clearAllMocks();
+ });
-describe('RoleRow', () => {
it('should render', () => {
const details = {
name: 'athens:role.ztssia_cert_rotate',
@@ -79,4 +83,106 @@ describe('RoleRow', () => {
fireEvent.mouseEnter(descriptionIcon);
await screen.findByText('test description');
});
+
+ it('getSmallestExpiryOrReview check memberExpiryDays value is picked up', async () => {
+ const role = {memberExpiryDays: 5, lastReviewedDate: '2024-07-24T10:58:07.533Z'};
+ let actualDays = getSmallestExpiryOrReview(role);
+
+ expect(_.isEqual(role.memberExpiryDays, actualDays)).toBeTruthy();
+ });
+
+ it('getSmallestExpiryOrReview check serviceExpiryDays value is picked up', async () => {
+ const role = {serviceExpiryDays: 9, lastReviewedDate: '2024-07-24T10:58:07.533Z'};
+ let actualDays = getSmallestExpiryOrReview(role);
+
+ expect(_.isEqual(role.serviceExpiryDays, actualDays)).toBeTruthy();
+ });
+
+ it('getSmallestExpiryOrReview check memberReviewDays value is picked up', async () => {
+ const role = {memberReviewDays: 6, lastReviewedDate: '2024-07-24T10:58:07.533Z'};
+ let actualDays = getSmallestExpiryOrReview(role);
+
+ expect(_.isEqual(role.memberReviewDays, actualDays)).toBeTruthy();
+ });
+
+ it('getSmallestExpiryOrReview check serviceReviewDays value is picked up', async () => {
+ const role = {serviceReviewDays: 10, lastReviewedDate: '2024-07-24T10:58:07.533Z'};
+ let actualDays = getSmallestExpiryOrReview(role);
+
+ expect(_.isEqual(role.serviceReviewDays, actualDays)).toBeTruthy();
+ });
+
+ it('getSmallestExpiryOrReview check groupReviewDays value is picked up', async () => {
+ const role = {groupReviewDays: 8, lastReviewedDate: '2024-07-24T10:58:07.533Z'};
+ let actualDays = getSmallestExpiryOrReview(role);
+
+ expect(_.isEqual(role.groupReviewDays, actualDays)).toBeTruthy();
+ });
+
+ it('getSmallestExpiryOrReview check groupExpiryDays value is picked up', async () => {
+ const role = {groupExpiryDays: 7, lastReviewedDate: '2024-07-24T10:58:07.533Z'};
+ let actualDays = getSmallestExpiryOrReview(role);
+
+ expect(_.isEqual(role.groupExpiryDays, actualDays)).toBeTruthy();
+ });
+
+ it("getSmallestExpiryOrReview check smallest value picked, non 0, null doesn't produce error", async () => {
+ const role = {
+ memberExpiryDays: 0,
+ serviceExpiryDays: null,
+ memberReviewDays: 6,
+ serviceReviewDays: 10,
+ groupReviewDays: 8,
+ groupExpiryDays: 7,
+ lastReviewedDate: '2024-07-24T10:58:07.533Z'
+ };
+ let actualDays = getSmallestExpiryOrReview(role);
+
+ expect(_.isEqual(role.memberReviewDays, actualDays)).toBeTruthy();
+ });
+
+ it("getSmallestExpiryOrReview when no expiry or review days assigned to the role, return 0", async () => {
+ const role = {
+ lastReviewedDate: '2024-07-24T10:58:07.533Z'
+ };
+ let actualDays = getSmallestExpiryOrReview(role);
+
+ expect(_.isEqual(0, actualDays)).toBeTruthy();
+ });
+
+ it("isReviewRequired when no expiry or review days assigned to the role, return false", async () => {
+ const role = {
+ };
+ let reviewRequired = isReviewRequired(role);
+
+ expect(_.isEqual(false, reviewRequired)).toBeTruthy();
+ });
+
+ it("isReviewRequired when lastReviewDate is more than 80% of memberExpiryDate ago - return true", async () => {
+ const role = {
+ lastReviewedDate: '2024-07-01T11:59:59.000Z',
+ memberExpiryDays: '10'
+ };
+ // mock current date
+ jest.spyOn(moment.prototype, 'utc')
+ .mockReturnValue(moment('2024-07-09T12:00:00.000Z').utc());
+
+ let reviewRequired = isReviewRequired(role);
+
+ expect(_.isEqual(true, reviewRequired)).toBeTruthy();
+ });
+
+ it("isReviewRequired when lastReviewDate is less than 80% of memberExpiryDate ago - return false", async () => {
+ const role = {
+ lastReviewedDate: '2024-07-01T12:00:01.000Z',
+ memberExpiryDays: '10'
+ };
+ // mock current date
+ // jest.spyOn(moment.prototype, 'utc')
+ // .mockReturnValue(moment('2024-07-09T12:00:00.000Z').utc());
+
+ let reviewRequired = isReviewRequired(role);
+
+ expect(_.isEqual(false, reviewRequired)).toBeTruthy();
+ });
});
diff --git a/ui/src/__tests__/components/role/__snapshots__/RoleRow.test.js.snap b/ui/src/__tests__/components/role/__snapshots__/RoleRow.test.js.snap
index 732b88bbb14..6f53681b80f 100644
--- a/ui/src/__tests__/components/role/__snapshots__/RoleRow.test.js.snap
+++ b/ui/src/__tests__/components/role/__snapshots__/RoleRow.test.js.snap
@@ -117,9 +117,6 @@ exports[`RoleRow should render 1`] = `
viewBox="0 0 1024 1024"
width="1.25em"
>
-
- assignment-priority
-
diff --git a/ui/src/__tests__/components/role/__snapshots__/RoleTable.test.js.snap b/ui/src/__tests__/components/role/__snapshots__/RoleTable.test.js.snap
index 98dfbc634c2..ae00fd9e4c5 100644
--- a/ui/src/__tests__/components/role/__snapshots__/RoleTable.test.js.snap
+++ b/ui/src/__tests__/components/role/__snapshots__/RoleTable.test.js.snap
@@ -200,9 +200,6 @@ exports[`RoleTable should render 1`] = `
viewBox="0 0 1024 1024"
width="1.25em"
>
-
- assignment-priority
-
@@ -396,9 +393,6 @@ exports[`RoleTable should render 1`] = `
viewBox="0 0 1024 1024"
width="1.25em"
>
-
- assignment-priority
-
diff --git a/ui/src/components/constants/constants.js b/ui/src/components/constants/constants.js
index a12e916c032..3aff9dc7fa2 100644
--- a/ui/src/components/constants/constants.js
+++ b/ui/src/components/constants/constants.js
@@ -277,3 +277,5 @@ export const ENVIRONMENT_DROPDOWN_OPTIONS = [
export const MEMBER_AUTHORITY_FILTER_DISABLED = 1;
export const MEMBER_AUTHORITY_SYSTEM_SUSPENDED = 2;
export const MEMBER_ATHENZ_SYSTEM_DISABLED = 4;
+
+export const ROLE_PERCENTAGE_OF_DAYS_TILL_NEXT_REVIEW = 0.2;
diff --git a/ui/src/components/role/RoleRow.js b/ui/src/components/role/RoleRow.js
index ae15d7415c9..1bce9cfd758 100644
--- a/ui/src/components/role/RoleRow.js
+++ b/ui/src/components/role/RoleRow.js
@@ -27,6 +27,8 @@ import { css, keyframes } from '@emotion/react';
import { deleteRole } from '../../redux/thunks/roles';
import { connect } from 'react-redux';
import { selectDomainAuditEnabled } from '../../redux/selectors/domainData';
+import moment from 'moment-timezone';
+import { ROLE_PERCENTAGE_OF_DAYS_TILL_NEXT_REVIEW } from '../constants/constants';
const TDStyledName = styled.div`
background-color: ${(props) => props.color};
@@ -93,6 +95,46 @@ const LeftSpan = styled.span`
padding-left: 20px;
`;
+export function isReviewRequired(role) {
+ // determine last review or last modified date
+ const reviewData = role.lastReviewedDate ? {lastReviewedDate: role.lastReviewedDate}
+ : {lastReviewedDate: role.modified};
+ // get smallest expiry or review days value for the role
+ const smallestExpiryOrReview = getSmallestExpiryOrReview(role);
+
+ if (smallestExpiryOrReview === 0) {
+ // review or expiry days were not set in settings - no review required
+ return false;
+ }
+
+ // get 20% of the smallest review period
+ reviewData.pct20 = Math.ceil(smallestExpiryOrReview * ROLE_PERCENTAGE_OF_DAYS_TILL_NEXT_REVIEW);
+
+ const lastReviewedDate = moment(reviewData.lastReviewedDate, 'YYYY-MM-DDTHH:mm:ss.SSSZ');
+ const now = moment().utc();
+
+ // check if expiry/review is coming up within 20% of the smallest review/expiry period
+ return now.subtract(smallestExpiryOrReview, 'days').add(reviewData.pct20, 'days').isAfter(lastReviewedDate);
+}
+
+export function getSmallestExpiryOrReview(role){
+ const values = [
+ role.memberExpiryDays,
+ role.memberReviewDays,
+ role.groupExpiryDays,
+ role.groupReviewDays,
+ role.serviceExpiryDays,
+ role.serviceReviewDays
+ ].filter(obj => obj > 0); // pick only those that have days set and days > 0
+
+ if (values.length > 0) {
+ // pick the one with the smallest days value
+ return values.reduce((obj1, obj2) => obj1 < obj2 ?
+ obj1 : obj2);
+ }
+ return 0;
+}
+
class RoleRow extends React.Component {
constructor(props) {
super(props);
@@ -277,8 +319,7 @@ class RoleRow extends React.Component {
);
- let reviewRequired =
- role.reviewEnabled && (role.memberExpiryDays || role.serviceExpiry);
+ let reviewRequired = isReviewRequired(role);
let roleTypeIcon = role.trust ? iconDelegated : '';
let roleDescriptionIcon = role.description ? iconDescription : '';
@@ -354,11 +395,12 @@ class RoleRow extends React.Component {
isLink
size={'1.25em'}
verticalAlign={'text-bottom'}
+ enableTitle={false}
/>
}
>
- Review Members
+ {reviewRequired ? 'Role Review is required' : 'Review Members'}