Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-07-11 15:07:02 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-07-11 15:07:02 +0300
commitbcd11d993d80d46053a97ee3b0344ed4d2b4571b (patch)
treee3b4047cafd580d3a3d7d8cde094c183ee9aabfc /app/assets/javascripts/environments
parent871b886a1794e5baefd6b2f96caf2ac4ce5da6ca (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/environments')
-rw-r--r--app/assets/javascripts/environments/components/edit_environment.vue10
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue133
-rw-r--r--app/assets/javascripts/environments/components/new_environment.vue1
-rw-r--r--app/assets/javascripts/environments/components/new_environment_item.vue18
-rw-r--r--app/assets/javascripts/environments/constants.js16
-rw-r--r--app/assets/javascripts/environments/edit.js3
-rw-r--r--app/assets/javascripts/environments/graphql/client.js9
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_namespace.query.graphql20
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_with_namespace.graphql15
-rw-r--r--app/assets/javascripts/environments/graphql/queries/k8s_namespaces.query.graphql7
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers.js25
-rw-r--r--app/assets/javascripts/environments/graphql/typedefs.graphql6
-rw-r--r--app/assets/javascripts/environments/helpers/k8s_integration_helper.js7
-rw-r--r--app/assets/javascripts/environments/new.js9
14 files changed, 260 insertions, 19 deletions
diff --git a/app/assets/javascripts/environments/components/edit_environment.vue b/app/assets/javascripts/environments/components/edit_environment.vue
index 9e3f8d996e0..a2405d23924 100644
--- a/app/assets/javascripts/environments/components/edit_environment.vue
+++ b/app/assets/javascripts/environments/components/edit_environment.vue
@@ -2,7 +2,9 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { visitUrl } from '~/lib/utils/url_utility';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getEnvironment from '../graphql/queries/environment.query.graphql';
+import getEnvironmentWithNamespace from '../graphql/queries/environment_with_namespace.graphql';
import updateEnvironment from '../graphql/mutations/update_environment.mutation.graphql';
import EnvironmentForm from './environment_form.vue';
@@ -11,10 +13,15 @@ export default {
GlLoadingIcon,
EnvironmentForm,
},
+ mixins: [glFeatureFlagsMixin()],
inject: ['projectEnvironmentsPath', 'projectPath', 'environmentName'],
apollo: {
environment: {
- query: getEnvironment,
+ query() {
+ return this.glFeatures?.kubernetesNamespaceForEnvironment
+ ? getEnvironmentWithNamespace
+ : getEnvironment;
+ },
variables() {
return {
environmentName: this.environmentName,
@@ -52,6 +59,7 @@ export default {
id: this.formEnvironment.id,
externalUrl: this.formEnvironment.externalUrl,
clusterAgentId: this.formEnvironment.clusterAgentId,
+ kubernetesNamespace: this.formEnvironment.kubernetesNamespace,
},
},
});
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
index 727d86fbb55..58e57f365eb 100644
--- a/app/assets/javascripts/environments/components/environment_form.vue
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -7,6 +7,7 @@ import {
GlCollapsibleListbox,
GlLink,
GlSprintf,
+ GlAlert,
} from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { isAbsolute } from '~/lib/utils/url_utility';
@@ -15,6 +16,10 @@ import {
ENVIRONMENT_NEW_HELP_TEXT,
ENVIRONMENT_EDIT_HELP_TEXT,
} from 'ee_else_ce/environments/constants';
+import csrf from '~/lib/utils/csrf';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import getNamespacesQuery from '../graphql/queries/k8s_namespaces.query.graphql';
import getUserAuthorizedAgents from '../graphql/queries/user_authorized_agents.query.graphql';
export default {
@@ -26,10 +31,13 @@ export default {
GlCollapsibleListbox,
GlLink,
GlSprintf,
+ GlAlert,
},
+ mixins: [glFeatureFlagsMixin()],
inject: {
protectedEnvironmentSettingsPath: { default: '' },
projectPath: { default: '' },
+ kasTunnelUrl: { default: '' },
},
props: {
environment: {
@@ -62,6 +70,8 @@ export default {
urlFeedback: __('The URL should start with http:// or https://'),
agentLabel: s__('Environments|GitLab agent'),
agentHelpText: s__('Environments|Select agent'),
+ namespaceLabel: s__('Environments|Kubernetes namespace (optional)'),
+ namespaceHelpText: s__('Environments|Select namespace'),
save: __('Save'),
cancel: __('Cancel'),
reset: __('Reset'),
@@ -79,10 +89,41 @@ export default {
userAccessAuthorizedAgents: [],
loadingAgentsList: false,
selectedAgentId: this.environment.clusterAgentId,
- searchTerm: '',
+ agentSearchTerm: '',
+ selectedNamespace: this.environment.kubernetesNamespace,
+ k8sNamespaces: [],
+ namespaceSearchTerm: '',
+ kubernetesError: '',
};
},
+ apollo: {
+ k8sNamespaces: {
+ query: getNamespacesQuery,
+ skip() {
+ return !this.showNamespaceSelector;
+ },
+ variables() {
+ return {
+ configuration: this.k8sAccessConfiguration,
+ };
+ },
+ update(data) {
+ return data?.k8sNamespaces || [];
+ },
+ error(error) {
+ this.kubernetesError = error.message;
+ },
+ result(result) {
+ if (!result?.error && !result.errors?.length) {
+ this.kubernetesError = null;
+ }
+ },
+ },
+ },
computed: {
+ loadingNamespacesList() {
+ return this.$apollo.queries.k8sNamespaces.loading;
+ },
isNameDisabled() {
return Boolean(this.environment.id);
},
@@ -103,7 +144,7 @@ export default {
};
});
},
- dropdownToggleText() {
+ agentDropdownToggleText() {
if (!this.selectedAgentId) {
return this.$options.i18n.agentHelpText;
}
@@ -113,11 +154,56 @@ export default {
return selectedAgentById?.text || this.environment.clusterAgent?.name;
},
filteredAgentsList() {
- const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
+ const lowerCasedSearchTerm = this.agentSearchTerm.toLowerCase();
return this.agentsList.filter((item) =>
item.text.toLowerCase().includes(lowerCasedSearchTerm),
);
},
+ namespacesList() {
+ return this.k8sNamespaces.map((item) => {
+ return {
+ value: item.metadata.name,
+ text: item.metadata.name,
+ };
+ });
+ },
+ filteredNamespacesList() {
+ const lowerCasedSearchTerm = this.namespaceSearchTerm.toLowerCase();
+ return this.namespacesList.filter((item) =>
+ item.text.toLowerCase().includes(lowerCasedSearchTerm),
+ );
+ },
+ isKasUserAccessAvailable() {
+ return this.glFeatures?.kasUserAccessProject;
+ },
+ isKasKubernetesNamespaceAvailable() {
+ return this.glFeatures?.kubernetesNamespaceForEnvironment;
+ },
+ showNamespaceSelector() {
+ return Boolean(
+ this.isKasUserAccessAvailable &&
+ this.isKasKubernetesNamespaceAvailable &&
+ this.selectedAgentId,
+ );
+ },
+ namespaceDropdownToggleText() {
+ return this.selectedNamespace || this.$options.i18n.namespaceHelpText;
+ },
+ k8sAccessConfiguration() {
+ if (!this.showNamespaceSelector) {
+ return null;
+ }
+ return {
+ basePath: this.kasTunnelUrl,
+ baseOptions: {
+ headers: {
+ 'GitLab-Agent-Id': getIdFromGraphQLId(this.selectedAgentId),
+ ...csrf.headers,
+ },
+ withCredentials: true,
+ },
+ };
+ },
},
watch: {
environment(change) {
@@ -146,7 +232,14 @@ export default {
});
},
onAgentSearch(search) {
- this.searchTerm = search;
+ this.agentSearchTerm = search;
+ },
+ onAgentChange($event) {
+ this.selectedNamespace = null;
+ this.onChange({ ...this.environment, clusterAgentId: $event, kubernetesNamespace: null });
+ },
+ onNamespaceSearch(search) {
+ this.namespaceSearchTerm = search;
},
},
};
@@ -225,20 +318,48 @@ export default {
id="environment_agent"
v-model="selectedAgentId"
class="gl-w-full"
+ data-testid="agent-selector"
block
:items="filteredAgentsList"
:loading="loadingAgentsList"
- :toggle-text="dropdownToggleText"
+ :toggle-text="agentDropdownToggleText"
:header-text="$options.i18n.agentHelpText"
:reset-button-label="$options.i18n.reset"
:searchable="true"
@shown="getAgentsList"
@search="onAgentSearch"
- @select="onChange({ ...environment, clusterAgentId: $event })"
+ @select="onAgentChange"
@reset="onChange({ ...environment, clusterAgentId: null })"
/>
</gl-form-group>
+ <gl-form-group
+ v-if="showNamespaceSelector"
+ :label="$options.i18n.namespaceLabel"
+ label-for="environment_namespace"
+ >
+ <gl-alert v-if="kubernetesError" variant="warning" :dismissible="false" class="gl-mb-5">
+ {{ kubernetesError }}
+ </gl-alert>
+ <gl-collapsible-listbox
+ v-else
+ id="environment_namespace"
+ v-model="selectedNamespace"
+ class="gl-w-full"
+ data-testid="namespace-selector"
+ block
+ :items="filteredNamespacesList"
+ :loading="loadingNamespacesList"
+ :toggle-text="namespaceDropdownToggleText"
+ :header-text="$options.i18n.namespaceHelpText"
+ :reset-button-label="$options.i18n.reset"
+ :searchable="true"
+ @search="onNamespaceSearch"
+ @select="onChange({ ...environment, kubernetesNamespace: $event })"
+ @reset="onChange({ ...environment, kubernetesNamespace: null })"
+ />
+ </gl-form-group>
+
<div class="gl-mr-6">
<gl-button
:loading="loading"
diff --git a/app/assets/javascripts/environments/components/new_environment.vue b/app/assets/javascripts/environments/components/new_environment.vue
index 464b530b503..c6bc94b0b80 100644
--- a/app/assets/javascripts/environments/components/new_environment.vue
+++ b/app/assets/javascripts/environments/components/new_environment.vue
@@ -34,6 +34,7 @@ export default {
externalUrl: this.environment.externalUrl,
projectPath: this.projectPath,
clusterAgentId: this.environment.clusterAgentId,
+ kubernetesNamespace: this.environment.kubernetesNamespace,
},
},
});
diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue
index 1f3d429cc3e..aa2960c810a 100644
--- a/app/assets/javascripts/environments/components/new_environment_item.vue
+++ b/app/assets/javascripts/environments/components/new_environment_item.vue
@@ -14,6 +14,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import isLastDeployment from '../graphql/queries/is_last_deployment.query.graphql';
import getEnvironmentClusterAgent from '../graphql/queries/environment_cluster_agent.query.graphql';
+import getEnvironmentClusterAgentWithNamespace from '../graphql/queries/environment_cluster_agent_with_namespace.query.graphql';
import ExternalUrl from './environment_external_url.vue';
import Actions from './environment_actions.vue';
import StopComponent from './environment_stop.vue';
@@ -82,7 +83,7 @@ export default {
tierTooltip: s__('Environment|Deployment tier'),
},
data() {
- return { visible: false, clusterAgent: null };
+ return { visible: false, clusterAgent: null, kubernetesNamespace: '' };
},
computed: {
icon() {
@@ -167,6 +168,9 @@ export default {
isKubernetesOverviewAvailable() {
return this.glFeatures?.kasUserAccessProject;
},
+ isKubernetesNamespaceAvailable() {
+ return this.glFeatures?.kubernetesNamespaceForEnvironment;
+ },
showKubernetesOverview() {
return Boolean(this.isKubernetesOverviewAvailable && this.clusterAgent);
},
@@ -186,9 +190,14 @@ export default {
variables() {
return { environmentName: this.environment.name, projectFullPath: this.projectPath };
},
- query: getEnvironmentClusterAgent,
+ query() {
+ return this.isKubernetesNamespaceAvailable
+ ? getEnvironmentClusterAgentWithNamespace
+ : getEnvironmentClusterAgent;
+ },
update(data) {
this.clusterAgent = data?.project?.environment?.clusterAgent;
+ this.kubernetesNamespace = data?.project?.environment?.kubernetesNamespace || '';
},
});
},
@@ -369,10 +378,7 @@ export default {
</gl-sprintf>
</div>
<div v-if="showKubernetesOverview" :class="$options.kubernetesOverviewClasses">
- <kubernetes-overview
- :cluster-agent="clusterAgent"
- :namespace="environment.kubernetesNamespace"
- />
+ <kubernetes-overview :cluster-agent="clusterAgent" :namespace="kubernetesNamespace" />
</div>
<div v-if="rolloutStatus" :class="$options.deployBoardClasses">
<deploy-board-wrapper
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index 2b178964c37..dc9481a5429 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -108,3 +108,19 @@ export const PHASE_RUNNING = 'Running';
export const PHASE_PENDING = 'Pending';
export const PHASE_SUCCEEDED = 'Succeeded';
export const PHASE_FAILED = 'Failed';
+
+const ERROR_UNAUTHORIZED = 'unauthorized';
+const ERROR_FORBIDDEN = 'forbidden';
+const ERROR_NOT_FOUND = 'not found';
+const ERROR_OTHER = 'other';
+
+export const CLUSTER_AGENT_ERROR_MESSAGES = {
+ [ERROR_UNAUTHORIZED]: s__(
+ 'Environment|Unauthorized to access the cluster agent from this environment. Check your authentication and try again.',
+ ),
+ [ERROR_FORBIDDEN]: s__(
+ 'Environment|Forbidden to access the cluster agent from this environment.',
+ ),
+ [ERROR_NOT_FOUND]: s__('Environment|Cluster agent not found.'),
+ [ERROR_OTHER]: s__('Environment|There was an error connecting to the cluster agent.'),
+};
diff --git a/app/assets/javascripts/environments/edit.js b/app/assets/javascripts/environments/edit.js
index f936085af15..3f22b83e618 100644
--- a/app/assets/javascripts/environments/edit.js
+++ b/app/assets/javascripts/environments/edit.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { removeLastSlashInUrlPath } from '~/lib/utils/url_utility';
import EditEnvironment from './components/edit_environment.vue';
import { apolloProvider } from './graphql/client';
@@ -15,6 +16,7 @@ export default (el) => {
protectedEnvironmentSettingsPath,
projectPath,
environmentName,
+ kasTunnelUrl,
} = el.dataset;
return new Vue({
@@ -25,6 +27,7 @@ export default (el) => {
protectedEnvironmentSettingsPath,
projectPath,
environmentName,
+ kasTunnelUrl: removeLastSlashInUrlPath(kasTunnelUrl),
},
render(h) {
return h(EditEnvironment);
diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js
index 6d06cff06b9..553b06e632f 100644
--- a/app/assets/javascripts/environments/graphql/client.js
+++ b/app/assets/javascripts/environments/graphql/client.js
@@ -8,6 +8,7 @@ import environmentToStopQuery from './queries/environment_to_stop.query.graphql'
import k8sPodsQuery from './queries/k8s_pods.query.graphql';
import k8sServicesQuery from './queries/k8s_services.query.graphql';
import k8sWorkloadsQuery from './queries/k8s_workloads.query.graphql';
+import k8sNamespacesQuery from './queries/k8s_namespaces.query.graphql';
import { resolvers } from './resolvers';
import typeDefs from './typedefs.graphql';
@@ -161,6 +162,14 @@ export const apolloProvider = (endpoint) => {
},
},
});
+ cache.writeQuery({
+ query: k8sNamespacesQuery,
+ data: {
+ metadata: {
+ name: null,
+ },
+ },
+ });
return new VueApollo({
defaultClient,
});
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_namespace.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_namespace.query.graphql
new file mode 100644
index 00000000000..5e72c2dac20
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_namespace.query.graphql
@@ -0,0 +1,20 @@
+query getEnvironmentClusterAgentWithNamespace($projectFullPath: ID!, $environmentName: String) {
+ project(fullPath: $projectFullPath) {
+ id
+ environment(name: $environmentName) {
+ id
+ kubernetesNamespace
+ clusterAgent {
+ id
+ name
+ webPath
+ tokens {
+ nodes {
+ id
+ lastUsedAt
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_with_namespace.graphql b/app/assets/javascripts/environments/graphql/queries/environment_with_namespace.graphql
new file mode 100644
index 00000000000..42796f982b6
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/environment_with_namespace.graphql
@@ -0,0 +1,15 @@
+query getEnvironmentWithNamespace($projectFullPath: ID!, $environmentName: String) {
+ project(fullPath: $projectFullPath) {
+ id
+ environment(name: $environmentName) {
+ id
+ name
+ externalUrl
+ kubernetesNamespace
+ clusterAgent {
+ id
+ name
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/k8s_namespaces.query.graphql b/app/assets/javascripts/environments/graphql/queries/k8s_namespaces.query.graphql
new file mode 100644
index 00000000000..c05d09b6ca2
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/k8s_namespaces.query.graphql
@@ -0,0 +1,7 @@
+query getK8sNamespaces($configuration: LocalConfiguration) {
+ k8sNamespaces(configuration: $configuration) @client {
+ metadata {
+ name
+ }
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js
index 044e7927606..8cfe44c5a05 100644
--- a/app/assets/javascripts/environments/graphql/resolvers.js
+++ b/app/assets/javascripts/environments/graphql/resolvers.js
@@ -6,6 +6,7 @@ import {
parseIntPagination,
normalizeHeaders,
} from '~/lib/utils/common_utils';
+import { humanizeClusterErrors } from '../helpers/k8s_integration_helper';
import pollIntervalQuery from './queries/poll_interval.query.graphql';
import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql';
@@ -72,6 +73,11 @@ const mapWorkloadItems = (items, kind) => {
});
};
+const handleClusterError = (err) => {
+ const error = err?.response?.data?.message ? new Error(err.response.data.message) : err;
+ throw error;
+};
+
export const resolvers = (endpoint) => ({
Query: {
environmentApp(_context, { page, scope, search }, { cache }) {
@@ -124,8 +130,7 @@ export const resolvers = (endpoint) => ({
return podsApi
.then((res) => res?.data?.items || [])
.catch((err) => {
- const error = err?.response?.data?.message ? new Error(err.response.data.message) : err;
- throw error;
+ handleClusterError(err);
});
},
k8sServices(_, { configuration }) {
@@ -148,8 +153,7 @@ export const resolvers = (endpoint) => ({
});
})
.catch((err) => {
- const error = err?.response?.data?.message ? new Error(err.response.data.message) : err;
- throw error;
+ handleClusterError(err);
});
},
k8sWorkloads(_, { configuration, namespace }) {
@@ -206,6 +210,19 @@ export const resolvers = (endpoint) => ({
return summaryList;
});
},
+ k8sNamespaces(_, { configuration }) {
+ const coreV1Api = new CoreV1Api(new Configuration(configuration));
+ const namespacesApi = coreV1Api.listCoreV1Namespace();
+
+ return namespacesApi
+ .then((res) => {
+ return res?.data?.items || [];
+ })
+ .catch((err) => {
+ const error = err?.response?.data?.reason || err;
+ throw new Error(humanizeClusterErrors(error));
+ });
+ },
},
Mutation: {
stopEnvironmentREST(_, { environment }, { client }) {
diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql
index 7e46385946f..e2c22dda554 100644
--- a/app/assets/javascripts/environments/graphql/typedefs.graphql
+++ b/app/assets/javascripts/environments/graphql/typedefs.graphql
@@ -160,6 +160,12 @@ type LocalK8sWorkloads {
JobList: [localK8sJob]
CronJobList: [localK8sCronJob]
}
+type k8sNamespaceMetadata {
+ name: String
+}
+type LocalK8sNamespaces {
+ metadata: k8sNamespaceMetadata
+}
extend type Query {
environmentApp(page: Int, scope: String): LocalEnvironmentApp
diff --git a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
index 45c65c93a91..e49f1451759 100644
--- a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
+++ b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
@@ -1,4 +1,5 @@
import { differenceInSeconds } from '~/lib/utils/datetime_utility';
+import { CLUSTER_AGENT_ERROR_MESSAGES } from '../constants';
export function generateServicePortsString(ports) {
if (!ports?.length) return '';
@@ -139,3 +140,9 @@ export function getCronJobsStatuses(items) {
...(ready.length && { ready }),
};
}
+
+export function humanizeClusterErrors(reason) {
+ const errorReason = reason.toLowerCase();
+ const errorMessage = CLUSTER_AGENT_ERROR_MESSAGES[errorReason];
+ return errorMessage || CLUSTER_AGENT_ERROR_MESSAGES.other;
+}
diff --git a/app/assets/javascripts/environments/new.js b/app/assets/javascripts/environments/new.js
index 5dd112ac5e6..652085b1f28 100644
--- a/app/assets/javascripts/environments/new.js
+++ b/app/assets/javascripts/environments/new.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { removeLastSlashInUrlPath } from '~/lib/utils/url_utility';
import NewEnvironment from './components/new_environment.vue';
import { apolloProvider } from './graphql/client';
@@ -10,12 +11,16 @@ export default (el) => {
return null;
}
- const { projectEnvironmentsPath, projectPath } = el.dataset;
+ const { projectEnvironmentsPath, projectPath, kasTunnelUrl } = el.dataset;
return new Vue({
el,
apolloProvider: apolloProvider(),
- provide: { projectEnvironmentsPath, projectPath },
+ provide: {
+ projectEnvironmentsPath,
+ projectPath,
+ kasTunnelUrl: removeLastSlashInUrlPath(kasTunnelUrl),
+ },
render(h) {
return h(NewEnvironment);
},