mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-11-02 16:43:38 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8743dcae0 | |||
| bd94382edb | |||
| 8abd3a8848 |
@@ -105,7 +105,7 @@ jobs:
|
||||
# the local install so the gate and the pre-commit run agree on the finding set.
|
||||
# Informational findings (template expansion of our own step outputs in pwsh) are
|
||||
# visible locally but do not fail the gate.
|
||||
- uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
|
||||
- uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
|
||||
with:
|
||||
version: 1.30.1
|
||||
min-severity: low
|
||||
|
||||
@@ -247,7 +247,7 @@ jobs:
|
||||
Get-ChildItem $outDir | ForEach-Object { Write-Host "$($_.Name): $([math]::Round($_.Length / 1MB, 2)) MB" }
|
||||
|
||||
- name: Upload unsigned build output
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: agent-unsigned
|
||||
path: ${{ runner.temp }}/agent-unsigned
|
||||
@@ -293,7 +293,7 @@ jobs:
|
||||
run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
|
||||
|
||||
- name: Download unsigned build output
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: agent-unsigned
|
||||
path: ${{ runner.temp }}/agent-unsigned
|
||||
@@ -790,13 +790,13 @@ jobs:
|
||||
staging/agent/AutopilotMonitor.SummaryDialog.exe
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: AutopilotMonitor-Agent-v${{ needs.reserve.outputs.version }}
|
||||
path: ${{ steps.artifact.outputs.versioned_zip_release }}
|
||||
|
||||
- name: Upload version metadata
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: version-metadata
|
||||
path: ${{ env.MANIFEST_NAME }}
|
||||
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
Write-Host "$env:RUNNER_SCRIPT : Valid, timestamped, signer $($sig.SignerCertificate.Subject.Split(',')[0])"
|
||||
|
||||
- name: Upload signed loader
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: bootstrap-loader-signed
|
||||
path: ${{ env.RUNNER_SCRIPT }}
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download signed loader
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: bootstrap-loader-signed
|
||||
path: ${{ runner.temp }}/loader
|
||||
@@ -187,7 +187,7 @@ jobs:
|
||||
Write-Host "Built $env:STABLE_MSI ($([math]::Round((Get-Item $env:STABLE_MSI).Length / 1KB)) KB)"
|
||||
|
||||
- name: Upload unsigned MSI
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: bootstrap-msi-unsigned
|
||||
path: ${{ env.STABLE_MSI }}
|
||||
@@ -215,7 +215,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download unsigned MSI
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: bootstrap-msi-unsigned
|
||||
path: ${{ runner.temp }}/msi
|
||||
@@ -310,7 +310,7 @@ jobs:
|
||||
"version=$version" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Download unsigned MSI
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: bootstrap-msi-unsigned
|
||||
path: ${{ runner.temp }}/msi
|
||||
@@ -442,7 +442,7 @@ jobs:
|
||||
subject-path: ${{ env.STABLE_MSI }}
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: AutopilotMonitor-Bootstrap-v${{ steps.version.outputs.version }}
|
||||
path: ${{ env.STABLE_MSI }}
|
||||
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
@@ -222,7 +222,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
@@ -276,7 +276,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -123,6 +123,6 @@ jobs:
|
||||
run: dotnet build AutopilotMonitor.sln --configuration Release
|
||||
|
||||
- name: Analyze
|
||||
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
category: /language:${{ matrix.language }}
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
# `git push` needs.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
run: tar -czf "$RUNNER_TEMP/backend-publish.tgz" -C publish .
|
||||
|
||||
- name: Upload publish output
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: backend-publish
|
||||
path: ${{ runner.temp }}/backend-publish.tgz
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
|
||||
|
||||
- name: Download publish output
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: backend-publish
|
||||
path: ${{ runner.temp }}/backend-artifact
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
id: docs
|
||||
run: echo "sha=$(git -C whats-new-docs rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
sha256sum "$archive"
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: portal-${{ github.sha }}
|
||||
path: ${{ steps.package.outputs.archive }}
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download build artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: portal-${{ github.sha }}
|
||||
path: ${{ runner.temp }}/portal
|
||||
@@ -215,7 +215,7 @@ jobs:
|
||||
run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
|
||||
|
||||
- name: Download build artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: portal-${{ github.sha }}
|
||||
path: ${{ runner.temp }}/portal
|
||||
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
# when someone wants to look at the file.
|
||||
- name: Upload signed scripts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: signed-scripts
|
||||
path: ${{ runner.temp }}/signed-scripts
|
||||
|
||||
+1
-1
Submodule internal updated: af17a42883...d41dbd1874
@@ -606,7 +606,9 @@ function EventRow({ event, showScriptOutput, prevEvent, clockDeltas }: { event:
|
||||
<div id={`event-${event.eventId}`} className="bg-gray-50 rounded-lg p-3 hover:bg-gray-100 transition-colors">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* flex-wrap: on a phone the event type, one long word, drops below the time and badges
|
||||
instead of pushing the whole page sideways. */}
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<span className="text-xs text-gray-500 font-mono">
|
||||
{(recordedAt ?? new Date(event.timestamp)).toLocaleTimeString()}
|
||||
{recordedAt && (
|
||||
@@ -632,9 +634,9 @@ function EventRow({ event, showScriptOutput, prevEvent, clockDeltas }: { event:
|
||||
⧖ Clamped
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm font-medium text-gray-900">{event.eventType}</span>
|
||||
<span className="text-sm font-medium text-gray-900 min-w-0 break-words">{event.eventType}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-600" title={event.message || undefined}>{shortenBuildHashInMessage(event.message)}</p>
|
||||
<p className="mt-1 text-sm text-gray-600 break-words" title={event.message || undefined}>{shortenBuildHashInMessage(event.message)}</p>
|
||||
{laterCorrection && (
|
||||
<p className="mt-0.5 text-xs text-gray-500">
|
||||
{laterCorrection === "foreign"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { partitionHistoricReplayEvents, type ReplayInputEvent } from "@/lib/hist
|
||||
import TruncatedLabel from "@/components/TruncatedLabel";
|
||||
import PendingAppRow from "@/components/PendingAppRow";
|
||||
import PhaseDivider from "@/components/PhaseDivider";
|
||||
import ProgressPanelHeader from "@/components/ProgressPanelHeader";
|
||||
import AssignmentPill from "@/components/AssignmentPill";
|
||||
import { userPhaseSplitIndex, type UserPhaseBoundary } from "@/lib/userPhaseBoundary";
|
||||
import { shouldSkipLowBytesTotal, shouldSkipNoActivity, hasByteActivity } from "@/lib/downloadProgressFilters";
|
||||
@@ -369,104 +370,98 @@ export default function DownloadProgress({ events, summaryStats, observedUntilMs
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-6 mb-6">
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center justify-between w-full text-left"
|
||||
>
|
||||
{/* flex-wrap + gap (not space-x): on narrow screens the pills wrap as whole units
|
||||
below the title instead of overflowing the viewport; whitespace-nowrap inherits
|
||||
so no pill breaks internally. */}
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 min-w-0 whitespace-nowrap">
|
||||
<ProgressPanelHeader
|
||||
title="Download Progress"
|
||||
expanded={expanded}
|
||||
onToggle={() => setExpanded(!expanded)}
|
||||
icon={
|
||||
<svg className="w-5 h-5 text-blue-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
<h2 className="text-lg font-semibold text-gray-900">Download Progress</h2>
|
||||
{totalDownloadable != null && downloadedCount != null ? (
|
||||
}
|
||||
>
|
||||
{totalDownloadable != null && downloadedCount != null ? (
|
||||
<span
|
||||
className="text-xs text-gray-400"
|
||||
title="Install assignments only — skipped apps and uninstall downloads are not counted here."
|
||||
>
|
||||
({downloadedCount} of {totalDownloadable} downloaded)
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">({downloads.length} {downloads.length === 1 ? 'download' : 'downloads'})</span>
|
||||
)}
|
||||
{totalDuration != null && (
|
||||
<span className="text-xs text-gray-400">
|
||||
— Total: {formatDuration(Math.floor(totalDuration / 1000))}
|
||||
</span>
|
||||
)}
|
||||
{totalP2P != null && (
|
||||
<span
|
||||
className="text-xs text-gray-400"
|
||||
title={`${formatBytes(totalP2P.peers)} from peers of ${formatBytes(totalP2P.total)} total`}
|
||||
>
|
||||
— P2P: {totalP2P.percent}%
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
|
||||
{activeCount > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-blue-100 text-blue-700 font-medium">
|
||||
{activeCount} active
|
||||
</span>
|
||||
)}
|
||||
{completedCount > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-green-100 text-green-700 font-medium">
|
||||
{completedCount} completed
|
||||
</span>
|
||||
)}
|
||||
{incompleteCount > 0 && (
|
||||
// Slate like the session-level Incomplete badge: unknown outcome, not a failure.
|
||||
<span
|
||||
className="text-xs text-gray-400"
|
||||
title="Install assignments only — skipped apps and uninstall downloads are not counted here."
|
||||
className="px-2 py-0.5 rounded-full bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 font-medium"
|
||||
title={frozenText.countTitle}
|
||||
>
|
||||
({downloadedCount} of {totalDownloadable} downloaded)
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">({downloads.length} {downloads.length === 1 ? 'download' : 'downloads'})</span>
|
||||
)}
|
||||
{totalDuration != null && (
|
||||
<span className="text-xs text-gray-400">
|
||||
— Total: {formatDuration(Math.floor(totalDuration / 1000))}
|
||||
{incompleteCount} {frozenText.countLabel}
|
||||
</span>
|
||||
)}
|
||||
{totalP2P != null && (
|
||||
{uninstallCount > 0 && (
|
||||
// Muted gray on purpose (dark-mode covered by the global gray overrides) —
|
||||
// it's context, not a state the user needs to act on.
|
||||
<span
|
||||
className="text-xs text-gray-400"
|
||||
title={`${formatBytes(totalP2P.peers)} from peers of ${formatBytes(totalP2P.total)} total`}
|
||||
className="px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 font-medium"
|
||||
title="Package content downloaded for uninstall assignments — Win32 uninstalls re-download the full package before removing the app."
|
||||
>
|
||||
— P2P: {totalP2P.percent}%
|
||||
{uninstallCount} uninstall
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
|
||||
{activeCount > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-blue-100 text-blue-700 font-medium">
|
||||
{activeCount} active
|
||||
</span>
|
||||
)}
|
||||
{completedCount > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-green-100 text-green-700 font-medium">
|
||||
{completedCount} completed
|
||||
</span>
|
||||
)}
|
||||
{incompleteCount > 0 && (
|
||||
// Slate like the session-level Incomplete badge: unknown outcome, not a failure.
|
||||
<span
|
||||
className="px-2 py-0.5 rounded-full bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 font-medium"
|
||||
title={frozenText.countTitle}
|
||||
>
|
||||
{incompleteCount} {frozenText.countLabel}
|
||||
</span>
|
||||
)}
|
||||
{uninstallCount > 0 && (
|
||||
// Muted gray on purpose (dark-mode covered by the global gray overrides) —
|
||||
// it's context, not a state the user needs to act on.
|
||||
<span
|
||||
className="px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 font-medium"
|
||||
title="Package content downloaded for uninstall assignments — Win32 uninstalls re-download the full package before removing the app."
|
||||
>
|
||||
{uninstallCount} uninstall
|
||||
</span>
|
||||
)}
|
||||
{skippedCount > 0 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowSkipped(!showSkipped); }}
|
||||
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showSkipped ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
|
||||
title={showSkipped ? "Hide skipped apps" : "Show skipped apps"}
|
||||
>
|
||||
{skippedCount} skipped {showSkipped ? "▾" : "▸"}
|
||||
</button>
|
||||
)}
|
||||
{noDownloadCount > 0 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowNoDownload(!showNoDownload); }}
|
||||
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showNoDownload ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
|
||||
title="Assignments the management extension reported without downloading any package content — typically Store/WinGet uninstalls and skips."
|
||||
>
|
||||
{noDownloadCount} no download {showNoDownload ? "▾" : "▸"}
|
||||
</button>
|
||||
)}
|
||||
{pendingNames.length > 0 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowPending(!showPending); }}
|
||||
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showPending ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
|
||||
title="Assigned to this device but not picked up by the management extension yet — no download activity has been observed."
|
||||
>
|
||||
{pendingNames.length} pending {showPending ? "▾" : "▸"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{skippedCount > 0 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowSkipped(!showSkipped); }}
|
||||
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showSkipped ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
|
||||
title={showSkipped ? "Hide skipped apps" : "Show skipped apps"}
|
||||
>
|
||||
{skippedCount} skipped {showSkipped ? "▾" : "▸"}
|
||||
</button>
|
||||
)}
|
||||
{noDownloadCount > 0 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowNoDownload(!showNoDownload); }}
|
||||
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showNoDownload ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
|
||||
title="Assignments the management extension reported without downloading any package content — typically Store/WinGet uninstalls and skips."
|
||||
>
|
||||
{noDownloadCount} no download {showNoDownload ? "▾" : "▸"}
|
||||
</button>
|
||||
)}
|
||||
{pendingNames.length > 0 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowPending(!showPending); }}
|
||||
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showPending ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
|
||||
title="Assigned to this device but not picked up by the management extension yet — no download activity has been observed."
|
||||
>
|
||||
{pendingNames.length} pending {showPending ? "▾" : "▸"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<svg className={`w-5 h-5 text-gray-400 flex-shrink-0 transition-transform duration-200 ${expanded ? 'rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</ProgressPanelHeader>
|
||||
|
||||
{expanded && <div className="space-y-3 mt-4">
|
||||
{userPhaseBoundary && userSplit > 0 && <PhaseDivider boundary={userPhaseBoundary} side="before" />}
|
||||
|
||||
@@ -9,6 +9,7 @@ import TruncatedLabel from "@/components/TruncatedLabel";
|
||||
import { InstallSourcePill } from "@/components/InstallSourcePill";
|
||||
import PendingAppRow from "@/components/PendingAppRow";
|
||||
import PhaseDivider from "@/components/PhaseDivider";
|
||||
import ProgressPanelHeader from "@/components/ProgressPanelHeader";
|
||||
import AssignmentPill from "@/components/AssignmentPill";
|
||||
import { userPhaseSplitIndex, type UserPhaseBoundary } from "@/lib/userPhaseBoundary";
|
||||
|
||||
@@ -158,100 +159,94 @@ export default function InstallProgress({ events, summaryStats, observedUntilMs
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-6 mb-6">
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center justify-between w-full text-left"
|
||||
>
|
||||
{/* flex-wrap + gap (not space-x): on narrow screens the pills wrap as whole units
|
||||
below the title instead of overflowing the viewport; whitespace-nowrap inherits
|
||||
so no pill breaks internally. */}
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 min-w-0 whitespace-nowrap">
|
||||
<ProgressPanelHeader
|
||||
title="Install Progress"
|
||||
expanded={expanded}
|
||||
onToggle={() => setExpanded(!expanded)}
|
||||
icon={
|
||||
<svg className="w-5 h-5 text-indigo-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||
</svg>
|
||||
<h2 className="text-lg font-semibold text-gray-900">Install Progress</h2>
|
||||
{totalFromSummary != null && installedFromSummary != null && (
|
||||
<span className="text-xs text-gray-400">
|
||||
({installedFromSummary} of {totalFromSummary} installed)
|
||||
}
|
||||
>
|
||||
{totalFromSummary != null && installedFromSummary != null && (
|
||||
<span className="text-xs text-gray-400">
|
||||
({installedFromSummary} of {totalFromSummary} installed)
|
||||
</span>
|
||||
)}
|
||||
{totalDuration != null && (
|
||||
<span className="text-xs text-gray-400">
|
||||
— Total: {formatDuration(totalDuration)}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
|
||||
{activeCount > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-indigo-100 text-indigo-700 font-medium">
|
||||
{activeCount} active
|
||||
</span>
|
||||
)}
|
||||
{totalDuration != null && (
|
||||
<span className="text-xs text-gray-400">
|
||||
— Total: {formatDuration(totalDuration)}
|
||||
{completedCount > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-green-100 text-green-700 font-medium">
|
||||
{completedCount} completed
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
|
||||
{activeCount > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-indigo-100 text-indigo-700 font-medium">
|
||||
{activeCount} active
|
||||
</span>
|
||||
)}
|
||||
{completedCount > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-green-100 text-green-700 font-medium">
|
||||
{completedCount} completed
|
||||
</span>
|
||||
)}
|
||||
{uninstalledCount > 0 && (
|
||||
// Deliberately muted (gray family, dark-mode covered by the global overrides) —
|
||||
// a saturated pill here pulls the eye to what is a successful non-event.
|
||||
<span
|
||||
className="px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 font-medium"
|
||||
title="Uninstall assignments enforced during this enrollment — the app was removed, not installed."
|
||||
>
|
||||
{uninstalledCount} uninstalled
|
||||
</span>
|
||||
)}
|
||||
{failedCount > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-red-100 text-red-700 font-medium">
|
||||
{failedCount} failed
|
||||
</span>
|
||||
)}
|
||||
{likelyStuckCount > 0 && (
|
||||
<span
|
||||
className="px-2 py-0.5 rounded-full bg-orange-100 text-orange-700 font-medium"
|
||||
title="ESP timed out while these apps were still installing — final status couldn't be confirmed."
|
||||
>
|
||||
{likelyStuckCount} likely stuck
|
||||
</span>
|
||||
)}
|
||||
{incompleteCount > 0 && (
|
||||
// Slate like the session-level Incomplete badge: unknown outcome, not a failure.
|
||||
<span
|
||||
className="px-2 py-0.5 rounded-full bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 font-medium"
|
||||
title={frozenText.countTitle}
|
||||
>
|
||||
{incompleteCount} {frozenText.countLabel}
|
||||
</span>
|
||||
)}
|
||||
{postponedCount > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 font-medium">
|
||||
{postponedCount} postponed
|
||||
</span>
|
||||
)}
|
||||
{skippedCount > 0 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowSkipped(!showSkipped); }}
|
||||
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showSkipped ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
|
||||
title={showSkipped ? "Hide skipped apps" : "Show skipped apps"}
|
||||
>
|
||||
{skippedCount} skipped {showSkipped ? "▾" : "▸"}
|
||||
</button>
|
||||
)}
|
||||
{pendingNames.length > 0 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowPending(!showPending); }}
|
||||
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showPending ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
|
||||
title="Assigned to this device but not picked up by the management extension yet — no install activity has been observed."
|
||||
>
|
||||
{pendingNames.length} pending {showPending ? "▾" : "▸"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{uninstalledCount > 0 && (
|
||||
// Deliberately muted (gray family, dark-mode covered by the global overrides) —
|
||||
// a saturated pill here pulls the eye to what is a successful non-event.
|
||||
<span
|
||||
className="px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 font-medium"
|
||||
title="Uninstall assignments enforced during this enrollment — the app was removed, not installed."
|
||||
>
|
||||
{uninstalledCount} uninstalled
|
||||
</span>
|
||||
)}
|
||||
{failedCount > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-red-100 text-red-700 font-medium">
|
||||
{failedCount} failed
|
||||
</span>
|
||||
)}
|
||||
{likelyStuckCount > 0 && (
|
||||
<span
|
||||
className="px-2 py-0.5 rounded-full bg-orange-100 text-orange-700 font-medium"
|
||||
title="ESP timed out while these apps were still installing — final status couldn't be confirmed."
|
||||
>
|
||||
{likelyStuckCount} likely stuck
|
||||
</span>
|
||||
)}
|
||||
{incompleteCount > 0 && (
|
||||
// Slate like the session-level Incomplete badge: unknown outcome, not a failure.
|
||||
<span
|
||||
className="px-2 py-0.5 rounded-full bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 font-medium"
|
||||
title={frozenText.countTitle}
|
||||
>
|
||||
{incompleteCount} {frozenText.countLabel}
|
||||
</span>
|
||||
)}
|
||||
{postponedCount > 0 && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 font-medium">
|
||||
{postponedCount} postponed
|
||||
</span>
|
||||
)}
|
||||
{skippedCount > 0 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowSkipped(!showSkipped); }}
|
||||
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showSkipped ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
|
||||
title={showSkipped ? "Hide skipped apps" : "Show skipped apps"}
|
||||
>
|
||||
{skippedCount} skipped {showSkipped ? "▾" : "▸"}
|
||||
</button>
|
||||
)}
|
||||
{pendingNames.length > 0 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowPending(!showPending); }}
|
||||
className={`px-2 py-0.5 rounded-full font-medium transition-colors ${showPending ? "bg-gray-200 text-gray-700" : "bg-gray-100 text-gray-400"}`}
|
||||
title="Assigned to this device but not picked up by the management extension yet — no install activity has been observed."
|
||||
>
|
||||
{pendingNames.length} pending {showPending ? "▾" : "▸"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<svg className={`w-5 h-5 text-gray-400 flex-shrink-0 transition-transform duration-200 ${expanded ? 'rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</ProgressPanelHeader>
|
||||
|
||||
{expanded && <div className="space-y-3 mt-4">
|
||||
{userPhaseBoundary && userSplit > 0 && <PhaseDivider boundary={userPhaseBoundary} side="before" />}
|
||||
@@ -538,7 +533,7 @@ function InstallItemRow({ item, agentSilent }: { item: InstallItem; agentSilent:
|
||||
);
|
||||
})()}
|
||||
{item.isError && item.errorDetail && !item.isLikelyStuck && (
|
||||
<div className="text-xs text-red-600">{item.errorDetail}</div>
|
||||
<div className="text-xs text-red-600 break-words">{item.errorDetail}</div>
|
||||
)}
|
||||
{item.isLikelyStuck && (
|
||||
<div className="text-xs text-orange-700">
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface ProgressPanelHeaderProps {
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
expanded: boolean;
|
||||
onToggle: () => void;
|
||||
// Summary text and pills after the title. May hold buttons of their own (the skipped and
|
||||
// pending toggles), which call stopPropagation so they do not also collapse the panel.
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapsible head of the Download and Install panels. A plain div takes the mouse click so the
|
||||
* whole head stays a click target, and only the chevron is a real button: the pills hold buttons,
|
||||
* and a button inside a button is invalid HTML that hides the inner ones from screen readers.
|
||||
*/
|
||||
export default function ProgressPanelHeader({ title, icon, expanded, onToggle, children }: ProgressPanelHeaderProps) {
|
||||
return (
|
||||
<div onClick={onToggle} className="flex items-center justify-between w-full text-left cursor-pointer select-none">
|
||||
{/* flex-wrap + gap (not space-x): on narrow screens the pills wrap as whole units
|
||||
below the title instead of overflowing the viewport; whitespace-nowrap inherits
|
||||
so no pill breaks internally. */}
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 min-w-0 whitespace-nowrap">
|
||||
{icon}
|
||||
<h2 className="text-lg font-semibold text-gray-900">{title}</h2>
|
||||
{children}
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onToggle(); }}
|
||||
aria-expanded={expanded}
|
||||
aria-label={title}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<svg className={`w-5 h-5 text-gray-400 flex-shrink-0 transition-transform duration-200 ${expanded ? 'rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import ProgressPanelHeader from "../ProgressPanelHeader";
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
function render(expanded: boolean) {
|
||||
return renderToStaticMarkup(
|
||||
createElement(
|
||||
ProgressPanelHeader,
|
||||
{ title: "Install Progress", icon: createElement("svg"), expanded, onToggle: noop },
|
||||
createElement("button", null, "3 skipped"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Walks the markup and reports whether a button ever opens while another one is still open.
|
||||
function hasNestedButton(html: string): boolean {
|
||||
let depth = 0;
|
||||
for (const [tag] of html.matchAll(/<\/?button\b/g)) {
|
||||
depth += tag === "<button" ? 1 : -1;
|
||||
if (depth > 1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
describe("ProgressPanelHeader", () => {
|
||||
it("keeps the pill buttons and the panel toggle as siblings, never nested", () => {
|
||||
const html = render(true);
|
||||
expect(html.match(/<button\b/g)?.length).toBe(2);
|
||||
expect(hasNestedButton(html)).toBe(false);
|
||||
});
|
||||
|
||||
it("names the toggle after the panel and reports its state", () => {
|
||||
expect(render(true)).toContain('aria-expanded="true" aria-label="Install Progress"');
|
||||
expect(render(false)).toContain('aria-expanded="false" aria-label="Install Progress"');
|
||||
});
|
||||
|
||||
it("detects a nested button", () => {
|
||||
expect(hasNestedButton("<button><span><button></button></span></button>")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -95,36 +95,41 @@ export function MySubmissionsList({ kind, getAccessToken, refreshKey, overrideTe
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<button
|
||||
{/* The div takes the mouse click so the whole head stays a click target; the title and the
|
||||
filter are sibling buttons, because a button inside a button hides the inner one from
|
||||
the keyboard and from screen readers. */}
|
||||
<div
|
||||
onClick={() => setCollapsed((c) => !c)}
|
||||
className="w-full flex items-center justify-between px-4 py-3 text-left"
|
||||
className="w-full flex items-center justify-between px-4 py-3 text-left cursor-pointer select-none"
|
||||
>
|
||||
<span className="flex items-center gap-2 text-sm font-medium text-gray-900">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setCollapsed((c) => !c); }}
|
||||
aria-expanded={!collapsed}
|
||||
className="flex items-center gap-2 text-sm font-medium text-gray-900 text-left"
|
||||
>
|
||||
<svg className={`w-4 h-4 text-gray-400 transition-transform ${collapsed ? "" : "rotate-90"}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
Community submissions
|
||||
<span className="text-xs font-normal text-gray-500">({items.length}{pending > 0 ? `, ${pending} pending` : ""})</span>
|
||||
</span>
|
||||
</button>
|
||||
{!collapsed && hidden > 0 && (
|
||||
<span
|
||||
role="button"
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowAll(true); }}
|
||||
className="text-xs text-indigo-600 hover:text-indigo-800"
|
||||
>
|
||||
Show all ({hidden} more)
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{!collapsed && showAll && items.length > pending && (
|
||||
<span
|
||||
role="button"
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowAll(false); }}
|
||||
className="text-xs text-indigo-600 hover:text-indigo-800"
|
||||
>
|
||||
Pending only
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{!collapsed && visible.length === 0 && (
|
||||
<p className="border-t border-gray-100 px-4 py-2 text-xs text-gray-500">Nothing pending.</p>
|
||||
)}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"1d7bffc15ccf7436": 1,
|
||||
"29bbb90f9b9e306a": 1,
|
||||
"2a985532f2e52066": 1,
|
||||
"33829a769f2eacb9": 1,
|
||||
"376b94382a0f2fc3": 1,
|
||||
"391740d0bd84d73b": 1,
|
||||
"3a83e25903a9f78b": 1,
|
||||
|
||||
Reference in New Issue
Block a user