Bulk operations dashboard: Add software page (#22584)

Related to #21928

Changes:
- Added a /software page, a page where users can manage
(upload/edit/download/delete) software installers on their Fleet
instance across multiple teams at once.
- ~~Removed the `deploy-bulk-operations-dashboard-on-heroku` GitHub
action (This dashboard will be hosted in Render in the future)~~
Reverted this change to unblock merging this PR, I will remove this file
in a separate PR.
This commit is contained in:
Eric 2024-10-15 10:17:05 -05:00 committed by GitHub
parent a2e7010ee2
commit 0da7afb332
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 57046 additions and 10 deletions

View file

@ -41,7 +41,8 @@
// Models:
"User": true,
"UndeployedProfile": true,
"UndeployedScript": true
"UndeployedScript": true,
"UndeployedSoftware": true
// …and any others.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

View file

@ -0,0 +1,109 @@
module.exports = {
friendlyName: 'Get software',
description: 'Builds and returns an array of deployed software installers on the Fleet instance and undeployed software stored in the dashboard\'s datastore.',
exits: {
success: {
outputType: [{}],
}
},
fn: async function () {
let teamsResponseData = await sails.helpers.http.get.with({
url: '/api/v1/fleet/teams',
baseUrl: sails.config.custom.fleetBaseUrl,
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
}
})
.timeout(120000)
.retry(['requestFailed', {name: 'TimeoutError'}]);
let allTeams = teamsResponseData.teams;
let teams = [];
for(let team of allTeams) {
teams.push({
fleetApid: team.id,
teamName: team.name,
});
}
// Add the "team" for hosts with no team
teams.push({
fleetApid: 0,
teamName: 'No team',
});
let allSoftware = [];
let allSoftwareWithPackages = [];
let teamsinformationForSoftware = [];
let teamApids = _.pluck(teams, 'fleetApid');
// Get all of the software packages on the Fleet instance.
for(let teamApid of teamApids){
let configurationProfilesResponseData = await sails.helpers.http.get.with({
url: `/api/latest/fleet/software/titles?team_id=${teamApid}`,
baseUrl: sails.config.custom.fleetBaseUrl,
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
}
})
.timeout(120000)
.retry(['requestFailed', {name: 'TimeoutError'}]);
let softwareForThisTeam = configurationProfilesResponseData.software_titles;
let softwareWithSoftwarePackages = _.filter(softwareForThisTeam, (software)=>{
return !_.isEmpty(software.software_package);
});
for(let softwareWithInstaller of softwareWithSoftwarePackages) {
let softwareWithInstallerResponse = await sails.helpers.http.get.with({
url: `/api/latest/fleet/software/titles/${softwareWithInstaller.id}?team_id=${teamApid}&available_for_install=true`,
baseUrl: sails.config.custom.fleetBaseUrl,
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
}
})
.timeout(120000)
.retry(['requestFailed', {name: 'TimeoutError'}]);
let packageInformation = softwareWithInstallerResponse.software_title.software_package;
let packageInfo = {
fleetApid: softwareWithInstaller.id,
name: packageInformation.name,
createdAt: new Date(packageInformation.uploaded_at).getTime(),
platform: _.endsWith(packageInformation.name, 'deb') ? 'Linux' : _.endsWith(packageInformation.name, 'pkg') ? 'macOS' : 'Windows',
preInstallQuery: packageInformation.pre_install_query,
installScript: packageInformation.install_script,
postInstallScript: packageInformation.post_install_script,
uninstallScript: packageInformation.uninstall_script,
teams: [],
};
let teamInfo = {
softwareFleetApid: softwareWithInstaller.id,
fleetApid: teamApid,
teamName: _.find(teams, {fleetApid: teamApid}).teamName,
};
teamsinformationForSoftware.push(teamInfo);
allSoftware.push(packageInfo);
allSoftwareWithPackages.push(packageInfo);
}
}
for(let software of allSoftwareWithPackages) {
software.teams = _.where(teamsinformationForSoftware, {'softwareFleetApid': software.fleetApid});
allSoftware.push(software);
}
allSoftware = _.uniq(allSoftware, 'fleetApid');
let undeployedSoftware = await UndeployedSoftware.find();
allSoftware = allSoftware.concat(undeployedSoftware);
return allSoftware;
}
};

View file

@ -0,0 +1,46 @@
module.exports = {
friendlyName: 'Delete software',
description: 'Deletes deployed software for all teams on a Fleet instance, or undeployed software in the app\'s database',
inputs: {
software: {
type: {},
description: 'The software that will be deleted.',
required: true,
}
},
exits: {
},
fn: async function ({software}) {
// If the provided software does not have a teams array and has an ID, it is an undeployed software that will be deleted.
if(software.id && !software.teams){
await sails.rm(sails.config.uploads.prefixForFileDeletion+software.uploadFd);
await UndeployedSoftware.destroy({id: software.id});
} else {// Otherwise, this is a deployed software, and we'll use information from the teams array to remove the software.
for(let team of software.teams){
await sails.helpers.http.sendHttpRequest.with({
method: 'DELETE',
baseUrl: sails.config.custom.fleetBaseUrl,
url: `/api/v1/fleet/software/titles/${software.fleetApid}/available_for_install?team_id=${team.fleetApid}`,
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
}
});
}
}
// All done.
return;
}
};

View file

@ -0,0 +1,74 @@
module.exports = {
friendlyName: 'Download software',
description: 'Downloads a software installer for deployed or undeployed software.',
inputs: {
id: {
type: 'number',
description: 'The database ID of the undeployed software to download.'
},
fleetApid: {
type: 'string',
description: 'The fleetApid of a software on a team.'
},
teamApid: {
type: 'string',
description: 'The team API ID of a team that the software is deployed to.'
}
},
exits: {
success: {
outputFriendlyName: 'File',
outputDescription: 'The streaming bytes of the file.',
outputType: 'ref'
},
notFound: {
description: 'No software exists with the specified ID.',
responseType: 'notFound'
},
},
fn: async function ({id, fleetApid, teamApid}) {
if(!fleetApid && !id){
return this.res.badRequest();
}
let downloading;
if(id){
let softwareToDownload = await UndeployedSoftware.findOne({id: id});
downloading = await sails.startDownload(softwareToDownload.uploadFd, {bucket: sails.config.uploads.bucketWithPostfix});
this.res.type(softwareToDownload.uploadMime);
this.res.attachment(softwareToDownload.name);
} else {
// Get information about the installer package from the Fleet server.
let packageInformation = await sails.helpers.http.get.with({
url: `${sails.config.custom.fleetBaseUrl}/api/latest/fleet/software/titles/${fleetApid}?team_id=${teamApid}&available_for_install=true`,
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
}
});
let filename = packageInformation.software_title.software_package.name;
// [?]: https://fleetdm.com/docs/rest-api/rest-api#download-package
// GET /api/v1/fleet/software/titles/:software_title_id/package?team_id=${teamId}
downloading = await sails.helpers.http.getStream.with({
url: `${sails.config.custom.fleetBaseUrl}/api/v1/fleet/software/titles/${fleetApid}/package?alt=media&team_id=${teamApid}`,
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
}
});
this.res.attachment(filename);
}
return downloading;
}
};

View file

