Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/utils.ts
2498 views
1
/**
2
* Copyright (c) 2020 Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
import { User } from "@gitpod/public-api/lib/gitpod/v1/user_pb";
8
import { AuthProviderDescription, AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
9
import EventEmitter from "events";
10
import { uniq } from "lodash";
11
12
export interface PollOptions<T> {
13
backoffFactor: number;
14
retryUntilSeconds: number;
15
16
stop?: () => void;
17
success: (result?: T) => void;
18
19
token?: { cancelled?: boolean };
20
}
21
22
export const poll = async <T>(
23
initialDelayInSeconds: number,
24
callback: () => Promise<{ done: boolean; result?: T }>,
25
opts: PollOptions<T>,
26
) => {
27
const start = new Date();
28
let delayInSeconds = initialDelayInSeconds;
29
30
while (true) {
31
const runSinceSeconds = (new Date().getTime() - start.getTime()) / 1000;
32
if (runSinceSeconds > opts.retryUntilSeconds) {
33
if (opts.stop) {
34
opts.stop();
35
}
36
return;
37
}
38
// eslint-disable-next-line no-loop-func
39
await new Promise((resolve) => setTimeout(resolve, delayInSeconds * 1000));
40
if (opts.token?.cancelled) {
41
return;
42
}
43
44
const { done, result } = await callback();
45
if (opts.token?.cancelled) {
46
return;
47
}
48
49
if (done) {
50
opts.success(result);
51
return;
52
} else {
53
delayInSeconds = opts.backoffFactor * delayInSeconds;
54
}
55
}
56
};
57
58
export function isGitpodIo() {
59
return (
60
window.location.hostname === "gitpod.io" ||
61
window.location.hostname === "gitpod-staging.com" ||
62
window.location.hostname.endsWith("gitpod-dev.com") ||
63
window.location.hostname.endsWith("gitpod-io-dev.com")
64
);
65
}
66
67
function trimResource(resource: string): string {
68
return resource.split("/").filter(Boolean).join("/");
69
}
70
71
// Returns 'true' if a 'pathname' is a part of 'resources' provided.
72
// `inResource("/app/testing/", ["new", "app", "teams"])` will return true
73
// because '/app/testing' is a part of root 'app'
74
//
75
// 'pathname' arg can be provided via `location.pathname`.
76
export function inResource(pathname: string, resources: string[]): boolean {
77
// Removes leading and trailing '/'
78
const trimmedResource = trimResource(pathname);
79
80
// Checks if a path is part of a resource.
81
// E.g. "api/userspace/resource" path is a part of resource "api/userspace"
82
return resources.map((res) => trimmedResource.startsWith(trimResource(res))).some(Boolean);
83
}
84
85
export const copyToClipboard = async (data: string) => {
86
await navigator.clipboard.writeText(data);
87
};
88
89
export function getURLHash() {
90
return window.location.hash.replace(/^[#/]+/, "");
91
}
92
93
export function isWebsiteSlug(pathName: string) {
94
const slugs = [
95
"about",
96
"blog",
97
"careers",
98
"cde",
99
"changelog",
100
"chat",
101
"code-of-conduct",
102
"contact",
103
"community",
104
"docs",
105
"events",
106
"features",
107
"for",
108
"gitpod-vs-github-codespaces",
109
"guides",
110
"imprint",
111
"media-kit",
112
"memes",
113
"pricing",
114
"privacy",
115
"security",
116
"screencasts",
117
"self-hosted",
118
"support",
119
"terms",
120
"values",
121
"webinars",
122
];
123
return slugs.some((slug) => pathName.startsWith("/" + slug + "/") || pathName === "/" + slug);
124
}
125
126
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#testing_for_availability
127
export function storageAvailable(type: "localStorage" | "sessionStorage"): boolean {
128
let storage;
129
try {
130
storage = window[type];
131
const x = "__storage_test__";
132
storage.setItem(x, x);
133
storage.removeItem(x);
134
return true;
135
} catch (e) {
136
if (!storage) {
137
return false;
138
}
139
140
return (
141
e instanceof DOMException &&
142
// everything except Firefox
143
(e.code === 22 ||
144
// Firefox
145
e.code === 1014 ||
146
// test name field too, because code might not be present
147
// everything except Firefox
148
e.name === "QuotaExceededError" ||
149
// Firefox
150
e.name === "NS_ERROR_DOM_QUOTA_REACHED") &&
151
// acknowledge QuotaExceededError only if there's something already stored
152
storage &&
153
storage.length !== 0
154
);
155
}
156
}
157
158
type EventMap = Record<string, any[]>;
159
export class ReplayableEventEmitter<EventTypes extends EventMap> extends EventEmitter {
160
private eventLog: { [K in keyof EventTypes]?: EventTypes[K][] } = {};
161
private reachedEnd = false;
162
163
emit(event: string | symbol, ...args: any[]): boolean;
164
emit<K extends keyof EventTypes>(event: K, ...args: EventTypes[K]): boolean;
165
emit(event: string | symbol, ...args: any[]): boolean {
166
const eventName = event as keyof EventTypes;
167
if (this.eventLog[eventName]) {
168
this.eventLog[eventName]!.push(args as any);
169
} else {
170
this.eventLog[eventName] = [args as any];
171
}
172
return super.emit(event, ...args);
173
}
174
175
on(event: string | symbol, listener: (...args: any[]) => void): this;
176
on<K extends keyof EventTypes>(event: K, listener: (...args: EventTypes[K]) => void): this;
177
on(event: string | symbol, listener: (...args: any[]) => void): this {
178
const eventName = event as keyof EventTypes;
179
const eventLog = this.eventLog[eventName];
180
if (eventLog) {
181
for (const args of eventLog) {
182
listener(...args);
183
}
184
}
185
super.on(event, listener);
186
return this;
187
}
188
189
once(event: string | symbol, listener: (...args: any[]) => void): this;
190
once<K extends keyof EventTypes>(event: K, listener: (...args: EventTypes[K]) => void): this;
191
once(event: string | symbol, listener: (...args: any[]) => void): this {
192
const eventName = event as keyof EventTypes;
193
const eventLog = this.eventLog[eventName];
194
if (eventLog) {
195
for (const args of eventLog) {
196
listener(...args);
197
}
198
}
199
super.once(event, listener);
200
return this;
201
}
202
203
clearLog(event?: keyof EventTypes): void {
204
if (event) {
205
delete this.eventLog[event];
206
} else {
207
this.eventLog = {};
208
}
209
}
210
211
markReachedEnd() {
212
this.reachedEnd = true;
213
}
214
215
hasReachedEnd() {
216
return this.reachedEnd;
217
}
218
}
219
220
export function parseUrl(url: string): URL | null {
221
try {
222
return new URL(url);
223
} catch (_) {
224
return null;
225
}
226
}
227
228
export function isTrustedUrlOrPath(urlOrPath: string) {
229
const url = parseUrl(urlOrPath);
230
const isTrusted = url
231
? window.location.hostname === url.hostname && url.protocol === "https:"
232
: urlOrPath.startsWith("/");
233
if (!isTrusted) {
234
console.warn("Untrusted URL", urlOrPath);
235
}
236
return isTrusted;
237
}
238
239
type UnifiedAuthProvider = "Bitbucket" | "GitLab" | "GitHub" | "Azure DevOps";
240
const unifyProviderType = (type: AuthProviderType): UnifiedAuthProvider | undefined => {
241
switch (type) {
242
case AuthProviderType.BITBUCKET:
243
case AuthProviderType.BITBUCKET_SERVER:
244
return "Bitbucket";
245
case AuthProviderType.GITHUB:
246
return "GitHub";
247
case AuthProviderType.GITLAB:
248
return "GitLab";
249
case AuthProviderType.AZURE_DEVOPS:
250
return "Azure DevOps";
251
default:
252
return undefined;
253
}
254
};
255
256
export const getDeduplicatedScmProviders = (
257
user: User,
258
descriptions: AuthProviderDescription[],
259
): UnifiedAuthProvider[] => {
260
const userIdentities = user.identities.map((identity) => identity.authProviderId);
261
const userProviders = userIdentities
262
.map((id) => descriptions?.find((provider) => provider.id === id))
263
.filter((p) => !!p)
264
.map((provider) => provider.type);
265
266
const unifiedProviders = userProviders
267
.map((type) => unifyProviderType(type))
268
.filter((t) => !!t)
269
.sort();
270
271
return uniq(unifiedProviders);
272
};
273
274
export const disjunctScmProviders = (providers: UnifiedAuthProvider[]): string => {
275
const formatter = new Intl.ListFormat("en", { style: "long", type: "disjunction" });
276
277
return formatter.format(providers);
278
};
279
280
export const conjunctScmProviders = (providers: UnifiedAuthProvider[]): string => {
281
const formatter = new Intl.ListFormat("en", { style: "long", type: "conjunction" });
282
283
return formatter.format(providers);
284
};
285
286