mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
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:
parent
a2e7010ee2
commit
0da7afb332
36 changed files with 57046 additions and 10 deletions
|
|
@ -41,7 +41,8 @@
|
|||
// Models:
|
||||
"User": true,
|
||||
"UndeployedProfile": true,
|
||||
"UndeployedScript": true
|
||||
"UndeployedScript": true,
|
||||
"UndeployedSoftware": true
|
||||
|
||||
// …and any others.
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
|
|
|||
109
ee/bulk-operations-dashboard/api/controllers/get-software.js
Normal file
109
ee/bulk-operations-dashboard/api/controllers/get-software.js
Normal 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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -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};
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
},
|
||||
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
|
||||
|
||||
|
||||
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
|
||||
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
||||
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
||||
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -46,6 +46,7 @@
|
|||
"Vue": true,
|
||||
"VueRouter": true,
|
||||
"moment": true,
|
||||
"ace": true,
|
||||
// "google": true,
|
||||
// ...etc.
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
|
|
|||
21389
ee/bulk-operations-dashboard/assets/dependencies/ace-editor/ace.js
Normal file
21389
ee/bulk-operations-dashboard/assets/dependencies/ace-editor/ace.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
|
|
@ -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 |
|
|
@ -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 */
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 ,
|
||||
});
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
||||
},
|
||||
|
||||
// ╔═╗╦ ╦╔╗ ╦ ╦╔═╗ ╔╦╗╔═╗╔╦╗╦ ╦╔═╗╔╦╗╔═╗
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -30,7 +30,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
&.file-mode {
|
||||
.btn-and-tips-if-relevant {
|
||||
display: inline-block;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
464
ee/bulk-operations-dashboard/scripts/test-file-upload.js
Normal file
464
ee/bulk-operations-dashboard/scripts/test-file-upload.js
Normal 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');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
200
ee/bulk-operations-dashboard/views/pages/software/software.ejs
Normal file
200
ee/bulk-operations-dashboard/views/pages/software/software.ejs
Normal 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()">×</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()">×</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()">×</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() %>
|
||||
Loading…
Reference in a new issue