From b77809cf9a4c512edad7832bc482e35988c25a99 Mon Sep 17 00:00:00 2001 From: Stefan Hengl Date: Fri, 12 Jul 2024 10:01:47 +0200 Subject: [PATCH 1/9] chore(search): update search API call sites to set the version explicitly (#63782) I went through all call sites of the 3 search APIs (Stream API, GQL API, SearchClient (internal)) and made sure that the query syntax version is set to "V3". Why? Without this change, a new default search syntax version might have caused a change in behavior for some of the call sites. ## Test plan - No functional change, so relying mostly on CI - The codeintel GQL queries set the patternType explicitly, so this change is a NOP. I tested manually - search based code intel sends GQL requests with version "V3" - repo badge still works - compute GQL returns results --- .../codeintel/legacy-extensions/util/api.ts | 10 ++++--- .../src/codeintel/ReferencesPanelQueries.ts | 4 +-- .../codeintel/useSearchBasedCodeIntel.ts | 2 ++ .../use-live-preview-lang-stats-insight.ts | 6 +++-- .../LangStatsInsightCreationPage.story.tsx | 3 ++- client/web/src/search/backend.tsx | 27 ------------------- .../results/StreamingSearchResults.test.tsx | 3 ++- .../SearchResultsInfoBar.test.tsx | 3 ++- cmd/frontend/backend/go_importers.go | 7 ++--- .../internal/compute/resolvers/resolvers.go | 2 +- .../internal/compute/streaming/compute.go | 2 +- internal/cmd/search-blitz/client.go | 2 +- internal/cmd/search-blitz/search.graphql | 4 +-- .../insights/query/streaming/search_client.go | 2 +- internal/search/streaming/http/client.go | 10 +++---- 15 files changed, 34 insertions(+), 53 deletions(-) diff --git a/client/shared/src/codeintel/legacy-extensions/util/api.ts b/client/shared/src/codeintel/legacy-extensions/util/api.ts index 32ff770e51f..7d9ef589918 100644 --- a/client/shared/src/codeintel/legacy-extensions/util/api.ts +++ b/client/shared/src/codeintel/legacy-extensions/util/api.ts @@ -2,6 +2,7 @@ import gql from 'tagged-template-noop' import { isErrorLike } from '@sourcegraph/common' +import { SearchVersion } from '../../../graphql-operations' import type * as sourcegraph from '../api' import { cache } from '../util' @@ -251,6 +252,7 @@ export class API { const data = await queryGraphQL(buildSearchQuery(fileLocal), { query, + version: SearchVersion.V3, }) return data.search.results.results.filter(isDefined) } @@ -322,8 +324,8 @@ function buildSearchQuery(fileLocal: boolean): string { if (fileLocal) { return gql` - query LegacyCodeIntelSearch2($query: String!) { - search(query: $query) { + query LegacyCodeIntelSearch2($query: String!, $version: SearchVersion!) { + search(query: $query, version: $version) { ...SearchResults ...FileLocal } @@ -334,8 +336,8 @@ function buildSearchQuery(fileLocal: boolean): string { } return gql` - query LegacyCodeIntelSearch3($query: String!) { - search(query: $query) { + query LegacyCodeIntelSearch3($query: String!, $version: SearchVersion!) { + search(query: $query, version: $version) { ...SearchResults } } diff --git a/client/web/src/codeintel/ReferencesPanelQueries.ts b/client/web/src/codeintel/ReferencesPanelQueries.ts index b279e89a98b..dc488fcf77b 100644 --- a/client/web/src/codeintel/ReferencesPanelQueries.ts +++ b/client/web/src/codeintel/ReferencesPanelQueries.ts @@ -241,8 +241,8 @@ export const FETCH_HIGHLIGHTED_BLOB = gql` ` export const CODE_INTEL_SEARCH_QUERY = gql` - query CodeIntelSearch2($query: String!) { - search(query: $query) { + query CodeIntelSearch2($query: String!, $version: SearchVersion!) { + search(query: $query, version: $version) { __typename results { __typename diff --git a/client/web/src/enterprise/codeintel/useSearchBasedCodeIntel.ts b/client/web/src/enterprise/codeintel/useSearchBasedCodeIntel.ts index 0d21a9a5625..093adc8d63e 100644 --- a/client/web/src/enterprise/codeintel/useSearchBasedCodeIntel.ts +++ b/client/web/src/enterprise/codeintel/useSearchBasedCodeIntel.ts @@ -16,6 +16,7 @@ import { CODE_INTEL_SEARCH_QUERY, LOCAL_CODE_INTEL_QUERY } from '../../codeintel import type { SettingsGetter } from '../../codeintel/settings' import { isDefined } from '../../codeintel/util/helpers' import type { CodeIntelSearch2Variables } from '../../graphql-operations' +import { SearchVersion } from '../../graphql-operations' import { syntaxHighlight } from '../../repo/blob/codemirror/highlight' import { getBlobEditView } from '../../repo/blob/use-blob-store' @@ -367,6 +368,7 @@ async function executeSearchQuery(terms: string[]): Promise { query: getDocumentNode(CODE_INTEL_SEARCH_QUERY), variables: { query: terms.join(' '), + version: SearchVersion.V3, }, }) diff --git a/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-lang-stats-insight.ts b/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-lang-stats-insight.ts index dc72917deed..645c26e25ae 100644 --- a/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-lang-stats-insight.ts +++ b/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-lang-stats-insight.ts @@ -14,6 +14,7 @@ import { type LangStatsInsightContentResult, type LangStatsInsightContentVariables, SearchPatternType, + SearchVersion, } from '../../../../../graphql-operations' import type { CategoricalChartContent } from '../../backend/code-insights-backend-types' @@ -162,8 +163,8 @@ function quoteIfNeeded(value: string): string { } export const GET_LANG_STATS_GQL = gql` - query LangStatsInsightContent($query: String!) { - search(query: $query) { + query LangStatsInsightContent($query: String!, $version: SearchVersion!) { + search(query: $query, version: $version) { results { limitHit } @@ -180,6 +181,7 @@ export const GET_LANG_STATS_GQL = gql` function fetchLangStatsInsight(query: string): Observable { return requestGraphQL(GET_LANG_STATS_GQL, { query, + version: SearchVersion.V3, }).pipe(map(dataOrThrowErrors)) } diff --git a/client/web/src/enterprise/insights/pages/insights/creation/lang-stats/LangStatsInsightCreationPage.story.tsx b/client/web/src/enterprise/insights/pages/insights/creation/lang-stats/LangStatsInsightCreationPage.story.tsx index e69c3863401..9ba09eebc6f 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/lang-stats/LangStatsInsightCreationPage.story.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/lang-stats/LangStatsInsightCreationPage.story.tsx @@ -10,6 +10,7 @@ import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo' import { WebStory } from '../../../../../../components/WebStory' import type { LangStatsInsightContentResult } from '../../../../../../graphql-operations' +import { SearchVersion } from '../../../../../../graphql-operations' import { GET_LANG_STATS_GQL } from '../../../../core/hooks/live-preview-insight' import { useCodeInsightsLicenseState } from '../../../../stores' @@ -31,7 +32,7 @@ export default defaultStory const LANG_STATS_MOCK: MockedResponse = { request: { query: getDocumentNode(GET_LANG_STATS_GQL), - variables: {}, + variables: { version: SearchVersion.V3 }, }, result: { data: { diff --git a/client/web/src/search/backend.tsx b/client/web/src/search/backend.tsx index b87222b0ecd..2f81d2a9214 100644 --- a/client/web/src/search/backend.tsx +++ b/client/web/src/search/backend.tsx @@ -1,7 +1,6 @@ import { type Observable, of } from 'rxjs' import { map } from 'rxjs/operators' -import { createAggregateError } from '@sourcegraph/common' import { dataOrThrowErrors, gql } from '@sourcegraph/http-client' import { queryGraphQL, requestGraphQL } from '../backend/graphql' @@ -16,35 +15,9 @@ import type { UpdateSavedSearchVariables, Scalars, SavedSearchFields, - ReposByQueryResult, SavedSearchResult, } from '../graphql-operations' -export function fetchReposByQuery(query: string): Observable<{ name: string; url: string }[]> { - return queryGraphQL( - gql` - query ReposByQuery($query: String!) { - search(query: $query) { - results { - repositories { - name - url - } - } - } - } - `, - { query } - ).pipe( - map(({ data, errors }) => { - if (!data?.search?.results?.repositories) { - throw createAggregateError(errors) - } - return data.search.results.repositories - }) - ) -} - const savedSearchFragment = gql` fragment SavedSearchFields on SavedSearch { id diff --git a/client/web/src/search/results/StreamingSearchResults.test.tsx b/client/web/src/search/results/StreamingSearchResults.test.tsx index 16650481045..7f04d5c876e 100644 --- a/client/web/src/search/results/StreamingSearchResults.test.tsx +++ b/client/web/src/search/results/StreamingSearchResults.test.tsx @@ -12,6 +12,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { GitRefType, SearchPatternType } from '@sourcegraph/shared/src/graphql-operations' import { SearchMode, SearchQueryStateStoreProvider } from '@sourcegraph/shared/src/search' import type { AggregateStreamingSearchResults, Skipped } from '@sourcegraph/shared/src/search/stream' +import { LATEST_VERSION } from '@sourcegraph/shared/src/search/stream' import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry' import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService' import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo' @@ -109,7 +110,7 @@ describe('StreamingSearchResults', () => { expect(receivedQuery).toEqual('r:golang/oauth2 test f:travis') expect(receivedOptions).toEqual({ - version: 'V3', + version: LATEST_VERSION, patternType: SearchPatternType.regexp, caseSensitive: true, searchMode: SearchMode.SmartSearch, diff --git a/client/web/src/search/results/components/search-results-info-bar/SearchResultsInfoBar.test.tsx b/client/web/src/search/results/components/search-results-info-bar/SearchResultsInfoBar.test.tsx index f97ed0eae87..5193c44f705 100644 --- a/client/web/src/search/results/components/search-results-info-bar/SearchResultsInfoBar.test.tsx +++ b/client/web/src/search/results/components/search-results-info-bar/SearchResultsInfoBar.test.tsx @@ -1,6 +1,7 @@ import { noop } from 'lodash' import { describe, expect, test } from 'vitest' +import { LATEST_VERSION } from '@sourcegraph/shared/src/search/stream' import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry' import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService' import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo' @@ -32,7 +33,7 @@ const COMMON_PROPS: Omit = { sidebarCollapsed: false, isSourcegraphDotCom: true, options: { - version: 'V3', + version: LATEST_VERSION, patternType: SearchPatternType.standard, caseSensitive: false, trace: undefined, diff --git a/cmd/frontend/backend/go_importers.go b/cmd/frontend/backend/go_importers.go index baf244b1946..009528563d1 100644 --- a/cmd/frontend/backend/go_importers.go +++ b/cmd/frontend/backend/go_importers.go @@ -64,7 +64,8 @@ func CountGoImporters(ctx context.Context, cli httpcli.Doer, repo api.RepoName) q.Query = countGoImportersGraphQLQuery q.Variables = map[string]any{ - "query": countGoImportersSearchQuery(repo), + "query": countGoImportersSearchQuery(repo), + "version": "V3", } body, err := json.Marshal(q) @@ -144,6 +145,6 @@ func countGoImportersSearchQuery(repo api.RepoName) string { } const countGoImportersGraphQLQuery = ` -query CountGoImporters($query: String!) { - search(query: $query) { results { matchCount } } +query CountGoImporters($query: String!, $version: SearchVersion!) { + search(query: $query, version: $version) { results { matchCount } } }` diff --git a/cmd/frontend/internal/compute/resolvers/resolvers.go b/cmd/frontend/internal/compute/resolvers/resolvers.go index 997dec00a11..ec4f1f6ca60 100644 --- a/cmd/frontend/internal/compute/resolvers/resolvers.go +++ b/cmd/frontend/internal/compute/resolvers/resolvers.go @@ -232,7 +232,7 @@ func NewBatchComputeImplementer(ctx context.Context, logger log.Logger, db datab log15.Debug("compute", "search", searchQuery) patternType := "regexp" - job, err := gql.NewBatchSearchImplementer(ctx, logger, db, &gql.SearchArgs{Query: searchQuery, PatternType: &patternType}) + job, err := gql.NewBatchSearchImplementer(ctx, logger, db, &gql.SearchArgs{Query: searchQuery, PatternType: &patternType, Version: "V3"}) if err != nil { return nil, err } diff --git a/cmd/frontend/internal/compute/streaming/compute.go b/cmd/frontend/internal/compute/streaming/compute.go index 854a68f489f..3b07dc247ad 100644 --- a/cmd/frontend/internal/compute/streaming/compute.go +++ b/cmd/frontend/internal/compute/streaming/compute.go @@ -73,7 +73,7 @@ func NewComputeStream(ctx context.Context, logger log.Logger, db database.DB, se searchClient := client.New(logger, db, gitserver.NewClient("http.compute.search")) inputs, err := searchClient.Plan( ctx, - "", + "V3", &patternType, searchQuery, search.Precise, diff --git a/internal/cmd/search-blitz/client.go b/internal/cmd/search-blitz/client.go index 5364d5fbe61..4bb33365f65 100644 --- a/internal/cmd/search-blitz/client.go +++ b/internal/cmd/search-blitz/client.go @@ -45,7 +45,7 @@ func (s *client) search(ctx context.Context, query, queryName string) (*metrics, return s.doGraphQL(ctx, graphQLRequest{ QueryName: queryName, GraphQLQuery: graphQLSearchQuery, - GraphQLVariables: map[string]string{"query": query}, + GraphQLVariables: map[string]string{"query": query, "version": "V3"}, MetricsFromBody: func(body io.Reader) (*metrics, error) { var respDec struct { Data struct { diff --git a/internal/cmd/search-blitz/search.graphql b/internal/cmd/search-blitz/search.graphql index 5cbdde1ad0a..32c76a6be43 100644 --- a/internal/cmd/search-blitz/search.graphql +++ b/internal/cmd/search-blitz/search.graphql @@ -86,8 +86,8 @@ fragment SearchResultsAlertFields on SearchResults { } } -query ($query: String!) { - search(query: $query) { +query ($query: String!, $version: SearchVersion!) { + search(query: $query, version: $version) { results { results { __typename diff --git a/internal/insights/query/streaming/search_client.go b/internal/insights/query/streaming/search_client.go index 1bcedd82248..e5d2303969f 100644 --- a/internal/insights/query/streaming/search_client.go +++ b/internal/insights/query/streaming/search_client.go @@ -33,7 +33,7 @@ type insightsSearchClient struct { func (r *insightsSearchClient) Search(ctx context.Context, query string, patternType *string, sender streaming.Sender) (*search.Alert, error) { inputs, err := r.searchClient.Plan( ctx, - "", + "V3", patternType, query, search.Precise, diff --git a/internal/search/streaming/http/client.go b/internal/search/streaming/http/client.go index 7853795ab66..09ad4d8494f 100644 --- a/internal/search/streaming/http/client.go +++ b/internal/search/streaming/http/client.go @@ -15,13 +15,11 @@ import ( const maxPayloadSize = 10 * 1024 * 1024 // 10mb -// TODO(stefan): Remove NewRequest in favor of NewRequestWithVersion. - -// NewRequest returns an http.Request against the streaming API for query. +// NewRequest returns an http.Request against the Stream API. Use +// NewRequestWithVersion if you want to specify the version of the search syntax +// or the default patternType. func NewRequest(baseURL string, query string) (*http.Request, error) { - // We don't set version or pattern type and rely on the defaults of the route - // handler. - return NewRequestWithVersion(baseURL, query, "", nil) + return NewRequestWithVersion(baseURL, query, "V3", nil) } // NewRequestWithVersion returns an http.Request against the streaming API for From ec7f9ab90b8d496eb159182dc7ea242847095f25 Mon Sep 17 00:00:00 2001 From: Varun Gandhi Date: Fri, 12 Jul 2024 17:07:54 +0800 Subject: [PATCH 2/9] chore: Bump autoindexing image SHAs (#63801) --- .../autoindexing/internal/inference/libs/indexes.go | 6 +++--- .../inference/testdata/JVM_project_with_Gradle.yaml | 2 +- .../inference/testdata/JVM_project_with_Maven.yaml | 2 +- .../inference/testdata/JVM_project_with_SBT.yaml | 2 +- .../testdata/JVM_project_with_lsif-java.json.yaml | 2 +- ...ested_JVM_project_WITHOUT_top-level_build_file.yaml | 4 ++-- .../Nested_JVM_project_with_top-level_build_file.yaml | 2 +- .../internal/inference/testdata/go_files_in_root.yaml | 2 +- .../internal/inference/testdata/go_modules.yaml | 8 ++++---- .../internal/inference/testdata/scip-ruby.yaml | 10 +++++----- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/internal/codeintel/autoindexing/internal/inference/libs/indexes.go b/internal/codeintel/autoindexing/internal/inference/libs/indexes.go index a66a2fbd555..a433f41f460 100644 --- a/internal/codeintel/autoindexing/internal/inference/libs/indexes.go +++ b/internal/codeintel/autoindexing/internal/inference/libs/indexes.go @@ -27,12 +27,12 @@ var defaultIndexers = map[string]string{ // To update, run `DOCKER_USER=... DOCKER_PASS=... ./update-shas.sh` var defaultIndexerSHAs = map[string]string{ - "sourcegraph/scip-go": "sha256:56414010d8917d6952c051dd5fcc0901fdf5c12031d352cc0b26778f040dddcc", + "sourcegraph/scip-go": "sha256:bf89e619382fed5efa21a51806d7c6f22d19b6dea458a58554c0af89120f4ca3", "sourcegraph/scip-rust": "sha256:adf0047fc3050ba4f7be71302b42c74b49901f38fb40916d94ac5fc9181ac078", - "sourcegraph/scip-java": "sha256:a2b3828145cd38758a43363f06d786f9e620c97979a9291463c6544f7f17c68f", + "sourcegraph/scip-java": "sha256:02b5dd2a14b3d0cd776b561ba68660676b4172677fd1fff59cb5d9a0b5351cea", "sourcegraph/scip-python": "sha256:e3c13f0cadca78098439c541d19a72c21672a3263e22aa706760d941581e068d", "sourcegraph/scip-typescript": "sha256:3df8b36a2ad4e073415bfbeaedf38b3cfff3e697614c8f578299f470d140c2c8", - "sourcegraph/scip-ruby": "sha256:ef53e5f1450330ddb4a3edce963b7e10d900d44ff1e7de4960680289ac25f319", + "sourcegraph/scip-ruby": "sha256:75a54c6032c977ae4960c5651f6095bb017bf55b7a4b86e608c6235443c38cbb", "sourcegraph/scip-dotnet": "sha256:1d8a590edfb3834020fceedacac6608811dd31fcba9092426140093876d8d52e", } diff --git a/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_Gradle.yaml b/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_Gradle.yaml index 0e49a617663..843b54a6f8d 100644 --- a/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_Gradle.yaml +++ b/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_Gradle.yaml @@ -1,7 +1,7 @@ - steps: [] local_steps: [] root: "" - indexer: sourcegraph/scip-java@sha256:a2b3828145cd38758a43363f06d786f9e620c97979a9291463c6544f7f17c68f + indexer: sourcegraph/scip-java@sha256:02b5dd2a14b3d0cd776b561ba68660676b4172677fd1fff59cb5d9a0b5351cea indexer_args: - scip-java - index diff --git a/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_Maven.yaml b/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_Maven.yaml index 0e49a617663..843b54a6f8d 100644 --- a/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_Maven.yaml +++ b/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_Maven.yaml @@ -1,7 +1,7 @@ - steps: [] local_steps: [] root: "" - indexer: sourcegraph/scip-java@sha256:a2b3828145cd38758a43363f06d786f9e620c97979a9291463c6544f7f17c68f + indexer: sourcegraph/scip-java@sha256:02b5dd2a14b3d0cd776b561ba68660676b4172677fd1fff59cb5d9a0b5351cea indexer_args: - scip-java - index diff --git a/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_SBT.yaml b/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_SBT.yaml index 0e49a617663..843b54a6f8d 100644 --- a/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_SBT.yaml +++ b/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_SBT.yaml @@ -1,7 +1,7 @@ - steps: [] local_steps: [] root: "" - indexer: sourcegraph/scip-java@sha256:a2b3828145cd38758a43363f06d786f9e620c97979a9291463c6544f7f17c68f + indexer: sourcegraph/scip-java@sha256:02b5dd2a14b3d0cd776b561ba68660676b4172677fd1fff59cb5d9a0b5351cea indexer_args: - scip-java - index diff --git a/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_lsif-java.json.yaml b/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_lsif-java.json.yaml index 0e49a617663..843b54a6f8d 100644 --- a/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_lsif-java.json.yaml +++ b/internal/codeintel/autoindexing/internal/inference/testdata/JVM_project_with_lsif-java.json.yaml @@ -1,7 +1,7 @@ - steps: [] local_steps: [] root: "" - indexer: sourcegraph/scip-java@sha256:a2b3828145cd38758a43363f06d786f9e620c97979a9291463c6544f7f17c68f + indexer: sourcegraph/scip-java@sha256:02b5dd2a14b3d0cd776b561ba68660676b4172677fd1fff59cb5d9a0b5351cea indexer_args: - scip-java - index diff --git a/internal/codeintel/autoindexing/internal/inference/testdata/Nested_JVM_project_WITHOUT_top-level_build_file.yaml b/internal/codeintel/autoindexing/internal/inference/testdata/Nested_JVM_project_WITHOUT_top-level_build_file.yaml index ca34d9695d5..600dfb8a1ba 100644 --- a/internal/codeintel/autoindexing/internal/inference/testdata/Nested_JVM_project_WITHOUT_top-level_build_file.yaml +++ b/internal/codeintel/autoindexing/internal/inference/testdata/Nested_JVM_project_WITHOUT_top-level_build_file.yaml @@ -1,7 +1,7 @@ - steps: [] local_steps: [] root: my-module - indexer: sourcegraph/scip-java@sha256:a2b3828145cd38758a43363f06d786f9e620c97979a9291463c6544f7f17c68f + indexer: sourcegraph/scip-java@sha256:02b5dd2a14b3d0cd776b561ba68660676b4172677fd1fff59cb5d9a0b5351cea indexer_args: - scip-java - index @@ -11,7 +11,7 @@ - steps: [] local_steps: [] root: our-module - indexer: sourcegraph/scip-java@sha256:a2b3828145cd38758a43363f06d786f9e620c97979a9291463c6544f7f17c68f + indexer: sourcegraph/scip-java@sha256:02b5dd2a14b3d0cd776b561ba68660676b4172677fd1fff59cb5d9a0b5351cea indexer_args: - scip-java - index diff --git a/internal/codeintel/autoindexing/internal/inference/testdata/Nested_JVM_project_with_top-level_build_file.yaml b/internal/codeintel/autoindexing/internal/inference/testdata/Nested_JVM_project_with_top-level_build_file.yaml index 0e49a617663..843b54a6f8d 100644 --- a/internal/codeintel/autoindexing/internal/inference/testdata/Nested_JVM_project_with_top-level_build_file.yaml +++ b/internal/codeintel/autoindexing/internal/inference/testdata/Nested_JVM_project_with_top-level_build_file.yaml @@ -1,7 +1,7 @@ - steps: [] local_steps: [] root: "" - indexer: sourcegraph/scip-java@sha256:a2b3828145cd38758a43363f06d786f9e620c97979a9291463c6544f7f17c68f + indexer: sourcegraph/scip-java@sha256:02b5dd2a14b3d0cd776b561ba68660676b4172677fd1fff59cb5d9a0b5351cea indexer_args: - scip-java - index diff --git a/internal/codeintel/autoindexing/internal/inference/testdata/go_files_in_root.yaml b/internal/codeintel/autoindexing/internal/inference/testdata/go_files_in_root.yaml index 537e066338f..b5d8409ddf0 100644 --- a/internal/codeintel/autoindexing/internal/inference/testdata/go_files_in_root.yaml +++ b/internal/codeintel/autoindexing/internal/inference/testdata/go_files_in_root.yaml @@ -8,7 +8,7 @@ echo "No netrc config set, continuing" fi root: "" - indexer: sourcegraph/scip-go@sha256:56414010d8917d6952c051dd5fcc0901fdf5c12031d352cc0b26778f040dddcc + indexer: sourcegraph/scip-go@sha256:bf89e619382fed5efa21a51806d7c6f22d19b6dea458a58554c0af89120f4ca3 indexer_args: - GO111MODULE=off - scip-go diff --git a/internal/codeintel/autoindexing/internal/inference/testdata/go_modules.yaml b/internal/codeintel/autoindexing/internal/inference/testdata/go_modules.yaml index 4b8a9043b3e..0c93486f3cb 100644 --- a/internal/codeintel/autoindexing/internal/inference/testdata/go_modules.yaml +++ b/internal/codeintel/autoindexing/internal/inference/testdata/go_modules.yaml @@ -1,6 +1,6 @@ - steps: - root: foo/bar - image: sourcegraph/scip-go@sha256:56414010d8917d6952c051dd5fcc0901fdf5c12031d352cc0b26778f040dddcc + image: sourcegraph/scip-go@sha256:bf89e619382fed5efa21a51806d7c6f22d19b6dea458a58554c0af89120f4ca3 commands: - | if [ "$NETRC_DATA" ]; then @@ -19,7 +19,7 @@ echo "No netrc config set, continuing" fi root: foo/bar - indexer: sourcegraph/scip-go@sha256:56414010d8917d6952c051dd5fcc0901fdf5c12031d352cc0b26778f040dddcc + indexer: sourcegraph/scip-go@sha256:bf89e619382fed5efa21a51806d7c6f22d19b6dea458a58554c0af89120f4ca3 indexer_args: - scip-go - --no-animation @@ -33,7 +33,7 @@ - NETRC_DATA - steps: - root: foo/baz - image: sourcegraph/scip-go@sha256:56414010d8917d6952c051dd5fcc0901fdf5c12031d352cc0b26778f040dddcc + image: sourcegraph/scip-go@sha256:bf89e619382fed5efa21a51806d7c6f22d19b6dea458a58554c0af89120f4ca3 commands: - | if [ "$NETRC_DATA" ]; then @@ -52,7 +52,7 @@ echo "No netrc config set, continuing" fi root: foo/baz - indexer: sourcegraph/scip-go@sha256:56414010d8917d6952c051dd5fcc0901fdf5c12031d352cc0b26778f040dddcc + indexer: sourcegraph/scip-go@sha256:bf89e619382fed5efa21a51806d7c6f22d19b6dea458a58554c0af89120f4ca3 indexer_args: - scip-go - --no-animation diff --git a/internal/codeintel/autoindexing/internal/inference/testdata/scip-ruby.yaml b/internal/codeintel/autoindexing/internal/inference/testdata/scip-ruby.yaml index ec882c42e44..4f078145951 100644 --- a/internal/codeintel/autoindexing/internal/inference/testdata/scip-ruby.yaml +++ b/internal/codeintel/autoindexing/internal/inference/testdata/scip-ruby.yaml @@ -1,7 +1,7 @@ - steps: [] local_steps: [] root: a - indexer: sourcegraph/scip-ruby@sha256:ef53e5f1450330ddb4a3edce963b7e10d900d44ff1e7de4960680289ac25f319 + indexer: sourcegraph/scip-ruby@sha256:75a54c6032c977ae4960c5651f6095bb017bf55b7a4b86e608c6235443c38cbb indexer_args: - scip-ruby-autoindex outfile: index.scip @@ -9,7 +9,7 @@ - steps: [] local_steps: [] root: b - indexer: sourcegraph/scip-ruby@sha256:ef53e5f1450330ddb4a3edce963b7e10d900d44ff1e7de4960680289ac25f319 + indexer: sourcegraph/scip-ruby@sha256:75a54c6032c977ae4960c5651f6095bb017bf55b7a4b86e608c6235443c38cbb indexer_args: - scip-ruby-autoindex outfile: index.scip @@ -17,7 +17,7 @@ - steps: [] local_steps: [] root: c - indexer: sourcegraph/scip-ruby@sha256:ef53e5f1450330ddb4a3edce963b7e10d900d44ff1e7de4960680289ac25f319 + indexer: sourcegraph/scip-ruby@sha256:75a54c6032c977ae4960c5651f6095bb017bf55b7a4b86e608c6235443c38cbb indexer_args: - scip-ruby-autoindex outfile: index.scip @@ -25,7 +25,7 @@ - steps: [] local_steps: [] root: d - indexer: sourcegraph/scip-ruby@sha256:ef53e5f1450330ddb4a3edce963b7e10d900d44ff1e7de4960680289ac25f319 + indexer: sourcegraph/scip-ruby@sha256:75a54c6032c977ae4960c5651f6095bb017bf55b7a4b86e608c6235443c38cbb indexer_args: - scip-ruby-autoindex outfile: index.scip @@ -33,7 +33,7 @@ - steps: [] local_steps: [] root: e - indexer: sourcegraph/scip-ruby@sha256:ef53e5f1450330ddb4a3edce963b7e10d900d44ff1e7de4960680289ac25f319 + indexer: sourcegraph/scip-ruby@sha256:75a54c6032c977ae4960c5651f6095bb017bf55b7a4b86e608c6235443c38cbb indexer_args: - scip-ruby-autoindex outfile: index.scip From 55f5dc7d913dced1a49fd3853f02565aa09ee587 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Fri, 12 Jul 2024 12:45:08 +0200 Subject: [PATCH 3/9] fix(sg): do not try to publish analytics when it is disabled (#63800) The background publisher was started regardless if analytics was disabled or not. This PR makes it so that we only publish analytics if it is enabled. To make it work and not duplicate the disabled analytics check, I moved the usershell + background context creation to happen earlier. ## Test plan CI and tested locally ## Changelog * sg - only start the analytics background publisher when analytics are enabled --------- Co-authored-by: Jean-Hadrien Chabran --- dev/sg/main.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/dev/sg/main.go b/dev/sg/main.go index 6722975a09b..f5b4fc77c41 100644 --- a/dev/sg/main.go +++ b/dev/sg/main.go @@ -196,6 +196,17 @@ var sg = &cli.App{ } else { cmd.Context = secrets.WithContext(cmd.Context, secretsStore) } + // Initialize context + cmd.Context, err = usershell.Context(cmd.Context) + if err != nil { + std.Out.WriteWarningf("Unable to infer user shell context: %s", err) + } + cmd.Context = background.Context(cmd.Context, verbose) + + // We need to register the wait in the interrupt handlers, because if interrupted + // the .After on cli.App won't run. This makes sure that both the happy and sad paths + // are waiting for background tasks to finish. + interrupt.Register(func() { background.Wait(cmd.Context, std.Out) }) // Set up analytics and hooks for each command - do this as the first context // setup @@ -213,7 +224,7 @@ var sg = &cli.App{ msg = "The problem occured while trying to get a secret via Google. Below is the error:\n" msg += fmt.Sprintf("\n```%v```\n", err) msg += "\nPossible fixes:\n" - msg += "- You should be in the `gcp-engineers@sourcegraph.com` group. Ask #ask-it-tech-ops or #discuss-dev-infra to check that\n" + msg += "- You should be in the `gcp-engineering@sourcegraph.com` group. Ask #ask-it-tech-ops or #discuss-dev-infra to check that\n" msg += "- Ensure you're currently authenticated with your sourcegraph.com account by running `gcloud auth list`\n" msg += "- Ensure you're authenticated with gcloud by running `gcloud auth application-default login`\n" } else { @@ -228,20 +239,11 @@ var sg = &cli.App{ // Add analytics to each command addAnalyticsHooks([]string{"sg"}, cmd.App.Commands) + // Start the analytics publisher + analytics.BackgroundEventPublisher(cmd.Context) + interrupt.Register(analytics.StopBackgroundEventPublisher) } - // Initialize context after analytics are set up - cmd.Context, err = usershell.Context(cmd.Context) - if err != nil { - std.Out.WriteWarningf("Unable to infer user shell context: %s", err) - } - cmd.Context = background.Context(cmd.Context, verbose) - interrupt.Register(func() { background.Wait(cmd.Context, std.Out) }) - - // start the analytics publisher - analytics.BackgroundEventPublisher(cmd.Context) - interrupt.Register(analytics.StopBackgroundEventPublisher) - // Configure logger, for commands that use components that use loggers if _, set := os.LookupEnv(log.EnvDevelopment); !set { os.Setenv(log.EnvDevelopment, "true") From 46837deb917ea4731d8fd0f94f6313549c34906b Mon Sep 17 00:00:00 2001 From: Erik Seliger Date: Fri, 12 Jul 2024 16:02:46 +0200 Subject: [PATCH 4/9] webhooks: Polish UI components (#63664) This PR overhauls the UI a bunch to make it look more in line with other pages, and fixes various smaller papercuts and bugs. Closes SRC-377 Test plan: Added storybooks and made sure existing ones still look good, created, updated, deleted various webhooks locally. --- client/web/BUILD.bazel | 5 +- .../ExternalServiceEditPage.tsx | 2 - .../externalServices/ExternalServicePage.tsx | 139 ++++--- .../ExternalServiceSyncJobsList.tsx | 39 +- .../ExternalServiceWebhook.story.tsx | 40 -- .../ExternalServiceWebhook.tsx | 102 ----- .../components/externalServices/backend.ts | 2 - .../SiteAdminWebhookCreatePage.story.tsx | 44 ++- .../site-admin/SiteAdminWebhookCreatePage.tsx | 6 +- .../site-admin/SiteAdminWebhookPage.story.tsx | 15 +- .../src/site-admin/SiteAdminWebhookPage.tsx | 310 +++++++++++---- .../SiteAdminWebhookUpdatePage.story.tsx | 32 +- .../site-admin/SiteAdminWebhookUpdatePage.tsx | 7 +- .../SiteAdminWebhooksPage.story.tsx | 28 +- .../src/site-admin/SiteAdminWebhooksPage.tsx | 17 +- .../SiteConfigurationChangeList.tsx | 3 + .../site-admin/WebhookConfirmDeleteModal.tsx | 69 ++++ .../WebhookCreateUpdatePage.module.scss | 17 +- .../site-admin/WebhookCreateUpdatePage.tsx | 356 +++++++++--------- .../WebhookInfoLogPageHeader.story.tsx | 3 +- .../site-admin/WebhookInformation.story.tsx | 8 +- .../web/src/site-admin/WebhookInformation.tsx | 20 +- client/web/src/site-admin/WebhookNode.tsx | 70 ++-- client/web/src/site-admin/backend.ts | 49 ++- client/web/src/site-admin/fixtures.ts | 1 - .../webhooks/WebhookLogNode.story.tsx | 2 +- .../webhooks/WebhookLogPage.module.scss | 13 - .../webhooks/WebhookLogPage.story.tsx | 157 -------- .../site-admin/webhooks/WebhookLogPage.tsx | 84 ----- .../webhooks/WebhookLogPageHeader.module.scss | 16 - .../webhooks/WebhookLogPageHeader.story.tsx | 59 +-- .../webhooks/WebhookLogPageHeader.tsx | 35 +- client/web/src/site-admin/webhooks/backend.ts | 134 +------ .../graphqlbackend/external_service.go | 14 + cmd/frontend/graphqlbackend/schema.graphql | 48 +-- 35 files changed, 815 insertions(+), 1131 deletions(-) delete mode 100644 client/web/src/components/externalServices/ExternalServiceWebhook.story.tsx delete mode 100644 client/web/src/components/externalServices/ExternalServiceWebhook.tsx create mode 100644 client/web/src/site-admin/WebhookConfirmDeleteModal.tsx delete mode 100644 client/web/src/site-admin/webhooks/WebhookLogPage.module.scss delete mode 100644 client/web/src/site-admin/webhooks/WebhookLogPage.story.tsx delete mode 100644 client/web/src/site-admin/webhooks/WebhookLogPage.tsx diff --git a/client/web/BUILD.bazel b/client/web/BUILD.bazel index dd09547e731..d35dd31b33e 100644 --- a/client/web/BUILD.bazel +++ b/client/web/BUILD.bazel @@ -380,7 +380,6 @@ ts_project( "src/components/externalServices/ExternalServicePage.tsx", "src/components/externalServices/ExternalServiceSyncJobNode.tsx", "src/components/externalServices/ExternalServiceSyncJobsList.tsx", - "src/components/externalServices/ExternalServiceWebhook.tsx", "src/components/externalServices/ExternalServicesPage.tsx", "src/components/externalServices/GerritIcon.tsx", "src/components/externalServices/backend.ts", @@ -1524,6 +1523,7 @@ ts_project( "src/site-admin/UserManagement/components/useUserListActions.tsx", "src/site-admin/UserManagement/index.tsx", "src/site-admin/UserManagement/queries.tsx", + "src/site-admin/WebhookConfirmDeleteModal.tsx", "src/site-admin/WebhookCreateUpdatePage.tsx", "src/site-admin/WebhookInfoLogPageHeader.tsx", "src/site-admin/WebhookInformation.tsx", @@ -1597,7 +1597,6 @@ ts_project( "src/site-admin/webhooks/PerformanceGauge.tsx", "src/site-admin/webhooks/StatusCode.tsx", "src/site-admin/webhooks/WebhookLogNode.tsx", - "src/site-admin/webhooks/WebhookLogPage.tsx", "src/site-admin/webhooks/WebhookLogPageHeader.tsx", "src/site-admin/webhooks/backend.ts", "src/site-admin/webhooks/story/StyledPerformanceGauge.tsx", @@ -2225,7 +2224,6 @@ ts_project( "src/components/externalServices/ExternalServiceEditPage.story.tsx", "src/components/externalServices/ExternalServicePage.story.tsx", "src/components/externalServices/ExternalServiceSyncJobNode.story.tsx", - "src/components/externalServices/ExternalServiceWebhook.story.tsx", "src/components/externalServices/ExternalServicesPage.story.tsx", "src/components/fuzzyFinder/FuzzyFinder.story.tsx", "src/components/time/Duration.story.tsx", @@ -2422,7 +2420,6 @@ ts_project( "src/site-admin/webhooks/PerformanceGauge.story.tsx", "src/site-admin/webhooks/StatusCode.story.tsx", "src/site-admin/webhooks/WebhookLogNode.story.tsx", - "src/site-admin/webhooks/WebhookLogPage.story.tsx", "src/site-admin/webhooks/WebhookLogPageHeader.story.tsx", "src/storm/pages/SearchPage/SearchPageContent.story.tsx", "src/team/area/TeamChildTeamsPage.story.tsx", diff --git a/client/web/src/components/externalServices/ExternalServiceEditPage.tsx b/client/web/src/components/externalServices/ExternalServiceEditPage.tsx index 29b0909e027..3d5257883bb 100644 --- a/client/web/src/components/externalServices/ExternalServiceEditPage.tsx +++ b/client/web/src/components/externalServices/ExternalServiceEditPage.tsx @@ -20,7 +20,6 @@ import { import { getBreadCrumbs } from './breadCrumbs' import { ExternalServiceForm } from './ExternalServiceForm' import { resolveExternalServiceCategory } from './externalServices' -import { ExternalServiceWebhook } from './ExternalServiceWebhook' interface Props extends TelemetryProps, TelemetryV2Props { externalServicesFromFile: boolean @@ -150,7 +149,6 @@ export const ExternalServiceEditPage: FC = ({ telemetryRecorder={telemetryRecorder} /> )} - ) } diff --git a/client/web/src/components/externalServices/ExternalServicePage.tsx b/client/web/src/components/externalServices/ExternalServicePage.tsx index 8de883e8dff..80932ab7dee 100644 --- a/client/web/src/components/externalServices/ExternalServicePage.tsx +++ b/client/web/src/components/externalServices/ExternalServicePage.tsx @@ -32,7 +32,6 @@ import { getBreadCrumbs } from './breadCrumbs' import { ExternalServiceInformation } from './ExternalServiceInformation' import { resolveExternalServiceCategory } from './externalServices' import { ExternalServiceSyncJobsList } from './ExternalServiceSyncJobsList' -import { ExternalServiceWebhook } from './ExternalServiceWebhook' import styles from './ExternalServicePage.module.scss' @@ -137,22 +136,13 @@ export const ExternalServicePage: FC = props => { const renderExternalService = (externalService: ExternalServiceFieldsWithConfig): JSX.Element => { let externalServiceAvailabilityStatus if (loading) { - externalServiceAvailabilityStatus = ( - - Checking code host connection status... - - ) + externalServiceAvailabilityStatus = Checking code host connection status... } else if (!error) { if (checkConnectionNode?.__typename === 'ExternalServiceAvailable') { - externalServiceAvailabilityStatus = ( - - Code host is reachable. - - ) + externalServiceAvailabilityStatus = Code host is reachable. } else if (checkConnectionNode?.__typename === 'ExternalServiceUnavailable') { externalServiceAvailabilityStatus = ( @@ -160,16 +150,12 @@ export const ExternalServicePage: FC = props => { } } else { externalServiceAvailabilityStatus = ( - + ) } const externalServiceCategory = resolveExternalServiceCategory(externalService) return ( - + <> = props => { } /> - {isErrorLike(isDeleting) && } - {externalServiceAvailabilityStatus} -

Information

- {externalService.unrestricted && ( - -

All repositories will be unrestricted

- This code host connection does not have authorization configured. Any repositories added by this - code host will be accessible by all users on the instance, even if another code host connection - with authorization syncs the same repository. See{' '} - the documentation for instructions on - configuring authorization. -
- )} - {externalServiceCategory && ( - + {isErrorLike(isDeleting) && } + {externalServiceAvailabilityStatus} +

Information

+ {externalService.unrestricted && ( + +

All repositories will be unrestricted

+ This code host connection does not have authorization configured. Any repositories added by + this code host will be accessible by all users on the instance, even if another code host + connection with authorization syncs the same repository. See{' '} + the documentation for instructions + on configuring authorization. +
+ )} + {externalServiceCategory && ( + + )} +

Configuration

+ {externalServiceCategory && ( + + )} +
+
+

Recent sync jobs

+ - )} -

Configuration

- {externalServiceCategory && ( - + + {syncExternalServiceError && } + - )} - - - {syncExternalServiceError && } - - + + ) } diff --git a/client/web/src/components/externalServices/ExternalServiceSyncJobsList.tsx b/client/web/src/components/externalServices/ExternalServiceSyncJobsList.tsx index 5059805c864..4196c1d035f 100644 --- a/client/web/src/components/externalServices/ExternalServiceSyncJobsList.tsx +++ b/client/web/src/components/externalServices/ExternalServiceSyncJobsList.tsx @@ -3,8 +3,6 @@ import React, { useCallback } from 'react' import type { Subject } from 'rxjs' import { repeat, tap } from 'rxjs/operators' -import { H2 } from '@sourcegraph/wildcard' - import { type ExternalServiceSyncJobConnectionFields, type ExternalServiceSyncJobListFields, @@ -56,25 +54,22 @@ export const ExternalServiceSyncJobsList: React.FunctionComponent -

Recent sync jobs

- , - {}, - ExternalServiceSyncJobConnectionFields - > - className="mb-0 mt-1" - noun="sync job" - listClassName="list-group-flush" - pluralNoun="sync jobs" - queryConnection={queryConnection} - nodeComponent={ExternalServiceSyncJobNode} - nodeComponentProps={{ onUpdate: updates }} - hideSearch={true} - noSummaryIfAllNodesVisible={true} - updates={updates} - /> - + , + {}, + ExternalServiceSyncJobConnectionFields + > + className="mb-0" + noun="sync job" + listClassName="list-group-flush" + pluralNoun="sync jobs" + queryConnection={queryConnection} + nodeComponent={ExternalServiceSyncJobNode} + nodeComponentProps={{ onUpdate: updates }} + hideSearch={true} + noSummaryIfAllNodesVisible={true} + updates={updates} + /> ) } diff --git a/client/web/src/components/externalServices/ExternalServiceWebhook.story.tsx b/client/web/src/components/externalServices/ExternalServiceWebhook.story.tsx deleted file mode 100644 index 79e6153c157..00000000000 --- a/client/web/src/components/externalServices/ExternalServiceWebhook.story.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import type { Decorator, Meta, StoryFn } from '@storybook/react' - -import { ExternalServiceKind } from '../../graphql-operations' -import { WebStory } from '../WebStory' - -import { ExternalServiceWebhook } from './ExternalServiceWebhook' - -const decorator: Decorator = story => {() =>
{story()}
}
- -const config: Meta = { - title: 'web/External services/ExternalServiceWebhook', - decorators: [decorator], -} - -export default config - -export const BitbucketServer: StoryFn = () => ( - -) - -export const GitHub: StoryFn = () => ( - -) - -export const GitLab: StoryFn = () => ( - -) - -GitLab.parameters = { - chromatic: { - // Visually the same as GitHub - disable: true, - }, -} diff --git a/client/web/src/components/externalServices/ExternalServiceWebhook.tsx b/client/web/src/components/externalServices/ExternalServiceWebhook.tsx deleted file mode 100644 index f1f9616ea54..00000000000 --- a/client/web/src/components/externalServices/ExternalServiceWebhook.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react' - -import { Alert, Link, H3, Text, H4 } from '@sourcegraph/wildcard' - -import { type ExternalServiceFields, ExternalServiceKind } from '../../graphql-operations' -import { CopyableText } from '../CopyableText' - -interface Props { - externalService: Pick - className?: string -} - -export const ExternalServiceWebhook: React.FunctionComponent> = ({ - externalService: { kind, webhookURL }, - className, -}) => { - if (!webhookURL) { - return <> - } - - let description = - - switch (kind) { - case ExternalServiceKind.BITBUCKETSERVER: { - description = ( - - - Webhooks - {' '} - will be created automatically on the configured Bitbucket Server instance. In case you don't provide - an admin token,{' '} - - follow the docs on how to set up webhooks manually - - . -
- To set up another webhook manually, use the following URL: -
- ) - break - } - - case ExternalServiceKind.GITHUB: { - description = commonDescription('github') - break - } - - case ExternalServiceKind.GITLAB: { - description = commonDescription('gitlab') - break - } - } - - return ( - -

Batch changes webhooks

-

- Adding webhooks via code host connections has been{' '} - - deprecated. - -

- {description} - - - Note that only{' '} - - batch changes - {' '} - make use of this webhook. To enable webhooks to trigger repository updates on Sourcegraph,{' '} - - see the docs on how to use them - - . - -
- ) -} - -function commonDescription(url: string): JSX.Element { - return ( - - Point{' '} - - webhooks - {' '} - for this code host connection at the following URL: - - ) -} diff --git a/client/web/src/components/externalServices/backend.ts b/client/web/src/components/externalServices/backend.ts index b46038b96fa..6665155bf32 100644 --- a/client/web/src/components/externalServices/backend.ts +++ b/client/web/src/components/externalServices/backend.ts @@ -75,7 +75,6 @@ export const externalServiceFragment = gql` username url } - webhookURL hasConnectionCheck unrestricted } @@ -237,7 +236,6 @@ export const LIST_EXTERNAL_SERVICE_FRAGMENT = gql` username url } - webhookURL hasConnectionCheck syncJobs(first: 1) { ...ExternalServiceSyncJobConnectionFields diff --git a/client/web/src/site-admin/SiteAdminWebhookCreatePage.story.tsx b/client/web/src/site-admin/SiteAdminWebhookCreatePage.story.tsx index 66585fa0115..a6c54b03da6 100644 --- a/client/web/src/site-admin/SiteAdminWebhookCreatePage.story.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookCreatePage.story.tsx @@ -8,10 +8,10 @@ import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry' import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService' import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo' -import { EXTERNAL_SERVICES } from '../components/externalServices/backend' import { WebStory } from '../components/WebStory' +import { WebhookExternalServiceFields } from '../graphql-operations' -import { createExternalService } from './fixtures' +import { WEBHOOK_EXTERNAL_SERVICES } from './backend' import { SiteAdminWebhookCreatePage } from './SiteAdminWebhookCreatePage' const decorator: Decorator = Story => @@ -27,14 +27,14 @@ export const WebhookCreatePage: StoryFn = () => { const mocks = new WildcardMockLink([ { request: { - query: getDocumentNode(EXTERNAL_SERVICES), - variables: { first: null, after: null, repo: null }, + query: getDocumentNode(WEBHOOK_EXTERNAL_SERVICES), + variables: {}, }, result: { data: { externalServices: { __typename: 'ExternalServiceConnection', - totalCount: 17, + totalCount: 6, pageInfo: { endCursor: null, hasNextPage: false, @@ -57,10 +57,12 @@ export const WebhookCreatePage: StoryFn = () => { {() => ( - +
+ +
)}
@@ -73,8 +75,8 @@ export const WebhookCreatePageWithError: StoryFn = () => { const mockedResponse: MockedResponse[] = [ { request: { - query: getDocumentNode(EXTERNAL_SERVICES), - variables: { first: null, after: null, repo: null }, + query: getDocumentNode(WEBHOOK_EXTERNAL_SERVICES), + variables: {}, }, error: new Error('oops'), }, @@ -83,10 +85,12 @@ export const WebhookCreatePageWithError: StoryFn = () => { {() => ( - +
+ +
)}
@@ -94,3 +98,13 @@ export const WebhookCreatePageWithError: StoryFn = () => { } WebhookCreatePageWithError.storyName = 'Error during external services fetch' + +function createExternalService(kind: ExternalServiceKind, url: string): WebhookExternalServiceFields { + return { + __typename: 'ExternalService', + id: `service-${url}`, + kind, + displayName: `${kind}-123`, + url, + } +} diff --git a/client/web/src/site-admin/SiteAdminWebhookCreatePage.tsx b/client/web/src/site-admin/SiteAdminWebhookCreatePage.tsx index d262050621d..b5738dfaea9 100644 --- a/client/web/src/site-admin/SiteAdminWebhookCreatePage.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookCreatePage.tsx @@ -4,7 +4,7 @@ import { mdiWebhook } from '@mdi/js' import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' -import { Container, PageHeader } from '@sourcegraph/wildcard' +import { PageHeader } from '@sourcegraph/wildcard' import { PageTitle } from '../components/PageTitle' @@ -22,7 +22,7 @@ export const SiteAdminWebhookCreatePage: FC = ( }, [telemetryService, telemetryRecorder]) return ( - + <> = ( className="mb-3" /> - + ) } diff --git a/client/web/src/site-admin/SiteAdminWebhookPage.story.tsx b/client/web/src/site-admin/SiteAdminWebhookPage.story.tsx index 4622a4ce6b3..496d62a5d42 100644 --- a/client/web/src/site-admin/SiteAdminWebhookPage.story.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookPage.story.tsx @@ -54,12 +54,13 @@ export const SiteAdminWebhookPageStory: StoryFn = args => { after: null, onlyErrors: false, onlyUnmatched: false, - webhookID: '', + webhookID: '1', }, }, result: { data: { webhookLogs: { + __typename: 'WebhookLogConnection', nodes: WEBHOOK_MOCK_DATA, pageInfo: { hasNextPage: false }, totalCount: 20, @@ -82,6 +83,7 @@ export const SiteAdminWebhookPageStory: StoryFn = args => { result: { data: { webhookLogs: { + __typename: 'WebhookLogConnection', nodes: ERRORED_WEBHOOK_MOCK_DATA, pageInfo: { hasNextPage: false }, totalCount: 20, @@ -116,10 +118,12 @@ export const SiteAdminWebhookPageStory: StoryFn = args => { +
+ +
} /> @@ -155,6 +159,7 @@ export const SiteAdminWebhookPageWithoutLogsStory: StoryFn = args => { result: { data: { webhookLogs: { + __typename: 'WebhookLogConnection', nodes: [], pageInfo: { hasNextPage: false }, totalCount: 0, diff --git a/client/web/src/site-admin/SiteAdminWebhookPage.tsx b/client/web/src/site-admin/SiteAdminWebhookPage.tsx index 13c8076b7b0..f3f952f8e4e 100644 --- a/client/web/src/site-admin/SiteAdminWebhookPage.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookPage.tsx @@ -1,10 +1,8 @@ -import { type FC, useEffect, useState } from 'react' +import { type FC, useEffect, useState, useCallback } from 'react' import { mdiWebhook, mdiDelete, mdiPencil } from '@mdi/js' -import { noop } from 'lodash' import { useNavigate, useParams } from 'react-router-dom' -import { useMutation } from '@sourcegraph/http-client' import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' import { @@ -14,10 +12,12 @@ import { H2, H5, Link, - LoadingSpinner, PageHeader, ErrorAlert, Icon, + Alert, + Text, + Code, } from '@sourcegraph/wildcard' import { CreatedByAndUpdatedByInfoByline } from '../components/Byline/CreatedByAndUpdatedByInfoByline' @@ -31,9 +31,10 @@ import { SummaryContainer, } from '../components/FilteredConnection/ui' import { PageTitle } from '../components/PageTitle' -import type { DeleteWebhookResult, DeleteWebhookVariables, WebhookFields } from '../graphql-operations' +import { ExternalServiceKind, type WebhookFields } from '../graphql-operations' -import { DELETE_WEBHOOK, useWebhookLogsConnection, useWebhookQuery } from './backend' +import { useWebhookLogsConnection, useWebhookQuery } from './backend' +import { WebhookConfirmDeleteModal } from './WebhookConfirmDeleteModal' import { WebhookInfoLogPageHeader } from './WebhookInfoLogPageHeader' import { WebhookInformation } from './WebhookInformation' import { WebhookLogNode } from './webhooks/WebhookLogNode' @@ -49,23 +50,30 @@ export const SiteAdminWebhookPage: FC = props => { const navigate = useNavigate() const [onlyErrors, setOnlyErrors] = useState(false) - const { loading, hasNextPage, fetchMore, connection, error } = useWebhookLogsConnection(id, 20, onlyErrors) - const { loading: webhookLoading, data: webhookData } = useWebhookQuery(id) + const { + loading, + hasNextPage, + fetchMore, + connection, + error: webhookLogsError, + } = useWebhookLogsConnection(id, 20, onlyErrors) + const { loading: webhookLoading, data: webhookData, error: webhookError } = useWebhookQuery(id) useEffect(() => { telemetryService.logPageView('SiteAdminWebhook') telemetryRecorder.recordEvent('admin.webhook', 'view') }, [telemetryService, telemetryRecorder]) - const [deleteWebhook, { error: deleteError, loading: isDeleting }] = useMutation< - DeleteWebhookResult, - DeleteWebhookVariables - >(DELETE_WEBHOOK, { variables: { hookID: id }, onCompleted: () => navigate('/site-admin/webhooks/incoming') }) + const [showDeleteModal, setShowDeleteModal] = useState(false) + const deleteWebhook = useCallback(() => { + setShowDeleteModal(true) + }, []) return ( - + <> {webhookLoading && !webhookData && } + {!webhookLoading && !webhookData && webhookError && } {webhookData?.node && webhookData.node.__typename === 'Webhook' && ( = props => { variant="secondary" display="inline" > - - {' Edit'} + Edit
} /> )} + +

Information

+ {webhookData?.node && webhookData.node.__typename === 'Webhook' && ( + + )} - {deleteError && } +

Logs

+ + + + {webhookLogsError && } + {loading && !connection && } + + + + {connection?.nodes?.map(node => ( + + ))} + + + {connection && ( + + } + /> + {hasNextPage && } + + )} + +
-

Information

{webhookData?.node && webhookData.node.__typename === 'Webhook' && ( - + <> +

Setup instructions

+ + + + )} -

Logs

- - - - {error && } - {loading && !connection && } - - - - {connection?.nodes?.map(node => ( - - ))} - - - {connection && ( - - } - /> - {hasNextPage && } - - )} - - + {showDeleteModal && webhookData?.node && webhookData.node.__typename === 'Webhook' && ( + setShowDeleteModal(false)} + afterDelete={() => navigate('/site-admin/webhooks/incoming')} + telemetryRecorder={telemetryRecorder} + /> + )} + ) } @@ -195,3 +204,170 @@ const EmptyList: FC<{ onlyErrors: boolean }> = ({ onlyErrors }) => ( )} ) + +interface WebhookSetupInstructionsProps { + webhook: WebhookFields +} + +const WebhookSetupInstructions: React.FunctionComponent = ({ webhook }) => { + if (webhook.codeHostKind === ExternalServiceKind.GITHUB) { + return ( + <> + + To set up a GitHub webhook, follow the instructions below, or see more in the{' '} + GitHub webooks documentation. + + + Note: For GitHub App integrations, webhooks are created automatically. You do not need to create + them manually. + + +
    +
  1. + Copy the webhook URL {webhook.url} +
  2. +
  3. + On GitHub, go to the settings page of your organization. From there, click{' '} + Settings, then + Webhooks, then Add webhook. +
  4. +
  5. + Fill in the webhook form: +
      +
    • Payload URL: the URL you just copied above.
    • +
    • + Content type: this must be set to application/json. +
    • +
    • Secret: the secret token you can find above.
    • +
    • Active: ensure this is enabled.
    • +
    • + Which events: select Let me select individual events, and then + enable: + + + + + + + + + + + + + + + +
      Repo updatesBatch ChangesRepo permissions
      +
        +
      • + push +
      • +
      +
      +
        +
      • + Issue comments +
      • +
      • + Pull requests +
      • +
      • + Pull request reviews +
      • +
      • + Pull request review comments +
      • +
      • + Check runs +
      • +
      • + Check suites +
      • +
      • + Statuses +
      • +
      +
      +
        +
      • + Collaborator add, remove, or changed +
      • +
      • + Memberships +
      • +
      • + Organizations +
      • +
      • + Repositories +
      • +
      • + Teams +
      • +
      +
      +
    • +
    +
  6. +
  7. + Click Add webhook. +
  8. +
  9. Confirm that the new webhook is listed.
  10. +
  11. You should see an initial ping event sent from GitHub in the webhook logs above.
  12. +
+
+ + ) + } + if (webhook.codeHostKind === ExternalServiceKind.GITLAB) { + return ( + <> + + To set up a GitLab webhook, follow the instructions in the{' '} + GitLab integration documentation. + + + ) + } + if (webhook.codeHostKind === ExternalServiceKind.BITBUCKETSERVER) { + return ( + <> + + To set up a Bitbucket Server webhook, follow the instructions in the{' '} + + Bitbucket Server integration documentation + + . + + + ) + } + if (webhook.codeHostKind === ExternalServiceKind.BITBUCKETCLOUD) { + return ( + <> + + To set up a Bitbucket Cloud webhook, follow the instructions in the{' '} + + Bitbucket Cloud integration documentation + + . + + + ) + } + if (webhook.codeHostKind === ExternalServiceKind.AZUREDEVOPS) { + return ( + <> + + To set up an Azure DevOps webhook, follow the instructions in the{' '} + + Azure DevOps integration documentation + + . + + + ) + } + return null +} diff --git a/client/web/src/site-admin/SiteAdminWebhookUpdatePage.story.tsx b/client/web/src/site-admin/SiteAdminWebhookUpdatePage.story.tsx index b646c4c38eb..279c9cf9dae 100644 --- a/client/web/src/site-admin/SiteAdminWebhookUpdatePage.story.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookUpdatePage.story.tsx @@ -8,11 +8,11 @@ import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry' import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService' import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo' -import { EXTERNAL_SERVICES } from '../components/externalServices/backend' import { WebStory } from '../components/WebStory' +import { WebhookExternalServiceFields } from '../graphql-operations' -import { WEBHOOK_BY_ID } from './backend' -import { createExternalService, createWebhookMock } from './fixtures' +import { WEBHOOK_BY_ID, WEBHOOK_EXTERNAL_SERVICES } from './backend' +import { createWebhookMock } from './fixtures' import { SiteAdminWebhookUpdatePage } from './SiteAdminWebhookUpdatePage' const decorator: Decorator = Story => @@ -32,14 +32,14 @@ export const WebhookUpdatePage: StoryFn = () => ( new WildcardMockLink([ { request: { - query: getDocumentNode(EXTERNAL_SERVICES), - variables: { first: null, after: null, repo: null }, + query: getDocumentNode(WEBHOOK_EXTERNAL_SERVICES), + variables: {}, }, result: { data: { externalServices: { __typename: 'ExternalServiceConnection', - totalCount: 17, + totalCount: 6, pageInfo: { endCursor: null, hasNextPage: false, @@ -90,10 +90,12 @@ export const WebhookUpdatePage: StoryFn = () => ( +
+ +
} /> @@ -103,3 +105,13 @@ export const WebhookUpdatePage: StoryFn = () => ( ) WebhookUpdatePage.storyName = 'Update webhook' + +function createExternalService(kind: ExternalServiceKind, url: string): WebhookExternalServiceFields { + return { + __typename: 'ExternalService', + id: `service-${url}`, + kind, + displayName: `${kind}-123`, + url, + } +} diff --git a/client/web/src/site-admin/SiteAdminWebhookUpdatePage.tsx b/client/web/src/site-admin/SiteAdminWebhookUpdatePage.tsx index 62f8adcae4d..f8bb20c8122 100644 --- a/client/web/src/site-admin/SiteAdminWebhookUpdatePage.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookUpdatePage.tsx @@ -5,7 +5,7 @@ import { useParams } from 'react-router-dom' import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' -import { Container, PageHeader } from '@sourcegraph/wildcard' +import { PageHeader } from '@sourcegraph/wildcard' import { CreatedByAndUpdatedByInfoByline } from '../components/Byline/CreatedByAndUpdatedByInfoByline' import { ConnectionLoading } from '../components/FilteredConnection/ui' @@ -30,8 +30,9 @@ export const SiteAdminWebhookUpdatePage: FC = ( const { loading, data } = useWebhookQuery(id) const webhook = data?.node && data.node.__typename === 'Webhook' ? data.node : undefined + return ( - + <> {loading && !data && } {webhook && ( @@ -56,6 +57,6 @@ export const SiteAdminWebhookUpdatePage: FC = ( )} - + ) } diff --git a/client/web/src/site-admin/SiteAdminWebhooksPage.story.tsx b/client/web/src/site-admin/SiteAdminWebhooksPage.story.tsx index e1d08be72e3..e0bbce83a47 100644 --- a/client/web/src/site-admin/SiteAdminWebhooksPage.story.tsx +++ b/client/web/src/site-admin/SiteAdminWebhooksPage.story.tsx @@ -56,8 +56,8 @@ export const NoWebhooksFound: StoryFn = () => ( webhooks: { nodes: [], }, - webhookLogs: { - totalCount: 0, + errorsOnly: { + nodes: [], }, }, }, @@ -66,10 +66,12 @@ export const NoWebhooksFound: StoryFn = () => ( ]) } > - +
+ +
)} @@ -163,8 +165,8 @@ export const FiveWebhooksFound: StoryFn = () => ( }, ], }, - webhookLogs: { - totalCount: 5, + errorsOnly: { + nodes: [], }, }, }, @@ -173,10 +175,12 @@ export const FiveWebhooksFound: StoryFn = () => ( ]) } > - +
+ +
)} diff --git a/client/web/src/site-admin/SiteAdminWebhooksPage.tsx b/client/web/src/site-admin/SiteAdminWebhooksPage.tsx index b107caef886..f99fa595df3 100644 --- a/client/web/src/site-admin/SiteAdminWebhooksPage.tsx +++ b/client/web/src/site-admin/SiteAdminWebhooksPage.tsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react' import { mdiWebhook, mdiMapSearch, mdiPlus } from '@mdi/js' +import classNames from 'classnames' import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -34,7 +35,7 @@ export const SiteAdminWebhooksPage: React.FunctionComponent @@ -42,7 +43,7 @@ export const SiteAdminWebhooksPage: React.FunctionComponent {!headerTotals.loading && ( -
+
0 ? 'text-danger' : ''} @@ -74,13 +75,13 @@ export const SiteAdminWebhooksPage: React.FunctionComponent} {loading && !connection && } - {connection?.nodes?.map(node => ( + {connection?.nodes?.map((node, index) => ( ))} diff --git a/client/web/src/site-admin/SiteConfigurationChangeList.tsx b/client/web/src/site-admin/SiteConfigurationChangeList.tsx index 416b44fa616..6316511f7a7 100644 --- a/client/web/src/site-admin/SiteConfigurationChangeList.tsx +++ b/client/web/src/site-admin/SiteConfigurationChangeList.tsx @@ -39,6 +39,9 @@ export const SiteConfigurationChangeList: FC = () => { >({ query: SITE_CONFIGURATION_CHANGE_CONNECTION_QUERY, variables: {}, + options: { + fetchPolicy: 'network-only', + }, getConnection: ({ data }) => data?.site?.configuration?.history || undefined, }) diff --git a/client/web/src/site-admin/WebhookConfirmDeleteModal.tsx b/client/web/src/site-admin/WebhookConfirmDeleteModal.tsx new file mode 100644 index 00000000000..a4b7286c229 --- /dev/null +++ b/client/web/src/site-admin/WebhookConfirmDeleteModal.tsx @@ -0,0 +1,69 @@ +import React, { useCallback } from 'react' + +import { logger } from '@sourcegraph/common' +import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' +import { Button, H3, Modal, ErrorAlert } from '@sourcegraph/wildcard' + +import { LoaderButton } from '../components/LoaderButton' +import { WebhookFields } from '../graphql-operations' + +import { useDeleteWebhook } from './backend' + +export interface WebhookConfirmDeleteModalProps extends TelemetryV2Props { + webhook: WebhookFields + + onCancel: () => void + afterDelete: () => void +} + +export const WebhookConfirmDeleteModal: React.FunctionComponent< + React.PropsWithChildren +> = ({ webhook, onCancel, afterDelete, telemetryRecorder }) => { + const labelId = 'deleteWebhook' + + const [deleteWebhook, { loading, error }] = useDeleteWebhook() + + const onDelete = useCallback( + async event => { + event.preventDefault() + + try { + await deleteWebhook({ variables: { id: webhook.id } }) + + telemetryRecorder.recordEvent('webhook', 'delete') + afterDelete() + } catch (error) { + // Non-request error. API errors will be available under `error` above. + logger.error(error) + telemetryRecorder.recordEvent('webhook', 'deleteFail') + } + }, + [deleteWebhook, webhook.id, telemetryRecorder, afterDelete] + ) + + return ( + +

Delete webhook {webhook.name}?

+ + + Removing webhooks is irreversible and all incoming webhooks will be rejected. + + + {error && } + +
+ + +
+
+ ) +} diff --git a/client/web/src/site-admin/WebhookCreateUpdatePage.module.scss b/client/web/src/site-admin/WebhookCreateUpdatePage.module.scss index af2427ed8b9..094922903ee 100644 --- a/client/web/src/site-admin/WebhookCreateUpdatePage.module.scss +++ b/client/web/src/site-admin/WebhookCreateUpdatePage.module.scss @@ -1,16 +1,3 @@ -.grid { - display: grid; - grid-template-columns: max-content max-content; - column-gap: 1rem; - row-gap: 0.5rem; -} - -.first { - grid-column: 1 / 2; - min-width: 16rem; -} - -.second { - grid-column: 2 / 3; - min-width: 16rem; +.form { + max-width: 20rem; } diff --git a/client/web/src/site-admin/WebhookCreateUpdatePage.tsx b/client/web/src/site-admin/WebhookCreateUpdatePage.tsx index 5d63b53bfdb..2fee5de9514 100644 --- a/client/web/src/site-admin/WebhookCreateUpdatePage.tsx +++ b/client/web/src/site-admin/WebhookCreateUpdatePage.tsx @@ -1,29 +1,26 @@ import React, { type FC, useCallback, useMemo, useState } from 'react' -import classNames from 'classnames' -import { parse as parseJSONC } from 'jsonc-parser' import { noop } from 'lodash' import { useNavigate } from 'react-router-dom' import { useMutation, useQuery } from '@sourcegraph/http-client' -import { Alert, Button, ButtonLink, ErrorAlert, Form, H2, Input, Select } from '@sourcegraph/wildcard' +import { Alert, Button, ButtonLink, Container, ErrorAlert, Form, Input, Select } from '@sourcegraph/wildcard' -import { EXTERNAL_SERVICES } from '../components/externalServices/backend' import { defaultExternalServices } from '../components/externalServices/externalServices' import { ConnectionLoading } from '../components/FilteredConnection/ui' import { type CreateWebhookResult, type CreateWebhookVariables, ExternalServiceKind, - type ExternalServicesResult, - type ExternalServicesVariables, + type WebhookExternalServicesResult, + type WebhookExternalServicesVariables, type UpdateWebhookResult, type UpdateWebhookVariables, type WebhookFields, } from '../graphql-operations' import { generateSecret } from '../util/security' -import { CREATE_WEBHOOK_QUERY, UPDATE_WEBHOOK_QUERY } from './backend' +import { CREATE_WEBHOOK_QUERY, UPDATE_WEBHOOK_QUERY, WEBHOOK_EXTERNAL_SERVICES } from './backend' import styles from './WebhookCreateUpdatePage.module.scss' @@ -57,48 +54,47 @@ export const WebhookCreateUpdatePage: FC = ({ exis } const [webhook, setWebhook] = useState(initialWebhook) - const [kindsToUrls, setKindsToUrls] = useState>(new Map()) + const [kindsToUrls, setKindsToUrls] = useState>>(new Map()) - const { loading, data, error } = useQuery(EXTERNAL_SERVICES, { - variables: { - first: null, - after: null, - repo: null, - }, - }) + const { loading, data, error } = useQuery( + WEBHOOK_EXTERNAL_SERVICES, + {} + ) useMemo(() => { - if (data?.externalServices && data?.externalServices?.__typename === 'ExternalServiceConnection') { - const kindToUrlMap = new Map() + if (!data) { + return + } - for (const extSvc of data.externalServices.nodes) { - if (!supportedExternalServiceKind(extSvc.kind)) { - continue - } - const conf = parseJSONC(extSvc.config) - if (conf.url) { - kindToUrlMap.set(extSvc.kind, (kindToUrlMap.get(extSvc.kind) || []).concat([conf.url])) - } + const kindToUrlMap = new Map>() + + for (const extSvc of data.externalServices.nodes) { + if (!supportedExternalServiceKind(extSvc.kind)) { + continue } + if (!kindToUrlMap.has(extSvc.kind)) { + kindToUrlMap.set(extSvc.kind, new Set()) + } + kindToUrlMap.get(extSvc.kind)!.add(extSvc.url) + } - // If there are no external services, then the warning is shown and webhook creation is blocked. - // At this point we can only have external services with existing URL which existence is enforced - // by the code host configuration schema. - if (kindToUrlMap.size !== 0) { - setKindsToUrls(kindToUrlMap) + setKindsToUrls(kindToUrlMap) - // only fill the initial values for webhook creation - if (!update) { - const [currentKind] = kindToUrlMap.keys() - const [currentUrls] = kindToUrlMap.values() - // we always generate a secret once and assign it to the webhook. Bitbucket Cloud special case - // is handled is an Input and during GraphQL query creation. - setWebhook(webhook => ({ - ...webhook, - secret: generateSecret(), - codeHostURN: currentUrls[0], - codeHostKind: currentKind, - })) - } + // If there are no external services, then the warning is shown and webhook creation is blocked. + // At this point we can only have external services with existing URL which existence is enforced + // by the code host configuration schema. + if (kindToUrlMap.size !== 0) { + // only fill the initial values for webhook creation + if (!update) { + const [currentKind] = kindToUrlMap.keys() + const [currentUrls] = kindToUrlMap.values() + // we always generate a secret once and assign it to the webhook. Bitbucket Cloud special case + // is handled is an Input and during GraphQL query creation. + setWebhook(webhook => ({ + ...webhook, + secret: generateSecret(), + codeHostURN: Array.from(currentUrls)[0], + codeHostKind: currentKind, + })) } } }, [data, update]) @@ -109,12 +105,12 @@ export const WebhookCreateUpdatePage: FC = ({ exis const selectedUrns = kindsToUrls.get(selected) // This cannot happen, because the form is not rendered when there are no created external services // which support webhooks (and effectively have URLs in their code host configurations). - if (!selectedUrns) { + if (!selectedUrns || selectedUrns.size === 0) { throw new Error( `${defaultExternalServices[selected].title} code host connection has no URL. Please check related code host configuration.` ) } - const selectedUrn = selectedUrns[0] + const selectedUrn = Array.from(selectedUrns)[0] setWebhook(webhook => ({ ...webhook, codeHostKind: selected, @@ -141,147 +137,145 @@ export const WebhookCreateUpdatePage: FC = ({ exis UpdateWebhookResult, UpdateWebhookVariables >(UPDATE_WEBHOOK_QUERY, { - variables: buildUpdateWebhookVariables(webhook, existingWebhook?.id), onCompleted: data => navigate(`/site-admin/webhooks/incoming/${data.updateWebhook.id}`), }) + if (loading) { + return + } + + if (error) { + return + } + + if (!data) { + // Should not happen. + return null + } + + if (kindsToUrls.size === 0) { + return ( + + Please add a code host connection in order to create a webhook. + + ) + } + return ( - <> - {error && } - {loading && } - {!loading && - !error && - (kindsToUrls.size === 0 ? ( - - Please add a code host connection in order to create a webhook. - - ) : ( -
-

Information

-
{ - event.preventDefault() - createWebhook({ variables: convertWebhookToCreateWebhookVariables(webhook) }).catch( - // noop here is used because creation error is handled directly when useMutation is called - noop - ) - }} - > -
- Webhook name} - pattern="^[a-zA-Z0-9_'\-\/\.\s]+$" - required={true} - defaultValue={update ? webhook.name : ''} - onChange={event => { - onNameChange(event.target.value) - }} - maxLength={100} - /> - - - Code Host doesn't support secrets. - ) : ( - Randomly generated. Alter as required. - ) - } - label={Secret} - disabled={ - webhook.codeHostKind !== null && !codeHostSupportsSecrets(webhook.codeHostKind) - } - pattern="^[a-zA-Z0-9]+$" - onChange={event => { - onSecretChange(event.target.value) - }} - value={ - webhook.codeHostKind && !codeHostSupportsSecrets(webhook.codeHostKind) - ? '' - : webhook.secret || '' - } - maxLength={100} - /> -
- {update ? ( -
-
- -
-
- - Cancel - -
-
+ { + event.preventDefault() + if (update) { + updateWebhook({ + variables: buildUpdateWebhookVariables(webhook, existingWebhook?.id), + }).catch( + // noop here is used because update error is handled directly when useMutation is called + noop + ) + return + } + createWebhook({ variables: convertWebhookToCreateWebhookVariables(webhook) }).catch( + // noop here is used because creation error is handled directly when useMutation is called + noop + ) + }} + > + +
+ { + onNameChange(event.target.value) + }} + maxLength={100} + /> + + + Code Host doesn't support secrets. ) : ( - - )} - {(createWebhookError || updateWebhookError) && ( - - )} - -
- ))} - + <>Randomly generated. Alter as required. + ) + } + label="Secret" + disabled={webhook.codeHostKind !== null && !codeHostSupportsSecrets(webhook.codeHostKind)} + // TODO: Is this pattern too prohibitive? It doesn't even allow `-`. + pattern="^[a-zA-Z0-9]+$" + onChange={event => { + onSecretChange(event.target.value) + }} + value={ + webhook.codeHostKind && !codeHostSupportsSecrets(webhook.codeHostKind) + ? '' + : webhook.secret || '' + } + maxLength={100} + /> +
+ +
+ {update ? ( + <> + + + Cancel + + + ) : ( + + )} +
+ {(createWebhookError || updateWebhookError) && ( + + )} + ) } diff --git a/client/web/src/site-admin/WebhookInfoLogPageHeader.story.tsx b/client/web/src/site-admin/WebhookInfoLogPageHeader.story.tsx index 2ddef350d1b..5c99f0483c0 100644 --- a/client/web/src/site-admin/WebhookInfoLogPageHeader.story.tsx +++ b/client/web/src/site-admin/WebhookInfoLogPageHeader.story.tsx @@ -11,7 +11,7 @@ import { WebStory } from '../components/WebStory' import type { WebhookByIDLogPageHeaderResult } from '../graphql-operations' import { WebhookInfoLogPageHeader } from './WebhookInfoLogPageHeader' -import { type SelectedExternalService, WEBHOOK_BY_ID_LOG_PAGE_HEADER } from './webhooks/backend' +import { WEBHOOK_BY_ID_LOG_PAGE_HEADER } from './webhooks/backend' const decorator: Decorator = story => ( @@ -36,7 +36,6 @@ export default config // WebhookInfoLogPageHeader. const WebhookInfoLogPageHeaderContainer: React.FunctionComponent< React.PropsWithChildren<{ - initialExternalService?: SelectedExternalService initialOnlyErrors?: boolean }> > = ({ initialOnlyErrors }) => { diff --git a/client/web/src/site-admin/WebhookInformation.story.tsx b/client/web/src/site-admin/WebhookInformation.story.tsx index 1a9f338bcf2..ef3c308b4e2 100644 --- a/client/web/src/site-admin/WebhookInformation.story.tsx +++ b/client/web/src/site-admin/WebhookInformation.story.tsx @@ -29,17 +29,17 @@ function createWebhook(): WebhookFields { name: 'webhook with name', secret: 'secret-secret', updatedAt: formatRFC3339(addMinutes(TIMESTAMP_MOCK, 5)), - url: 'sg.com/.api/webhooks/1aa2b42c-a14c-4aaa-b756-70c82e94d3e7', + url: 'https://sg.com/.api/webhooks/1aa2b42c-a14c-4aaa-b756-70c82e94d3e7', uuid: '1aa2b42c-a14c-4aaa-b756-70c82e94d3e7', codeHostKind: ExternalServiceKind.GITHUB, - codeHostURN: 'github.com/repo1', + codeHostURN: 'https://github.com/', createdBy: { username: 'alice', url: 'users/alice', }, updatedBy: { - username: 'alice', - url: 'users/alice', + username: 'bob', + url: 'users/bob', }, } } diff --git a/client/web/src/site-admin/WebhookInformation.tsx b/client/web/src/site-admin/WebhookInformation.tsx index f8a5247d257..7a8928dfb39 100644 --- a/client/web/src/site-admin/WebhookInformation.tsx +++ b/client/web/src/site-admin/WebhookInformation.tsx @@ -2,7 +2,10 @@ import type { FC } from 'react' import classNames from 'classnames' +import { Icon } from '@sourcegraph/wildcard' + import { CopyableText } from '../components/CopyableText' +import { defaultExternalServices } from '../components/externalServices/externalServices' import type { WebhookFields } from '../graphql-operations' import styles from './WebhookInformation.module.scss' @@ -14,12 +17,19 @@ export interface WebhookInformationProps { export const WebhookInformation: FC = props => { const { webhook } = props + const IconComponent = defaultExternalServices[webhook.codeHostKind].icon + + const codeHostKindName = defaultExternalServices[webhook.codeHostKind].defaultDisplayName + return ( - + @@ -34,7 +44,13 @@ export const WebhookInformation: FC = props => { diff --git a/client/web/src/site-admin/WebhookNode.tsx b/client/web/src/site-admin/WebhookNode.tsx index 5b9d363a00f..c3ce72d1cb5 100644 --- a/client/web/src/site-admin/WebhookNode.tsx +++ b/client/web/src/site-admin/WebhookNode.tsx @@ -1,41 +1,65 @@ -import React from 'react' +import React, { useCallback } from 'react' -import { H3, Icon, Link, Text } from '@sourcegraph/wildcard' +import { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' +import { Button, ButtonLink, H3, Icon, Text } from '@sourcegraph/wildcard' import { defaultExternalServices } from '../components/externalServices/externalServices' -import type { ExternalServiceKind } from '../graphql-operations' +import type { WebhookFields } from '../graphql-operations' + +import { WebhookConfirmDeleteModal } from './WebhookConfirmDeleteModal' import styles from './WebhookNode.module.scss' -export interface WebhookProps { - id: string - name: string - codeHostKind: ExternalServiceKind - codeHostURN: string +export interface WebhookProps extends TelemetryV2Props { + webhook: WebhookFields + first: boolean + afterDelete: () => void } export const WebhookNode: React.FunctionComponent> = ({ - id, - name, - codeHostKind, - codeHostURN, + webhook, + first, + afterDelete, + telemetryRecorder, }) => { - const IconComponent = defaultExternalServices[codeHostKind].icon + const IconComponent = defaultExternalServices[webhook.codeHostKind].icon + const [showDeleteModal, setShowDeleteModal] = React.useState(false) + const deleteWebhook = useCallback(() => { + setShowDeleteModal(true) + }, []) return ( <> - -
-

- {' '} - {name} + {showDeleteModal && ( + setShowDeleteModal(false)} + afterDelete={afterDelete} + telemetryRecorder={telemetryRecorder} + /> + )} + {!first && } +
+
+

{webhook.name}

- - - {codeHostURN} - + + {webhook.codeHostURN} -

+
+
+ + Edit + + +
) diff --git a/client/web/src/site-admin/backend.ts b/client/web/src/site-admin/backend.ts index 63cf9050c81..a57c7fb8d24 100644 --- a/client/web/src/site-admin/backend.ts +++ b/client/web/src/site-admin/backend.ts @@ -1,10 +1,16 @@ -import type { QueryResult } from '@apollo/client' +import type { MutationTuple, QueryResult } from '@apollo/client' import { parse as parseJSONC } from 'jsonc-parser' import { lastValueFrom, type Observable } from 'rxjs' import { map, tap } from 'rxjs/operators' import { resetAllMemoizationCaches } from '@sourcegraph/common' -import { createInvalidGraphQLMutationResponseError, dataOrThrowErrors, gql, useQuery } from '@sourcegraph/http-client' +import { + createInvalidGraphQLMutationResponseError, + dataOrThrowErrors, + gql, + useMutation, + useQuery, +} from '@sourcegraph/http-client' import type { Settings } from '@sourcegraph/shared/src/settings/settings' import { mutateGraphQL, queryGraphQL, requestGraphQL } from '../backend/graphql' @@ -57,6 +63,8 @@ import type { WebhookPageHeaderVariables, WebhooksListResult, WebhooksListVariables, + DeleteWebhookResult, + DeleteWebhookVariables, } from '../graphql-operations' import { accessTokenFragment } from '../settings/tokens/AccessTokenNode' @@ -877,14 +885,6 @@ export const WEBHOOK_BY_ID = gql` } ` -export const DELETE_WEBHOOK = gql` - mutation DeleteWebhook($hookID: ID!) { - deleteWebhook(id: $hookID) { - alwaysNil - } - } -` - export const WEBHOOK_PAGE_HEADER = gql` query WebhookPageHeader { webhooks { @@ -1083,3 +1083,32 @@ export const useGitserversConnection = (): UseShowMorePaginationResult { + return useMutation(DELETE_WEBHOOK) +} diff --git a/client/web/src/site-admin/fixtures.ts b/client/web/src/site-admin/fixtures.ts index 6f5104bb094..3cd774ebd19 100644 --- a/client/web/src/site-admin/fixtures.ts +++ b/client/web/src/site-admin/fixtures.ts @@ -37,7 +37,6 @@ export function createExternalService(kind: ExternalServiceKind, url: string): L lastReplenishment: '2021-03-15T19:39:11Z', infinite: false, }, - webhookURL: null, hasConnectionCheck: true, unrestricted: false, syncJobs: { diff --git a/client/web/src/site-admin/webhooks/WebhookLogNode.story.tsx b/client/web/src/site-admin/webhooks/WebhookLogNode.story.tsx index 522b3a19b7e..fccf0dcdc4a 100644 --- a/client/web/src/site-admin/webhooks/WebhookLogNode.story.tsx +++ b/client/web/src/site-admin/webhooks/WebhookLogNode.story.tsx @@ -15,7 +15,7 @@ import { } from './story/fixtures' import { WebhookLogNode } from './WebhookLogNode' -import gridStyles from './WebhookLogPage.module.scss' +import gridStyles from '../SiteAdminWebhookPage.module.scss' const decorator: Decorator = story => ( diff --git a/client/web/src/site-admin/webhooks/WebhookLogPage.module.scss b/client/web/src/site-admin/webhooks/WebhookLogPage.module.scss deleted file mode 100644 index 2e39f0118f5..00000000000 --- a/client/web/src/site-admin/webhooks/WebhookLogPage.module.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import 'wildcard/src/global-styles/breakpoints'; - -.logs { - display: grid; - grid-template-columns: [caret] min-content [status] min-content [service] minmax(min-content, 1fr) [timestamp] min-content; - align-items: center; - column-gap: 2rem; - row-gap: 1rem; - - @media (--sm-breakpoint-down) { - grid-template-columns: 1fr; - } -} diff --git a/client/web/src/site-admin/webhooks/WebhookLogPage.story.tsx b/client/web/src/site-admin/webhooks/WebhookLogPage.story.tsx deleted file mode 100644 index 669a9276156..00000000000 --- a/client/web/src/site-admin/webhooks/WebhookLogPage.story.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import type { Decorator, Meta, StoryFn } from '@storybook/react' -import { addMinutes, formatRFC3339 } from 'date-fns' -import { of } from 'rxjs' - -import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo' - -import { WebStory } from '../../components/WebStory' -import type { WebhookLogFields, WebhookLogsVariables } from '../../graphql-operations' - -import type { queryWebhookLogs, SelectedExternalService } from './backend' -import { BODY_JSON, BODY_PLAIN, buildHeaderMock, HEADERS_JSON, HEADERS_PLAIN } from './story/fixtures' -import { WebhookLogPage } from './WebhookLogPage' - -const decorator: Decorator = story =>
{story()}
- -const config: Meta = { - title: 'web/site-admin/webhooks/WebhookLogPage', - parameters: { - chromatic: { - viewports: [320, 576, 978, 1440], - }, - }, - decorators: [decorator], - argTypes: { - externalServiceCount: { - name: 'external service count', - control: { type: 'number' }, - }, - erroredWebhookCount: { - name: 'errored webhook count', - control: { type: 'number' }, - }, - }, - args: { - externalServiceCount: 2, - erroredWebhookCount: 2, - }, -} - -export default config - -const buildQueryWebhookLogs: (logs: WebhookLogFields[]) => typeof queryWebhookLogs = - logs => - ( - { first, after }: Pick, - externalService: SelectedExternalService, - onlyErrors: boolean - ) => { - const filtered = logs.filter(log => { - if (onlyErrors && log.statusCode < 400) { - return false - } - - if (externalService === 'unmatched' && log.externalService) { - return false - } - if ( - externalService !== 'all' && - externalService !== 'unmatched' && - externalService !== log.externalService?.displayName - ) { - return false - } - - return true - }) - - first = first ?? 20 - const afterNumber = after?.length ? +after : 0 - const page = filtered.slice(afterNumber, afterNumber + first) - const cursor = afterNumber + first - - return of({ - nodes: page, - pageInfo: { - hasNextPage: logs.length > cursor, - endCursor: cursor.toString(), - }, - totalCount: logs.length, - }) - } - -const buildWebhookLogs = (count: number, externalServiceCount: number): WebhookLogFields[] => { - const logs: WebhookLogFields[] = [] - const time = new Date(2021, 10, 8, 16, 40, 30) - - for (let index = 0; index < count; index++) { - const externalServiceID = index % (externalServiceCount + 1) - const statusCode = - index % 3 === 0 - ? 200 + Math.floor(index / 3) - : index % 3 === 1 - ? 400 + Math.floor(index / 3) - : 500 + Math.floor(index / 3) - - logs.push({ - id: index.toString(), - receivedAt: formatRFC3339(addMinutes(time, index)), - externalService: - externalServiceID === externalServiceCount - ? null - : { - displayName: `External service ${externalServiceID}`, - }, - statusCode, - request: { - headers: HEADERS_JSON, - body: BODY_JSON, - method: 'POST', - url: '/my/url', - version: 'HTTP/1.1', - }, - response: { - headers: HEADERS_PLAIN, - body: BODY_PLAIN, - }, - }) - } - - return logs -} - -export const NoLogs: StoryFn = args => ( - - {props => ( - - - - )} - -) - -NoLogs.storyName = 'no logs' - -export const OnePageOfLogs: StoryFn = args => ( - - {props => ( - - - - )} - -) - -OnePageOfLogs.storyName = 'one page of logs' - -export const TwoPagesOfLogs: StoryFn = args => ( - - {props => ( - - - - )} - -) - -TwoPagesOfLogs.storyName = 'two pages of logs' diff --git a/client/web/src/site-admin/webhooks/WebhookLogPage.tsx b/client/web/src/site-admin/webhooks/WebhookLogPage.tsx deleted file mode 100644 index 7b2917beacb..00000000000 --- a/client/web/src/site-admin/webhooks/WebhookLogPage.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { useCallback, useState } from 'react' - -import classNames from 'classnames' - -import { Alert, Container, PageHeader, H5, Link } from '@sourcegraph/wildcard' - -import { FilteredConnection, type FilteredConnectionQueryArguments } from '../../components/FilteredConnection' -import { PageTitle } from '../../components/PageTitle' - -import { queryWebhookLogs as _queryWebhookLogs, type SelectedExternalService } from './backend' -import { WebhookLogNode } from './WebhookLogNode' -import { WebhookLogPageHeader } from './WebhookLogPageHeader' - -import styles from './WebhookLogPage.module.scss' - -export interface Props { - queryWebhookLogs?: typeof _queryWebhookLogs - webhookID?: string -} - -export const WebhookLogPage: React.FunctionComponent> = ({ - queryWebhookLogs = _queryWebhookLogs, - webhookID, -}) => { - const [onlyErrors, setOnlyErrors] = useState(false) - const [externalService, setExternalService] = useState('all') - - const query = useCallback( - ({ first, after }: FilteredConnectionQueryArguments) => - queryWebhookLogs( - { - first: first ?? null, - after: after ?? null, - }, - externalService, - onlyErrors, - webhookID - ), - [externalService, onlyErrors, queryWebhookLogs, webhookID] - ) - - return ( - <> - - - - This webhooks page has been deprecated, please see our{' '} - new webhooks page. - - - - No webhook logs found} - /> - - - ) -} - -const Header: React.FunctionComponent> = () => ( - <> - -
Status code
-
External service
-
Received at
- -) diff --git a/client/web/src/site-admin/webhooks/WebhookLogPageHeader.module.scss b/client/web/src/site-admin/webhooks/WebhookLogPageHeader.module.scss index a99dad336d8..fb09509117a 100644 --- a/client/web/src/site-admin/webhooks/WebhookLogPageHeader.module.scss +++ b/client/web/src/site-admin/webhooks/WebhookLogPageHeader.module.scss @@ -41,22 +41,6 @@ } } -.select-service { - > div { - // The .form-group container on onSelect(value)} - value={externalService} - > - - - {data?.externalServices.nodes.map(({ displayName, id }) => ( - - ))} - -
-{#if (alwaysVisible || visible) && target && tooltip} -
+ data-tooltip-root>
{#if (alwaysVisible || visible) && target && tooltip}
{tooltip}
diff --git a/client/web-sveltekit/src/lib/dom.ts b/client/web-sveltekit/src/lib/dom.ts index 184eba517d6..ed63f87eb40 100644 --- a/client/web-sveltekit/src/lib/dom.ts +++ b/client/web-sveltekit/src/lib/dom.ts @@ -341,58 +341,58 @@ export const portal: Action boolean; shrink: () => boolean }> = ( target, { grow, shrink } ) => { - async function resize(): Promise { - if (target.scrollWidth > target.clientWidth) { - // Shrink until we fit - while (target.scrollWidth > target.clientWidth) { - if (!shrink()) { - return - } - await tick() - } - } else { - // Grow until we overflow, then shrink once - while (target.scrollWidth <= target.clientWidth && grow()) { - await tick() - } - await tick() - if (target.scrollWidth > target.clientWidth) { - shrink() - await tick() - } - } - } - - // Resizing can (and probably will) trigger mutations, so do not trigger a - // new resize if there is a resize in progress. let resizing = false - function resizeOnce() { + async function resize(): Promise { if (resizing) { + // Growing and shrinking can cause child nodes to be added + // or removed, triggering resize observer during resizing. + // If we're already resizing, we can safely ignore those events. return } resizing = true - resize().then(() => { - resizing = false - }) + // Grow until we overflow + while (target.scrollWidth <= target.clientWidth && grow()) { + await tick() + } + await tick() + // Then shrink until we fit + while (target.scrollWidth > target.clientWidth && shrink()) { + await tick() + } + await tick() + resizing = false } - const resizeObserver = new ResizeObserver(resizeOnce) + const resizeObserver = new ResizeObserver(resize) resizeObserver.observe(target) - const mutationObserver = new MutationObserver(resizeOnce) - mutationObserver.observe(target, { attributes: true, childList: true, characterData: true, subtree: true }) + + function isElement(node: Node): node is Element { + return node.nodeType === Node.ELEMENT_NODE + } + + // If any children change size, that could trigger an overflow, so check the size again + target.childNodes.forEach(child => isElement(child) && resizeObserver.observe(child)) + const mutationObserver = new MutationObserver(mutationList => { + for (const mutation of mutationList) { + mutation.addedNodes.forEach(node => isElement(node) && resizeObserver.observe(node)) + mutation.removedNodes.forEach(node => isElement(node) && resizeObserver.unobserve(node)) + } + }) + mutationObserver.observe(target, { childList: true }) + return { update(params) { grow = params.grow shrink = params.shrink - resizeOnce() + resize() }, destroy() { resizeObserver.disconnect() diff --git a/client/web-sveltekit/src/lib/path/DisplayPath.stories.svelte b/client/web-sveltekit/src/lib/path/DisplayPath.stories.svelte new file mode 100644 index 00000000000..463f2a57812 --- /dev/null +++ b/client/web-sveltekit/src/lib/path/DisplayPath.stories.svelte @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/client/web-sveltekit/src/lib/path/DisplayPath.svelte b/client/web-sveltekit/src/lib/path/DisplayPath.svelte new file mode 100644 index 00000000000..fb1c9fbb7de --- /dev/null +++ b/client/web-sveltekit/src/lib/path/DisplayPath.svelte @@ -0,0 +1,129 @@ + + + + + + {#each parts as { part, path }, index}{@const last = + index === parts.length - 1}{#if index > 0}/{/if}{#if pathHref}{part}{:else}{part}{/if}{#if last}{/if}{/each}{#if showCopyButton}{/if} + + + diff --git a/client/web-sveltekit/src/lib/path/ShrinkablePath.stories.svelte b/client/web-sveltekit/src/lib/path/ShrinkablePath.stories.svelte new file mode 100644 index 00000000000..ef3527dfbfe --- /dev/null +++ b/client/web-sveltekit/src/lib/path/ShrinkablePath.stories.svelte @@ -0,0 +1,49 @@ + + + +
shrinkableDefault.grow(), shrink: () => shrinkableDefault.shrink() }}> + +
+
+ + +
shrinkableLinkified.grow(), shrink: () => shrinkableLinkified.shrink() }}> + + + +
+
+ + diff --git a/client/web-sveltekit/src/lib/path/ShrinkablePath.svelte b/client/web-sveltekit/src/lib/path/ShrinkablePath.svelte new file mode 100644 index 00000000000..2ec7f2a036e --- /dev/null +++ b/client/web-sveltekit/src/lib/path/ShrinkablePath.svelte @@ -0,0 +1,77 @@ + + + + part).join('/')} {showCopyButton} pathHref={scopedPathHref}> + + {#if collapsedParts.length > 0} + + + {#each collapsedParts as { part, path }} + + + {/each} + + / + {/if} + + + diff --git a/client/web-sveltekit/src/lib/path/index.ts b/client/web-sveltekit/src/lib/path/index.ts new file mode 100644 index 00000000000..bf3ce3dce55 --- /dev/null +++ b/client/web-sveltekit/src/lib/path/index.ts @@ -0,0 +1,28 @@ +import { resolveRoute } from '$app/paths' +import { encodeURIPathComponent } from '$lib/common' + +const TREE_ROUTE_ID = '/[...repo=reporev]/(validrev)/(code)/-/tree/[...path]' +const BLOB_ROUTE_ID = '/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]' + +export function pathHrefFactory({ + repoName, + revision, + fullPath, + fullPathType, +}: { + repoName: string + revision: string | undefined + fullPath: string + fullPathType: 'blob' | 'tree' +}): (targetPath: string) => string { + return (targetPath: string) => + resolveRoute( + // If we are targeting the last item in the path, respect the passed-in type. + // Otherwise, we know we are targeting a tree higher up in the path. + fullPath === targetPath && fullPathType === 'blob' ? BLOB_ROUTE_ID : TREE_ROUTE_ID, + { + repo: revision ? `${repoName}@${revision}` : repoName, + path: encodeURIPathComponent(targetPath), + } + ) +} diff --git a/client/web-sveltekit/src/lib/repo/FileHeader.svelte b/client/web-sveltekit/src/lib/repo/FileHeader.svelte index 7d9d5b35e3b..57444227f2a 100644 --- a/client/web-sveltekit/src/lib/repo/FileHeader.svelte +++ b/client/web-sveltekit/src/lib/repo/FileHeader.svelte @@ -1,105 +1,49 @@ -
-

- {#if collapsedBreadcrumbCount > 0} - - - - - {#each breadcrumbs.slice(0, collapsedBreadcrumbCount) as [name, path]} - - - {name} - - {/each} - - / - {/if} - {#each breadcrumbs.slice(collapsedBreadcrumbCount) as [name, path], index} - {@const last = index === breadcrumbs.length - collapsedBreadcrumbCount - 1} - - {#if index > 0} - / - {/if} - {#if last} - - {/if} - {#if path} - {name} - {:else} - {name} - {/if} - - {/each} - +
+

+ + +

@@ -118,10 +62,10 @@
{/if}

-
+ diff --git a/client/web-sveltekit/src/lib/wildcard/menu/MenuText.svelte b/client/web-sveltekit/src/lib/wildcard/menu/MenuText.svelte new file mode 100644 index 00000000000..c768964c269 --- /dev/null +++ b/client/web-sveltekit/src/lib/wildcard/menu/MenuText.svelte @@ -0,0 +1,26 @@ + + +
+ +
+ + + diff --git a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/DiffView.svelte b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/DiffView.svelte index 9575b6dc2c3..a614b08696c 100644 --- a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/DiffView.svelte +++ b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/DiffView.svelte @@ -19,7 +19,7 @@ - + {#if $commit.value?.blob} {/if} diff --git a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/FileView.svelte b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/FileView.svelte index c7563e53baf..5cf134ac7a8 100644 --- a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/FileView.svelte +++ b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/FileView.svelte @@ -22,6 +22,7 @@ import FileIcon from '$lib/repo/FileIcon.svelte' import { renderMermaid } from '$lib/repo/mermaid' import OpenInEditor from '$lib/repo/open-in-editor/OpenInEditor.svelte' + import OpenCodyAction from '$lib/repo/OpenCodyAction.svelte' import Permalink from '$lib/repo/Permalink.svelte' import { createCodeIntelAPI, replaceRevisionInURL } from '$lib/shared' import { isLightTheme, settings } from '$lib/stores' @@ -36,7 +37,6 @@ import { FileViewGitBlob, FileViewHighlightedFile } from './FileView.gql' import FileViewModeSwitcher from './FileViewModeSwitcher.svelte' import OpenInCodeHostAction from './OpenInCodeHostAction.svelte' - import OpenCodyAction from '$lib/repo/OpenCodyAction.svelte' import { CodeViewMode, toCodeViewMode } from './util' export let data: Extract @@ -166,14 +166,12 @@ {#if embedded} - - - - + + {:else} - + {#if !revisionOverride} {#await data.externalServiceType then externalServiceType} diff --git a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/page.spec.ts b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/page.spec.ts index 6affc477fb1..fe7b1e22958 100644 --- a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/page.spec.ts +++ b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/-/blob/[...path]/page.spec.ts @@ -245,14 +245,14 @@ test.describe('file header', () => { await expect(page.getByRole('link', { name: 'src' })).toBeVisible() }) - test('select and copy file path', async ({ page, context }) => { + test('textContent is exactly the path', async ({ page, context }) => { await context.grantPermissions(['clipboard-read', 'clipboard-write']) await page.goto(url) - await page.getByTestId('file-header-path').selectText() - await page.keyboard.press(`Meta+KeyC`) - await page.keyboard.press(`Control+KeyC`) - const clipboardText = await page.evaluate('navigator.clipboard.readText()') - expect(clipboardText, 'path should be copied to clipboard and not contain spaces').toBe('src/readme.md') + // We specifically check the textContent here because this is what is + // used to apply highlights. It must exactly equal the path (no additional + // whitespace) or the highlights will be incorrectly offset. + const pathContainer = page.locator('css=[data-path-container]').first() + await expect(pathContainer).toHaveText(/^src\/readme.md$/) }) test('copy path button', async ({ page, context }) => { diff --git a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/page.spec.ts b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/page.spec.ts index eaf33ee58dc..7bd3eade30c 100644 --- a/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/page.spec.ts +++ b/client/web-sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/page.spec.ts @@ -250,7 +250,7 @@ test('history panel', async ({ page, sg }) => { test('file popover', async ({ page, sg }, testInfo) => { // Test needs more time to teardown - test.setTimeout(testInfo.timeout * 3000) + test.setTimeout(testInfo.timeout * 4) await page.goto(`/${repoName}`) @@ -309,7 +309,7 @@ test('file popover', async ({ page, sg }, testInfo) => { await expect(page.getByText('Last Changed')).toBeVisible() // Click the parent dir in the popover and expect to navigate to that page - await page.locator('span').filter({ hasText: /^src$/ }).getByRole('link').click() + await page.locator('div').filter({ hasText: /^src$/ }).getByRole('link').click() await page.waitForURL(/src$/) }) diff --git a/client/web-sveltekit/src/routes/[...repo=reporev]/+layout.svelte b/client/web-sveltekit/src/routes/[...repo=reporev]/+layout.svelte index 97eebe4f63d..cbaf572d9f3 100644 --- a/client/web-sveltekit/src/routes/[...repo=reporev]/+layout.svelte +++ b/client/web-sveltekit/src/routes/[...repo=reporev]/+layout.svelte @@ -87,6 +87,21 @@ entry => entry.visibility === 'user' || (entry.visibility === 'admin' && data.user?.siteAdmin) ) $: visibleNavEntryCount = viewableNavEntries.length + function grow(): boolean { + if (visibleNavEntryCount < viewableNavEntries.length) { + visibleNavEntryCount++ + return true + } + return false + } + function shrink(): boolean { + if (visibleNavEntryCount > 0) { + visibleNavEntryCount-- + return true + } + return false + } + $: navEntriesToShow = viewableNavEntries.slice(0, visibleNavEntryCount) $: overflowNavEntries = viewableNavEntries.slice(visibleNavEntryCount) $: allMenuEntries = [...overflowNavEntries, ...menuEntries] @@ -138,19 +153,7 @@ -
Code host{webhook.codeHostKind} + + {codeHostKindName} +
URN
Secret - + {webhook.secret === null ? ( + + No secret + + ) : ( + + )}