feat(editor): Load fixed template list as experiment (#6632)

* feat(editor): Load fixed template list as experiment

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Improve templates loading

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* get rid of endResult message

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Do not lazy-load when fixedListExperiment

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
OlegIvaniv 2023-07-10 17:11:42 +02:00 committed by GitHub
parent aa53c46367
commit e9966224ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 71 additions and 59 deletions

View file

@ -31,7 +31,7 @@ import type { PropType } from 'vue';
import type { ITemplatesCollection } from '@/Interface'; import type { ITemplatesCollection } from '@/Interface';
import Card from '@/components/CollectionWorkflowCard.vue'; import Card from '@/components/CollectionWorkflowCard.vue';
import CollectionCard from '@/components/CollectionCard.vue'; import CollectionCard from '@/components/CollectionCard.vue';
import VueAgile from 'vue-agile'; import { VueAgile } from 'vue-agile';
import { genericHelpers } from '@/mixins/genericHelpers'; import { genericHelpers } from '@/mixins/genericHelpers';

View file

@ -55,6 +55,7 @@ import { abbreviateNumber, filterTemplateNodes } from '@/utils';
import type { ITemplatesNode, ITemplatesWorkflow, ITemplatesWorkflowFull } from '@/Interface'; import type { ITemplatesNode, ITemplatesWorkflow, ITemplatesWorkflowFull } from '@/Interface';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useTemplatesStore } from '@/stores/templates.store'; import { useTemplatesStore } from '@/stores/templates.store';
import TimeAgo from '@/components/TimeAgo.vue';
export default defineComponent({ export default defineComponent({
name: 'TemplateDetails', name: 'TemplateDetails',
@ -72,6 +73,7 @@ export default defineComponent({
components: { components: {
NodeIcon, NodeIcon,
TemplateDetailsBlock, TemplateDetailsBlock,
TimeAgo,
}, },
computed: { computed: {
...mapStores(useTemplatesStore), ...mapStores(useTemplatesStore),

View file

@ -1,6 +1,6 @@
<template> <template>
<div :class="$style.list" v-if="loading || workflows.length"> <div :class="$style.list" v-if="loading || workflows.length">
<div :class="$style.header"> <div :class="$style.header" v-if="!hideHeader">
<n8n-heading :bold="true" size="medium" color="text-light"> <n8n-heading :bold="true" size="medium" color="text-light">
{{ $locale.baseText('templates.workflows') }} {{ $locale.baseText('templates.workflows') }}
<span v-if="!loading && totalWorkflows" v-text="`(${totalWorkflows})`" /> <span v-if="!loading && totalWorkflows" v-text="`(${totalWorkflows})`" />
@ -57,6 +57,10 @@ export default defineComponent({
totalWorkflows: { totalWorkflows: {
type: Number, type: Number,
}, },
hideHeader: {
type: Boolean,
default: false,
},
}, },
mounted() { mounted() {
if (this.infiniteScrollEnabled) { if (this.infiniteScrollEnabled) {

View file

@ -526,10 +526,11 @@ export const KEEP_AUTH_IN_NDV_FOR_NODES = [
export const MAIN_AUTH_FIELD_NAME = 'authentication'; export const MAIN_AUTH_FIELD_NAME = 'authentication';
export const NODE_RESOURCE_FIELD_NAME = 'resource'; export const NODE_RESOURCE_FIELD_NAME = 'resource';
export const TEMPLATE_EXPERIMENT = { export const TEMPLATES_EXPERIMENT = {
name: '002_remove_templates', name: '008_template_variants',
control: 'control', control: 'control',
variant: 'variant', variant: 'variant',
variantIds: ['1932', '1930', '1931', '1933', '1750', '1748', '1435'],
}; };
export const ONBOARDING_EXPERIMENT = { export const ONBOARDING_EXPERIMENT = {
@ -538,7 +539,7 @@ export const ONBOARDING_EXPERIMENT = {
variant: 'variant', variant: 'variant',
}; };
export const EXPERIMENTS_TO_TRACK = [TEMPLATE_EXPERIMENT.name, ONBOARDING_EXPERIMENT.name]; export const EXPERIMENTS_TO_TRACK = [TEMPLATES_EXPERIMENT.name, ONBOARDING_EXPERIMENT.name];
export const NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND = [FILTER_NODE_TYPE]; export const NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND = [FILTER_NODE_TYPE];

View file

@ -1462,7 +1462,6 @@
"templates.collections": "Collections", "templates.collections": "Collections",
"templates.collectionsNotFound": "Collection could not be found", "templates.collectionsNotFound": "Collection could not be found",
"templates.connectionWarning": "⚠️ There was a problem fetching workflow templates. Check your internet connection.", "templates.connectionWarning": "⚠️ There was a problem fetching workflow templates. Check your internet connection.",
"templates.endResult": "Share your own useful workflows through your <a href='https://n8n.io/dashboard' target='_blank'>n8n.io account</a>",
"templates.heading": "Workflow templates", "templates.heading": "Workflow templates",
"templates.newButton": "New blank workflow", "templates.newButton": "New blank workflow",
"templates.noSearchResults": "Nothing found. Try adjusting your search to see more.", "templates.noSearchResults": "Nothing found. Try adjusting your search to see more.",

View file

@ -33,7 +33,6 @@ import VariablesView from '@/views/VariablesView.vue';
import type { IPermissions } from './Interface'; import type { IPermissions } from './Interface';
import { LOGIN_STATUS, ROLE } from '@/utils'; import { LOGIN_STATUS, ROLE } from '@/utils';
import type { RouteConfigSingleView } from 'vue-router/types/router'; import type { RouteConfigSingleView } from 'vue-router/types/router';
import { TEMPLATE_EXPERIMENT, VIEWS } from './constants';
import { useSettingsStore } from './stores/settings.store'; import { useSettingsStore } from './stores/settings.store';
import { useTemplatesStore } from './stores/templates.store'; import { useTemplatesStore } from './stores/templates.store';
import { useSSOStore } from './stores/sso.store'; import { useSSOStore } from './stores/sso.store';
@ -43,7 +42,7 @@ import SignoutView from '@/views/SignoutView.vue';
import SamlOnboarding from '@/views/SamlOnboarding.vue'; import SamlOnboarding from '@/views/SamlOnboarding.vue';
import SettingsSourceControl from './views/SettingsSourceControl.vue'; import SettingsSourceControl from './views/SettingsSourceControl.vue';
import SettingsAuditLogs from './views/SettingsAuditLogs.vue'; import SettingsAuditLogs from './views/SettingsAuditLogs.vue';
import { usePostHog } from './stores/posthog.store'; import { VIEWS } from '@/constants';
Vue.use(Router); Vue.use(Router);
@ -63,12 +62,8 @@ interface IRouteConfig extends RouteConfigSingleView {
function getTemplatesRedirect() { function getTemplatesRedirect() {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const posthog = usePostHog();
const isTemplatesEnabled: boolean = settingsStore.isTemplatesEnabled; const isTemplatesEnabled: boolean = settingsStore.isTemplatesEnabled;
if ( if (!isTemplatesEnabled) {
!posthog.isVariantEnabled(TEMPLATE_EXPERIMENT.name, TEMPLATE_EXPERIMENT.variant) &&
!isTemplatesEnabled
) {
return { name: VIEWS.NOT_FOUND }; return { name: VIEWS.NOT_FOUND };
} }

View file

@ -10,7 +10,6 @@ import {
EXPERIMENTS_TO_TRACK, EXPERIMENTS_TO_TRACK,
LOCAL_STORAGE_EXPERIMENT_OVERRIDES, LOCAL_STORAGE_EXPERIMENT_OVERRIDES,
ONBOARDING_EXPERIMENT, ONBOARDING_EXPERIMENT,
TEMPLATE_EXPERIMENT,
} from '@/constants'; } from '@/constants';
import { useTelemetryStore } from './telemetry.store'; import { useTelemetryStore } from './telemetry.store';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
@ -134,7 +133,6 @@ export const usePostHog = defineStore('posthog', () => {
// does not need to be debounced really, but tracking does not fire without delay on page load // does not need to be debounced really, but tracking does not fire without delay on page load
addExperimentOverrides(); addExperimentOverrides();
trackExperimentsDebounced(featureFlags.value); trackExperimentsDebounced(featureFlags.value);
evaluateExperiments(featureFlags.value);
} else { } else {
// depend on client side evaluation if serverside evaluation fails // depend on client side evaluation if serverside evaluation fails
window.posthog?.onFeatureFlags?.((keys: string[], map: FeatureFlags) => { window.posthog?.onFeatureFlags?.((keys: string[], map: FeatureFlags) => {
@ -143,21 +141,10 @@ export const usePostHog = defineStore('posthog', () => {
// must be debounced because it is called multiple times by posthog // must be debounced because it is called multiple times by posthog
trackExperimentsDebounced(featureFlags.value); trackExperimentsDebounced(featureFlags.value);
evaluateExperimentsDebounced(featureFlags.value);
}); });
} }
}; };
const evaluateExperiments = (featureFlags: FeatureFlags) => {
Object.keys(featureFlags).forEach((name) => {
const variant = featureFlags[name];
if (name === TEMPLATE_EXPERIMENT.name && variant === TEMPLATE_EXPERIMENT.variant) {
settingsStore.disableTemplates();
}
});
};
const evaluateExperimentsDebounced = debounce(evaluateExperiments, 2000);
const trackExperiments = (featureFlags: FeatureFlags) => { const trackExperiments = (featureFlags: FeatureFlags) => {
EXPERIMENTS_TO_TRACK.forEach((name) => trackExperiment(featureFlags, name)); EXPERIMENTS_TO_TRACK.forEach((name) => trackExperiment(featureFlags, name));
}; };

View file

@ -18,7 +18,7 @@
</template> </template>
<template #content> <template #content>
<div :class="$style.contentWrapper"> <div :class="$style.contentWrapper">
<div :class="$style.filters"> <div :class="$style.filters" v-if="!isFixedListExperiment">
<TemplateFilters <TemplateFilters
:categories="templatesStore.allCategories" :categories="templatesStore.allCategories"
:sortOnPopulate="areCategoriesPrepopulated" :sortOnPopulate="areCategoriesPrepopulated"
@ -30,6 +30,7 @@
/> />
</div> </div>
<div :class="$style.search"> <div :class="$style.search">
<template v-if="!isFixedListExperiment">
<n8n-input <n8n-input
:value="search" :value="search"
:placeholder="$locale.baseText('templates.searchPlaceholder')" :placeholder="$locale.baseText('templates.searchPlaceholder')"
@ -41,7 +42,10 @@
<font-awesome-icon icon="search" /> <font-awesome-icon icon="search" />
</template> </template>
</n8n-input> </n8n-input>
<div :class="$style.carouselContainer" v-show="collections.length || loadingCollections"> <div
:class="$style.carouselContainer"
v-show="collections.length || loadingCollections"
>
<div :class="$style.header"> <div :class="$style.header">
<n8n-heading :bold="true" size="medium" color="text-light"> <n8n-heading :bold="true" size="medium" color="text-light">
{{ $locale.baseText('templates.collections') }} {{ $locale.baseText('templates.collections') }}
@ -55,11 +59,13 @@
@openCollection="onOpenCollection" @openCollection="onOpenCollection"
/> />
</div> </div>
</template>
<TemplateList <TemplateList
:infinite-scroll-enabled="true" :infinite-scroll-enabled="!isFixedListExperiment"
:loading="loadingWorkflows" :loading="loadingWorkflows"
:total-workflows="totalWorkflows" :total-workflows="totalWorkflows"
:workflows="workflows" :workflows="isFixedListExperiment ? fixedTemplatesList : workflows"
:hide-header="isFixedListExperiment"
@loadMore="onLoadMore" @loadMore="onLoadMore"
@openTemplate="onOpenTemplate" @openTemplate="onOpenTemplate"
/> />
@ -91,13 +97,14 @@ import type {
} from '@/Interface'; } from '@/Interface';
import type { IDataObject } from 'n8n-workflow'; import type { IDataObject } from 'n8n-workflow';
import { setPageTitle } from '@/utils'; import { setPageTitle } from '@/utils';
import { VIEWS } from '@/constants'; import { TEMPLATES_EXPERIMENT, VIEWS } from '@/constants';
import { debounceHelper } from '@/mixins/debounce'; import { debounceHelper } from '@/mixins/debounce';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { useTemplatesStore } from '@/stores/templates.store'; import { useTemplatesStore } from '@/stores/templates.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useToast } from '@/composables'; import { useToast } from '@/composables';
import { usePostHog } from '@/stores/posthog.store';
interface ISearchEvent { interface ISearchEvent {
search_string: string; search_string: string;
@ -135,7 +142,18 @@ export default defineComponent({
}; };
}, },
computed: { computed: {
...mapStores(useSettingsStore, useTemplatesStore, useUIStore, useUsersStore), ...mapStores(useSettingsStore, useTemplatesStore, useUIStore, useUsersStore, usePostHog),
isFixedListExperiment() {
return this.posthogStore.isVariantEnabled(
TEMPLATES_EXPERIMENT.name,
TEMPLATES_EXPERIMENT.variant,
);
},
fixedTemplatesList() {
return TEMPLATES_EXPERIMENT.variantIds
.map((id) => this.templatesStore.workflows[id])
.filter(Boolean);
},
totalWorkflows(): number { totalWorkflows(): number {
return this.templatesStore.getSearchedWorkflowsTotal(this.query); return this.templatesStore.getSearchedWorkflowsTotal(this.query);
}, },
@ -149,9 +167,6 @@ export default defineComponent({
if (this.loadingWorkflows) { if (this.loadingWorkflows) {
return null; return null;
} }
if (this.workflows.length && this.workflows.length >= this.totalWorkflows) {
return this.$locale.baseText('templates.endResult');
}
if ( if (
!this.loadingCollections && !this.loadingCollections &&
this.workflows.length === 0 && this.workflows.length === 0 &&
@ -391,6 +406,15 @@ export default defineComponent({
}, 100); }, 100);
}, },
async created() { async created() {
if (this.isFixedListExperiment) {
// Templates are lazy-loaded so we need to make sure the fixed ids are loaded
TEMPLATES_EXPERIMENT.variantIds.forEach(async (templateId) =>
this.templatesStore.fetchTemplateById(templateId),
);
// Categorization and filtering based on search is not supported if fixed list is enabled
return;
}
if (this.$route.query.search && typeof this.$route.query.search === 'string') { if (this.$route.query.search && typeof this.$route.query.search === 'string') {
this.search = this.$route.query.search; this.search = this.$route.query.search;
} }
@ -423,11 +447,11 @@ export default defineComponent({
.filters { .filters {
width: 200px; width: 200px;
margin-bottom: var(--spacing-xl); margin-bottom: var(--spacing-xl);
margin-right: var(--spacing-2xl);
} }
.search { .search {
width: 100%; width: 100%;
padding-left: var(--spacing-2xl);
> * { > * {
margin-bottom: var(--spacing-l); margin-bottom: var(--spacing-l);