Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Refine update progress reports #443

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 43 additions & 16 deletions backend/pkg/api/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ type InstancesStatusStats struct {
Downloaded null.Int `db:"downloaded" json:"downloaded"`
Downloading null.Int `db:"downloading" json:"downloading"`
OnHold null.Int `db:"onhold" json:"onhold"`
OtherVersions null.Int `db:"other_versions" json:"other_versions"`
TimedOut null.Int `db:"timed_out" json:"timed_out"`
}

// UpdatesStats represents a set of statistics about the status of the updates
Expand Down Expand Up @@ -556,22 +558,47 @@ func (api *API) GetGroupInstancesStats(groupID, duration string) (*InstancesStat
if err != nil {
return nil, err
}
query := fmt.Sprintf(`
SELECT
count(*) total,
sum(case when status IS NULL then 1 else 0 end) undefined,
sum(case when status = %d then 1 else 0 end) error,
sum(case when status = %d then 1 else 0 end) update_granted,
sum(case when status = %d then 1 else 0 end) complete,
sum(case when status = %d then 1 else 0 end) installed,
sum(case when status = %d then 1 else 0 end) downloaded,
sum(case when status = %d then 1 else 0 end) downloading,
sum(case when status = %d then 1 else 0 end) onhold
FROM instance_application
WHERE group_id=$1 AND last_check_for_updates > now() at time zone 'utc' - interval '%s' AND %s`,
InstanceStatusError, InstanceStatusUpdateGranted, InstanceStatusComplete, InstanceStatusInstalled,
InstanceStatusDownloaded, InstanceStatusDownloading, InstanceStatusOnHold, durationString, ignoreFakeInstanceCondition("instance_id"))
err = api.db.QueryRowx(query, groupID).StructScan(&instancesStats)

packageVersion := ""
group, err := api.GetGroup(groupID)

if err == nil {
packageVersion = ""
if group.Channel != nil && group.Channel.Package != nil {
packageVersion = group.Channel.Package.Version
}
}

query := ""
// If we have no package assigned, then we cannot thoroughly report on the status for the group's version,
// so we send out just the total
if packageVersion == "" {
query, _, err = goqu.From("instance_application").Select(
goqu.COUNT("*").As("total"),
).Where(goqu.C("group_id").Eq(groupID), goqu.L("last_check_for_updates > now() at time zone 'utc' - interval ?", durationString),
goqu.L(ignoreFakeInstanceCondition("instance_id")),
).ToSQL()
} else {
query, _, err = goqu.From("instance_application").Select(
goqu.COUNT("*").As("total"),
goqu.COALESCE(goqu.SUM(goqu.L("case when (update_in_progress = 'false' or now() at time zone 'utc' - last_update_granted_ts < interval ?) and version != ? and status IS NULL then 1 else 0 end", group.PolicyUpdateTimeout, packageVersion)), 0).As("undefined"),
goqu.COALESCE(goqu.SUM(goqu.L("case when (update_in_progress = 'false' or now() at time zone 'utc' - last_update_granted_ts < interval ?) and last_update_version = ? and status = ? then 1 else 0 end", group.PolicyUpdateTimeout, packageVersion, InstanceStatusError)), 0).As("error"),
goqu.COALESCE(goqu.SUM(goqu.L("case when (update_in_progress = 'false' or now() at time zone 'utc' - last_update_granted_ts < interval ?) and last_update_version = ? and status = ? then 1 else 0 end", group.PolicyUpdateTimeout, packageVersion, InstanceStatusUpdateGranted)), 0).As("update_granted"),
goqu.COALESCE(goqu.SUM(goqu.L("case when (update_in_progress = 'false' or now() at time zone 'utc' - last_update_granted_ts < interval ?) and (version = ? and status IS NULL) or (version = ? and status = ?) then 1 else 0 end", group.PolicyUpdateTimeout, packageVersion, packageVersion, InstanceStatusComplete)), 0).As("complete"),
goqu.COALESCE(goqu.SUM(goqu.L("case when (update_in_progress = 'false' or now() at time zone 'utc' - last_update_granted_ts < interval ?) and last_update_version = ? and status = ? then 1 else 0 end", group.PolicyUpdateTimeout, packageVersion, InstanceStatusInstalled)), 0).As("installed"),
goqu.COALESCE(goqu.SUM(goqu.L("case when (update_in_progress = 'false' or now() at time zone 'utc' - last_update_granted_ts < interval ?) and last_update_version = ? and status = ? then 1 else 0 end", group.PolicyUpdateTimeout, packageVersion, InstanceStatusDownloaded)), 0).As("downloaded"),
goqu.COALESCE(goqu.SUM(goqu.L("case when (update_in_progress = 'false' or now() at time zone 'utc' - last_update_granted_ts < interval ?) and last_update_version = ? and status = ? then 1 else 0 end", group.PolicyUpdateTimeout, packageVersion, InstanceStatusDownloading)), 0).As("downloading"),
goqu.COALESCE(goqu.SUM(goqu.L("case when (update_in_progress = 'false' or now() at time zone 'utc' - last_update_granted_ts < interval ?) and last_update_version = ? and status = ? then 1 else 0 end", group.PolicyUpdateTimeout, packageVersion, InstanceStatusOnHold)), 0).As("onhold"),
goqu.COALESCE(goqu.SUM(goqu.L("case when (update_in_progress = 'false' or now() at time zone 'utc' - last_update_granted_ts < interval ?) and last_update_version != ? and version != ? and status IS NOT NULL then 1 else 0 end", group.PolicyUpdateTimeout, packageVersion, packageVersion)), 0).As("other_versions"),
goqu.COALESCE(goqu.SUM(goqu.L("case when update_in_progress = 'true' and now() at time zone 'utc' - last_update_granted_ts > interval ? then 1 else 0 end", group.PolicyUpdateTimeout)), 0).As("timed_out"),
).Where(goqu.C("group_id").Eq(groupID), goqu.L("last_check_for_updates > now() at time zone 'utc' - interval ?", durationString),
goqu.L(ignoreFakeInstanceCondition("instance_id")),
).ToSQL()
}
if err != nil {
return nil, err
}
err = api.db.QueryRowx(query).StructScan(&instancesStats)
if err != nil {
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/js/components/Groups/ItemExtended.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ function ItemExtended(props: {
<InstanceStatusArea
instanceStats={instancesStats}
period={updateProgressChartDuration.displayValue}
groupHasVersion={!!group.channel?.package?.version}
href={{
pathname: `/apps/${props.appID}/groups/${props.groupID}/instances`,
search: `period=${updateProgressChartDuration.queryValue}`,
Expand Down
106 changes: 60 additions & 46 deletions frontend/src/js/components/Instances/Charts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ interface InstanceStatusAreaProps {
instanceStats: InstanceStats | null;
href?: object;
period: string;
groupHasVersion: boolean;
}

interface InstanceStatusCount {
Expand All @@ -202,42 +203,46 @@ export default function InstanceStatusArea(props: InstanceStatusAreaProps) {
const statusDefs = makeStatusDefs(theme);
const { t } = useTranslation();

const { instanceStats, href, period } = props;
const { instanceStats, href, period, groupHasVersion } = props;
const instanceStateCount: InstanceStatusCount[] = [
{
status: 'InstanceStatusComplete',
count: [{ key: 'complete' }],
},
{
status: 'InstanceStatusDownloaded',
count: [{ key: 'downloaded' }],
status: 'InstanceStatusNotUpdating',
count: [{ key: 'other_versions', label: t('instances|InstanceStatusOtherVersions') }],
},
{
status: 'InstanceStatusOther',
count: [
{ key: 'onhold', label: t('instances|InstanceStatusOnHold') },
{ key: 'undefined', label: t('instances|InstanceStatusUndefined') },
],
},
{
status: 'InstanceStatusInstalled',
count: [{ key: 'installed' }],
status: 'InstanceStatusOnHold',
count: [{ key: 'onhold' }],
},
{
status: 'InstanceStatusDownloading',
status: 'InstanceStatusUpdating',
count: [
{ key: 'downloading', label: t('instances|InstanceStatusDownloading') },
{ key: 'update_granted', label: t('instances|InstanceStatusUpdateGranted') },
{ key: 'downloading', label: t('instances|InstanceStatusDownloading') },
{ key: 'downloaded', label: t('instances|InstanceStatusDownloaded') },
{ key: 'installed', label: t('instances|InstanceStatusInstalled') },
],
},
{
status: 'InstanceStatusError',
count: [{ key: 'error' }],
},
{
status: 'InstanceStatusTimedOut',
count: [{ key: 'timed_out' }],
},
];

statusDefs['InstanceStatusOther'] = { ...statusDefs['InstanceStatusUndefined'] };
statusDefs['InstanceStatusOther'].label = t('instances|Other');
statusDefs['InstanceStatusNotUpdating'] = { ...statusDefs['InstanceStatusUndefined'] };
statusDefs['InstanceStatusNotUpdating'].label = t('instances|Not updating');

statusDefs['InstanceStatusUpdating'] = {
...statusDefs['InstanceStatusDownloading'],
label: t('instances|Updating'),
};

const totalInstances = instanceStats ? instanceStats.total : 0;

Expand All @@ -251,37 +256,46 @@ export default function InstanceStatusArea(props: InstanceStatusAreaProps) {
<InstanceCountLabel countText={totalInstances} href={href} />
</Grid>
<Grid item container justify="space-between" xs={8}>
{instanceStateCount.map(({ status, count }, i) => {
// Sort the data entries so the smaller amounts are shown first.
count.sort((obj1, obj2) => {
const stats1 = instanceStats[obj1.key];
const stats2 = instanceStats[obj2.key];
if (stats1 === stats2) return 0;
if (stats1 < stats2) return -1;
return 1;
});
{!groupHasVersion ? (
<Empty>
<Trans ns="instances">
It's not possible to get an accurate report as the group has no channel/version
assigned to it.
</Trans>
</Empty>
) : (
instanceStateCount.map(({ status, count }, i) => {
// Sort the data entries so the smaller amounts are shown first.
count.sort((obj1, obj2) => {
const stats1 = instanceStats[obj1.key];
const stats2 = instanceStats[obj2.key];
if (stats1 === stats2) return 0;
if (stats1 < stats2) return -1;
return 1;
});

return (
<Grid item key={i}>
<ProgressDoughnut
data={count.map(({ key, label = status }) => {
const statusLabel = statusDefs[label].label;
return {
value: instanceStats[key] / instanceStats.total,
color: statusDefs[label].color,
description: t('{{statusLabel}}: {{stat, number}} instances', {
statusLabel: statusLabel,
stat: instanceStats[key],
}),
};
})}
width={140}
height={140}
{...statusDefs[status]}
/>
</Grid>
);
})}
return (
<Grid item key={i}>
<ProgressDoughnut
data={count.map(({ key, label = status }) => {
const statusLabel = statusDefs[label].label;
return {
value: instanceStats[key] / instanceStats.total,
color: statusDefs[label].color,
description: t('{{statusLabel}}: {{stat, number}} instances', {
statusLabel: statusLabel,
stat: instanceStats[key],
}),
};
})}
width={140}
height={140}
{...statusDefs[status]}
/>
</Grid>
);
})
)}
</Grid>
</Grid>
) : (
Expand Down
30 changes: 21 additions & 9 deletions frontend/src/js/components/Instances/StatusDefs.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import alertCircleOutline from '@iconify/icons-mdi/alert-circle-outline';
import checkCircleOutline from '@iconify/icons-mdi/check-circle-outline';
import downloadCircleOutline from '@iconify/icons-mdi/download-circle-outline';
import helpCircleOutline from '@iconify/icons-mdi/help-circle-outline';
import clockAlertOutline from '@iconify/icons-mdi/clock-alert-outline';
import closeCircleOutline from '@iconify/icons-mdi/close-circle-outline';
import packageVariantClosed from '@iconify/icons-mdi/package-variant-closed';
import pauseCircle from '@iconify/icons-mdi/pause-circle';
import playCircle from '@iconify/icons-mdi/play-circle';
import progressDownload from '@iconify/icons-mdi/progress-download';
import refresh from '@iconify/icons-mdi/refresh';
import { Theme } from '@material-ui/core';
import { useTranslation } from 'react-i18next';

Expand All @@ -21,20 +21,20 @@ function makeStatusDefs(theme: Theme): {

return {
InstanceStatusComplete: {
label: t('instances|Complete'),
label: t('instances|Up to date'),
color: 'rgba(15,15,15,1)',
icon: checkCircleOutline,
queryValue: '4',
},
InstanceStatusDownloaded: {
label: t('instances|Downloaded'),
color: 'rgba(40,95,43,1)',
icon: downloadCircleOutline,
icon: refresh,
queryValue: '6',
},
InstanceStatusOnHold: {
label: t('instances|On Hold'),
color: theme.palette.grey['400'],
color: 'rgb(89, 89, 89)',
icon: pauseCircle,
queryValue: '8',
},
Expand All @@ -47,19 +47,19 @@ function makeStatusDefs(theme: Theme): {
InstanceStatusDownloading: {
label: t('instances|Downloading'),
color: 'rgba(17,40,141,1)',
icon: progressDownload,
icon: refresh,
queryValue: '7',
},
InstanceStatusError: {
label: t('instances|Error'),
color: 'rgba(164,45,36,1)',
icon: alertCircleOutline,
icon: closeCircleOutline,
queryValue: '3',
},
InstanceStatusUndefined: {
label: t('instances|Unknown'),
color: 'rgb(89, 89, 89)',
icon: helpCircleOutline,
icon: alertCircleOutline,
queryValue: '1',
},
InstanceStatusUpdateGranted: {
Expand All @@ -68,6 +68,18 @@ function makeStatusDefs(theme: Theme): {
icon: playCircle,
queryValue: '2',
},
InstanceStatusTimedOut: {
label: t('instances|Timed out'),
color: 'rgba(164,45,36,1)',
icon: clockAlertOutline,
queryValue: '8',
},
InstanceStatusOtherVersions: {
label: t('instances|Not updating to this version'),
color: 'rgb(89, 89, 89)',
icon: alertCircleOutline,
queryValue: '',
},
};
}

Expand Down
20 changes: 20 additions & 0 deletions frontend/src/js/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,26 @@ export function getInstanceStatus(statusID: number, version?: string) {
explanation:
'There was an update pending for the instance but it was put on hold because of the rollout policy',
},
9: {
type: 'InstanceStatusTimedOut',
className: 'danger',
spinning: false,
icon: 'glyphicon glyphicon-remove',
description: 'Error updating',
bgColor: 'rgba(244, 67, 54, 0.1)',
textColor: '#F44336',
status: 'Error',
explanation: 'The instance reported an error while updating to version ' + version,
},
10: {
type: 'InstanceStatusOtherVersions',
className: '',
spinning: false,
icon: '',
description: '',
status: 'Undefined',
explanation: '',
},
};

const statusDetails = statusID ? status[statusID] : status[1];
Expand Down