Add prettier as a dev dependency, add prettier ignore file, and run for all .ts .html and .scss files in app
This commit is contained in:
1 parent
e663c1fa8c
commit
08145fb806
142 files changed
+2521
-2013
No files matched your search
@@ -0,0 +1,4 @@
|
||||
# Ignore openapi files
|
||||
src/app/api/api-base.service.ts
|
||||
src/app/api/api-schema.d.ts
|
||||
src/app/api/allauth-schema.d.ts
|
||||
Generated
+17
@@ -87,6 +87,7 @@
|
||||
"karma-jasmine-html-reporter": "^2.1.0",
|
||||
"ng-extract-i18n-merge": "^2.14.3",
|
||||
"openapi-typescript": "^7.6.1",
|
||||
"prettier": "3.5.3",
|
||||
"storybook": "^8.6.4",
|
||||
"storybook-dark-mode": "^4.0.2",
|
||||
"ts-loader": "^9.2.6",
|
||||
@@ -19494,6 +19495,22 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-bytes": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"karma-jasmine-html-reporter": "^2.1.0",
|
||||
"ng-extract-i18n-merge": "^2.14.3",
|
||||
"openapi-typescript": "^7.6.1",
|
||||
"prettier": "3.5.3",
|
||||
"storybook": "^8.6.4",
|
||||
"storybook-dark-mode": "^4.0.2",
|
||||
"ts-loader": "^9.2.6",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, RouterLink } from "@angular/router";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
import { MatCardModule } from "@angular/material/card";
|
||||
|
||||
@@ -29,7 +29,7 @@ export class AcceptInviteService {
|
||||
private readonly state = new BehaviorSubject<AcceptState>(initialState);
|
||||
readonly acceptInfo$ = this.state.pipe(map((state) => state.acceptInfo));
|
||||
readonly orgSlug$ = this.acceptInfo$.pipe(
|
||||
map((acceptInfo) => acceptInfo?.orgUser.organization.slug)
|
||||
map((acceptInfo) => acceptInfo?.orgUser.organization.slug),
|
||||
);
|
||||
readonly alreadyInOrg$ = combineLatest([
|
||||
this.orgSlug$,
|
||||
@@ -38,7 +38,7 @@ export class AcceptInviteService {
|
||||
map(([orgSlugToMatch, organizations]) => {
|
||||
if (orgSlugToMatch) {
|
||||
const match = organizations.find(
|
||||
(organization) => organization.slug === orgSlugToMatch
|
||||
(organization) => organization.slug === orgSlugToMatch,
|
||||
);
|
||||
if (match) {
|
||||
return true;
|
||||
@@ -47,7 +47,7 @@ export class AcceptInviteService {
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
private readonly url = baseUrl + "/accept/";
|
||||
|
||||
@@ -68,7 +68,7 @@ export class AcceptInviteService {
|
||||
}
|
||||
this.router.navigate(["/"]);
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
@@ -77,9 +77,9 @@ export class AcceptInviteService {
|
||||
this.postAcceptInvite(memberId, token)
|
||||
.pipe(
|
||||
tap((response: AcceptAPIResponse) => {
|
||||
this.organizationsService.reload()
|
||||
this.organizationsService.reload();
|
||||
this.snackBar.open(
|
||||
`You have been added to ${response.orgUser.organization.name}.`
|
||||
`You have been added to ${response.orgUser.organization.name}.`,
|
||||
);
|
||||
this.router.navigate(["/"]);
|
||||
}),
|
||||
@@ -104,7 +104,7 @@ export class AcceptInviteService {
|
||||
this.snackBar.open(error.error?.detail);
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
@@ -118,7 +118,7 @@ export class AcceptInviteService {
|
||||
`${this.url}${memberId}/${token}/`,
|
||||
{
|
||||
acceptInvite: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ interface RecoveryCodes {
|
||||
export class AccountService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
|
||||
changePassword(current_password: string, new_password: string) {
|
||||
return this.http.post<AllAuthSessionResponse>(
|
||||
baseUrl + "/password/change",
|
||||
|
||||
@@ -36,7 +36,6 @@ function postForm(action: string, data: JsonObject) {
|
||||
export class AuthenticationService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
|
||||
getAuthenticationStatus() {
|
||||
return this.http.get<AllAuthSessionResponse>(baseUrl + "/session");
|
||||
}
|
||||
@@ -55,7 +54,7 @@ export class AuthenticationService {
|
||||
mfaAuthenticate(code: string) {
|
||||
return this.http.post<AllAuthSessionResponse>(
|
||||
baseUrl + "/2fa/authenticate",
|
||||
{ code }
|
||||
{ code },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,7 +72,7 @@ export class AuthenticationService {
|
||||
headers: {
|
||||
"X-Email-Verification-Key": key,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,7 +96,7 @@ export class AuthenticationService {
|
||||
|
||||
resetPassword(
|
||||
key: string,
|
||||
password: string
|
||||
password: string,
|
||||
): Observable<AllAuthSessionResponse> {
|
||||
return this.http
|
||||
.post<AllAuthSessionResponse>(baseUrl + "/password/reset", {
|
||||
@@ -111,14 +110,14 @@ export class AuthenticationService {
|
||||
return of(error.error);
|
||||
}
|
||||
return throwError(() => error);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
providerRedirect(
|
||||
provider: string,
|
||||
callbackUrl = "/",
|
||||
process: "login" | "connect" = "login"
|
||||
process: "login" | "connect" = "login",
|
||||
) {
|
||||
postForm(baseUrl + "/provider/redirect", {
|
||||
provider,
|
||||
@@ -130,14 +129,14 @@ export class AuthenticationService {
|
||||
|
||||
getWebAuthnCredentialRequest() {
|
||||
return this.http.get<GetWebAuthnCredentialRequestResponse>(
|
||||
baseUrl + "/webauthn/authenticate"
|
||||
baseUrl + "/webauthn/authenticate",
|
||||
);
|
||||
}
|
||||
|
||||
perform2FAWebAuthn(credential: AuthenticationPublicKeyCredential) {
|
||||
return this.http.post<AllAuthSessionResponse>(
|
||||
baseUrl + "/webauthn/authenticate",
|
||||
{ credential }
|
||||
{ credential },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export class APITokenService extends APIBaseService {
|
||||
const http = inject(HttpClient);
|
||||
|
||||
super(http);
|
||||
|
||||
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export class EnvironmentsAPIService extends APIBaseService {
|
||||
const http = inject(HttpClient);
|
||||
|
||||
super(http);
|
||||
|
||||
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export class IssuesAPIService extends APIBaseService {
|
||||
const http = inject(HttpClient);
|
||||
|
||||
super(http);
|
||||
|
||||
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export class MonitorChecksAPIService {
|
||||
organizationSlug: string,
|
||||
monitorId: string,
|
||||
cursor?: string | null,
|
||||
isChange = true
|
||||
isChange = true,
|
||||
) {
|
||||
let httpParams = new HttpParams();
|
||||
const url = `${baseUrl}/organizations/${organizationSlug}/monitors/${monitorId}/checks/`;
|
||||
|
||||
@@ -21,7 +21,7 @@ export class OrganizationsService {
|
||||
#activeOrganizationSlug = signal<string | null>(null);
|
||||
activeOrganizationSlug = computed(
|
||||
() =>
|
||||
this.#activeOrganizationSlug() ?? this.organizations()?.[0]?.slug ?? null
|
||||
this.#activeOrganizationSlug() ?? this.organizations()?.[0]?.slug ?? null,
|
||||
);
|
||||
organizationsResource = resource({
|
||||
request: () => ({ isAuthenticated: this.authService.isAuthenticated() }),
|
||||
@@ -45,7 +45,7 @@ export class OrganizationsService {
|
||||
params: {
|
||||
path: { organization_slug: request.organization_slug },
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
if (error) {
|
||||
throw error;
|
||||
@@ -57,16 +57,16 @@ export class OrganizationsService {
|
||||
organizationsCount = computed(() => this.organizations.length);
|
||||
activeOrganization = computed(() => this.activeOrganizationResource.value());
|
||||
activeOrganizationLoaded = computed(
|
||||
() => this.activeOrganizationResource.status() >= ResourceStatus.Resolved
|
||||
() => this.activeOrganizationResource.status() >= ResourceStatus.Resolved,
|
||||
);
|
||||
activeOrganizationProjects = computed(
|
||||
() => this.activeOrganization()?.projects || []
|
||||
() => this.activeOrganization()?.projects || [],
|
||||
);
|
||||
projectsCount = computed(() => this.activeOrganizationProjects().length);
|
||||
initialLoad = computed(
|
||||
() =>
|
||||
this.organizationsResource.status() > ResourceStatus.Loading &&
|
||||
this.activeOrganizationResource.status() > ResourceStatus.Loading
|
||||
this.activeOrganizationResource.status() > ResourceStatus.Loading,
|
||||
);
|
||||
|
||||
// For compatibility, remove when possible
|
||||
@@ -97,7 +97,7 @@ export class OrganizationsService {
|
||||
});
|
||||
if (data) {
|
||||
this.organizationsResource.update((orgs) =>
|
||||
orgs ? [...orgs, data] : [data]
|
||||
orgs ? [...orgs, data] : [data],
|
||||
);
|
||||
}
|
||||
return { data, error };
|
||||
@@ -114,8 +114,8 @@ export class OrganizationsService {
|
||||
takeUntil(interval(2000).pipe(takeUntil(interval(12000)))),
|
||||
takeWhile(
|
||||
(org) =>
|
||||
org?.eventThrottleRate === undefined || org.eventThrottleRate > 0
|
||||
)
|
||||
org?.eventThrottleRate === undefined || org.eventThrottleRate > 0,
|
||||
),
|
||||
)
|
||||
.subscribe(() => this.activeOrganizationResource.reload());
|
||||
}
|
||||
|
||||
@@ -42,10 +42,10 @@ export class MembersService {
|
||||
private readonly state = new BehaviorSubject<MembersState>(initialState);
|
||||
private readonly getState$ = this.state.asObservable();
|
||||
readonly loadingResendInvite$ = this.getState$.pipe(
|
||||
map((state) => state.loadingResendInvite)
|
||||
map((state) => state.loadingResendInvite),
|
||||
);
|
||||
readonly sentResendInvite$ = this.getState$.pipe(
|
||||
map((state) => state.sentResendInvite)
|
||||
map((state) => state.sentResendInvite),
|
||||
);
|
||||
/** Organization members with computed loading/success data */
|
||||
readonly members$ = combineLatest([
|
||||
@@ -66,7 +66,7 @@ export class MembersService {
|
||||
isMe: member.email === activeUserEmail ? true : false,
|
||||
};
|
||||
});
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
/** Send another invite to already invited org member */
|
||||
@@ -82,14 +82,14 @@ export class MembersService {
|
||||
this.organizationsService.activeOrganizationSlug$.pipe(
|
||||
take(1),
|
||||
mergeMap((orgSlug) =>
|
||||
this.membersAPIService.inviteUser(orgSlug!, data as any)
|
||||
this.membersAPIService.inviteUser(orgSlug!, data as any),
|
||||
),
|
||||
tap(() => this.setResendInviteSuccess(member.id)),
|
||||
catchError(() => {
|
||||
this.clearLoadingResendInvite();
|
||||
return EMPTY;
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,16 +104,16 @@ export class MembersService {
|
||||
.pipe(
|
||||
exhaustMap(() => {
|
||||
this.snackBar.open(
|
||||
`Successfully removed ${member.email} from organization`
|
||||
`Successfully removed ${member.email} from organization`,
|
||||
);
|
||||
if (isRemovingSelf) {
|
||||
this.organizationsService.reload()
|
||||
this.organizationsService.setActiveOrganizationSlug(null)
|
||||
this.organizationsService.reload();
|
||||
this.organizationsService.setActiveOrganizationSlug(null);
|
||||
this.router.navigate(["/"]);
|
||||
}
|
||||
if (orgSlug) {
|
||||
return this.organizationDetailService.retrieveOrganizationMembers(
|
||||
orgSlug
|
||||
orgSlug,
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
@@ -129,11 +129,11 @@ export class MembersService {
|
||||
}
|
||||
this.snackBar.open(message);
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
);
|
||||
})
|
||||
}),
|
||||
),
|
||||
{ defaultValue: null }
|
||||
{ defaultValue: null },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
|
||||
readonly initialLoad = computed(() => this.state().initialLoad);
|
||||
readonly organizationMembers = computed(
|
||||
() => this.state().organizationMembers
|
||||
() => this.state().organizationMembers,
|
||||
);
|
||||
|
||||
readonly orgHasAProject = computed(() => {
|
||||
@@ -94,8 +94,8 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
return organizationMembers.filter(
|
||||
(orgMembers) =>
|
||||
!teamMembers.find(
|
||||
(teamMems) => orgMembers.id === teamMems.id.toString()
|
||||
)
|
||||
(teamMems) => orgMembers.id === teamMems.id.toString(),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -110,7 +110,7 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
});
|
||||
|
||||
readonly organizationEnvironments = computed(
|
||||
() => this.state().organizationEnvironments
|
||||
() => this.state().organizationEnvironments,
|
||||
);
|
||||
|
||||
readonly organizationEnvironmentsProcessed = computed(() => {
|
||||
@@ -120,7 +120,7 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
...accumulator,
|
||||
...(!accumulator.includes(environment.name) ? [environment.name] : []),
|
||||
],
|
||||
[] as string[]
|
||||
[] as string[],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -138,14 +138,14 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
.pipe(
|
||||
filter(
|
||||
([billingEnabled, activeOrganization]) =>
|
||||
!!billingEnabled && !!activeOrganization
|
||||
!!billingEnabled && !!activeOrganization,
|
||||
),
|
||||
distinctUntilChanged((a, b) => a[1]?.id === b[1]?.id),
|
||||
tap(([_, activeOrganization]) => {
|
||||
this.subscriptionService.checkIfUserHasSubscription(
|
||||
activeOrganization!.slug
|
||||
activeOrganization!.slug,
|
||||
);
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
@@ -157,7 +157,7 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
return this.organizationAPIService.update(orgSlug, data).pipe(
|
||||
tap((resp) => {
|
||||
this.updateOrgName(resp);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
@@ -172,13 +172,13 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
.then((result) => {
|
||||
if (result.response.status === 204) {
|
||||
this.organizationsService.organizationsResource.update((orgs) =>
|
||||
orgs?.filter((org) => org.slug !== slug)
|
||||
orgs?.filter((org) => org.slug !== slug),
|
||||
);
|
||||
|
||||
const organizations = this.organizationsService.organizations();
|
||||
if (organizations.length) {
|
||||
this.organizationsService.setActiveOrganizationSlug(
|
||||
organizations[0].slug
|
||||
organizations[0].slug,
|
||||
);
|
||||
} else {
|
||||
this.organizationsService.setActiveOrganizationSlug(null);
|
||||
@@ -194,7 +194,7 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
"/api/0/organizations/{organization_slug}/members/",
|
||||
{
|
||||
params: { path: { organization_slug: orgSlug } },
|
||||
}
|
||||
},
|
||||
);
|
||||
if (result.data) {
|
||||
this.setActiveOrganizationMembers(result.data!);
|
||||
@@ -206,7 +206,7 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
inviteOrganizationMembers(
|
||||
emailInput: string,
|
||||
teamsInput: string[],
|
||||
roleInput: MemberRole
|
||||
roleInput: MemberRole,
|
||||
) {
|
||||
const orgSlug = this.organizationsService.activeOrganizationSlug();
|
||||
const data = {
|
||||
@@ -226,19 +226,19 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
.then((result) => {
|
||||
if (result.data) {
|
||||
this.snackBar.open(
|
||||
`An email invite has been sent to ${result.data.email}`
|
||||
`An email invite has been sent to ${result.data.email}`,
|
||||
);
|
||||
this.router.navigate([orgSlug, "settings", "members"]);
|
||||
} else {
|
||||
if (result.response.status === 403) {
|
||||
this.setAddMemberError(
|
||||
"Only organization members with a role of manager or owner can invite new members."
|
||||
"Only organization members with a role of manager or owner can invite new members.",
|
||||
);
|
||||
// } else if (error.detail) {
|
||||
// this.setAddMemberError(error.error?.detail);
|
||||
} else {
|
||||
this.setAddMemberError(
|
||||
"There was an error processing this request."
|
||||
"There was an error processing this request.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -250,8 +250,8 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
this.teamsAPIService.list(orgSlug).pipe(
|
||||
tap((resp) => {
|
||||
this.setOrganizationTeams(resp);
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
{
|
||||
params: { path: { organization_slug: orgSlug } },
|
||||
body: { slug: teamSlug },
|
||||
}
|
||||
},
|
||||
);
|
||||
if (result.data) {
|
||||
this.organizationsService.refreshActiveOrganization();
|
||||
@@ -274,10 +274,10 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
return this.teamsAPIService.addTeamMember(member, orgSlug, teamSlug).pipe(
|
||||
tap((team: Team) => {
|
||||
lastValueFrom(
|
||||
this.teamsService.retrieveTeamMembers(orgSlug, team.slug)
|
||||
this.teamsService.retrieveTeamMembers(orgSlug, team.slug),
|
||||
);
|
||||
this.retrieveOrganizationMembers(orgSlug);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.teamsService.removeMember(memberId);
|
||||
})
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
return EMPTY;
|
||||
@@ -308,8 +308,8 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
this.setLeaveTeamError(error);
|
||||
return EMPTY;
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -325,8 +325,8 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
this.setJoinTeamError(error);
|
||||
return EMPTY;
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -346,7 +346,7 @@ export class OrganizationDetailService extends StatefulService<OrganizationsStat
|
||||
return this.environmentsAPIService.list(orgSlug).pipe(
|
||||
tap((environments) => {
|
||||
this.setOrganizationEnvironments(environments);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export class OrganizationAPIService extends APIBaseService {
|
||||
const http = inject(HttpClient);
|
||||
|
||||
super(http);
|
||||
|
||||
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export class OrganizationAPIService extends APIBaseService {
|
||||
project.id = normalizeID(project.id);
|
||||
});
|
||||
return orgDetail;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export class ProjectAlertsAPIService extends APIBaseService {
|
||||
const http = inject(HttpClient);
|
||||
|
||||
super(http);
|
||||
|
||||
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { ProjectEnvironment } from "../organizations/organizations.interface";
|
||||
export class ProjectEnvironmentsAPIService {
|
||||
protected http = inject(HttpClient);
|
||||
|
||||
|
||||
list(organizationSlug: string, projectSlug: string) {
|
||||
return this.http.get<ProjectEnvironment[]>(
|
||||
this.projectEnvironmentsURL(organizationSlug, projectSlug) +
|
||||
|
||||
@@ -15,7 +15,7 @@ export class ProjectKeysAPIService extends APIBaseService {
|
||||
const http = inject(HttpClient);
|
||||
|
||||
super(http);
|
||||
|
||||
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { normalizeID } from "../shared-api.utils";
|
||||
export class ProjectTeamsAPIService {
|
||||
protected http = inject(HttpClient);
|
||||
|
||||
|
||||
addProjectToTeam(
|
||||
organizationSlug: string,
|
||||
teamSlug: string,
|
||||
|
||||
@@ -19,7 +19,7 @@ export class ProjectsAPIService extends APIBaseService {
|
||||
const http = inject(HttpClient);
|
||||
|
||||
super(http);
|
||||
|
||||
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export class ReleasesAPIService extends APIBaseService {
|
||||
const http = inject(HttpClient);
|
||||
|
||||
super(http);
|
||||
|
||||
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Injectable, inject } from "@angular/core";
|
||||
export class SocialAuthAPIService {
|
||||
protected http = inject(HttpClient);
|
||||
|
||||
|
||||
disconnect(account: string, provider: string) {
|
||||
return this.http.request(
|
||||
"delete",
|
||||
|
||||
@@ -55,10 +55,10 @@ export class SubscriptionService extends StatefulService<SubscriptionState> {
|
||||
subscriptionLoading = computed(
|
||||
() =>
|
||||
this.subscriptionResource.isLoading() ||
|
||||
this.state().subscriptionRefreshing
|
||||
this.state().subscriptionRefreshing,
|
||||
);
|
||||
subscriptionRefreshTimeout = computed(
|
||||
() => this.state().subscriptionRefreshTimeout
|
||||
() => this.state().subscriptionRefreshTimeout,
|
||||
);
|
||||
|
||||
eventCountResource = resource({
|
||||
@@ -76,7 +76,7 @@ export class SubscriptionService extends StatefulService<SubscriptionState> {
|
||||
params: {
|
||||
path: { organization_slug: request.orgSlug },
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
if (error) {
|
||||
throw error;
|
||||
@@ -99,7 +99,7 @@ export class SubscriptionService extends StatefulService<SubscriptionState> {
|
||||
|
||||
billingPortalLoading = computed(() => this.state().billingPortalLoading);
|
||||
billingPortalLoadingError = computed(
|
||||
() => this.state().billingPortalLoadingError
|
||||
() => this.state().billingPortalLoadingError,
|
||||
);
|
||||
fromStripe = computed(() => this.state().fromStripe);
|
||||
totalEventsAllowed = computed(() => {
|
||||
@@ -147,11 +147,11 @@ export class SubscriptionService extends StatefulService<SubscriptionState> {
|
||||
params: {
|
||||
path: { organization_slug: orgSlug },
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
if (error) {
|
||||
this.setBillingPortalLoadingError(
|
||||
"Something went wrong. Only organization owners can manage subscription settings."
|
||||
"Something went wrong. Only organization owners can manage subscription settings.",
|
||||
);
|
||||
}
|
||||
if (data) {
|
||||
@@ -185,7 +185,7 @@ export class SubscriptionService extends StatefulService<SubscriptionState> {
|
||||
params: {
|
||||
path: { organization_slug: orgSlug },
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
if (error) {
|
||||
throw error;
|
||||
|
||||
@@ -46,13 +46,13 @@ export class TeamsAPIService extends APIBaseService {
|
||||
addTeamMember(member: any, orgSlug: string, teamSlug: string) {
|
||||
return this.http.post<Team>(
|
||||
this.teamMemberURL(member.id, orgSlug, teamSlug),
|
||||
member
|
||||
member,
|
||||
);
|
||||
}
|
||||
|
||||
removeTeamMember(memberId: number, orgSlug: string, teamSlug: string) {
|
||||
return this.http.delete<Team>(
|
||||
this.teamMemberURL(memberId, orgSlug, teamSlug)
|
||||
this.teamMemberURL(memberId, orgSlug, teamSlug),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export class TeamsService {
|
||||
readonly teams$ = this.getState$.pipe(map((state) => state.teams));
|
||||
readonly team$ = this.getState$.pipe(map((data) => data.team));
|
||||
readonly teamMembers$ = this.getState$.pipe(
|
||||
map((state) => state.teamMembers)
|
||||
map((state) => state.teamMembers),
|
||||
);
|
||||
readonly loading$ = this.getState$.pipe(map((data) => data.loading));
|
||||
readonly errors$ = this.getState$.pipe(map((data) => data.errors));
|
||||
@@ -53,10 +53,10 @@ export class TeamsService {
|
||||
]).pipe(
|
||||
map(([teamMembers, userEmail]) => {
|
||||
const activeTeamMember = teamMembers.find(
|
||||
(teamMember) => teamMember.email === userEmail
|
||||
(teamMember) => teamMember.email === userEmail,
|
||||
);
|
||||
return activeTeamMember?.role;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
retrieveTeamsByOrg(orgSlug: string) {
|
||||
@@ -69,7 +69,7 @@ export class TeamsService {
|
||||
lastValueFrom(
|
||||
this.teamsAPIService
|
||||
.retrieve(orgSlug, teamSlug)
|
||||
.pipe(tap((resp) => this.setSingleTeam(resp)))
|
||||
.pipe(tap((resp) => this.setSingleTeam(resp))),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export class TeamsService {
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
this.setUpdateTeamSlugError(error);
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export class TeamsService {
|
||||
"/api/0/teams/{organization_slug}/{team_slug}/",
|
||||
{
|
||||
params: { path: { organization_slug: orgSlug, team_slug: teamSlug } },
|
||||
}
|
||||
},
|
||||
);
|
||||
this.setDeleteTeamLoading(false);
|
||||
if (result.error) {
|
||||
|
||||
@@ -16,7 +16,7 @@ export class SpansAPIService extends APIBaseService {
|
||||
const http = inject(HttpClient);
|
||||
|
||||
super(http);
|
||||
|
||||
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export class TransactionGroupsAPIService extends APIBaseService {
|
||||
const http = inject(HttpClient);
|
||||
|
||||
super(http);
|
||||
|
||||
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export class TransactionsAPIService extends APIBaseService {
|
||||
const http = inject(HttpClient);
|
||||
|
||||
super(http);
|
||||
|
||||
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export class AuthService {
|
||||
effect(() => {
|
||||
localStorage.setItem(
|
||||
"isAuthenticated",
|
||||
this.isAuthenticated().toString()
|
||||
this.isAuthenticated().toString(),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export class AuthService {
|
||||
}
|
||||
this.initialized.set(true);
|
||||
return throwError(() => new Error("Unable to check auth status"));
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export class AuthService {
|
||||
return this.authenticationService
|
||||
.login(email, password)
|
||||
.pipe(
|
||||
tap((resp) => this.isAuthenticated.set(resp.meta.is_authenticated))
|
||||
tap((resp) => this.isAuthenticated.set(resp.meta.is_authenticated)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ export class AuthService {
|
||||
return this.authenticationService
|
||||
.mfaAuthenticate(code)
|
||||
.pipe(
|
||||
tap((resp) => this.isAuthenticated.set(resp.meta.is_authenticated))
|
||||
tap((resp) => this.isAuthenticated.set(resp.meta.is_authenticated)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,13 +110,13 @@ export class AuthService {
|
||||
return this.authenticationService.getWebAuthnCredentialRequest().pipe(
|
||||
exhaustMap(async (resp) => {
|
||||
return await get(
|
||||
parseRequestOptionsFromJSON(resp.data.request_options)
|
||||
parseRequestOptionsFromJSON(resp.data.request_options),
|
||||
);
|
||||
}),
|
||||
exhaustMap((credential) => {
|
||||
return this.authenticationService.perform2FAWebAuthn(credential);
|
||||
}),
|
||||
tap((resp) => this.isAuthenticated.set(resp.meta.is_authenticated))
|
||||
tap((resp) => this.isAuthenticated.set(resp.meta.is_authenticated)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,14 +124,14 @@ export class AuthService {
|
||||
return this.authenticationService
|
||||
.signup(email, password)
|
||||
.pipe(
|
||||
tap((resp) => this.isAuthenticated.set(resp.meta.is_authenticated))
|
||||
tap((resp) => this.isAuthenticated.set(resp.meta.is_authenticated)),
|
||||
);
|
||||
}
|
||||
|
||||
providerRedirect(
|
||||
provider: string,
|
||||
callbackUrl: string,
|
||||
process: "login" | "connect"
|
||||
process: "login" | "connect",
|
||||
) {
|
||||
this.authenticationService.providerRedirect(provider, callbackUrl, process);
|
||||
}
|
||||
@@ -145,7 +145,7 @@ export class AuthService {
|
||||
return of(EMPTY);
|
||||
}
|
||||
return throwError(() => new Error("Unable to log out"));
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -9,7 +9,7 @@ import {
|
||||
import { AuthService } from "./auth.service";
|
||||
|
||||
export const alreadyLoggedInGuard: CanActivateFn = (
|
||||
next: ActivatedRouteSnapshot
|
||||
next: ActivatedRouteSnapshot,
|
||||
): boolean | UrlTree => {
|
||||
const authService = inject(AuthService);
|
||||
if (authService.loggedInGuard()) {
|
||||
@@ -20,7 +20,7 @@ export const alreadyLoggedInGuard: CanActivateFn = (
|
||||
|
||||
export const loggedInGuard: CanActivateFn = (
|
||||
next: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
state: RouterStateSnapshot,
|
||||
): boolean | UrlTree => {
|
||||
const authService = inject(AuthService);
|
||||
if (authService.loggedInGuard()) {
|
||||
@@ -29,6 +29,6 @@ export const loggedInGuard: CanActivateFn = (
|
||||
return createUrlTreeFromSnapshot(
|
||||
next,
|
||||
["/", "login"],
|
||||
state.url !== "/" ? { next: state.url } : {}
|
||||
state.url !== "/" ? { next: state.url } : {},
|
||||
);
|
||||
};
|
||||
@@ -31,7 +31,7 @@ export class CommentFormComponent implements OnInit {
|
||||
readonly commentSubmitted = output<{
|
||||
text: string;
|
||||
id?: number;
|
||||
}>();
|
||||
}>();
|
||||
readonly cancelUpdate = output<number>();
|
||||
|
||||
commentForm = new FormGroup({
|
||||
|
||||
@@ -1,64 +1,89 @@
|
||||
@if (commentsListLoading$ | async) {
|
||||
<div class="comments-loading-wrapper">
|
||||
<mat-progress-spinner [diameter]="33" color="primary" mode="indeterminate"></mat-progress-spinner>
|
||||
</div>
|
||||
<div class="comments-loading-wrapper">
|
||||
<mat-progress-spinner
|
||||
[diameter]="33"
|
||||
color="primary"
|
||||
mode="indeterminate"
|
||||
></mat-progress-spinner>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="comment-create-wrapper">
|
||||
@if (displayCommentCreation$ | async) {
|
||||
<gt-comment-form [loading]="(createCommentLoading$ | async)!"
|
||||
(commentSubmitted)="createOrUpdateComment($event)"></gt-comment-form>
|
||||
}
|
||||
</div>
|
||||
<div class="comment-list">
|
||||
@for (comment of comments$ | async; track comment; let i = $index) {
|
||||
@if (i === 0) {
|
||||
<mat-divider class="list-head-divider"></mat-divider>
|
||||
}
|
||||
<div class="comment">
|
||||
@if (comment.updateMode) {
|
||||
<gt-comment-form [comment]="comment" [loading]="comment.updateLoading"
|
||||
(cancelUpdate)="cancelCommentUpdateMode($event)"
|
||||
(commentSubmitted)="createOrUpdateComment($event)"></gt-comment-form>
|
||||
} @else {
|
||||
<div class="delete-loading-wrapper">
|
||||
@if (comment.deleteLoading) {
|
||||
<div class="delete-loading-overlay">
|
||||
<div class="delete-spinner-wrapper">
|
||||
<mat-progress-spinner [diameter]="33" color="primary" mode="indeterminate"></mat-progress-spinner>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="comment-header">
|
||||
<div class="comment-user-details">
|
||||
<div>
|
||||
<span class="name mat-subtitle-1">{{
|
||||
comment.user?.email
|
||||
}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="date mat-caption">{{
|
||||
comment.dateCreated | date: " LLL d, Y hh:mm a"
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (user(); as user) {
|
||||
@if (user.id === comment.user?.id) {
|
||||
<div class="comment-edit-options">
|
||||
<button mat-icon-button class="small-icon-button" (click)="triggerCommentUpdateMode(comment.id)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button class="small-icon-button" (click)="deleteComment(comment.id)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div markdown [data]="comment.data.text" class="comment-markdown"></div>
|
||||
</div>
|
||||
<div class="comment-create-wrapper">
|
||||
@if (displayCommentCreation$ | async) {
|
||||
<gt-comment-form
|
||||
[loading]="(createCommentLoading$ | async)!"
|
||||
(commentSubmitted)="createOrUpdateComment($event)"
|
||||
></gt-comment-form>
|
||||
}
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="comment-list">
|
||||
@for (comment of comments$ | async; track comment; let i = $index) {
|
||||
@if (i === 0) {
|
||||
<mat-divider class="list-head-divider"></mat-divider>
|
||||
}
|
||||
<div class="comment">
|
||||
@if (comment.updateMode) {
|
||||
<gt-comment-form
|
||||
[comment]="comment"
|
||||
[loading]="comment.updateLoading"
|
||||
(cancelUpdate)="cancelCommentUpdateMode($event)"
|
||||
(commentSubmitted)="createOrUpdateComment($event)"
|
||||
></gt-comment-form>
|
||||
} @else {
|
||||
<div class="delete-loading-wrapper">
|
||||
@if (comment.deleteLoading) {
|
||||
<div class="delete-loading-overlay">
|
||||
<div class="delete-spinner-wrapper">
|
||||
<mat-progress-spinner
|
||||
[diameter]="33"
|
||||
color="primary"
|
||||
mode="indeterminate"
|
||||
></mat-progress-spinner>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="comment-header">
|
||||
<div class="comment-user-details">
|
||||
<div>
|
||||
<span class="name mat-subtitle-1">{{
|
||||
comment.user?.email
|
||||
}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="date mat-caption">{{
|
||||
comment.dateCreated | date: " LLL d, Y hh:mm a"
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (user(); as user) {
|
||||
@if (user.id === comment.user?.id) {
|
||||
<div class="comment-edit-options">
|
||||
<button
|
||||
mat-icon-button
|
||||
class="small-icon-button"
|
||||
(click)="triggerCommentUpdateMode(comment.id)"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
class="small-icon-button"
|
||||
(click)="deleteComment(comment.id)"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
markdown
|
||||
[data]="comment.data.text"
|
||||
class="comment-markdown"
|
||||
></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export class CommentsComponent
|
||||
commentUpdateLoading$ = this.commentsService.commentUpdateLoading$;
|
||||
user = this.userService.user;
|
||||
displayCommentCreation$ = this.comments$.pipe(
|
||||
map((comments) => comments.length < 50)
|
||||
map((comments) => comments.length < 50),
|
||||
);
|
||||
|
||||
constructor(private commentsService: CommentsService) {
|
||||
@@ -64,13 +64,13 @@ export class CommentsComponent
|
||||
this.commentsService.updateComment(
|
||||
+params["issue-id"],
|
||||
data.id,
|
||||
data.text
|
||||
data.text,
|
||||
);
|
||||
} else {
|
||||
this.commentsService.createComment(+params["issue-id"], data.text);
|
||||
}
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ export class CommentsComponent
|
||||
this.route.params.pipe(
|
||||
tap((params) => {
|
||||
this.commentsService.deleteComment(+params["issue-id"], commentId);
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,31 @@
|
||||
<section class="grid-wrapper">
|
||||
@for (contexts of specialContexts$ | async; track contexts.title +
|
||||
contexts.type) {
|
||||
<div class="wrapper">
|
||||
@if (contexts.icon || contexts.matIcon) { @if (contexts.icon) {
|
||||
<img class="image" [src]="contexts.icon" [alt]="contexts.icon" />
|
||||
} @else {
|
||||
<mat-icon class="image">{{ contexts.matIcon }}</mat-icon>
|
||||
} } @else {
|
||||
<mat-icon class="image">help</mat-icon>
|
||||
}
|
||||
<div class="spacer">
|
||||
<p class="mat-body-strong">{{ contexts.title }}</p>
|
||||
@if ( !contexts.title.includes("Unknown") && contexts.key &&
|
||||
contexts.subtitle ) {
|
||||
<p class="mat-small mat-caption">
|
||||
<span class="bold">{{ contexts.key }}: </span
|
||||
><span class="spacer-small">{{ contexts.subtitle }}</span>
|
||||
</p>
|
||||
@for (
|
||||
contexts of specialContexts$ | async;
|
||||
track contexts.title + contexts.type
|
||||
) {
|
||||
<div class="wrapper">
|
||||
@if (contexts.icon || contexts.matIcon) {
|
||||
@if (contexts.icon) {
|
||||
<img class="image" [src]="contexts.icon" [alt]="contexts.icon" />
|
||||
} @else {
|
||||
<mat-icon class="image">{{ contexts.matIcon }}</mat-icon>
|
||||
}
|
||||
} @else {
|
||||
<mat-icon class="image">help</mat-icon>
|
||||
}
|
||||
<div class="spacer">
|
||||
<p class="mat-body-strong">{{ contexts.title }}</p>
|
||||
@if (
|
||||
!contexts.title.includes("Unknown") &&
|
||||
contexts.key &&
|
||||
contexts.subtitle
|
||||
) {
|
||||
<p class="mat-small mat-caption">
|
||||
<span class="bold">{{ contexts.key }}: </span
|
||||
><span class="spacer-small">{{ contexts.subtitle }}</span>
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { IssueDetailService } from "../../issue-detail.service";
|
||||
import { MatIconModule } from "@angular/material/icon";
|
||||
import { AsyncPipe } from "@angular/common";
|
||||
|
||||
@@ -136,7 +136,7 @@ export const IconPaths = () => {
|
||||
src="${generateIconPath(icon)}"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+8
-1
@@ -6,7 +6,14 @@ import {
|
||||
DatePipe,
|
||||
KeyValuePipe,
|
||||
} from "@angular/common";
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, ViewChild, inject } from "@angular/core";
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
ViewChild,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { Json } from "src/app/interface-primitives";
|
||||
import { IssueDetailService } from "../../issue-detail.service";
|
||||
import { MatIconModule } from "@angular/material/icon";
|
||||
|
||||
+6
-1
@@ -1,4 +1,9 @@
|
||||
import { Component, ChangeDetectionStrategy, input, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
input,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { IssueDetailService } from "../../issue-detail.service";
|
||||
import { isStacktrace } from "src/app/issues/utils";
|
||||
import { RawStacktraceComponent } from "./raw-stacktrace/raw-stacktrace.component";
|
||||
|
||||
+6
-1
@@ -1,5 +1,10 @@
|
||||
import { KeyValuePipe } from "@angular/common";
|
||||
import { Component, ChangeDetectionStrategy, Input, input } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
input,
|
||||
} from "@angular/core";
|
||||
import { MatDividerModule } from "@angular/material/divider";
|
||||
import { JsonArrayOrObject, Json } from "src/app/interface-primitives";
|
||||
import { FrameContextTuple } from "src/app/issues/interfaces";
|
||||
|
||||
+6
-1
@@ -1,4 +1,9 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, input } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
input,
|
||||
} from "@angular/core";
|
||||
import type { Frame } from "src/app/issues/interfaces";
|
||||
import { MatIconModule } from "@angular/material/icon";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
|
||||
+6
-1
@@ -1,4 +1,9 @@
|
||||
import { Component, ChangeDetectionStrategy, input, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
input,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { isStacktrace } from "src/app/issues/utils";
|
||||
import { IssueDetailService } from "../../../issue-detail.service";
|
||||
import { AsyncPipe } from "@angular/common";
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, RouterLink } from "@angular/router";
|
||||
import { map, exhaustMap } from "rxjs/operators";
|
||||
import { IssueDetailService } from "../issue-detail.service";
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnInit,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { IssueDetailService } from "../issue-detail.service";
|
||||
import { exhaustMap, map } from "rxjs/operators";
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { FormControl, FormGroup } from "@angular/forms";
|
||||
import { ActivatedRoute, RouterModule } from "@angular/router";
|
||||
@@ -62,7 +67,7 @@ export class IssueDetailComponent implements OnInit {
|
||||
default:
|
||||
return [metadata.title!, null];
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
issueSubtitle$ = this.issue$.pipe(
|
||||
map((issue) => {
|
||||
@@ -82,14 +87,14 @@ export class IssueDetailComponent implements OnInit {
|
||||
default:
|
||||
return issue.culprit;
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
initialLoadComplete$ = this.issueService.issueInitialLoadComplete$;
|
||||
form = new FormGroup({
|
||||
assignee: new FormControl(""),
|
||||
});
|
||||
issueIdParam$ = this.route.paramMap.pipe(
|
||||
map((params) => params.get("issue-id"))
|
||||
map((params) => params.get("issue-id")),
|
||||
);
|
||||
organization$ = this.organizationsService.activeOrganization$;
|
||||
participantCountPluralMapping: { [k: string]: string } = {
|
||||
@@ -107,7 +112,7 @@ export class IssueDetailComponent implements OnInit {
|
||||
return this.issueService.retrieveIssue(+issueId);
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
@@ -131,12 +136,12 @@ export class IssueDetailComponent implements OnInit {
|
||||
if (
|
||||
id &&
|
||||
window.confirm(
|
||||
`Are you sure you want delete this issue? You will permanently lose this issue and all associated events.`
|
||||
`Are you sure you want delete this issue? You will permanently lose this issue and all associated events.`,
|
||||
)
|
||||
) {
|
||||
this.issueService.deleteIssue(id.toString());
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
@@ -51,11 +51,11 @@ describe("IssueDetailService", () => {
|
||||
const testApiData = sampleApiIssueDetail;
|
||||
service.retrieveIssue(testApiData.id).toPromise();
|
||||
const req = httpTestingController.expectOne(
|
||||
`/api/0/issues/${testApiData.id}/`
|
||||
`/api/0/issues/${testApiData.id}/`,
|
||||
);
|
||||
req.flush(testApiData);
|
||||
service.issue$.subscribe((issue) =>
|
||||
expect(issue).toEqual(sampleIssueDetail)
|
||||
expect(issue).toEqual(sampleIssueDetail),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -270,7 +270,7 @@ describe("IssueDetailService", () => {
|
||||
service.setEvent(testData);
|
||||
service.breadcrumbs$.subscribe((breadcrumb: any) => {
|
||||
expect(breadcrumb.values[0].timestamp).toEqual(
|
||||
"2020-12-18T18:09:47.841276Z"
|
||||
"2020-12-18T18:09:47.841276Z",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,24 +68,24 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
|
||||
readonly issue$ = this.getState$.pipe(map((state) => state.issue));
|
||||
readonly issueInitialLoadComplete$ = this.getState$.pipe(
|
||||
map((state) => state.issueInitialLoadComplete)
|
||||
map((state) => state.issueInitialLoadComplete),
|
||||
);
|
||||
readonly event$ = this.getState$.pipe(map((state) => state.event));
|
||||
readonly tags$ = this.getState$.pipe(
|
||||
map((state) => state.tags && this.tagsWithPercent(state.tags))
|
||||
map((state) => state.tags && this.tagsWithPercent(state.tags)),
|
||||
);
|
||||
readonly eventInitialLoadComplete$ = this.getState$.pipe(
|
||||
map((state) => state.eventInitialLoadComplete)
|
||||
map((state) => state.eventInitialLoadComplete),
|
||||
);
|
||||
readonly isReversed$ = this.getState$.pipe(map((state) => state.isReversed));
|
||||
readonly showShowMore$ = this.getState$.pipe(
|
||||
map((state) => state.showShowMore)
|
||||
map((state) => state.showShowMore),
|
||||
);
|
||||
readonly hasNextEvent$ = this.event$.pipe(
|
||||
map((event) => event && event.nextEventID !== null)
|
||||
map((event) => event && event.nextEventID !== null),
|
||||
);
|
||||
readonly hasPreviousEvent$ = this.event$.pipe(
|
||||
map((event) => event && event.previousEventID !== null)
|
||||
map((event) => event && event.previousEventID !== null),
|
||||
);
|
||||
readonly nextEventUrl$ = combineLatest([
|
||||
this.organization.activeOrganizationSlug$,
|
||||
@@ -97,7 +97,7 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
return this.eventUrl(orgSlug, issue, event.nextEventID);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
}),
|
||||
);
|
||||
readonly previousEventUrl$ = combineLatest([
|
||||
this.organization.activeOrganizationSlug$,
|
||||
@@ -109,33 +109,33 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
return this.eventUrl(orgSlug, issue, event.previousEventID);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
}),
|
||||
);
|
||||
readonly eventEntryException$ = combineLatest([
|
||||
this.event$,
|
||||
this.isReversed$,
|
||||
]).pipe(
|
||||
map(([event, isReversed]) =>
|
||||
event ? this.reverseFrames(event, isReversed) : undefined
|
||||
)
|
||||
event ? this.reverseFrames(event, isReversed) : undefined,
|
||||
),
|
||||
);
|
||||
readonly rawStacktraceValues$ = this.event$.pipe(
|
||||
map((event) => (event ? this.rawStacktraceValues(event) : undefined))
|
||||
map((event) => (event ? this.rawStacktraceValues(event) : undefined)),
|
||||
);
|
||||
readonly eventEntryRequest$ = this.event$.pipe(
|
||||
map((event) => (event ? this.entryRequestData(event) : undefined))
|
||||
map((event) => (event ? this.entryRequestData(event) : undefined)),
|
||||
);
|
||||
readonly eventEntryCSP$ = this.event$.pipe(
|
||||
map((event) => (event ? this.eventEntryCSP(event) : undefined))
|
||||
map((event) => (event ? this.eventEntryCSP(event) : undefined)),
|
||||
);
|
||||
readonly eventEntryMessage$ = this.event$.pipe(
|
||||
map((event) => (event ? this.eventEntryMessage(event) : undefined))
|
||||
map((event) => (event ? this.eventEntryMessage(event) : undefined)),
|
||||
);
|
||||
readonly specialContexts$ = this.event$.pipe(
|
||||
map((event) => (event ? this.specialContexts(event) : undefined))
|
||||
map((event) => (event ? this.specialContexts(event) : undefined)),
|
||||
);
|
||||
readonly breadcrumbs$ = this.event$.pipe(
|
||||
map((event) => (event ? this.eventEntryBreadcrumbs(event) : undefined))
|
||||
map((event) => (event ? this.eventEntryBreadcrumbs(event) : undefined)),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
@@ -151,7 +151,7 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
this.clearIssue();
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
return this.issuesAPIService.retrieveTags(id.toString(), query).pipe(
|
||||
tap((resp) => {
|
||||
this.setTags(resp);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -216,11 +216,11 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
lastValueFrom(
|
||||
this.issuesAPIService
|
||||
.update(status, slug, issue.id)
|
||||
.pipe(tap((resp) => this.setIssueStatus(resp.status)))
|
||||
.pipe(tap((resp) => this.setIssueStatus(resp.status))),
|
||||
);
|
||||
}
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -240,10 +240,10 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
}),
|
||||
catchError((_) => {
|
||||
this.snackBar.open(
|
||||
`There was an error deleting this issue. Please try again.`
|
||||
`There was an error deleting this issue. Please try again.`,
|
||||
);
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
@@ -266,7 +266,7 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
});
|
||||
const sumOfPercents = tagWithPercent.reduce(
|
||||
(accum, item) => accum + item.percent,
|
||||
0
|
||||
0,
|
||||
);
|
||||
if (sumOfPercents < 100) {
|
||||
return {
|
||||
@@ -302,7 +302,7 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
this.clearEvent();
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -316,7 +316,7 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
this.clearEvent();
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
|
||||
/* Return the breadcrumbs entry type for an event */
|
||||
private eventEntryBreadcrumbs(
|
||||
event: EventDetail
|
||||
event: EventDetail,
|
||||
): BreadcrumbValueData | undefined {
|
||||
const breadcrumbs = this.getBreadcrumbs(event);
|
||||
if (breadcrumbs) {
|
||||
@@ -428,7 +428,7 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
/* Reverse frame array, nested in the event object */
|
||||
private reverseFrames(
|
||||
event: EventDetail,
|
||||
isReversed: boolean
|
||||
isReversed: boolean,
|
||||
): ExceptionValueData | undefined {
|
||||
const eventException = this.getExceptionEntryData(event);
|
||||
|
||||
@@ -482,7 +482,7 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
|
||||
checkContextName(
|
||||
contextsObject: { [key: string]: Json },
|
||||
defaultUnknown: string
|
||||
defaultUnknown: string,
|
||||
) {
|
||||
if (
|
||||
contextsObject.name !== "Other" &&
|
||||
@@ -547,13 +547,13 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
subtitle: contextsObject.version
|
||||
? (contextsObject.version as string)
|
||||
: contextsObject.kernel_version
|
||||
? (contextsObject.kernel_version as string)
|
||||
: "Unknown",
|
||||
? (contextsObject.kernel_version as string)
|
||||
: "Unknown",
|
||||
key: contextsObject.version
|
||||
? "Version"
|
||||
: contextsObject.kernel_version
|
||||
? "Kernel"
|
||||
: "Version",
|
||||
? "Kernel"
|
||||
: "Version",
|
||||
});
|
||||
}
|
||||
if (key === "device") {
|
||||
@@ -569,13 +569,13 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
subtitle: contextsObject.arch
|
||||
? (contextsObject.arch as string)
|
||||
: contextsObject.model_id
|
||||
? (contextsObject.model_id as string)
|
||||
: null,
|
||||
? (contextsObject.model_id as string)
|
||||
: null,
|
||||
key: contextsObject.arch
|
||||
? "Arch"
|
||||
: contextsObject.model_id
|
||||
? "Model"
|
||||
: null,
|
||||
? "Model"
|
||||
: null,
|
||||
});
|
||||
}
|
||||
if (key === "gpu") {
|
||||
@@ -671,7 +671,7 @@ export class IssueDetailService extends StatefulService<IssueDetailState> {
|
||||
private eventUrl(
|
||||
orgSlug: string | null,
|
||||
issue: IssueDetail | null,
|
||||
eventID: string
|
||||
eventID: string,
|
||||
) {
|
||||
if (orgSlug && issue) {
|
||||
return `/${orgSlug}/issues/${issue.id}/events/${eventID}`;
|
||||
|
||||
@@ -1,118 +1,157 @@
|
||||
<div class="empty-states">
|
||||
<!-- Don't show any of this until loading state is resolved -->
|
||||
@if (displayZeroStates$ | async) {
|
||||
<!-- If there are errors, only show the error -->
|
||||
@if ((errors$ | async)?.length) {
|
||||
@if ((errors$ | async)?.length === 1) {
|
||||
<h3 class="mat-headline-4" i18n>There was an error processing your request</h3>
|
||||
} @else {
|
||||
<h3 class="mat-headline-4" i18n>There were errors while processing your request</h3>
|
||||
}
|
||||
<ul>
|
||||
@for (error of errors$ | async; track error) {
|
||||
<li>{{ error }}</li>
|
||||
}
|
||||
</ul>
|
||||
} @else {
|
||||
<!-- If there are zero projects -->
|
||||
@if (orgHasAProject() === false) {
|
||||
<h3 class="mat-headline-4" i18n>This organization has no projects.</h3>
|
||||
<p class="mat-body-1" i18n>To add a project to this organization, press the
|
||||
"Add Project" button.</p>
|
||||
} @else {
|
||||
<!-- You're an admin, but you're not on the team -->
|
||||
@if (projectsWhereAdminIsNotOnTheTeam$ | async; as notOnTeam) {
|
||||
@if (notOnTeam.length > 0 && notOnTeam.length < 2) { <p class="not-on-team">
|
||||
@if (singleProjectApplied$ | async) {
|
||||
<ng-container i18n>You can't see issues for this project because you're not on
|
||||
the team.</ng-container>
|
||||
} @else {
|
||||
<ng-container i18n>Out of the projects you have selected, you are not a part of
|
||||
that project's team for at least one of them.</ng-container>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
@if (notOnTeam.length > 2) {}
|
||||
}
|
||||
<!-- If there is one project applied -->
|
||||
@if (singleProjectApplied$ | async) {
|
||||
<!-- If no event has been sent -->
|
||||
@if (showOnboarding$ | async) {
|
||||
<div class="install-wrapper">
|
||||
@if (activeProjectPlatform$ | async; as activeProjectPlatform) {
|
||||
<div class="install-instructions mat-body-1">
|
||||
@if (
|
||||
activeProjectPlatform !== "other" &&
|
||||
activeProjectPlatform !== "" &&
|
||||
(activeProjectPlatformName$ | async)
|
||||
) {
|
||||
<h2 class="mat-headline-4" i18n>Connect your
|
||||
{{ activeProjectPlatformName$ | async }} project to
|
||||
GlitchTip</h2>
|
||||
<markdown src="/static/sdk-docs/{{ activeProjectPlatform }}.md"></markdown>
|
||||
} @else {
|
||||
<h2 class="mat-headline-4" i18n>Connect your project to GlitchTip</h2>
|
||||
<markdown src="/static/sdk-docs/all-sdks.md"></markdown>
|
||||
<markdown src="/static/sdk-docs/other.md"></markdown>
|
||||
}
|
||||
</div>
|
||||
<!-- If there are errors, only show the error -->
|
||||
@if ((errors$ | async)?.length) {
|
||||
@if ((errors$ | async)?.length === 1) {
|
||||
<h3 class="mat-headline-4" i18n
|
||||
>There was an error processing your request</h3
|
||||
>
|
||||
} @else {
|
||||
<div class="install-instructions mat-body-1">
|
||||
<h2 class="mat-headline-4" i18n>Connect your project to GlitchTip</h2>
|
||||
<markdown src="/static/sdk-docs/all-sdks.md"></markdown>
|
||||
<p class="mat-body-1" i18n>If you go to this project's
|
||||
<a [routerLink]="[
|
||||
<h3 class="mat-headline-4" i18n
|
||||
>There were errors while processing your request</h3
|
||||
>
|
||||
}
|
||||
<ul>
|
||||
@for (error of errors$ | async; track error) {
|
||||
<li>{{ error }}</li>
|
||||
}
|
||||
</ul>
|
||||
} @else {
|
||||
<!-- If there are zero projects -->
|
||||
@if (orgHasAProject() === false) {
|
||||
<h3 class="mat-headline-4" i18n>This organization has no projects.</h3>
|
||||
<p class="mat-body-1" i18n
|
||||
>To add a project to this organization, press the
|
||||
"Add Project" button.</p
|
||||
>
|
||||
} @else {
|
||||
<!-- You're an admin, but you're not on the team -->
|
||||
@if (projectsWhereAdminIsNotOnTheTeam$ | async; as notOnTeam) {
|
||||
@if (notOnTeam.length > 0 && notOnTeam.length < 2) {
|
||||
<p class="not-on-team">
|
||||
@if (singleProjectApplied$ | async) {
|
||||
<ng-container i18n
|
||||
>You can't see issues for this project because you're not on
|
||||
the team.</ng-container
|
||||
>
|
||||
} @else {
|
||||
<ng-container i18n
|
||||
>Out of the projects you have selected, you are not a part of
|
||||
that project's team for at least one of them.</ng-container
|
||||
>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
@if (notOnTeam.length > 2) {}
|
||||
}
|
||||
<!-- If there is one project applied -->
|
||||
@if (singleProjectApplied$ | async) {
|
||||
<!-- If no event has been sent -->
|
||||
@if (showOnboarding$ | async) {
|
||||
<div class="install-wrapper">
|
||||
@if (activeProjectPlatform$ | async; as activeProjectPlatform) {
|
||||
<div class="install-instructions mat-body-1">
|
||||
@if (
|
||||
activeProjectPlatform !== "other" &&
|
||||
activeProjectPlatform !== "" &&
|
||||
(activeProjectPlatformName$ | async)
|
||||
) {
|
||||
<h2 class="mat-headline-4" i18n
|
||||
>Connect your
|
||||
{{ activeProjectPlatformName$ | async }} project to
|
||||
GlitchTip</h2
|
||||
>
|
||||
<markdown
|
||||
src="/static/sdk-docs/{{ activeProjectPlatform }}.md"
|
||||
></markdown>
|
||||
} @else {
|
||||
<h2 class="mat-headline-4" i18n
|
||||
>Connect your project to GlitchTip</h2
|
||||
>
|
||||
<markdown src="/static/sdk-docs/all-sdks.md"></markdown>
|
||||
<markdown src="/static/sdk-docs/other.md"></markdown>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="install-instructions mat-body-1">
|
||||
<h2 class="mat-headline-4" i18n
|
||||
>Connect your project to GlitchTip</h2
|
||||
>
|
||||
<markdown src="/static/sdk-docs/all-sdks.md"></markdown>
|
||||
<p class="mat-body-1" i18n
|
||||
>If you go to this project's
|
||||
<a
|
||||
[routerLink]="[
|
||||
'/',
|
||||
activeOrganizationSlug$ | async,
|
||||
'settings',
|
||||
'projects',
|
||||
activeProjectSlug$ | async,
|
||||
]">
|
||||
settings page</a>, you can pick a platform. If you do, you'll get more
|
||||
specific instructions about how to get started.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
@if (firstProjectKey$ | async; as firstProjectKey) {
|
||||
<div class="dsn">
|
||||
<h2 class="mat-headline-4">DSN</h2>
|
||||
<p class="mat-body-1" i18n>Most instructions will have a spot that says
|
||||
"YOUR-GLITCHTIP-DSN-HERE". Copy the DSN and paste it into
|
||||
that spot.</p>
|
||||
<div class="dsn-area">
|
||||
<gt-copy-input [value]="firstProjectKey.dsn.public" placeholder="DSN"></gt-copy-input>
|
||||
<gt-copy-input [value]="firstProjectKey.dsn.security" placeholder="Security Endpoint"></gt-copy-input>
|
||||
</div>
|
||||
@if (
|
||||
activeProjectPlatform$ | async;
|
||||
as activeProjectPlatform
|
||||
) {
|
||||
@if (activeProjectPlatform !== "other") {
|
||||
<h2 class="mat-body-strong" i18n>What's this all about?</h2>
|
||||
<div class="mat-body-1">
|
||||
<markdown src="/static/sdk-docs/all-sdks.md"></markdown>
|
||||
</div>
|
||||
<p class="mat-body-1" i18n>This page is showing instructions on how to connect
|
||||
GlitchTip to your project based on the platform you
|
||||
specified.</p>
|
||||
]"
|
||||
>
|
||||
settings page</a
|
||||
>, you can pick a platform. If you do, you'll get more
|
||||
specific instructions about how to get started.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
@if (firstProjectKey$ | async; as firstProjectKey) {
|
||||
<div class="dsn">
|
||||
<h2 class="mat-headline-4">DSN</h2>
|
||||
<p class="mat-body-1" i18n
|
||||
>Most instructions will have a spot that says
|
||||
"YOUR-GLITCHTIP-DSN-HERE". Copy the DSN and paste it into
|
||||
that spot.</p
|
||||
>
|
||||
<div class="dsn-area">
|
||||
<gt-copy-input
|
||||
[value]="firstProjectKey.dsn.public"
|
||||
placeholder="DSN"
|
||||
></gt-copy-input>
|
||||
<gt-copy-input
|
||||
[value]="firstProjectKey.dsn.security"
|
||||
placeholder="Security Endpoint"
|
||||
></gt-copy-input>
|
||||
</div>
|
||||
@if (
|
||||
activeProjectPlatform$ | async;
|
||||
as activeProjectPlatform
|
||||
) {
|
||||
@if (activeProjectPlatform !== "other") {
|
||||
<h2 class="mat-body-strong" i18n
|
||||
>What's this all about?</h2
|
||||
>
|
||||
<div class="mat-body-1">
|
||||
<markdown src="/static/sdk-docs/all-sdks.md"></markdown>
|
||||
</div>
|
||||
<p class="mat-body-1" i18n
|
||||
>This page is showing instructions on how to connect
|
||||
GlitchTip to your project based on the platform you
|
||||
specified.</p
|
||||
>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<!-- No issues match the filters, in general -->
|
||||
<h3 class="mat-headline-4" i18n>No events match your filters.</h3>
|
||||
<p class="mat-body-1" i18n
|
||||
>Try changing your search parameters to get something to show
|
||||
here.</p
|
||||
>
|
||||
}
|
||||
} @else {
|
||||
<h3 i18n>No events match your filters.</h3>
|
||||
<p class="mat-body-1" i18n
|
||||
>You have multiple projects selected. Select a single project to
|
||||
learn more about why there's nothing here.</p
|
||||
>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<!-- No issues match the filters, in general -->
|
||||
<h3 class="mat-headline-4" i18n>No events match your filters.</h3>
|
||||
<p class="mat-body-1" i18n>Try changing your search parameters to get something to show
|
||||
here.</p>
|
||||
}
|
||||
} @else {
|
||||
<h3 i18n>No events match your filters.</h3>
|
||||
<p class="mat-body-1" i18n>You have multiple projects selected. Select a single project to
|
||||
learn more about why there's nothing here.</p>
|
||||
}
|
||||
}
|
||||
}
|
||||
<!-- If there are no errors, show an empty state option -->
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, computed, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
computed,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, RouterLink } from "@angular/router";
|
||||
import { combineLatest } from "rxjs";
|
||||
import { distinctUntilChanged, filter, map, switchMap } from "rxjs/operators";
|
||||
@@ -40,13 +46,13 @@ export class IssueZeroStatesComponent implements OnInit {
|
||||
this.loading$,
|
||||
this.initialLoadComplete$,
|
||||
]).pipe(
|
||||
map(([loading, initialLoadComplete]) => !loading && initialLoadComplete)
|
||||
map(([loading, initialLoadComplete]) => !loading && initialLoadComplete),
|
||||
);
|
||||
orgHasAProject = computed(
|
||||
() => this.organizationsService.projectsCount() > 0
|
||||
() => this.organizationsService.projectsCount() > 0,
|
||||
);
|
||||
projectsFromParams$ = this.activatedRoute.queryParams.pipe(
|
||||
map((params) => normalizeProjectParams(params.project))
|
||||
map((params) => normalizeProjectParams(params.project)),
|
||||
);
|
||||
activeOrganizationProjects$ =
|
||||
this.organizationsService.activeOrganizationProjects$;
|
||||
@@ -64,7 +70,7 @@ export class IssueZeroStatesComponent implements OnInit {
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
distinctUntilChanged()
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
activeProject$ = combineLatest([
|
||||
this.projectsService.projects$,
|
||||
@@ -73,25 +79,27 @@ export class IssueZeroStatesComponent implements OnInit {
|
||||
map(([projects, activeProjectID]) => {
|
||||
if (projects && activeProjectID) {
|
||||
const activeProject = projects.find(
|
||||
(project) => project.id === activeProjectID
|
||||
(project) => project.id === activeProjectID,
|
||||
);
|
||||
return activeProject ? activeProject : null;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
distinctUntilChanged()
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
showOnboarding$ = this.activeProject$.pipe(
|
||||
map((project) => !project?.firstEvent)
|
||||
map((project) => !project?.firstEvent),
|
||||
);
|
||||
activeProjectPlatform$ = this.activeProject$.pipe(
|
||||
map((project) => project?.platform)
|
||||
map((project) => project?.platform),
|
||||
);
|
||||
activeProjectSlug$ = this.activeProject$.pipe(
|
||||
map((project) => project?.slug)
|
||||
map((project) => project?.slug),
|
||||
);
|
||||
activeProjectPlatformName$ = this.activeProjectPlatform$.pipe(
|
||||
map((id) => flattenedPlatforms.find((platform) => platform.id === id)?.name)
|
||||
map(
|
||||
(id) => flattenedPlatforms.find((platform) => platform.id === id)?.name,
|
||||
),
|
||||
);
|
||||
|
||||
firstProjectKey$ = combineLatest([
|
||||
@@ -100,14 +108,14 @@ export class IssueZeroStatesComponent implements OnInit {
|
||||
]).pipe(
|
||||
filter(
|
||||
([organizationSlug, activeProject]) =>
|
||||
!!organizationSlug && !!activeProject
|
||||
!!organizationSlug && !!activeProject,
|
||||
),
|
||||
distinctUntilChanged((x, y) => JSON.stringify(x) === JSON.stringify(y)),
|
||||
switchMap(([organizationSlug, activeProject]) =>
|
||||
this.projectKeysAPIService
|
||||
.list(organizationSlug!, activeProject!.slug)
|
||||
.pipe(map((keys) => keys[0]))
|
||||
)
|
||||
.pipe(map((keys) => keys[0])),
|
||||
),
|
||||
);
|
||||
errors$ = this.issuesService.errors$;
|
||||
|
||||
@@ -121,7 +129,7 @@ export class IssueZeroStatesComponent implements OnInit {
|
||||
return projects.length;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -134,8 +142,8 @@ export class IssueZeroStatesComponent implements OnInit {
|
||||
]).pipe(
|
||||
map(
|
||||
([appliedProjectCount, activeOrganizationProjects]) =>
|
||||
appliedProjectCount === 1 || activeOrganizationProjects?.length === 1
|
||||
)
|
||||
appliedProjectCount === 1 || activeOrganizationProjects?.length === 1,
|
||||
),
|
||||
);
|
||||
|
||||
projectsWhereAdminIsNotOnTheTeam$ = combineLatest([
|
||||
@@ -149,16 +157,16 @@ export class IssueZeroStatesComponent implements OnInit {
|
||||
const projectsMatchedFromParams: OrganizationProject[] = [];
|
||||
projectsFromParams.forEach((projectId) => {
|
||||
const matchedProject = activeOrgProjects?.find(
|
||||
(project) => (project.id as any) === projectId
|
||||
(project) => (project.id as any) === projectId,
|
||||
);
|
||||
if (matchedProject) {
|
||||
projectsMatchedFromParams.push(matchedProject as any);
|
||||
}
|
||||
});
|
||||
return projectsMatchedFromParams.filter(
|
||||
(project) => project.isMember === false
|
||||
(project) => project.isMember === false,
|
||||
);
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
userNotInSomeTeams$ = combineLatest([
|
||||
@@ -167,8 +175,8 @@ export class IssueZeroStatesComponent implements OnInit {
|
||||
]).pipe(
|
||||
map(
|
||||
([projectsWhereAdminIsNotOnTheTeam, appliedProjectCount]) =>
|
||||
projectsWhereAdminIsNotOnTheTeam.length && appliedProjectCount > 1
|
||||
)
|
||||
projectsWhereAdminIsNotOnTheTeam.length && appliedProjectCount > 1,
|
||||
),
|
||||
);
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
@@ -68,7 +68,7 @@ export class IssuesPageComponent implements OnInit, OnDestroy {
|
||||
displayedColumns: string[] = ["select", "title", "events"];
|
||||
paginator$ = this.service.paginator$;
|
||||
loading$ = this.service.getState$.pipe(
|
||||
map((state) => state.pagination.loading)
|
||||
map((state) => state.pagination.loading),
|
||||
);
|
||||
searchHits$ = this.service.searchHits$;
|
||||
searchDirectHit$ = this.service.searchDirectHit$;
|
||||
@@ -129,11 +129,11 @@ export class IssuesPageComponent implements OnInit, OnDestroy {
|
||||
sort: params.get("sort"),
|
||||
environment: params.get("environment"),
|
||||
};
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
projectsFromParams$ = this.route.queryParams.pipe(
|
||||
map((params) => normalizeProjectParams(params.project))
|
||||
map((params) => normalizeProjectParams(params.project)),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -146,7 +146,7 @@ export class IssuesPageComponent implements OnInit, OnDestroy {
|
||||
return projects.length;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
showBulkSelectProject$ = combineLatest([
|
||||
@@ -161,32 +161,32 @@ export class IssuesPageComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
organizationEnvironments$ = combineLatest([
|
||||
this.appliedProjectCount$,
|
||||
toObservable(
|
||||
this.organizationDetailService.organizationEnvironmentsProcessed
|
||||
this.organizationDetailService.organizationEnvironmentsProcessed,
|
||||
),
|
||||
this.projectEnvironmentsService.visibleEnvironments$,
|
||||
]).pipe(
|
||||
map(([appliedProjectCount, orgEnvironments, projectEnvironments]) =>
|
||||
appliedProjectCount !== 1 ? orgEnvironments : projectEnvironments
|
||||
)
|
||||
appliedProjectCount !== 1 ? orgEnvironments : projectEnvironments,
|
||||
),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
this.issues$.subscribe((resp) =>
|
||||
resp.length === 0
|
||||
? this.sortForm.controls.sort.disable()
|
||||
: this.sortForm.controls.sort.enable()
|
||||
: this.sortForm.controls.sort.enable(),
|
||||
);
|
||||
|
||||
this.organizationEnvironments$.subscribe((environments) =>
|
||||
environments.length === 0
|
||||
? this.environmentForm.controls.environment.disable()
|
||||
: this.environmentForm.controls.environment.enable()
|
||||
: this.environmentForm.controls.environment.enable(),
|
||||
);
|
||||
|
||||
this.currentQueryParams$
|
||||
@@ -201,12 +201,12 @@ export class IssuesPageComponent implements OnInit, OnDestroy {
|
||||
params.start,
|
||||
params.end,
|
||||
params.sort,
|
||||
params.environment
|
||||
params.environment,
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
takeUntilDestroyed()
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
@@ -215,12 +215,12 @@ export class IssuesPageComponent implements OnInit, OnDestroy {
|
||||
switchMap((orgSlug) => {
|
||||
if (orgSlug) {
|
||||
return this.organizationDetailService.getOrganizationEnvironments(
|
||||
orgSlug
|
||||
orgSlug,
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
takeUntilDestroyed()
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
@@ -229,24 +229,24 @@ export class IssuesPageComponent implements OnInit, OnDestroy {
|
||||
this.route.queryParamMap.pipe(
|
||||
map((params) => params.getAll("project")[0]),
|
||||
filter((project) => !!project),
|
||||
distinctUntilChanged()
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
this.organizationsService.activeOrganizationProjects$,
|
||||
])
|
||||
.pipe(
|
||||
switchMap(([orgSlug, projectId, orgProjects]) => {
|
||||
const projectSlug = orgProjects?.find(
|
||||
(orgProject) => orgProject.id.toString() === projectId
|
||||
(orgProject) => orgProject.id.toString() === projectId,
|
||||
)?.slug;
|
||||
if (orgSlug && projectSlug) {
|
||||
return this.projectEnvironmentsService.retrieveEnvironmentsWithProperties(
|
||||
orgSlug,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
takeUntilDestroyed()
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
@@ -272,7 +272,7 @@ export class IssuesPageComponent implements OnInit, OnDestroy {
|
||||
queryParamsHandling: "merge",
|
||||
});
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
@@ -284,7 +284,7 @@ export class IssuesPageComponent implements OnInit, OnDestroy {
|
||||
queryParams: { query: null },
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true, // so the browser back button works
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -365,14 +365,14 @@ export class IssuesPageComponent implements OnInit, OnDestroy {
|
||||
params.query,
|
||||
params.start,
|
||||
params.end,
|
||||
params.environment
|
||||
params.environment,
|
||||
);
|
||||
} else {
|
||||
this.service.updateStatusByIssueId(params.orgSlug, status);
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Component, ChangeDetectionStrategy, OnDestroy, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnDestroy,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, RouterLink } from "@angular/router";
|
||||
import { map } from "rxjs/operators";
|
||||
import { combineLatest } from "rxjs";
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Search</mat-label>
|
||||
<input
|
||||
<input
|
||||
matInput
|
||||
type="search"
|
||||
formControlName="query"
|
||||
|
||||
@@ -25,10 +25,12 @@ import { MatSelectChange, MatSelectModule } from "@angular/material/select";
|
||||
export class DataFilterBarComponent {
|
||||
@Input() dateForm?: FormGroup;
|
||||
@Input() sortForm?: FormGroup;
|
||||
readonly sorts = input<{
|
||||
param: string;
|
||||
display: string;
|
||||
}[]>();
|
||||
readonly sorts = input<
|
||||
{
|
||||
param: string;
|
||||
display: string;
|
||||
}[]
|
||||
>();
|
||||
@Input() environmentForm?: FormGroup;
|
||||
@Input() searchForm?: FormGroup;
|
||||
readonly organizationEnvironments = input<string[]>([]);
|
||||
|
||||
@@ -1,61 +1,91 @@
|
||||
@if (projects$ | async; as projects) {
|
||||
<section [class.no-projects]="(projects$ | async)?.length === 0">
|
||||
<div [class.no-projects__picker]="!projects.length" id="project-picker">
|
||||
<mat-accordion displayMode="flat" [hideToggle]="projects.length < 2">
|
||||
<mat-expansion-panel #expansionPanel class="mat-elevation-z1" (afterExpand)="focusPanel()"
|
||||
[disabled]="projects.length < 2">
|
||||
<mat-expansion-panel-header [class.disabled-looking]="projects.length === 0">
|
||||
<mat-panel-title class="mat-body-1">
|
||||
{{ selectedProjectsString$ | async }}
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Filter projects</mat-label>
|
||||
<input #filterInput type="text" aria-label="Filter projects" matInput [formControl]="filterProjectInput" />
|
||||
</mat-form-field>
|
||||
<ul>
|
||||
@for (project of filteredProjects$ | async; track project) {
|
||||
<li class="list-item">
|
||||
<button id="picker-button-{{ project.id }}" class="picker-button mat-body-1"
|
||||
[class.disabled]="!project.isMember" (click)="selectProjectAndClose(forceString(project.id))">
|
||||
{{ project.name }}
|
||||
</button>
|
||||
<mat-checkbox (change)="toggleProject(forceString(project.id))"
|
||||
[checked]="isSelected(forceString(project.id))" [disabled]="!project.isMember">
|
||||
</mat-checkbox>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div class="button-group">
|
||||
@if (someProjectsAreSelected$ | async) {
|
||||
<button mat-stroked-button (click)="navigate(null); expansionPanel.close()">
|
||||
View All
|
||||
</button>
|
||||
}
|
||||
<button mat-flat-button (click)="closePanel()" color="primary">
|
||||
@if ((selectedAndAppliedIdsAreEqual$ | async) === false) {
|
||||
Apply &
|
||||
<section [class.no-projects]="(projects$ | async)?.length === 0">
|
||||
<div [class.no-projects__picker]="!projects.length" id="project-picker">
|
||||
<mat-accordion displayMode="flat" [hideToggle]="projects.length < 2">
|
||||
<mat-expansion-panel
|
||||
#expansionPanel
|
||||
class="mat-elevation-z1"
|
||||
(afterExpand)="focusPanel()"
|
||||
[disabled]="projects.length < 2"
|
||||
>
|
||||
<mat-expansion-panel-header
|
||||
[class.disabled-looking]="projects.length === 0"
|
||||
>
|
||||
<mat-panel-title class="mat-body-1">
|
||||
{{ selectedProjectsString$ | async }}
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Filter projects</mat-label>
|
||||
<input
|
||||
#filterInput
|
||||
type="text"
|
||||
aria-label="Filter projects"
|
||||
matInput
|
||||
[formControl]="filterProjectInput"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<ul>
|
||||
@for (project of filteredProjects$ | async; track project) {
|
||||
<li class="list-item">
|
||||
<button
|
||||
id="picker-button-{{ project.id }}"
|
||||
class="picker-button mat-body-1"
|
||||
[class.disabled]="!project.isMember"
|
||||
(click)="selectProjectAndClose(forceString(project.id))"
|
||||
>
|
||||
{{ project.name }}
|
||||
</button>
|
||||
<mat-checkbox
|
||||
(change)="toggleProject(forceString(project.id))"
|
||||
[checked]="isSelected(forceString(project.id))"
|
||||
[disabled]="!project.isMember"
|
||||
>
|
||||
</mat-checkbox>
|
||||
</li>
|
||||
}
|
||||
Close
|
||||
</ul>
|
||||
<div class="button-group">
|
||||
@if (someProjectsAreSelected$ | async) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
(click)="navigate(null); expansionPanel.close()"
|
||||
>
|
||||
View All
|
||||
</button>
|
||||
}
|
||||
<button mat-flat-button (click)="closePanel()" color="primary">
|
||||
@if ((selectedAndAppliedIdsAreEqual$ | async) === false) {
|
||||
Apply &
|
||||
}
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
@if ((appliedProjectIds$ | async)?.length) {
|
||||
<button mat-button class="reset-button" (click)="resetProjects()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
@if ((appliedProjectIds$ | async)?.length) {
|
||||
<button mat-button class="reset-button" (click)="resetProjects()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</mat-accordion>
|
||||
</div>
|
||||
@if (orgSlug$ | async; as orgSlug) {
|
||||
<div class="nav-action-group">
|
||||
<a mat-stroked-button color="primary" class="nav-action"
|
||||
[routerLink]="['/', orgSlug, 'settings', 'projects', 'new']">
|
||||
<mat-icon>add</mat-icon>
|
||||
<ng-container i18n>Add Project</ng-container>
|
||||
</a>
|
||||
@if (projects.length > 0) {
|
||||
<a mat-stroked-button color="primary" class="nav-action" [routerLink]="
|
||||
}
|
||||
</mat-accordion>
|
||||
</div>
|
||||
@if (orgSlug$ | async; as orgSlug) {
|
||||
<div class="nav-action-group">
|
||||
<a
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
class="nav-action"
|
||||
[routerLink]="['/', orgSlug, 'settings', 'projects', 'new']"
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
<ng-container i18n>Add Project</ng-container>
|
||||
</a>
|
||||
@if (projects.length > 0) {
|
||||
<a
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
class="nav-action"
|
||||
[routerLink]="
|
||||
(singleProjectSlug$ | async)
|
||||
? [
|
||||
'/',
|
||||
@@ -65,12 +95,13 @@
|
||||
singleProjectSlug$ | async,
|
||||
]
|
||||
: ['/', orgSlug, 'settings', 'projects']
|
||||
">
|
||||
<mat-icon>settings</mat-icon>
|
||||
Project Settings
|
||||
</a>
|
||||
"
|
||||
>
|
||||
<mat-icon>settings</mat-icon>
|
||||
Project Settings
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Component, ChangeDetectionStrategy, ViewChild, OnInit, HostListener, ElementRef, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
ViewChild,
|
||||
OnInit,
|
||||
HostListener,
|
||||
ElementRef,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
import { MatDatepickerModule } from "@angular/material/datepicker";
|
||||
@@ -57,7 +65,7 @@ export class ProjectFilterBarComponent implements OnInit {
|
||||
const normalizedParams = normalizeProjectParams(params.project);
|
||||
this.selectedProjectIds.next(normalizedParams);
|
||||
return normalizedParams;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
appliedProjectIds?: number[];
|
||||
@@ -80,11 +88,11 @@ export class ProjectFilterBarComponent implements OnInit {
|
||||
return ids
|
||||
.map(
|
||||
(id) =>
|
||||
projects?.find((project) => id.toString() === project.id)?.name
|
||||
projects?.find((project) => id.toString() === project.id)?.name,
|
||||
)
|
||||
.join(", ");
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
selectedAndAppliedIdsAreEqual$ = combineLatest([
|
||||
@@ -93,8 +101,8 @@ export class ProjectFilterBarComponent implements OnInit {
|
||||
]).pipe(
|
||||
map(
|
||||
([selected, applied]) =>
|
||||
selected.sort().join(",") === applied.sort().join(",")
|
||||
)
|
||||
selected.sort().join(",") === applied.sort().join(","),
|
||||
),
|
||||
);
|
||||
|
||||
//** Yikes delete this please */
|
||||
@@ -113,14 +121,14 @@ export class ProjectFilterBarComponent implements OnInit {
|
||||
map(([projects, value]) =>
|
||||
projects
|
||||
? projects.filter((project) =>
|
||||
project.name.toLowerCase().includes(value.toLowerCase())
|
||||
project.name.toLowerCase().includes(value.toLowerCase()),
|
||||
)
|
||||
: null
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
|
||||
someProjectsAreSelected$ = this.appliedProjectIds$.pipe(
|
||||
map((ids) => ids.length !== 0)
|
||||
map((ids) => ids.length !== 0),
|
||||
);
|
||||
|
||||
singleProjectSlug$ = combineLatest([
|
||||
@@ -130,7 +138,7 @@ export class ProjectFilterBarComponent implements OnInit {
|
||||
map(([projects, ids]) => {
|
||||
if (ids.length === 1 && projects) {
|
||||
const matchedProject = projects.find(
|
||||
(project) => project.id === ids[0].toString()
|
||||
(project) => project.id === ids[0].toString(),
|
||||
);
|
||||
if (matchedProject) {
|
||||
return matchedProject.slug;
|
||||
@@ -138,7 +146,7 @@ export class ProjectFilterBarComponent implements OnInit {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
@ViewChild("expansionPanel", { static: false })
|
||||
@@ -148,7 +156,7 @@ export class ProjectFilterBarComponent implements OnInit {
|
||||
filterInput?: ElementRef<HTMLInputElement>;
|
||||
|
||||
@HostListener("document:keydown", ["$event"]) onKeydownHandler(
|
||||
event: KeyboardEvent
|
||||
event: KeyboardEvent,
|
||||
) {
|
||||
if (this.expansionPanel?.expanded) {
|
||||
if (event.key === "Escape") {
|
||||
@@ -172,14 +180,14 @@ export class ProjectFilterBarComponent implements OnInit {
|
||||
|
||||
moveDown() {
|
||||
const projectButtons = Array.from(
|
||||
document.querySelectorAll(".picker-button")
|
||||
document.querySelectorAll(".picker-button"),
|
||||
) as HTMLElement[];
|
||||
// If the text box is focused, go to the first item
|
||||
if (this.filterInput?.nativeElement.id === document.activeElement?.id) {
|
||||
projectButtons[0]?.focus();
|
||||
} else {
|
||||
const indexOfActive = projectButtons.findIndex(
|
||||
(button) => button.id === document.activeElement?.id
|
||||
(button) => button.id === document.activeElement?.id,
|
||||
);
|
||||
if (indexOfActive <= projectButtons.length - 2) {
|
||||
// If we're in the list items, go to the next list item
|
||||
@@ -193,10 +201,10 @@ export class ProjectFilterBarComponent implements OnInit {
|
||||
|
||||
moveUp() {
|
||||
const projectButtons = Array.from(
|
||||
document.querySelectorAll(".picker-button")
|
||||
document.querySelectorAll(".picker-button"),
|
||||
) as HTMLElement[];
|
||||
const indexOfActive = projectButtons.findIndex(
|
||||
(button) => button.id === document.activeElement?.id
|
||||
(button) => button.id === document.activeElement?.id,
|
||||
);
|
||||
if (indexOfActive > 0) {
|
||||
// If we're in the list items, go to the previous list item
|
||||
@@ -250,7 +258,7 @@ export class ProjectFilterBarComponent implements OnInit {
|
||||
|
||||
closePanel() {
|
||||
this.navigate(
|
||||
this.selectedProjectIds.getValue().map((id) => id.toString())
|
||||
this.selectedProjectIds.getValue().map((id) => id.toString()),
|
||||
);
|
||||
this.expansionPanel?.close();
|
||||
}
|
||||
@@ -264,7 +272,7 @@ export class ProjectFilterBarComponent implements OnInit {
|
||||
.pipe(
|
||||
map((params) => params["org-slug"]),
|
||||
filter((orgSlug: string) => orgSlug !== undefined),
|
||||
tap(() => this.expansionPanel?.close())
|
||||
tap(() => this.expansionPanel?.close()),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { UserService } from "./api/user/user.service";
|
||||
export class LoggedInComponent implements OnInit {
|
||||
private userService = inject(UserService);
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
// this.organizationService.retrieveOrganizations().subscribe();
|
||||
this.userService.getUserDetails();
|
||||
|
||||
@@ -62,7 +62,7 @@ export class LoginTotpComponent
|
||||
const loginService = inject(LoginService);
|
||||
|
||||
toObservable(loginService.fieldErrors).subscribe((fieldErrors) =>
|
||||
mapFormErrors(fieldErrors, this.form)
|
||||
mapFormErrors(fieldErrors, this.form),
|
||||
);
|
||||
super(loginService);
|
||||
this.hasWebAuthn = loginService.hasWebAuthn;
|
||||
@@ -92,7 +92,7 @@ export class LoginTotpComponent
|
||||
lastValueFrom(
|
||||
this.loginService
|
||||
.totpAuthenticate(code)
|
||||
.pipe(tap(() => this.router.navigate(["/"])))
|
||||
.pipe(tap(() => this.router.navigate(["/"]))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnInit,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { RouterLink } from "@angular/router";
|
||||
import { MatCheckboxModule } from "@angular/material/checkbox";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
|
||||
@@ -1,263 +1,267 @@
|
||||
<mat-sidenav-container>
|
||||
@if (isLoggedIn()) {
|
||||
<mat-sidenav
|
||||
#sidenav
|
||||
class="mat-elevation-z4"
|
||||
[opened]="navOpen()"
|
||||
(closed)="closeSideNav()"
|
||||
[mode]="mobileNav() === true ? 'over' : 'side'"
|
||||
>
|
||||
<mat-toolbar class="logo-toolbar">
|
||||
<img
|
||||
class="main-logo"
|
||||
routerLink="."
|
||||
(click)="reload()"
|
||||
src="static/assets/images/glitchtip-logo-v1.svg"
|
||||
alt="GlitchTip"
|
||||
/>
|
||||
</mat-toolbar>
|
||||
@for (org of organizations(); track org.id) { } @if ( (contextLoaded()) &&
|
||||
organizations().length === 0 && (canCreateOrg()) ) {
|
||||
<a
|
||||
mat-button
|
||||
id="create-new-link"
|
||||
routerLink="/organizations/new"
|
||||
class="org-create no-org-create"
|
||||
i18n
|
||||
>Create New Organization</a
|
||||
<mat-sidenav
|
||||
#sidenav
|
||||
class="mat-elevation-z4"
|
||||
[opened]="navOpen()"
|
||||
(closed)="closeSideNav()"
|
||||
[mode]="mobileNav() === true ? 'over' : 'side'"
|
||||
>
|
||||
} @else {
|
||||
<button
|
||||
mat-button
|
||||
id="org-dropdown"
|
||||
[disabled]="contextLoaded() === false"
|
||||
[matMenuTriggerFor]="menu"
|
||||
class="org-dropdown"
|
||||
>
|
||||
<div class="label-container">
|
||||
<span class="overflow">
|
||||
{{ activeOrganization()?.name }}
|
||||
</span>
|
||||
<span class="down-caret">▼</span>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
<mat-menu #menu="matMenu">
|
||||
@for (organization of organizations(); track organization.id) {
|
||||
<button (click)="setOrganization(organization.slug)" mat-menu-item>
|
||||
{{ organization.name }}
|
||||
</button>
|
||||
}
|
||||
<mat-divider></mat-divider>
|
||||
@if (canCreateOrg()) {
|
||||
<a mat-menu-item id="create-new-link" routerLink="/organizations/new" i18n
|
||||
>Create New Organization</a
|
||||
>
|
||||
<mat-toolbar class="logo-toolbar">
|
||||
<img
|
||||
class="main-logo"
|
||||
routerLink="."
|
||||
(click)="reload()"
|
||||
src="static/assets/images/glitchtip-logo-v1.svg"
|
||||
alt="GlitchTip"
|
||||
/>
|
||||
</mat-toolbar>
|
||||
@for (org of organizations(); track org.id) {}
|
||||
@if (contextLoaded() && organizations().length === 0 && canCreateOrg()) {
|
||||
<a
|
||||
mat-button
|
||||
id="create-new-link"
|
||||
routerLink="/organizations/new"
|
||||
class="org-create no-org-create"
|
||||
i18n
|
||||
>Create New Organization</a
|
||||
>
|
||||
} @else {
|
||||
<span
|
||||
mat-menu-item
|
||||
disabled
|
||||
matTooltip="Organization creation is currently disabled"
|
||||
matTooltipPosition="below"
|
||||
>
|
||||
Create New Organization
|
||||
</span>
|
||||
<button
|
||||
mat-button
|
||||
id="org-dropdown"
|
||||
[disabled]="contextLoaded() === false"
|
||||
[matMenuTriggerFor]="menu"
|
||||
class="org-dropdown"
|
||||
>
|
||||
<div class="label-container">
|
||||
<span class="overflow">
|
||||
{{ activeOrganization()?.name }}
|
||||
</span>
|
||||
<span class="down-caret">▼</span>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
<mat-nav-list class="nav-link-list">
|
||||
<a
|
||||
mat-list-item
|
||||
[disabled]="!activeOrganization()"
|
||||
[routerLink]="[activeOrganizationSlug(), 'issues']"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Issues</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[disabled]="!activeOrganization()"
|
||||
[routerLink]="[activeOrganizationSlug(), 'uptime-monitors']"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Uptime Monitors</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[disabled]="!activeOrganization()"
|
||||
[routerLink]="[
|
||||
activeOrganizationSlug(),
|
||||
'performance',
|
||||
'transaction-groups'
|
||||
]"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Performance</a
|
||||
>
|
||||
<mat-divider class="org-details-divider"></mat-divider>
|
||||
<a
|
||||
mat-list-item
|
||||
[disabled]="!activeOrganization()"
|
||||
[routerLink]="[activeOrganizationSlug(), 'projects']"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Projects</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[disabled]="!activeOrganization()"
|
||||
[routerLink]="[activeOrganizationSlug(), 'releases']"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Releases</a
|
||||
>
|
||||
<a
|
||||
data-test-settings
|
||||
mat-list-item
|
||||
[disabled]="!activeOrganization()"
|
||||
[routerLink]="[activeOrganizationSlug(), 'settings']"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Settings</a
|
||||
>
|
||||
<a
|
||||
class="sub-items"
|
||||
mat-list-item
|
||||
[disabled]="!activeOrganization()"
|
||||
[routerLink]="['/', activeOrganizationSlug(), 'settings', 'projects']"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Projects</a
|
||||
>
|
||||
@if (billingEnabled()) {
|
||||
<a
|
||||
class="sub-items"
|
||||
mat-list-item
|
||||
[routerLink]="[
|
||||
'/',
|
||||
activeOrganizationSlug(),
|
||||
'settings',
|
||||
'subscription'
|
||||
]"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Subscription</a
|
||||
>
|
||||
<mat-menu #menu="matMenu">
|
||||
@for (organization of organizations(); track organization.id) {
|
||||
<button (click)="setOrganization(organization.slug)" mat-menu-item>
|
||||
{{ organization.name }}
|
||||
</button>
|
||||
}
|
||||
<mat-divider></mat-divider>
|
||||
@if (canCreateOrg()) {
|
||||
<a
|
||||
mat-menu-item
|
||||
id="create-new-link"
|
||||
routerLink="/organizations/new"
|
||||
i18n
|
||||
>Create New Organization</a
|
||||
>
|
||||
} @else {
|
||||
<span
|
||||
mat-menu-item
|
||||
disabled
|
||||
matTooltip="Organization creation is currently disabled"
|
||||
matTooltipPosition="below"
|
||||
>
|
||||
Create New Organization
|
||||
</span>
|
||||
}
|
||||
</mat-menu>
|
||||
<mat-nav-list class="nav-link-list">
|
||||
<a
|
||||
mat-list-item
|
||||
[disabled]="!activeOrganization()"
|
||||
[routerLink]="[activeOrganizationSlug(), 'issues']"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Issues</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[disabled]="!activeOrganization()"
|
||||
[routerLink]="[activeOrganizationSlug(), 'uptime-monitors']"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Uptime Monitors</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[disabled]="!activeOrganization()"
|
||||
[routerLink]="[
|
||||
activeOrganizationSlug(),
|
||||
'performance',
|
||||
'transaction-groups',
|
||||
]"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Performance</a
|
||||
>
|
||||
<mat-divider class="org-details-divider"></mat-divider>
|
||||
<a
|
||||
mat-list-item
|
||||
[disabled]="!activeOrganization()"
|
||||
[routerLink]="[activeOrganizationSlug(), 'projects']"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Projects</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[disabled]="!activeOrganization()"
|
||||
[routerLink]="[activeOrganizationSlug(), 'releases']"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Releases</a
|
||||
>
|
||||
<a
|
||||
data-test-settings
|
||||
mat-list-item
|
||||
[disabled]="!activeOrganization()"
|
||||
[routerLink]="[activeOrganizationSlug(), 'settings']"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Settings</a
|
||||
>
|
||||
<a
|
||||
class="sub-items"
|
||||
mat-list-item
|
||||
[disabled]="!activeOrganization()"
|
||||
[routerLink]="['/', activeOrganizationSlug(), 'settings', 'projects']"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Projects</a
|
||||
>
|
||||
@if (billingEnabled()) {
|
||||
<a
|
||||
class="sub-items"
|
||||
mat-list-item
|
||||
[routerLink]="[
|
||||
'/',
|
||||
activeOrganizationSlug(),
|
||||
'settings',
|
||||
'subscription',
|
||||
]"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Subscription</a
|
||||
>
|
||||
}
|
||||
<a
|
||||
mat-list-item
|
||||
[routerLink]="['/', activeOrganizationSlug(), 'settings', 'teams']"
|
||||
routerLinkActive="active-route"
|
||||
class="sub-items"
|
||||
i18n
|
||||
>Teams</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[routerLink]="['/', activeOrganizationSlug(), 'settings', 'members']"
|
||||
routerLinkActive="active-route"
|
||||
class="sub-items"
|
||||
i18n
|
||||
>Members</a
|
||||
>
|
||||
<mat-divider class="org-details-divider"></mat-divider>
|
||||
<a
|
||||
mat-list-item
|
||||
[routerLink]="['/profile']"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Profile</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[routerLink]="['/profile']"
|
||||
routerLinkActive="active-route"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
class="sub-items"
|
||||
i18n
|
||||
>Account</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[routerLink]="['/profile', 'multi-factor-auth']"
|
||||
routerLinkActive="active-route"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
class="sub-items"
|
||||
i18n
|
||||
>Multi-factor Authentication</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[routerLink]="['/profile', 'notifications']"
|
||||
routerLinkActive="active-route"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
class="sub-items"
|
||||
i18n
|
||||
>Notifications</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[routerLink]="['/profile', 'auth-tokens']"
|
||||
routerLinkActive="active-route"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
class="sub-items"
|
||||
i18n
|
||||
>Auth Tokens</a
|
||||
>
|
||||
<button mat-menu-item (click)="this.logout()" i18n>Log Out</button>
|
||||
</mat-nav-list>
|
||||
@if (paidForGlitchTip() === false) {
|
||||
<mat-card class="nagware">
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
<h3 i18n>Support GlitchTip</h3>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<ul class="mat-body-1">
|
||||
<li i18n
|
||||
>Donate
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://liberapay.com/GlitchTip/donate"
|
||||
>
|
||||
via Liberapay
|
||||
</a>
|
||||
</li>
|
||||
<li i18n>
|
||||
Switch to
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://app.glitchtip.com/register?utm_medium=website&utm_source=glitchtip&utm_campaign=selfhost_nagware"
|
||||
>Hosted GlitchTip</a
|
||||
>
|
||||
</li>
|
||||
<li i18n>
|
||||
Purchase support:
|
||||
<a
|
||||
href="mailto:sales@glitchtip.com?subject=Purchase support inquiry"
|
||||
>sales@glitchtip.com</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
<a
|
||||
mat-list-item
|
||||
[routerLink]="['/', activeOrganizationSlug(), 'settings', 'teams']"
|
||||
routerLinkActive="active-route"
|
||||
class="sub-items"
|
||||
i18n
|
||||
>Teams</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[routerLink]="['/', activeOrganizationSlug(), 'settings', 'members']"
|
||||
routerLinkActive="active-route"
|
||||
class="sub-items"
|
||||
i18n
|
||||
>Members</a
|
||||
>
|
||||
<mat-divider class="org-details-divider"></mat-divider>
|
||||
<a
|
||||
mat-list-item
|
||||
[routerLink]="['/profile']"
|
||||
routerLinkActive="active-route"
|
||||
i18n
|
||||
>Profile</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[routerLink]="['/profile']"
|
||||
routerLinkActive="active-route"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
class="sub-items"
|
||||
i18n
|
||||
>Account</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[routerLink]="['/profile', 'multi-factor-auth']"
|
||||
routerLinkActive="active-route"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
class="sub-items"
|
||||
i18n
|
||||
>Multi-factor Authentication</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[routerLink]="['/profile', 'notifications']"
|
||||
routerLinkActive="active-route"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
class="sub-items"
|
||||
i18n
|
||||
>Notifications</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
[routerLink]="['/profile', 'auth-tokens']"
|
||||
routerLinkActive="active-route"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
class="sub-items"
|
||||
i18n
|
||||
>Auth Tokens</a
|
||||
>
|
||||
<button mat-menu-item (click)="this.logout()" i18n>Log Out</button>
|
||||
</mat-nav-list>
|
||||
@if ((paidForGlitchTip()) === false) {
|
||||
<mat-card class="nagware">
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
<h3 i18n>Support GlitchTip</h3>
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<ul class="mat-body-1">
|
||||
<li i18n
|
||||
>Donate
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://liberapay.com/GlitchTip/donate"
|
||||
>
|
||||
via Liberapay
|
||||
</a>
|
||||
</li>
|
||||
<li i18n>
|
||||
Switch to
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://app.glitchtip.com/register?utm_medium=website&utm_source=glitchtip&utm_campaign=selfhost_nagware"
|
||||
>Hosted GlitchTip</a
|
||||
>
|
||||
</li>
|
||||
<li i18n>
|
||||
Purchase support:
|
||||
<a
|
||||
href="mailto:sales@glitchtip.com?subject=Purchase support inquiry"
|
||||
>sales@glitchtip.com</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
<div class="mat-typography version">
|
||||
<a [routerLink]="['/system-info']" routerLinkActive="active-route" i18n
|
||||
>GlitchTip {{ version() }}</a
|
||||
>
|
||||
</div>
|
||||
</mat-sidenav>
|
||||
<div class="mat-typography version">
|
||||
<a [routerLink]="['/system-info']" routerLinkActive="active-route" i18n
|
||||
>GlitchTip {{ version() }}</a
|
||||
>
|
||||
</div>
|
||||
</mat-sidenav>
|
||||
}
|
||||
|
||||
<mat-sidenav-content>
|
||||
@if (isLoggedIn()) {
|
||||
<gt-mobile-nav-toolbar
|
||||
[activeOrg]="activeOrganization()"
|
||||
(buttonClicked)="toggleSideNav()"
|
||||
></gt-mobile-nav-toolbar>
|
||||
<gt-mobile-nav-toolbar
|
||||
[activeOrg]="activeOrganization()"
|
||||
(buttonClicked)="toggleSideNav()"
|
||||
></gt-mobile-nav-toolbar>
|
||||
}
|
||||
<div class="wrapper">
|
||||
<ng-content></ng-content>
|
||||
|
||||
@@ -65,19 +65,19 @@ export class MainNavComponent {
|
||||
() =>
|
||||
this.settingsService.initialLoad() &&
|
||||
this.organizationsInitialLoad() &&
|
||||
!!this.userService.user()
|
||||
!!this.userService.user(),
|
||||
);
|
||||
|
||||
canCreateOrg = computed(
|
||||
() =>
|
||||
this.settingsService.enableOrganizationCreation() ||
|
||||
this.userService.user() ||
|
||||
this.organizationsService.organizationsCount()
|
||||
this.organizationsService.organizationsCount(),
|
||||
);
|
||||
|
||||
logout() {
|
||||
firstValueFrom(
|
||||
this.auth.logout().pipe(tap(() => (window.location.href = "/login")))
|
||||
this.auth.logout().pipe(tap(() => (window.location.href = "/login"))),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<div class="l-body">
|
||||
@if (contextLoaded$ | async) {
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<ng-container>
|
||||
@if (canCreateOrg$ | async) {
|
||||
<mat-card-title i18n>Create a New Organization</mat-card-title>
|
||||
@if ((organizationCount()) === 0) {
|
||||
<p i18n>This is the first step to get started with GlitchTip.</p>
|
||||
}
|
||||
<form
|
||||
[formGroup]="form"
|
||||
(ngSubmit)="onSubmit()"
|
||||
id="create-organization-form"
|
||||
>
|
||||
@if (error(); as error) {
|
||||
<mat-error>{{ error }}</mat-error>
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<ng-container>
|
||||
@if (canCreateOrg$ | async) {
|
||||
<mat-card-title i18n>Create a New Organization</mat-card-title>
|
||||
@if (organizationCount() === 0) {
|
||||
<p i18n>This is the first step to get started with GlitchTip.</p>
|
||||
}
|
||||
<form
|
||||
[formGroup]="form"
|
||||
(ngSubmit)="onSubmit()"
|
||||
id="create-organization-form"
|
||||
>
|
||||
@if (error(); as error) {
|
||||
<mat-error>{{ error }}</mat-error>
|
||||
}
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" i18n-placeholder />
|
||||
</mat-form-field>
|
||||
<button id="submit" mat-flat-button color="primary" i18n
|
||||
>Create Organization</button
|
||||
>
|
||||
</form>
|
||||
} @else {
|
||||
<ng-container i18n
|
||||
>Organization creation has been disabled on this instance of
|
||||
GlitchTip. Contact an administrator to be invited into an existing
|
||||
organization.</ng-container
|
||||
>
|
||||
}
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" i18n-placeholder />
|
||||
</mat-form-field>
|
||||
<button id="submit" mat-flat-button color="primary" i18n
|
||||
>Create Organization</button
|
||||
>
|
||||
</form>
|
||||
} @else {
|
||||
<ng-container i18n
|
||||
>Organization creation has been disabled on this instance of
|
||||
GlitchTip. Contact an administrator to be invited into an existing
|
||||
organization.</ng-container
|
||||
>
|
||||
}
|
||||
</ng-container>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</ng-container>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, signal, inject } from "@angular/core";
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
signal,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import {
|
||||
FormGroup,
|
||||
FormControl,
|
||||
@@ -49,7 +54,7 @@ export class NewOrganizationsComponent {
|
||||
]).pipe(
|
||||
map(([user, orgCount, enableOrgCreation]) => {
|
||||
return enableOrgCreation || user?.isSuperuser || orgCount === 0;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
contextLoaded$ = combineLatest([
|
||||
@@ -59,7 +64,7 @@ export class NewOrganizationsComponent {
|
||||
]).pipe(
|
||||
map(([settingsLoaded, orgsLoaded, user]) => {
|
||||
return settingsLoaded && orgsLoaded && !!user;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
loading = false;
|
||||
|
||||
@@ -58,7 +58,7 @@ export class OrganizationFrameComponent implements OnDestroy, OnInit {
|
||||
["../", activeOrganizationSlug, this.firstChildRoute],
|
||||
{
|
||||
relativeTo: this.route,
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ export class OrganizationFrameComponent implements OnDestroy, OnInit {
|
||||
.pipe(filter((event) => event instanceof NavigationEnd))
|
||||
.subscribe(() => {
|
||||
this.extractFirstChildRoute();
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export class PerformanceService extends PaginationStatefulService<PerformanceSta
|
||||
private organizationsService = inject(OrganizationsService);
|
||||
|
||||
transactionGroups$ = this.getState$.pipe(
|
||||
map((state) => state.transactionGroups)
|
||||
map((state) => state.transactionGroups),
|
||||
);
|
||||
|
||||
transactionGroupsDisplay$ = combineLatest([
|
||||
@@ -42,14 +42,14 @@ export class PerformanceService extends PaginationStatefulService<PerformanceSta
|
||||
map(([projects, groups]) => {
|
||||
return groups.map((group) => {
|
||||
const projectSlug = projects?.find(
|
||||
(project) => +project.id === group.project
|
||||
(project) => +project.id === group.project,
|
||||
)?.name;
|
||||
return {
|
||||
...group,
|
||||
projectSlug,
|
||||
};
|
||||
});
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
errors$ = this.getState$.pipe(map((state) => state.errors));
|
||||
@@ -66,7 +66,7 @@ export class PerformanceService extends PaginationStatefulService<PerformanceSta
|
||||
end: string | undefined | null,
|
||||
sort: string | undefined | null,
|
||||
environment: string | undefined | null,
|
||||
query: string | undefined | null
|
||||
query: string | undefined | null,
|
||||
) {
|
||||
return this.retrieveTransactionGroups(
|
||||
orgSlug,
|
||||
@@ -76,7 +76,7 @@ export class PerformanceService extends PaginationStatefulService<PerformanceSta
|
||||
end,
|
||||
sort,
|
||||
environment,
|
||||
query
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export class PerformanceService extends PaginationStatefulService<PerformanceSta
|
||||
end?: string | null,
|
||||
sort?: string | null,
|
||||
environment?: string | null,
|
||||
query?: string | null
|
||||
query?: string | null,
|
||||
) {
|
||||
this.setLoadingStart();
|
||||
return this.transactionGroupsAPIService
|
||||
@@ -100,7 +100,7 @@ export class PerformanceService extends PaginationStatefulService<PerformanceSta
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
this.setTransactionGroupsError(err);
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export class TransactionGroupDetailComponent implements OnInit, OnDestroy {
|
||||
this.transactionGroupDetailService.transactionGroupInitialLoadComplete$;
|
||||
transactionGroup$ = this.transactionGroupDetailService.transactionGroup$;
|
||||
transactionGroupIdParam$ = this.route.paramMap.pipe(
|
||||
map((params) => params.get("transaction-group-id"))
|
||||
map((params) => params.get("transaction-group-id")),
|
||||
);
|
||||
retrieveDetailsSubscription?: Subscription;
|
||||
|
||||
@@ -51,11 +51,11 @@ export class TransactionGroupDetailComponent implements OnInit, OnDestroy {
|
||||
if (orgSlug && groupId) {
|
||||
return this.transactionGroupDetailService.retrieveTransactionGroup(
|
||||
orgSlug,
|
||||
parseInt(groupId)
|
||||
parseInt(groupId),
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export class TransactionGroupsComponent implements OnInit, OnDestroy {
|
||||
this.organizationsService.activeOrganizationProjects$;
|
||||
|
||||
projectsFromParams$ = this.route.queryParams.pipe(
|
||||
map((params) => normalizeProjectParams(params.project))
|
||||
map((params) => normalizeProjectParams(params.project)),
|
||||
);
|
||||
|
||||
appliedProjectCount$ = this.projectsFromParams$.pipe(
|
||||
@@ -99,19 +99,19 @@ export class TransactionGroupsComponent implements OnInit, OnDestroy {
|
||||
return projects.length;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
organizationEnvironments$ = combineLatest([
|
||||
this.appliedProjectCount$,
|
||||
toObservable(
|
||||
this.organizationDetailService.organizationEnvironmentsProcessed
|
||||
this.organizationDetailService.organizationEnvironmentsProcessed,
|
||||
),
|
||||
this.projectEnvironmentsService.visibleEnvironments$,
|
||||
]).pipe(
|
||||
map(([appliedProjectCount, orgEnvironments, projectEnvironments]) =>
|
||||
appliedProjectCount !== 1 ? orgEnvironments : projectEnvironments
|
||||
)
|
||||
appliedProjectCount !== 1 ? orgEnvironments : projectEnvironments,
|
||||
),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
@@ -128,19 +128,19 @@ export class TransactionGroupsComponent implements OnInit, OnDestroy {
|
||||
params.get("end"),
|
||||
params.get("sort"),
|
||||
params.get("environment"),
|
||||
params.get("query")
|
||||
params.get("query"),
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
takeUntilDestroyed()
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.organizationEnvironments$.subscribe((environments) =>
|
||||
environments.length === 0
|
||||
? this.environmentForm.controls.environment.disable()
|
||||
: this.environmentForm.controls.environment.enable()
|
||||
: this.environmentForm.controls.environment.enable(),
|
||||
);
|
||||
|
||||
this.transactionGroupsDisplay$
|
||||
@@ -148,7 +148,7 @@ export class TransactionGroupsComponent implements OnInit, OnDestroy {
|
||||
.subscribe((groups) =>
|
||||
groups.length === 0
|
||||
? this.sortForm.controls.sort.disable()
|
||||
: this.sortForm.controls.sort.enable()
|
||||
: this.sortForm.controls.sort.enable(),
|
||||
);
|
||||
|
||||
this.orgSlug$
|
||||
@@ -156,12 +156,12 @@ export class TransactionGroupsComponent implements OnInit, OnDestroy {
|
||||
switchMap((orgSlug) => {
|
||||
if (orgSlug) {
|
||||
return this.organizationDetailService.getOrganizationEnvironments(
|
||||
orgSlug
|
||||
orgSlug,
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
takeUntilDestroyed()
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
@@ -170,23 +170,23 @@ export class TransactionGroupsComponent implements OnInit, OnDestroy {
|
||||
this.route.queryParamMap.pipe(
|
||||
map((params) => params.getAll("project")[0]),
|
||||
filter((project) => !!project),
|
||||
distinctUntilChanged()
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
this.organizationsService.activeOrganizationProjects$,
|
||||
]).pipe(
|
||||
switchMap(([orgSlug, projectId, orgProjects]) => {
|
||||
const projectSlug = orgProjects?.find(
|
||||
(orgProject) => orgProject.id.toString() === projectId
|
||||
(orgProject) => orgProject.id.toString() === projectId,
|
||||
)?.slug;
|
||||
if (orgSlug && projectSlug) {
|
||||
return this.projectEnvironmentsService.retrieveEnvironmentsWithProperties(
|
||||
orgSlug,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
takeUntilDestroyed()
|
||||
takeUntilDestroyed(),
|
||||
);
|
||||
|
||||
combineLatest([
|
||||
@@ -207,7 +207,7 @@ export class TransactionGroupsComponent implements OnInit, OnDestroy {
|
||||
queryParamsHandling: "merge",
|
||||
});
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { AfterViewInit, Directive, ElementRef, HostBinding, OnInit, input, inject } from "@angular/core";
|
||||
import {
|
||||
AfterViewInit,
|
||||
Directive,
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
OnInit,
|
||||
input,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import Prism from "prismjs";
|
||||
import { GRAMMAR_MAPPINGS, PRISM_SUPPORTED_GRAMMAR } from "./constants";
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<gt-loading-button
|
||||
data-test
|
||||
buttonText="Delete User Account"
|
||||
[loading]="(userDeleteLoading()) === true"
|
||||
[loading]="userDeleteLoading() === true"
|
||||
(buttonClick)="deleteUser()"
|
||||
id="delete-user"
|
||||
>
|
||||
|
||||
@@ -35,7 +35,7 @@ export class AccountComponent implements OnDestroy {
|
||||
deleteUser() {
|
||||
if (
|
||||
window.confirm(
|
||||
`Are you sure you want to delete your user account? You will permanently lose access to all organizations, projects, and teams associated with it.`
|
||||
`Are you sure you want to delete your user account? You will permanently lose access to all organizations, projects, and teams associated with it.`,
|
||||
)
|
||||
) {
|
||||
this.userService.deleteUser().then((result) => {
|
||||
|
||||
@@ -37,7 +37,7 @@ export class AuthTokensComponent
|
||||
const service = inject(AuthTokensService);
|
||||
|
||||
super(service);
|
||||
|
||||
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,60 +1,89 @@
|
||||
@if (user(); as user) {
|
||||
<mat-card class="half-width">
|
||||
<mat-card-header>
|
||||
<mat-card-title i18n>Change Password</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content>
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()" id="change-password-form">
|
||||
<gt-form-error [errors]="formErrors()"></gt-form-error>
|
||||
@if (user.hasPasswordAuth) {
|
||||
<mat-form-field class="full-width first-field" appearance="outline">
|
||||
<mat-label i18n>Current Password</mat-label>
|
||||
<input matInput type="password" formControlName="current_password" />
|
||||
<mat-error>
|
||||
@if (current_password?.errors?.required) {
|
||||
<span i18n>Your current is required to change your password</span>
|
||||
}
|
||||
@if (current_password?.errors?.serverError) {
|
||||
<span>{{ current_password?.errors?.serverError }}</span>
|
||||
}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
}
|
||||
<mat-form-field class="full-width" [class.first-field]="!user.hasPasswordAuth" appearance="outline">
|
||||
<mat-label i18n>New Password</mat-label>
|
||||
<input matInput type="password" formControlName="new_password" />
|
||||
<mat-error>
|
||||
@if (new_password?.errors?.minlength) {
|
||||
<span i18n>Your password should be at least 8 characters long</span>
|
||||
}
|
||||
@if (new_password?.errors?.required) {
|
||||
<span i18n>Type in your new password</span>
|
||||
}
|
||||
@if (new_password?.errors?.serverError) {
|
||||
<span>{{ new_password?.errors?.serverError }}</span>
|
||||
}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width" data-cy="confirm-password" appearance="outline">
|
||||
<mat-label i18n>Confirm New Password</mat-label>
|
||||
<input matInput type="password" formControlName="new_password2" gtInputMatcher="new_password" />
|
||||
<mat-error>
|
||||
@if (new_password2?.errors?.required) {
|
||||
<span i18n>Type in your new password</span>
|
||||
}
|
||||
@if (new_password2?.errors?.notEqual) {
|
||||
<span i18n>Your passwords don't match</span>
|
||||
}
|
||||
@if (new_password2?.errors?.serverError) {
|
||||
<span>{{ new_password2?.errors?.serverError }}</span>
|
||||
}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<gt-loading-button i18n-buttonText buttonText="Change Password" [loading]="loading()">
|
||||
</gt-loading-button>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<section></section>
|
||||
}
|
||||
<mat-card class="half-width">
|
||||
<mat-card-header>
|
||||
<mat-card-title i18n>Change Password</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content>
|
||||
<form
|
||||
[formGroup]="form"
|
||||
(ngSubmit)="onSubmit()"
|
||||
id="change-password-form"
|
||||
>
|
||||
<gt-form-error [errors]="formErrors()"></gt-form-error>
|
||||
@if (user.hasPasswordAuth) {
|
||||
<mat-form-field class="full-width first-field" appearance="outline">
|
||||
<mat-label i18n>Current Password</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="password"
|
||||
formControlName="current_password"
|
||||
/>
|
||||
<mat-error>
|
||||
@if (current_password?.errors?.required) {
|
||||
<span i18n
|
||||
>Your current is required to change your password</span
|
||||
>
|
||||
}
|
||||
@if (current_password?.errors?.serverError) {
|
||||
<span>{{ current_password?.errors?.serverError }}</span>
|
||||
}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
}
|
||||
<mat-form-field
|
||||
class="full-width"
|
||||
[class.first-field]="!user.hasPasswordAuth"
|
||||
appearance="outline"
|
||||
>
|
||||
<mat-label i18n>New Password</mat-label>
|
||||
<input matInput type="password" formControlName="new_password" />
|
||||
<mat-error>
|
||||
@if (new_password?.errors?.minlength) {
|
||||
<span i18n
|
||||
>Your password should be at least 8 characters long</span
|
||||
>
|
||||
}
|
||||
@if (new_password?.errors?.required) {
|
||||
<span i18n>Type in your new password</span>
|
||||
}
|
||||
@if (new_password?.errors?.serverError) {
|
||||
<span>{{ new_password?.errors?.serverError }}</span>
|
||||
}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field
|
||||
class="full-width"
|
||||
data-cy="confirm-password"
|
||||
appearance="outline"
|
||||
>
|
||||
<mat-label i18n>Confirm New Password</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="password"
|
||||
formControlName="new_password2"
|
||||
gtInputMatcher="new_password"
|
||||
/>
|
||||
<mat-error>
|
||||
@if (new_password2?.errors?.required) {
|
||||
<span i18n>Type in your new password</span>
|
||||
}
|
||||
@if (new_password2?.errors?.notEqual) {
|
||||
<span i18n>Your passwords don't match</span>
|
||||
}
|
||||
@if (new_password2?.errors?.serverError) {
|
||||
<span>{{ new_password2?.errors?.serverError }}</span>
|
||||
}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<gt-loading-button
|
||||
i18n-buttonText
|
||||
buttonText="Change Password"
|
||||
[loading]="loading()"
|
||||
>
|
||||
</gt-loading-button>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<section></section>
|
||||
}
|
||||
@@ -88,7 +88,7 @@ export class ChangePasswordComponent
|
||||
const service = inject(PasswordService);
|
||||
|
||||
toObservable(service.fieldErrors).subscribe((fieldErrors) =>
|
||||
mapFormErrors(fieldErrors, this.form)
|
||||
mapFormErrors(fieldErrors, this.form),
|
||||
);
|
||||
super(service);
|
||||
|
||||
@@ -105,7 +105,7 @@ export class ChangePasswordComponent
|
||||
this.service
|
||||
.changePassword(
|
||||
this.form.value.current_password!,
|
||||
this.form.value.new_password!
|
||||
this.form.value.new_password!,
|
||||
)
|
||||
.pipe(
|
||||
tap(() => {
|
||||
@@ -114,8 +114,8 @@ export class ChangePasswordComponent
|
||||
Object.keys(this.form.controls).forEach((key) => {
|
||||
this.form.get(key)!.setErrors(null);
|
||||
});
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ export class PasswordService extends StatefulService<PasswordState> {
|
||||
errors = computed(() => this.state().errors);
|
||||
success = computed(() => this.state().success);
|
||||
formErrors = computed(() =>
|
||||
messagesLookup(this.state().errors.filter((err) => !err.param))
|
||||
messagesLookup(this.state().errors.filter((err) => !err.param)),
|
||||
);
|
||||
fieldErrors = computed(() =>
|
||||
reduceParamErrors(this.state().errors.filter((err) => err.param))
|
||||
reduceParamErrors(this.state().errors.filter((err) => err.param)),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
@@ -65,7 +65,7 @@ export class PasswordService extends StatefulService<PasswordState> {
|
||||
return of(EMPTY);
|
||||
}
|
||||
return throwError(() => new Error("Unable to change password"));
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { map } from "rxjs/operators";
|
||||
import { ConfirmEmailService } from "../../api/confirm-email/confirm-email.service";
|
||||
@@ -13,7 +18,6 @@ export class ConfirmEmailComponent implements OnInit {
|
||||
private activatedRoute = inject(ActivatedRoute);
|
||||
private confirmEmailService = inject(ConfirmEmailService);
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
this.activatedRoute.params
|
||||
.pipe(map((params) => this.confirmEmailService.confirmEmail(params.key)))
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import { MultiFactorAuthService } from "./multi-factor-auth.service";
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Component, ChangeDetectionStrategy, ElementRef, ViewChild, OnInit, OnDestroy, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
ElementRef,
|
||||
ViewChild,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { NotificationsService } from "./notifications.service";
|
||||
import { NotificationStatus } from "./notifications.interface";
|
||||
import { RouterLink } from "@angular/router";
|
||||
|
||||
@@ -65,7 +65,7 @@ export class PreferencesComponent implements OnInit {
|
||||
}),
|
||||
theme: new FormControl(
|
||||
"",
|
||||
autocompleteStringValidator(["system", "light", "dark"])
|
||||
autocompleteStringValidator(["system", "light", "dark"]),
|
||||
),
|
||||
});
|
||||
filteredOptions?: Observable<string[]>;
|
||||
@@ -79,7 +79,7 @@ export class PreferencesComponent implements OnInit {
|
||||
this.serverTimeZone$
|
||||
.pipe(
|
||||
filter((serverTimeZone) => !!serverTimeZone),
|
||||
take(1)
|
||||
take(1),
|
||||
)
|
||||
.subscribe((serverTimeZone) => {
|
||||
this.defaultTimeZone = this.defaultTimeZone + ` \(${serverTimeZone}\)`;
|
||||
@@ -106,7 +106,7 @@ export class PreferencesComponent implements OnInit {
|
||||
});
|
||||
this.filteredOptions = this.form.controls["timeZone"].valueChanges.pipe(
|
||||
startWith(""),
|
||||
map((value) => this._filter(value || ""))
|
||||
map((value) => this._filter(value || "")),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ export class PreferencesComponent implements OnInit {
|
||||
const filterValue = value.toLowerCase().replace(/\s/g, "_");
|
||||
|
||||
return this.timeZones.filter((option) =>
|
||||
option.toLowerCase().includes(filterValue)
|
||||
option.toLowerCase().includes(filterValue),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,30 +2,53 @@
|
||||
<mat-drawer class="mat-elevation-z2" mode="side" opened>
|
||||
<nav>
|
||||
<mat-nav-list>
|
||||
<a mat-list-item routerLink="/profile" routerLinkActive="active-route"
|
||||
[routerLinkActiveOptions]="{ exact: true }">Account</a>
|
||||
<a mat-list-item routerLink="./multi-factor-auth" routerLinkActive="active-route"
|
||||
[routerLinkActiveOptions]="{ exact: true }">Multi-factor Authentication</a>
|
||||
<a mat-list-item routerLink="./notifications" routerLinkActive="active-route"
|
||||
[routerLinkActiveOptions]="{ exact: true }">Notifications</a>
|
||||
<a mat-list-item routerLink="./auth-tokens" routerLinkActive="active-route">Auth Tokens</a>
|
||||
<a
|
||||
mat-list-item
|
||||
routerLink="/profile"
|
||||
routerLinkActive="active-route"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>Account</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
routerLink="./multi-factor-auth"
|
||||
routerLinkActive="active-route"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>Multi-factor Authentication</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
routerLink="./notifications"
|
||||
routerLinkActive="active-route"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>Notifications</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
routerLink="./auth-tokens"
|
||||
routerLinkActive="active-route"
|
||||
>Auth Tokens</a
|
||||
>
|
||||
</mat-nav-list>
|
||||
</nav>
|
||||
</mat-drawer>
|
||||
<mat-drawer-content>
|
||||
@if (isLoggedIn()) {
|
||||
<gt-mobile-nav-toolbar [activeOrg]="activeOrganization()" (buttonClicked)="toggleSideNav()"></gt-mobile-nav-toolbar>
|
||||
<gt-mobile-nav-toolbar
|
||||
[activeOrg]="activeOrganization()"
|
||||
(buttonClicked)="toggleSideNav()"
|
||||
></gt-mobile-nav-toolbar>
|
||||
}
|
||||
<section class="l-body">
|
||||
<header class="page-header">
|
||||
<h1 class="mat-headline-4">User Profile</h1>
|
||||
@if (user(); as user) {
|
||||
<p class="mat-body-1 subtitle">
|
||||
{{ user.email }}
|
||||
</p>
|
||||
<p class="mat-body-1 subtitle">
|
||||
{{ user.email }}
|
||||
</p>
|
||||
}
|
||||
</header>
|
||||
<router-outlet></router-outlet>
|
||||
</section>
|
||||
</mat-drawer-content>
|
||||
</mat-drawer-container>
|
||||
</mat-drawer-container>
|
||||
@@ -1,58 +1,73 @@
|
||||
@if (socialApps().length || (user$ | async)?.identities?.length) {
|
||||
@if (user$ | async; as user) {
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Social Auth Accounts</mat-card-title>
|
||||
@if ((socialApps()).length) {
|
||||
@if (socialApps(); as socialApps) {
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Add Account</mat-label>
|
||||
<mat-select (selectionChange)="addAccount()" [formControl]="account">
|
||||
@for (socialApp of socialApps; track socialApp) {
|
||||
<mat-option [value]="socialApp">
|
||||
<gt-auth-svg source="dropdown" [provider]="socialApp.provider" [text]="socialApp.name"></gt-auth-svg>
|
||||
</mat-option>
|
||||
@if (user$ | async; as user) {
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Social Auth Accounts</mat-card-title>
|
||||
@if (socialApps().length) {
|
||||
@if (socialApps(); as socialApps) {
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Add Account</mat-label>
|
||||
<mat-select
|
||||
(selectionChange)="addAccount()"
|
||||
[formControl]="account"
|
||||
>
|
||||
@for (socialApp of socialApps; track socialApp) {
|
||||
<mat-option [value]="socialApp">
|
||||
<gt-auth-svg
|
||||
source="dropdown"
|
||||
[provider]="socialApp.provider"
|
||||
[text]="socialApp.name"
|
||||
></gt-auth-svg>
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
}
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content>
|
||||
@if ((user$ | async)?.identities?.length === 0) {
|
||||
<p class="empty-auth">
|
||||
You haven't connected any social auth accounts.
|
||||
</p>
|
||||
} @else {
|
||||
<div class="socialaccount-list-wrapper">
|
||||
@for (
|
||||
socialAccount of (user$ | async)?.identities;
|
||||
track socialAccount;
|
||||
let lastItem = $last
|
||||
) {
|
||||
<div class="socialaccount-wrapper">
|
||||
<button mat-stroked-button (click)="
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content>
|
||||
@if ((user$ | async)?.identities?.length === 0) {
|
||||
<p class="empty-auth">
|
||||
You haven't connected any social auth accounts.
|
||||
</p>
|
||||
} @else {
|
||||
<div class="socialaccount-list-wrapper">
|
||||
@for (
|
||||
socialAccount of (user$ | async)?.identities;
|
||||
track socialAccount;
|
||||
let lastItem = $last
|
||||
) {
|
||||
<div class="socialaccount-wrapper">
|
||||
<button
|
||||
mat-stroked-button
|
||||
(click)="
|
||||
disconnect(
|
||||
socialAccount.id!,
|
||||
socialAccount.provider,
|
||||
socialAccount.uid
|
||||
)
|
||||
" [disabled]="disconnectLoadingId() === socialAccount.id">
|
||||
<gt-auth-svg [loading]="disconnectLoadingId() === socialAccount.id" source="disconnect"
|
||||
text="Disconnect {{ socialAccount.name }}" [provider]="socialAccount.provider"></gt-auth-svg>
|
||||
</button>
|
||||
<div class="email">
|
||||
{{ socialAccount.email }}
|
||||
</div>
|
||||
</div>
|
||||
@if (!lastItem) {
|
||||
<mat-divider></mat-divider>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
"
|
||||
[disabled]="disconnectLoadingId() === socialAccount.id"
|
||||
>
|
||||
<gt-auth-svg
|
||||
[loading]="disconnectLoadingId() === socialAccount.id"
|
||||
source="disconnect"
|
||||
text="Disconnect {{ socialAccount.name }}"
|
||||
[provider]="socialAccount.provider"
|
||||
></gt-auth-svg>
|
||||
</button>
|
||||
<div class="email">
|
||||
{{ socialAccount.email }}
|
||||
</div>
|
||||
</div>
|
||||
@if (!lastItem) {
|
||||
<mat-divider></mat-divider>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { FormControl, ReactiveFormsModule } from "@angular/forms";
|
||||
import { catchError, lastValueFrom, of, tap, throwError } from "rxjs";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
@@ -54,7 +59,7 @@ export class SocialAuthComponent
|
||||
const service = inject(SocialAuthService);
|
||||
|
||||
super(service);
|
||||
|
||||
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@@ -69,7 +74,7 @@ export class SocialAuthComponent
|
||||
this.authenticationService.providerRedirect(
|
||||
this.account.value.provider,
|
||||
window.location.href,
|
||||
"connect"
|
||||
"connect",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,7 +84,7 @@ export class SocialAuthComponent
|
||||
this.service.disconnect(id, provider, account).pipe(
|
||||
tap(() => {
|
||||
this.snackBar.open(
|
||||
$localize`You have successfully disconnected your social auth account`
|
||||
$localize`You have successfully disconnected your social auth account`,
|
||||
);
|
||||
}),
|
||||
catchError((err: AllAuthHttpErrorResponse) => {
|
||||
@@ -89,8 +94,8 @@ export class SocialAuthComponent
|
||||
}
|
||||
this.snackBar.open(UNHANDLED_ERROR);
|
||||
return throwError(() => err);
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
this.snackBar.open($localize`Your account has no password set up.`);
|
||||
|
||||
@@ -33,16 +33,16 @@ export class SocialAuthService extends StatefulService<SocialAuthState> {
|
||||
return {
|
||||
...socialAccount,
|
||||
name: socialApps.find(
|
||||
(socialApp) => socialApp.provider === socialAccount.provider
|
||||
(socialApp) => socialApp.provider === socialAccount.provider,
|
||||
)?.name,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
return {
|
||||
...userDetails,
|
||||
identities: socialAccountsWithNames,
|
||||
};
|
||||
})
|
||||
}),
|
||||
);
|
||||
constructor() {
|
||||
super(initialState);
|
||||
@@ -58,7 +58,7 @@ export class SocialAuthService extends StatefulService<SocialAuthState> {
|
||||
catchError((err: AllAuthHttpErrorResponse) => {
|
||||
this.setState({ loadingId: null });
|
||||
return throwError(() => err);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export class RateLimitBannerComponent {
|
||||
private organizationsService = inject(OrganizationsService);
|
||||
|
||||
eventThrottleRate$ = this.organizationsService.activeOrganization$.pipe(
|
||||
map((activeOrganization) => activeOrganization?.eventThrottleRate)
|
||||
map((activeOrganization) => activeOrganization?.eventThrottleRate),
|
||||
);
|
||||
activeOrgSlug$ = this.organizationsService.activeOrganizationSlug$;
|
||||
bannerVisible = true;
|
||||
|
||||
@@ -7,46 +7,63 @@
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
@if (success()) {
|
||||
<p class="success" data-cy="reset-success" i18n>If you have an account with GlitchTip, an email has been sent to
|
||||
the
|
||||
email address you provided.</p>
|
||||
<button mat-flat-button color="primary" class="full-width" routerLink="/login">
|
||||
<span i18n>Return to login</span>
|
||||
</button>
|
||||
<p class="success" data-cy="reset-success" i18n
|
||||
>If you have an account with GlitchTip, an email has been sent to the
|
||||
email address you provided.</p
|
||||
>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
class="full-width"
|
||||
routerLink="/login"
|
||||
>
|
||||
<span i18n>Return to login</span>
|
||||
</button>
|
||||
} @else {
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||
<gt-form-error [errors]="formErrors()"></gt-form-error>
|
||||
<mat-form-field class="full-width" appearance="outline">
|
||||
<mat-label i18n>Email</mat-label>
|
||||
<input [readonly]="success()" matInput type="email" formControlName="email" />
|
||||
@if (email?.errors) {
|
||||
<mat-error>
|
||||
@if (email?.errors?.required || email?.errors?.email) {
|
||||
<span i18n>Enter a valid email address</span>
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||
<gt-form-error [errors]="formErrors()"></gt-form-error>
|
||||
<mat-form-field class="full-width" appearance="outline">
|
||||
<mat-label i18n>Email</mat-label>
|
||||
<input
|
||||
[readonly]="success()"
|
||||
matInput
|
||||
type="email"
|
||||
formControlName="email"
|
||||
/>
|
||||
@if (email?.errors) {
|
||||
<mat-error>
|
||||
@if (email?.errors?.required || email?.errors?.email) {
|
||||
<span i18n>Enter a valid email address</span>
|
||||
}
|
||||
@if (email?.errors?.serverError) {
|
||||
<span>
|
||||
{{ email?.errors?.serverError }}
|
||||
</span>
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
@if (email?.errors?.serverError) {
|
||||
<span>
|
||||
{{ email?.errors?.serverError }}
|
||||
</span>
|
||||
}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<gt-loading-button
|
||||
class="full-width"
|
||||
buttonText="Send Reset Password Email"
|
||||
[fullWidth]="true"
|
||||
[loading]="loading()"
|
||||
[disabled]="success()"
|
||||
id="submit"
|
||||
>
|
||||
</gt-loading-button>
|
||||
</form>
|
||||
<div class="sign-up">
|
||||
<p class="mat-body-1">
|
||||
<a routerLink="/login" i18n>Log in</a>
|
||||
</p>
|
||||
@if (enableUserRegistration()) {
|
||||
<p class="mat-body-1">
|
||||
<a routerLink="/register" i18n>Register</a>
|
||||
</p>
|
||||
}
|
||||
</mat-form-field>
|
||||
<gt-loading-button class="full-width" buttonText="Send Reset Password Email" [fullWidth]="true"
|
||||
[loading]="loading()" [disabled]="success()" id="submit">
|
||||
</gt-loading-button>
|
||||
</form>
|
||||
<div class="sign-up">
|
||||
<p class="mat-body-1">
|
||||
<a routerLink="/login" i18n>Log in</a>
|
||||
</p>
|
||||
@if (enableUserRegistration()) {
|
||||
<p class="mat-body-1">
|
||||
<a routerLink="/register" i18n>Register</a>
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ export class ResetPasswordComponent extends StatefulComponent<
|
||||
const service = inject(ResetPasswordService);
|
||||
|
||||
toObservable(service.fieldErrors).subscribe((fieldErrors) =>
|
||||
mapFormErrors(fieldErrors, this.form)
|
||||
mapFormErrors(fieldErrors, this.form),
|
||||
);
|
||||
super(service);
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ export class SetNewPasswordComponent extends StatefulComponent<
|
||||
mapFormErrors(fieldErrors, this.form),
|
||||
);
|
||||
super(service);
|
||||
|
||||
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,152 +1,156 @@
|
||||
@if (member(); as member) {
|
||||
<section class="l-body">
|
||||
<gt-detail-header [title]="member.email" backLinkText="Back to Members" />
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title il8n>Basics</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content class="basics-content">
|
||||
<div class="column">
|
||||
<div class="mat-body-strong" i18n>Email</div>
|
||||
<div>{{ member.email }}</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="mat-body-strong" i18n>Role</div>
|
||||
<div>{{ member.roleName }}</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="mat-body-strong" i18n>Added</div>
|
||||
<div>{{ member.dateCreated | date }}</div>
|
||||
</div>
|
||||
@if (member.isOwner) {
|
||||
<div class="column">
|
||||
<mat-chip color="primary" highlighted="true" i18n
|
||||
>Account Owner</mat-chip
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title i18n>Role</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content>
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||
@if (updateMemberError(); as error) {
|
||||
<mat-error>{{ error }}</mat-error>
|
||||
}
|
||||
<div class="role-form-elements">
|
||||
<mat-radio-group formControlName="role">
|
||||
<mat-radio-button value="member">Member</mat-radio-button>
|
||||
<mat-radio-button value="admin">Admin</mat-radio-button>
|
||||
<mat-radio-button value="manager">Manager</mat-radio-button>
|
||||
<mat-radio-button value="owner">Owner</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<div class="role-button-container">
|
||||
<gt-loading-button
|
||||
[disabled]="member.role === formRole.value"
|
||||
[loading]="updateMemberLoading() === true"
|
||||
buttonText="Update Member"
|
||||
></gt-loading-button>
|
||||
</div>
|
||||
<section class="l-body">
|
||||
<gt-detail-header [title]="member.email" backLinkText="Back to Members" />
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title il8n>Basics</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content class="basics-content">
|
||||
<div class="column">
|
||||
<div class="mat-body-strong" i18n>Email</div>
|
||||
<div>{{ member.email }}</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="role-description">
|
||||
@if (formRole.value === "member") {
|
||||
<span i18n
|
||||
>Members can view and act on events, as well as view most other data
|
||||
within the organization.</span
|
||||
>
|
||||
} @if (formRole.value === "admin") {
|
||||
<span i18n
|
||||
>Admin privileges on any teams of which user is a member. They can
|
||||
create new teams and projects, as well as delete teams and projects
|
||||
they are already members of. Additionally, they can manage memberships
|
||||
of teams that they are members of.</span
|
||||
>
|
||||
} @if (formRole.value === "manager") {
|
||||
<span i18n
|
||||
>Gains admin access on all teams as well as the ability to add and
|
||||
remove members.</span
|
||||
>
|
||||
} @if (formRole.value === "owner") {
|
||||
<span i18n
|
||||
>Unrestricted access to the organization, its data, and its settings.
|
||||
Can add, modify, and delete projects and members.</span
|
||||
>
|
||||
<div class="column">
|
||||
<div class="mat-body-strong" i18n>Role</div>
|
||||
<div>{{ member.roleName }}</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="mat-body-strong" i18n>Added</div>
|
||||
<div>{{ member.dateCreated | date }}</div>
|
||||
</div>
|
||||
@if (member.isOwner) {
|
||||
<div class="column">
|
||||
<mat-chip color="primary" highlighted="true" i18n
|
||||
>Account Owner</mat-chip
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="role-scopes">
|
||||
@if (selectedRoleScopes(); as scopes) {
|
||||
<div class="mat-body-strong" i18n>Scopes:</div>
|
||||
<div>{{ scopes }}</div>
|
||||
}
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title i18n>Organization Account Ownership</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content>
|
||||
@if (!member.isOwner) { @if (transferOrgOwnershipError(); as error) {
|
||||
<mat-error>{{ error }}</mat-error>
|
||||
}
|
||||
<p i18n>
|
||||
The account owner has access to the organization's GlitchTip
|
||||
subscription settings. If you are the current account owner or have an
|
||||
owner role, you may transfer ownership to this member.
|
||||
</p>
|
||||
<gt-loading-button
|
||||
buttonText="Transfer Ownership"
|
||||
[loading]="transferOrgOwnershipLoading() === true"
|
||||
(buttonClick)="transferOrgOwnership()"
|
||||
>
|
||||
Change Password
|
||||
</gt-loading-button>
|
||||
} @else {
|
||||
<p i18n>
|
||||
This member is the organization account owner and has access to the
|
||||
organization's GlitchTip subscription settings.
|
||||
</p>
|
||||
}
|
||||
<ng-template #isOwner>
|
||||
<p i18n>
|
||||
This member is the organization account owner and has access to the
|
||||
organization's GlitchTip subscription settings.
|
||||
</p>
|
||||
</ng-template>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@if (memberTeams(); as memberTeams) {
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title i18n>Team(s)</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content>
|
||||
@if (memberTeams.length === 0) {
|
||||
<p class="center-text" i18n>This user isn't on any teams.</p>
|
||||
} @else {
|
||||
<mat-list>
|
||||
@for (team of memberTeams; track team) {
|
||||
<mat-list-item>
|
||||
<a
|
||||
mat-line
|
||||
[routerLink]="['teams', team, 'members']"
|
||||
[relativeTo]="this.route.parent?.parent"
|
||||
>{{ team }}</a
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title i18n>Role</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content>
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||
@if (updateMemberError(); as error) {
|
||||
<mat-error>{{ error }}</mat-error>
|
||||
}
|
||||
<div class="role-form-elements">
|
||||
<mat-radio-group formControlName="role">
|
||||
<mat-radio-button value="member">Member</mat-radio-button>
|
||||
<mat-radio-button value="admin">Admin</mat-radio-button>
|
||||
<mat-radio-button value="manager">Manager</mat-radio-button>
|
||||
<mat-radio-button value="owner">Owner</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<div class="role-button-container">
|
||||
<gt-loading-button
|
||||
[disabled]="member.role === formRole.value"
|
||||
[loading]="updateMemberLoading() === true"
|
||||
buttonText="Update Member"
|
||||
></gt-loading-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="role-description">
|
||||
@if (formRole.value === "member") {
|
||||
<span i18n
|
||||
>Members can view and act on events, as well as view most other
|
||||
data within the organization.</span
|
||||
>
|
||||
}
|
||||
@if (formRole.value === "admin") {
|
||||
<span i18n
|
||||
>Admin privileges on any teams of which user is a member. They can
|
||||
create new teams and projects, as well as delete teams and
|
||||
projects they are already members of. Additionally, they can
|
||||
manage memberships of teams that they are members of.</span
|
||||
>
|
||||
}
|
||||
@if (formRole.value === "manager") {
|
||||
<span i18n
|
||||
>Gains admin access on all teams as well as the ability to add and
|
||||
remove members.</span
|
||||
>
|
||||
}
|
||||
@if (formRole.value === "owner") {
|
||||
<span i18n
|
||||
>Unrestricted access to the organization, its data, and its
|
||||
settings. Can add, modify, and delete projects and members.</span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
<div class="role-scopes">
|
||||
@if (selectedRoleScopes(); as scopes) {
|
||||
<div class="mat-body-strong" i18n>Scopes:</div>
|
||||
<div>{{ scopes }}</div>
|
||||
}
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title i18n>Organization Account Ownership</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content>
|
||||
@if (!member.isOwner) {
|
||||
@if (transferOrgOwnershipError(); as error) {
|
||||
<mat-error>{{ error }}</mat-error>
|
||||
}
|
||||
<p i18n>
|
||||
The account owner has access to the organization's GlitchTip
|
||||
subscription settings. If you are the current account owner or have
|
||||
an owner role, you may transfer ownership to this member.
|
||||
</p>
|
||||
<gt-loading-button
|
||||
buttonText="Transfer Ownership"
|
||||
[loading]="transferOrgOwnershipLoading() === true"
|
||||
(buttonClick)="transferOrgOwnership()"
|
||||
>
|
||||
</mat-list-item>
|
||||
Change Password
|
||||
</gt-loading-button>
|
||||
} @else {
|
||||
<p i18n>
|
||||
This member is the organization account owner and has access to the
|
||||
organization's GlitchTip subscription settings.
|
||||
</p>
|
||||
}
|
||||
</mat-list>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</section>
|
||||
<ng-template #isOwner>
|
||||
<p i18n>
|
||||
This member is the organization account owner and has access to the
|
||||
organization's GlitchTip subscription settings.
|
||||
</p>
|
||||
</ng-template>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@if (memberTeams(); as memberTeams) {
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title i18n>Team(s)</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content>
|
||||
@if (memberTeams.length === 0) {
|
||||
<p class="center-text" i18n>This user isn't on any teams.</p>
|
||||
} @else {
|
||||
<mat-list>
|
||||
@for (team of memberTeams; track team) {
|
||||
<mat-list-item>
|
||||
<a
|
||||
mat-line
|
||||
[routerLink]="['teams', team, 'members']"
|
||||
[relativeTo]="this.route.parent?.parent"
|
||||
>{{ team }}</a
|
||||
>
|
||||
</mat-list-item>
|
||||
}
|
||||
</mat-list>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export class MemberDetailComponent implements OnInit, OnDestroy {
|
||||
this.memberDetailService.transferOrgOwnershipLoading;
|
||||
orgSlug$ = this.route.paramMap.pipe(map((params) => params.get("org-slug")));
|
||||
memberIdParam$ = this.route.paramMap.pipe(
|
||||
map((params) => params.get("member-id"))
|
||||
map((params) => params.get("member-id")),
|
||||
);
|
||||
routeParams$ = combineLatest([this.orgSlug$, this.memberIdParam$]);
|
||||
form = new FormGroup({
|
||||
@@ -98,10 +98,10 @@ export class MemberDetailComponent implements OnInit, OnDestroy {
|
||||
if (organizationSlug && memberIdParam) {
|
||||
this.memberDetailService.retrieveMemberDetail(
|
||||
organizationSlug,
|
||||
+memberIdParam
|
||||
+memberIdParam,
|
||||
);
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
@@ -43,16 +43,16 @@ export class MemberDetailService extends StatefulService<MemberDetailState> {
|
||||
readonly memberTeams = computed(() => this.state().memberTeams);
|
||||
readonly availableRoles = computed(() => this.state().availableRoles);
|
||||
readonly updateMemberRoleError = computed(
|
||||
() => this.state().updateMemberRoleError
|
||||
() => this.state().updateMemberRoleError,
|
||||
);
|
||||
readonly updateMemberRoleLoading = computed(
|
||||
() => this.state().updateMemberRoleLoading
|
||||
() => this.state().updateMemberRoleLoading,
|
||||
);
|
||||
readonly transferOrgOwnershipError = computed(
|
||||
() => this.state().transferOrgOwnershipError
|
||||
() => this.state().transferOrgOwnershipError,
|
||||
);
|
||||
readonly transferOrgOwnershipLoading = computed(
|
||||
() => this.state().transferOrgOwnershipLoading
|
||||
() => this.state().transferOrgOwnershipLoading,
|
||||
);
|
||||
|
||||
constructor() {
|
||||
@@ -77,15 +77,15 @@ export class MemberDetailService extends StatefulService<MemberDetailState> {
|
||||
tap((resp) => {
|
||||
this.setUpdateMemberRole(resp);
|
||||
this.snackBar.open(
|
||||
`Successfully updated ${resp.email}'s role to ${resp.roleName}`
|
||||
`Successfully updated ${resp.email}'s role to ${resp.roleName}`,
|
||||
);
|
||||
}),
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
this.setUpdateMemberRoleError(
|
||||
`${error.statusText}: ${error.status}`
|
||||
`${error.statusText}: ${error.status}`,
|
||||
);
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.toPromise();
|
||||
} else {
|
||||
@@ -105,7 +105,7 @@ export class MemberDetailService extends StatefulService<MemberDetailState> {
|
||||
.pipe(
|
||||
tap((resp) => {
|
||||
this.snackBar.open(
|
||||
`Successfully transferred organization account ownership to ${resp.email}.`
|
||||
`Successfully transferred organization account ownership to ${resp.email}.`,
|
||||
);
|
||||
this.setTransferOrgOwnership(resp);
|
||||
}),
|
||||
@@ -116,11 +116,11 @@ export class MemberDetailService extends StatefulService<MemberDetailState> {
|
||||
this.setTransferOrgOwnershipError(err.error?.message);
|
||||
} else {
|
||||
this.setTransferOrgOwnershipError(
|
||||
"Unable to transfer account ownership."
|
||||
"Unable to transfer account ownership.",
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.toPromise();
|
||||
} else {
|
||||
@@ -133,7 +133,7 @@ export class MemberDetailService extends StatefulService<MemberDetailState> {
|
||||
return lastValueFrom(
|
||||
this.membersAPIService
|
||||
.retrieve(orgSlug, memberId)
|
||||
.pipe(tap((memberDetail) => this.setMemberDetails(memberDetail)))
|
||||
.pipe(tap((memberDetail) => this.setMemberDetails(memberDetail))),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,49 +6,66 @@
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
{{ (activeOrganizationDetail())?.name }}
|
||||
{{ activeOrganizationDetail()?.name }}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content>
|
||||
<div class="list">
|
||||
@for (member of members$ | async; track member) {
|
||||
<div class="list--item">
|
||||
<div class="email-and-badge">
|
||||
<a class="email" mat-line [routerLink]="[member.id]">{{ member.email }}
|
||||
</a>
|
||||
@if (member.isOwner) {
|
||||
<mat-chip class="mat-caption" color="primary" highlighted="true">
|
||||
Account Owner
|
||||
</mat-chip>
|
||||
}
|
||||
@if (member.pending) {
|
||||
<mat-chip class="mat-caption" color="primary">
|
||||
Invite Pending
|
||||
</mat-chip>
|
||||
}
|
||||
</div>
|
||||
<div class="reinvite-and-role">
|
||||
@if (member.pending) {
|
||||
<div class="reinvite">
|
||||
@if (!member.loadingResendInvite) {
|
||||
<gt-loading-button class="resend-invite" [loading]="member.loadingResendInvite" [buttonText]="
|
||||
<div class="list--item">
|
||||
<div class="email-and-badge">
|
||||
<a class="email" mat-line [routerLink]="[member.id]"
|
||||
>{{ member.email }}
|
||||
</a>
|
||||
@if (member.isOwner) {
|
||||
<mat-chip
|
||||
class="mat-caption"
|
||||
color="primary"
|
||||
highlighted="true"
|
||||
>
|
||||
Account Owner
|
||||
</mat-chip>
|
||||
}
|
||||
@if (member.pending) {
|
||||
<mat-chip class="mat-caption" color="primary">
|
||||
Invite Pending
|
||||
</mat-chip>
|
||||
}
|
||||
</div>
|
||||
<div class="reinvite-and-role">
|
||||
@if (member.pending) {
|
||||
<div class="reinvite">
|
||||
@if (!member.loadingResendInvite) {
|
||||
<gt-loading-button
|
||||
class="resend-invite"
|
||||
[loading]="member.loadingResendInvite"
|
||||
[buttonText]="
|
||||
member.sentResendInvite
|
||||
? 'Invite Resent'
|
||||
: 'Resend Invite'
|
||||
" [disabled]="member.sentResendInvite" (buttonClick)="resendInvite(member)">
|
||||
</gt-loading-button>
|
||||
"
|
||||
[disabled]="member.sentResendInvite"
|
||||
(buttonClick)="resendInvite(member)"
|
||||
>
|
||||
</gt-loading-button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="role">{{ member.roleName }}</div>
|
||||
</div>
|
||||
}
|
||||
<div class="role">{{ member.roleName }}</div>
|
||||
<gt-loading-button
|
||||
class="remove-button"
|
||||
matTooltip="Account ownership must be transferred."
|
||||
[matTooltipDisabled]="!member.isOwner"
|
||||
[disabled]="member.isOwner"
|
||||
(buttonClick)="removeMember(member)"
|
||||
[buttonText]="member.isMe ? 'Leave' : 'Remove'"
|
||||
buttonStyle="stroked"
|
||||
></gt-loading-button>
|
||||
</div>
|
||||
<gt-loading-button class="remove-button" matTooltip="Account ownership must be transferred."
|
||||
[matTooltipDisabled]="!member.isOwner" [disabled]="member.isOwner" (buttonClick)="removeMember(member)"
|
||||
[buttonText]="member.isMe ? 'Leave' : 'Remove'" buttonStyle="stroked"></gt-loading-button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</section>
|
||||
</section>
|
||||
@@ -49,7 +49,7 @@ export class MembersComponent implements OnInit {
|
||||
this.route.params
|
||||
.pipe(
|
||||
map((params) => params["org-slug"] as string),
|
||||
filter((slug) => !!slug)
|
||||
filter((slug) => !!slug),
|
||||
)
|
||||
.subscribe((slug) => {
|
||||
this.organizationDetailService.retrieveOrganizationMembers(slug);
|
||||
|
||||
@@ -6,32 +6,35 @@
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content>
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||
@if ((errors()).addOrganizationMember; as error) {
|
||||
<mat-error class="server-error">{{ error }}</mat-error>
|
||||
@if (errors().addOrganizationMember; as error) {
|
||||
<mat-error class="server-error">{{ error }}</mat-error>
|
||||
}
|
||||
<div>
|
||||
<h2>Email(s)</h2>
|
||||
<p class="email-paragraph">
|
||||
Invite a member to join this organization via their email address.
|
||||
@if (enableUserRegistration()) {
|
||||
<span
|
||||
>If they do not already have an account, they will first be asked
|
||||
to create one.</span
|
||||
>
|
||||
} @else { Only existing GlitchTip users may be invited. } Multiple
|
||||
emails delimited by commas.
|
||||
<span
|
||||
>If they do not already have an account, they will first be
|
||||
asked to create one.</span
|
||||
>
|
||||
} @else {
|
||||
Only existing GlitchTip users may be invited.
|
||||
}
|
||||
Multiple emails delimited by commas.
|
||||
</p>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput type="email" formControlName="email" />
|
||||
@if (form.get("email")?.errors) {
|
||||
<mat-error>
|
||||
@if (form.get("email")?.errors?.invalidEmails) {
|
||||
<span> Enter a valid email</span>
|
||||
} @if (form.get("email")?.errors?.required) {
|
||||
<span>Enter at least one email</span>
|
||||
}
|
||||
</mat-error>
|
||||
<mat-error>
|
||||
@if (form.get("email")?.errors?.invalidEmails) {
|
||||
<span> Enter a valid email</span>
|
||||
}
|
||||
@if (form.get("email")?.errors?.required) {
|
||||
<span>Enter at least one email</span>
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@@ -45,27 +48,31 @@
|
||||
</mat-radio-group>
|
||||
<div class="role-description">
|
||||
@if (formRole.value === "member") {
|
||||
<span i18n
|
||||
>Members can view and act on events, as well as view most other
|
||||
data within the organization.</span
|
||||
>
|
||||
} @if (formRole.value === "admin") {
|
||||
<span i18n
|
||||
>Admin privileges on any teams of which user is a member. They can
|
||||
create new teams and projects, as well as delete teams and
|
||||
projects they are already members of. Additionally, they can
|
||||
manage memberships of teams that they are members of.</span
|
||||
>
|
||||
} @if (formRole.value === "manager") {
|
||||
<span i18n
|
||||
>Gains admin access on all teams as well as the ability to add and
|
||||
remove members.</span
|
||||
>
|
||||
} @if (formRole.value === "owner") {
|
||||
<span i18n
|
||||
>Unrestricted access to the organization, its data, and its
|
||||
settings. Can add, modify, and delete projects and members.</span
|
||||
>
|
||||
<span i18n
|
||||
>Members can view and act on events, as well as view most other
|
||||
data within the organization.</span
|
||||
>
|
||||
}
|
||||
@if (formRole.value === "admin") {
|
||||
<span i18n
|
||||
>Admin privileges on any teams of which user is a member. They
|
||||
can create new teams and projects, as well as delete teams and
|
||||
projects they are already members of. Additionally, they can
|
||||
manage memberships of teams that they are members of.</span
|
||||
>
|
||||
}
|
||||
@if (formRole.value === "manager") {
|
||||
<span i18n
|
||||
>Gains admin access on all teams as well as the ability to add
|
||||
and remove members.</span
|
||||
>
|
||||
}
|
||||
@if (formRole.value === "owner") {
|
||||
<span i18n
|
||||
>Unrestricted access to the organization, its data, and its
|
||||
settings. Can add, modify, and delete projects and
|
||||
members.</span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,13 +81,14 @@
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Add Team</mat-label>
|
||||
<mat-select multiple formControlName="teams" disableOptionCentering>
|
||||
@if ((organizationTeams()).length) { @for (team of
|
||||
organizationTeams(); track team.id) {
|
||||
<mat-option [value]="team.slug">
|
||||
{{ team.slug }}
|
||||
</mat-option>
|
||||
} } @else {
|
||||
<mat-option disabled>No Teams</mat-option>
|
||||
@if (organizationTeams().length) {
|
||||
@for (team of organizationTeams(); track team.id) {
|
||||
<mat-option [value]="team.slug">
|
||||
{{ team.slug }}
|
||||
</mat-option>
|
||||
}
|
||||
} @else {
|
||||
<mat-option disabled>No Teams</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
@@ -28,7 +28,7 @@ function validateEmails(emails: string) {
|
||||
.map((email) =>
|
||||
Validators.email({
|
||||
value: email.replace(/\s/g, ""),
|
||||
} as AbstractControl)
|
||||
} as AbstractControl),
|
||||
)
|
||||
.find((email) => email !== null) === undefined
|
||||
);
|
||||
@@ -100,7 +100,7 @@ export class NewMemberComponent implements OnInit, OnDestroy {
|
||||
this.organizationsService.inviteOrganizationMembers(
|
||||
email.replace(/\s/g, ""),
|
||||
teams!,
|
||||
role! as any
|
||||
role! as any,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,45 +3,55 @@
|
||||
<h1 class="mat-headline-4">General Settings</h1>
|
||||
</header>
|
||||
@if (activeOrganizationDetail(); as activeOrg) {
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>General</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content>
|
||||
<form [formGroup]="form" (ngSubmit)="updateOrganization()">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" [value]="activeOrg.name" />
|
||||
@if (updateError) {
|
||||
<mat-error>{{ updateError }}</mat-error>
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>General</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content>
|
||||
<form [formGroup]="form" (ngSubmit)="updateOrganization()">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" [value]="activeOrg.name" />
|
||||
@if (updateError) {
|
||||
<mat-error>{{ updateError }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<gt-loading-button
|
||||
[disabled]="name?.value === activeOrg.name"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
buttonText="Update Name"
|
||||
id="update-org"
|
||||
>
|
||||
</gt-loading-button>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Delete Organization</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content class="remove-content">
|
||||
<div>
|
||||
<p>
|
||||
By deleting this organization, you will lose all data including
|
||||
projects and their associated events.
|
||||
</p>
|
||||
@if (deleteError) {
|
||||
<mat-error>{{ deleteError }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<gt-loading-button [disabled]="name?.value === activeOrg.name" mat-flat-button color="primary"
|
||||
buttonText="Update Name" id="update-org">
|
||||
</div>
|
||||
<gt-loading-button
|
||||
data-test
|
||||
buttonText="Delete Organization"
|
||||
[loading]="deleteLoading"
|
||||
(click)="removeOrganization(activeOrg.slug, activeOrg.name)"
|
||||
id="delete-org"
|
||||
>
|
||||
</gt-loading-button>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Delete Organization</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-content class="remove-content">
|
||||
<div>
|
||||
<p>
|
||||
By deleting this organization, you will lose all data including
|
||||
projects and their associated events.
|
||||
</p>
|
||||
@if (deleteError) {
|
||||
<mat-error>{{ deleteError }}</mat-error>
|
||||
}
|
||||
</div>
|
||||
<gt-loading-button data-test buttonText="Delete Organization" [loading]="deleteLoading"
|
||||
(click)="removeOrganization(activeOrg.slug, activeOrg.name)" id="delete-org">
|
||||
</gt-loading-button>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</section>
|
||||
</section>
|
||||
@@ -51,11 +51,11 @@ export class OrganizationComponent implements OnInit {
|
||||
// this.organizationsService.retrieveOrganizations().toPromise();
|
||||
this.organizationsService.refreshActiveOrganization();
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.toPromise();
|
||||
this.activeOrganizationDetail$.subscribe((data) =>
|
||||
data ? this.form.patchValue({ name: data.name }) : undefined
|
||||
data ? this.form.patchValue({ name: data.name }) : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,20 +71,20 @@ export class OrganizationComponent implements OnInit {
|
||||
(org: Organization) => {
|
||||
this.updateLoading = false;
|
||||
this.snackBar.open(
|
||||
`The name of your organization has been updated to ${org.name}`
|
||||
`The name of your organization has been updated to ${org.name}`,
|
||||
);
|
||||
},
|
||||
(err) => {
|
||||
this.updateLoading = false;
|
||||
this.updateError = `${err.statusText}: ${err.status}`;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
removeOrganization(slug: string, name: string) {
|
||||
if (
|
||||
window.confirm(
|
||||
`Are you sure you want to remove ${name}? You will permanently lose all projects and teams associated with it.`
|
||||
`Are you sure you want to remove ${name}? You will permanently lose all projects and teams associated with it.`,
|
||||
)
|
||||
) {
|
||||
this.deleteLoading = true;
|
||||
@@ -92,7 +92,7 @@ export class OrganizationComponent implements OnInit {
|
||||
if (result.response.status === 204) {
|
||||
this.deleteLoading = false;
|
||||
this.snackBar.open(
|
||||
`You have successfully deleted ${name} from your organizations`
|
||||
`You have successfully deleted ${name} from your organizations`,
|
||||
);
|
||||
} else {
|
||||
this.deleteLoading = false;
|
||||
|
||||
@@ -81,7 +81,6 @@
|
||||
buttonText="Create Project"
|
||||
>
|
||||
</gt-loading-button>
|
||||
</form>
|
||||
</mat-card-content
|
||||
</form> </mat-card-content
|
||||
></mat-card>
|
||||
</section>
|
||||
@@ -88,7 +88,7 @@ export class NewProjectComponent implements OnInit {
|
||||
.pipe(
|
||||
map((params) => params["org-slug"] as string),
|
||||
filter((slug) => !!slug),
|
||||
tap((slug) => (this.orgSlug = slug))
|
||||
tap((slug) => (this.orgSlug = slug)),
|
||||
)
|
||||
.subscribe((slug) => {
|
||||
this.teamsService.retrieveTeamsByOrg(slug).toPromise();
|
||||
@@ -114,7 +114,7 @@ export class NewProjectComponent implements OnInit {
|
||||
platform: this.form.value.platform ?? "",
|
||||
},
|
||||
this.form.value.team!,
|
||||
this.orgSlug
|
||||
this.orgSlug,
|
||||
)
|
||||
.pipe(
|
||||
tap((project) => {
|
||||
@@ -125,14 +125,14 @@ export class NewProjectComponent implements OnInit {
|
||||
[this.orgService.activeOrganizationSlug(), "issues"],
|
||||
{
|
||||
queryParams: { project: project.id },
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.loading = false;
|
||||
this.error = `${err.statusText}: ${err.status}`;
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
HostListener,
|
||||
input
|
||||
input,
|
||||
} from "@angular/core";
|
||||
import {
|
||||
MatExpansionPanel,
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ export class AlertFormComponent implements OnInit {
|
||||
timespanMinutes: number;
|
||||
quantity: number;
|
||||
uptime: boolean;
|
||||
}>();
|
||||
}>();
|
||||
readonly newAlert = input<boolean | undefined>(false);
|
||||
|
||||
timesI18nMapping = {
|
||||
|
||||
+6
-1
@@ -1,4 +1,9 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, inject } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ChangeDetectionStrategy,
|
||||
inject,
|
||||
} from "@angular/core";
|
||||
import { MatDialogRef, MatDialogModule } from "@angular/material/dialog";
|
||||
import {
|
||||
FormGroup,
|
||||
|
||||
@@ -90,7 +90,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
|
||||
readonly initialLoad$ = this.getState$.pipe(map((data) => data.initialLoad));
|
||||
readonly initialLoadError$ = this.getState$.pipe(
|
||||
map((data) => data.initialLoadError)
|
||||
map((data) => data.initialLoadError),
|
||||
);
|
||||
readonly projectAlerts$ = this.getState$.pipe(
|
||||
map((data) => data.projectAlerts),
|
||||
@@ -101,33 +101,33 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
...alert,
|
||||
errorAlert: !alert.timespanMinutes && !alert.quantity ? false : true,
|
||||
};
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
/** New Alert */
|
||||
readonly newAlertOpen$ = this.getState$.pipe(
|
||||
map((data) => data.newAlertState.newAlertOpen)
|
||||
map((data) => data.newAlertState.newAlertOpen),
|
||||
);
|
||||
readonly newProjectAlertRecipients$ = this.getState$.pipe(
|
||||
map((data) => data.newAlertState.newProjectAlertRecipients)
|
||||
map((data) => data.newAlertState.newProjectAlertRecipients),
|
||||
);
|
||||
readonly newAlertLoading$ = this.getState$.pipe(
|
||||
map((data) => data.newAlertState.newAlertLoading)
|
||||
map((data) => data.newAlertState.newAlertLoading),
|
||||
);
|
||||
readonly newAlertError$ = this.getState$.pipe(
|
||||
map((data) => data.newAlertState.newAlertError)
|
||||
map((data) => data.newAlertState.newAlertError),
|
||||
);
|
||||
|
||||
/** Recipient Dialog */
|
||||
readonly recipientError$ = this.getState$.pipe(
|
||||
map((data) => data.recipientDialogState.recipientError)
|
||||
map((data) => data.recipientDialogState.recipientError),
|
||||
);
|
||||
readonly recipientDialogOpen$ = this.getState$.pipe(
|
||||
map((data) => data.recipientDialogState.recipientDialogOpen)
|
||||
map((data) => data.recipientDialogState.recipientDialogOpen),
|
||||
);
|
||||
readonly activeAlert$ = this.getState$.pipe(
|
||||
map((data) => data.recipientDialogState.activeAlert)
|
||||
map((data) => data.recipientDialogState.activeAlert),
|
||||
);
|
||||
readonly emailSelected$ = combineLatest([
|
||||
this.newProjectAlertRecipients$,
|
||||
@@ -136,33 +136,33 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
map(([newRecipients, activeAlert]) => {
|
||||
if (activeAlert?.id) {
|
||||
return activeAlert.alertRecipients.some(
|
||||
(data) => data.recipientType === "email"
|
||||
(data) => data.recipientType === "email",
|
||||
);
|
||||
} else if (newRecipients !== null) {
|
||||
return newRecipients.some((data) => data.recipientType === "email");
|
||||
}
|
||||
return;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
/** Current Alerts */
|
||||
readonly removeAlertLoading$ = this.getState$.pipe(
|
||||
map((data) => data.removeAlertLoading)
|
||||
map((data) => data.removeAlertLoading),
|
||||
);
|
||||
readonly removeAlertError$ = this.getState$.pipe(
|
||||
map((data) => data.removeAlertError)
|
||||
map((data) => data.removeAlertError),
|
||||
);
|
||||
readonly updatePropertiesLoading$ = this.getState$.pipe(
|
||||
map((data) => data.updatePropertiesLoading)
|
||||
map((data) => data.updatePropertiesLoading),
|
||||
);
|
||||
readonly updatePropertiesError$ = this.getState$.pipe(
|
||||
map((data) => data.updatePropertiesError)
|
||||
map((data) => data.updatePropertiesError),
|
||||
);
|
||||
readonly deleteRecipientLoading$ = this.getState$.pipe(
|
||||
map((data) => data.deleteRecipientLoading)
|
||||
map((data) => data.deleteRecipientLoading),
|
||||
);
|
||||
readonly deleteRecipientError$ = this.getState$.pipe(
|
||||
map((data) => data.deleteRecipientError)
|
||||
map((data) => data.deleteRecipientError),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
@@ -186,11 +186,11 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
this.setProjectAlertsListError(err);
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
@@ -219,7 +219,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
this.updateAlertRecipient(event);
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
@@ -255,7 +255,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
tap((resp) => {
|
||||
this.setCreateAlert(resp);
|
||||
this.snackBar.open(`Success! Your new alert has been added.`);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
@@ -263,7 +263,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
this.setCreateAlertError(err);
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
@@ -285,7 +285,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
tap((_) => {
|
||||
this.setDeleteProjectAlert(id);
|
||||
this.snackBar.open(`Success: Your alert has been deleted`);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
@@ -293,7 +293,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
this.setDeleteAlertError(err, id);
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
@@ -303,7 +303,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
newQuantity: number,
|
||||
uptime: boolean,
|
||||
id: number,
|
||||
recipients: AlertRecipient[]
|
||||
recipients: AlertRecipient[],
|
||||
) {
|
||||
this.setUpdatePropertiesLoading(id);
|
||||
const data: PartialProjectAlert = {
|
||||
@@ -327,7 +327,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
tap((resp) => {
|
||||
this.setUpdateAlertProperties(resp);
|
||||
this.snackBar.open(`Success: Your alert has been updated`);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
@@ -335,7 +335,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
this.setUpdatePropertiesError(err, id);
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
@@ -373,7 +373,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
tap((resp) => {
|
||||
this.setUpdateAlertRecipients(resp.alertRecipients, resp.id);
|
||||
this.snackBar.open(`Success: Your alert has been updated`);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
@@ -381,7 +381,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
this.setUpdateAlertRecipientsError(err, activeErrorId);
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
@@ -391,7 +391,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
const data = {
|
||||
...alert,
|
||||
alertRecipients: alert.alertRecipients.filter(
|
||||
(currentRecipient) => currentRecipient.id !== recipientToRemove.id
|
||||
(currentRecipient) => currentRecipient.id !== recipientToRemove.id,
|
||||
),
|
||||
};
|
||||
combineLatest([
|
||||
@@ -408,9 +408,9 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
tap((resp) => {
|
||||
this.setUpdateAlertRecipients(resp.alertRecipients, resp.id);
|
||||
this.snackBar.open(
|
||||
`Success: Your recipient has been deleted`
|
||||
`Success: Your recipient has been deleted`,
|
||||
);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
return EMPTY;
|
||||
@@ -418,7 +418,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
this.setDeleteRecipientError(err);
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
@@ -500,7 +500,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
...newAlertState,
|
||||
newProjectAlertRecipients:
|
||||
newAlertState.newProjectAlertRecipients?.filter(
|
||||
(recipient) => recipient.url !== url
|
||||
(recipient) => recipient.url !== url,
|
||||
) ?? null,
|
||||
},
|
||||
});
|
||||
@@ -608,7 +608,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
this.setState({
|
||||
projectAlerts: this.findAndReplaceAlert(
|
||||
state.projectAlerts,
|
||||
updatedAlert
|
||||
updatedAlert,
|
||||
),
|
||||
updatePropertiesLoading: null,
|
||||
updatePropertiesError: null,
|
||||
@@ -649,7 +649,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
const recipientDialogState = this.state.getValue().recipientDialogState;
|
||||
this.setState({
|
||||
projectAlerts: state.projectAlerts?.map((alert) =>
|
||||
alert.id === id ? { ...alert, alertRecipients: recipients } : alert
|
||||
alert.id === id ? { ...alert, alertRecipients: recipients } : alert,
|
||||
),
|
||||
recipientDialogState: {
|
||||
...recipientDialogState,
|
||||
@@ -688,7 +688,7 @@ export class ProjectAlertsService extends StatefulService<ProjectAlertState> {
|
||||
|
||||
findAndReplaceAlert(
|
||||
currentAlerts: ProjectAlert[] | null,
|
||||
newAlert: ProjectAlert
|
||||
newAlert: ProjectAlert,
|
||||
): ProjectAlert[] | null {
|
||||
const updatedAlert = currentAlerts?.map((alert) => {
|
||||
if (alert.id === newAlert.id) {
|
||||
|
||||
Loaded 100 of 142 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user