diff --git a/README.md b/README.md
index 97a756f..832cf9a 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,8 @@ A dark-mode desktop application for visualizing SQL Server Agent maintenance job
- **Drag-to-reschedule** — Scheduled jobs appear as hollow outlined bars. Drag one left or right to a new start time (snaps to 5-minute marks), confirm in the dialog, and the SQL Agent schedule is updated immediately via `msdb.dbo.sp_update_schedule` — no need to open SSMS. Saves significant DBA time when redistributing load across the day.
- **Multiple servers in parallel** — Queries all configured servers simultaneously. If one server is unreachable the others still load; a colour-coded dot in the toolbar shows each server's status.
- **Hover tooltips** — Every cell and bar shows server name, job type, start time, end time, and duration. For a **running job** the tooltip also shows live progress: spid, database name, command, and percent complete. Hovering a **row label** shows the actual T-SQL from the SQL Agent job step.
-- **Config file** — Servers and job definitions are read from `config.json` next to the exe. No recompile needed to add servers or jobs.
+- **Config file** — Servers and curated maintenance job definitions are read from `config.json` next to the exe. No recompile needed to add servers or curated jobs.
+- **Dynamic job discovery** — Turn on **Show all SQL Agent jobs** in the toolbar to include additional jobs found for the selected date or week without editing `config.json`.
## Jobs Visualized
@@ -32,7 +33,7 @@ Out of the box the app is configured for [Ola Hallengren's](https://ola.hallengr
| IndexOptimize | `IndexOptimize%` |
| IntegrityCheck | `DatabaseIntegrityCheck%` |
-Any SQL Server Agent job can be added by editing `config.json`.
+Curated jobs can be added by editing `config.json`. To spot overlaps with extra jobs without changing configuration, enable **Show all SQL Agent jobs** in the Week Overview or Day Detail toolbar.
## Requirements
@@ -68,10 +69,12 @@ Any SQL Server Agent job can be added by editing `config.json`.
}
```
- - `servers` — hostnames or IP addresses; Windows auth is used, no credentials in the file.
- - `sqlPattern` — SQL `LIKE` pattern matched against `msdb.dbo.sysjobs.name`.
- - `label` — display name used in tooltips.
- - `shortLabel` — abbreviated name shown in the row label panel.
+ - `servers` — hostnames or IP addresses; Windows auth is used, no credentials in the file.
+ - `sqlPattern` — SQL `LIKE` pattern matched against `msdb.dbo.sysjobs.name`.
+ - `label` — display name used in tooltips.
+ - `shortLabel` — abbreviated name shown in the row label panel.
+
+ Extra SQL Agent jobs do not need to be added here unless you want them shown by default as curated rows. Use **Show all SQL Agent jobs** to discover them dynamically per server for the selected date or week.
4. Restart the app after editing `config.json`.
diff --git a/src/SQLJobVisualizer/Controls/DayDetailControl.axaml b/src/SQLJobVisualizer/Controls/DayDetailControl.axaml
index 8bb3cec..df8e87e 100644
--- a/src/SQLJobVisualizer/Controls/DayDetailControl.axaml
+++ b/src/SQLJobVisualizer/Controls/DayDetailControl.axaml
@@ -16,6 +16,11 @@
Foreground="{DynamicResource ForegroundBrush}" FontSize="13"/>
+
_stepCommands =
new Dictionary<(string, string), string>();
private readonly DispatcherTimer _clockTimer = new() { Interval = TimeSpan.FromSeconds(1) };
@@ -41,6 +42,11 @@ public DayDetailControl()
DatePicker.SelectedDate = DateTime.Today;
DatePicker.SelectedDateChanged += (_, _) => _ = LoadAsync();
RefreshButton.Click += (_, _) => _ = LoadAsync();
+ ShowAllJobsCheckBox.IsCheckedChanged += (_, _) =>
+ {
+ _ = LoadStepCommandsAsync();
+ _ = LoadAsync();
+ };
_clockTimer.Tick += (_, _) => UpdateClock();
_clockTimer.Start();
@@ -63,7 +69,7 @@ protected override void OnLoaded(RoutedEventArgs e)
private async Task LoadStepCommandsAsync()
{
- _stepCommands = await _service.LoadJobStepCommandsAsync();
+ _stepCommands = await _service.LoadJobStepCommandsAsync(IsShowingAllJobs());
PopulateLabelPanel();
}
@@ -101,9 +107,9 @@ private void PopulateLabelPanel()
// Data rows — y matches HeaderH + rowIndex * RowH exactly
string? lastJob = null;
- for (int i = 0; i < ServerList.AllRows.Count; i++)
+ for (int i = 0; i < _rowCatalog.AllRows.Count; i++)
{
- var row = ServerList.AllRows[i];
+ var row = _rowCatalog.AllRows[i];
bool isGroupStart = row.JobLabel != lastJob;
lastJob = row.JobLabel;
@@ -150,7 +156,7 @@ private void PopulateLabelPanel()
LabelCanvas.Children.Add(border);
}
- LabelCanvas.Height = HeaderH + ServerList.AllRows.Count * RowH;
+ LabelCanvas.Height = HeaderH + _rowCatalog.AllRows.Count * RowH;
}
private string BuildRowTooltip(JobRow row)
@@ -192,12 +198,13 @@ private async Task LoadAsync()
try
{
var day = DatePicker.SelectedDate?.Date ?? DateTime.Today;
+ bool includeAllJobs = IsShowingAllJobs();
- var completedTask = _service.LoadDayAsync(day, ct);
+ var completedTask = _service.LoadDayAsync(day, includeAllJobs, ct);
var runningTask = day == DateTime.Today
- ? _service.LoadRunningAsync(ct)
+ ? _service.LoadRunningAsync(includeAllJobs, ct)
: Task.FromResult<(List, IReadOnlyList)>(([], []));
- var scheduledTask = _service.LoadScheduledAsync(day, ct);
+ var scheduledTask = _service.LoadScheduledAsync(day, includeAllJobs, ct);
await Task.WhenAll(completedTask, runningTask, scheduledTask);
if (ct.IsCancellationRequested) return;
@@ -216,7 +223,24 @@ private async Task LoadAsync()
.ToList();
var failedServers = failedCompleted.Concat(failedRunning).Distinct().ToList();
- var executions = BuildExecutions(merged, day);
+ _rowCatalog = includeAllJobs
+ ? JobRowCatalog.FromEntries(merged, scheduled)
+ : JobRowCatalog.FromConfigured();
+ PopulateLabelPanel();
+
+ var executions = BuildExecutions(merged, day, _rowCatalog, includeAllJobs);
+ var scheduledRows = scheduled
+ .Select(s => new ScheduledRun
+ {
+ ServerName = s.ServerName,
+ JobLabel = s.JobLabel,
+ RowIndex = _rowCatalog.GetRowIndex(s.ServerName, s.JobLabel),
+ ScheduledTime = s.ScheduledTime,
+ EstimatedDuration = s.EstimatedDuration,
+ ScheduleId = s.ScheduleId,
+ })
+ .Where(s => s.RowIndex >= 0)
+ .ToList();
// Average historical duration per row — used to size scheduled bars
var avgDurations = executions
@@ -225,7 +249,7 @@ private async Task LoadAsync()
.ToDictionary(g => g.Key,
g => TimeSpan.FromSeconds(g.Average(e => e.Duration.TotalSeconds)));
- RenderDay(executions, scheduled, avgDurations, day);
+ RenderDay(executions, scheduledRows, avgDurations, day, _rowCatalog);
UpdateServerStatus(failedServers);
}
catch (OperationCanceledException) { }
@@ -239,15 +263,17 @@ private async Task LoadAsync()
}
}
- private static List BuildExecutions(List entries, DateTime day)
+ private static List BuildExecutions(
+ List entries, DateTime day,
+ JobRowCatalog rowCatalog, bool includeAllJobs)
{
var list = new List();
foreach (var entry in entries)
{
- var jobLabel = JobParser.ParseJobLabel(entry.CommandType, entry.Command);
+ var jobLabel = JobParser.ParseJobLabel(entry.CommandType, entry.Command, includeAllJobs);
if (jobLabel is null) continue;
- int rowIdx = ServerList.GetRowIndex(entry.ServerName, jobLabel);
+ int rowIdx = rowCatalog.GetRowIndex(entry.ServerName, jobLabel);
if (rowIdx < 0) continue;
// Still-running jobs: use current time as provisional end time
@@ -255,7 +281,7 @@ private static List BuildExecutions(List entries,
list.Add(new JobExecution
{
- Row = ServerList.AllRows[rowIdx],
+ Row = rowCatalog.AllRows[rowIdx],
RowIndex = rowIdx,
StartTime = entry.StartTime,
EndTime = endTime,
@@ -268,13 +294,12 @@ private static List BuildExecutions(List entries,
}
private void RenderDay(List executions, List scheduled,
- Dictionary avgDurations, DateTime day)
+ Dictionary avgDurations, DateTime day,
+ JobRowCatalog rowCatalog)
{
DayCanvas.Children.Clear();
- int totalRows = ServerList.AllRows.Count;
- int jobCount = ServerList.JobLabels.Length;
- int srvCount = ServerList.ServerNames.Length;
+ int totalRows = rowCatalog.AllRows.Count;
double canvasH = HeaderH + totalRows * RowH;
DayCanvas.Width = CanvasW;
@@ -288,8 +313,13 @@ private void RenderDay(List executions, List schedul
// Alternating row group backgrounds
string[] groupBg = ["#1A1D23", "#1D2028"];
- for (int g = 0; g < jobCount; g++)
- AddRect(0, HeaderH + g * srvCount * RowH, CanvasW, srvCount * RowH, groupBg[g % 2]);
+ int groupIndex = 0;
+ foreach (var group in rowCatalog.GetGroups())
+ {
+ AddRect(0, HeaderH + group.StartIndex * RowH, CanvasW, group.RowCount * RowH,
+ groupBg[groupIndex % 2]);
+ groupIndex++;
+ }
// Hour header labels and vertical grid lines
for (int h = 0; h <= 24; h++)
@@ -317,8 +347,8 @@ private void RenderDay(List executions, List schedul
AddRect(0, HeaderH - 1, CanvasW, 1, "#3A3D45");
// Job-group separators
- for (int g = 1; g < jobCount; g++)
- AddRect(0, HeaderH + g * srvCount * RowH, CanvasW, 2, "#3A3D45");
+ foreach (var group in rowCatalog.GetGroups().Skip(1))
+ AddRect(0, HeaderH + group.StartIndex * RowH, CanvasW, 2, "#3A3D45");
// Job execution bars
foreach (var ex in executions)
@@ -505,6 +535,8 @@ private void UpdateServerStatus(IReadOnlyList failedServers)
}
}
+ private bool IsShowingAllJobs() => ShowAllJobsCheckBox.IsChecked == true;
+
private void OnDragStart(Rectangle bar, ScheduledRun run, PointerPressedEventArgs e)
{
if (_dragBar is not null) return;
diff --git a/src/SQLJobVisualizer/Controls/WeekMatrixControl.axaml b/src/SQLJobVisualizer/Controls/WeekMatrixControl.axaml
index 1d18210..8d438d9 100644
--- a/src/SQLJobVisualizer/Controls/WeekMatrixControl.axaml
+++ b/src/SQLJobVisualizer/Controls/WeekMatrixControl.axaml
@@ -20,6 +20,11 @@
TextAlignment="Center"/>
+
_stepCommands =
new Dictionary<(string, string), string>();
@@ -30,6 +31,11 @@ public WeekMatrixControl()
PrevWeekButton.Click += (_, _) => NavigateWeek(-1);
NextWeekButton.Click += (_, _) => NavigateWeek(1);
RefreshButton.Click += (_, _) => _ = LoadAsync();
+ ShowAllJobsCheckBox.IsCheckedChanged += (_, _) =>
+ {
+ _ = LoadStepCommandsAsync();
+ _ = LoadAsync();
+ };
}
protected override void OnLoaded(RoutedEventArgs e)
@@ -41,7 +47,7 @@ protected override void OnLoaded(RoutedEventArgs e)
private async Task LoadStepCommandsAsync()
{
- _stepCommands = await _service.LoadJobStepCommandsAsync();
+ _stepCommands = await _service.LoadJobStepCommandsAsync(IsShowingAllJobs());
PopulateLabelPanel();
}
@@ -82,9 +88,9 @@ private void PopulateLabelPanel()
// Data rows — y matches HeaderH + rowIndex * CellH exactly
string? lastJob = null;
- for (int i = 0; i < ServerList.AllRows.Count; i++)
+ for (int i = 0; i < _rowCatalog.AllRows.Count; i++)
{
- var row = ServerList.AllRows[i];
+ var row = _rowCatalog.AllRows[i];
bool isGroupStart = row.JobLabel != lastJob;
lastJob = row.JobLabel;
@@ -131,7 +137,7 @@ private void PopulateLabelPanel()
LabelCanvas.Children.Add(border);
}
- LabelCanvas.Height = HeaderH + ServerList.AllRows.Count * CellH;
+ LabelCanvas.Height = HeaderH + _rowCatalog.AllRows.Count * CellH;
}
private string BuildRowTooltip(JobRow row)
@@ -173,11 +179,17 @@ private async Task LoadAsync()
try
{
- var (entries, failedServers) = await _service.LoadWeekAsync(weekStart, ct);
+ bool includeAllJobs = IsShowingAllJobs();
+ var (entries, failedServers) = await _service.LoadWeekAsync(weekStart, includeAllJobs, ct);
if (ct.IsCancellationRequested) return;
- var slots = BuildSlots(entries, weekStart);
- RenderMatrix(slots, weekStart);
+ _rowCatalog = includeAllJobs
+ ? JobRowCatalog.FromEntries(entries)
+ : JobRowCatalog.FromConfigured();
+ PopulateLabelPanel();
+
+ var slots = BuildSlots(entries, weekStart, _rowCatalog, includeAllJobs);
+ RenderMatrix(slots, weekStart, _rowCatalog);
UpdateServerStatus(failedServers);
}
catch (OperationCanceledException) { }
@@ -191,15 +203,17 @@ private async Task LoadAsync()
}
}
- private static List BuildSlots(List entries, DateTime weekStart)
+ private static List BuildSlots(
+ List entries, DateTime weekStart,
+ JobRowCatalog rowCatalog, bool includeAllJobs)
{
var slots = new List();
foreach (var entry in entries)
{
- var jobLabel = JobParser.ParseJobLabel(entry.CommandType, entry.Command);
+ var jobLabel = JobParser.ParseJobLabel(entry.CommandType, entry.Command, includeAllJobs);
if (jobLabel is null) continue;
- int rowIdx = ServerList.GetRowIndex(entry.ServerName, jobLabel);
+ int rowIdx = rowCatalog.GetRowIndex(entry.ServerName, jobLabel);
if (rowIdx < 0) continue;
int startHour = (int)(entry.StartTime - weekStart).TotalHours;
@@ -215,7 +229,7 @@ private static List BuildSlots(List entries, DateTime
{
slots.Add(new JobSlot
{
- Row = ServerList.AllRows[rowIdx],
+ Row = rowCatalog.AllRows[rowIdx],
RowIndex = rowIdx,
HourOffset = h,
Success = success,
@@ -228,14 +242,12 @@ private static List BuildSlots(List entries, DateTime
return slots;
}
- private void RenderMatrix(List slots, DateTime weekStart)
+ private void RenderMatrix(List slots, DateTime weekStart, JobRowCatalog rowCatalog)
{
MatrixCanvas.Children.Clear();
const int totalCols = 168;
- int totalRows = ServerList.AllRows.Count;
- int jobCount = ServerList.JobLabels.Length;
- int srvCount = ServerList.ServerNames.Length;
+ int totalRows = rowCatalog.AllRows.Count;
double canvasW = totalCols * CellW;
double canvasH = HeaderH + totalRows * CellH;
@@ -250,8 +262,13 @@ private void RenderMatrix(List slots, DateTime weekStart)
// Alternating group backgrounds
string[] groupBg = ["#1A1D23", "#1D2028"];
- for (int g = 0; g < jobCount; g++)
- AddRect(0, HeaderH + g * srvCount * CellH, canvasW, srvCount * CellH, groupBg[g % 2]);
+ int groupIndex = 0;
+ foreach (var group in rowCatalog.GetGroups())
+ {
+ AddRect(0, HeaderH + group.StartIndex * CellH, canvasW, group.RowCount * CellH,
+ groupBg[groupIndex % 2]);
+ groupIndex++;
+ }
// Day column headers and separators
string[] dayNames = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
@@ -297,8 +314,8 @@ private void RenderMatrix(List slots, DateTime weekStart)
AddRect(0, HeaderH - 1, canvasW, 1, "#3A3D45");
// Job-group separators
- for (int g = 1; g < jobCount; g++)
- AddRect(0, HeaderH + g * srvCount * CellH, canvasW, 2, "#3A3D45");
+ foreach (var group in rowCatalog.GetGroups().Skip(1))
+ AddRect(0, HeaderH + group.StartIndex * CellH, canvasW, 2, "#3A3D45");
// Faint 6-hour grid lines inside days
for (int h = 6; h < totalCols; h += 6)
@@ -434,6 +451,8 @@ private void UpdateServerStatus(IReadOnlyList failedServers)
}
}
+ private bool IsShowingAllJobs() => ShowAllJobsCheckBox.IsChecked == true;
+
private static DateTime GetWeekMonday(DateTime date)
{
int diff = ((int)date.DayOfWeek - (int)DayOfWeek.Monday + 7) % 7;
diff --git a/src/SQLJobVisualizer/Models/JobRowCatalog.cs b/src/SQLJobVisualizer/Models/JobRowCatalog.cs
new file mode 100644
index 0000000..3c89716
--- /dev/null
+++ b/src/SQLJobVisualizer/Models/JobRowCatalog.cs
@@ -0,0 +1,87 @@
+using SQLJobVisualizer.Services;
+
+namespace SQLJobVisualizer.Models;
+
+public sealed class JobRowCatalog
+{
+ private readonly Dictionary<(string ServerName, string JobLabel), int> _rowIndexes;
+
+ public JobRowCatalog(IReadOnlyList rows)
+ {
+ AllRows = rows;
+ JobLabels = [.. rows.Select(r => r.JobLabel).Distinct()];
+ _rowIndexes = rows
+ .Select((row, index) => (row, index))
+ .ToDictionary(x => (x.row.ServerName, x.row.JobLabel), x => x.index);
+ }
+
+ public IReadOnlyList AllRows { get; }
+
+ public IReadOnlyList JobLabels { get; }
+
+ public int GetRowIndex(string serverName, string jobLabel) =>
+ _rowIndexes.TryGetValue((serverName, jobLabel), out var index) ? index : -1;
+
+ public IEnumerable<(int StartIndex, int RowCount)> GetGroups()
+ {
+ string? currentLabel = null;
+ int startIndex = 0;
+ int rowCount = 0;
+
+ for (int i = 0; i < AllRows.Count; i++)
+ {
+ var label = AllRows[i].JobLabel;
+ if (currentLabel is not null && label != currentLabel)
+ {
+ yield return (startIndex, rowCount);
+ startIndex = i;
+ rowCount = 0;
+ }
+
+ currentLabel = label;
+ rowCount++;
+ }
+
+ if (currentLabel is not null)
+ yield return (startIndex, rowCount);
+ }
+
+ public static JobRowCatalog FromConfigured() => new(ServerList.AllRows);
+
+ public static JobRowCatalog FromEntries(
+ IEnumerable entries,
+ IEnumerable? scheduled = null)
+ {
+ var discovered = new HashSet<(string ServerName, string JobLabel)>();
+
+ foreach (var entry in entries)
+ {
+ var jobLabel = JobParser.ParseJobLabel(entry.CommandType, entry.Command, includeUnconfigured: true);
+ if (jobLabel is not null)
+ discovered.Add((entry.ServerName, jobLabel));
+ }
+
+ if (scheduled is not null)
+ {
+ foreach (var run in scheduled)
+ discovered.Add((run.ServerName, run.JobLabel));
+ }
+
+ var configuredLabels = ServerList.Jobs.Select(j => j.Label).ToHashSet(StringComparer.OrdinalIgnoreCase);
+ var rows = new List(ServerList.AllRows);
+
+ var serverOrder = ServerList.ServerNames
+ .Select((server, index) => (server, index))
+ .ToDictionary(x => x.server, x => x.index, StringComparer.OrdinalIgnoreCase);
+
+ var extraRows = discovered
+ .Where(r => !configuredLabels.Contains(r.JobLabel))
+ .OrderBy(r => r.JobLabel, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(r => serverOrder.TryGetValue(r.ServerName, out var index) ? index : int.MaxValue)
+ .ThenBy(r => r.ServerName, StringComparer.OrdinalIgnoreCase)
+ .Select(r => new JobRow(r.ServerName, r.JobLabel, r.JobLabel));
+
+ rows.AddRange(extraRows);
+ return new JobRowCatalog(rows);
+ }
+}
diff --git a/src/SQLJobVisualizer/Services/CommandLogService.cs b/src/SQLJobVisualizer/Services/CommandLogService.cs
index 8df3fe9..1ca8d92 100644
--- a/src/SQLJobVisualizer/Services/CommandLogService.cs
+++ b/src/SQLJobVisualizer/Services/CommandLogService.cs
@@ -7,17 +7,19 @@ namespace SQLJobVisualizer.Services;
public sealed class CommandLogService
{
public async Task<(List Entries, IReadOnlyList FailedServers)>
- LoadWeekAsync(DateTime weekStart, CancellationToken ct = default) =>
+ LoadWeekAsync(DateTime weekStart, bool includeAllJobs = false, CancellationToken ct = default) =>
await QueryRangeAsync(
int.Parse(weekStart.ToString("yyyyMMdd")),
int.Parse(weekStart.AddDays(6).ToString("yyyyMMdd")),
+ includeAllJobs,
ct);
public async Task<(List Entries, IReadOnlyList FailedServers)>
- LoadDayAsync(DateTime day, CancellationToken ct = default) =>
+ LoadDayAsync(DateTime day, bool includeAllJobs = false, CancellationToken ct = default) =>
await QueryRangeAsync(
int.Parse(day.Date.ToString("yyyyMMdd")),
int.Parse(day.Date.ToString("yyyyMMdd")),
+ includeAllJobs,
ct);
public async Task RescheduleAsync(string serverName, int scheduleId, TimeSpan newTime,
@@ -35,17 +37,18 @@ public async Task RescheduleAsync(string serverName, int scheduleId, TimeSpan ne
await cmd.ExecuteNonQueryAsync(ct);
}
- public async Task> LoadScheduledAsync(DateTime day, CancellationToken ct = default)
+ public async Task> LoadScheduledAsync(
+ DateTime day, bool includeAllJobs = false, CancellationToken ct = default)
{
var tasks = ServerList.ServerNames
- .Select(s => QueryScheduledAsync(s, day, ct))
+ .Select(s => QueryScheduledAsync(s, day, includeAllJobs, ct))
.ToArray();
var results = await Task.WhenAll(tasks);
return results.SelectMany(r => r).ToList();
}
private static async Task> QueryScheduledAsync(
- string serverName, DateTime day, CancellationToken ct)
+ string serverName, DateTime day, bool includeAllJobs, CancellationToken ct)
{
var list = new List();
try
@@ -55,22 +58,20 @@ private static async Task> QueryScheduledAsync(
await using var cmd = conn.CreateCommand();
cmd.CommandTimeout = 10;
cmd.Parameters.AddWithValue("@Day", day.Date);
- cmd.CommandText = BuildScheduledQuery(cmd);
+ cmd.CommandText = BuildScheduledQuery(cmd, includeAllJobs);
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
{
var jobName = reader.GetString(0);
var scheduledTime = reader.GetDateTime(1);
var scheduleId = reader.IsDBNull(2) ? 0 : reader.GetInt32(2);
- var jobLabel = JobParser.ParseJobLabel(jobName, "");
+ var jobLabel = JobParser.ParseJobLabel(jobName, "", includeAllJobs);
if (jobLabel is null) continue;
- int rowIdx = ServerList.GetRowIndex(serverName, jobLabel);
- if (rowIdx < 0) continue;
list.Add(new ScheduledRun
{
ServerName = serverName,
JobLabel = jobLabel,
- RowIndex = rowIdx,
+ RowIndex = -1,
ScheduledTime = scheduledTime,
ScheduleId = scheduleId,
});
@@ -83,9 +84,9 @@ private static async Task> QueryScheduledAsync(
return list;
}
- private static string BuildScheduledQuery(SqlCommand cmd)
+ private static string BuildScheduledQuery(SqlCommand cmd, bool includeAllJobs)
{
- var filter = BuildJobFilter(cmd);
+ var filter = BuildJobFilter(cmd, includeAllJobs);
return $"""
SELECT j.name,
ja.next_scheduled_run_date,
@@ -110,11 +111,11 @@ AND CAST(ja.next_scheduled_run_date AS date) = @Day
}
public async Task<(List Entries, IReadOnlyList FailedServers)>
- LoadRunningAsync(CancellationToken ct = default)
+ LoadRunningAsync(bool includeAllJobs = false, CancellationToken ct = default)
{
var failed = new ConcurrentBag<(string Server, string Error)>();
var tasks = ServerList.ServerNames
- .Select(s => QueryRunningAsync(s, failed, ct))
+ .Select(s => QueryRunningAsync(s, includeAllJobs, failed, ct))
.ToArray();
var results = await Task.WhenAll(tasks);
return (results.SelectMany(r => r).ToList(),
@@ -122,7 +123,8 @@ AND CAST(ja.next_scheduled_run_date AS date) = @Day
}
private static async Task> QueryRunningAsync(
- string serverName, ConcurrentBag<(string, string)> failed, CancellationToken ct)
+ string serverName, bool includeAllJobs,
+ ConcurrentBag<(string, string)> failed, CancellationToken ct)
{
var list = new List();
try
@@ -132,7 +134,7 @@ private static async Task> QueryRunningAsync(
await using var cmd = conn.CreateCommand();
cmd.CommandTimeout = 10;
- cmd.CommandText = BuildRunningQuery(cmd);
+ cmd.CommandText = BuildRunningQuery(cmd, includeAllJobs);
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
{
@@ -214,11 +216,11 @@ OR r.command LIKE N'RESTORE HEADERONLY%'
""";
private static async Task<(List, IReadOnlyList)>
- QueryRangeAsync(int fromDate, int toDate, CancellationToken ct)
+ QueryRangeAsync(int fromDate, int toDate, bool includeAllJobs, CancellationToken ct)
{
var failed = new ConcurrentBag<(string Server, string Error)>();
var tasks = ServerList.ServerNames
- .Select(s => QueryServerAsync(s, fromDate, toDate, failed, ct))
+ .Select(s => QueryServerAsync(s, fromDate, toDate, includeAllJobs, failed, ct))
.ToArray();
var results = await Task.WhenAll(tasks);
return (results.SelectMany(r => r).ToList(),
@@ -227,7 +229,7 @@ OR r.command LIKE N'RESTORE HEADERONLY%'
private static async Task> QueryServerAsync(
string serverName, int fromDate, int toDate,
- ConcurrentBag<(string, string)> failed, CancellationToken ct)
+ bool includeAllJobs, ConcurrentBag<(string, string)> failed, CancellationToken ct)
{
var list = new List();
try
@@ -237,7 +239,7 @@ private static async Task> QueryServerAsync(
await using var cmd = conn.CreateCommand();
cmd.CommandTimeout = 30;
- cmd.CommandText = BuildRangedQuery(cmd);
+ cmd.CommandText = BuildRangedQuery(cmd, includeAllJobs);
cmd.Parameters.AddWithValue("@FromDate", fromDate);
cmd.Parameters.AddWithValue("@ToDate", toDate);
@@ -264,10 +266,10 @@ private static async Task> QueryServerAsync(
}
public async Task>
- LoadJobStepCommandsAsync(CancellationToken ct = default)
+ LoadJobStepCommandsAsync(bool includeAllJobs = false, CancellationToken ct = default)
{
var tasks = ServerList.ServerNames
- .Select(s => QueryJobStepsAsync(s, ct))
+ .Select(s => QueryJobStepsAsync(s, includeAllJobs, ct))
.ToArray();
var results = await Task.WhenAll(tasks);
return results
@@ -276,7 +278,7 @@ private static async Task> QueryServerAsync(
}
private static async Task> QueryJobStepsAsync(
- string serverName, CancellationToken ct)
+ string serverName, bool includeAllJobs, CancellationToken ct)
{
var list = new List<(string, string, string)>();
try
@@ -285,13 +287,13 @@ private static async Task> QueryServerAsync(
await conn.OpenAsync(ct);
await using var cmd = conn.CreateCommand();
cmd.CommandTimeout = 10;
- cmd.CommandText = BuildJobStepQuery(cmd);
+ cmd.CommandText = BuildJobStepQuery(cmd, includeAllJobs);
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
{
var jobName = reader.GetString(0);
var stepCmd = reader.GetString(1);
- var jobLabel = JobParser.ParseJobLabel(jobName, "");
+ var jobLabel = JobParser.ParseJobLabel(jobName, "", includeAllJobs);
if (jobLabel is not null)
list.Add((serverName, jobLabel, stepCmd));
}
@@ -303,9 +305,9 @@ private static async Task> QueryServerAsync(
return list;
}
- private static string BuildJobStepQuery(SqlCommand cmd)
+ private static string BuildJobStepQuery(SqlCommand cmd, bool includeAllJobs)
{
- var filter = BuildJobFilter(cmd);
+ var filter = BuildJobFilter(cmd, includeAllJobs);
return $"""
SELECT j.name, js.command
FROM msdb.dbo.sysjobs j
@@ -315,8 +317,10 @@ FROM msdb.dbo.sysjobs j
""";
}
- private static string BuildJobFilter(SqlCommand cmd)
+ private static string BuildJobFilter(SqlCommand cmd, bool includeAllJobs)
{
+ if (includeAllJobs) return "1=1";
+
var jobs = ServerList.Jobs;
if (jobs.Length == 0) return "1=0";
var parts = new string[jobs.Length];
@@ -329,9 +333,9 @@ private static string BuildJobFilter(SqlCommand cmd)
return $"({string.Join(" OR ", parts)})";
}
- private static string BuildRangedQuery(SqlCommand cmd)
+ private static string BuildRangedQuery(SqlCommand cmd, bool includeAllJobs)
{
- var filter = BuildJobFilter(cmd);
+ var filter = BuildJobFilter(cmd, includeAllJobs);
return $"""
SELECT
j.name AS CommandType,
@@ -363,9 +367,9 @@ AND h.run_date BETWEEN @FromDate AND @ToDate
""";
}
- private static string BuildRunningQuery(SqlCommand cmd)
+ private static string BuildRunningQuery(SqlCommand cmd, bool includeAllJobs)
{
- var filter = BuildJobFilter(cmd);
+ var filter = BuildJobFilter(cmd, includeAllJobs);
return $"""
SELECT j.name AS CommandType,
MAX(ja.start_execution_date) AS StartTime
diff --git a/src/SQLJobVisualizer/Services/JobParser.cs b/src/SQLJobVisualizer/Services/JobParser.cs
index cb226d8..6b49d98 100644
--- a/src/SQLJobVisualizer/Services/JobParser.cs
+++ b/src/SQLJobVisualizer/Services/JobParser.cs
@@ -4,14 +4,14 @@ namespace SQLJobVisualizer.Services;
public static class JobParser
{
- public static string? ParseJobLabel(string jobName, string _command)
+ public static string? ParseJobLabel(string jobName, string _command, bool includeUnconfigured = false)
{
foreach (var job in ServerList.Jobs)
{
if (MatchesLike(jobName, job.SqlPattern))
return job.Label;
}
- return null;
+ return includeUnconfigured ? jobName : null;
}
private static bool MatchesLike(string input, string pattern)