Initial
This commit is contained in:
1 parent
56e34475fc
commit
6c6514e70f
4 files changed
+97
-27
No files matched your search
+82
-24
@@ -216,6 +216,7 @@
|
|||||||
:credentials="credentials"
|
:credentials="credentials"
|
||||||
:job="scriptTestJob"
|
:job="scriptTestJob"
|
||||||
:running="scriptTestRunning"
|
:running="scriptTestRunning"
|
||||||
|
:error="scriptTestError"
|
||||||
@close="closeScriptTestRun"
|
@close="closeScriptTestRun"
|
||||||
@execute="executeScriptTestRun"
|
@execute="executeScriptTestRun"
|
||||||
/>
|
/>
|
||||||
@@ -952,6 +953,7 @@ const scriptTestModalOpen = ref(false);
|
|||||||
const scriptTestTarget = ref(null);
|
const scriptTestTarget = ref(null);
|
||||||
const scriptTestJob = ref(null);
|
const scriptTestJob = ref(null);
|
||||||
const scriptTestRunning = ref(false);
|
const scriptTestRunning = ref(false);
|
||||||
|
const scriptTestError = ref('');
|
||||||
const userModalOpen = ref(false);
|
const userModalOpen = ref(false);
|
||||||
const groupModalOpen = ref(false);
|
const groupModalOpen = ref(false);
|
||||||
const hostModalOpen = ref(false);
|
const hostModalOpen = ref(false);
|
||||||
@@ -1113,6 +1115,7 @@ const selectedScript = ref(null);
|
|||||||
const editorMount = ref(null);
|
const editorMount = ref(null);
|
||||||
let editor;
|
let editor;
|
||||||
let scriptTestPollTimer = null;
|
let scriptTestPollTimer = null;
|
||||||
|
let scriptTestPollFailures = 0;
|
||||||
|
|
||||||
const loginForm = reactive({ email: 'admin@posh.local', password: 'change-me-now' });
|
const loginForm = reactive({ email: 'admin@posh.local', password: 'change-me-now' });
|
||||||
const scriptDraft = reactive({ id: null, folderId: null, name: '', description: '', content: '', visibility: 'personal', groupId: null });
|
const scriptDraft = reactive({ id: null, folderId: null, name: '', description: '', content: '', visibility: 'personal', groupId: null });
|
||||||
@@ -1370,10 +1373,11 @@ const filteredRunPlans = computed(() => filterRows(runplans.value, runPlanFilter
|
|||||||
const runPlanPageCount = computed(() => pageCount(filteredRunPlans.value, runPlanFilters));
|
const runPlanPageCount = computed(() => pageCount(filteredRunPlans.value, runPlanFilters));
|
||||||
const pagedRunPlans = computed(() => pageRows(filteredRunPlans.value, runPlanFilters));
|
const pagedRunPlans = computed(() => pageRows(filteredRunPlans.value, runPlanFilters));
|
||||||
|
|
||||||
watch(view, async () => {
|
watch(view, async (nextView, previousView) => {
|
||||||
contextMenu.open = false;
|
contextMenu.open = false;
|
||||||
if (view.value === 'scripts') await ensureEditorReady();
|
if (previousView === 'scripts' && nextView !== 'scripts') disposeEditor();
|
||||||
if (view.value === 'profile') hydrateProfileForm();
|
if (nextView === 'scripts') await ensureEditorReady();
|
||||||
|
if (nextView === 'profile') hydrateProfileForm();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => scriptDraft.content, (value) => {
|
watch(() => scriptDraft.content, (value) => {
|
||||||
@@ -1452,7 +1456,7 @@ async function probeEntraLogin() {
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('click', closeContext);
|
window.removeEventListener('click', closeContext);
|
||||||
stopScriptTestPolling();
|
stopScriptTestPolling();
|
||||||
editor?.dispose();
|
disposeEditor();
|
||||||
});
|
});
|
||||||
|
|
||||||
function filterRows(rows, filters, fields) {
|
function filterRows(rows, filters, fields) {
|
||||||
@@ -1520,32 +1524,49 @@ function toggleNav() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setEditorMount(el) {
|
function setEditorMount(el) {
|
||||||
if (el) {
|
if (!el) {
|
||||||
|
editorMount.value = null;
|
||||||
|
disposeEditor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (editorMount.value && editorMount.value !== el) {
|
||||||
|
disposeEditor();
|
||||||
|
}
|
||||||
editorMount.value = el;
|
editorMount.value = el;
|
||||||
if (view.value === 'scripts') void ensureEditorReady();
|
if (view.value === 'scripts') void ensureEditorReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function safeRefresh(label, promise, fallback, failures, quiet = false) {
|
||||||
|
try {
|
||||||
|
return await promise;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`POSHManager refresh failed for ${label}`, err);
|
||||||
|
if (!quiet) failures.push(`${label}: ${err.message || 'request failed'}`);
|
||||||
|
return typeof fallback === 'function' ? fallback() : fallback;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshAll() {
|
async function refreshAll() {
|
||||||
// Bootstrap all API-backed view state together so navigation does not trigger partial reload races.
|
// Bootstrap all API-backed view state together so navigation does not trigger partial reload races.
|
||||||
|
const refreshFailures = [];
|
||||||
const [meResult, boot, userRows, groupRows, credentialRows, hostRows, folderRows, scriptRows, assetFolderRows, assetRows, variableRows, psadtCatalogRows, psadtProfileRows, psadtIntuneRows, graphRows, runplanRows, jobRows] = await Promise.all([
|
const [meResult, boot, userRows, groupRows, credentialRows, hostRows, folderRows, scriptRows, assetFolderRows, assetRows, variableRows, psadtCatalogRows, psadtProfileRows, psadtIntuneRows, graphRows, runplanRows, jobRows] = await Promise.all([
|
||||||
api.get('/api/auth/me'),
|
api.get('/api/auth/me'),
|
||||||
api.get('/api/bootstrap'),
|
safeRefresh('bootstrap', api.get('/api/bootstrap'), () => ({ summary: summary.value, settings: settings.value }), refreshFailures),
|
||||||
api.get('/api/users').catch(() => []),
|
safeRefresh('users', api.get('/api/users'), () => users.value, refreshFailures, true),
|
||||||
api.get('/api/groups'),
|
safeRefresh('groups', api.get('/api/groups'), () => groups.value, refreshFailures),
|
||||||
api.get('/api/credentials'),
|
safeRefresh('credentials', api.get('/api/credentials'), () => credentials.value, refreshFailures),
|
||||||
api.get('/api/hosts'),
|
safeRefresh('hosts', api.get('/api/hosts'), () => hosts.value, refreshFailures),
|
||||||
api.get('/api/folders'),
|
safeRefresh('folders', api.get('/api/folders'), () => folders.value, refreshFailures),
|
||||||
api.get('/api/scripts'),
|
safeRefresh('scripts', api.get('/api/scripts'), () => scripts.value, refreshFailures),
|
||||||
api.get('/api/assets/folders'),
|
safeRefresh('asset folders', api.get('/api/assets/folders'), () => assetFolders.value, refreshFailures),
|
||||||
api.get('/api/assets'),
|
safeRefresh('assets', api.get('/api/assets'), () => assets.value, refreshFailures),
|
||||||
api.get('/api/variables/catalog'),
|
safeRefresh('variables', api.get('/api/variables/catalog'), () => variableCatalog.value, refreshFailures),
|
||||||
api.get('/api/psadt/catalog'),
|
safeRefresh('PSADT catalog', api.get('/api/psadt/catalog'), () => psadtCatalog.value, refreshFailures),
|
||||||
api.get('/api/psadt/profiles'),
|
safeRefresh('PSADT profiles', api.get('/api/psadt/profiles'), () => psadtProfiles.value, refreshFailures),
|
||||||
api.get('/api/psadt/intune/deployments'),
|
safeRefresh('Intune deployments', api.get('/api/psadt/intune/deployments'), () => psadtIntuneDeployments.value, refreshFailures),
|
||||||
api.get('/api/graph/connections'),
|
safeRefresh('Graph connections', api.get('/api/graph/connections'), () => graphConnections.value, refreshFailures),
|
||||||
api.get('/api/runplans'),
|
safeRefresh('RunPlans', api.get('/api/runplans'), () => runplans.value, refreshFailures),
|
||||||
api.get('/api/jobs')
|
safeRefresh('jobs', api.get('/api/jobs'), () => jobs.value, refreshFailures)
|
||||||
]);
|
]);
|
||||||
syncUser(meResult.user);
|
syncUser(meResult.user);
|
||||||
summary.value = boot.summary;
|
summary.value = boot.summary;
|
||||||
@@ -1567,7 +1588,13 @@ async function refreshAll() {
|
|||||||
graphConnections.value = graphRows;
|
graphConnections.value = graphRows;
|
||||||
runplans.value = runplanRows;
|
runplans.value = runplanRows;
|
||||||
jobs.value = jobRows;
|
jobs.value = jobRows;
|
||||||
if (!selectedScript.value && scriptRows.length) selectScript(scriptRows[0]);
|
if (selectedScript.value) {
|
||||||
|
const refreshedSelection = scriptRows.find((script) => script.id === selectedScript.value.id);
|
||||||
|
if (refreshedSelection) selectScript(refreshedSelection);
|
||||||
|
} else if (scriptRows.length) {
|
||||||
|
selectScript(scriptRows[0]);
|
||||||
|
}
|
||||||
|
if (refreshFailures.length) notify(`Some data did not refresh: ${refreshFailures.slice(0, 2).join('; ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openHelpCenter() {
|
async function openHelpCenter() {
|
||||||
@@ -1740,6 +1767,8 @@ function openScriptTestRun(script) {
|
|||||||
scriptTestTarget.value = script;
|
scriptTestTarget.value = script;
|
||||||
scriptTestJob.value = null;
|
scriptTestJob.value = null;
|
||||||
scriptTestRunning.value = false;
|
scriptTestRunning.value = false;
|
||||||
|
scriptTestError.value = '';
|
||||||
|
scriptTestPollFailures = 0;
|
||||||
scriptTestModalOpen.value = true;
|
scriptTestModalOpen.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1748,6 +1777,8 @@ function closeScriptTestRun() {
|
|||||||
scriptTestTarget.value = null;
|
scriptTestTarget.value = null;
|
||||||
scriptTestJob.value = null;
|
scriptTestJob.value = null;
|
||||||
scriptTestRunning.value = false;
|
scriptTestRunning.value = false;
|
||||||
|
scriptTestError.value = '';
|
||||||
|
scriptTestPollFailures = 0;
|
||||||
stopScriptTestPolling();
|
stopScriptTestPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1755,6 +1786,8 @@ async function executeScriptTestRun(payload) {
|
|||||||
if (!scriptTestTarget.value) return;
|
if (!scriptTestTarget.value) return;
|
||||||
scriptTestRunning.value = true;
|
scriptTestRunning.value = true;
|
||||||
scriptTestJob.value = null;
|
scriptTestJob.value = null;
|
||||||
|
scriptTestError.value = '';
|
||||||
|
scriptTestPollFailures = 0;
|
||||||
stopScriptTestPolling();
|
stopScriptTestPolling();
|
||||||
try {
|
try {
|
||||||
const job = await api.post(`/api/scripts/${scriptTestTarget.value.id}/test-run`, payload);
|
const job = await api.post(`/api/scripts/${scriptTestTarget.value.id}/test-run`, payload);
|
||||||
@@ -1763,6 +1796,13 @@ async function executeScriptTestRun(payload) {
|
|||||||
notify('Script test run started');
|
notify('Script test run started');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
scriptTestRunning.value = false;
|
scriptTestRunning.value = false;
|
||||||
|
scriptTestError.value = err.message;
|
||||||
|
scriptTestJob.value = {
|
||||||
|
id: 'not-started',
|
||||||
|
scriptName: scriptTestTarget.value?.name,
|
||||||
|
status: 'failed',
|
||||||
|
logs: [{ stream: 'stderr', line: err.message, createdAt: new Date().toISOString() }]
|
||||||
|
};
|
||||||
notify(err.message);
|
notify(err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1772,15 +1812,21 @@ function startScriptTestPolling(jobId) {
|
|||||||
scriptTestPollTimer = window.setInterval(async () => {
|
scriptTestPollTimer = window.setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const job = await api.get(`/api/jobs/${jobId}`);
|
const job = await api.get(`/api/jobs/${jobId}`);
|
||||||
|
scriptTestPollFailures = 0;
|
||||||
|
scriptTestError.value = '';
|
||||||
scriptTestJob.value = job;
|
scriptTestJob.value = job;
|
||||||
if (!['queued', 'running'].includes(job.status)) {
|
if (!['queued', 'running'].includes(job.status)) {
|
||||||
scriptTestRunning.value = false;
|
scriptTestRunning.value = false;
|
||||||
stopScriptTestPolling();
|
stopScriptTestPolling();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
scriptTestPollFailures += 1;
|
||||||
|
scriptTestError.value = `Could not refresh test output (${scriptTestPollFailures}/3): ${err.message}`;
|
||||||
|
if (scriptTestPollFailures >= 3) {
|
||||||
scriptTestRunning.value = false;
|
scriptTestRunning.value = false;
|
||||||
stopScriptTestPolling();
|
stopScriptTestPolling();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2406,10 +2452,22 @@ async function ensureEditorReady(attempt = 0) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!editor) mountEditor();
|
if (!editor) mountEditor();
|
||||||
editor?.layout();
|
if (editor) {
|
||||||
|
editor.setValue(scriptDraft.content || '');
|
||||||
|
editor.layout({ width: Math.floor(box.width), height: Math.floor(box.height) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disposeEditor() {
|
||||||
|
if (editor) {
|
||||||
|
scriptDraft.content = editor.getValue();
|
||||||
|
editor.dispose();
|
||||||
|
editor = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mountEditor() {
|
function mountEditor() {
|
||||||
|
if (!editorMount.value) return;
|
||||||
configurePowerShellMonaco(monaco, () => ({
|
configurePowerShellMonaco(monaco, () => ({
|
||||||
variableCatalog: variableCatalog.value,
|
variableCatalog: variableCatalog.value,
|
||||||
psadtCatalog: psadtCatalog.value,
|
psadtCatalog: psadtCatalog.value,
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ const props = defineProps({
|
|||||||
hosts: { type: Array, default: () => [] },
|
hosts: { type: Array, default: () => [] },
|
||||||
credentials: { type: Array, default: () => [] },
|
credentials: { type: Array, default: () => [] },
|
||||||
job: { type: Object, default: null },
|
job: { type: Object, default: null },
|
||||||
running: Boolean
|
running: Boolean,
|
||||||
|
error: { type: String, default: '' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'execute']);
|
const emit = defineEmits(['close', 'execute']);
|
||||||
@@ -95,6 +96,7 @@ const terminalRows = computed(() => {
|
|||||||
`Status: ${props.job.status || 'queued'}`,
|
`Status: ${props.job.status || 'queued'}`,
|
||||||
''
|
''
|
||||||
];
|
];
|
||||||
|
if (props.error) lines.push(`[${new Date().toISOString()}] [ERROR] ${props.error}`);
|
||||||
for (const log of props.job.logs || []) {
|
for (const log of props.job.logs || []) {
|
||||||
lines.push(`[${log.createdAt || ''}] [${String(log.stream || 'system').toUpperCase()}] [${log.hostName || selectedHost.value?.name || 'host'}] ${log.line}`);
|
lines.push(`[${log.createdAt || ''}] [${String(log.stream || 'system').toUpperCase()}] [${log.hostName || selectedHost.value?.name || 'host'}] ${log.line}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,11 +149,11 @@ async function runHost(jobId, script, host, executionContext) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = decryptSecret(host);
|
|
||||||
const wrapper = buildWrapper(script.content, host, executionContext);
|
|
||||||
const file = path.join(os.tmpdir(), `poshmanager-${jobId}-${host.id}.ps1`);
|
const file = path.join(os.tmpdir(), `poshmanager-${jobId}-${host.id}.ps1`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const secret = decryptSecret(host);
|
||||||
|
const wrapper = buildWrapper(script.content, host, executionContext);
|
||||||
await fs.writeFile(file, wrapper, { mode: 0o600 });
|
await fs.writeFile(file, wrapper, { mode: 0o600 });
|
||||||
await spawnPowerShell(jobId, host, file, secret, executionContext);
|
await spawnPowerShell(jobId, host, file, secret, executionContext);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -20,12 +20,22 @@ db.prepare(`INSERT INTO runplans (id, name, script_id, visibility, owner_user_id
|
|||||||
const jobId = 'job_test_1';
|
const jobId = 'job_test_1';
|
||||||
db.prepare(`INSERT INTO jobs (id, runplan_id, script_id, status, triggered_by, created_at)
|
db.prepare(`INSERT INTO jobs (id, runplan_id, script_id, status, triggered_by, created_at)
|
||||||
VALUES (?, ?, ?, 'completed', ?, ?)`).run(jobId, runplanId, scriptId, owner, ts);
|
VALUES (?, ?, ?, 'completed', ?, ?)`).run(jobId, runplanId, scriptId, owner, ts);
|
||||||
|
const scriptTestJobId = 'job_script_test_1';
|
||||||
|
db.prepare(`INSERT INTO jobs (id, runplan_id, script_id, status, triggered_by, created_at)
|
||||||
|
VALUES (?, NULL, ?, 'failed', ?, ?)`).run(scriptTestJobId, scriptId, owner, ts);
|
||||||
|
|
||||||
test('the triggering operator can list and read their job', () => {
|
test('the triggering operator can list and read their job', () => {
|
||||||
assert.ok(listJobs(owner, false).some((j) => j.id === jobId));
|
assert.ok(listJobs(owner, false).some((j) => j.id === jobId));
|
||||||
assert.ok(getJob(jobId, owner, false));
|
assert.ok(getJob(jobId, owner, false));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('script test jobs without a RunPlan stay visible to their triggering operator', () => {
|
||||||
|
assert.ok(listJobs(owner, false).some((j) => j.id === scriptTestJobId));
|
||||||
|
const job = getJob(scriptTestJobId, owner, false);
|
||||||
|
assert.equal(job.id, scriptTestJobId);
|
||||||
|
assert.equal(job.runplanId, null);
|
||||||
|
});
|
||||||
|
|
||||||
test('an unrelated user cannot see a personal job', () => {
|
test('an unrelated user cannot see a personal job', () => {
|
||||||
assert.ok(!listJobs(stranger, false).some((j) => j.id === jobId));
|
assert.ok(!listJobs(stranger, false).some((j) => j.id === jobId));
|
||||||
assert.equal(getJob(jobId, stranger, false), null);
|
assert.equal(getJob(jobId, stranger, false), null);
|
||||||
|
|||||||
Reference in new issue
Block a user