mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
MSP Dashboard: Add labels to profiles page (#23145)
#22073 Changes: - Added the ability to use labels to assign configuration profiles on the profiles page.
This commit is contained in:
parent
6514631dcd
commit
102f9f3cb1
14 changed files with 523 additions and 140 deletions
45
ee/bulk-operations-dashboard/api/controllers/get-labels.js
Normal file
45
ee/bulk-operations-dashboard/api/controllers/get-labels.js
Normal file
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 713 B |
|
|
@ -13,7 +13,7 @@
|
|||
Cloud.setup({
|
||||
|
||||
/* eslint-disable */
|
||||
methods: {"confirmEmail":{"verb":"GET","url":"/email/confirm","args":["token"]},"logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]},"updatePassword":{"verb":"PUT","url":"/api/v1/account/update-password","args":["password"]},"updateProfile":{"verb":"PUT","url":"/api/v1/account/update-profile","args":["fullName","emailAddress"]},"login":{"verb":"PUT","url":"/api/v1/entrance/login","args":["emailAddress","password","rememberMe"]},"sendPasswordRecoveryEmail":{"verb":"POST","url":"/api/v1/entrance/send-password-recovery-email","args":["emailAddress"]},"updatePasswordAndLogin":{"verb":"POST","url":"/api/v1/entrance/update-password-and-login","args":["password","token"]},"deleteProfile":{"verb":"POST","url":"/api/v1/delete-profile","args":["profile"]},"downloadProfile":{"verb":"GET","url":"/download-profile","args":["id","uuid"]},"uploadProfile":{"verb":"POST","url":"/api/v1/upload-profile","args":["newProfile","teams"]},"editProfile":{"verb":"POST","url":"/api/v1/edit-profile","args":["profile","newTeamIds","newProfile"]},"getProfiles":{"verb":"GET","url":"/api/v1/get-profiles","args":[]},"getScripts":{"verb":"GET","url":"/api/v1/get-scripts","args":[]},"deleteScript":{"verb":"POST","url":"/api/v1/delete-script","args":["script"]},"downloadScript":{"verb":"GET","url":"/download-script","args":["fleetApid","id"]},"uploadScript":{"verb":"POST","url":"/api/v1/upload-script","args":["newScript","teams"]},"editScript":{"verb":"POST","url":"/api/v1/edit-script","args":["script","newTeamIds","newScript"]},"getSoftware":{"verb":"GET","url":"/api/v1/get-software","args":[]},"downloadSoftware":{"verb":"GET","url":"/download-software","args":["id","fleetApid","teamApid"]},"deleteSoftware":{"verb":"POST","url":"/api/v1/software/delete-software","args":["software"]},"editSoftware":{"verb":"POST","url":"/api/v1/software/edit-software","args":["newSoftware","newTeamIds","software","preInstallQuery","installScript","postInstallScript","uninstallScript"]},"uploadSoftware":{"verb":"POST","url":"/api/v1/software/upload-software","args":["newSoftware","teams"]}}
|
||||
methods: {"confirmEmail":{"verb":"GET","url":"/email/confirm","args":["token"]},"logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]},"updatePassword":{"verb":"PUT","url":"/api/v1/account/update-password","args":["password"]},"updateProfile":{"verb":"PUT","url":"/api/v1/account/update-profile","args":["fullName","emailAddress"]},"login":{"verb":"PUT","url":"/api/v1/entrance/login","args":["emailAddress","password","rememberMe"]},"sendPasswordRecoveryEmail":{"verb":"POST","url":"/api/v1/entrance/send-password-recovery-email","args":["emailAddress"]},"updatePasswordAndLogin":{"verb":"POST","url":"/api/v1/entrance/update-password-and-login","args":["password","token"]},"deleteProfile":{"verb":"POST","url":"/api/v1/delete-profile","args":["profile"]},"downloadProfile":{"verb":"GET","url":"/download-profile","args":["id","uuid"]},"uploadProfile":{"verb":"POST","url":"/api/v1/upload-profile","args":["newProfile","teams","profileTarget","labelTargetBehavior","labels"]},"editProfile":{"verb":"POST","url":"/api/v1/edit-profile","args":["profile","newTeamIds","newProfile","profileTarget","labelTargetBehavior","labels"]},"getProfiles":{"verb":"GET","url":"/api/v1/get-profiles","args":[]},"getScripts":{"verb":"GET","url":"/api/v1/get-scripts","args":[]},"deleteScript":{"verb":"POST","url":"/api/v1/delete-script","args":["script"]},"downloadScript":{"verb":"GET","url":"/download-script","args":["fleetApid","id"]},"uploadScript":{"verb":"POST","url":"/api/v1/upload-script","args":["newScript","teams"]},"editScript":{"verb":"POST","url":"/api/v1/edit-script","args":["script","newTeamIds","newScript"]},"getSoftware":{"verb":"GET","url":"/api/v1/get-software","args":[]},"downloadSoftware":{"verb":"GET","url":"/download-software","args":["id","fleetApid","teamApid"]},"deleteSoftware":{"verb":"POST","url":"/api/v1/software/delete-software","args":["software"]},"editSoftware":{"verb":"POST","url":"/api/v1/software/edit-software","args":["newSoftware","newTeamIds","software","preInstallQuery","installScript","postInstallScript","uninstallScript"]},"uploadSoftware":{"verb":"POST","url":"/api/v1/software/upload-software","args":["newSoftware","teams"]},"getLabels":{"verb":"GET","url":"/api/v1/get-labels","args":[]}}
|
||||
/* eslint-enable */
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ parasails.registerComponent('cloud-error', {
|
|||
// ╠═╣ ║ ║║║║
|
||||
// ╩ ╩ ╩ ╩ ╩╩═╝
|
||||
template: `
|
||||
<div>
|
||||
<p :class="{ 'm-0': beWithoutMargins }" class="text-danger"><slot name="default">An unexpected error occurred communicating with the Fleet API</slot></p>
|
||||
<div class="d-flex flex-row align-items-center">
|
||||
<img src="/images/Icon-error-16x16@2x.png" class="d-block mr-2" style="height: 16px;"><p :class="{ 'm-0': beWithoutMargins }" class="text-danger"><slot name="default"> An unexpected error occurred communicating with the Fleet API</slot></p>
|
||||
</div>
|
||||
`,
|
||||
|
||||
|
|
|
|||
|
|
@ -51,33 +51,44 @@ parasails.registerComponent('multifield', {
|
|||
// ╩ ╩ ╩ ╩ ╩╩═╝
|
||||
template: `
|
||||
<div class="multifield-set">
|
||||
<div class="multifield-item" v-for="(unused,idx) in currentFieldValues" :key="idx" :role="'item-'+idx">
|
||||
<!-- <span class="multifield-item-label">{{idx+1}}.</span> -->
|
||||
<slot name="item-field" :item="currentFieldValues[idx]" :do-set="_getCurriedDoSetFn(idx)" :all-items="currentFieldValues" :idx="idx">
|
||||
<input type="text" :placeholder="inputPlaceholder" :class="[cloudError && _.contains(cloudError.responseInfo.data, currentFieldValues[idx]) ? 'text-danger is-invalid' : '']" :value.sync="currentFieldValues[idx]" @input="inputDefaultItemField($event, idx)" role="focusable" v-if="!inputType"/>
|
||||
<select class="custom-select" :value.sync="currentFieldValues[idx]" @input="inputDefaultItemField($event, idx)" role="focusable" v-else-if="inputType && inputType === 'nameAndHostCountSelect'">
|
||||
<option :value="undefined" selected>---</option>
|
||||
<option v-for="option in optionsForSelect" :value="option.id">{{option.name}} ({{option.hostCount}} {{option.hostCount > 1 || option.hostCount === 0 ? 'hosts' : 'host'}})</option>
|
||||
</select>
|
||||
<select class="custom-select" :value.sync="currentFieldValues[idx]" @input="inputDefaultItemField($event, idx)" role="focusable" v-else-if="inputType && inputType === 'select'">
|
||||
<option :value="undefined" selected>---</option>
|
||||
<option v-for="option in optionsForSelect" :value="option.fleetApid">{{option.name}}</option>
|
||||
</select>
|
||||
<select class="custom-select" :value.sync="currentFieldValues[idx]" @input="inputTeamSelectItemField($event, idx)" role="focusable" v-else-if="inputType && inputType === 'teamSelect'">
|
||||
<option :value="undefined" selected>---</option>
|
||||
<option value="allTeams">All teams</option>
|
||||
<option v-for="option in optionsForSelect" :value="option.fleetApid">{{option.teamName}}</option>
|
||||
</select>
|
||||
<input :type="inputType" :placeholder="inputPlaceholder" :value.sync="currentFieldValues[idx]" @input="inputDefaultItemField($event, idx)" role="focusable" v-else-if="inputType">
|
||||
</slot>
|
||||
<button class="multifield-item-remove-button" type="button" v-if="currentFieldValues.length >= 2" @click="clickRemoveItem(idx)"></button>
|
||||
<button class="multifield-item-remove-button" type="button" v-else-if="currentFieldValues.length === 1 && currentFieldValues[0] !== undefined" @click="clickResetSingleItem()"></button>
|
||||
<div v-if="inputType === 'checkboxes'">
|
||||
<div class="d-flex flex-wrap flex-row">
|
||||
<div v-for="option in optionsForSelect" :key="option.name" class="form-check mr-3 mb-3">
|
||||
<input type="checkbox" :value="option.name" :id="'checkbox-' + option.name" class="form-check-input" @change="inputCheckboxItemField($event)" :checked="_.contains(currentFieldValues, option.name)"/>
|
||||
<label :for="'checkbox-' + option.name" class="form-check-label">{{ option.name }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-button-wrapper d-flex flex-row justify-content-start" :class="_.all(currentFieldValues, (item)=> item !== undefined) ? '' : 'empty'">
|
||||
<a class="add-button" @click="clickAddItem()" v-if="_.all(currentFieldValues, (item)=> item !== undefined)"><strong>+</strong> {{addButtonText || 'Add another'}}</a>
|
||||
<span v-else> </span>
|
||||
<div v-else>
|
||||
<div class="multifield-item" v-for="(unused,idx) in currentFieldValues" :key="idx" :role="'item-'+idx">
|
||||
<!-- <span class="multifield-item-label">{{idx+1}}.</span> -->
|
||||
<slot name="item-field" :item="currentFieldValues[idx]" :do-set="_getCurriedDoSetFn(idx)" :all-items="currentFieldValues" :idx="idx">
|
||||
<input type="text" :placeholder="inputPlaceholder" :class="[cloudError && _.contains(cloudError.responseInfo.data, currentFieldValues[idx]) ? 'text-danger is-invalid' : '']" :value.sync="currentFieldValues[idx]" @input="inputDefaultItemField($event, idx)" role="focusable" v-if="!inputType"/>
|
||||
<select class="custom-select" :value.sync="currentFieldValues[idx]" @input="inputDefaultItemField($event, idx)" role="focusable" v-else-if="inputType && inputType === 'nameAndHostCountSelect'">
|
||||
<option :value="undefined" selected>---</option>
|
||||
<option v-for="option in optionsForSelect" :value="option.id">{{option.name}} ({{option.hostCount}} {{option.hostCount > 1 || option.hostCount === 0 ? 'hosts' : 'host'}})</option>
|
||||
</select>
|
||||
<select class="custom-select" :value.sync="currentFieldValues[idx]" @input="inputDefaultItemField($event, idx)" role="focusable" v-else-if="inputType && inputType === 'select'">
|
||||
<option :value="undefined" selected>---</option>
|
||||
<option v-for="option in optionsForSelect" :value="option.fleetApid">{{option.name}}</option>
|
||||
</select>
|
||||
<select class="custom-select" :value.sync="currentFieldValues[idx]" @input="inputTeamSelectItemField($event, idx)" role="focusable" v-else-if="inputType && inputType === 'teamSelect'">
|
||||
<option :value="undefined" selected>---</option>
|
||||
<option value="allTeams">All teams</option>
|
||||
<option v-for="option in optionsForSelect" :value="option.fleetApid">{{option.teamName}}</option>
|
||||
</select>
|
||||
<input :type="inputType" :placeholder="inputPlaceholder" :value.sync="currentFieldValues[idx]" @input="inputDefaultItemField($event, idx)" role="focusable" v-else-if="inputType">
|
||||
</slot>
|
||||
<button class="multifield-item-remove-button" type="button" v-if="currentFieldValues.length >= 2" @click="clickRemoveItem(idx)"></button>
|
||||
<button class="multifield-item-remove-button" type="button" v-else-if="currentFieldValues.length === 1 && currentFieldValues[0] !== undefined" @click="clickResetSingleItem()"></button>
|
||||
</div>
|
||||
<div class="add-button-wrapper d-flex flex-row justify-content-start" :class="_.all(currentFieldValues, (item)=> item !== undefined) ? '' : 'empty'">
|
||||
<a class="add-button" @click="clickAddItem()" v-if="_.all(currentFieldValues, (item)=> item !== undefined)"><strong>+</strong> {{addButtonText || 'Add another'}}</a>
|
||||
<span v-else> </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
`,
|
||||
|
||||
// ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗
|
||||
|
|
@ -88,7 +99,6 @@ parasails.registerComponent('multifield', {
|
|||
if (this.value !== undefined && !_.isArray(this.value)) {
|
||||
throw new Error('In <multifield>, 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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -85,23 +85,59 @@
|
|||
<div class="pointer" @click="closeModal()">×</div>
|
||||
</div>
|
||||
<ajax-form :handle-submitting="handleSubmittingEditProfileForm" :syncing.sync="syncing" :cloud-error.sync="cloudError" :form-errors.sync="formErrors" :form-data="formData" :form-rules="editProfileFormRules" @submitted="submittedForm()">
|
||||
<div purpose="profile-information" v-if="!formData.newProfile">
|
||||
<div class="d-flex flex-row justify-content-start">
|
||||
<img style="height: 40px; width: 34px;" alt="Configuration profile" src="/images/profile-34x40@2x.png">
|
||||
<div class="d-flex flex-column">
|
||||
<p><strong>{{profileToEdit.name}}</strong></p>
|
||||
<p class="muted">{{platformFriendlyNames[profileToEdit.platform]}}</p>
|
||||
<div v-if="labelsSyncing">
|
||||
<div class="d-flex flex-row align-items-center justify-content-center my-4" >
|
||||
<div v-if="!cloudError">
|
||||
<span style="top: -4px; font-size: 16px; margin: 0 8px 0 0;" class="loading-dot dot1 position-relative"><span class="fa fa-circle"></span></span>
|
||||
<span style="top: -4px; font-size: 16px; margin: 0 8px 0 0;" class="loading-dot dot2 position-relative"><span class="fa fa-circle"></span></span>
|
||||
<span style="top: -4px; font-size: 16px; margin: 0 8px 0 0;" class="loading-dot dot3 position-relative"><span class="fa fa-circle"></span></span>
|
||||
<span style="top: -4px; font-size: 16px; margin: 0 8px 0 0;" class="loading-dot dot4 position-relative"><span class="fa fa-circle"></span></span>
|
||||
</div>
|
||||
<cloud-error v-if="cloudError"></cloud-error>
|
||||
</div>
|
||||
</div>
|
||||
<cloud-error v-if="cloudError === 'payloadIdentifierDoesNotMatch'"></cloud-error>
|
||||
<file-upload id="edit-file-upload" mode="profiles" :disabled="syncing" accept=".xml,.mobileconfig" v-model="formData.newProfile"></file-upload>
|
||||
<div purpose="teams-picker">
|
||||
<p class="mb-2"><strong>Teams</strong></p>
|
||||
<multifield :value="formData.teams" v-model="formData.newTeamIds" input-type="teamSelect" :select-options="teams" add-button-text="Add team"></multifield>
|
||||
<div v-else>
|
||||
<div purpose="profile-information" v-if="!formData.newProfile">
|
||||
<div class="d-flex flex-row justify-content-start">
|
||||
<img style="height: 40px; width: 34px;" alt="Configuration profile" src="/images/profile-34x40@2x.png">
|
||||
<div class="d-flex flex-column">
|
||||
<p><strong>{{profileToEdit.name}}</strong></p>
|
||||
<p class="muted">{{platformFriendlyNames[profileToEdit.platform]}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<cloud-error v-if="cloudError === 'payloadIdentifierDoesNotMatch'"></cloud-error>
|
||||
<file-upload id="edit-file-upload" mode="profiles" :disabled="syncing" accept=".xml,.mobileconfig" v-model="formData.newProfile"></file-upload>
|
||||
<div purpose="teams-picker">
|
||||
<p class="mb-2"><strong>Teams</strong></p>
|
||||
<multifield :value="formData.teams" v-model="formData.newTeamIds" input-type="teamSelect" :select-options="teams" add-button-text="Add team"></multifield>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>Target</strong></p>
|
||||
<div class="form-group">
|
||||
<label purpose="form-option" class="form-control border-0 mb-2 p-0 h-auto" :class="[formData.profileTarget === 'all' ? 'selected' : '']">
|
||||
<input type="radio" :class="[formErrors.profileTarget ? 'is-invalid' : '']" v-model.trim="formData.profileTarget" value="all">
|
||||
<span purpose="custom-radio"><span purpose="custom-radio-selected"></span></span>
|
||||
All hosts on selected teams
|
||||
</label>
|
||||
<label purpose="form-option" class="form-control border-0 mb-0 p-0" :class="[formData.profileTarget === 'custom' ? 'selected' : '']">
|
||||
<input type="radio" :class="[formErrors.profileTarget ? 'is-invalid' : '']" v-model.trim="formData.profileTarget" value="custom">
|
||||
<span purpose="custom-radio"><span purpose="custom-radio-selected"></span></span>
|
||||
Custom
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="formData.profileTarget === 'custom'">
|
||||
<select class="custom-select form-control mb-4" v-model="formData.labelTargetBehavior">
|
||||
<option value="include" selected>Hosts with all of these labels</option>
|
||||
<option value="exclude">Hosts with none of these labels</option>
|
||||
</select>
|
||||
|
||||
<multifield :value="formData.labels" v-model="formData.labels" input-type="checkboxes" :select-options="labels"></multifield>
|
||||
</div>
|
||||
<cloud-error v-if="cloudError && cloudError !== 'payloadIdentifierDoesNotMatch'"></cloud-error>
|
||||
</div>
|
||||
<div purpose="modal-buttons" class="d-flex flex-row justify-content-end align-items-center">
|
||||
<cloud-error v-if="cloudError && cloudError !== 'payloadIdentifierDoesNotMatch'"></cloud-error>
|
||||
<ajax-button :syncing.sync="syncing" purpose="modal-button" type="submit">Save</ajax-button>
|
||||
</div>
|
||||
</ajax-form>
|
||||
|
|
@ -134,15 +170,50 @@
|
|||
<div class="pointer" @click="closeModal()">×</div>
|
||||
</div>
|
||||
<ajax-form :handle-submitting="handleSubmittingAddProfileForm" :syncing.sync="syncing" :cloud-error.sync="cloudError" :form-errors.sync="formErrors" :form-data="formData" :form-rules="addProfileFormRules" @submitted="submittedForm()">
|
||||
<file-upload id="add-file-upload" mode="profiles" :class="[formErrors.newProfile ? 'is-invalid' : '']" :disabled="syncing" v-model="formData.newProfile">
|
||||
</file-upload>
|
||||
<div class="invalid-feedback text-center" v-if="formErrors.newProfile">Please upload a new profile.</div>
|
||||
<div v-if="labelsSyncing">
|
||||
<div class="d-flex flex-row align-items-center justify-content-center my-4" >
|
||||
<div v-if="!cloudError">
|
||||
<span style="top: -4px; font-size: 16px; margin: 0 8px 0 0;" class="loading-dot dot1 position-relative"><span class="fa fa-circle"></span></span>
|
||||
<span style="top: -4px; font-size: 16px; margin: 0 8px 0 0;" class="loading-dot dot2 position-relative"><span class="fa fa-circle"></span></span>
|
||||
<span style="top: -4px; font-size: 16px; margin: 0 8px 0 0;" class="loading-dot dot3 position-relative"><span class="fa fa-circle"></span></span>
|
||||
<span style="top: -4px; font-size: 16px; margin: 0 8px 0 0;" class="loading-dot dot4 position-relative"><span class="fa fa-circle"></span></span>
|
||||
</div>
|
||||
<cloud-error v-if="cloudError"></cloud-error>
|
||||
</div>
|
||||
|
||||
<div purpose="teams-picker" class="mt-4">
|
||||
<p class="mb-2"><strong>Teams</strong></p>
|
||||
<multifield :value="formData.teams" v-model="formData.teams" input-type="teamSelect" :select-options="teams" add-button-text="Add team"></multifield>
|
||||
</div>
|
||||
<cloud-error v-if="cloudError"></cloud-error>
|
||||
<div v-else>
|
||||
<file-upload id="add-file-upload" mode="profiles" :class="[formErrors.newProfile ? 'is-invalid' : '']" :disabled="syncing" v-model="formData.newProfile">
|
||||
</file-upload>
|
||||
<div class="invalid-feedback text-center" v-if="formErrors.newProfile">Please upload a new profile.</div>
|
||||
|
||||
<div purpose="teams-picker" class="mt-4">
|
||||
<p class="mb-2"><strong>Teams</strong></p>
|
||||
<multifield :value="formData.teams" v-model="formData.teams" input-type="teamSelect" :select-options="teams" add-button-text="Add team"></multifield>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p><strong>Target</strong></p>
|
||||
<label purpose="form-option" class="form-control border-0 mb-2 p-0 h-auto">
|
||||
<input type="radio" :class="[formErrors.profileTarget ? 'is-invalid' : '']" v-model.trim="formData.profileTarget" value="all">
|
||||
<span purpose="custom-radio"><span purpose="custom-radio-selected"></span></span>
|
||||
All hosts on selected teams
|
||||
</label>
|
||||
<label purpose="form-option" class="form-control border-0 mb-0 p-0">
|
||||
<input type="radio" :class="[formErrors.profileTarget ? 'is-invalid' : '']" v-model.trim="formData.profileTarget" value="custom">
|
||||
<span purpose="custom-radio"><span purpose="custom-radio-selected"></span></span>
|
||||
Custom
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="formData.profileTarget === 'custom'">
|
||||
<select class="custom-select form-control mb-4" v-model="formData.labelTargetBehavior">
|
||||
<option value="include" selected>Hosts with all of these labels</option>
|
||||
<option value="exclude">Hosts with none of these labels</option>
|
||||
</select>
|
||||
|
||||
<multifield :value="formData.labels" v-model="formData.labels" input-type="checkboxes" :select-options="labels"></multifield>
|
||||
</div>
|
||||
<cloud-error v-if="cloudError"></cloud-error>
|
||||
</div>
|
||||
<div purpose="modal-buttons" class="d-flex flex-row justify-content-end align-items-center">
|
||||
<a purpose="cancel-button" @click="closeModal()">Cancel</a>
|
||||
<ajax-button :syncing.sync="syncing" purpose="modal-button" :disabled="!formData.newProfile" type="submit">Add</ajax-button>
|
||||
|
|
|
|||
Loading…
Reference in a new issue