Skip to content
Merged
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
21 changes: 21 additions & 0 deletions src/util/hostnames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,24 @@ export function preferKnownHostnames(hosts: string[]): string[] {
? [...knownHosts, ...hosts.filter(host => host === 'unknown')]
: hosts;
}

export function knownHostnames(hosts: string[]): string[] {
return hosts.filter(host => Boolean(host) && host !== 'unknown');
}

export function selectSoleKnownHostname(hosts: string[]): string | undefined {
const known = knownHostnames(hosts);
return known.length === 1 ? known[0] : undefined;
}

export type CategoryBuilderHostnameEmptyKind = 'no-hosts' | 'hostname-unselected' | null;

export function categoryBuilderHostnameEmptyKind(
hosts: string[],
hostname?: string | null
): CategoryBuilderHostnameEmptyKind {
if (hostname) {
return null;
}
return hosts.filter(Boolean).length === 0 ? 'no-hosts' : 'hostname-unselected';
}
47 changes: 32 additions & 15 deletions src/views/settings/CategoryBuilder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ div
div
b Options
div
small Hostname: {{ queryOptions.hostname }}
small Hostname: {{ queryOptions.hostname || '(not selected)' }}
div
small Range: {{ queryOptions.start }} - {{ queryOptions.stop }}
div.flex-grow-0
b-button(variant="outline-dark" @click="show_options = !show_options" size="sm")
span(v-if="!show_options") Show options
span(v-else) Hide options

div(v-show="show_options")
div(v-if="show_options")
hr
h4 Options
aw-query-options(v-model="queryOptions")
Expand All @@ -36,11 +36,16 @@ div
div(v-if="loading")
b-spinner.mr-2(small)
span.text-muted Loading...
div(v-else-if="!queryOptions.hostname")
div(v-else-if="hostnameEmptyKind === 'no-hosts'")
p.text-muted.mb-0
| No host with window/AFK buckets is available. Install
| #[a(href="https://docs.activitywatch.net/en/latest/watchers.html") a watcher]
| to start collecting data.
div(v-else-if="hostnameEmptyKind === 'hostname-unselected'")
p.text-muted.mb-0
| Select a hostname under
| #[b Show options]
| to load uncategorized words. The hostname picker is hidden until you open options.
div(v-else)
div(v-if="words_by_duration.length == 0")
| No words with significant duration. You're good to go!
Expand Down Expand Up @@ -119,6 +124,7 @@ import { getClient } from '~/util/awclient';
import CategoryEditModal from '~/components/CategoryEditModal.vue';
import { isRegexBroad, validateRegex } from '~/util/validate';
import { findCommonPhrases } from '~/util/categorization';
import { categoryBuilderHostnameEmptyKind, selectSoleKnownHostname } from '~/util/hostnames';

export default {
name: 'CategoryBuilder',
Expand All @@ -143,6 +149,7 @@ export default {
// Options
show_options: false,
queryOptions: {
hostname: '',
start: moment().subtract(1, 'day'),
stop: moment().add(1, 'day'),
},
Expand Down Expand Up @@ -193,6 +200,9 @@ export default {
broad_pattern: function () {
return isRegexBroad(this.append.word);
},
hostnameEmptyKind: function () {
return categoryBuilderHostnameEmptyKind(useBucketsStore().hosts, this.queryOptions.hostname);
},
},
watch: {
queryOptions: {
Expand All @@ -204,10 +214,16 @@ export default {
},
async mounted() {
// Make sure we don't have stale unsaved changes in categoryStore
await useBucketsStore().ensureLoaded();
const bucketsStore = useBucketsStore();
await bucketsStore.ensureLoaded();
await this.categoryStore.load();
// Called by watch
//await this.fetchWords();
const sole = selectSoleKnownHostname(bucketsStore.hosts);
if (sole && !this.queryOptions.hostname) {
this.$set(this.queryOptions, 'hostname', sole);
// Deep watch on queryOptions calls fetchWords.
} else {
await this.fetchWords();
}
},
methods: {
async fetchWords() {
Expand All @@ -216,17 +232,18 @@ export default {
// after every requery.
this.visible_count = this.page_size;
if (!this.queryOptions.hostname) {
// Try to resolve hostname from loaded buckets
// Don't ever return the "unknown" hostname
const hosts = useBucketsStore().hosts;
if (hosts && hosts.length > 0) {
this.queryOptions.hostname = _.filter(hosts, host => host !== 'unknown')[0];
}
// If still no valid hostname, bail out and wait for QueryOptions to provide one
if (!this.queryOptions.hostname) {
this.loading = false;
// Auto-select only when there is exactly one real hostname. Several
// known hosts (or only "unknown") stay unset so the empty-state copy
// can point at Show options / the hostname picker instead of
// silently querying the first device.
const sole = selectSoleKnownHostname(useBucketsStore().hosts);
if (sole) {
this.$set(this.queryOptions, 'hostname', sole);
// Deep watch re-enters fetchWords with hostname set.
return;
}
this.loading = false;
return;
}
await this.categoryStore.load();
const awclient = getClient();
Expand Down
56 changes: 55 additions & 1 deletion test/unit/hostnames.test.node.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { preferKnownHostnames } from '~/util/hostnames';
import {
preferKnownHostnames,
selectSoleKnownHostname,
categoryBuilderHostnameEmptyKind,
} from '~/util/hostnames';

describe('preferKnownHostnames', () => {
test('moves unknown to the end when a known host exists', () => {
Expand All @@ -17,3 +21,53 @@ describe('preferKnownHostnames', () => {
expect(preferKnownHostnames(['unknown'])).toEqual(['unknown']);
});
});

describe('selectSoleKnownHostname', () => {
test('returns the only non-unknown host', () => {
expect(selectSoleKnownHostname(['laptop'])).toBe('laptop');
});

test('returns the only known host even when unknown is also present', () => {
expect(selectSoleKnownHostname(['unknown', 'laptop'])).toBe('laptop');
});

test('returns undefined when several known hosts exist', () => {
expect(selectSoleKnownHostname(['laptop', 'desktop'])).toBeUndefined();
});

test('returns undefined when hosts is empty', () => {
expect(selectSoleKnownHostname([])).toBeUndefined();
});

test('returns undefined when only unknown exists', () => {
expect(selectSoleKnownHostname(['unknown'])).toBeUndefined();
});

test('ignores empty/falsy host strings', () => {
expect(selectSoleKnownHostname(['', undefined as unknown as string, 'laptop'])).toBe('laptop');
});
});

describe('categoryBuilderHostnameEmptyKind', () => {
test('is null when a hostname is already selected', () => {
expect(categoryBuilderHostnameEmptyKind(['laptop'], 'laptop')).toBeNull();
});

test('is no-hosts when there are no hosts at all', () => {
expect(categoryBuilderHostnameEmptyKind([], undefined)).toBe('no-hosts');
});

test('is hostname-unselected when hosts exist but none is chosen', () => {
expect(categoryBuilderHostnameEmptyKind(['laptop', 'desktop'], undefined)).toBe(
'hostname-unselected'
);
});

test('is hostname-unselected when only unknown is listed', () => {
expect(categoryBuilderHostnameEmptyKind(['unknown'], undefined)).toBe('hostname-unselected');
});

test('treats empty string hostname as unselected when hosts exist', () => {
expect(categoryBuilderHostnameEmptyKind(['laptop'], '')).toBe('hostname-unselected');
});
});
Loading