@ -0,0 +1,343 @@
module.exports = {
friendlyName: 'Edit software',
description: 'Edits deployed software on a Fleet instance or undeployed software in the app\'s database',
files: ['newSoftware'],
inputs: {
newSoftware: {
type: 'ref',
description: 'The optional streaming bytes of a new software version.'
},
newTeamIds: {
type: ['string'],
description: 'The new teams that this software will be deployed to.'
},
software: {
type: {},
description: 'The software that will be editted.'
},
preInstallQuery: {
type: 'string',
},
installScript: {
type: 'string',
},
postInstallScript: {
type: 'string',
},
uninstallScript: {
type: 'string',
},
},
exits: {
wrongInstallerExtension: {
description: 'The provided replacement software\'s has the wrong extension.',
statusCode: 400,
},
softwareUploadFailed: {
description: 'The software upload failed'
}
},
fn: async function ({newSoftware, newTeamIds, software, preInstallQuery, installScript, postInstallScript, uninstallScript}) {
if(newSoftware.isNoop) {
newSoftware.noMoreFiles();
newSoftware = undefined;
}
var WritableStream = require('stream').Writable;
// let { Readable } = require('stream');
let axios = require('axios');
// Cast the strings in the newTeamIds array to numbers.
newTeamIds = newTeamIds.map(Number);
let currentSoftwareTeamIds = _.pluck(software.teams, 'fleetApid');
// If the teams have changed, or a new installer package was provided, we'll need to upload the package to an s3 bucket to deploy it to other teams.
if(_.xor(newTeamIds, currentSoftwareTeamIds).length !== 0 || newSoftware) {
let softwareFd;
let softwareName;
let softwareMime;
if(software.teams && !newSoftware) {
// console.log('Editing deployed software!');
// This software is deployed.
// get software from Fleet instance and upload to s3.
let teamIdToGetInstallerFrom = software.teams[0].fleetApid;
let downloadApiUrl = `${sails.config.custom.fleetBaseUrl}/api/v1/fleet/software/titles/${software.fleetApid}/package?alt=media&team_id=${teamIdToGetInstallerFrom}`;
let softwareStream = await sails.helpers.http.getStream.with({
url: downloadApiUrl,
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
}
});
let tempUploadedSoftware = await sails.uploadOne(softwareStream, {bucket: sails.config.uploads.bucketWithPostfix});
softwareFd = tempUploadedSoftware.fd;
softwareName = software.name;
softwareMime = tempUploadedSoftware.type;
} else if(newSoftware) {
// If a new copy of the installer was uploaded, we'll
// console.log('replacing software package!');
let uploadedSoftware = await sails.uploadOne(newSoftware, {bucket: sails.config.uploads.bucketWithPostfix});
softwareFd = uploadedSoftware.fd;
softwareName = uploadedSoftware.filename;
softwareMime = uploadedSoftware.type;
let newSoftwareExtension = '.'+softwareName.split('.').pop();
let existingSoftwareExtension = '.'+software.name.split('.').pop();
if(newSoftwareExtension !== existingSoftwareExtension) {
await sails.rm(sails.config.uploads.prefixForFileDeletion+softwareFd);
throw {wrongInstallerExtension: `Couldn't edit ${software.name}. The selected package should be a ${existingSoftwareExtension} file`};
}
} else {
// console.log('Editing undeployed software!');
softwareFd = software.uploadFd;
softwareName = software.name;
softwareMime = software.uploadMime;
}
// Now apply the edits.
if(newTeamIds.length !== 0) {
let currentSoftwareTeamIds = _.pluck(software.teams, 'fleetApid') || [];
let addedTeams = _.difference(newTeamIds, currentSoftwareTeamIds);
let removedTeams = _.difference(currentSoftwareTeamIds, newTeamIds);
let unchangedTeamIds = _.difference(currentSoftwareTeamIds, removedTeams);
// for(let team of addedTeams) {
await sails.helpers.flow.simultaneouslyForEach(addedTeams, async (team)=>{
// console.log(`transfering ${software.name} to fleet instance for team id ${team}`);
// Send an api request to send the file to the Fleet server for each added team.
await sails.cp(softwareFd, {bucket: sails.config.uploads.bucketWithPostfix},
{
adapter: ()=>{
return {
ls: undefined,
rm: undefined,
read: undefined,
receive: (unusedOpts)=>{
// This `_write` method is invoked each time a new file is received
// from the Readable stream (Upstream) which is pumping filestreams
// into this receiver. (filename === `__newFile.filename`).
var receiver__ = WritableStream({ objectMode: true });
// Create a new drain (writable stream) to send through the individual bytes of this file.
receiver__._write = (__newFile, encoding, doneWithThisFile)=>{
let FormData = require('form-data');
let form = new FormData();
form.append('team_id', team);
form.append('install_script', installScript);
form.append('post_install_script', postInstallScript);
form.append('pre_install_query', preInstallQuery);
form.append('uninstall_script', uninstallScript);
form.append('software', __newFile, {
filename: software.name,
contentType: 'application/octet-stream'
});
(async ()=>{
await axios.post(`${sails.config.custom.fleetBaseUrl}/api/v1/fleet/software/package`, form, {
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
...form.getHeaders()
},
});
})()
.then(()=>{
// console.log('ok supposedly a file is finished uploading');
doneWithThisFile();
})
.catch((err)=>{
doneWithThisFile(err);
});
};//ƒ
return receiver__;
}
};
},
})
.intercept((error)=>{
// Note: with this current behavior, all errors from this upload are currently swallowed and a softwareUploadFailed response is returned.
// FUTURE: Test to make sure that uploading duplicate software to a team results in a 409 response.
return {'softwareUploadFailed': error};
});
// console.timeEnd(`transfering ${software.name} to fleet instance for team id ${team}`);
});
// }// After every new team this is deployed to.
if(newSoftware) {
// If a new installer package was provided, send patch requests to update the installer package on teams that it is already deployed to.
await sails.helpers.flow.simultaneouslyForEach(unchangedTeamIds, async (teamApid)=>{
// console.log(`Adding new version of ${softwareName} to teamId ${teamApid}`);
await sails.helpers.http.sendHttpRequest.with({
method: 'DELETE',
baseUrl: sails.config.custom.fleetBaseUrl,
url: `/api/v1/fleet/software/titles/${software.fleetApid}/available_for_install?team_id=${teamApid}`,
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
}
});
// console.log(`transfering the changed installer ${software.name} to fleet instance for team id ${teamApid}`);
// console.time(`transfering ${software.name} to fleet instance for team id ${teamApid}`);
await sails.cp(softwareFd, {bucket: sails.config.uploads.bucketWithPostfix},
{
adapter: ()=>{
return {
ls: undefined,
rm: undefined,
read: undefined,
receive: (unusedOpts)=>{
// This `_write` method is invoked each time a new file is received
// from the Readable stream (Upstream) which is pumping filestreams
// into this receiver. (filename === `__newFile.filename`).
var receiver__ = WritableStream({ objectMode: true });
// Create a new drain (writable stream) to send through the individual bytes of this file.
receiver__._write = (__newFile, encoding, doneWithThisFile)=>{
let FormData = require('form-data');
let form = new FormData();
form.append('team_id', teamApid);
form.append('install_script', installScript);
form.append('post_install_script', postInstallScript);
form.append('pre_install_query', preInstallQuery);
form.append('uninstall_script', uninstallScript);
form.append('software', __newFile, {
filename: software.name,
contentType: 'application/octet-stream'
});
(async ()=>{
await axios.post(`${sails.config.custom.fleetBaseUrl}/api/v1/fleet/software/package`, form, {
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
...form.getHeaders()
},
});
})()
.then(()=>{
// console.log('ok supposedly a file is finished uploading');
doneWithThisFile();
})
.catch((err)=>{
doneWithThisFile(err);
});
};//ƒ
return receiver__;
}
};
},
})
.intercept((error)=>{
// Note: with this current behavior, all errors from this upload are currently swallowed and a softwareUploadFailed response is returned.
// FUTURE: Test to make sure that uploading duplicate software to a team results in a 409 response.
return {'softwareUploadFailed': error};
});
// console.timeEnd(`transfering ${software.name} to fleet instance for team id ${teamApid}`);
});// After every team the software is currently deployed to.
}
// Now delete the software from teams it was removed from.
for(let team of removedTeams) {
await sails.helpers.http.sendHttpRequest.with({
method: 'DELETE',
baseUrl: sails.config.custom.fleetBaseUrl,
url: `/api/v1/fleet/software/titles/${software.fleetApid}/available_for_install?team_id=${team}`,
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
}
});
}
// If the software had been previously undeployed, delete the installer in s3 and the db record.
if(software.id) {
await sails.rm(sails.config.uploads.prefixForFileDeletion+software.uploadFd);
await UndeployedSoftware.destroyOne({id: software.id});
}
} else if(software.teams && newTeamIds.length === 0) {
// If this is a deployed software that is being unassigned, save information about the uploaded file in our s3 bucket.
if(newSoftware) {
// remove the old copy.
// console.log('Removing old package for ',softwareName);
await UndeployedSoftware.create({
uploadFd: softwareFd,
uploadMime: softwareMime,
name: softwareName,
platform: _.endsWith(softwareName, '.deb') ? 'Linux' : _.endsWith(softwareName, '.pkg') ? 'macOS' : 'Windows',
});
} else {
// Save the information about the undeployed software in the app's DB.
await UndeployedSoftware.create({
uploadFd: softwareFd,
uploadMime: softwareMime,
name: software.name,
platform: software.platform,
postInstallScript,
preInstallQuery,
installScript,
uninstallScript,
});
// Now delete the software on the Fleet instance.
for(let team of software.teams) {
await sails.helpers.http.sendHttpRequest.with({
method: 'DELETE',
baseUrl: sails.config.custom.fleetBaseUrl,
url: `/api/v1/fleet/software/titles/${software.fleetApid}/available_for_install?team_id=${team.fleetApid}`,
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
}
});
}
}
} else {
// console.log('updating existing db record!');
await UndeployedSoftware.updateOne({id: software.id}).set({
name: softwareName,
uploadMime: softwareMime,
uploadFd: softwareFd,
});
// console.log('removing old stored copy of '+softwareName);
await sails.rm(sails.config.uploads.prefixForFileDeletion+software.uploadFd);
}
} else if(preInstallQuery !== software.preInstallQuery ||
installScript !== software.installScript ||
postInstallScript !== software.postInstallScript ||
uninstallScript !== software.uninstallScript) {
// PATCH /api/v1/fleet/software/titles/:title_id/package
if(newTeamIds.length !== 0) {
for(let teamApid of newTeamIds){
await sails.helpers.http.sendHttpRequest.with({
method: 'PATCH',
baseUrl: sails.config.custom.fleetBaseUrl,
url: `/api/v1/fleet/software/titles/${software.fleetApid}/package?team_id=${teamApid}`,
enctype: 'multipart/form-data',
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
},
body: {
team_id: teamApid, // eslint-disable-line camelcase
pre_install_query: preInstallQuery, // eslint-disable-line camelcase
install_script: installScript, // eslint-disable-line camelcase
post_install_script: postInstallScript, // eslint-disable-line camelcase
uninstall_script: uninstallScript, // eslint-disable-line camelcase
}
});
}
} else if(software.id) {
await UndeployedSoftware.updateOne({id: software.id}).set({
preInstallQuery,
installScript,
postInstallScript,
uninstallScript,
});
}
}
return;
}
};

View file

@ -0,0 +1,111 @@
module.exports = {
friendlyName: 'Upload software',
description: '',
files: ['newSoftware'],
inputs: {
newSoftware: {
type: 'ref',
description: 'An Upstream with an incoming file upload.',
required: true,
},
teams: {
type: ['string'],
description: 'An array of team IDs that this profile will be added to'
}
},
exits: {
success: {
outputDescription: 'The new software has been uploaded',
outputType: {},
},
softwareAlreadyExistsOnThisTeam: {
description: 'A software with this name already exists on the Fleet Instance',
statusCode: 409,
},
},
fn: async function ({newSoftware, teams}) {
let uploadedSoftware;
if(!teams) {
uploadedSoftware = await sails.uploadOne(newSoftware, {bucket: sails.config.uploads.bucketWithPostfix});
let datelessFilename = uploadedSoftware.filename.replace(/^\d{4}-\d{2}-\d{2}\s/, '');
// Build a dictonary of information about this software to return to the softwares page.
let newSoftwareInfo = {
name: datelessFilename,
platform: _.endsWith(datelessFilename, '.deb') ? 'Linux' : _.endsWith(datelessFilename, '.pkg') ? 'macOS' : 'Windows',
createdAt: Date.now(),
uploadFd: uploadedSoftware.fd,
uploadMime: uploadedSoftware.type,
};
await UndeployedSoftware.create(newSoftwareInfo);
} else {
for(let teamApid of teams) {
uploadedSoftware = await sails.uploadOne(newSoftware, {bucket: sails.config.uploads.bucketWithPostfix});
var WritableStream = require('stream').Writable;
await sails.cp(uploadedSoftware.fd, {bucket: sails.config.uploads.bucketWithPostfix}, {
adapter: ()=>{
return {
ls: undefined,
rm: undefined,
read: undefined,
receive: (unusedOpts)=>{
// This `_write` method is invoked each time a new file is received
// from the Readable stream (Upstream) which is pumping filestreams
// into this receiver. (filename === `__newFile.filename`).
var receiver__ = WritableStream({ objectMode: true });
// Create a new drain (writable stream) to send through the individual bytes of this file.
receiver__._write = (__newFile, encoding, doneWithThisFile)=>{
let axios = require('axios');
let FormData = require('form-data');
let form = new FormData();
form.append('team_id', teamApid);
form.append('software', __newFile, {
filename: uploadedSoftware.filename,
contentType: 'application/octet-stream'
});
(async ()=>{
await axios.post(`${sails.config.custom.fleetBaseUrl}/api/v1/fleet/software/package`, form, {
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
...form.getHeaders()
},
});
})()
.then(()=>{
// console.log('ok supposedly a file is finished uploading');
doneWithThisFile();
})
.catch((err)=>{
doneWithThisFile(err);
});
};//ƒ
return receiver__;
}
};
}
})
.intercept({response: {status: 409}}, (error)=>{
return {'softwareAlreadyExistsOnThisTeam': error};
});
}
// Remove the file from the s3 bucket after it has been sent to the Fleet server.
await sails.rm(sails.config.uploads.prefixForFileDeletion+uploadedSoftware.fd);
}
return;
}
};

View file

@ -0,0 +1,112 @@
module.exports = {
friendlyName: 'View software',
description: 'Display "Software" page.',
exits: {
success: {
viewTemplatePath: 'pages/software/software'
}
},
fn: async function () {
let teamsResponseData = await sails.helpers.http.get.with({
url: '/api/v1/fleet/teams',
baseUrl: sails.config.custom.fleetBaseUrl,
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
}
})
.timeout(120000)
.retry(['requestFailed', {name: 'TimeoutError'}]);
let allTeams = teamsResponseData.teams;
let teams = [];
for(let team of allTeams) {
teams.push({
fleetApid: team.id,
teamName: team.name,
});
}
// Add the "team" for hosts with no team
teams.push({
fleetApid: 0,
teamName: 'No team',
});
let allSoftware = [];
let allSoftwareWithPackages = [];
let teamsinformationForSoftware = [];
let teamApids = _.pluck(teams, 'fleetApid');
// Get all of the software packages on the Fleet instance.
for(let teamApid of teamApids){
let configurationProfilesResponseData = await sails.helpers.http.get.with({
url: `/api/latest/fleet/software/titles?team_id=${teamApid}`,
baseUrl: sails.config.custom.fleetBaseUrl,
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
}
})
.timeout(120000)
.retry(['requestFailed', {name: 'TimeoutError'}]);
let softwareForThisTeam = configurationProfilesResponseData.software_titles;
let softwareWithSoftwarePackages = _.filter(softwareForThisTeam, (software)=>{
return !_.isEmpty(software.software_package);
});
for(let softwareWithInstaller of softwareWithSoftwarePackages) {
let softwareWithInstallerResponse = await sails.helpers.http.get.with({
url: `/api/latest/fleet/software/titles/${softwareWithInstaller.id}?team_id=${teamApid}&available_for_install=true`,
baseUrl: sails.config.custom.fleetBaseUrl,
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
}
})
.timeout(120000)
.retry(['requestFailed', {name: 'TimeoutError'}]);
let packageInformation = softwareWithInstallerResponse.software_title.software_package;
if(!packageInformation){ console.log('skipping a softwareWithInstallerResponse', softwareWithInstallerResponse); continue;}
let packageInfo = {
fleetApid: softwareWithInstaller.id,
name: packageInformation.name,
createdAt: new Date(packageInformation.uploaded_at).getTime(),
platform: _.endsWith(packageInformation.name, 'deb') ? 'Linux' : _.endsWith(packageInformation.name, 'pkg') ? 'macOS' : 'Windows',
preInstallQuery: packageInformation.pre_install_query,
installScript: packageInformation.install_script,
postInstallScript: packageInformation.post_install_script,
uninstallScript: packageInformation.uninstall_script,
teams: [],
};
let teamInfo = {
softwareFleetApid: softwareWithInstaller.id,
fleetApid: teamApid,
teamName: _.find(teams, {fleetApid: teamApid}).teamName,
};
teamsinformationForSoftware.push(teamInfo);
allSoftware.push(packageInfo);
allSoftwareWithPackages.push(packageInfo);
}
}
// console.log(teamsinformationForSoftware);
for(let software of allSoftwareWithPackages) {
software.teams = _.where(teamsinformationForSoftware, {'softwareFleetApid': software.fleetApid});
// console.log(software)
allSoftware.push(software);
}
allSoftware = _.uniq(allSoftware, 'fleetApid');
let undeployedSoftware = await UndeployedSoftware.find();
allSoftware = allSoftware.concat(undeployedSoftware);
return {software: allSoftware, teams};
}
};

