Skip to content

Commit 47b5fd7

Browse files
authored
Issue 5449 show license audit and special permissions checks (#5563)
* Show license audit and special permissions checks in the Submit to Community Library Side Panel merge# * [pre-commit.ci lite] apply automatic fixes * fix linting * fix bug * [pre-commit.ci lite] apply automatic fixes * fix linting merge# * [pre-commit.ci lite] apply automatic fixes * fix code * [pre-commit.ci lite] apply automatic fixes * fix linting * [pre-commit.ci lite] apply automatic fixes * fix code * fix code ---------
1 parent da5fb15 commit 47b5fd7

File tree

16 files changed

+928
-35
lines changed

16 files changed

+928
-35
lines changed

contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/Box.vue

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<div class="box-icon">
1818
<KIcon
1919
:icon="icon"
20+
:color="iconColor"
2021
:style="{ fontSize: '18px' }"
2122
/>
2223
</div>
@@ -63,6 +64,8 @@
6364
switch (props.kind) {
6465
case 'warning':
6566
return paletteTheme.red.v_100;
67+
case 'success':
68+
return paletteTheme.green.v_100;
6669
case 'info':
6770
return paletteTheme.grey.v_100;
6871
default:
@@ -73,6 +76,8 @@
7376
switch (props.kind) {
7477
case 'warning':
7578
return paletteTheme.red.v_300;
79+
case 'success':
80+
return paletteTheme.green.v_300;
7681
case 'info':
7782
return 'transparent';
7883
default:
@@ -83,6 +88,8 @@
8388
switch (props.kind) {
8489
case 'warning':
8590
return 'error';
91+
case 'success':
92+
return 'circleCheckmark';
8693
case 'info':
8794
return 'infoOutline';
8895
default:
@@ -91,27 +98,36 @@
9198
});
9299
93100
const titleColor = computed(() => {
94-
return props.kind === 'warning' ? paletteTheme.red.v_600 : tokensTheme.text;
101+
if (props.kind === 'warning') return paletteTheme.red.v_600;
102+
if (props.kind === 'success') return paletteTheme.green.v_600;
103+
return tokensTheme.text;
95104
});
96105
97106
const descriptionColor = computed(() => {
98107
return props.kind === 'warning' ? paletteTheme.grey.v_800 : tokensTheme.text;
99108
});
100109
110+
const iconColor = computed(() => {
111+
if (props.kind === 'warning') return paletteTheme.red.v_600;
112+
if (props.kind === 'success') return paletteTheme.green.v_600;
113+
return tokensTheme.text;
114+
});
115+
101116
return {
102117
boxBackgroundColor,
103118
boxBorderColor,
104119
titleColor,
105120
descriptionColor,
106121
icon,
122+
iconColor,
107123
};
108124
},
109125
props: {
110126
kind: {
111127
type: String,
112128
required: false,
113129
default: 'info',
114-
validator: value => ['warning', 'info'].includes(value),
130+
validator: value => ['warning', 'success', 'info'].includes(value),
115131
},
116132
loading: {
117133
type: Boolean,

contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/__tests__/SubmitToCommunityLibrarySidePanel.spec.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,9 @@ import { communityChannelsStrings } from 'shared/strings/communityChannelsString
1313
import { CommunityLibrarySubmission } from 'shared/data/resources';
1414
import CountryField from 'shared/views/form/CountryField.vue';
1515

16-
jest.mock('../composables/usePublishedData', () => ({
17-
usePublishedData: jest.fn(),
18-
}));
19-
jest.mock('../composables/useLatestCommunityLibrarySubmission', () => ({
20-
useLatestCommunityLibrarySubmission: jest.fn(),
21-
}));
16+
jest.mock('../composables/usePublishedData');
17+
jest.mock('../composables/useLatestCommunityLibrarySubmission');
18+
jest.mock('../composables/useLicenseAudit');
2219
jest.mock('shared/data/resources', () => ({
2320
CommunityLibrarySubmission: {
2421
create: jest.fn(() => Promise.resolve()),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { computed, ref } from 'vue';
2+
3+
const MOCK_DEFAULTS = {
4+
isLoading: ref(true),
5+
isFinished: ref(false),
6+
data: computed(() => null),
7+
fetchData: jest.fn(() => Promise.resolve()),
8+
};
9+
10+
export function useLatestCommunityLibrarySubmissionMock(overrides = {}) {
11+
return {
12+
...MOCK_DEFAULTS,
13+
...overrides,
14+
};
15+
}
16+
17+
export const useLatestCommunityLibrarySubmission = jest.fn(() =>
18+
useLatestCommunityLibrarySubmissionMock(),
19+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { computed, ref } from 'vue';
2+
3+
const MOCK_DEFAULTS = {
4+
isLoading: computed(() => false),
5+
isFinished: computed(() => true),
6+
invalidLicenses: computed(() => []),
7+
specialPermissions: computed(() => []),
8+
includedLicenses: computed(() => []),
9+
isAuditing: ref(false),
10+
hasAuditData: computed(() => false),
11+
auditTaskId: ref(null),
12+
error: ref(null),
13+
checkAndTriggerAudit: jest.fn(),
14+
triggerAudit: jest.fn(),
15+
fetchPublishedData: jest.fn(),
16+
};
17+
18+
export function useLicenseAuditMock(overrides = {}) {
19+
return {
20+
...MOCK_DEFAULTS,
21+
...overrides,
22+
};
23+
}
24+
25+
export const useLicenseAudit = jest.fn(() => useLicenseAuditMock());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { computed, ref } from 'vue';
2+
3+
const MOCK_DEFAULTS = {
4+
isLoading: ref(true),
5+
isFinished: ref(false),
6+
data: computed(() => null),
7+
fetchData: jest.fn(() => Promise.resolve()),
8+
};
9+
10+
export function usePublishedDataMock(overrides = {}) {
11+
return {
12+
...MOCK_DEFAULTS,
13+
...overrides,
14+
};
15+
}
16+
17+
export const usePublishedData = jest.fn(() => usePublishedDataMock());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { computed, ref, unref, watch } from 'vue';
2+
import { Channel } from 'shared/data/resources';
3+
4+
export function useLicenseAudit(channelRef, channelVersionRef) {
5+
const isAuditing = ref(false);
6+
const auditTaskId = ref(null);
7+
const auditError = ref(null);
8+
const publishedData = ref(null);
9+
10+
watch(
11+
() => unref(channelRef)?.published_data,
12+
newPublishedData => {
13+
if (newPublishedData) {
14+
publishedData.value = newPublishedData;
15+
if (isAuditing.value) {
16+
isAuditing.value = false;
17+
auditError.value = null;
18+
}
19+
}
20+
},
21+
{ immediate: true, deep: true },
22+
);
23+
24+
const currentVersionData = computed(() => {
25+
const version = unref(channelVersionRef);
26+
if (!publishedData.value || version == null) {
27+
return undefined;
28+
}
29+
return publishedData.value[version];
30+
});
31+
32+
const hasAuditData = computed(() => {
33+
const versionData = currentVersionData.value;
34+
if (!versionData) {
35+
return false;
36+
}
37+
38+
return (
39+
'community_library_invalid_licenses' in versionData &&
40+
'community_library_special_permissions' in versionData
41+
);
42+
});
43+
44+
const invalidLicenses = computed(() => {
45+
const versionData = currentVersionData.value;
46+
return versionData?.community_library_invalid_licenses || [];
47+
});
48+
49+
const specialPermissions = computed(() => {
50+
const versionData = currentVersionData.value;
51+
return versionData?.community_library_special_permissions || [];
52+
});
53+
54+
const includedLicenses = computed(() => {
55+
const versionData = currentVersionData.value;
56+
return versionData?.included_licenses || [];
57+
});
58+
59+
const isAuditComplete = computed(() => {
60+
return publishedData.value !== null && hasAuditData.value;
61+
});
62+
63+
async function triggerAudit() {
64+
if (isAuditing.value) return;
65+
66+
try {
67+
isAuditing.value = true;
68+
auditError.value = null;
69+
70+
const channelId = unref(channelRef)?.id;
71+
if (!channelId) {
72+
throw new Error('Channel ID is required to trigger audit');
73+
}
74+
75+
const response = await Channel.auditLicenses(channelId);
76+
auditTaskId.value = response.task_id;
77+
} catch (error) {
78+
isAuditing.value = false;
79+
auditError.value = error;
80+
throw error;
81+
}
82+
}
83+
84+
async function fetchPublishedData() {
85+
const channelId = unref(channelRef)?.id;
86+
if (!channelId) return;
87+
88+
try {
89+
const data = await Channel.getPublishedData(channelId);
90+
publishedData.value = data;
91+
} catch (error) {
92+
auditError.value = error;
93+
throw error;
94+
}
95+
}
96+
97+
async function checkAndTriggerAudit() {
98+
if (!publishedData.value) {
99+
await fetchPublishedData();
100+
}
101+
102+
if (hasAuditData.value || isAuditing.value) {
103+
return;
104+
}
105+
106+
await triggerAudit();
107+
}
108+
109+
return {
110+
isLoading: computed(() => {
111+
if (isAuditComplete.value || auditError.value) return false;
112+
return isAuditing.value;
113+
}),
114+
isFinished: computed(() => isAuditComplete.value),
115+
isAuditing,
116+
invalidLicenses,
117+
specialPermissions,
118+
includedLicenses,
119+
hasAuditData,
120+
auditTaskId,
121+
error: auditError,
122+
123+
checkAndTriggerAudit,
124+
triggerAudit,
125+
fetchPublishedData,
126+
};
127+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { computed, ref, unref, watch } from 'vue';
2+
import { AuditedSpecialPermissionsLicense } from 'shared/data/resources';
3+
4+
const ITEMS_PER_PAGE = 3;
5+
6+
/**
7+
* Composable that fetches and paginates audited special-permissions licenses
8+
* for a given set of permission IDs.
9+
*
10+
* @param {Array<string|number>|import('vue').Ref<Array<string|number>>} permissionIds
11+
* A list (or ref to a list) of special-permissions license IDs to fetch.
12+
*
13+
* @returns {{
14+
* permissions: import('vue').Ref<Array<Object>>,
15+
* currentPagePermissions: import('vue').ComputedRef<Array<Object>>,
16+
* isLoading: import('vue').Ref<boolean>,
17+
* error: import('vue').Ref<Error|null>,
18+
* currentPage: import('vue').Ref<number>,
19+
* totalPages: import('vue').ComputedRef<number>,
20+
* nextPage: () => void,
21+
* previousPage: () => void,
22+
* }}
23+
* Reactive state for the fetched, flattened permissions and pagination
24+
* helpers used by `SpecialPermissionsList.vue`.
25+
*/
26+
export function useSpecialPermissions(permissionIds) {
27+
const permissions = ref([]);
28+
const isLoading = ref(false);
29+
const error = ref(null);
30+
const currentPage = ref(1);
31+
32+
const totalPages = computed(() => {
33+
return Math.ceil(permissions.value.length / ITEMS_PER_PAGE);
34+
});
35+
36+
const currentPagePermissions = computed(() => {
37+
const start = (currentPage.value - 1) * ITEMS_PER_PAGE;
38+
const end = start + ITEMS_PER_PAGE;
39+
return permissions.value.slice(start, end);
40+
});
41+
42+
async function fetchPermissions(ids) {
43+
if (!ids || ids.length === 0) {
44+
permissions.value = [];
45+
return;
46+
}
47+
48+
isLoading.value = true;
49+
error.value = null;
50+
51+
try {
52+
const response = await AuditedSpecialPermissionsLicense.fetchCollection({
53+
by_ids: ids.join(','),
54+
distributable: false,
55+
});
56+
57+
permissions.value = response.map(permission => ({
58+
id: permission.id,
59+
description: permission.description,
60+
distributable: permission.distributable,
61+
}));
62+
} catch (err) {
63+
error.value = err;
64+
permissions.value = [];
65+
} finally {
66+
isLoading.value = false;
67+
}
68+
}
69+
70+
function nextPage() {
71+
if (currentPage.value < totalPages.value) {
72+
currentPage.value += 1;
73+
}
74+
}
75+
76+
function previousPage() {
77+
if (currentPage.value > 1) {
78+
currentPage.value -= 1;
79+
}
80+
}
81+
82+
const resolvedPermissionIds = computed(() => {
83+
const ids = unref(permissionIds);
84+
if (!ids || ids.length === 0) {
85+
return [];
86+
}
87+
return ids;
88+
});
89+
90+
watch(
91+
resolvedPermissionIds,
92+
ids => {
93+
fetchPermissions(ids);
94+
},
95+
{ immediate: true },
96+
);
97+
98+
return {
99+
permissions,
100+
currentPagePermissions,
101+
isLoading,
102+
error,
103+
currentPage,
104+
totalPages,
105+
nextPage,
106+
previousPage,
107+
};
108+
}

0 commit comments

Comments
 (0)