From 102f9f3cb19bb92e93e5d778b6949e4dec716ba8 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 5 Dec 2024 17:39:47 -0600 Subject: [PATCH] MSP Dashboard: Add labels to profiles page (#23145) #22073 Changes: - Added the ability to use labels to assign configuration profiles on the profiles page. --- .../api/controllers/get-labels.js | 45 ++++++++ .../api/controllers/get-profiles.js | 71 +++++++----- .../api/controllers/profiles/edit-profile.js | 78 +++++++++---- .../controllers/profiles/upload-profile.js | 42 +++++-- .../api/controllers/profiles/view-profiles.js | 61 ++++++---- .../api/models/UndeployedProfile.js | 20 ++++ .../assets/images/Icon-error-16x16@2x.png | Bin 0 -> 713 bytes .../assets/js/cloud.setup.js | 2 +- .../js/components/cloud-error.component.js | 4 +- .../js/components/multifield.component.js | 94 +++++++++++---- .../assets/js/pages/profiles.page.js | 59 +++++++++- .../assets/styles/pages/profiles.less | 77 +++++++++++++ ee/bulk-operations-dashboard/config/routes.js | 1 + .../views/pages/profiles.ejs | 109 +++++++++++++++--- 14 files changed, 523 insertions(+), 140 deletions(-) create mode 100644 ee/bulk-operations-dashboard/api/controllers/get-labels.js create mode 100644 ee/bulk-operations-dashboard/assets/images/Icon-error-16x16@2x.png diff --git a/ee/bulk-operations-dashboard/api/controllers/get-labels.js b/ee/bulk-operations-dashboard/api/controllers/get-labels.js new file mode 100644 index 0000000000..ba8917c91c --- /dev/null +++ b/ee/bulk-operations-dashboard/api/controllers/get-labels.js @@ -0,0 +1,45 @@ +module.exports = { + + + friendlyName: 'Get labels', + + + description: 'Builds and returns an array of labels on the Fleet instance.', + + + exits: { + success: { + outputType: [{}], + } + }, + + + fn: async function () { + + + let labelsOnThisInstance = []; + + let labelsResponseData = await sails.helpers.http.get.with({ + url: '/api/v1/fleet/labels', + baseUrl: sails.config.custom.fleetBaseUrl, + headers: { + Authorization: `Bearer ${sails.config.custom.fleetApiToken}` + } + }) + .timeout(120000) + .retry(['requestFailed', {name: 'TimeoutError'}]); + + for(let label of labelsResponseData.labels) { + labelsOnThisInstance.push({ + name: label.name, + value: label.id + }); + } + labelsOnThisInstance = _.sortBy(labelsOnThisInstance, 'name'); + // All done. + return labelsOnThisInstance; + + } + + +}; diff --git a/ee/bulk-operations-dashboard/api/controllers/get-profiles.js b/ee/bulk-operations-dashboard/api/controllers/get-profiles.js index 7326fea9ae..dfe52aa8f4 100644 --- a/ee/bulk-operations-dashboard/api/controllers/get-profiles.js +++ b/ee/bulk-operations-dashboard/api/controllers/get-profiles.js @@ -13,7 +13,9 @@ module.exports = { }, + fn: async function () { + // Get all teams on the Fleet instance. let teamsResponseData = await sails.helpers.http.get.with({ url: '/api/v1/fleet/teams', @@ -57,7 +59,6 @@ module.exports = { let profilesForThisTeam = configurationProfilesResponseData.profiles; allProfiles = allProfiles.concat(profilesForThisTeam); } - // Add the configurations profiles that are assigned to the "no team" team. let noTeamConfigurationProfilesResponseData = await sails.helpers.http.get.with({ url: '/api/v1/fleet/configuration_profiles', @@ -68,45 +69,53 @@ module.exports = { }) .timeout(120000) .retry(['requestFailed', {name: 'TimeoutError'}]); - let profilesForThisTeam = noTeamConfigurationProfilesResponseData.profiles; - allProfiles = allProfiles.concat(profilesForThisTeam); - - // console.log(allProfiles); - - let profilesOnThisFleetInstance = []; - // Group configuration profiles by their identifier. - let allProfilesByIdentifier = _.groupBy(allProfiles, 'identifier'); - for(let profileIdentifier in allProfilesByIdentifier) { - // Iterate through the arrays of profiles with the same unique identifier. - let teamsForThisProfile = []; - // Add the profile's UUID and information about the team this profile is assigned to the teams array for profiles. - for(let profile of allProfilesByIdentifier[profileIdentifier]){ - let informationAboutThisProfile = { + let profilesForNoTeam = noTeamConfigurationProfilesResponseData.profiles; + allProfiles = allProfiles.concat(profilesForNoTeam); + let profilesInformation = []; + for(let profile of allProfiles) { + let profileInformation = { + name: profile.name, + identifier: profile.identifier, + platform: profile.platform, + createdAt: new Date(profile.created_at).getTime(), + team: { uuid: profile.profile_uuid, fleetApid: profile.team_id, teamName: _.find(teams, {fleetApid: profile.team_id}).teamName, - }; - teamsForThisProfile.push(informationAboutThisProfile); - } - let profile = allProfilesByIdentifier[profileIdentifier][0];// Grab the first profile returned in the api repsonse to build our profile configuration. - let profileInformation = { - name: profile.name, - identifier: profileIdentifier, - platform: profile.platform, - createdAt: new Date(profile.created_at).getTime(), - teams: teamsForThisProfile + }, + profileTarget: 'all', }; - profilesOnThisFleetInstance.push(profileInformation); + if(profile.labels_include_all) { + profileInformation.labels = _.pluck(profile.labels_include_all, 'name'); + profileInformation.profileTarget = 'custom'; + profileInformation.labelTargetBehavior = 'include'; + } else if(profile.labels_exclude_any){ + profileInformation.labels = _.pluck(profile.labels_exclude_any, 'name'); + profileInformation.profileTarget = 'custom'; + profileInformation.labelTargetBehavior = 'exclude'; + } + profilesInformation.push(profileInformation); } + // Group the profiles based on identifier, labels, and labelTargetBehavior + let profilesGroupedbyLabelsAndIdentifier = _.groupBy(profilesInformation, (profile)=>{ + return `${profile.identifier}|${JSON.stringify(profile.labels)}|${profile.labelTargetBehavior}`; + }); + + // map the grouped profiles and merge profiles that have the same labels, target behavior, and identifier. + let allProfilesOnFleetInstance = Object.values(profilesGroupedbyLabelsAndIdentifier).map(profileGroup => { + return { + ...profileGroup[0],// Expand the first item in the profileGroup + teams: profileGroup.map(item => item.team)// Merge the teams arrays + }; + }); // Get the undeployed profiles from the app's database. let undeployedProfiles = await UndeployedProfile.find(); - profilesOnThisFleetInstance = _.union(profilesOnThisFleetInstance, undeployedProfiles); - + allProfilesOnFleetInstance = _.union(allProfilesOnFleetInstance, undeployedProfiles); // Sort profiles by their name. - profilesOnThisFleetInstance = _.sortByOrder(profilesOnThisFleetInstance, 'name', 'asc'); - - return profilesOnThisFleetInstance; + allProfilesOnFleetInstance = _.sortByOrder(allProfilesOnFleetInstance, 'name', 'asc'); + // return the updated list of profiles + return allProfilesOnFleetInstance; } diff --git a/ee/bulk-operations-dashboard/api/controllers/profiles/edit-profile.js b/ee/bulk-operations-dashboard/api/controllers/profiles/edit-profile.js index bede01b44f..512fd95813 100644 --- a/ee/bulk-operations-dashboard/api/controllers/profiles/edit-profile.js +++ b/ee/bulk-operations-dashboard/api/controllers/profiles/edit-profile.js @@ -22,6 +22,20 @@ module.exports = { type: 'ref', description: 'A file that will be replacing the profile.' }, + profileTarget: { + type: 'string', + description: 'The target for this configuration profile', + defaultsTo: 'all', + isIn: ['all', 'custom'], + }, + labelTargetBehavior: { + type: 'string', + isIn: ['include', 'exclude'], + }, + labels: { + type: ['string'], + description: 'A list of the names of labels that will be included/excluded.' + } }, @@ -34,7 +48,7 @@ module.exports = { - fn: async function ({profile, newTeamIds, newProfile}) { + fn: async function ({profile, newTeamIds, newProfile, profileTarget, labelTargetBehavior, labels}) { if(newProfile.isNoop){ newProfile.noMoreFiles(); newProfile = undefined; @@ -92,6 +106,8 @@ module.exports = { filename = profile.name; extension = profile.profileType; } + + // ╔═╗╔═╗╔═╗╦╔═╗╔╗╔ ╔═╗╦═╗╔═╗╔═╗╦╦ ╔═╗ // ╠═╣╚═╗╚═╗║║ ╦║║║ ╠═╝╠╦╝║ ║╠╣ ║║ ║╣ // ╩ ╩╚═╝╚═╝╩╚═╝╝╚╝ ╩ ╩╚═╚═╝╚ ╩╩═╝╚═╝ @@ -116,21 +132,24 @@ module.exports = { } for(let teamApid of addedTeams){ // console.log(`Adding ${profile.name} to team id ${teamApid}`); + let bodyForThisRequest = { + team_id: teamApid,// eslint-disable-line camelcase + labels_exclude_any: labelTargetBehavior === 'exclude' ? labels : undefined,// eslint-disable-line camelcase + labels_include_all: labelTargetBehavior === 'include' ? labels : undefined,// eslint-disable-line camelcase + profile: { + options: { + filename: filename + extension, + contentType: 'application/octet-stream' + }, + value: profileContents, + } + }; await sails.helpers.http.sendHttpRequest.with({ method: 'POST', baseUrl: sails.config.custom.fleetBaseUrl, url: `/api/v1/fleet/configuration_profiles?team_id=${teamApid}`, enctype: 'multipart/form-data', - body: { - team_id: teamApid,// eslint-disable-line camelcase - profile: { - options: { - filename: filename + extension, - contentType: 'application/octet-stream' - }, - value: profileContents, - } - }, + body: bodyForThisRequest, headers: { Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, }, @@ -152,22 +171,25 @@ module.exports = { } } for(let teamApid of newTeamIds){ + let bodyForThisRequest = { + team_id: teamApid,// eslint-disable-line camelcase + labels_exclude_any: labelTargetBehavior === 'exclude' ? labels : undefined,// eslint-disable-line camelcase + labels_include_all: labelTargetBehavior === 'include' ? labels : undefined,// eslint-disable-line camelcase + profile: { + options: { + filename: filename + extension, + contentType: 'application/octet-stream' + }, + value: profileContents, + } + }; // console.log(`Adding ${profile.name} to team id ${teamApid}`); await sails.helpers.http.sendHttpRequest.with({ method: 'POST', baseUrl: sails.config.custom.fleetBaseUrl, url: `/api/v1/fleet/configuration_profiles?team_id=${teamApid}`, enctype: 'multipart/form-data', - body: { - team_id: teamApid,// eslint-disable-line camelcase - profile: { - options: { - filename: filename + extension, - contentType: 'application/octet-stream' - }, - value: profileContents, - } - }, + body: bodyForThisRequest, headers: { Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, }, @@ -187,12 +209,26 @@ module.exports = { platform: extension === '.xml' ? 'windows' : 'darwin', profileContents, profileType: extension, + labels, + labelTargetBehavior, + profileTarget, }); } else if(profile.id && newProfile){ // If there is a new profile that is replacing a database record, update the profileContents in the database. // console.log('Updating existing undeployed profile!'); await UndeployedProfile.updateOne({id: profile.id}).set({ profileContents, + labels, + labelTargetBehavior, + profileTarget, + }); + } else if(profile.id && labels) { + // Update label target behavior for undeployed profiles. + await UndeployedProfile.updateOne({id: profile.id}).set({ + profileContents, + labels, + labelTargetBehavior, + profileTarget, }); } // All done. diff --git a/ee/bulk-operations-dashboard/api/controllers/profiles/upload-profile.js b/ee/bulk-operations-dashboard/api/controllers/profiles/upload-profile.js index 19c847bf62..ddb28f9d40 100644 --- a/ee/bulk-operations-dashboard/api/controllers/profiles/upload-profile.js +++ b/ee/bulk-operations-dashboard/api/controllers/profiles/upload-profile.js @@ -17,6 +17,20 @@ module.exports = { teams: { type: ['string'], description: 'An array of team IDs that this profile will be added to' + }, + profileTarget: { + type: 'string', + description: 'The target for this configuration profile', + defaultsTo: 'all', + isIn: ['all', 'custom'], + }, + labelTargetBehavior: { + type: 'string', + isIn: ['include', 'exclude'], + }, + labels: { + type: ['string'], + description: 'A list of the names of labels that will be included/excluded.' } }, @@ -40,7 +54,7 @@ module.exports = { }, - fn: async function ({newProfile, teams}) { + fn: async function ({newProfile, teams, profileTarget, labelTargetBehavior, labels}) { let util = require('util'); let profile = await sails.reservoir(newProfile) .intercept('E_EXCEEDS_UPLOAD_LIMIT', 'tooBig') @@ -63,6 +77,9 @@ module.exports = { platform: profilePlatform, profileType: extension, createdAt: Date.now(), + profileTarget, + labels, + labelTargetBehavior, }; if(!teams) { newProfileInfo.profileContents = profileContents; @@ -70,21 +87,24 @@ module.exports = { } else { let newTeams = []; for(let teamApid of teams){ + let bodyForThisRequest = { + team_id: teamApid,// eslint-disable-line camelcase + labels_exclude_any: labelTargetBehavior === 'exclude' ? labels : undefined,// eslint-disable-line camelcase + labels_include_all: labelTargetBehavior === 'include' ? labels : undefined,// eslint-disable-line camelcase + profile: { + options: { + filename: profileFileName, + contentType: 'application/octet-stream' + }, + value: profileContents, + } + }; let newProfileResponse = await sails.helpers.http.sendHttpRequest.with({ method: 'POST', baseUrl: sails.config.custom.fleetBaseUrl, url: `/api/v1/fleet/configuration_profiles?team_id=${teamApid}`, enctype: 'multipart/form-data', - body: { - team_id: teamApid,// eslint-disable-line camelcase - profile: { - options: { - filename: profileFileName, - contentType: 'application/octet-stream' - }, - value: profileContents, - } - }, + body: bodyForThisRequest, headers: { Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, }, diff --git a/ee/bulk-operations-dashboard/api/controllers/profiles/view-profiles.js b/ee/bulk-operations-dashboard/api/controllers/profiles/view-profiles.js index 9ebe807932..05dff23e1e 100644 --- a/ee/bulk-operations-dashboard/api/controllers/profiles/view-profiles.js +++ b/ee/bulk-operations-dashboard/api/controllers/profiles/view-profiles.js @@ -71,42 +71,55 @@ module.exports = { }) .timeout(120000) .retry(['requestFailed', {name: 'TimeoutError'}]); - let profilesForThisTeam = noTeamConfigurationProfilesResponseData.profiles; - allProfiles = allProfiles.concat(profilesForThisTeam); + + let profilesForNoTeam = noTeamConfigurationProfilesResponseData.profiles; + allProfiles = allProfiles.concat(profilesForNoTeam); + let profilesInformation = []; - // Group configuration profiles by their identifier. - let allProfilesByIdentifier = _.groupBy(allProfiles, 'identifier'); - for(let profileIdentifier in allProfilesByIdentifier) { - // Iterate through the arrays of profiles with the same unique identifier. - let teamsForThisProfile = []; - // Add the profile's UUID and information about the team this profile is assigned to the teams array for profiles. - for(let profile of allProfilesByIdentifier[profileIdentifier]) { - let informationAboutThisProfile = { + for(let profile of allProfiles) { + let profileInformation = { + name: profile.name, + identifier: profile.identifier, + platform: profile.platform, + createdAt: new Date(profile.created_at).getTime(), + team: { uuid: profile.profile_uuid, fleetApid: profile.team_id, teamName: _.find(teams, {fleetApid: profile.team_id}).teamName, - }; - teamsForThisProfile.push(informationAboutThisProfile); - } - let profile = allProfilesByIdentifier[profileIdentifier][0];// Grab the first profile returned in the api repsonse to build our profile configuration. - let profileInformation = { - name: profile.name, - identifier: profileIdentifier, - platform: profile.platform, - createdAt: new Date(profile.created_at).getTime(), - teams: teamsForThisProfile + }, + profileTarget: 'all', }; + if(profile.labels_include_all) { + profileInformation.labels = _.pluck(profile.labels_include_all, 'name'); + profileInformation.profileTarget = 'custom'; + profileInformation.labelTargetBehavior = 'include'; + } else if(profile.labels_exclude_any){ + profileInformation.labels = _.pluck(profile.labels_exclude_any, 'name'); + profileInformation.profileTarget = 'custom'; + profileInformation.labelTargetBehavior = 'exclude'; + } profilesInformation.push(profileInformation); } + // Group the profiles based on identifier, labels, and labelTargetBehavior + let profilesGroupedbyLabelsAndIdentifier = _.groupBy(profilesInformation, (profile)=>{ + return `${profile.identifier}|${JSON.stringify(profile.labels)}|${profile.labelTargetBehavior}`; + }); + + // map the grouped profiles and merge profiles that have the same labels, target behavior, and identifier. + let allProfilesOnFleetInstance = Object.values(profilesGroupedbyLabelsAndIdentifier).map(profileGroup => { + return { + ...profileGroup[0],// expand the first item in the profileGroup + teams: profileGroup.map(item => item.team)// Merge the teams arrays + }; + }); // Get the undeployed profiles from the app's database. let undeployedProfiles = await UndeployedProfile.find(); - profilesInformation = _.union(profilesInformation, undeployedProfiles); - + allProfilesOnFleetInstance = _.union(allProfilesOnFleetInstance, undeployedProfiles); // Sort profiles by their name. - profilesInformation = _.sortByOrder(profilesInformation, 'name', 'asc'); + allProfilesOnFleetInstance = _.sortByOrder(allProfilesOnFleetInstance, 'name', 'asc'); // Respond with view. - return {profiles: profilesInformation, teams}; + return {profiles: allProfilesOnFleetInstance, teams}; } diff --git a/ee/bulk-operations-dashboard/api/models/UndeployedProfile.js b/ee/bulk-operations-dashboard/api/models/UndeployedProfile.js index 2d13a1993f..e869256c5f 100644 --- a/ee/bulk-operations-dashboard/api/models/UndeployedProfile.js +++ b/ee/bulk-operations-dashboard/api/models/UndeployedProfile.js @@ -46,6 +46,26 @@ module.exports = { }, + labels: { + type: 'json', + example: ['All hosts', 'Linux hosts'], + description: 'A list of the Fleet API IDs of labels this profile is associated with (if any).', + }, + + labelTargetBehavior: { + type: 'string', + description: 'Whether to exclude or include hosts with the labels in the labels attribute when assigning this profile.', + isIn: ['exclude', 'include'], + defaultsTo: 'include', + }, + + profileTarget: { + type: 'string', + description: 'What hosts will be targetted when this profile is deployed. "all" for all hosts, or "custom" if a profile targets hosts by labels', + isIn: ['all', 'custom'], + defaultsTo: 'all', + }, + // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ // ║╣ ║║║╠╩╗║╣ ║║╚═╗ // ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝ diff --git a/ee/bulk-operations-dashboard/assets/images/Icon-error-16x16@2x.png b/ee/bulk-operations-dashboard/assets/images/Icon-error-16x16@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec36a820bed4246b98bb87fabde9345f98d9f2d GIT binary patch literal 713 zcmV;)0yh1LP)#-)6>&KD9Ddq-281XyE9OS7;S(efSc)Cnn*ig!dR}O z`$7F=cMwmC*riP$ocH2rW~Yb5)Lja0Mx6O%f?B^dh!bq%fcE&82!qkXO&|<*#F6Vp zB-a7$HQVVI5sy#?goJ|He%@%9s}IgzzG;^#u81wQ`CV*d&9@aw@A2e@NIv#W7u>XKej4b2N{yD|mTK}8tMI!2Oq z8x6m?U>k@Z=C=JvLIQu2hWAR) zko{zAnD{HUM1^mbMR{Z{rGTs|4jQ6u_T0YApW)B(PFQ -

An unexpected error occurred communicating with the Fleet API

+
+

An unexpected error occurred communicating with the Fleet API

`, diff --git a/ee/bulk-operations-dashboard/assets/js/components/multifield.component.js b/ee/bulk-operations-dashboard/assets/js/components/multifield.component.js index 530ab7ef6e..bf386ef14a 100644 --- a/ee/bulk-operations-dashboard/assets/js/components/multifield.component.js +++ b/ee/bulk-operations-dashboard/assets/js/components/multifield.component.js @@ -51,33 +51,44 @@ parasails.registerComponent('multifield', { // ╩ ╩ ╩ ╩ ╩╩═╝ template: `
-
- - - - - - - - - - +
+
+
+ + +
+
-
- +  {{addButtonText || 'Add another'}} -   +
+
+ + + + + + + + + + +
+
+ `, // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ @@ -88,7 +99,6 @@ parasails.registerComponent('multifield', { if (this.value !== undefined && !_.isArray(this.value)) { throw new Error('In , if specified, `v-model`/`:value` must be either an array or `undefined`. But instead, got: '+this.value); }//• - if (this.value === undefined || _.isEqual(this.value, [])) { this.currentFieldValues = [ undefined ]; } else { @@ -146,6 +156,26 @@ parasails.registerComponent('multifield', { this.optionsForSelect = _.clone(this.selectOptions); } } + if(this.inputType === 'checkboxes') { + if(!_.isArray(this.selectOptions)){ + throw new Error('Missing selectOptions. When using inputType="select", an array of selectOptions is required.'); + } else { + for(let option of this.selectOptions){ + // If we're using inputType="select", we will validate all options before cloning the object. + if(!option.value){ + throw new Error(`Option in selectOptions is missing a value. When using inputType="select", A value property is required for all objects in the selectOptions array. Object missing a value. ${option}`); + } + if(!option.name){ + throw new Error(`Option in selectOptions is missing a name. When using inputType="select", A name property is required for all objects in the selectOptions array. Object missing a name. ${option}`); + } + } + this.optionsForSelect = _.clone(this.selectOptions); + if(this.currentFieldValues === [null]){ + this.currentFieldValues = []; + + } + } + } if(this.placeholder){ this.inputPlaceholder = this.placeholder; } @@ -196,6 +226,20 @@ parasails.registerComponent('multifield', { this._handleChangingFieldValues(); }, + inputCheckboxItemField: async function($event) { + let checkboxValue = $event.target.value; + if($event.target.checked) { + this.currentFieldValues.push(checkboxValue); + } else { + this.currentFieldValues = this.currentFieldValues + .filter(value => value !== checkboxValue); + } + this.currentFieldValues = this.currentFieldValues.filter(value => value !== undefined && value !== null); + await this.forceRender(); + this._handleChangingFieldValues(); + }, + + clickAddItem: async function() { this.currentFieldValues.push(undefined); await this.forceRender();//«« this is so that the programmatic focusing code below will work diff --git a/ee/bulk-operations-dashboard/assets/js/pages/profiles.page.js b/ee/bulk-operations-dashboard/assets/js/pages/profiles.page.js index 453647a428..9f09bd0ff8 100644 --- a/ee/bulk-operations-dashboard/assets/js/pages/profiles.page.js +++ b/ee/bulk-operations-dashboard/assets/js/pages/profiles.page.js @@ -55,13 +55,13 @@ parasails.registerPage('profiles', { if(this.teamFilter !== undefined){ this.selectedTeam = _.find(this.teams, {fleetApid: this.teamFilter}); let profilesOnThisTeam = _.filter(this.profiles, (profile)=>{ - // console.log(profile.profiles); return profile.teams && _.where(profile.teams, {'fleetApid': this.selectedTeam.fleetApid}).length > 0; }); this.profilesToDisplay = profilesOnThisTeam; } else { this.profilesToDisplay = this.profiles; } + await this.forceRender(); }, clickChangeTeamFilter: async function(teamApid) {// Used by the tooltip links. this.teamFilter = teamApid; @@ -79,22 +79,35 @@ parasails.registerPage('profiles', { } }, clickOpenEditModal: async function(profile) { - this.profileToEdit = _.clone(profile); - this.formData.newTeamIds = _.pluck(this.profileToEdit.teams, 'fleetApid'); - this.formData.profile = profile; + this.profileToEdit = _.cloneDeep(profile); + console.log(this.profileToEdit); + this.formData = { + profile: _.clone(this.profileToEdit), + newTeamIds: _.pluck(this.profileToEdit.teams, 'fleetApid'), + profileTarget: this.profileToEdit.profileTarget === 'custom' ? 'custom' : 'all', + labelTargetBehavior: this.profileToEdit.labelTargetBehavior ? this.profileToEdit.labelTargetBehavior : 'include', + labels: this.profileToEdit.labels ? this.profileToEdit.labels : [], + }; + console.log(this.formData); this.modal = 'edit-profile'; + await this._getLabels(); }, clickOpenDeleteModal: async function(profile) { this.formData.profile = _.clone(profile); this.modal = 'delete-profile'; }, clickOpenAddProfileModal: async function() { + this.$set(this.formData, 'profileTarget', 'all'); + this.$set(this.formData, 'labels', []); + this.$set(this.formData, 'labelTargetBehavior', 'include'); this.modal = 'add-profile'; + await this._getLabels(); }, closeModal: async function() { this.modal = ''; this.formErrors = {}; this.formData = {}; + this.cloudError = ''; await this.forceRender(); }, submittedForm: async function() { @@ -108,7 +121,13 @@ parasails.registerPage('profiles', { }, handleSubmittingAddProfileForm: async function() { let argins = _.clone(this.formData); - await Cloud.uploadProfile.with({newProfile: argins.newProfile, teams: argins.teams}); + await Cloud.uploadProfile.with({ + newProfile: argins.newProfile, + teams: argins.teams, + profileTarget: argins.profileTarget, + labels: argins.profileTarget !== 'all' ? argins.labels : [], + labelTargetBehavior: argins.profileTarget !== 'all' ? argins.labelTargetBehavior : undefined, + }); await this._getProfiles(); }, handleSubmittingEditProfileForm: async function() { @@ -116,7 +135,22 @@ parasails.registerPage('profiles', { if(argins.newTeamIds === [undefined]){ argins.newTeamIds = []; } - await Cloud.editProfile.with({profile: argins.profile, newProfile: argins.newProfile, newTeamIds: argins.newTeamIds}); + if(argins.profileTarget === 'custom'){ + await Cloud.editProfile.with({ + profile: argins.profile, + newTeamIds: argins.newTeamIds, + newProfile: argins.newProfile, + labels: argins.labels, + profileTarget: argins.profileTarget, + labelTargetBehavior: argins.labelTargetBehavior, + }); + } else { + await Cloud.editProfile.with({ + profile: argins.profile, + newTeamIds: argins.newTeamIds, + newProfile: argins.newProfile + }); + } await this._getProfiles(); }, _getProfiles: async function() { @@ -126,6 +160,19 @@ parasails.registerPage('profiles', { this.profiles = newProfilesInformation; this.overlaySyncing = false; await this.changeTeamFilter(); + }, + _getLabels: async function() { + this.syncing = true; + this.labelsSyncing = true; + this.labels = await Cloud.getLabels().tolerate((err)=>{ + this.cloudError = err; + this.syncing = false; + }); + if(!this.cloudError){ + this.labelsSyncing = false; + this.syncing = false; + + } } } }); diff --git a/ee/bulk-operations-dashboard/assets/styles/pages/profiles.less b/ee/bulk-operations-dashboard/assets/styles/pages/profiles.less index 68e3a3736b..96faed475e 100644 --- a/ee/bulk-operations-dashboard/assets/styles/pages/profiles.less +++ b/ee/bulk-operations-dashboard/assets/styles/pages/profiles.less @@ -213,6 +213,83 @@ margin-right: 8px; } } + [purpose='form-option'] { + user-select: none; + cursor: pointer; + width: fit-content; + padding: 8px 12px 8px 8px; + margin-bottom: 16px; + display: flex; + flex-direction: row; + align-items: center; + border-radius: 7px; + border: 1px solid #E2E4EA; + font-size: 16px; + line-height: 24px; + color: #515774; + white-space: nowrap; + height: fit-content; + input { + cursor: pointer; + margin-right: 8px; + display: none; + } + [purpose='custom-radio'] { + margin-right: 8px; + display: flex; + min-width: 18px; + min-height: 18px; + border-radius: 50%; + border: 1px solid #E2E4EA; + justify-content: center; + align-items: center; + [purpose='custom-radio-selected'] { + min-width: 10px; + min-height: 10px; + border-radius: 50%; + background-color: @core-vibrant-blue; + transform: scale(0); + transition: 180ms transform ease-in-out; + } + } + input[type='radio']:checked + [purpose='custom-radio'] { + [purpose='custom-radio-selected'] { + transform: scale(1); + } + } + .form-control { + height: 40px; + } + &:hover { + border: 1px solid @core-vibrant-blue; + } + &.selected { + border: 1px solid @core-vibrant-blue; + } + } + .loading-dot { + opacity: 0; + display: inline; + color: #6A67FE; + font-size: 16px; + gap: 8px; + .fade-in(); + .animation-duration(1s); + .animation-iteration-count(infinite); + .animation-direction(linear); + &.dot1 { + .animation-delay(0.25s); + } + &.dot2 { + .animation-delay(0.5s); + } + &.dot3 { + .animation-delay(0.75s); + } + &.dot4 { + .animation-delay(1s); + } + } [purpose='delete-button'] { border-radius: 6px; background: #D66C7B; diff --git a/ee/bulk-operations-dashboard/config/routes.js b/ee/bulk-operations-dashboard/config/routes.js index c9991d774a..74c428effc 100644 --- a/ee/bulk-operations-dashboard/config/routes.js +++ b/ee/bulk-operations-dashboard/config/routes.js @@ -67,4 +67,5 @@ module.exports.routes = { 'POST /api/v1/software/delete-software': { action: 'software/delete-software' }, 'POST /api/v1/software/edit-software': { action: 'software/edit-software' }, 'POST /api/v1/software/upload-software': { action: 'software/upload-software' }, + 'GET /api/v1/get-labels': { action: 'get-labels' }, }; diff --git a/ee/bulk-operations-dashboard/views/pages/profiles.ejs b/ee/bulk-operations-dashboard/views/pages/profiles.ejs index 4f0b8d9e28..275838b465 100644 --- a/ee/bulk-operations-dashboard/views/pages/profiles.ejs +++ b/ee/bulk-operations-dashboard/views/pages/profiles.ejs @@ -85,23 +85,59 @@
×
-
-
- Configuration profile -
-

{{profileToEdit.name}}

-

{{platformFriendlyNames[profileToEdit.platform]}}

+
+
+
+ + + +
+
- - -
-

Teams

- +
+
+
+ Configuration profile +
+

{{profileToEdit.name}}

+

{{platformFriendlyNames[profileToEdit.platform]}}

+
+
+
+ + +
+

Teams

+ +
+
+

Target

+
+ + +
+
+
+ + + +
+
- Save
@@ -134,15 +170,50 @@
×
- - -
Please upload a new profile.
+
+
+
+ + + + +
+ +
-
-

Teams

-
- +
+ + +
Please upload a new profile.
+ +
+

Teams

+ +
+
+

Target

+ + +
+
+ + + +
+ +
Cancel Add