View file

@ -67,7 +67,7 @@ module.exports = function defineCustomHook(sails) {
sails.config.custom.fleetBaseUrl = _.trimRight(sails.config.custom.fleetBaseUrl, '/');
sails.log.warn('Warning: The provided sails.config.custom.fleetBaseUrl has a trailing slash. To make sure all auto-generated URLs work as expected, this trailing slash has been removed for you.');
}
if (!_.startsWith(sails.config.custom.fleetBaseUrl, 'https://')) {
if (!_.startsWith(sails.config.custom.fleetBaseUrl, 'https://') && !_.startsWith(sails.config.custom.fleetBaseUrl, 'http://')) {
sails.log.warn('Warning: The provided sails.config.custom.fleetBaseUrl is missing a protocol (https://). To make sure all auto-generated URLs work as expected, the protocol has been added to the fleetBaseUrl.');
sails.config.custom.fleetBaseUrl = 'https://'+sails.config.custom.fleetBaseUrl;
}

View file

@ -0,0 +1,77 @@
/**
* UndeployedSoftware.js
*
* @description :: A model definition represents a database table/collection.
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
*/
module.exports = {
attributes: {
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
name: {
type: 'string',
required: true,
description: 'The filename of the software installer package.',
},
platform: {
type: 'string',
description: 'The type of operating system this software installer is for.',
required: true,
isIn: [
'macOS',
'Linux',
'Windows'
],
},
uploadMime: {
type: 'string',
defaultsTo: '',
description: 'The mime type of the uploaded software installer'
},
uploadFd: {
type: 'string',
defaultsTo: '',
description: 'The file descriptor of the installer file.'
},
preInstallQuery: {
type: 'string',
defaultsTo: '',
},
installScript: {
type: 'string',
defaultsTo: '',
},
postInstallScript: {
type: 'string',
defaultsTo: '',
},
uninstallScript: {
type: 'string',
defaultsTo: '',
},
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
},
};

View file

@ -46,6 +46,7 @@
"Vue": true,
"VueRouter": true,
"moment": true,
"ace": true,
// "google": true,
// ...etc.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,35 @@
/* eslint-disable */
// @ts-nocheck
ace.define(
"ace/mode/fleet",
[
"require",
"exports",
"module",
"ace/lib/oop",
"ace/mode/sql",
"ace/mode/fleet_highlight_rules",
"ace/range",
],
function (acequire, exports, module) {
"use strict";
var oop = acequire("../lib/oop");
var TextMode = acequire("./sql").Mode;
var FleetHighlightRules = acequire("./fleet_highlight_rules").FleetHighlightRules;
var Range = acequire("../range").Range;
var Mode = function () {
this.HighlightRules = FleetHighlightRules;
// ... any additional mode setup ...
};
oop.inherits(Mode, TextMode);
(function () {
this.lineCommentStart = "--";
this.$id = "ace/mode/fleet";
}.call(Mode.prototype));
exports.Mode = Mode;
}
);

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,387 @@
ace.define("ace/mode/sh_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module){"use strict";
var oop = require("../lib/oop");
var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
var reservedKeywords = exports.reservedKeywords = ('!|{|}|case|do|done|elif|else|' +
'esac|fi|for|if|in|then|until|while|' +
'&|;|export|local|read|typeset|unset|' +
'elif|select|set|function|declare|readonly');
var languageConstructs = exports.languageConstructs = ('[|]|alias|bg|bind|break|builtin|' +
'cd|command|compgen|complete|continue|' +
'dirs|disown|echo|enable|eval|exec|' +
'exit|fc|fg|getopts|hash|help|history|' +
'jobs|kill|let|logout|popd|printf|pushd|' +
'pwd|return|set|shift|shopt|source|' +
'suspend|test|times|trap|type|ulimit|' +
'umask|unalias|wait');
var ShHighlightRules = function () {
var keywordMapper = this.createKeywordMapper({
"keyword": reservedKeywords,
"support.function.builtin": languageConstructs,
"invalid.deprecated": "debugger"
}, "identifier");
var integer = "(?:(?:[1-9]\\d*)|(?:0))";
var fraction = "(?:\\.\\d+)";
var intPart = "(?:\\d+)";
var pointFloat = "(?:(?:" + intPart + "?" + fraction + ")|(?:" + intPart + "\\.))";
var exponentFloat = "(?:(?:" + pointFloat + "|" + intPart + ")" + ")";
var floatNumber = "(?:" + exponentFloat + "|" + pointFloat + ")";
var fileDescriptor = "(?:&" + intPart + ")";
var variableName = "[a-zA-Z_][a-zA-Z0-9_]*";
var variable = "(?:" + variableName + "(?==))";
var builtinVariable = "(?:\\$(?:SHLVL|\\$|\\!|\\?))";
var func = "(?:" + variableName + "\\s*\\(\\))";
this.$rules = {
"start": [{
token: "constant",
regex: /\\./
}, {
token: ["text", "comment"],
regex: /(^|\s)(#.*)$/
}, {
token: "string.start",
regex: '"',
push: [{
token: "constant.language.escape",
regex: /\\(?:[$`"\\]|$)/
}, {
include: "variables"
}, {
token: "keyword.operator",
regex: /`/ // TODO highlight `
}, {
token: "string.end",
regex: '"',
next: "pop"
}, {
defaultToken: "string"
}]
}, {
token: "string",
regex: "\\$'",
push: [{
token: "constant.language.escape",
regex: /\\(?:[abeEfnrtv\\'"]|x[a-fA-F\d]{1,2}|u[a-fA-F\d]{4}([a-fA-F\d]{4})?|c.|\d{1,3})/
}, {
token: "string",
regex: "'",
next: "pop"
}, {
defaultToken: "string"
}]
}, {
regex: "<<<",
token: "keyword.operator"
}, {
stateName: "heredoc",
regex: "(<<-?)(\\s*)(['\"`]?)([\\w\\-]+)(['\"`]?)",
onMatch: function (value, currentState, stack) {
var next = value[2] == '-' ? "indentedHeredoc" : "heredoc";
var tokens = value.split(this.splitRegex);
stack.push(next, tokens[4]);
return [
{ type: "constant", value: tokens[1] },
{ type: "text", value: tokens[2] },
{ type: "string", value: tokens[3] },
{ type: "support.class", value: tokens[4] },
{ type: "string", value: tokens[5] }
];
},
rules: {
heredoc: [{
onMatch: function (value, currentState, stack) {
if (value === stack[1]) {
stack.shift();
stack.shift();
this.next = stack[0] || "start";
return "support.class";
}
this.next = "";
return "string";
},
regex: ".*$",
next: "start"
}],
indentedHeredoc: [{
token: "string",
regex: "^\t+"
}, {
onMatch: function (value, currentState, stack) {
if (value === stack[1]) {
stack.shift();
stack.shift();
this.next = stack[0] || "start";
return "support.class";
}
this.next = "";
return "string";
},
regex: ".*$",
next: "start"
}]
}
}, {
regex: "$",
token: "empty",
next: function (currentState, stack) {
if (stack[0] === "heredoc" || stack[0] === "indentedHeredoc")
return stack[0];
return currentState;
}
}, {
token: ["keyword", "text", "text", "text", "variable"],
regex: /(declare|local|readonly)(\s+)(?:(-[fixar]+)(\s+))?([a-zA-Z_][a-zA-Z0-9_]*\b)/
}, {
token: "variable.language",
regex: builtinVariable
}, {
token: "variable",
regex: variable
}, {
include: "variables"
}, {
token: "support.function",
regex: func
}, {
token: "support.function",
regex: fileDescriptor
}, {
token: "string", // ' string
start: "'", end: "'"
}, {
token: "constant.numeric", // float
regex: floatNumber
}, {
token: "constant.numeric", // integer
regex: integer + "\\b"
}, {
token: keywordMapper,
regex: "[a-zA-Z_][a-zA-Z0-9_]*\\b"
}, {
token: "keyword.operator",
regex: "\\+|\\-|\\*|\\*\\*|\\/|\\/\\/|~|<|>|<=|=>|=|!=|[%&|`]"
}, {
token: "punctuation.operator",
regex: ";"
}, {
token: "paren.lparen",
regex: "[\\[\\(\\{]"
}, {
token: "paren.rparen",
regex: "[\\]]"
}, {
token: "paren.rparen",
regex: "[\\)\\}]",
next: "pop"
}],
variables: [{
token: "variable",
regex: /(\$)(\w+)/
}, {
token: ["variable", "paren.lparen"],
regex: /(\$)(\()/,
push: "start"
}, {
token: ["variable", "paren.lparen", "keyword.operator", "variable", "keyword.operator"],
regex: /(\$)(\{)([#!]?)(\w+|[*@#?\-$!0_])(:[?+\-=]?|##?|%%?|,,?\/|\^\^?)?/,
push: "start"
}, {
token: "variable",
regex: /\$[*@#?\-$!0_]/
}, {
token: ["variable", "paren.lparen"],
regex: /(\$)(\{)/,
push: "start"
}]
};
this.normalizeRules();
};
oop.inherits(ShHighlightRules, TextHighlightRules);
exports.ShHighlightRules = ShHighlightRules;
});
ace.define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"], function(require, exports, module){"use strict";
var oop = require("../../lib/oop");
var Range = require("../../range").Range;
var BaseFoldMode = require("./fold_mode").FoldMode;
var FoldMode = exports.FoldMode = function (commentRegex) {
if (commentRegex) {
this.foldingStartMarker = new RegExp(this.foldingStartMarker.source.replace(/\|[^|]*?$/, "|" + commentRegex.start));
this.foldingStopMarker = new RegExp(this.foldingStopMarker.source.replace(/\|[^|]*?$/, "|" + commentRegex.end));
}
};
oop.inherits(FoldMode, BaseFoldMode);
(function () {
this.foldingStartMarker = /([\{\[\(])[^\}\]\)]*$|^\s*(\/\*)/;
this.foldingStopMarker = /^[^\[\{\(]*([\}\]\)])|^[\s\*]*(\*\/)/;
this.singleLineBlockCommentRe = /^\s*(\/\*).*\*\/\s*$/;
this.tripleStarBlockCommentRe = /^\s*(\/\*\*\*).*\*\/\s*$/;
this.startRegionRe = /^\s*(\/\*|\/\/)#?region\b/;
this._getFoldWidgetBase = this.getFoldWidget;
this.getFoldWidget = function (session, foldStyle, row) {
var line = session.getLine(row);
if (this.singleLineBlockCommentRe.test(line)) {
if (!this.startRegionRe.test(line) && !this.tripleStarBlockCommentRe.test(line))
return "";
}
var fw = this._getFoldWidgetBase(session, foldStyle, row);
if (!fw && this.startRegionRe.test(line))
return "start"; // lineCommentRegionStart
return fw;
};
this.getFoldWidgetRange = function (session, foldStyle, row, forceMultiline) {
var line = session.getLine(row);
if (this.startRegionRe.test(line))
return this.getCommentRegionBlock(session, line, row);
var match = line.match(this.foldingStartMarker);
if (match) {
var i = match.index;
if (match[1])
return this.openingBracketBlock(session, match[1], row, i);
var range = session.getCommentFoldRange(row, i + match[0].length, 1);
if (range && !range.isMultiLine()) {
if (forceMultiline) {
range = this.getSectionRange(session, row);
}
else if (foldStyle != "all")
range = null;
}
return range;
}
if (foldStyle === "markbegin")
return;
var match = line.match(this.foldingStopMarker);
if (match) {
var i = match.index + match[0].length;
if (match[1])
return this.closingBracketBlock(session, match[1], row, i);
return session.getCommentFoldRange(row, i, -1);
}
};
this.getSectionRange = function (session, row) {
var line = session.getLine(row);
var startIndent = line.search(/\S/);
var startRow = row;
var startColumn = line.length;
row = row + 1;
var endRow = row;
var maxRow = session.getLength();
while (++row < maxRow) {
line = session.getLine(row);
var indent = line.search(/\S/);
if (indent === -1)
continue;
if (startIndent > indent)
break;
var subRange = this.getFoldWidgetRange(session, "all", row);
if (subRange) {
if (subRange.start.row <= startRow) {
break;
}
else if (subRange.isMultiLine()) {
row = subRange.end.row;
}
else if (startIndent == indent) {
break;
}
}
endRow = row;
}
return new Range(startRow, startColumn, endRow, session.getLine(endRow).length);
};
this.getCommentRegionBlock = function (session, line, row) {
var startColumn = line.search(/\s*$/);
var maxRow = session.getLength();
var startRow = row;
var re = /^\s*(?:\/\*|\/\/|--)#?(end)?region\b/;
var depth = 1;
while (++row < maxRow) {
line = session.getLine(row);
var m = re.exec(line);
if (!m)
continue;
if (m[1])
depth--;
else
depth++;
if (!depth)
break;
}
var endRow = row;
if (endRow > startRow) {
return new Range(startRow, startColumn, endRow, line.length);
}
};
}).call(FoldMode.prototype);
});
ace.define("ace/mode/sh",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/sh_highlight_rules","ace/range","ace/mode/folding/cstyle"], function(require, exports, module){"use strict";
var oop = require("../lib/oop");
var TextMode = require("./text").Mode;
var ShHighlightRules = require("./sh_highlight_rules").ShHighlightRules;
var Range = require("../range").Range;
var CStyleFoldMode = require("./folding/cstyle").FoldMode;
var Mode = function () {
this.HighlightRules = ShHighlightRules;
this.foldingRules = new CStyleFoldMode();
this.$behaviour = this.$defaultBehaviour;
};
oop.inherits(Mode, TextMode);
(function () {
this.lineCommentStart = "#";
this.getNextLineIndent = function (state, line, tab) {
var indent = this.$getIndent(line);
var tokenizedLine = this.getTokenizer().getLineTokens(line, state);
var tokens = tokenizedLine.tokens;
if (tokens.length && tokens[tokens.length - 1].type == "comment") {
return indent;
}
if (state == "start") {
var match = line.match(/^.*[\{\(\[:]\s*$/);
if (match) {
indent += tab;
}
}
return indent;
};
var outdents = {
"pass": 1,
"return": 1,
"raise": 1,
"break": 1,
"continue": 1
};
this.checkOutdent = function (state, line, input) {
if (input !== "\r\n" && input !== "\r" && input !== "\n")
return false;
var tokens = this.getTokenizer().getLineTokens(line.trim(), state).tokens;
if (!tokens)
return false;
do {
var last = tokens.pop();
} while (last && (last.type == "comment" || (last.type == "text" && last.value.match(/^\s+$/))));
if (!last)
return false;
return (last.type == "keyword" && outdents[last.value]);
};
this.autoOutdent = function (state, doc, row) {
row += 1;
var indent = this.$getIndent(doc.getLine(row));
var tab = doc.getTabString();
if (indent.slice(-tab.length) == tab)
doc.remove(new Range(row, indent.length - tab.length, row, indent.length));
};
this.$id = "ace/mode/sh";
this.snippetFileId = "ace/snippets/sh";
}).call(Mode.prototype);
exports.Mode = Mode;
}); (function() {
ace.require(["ace/mode/sh"], function(m) {
if (typeof module == "object" && typeof exports == "object" && module) {
module.exports = m;
}
});
})();

View file

@ -0,0 +1,221 @@
ace.define("ace/mode/sql_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module){"use strict";
var oop = require("../lib/oop");
var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
var SqlHighlightRules = function () {
var keywords = ("select|insert|update|delete|from|where|and|or|group|by|order|limit|offset|having|as|case|" +
"when|then|else|end|type|left|right|join|on|outer|desc|asc|union|create|table|primary|key|if|" +
"foreign|not|references|default|null|inner|cross|natural|database|drop|grant|distinct|is|in|" +
"all|alter|any|array|at|authorization|between|both|cast|check|collate|column|commit|constraint|" +
"cube|current|current_date|current_time|current_timestamp|current_user|describe|escape|except|" +
"exists|external|extract|fetch|filter|for|full|function|global|grouping|intersect|interval|" +
"into|leading|like|local|no|of|only|out|overlaps|partition|position|range|revoke|rollback|rollup|" +
"row|rows|session_user|set|some|start|tablesample|time|to|trailing|truncate|unique|unknown|" +
"user|using|values|window|with");
var builtinConstants = ("true|false");
var builtinFunctions = ("avg|count|first|last|max|min|sum|ucase|lcase|mid|len|round|rank|now|format|" +
"coalesce|ifnull|isnull|nvl");
var dataTypes = ("int|numeric|decimal|date|varchar|char|bigint|float|double|bit|binary|text|set|timestamp|" +
"money|real|number|integer|string");
var keywordMapper = this.createKeywordMapper({
"support.function": builtinFunctions,
"keyword": keywords,
"constant.language": builtinConstants,
"storage.type": dataTypes
}, "identifier", true);
this.$rules = {
"start": [{
token: "comment",
regex: "--.*$"
}, {
token: "comment",
start: "/\\*",
end: "\\*/"
}, {
token: "string", // " string
regex: '".*?"'
}, {
token: "string", // ' string
regex: "'.*?'"
}, {
token: "string", // ` string (apache drill)
regex: "`.*?`"
}, {
token: "constant.numeric", // float
regex: "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"
}, {
token: keywordMapper,
regex: "[a-zA-Z_$][a-zA-Z0-9_$]*\\b"
}, {
token: "keyword.operator",
regex: "\\+|\\-|\\/|\\/\\/|%|<@>|@>|<@|&|\\^|~|<|>|<=|=>|==|!=|<>|="
}, {
token: "paren.lparen",
regex: "[\\(]"
}, {
token: "paren.rparen",
regex: "[\\)]"
}, {
token: "text",
regex: "\\s+"
}]
};
this.normalizeRules();
};
oop.inherits(SqlHighlightRules, TextHighlightRules);
exports.SqlHighlightRules = SqlHighlightRules;
});
ace.define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"], function(require, exports, module){"use strict";
var oop = require("../../lib/oop");
var Range = require("../../range").Range;
var BaseFoldMode = require("./fold_mode").FoldMode;
var FoldMode = exports.FoldMode = function (commentRegex) {
if (commentRegex) {
this.foldingStartMarker = new RegExp(this.foldingStartMarker.source.replace(/\|[^|]*?$/, "|" + commentRegex.start));
this.foldingStopMarker = new RegExp(this.foldingStopMarker.source.replace(/\|[^|]*?$/, "|" + commentRegex.end));
}
};
oop.inherits(FoldMode, BaseFoldMode);
(function () {
this.foldingStartMarker = /([\{\[\(])[^\}\]\)]*$|^\s*(\/\*)/;
this.foldingStopMarker = /^[^\[\{\(]*([\}\]\)])|^[\s\*]*(\*\/)/;
this.singleLineBlockCommentRe = /^\s*(\/\*).*\*\/\s*$/;
this.tripleStarBlockCommentRe = /^\s*(\/\*\*\*).*\*\/\s*$/;
this.startRegionRe = /^\s*(\/\*|\/\/)#?region\b/;
this._getFoldWidgetBase = this.getFoldWidget;
this.getFoldWidget = function (session, foldStyle, row) {
var line = session.getLine(row);
if (this.singleLineBlockCommentRe.test(line)) {
if (!this.startRegionRe.test(line) && !this.tripleStarBlockCommentRe.test(line))
return "";
}
var fw = this._getFoldWidgetBase(session, foldStyle, row);
if (!fw && this.startRegionRe.test(line))
return "start"; // lineCommentRegionStart
return fw;
};
this.getFoldWidgetRange = function (session, foldStyle, row, forceMultiline) {
var line = session.getLine(row);
if (this.startRegionRe.test(line))
return this.getCommentRegionBlock(session, line, row);
var match = line.match(this.foldingStartMarker);
if (match) {
var i = match.index;
if (match[1])
return this.openingBracketBlock(session, match[1], row, i);
var range = session.getCommentFoldRange(row, i + match[0].length, 1);
if (range && !range.isMultiLine()) {
if (forceMultiline) {
range = this.getSectionRange(session, row);
}
else if (foldStyle != "all")
range = null;
}
return range;
}
if (foldStyle === "markbegin")
return;
var match = line.match(this.foldingStopMarker);
if (match) {
var i = match.index + match[0].length;
if (match[1])
return this.closingBracketBlock(session, match[1], row, i);
return session.getCommentFoldRange(row, i, -1);
}
};
this.getSectionRange = function (session, row) {
var line = session.getLine(row);
var startIndent = line.search(/\S/);
var startRow = row;
var startColumn = line.length;
row = row + 1;
var endRow = row;
var maxRow = session.getLength();
while (++row < maxRow) {
line = session.getLine(row);
var indent = line.search(/\S/);
if (indent === -1)
continue;
if (startIndent > indent)
break;
var subRange = this.getFoldWidgetRange(session, "all", row);
if (subRange) {
if (subRange.start.row <= startRow) {
break;
}
else if (subRange.isMultiLine()) {
row = subRange.end.row;
}
else if (startIndent == indent) {
break;
}
}
endRow = row;
}
return new Range(startRow, startColumn, endRow, session.getLine(endRow).length);
};
this.getCommentRegionBlock = function (session, line, row) {
var startColumn = line.search(/\s*$/);
var maxRow = session.getLength();
var startRow = row;
var re = /^\s*(?:\/\*|\/\/|--)#?(end)?region\b/;
var depth = 1;
while (++row < maxRow) {
line = session.getLine(row);
var m = re.exec(line);
if (!m)
continue;
if (m[1])
depth--;
else
depth++;
if (!depth)
break;
}
var endRow = row;
if (endRow > startRow) {
return new Range(startRow, startColumn, endRow, line.length);
}
};
}).call(FoldMode.prototype);
});
ace.define("ace/mode/folding/sql",["require","exports","module","ace/lib/oop","ace/mode/folding/cstyle"], function(require, exports, module){"use strict";
var oop = require("../../lib/oop");
var BaseFoldMode = require("./cstyle").FoldMode;
var FoldMode = exports.FoldMode = function () { };
oop.inherits(FoldMode, BaseFoldMode);
(function () {
}).call(FoldMode.prototype);
});
ace.define("ace/mode/sql",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/sql_highlight_rules","ace/mode/folding/sql"], function(require, exports, module){"use strict";
var oop = require("../lib/oop");
var TextMode = require("./text").Mode;
var SqlHighlightRules = require("./sql_highlight_rules").SqlHighlightRules;
var SqlFoldMode = require("./folding/sql").FoldMode;
var Mode = function () {
this.HighlightRules = SqlHighlightRules;
this.foldingRules = new SqlFoldMode();
this.$behaviour = this.$defaultBehaviour;
};
oop.inherits(Mode, TextMode);
(function () {
this.lineCommentStart = "--";
this.blockComment = { start: "/*", end: "*/" };
this.$id = "ace/mode/sql";
this.snippetFileId = "ace/snippets/sql";
}).call(Mode.prototype);
exports.Mode = Mode;
}); (function() {
ace.require(["ace/mode/sql"], function(m) {
if (typeof module == "object" && typeof exports == "object" && module) {
module.exports = m;
}
});
})();

View file

@ -0,0 +1,195 @@
ace.define(
"ace/theme/fleet",
["require", "exports", "module", "ace/lib/dom"],
function (acequire, exports, module) {
// The CSS is inlined and backslashes are used to escape newlines
var cssText = ".ace_editor.ace-fleet {\
font-size: 14px;\
background-color: #fafafa;\
color: #66696f;\
border-radius: 4px;\
border: solid 1px #dbe3e5;\
line-height: 24px;\
}\
\
.ace_editor.ace-fleet.ace_focus {\
box-shadow: inset 0 0 6px 0 rgba(0, 0, 0, 0.16);\
background: white;\
}\
\
.ace_editor.ace-fleet.ace_focus .ace_gutter {\
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.16);\
}\
.ace_editor.ace-fleet.ace_focus .ace_scroller {\
border-bottom: solid 1px #c38dec;\
}\
\
.ace-fleet.ace_autocomplete .ace_content {\
padding-left: 0px;\
}\
\
.ace_editor.ace-fleet.ace_autocomplete {\
width: 350px;\
}\
\
.ace-fleet .ace_content {\
height: 100% !important;\
}\
\
.ace-fleet .ace_gutter {\
background: #fff;\
color: #c38dec;\
z-index: 1;\
border-right: solid 1px #e3e3e3;\
}\
\
.ace-fleet .ace_gutter-active-line {\
background-color: rgba(174, 109, 223, 0.15);\
border-radius: 2px;\
}\
\
.ace-fleet .ace_print-margin {\
width: 1px;\
background: #f6f6f6;\
}\
\
.ace-fleet .ace_scrollbar {\
z-index: 1;\
}\
\
.ace-fleet .ace_cursor {\
color: #aeafad;\
}\
\
/* Hide cursor in read-only mode */\
.ace-fleet .ace_hidden-cursors {\
opacity: 0;\
}\
\
.ace-fleet .ace_marker-layer .ace_selection {\
background: rgba(74, 144, 226, 0.13);\
}\
\
.ace-fleet.ace_multiselect .ace_selection.ace_start {\
box-shadow: 0 0 3px 0px #ffffff;\
}\
\
.ace-fleet .ace_marker-layer .ace_step {\
background: rgb(255, 255, 0);\
}\
\
.ace-fleet .ace_marker-layer .ace_bracket {\
margin: -1px 0 0 -1px;\
border: 1px solid #d1d1d1;\
}\
\
.ace-fleet .ace_marker-layer .ace_selected-word {\
border: 1px solid #d6d6d6;\
}\
\
.ace-fleet .ace_invisible {\
color: #d1d1d1;\
}\
\
.ace-fleet .ace_keyword {\
color: #ae6ddf;\
font-weight: $bold;\
}\
\
.ace-fleet .ace_osquery-token {\
border-radius: 3px;\
background-color: #ae6ddf;\
color: #ffffff;\
}\
\
.ace-fleet .ace_identifier {\
color: #ff5850;\
}\
\
.ace-fleet .ace_string,\
.ace-fleet .ace_osquery-column {\
color: #4fd061;\
}\
\
.ace-fleet .ace_meta,\
.ace-fleet .ace_storage,\
.ace-fleet .ace_storage.ace_type,\
.ace-fleet .ace_support.ace_type {\
color: #8959a8;\
}\
\
.ace-fleet .ace_keyword.ace_operator {\
color: #3e999f;\
}\
\
.ace-fleet .ace_constant.ace_character,\
.ace-fleet .ace_constant.ace_language,\
.ace-fleet .ace_constant.ace_numeric,\
.ace-fleet .ace_keyword.ace_other.ace_unit,\
.ace-fleet .ace_support.ace_constant,\
.ace-fleet .ace_variable.ace_parameter {\
color: #f5871f;\
}\
\
.ace-fleet .ace_constant.ace_other {\
color: #666969;\
}\
\
.ace-fleet .ace_invalid {\
color: #ffffff;\
background-color: #c82829;\
}\
\
.ace-fleet .ace_invalid.ace_deprecated {\
color: #ffffff;\
background-color: #ae6ddf;\
}\
\
.ace-fleet .ace_fold {\
background-color: #4271ae;\
border-color: #4d4d4c;\
}\
\
.ace-fleet .ace_entity.ace_name.ace_function,\
.ace-fleet .ace_support.ace_function,\
.ace-fleet .ace_variable {\
color: #4271ae;\
}\
\
.ace-fleet .ace_support.ace_class,\
.ace-fleet .ace_support.ace_type {\
color: #c99e00;\
}\
\
.ace-fleet .ace_heading,\
.ace-fleet .ace_markup.ace_heading,\
.ace-fleet .ace_string {\
color: #4fd061;\
}\
\
.ace-fleet .ace_entity.ace_name.ace_tag,\
.ace-fleet .ace_entity.ace_other.ace_attribute-name,\
.ace-fleet .ace_meta.ace_tag,\
.ace-fleet .ace_string.ace_regexp,\
.ace-fleet .ace_variable {\
color: #c82829;\
}\
\
.ace-fleet .ace_comment {\
color: #8e908c;\
}\
\
.ace-fleet .ace_indent-guide {\
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bdu3f/BwAlfgctduB85QAAAABJRU5ErkJggg==)\
right repeat-y;\
}\
";
exports.isDark = false;
exports.cssClass = "ace-fleet";
exports.cssText = cssText;
var dom = acequire("../lib/dom");
dom.importCssString(exports.cssText, exports.cssClass);
}
);

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -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"]},"observeMySession":{"verb":"POST","url":"/api/v1/observe-my-session","args":[],"protocol":"io.socket"},"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":["id"]},"uploadScript":{"verb":"POST","url":"/api/v1/upload-script","args":["newScript","teams"]},"editScript":{"verb":"POST","url":"/api/v1/edit-script","args":["script","newTeamIds","newScript"]}}
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"]}}
/* eslint-enable */
});

View file

@ -0,0 +1,160 @@
/**
* <ace-editor>
* -----------------------------------------------------------------------------
*
* @type {Component}
*
* --- SLOTS: ---
* @slot item-field
* The template to use for each field.
* > Also note:
* > If this slot contains exactly one element with `role="focusable"` or
* > `focus-first`, that element will be focused automatically on add/remove.
* @param {Ref} item
* @param {Function} doSet
* @param {Array} allItems
* @param {Number} idx
*
* --- EVENTS EMITTED: ---
* @event input
*
* -----------------------------------------------------------------------------
*/
parasails.registerComponent('aceEditor', {
// ╔═╗╦═╗╔═╗╔═╗╔═╗
// ╠═╝╠╦╝║ ║╠═╝╚═╗
// ╩ ╩╚═╚═╝╩ ╚═╝
props: [
'value',// « 2-way (for v-model)
'mode',// For customizing the type of editor
'maxLines',
'minLines',
],
// ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
// ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
// ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
data: function () {
return {
currentValue: undefined, //« will be initialized to a string in beforeMount
uniqueId: crypto.randomUUID(),// Used to create a unique ID for the ace editor component.
};
},
// ╦ ╦╔╦╗╔╦╗╦
// ╠═╣ ║ ║║║║
// ╩ ╩ ╩ ╩ ╩╩═╝
template: `
<div class="ace-editor-container">
<div @input="inputDefaultItemField($event)" style="height: 300px;" :id="'editor' + uniqueId" :do-set="_getCurriedDoSetFn()">{{value}}</div>
</div>
`,
// ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗
// ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣
// ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
beforeMount: function() {
if (this.value === undefined) {
this.currentValue = undefined;
} else {
this.currentValue = _.clone(this.value);
// ^^ The clone is to prevent entanglement risk.
}
if(this.mode){
if(!['sh', 'fleet', 'powershell'].includes(this.mode)){
throw new Error(`Invalid mode passed into <ace-editor> component, currently, only 'sh' and 'fleet' are supported.`, this.mode);
}
}
},
mounted: async function () {
this._setUpAceEditor(this.mode);
},
beforeDestroy: function() {
},
watch: {
currentValue: function() {
this.currentValue = ace.edit('editor'+this.uniqueId).getValue();
}
},
// ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗
// ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
methods: {
// ╔═╗╦ ╦╔═╗╔╗╔╔╦╗ ╦ ╦╔═╗╔╗╔╔╦╗╦ ╔═╗╦═╗╔═╗
// ║╣ ╚╗╔╝║╣ ║║║ ║ ╠═╣╠═╣║║║ ║║║ ║╣ ╠╦╝╚═╗
// ╚═╝ ╚╝ ╚═╝╝╚╝ ╩ ╩ ╩╩ ╩╝╚╝═╩╝╩═╝╚═╝╩╚═╚═╝
inputDefaultItemField: async function($event) {
var parsedValue = $event.target.value || undefined;
this.currentValue = parsedValue;
await this.forceRender();
this._handleChangingFieldValues();
},
// ╔═╗╦ ╦╔╗ ╦ ╦╔═╗ ╔╦╗╔═╗╔╦╗╦ ╦╔═╗╔╦╗╔═╗
// ╠═╝║ ║╠╩╗║ ║║ ║║║║╣ ║ ╠═╣║ ║ ║║╚═╗
// ╩ ╚═╝╚═╝╩═╝╩╚═╝ ╩ ╩╚═╝ ╩ ╩ ╩╚═╝═╩╝╚═╝
//…
// ╔═╗╦═╗╦╦ ╦╔═╗╔╦╗╔═╗ ╔╦╗╔═╗╔╦╗╦ ╦╔═╗╔╦╗╔═╗
// ╠═╝╠╦╝║╚╗╔╝╠═╣ ║ ║╣ ║║║║╣ ║ ╠═╣║ ║ ║║╚═╗
// ╩ ╩╚═╩ ╚╝ ╩ ╩ ╩ ╚═╝ ╩ ╩╚═╝ ╩ ╩ ╩╚═╝═╩╝╚═╝
_getCurriedDoSetFn: function() {
return async (newVal)=>{
// Note that it is the responsibility of the userland contents of the
// slot to make sure this incoming value is proper. For example, if
// the slot contains an `<input>`, then when invoking doSet, you should
// do so like:
// ```
// <input :value="item" @input="doSet($event.target.value||undefined)"/>
// ```
//
// The `||undefined` is because otherwise, you get `null`, and you
// probably want blank fields to be treated as undefined so we can
// automatically splice them out of the array before emitting our input
// event.
//
// The reason this is left as a userland concern is because the `null`
// value itself, just like `''`, `0`, `false`, `NaN` or other similar
// values, is technically a valid thing that might be relevant under
// unusual circumstances.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// console.log('FIRED DOSET for idx',idx,'and newVal', newVal);
this.currentValue = newVal;
await this.forceRender();
this._handleChangingFieldValues();
};
},
_handleChangingFieldValues: function() {
// > Note that we do a `_.clone()`. This is to prevent an entanglement
// > issue caused by emitting the same reference if we were to simply emit
// > `this.currentValue` directly.
this.$emit('input', _.clone(this.currentValue));
// console.log('emitting in <multifield>…', _.cloneDeep(this.currentValue));
},
_setUpAceEditor: function(mode) {
var editor = ace.edit('editor'+ this.uniqueId);
ace.config.setModuleUrl('ace/mode/fleet', '/dependencies/src-min/mode-fleet.js');
editor.setTheme('ace/theme/fleet');
editor.session.setMode('ace/mode/'+mode);
editor.setOptions({
minLines: this.minLines ? this.minLines : 4 ,
maxLines: this.maxLines ? this.maxLines : 11 ,
});
},
}
});

View file

@ -76,6 +76,7 @@ parasails.registerComponent('fileUpload', {
selectedFileMimeType: undefined,
selectedFileIconClass: undefined,
selectedFileSize: undefined,
uploadedFilename: undefined,
};
},
@ -141,6 +142,41 @@ parasails.registerComponent('fileUpload', {
</div>
</div>
</div>
<div v-else-if="mode === 'software'">
<div purpose="software-upload-input" v-if="isEmpty">
<div class="d-flex flex-column align-items-center">
<div class="d-flex flex-row justify-content-center mb-2">
<img style="height: 40px; width: 34px; margin-right: 16px;" src="/images/software-icon-34x40@2x.png">
</div>
<p style="color: #8B8FA2" class="muted">.pkg, .msi, .exe, or .deb</p>
<div class="btn-and-tips-if-relevant d-flex flex-row justify-content-center mt-0">
<label purpose="file-upload" for="file-upload-input">
<img src="/images/upload-16x17@2x.png" style="height: 16px; width: 16px; margin-right: 8px">Choose file
</label>
<input id="file-upload-input" type="file" class="file-input d-none" :disabled="isCurrentlyDisabled" accept=".exe,.pkg,.deb,.msi" @change="changeFileInput($event)"/>
</div>
</div>
</div>
<div purpose="software-information" v-else>
<div class="d-flex flex-row justify-content-start">
<img style="height: 40px; width: 34px; margin-right: 16px;" src="/images/software-icon-34x40@2x.png">
<div class="d-flex flex-column">
<p><strong>{{selectedFileName}}</strong></p>
<p class="muted" v-if="_.endsWith(selectedFileName, '.exe') || _.endsWith(selectedFileName, '.msi')">Windows</p>
<p class="muted" v-else-if="_.endsWith(selectedFileName, '.pkg')">macOS</p>
<p class="muted" v-else-if="_.endsWith(selectedFileName, '.deb')">Linux</p>
</div>
</div>
</div>
</div>
<div v-else-if="mode === 'software-pencil'">
<div class="btn-and-tips-if-relevant">
<label purpose="file-upload" for="file-upload-input">
<img src="/images/icon-edit-software-16x16@2x.png" style="height: 16px; margin-right: 8px">
</label>
<input id="file-upload-input" type="file" class="file-input d-none" :disabled="isCurrentlyDisabled" accept=".exe,.pkg,.deb,.msi" @change="changeFileInput($event)"/>
</div>
</div>
</div>
`,
@ -187,6 +223,10 @@ parasails.registerComponent('fileUpload', {
value: function(newFile, unusedOldVal) {
this._absorbValue(newFile);
},
selectedFileName: function(newFileName) {
// Emit the update to the parent component
this.$emit('update:uploadedFilename', newFileName);
}
},
// ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
@ -235,7 +275,6 @@ parasails.registerComponent('fileUpload', {
// Emit an event so the v-model can update with our selected file.
this.$emit('input', selectedFile);
},
// ╔═╗╦ ╦╔╗ ╦ ╦╔═╗ ╔╦╗╔═╗╔╦╗╦ ╦╔═╗╔╦╗╔═╗

View file

@ -0,0 +1,143 @@
parasails.registerPage('software', {
// ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
// ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
// ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
data: {
sortDirection: 'ASC',
teamFilter: undefined,
softwareToDisplay: [],
platformFriendlyNames: {
'darwin': 'macOS, iOS, ipadOS',
'windows': 'Windows',
'linux': 'Linux'
},
selectedTeam: {},
modal: '',
syncing: false,
formData: {},
formErrors: {},
addSoftwareFormRules: {
newSoftware: {required: true},
},
editSoftwareFormRules: {},
profileToEdit: {},
cloudError: '',
newSoftware: undefined,
showAdvancedOptions: false,
newSoftwareFilename: undefined,
},
// ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗
// ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣
// ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
beforeMount: function() {
this.softwareToDisplay = this.software;
},
mounted: async function() {
//…
},
// ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗
// ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
methods: {
clickChangeSortDirection: async function() {
if(this.sortDirection === 'ASC') {
this.sortDirection = 'DESC';
this.softwareToDisplay = _.sortByOrder(this.software, 'name', 'desc');
} else {
this.sortDirection = 'ASC';
this.softwareToDisplay = _.sortByOrder(this.software, 'name', 'asc');
}
await this.forceRender();
},
clickDownloadSoftware: async function(software) {
if(!software.teams){
window.open('/download-software?id='+encodeURIComponent(software.id));
} else {
window.open('/download-software?fleetApid='+encodeURIComponent(software.teams[0].softwareFleetApid)+'&teamApid='+software.teams[0].fleetApid);
}
},
clickOpenEditModal: async function(software) {
this.softwareToEdit = _.clone(software);
this.formData.newTeamIds = _.pluck(this.softwareToEdit.teams, 'fleetApid');
this.formData.software = software;
this.formData.preInstallQuery = software.preInstallQuery;
this.formData.installScript = software.installScript;
this.formData.postInstallScript = software.postInstallScript;
this.formData.uninstallScript = software.uninstallScript;
this.modal = 'edit-software';
},
clickOpenDeleteModal: async function(software) {
this.formData.software = _.clone(software);
this.modal = 'delete-software';
},
clickOpenAddSoftwareModal: async function() {
this.modal = 'add-software';
},
changeTeamFilter: async function() {
if(this.teamFilter !== undefined){
this.selectedTeam = _.find(this.teams, {fleetApid: this.teamFilter});
let softwareOnThisTeam = _.filter(this.software, (software)=>{
return _.where(software.teams, {'fleetApid': this.selectedTeam.fleetApid}).length > 0;
});
this.softwareToDisplay = softwareOnThisTeam;
} else {
this.softwareToDisplay = this.software;
}
},
submittedForm: async function() {
this.syncing = false;
this.closeModal();
},
closeModal: async function() {
this.modal = '';
this.formErrors = {};
this.formData = {};
this.showAdvancedOptions = false;
await this.forceRender();
},
clickChangeTeamFilter: async function(teamApid) {
this.teamFilter = teamApid;
this.selectedTeam = _.find(this.teams, {'fleetApid': teamApid});
let softwareOnThisTeam = _.filter(this.software, (software)=>{
return _.where(software.teams, {'fleetApid': this.selectedTeam.fleetApid}).length > 0;
});
this.softwareToDisplay = softwareOnThisTeam;
},
handleSubmittingEditSoftwareForm: async function() {
let argins = _.clone(this.formData);
if(argins.newTeamIds === [undefined]){
argins.newTeamIds = [];
}
await Cloud.editSoftware.with(argins);
if(!this.cloudError) {
this.syncing = false;
this.closeModal();
await this._getSoftware();
}
},
handleSubmittingAddSoftwareForm: async function() {
let argins = _.clone(this.formData);
await Cloud.uploadSoftware.with({newSoftware: argins.newSoftware, teams: argins.teams});
await this._getSoftware();
},
handleSubmittingDeleteSoftwareForm: async function() {
let argins = _.clone(this.formData);
await Cloud.deleteSoftware.with({software: argins.software});
if(!this.cloudError) {
this.syncing = false;
this.closeModal();
await this._getSoftware();
}
},
_getSoftware: async function() {
this.syncing = true;
let newSoftwareInformation = await Cloud.getSoftware();
this.software = newSoftwareInformation;
this.syncing = false;
await this.changeTeamFilter();
}
}
});

View file

@ -30,7 +30,6 @@
}
}
&.file-mode {
.btn-and-tips-if-relevant {
display: inline-block;

View file

@ -27,6 +27,7 @@
// Per-page styles
@import 'pages/scripts.less';
@import 'pages/profiles.less';
@import 'pages/software/software.less';
@import 'pages/entrance/confirmed-email.less';
@import 'pages/entrance/login.less';
@import 'pages/entrance/forgot-password.less';

View file

@ -0,0 +1,271 @@
#software {
[purpose='page-content'] {
padding: 24px 32px 64px 32px;
}
p {
margin-block-end: 0px;
font-size: 14px;
line-height: 150%;
}
small {
font-size: 12px;
line-height: 150%;
}
a {
color: @core-vibrant-blue;
font-weight: 700;
border-bottom: none;
}
.table td {
font-size: 14px;
line-height: 150%;
a {
cursor: pointer;
display: block;
}
}
.table thead th {
border-top: none;
border-bottom: none;
border-right: 1px solid #e2e4ea;
vertical-align: middle;
}
th.sortable {
cursor: pointer;
}
.table th, .table td {
padding: 0.5rem 1rem;
white-space: nowrap;
}
.table thead {
tr:first-child {
th {
border-top: none;
background-color: rgba(0, 43, 128, 0.0235294);
border-right: none;
}
th:first-child {
border-top-left-radius: 8px;
}
th:last-child {
border-top-right-radius: 8px;
}
}
}
.table tbody {
color: #515774;
border-radius: 8px;
td {
max-height: 48px;
height: 48px;
padding-left: 16px;
padding-right: 16px;
border-top: 1px solid @border-lt-gray;
position: relative;
}
tr {
td:last-child {
border-right: none;
}
}
tr:last-child {
td:first-child {
border-bottom-left-radius: 8px;
}
td:last-child {
border-bottom-right-radius: 8px;
}
}
}
.sort-arrows {
height: 14px;
padding-left: 0.5rem;
display: inline-flex;
flex-direction: column;
justify-content: space-between;
span {
display: flex;
align-items: center;
gap: 3px;
}
.ascending-arrow {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 6px solid #c5c7d1;
}
.descending-arrow {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #c5c7d1;
}
}
.ascending .ascending-arrow {
border-bottom-color: #6a67fe;
}
.descending .descending-arrow {
border-top-color: #6a67fe;
}
.table td {
white-space: nowrap;
vertical-align: middle;
}
.affected-teams-link {
color: #515774;
cursor: pointer;
font-weight: 400;
}
.truncated-affected-teams {
color: @core-vibrant-blue;
cursor: pointer;
position: relative;
font-weight: 400;
.teams-tooltip {
display: none;
}
}
.truncated-affected-teams:hover {
text-decoration: underline;
.teams-tooltip {
z-index: 3;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
padding: 6px;
width: 250px;
background: #515774;
border-radius: 4px;
position: absolute;
top: 15px;
left: -30px;
color: #FFF;
p {
white-space: normal;
margin-bottom: 4px;
cursor: pointer;
color: #FFF;
&:hover {
text-decoration: underline;
}
}
}
}
.pointer {
cursor: pointer;
}
.advanced-options-btn {
color: #6A67FE;
font-family: Inter;
font-size: 14px;
font-weight: 700;
line-height: 21px;
cursor: pointer;
img {
height: 16px;
}
}
.rotate {
transform: rotate(180deg);
}
[purpose='software-upload-input'] {
display: flex;
padding: 32px;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 16px;
align-self: stretch;
border-radius: 4px;
border: 1px solid #E2E4EA;
background: #F9FAFC;
}
[parasails-component='file-upload'] {
&.is-invalid {
border: 1px solid #dc3545;
}
}
.is-invalid ~ .invalid-feedback {
display: block;
}
[purpose='software-information'] {
[purpose='edited-software-information'] {
width: 100%;
}
display: flex;
// flex-direction: column;
// justify-content: center;
// align-items: start;
align-items: center;
gap: 16px;
position: relative;
border-radius: 4px;
padding: 16px 24px;
height: 72px;
border: 1px solid #E2E4EA;
background: #F9FAFC;
margin-bottom: 24px;
img {
margin-right: 16px;
}
[purpose='edit-upload-btn'] {
position: absolute;
right: 24px;
bottom: 28%;
}
[purpose='file-selected-edit-upload-btn'] {
position: absolute;
right: -144px;
bottom: 28%;
}
}
[purpose='teams-picker'] {
padding: 16px 24px;
gap: 8px;
border-radius: 4px;
border: 1px solid #E2E4EA;
background: #F9FAFC;
margin-bottom: 24px;
&.is-invalid {
border: 1px solid #dc3545;
}
}
[purpose='delete-button'] {
border-radius: 6px;
background: #D66C7B;
border-color: #D66C7B;
color: #FFF;
}
[purpose='file-upload'] {
color: @core-vibrant-blue;
display: flex;
font-weight: 700;
font-size: 14px;
line-height: 150%;
margin-top: 24px;
cursor: pointer;
img {
margin-right: 8px;
}
}
[purpose='modal-button'] {
border-radius: 6px;
background: #6A67FE;
color: #FFF;
border-color: #6A67FE;
}
[purpose='modal-buttons'] {
[purpose='cancel-button'] {
color: @core-vibrant-blue;
margin-right: 16px;
cursor: pointer;
}
}
}

View file

@ -190,9 +190,9 @@ module.exports = {
***************************************************************************/
adapter: '@sailshq/connect-redis',
url: process.env.REDIS_TLS_URL,
tls: {
rejectUnauthorized: false
},
// tls: {
// rejectUnauthorized: false
// },
//--------------------------------------------------------------------------
// /\ OR, to avoid checking it in to version control, you might opt to
// || set sensitive credentials like this using an environment variable.
@ -414,6 +414,10 @@ module.exports = {
},
uploads: {
adapter: require('skipper-s3'),
}
};

View file

@ -15,7 +15,7 @@ module.exports.routes = {
// ╚╩╝╚═╝╚═╝╩ ╩ ╩╚═╝╚═╝╚═╝
'GET /profiles': { action: 'profiles/view-profiles' },
'GET /scripts': { action: 'scripts/view-scripts' },
'GET /software': { action: 'software/view-software' },
'GET /email/confirm': { action: 'entrance/confirm-email' },
'GET /email/confirmed': { action: 'entrance/view-confirmed-email' },
@ -62,4 +62,9 @@ module.exports.routes = {
'GET /download-script': { action: 'scripts/download-script' },
'POST /api/v1/upload-script': { action: 'scripts/upload-script' },
'POST /api/v1/edit-script': { action: 'scripts/edit-script' },
'GET /api/v1/get-software': { action: 'get-software' },
'GET /download-software': { action: 'software/download-software' },
'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' },
};

View file

@ -8,13 +8,15 @@
"@sailshq/connect-redis": "^6.1.3",
"@sailshq/lodash": "^3.10.6",
"@sailshq/socket.io-redis": "^6.1.2",
"axios": "1.7.7",
"sails": "^1.5.11",
"sails-hook-apianalytics": "^2.0.6",
"sails-hook-organics": "^2.2.2",
"sails-hook-orm": "^4.0.3",
"sails-hook-sockets": "^3.0.0",
"sails-hook-uploads": "^0.4.3",
"sails-postgresql": "^5.0.1"
"sails-postgresql": "^5.0.1",
"skipper-s3": "^0.6.0"
},
"devDependencies": {
"eslint": "5.16.0",

View file

@ -0,0 +1,464 @@
module.exports = {
friendlyName: 'Test file transfers',
description: '',
fn: async function () {
var WritableStream = require('stream').Writable;
let { Readable } = require('stream');
let axios = require('axios');
sails.log('copying file Fleet instance » Fleet instance (other team)');
let softwareApiUrl = `${sails.config.custom.fleetBaseUrl}/api/v1/fleet/software/titles/7/package?alt=media&team_id=2`;
await sails.cp(softwareApiUrl, {
adapter: () => {
return {
ls: undefined,
rm: undefined,
receive: undefined,
read: (softwareApiUrl) => {
// Create a readable stream
const readable = new Readable({
read() {
// Empty _read method; we'll handle data pushing with events below
}
});
// Now we'll fetch the data asynchronously and pipe it into the readable stream
(async () => {
try {
const streamResponse = await axios({
url: softwareApiUrl,
method: 'GET',
responseType: 'stream',
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
},
});
console.log('Received stream from API, piping data...');
// Pipe data from the response stream into the readable stream
streamResponse.data.on('data', (chunk) => {
const canContinue = readable.push(chunk);
if (!canContinue) {
streamResponse.data.pause(); // Pause if we can't push more data
}
});
// Resume the stream when readable is ready
readable.on('drain', () => {
streamResponse.data.resume();
});
// When the source stream ends, we signal end of the readable stream
streamResponse.data.on('end', () => {
readable.push(null); // Signal end of stream
});
// Handle any errors from the source stream
streamResponse.data.on('error', (err) => {
readable.emit('error', err); // Propagate the error to the readable stream
});
} catch (error) {
console.error('Error during read operation:', error);
readable.emit('error', new Error('Failed to download file: ' + error.message));
}
})();
return readable;
},
};
},
},
{
adapter: ()=>{
return {
ls: undefined,
rm: undefined,
read: undefined,
receive: (unusedOpts)=>{
// This `_write` method is invoked each time a new file is received
// from the Readable stream (Upstream) which is pumping filestreams
// into this receiver. (filename === `__newFile.filename`).
var receiver__ = WritableStream({ objectMode: true });
// Create a new drain (writable stream) to send through the individual bytes of this file.
receiver__._write = (__newFile, encoding, doneWithThisFile)=>{
let axios = require('axios');
let FormData = require('form-data');
let form = new FormData();
form.append('team_id', 0);
form.append('software', __newFile, {
filename: 'test.exe',
contentType: 'application/octet-stream'
});
(async ()=>{
try {
await axios.post(`${sails.config.custom.fleetBaseUrl}/api/v1/fleet/software/package`, form, {
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
...form.getHeaders()
},
});
} catch(error){
throw new Error('Failed to upload file:'+ require('util').inspect(error, {depth: null}));
}
})()
.then(()=>{
console.log('ok supposedly a file is finished uploading');
doneWithThisFile();
})
.catch((err)=>{
doneWithThisFile(err);
});
};//ƒ
return receiver__;
}
};
},
});
let software = await UndeployedSoftware.find();
let uploadedSoftware = software[0];
sails.log(`Uploading file S3 » Fleet instance`);
await sails.cp(uploadedSoftware.fd, {}, {
adapter: ()=>{
return {
ls: undefined,
rm: undefined,
read: undefined,
receive: (unusedOpts)=>{
// This `_write` method is invoked each time a new file is received
// from the Readable stream (Upstream) which is pumping filestreams
// into this receiver. (filename === `__newFile.filename`).
var receiver__ = WritableStream({ objectMode: true });
// Create a new drain (writable stream) to send through the individual bytes of this file.
receiver__._write = (__newFile, encoding, doneWithThisFile)=>{
let axios = require('axios');
let FormData = require('form-data');
let form = new FormData();
form.append('team_id', 0);
form.append('software', __newFile, {
filename: uploadedSoftware.filename,
contentType: 'application/octet-stream'
});
(async ()=>{
try {
await axios.post(`${sails.config.custom.fleetBaseUrl}/api/v1/fleet/software/package`, form, {
headers: {
Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
...form.getHeaders()
},
});
} catch(error){
throw new Error('Failed to upload file:'+ require('util').inspect(error, {depth: null}));
}
})()
.then(()=>{
console.log('ok supposedly a file is finished uploading');
doneWithThisFile();
})
.catch((err)=>{
doneWithThisFile(err);
});
};//ƒ
return receiver__;
}
};
}
});
// Uses both things
// await sails.cp(uploadedFile.fd, {
// adapter: ()=>{
// return {
// ls: undefined,
// rm: undefined,
// receive: undefined,
// read: (dowloadApiUrl) => {
// let stream = new require('stream').PassThrough(); // Create a PassThrough stream (readable)
// (async () => {
// try {
// let response = await sails.helpers.http.getStream.with({
// url: dowloadApiUrl,
// headers: {
// Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
// },
// });
// if (response && typeof response.pipe === 'function') {
// console.log('piping this response')
// response.pipe(stream); // Pipe the response stream to the PassThrough stream
// } else {
// // stream.emit('error', new Error('No valid stream returned from the API.'));
// }
// } catch (error) {
// throw new Error(error);
// // stream.emit('error', new Error('Failed to download file: ' + require('util').inspect(error, { depth: null })));
// }
// })();
// return stream; // Return the PassThrough readable stream immediately to `sails.cp()`
// }
// }
// },
// },
// {
// adapter: ()=>{
// return {
// ls: undefined,
// rm: undefined,
// read: undefined,
// receive: (unusedOpts)=>{
// // This `_write` method is invoked each time a new file is received
// // from the Readable stream (Upstream) which is pumping filestreams
// // into this receiver. (filename === `__newFile.filename`).
// var receiver__ = new WritableStream({ objectMode: true });
// // Create a new drain (writable stream) to send through the individual bytes of this file.
// receiver__._write = (__newFile, encoding, doneWithThisFile)=>{
// let axios = require('axios');
// let FormData = require('form-data');
// let form = new FormData();
// form.append('team_id', team);
// form.append('software', __newFile, {
// filename: software.name,
// contentType: 'application/octet-stream'
// });
// (async ()=>{
// try {
// await axios.post(`${sails.config.custom.fleetBaseUrl}/api/v1/fleet/software/package`, form, {
// headers: {
// Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
// ...form.getHeaders()
// },
// })
// } catch(error){
// throw new Error('Failed to upload file:'+ require('util').inspect(error, {depth: null}));
// }
// })()
// .then(()=>{
// console.log('ok supposedly a file is finished uploading');
// doneWithThisFile();
// })
// .catch((err)=>{
// doneWithThisFile(err);
// });
// };//ƒ
// return receiver__;
// }
// }
// },
// });
// await sails.cp(softwareApiUrl, {
// adapter: ()=>{
// return {
// ls: undefined,
// rm: undefined,
// receive: undefined,
// read: (softwareApiUrl) => {
// (async ()=>{
// try {
// return await sails.helpers.http.getStream.with({
// url: softwareApiUrl,
// headers: {
// Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
// },
// });
// } catch(error){
// throw new Error('Failed to download file:'+ require('util').inspect(error, {depth: null}));
// }
// })()
// .then(()=>{
// console.log('ok supposedly a file is finished downloading');
// })
// .catch((err)=>{
// throw new Error(err)
// });
// },
// }
// },
// },
// {
// adapter: ()=>{
// return {
// ls: undefined,
// rm: undefined,
// read: undefined,
// receive: (unusedOpts)=>{
// // This `_write` method is invoked each time a new file is received
// // from the Readable stream (Upstream) which is pumping filestreams
// // into this receiver. (filename === `__newFile.filename`).
// var receiver__ = WritableStream({ objectMode: true });
// // Create a new drain (writable stream) to send through the individual bytes of this file.
// receiver__._write = (__newFile, encoding, doneWithThisFile)=>{
// let axios = require('axios');
// let FormData = require('form-data');
// let form = new FormData();
// form.append('team_id', 0);
// form.append('software', __newFile, {
// filename: 'test.exe',
// contentType: 'application/octet-stream'
// });
// (async ()=>{
// try {
// await axios.post(`${sails.config.custom.fleetBaseUrl}/api/v1/fleet/software/package`, form, {
// headers: {
// Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
// ...form.getHeaders()
// },
// })
// } catch(error){
// throw new Error('Failed to upload file:'+ require('util').inspect(error, {depth: null}));
// }
// })()
// .then(()=>{
// console.log('ok supposedly a file is finished uploading');
// doneWithThisFile();
// })
// .catch((err)=>{
// doneWithThisFile(err);
// });
// };//ƒ
// return receiver__;
// }
// }
// },
// });
// // from Fleet instance to s3:
// let fi = await sails.cp(softwareApiUrl, {
// adapter: ()=>{
// return {
// ls: undefined,
// rm: undefined,
// read: (softwareApiUrl) => {
// // Create a Readable stream
// let readableStream = new Readable({
// read(size) {
// // Make an async call to get data from the software API
// (async () => {
// try {
// let response = await axios.get(softwareApiUrl, {
// responseType: 'stream', // Ensure the response is a stream
// headers: {
// Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
// }
// });
// // Pipe the response data into this stream
// response.data.on('data', (chunk) => {
// this.push(chunk); // Push each chunk into the readable stream
// });
// response.data.on('end', () => {
// this.push(null); // Signal end of stream
// });
// } catch (error) {
// }
// })();
// }
// });
// return readableStream; // Return the readable stream
// },
// }
// },
// })
// console.log(fi);
// let downloading = await sails.helpers.http.getStream.with({
// url: `${sails.config.custom.fleetBaseUrl}/api/v1/fleet/software/titles/4/package?alt=media&team_id=3`,
// headers: {
// Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
// },
// });
// await sails.upload(downloading, {
// adapter: ()=>{
// return {
// ls: undefined,
// rm: undefined,
// read: undefined,
// receive: (unusedOpts)=>{
// // This `_write` method is invoked each time a new file is received
// // from the Readable stream (Upstream) which is pumping filestreams
// // into this receiver. (filename === `__newFile.filename`).
// var receiver__ = WritableStream({ objectMode: true });
// // Create a new drain (writable stream) to send through the individual bytes of this file.
// receiver__._write = (__newFile, encoding, doneWithThisFile)=>{
// // var newFileDrain__ = fsx.createWriteStream(`${sails.config.appPath}/assets/foobar.fake`, encoding);
// let axios = require('axios');
// let FormData = require('form-data');
// let form = new FormData();
// form.append('team_id', 1);
// form.append('software', __newFile, {
// filename: 'foo.exe',
// contentType: 'application/octet-stream'
// });
// (async ()=>{
// try {
// // await sails.helpers.http.sendHttpRequest.with({
// // method: 'POST',
// // baseUrl: sails.config.custom.fleetBaseUrl,
// // url: `/api/v1/fleet/software/package?team_id=2`,
// // enctype: 'multipart/form-data',
// // body: form,
// // headers: {
// // Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
// // ...form.getHeaders()
// // },
// // });
// await axios.post(`${sails.config.custom.fleetBaseUrl}/api/v1/fleet/software/package`, form, {
// headers: {
// Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
// ...form.getHeaders()
// },
// })
// } catch(error){
// throw new Error('Failed to upload file:'+ require('util').inspect(error, {depth: null}));
// }
// })()
// .then(()=>{
// console.log('ok supposedly a file is finished uploading');
// doneWithThisFile();
// // newFileDrain__.on('finish', ()=>{
// // receiver__.emit('writefile', __newFile);// Indicate that a file was persisted.
// // console.log('ok supposedly a file is finished uploading');
// // doneWithThisFile();
// // });
// // __newFile.pipe(newFileDrain__);
// })
// .catch((err)=>{
// doneWithThisFile(err);
// });
// };//ƒ
// return receiver__;
// }
// }
// }
// });
// console.log('ok supposedly everything is now uploaded');
}
};

View file

@ -55,6 +55,7 @@
<!-- LOGGED-IN NAVIGATION -->
<a class="nav-item nav-link mr-3 d-flex align-items-center" href="/profiles">Configuration profiles</a>
<a class="nav-item nav-link ml-3 mr-3 d-flex align-items-center" href="/scripts">Scripts</a>
<a class="nav-item nav-link ml-3 mr-3 d-flex align-items-center" href="/software">Software</a>
<% if(me) { %>
<a class="nav-item nav-link ml-3 d-flex align-items-center" href="/logout">Sign out</a>
<% } else { %>
@ -125,12 +126,21 @@
<script src="/dependencies/jquery.min.js"></script>
<script src="/dependencies/vue.js"></script>
<script src="/dependencies/vue-router.js"></script>
<script src="/dependencies/ace-editor/ace.js"></script>
<script src="/dependencies/ace-editor/mode-fleet-highlight-rules.js"></script>
<script src="/dependencies/ace-editor/mode-fleet.js"></script>
<script src="/dependencies/ace-editor/mode-powershell.js"></script>
<script src="/dependencies/ace-editor/mode-sh.js"></script>
<script src="/dependencies/ace-editor/mode-sql.js"></script>
<script src="/dependencies/ace-editor/theme-fleet.js"></script>
<script src="/dependencies/ace-editor/worker-base.js"></script>
<script src="/dependencies/bootstrap-4/bootstrap-4.bundle.js"></script>
<script src="/dependencies/cloud.js"></script>
<script src="/dependencies/moment.js"></script>
<script src="/dependencies/parasails.js"></script>
<script src="/js/cloud.setup.js"></script>
<script src="/js/components/account-notification-banner.component.js"></script>
<script src="/js/components/ace-editor.component.js"></script>
<script src="/js/components/ajax-button.component.js"></script>
<script src="/js/components/ajax-form.component.js"></script>
<script src="/js/components/cloud-error.component.js"></script>
@ -157,6 +167,7 @@
<script src="/js/pages/legal/terms.page.js"></script>
<script src="/js/pages/profiles.page.js"></script>
<script src="/js/pages/scripts.page.js"></script>
<script src="/js/pages/software/software.page.js"></script>
<!--SCRIPTS END-->
<% /* Display an overlay if the current browser is not supported.

View file

@ -0,0 +1,200 @@
<div id="software" v-cloak>
<div purpose="page-content" class="container-fluid">
<div class="d-flex flex-column">
<div class="pb-4 d-flex flex-row justify-content-between">
<div>
<h3 purpose="page-heading" @click="clickChangeTeamFilter(9)">Software</h3>
</div>
<div class="d-flex flex-row align-items-center">
<p class="mb-0 mr-2">Team:</p>
<select class="custom-select team-select" v-model="teamFilter" @change="changeTeamFilter()">
<option :value="undefined" selected>All teams</option>
<option v-for="team of teams" :value="team.fleetApid">{{team.teamName}}</option>
</select>
</div>
</div>
</div>
<div class="d-flex flex-row justify-content-between pb-3">
<div>
<p><strong>Software</strong></p>
</div>
<div>
<p style="color: #6A67FE; cursor: pointer;" @click="clickOpenAddSoftwareModal()">+ Add software</p>
</div>
</div>
<div style="overflow-y: visible;" class="mb-4 border rounded table-responsive-md" v-if="softwareToDisplay.length > 0">
<table class="table my-0">
<thead>
<tr>
<th class="sortable" :class="sortDirection === 'ASC' ? 'ascending' : sortDirection === 'DESC' ? 'descending' : ''" @click="clickChangeSortDirection()">
<div style="cursor: pointer;" class="d-flex flex-row align-items-center pointer">
<small><strong>Name</strong></small>
<div class="sort-arrows">
<span class="ascending-arrow"></span>
<span class="descending-arrow"></span>
</div>
</div>
</th>
<th>
<small><strong>Platform</strong></small>
</th>
<th v-if="teamFilter === undefined">
<span><small><strong>Team</strong></small></span>
</th>
<th>
<span><small><strong>Upload date</strong></small></span>
</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="software in softwareToDisplay" :key="software.id">
<td class="name-column">{{software.name}}</td>
<td>{{software.platform}}</td>
<td v-if="teamFilter === undefined">
<a class="affected-teams-link" v-if="software.teams && software.teams.length > 0">
<span class="truncated-affected-teams" v-if="software.teams.length > 1">
{{software.teams.length}} teams
<div class="teams-tooltip">
<p v-for="team in software.teams" @click="clickChangeTeamFilter(team.fleetApid)">{{team.teamName}}</p>
</div>
</span>
<span v-else @click="clickChangeTeamFilter(software.teams[0].fleetApid)">
{{software.teams[0].teamName}}
</span>
</a>
<span v-else>---</span>
</td>
<td>
<js-timestamp :at="software.createdAt" format="timeago"></js-timestamp>
</td>
<td>
<div class="d-flex flex-row align-items-start justify-content-end">
<img style="height: 16px; margin-right: 24px;" alt="download" class="pointer" src="/images/download-16x16@2x.png" @click="clickDownloadSoftware(software)">
<img style="height: 16px; margin-right: 24px;" alt="edit" class="pointer" src="/images/edit-pencil-16x21@2x.png" @click="clickOpenEditModal(software)">
<img style="height: 16px" alt="delete" class="pointer" src="/images/delete-16x21@2x.png" @click="clickOpenDeleteModal(software)">
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else>
<h3 class="px-3 text-center mx-auto pt-5">No software matching the selected filters were found.</h3>
</div>
</div>
<modal v-if="modal === 'edit-software'" hide-close-button="true" @close="closeModal()">
<div>
<div class="d-flex flex-row justify-content-between">
<h3 class="mb-4">Edit software</h3>
<div class="pointer" @click="closeModal()">&times;</div>
</div>
<ajax-form :handle-submitting="handleSubmittingEditSoftwareForm" :syncing.sync="syncing" :cloud-error.sync="cloudError" :form-errors.sync="formErrors" :form-data="formData" :form-rules="editSoftwareFormRules" @submitted="submittedForm()">
<div purpose="software-information">
<div class="d-flex flex-row justify-content-start" >
<div class="d-flex" >
<img style="height: 40px; width: 34px;" alt="macOs and Linux software" src="/images/icon-software-34x40@2x.png">
<div class="d-flex flex-column">
<p v-if="!formData.newSoftware"><strong>{{softwareToEdit.name}}</strong></p>
<p v-else><strong>{{newSoftwareFilename}}</strong></p>
<p class="muted">{{softwareToEdit.platform}}</p>
</div>
</div>
<file-upload purpose="edit-upload-btn" :uploaded-filename.sync="newSoftwareFilename" id="edit-file-upload" mode="software-pencil" :disabled="syncing" v-model="formData.newSoftware"></file-upload>
</div>
</div>
<a class="advanced-options-btn d-flex flex-row align-items-center" @click="showAdvancedOptions = !showAdvancedOptions">Advanced options <img class="d-inline ml-2" src="/images/icon-chevron-down-32x32@2x.png" alt="chevron" :class="[showAdvancedOptions ? 'rotate' : '']" ></a>
<div v-if="showAdvancedOptions">
<div class="my-4">
<p class="mb-2"><strong>Pre-install query</strong></p>
<ace-editor v-model="formData.preInstallQuery" mode="fleet" :value="formData.preInstallQuery"></ace-editor>
<p><small>Software will be installed only if the <a href="https://fleetdm.com/tables/account_policy_data" target="_blank">query returns results <img style="display: inline; height: 12px;" alt="external link" src="/images/icon-external-link-12x12@2x.png"></a></small></p>
</div>
<div class="my-4" v-if="softwareToEdit.platform !== 'Windows'">
<p class="mb-2"><strong>Install script</strong></p>
<ace-editor v-model="formData.installScript" mode="sh" :value="formData.installScript"></ace-editor>
<p><small>Use the $INSTALLER_PATH variable to point to the installer. Currently, Shell scripts are supported.</small></p>
</div>
<div class="my-4" v-else>
<p class="mb-2"><strong>Install script</strong></p>
<ace-editor v-model="formData.installScript" mode="powershell" :value="formData.installScript"></ace-editor>
<p><small>Use the $INSTALLER_PATH variable to point to the installer. Currently, Powershell scripts are supported.</small></p>
</div>
<div class="my-4" v-if="softwareToEdit.platform !== 'Windows'">
<p class="mb-2"><strong>Post-install script</strong></p>
<ace-editor v-model="formData.postInstallScript" mode="sh" :value="formData.postInstallScript"></ace-editor>
<p><small>Currently, Shell scripts are supported.</small></p>
</div>
<div class="my-4" v-else>
<p class="mb-2"><strong>Post-install script</strong></p>
<ace-editor v-model="formData.postInstallScript" mode="powershell" :value="formData.postInstallScript"></ace-editor>
<p><small>Currently, Powershell scripts are supported.</small></p>
</div>
<div class="my-4" v-if="softwareToEdit.platform !== 'Windows'">
<p class="mb-2"><strong>Uninstall script</strong></p>
<ace-editor v-model="formData.uninstallScript" mode="sh" :value="formData.uninstallScript"></ace-editor>
<p><small>Currently, Shell scripts are supported.</small></p>
</div>
<div class="my-4" v-else>
<p class="mb-2"><strong>Uninstall script</strong></p>
<ace-editor v-model="formData.uninstallScript" mode="powershell" :value="formData.uninstallScript"></ace-editor>
<p><small>Currently, Powershell scripts are supported.</small></p>
</div>
</div>
<div purpose="teams-picker" class="mt-4">
<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>
<cloud-error v-if="cloudError && cloudError.exit === 'wrongInstallerExtension'">{{cloudError.responseInfo.body}}</cloud-error>
<cloud-error v-else-if="cloudError"></cloud-error>
<div purpose="modal-buttons" class="d-flex flex-row justify-content-end align-items-center">
<ajax-button :syncing.sync="syncing" purpose="modal-button" type="submit">Save</ajax-button>
</div>
</ajax-form>
</div>
</modal>
<%// ╔╦╗╔═╗╦ ╔═╗╔╦╗╔═╗
// ║║║╣ ║ ║╣ ║ ║╣
// ═╩╝╚═╝╩═╝╚═╝ ╩ ╚═╝ %>
<modal v-if="modal === 'delete-software'" hide-close-button="true" @close="closeModal()">
<div class="d-flex flex-row justify-content-between">
<h3 class="mb-4">Delete software</h3>
<div class="pointer" @click="closeModal()">&times;</div>
</div>
<p>{{formData.software.name}} will be removed from your library.</p>
<ajax-form :handle-submitting="handleSubmittingDeleteSoftwareForm" :syncing.sync="syncing" :cloud-error.sync="cloudError" :form-errors.sync="formErrors" :form-data="formData" :form-rules="editSoftwareFormRules" @submitted="submittedForm()">
<cloud-error v-if="cloudError"></cloud-error>
<div class="d-flex flex-row justify-content-end align-items-center">
<a class="mr-3" style="color: #D66C7B; cursor: pointer;" @click="closeModal()">Cancel</a>
<ajax-button class="btn" purpose="delete-button" :syncing.sync="syncing">Delete</ajax-button>
</div>
</ajax-form>
</modal>
<%// ╔═╗╔╦╗╔╦╗
// ╠═╣ ║║ ║║
// ╩ ╩═╩╝═╩╝ %>
<modal v-if="modal === 'add-software'" hide-close-button="true" @close="closeModal()">
<div>
<div class="d-flex flex-row justify-content-between">
<h3 class="mb-4">Add software</h3>
<div class="pointer" @click="closeModal()">&times;</div>
</div>
<ajax-form :handle-submitting="handleSubmittingAddSoftwareForm" :syncing.sync="syncing" :cloud-error.sync="cloudError" :form-errors.sync="formErrors" :form-data="formData" :form-rules="addSoftwareFormRules" @submitted="submittedForm()">
<file-upload id="file-upload" :class="[formErrors.newSoftware ? 'is-invalid' : '']" mode="software" :disabled="syncing" v-model="formData.newSoftware"></file-upload>
<div class="invalid-feedback text-center" v-if="formErrors.newSoftware">Please upload a new software.</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="invalid-feedback text-center" v-if="formErrors.teams">Please select the teams you want to deploy this software to.</div>
<cloud-error v-if="cloudError && cloudError === 'softwareWithThisNameAlreadyExists'">A software with the same name as the uploaded software already exists on one or more of the selected teams.</cloud-error>
<cloud-error v-else-if="cloudError"></cloud-error>
<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.newSoftware" type="submit">Add</ajax-button>
</div>
</ajax-form>
</div>
</modal>
</div>
<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %>