From a66f84c8a75c67615b6e0956f190086e35749e15 Mon Sep 17 00:00:00 2001 From: Bolaji Olajide <25608335+BolajiOlajide@users.noreply.github.com> Date: Tue, 17 Jan 2023 20:40:00 +0100 Subject: [PATCH 001/678] update maxVersionString constant for migration (#46608) --- .../shared/data/cmd/generator/consts.go | 2 +- .../shared/data/stitched-migration-graph.json | 309 ++++++++++++++++++ 2 files changed, 310 insertions(+), 1 deletion(-) diff --git a/internal/database/migration/shared/data/cmd/generator/consts.go b/internal/database/migration/shared/data/cmd/generator/consts.go index cbefd8a83558..5b439495a91b 100644 --- a/internal/database/migration/shared/data/cmd/generator/consts.go +++ b/internal/database/migration/shared/data/cmd/generator/consts.go @@ -8,7 +8,7 @@ import ( // NOTE: This should be kept up-to-date with cmd/migrator/build.sh so that we "bake in" // fallback schemas everything we support migrating to. -const maxVersionString = "4.3.0" +const maxVersionString = "4.4.0" // MaxVersion is the highest known released version at the time the migrator was built. var MaxVersion = func() oobmigration.Version { diff --git a/internal/database/migration/shared/data/stitched-migration-graph.json b/internal/database/migration/shared/data/stitched-migration-graph.json index 11fcd35d9054..8bcb7368975d 100755 --- a/internal/database/migration/shared/data/stitched-migration-graph.json +++ b/internal/database/migration/shared/data/stitched-migration-graph.json @@ -587,6 +587,59 @@ ], "IsCreateIndexConcurrently": false, "IndexMetadata": null + }, + { + "ID": 1670253074, + "Name": "insight series repo criteria", + "UpQuery": "ALTER TABLE IF EXISTS insight_series\n\tADD COLUMN IF NOT EXISTS repository_criteria text;\n\nCOMMENT ON COLUMN insight_series.repository_criteria IS 'The search criteria used to determine the repositories that are included in this series.';", + "DownQuery": "ALTER TABLE IF EXISTS insight_series DROP COLUMN IF EXISTS repository_criteria;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1666729025, + 1667309737 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1672740238, + "Name": "add_insights_data_pruning_jobs", + "UpQuery": "CREATE TABLE IF NOT EXISTS insights_data_retention_jobs (\n id SERIAL PRIMARY KEY,\n state text DEFAULT 'queued',\n failure_message text,\n queued_at timestamp with time zone DEFAULT NOW(),\n started_at timestamp with time zone,\n finished_at timestamp with time zone,\n process_after timestamp with time zone,\n num_resets integer not null default 0,\n num_failures integer not null default 0,\n last_heartbeat_at timestamp with time zone,\n execution_logs json[],\n worker_hostname text not null default '',\n cancel boolean not null default false,\n\n series_id int not null\n);", + "DownQuery": "DROP TABLE IF EXISTS insights_data_retention_jobs;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1670253074 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1672917501, + "Name": "add_retention_tables", + "UpQuery": "CREATE TABLE IF NOT EXISTS archived_series_points (\n series_id text NOT NULL,\n \"time\" timestamp with time zone NOT NULL,\n value double precision NOT NULL,\n repo_id integer,\n repo_name_id integer,\n original_repo_name_id integer,\n capture text,\n CONSTRAINT check_repo_fields_specifity CHECK ((((repo_id IS NULL) AND (repo_name_id IS NULL) AND (original_repo_name_id IS NULL)) OR ((repo_id IS NOT NULL) AND (repo_name_id IS NOT NULL) AND (original_repo_name_id IS NOT NULL)))),\n CONSTRAINT insight_series_series_id_fkey FOREIGN KEY (series_id) REFERENCES insight_series (series_id) ON DELETE CASCADE \n); -- any new column added to series_points should be added here too. we add a foreign key constraint for deletion.\n\nCREATE TABLE IF NOT EXISTS archived_insight_series_recording_times (\n insight_series_id integer not null,\n recording_time timestamp with time zone not null,\n snapshot boolean not null,\n UNIQUE (insight_series_id, recording_time),\n CONSTRAINT insight_series_id_fkey FOREIGN KEY (insight_series_id) REFERENCES insight_series (id) ON DELETE CASCADE\n); -- this structure should be kept the same as insight_series_recording_times.", + "DownQuery": "DROP TABLE IF EXISTS archived_series_points;\nDROP TABLE IF EXISTS archived_insight_series_recording_times;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1672740238 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1672921606, + "Name": "data_retention_jobs_series_metadata", + "UpQuery": "ALTER TABLE IF EXISTS insights_data_retention_jobs\nADD COLUMN IF NOT EXISTS series_id_string text not null default '';", + "DownQuery": "ALTER TABLE IF EXISTS insight_data_retention_jobs\nDROP COLUMN IF EXISTS series_id_string;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1672917501 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null } ], "BoundsByRev": { @@ -779,6 +832,13 @@ 1667309737 ], "PreCreation": false + }, + "v4.4.0": { + "RootID": 1000000027, + "LeafIDs": [ + 1672921606 + ], + "PreCreation": false } } }, @@ -1375,6 +1435,59 @@ ], "IsCreateIndexConcurrently": false, "IndexMetadata": null + }, + { + "ID": 1670881409, + "Name": "Fix SCIP document schema counting (again)", + "UpQuery": "DROP TRIGGER IF EXISTS codeintel_scip_documents_schema_versions_insert ON codeintel_scip_documents;\nDROP TRIGGER IF EXISTS codeintel_scip_documents_schema_versions_insert ON codeintel_scip_document_lookup;\nDROP TRIGGER IF EXISTS codeintel_scip_documents_schema_versions_insert ON codeintel_scip_documents_schema_versions;\nDROP FUNCTION update_codeintel_scip_documents_schema_versions_insert;\n\nDROP TABLE IF EXISTS codeintel_scip_documents_schema_versions;\nALTER TABLE codeintel_scip_documents DROP COLUMN IF EXISTS metadata_shard_id;\n\nCREATE TABLE codeintel_scip_documents_schema_versions (\n upload_id integer NOT NULL,\n min_schema_version integer,\n max_schema_version integer,\n PRIMARY KEY(upload_id)\n);\n\nCOMMENT ON TABLE codeintel_scip_documents_schema_versions IS 'Tracks the range of `schema_versions` values associated with each document referenced from the [`codeintel_scip_document_lookup`](#table-publiccodeintel_scip_document_lookup) table.';\nCOMMENT ON COLUMN codeintel_scip_documents_schema_versions.upload_id IS 'The identifier of the associated SCIP index.';\nCOMMENT ON COLUMN codeintel_scip_documents_schema_versions.min_schema_version IS 'A lower-bound on the `schema_version` values of the document records referenced from the table [`codeintel_scip_document_lookup`](#table-publiccodeintel_scip_document_lookup) where the `upload_id` column matches the associated SCIP index.';\nCOMMENT ON COLUMN codeintel_scip_documents_schema_versions.max_schema_version IS 'An upper-bound on the `schema_version` values of the document records referenced from the table [`codeintel_scip_document_lookup`](#table-publiccodeintel_scip_document_lookup) where the `upload_id` column matches the associated SCIP index.';\n\nCREATE OR REPLACE FUNCTION update_codeintel_scip_documents_schema_versions_insert() RETURNS trigger\n LANGUAGE plpgsql\n AS $$ BEGIN\n INSERT INTO codeintel_scip_documents_schema_versions\n SELECT\n newtab.upload_id,\n MIN(codeintel_scip_documents.schema_version) as min_schema_version,\n MAX(codeintel_scip_documents.schema_version) as max_schema_version\n FROM newtab\n JOIN codeintel_scip_documents ON codeintel_scip_documents.id = newtab.document_id\n GROUP BY newtab.upload_id\n ON CONFLICT (upload_id) DO UPDATE SET\n -- Update with min(old_min, new_min) and max(old_max, new_max)\n min_schema_version = LEAST(codeintel_scip_documents_schema_versions.min_schema_version, EXCLUDED.min_schema_version),\n max_schema_version = GREATEST(codeintel_scip_documents_schema_versions.max_schema_version, EXCLUDED.max_schema_version);\n RETURN NULL;\nEND $$;\n\nCREATE TRIGGER codeintel_scip_documents_schema_versions_insert AFTER INSERT ON codeintel_scip_document_lookup\nREFERENCING NEW TABLE AS newtab FOR EACH STATEMENT EXECUTE FUNCTION update_codeintel_scip_documents_schema_versions_insert();", + "DownQuery": "-- Add shard id to documents\nALTER TABLE codeintel_scip_documents ADD COLUMN IF NOT EXISTS metadata_shard_id integer NOT NULL DEFAULT floor(random() * 128 + 1)::integer;\nCOMMENT ON COLUMN codeintel_scip_documents.metadata_shard_id IS 'A randomly generated integer used to arbitrarily bucket groups of documents for things like expiration checks and data migrations.';\n\n-- Replace table and triggers\nDROP TABLE IF EXISTS codeintel_scip_documents_schema_versions;\nCREATE TABLE codeintel_scip_documents_schema_versions (\n metadata_shard_id integer NOT NULL,\n min_schema_version integer,\n max_schema_version integer,\n PRIMARY KEY(metadata_shard_id)\n);\n\nCOMMENT ON TABLE codeintel_scip_documents_schema_versions IS 'Tracks the range of `schema_versions` values associated with each document metadata shard in the [`codeintel_scip_documents`](#table-publiccodeintel_scip_documents) table.';\nCOMMENT ON COLUMN codeintel_scip_documents_schema_versions.metadata_shard_id IS 'The identifier of the associated document metadata shard.';\nCOMMENT ON COLUMN codeintel_scip_documents_schema_versions.min_schema_version IS 'A lower-bound on the `schema_version` values of the records in the table [`codeintel_scip_documents`](#table-publiccodeintel_scip_documents) where the `metadata_shard_id` column matches the associated document metadata shard.';\nCOMMENT ON COLUMN codeintel_scip_documents_schema_versions.max_schema_version IS 'An upper-bound on the `schema_version` values of the records in the table [`codeintel_scip_documents`](#table-publiccodeintel_scip_documents) where the `metadata_shard_id` column matches the associated document metadata shard.';\n\nCREATE OR REPLACE FUNCTION update_codeintel_scip_documents_schema_versions_insert() RETURNS trigger\n LANGUAGE plpgsql\n AS $$ BEGIN\n INSERT INTO codeintel_scip_documents_schema_versions\n SELECT\n newtab.metadata_shard_id,\n MIN(codeintel_scip_documents.schema_version) as min_schema_version,\n MAX(codeintel_scip_documents.schema_version) as max_schema_version\n FROM newtab\n JOIN codeintel_scip_documents ON codeintel_scip_documents.metadata_shard_id = newtab.metadata_shard_id\n GROUP BY newtab.metadata_shard_id\n ON CONFLICT (metadata_shard_id) DO UPDATE SET\n -- Update with min(old_min, new_min) and max(old_max, new_max)\n min_schema_version = LEAST(codeintel_scip_documents_schema_versions.min_schema_version, EXCLUDED.min_schema_version),\n max_schema_version = GREATEST(codeintel_scip_documents_schema_versions.max_schema_version, EXCLUDED.max_schema_version);\n RETURN NULL;\nEND $$;\n\nDROP TRIGGER IF EXISTS codeintel_scip_documents_schema_versions_insert ON codeintel_scip_documents;\nDROP TRIGGER IF EXISTS codeintel_scip_documents_schema_versions_insert ON codeintel_scip_document_lookup;\nDROP TRIGGER IF EXISTS codeintel_scip_documents_schema_versions_insert ON codeintel_scip_documents_schema_versions;\nCREATE TRIGGER codeintel_scip_documents_schema_versions_insert AFTER INSERT ON codeintel_scip_documents\nREFERENCING NEW TABLE AS newtab FOR EACH STATEMENT EXECUTE FUNCTION update_codeintel_scip_documents_schema_versions_insert();", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1670370058 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1670940342, + "Name": "Add codeintel_scip_symbol_names table", + "UpQuery": "CREATE TABLE IF NOT EXISTS codeintel_scip_symbol_names (\n id integer NOT NULL,\n upload_id integer NOT NULL,\n name_segment text NOT NULL,\n prefix_id integer,\n PRIMARY KEY (upload_id, id)\n);\n\nCOMMENT ON TABLE codeintel_scip_symbol_names IS 'Stores a prefix tree of symbol names within a particular upload.';\nCOMMENT ON COLUMN codeintel_scip_symbol_names.id IS 'An identifier unique within the index for this symbol name segment.';\nCOMMENT ON COLUMN codeintel_scip_symbol_names.upload_id IS 'The identifier of the upload that provided this SCIP index.';\nCOMMENT ON COLUMN codeintel_scip_symbol_names.name_segment IS 'The portion of the symbol name that is unique to this symbol and its children.';\nCOMMENT ON COLUMN codeintel_scip_symbol_names.prefix_id IS 'The identifier of the segment that forms the prefix of this symbol, if any.';\n\nALTER TABLE codeintel_scip_symbols ADD COLUMN IF NOT EXISTS symbol_id integer NOT NULL;\nCOMMENT ON COLUMN codeintel_scip_symbols.symbol_id IS 'The identifier of the segment that terminates the name of this symbol. See the table [`codeintel_scip_symbol_names`](#table-publiccodeintel_scip_symbol_names) on how to reconstruct the full symbol name.';\nALTER TABLE codeintel_scip_symbols DROP CONSTRAINT IF EXISTS codeintel_scip_symbols_pkey;\nALTER TABLE codeintel_scip_symbols ADD PRIMARY KEY (upload_id, symbol_id, document_lookup_id);\nALTER TABLE codeintel_scip_symbols DROP COLUMN IF EXISTS symbol_name;", + "DownQuery": "ALTER TABLE codeintel_scip_symbols ADD COLUMN IF NOT EXISTS symbol_name text NOT NULL;\nALTER TABLE codeintel_scip_symbols DROP CONSTRAINT IF EXISTS codeintel_scip_symbols_pkey;\nALTER TABLE codeintel_scip_symbols ADD PRIMARY KEY (upload_id, symbol_name, document_lookup_id);\nALTER TABLE codeintel_scip_symbols DROP COLUMN IF EXISTS symbol_id;\n\nDROP TABLE IF EXISTS codeintel_scip_symbol_names;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1670370058 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1670967960, + "Name": "Add codeintel_scip_symbol_names indexes", + "UpQuery": "CREATE INDEX IF NOT EXISTS codeintel_scip_symbol_names_upload_id_roots ON codeintel_scip_symbol_names(upload_id) WHERE prefix_id IS NULL;\nCREATE INDEX IF NOT EXISTS codeisdntel_scip_symbol_names_upload_id_children ON codeintel_scip_symbol_names(upload_id, prefix_id) WHERE prefix_id IS NOT NULL;", + "DownQuery": "DROP INDEX IF EXISTS codeintel_scip_symbol_names_upload_id_roots;\nDROP INDEX IF EXISTS codeisdntel_scip_symbol_names_upload_id_children;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1670881409, + 1670940342 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1671059396, + "Name": "Remove duplicate trigger", + "UpQuery": "DROP TRIGGER IF EXISTS codeintel_scip_documents_schema_versions_insert ON codeintel_scip_document_lookup;\nDROP FUNCTION IF EXISTS update_codeintel_scip_documents_schema_versions_insert;", + "DownQuery": "CREATE OR REPLACE FUNCTION update_codeintel_scip_documents_schema_versions_insert() RETURNS trigger\n LANGUAGE plpgsql\n AS $$ BEGIN\n INSERT INTO codeintel_scip_documents_schema_versions\n SELECT\n newtab.upload_id,\n MIN(codeintel_scip_documents.schema_version) as min_schema_version,\n MAX(codeintel_scip_documents.schema_version) as max_schema_version\n FROM newtab\n JOIN codeintel_scip_documents ON codeintel_scip_documents.id = newtab.document_id\n GROUP BY newtab.upload_id\n ON CONFLICT (upload_id) DO UPDATE SET\n -- Update with min(old_min, new_min) and max(old_max, new_max)\n min_schema_version = LEAST(codeintel_scip_documents_schema_versions.min_schema_version, EXCLUDED.min_schema_version),\n max_schema_version = GREATEST(codeintel_scip_documents_schema_versions.max_schema_version, EXCLUDED.max_schema_version);\n RETURN NULL;\nEND $$;\n\nDROP TRIGGER IF EXISTS codeintel_scip_documents_schema_versions_insert ON codeintel_scip_document_lookup;\nCREATE TRIGGER codeintel_scip_documents_schema_versions_insert AFTER INSERT ON codeintel_scip_document_lookup \nREFERENCING NEW TABLE AS newtab FOR EACH STATEMENT EXECUTE FUNCTION update_codeintel_scip_documents_schema_versions_insert();", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1670967960 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null } ], "BoundsByRev": { @@ -1569,6 +1682,13 @@ 1670370058 ], "PreCreation": false + }, + "v4.4.0": { + "RootID": 1000000033, + "LeafIDs": [ + 1671059396 + ], + "PreCreation": false } } }, @@ -7699,6 +7819,45 @@ "IsCreateIndexConcurrently": false, "IndexMetadata": null }, + { + "ID": 1669297489, + "Name": "add_permission_sync_jobs", + "UpQuery": "CREATE TABLE IF NOT EXISTS permission_sync_jobs (\n id SERIAL PRIMARY KEY,\n state text DEFAULT 'queued',\n failure_message text,\n queued_at timestamp with time zone DEFAULT NOW(),\n started_at timestamp with time zone,\n finished_at timestamp with time zone,\n process_after timestamp with time zone,\n num_resets integer not null default 0,\n num_failures integer not null default 0,\n last_heartbeat_at timestamp with time zone,\n execution_logs json[],\n worker_hostname text not null default '',\n cancel boolean not null default false,\n\n repository_id integer,\n user_id integer,\n\n high_priority boolean not null default false,\n invalidate_caches boolean not null default false\n);\n\nCREATE INDEX IF NOT EXISTS permission_sync_jobs_state ON permission_sync_jobs (state);\nCREATE INDEX IF NOT EXISTS permission_sync_jobs_process_after ON permission_sync_jobs (process_after);\nCREATE INDEX IF NOT EXISTS permission_sync_jobs_repository_id ON permission_sync_jobs (repository_id);\nCREATE INDEX IF NOT EXISTS permission_sync_jobs_user_id ON permission_sync_jobs (user_id);", + "DownQuery": "DROP TABLE IF EXISTS permission_sync_jobs;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1669184869 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1669576792, + "Name": "Make batch spec of batch change nullable", + "UpQuery": "ALTER TABLE batch_changes ALTER COLUMN batch_spec_id DROP NOT NULL;\n\nCREATE TEMPORARY TABLE minimal_batch_specs (id bigint);\n\nINSERT INTO minimal_batch_specs\nSELECT batch_spec_id FROM batch_changes WHERE batch_spec_id IS NOT NULL AND last_applied_at IS NULL;\n\nUPDATE batch_changes SET batch_spec_id = NULL WHERE batch_spec_id IN (SELECT id FROM minimal_batch_specs);\n\n-- Delete existing empty batch specs.\nDELETE FROM batch_specs WHERE id IN (SELECT id FROM minimal_batch_specs);-- Delete existing empty batch specs.\n\nDROP TABLE minimal_batch_specs;", + "DownQuery": "WITH reconstructed_batch_specs AS (\n INSERT INTO batch_specs\n (batch_change_id, user_id, namespace_user_id, namespace_org_id, rand_id, raw_spec, spec, created_from_raw)\n SELECT\n id, creator_id, namespace_user_id, namespace_org_id, md5(CONCAT(id, name)::bytea), CONCAT('name: ', name), json_build_object('name', name), TRUE\n FROM\n batch_changes\n WHERE\n batch_spec_id IS NULL\n RETURNING\n\t batch_change_id, id\n)\nUPDATE\n batch_changes\nSET batch_spec_id = (SELECT id FROM reconstructed_batch_specs WHERE batch_change_id = batch_changes.id)\nWHERE id IN (SELECT batch_change_id FROM reconstructed_batch_specs);\n\nALTER TABLE batch_changes ALTER COLUMN batch_spec_id SET NOT NULL;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1669184869 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1669645608, + "Name": "Make batch change name pattern", + "UpQuery": "ALTER TABLE batch_changes DROP CONSTRAINT IF EXISTS batch_change_name_is_valid;\nALTER TABLE batch_changes ADD CONSTRAINT batch_change_name_is_valid CHECK (name ~ '^[\\w.-]+$');", + "DownQuery": "ALTER TABLE batch_changes DROP CONSTRAINT IF EXISTS batch_change_name_is_valid;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1669576792 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, { "ID": 1669836151, "Name": "Add manager to lsif_package/references", @@ -7805,6 +7964,149 @@ ], "IsCreateIndexConcurrently": false, "IndexMetadata": null + }, + { + "ID": 1670870072, + "Name": "add_read_only_column_roles", + "UpQuery": "ALTER TABLE roles\n ADD COLUMN IF NOT EXISTS readonly BOOLEAN DEFAULT FALSE;\n\nCOMMENT ON COLUMN roles.readonly IS 'This is used to indicate whether a role is read-only or can be modified.';\n\nUPDATE roles SET readonly = FALSE;\n\nALTER TABLE roles\n ALTER COLUMN readonly SET NOT NULL;", + "DownQuery": "ALTER TABLE roles\n DROP COLUMN IF EXISTS readonly;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1670350006, + 1670543231 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1670934184, + "Name": "add_gitserver_corruption_columns", + "UpQuery": "ALTER TABLE gitserver_repos\n ADD COLUMN IF NOT EXISTS corrupted_at TIMESTAMP WITH TIME ZONE,\n ADD COLUMN IF NOT EXISTS corruption_logs JSONB NOT NULL DEFAULT '[]';\n\nCOMMENT ON COLUMN gitserver_repos.corrupted_at IS 'Timestamp of when repo corruption was detected';\nCOMMENT ON COLUMN gitserver_repos.corruption_logs IS 'Log output of repo corruptions that have been detected - encoded as json';", + "DownQuery": "ALTER TABLE gitserver_repos\n DROP COLUMN IF EXISTS corrupted_at,\n DROP COLUMN IF EXISTS corrupted_logs;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1670350006, + 1670543231 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1670600028, + "Name": "executor_secrets_accesslogs_codeintel_user", + "UpQuery": "ALTER TABLE executor_secret_access_logs\nADD COLUMN IF NOT EXISTS machine_user text NOT NULL DEFAULT '';\n\nALTER TABLE executor_secret_access_logs\nDROP CONSTRAINT IF EXISTS user_id_or_machine_user;\n\nALTER TABLE executor_secret_access_logs\nADD CONSTRAINT user_id_or_machine_user\nCHECK (\n (user_id IS NULL AND machine_user \u003c\u003e '') OR\n (user_id IS NOT NULL AND machine_user = '')\n);\n\nALTER TABLE executor_secret_access_logs\nALTER COLUMN user_id\nDROP NOT NULL;\n\nDO $$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE\n table_name = 'executor_secret_access_logs_machineuser_bak_1670600028' AND\n table_schema = current_schema()\n ) THEN\n DECLARE\n err_details text;\n BEGIN\n INSERT INTO executor_secret_access_logs\n SELECT * FROM executor_secret_access_logs_machineuser_bak_1670600028;\n EXCEPTION WHEN unique_violation THEN\n GET STACKED DIAGNOSTICS err_details = PG_EXCEPTION_DETAIL;\n RAISE unique_violation\n USING MESSAGE = SQLERRM,\n DETAIL = err_details,\n HINT = 'This up-migration attempts to re-insert executor secret access logs '\n 'from a backup table that would have lost information due to the associated '\n 'down-migration changing the table schema. In doing so, a unique violation exception '\n 'occurred and will have to be resolved manually. The backed up access logs are stored '\n 'in the executor_secret_access_logs_machineuser_bak_1670600028 table.';\n END;\n END IF;\nEND\n$$;\n\nDROP TABLE IF EXISTS executor_secret_access_logs_machineuser_bak_1670600028;\n\nALTER TABLE lsif_indexes\nADD COLUMN IF NOT EXISTS requested_envvars text[];\n\nDROP VIEW IF EXISTS lsif_indexes_with_repository_name;\n\nCREATE VIEW lsif_indexes_with_repository_name AS\n SELECT u.id,\n u.commit,\n u.queued_at,\n u.state,\n u.failure_message,\n u.started_at,\n u.finished_at,\n u.repository_id,\n u.process_after,\n u.num_resets,\n u.num_failures,\n u.docker_steps,\n u.root,\n u.indexer,\n u.indexer_args,\n u.outfile,\n u.log_contents,\n u.execution_logs,\n u.local_steps,\n u.should_reindex,\n u.requested_envvars,\n r.name AS repository_name\n FROM (lsif_indexes u\n JOIN repo r ON ((r.id = u.repository_id)))\n WHERE (r.deleted_at IS NULL);", + "DownQuery": "DO $$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE\n table_name = 'executor_secret_access_logs_machineuser_bak_1670600028' AND\n table_schema = current_schema()\n ) THEN\n -- create and copy\n CREATE TABLE executor_secret_access_logs_machineuser_bak_1670600028 AS\n SELECT * FROM executor_secret_access_logs\n -- these two should match the same rows, but just in case\n WHERE machine_user \u003c\u003e '' OR user_id IS NULL;\n ELSEIF EXISTS (\n -- must check for double-down idempotency test\n SELECT 1 FROM information_schema.columns\n WHERE\n table_name = 'executor_secret_access_logs' AND\n table_schema = current_schema() AND\n column_name = 'machine_user'\n ) THEN\n -- copy over any rows that may have been added since (unlikely edge-case)\n INSERT INTO executor_secret_access_logs_machineuser_bak_1670600028\n SELECT * FROM executor_secret_access_logs AS esal\n LEFT JOIN executor_secret_access_logs_machineuser_bak_1670600028 AS bak\n ON esal.id = bak.id\n WHERE bak.id IS NULL AND esal.machine_user \u003c\u003e '' OR esal.user_id IS NULL;\n END IF;\nEND\n$$;\n\nALTER TABLE executor_secret_access_logs\nDROP CONSTRAINT IF EXISTS user_id_or_machine_user;\n\nALTER TABLE executor_secret_access_logs\nDROP COLUMN IF EXISTS machine_user;\n\nDELETE FROM executor_secret_access_logs WHERE user_id IS NULL;\n\nALTER TABLE executor_secret_access_logs\nALTER COLUMN user_id\nSET NOT NULL;\n\nDROP VIEW IF EXISTS lsif_indexes_with_repository_name;\n\nCREATE VIEW lsif_indexes_with_repository_name AS\n SELECT u.id,\n u.commit,\n u.queued_at,\n u.state,\n u.failure_message,\n u.started_at,\n u.finished_at,\n u.repository_id,\n u.process_after,\n u.num_resets,\n u.num_failures,\n u.docker_steps,\n u.root,\n u.indexer,\n u.indexer_args,\n u.outfile,\n u.log_contents,\n u.execution_logs,\n u.local_steps,\n u.should_reindex,\n r.name AS repository_name\n FROM (lsif_indexes u\n JOIN repo r ON ((r.id = u.repository_id)))\n WHERE (r.deleted_at IS NULL);\n\nALTER TABLE lsif_indexes\nDROP COLUMN IF EXISTS requested_envvars;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1668813365, + 1670256530 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1672884222, + "Name": "create_namespace_permissions_table", + "UpQuery": "CREATE TABLE IF NOT EXISTS namespace_permissions (\n id SERIAL PRIMARY KEY,\n namespace text NOT NULL,\n resource_id integer NOT NULL,\n action text NOT NULL,\n\n user_id integer REFERENCES users(id) ON DELETE CASCADE DEFERRABLE,\n\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n\n CONSTRAINT namespace_not_blank CHECK (namespace \u003c\u003e ''::text),\n CONSTRAINT action_not_blank CHECK (action \u003c\u003e ''::text),\n\n CONSTRAINT unique_resource_permission UNIQUE (namespace, resource_id, action, user_id)\n);", + "DownQuery": "DROP TABLE IF EXISTS namespace_permissions;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1669645608, + 1670600028, + 1670870072 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1673019611, + "Name": "lsif_uploads_audit_logs_bigint_upload_size", + "UpQuery": "ALTER TABLE lsif_uploads_audit_logs\nALTER COLUMN upload_size\nTYPE bigint;", + "DownQuery": "ALTER TABLE lsif_uploads_audit_logs\nALTER COLUMN upload_size\nTYPE integer;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1672884222 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1672897105, + "Name": "add column author_user_id to critical_and_site_config", + "UpQuery": "ALTER TABLE critical_and_site_config\n ADD COLUMN IF NOT EXISTS author_user_id integer;\nCOMMENT ON COLUMN critical_and_site_config.author_user_id IS 'A null value indicates that this config was most likely added by code on the start-up path, for example from the SITE_CONFIG_FILE unless the config itself was added before this column existed in which case it could also have been a user.';", + "DownQuery": "ALTER TABLE critical_and_site_config\n DROP COLUMN IF EXISTS author_user_id;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1669645608, + 1670600028, + 1670870072 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1673351808, + "Name": "add_repo_corruption_stat", + "UpQuery": "ALTER TABLE gitserver_repos_statistics\n ADD COLUMN IF NOT EXISTS corrupted bigint NOT NULL DEFAULT 0;\nCOMMENT ON COLUMN gitserver_repos_statistics.corrupted IS 'Number of repositories that are NOT soft-deleted and not blocked and have corrupted_at set in gitserver_repos table';\n\nALTER TABLE repo_statistics\n ADD COLUMN IF NOT EXISTS corrupted bigint NOT NULL DEFAULT 0;\nCOMMENT ON COLUMN repo_statistics.corrupted IS 'Number of repositories that are NOT soft-deleted and not blocked and have corrupted_at set in gitserver_repos table';\n\n--- repo UPDATE trigger\nCREATE OR REPLACE FUNCTION recalc_repo_statistics_on_repo_update() RETURNS trigger\n LANGUAGE plpgsql\n AS $$ BEGIN\n -- Insert diff of changes\n WITH diff(total, soft_deleted, not_cloned, cloning, cloned, failed_fetch, corrupted) AS (\n VALUES (\n (SELECT COUNT(*) FROM newtab WHERE deleted_at IS NULL AND blocked IS NULL) - (SELECT COUNT(*) FROM oldtab WHERE deleted_at IS NULL AND blocked IS NULL),\n (SELECT COUNT(*) FROM newtab WHERE deleted_at IS NOT NULL AND blocked IS NULL) - (SELECT COUNT(*) FROM oldtab WHERE deleted_at IS NOT NULL AND blocked IS NULL),\n (\n (SELECT COUNT(*) FROM newtab JOIN gitserver_repos gr ON gr.repo_id = newtab.id WHERE newtab.deleted_at is NULL AND newtab.blocked IS NULL AND gr.clone_status = 'not_cloned')\n -\n (SELECT COUNT(*) FROM oldtab JOIN gitserver_repos gr ON gr.repo_id = oldtab.id WHERE oldtab.deleted_at is NULL AND oldtab.blocked IS NULL AND gr.clone_status = 'not_cloned')\n ),\n (\n (SELECT COUNT(*) FROM newtab JOIN gitserver_repos gr ON gr.repo_id = newtab.id WHERE newtab.deleted_at is NULL AND newtab.blocked IS NULL AND gr.clone_status = 'cloning')\n -\n (SELECT COUNT(*) FROM oldtab JOIN gitserver_repos gr ON gr.repo_id = oldtab.id WHERE oldtab.deleted_at is NULL AND oldtab.blocked IS NULL AND gr.clone_status = 'cloning')\n ),\n (\n (SELECT COUNT(*) FROM newtab JOIN gitserver_repos gr ON gr.repo_id = newtab.id WHERE newtab.deleted_at is NULL AND newtab.blocked IS NULL AND gr.clone_status = 'cloned')\n -\n (SELECT COUNT(*) FROM oldtab JOIN gitserver_repos gr ON gr.repo_id = oldtab.id WHERE oldtab.deleted_at is NULL AND oldtab.blocked IS NULL AND gr.clone_status = 'cloned')\n ),\n (\n (SELECT COUNT(*) FROM newtab JOIN gitserver_repos gr ON gr.repo_id = newtab.id WHERE newtab.deleted_at is NULL AND newtab.blocked IS NULL AND gr.last_error IS NOT NULL)\n -\n (SELECT COUNT(*) FROM oldtab JOIN gitserver_repos gr ON gr.repo_id = oldtab.id WHERE oldtab.deleted_at is NULL AND oldtab.blocked IS NULL AND gr.last_error IS NOT NULL)\n ),\n (\n (SELECT COUNT(*) FROM newtab JOIN gitserver_repos gr ON gr.repo_id = newtab.id WHERE newtab.deleted_at is NULL AND newtab.blocked IS NULL AND gr.corrupted_at IS NOT NULL)\n -\n (SELECT COUNT(*) FROM oldtab JOIN gitserver_repos gr ON gr.repo_id = oldtab.id WHERE oldtab.deleted_at is NULL AND oldtab.blocked IS NULL AND gr.corrupted_at IS NOT NULL)\n )\n )\n )\n INSERT INTO\n repo_statistics (total, soft_deleted, not_cloned, cloning, cloned, failed_fetch, corrupted)\n SELECT total, soft_deleted, not_cloned, cloning, cloned, failed_fetch, corrupted\n FROM diff\n WHERE\n total != 0\n OR soft_deleted != 0\n OR not_cloned != 0\n OR cloning != 0\n OR cloned != 0\n OR failed_fetch != 0\n OR corrupted != 0\n ;\n RETURN NULL;\n END\n$$;\n\nCREATE OR REPLACE FUNCTION recalc_gitserver_repos_statistics_on_update() RETURNS trigger\n LANGUAGE plpgsql\n -------------------------------------------------\n -- IMPORTANT: THIS IS CHANGED TO INCLUDE `corrupted`\n -------------------------------------------------\n AS $$ BEGIN\n INSERT INTO gitserver_repos_statistics AS grs (shard_id, total, not_cloned, cloning, cloned, failed_fetch, corrupted)\n SELECT\n newtab.shard_id AS shard_id,\n COUNT(*) AS total,\n COUNT(*) FILTER(WHERE clone_status = 'not_cloned') AS not_cloned,\n COUNT(*) FILTER(WHERE clone_status = 'cloning') AS cloning,\n COUNT(*) FILTER(WHERE clone_status = 'cloned') AS cloned,\n COUNT(*) FILTER(WHERE last_error IS NOT NULL) AS failed_fetch,\n COUNT(*) FILTER(WHERE corrupted_at IS NOT NULL) AS corrupted\n FROM\n newtab\n GROUP BY newtab.shard_id\n ON CONFLICT(shard_id) DO\n UPDATE\n SET\n total = grs.total + (excluded.total - (SELECT COUNT(*) FROM oldtab ot WHERE ot.shard_id = excluded.shard_id)),\n not_cloned = grs.not_cloned + (excluded.not_cloned - (SELECT COUNT(*) FILTER(WHERE ot.clone_status = 'not_cloned') FROM oldtab ot WHERE ot.shard_id = excluded.shard_id)),\n cloning = grs.cloning + (excluded.cloning - (SELECT COUNT(*) FILTER(WHERE ot.clone_status = 'cloning') FROM oldtab ot WHERE ot.shard_id = excluded.shard_id)),\n cloned = grs.cloned + (excluded.cloned - (SELECT COUNT(*) FILTER(WHERE ot.clone_status = 'cloned') FROM oldtab ot WHERE ot.shard_id = excluded.shard_id)),\n failed_fetch = grs.failed_fetch + (excluded.failed_fetch - (SELECT COUNT(*) FILTER(WHERE ot.last_error IS NOT NULL) FROM oldtab ot WHERE ot.shard_id = excluded.shard_id)),\n corrupted = grs.corrupted + (excluded.corrupted - (SELECT COUNT(*) FILTER(WHERE ot.corrupted_at IS NOT NULL) FROM oldtab ot WHERE ot.shard_id = excluded.shard_id))\n ;\n\n -------------------------------------------------\n -- IMPORTANT: THIS IS CHANGED TO INCLUDE `corrupted`\n -------------------------------------------------\n WITH moved AS (\n SELECT\n oldtab.shard_id AS shard_id,\n COUNT(*) AS total,\n COUNT(*) FILTER(WHERE oldtab.clone_status = 'not_cloned') AS not_cloned,\n COUNT(*) FILTER(WHERE oldtab.clone_status = 'cloning') AS cloning,\n COUNT(*) FILTER(WHERE oldtab.clone_status = 'cloned') AS cloned,\n COUNT(*) FILTER(WHERE oldtab.last_error IS NOT NULL) AS failed_fetch,\n COUNT(*) FILTER(WHERE oldtab.corrupted_at IS NOT NULL) AS corrupted\n FROM\n oldtab\n JOIN newtab ON newtab.repo_id = oldtab.repo_id\n WHERE\n oldtab.shard_id != newtab.shard_id\n GROUP BY oldtab.shard_id\n )\n UPDATE gitserver_repos_statistics grs\n SET\n total = grs.total - moved.total,\n not_cloned = grs.not_cloned - moved.not_cloned,\n cloning = grs.cloning - moved.cloning,\n cloned = grs.cloned - moved.cloned,\n failed_fetch = grs.failed_fetch - moved.failed_fetch,\n corrupted = grs.corrupted - moved.corrupted\n FROM moved\n WHERE moved.shard_id = grs.shard_id;\n\n -------------------------------------------------\n -- IMPORTANT: THIS IS CHANGED TO INCLUDE `corrupted`\n -------------------------------------------------\n WITH diff(not_cloned, cloning, cloned, failed_fetch, corrupted) AS (\n VALUES (\n (\n (SELECT COUNT(*) FROM newtab JOIN repo r ON newtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND newtab.clone_status = 'not_cloned')\n -\n (SELECT COUNT(*) FROM oldtab JOIN repo r ON oldtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND oldtab.clone_status = 'not_cloned')\n ),\n (\n (SELECT COUNT(*) FROM newtab JOIN repo r ON newtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND newtab.clone_status = 'cloning')\n -\n (SELECT COUNT(*) FROM oldtab JOIN repo r ON oldtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND oldtab.clone_status = 'cloning')\n ),\n (\n (SELECT COUNT(*) FROM newtab JOIN repo r ON newtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND newtab.clone_status = 'cloned')\n -\n (SELECT COUNT(*) FROM oldtab JOIN repo r ON oldtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND oldtab.clone_status = 'cloned')\n ),\n (\n (SELECT COUNT(*) FROM newtab JOIN repo r ON newtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND newtab.last_error IS NOT NULL)\n -\n (SELECT COUNT(*) FROM oldtab JOIN repo r ON oldtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND oldtab.last_error IS NOT NULL)\n ),\n (\n (SELECT COUNT(*) FROM newtab JOIN repo r ON newtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND newtab.corrupted_at IS NOT NULL)\n -\n (SELECT COUNT(*) FROM oldtab JOIN repo r ON oldtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND oldtab.corrupted_at IS NOT NULL)\n )\n\n )\n )\n INSERT INTO repo_statistics (not_cloned, cloning, cloned, failed_fetch, corrupted)\n SELECT not_cloned, cloning, cloned, failed_fetch, corrupted\n FROM diff\n WHERE\n not_cloned != 0\n OR cloning != 0\n OR cloned != 0\n OR failed_fetch != 0\n OR corrupted != 0\n ;\n\n RETURN NULL;\n END\n$$;", + "DownQuery": "ALTER TABLE gitserver_repos_statistics\n DROP COLUMN IF EXISTS corrupted;\n\nALTER TABLE repo_statistics\n DROP COLUMN IF EXISTS corrupted;\n\n--- Restore previous version of trigger\n--- repo UPDATE trigger\nCREATE OR REPLACE FUNCTION recalc_repo_statistics_on_repo_update() RETURNS trigger\n LANGUAGE plpgsql\n AS $$ BEGIN\n -- Insert diff of changes\n WITH diff(total, soft_deleted, not_cloned, cloning, cloned, failed_fetch) AS (\n VALUES (\n (SELECT COUNT(*) FROM newtab WHERE deleted_at IS NULL AND blocked IS NULL) - (SELECT COUNT(*) FROM oldtab WHERE deleted_at IS NULL AND blocked IS NULL),\n (SELECT COUNT(*) FROM newtab WHERE deleted_at IS NOT NULL AND blocked IS NULL) - (SELECT COUNT(*) FROM oldtab WHERE deleted_at IS NOT NULL AND blocked IS NULL),\n (\n (SELECT COUNT(*) FROM newtab JOIN gitserver_repos gr ON gr.repo_id = newtab.id WHERE newtab.deleted_at is NULL AND newtab.blocked IS NULL AND gr.clone_status = 'not_cloned')\n -\n (SELECT COUNT(*) FROM oldtab JOIN gitserver_repos gr ON gr.repo_id = oldtab.id WHERE oldtab.deleted_at is NULL AND oldtab.blocked IS NULL AND gr.clone_status = 'not_cloned')\n ),\n (\n (SELECT COUNT(*) FROM newtab JOIN gitserver_repos gr ON gr.repo_id = newtab.id WHERE newtab.deleted_at is NULL AND newtab.blocked IS NULL AND gr.clone_status = 'cloning')\n -\n (SELECT COUNT(*) FROM oldtab JOIN gitserver_repos gr ON gr.repo_id = oldtab.id WHERE oldtab.deleted_at is NULL AND oldtab.blocked IS NULL AND gr.clone_status = 'cloning')\n ),\n (\n (SELECT COUNT(*) FROM newtab JOIN gitserver_repos gr ON gr.repo_id = newtab.id WHERE newtab.deleted_at is NULL AND newtab.blocked IS NULL AND gr.clone_status = 'cloned')\n -\n (SELECT COUNT(*) FROM oldtab JOIN gitserver_repos gr ON gr.repo_id = oldtab.id WHERE oldtab.deleted_at is NULL AND oldtab.blocked IS NULL AND gr.clone_status = 'cloned')\n ),\n (\n (SELECT COUNT(*) FROM newtab JOIN gitserver_repos gr ON gr.repo_id = newtab.id WHERE newtab.deleted_at is NULL AND newtab.blocked IS NULL AND gr.last_error IS NOT NULL)\n -\n (SELECT COUNT(*) FROM oldtab JOIN gitserver_repos gr ON gr.repo_id = oldtab.id WHERE oldtab.deleted_at is NULL AND oldtab.blocked IS NULL AND gr.last_error IS NOT NULL)\n )\n )\n )\n INSERT INTO\n repo_statistics (total, soft_deleted, not_cloned, cloning, cloned, failed_fetch)\n SELECT total, soft_deleted, not_cloned, cloning, cloned, failed_fetch\n FROM diff\n WHERE\n total != 0\n OR soft_deleted != 0\n OR not_cloned != 0\n OR cloning != 0\n OR cloned != 0\n OR failed_fetch != 0\n ;\n RETURN NULL;\n END\n$$;\n\nCREATE OR REPLACE FUNCTION recalc_gitserver_repos_statistics_on_update() RETURNS trigger\n LANGUAGE plpgsql\n AS $$ BEGIN\n INSERT INTO gitserver_repos_statistics AS grs (shard_id, total, not_cloned, cloning, cloned, failed_fetch)\n SELECT\n newtab.shard_id AS shard_id,\n COUNT(*) AS total,\n COUNT(*) FILTER(WHERE clone_status = 'not_cloned') AS not_cloned,\n COUNT(*) FILTER(WHERE clone_status = 'cloning') AS cloning,\n COUNT(*) FILTER(WHERE clone_status = 'cloned') AS cloned,\n COUNT(*) FILTER(WHERE last_error IS NOT NULL) AS failed_fetch\n FROM\n newtab\n GROUP BY newtab.shard_id\n ON CONFLICT(shard_id) DO\n UPDATE\n SET\n total = grs.total + (excluded.total - (SELECT COUNT(*) FROM oldtab ot WHERE ot.shard_id = excluded.shard_id)),\n not_cloned = grs.not_cloned + (excluded.not_cloned - (SELECT COUNT(*) FILTER(WHERE ot.clone_status = 'not_cloned') FROM oldtab ot WHERE ot.shard_id = excluded.shard_id)),\n cloning = grs.cloning + (excluded.cloning - (SELECT COUNT(*) FILTER(WHERE ot.clone_status = 'cloning') FROM oldtab ot WHERE ot.shard_id = excluded.shard_id)),\n cloned = grs.cloned + (excluded.cloned - (SELECT COUNT(*) FILTER(WHERE ot.clone_status = 'cloned') FROM oldtab ot WHERE ot.shard_id = excluded.shard_id)),\n failed_fetch = grs.failed_fetch + (excluded.failed_fetch - (SELECT COUNT(*) FILTER(WHERE ot.last_error IS NOT NULL) FROM oldtab ot WHERE ot.shard_id = excluded.shard_id))\n ;\n\n WITH moved AS (\n SELECT\n oldtab.shard_id AS shard_id,\n COUNT(*) AS total,\n COUNT(*) FILTER(WHERE oldtab.clone_status = 'not_cloned') AS not_cloned,\n COUNT(*) FILTER(WHERE oldtab.clone_status = 'cloning') AS cloning,\n COUNT(*) FILTER(WHERE oldtab.clone_status = 'cloned') AS cloned,\n COUNT(*) FILTER(WHERE oldtab.last_error IS NOT NULL) AS failed_fetch\n FROM\n oldtab\n JOIN newtab ON newtab.repo_id = oldtab.repo_id\n WHERE\n oldtab.shard_id != newtab.shard_id\n GROUP BY oldtab.shard_id\n )\n UPDATE gitserver_repos_statistics grs\n SET\n total = grs.total - moved.total,\n not_cloned = grs.not_cloned - moved.not_cloned,\n cloning = grs.cloning - moved.cloning,\n cloned = grs.cloned - moved.cloned,\n failed_fetch = grs.failed_fetch - moved.failed_fetch\n FROM moved\n WHERE moved.shard_id = grs.shard_id;\n\n WITH diff(not_cloned, cloning, cloned, failed_fetch) AS (\n VALUES (\n (\n (SELECT COUNT(*) FROM newtab JOIN repo r ON newtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND newtab.clone_status = 'not_cloned')\n -\n (SELECT COUNT(*) FROM oldtab JOIN repo r ON oldtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND oldtab.clone_status = 'not_cloned')\n ),\n (\n (SELECT COUNT(*) FROM newtab JOIN repo r ON newtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND newtab.clone_status = 'cloning')\n -\n (SELECT COUNT(*) FROM oldtab JOIN repo r ON oldtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND oldtab.clone_status = 'cloning')\n ),\n (\n (SELECT COUNT(*) FROM newtab JOIN repo r ON newtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND newtab.clone_status = 'cloned')\n -\n (SELECT COUNT(*) FROM oldtab JOIN repo r ON oldtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND oldtab.clone_status = 'cloned')\n ),\n (\n (SELECT COUNT(*) FROM newtab JOIN repo r ON newtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND newtab.last_error IS NOT NULL)\n -\n (SELECT COUNT(*) FROM oldtab JOIN repo r ON oldtab.repo_id = r.id WHERE r.deleted_at is NULL AND r.blocked IS NULL AND oldtab.last_error IS NOT NULL)\n )\n )\n )\n INSERT INTO repo_statistics (not_cloned, cloning, cloned, failed_fetch)\n SELECT not_cloned, cloning, cloned, failed_fetch\n FROM diff\n WHERE\n not_cloned != 0\n OR cloning != 0\n OR cloned != 0\n OR failed_fetch != 0\n ;\n\n RETURN NULL;\n END\n$$;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1670934184, + 1672897105, + 1673019611 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1673405886, + "Name": "make batch spec of batch change not nullable again", + "UpQuery": "-- This reverts a previous migration, so this is literally just 1669576792_make_batch_spec_of_batch_change_nullable/down.sql\nWITH reconstructed_batch_specs AS (\n INSERT INTO batch_specs\n (batch_change_id, user_id, namespace_user_id, namespace_org_id, rand_id, raw_spec, spec, created_from_raw)\n SELECT\n id, creator_id, namespace_user_id, namespace_org_id, md5(CONCAT(id, name)::bytea), CONCAT('name: ', name), json_build_object('name', name), TRUE\n FROM\n batch_changes\n WHERE\n batch_spec_id IS NULL\n RETURNING\n\t batch_change_id, id\n)\nUPDATE\n batch_changes\nSET batch_spec_id = (SELECT id FROM reconstructed_batch_specs WHERE batch_change_id = batch_changes.id)\nWHERE id IN (SELECT batch_change_id FROM reconstructed_batch_specs);\n\nALTER TABLE batch_changes ALTER COLUMN batch_spec_id SET NOT NULL;", + "DownQuery": "-- This is literally just 1669576792_make_batch_spec_of_batch_change_nullable/up.sql\nALTER TABLE batch_changes ALTER COLUMN batch_spec_id DROP NOT NULL;\n\nCREATE TEMPORARY TABLE minimal_batch_specs (id bigint);\n\nINSERT INTO minimal_batch_specs\nSELECT batch_spec_id FROM batch_changes WHERE batch_spec_id IS NOT NULL AND last_applied_at IS NULL;\n\nUPDATE batch_changes SET batch_spec_id = NULL WHERE batch_spec_id IN (SELECT id FROM minimal_batch_specs);\n\n-- Delete existing empty batch specs.\nDELETE FROM batch_specs WHERE id IN (SELECT id FROM minimal_batch_specs);-- Delete existing empty batch specs.\n\nDROP TABLE minimal_batch_specs;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1670934184, + 1672897105, + 1673019611 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1673871310, + "Name": "add columns to permission_sync_jobs table", + "UpQuery": "ALTER TABLE permission_sync_jobs\n ADD COLUMN IF NOT EXISTS reason TEXT,\n ADD COLUMN IF NOT EXISTS triggered_by_user_id INTEGER,\n ADD FOREIGN KEY (triggered_by_user_id) REFERENCES users(id) ON DELETE SET NULL DEFERRABLE;\n\nCOMMENT ON COLUMN permission_sync_jobs.reason IS 'Specifies why permissions sync job was triggered.';\nCOMMENT ON COLUMN permission_sync_jobs.triggered_by_user_id IS 'Specifies an ID of a user who triggered a sync.';", + "DownQuery": "ALTER TABLE permission_sync_jobs\n DROP COLUMN IF EXISTS reason,\n DROP COLUMN IF EXISTS triggered_by_user_id;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1669297489, + 1673405886 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null + }, + { + "ID": 1673897709, + "Name": "add_cascade_batch_spec_resolution_jobs", + "UpQuery": "ALTER TABLE \n batch_spec_resolution_jobs\nDROP \n CONSTRAINT IF EXISTS batch_spec_resolution_jobs_initiator_id_fkey;\n\nALTER TABLE \n batch_spec_resolution_jobs\nADD \n CONSTRAINT batch_spec_resolution_jobs_initiator_id_fkey \n FOREIGN KEY (initiator_id) \n REFERENCES users(id) \n ON DELETE CASCADE\n ON UPDATE CASCADE \n DEFERRABLE;", + "DownQuery": "ALTER TABLE \n batch_spec_resolution_jobs\nDROP \n CONSTRAINT IF EXISTS batch_spec_resolution_jobs_initiator_id_fkey;\n\nALTER TABLE \n batch_spec_resolution_jobs\nADD \n CONSTRAINT batch_spec_resolution_jobs_initiator_id_fkey \n FOREIGN KEY (initiator_id) \n REFERENCES users(id) \n ON UPDATE CASCADE DEFERRABLE;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1673351808, + 1673871310 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null } ], "BoundsByRev": { @@ -8014,6 +8316,13 @@ 1670543231 ], "PreCreation": false + }, + "v4.4.0": { + "RootID": 1648051770, + "LeafIDs": [ + 1673897709 + ], + "PreCreation": false } } } From eb2297f69ab952f0b45bfaad93d8f06b515af56b Mon Sep 17 00:00:00 2001 From: Eric Fritz Date: Tue, 17 Jan 2023 13:44:09 -0600 Subject: [PATCH 002/678] codeintel: Add code graph data link next to settings in repo page (#46444) --- client/web/src/site-admin/SiteAdminRepositoriesPage.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx b/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx index e7fd05d6ff47..d7e2efd63303 100644 --- a/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx +++ b/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo } from 'react' -import { mdiCloudDownload, mdiCog } from '@mdi/js' +import { mdiCloudDownload, mdiCog, mdiBrain } from '@mdi/js' import { RouteComponentProps } from 'react-router' import { Observable } from 'rxjs' @@ -72,6 +72,11 @@ const RepositoryNode: React.FunctionComponent Clone now )}{' '} + + + {' '} + + + {/* eslint-disable-next-line react/forbid-elements */} +
+ + setSearch(event.target.value)} + /> + + + + Don't see an insight?{' '} + + Create a new insight. + + + {connection && ( + + {plural('result', connection.nodes.length)} + {connection.totalCount && <> out of {plural('total', connection.totalCount)}} + + )} + + + + {items => ( + <> + {items.map((item, index) => ( + + ))} + {items.length === 0 && ( + + {loading ? 'Loading...' : 'No insights found'} + + )} + {connection?.pageInfo?.hasNextPage && ( + + )} + + )} + + + + {isErrorLike(submittingOrError) && } + +
+ + Press + to navigate through results + + + + + +
+ + ) +} - const handleSubmit = async (values: AddInsightFormValues): Promise => { - const { insightIds } = values +function plural(what: string, count: number): string { + return `${count.toLocaleString()} ${pluralize(what, count)}` +} - await assignInsightsToDashboard({ - id: dashboard.id, - prevInsightIds: dashboardInsightIds, - nextInsightIds: insightIds, - }).toPromise() +interface InsightSuggestionCardProps { + item: InsightSuggestion + index: number +} - onClose() - } +function InsightSuggestionCard(props: InsightSuggestionCardProps): ReactElement { + const { item, index } = props return ( - - - - {loading && !data && } - {error && } - {data && ( - <> -

- Add insight to {dashboard.title} -

- - {!insights.length && There are no insights for this dashboard.} - - {insights.length > 0 && ( - - )} - - )} -
+ + + + + + {item.type} insight {getInsightDetails(item)} + + ) } -function getDashboardInsightIds(data?: GetDashboardAccessibleInsightsResult): string[] { - if (!data || !data.dashboardInsightsIds.nodes[0]?.views) { - return [] +function getInsightDetails(insight: InsightSuggestion): ReactNode { + switch (insight.type) { + case InsightType.Detect: + return insight.queries.join(', ') + case InsightType.DetectAndTrack: + return insight.query + case InsightType.Compute: + return `${insight.query}, grouped by ${formatGroupBy(insight.groupBy)}` + case InsightType.LanguageStats: + return '' } - - return data.dashboardInsightsIds.nodes[0].views.nodes.filter(isDefined).map(view => view.id) } -function getAvailableInsights(data?: GetDashboardAccessibleInsightsResult): AccessibleInsight[] { - return data?.accessibleInsights?.nodes.filter(isDefined) ?? [] +const formatGroupBy = (groupBy: GroupByField): string => { + const str = groupBy.toString() + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() } diff --git a/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/components/add-insight-modal-content/AddInsightModalContent.module.scss b/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/components/add-insight-modal-content/AddInsightModalContent.module.scss deleted file mode 100644 index e0cb5827dc31..000000000000 --- a/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/components/add-insight-modal-content/AddInsightModalContent.module.scss +++ /dev/null @@ -1,28 +0,0 @@ -.insights-container { - margin-top: 0.5rem; - max-height: 14rem; - overflow: auto; -} - -.insight-item { - display: flex; - align-items: center; - width: 100%; - margin: 0; - padding: 0.5rem 0.5rem; - - &:hover, - &:focus-within { - cursor: pointer; - background: var(--color-bg-3); - } -} - -.checkbox-wrapper { - display: flex; - align-items: center; -} - -.checkbox { - margin-top: 0; -} diff --git a/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/components/add-insight-modal-content/AddInsightModalContent.tsx b/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/components/add-insight-modal-content/AddInsightModalContent.tsx deleted file mode 100644 index 18fa2bf484e1..000000000000 --- a/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/components/add-insight-modal-content/AddInsightModalContent.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react' - -import { escapeRegExp } from 'lodash' - -import { Button, Input, Link, Label, Checkbox, ErrorAlert } from '@sourcegraph/wildcard' - -import { LoaderButton } from '../../../../../../../../../components/LoaderButton' -import { AccessibleInsight } from '../../../../../../../../../graphql-operations' -import { - TruncatedText, - useCheckboxes, - useField, - SubmissionErrors, - useForm, - FORM_ERROR, -} from '../../../../../../../components' -import { encodeDashboardIdQueryParam } from '../../../../../../../routers.constant' - -import styles from './AddInsightModalContent.module.scss' - -interface AddInsightModalContentProps { - insights: AccessibleInsight[] - initialValues: AddInsightFormValues - dashboardID: string - onSubmit: (values: AddInsightFormValues) => SubmissionErrors | Promise | void - onCancel: () => void -} - -export interface AddInsightFormValues { - searchInput: string - insightIds: string[] -} - -export const AddInsightModalContent: React.FunctionComponent< - React.PropsWithChildren -> = props => { - const { initialValues, insights, dashboardID, onSubmit, onCancel } = props - - const { formAPI, ref, handleSubmit } = useForm({ - initialValues, - onSubmit, - }) - - const searchInput = useField({ - name: 'searchInput', - formApi: formAPI, - }) - - const { - input: { isChecked, onChange, onBlur }, - } = useCheckboxes('insightIds', formAPI) - - const filteredInsights = insights.filter(insight => - insight.presentation.title.match(new RegExp(escapeRegExp(searchInput.input.value), 'gi')) - ) - - return ( - // eslint-disable-next-line react/forbid-elements -
- - Don't see an insight? Check the insight's visibility settings or{' '} - - create a new insight - - - } - placeholder="Search insights..." - {...searchInput.input} - /> - -
- {filteredInsights.map(insight => ( - - ))} -
- -
- - {formAPI.submitErrors?.[FORM_ERROR] && ( - - )} - -
- - - -
- - ) -} diff --git a/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/index.ts b/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/index.ts index 8394e50a65ac..a74912864490 100644 --- a/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/index.ts +++ b/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/index.ts @@ -1,4 +1,7 @@ +// Export in order to use it for dashboard unit test mocks +export { GET_INSIGHTS_BY_SEARCH_TERM } from './query' + export { AddInsightModal } from './AddInsightModal' -export type { AddInsightModalProps } from './AddInsightModal' -export { GET_ACCESSIBLE_INSIGHTS_LIST } from './query' +// Type exports +export type { AddInsightModalProps } from './AddInsightModal' diff --git a/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/query.ts b/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/query.ts index 1274bccfe523..d698ae0befb2 100644 --- a/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/query.ts +++ b/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/query.ts @@ -1,7 +1,71 @@ -import { gql } from '@apollo/client' +import { ApolloClient, ApolloError } from '@apollo/client' +import { groupBy } from 'lodash' -export const GET_ACCESSIBLE_INSIGHTS_LIST = gql` - fragment AccessibleInsight on InsightView { +import { isDefined } from '@sourcegraph/common' +import { dataOrThrowErrors, getDocumentNode, gql } from '@sourcegraph/http-client' + +import { Connection } from '../../../../../../../components/FilteredConnection' +import { useShowMorePagination } from '../../../../../../../components/FilteredConnection/hooks/useShowMorePagination' +import { + AssignableInsight, + DashboardInsights, + FindInsightsBySearchTermResult, + FindInsightsBySearchTermVariables, + GroupByField, +} from '../../../../../../../graphql-operations' + +import { DashboardInsight, InsightSuggestion, InsightType } from './types' + +const SYNC_DASHBOARD_INSIGHTS = gql` + fragment DashboardInsights on InsightsDashboard { + views { + nodes { + id + presentation { + __typename + ... on LineChartInsightViewPresentation { + title + } + ... on PieChartInsightViewPresentation { + title + } + } + } + } + } +` + +/** + * This cache function returns a minimal data object from the apollo cache about + * insights that the currently viewed dashboard has see {@link GET_DASHBOARD_INSIGHTS_GQL} + */ +export function getCachedDashboardInsights(client: ApolloClient, dashboardId: string): DashboardInsight[] { + const dashboardInsights = + client + .readFragment({ + id: `InsightsDashboard:${dashboardId}`, + fragment: getDocumentNode(SYNC_DASHBOARD_INSIGHTS), + }) + ?.views?.nodes.filter(isDefined) ?? [] + + return dashboardInsights.map(insight => ({ id: insight.id, title: insight.presentation.title })) +} + +export const GET_INSIGHTS_BY_SEARCH_TERM = gql` + query FindInsightsBySearchTerm($search: String!, $first: Int, $after: String, $excludeIds: [ID!]) { + insightViews(find: $search, first: $first, after: $after, excludeIds: $excludeIds) { + nodes { + ...AssignableInsight + } + totalCount + pageInfo { + hasNextPage + endCursor + } + } + } + + fragment AssignableInsight on InsightView { id presentation { __typename @@ -12,23 +76,133 @@ export const GET_ACCESSIBLE_INSIGHTS_LIST = gql` title } } + dataSeriesDefinitions { + ... on SearchInsightDataSeriesDefinition { + query + groupBy + generatedFromCaptureGroups + } + } } +` - query GetDashboardAccessibleInsights($id: ID!) { - dashboardInsightsIds: insightsDashboards(id: $id) { - nodes { - views { - nodes { - id - } +interface UseInsightSuggestionsInput { + search: string + excludeIds: string[] +} + +interface UseInsightSuggestionsResult { + connection?: Connection + loading: boolean + hasNextPage: boolean + error?: ApolloError + fetchMore: () => void +} + +export function useInsightSuggestions(input: UseInsightSuggestionsInput): UseInsightSuggestionsResult { + const { search, excludeIds } = input + + const { connection, loading, hasNextPage, error, fetchMore } = useShowMorePagination< + FindInsightsBySearchTermResult, + FindInsightsBySearchTermVariables, + AssignableInsight | null + >({ + query: GET_INSIGHTS_BY_SEARCH_TERM, + variables: { first: 20, after: null, search, excludeIds }, + getConnection: result => { + const { insightViews } = dataOrThrowErrors(result) + + return insightViews + }, + options: { fetchPolicy: 'cache-and-network' }, + }) + + if (!connection) { + return { + loading, + hasNextPage, + error, + fetchMore, + } + } + + const normalizedConnection: Connection = { + ...connection, + nodes: makeInsightTitlesUnique( + connection.nodes.filter( + (insight): insight is AssignableInsight => isDefined(insight) && !excludeIds.includes(insight.id) + ) + ).map(insight => { + const isLangStat = insight.presentation.__typename === 'PieChartInsightViewPresentation' + + if (isLangStat) { + return { + id: insight.id, + title: insight.presentation.title, + type: InsightType.LanguageStats, } } - } - accessibleInsights: insightViews { - nodes { - ...AccessibleInsight + const isCompute = insight.dataSeriesDefinitions.some(series => series.groupBy) + + if (isCompute) { + const { groupBy, query } = insight.dataSeriesDefinitions[0] ?? {} + return { + id: insight.id, + title: insight.presentation.title, + type: InsightType.Compute, + query, + groupBy: groupBy ?? GroupByField.AUTHOR, + } } - } + + const isCaptureGroup = insight.dataSeriesDefinitions.some( + series => series.generatedFromCaptureGroups && !series.groupBy + ) + + if (isCaptureGroup) { + const { query } = insight.dataSeriesDefinitions[0] ?? {} + + return { + id: insight.id, + title: insight.presentation.title, + type: InsightType.DetectAndTrack, + query, + } + } + + return { + id: insight.id, + title: insight.presentation.title, + type: InsightType.Detect, + queries: insight.dataSeriesDefinitions.map(def => def.query), + } + }), } -` + + return { + connection: normalizedConnection, + loading, + hasNextPage, + error, + fetchMore, + } +} + +function makeInsightTitlesUnique(insights: AssignableInsight[]): AssignableInsight[] { + const groupedByTitle = groupBy(insights, insight => insight.presentation.title) + + return Object.keys(groupedByTitle).flatMap(title => { + if (groupedByTitle[title].length === 1) { + return groupedByTitle[title] + } + + return groupedByTitle[title].map((insight, index) => ({ + ...insight, + presentation: { + ...insight.presentation, + title: `${insight.presentation.title} (${index + 1})`, + }, + })) + }) +} diff --git a/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/types.ts b/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/types.ts new file mode 100644 index 000000000000..be6e7876558d --- /dev/null +++ b/client/web/src/enterprise/insights/pages/dashboards/dashboard-view/components/add-insight-modal/types.ts @@ -0,0 +1,38 @@ +import { GroupByField } from '../../../../../../../graphql-operations' + +export enum InsightType { + Detect = 'Detect', + DetectAndTrack = 'Detect and Track', + Compute = 'Compute', + LanguageStats = 'Language stats', +} + +export interface DashboardInsight { + id: string + title: string +} + +export interface DetectInsight extends DashboardInsight { + type: InsightType.Detect + queries: string[] +} + +export interface DetectAndTrackInsight extends DashboardInsight { + type: InsightType.DetectAndTrack + query: string +} + +export interface ComputeInsight extends DashboardInsight { + type: InsightType.Compute + query: string + groupBy: GroupByField +} + +export interface LanguageStatsInsight extends DashboardInsight { + type: InsightType.LanguageStats +} + +export type InsightSuggestion = DetectInsight | DetectAndTrackInsight | ComputeInsight | LanguageStatsInsight + +export const getInsightId = (insight: DashboardInsight): string => insight.id +export const getInsightTitle = (insight: DashboardInsight): string => insight.title diff --git a/client/web/src/integration/insights/create-insights.test.ts b/client/web/src/integration/insights/create-insights.test.ts index d7ad87c48867..38a0ffa13fdd 100644 --- a/client/web/src/integration/insights/create-insights.test.ts +++ b/client/web/src/integration/insights/create-insights.test.ts @@ -181,14 +181,14 @@ describe('Code insight create insight page', () => { }, ], }, + repositoryDefinition: { + repositories: ['github.com/sourcegraph/sourcegraph'], + __typename: 'InsightRepositoryScope', + }, dataSeriesDefinitions: [ { seriesId: '1', query: 'test series #1 query', - repositoryScope: { - repositories: ['github.com/sourcegraph/sourcegraph'], - __typename: 'InsightRepositoryScope', - }, timeScope: { unit: TimeIntervalStepUnit.MONTH, value: 2, @@ -202,10 +202,6 @@ describe('Code insight create insight page', () => { { seriesId: '1', query: 'test series #2 query', - repositoryScope: { - repositories: ['github.com/sourcegraph/sourcegraph'], - __typename: 'InsightRepositoryScope', - }, timeScope: { unit: TimeIntervalStepUnit.MONTH, value: 2, diff --git a/client/web/src/integration/insights/dashboards/add-remove-insights.test.ts b/client/web/src/integration/insights/dashboards/add-remove-insights.test.ts index 2940dd1691dc..ae06b9a3a7ca 100644 --- a/client/web/src/integration/insights/dashboards/add-remove-insights.test.ts +++ b/client/web/src/integration/insights/dashboards/add-remove-insights.test.ts @@ -5,14 +5,18 @@ import expect from 'expect' import { createDriverForTest, Driver } from '@sourcegraph/shared/src/testing/driver' import { afterEachSaveScreenshotIfFailed } from '@sourcegraph/shared/src/testing/screenshotReporter' -import { GetDashboardAccessibleInsightsResult } from '../../../graphql-operations' +import { FindInsightsBySearchTermResult } from '../../../graphql-operations' import { createWebIntegrationTestContext, WebIntegrationTestContext } from '../../context' import { GET_DASHBOARD_INSIGHTS_EMPTY, INSIGHTS_DASHBOARDS } from '../fixtures/dashboards' import { overrideInsightsGraphQLApi } from '../utils/override-insights-graphql-api' -const ALL_AVAILABLE_INSIGHTS_LIST: GetDashboardAccessibleInsightsResult = { - dashboardInsightsIds: { nodes: [{ views: { nodes: [] } }] }, - accessibleInsights: { +const ALL_AVAILABLE_INSIGHTS_LIST: FindInsightsBySearchTermResult = { + insightViews: { + pageInfo: { + endCursor: null, + hasNextPage: false, + }, + totalCount: 3, nodes: [ { __typename: 'InsightView', @@ -21,6 +25,14 @@ const ALL_AVAILABLE_INSIGHTS_LIST: GetDashboardAccessibleInsightsResult = { __typename: 'LineChartInsightViewPresentation', title: 'First Insight', }, + dataSeriesDefinitions: [ + { + __typename: 'SearchInsightDataSeriesDefinition', + query: 'Test query 1', + groupBy: null, + generatedFromCaptureGroups: false, + }, + ], }, { __typename: 'InsightView', @@ -29,6 +41,14 @@ const ALL_AVAILABLE_INSIGHTS_LIST: GetDashboardAccessibleInsightsResult = { __typename: 'LineChartInsightViewPresentation', title: 'Second Insight', }, + dataSeriesDefinitions: [ + { + __typename: 'SearchInsightDataSeriesDefinition', + query: 'Test query 2', + groupBy: null, + generatedFromCaptureGroups: false, + }, + ], }, { __typename: 'InsightView', @@ -37,6 +57,14 @@ const ALL_AVAILABLE_INSIGHTS_LIST: GetDashboardAccessibleInsightsResult = { __typename: 'LineChartInsightViewPresentation', title: 'Third Insight', }, + dataSeriesDefinitions: [ + { + __typename: 'SearchInsightDataSeriesDefinition', + query: 'Test query 3', + groupBy: null, + generatedFromCaptureGroups: false, + }, + ], }, ], }, @@ -76,7 +104,7 @@ describe('Code insights empty dashboard', () => { }), InsightsDashboards: () => INSIGHTS_DASHBOARDS, GetDashboardInsights: () => GET_DASHBOARD_INSIGHTS_EMPTY, - GetDashboardAccessibleInsights: () => ALL_AVAILABLE_INSIGHTS_LIST, + FindInsightsBySearchTerm: () => ALL_AVAILABLE_INSIGHTS_LIST, AddInsightViewToDashboard: () => ({ addInsightViewToDashboard: { dashboard: { id: 'EMPTY_DASHBOARD' }, @@ -95,9 +123,10 @@ describe('Code insights empty dashboard', () => { expect(driver.page.url()).toBe(`${driver.sourcegraphBaseUrl}/insights/dashboards/EMPTY_DASHBOARD`) await (await driver.page.$x("//button[contains(., 'Add or remove insights')]"))[0].click() - await driver.page.waitForSelector('form') + // Wait for the suggestion list item are rendered + await driver.page.waitForSelector('form ul li') - await driver.page.click('input[value="insight_003"]') + await (await driver.page.$x("//li[contains(., 'Second Insight')]"))[0].click() const variables = await testContext.waitForGraphQLRequest(async () => { const [button] = await driver.page.$x("//button[contains(., 'Save')]") @@ -109,7 +138,7 @@ describe('Code insights empty dashboard', () => { assert.deepStrictEqual(variables, { dashboardId: 'EMPTY_DASHBOARD', - insightViewId: 'insight_003', + insightViewId: 'insight_002', }) }) }) diff --git a/client/web/src/integration/insights/fixtures/dashboards.ts b/client/web/src/integration/insights/fixtures/dashboards.ts index 03ea43ed3edf..190e37ed966d 100644 --- a/client/web/src/integration/insights/fixtures/dashboards.ts +++ b/client/web/src/integration/insights/fixtures/dashboards.ts @@ -91,14 +91,14 @@ export const CAPTURE_GROUP_INSIGHT: InsightViewNode = { }, ], }, + repositoryDefinition: { + __typename: 'InsightRepositoryScope', + repositories: [], + }, dataSeriesDefinitions: [ { seriesId: '2CGKrC1dcbOpawrHQUOkiSu0NC8', query: 'machine_type \\"([\\w]+\\-[\\w]+[\\-[\\w]+]?)\\" lang:Terraform patterntype:regexp', - repositoryScope: { - repositories: [], - __typename: 'InsightRepositoryScope', - }, timeScope: { unit: TimeIntervalStepUnit.MONTH, value: 1, @@ -179,19 +179,19 @@ export const SEARCH_BASED_INSIGHT: InsightViewNode = { }, ], }, + repositoryDefinition: { + __typename: 'InsightRepositoryScope', + repositories: [ + 'github.com/sourcegraph/sourcegraph', + 'github.com/sourcegraph/deploy-sourcegraph-managed', + 'github.com/sourcegraph/infrastructure', + 'github.com/sourcegraph/deploy-sourcegraph-cloud', + ], + }, dataSeriesDefinitions: [ { seriesId: '2D2MUtp6DzHhwhjUo9mIlBbhqoO', query: 'lang:go exec.Cmd OR exec.CommandContext', - repositoryScope: { - repositories: [ - 'github.com/sourcegraph/sourcegraph', - 'github.com/sourcegraph/deploy-sourcegraph-managed', - 'github.com/sourcegraph/infrastructure', - 'github.com/sourcegraph/deploy-sourcegraph-cloud', - ], - __typename: 'InsightRepositoryScope', - }, timeScope: { unit: TimeIntervalStepUnit.WEEK, value: 2, @@ -205,15 +205,6 @@ export const SEARCH_BASED_INSIGHT: InsightViewNode = { { seriesId: '2D2MUHBTNUe5v18Je4o9woc8zrH', query: 'lang:go content:"github.com/sourcegraph/run" AND (run.Cmd OR run.Bash)', - repositoryScope: { - repositories: [ - 'github.com/sourcegraph/sourcegraph', - 'github.com/sourcegraph/deploy-sourcegraph-managed', - 'github.com/sourcegraph/infrastructure', - 'github.com/sourcegraph/deploy-sourcegraph-cloud', - ], - __typename: 'InsightRepositoryScope', - }, timeScope: { unit: TimeIntervalStepUnit.WEEK, value: 2, @@ -276,14 +267,14 @@ export const LANG_STATS_INSIGHT: InsightViewNode = { title: 'Lang Stats', otherThreshold: 0.03, }, + repositoryDefinition: { + repositories: ['github.com/sourcegraph/about'], + __typename: 'InsightRepositoryScope', + }, dataSeriesDefinitions: [ { seriesId: '2CuLABWoJVNlP8KqoB49hdes8MK', query: '', - repositoryScope: { - repositories: ['github.com/sourcegraph/about'], - __typename: 'InsightRepositoryScope', - }, timeScope: { unit: TimeIntervalStepUnit.MONTH, value: 0, @@ -340,14 +331,14 @@ export const COMPUTE_INSIGHT: InsightViewNode = { }, ], }, + repositoryDefinition: { + repositories: ['github.com/sourcegraph/test_DEPRECATED', 'github.com/sourcegraph/deploy-k8s-helper'], + __typename: 'InsightRepositoryScope', + }, dataSeriesDefinitions: [ { seriesId: '2F7eRYTr4EyEblhHeoQE2lRXG2y', query: 'DEP case:yes', - repositoryScope: { - repositories: ['github.com/sourcegraph/test_DEPRECATED', 'github.com/sourcegraph/deploy-k8s-helper'], - __typename: 'InsightRepositoryScope', - }, timeScope: { unit: TimeIntervalStepUnit.WEEK, value: 2, __typename: 'InsightIntervalTimeScope' }, isCalculated: true, generatedFromCaptureGroups: true, diff --git a/client/web/src/integration/insights/fixtures/insights-metadata.ts b/client/web/src/integration/insights/fixtures/insights-metadata.ts index e37c3671b50c..24c54cf92bbc 100644 --- a/client/web/src/integration/insights/fixtures/insights-metadata.ts +++ b/client/web/src/integration/insights/fixtures/insights-metadata.ts @@ -46,6 +46,10 @@ export const createJITMigrationToGQLInsightMetadataFixture = (options: InsightOp }, ], }, + repositoryDefinition: { + __typename: 'InsightRepositoryScope', + repositories: ['github.com/sourcegraph/sourcegraph'], + }, dataSeriesDefinitions: [ { __typename: 'SearchInsightDataSeriesDefinition', @@ -53,10 +57,6 @@ export const createJITMigrationToGQLInsightMetadataFixture = (options: InsightOp query: 'patternType:regex case:yes \\*\\sas\\sGQL', isCalculated: options.type === 'calculated', generatedFromCaptureGroups: false, - repositoryScope: { - __typename: 'InsightRepositoryScope', - repositories: ['github.com/sourcegraph/sourcegraph'], - }, timeScope: { __typename: 'InsightIntervalTimeScope', unit: TimeIntervalStepUnit.WEEK, @@ -70,10 +70,6 @@ export const createJITMigrationToGQLInsightMetadataFixture = (options: InsightOp query: "patternType:regexp case:yes /graphql-operations'", isCalculated: options.type === 'calculated', generatedFromCaptureGroups: false, - repositoryScope: { - __typename: 'InsightRepositoryScope', - repositories: ['github.com/sourcegraph/sourcegraph'], - }, timeScope: { __typename: 'InsightIntervalTimeScope', unit: TimeIntervalStepUnit.WEEK, @@ -111,6 +107,10 @@ export const STORYBOOK_GROWTH_INSIGHT_METADATA_FIXTURE: InsightViewNode = { }, ], }, + repositoryDefinition: { + __typename: 'InsightRepositoryScope', + repositories: ['github.com/sourcegraph/sourcegraph'], + }, dataSeriesDefinitions: [ { __typename: 'SearchInsightDataSeriesDefinition', @@ -118,10 +118,6 @@ export const STORYBOOK_GROWTH_INSIGHT_METADATA_FIXTURE: InsightViewNode = { query: 'patternType:regexp f:\\.story\\.tsx$ \\badd\\(', isCalculated: false, generatedFromCaptureGroups: false, - repositoryScope: { - __typename: 'InsightRepositoryScope', - repositories: ['github.com/sourcegraph/sourcegraph'], - }, timeScope: { __typename: 'InsightIntervalTimeScope', unit: TimeIntervalStepUnit.WEEK, @@ -152,14 +148,14 @@ export const SOURCEGRAPH_LANG_STATS_INSIGHT_METADATA_FIXTURE: InsightViewNode = title: 'Sourcegraph languages', otherThreshold: 0.03, }, + repositoryDefinition: { + __typename: 'InsightRepositoryScope', + repositories: ['github.com/sourcegraph/sourcegraph'], + }, dataSeriesDefinitions: [ { seriesId: '001', query: '', - repositoryScope: { - repositories: ['github.com/sourcegraph/sourcegraph'], - __typename: 'InsightRepositoryScope', - }, timeScope: { unit: TimeIntervalStepUnit.MONTH, value: 0, diff --git a/client/web/src/integration/insights/insight/dashboard-cards.test.ts b/client/web/src/integration/insights/insight/dashboard-cards.test.ts index 85410fc5d502..5f72f894cd93 100644 --- a/client/web/src/integration/insights/insight/dashboard-cards.test.ts +++ b/client/web/src/integration/insights/insight/dashboard-cards.test.ts @@ -100,6 +100,7 @@ describe('Code insights [Dashboard card]', () => { await driver.page.goto(driver.sourcegraphBaseUrl + '/insights/dashboards/DASHBOARD_WITH_SEARCH') await driver.page.waitForSelector('[aria-label="Line chart"]') + await driver.page.waitForSelector('[aria-label="Line chart"] path') const numberOfLines = await driver.page.$$eval('[aria-label="Line chart"] path', elements => elements.length) const numberOfPointLinks = await driver.page.$$eval('[aria-label="Line chart"] a', elements => elements.length) diff --git a/client/wildcard/src/components/Combobox/MultiCombobox.tsx b/client/wildcard/src/components/Combobox/MultiCombobox.tsx index 4b7924bac4b7..98e17219c93e 100644 --- a/client/wildcard/src/components/Combobox/MultiCombobox.tsx +++ b/client/wildcard/src/components/Combobox/MultiCombobox.tsx @@ -85,6 +85,7 @@ export interface MultiComboboxProps extends Omit { selectedItems: T[] getItemName: (item: T) => string getItemKey: (item: T) => string | number + className?: string onSelectedItemsChange: (selectedItems: T[]) => void } @@ -211,6 +212,7 @@ const MultiValueInput = forwardRef((props: MultiValueInputProps, ref: Ref { items: T[] children: (items: T[]) => ReactNode + renderEmptyList?: boolean className?: string } export function MultiComboboxList(props: MultiComboboxListProps): ReactElement | null { - const { items, children, className } = props + const { items, children, renderEmptyList = false, className } = props const { setSuggestOptions } = useContext(MultiComboboxContext) // Register rendered item in top level object in order to use it // when user selects one of these options useLayoutEffect(() => setSuggestOptions(items), [items, setSuggestOptions]) - if (items.length === 0) { + if (items.length === 0 && !renderEmptyList) { return null } From e1dc53373e2b16ffc01b6cd97330d44b2e0177c4 Mon Sep 17 00:00:00 2001 From: Idan Varsano Date: Thu, 19 Jan 2023 00:31:25 -0500 Subject: [PATCH 033/678] Implement ADO client (#46615) * Implemment ADO client --- internal/extsvc/azuredevops/client.go | 145 ++++++++++++++++++ internal/extsvc/azuredevops/client_test.go | 96 ++++++++++++ .../testdata/golden/ListProjects.json | 29 ++++ .../vcr/ListRepositoriesByProjectOrOrg.yaml | 54 +++++++ internal/extsvc/gerrit/client_test.go | 4 - 5 files changed, 324 insertions(+), 4 deletions(-) create mode 100644 internal/extsvc/azuredevops/client.go create mode 100644 internal/extsvc/azuredevops/client_test.go create mode 100644 internal/extsvc/azuredevops/testdata/golden/ListProjects.json create mode 100644 internal/extsvc/azuredevops/testdata/vcr/ListRepositoriesByProjectOrOrg.yaml diff --git a/internal/extsvc/azuredevops/client.go b/internal/extsvc/azuredevops/client.go new file mode 100644 index 000000000000..0b788d071ced --- /dev/null +++ b/internal/extsvc/azuredevops/client.go @@ -0,0 +1,145 @@ +//nolint:bodyclose // Body is closed in Client.Do, but the response is still returned to provide access to the headers +package azuredevops + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/sourcegraph/sourcegraph/internal/httpcli" + "github.com/sourcegraph/sourcegraph/internal/ratelimit" +) + +// Client used to access an ADO code host via the REST API. +type Client struct { + // HTTP Client used to communicate with the API. + httpClient httpcli.Doer + + // Config is the code host connection config for this client. + Config *ADOConnection + + // URL is the base URL of ADO. + URL *url.URL + + // RateLimit is the self-imposed rate limiter (since ADO does not have a concept + // of rate limiting in HTTP response headers). + rateLimit *ratelimit.InstrumentedLimiter +} + +// TODO: @varsanojidan remove this when the shcema is updated to include ADO: https://github.com/sourcegraph/sourcegraph/issues/46266. +type ADOConnection struct { + Username string + Token string +} + +// NewClient returns an authenticated ADO API client with +// the provided configuration. If a nil httpClient is provided, http.DefaultClient +// will be used. +func NewClient(urn string, config *ADOConnection, httpClient httpcli.Doer) (*Client, error) { + u, err := url.Parse("https://dev.azure.com") + if err != nil { + return nil, err + } + + if httpClient == nil { + httpClient = httpcli.ExternalDoer + } + + return &Client{ + httpClient: httpClient, + Config: config, + URL: u, + rateLimit: ratelimit.DefaultRegistry.Get(urn), + }, nil +} + +// ListRepositoriesByProjectOrOrgArgs defines options to be set on the ListRepositories methods' calls. +type ListRepositoriesByProjectOrOrgArgs struct { + // Should be in the form of 'org/project' for projects and 'org' for orgs. + ProjectOrOrgName string +} + +func (c *Client) ListRepositoriesByProjectOrOrg(ctx context.Context, opts ListRepositoriesByProjectOrOrgArgs) (*ListRepositoriesResponse, error) { + qs := make(url.Values) + + // TODO: @varsanojidan look into which API version/s we want to support. + qs.Set("api-version", "7.0") + + urlRepositoriesByProjects := url.URL{Path: fmt.Sprintf("%s/_apis/git/repositories", opts.ProjectOrOrgName), RawQuery: qs.Encode()} + + req, err := http.NewRequest("GET", urlRepositoriesByProjects.String(), nil) + if err != nil { + return nil, err + } + + var repos ListRepositoriesResponse + if _, err = c.do(ctx, req, &repos); err != nil { + return nil, err + } + + return &repos, nil +} + +//nolint:unparam // http.Response is never used, but it makes sense API wise. +func (c *Client) do(ctx context.Context, req *http.Request, result any) (*http.Response, error) { + req.URL = c.URL.ResolveReference(req.URL) + + // Add Basic Auth headers for authenticated requests. + req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(c.Config.Username+":"+c.Config.Token))) + + if err := c.rateLimit.Wait(ctx); err != nil { + return nil, err + } + + resp, err := c.httpClient.Do(req) + + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + bs, err := io.ReadAll(resp.Body) + + if err != nil { + return nil, err + } + + if resp.StatusCode < 200 || resp.StatusCode >= 400 { + return nil, &httpError{ + URL: req.URL, + StatusCode: resp.StatusCode, + Body: bs, + } + } + + return resp, json.Unmarshal(bs, result) +} + +type ListRepositoriesResponse struct { + Value []RepositoriesValue `json:"value"` + Count int `json:"count"` +} + +type RepositoriesValue struct { + ID string `json:"id"` + Name string `json:"name"` + APIURL string `json:"url"` + SSHURL string `json:"sshUrl"` + WebURL string `json:"webUrl"` + IsDisabled bool `json:"isDisabled"` +} + +type httpError struct { + StatusCode int + URL *url.URL + Body []byte +} + +func (e *httpError) Error() string { + return fmt.Sprintf("ADO API HTTP error: code=%d url=%q body=%q", e.StatusCode, e.URL, e.Body) +} diff --git a/internal/extsvc/azuredevops/client_test.go b/internal/extsvc/azuredevops/client_test.go new file mode 100644 index 000000000000..35662fe9a0f2 --- /dev/null +++ b/internal/extsvc/azuredevops/client_test.go @@ -0,0 +1,96 @@ +package azuredevops + +import ( + "context" + "flag" + "net/http" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/dnaeon/go-vcr/cassette" + "github.com/sourcegraph/sourcegraph/internal/httpcli" + "github.com/sourcegraph/sourcegraph/internal/httptestutil" + "github.com/sourcegraph/sourcegraph/internal/lazyregexp" + "github.com/sourcegraph/sourcegraph/internal/testutil" +) + +var update = flag.Bool("update", false, "update testdata") + +func TestClient_ListRepositoriesByProjectOrOrg(t *testing.T) { + cli, save := NewTestClient(t, "ListRepositoriesByProjectOrOrg", *update) + defer save() + + ctx := context.Background() + + opts := ListRepositoriesByProjectOrOrgArgs{ + // TODO: use an sg owned org rather than a personal. + ProjectOrOrgName: "sgadotest", + } + + resp, err := cli.ListRepositoriesByProjectOrOrg(ctx, opts) + if err != nil { + t.Fatal(err) + } + + testutil.AssertGolden(t, "testdata/golden/ListProjects.json", *update, resp) +} + +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} + +// NewTestClient returns an azuredevops.Client that records its interactions +// to testdata/vcr/. +func NewTestClient(t testing.TB, name string, update bool) (*Client, func()) { + t.Helper() + + cassete := filepath.Join("testdata/vcr/", normalize(name)) + rec, err := httptestutil.NewRecorder(cassete, update) + if err != nil { + t.Fatal(err) + } + rec.SetMatcher(ignoreHostMatcher) + + hc, err := httpcli.NewFactory(nil, httptestutil.NewRecorderOpt(rec)).Doer() + if err != nil { + t.Fatal(err) + } + + c := &ADOConnection{ + Username: "testuser", + Token: "testpassword", + } + + cli, err := NewClient("urn", c, hc) + if err != nil { + t.Fatal(err) + } + + return cli, func() { + if err := rec.Stop(); err != nil { + t.Errorf("failed to update test data: %s", err) + } + } +} + +var normalizer = lazyregexp.New("[^A-Za-z0-9-]+") + +func normalize(path string) string { + return normalizer.ReplaceAllLiteralString(path, "-") +} + +func ignoreHostMatcher(r *http.Request, i cassette.Request) bool { + if r.Method != i.Method { + return false + } + u, err := url.Parse(i.URL) + if err != nil { + return false + } + u.Host = r.URL.Host + u.Scheme = r.URL.Scheme + return r.URL.String() == u.String() +} diff --git a/internal/extsvc/azuredevops/testdata/golden/ListProjects.json b/internal/extsvc/azuredevops/testdata/golden/ListProjects.json new file mode 100644 index 000000000000..7767ffe9721c --- /dev/null +++ b/internal/extsvc/azuredevops/testdata/golden/ListProjects.json @@ -0,0 +1,29 @@ +{ + "value": [ + { + "id": "d43b669c-d6da-4c9e-9952-11f411153cf9", + "name": "sgadotest", + "url": "https://dev.azure.com/sgadotest/889cc0c6-c7ed-40f0-b6f1-053acfc1397d/_apis/git/repositories/d43b669c-d6da-4c9e-9952-11f411153cf9", + "sshUrl": "git@ssh.dev.azure.com:v3/sgadotest/sgadotest/sgadotest", + "webUrl": "https://dev.azure.com/sgadotest/sgadotest/_git/sgadotest", + "isDisabled": false + }, + { + "id": "47c49ea1-5dc8-468e-ae6d-52410a1084a7", + "name": "idano", + "url": "https://dev.azure.com/sgadotest/889cc0c6-c7ed-40f0-b6f1-053acfc1397d/_apis/git/repositories/47c49ea1-5dc8-468e-ae6d-52410a1084a7", + "sshUrl": "git@ssh.dev.azure.com:v3/sgadotest/sgadotest/idano", + "webUrl": "https://dev.azure.com/sgadotest/sgadotest/_git/idano", + "isDisabled": false + }, + { + "id": "2c8bc3c0-841b-478e-ae1b-f5736b4f9cf5", + "name": "sgadotest2", + "url": "https://dev.azure.com/sgadotest/450fc69b-ff88-4604-88d3-84b488e290b2/_apis/git/repositories/2c8bc3c0-841b-478e-ae1b-f5736b4f9cf5", + "sshUrl": "git@ssh.dev.azure.com:v3/sgadotest/sgadotest2/sgadotest2", + "webUrl": "https://dev.azure.com/sgadotest/sgadotest2/_git/sgadotest2", + "isDisabled": false + } + ], + "count": 3 + } \ No newline at end of file diff --git a/internal/extsvc/azuredevops/testdata/vcr/ListRepositoriesByProjectOrOrg.yaml b/internal/extsvc/azuredevops/testdata/vcr/ListRepositoriesByProjectOrOrg.yaml new file mode 100644 index 000000000000..ec91851125a6 --- /dev/null +++ b/internal/extsvc/azuredevops/testdata/vcr/ListRepositoriesByProjectOrOrg.yaml @@ -0,0 +1,54 @@ +--- +version: 1 +interactions: +- request: + body: "" + form: {} + headers: {} + url: https://dev.azure.com/sgadotest/_apis/git/repositories?api-version=7.0 + method: GET + response: + body: '{"value":[{"id":"d43b669c-d6da-4c9e-9952-11f411153cf9","name":"sgadotest","url":"https://dev.azure.com/sgadotest/889cc0c6-c7ed-40f0-b6f1-053acfc1397d/_apis/git/repositories/d43b669c-d6da-4c9e-9952-11f411153cf9","project":{"id":"889cc0c6-c7ed-40f0-b6f1-053acfc1397d","name":"sgadotest","url":"https://dev.azure.com/sgadotest/_apis/projects/889cc0c6-c7ed-40f0-b6f1-053acfc1397d","state":"wellFormed","revision":11,"visibility":"private","lastUpdateTime":"2023-01-17T22:13:58.063Z"},"size":0,"remoteUrl":"https://sgadotest@dev.azure.com/sgadotest/sgadotest/_git/sgadotest","sshUrl":"git@ssh.dev.azure.com:v3/sgadotest/sgadotest/sgadotest","webUrl":"https://dev.azure.com/sgadotest/sgadotest/_git/sgadotest","isDisabled":false,"isInMaintenance":false},{"id":"47c49ea1-5dc8-468e-ae6d-52410a1084a7","name":"idano","url":"https://dev.azure.com/sgadotest/889cc0c6-c7ed-40f0-b6f1-053acfc1397d/_apis/git/repositories/47c49ea1-5dc8-468e-ae6d-52410a1084a7","project":{"id":"889cc0c6-c7ed-40f0-b6f1-053acfc1397d","name":"sgadotest","url":"https://dev.azure.com/sgadotest/_apis/projects/889cc0c6-c7ed-40f0-b6f1-053acfc1397d","state":"wellFormed","revision":11,"visibility":"private","lastUpdateTime":"2023-01-17T22:13:58.063Z"},"defaultBranch":"refs/heads/main","size":726,"remoteUrl":"https://sgadotest@dev.azure.com/sgadotest/sgadotest/_git/idano","sshUrl":"git@ssh.dev.azure.com:v3/sgadotest/sgadotest/idano","webUrl":"https://dev.azure.com/sgadotest/sgadotest/_git/idano","isDisabled":false,"isInMaintenance":false},{"id":"2c8bc3c0-841b-478e-ae1b-f5736b4f9cf5","name":"sgadotest2","url":"https://dev.azure.com/sgadotest/450fc69b-ff88-4604-88d3-84b488e290b2/_apis/git/repositories/2c8bc3c0-841b-478e-ae1b-f5736b4f9cf5","project":{"id":"450fc69b-ff88-4604-88d3-84b488e290b2","name":"sgadotest2","url":"https://dev.azure.com/sgadotest/_apis/projects/450fc69b-ff88-4604-88d3-84b488e290b2","state":"wellFormed","revision":19,"visibility":"private","lastUpdateTime":"2023-01-18T05:29:24.46Z"},"size":0,"remoteUrl":"https://sgadotest@dev.azure.com/sgadotest/sgadotest2/_git/sgadotest2","sshUrl":"git@ssh.dev.azure.com:v3/sgadotest/sgadotest2/sgadotest2","webUrl":"https://dev.azure.com/sgadotest/sgadotest2/_git/sgadotest2","isDisabled":false,"isInMaintenance":false}],"count":3}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Activityid: + - bb687111-e265-4a33-8b7f-5b4d55d3b878 + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Type: + - application/json; charset=utf-8; api-version=7.0 + Date: + - Wed, 18 Jan 2023 14:49:16 GMT + Expires: + - "-1" + P3p: + - CP="CAO DSP COR ADMa DEV CONo TELo CUR PSA PSD TAI IVDo OUR SAMi BUS DEM NAV + STA UNI COM INT PHY ONL FIN PUR LOC CNT" + Pragma: + - no-cache + Request-Context: + - appId=cid-v1:72d31d95-1757-44f5-b910-a46611808454 + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Cache: + - CONFIG_NOCACHE + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Msedge-Ref: + - 'Ref A: D4246B6C514F430BA3FB288B164F66E5 Ref B: YTO01EDGE0510 Ref C: 2023-01-18T14:49:15Z' + X-Tfs-Processid: + - a9e61ddf-ab88-4476-9b7e-1af9921bf7ed + X-Tfs-Session: + - bb687111-e265-4a33-8b7f-5b4d55d3b878 + X-Vss-E2eid: + - bb687111-e265-4a33-8b7f-5b4d55d3b878 + X-Vss-Senderdeploymentid: + - 4ff21e82-8865-0b2e-ffe8-9598818f8190 + X-Vss-Userdata: + - 003fb9ec-23b7-699d-9973-544b77eb2595:varsanojidan@gmail.com + status: 200 OK + code: 200 + duration: "" diff --git a/internal/extsvc/gerrit/client_test.go b/internal/extsvc/gerrit/client_test.go index c7e7152aa70d..841bd72c5f31 100644 --- a/internal/extsvc/gerrit/client_test.go +++ b/internal/extsvc/gerrit/client_test.go @@ -10,7 +10,6 @@ import ( "testing" "github.com/dnaeon/go-vcr/cassette" - "github.com/inconshreveable/log15" "github.com/sourcegraph/sourcegraph/internal/httpcli" "github.com/sourcegraph/sourcegraph/internal/httptestutil" "github.com/sourcegraph/sourcegraph/internal/lazyregexp" @@ -40,9 +39,6 @@ func TestClient_ListProjects(t *testing.T) { func TestMain(m *testing.M) { flag.Parse() - if !testing.Verbose() { - log15.Root().SetHandler(log15.LvlFilterHandler(log15.LvlError, log15.Root().GetHandler())) - } os.Exit(m.Run()) } From e449cee2a93f3017b575be3d617a11cf1f007b8d Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Thu, 19 Jan 2023 11:29:48 +0200 Subject: [PATCH 034/678] rcache: only get connection in Set if it will be used (#46655) Minor optimization, we would acquire a connection from the pool twice for Set with TTL since Set would acquire a connection but then just call SetWithTTL which does the same thing. Test Plan: go test --- internal/rcache/rcache.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/rcache/rcache.go b/internal/rcache/rcache.go index 2d2e706121d0..a54190c07b83 100644 --- a/internal/rcache/rcache.go +++ b/internal/rcache/rcache.go @@ -130,14 +130,14 @@ func (r *Cache) Get(key string) ([]byte, bool) { // Set implements httpcache.Cache.Set func (r *Cache) Set(key string, b []byte) { - c := poolGet() - defer c.Close() - if !utf8.Valid([]byte(key)) { log15.Error("rcache: keys must be valid utf8", "key", []byte(key)) } if r.ttlSeconds == 0 { + c := poolGet() + defer c.Close() + _, err := c.Do("SET", r.rkeyPrefix()+key, b) if err != nil { log15.Warn("failed to execute redis command", "cmd", "SET", "error", err) From 464e5bf4eefbb63ab8f102b24b74459c51f440bd Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Thu, 19 Jan 2023 12:23:50 +0200 Subject: [PATCH 035/678] redispool: add our uses of redis list to KeyValue (#46657) This adds what is currently missing in our use of redis to the generic interface. This interface is getting quite large, but I'd like to follow up once I've implemented postgres and memory keyvalue to see how to maybe split this up. Test Plan: added unit tests --- internal/redispool/keyvalue.go | 23 +++++++++++ internal/redispool/keyvalue_test.go | 63 +++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/internal/redispool/keyvalue.go b/internal/redispool/keyvalue.go index 236098919507..b3a8af5634c8 100644 --- a/internal/redispool/keyvalue.go +++ b/internal/redispool/keyvalue.go @@ -27,6 +27,11 @@ type KeyValue interface { HGet(key, field string) Value HSet(key, field string, value any) error + LPush(key string, value any) error + LTrim(key string, start, stop int) error + LLen(key string) (int, error) + LRange(key string, start, stop int) Value + Expire(key string, seconds int) error // WithContext will return a KeyValue that should respect ctx for all @@ -58,6 +63,10 @@ func (v Value) Bytes() ([]byte, error) { return redis.Bytes(v.reply, v.err) } +func (v Value) ByteSlices() ([][]byte, error) { + return redis.ByteSlices(redis.Values(v.reply, v.err)) +} + func (v Value) String() (string, error) { return redis.String(v.reply, v.err) } @@ -105,6 +114,20 @@ func (r *redisKeyValue) HSet(key, field string, val any) error { return r.do("HSET", r.prefix+key, field, val).err } +func (r *redisKeyValue) LPush(key string, value any) error { + return r.do("LPUSH", r.prefix+key, value).err +} +func (r *redisKeyValue) LTrim(key string, start, stop int) error { + return r.do("LTRIM", r.prefix+key, start, stop).err +} +func (r *redisKeyValue) LLen(key string) (int, error) { + raw := r.do("LLEN", r.prefix+key) + return redis.Int(raw.reply, raw.err) +} +func (r *redisKeyValue) LRange(key string, start, stop int) Value { + return r.do("LRANGE", r.prefix+key, start, stop) +} + func (r *redisKeyValue) Expire(key string, seconds int) error { return r.do("EXPIRE", r.prefix+key, seconds).err } diff --git a/internal/redispool/keyvalue_test.go b/internal/redispool/keyvalue_test.go index a94a888c016d..6188ff81b9ef 100644 --- a/internal/redispool/keyvalue_test.go +++ b/internal/redispool/keyvalue_test.go @@ -41,6 +41,12 @@ func testKeyValue(t *testing.T, kv redispool.KeyValue) { if !reflect.DeepEqual(gotV, wantV) { t.Fatalf("got %q, wanted %q", gotV, wantV) } + case [][]byte: + gotV, err := got.ByteSlices() + assertWorks(err) + if !reflect.DeepEqual(gotV, wantV) { + t.Fatalf("got %q, wanted %q", gotV, wantV) + } case string: gotV, err := got.String() assertWorks(err) @@ -64,6 +70,16 @@ func testKeyValue(t *testing.T, kv redispool.KeyValue) { t.Fatalf("unsupported want type for %q: %T", want, want) } } + assertListLen := func(key string, want int) { + t.Helper() + got, err := kv.LLen(key) + if err != nil { + t.Fatal("LLen returned error", err) + } + if got != want { + t.Fatalf("unexpected list length got=%d want=%d", got, want) + } + } // Redis returns nil on unset values assertEqual(kv.Get("hi"), redis.ErrNil) @@ -115,6 +131,45 @@ func testKeyValue(t *testing.T, kv redispool.KeyValue) { assertWorks(kv.HSet("hash", "funky", []byte{0, 10, 100, 255})) assertEqual(kv.HGet("hash", "funky"), []byte{0, 10, 100, 255}) + // Lists + + // Redis behaviour on unset lists + assertListLen("list-unset-0", 0) + assertEqual(kv.LRange("list-unset-1", 0, 10), bytes()) + assertWorks(kv.LTrim("list-unset-2", 0, 10)) + + assertWorks(kv.LPush("list", "4")) + assertWorks(kv.LPush("list", "3")) + assertWorks(kv.LPush("list", "2")) + assertWorks(kv.LPush("list", "1")) + assertWorks(kv.LPush("list", "0")) + + // Different ways we get the full list back + assertEqual(kv.LRange("list", 0, 10), bytes("0", "1", "2", "3", "4")) + assertEqual(kv.LRange("list", 0, -1), bytes("0", "1", "2", "3", "4")) + assertEqual(kv.LRange("list", -5, -1), bytes("0", "1", "2", "3", "4")) + assertEqual(kv.LRange("list", 0, 4), bytes("0", "1", "2", "3", "4")) + + // Subsets + assertEqual(kv.LRange("list", 1, 3), bytes("1", "2", "3")) + assertEqual(kv.LRange("list", 1, -2), bytes("1", "2", "3")) + assertEqual(kv.LRange("list", -4, 3), bytes("1", "2", "3")) + assertEqual(kv.LRange("list", -4, -2), bytes("1", "2", "3")) + + // Trim noop + assertWorks(kv.LTrim("list", 0, 10)) + assertEqual(kv.LRange("list", 0, 4), bytes("0", "1", "2", "3", "4")) + + // Trim popback + assertWorks(kv.LTrim("list", 0, -2)) + assertEqual(kv.LRange("list", 0, 4), bytes("0", "1", "2", "3")) + assertListLen("list", 4) + + // Trim popfront + assertWorks(kv.LTrim("list", 1, 10)) + assertEqual(kv.LRange("list", 0, 4), bytes("1", "2", "3")) + assertListLen("list", 3) + // We intentionally do not test EXPIRE since I don't like sleeps in tests. } @@ -186,3 +241,11 @@ return result _, err := c.Do("EVAL", script, 0, prefix+":*", deleteBatchSize) return err } + +func bytes(ss ...string) [][]byte { + bs := make([][]byte, 0, len(ss)) + for _, s := range ss { + bs = append(bs, []byte(s)) + } + return bs +} From a415aea5588e6f1eeab2e211343f1c8b60aca4cb Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Thu, 19 Jan 2023 12:36:38 +0200 Subject: [PATCH 036/678] rcache: use KeyValue abstraction in FIFOList (#46658) I can see things we can improve here now that this exists, but this feels quite nice. Test Plan: go test --- internal/rcache/fifo_list.go | 43 ++++++++++++------------------------ 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/internal/rcache/fifo_list.go b/internal/rcache/fifo_list.go index 89725da91bdb..636cda760e42 100644 --- a/internal/rcache/fifo_list.go +++ b/internal/rcache/fifo_list.go @@ -5,7 +5,6 @@ import ( "fmt" "unicode/utf8" - "github.com/gomodule/redigo/redis" "github.com/sourcegraph/sourcegraph/lib/errors" "go.uber.org/atomic" ) @@ -26,9 +25,6 @@ func NewFIFOList(key string, size int) *FIFOList { // Insert b in the cache and drops the oldest inserted item if the size exceeds the configured limit. func (l *FIFOList) Insert(b []byte) error { - c := poolGet() - defer c.Close() - if !utf8.Valid(b) { errors.Newf("rcache: keys must be valid utf8", "key", b) } @@ -36,34 +32,29 @@ func (l *FIFOList) Insert(b []byte) error { // Special case maxSize 0 to mean keep the list empty. Used to handle // disabling. - if l.maxSize.Load() == 0 { - _, err := c.Do("LTRIM", key, 0, 0) - if err != nil { + maxSize := l.MaxSize() + if maxSize == 0 { + if err := pool.LTrim(key, 0, 0); err != nil { return errors.Wrap(err, "failed to execute redis command LTRIM") } return nil } // O(1) because we're just adding a single element. - _, err := c.Do("LPUSH", key, b) - if err != nil { + if err := pool.LPush(key, b); err != nil { return errors.Wrap(err, "failed to execute redis command LPUSH") } // O(1) because the average case if just about dropping the last element. - _, err = c.Do("LTRIM", key, 0, l.maxSize.Load()-1) - if err != nil { + if err := pool.LTrim(key, 0, maxSize-1); err != nil { return errors.Wrap(err, "failed to execute redis command LTRIM") } return nil } func (l *FIFOList) Size() (int, error) { - c := poolGet() - defer c.Close() - key := l.globalPrefixKey() - n, err := redis.Int(c.Do("LLEN", key)) + n, err := pool.LLen(key) if err != nil { return 0, errors.Wrap(err, "failed to execute redis command LLEN") } @@ -93,27 +84,21 @@ func (l *FIFOList) All(ctx context.Context) ([][]byte, error) { // // This a O(n) operation, where n is the list size. func (l *FIFOList) Slice(ctx context.Context, from, to int) ([][]byte, error) { - c, err := poolGetContext(ctx) - if err != nil { - return nil, errors.Wrap(err, "get redis conn") - } - defer c.Close() - select { - case <-ctx.Done(): + // Return early if context is already cancelled + if ctx.Err() != nil { return nil, ctx.Err() - default: } key := l.globalPrefixKey() - res, err := redis.Values(c.Do("LRANGE", key, from, to)) - if err != nil { - return nil, err - } - bs, err := redis.ByteSlices(res, nil) + bs, err := pool.WithContext(ctx).LRange(key, from, to).ByteSlices() if err != nil { + // Return ctx error if it expired + if ctx.Err() != nil { + return nil, ctx.Err() + } return nil, err } - if maxSize := int(l.maxSize.Load()); len(bs) > maxSize { + if maxSize := l.MaxSize(); len(bs) > maxSize { bs = bs[:maxSize] } return bs, nil From 7b88ec6273b6fb5c116138c578e4998e748f837e Mon Sep 17 00:00:00 2001 From: Bolaji Olajide <25608335+BolajiOlajide@users.noreply.github.com> Date: Thu, 19 Jan 2023 13:05:41 +0100 Subject: [PATCH 037/678] rbac: remove default apply from schema (#46635) --- internal/rbac/types.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/rbac/types.go b/internal/rbac/types.go index 554dcc2148ad..c66b721c1c97 100644 --- a/internal/rbac/types.go +++ b/internal/rbac/types.go @@ -8,7 +8,6 @@ type Schema struct { // Namespace represents a feature to be guarded by RBAC. (example: Batch Changes, Code Insights e.t.c) type Namespace struct { - Name string `json:"name"` - Actions []string `json:"actions"` - DefaultApply bool `json:"defaultApply"` + Name string `json:"name"` + Actions []string `json:"actions"` } From 735e08416ed895d2c164fe77a381eb36e5325559 Mon Sep 17 00:00:00 2001 From: Bolaji Olajide <25608335+BolajiOlajide@users.noreply.github.com> Date: Thu, 19 Jan 2023 14:33:47 +0100 Subject: [PATCH 038/678] rbac: add default roles to database and map existing users to roles (#45854) --- dev/sg/internal/db/db.go | 2 +- internal/database/roles.go | 79 +++++++++------- internal/database/roles_test.go | 76 ++++++++------- internal/database/schema.json | 2 +- internal/database/schema.md | 4 +- internal/database/user_roles.go | 62 ++++++++++-- internal/database/user_roles_test.go | 45 ++++++++- .../batches/role_assignment_migrator.go | 82 ++++++++++++++++ .../batches/role_assignment_migrator_test.go | 94 +++++++++++++++++++ internal/oobmigration/migrations/register.go | 1 + internal/oobmigration/oobmigrations.yaml | 8 ++ internal/types/types.go | 9 +- .../1671543381_add_default_roles/down.sql | 1 + .../metadata.yaml | 2 + .../1671543381_add_default_roles/up.sql | 9 ++ .../down.sql | 15 +++ .../metadata.yaml | 2 + .../up.sql | 15 +++ migrations/frontend/squashed.sql | 11 ++- 19 files changed, 434 insertions(+), 85 deletions(-) create mode 100644 internal/oobmigration/migrations/batches/role_assignment_migrator.go create mode 100644 internal/oobmigration/migrations/batches/role_assignment_migrator_test.go create mode 100644 migrations/frontend/1671543381_add_default_roles/down.sql create mode 100644 migrations/frontend/1671543381_add_default_roles/metadata.yaml create mode 100644 migrations/frontend/1671543381_add_default_roles/up.sql create mode 100644 migrations/frontend/1674047296_rename_roles_readonly_column_to_system/down.sql create mode 100644 migrations/frontend/1674047296_rename_roles_readonly_column_to_system/metadata.yaml create mode 100644 migrations/frontend/1674047296_rename_roles_readonly_column_to_system/up.sql diff --git a/dev/sg/internal/db/db.go b/dev/sg/internal/db/db.go index a12fa87f3446..4c3c81afea30 100644 --- a/dev/sg/internal/db/db.go +++ b/dev/sg/internal/db/db.go @@ -47,7 +47,7 @@ var ( Name: "frontend", MigrationsTable: "schema_migrations", FS: GetFSForPath("frontend"), - DataTables: []string{"lsif_configuration_policies"}, + DataTables: []string{"lsif_configuration_policies", "roles"}, CountTables: nil, } diff --git a/internal/database/roles.go b/internal/database/roles.go index b5e1782f35d9..9dd61cdb22d2 100644 --- a/internal/database/roles.go +++ b/internal/database/roles.go @@ -16,20 +16,14 @@ import ( var roleColumns = []*sqlf.Query{ sqlf.Sprintf("roles.id"), sqlf.Sprintf("roles.name"), - sqlf.Sprintf("roles.readonly"), + sqlf.Sprintf("roles.system"), sqlf.Sprintf("roles.created_at"), sqlf.Sprintf("roles.deleted_at"), } -var permissionColumnsForRole = []*sqlf.Query{ - sqlf.Sprintf("permissions.id AS permission_id"), - sqlf.Sprintf("permissions.action"), - sqlf.Sprintf("permissions.namespace"), -} - var roleInsertColumns = []*sqlf.Query{ sqlf.Sprintf("name"), - sqlf.Sprintf("readonly"), + sqlf.Sprintf("system"), } type RoleStore interface { @@ -38,11 +32,12 @@ type RoleStore interface { // Count counts all roles in the database. Count(ctx context.Context, opts RolesListOptions) (int, error) // Create inserts the given role into the database. - Create(ctx context.Context, name string, readonly bool) (*types.Role, error) + Create(ctx context.Context, name string, isSystemRole bool) (*types.Role, error) // Delete removes an existing role from the database. Delete(ctx context.Context, opts DeleteRoleOpts) error - // GetByID returns the role matching the given ID, or RoleNotFoundErr if no such record exists. - GetByID(ctx context.Context, opts GetRoleOpts) (*types.Role, error) + // Get returns the role matching the given ID or name provided. If no such role exists, + // a RoleNotFoundErr is returned. + Get(ctx context.Context, opts GetRoleOpts) (*types.Role, error) // List returns all roles matching the given options. List(ctx context.Context, opts RolesListOptions) ([]*types.Role, error) // Update updates an existing role in the database. @@ -54,7 +49,8 @@ func RolesWith(other basestore.ShareableStore) RoleStore { } type RoleOpts struct { - ID int32 + ID int32 + Name string } type ( @@ -64,6 +60,7 @@ type ( type RolesListOptions struct { *LimitOffset + System bool } type RoleNotFoundErr struct { @@ -90,17 +87,26 @@ WHERE %s LIMIT 1; ` -func (r *roleStore) GetByID(ctx context.Context, opts GetRoleOpts) (*types.Role, error) { - if opts.ID <= 0 { - return nil, errors.New("missing id from sql query") +func (r *roleStore) Get(ctx context.Context, opts GetRoleOpts) (*types.Role, error) { + if opts.ID == 0 && opts.Name == "" { + return nil, errors.New("missing id or name") + } + + var conds []*sqlf.Query + if opts.ID != 0 { + conds = append(conds, sqlf.Sprintf("id = %s", opts.ID)) } - whereClause := sqlf.Sprintf("id = %s AND deleted_at IS NULL", opts.ID) + if opts.Name != "" { + conds = append(conds, sqlf.Sprintf("name = %s", opts.Name)) + } + + conds = append(conds, sqlf.Sprintf("deleted_at IS NULL")) q := sqlf.Sprintf( getRoleFmtStr, sqlf.Join(roleColumns, ", "), - whereClause, + sqlf.Join(conds, " AND "), ) role, err := scanRole(r.QueryRow(ctx, q)) @@ -119,7 +125,7 @@ func scanRole(sc dbutil.Scanner) (*types.Role, error) { if err := sc.Scan( &role.ID, &role.Name, - &role.ReadOnly, + &role.System, &role.CreatedAt, &dbutil.NullTime{Time: &role.DeletedAt}, ); err != nil { @@ -129,13 +135,6 @@ func scanRole(sc dbutil.Scanner) (*types.Role, error) { return &role, nil } -const roleListQueryFmtstr = ` -SELECT - %s -FROM roles -WHERE %s -` - func (r *roleStore) List(ctx context.Context, opts RolesListOptions) ([]*types.Role, error) { roles := make([]*types.Role, 0, 20) @@ -148,14 +147,26 @@ func (r *roleStore) List(ctx context.Context, opts RolesListOptions) ([]*types.R return nil } - err := r.list(ctx, opts, sqlf.Join(roleColumns, ", "), scanFunc) + err := r.list(ctx, opts, sqlf.Join(roleColumns, ", "), sqlf.Sprintf("ORDER BY roles.created_at ASC"), scanFunc) return roles, err } -func (r *roleStore) list(ctx context.Context, opts RolesListOptions, selects *sqlf.Query, scanRole func(rows *sql.Rows) error) error { - var whereClause = sqlf.Sprintf("deleted_at IS NULL") +const roleListQueryFmtstr = ` +SELECT + %s +FROM roles +WHERE %s +%s +` + +func (r *roleStore) list(ctx context.Context, opts RolesListOptions, selects, orderByQuery *sqlf.Query, scanRole func(rows *sql.Rows) error) error { + var conds = []*sqlf.Query{sqlf.Sprintf("deleted_at IS NULL")} + + if opts.System { + conds = append(conds, sqlf.Sprintf("system IS TRUE")) + } - q := sqlf.Sprintf(roleListQueryFmtstr, selects, whereClause) + q := sqlf.Sprintf(roleListQueryFmtstr, selects, sqlf.Join(conds, " AND "), orderByQuery) if opts.LimitOffset != nil { q = sqlf.Sprintf("%s\n%s", q, opts.LimitOffset.SQL()) @@ -186,12 +197,12 @@ INSERT INTO ` -func (r *roleStore) Create(ctx context.Context, name string, readonly bool) (_ *types.Role, err error) { +func (r *roleStore) Create(ctx context.Context, name string, isSystemRole bool) (_ *types.Role, err error) { q := sqlf.Sprintf( roleCreateQueryFmtStr, sqlf.Join(roleInsertColumns, ", "), name, - readonly, + isSystemRole, // Returning sqlf.Join(roleColumns, ", "), ) @@ -206,7 +217,7 @@ func (r *roleStore) Create(ctx context.Context, name string, readonly bool) (_ * func (r *roleStore) Count(ctx context.Context, opts RolesListOptions) (c int, err error) { opts.LimitOffset = nil - err = r.list(ctx, opts, sqlf.Sprintf("COUNT(1)"), func(rows *sql.Rows) error { + err = r.list(ctx, opts, sqlf.Sprintf("COUNT(1)"), sqlf.Sprintf(""), func(rows *sql.Rows) error { return rows.Scan(&c) }) return c, err @@ -239,7 +250,7 @@ const roleDeleteQueryFmtStr = ` UPDATE roles SET deleted_at = NOW() -WHERE id = %s AND NOT readonly +WHERE id = %s AND NOT system ` func (r *roleStore) Delete(ctx context.Context, opts DeleteRoleOpts) error { @@ -247,7 +258,7 @@ func (r *roleStore) Delete(ctx context.Context, opts DeleteRoleOpts) error { return errors.New("missing id from sql query") } - // We don't allow deletion of readonly roles such as DEFAULT & SITE_ADMINISTRATOR + // We don't allow deletion of system roles such as USER & SITE_ADMINISTRATOR q := sqlf.Sprintf(roleDeleteQueryFmtStr, opts.ID) result, err := r.ExecResult(ctx, q) if err != nil { diff --git a/internal/database/roles_test.go b/internal/database/roles_test.go index 6d6e4a5c8b05..48b51a107cf1 100644 --- a/internal/database/roles_test.go +++ b/internal/database/roles_test.go @@ -12,42 +12,46 @@ import ( "github.com/sourcegraph/sourcegraph/internal/types" ) -func TestRoleGetByID(t *testing.T) { - t.Parallel() - +// The database is already seeded with two roles: +// - DEFAULT +// - SITE_ADMINISTRATOR +// +// These roles come by default on any sourcegraph instance and will always exist in the database, +// so we need to account for these roles when accessing the database. +var numberOfDefaultRoles = 2 + +func TestRoleGet(t *testing.T) { ctx := context.Background() logger := logtest.Scoped(t) db := NewDB(logger, dbtest.NewDB(logger, t)) store := db.Roles() - created, err := store.Create(ctx, "OPERATOR", true) - if err != nil { - t.Fatal(err, "unable to create role") - } + roleName := "OPERATOR" + createdRole, err := store.Create(ctx, roleName, true) + assert.NoError(t, err) - t.Run("no ID", func(t *testing.T) { - role, err := store.GetByID(ctx, GetRoleOpts{}) + t.Run("without role ID or name", func(t *testing.T) { + _, err := store.Get(ctx, GetRoleOpts{}) assert.Error(t, err) - assert.Nil(t, role) - assert.Equal(t, err.Error(), "missing id from sql query") + assert.Equal(t, err.Error(), "missing id or name") }) - t.Run("non-existent role", func(t *testing.T) { - role, err := store.GetByID(ctx, GetRoleOpts{ID: 100}) - assert.Error(t, err) - assert.EqualError(t, err, "role with ID 100 not found") - assert.Nil(t, role) + t.Run("with role ID", func(t *testing.T) { + role, err := store.Get(ctx, GetRoleOpts{ + ID: createdRole.ID, + }) + assert.NoError(t, err) + assert.Equal(t, role.ID, createdRole.ID) + assert.Equal(t, role.Name, createdRole.Name) }) - t.Run("existing role", func(t *testing.T) { - role, err := store.GetByID(ctx, GetRoleOpts{ID: created.ID}) + t.Run("with role name", func(t *testing.T) { + role, err := store.Get(ctx, GetRoleOpts{ + Name: roleName, + }) assert.NoError(t, err) - assert.NotNil(t, role) - assert.Equal(t, role.ID, created.ID) - assert.Equal(t, role.Name, created.Name) - assert.Equal(t, role.ReadOnly, created.ReadOnly) - assert.Equal(t, role.CreatedAt, created.CreatedAt) - assert.Equal(t, role.DeletedAt, created.DeletedAt) + assert.Equal(t, role.ID, createdRole.ID) + assert.Equal(t, role.Name, createdRole.Name) }) } @@ -62,7 +66,15 @@ func TestRoleList(t *testing.T) { t.Run("basic no opts", func(t *testing.T) { allRoles, err := store.List(ctx, RolesListOptions{}) assert.NoError(t, err) - assert.Len(t, allRoles, total) + assert.Len(t, allRoles, total+numberOfDefaultRoles) + }) + + t.Run("system roles", func(t *testing.T) { + allSystemRoles, err := store.List(ctx, RolesListOptions{ + System: true, + }) + assert.NoError(t, err) + assert.Len(t, allSystemRoles, numberOfDefaultRoles) }) t.Run("with pagination", func(t *testing.T) { @@ -71,8 +83,6 @@ func TestRoleList(t *testing.T) { }) assert.NoError(t, err) assert.Len(t, roles, 2) - assert.Equal(t, roles[0].ID, int32(2)) - assert.Equal(t, roles[1].ID, int32(3)) }) } @@ -97,7 +107,7 @@ func TestRoleCount(t *testing.T) { count, err := store.Count(ctx, RolesListOptions{}) assert.NoError(t, err) - assert.Equal(t, count, total) + assert.Equal(t, count, total+numberOfDefaultRoles) } func TestRoleUpdate(t *testing.T) { @@ -147,10 +157,10 @@ func TestRoleDelete(t *testing.T) { role, err := createTestRole(ctx, "TEST ROLE 1", false, t, store) assert.NoError(t, err) - err = store.Delete(ctx, DeleteRoleOpts{role.ID}) + err = store.Delete(ctx, DeleteRoleOpts{ID: role.ID}) assert.NoError(t, err) - r, err := store.GetByID(ctx, GetRoleOpts{role.ID}) + r, err := store.Get(ctx, GetRoleOpts{ID: role.ID}) assert.Error(t, err) assert.Equal(t, err, &RoleNotFoundErr{role.ID}) assert.Nil(t, r) @@ -158,7 +168,7 @@ func TestRoleDelete(t *testing.T) { t.Run("non-existent role", func(t *testing.T) { nonExistentRoleID := int32(2381) - err := store.Delete(ctx, DeleteRoleOpts{nonExistentRoleID}) + err := store.Delete(ctx, DeleteRoleOpts{ID: nonExistentRoleID}) assert.Error(t, err) assert.ErrorContains(t, err, "failed to delete role") }) @@ -175,7 +185,7 @@ func createTestRoles(ctx context.Context, t *testing.T, store RoleStore) int { return totalRoles } -func createTestRole(ctx context.Context, name string, readonly bool, t *testing.T, store RoleStore) (*types.Role, error) { +func createTestRole(ctx context.Context, name string, isSystemRole bool, t *testing.T, store RoleStore) (*types.Role, error) { t.Helper() - return store.Create(ctx, name, readonly) + return store.Create(ctx, name, isSystemRole) } diff --git a/internal/database/schema.json b/internal/database/schema.json index 60eceba32a00..305fda43d83c 100755 --- a/internal/database/schema.json +++ b/internal/database/schema.json @@ -19179,7 +19179,7 @@ "Comment": "The uniquely identifying name of the role." }, { - "Name": "readonly", + "Name": "system", "Index": 5, "TypeName": "boolean", "IsNullable": false, diff --git a/internal/database/schema.md b/internal/database/schema.md index d309c8f38395..a7d6c08a3d4c 100755 --- a/internal/database/schema.md +++ b/internal/database/schema.md @@ -2969,7 +2969,7 @@ Foreign-key constraints: name | text | | not null | created_at | timestamp with time zone | | not null | now() deleted_at | timestamp with time zone | | | - readonly | boolean | | not null | false + system | boolean | | not null | false Indexes: "roles_pkey" PRIMARY KEY, btree (id) "roles_name" UNIQUE CONSTRAINT, btree (name) @@ -2983,7 +2983,7 @@ Referenced by: **name**: The uniquely identifying name of the role. -**readonly**: This is used to indicate whether a role is read-only or can be modified. +**system**: This is used to indicate whether a role is read-only or can be modified. # Table "public.saved_searches" ``` diff --git a/internal/database/user_roles.go b/internal/database/user_roles.go index c327ce67d0bd..53a93883c2a2 100644 --- a/internal/database/user_roles.go +++ b/internal/database/user_roles.go @@ -35,11 +35,19 @@ type ( GetUserRoleOpts UserRoleOpts ) +type BulkCreateForUserOpts struct { + UserID int32 + RoleIDs []int32 +} + type UserRoleStore interface { basestore.ShareableStore // Create inserts the given user and role relationship into the database. Create(ctx context.Context, opts CreateUserRoleOpts) (*types.UserRole, error) + // BulkCreateForUser assigns multiple roles to a single user. This is useful + // when we want to assign a user more than one role. + BulkCreateForUser(ctx context.Context, opts BulkCreateForUserOpts) ([]*types.UserRole, error) // GetByRoleID returns all UserRole associated with the provided role ID GetByRoleID(ctx context.Context, opts GetUserRoleOpts) ([]*types.UserRole, error) // GetByRoleIDAndUserID returns one UserRole associated with the provided role and user. @@ -76,10 +84,7 @@ func (r *userRoleStore) Transact(ctx context.Context) (UserRoleStore, error) { const userRoleCreateQueryFmtStr = ` INSERT INTO user_roles (%s) -VALUES ( - %s, - %s -) +VALUES %s RETURNING %s; ` @@ -95,8 +100,7 @@ func (r *userRoleStore) Create(ctx context.Context, opts CreateUserRoleOpts) (*t q := sqlf.Sprintf( userRoleCreateQueryFmtStr, sqlf.Join(userRoleInsertColumns, ", "), - opts.UserID, - opts.RoleID, + sqlf.Sprintf("( %s, %s )", opts.UserID, opts.RoleID), sqlf.Join(userRoleColumns, ", "), ) @@ -107,6 +111,46 @@ func (r *userRoleStore) Create(ctx context.Context, opts CreateUserRoleOpts) (*t return rm, nil } +func (r *userRoleStore) BulkCreateForUser(ctx context.Context, opts BulkCreateForUserOpts) ([]*types.UserRole, error) { + if opts.UserID == 0 { + return nil, errors.New("missing user id") + } + + if len(opts.RoleIDs) == 0 { + return nil, errors.New("missing role ids") + } + + var urs []*sqlf.Query + + for _, roleId := range opts.RoleIDs { + urs = append(urs, sqlf.Sprintf("(%s, %s)", opts.UserID, roleId)) + } + + q := sqlf.Sprintf( + userRoleCreateQueryFmtStr, + sqlf.Join(userRoleInsertColumns, ", "), + sqlf.Join(urs, ", "), + sqlf.Join(userRoleColumns, ", "), + ) + + rows, err := r.Query(ctx, q) + if err != nil { + return nil, errors.Wrap(err, "error running query") + } + defer rows.Close() + + var userRoles []*types.UserRole + for rows.Next() { + ur, err := scanUserRole(rows) + if err != nil { + return userRoles, err + } + userRoles = append(userRoles, ur) + } + + return userRoles, nil +} + type UserRoleNotFoundErr struct { UserID int32 RoleID int32 @@ -176,7 +220,7 @@ func (r *userRoleStore) GetByUserID(ctx context.Context, opts GetUserRoleOpts) ( } func (r *userRoleStore) GetByRoleID(ctx context.Context, opts GetUserRoleOpts) ([]*types.UserRole, error) { - role, err := RolesWith(r).GetByID(ctx, GetRoleOpts{ + role, err := RolesWith(r).Get(ctx, GetRoleOpts{ ID: opts.RoleID, }) if err != nil { @@ -248,12 +292,12 @@ WHERE %s ` func (r *userRoleStore) get(ctx context.Context, w *sqlf.Query, scanFunc func(rows *sql.Rows) error) error { - whereClause := sqlf.Sprintf("%s AND users.deleted_at IS NULL", w) + conds := sqlf.Sprintf("%s AND users.deleted_at IS NULL", w) q := sqlf.Sprintf( getUserRoleQueryFmtStr, sqlf.Join(userRoleColumns, ", "), sqlf.Sprintf("INNER JOIN users ON user_roles.user_id = users.id"), - whereClause, + conds, ) rows, err := r.Query(ctx, q) diff --git a/internal/database/user_roles_test.go b/internal/database/user_roles_test.go index 484c972dce87..cd75ed3e7ad9 100644 --- a/internal/database/user_roles_test.go +++ b/internal/database/user_roles_test.go @@ -52,6 +52,49 @@ func TestUserRoleCreate(t *testing.T) { }) } +func TestUserRoleBulkCreateForUser(t *testing.T) { + t.Parallel() + + ctx := context.Background() + logger := logtest.Scoped(t) + db := NewDB(logger, dbtest.NewDB(logger, t)) + store := db.UserRoles() + + user, role := createUserAndRole(ctx, t, db) + role2, err := createTestRole(ctx, "another-role", false, t, db.Roles()) + assert.NoError(t, err) + + t.Run("without user id", func(t *testing.T) { + urs, err := store.BulkCreateForUser(ctx, BulkCreateForUserOpts{}) + assert.Nil(t, urs) + assert.Error(t, err) + assert.Equal(t, err.Error(), "missing user id") + }) + + t.Run("without role ids", func(t *testing.T) { + urs, err := store.BulkCreateForUser(ctx, BulkCreateForUserOpts{ + UserID: user.ID, + }) + assert.Nil(t, urs) + assert.Error(t, err) + assert.Equal(t, err.Error(), "missing role ids") + }) + + t.Run("success", func(t *testing.T) { + roleIDs := []int32{role.ID, role2.ID} + urs, err := store.BulkCreateForUser(ctx, BulkCreateForUserOpts{ + UserID: user.ID, + RoleIDs: roleIDs, + }) + assert.NoError(t, err) + assert.Len(t, urs, 2) + for i, ur := range urs { + assert.Equal(t, ur.UserID, user.ID) + assert.Equal(t, ur.RoleID, roleIDs[i]) + } + }) +} + func TestUserRoleDelete(t *testing.T) { t.Parallel() @@ -145,7 +188,7 @@ func TestUserRoleGetByRoleID(t *testing.T) { urs, err := store.GetByRoleID(ctx, GetUserRoleOpts{}) assert.Error(t, err) assert.Nil(t, urs) - assert.Equal(t, err.Error(), "missing id from sql query") + assert.Equal(t, err.Error(), "missing id or name") }) t.Run("with provided role id", func(t *testing.T) { diff --git a/internal/oobmigration/migrations/batches/role_assignment_migrator.go b/internal/oobmigration/migrations/batches/role_assignment_migrator.go new file mode 100644 index 000000000000..61ef07410969 --- /dev/null +++ b/internal/oobmigration/migrations/batches/role_assignment_migrator.go @@ -0,0 +1,82 @@ +package batches + +import ( + "context" + "time" + + "github.com/keegancsmith/sqlf" + + "github.com/sourcegraph/sourcegraph/internal/database/basestore" + "github.com/sourcegraph/sourcegraph/internal/oobmigration" + "github.com/sourcegraph/sourcegraph/internal/types" +) + +type roleAssignmentMigrator struct { + store *basestore.Store + batchSize int +} + +func NewRoleAssignmentMigrator(store *basestore.Store, batchSize int) *roleAssignmentMigrator { + return &roleAssignmentMigrator{ + store: store, + batchSize: batchSize, + } +} + +var _ oobmigration.Migrator = &roleAssignmentMigrator{} + +func (m *roleAssignmentMigrator) ID() int { return 19 } +func (m *roleAssignmentMigrator) Interval() time.Duration { return time.Second * 10 } + +// Progress returns the percentage (ranged [0, 1]) of users who have a system role (USER or SITE_ADMINISTRATOR) assigned. +func (m *roleAssignmentMigrator) Progress(ctx context.Context, _ bool) (float64, error) { + progress, _, err := basestore.ScanFirstFloat(m.store.Query(ctx, sqlf.Sprintf(roleAssignmentMigratorProgressQuery))) + return progress, err +} + +// This query checks the total number of user_roles in the database vs. the sum of the total number of users and the total number of users who are site_admin. +// We use a CTE here to only check for system roles (e.g USER and SITE_ADMINISTRATOR) since those are the two system roles that should be available on every instance. +const roleAssignmentMigratorProgressQuery = ` +WITH system_roles AS MATERIALIZED ( + SELECT id FROM roles WHERE system +) +SELECT + CASE u1.regular_count WHEN 0 THEN 1 ELSE + CAST(ur1.count AS FLOAT) / CAST((u1.regular_count + u1.siteadmin_count) AS FLOAT) + END +FROM + (SELECT COUNT(1) AS regular_count, COUNT(1) FILTER (WHERE site_admin) AS siteadmin_count from users u) u1, + (SELECT COUNT(1) AS count FROM user_roles WHERE role_id IN (SELECT id FROM system_roles)) ur1 +` + +func (m *roleAssignmentMigrator) Up(ctx context.Context) (err error) { + return m.store.Exec(ctx, sqlf.Sprintf(userRolesMigratorUpQuery, string(types.UserSystemRole), string(types.SiteAdministratorSystemRole), m.batchSize)) +} + +func (m *roleAssignmentMigrator) Down(ctx context.Context) error { + // non-destructive + return nil +} + +const userRolesMigratorUpQuery = ` +WITH user_system_role AS MATERIALIZED ( + SELECT id FROM roles WHERE name = %s +), +site_admin_system_role AS MATERIALIZED ( + SELECT id FROM roles WHERE name = %s +), +users_without_roles AS MATERIALIZED ( + SELECT + id, site_admin + FROM users u + WHERE + u.id NOT IN (SELECT user_id from user_roles) + LIMIT %s + FOR UPDATE SKIP LOCKED +) +INSERT INTO user_roles (user_id, role_id) + SELECT id, (SELECT id FROM user_system_role) FROM users_without_roles + UNION ALL + SELECT id, (SELECT id FROM site_admin_system_role) FROM users_without_roles uwr WHERE uwr.site_admin +ON CONFLICT DO NOTHING +` diff --git a/internal/oobmigration/migrations/batches/role_assignment_migrator_test.go b/internal/oobmigration/migrations/batches/role_assignment_migrator_test.go new file mode 100644 index 000000000000..36e514ec1152 --- /dev/null +++ b/internal/oobmigration/migrations/batches/role_assignment_migrator_test.go @@ -0,0 +1,94 @@ +package batches + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/keegancsmith/sqlf" + "github.com/stretchr/testify/assert" + + "github.com/sourcegraph/log/logtest" + + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/database/basestore" + "github.com/sourcegraph/sourcegraph/internal/database/dbtest" + "github.com/sourcegraph/sourcegraph/internal/types" +) + +func TestRoleAssignmentMigrator(t *testing.T) { + ctx := context.Background() + logger := logtest.Scoped(t) + db := database.NewDB(logger, dbtest.NewDB(logger, t)) + store := basestore.NewWithHandle(db.Handle()) + + migrator := NewRoleAssignmentMigrator(store, 5) + progress, err := migrator.Progress(ctx, false) + assert.NoError(t, err) + + if have, want := progress, 1.0; have != want { + t.Fatalf("got invalid progress with no DB entries, want=%f have=%f", want, have) + } + + if err = store.Exec(ctx, sqlf.Sprintf(` + INSERT INTO users (username, display_name, created_at, site_admin) + VALUES + (%s, %s, NOW(), %s), + (%s, %s, NOW(), %s) + `, + "testuser-0", + "testuser", + true, + "testuser-1", + "testuser1", + false, + )); err != nil { + t.Fatal(err) + } + + progress, err = migrator.Progress(ctx, false) + assert.NoError(t, err) + + if have, want := progress, 0.0; have != want { + t.Fatalf("got invalid progress with one unmigrated entry, want=%f have=%f", want, have) + } + + if err := migrator.Up(ctx); err != nil { + t.Fatal(err) + } + + progress, err = migrator.Progress(ctx, false) + assert.NoError(t, err) + + if have, want := progress, 1.0; have != want { + t.Fatalf("got invalid progress after up migration, want=%f have=%f", want, have) + } + + // Three records should be inserted into the user_roles table: + // 1. For testuser-0 with DEFAULT role + // 2. For testuser-0 WITH SITE_ADMINISTRATOR role + // 3. For testuser-1 WITH DEFAULT role + q := `SELECT role_id, user_id FROM user_roles ORDER BY user_id, role_id` + rows, err := db.QueryContext(ctx, q) + assert.NoError(t, err) + defer rows.Close() + var have []*types.UserRole + for rows.Next() { + var ur = types.UserRole{} + if err := rows.Scan(&ur.RoleID, &ur.UserID); err != nil { + t.Fatal(err, "error scanning user role") + } + have = append(have, &ur) + } + + want := []*types.UserRole{ + {UserID: 1, RoleID: 1}, + {UserID: 1, RoleID: 2}, + {UserID: 2, RoleID: 1}, + } + + assert.Len(t, have, 3) + if diff := cmp.Diff(have, want); diff != "" { + t.Error(diff) + } +} diff --git a/internal/oobmigration/migrations/register.go b/internal/oobmigration/migrations/register.go index bc4ea6582149..54193c6a2fc5 100644 --- a/internal/oobmigration/migrations/register.go +++ b/internal/oobmigration/migrations/register.go @@ -54,6 +54,7 @@ type migratorDependencies struct { func registerOSSMigrators(runner *oobmigration.Runner, noDelay bool, deps migratorDependencies) error { return RegisterAll(runner, noDelay, []TaggedMigrator{ batches.NewExternalServiceWebhookMigratorWithDB(deps.store, deps.keyring.ExternalServiceKey, 50), + batches.NewRoleAssignmentMigrator(deps.store, 500), }) } diff --git a/internal/oobmigration/oobmigrations.yaml b/internal/oobmigration/oobmigrations.yaml index e0663c5423b2..9873908c2b6e 100644 --- a/internal/oobmigration/oobmigrations.yaml +++ b/internal/oobmigration/oobmigrations.yaml @@ -108,3 +108,11 @@ introduced_version_minor: 3 deprecated_version_major: 4 deprecated_version_minor: 4 +- id: 19 + team: batch-changes + component: db.user_roles + description: Assigns roles to existing users + non_destructive: true + is_enterprise: false + introduced_version_major: 4 + introduced_version_minor: 5 diff --git a/internal/types/types.go b/internal/types/types.go index c46aa2aa8f0b..4dd2056f0dff 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -805,10 +805,17 @@ type User struct { Searchable bool } +type SystemRole string + +var ( + UserSystemRole SystemRole = "USER" + SiteAdministratorSystemRole SystemRole = "SITE_ADMINISTRATOR" +) + type Role struct { ID int32 Name string - ReadOnly bool + System bool CreatedAt time.Time DeletedAt time.Time } diff --git a/migrations/frontend/1671543381_add_default_roles/down.sql b/migrations/frontend/1671543381_add_default_roles/down.sql new file mode 100644 index 000000000000..6a047404031b --- /dev/null +++ b/migrations/frontend/1671543381_add_default_roles/down.sql @@ -0,0 +1 @@ +DELETE FROM roles WHERE id IN (1, 2); diff --git a/migrations/frontend/1671543381_add_default_roles/metadata.yaml b/migrations/frontend/1671543381_add_default_roles/metadata.yaml new file mode 100644 index 000000000000..355492abb26f --- /dev/null +++ b/migrations/frontend/1671543381_add_default_roles/metadata.yaml @@ -0,0 +1,2 @@ +name: add_default_roles +parents: [1669645608, 1670870072, 1670600028] diff --git a/migrations/frontend/1671543381_add_default_roles/up.sql b/migrations/frontend/1671543381_add_default_roles/up.sql new file mode 100644 index 000000000000..ac33122d15c4 --- /dev/null +++ b/migrations/frontend/1671543381_add_default_roles/up.sql @@ -0,0 +1,9 @@ +-- system roles that come with every sourcegraph instance +INSERT INTO + roles +VALUES + (1, 'USER', '2023-01-04 16:29:41.195966+00', NULL, TRUE), + (2, 'SITE_ADMINISTRATOR', '2023-01-04 16:29:41.195966+00', NULL, TRUE) +ON CONFLICT (id) DO NOTHING; + +SELECT pg_catalog.setval('roles_id_seq', 3, true); diff --git a/migrations/frontend/1674047296_rename_roles_readonly_column_to_system/down.sql b/migrations/frontend/1674047296_rename_roles_readonly_column_to_system/down.sql new file mode 100644 index 000000000000..8fdd057e82ab --- /dev/null +++ b/migrations/frontend/1674047296_rename_roles_readonly_column_to_system/down.sql @@ -0,0 +1,15 @@ +-- ALTER TABLE IF EXISTS +-- roles +-- RENAME COLUMN system TO readonly; + +-- The above query isn't idempotent because `RENAME COLUMN` doesn't have an `IF EXISTS` +-- clause that checks for the existence of the column in question. + +DO $$ +BEGIN + ALTER TABLE IF EXISTS + roles + RENAME COLUMN system TO readonly; +EXCEPTION + WHEN undefined_column THEN RAISE NOTICE 'column system does not exist in table roles'; +END $$; diff --git a/migrations/frontend/1674047296_rename_roles_readonly_column_to_system/metadata.yaml b/migrations/frontend/1674047296_rename_roles_readonly_column_to_system/metadata.yaml new file mode 100644 index 000000000000..528a2a1200dc --- /dev/null +++ b/migrations/frontend/1674047296_rename_roles_readonly_column_to_system/metadata.yaml @@ -0,0 +1,2 @@ +name: rename_roles_readonly_column_to_system +parents: [1671543381, 1673897709] diff --git a/migrations/frontend/1674047296_rename_roles_readonly_column_to_system/up.sql b/migrations/frontend/1674047296_rename_roles_readonly_column_to_system/up.sql new file mode 100644 index 000000000000..ee511eaaa224 --- /dev/null +++ b/migrations/frontend/1674047296_rename_roles_readonly_column_to_system/up.sql @@ -0,0 +1,15 @@ +-- ALTER TABLE IF EXISTS +-- roles +-- RENAME COLUMN readonly TO system; + +-- The above query isn't idempotent because `RENAME COLUMN` doesn't have an `IF EXISTS` +-- clause that checks for the existence of the column in question. + +DO $$ +BEGIN + ALTER TABLE IF EXISTS + roles + RENAME COLUMN readonly TO system; +EXCEPTION + WHEN undefined_column THEN RAISE NOTICE 'column readonly does not exist in table roles'; +END $$; diff --git a/migrations/frontend/squashed.sql b/migrations/frontend/squashed.sql index 2272520f534a..1d3f2ad939df 100755 --- a/migrations/frontend/squashed.sql +++ b/migrations/frontend/squashed.sql @@ -3568,13 +3568,13 @@ CREATE TABLE roles ( name text NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL, deleted_at timestamp with time zone, - readonly boolean DEFAULT false NOT NULL, + system boolean DEFAULT false NOT NULL, CONSTRAINT name_not_blank CHECK ((name <> ''::text)) ); COMMENT ON COLUMN roles.name IS 'The uniquely identifying name of the role.'; -COMMENT ON COLUMN roles.readonly IS 'This is used to indicate whether a role is read-only or can be modified.'; +COMMENT ON COLUMN roles.system IS 'This is used to indicate whether a role is read-only or can be modified.'; CREATE SEQUENCE roles_id_seq AS integer @@ -5478,4 +5478,9 @@ INSERT INTO lsif_configuration_policies VALUES (1, NULL, 'Default tip-of-branch INSERT INTO lsif_configuration_policies VALUES (2, NULL, 'Default tag retention policy', 'GIT_TAG', '*', true, 8064, false, false, 0, false, true, NULL, NULL, false); INSERT INTO lsif_configuration_policies VALUES (3, NULL, 'Default commit retention policy', 'GIT_TREE', '*', true, 168, true, false, 0, false, true, NULL, NULL, false); -SELECT pg_catalog.setval('lsif_configuration_policies_id_seq', 3, true); \ No newline at end of file +SELECT pg_catalog.setval('lsif_configuration_policies_id_seq', 3, true); + +INSERT INTO roles VALUES (1, 'USER', '2023-01-04 16:29:41.195966+00', NULL, true); +INSERT INTO roles VALUES (2, 'SITE_ADMINISTRATOR', '2023-01-04 16:29:41.195966+00', NULL, true); + +SELECT pg_catalog.setval('roles_id_seq', 3, true); \ No newline at end of file From 13559eef4cb444b62013ffd923d0eb0549c72388 Mon Sep 17 00:00:00 2001 From: Jean-Hadrien Chabran Date: Thu, 19 Jan 2023 14:34:04 +0100 Subject: [PATCH 039/678] Adds a new runtype to work with Bazel commands without affecting others. (#46660) --- dev/ci/runtype/runtype.go | 11 +++++-- .../background-information/ci/reference.md | 15 +++++++++ .../background-information/sg/reference.md | 1 + .../dev/ci/internal/ci/bazel_operations.go | 33 +++++++++++++++++++ enterprise/dev/ci/internal/ci/pipeline.go | 2 ++ 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 enterprise/dev/ci/internal/ci/bazel_operations.go diff --git a/dev/ci/runtype/runtype.go b/dev/ci/runtype/runtype.go index 836fb573c416..19b6c29459b4 100644 --- a/dev/ci/runtype/runtype.go +++ b/dev/ci/runtype/runtype.go @@ -13,7 +13,8 @@ type RunType int const ( // RunTypes should be defined by order of precedence. - PullRequest RunType = iota // pull request build + PullRequest RunType = iota // pull request build + BazelExpBranch // branch that runs specific bazel steps // Nightly builds - must be first because they take precedence @@ -130,7 +131,10 @@ func (t RunType) Matcher() *RunTypeMatcher { return &RunTypeMatcher{ Branch: "main-dry-run/", } - + case BazelExpBranch: + return &RunTypeMatcher{ + Branch: "bzl/", + } case ImagePatch: return &RunTypeMatcher{ Branch: "docker-images-patch/", @@ -163,7 +167,8 @@ func (t RunType) String() string { switch t { case PullRequest: return "Pull request" - + case BazelExpBranch: + return "Bazel Exp Branch" case ReleaseNightly: return "Release branch nightly healthcheck build" case BextNightly: diff --git a/doc/dev/background-information/ci/reference.md b/doc/dev/background-information/ci/reference.md index 79ee5a5a6322..7e878cda5e81 100644 --- a/doc/dev/background-information/ci/reference.md +++ b/doc/dev/background-information/ci/reference.md @@ -83,6 +83,21 @@ The default run type. - **Scan test builds**: Scan alpine-3.14, Scan cadvisor, Scan codeinsights-db, Scan codeintel-db, Scan frontend, Scan github-proxy, Scan gitserver, Scan grafana, Scan indexed-searcher, Scan jaeger-agent, Scan jaeger-all-in-one, Scan blobstore2, Scan node-exporter, Scan postgres-12-alpine, Scan postgres_exporter, Scan precise-code-intel-worker, Scan prometheus, Scan prometheus-gcp, Scan redis-cache, Scan redis-store, Scan redis_exporter, Scan repo-updater, Scan search-indexer, Scan searcher, Scan symbols, Scan syntax-highlighter, Scan worker, Scan migrator, Scan executor, Scan executor-vm, Scan batcheshelper, Scan opentelemetry-collector, Scan sg - Upload build trace +### Bazel Exp Branch + +The run type for branches matching `bzl/`. +You can create a build of this run type for your changes using: + +```sh +sg ci build bzl +``` + +Base pipeline (more steps might be included based on branch changes): + +- **Metadata**: Pipeline metadata +- Build //dev/sg +- Upload build trace + ### Release branch nightly healthcheck build The run type for environment including `{"RELEASE_NIGHTLY":"true"}`. diff --git a/doc/dev/background-information/sg/reference.md b/doc/dev/background-information/sg/reference.md index 705cbbe01048..f630075180a5 100644 --- a/doc/dev/background-information/sg/reference.md +++ b/doc/dev/background-information/sg/reference.md @@ -236,6 +236,7 @@ This command is useful when: Supported run types when providing an argument for 'sg ci build [runtype]': +* bzl * main-dry-run * docker-images-patch * docker-images-patch-notest diff --git a/enterprise/dev/ci/internal/ci/bazel_operations.go b/enterprise/dev/ci/internal/ci/bazel_operations.go new file mode 100644 index 000000000000..b50fbc7e0893 --- /dev/null +++ b/enterprise/dev/ci/internal/ci/bazel_operations.go @@ -0,0 +1,33 @@ +package ci + +import ( + "fmt" + "strings" + + bk "github.com/sourcegraph/sourcegraph/enterprise/dev/ci/internal/buildkite" + "github.com/sourcegraph/sourcegraph/enterprise/dev/ci/internal/ci/operations" +) + +const bazelRemoteCacheURL = "https://storage.googleapis.com/sourcegraph_bazel_cache" + +func BazelOperations() *operations.Set { + ops := operations.NewSet() + ops.Append(build("//dev/sg")) + return ops +} + +func build(target string) func(*bk.Pipeline) { + bazelCmd := []string{ + fmt.Sprintf("bazel build %s", target), + "--remote_cache=$CI_BAZEL_REMOTE_CACHE", + "--google_credentials=/mnt/gcloud-service-account/gcloud-service-account.json", + } + + return func(pipeline *bk.Pipeline) { + pipeline.AddStep(fmt.Sprintf(":bazel: Build %s", target), + bk.Env("CI_BAZEL_REMOTE_CACHE", bazelRemoteCacheURL), + bk.Cmd(strings.Join(bazelCmd, " ")), + bk.Agent("queue", "bazel"), + ) + } +} diff --git a/enterprise/dev/ci/internal/ci/pipeline.go b/enterprise/dev/ci/internal/ci/pipeline.go index dd686f3e9df0..3ffabc8cd66f 100644 --- a/enterprise/dev/ci/internal/ci/pipeline.go +++ b/enterprise/dev/ci/internal/ci/pipeline.go @@ -89,6 +89,8 @@ func GeneratePipeline(c Config) (*bk.Pipeline, error) { // // PERF: Try to order steps such that slower steps are first. switch c.RunType { + case runtype.BazelExpBranch: + ops.Merge(BazelOperations()) case runtype.PullRequest: // First, we set up core test operations that apply both to PRs and to other run // types such as main. From 4807df1ebec5d7f067ceeac8573eaf4768496484 Mon Sep 17 00:00:00 2001 From: Idan Varsano Date: Thu, 19 Jan 2023 10:52:23 -0500 Subject: [PATCH 040/678] Backend: Paginate Blob Content (#45435) * persist unhighlighted blob content --- .../web/src/integration/blob-viewer.test.ts | 3 + .../src/integration/graphQlResponseHelpers.ts | 1 + client/web/src/repo/blob/BlobPage.tsx | 1 + client/web/src/repo/blob/backend.ts | 21 ++- cmd/frontend/graphqlbackend/batches.go | 5 +- cmd/frontend/graphqlbackend/batches.graphql | 36 ++++- cmd/frontend/graphqlbackend/file.go | 5 +- cmd/frontend/graphqlbackend/file_match.go | 6 +- cmd/frontend/graphqlbackend/git_commit.go | 7 +- .../graphqlbackend/git_commit_test.go | 6 +- cmd/frontend/graphqlbackend/git_tree.go | 9 +- cmd/frontend/graphqlbackend/git_tree_entry.go | 113 ++++++++++--- .../graphqlbackend/git_tree_entry_test.go | 148 ++++++++++++++++-- cmd/frontend/graphqlbackend/highlight.go | 2 + .../preview_repository_comparison.go | 2 +- .../preview_repository_comparison_test.go | 2 +- .../graphqlbackend/repository_comparison.go | 17 +- .../repository_comparison_test.go | 21 ++- cmd/frontend/graphqlbackend/schema.graphql | 130 ++++++++++++++- cmd/frontend/graphqlbackend/symbols.go | 6 +- cmd/frontend/graphqlbackend/virtual_file.go | 26 ++- .../graphqlbackend/virtual_file_test.go | 4 +- .../resolvers/batch_spec_workspace_file.go | 14 +- .../batch_spec_workspace_file_test.go | 15 +- .../resolvers/git_tree_entry_resolver.go | 4 +- internal/codeintel/resolvers/all.go | 7 +- 26 files changed, 522 insertions(+), 89 deletions(-) diff --git a/client/web/src/integration/blob-viewer.test.ts b/client/web/src/integration/blob-viewer.test.ts index c6f230ae4ca9..a0ce0efb5284 100644 --- a/client/web/src/integration/blob-viewer.test.ts +++ b/client/web/src/integration/blob-viewer.test.ts @@ -109,6 +109,7 @@ describe('Blob viewer', () => { file: { __typename: 'VirtualFile', content: '// Log to console\nconsole.log("Hello world")\n// Third line', + totalLines: 3, richHTML: '', highlight: { aborted: false, @@ -195,6 +196,7 @@ describe('Blob viewer', () => { file: { __typename: 'VirtualFile', content: '// Log to console\nconsole.log("Hello world")', + totalLines: 2, richHTML: '', highlight: { aborted: false, @@ -351,6 +353,7 @@ describe('Blob viewer', () => { file: { __typename: 'VirtualFile', content: `// file path: ${filePath}\nconsole.log("Hello world")`, + totalLines: 2, richHTML: '', highlight: { aborted: false, diff --git a/client/web/src/integration/graphQlResponseHelpers.ts b/client/web/src/integration/graphQlResponseHelpers.ts index 4a3fbefbcd46..9d474264a8e4 100644 --- a/client/web/src/integration/graphQlResponseHelpers.ts +++ b/client/web/src/integration/graphQlResponseHelpers.ts @@ -41,6 +41,7 @@ export const createBlobContentResult = ( __typename: 'VirtualFile', content, richHTML: '', + totalLines: content.split('\n').length, highlight: { aborted: false, html, diff --git a/client/web/src/repo/blob/BlobPage.tsx b/client/web/src/repo/blob/BlobPage.tsx index 30c0fffa4d83..7033c7c1cd7e 100644 --- a/client/web/src/repo/blob/BlobPage.tsx +++ b/client/web/src/repo/blob/BlobPage.tsx @@ -188,6 +188,7 @@ export const BlobPage: React.FunctionComponent diff --git a/client/web/src/repo/blob/backend.ts b/client/web/src/repo/blob/backend.ts index b2d568775ba2..c55a3455c4d5 100644 --- a/client/web/src/repo/blob/backend.ts +++ b/client/web/src/repo/blob/backend.ts @@ -23,11 +23,15 @@ import { useExperimentalFeatures } from '../../stores' const applyDefaultValuesToFetchBlobOptions = ({ disableTimeout = false, format = HighlightResponseFormat.HTML_HIGHLIGHT, + startLine = null, + endLine = null, ...options }: FetchBlobOptions): Required => ({ ...options, disableTimeout, format, + startLine, + endLine, }) function fetchBlobCacheKey(options: FetchBlobOptions): string { @@ -42,17 +46,19 @@ interface FetchBlobOptions { filePath: string disableTimeout?: boolean format?: HighlightResponseFormat + startLine?: number | null + endLine?: number | null } export const fetchBlob = memoizeObservable((options: FetchBlobOptions): Observable => { - const { repoName, revision, filePath, disableTimeout, format } = applyDefaultValuesToFetchBlobOptions(options) + const { repoName, revision, filePath, disableTimeout, format, startLine, endLine } = + applyDefaultValuesToFetchBlobOptions(options) // We only want to include HTML data if explicitly requested. We always // include LSIF because this is used for languages that are configured // to be processed with tree sitter (and is used when explicitly // requested via JSON_SCIP). const html = [HighlightResponseFormat.HTML_PLAINTEXT, HighlightResponseFormat.HTML_HIGHLIGHT].includes(format) - return requestGraphQL( gql` query Blob( @@ -62,6 +68,8 @@ export const fetchBlob = memoizeObservable((options: FetchBlobOptions): Observab $disableTimeout: Boolean! $format: HighlightResponseFormat! $html: Boolean! + $startLine: Int + $endLine: Int ) { repository(name: $repoName) { commit(rev: $revision) { @@ -74,13 +82,14 @@ export const fetchBlob = memoizeObservable((options: FetchBlobOptions): Observab fragment BlobFileFields on File2 { __typename - content - richHTML - highlight(disableTimeout: $disableTimeout, format: $format) { + content(startLine: $startLine, endLine: $endLine) + richHTML(startLine: $startLine, endLine: $endLine) + highlight(disableTimeout: $disableTimeout, format: $format, startLine: $startLine, endLine: $endLine) { aborted html @include(if: $html) lsif } + totalLines ... on GitBlob { lfs { byteSize @@ -92,7 +101,7 @@ export const fetchBlob = memoizeObservable((options: FetchBlobOptions): Observab } } `, - { repoName, revision, filePath, disableTimeout, format, html } + { repoName, revision, filePath, disableTimeout, format, html, startLine, endLine } ).pipe( map(dataOrThrowErrors), map(data => { diff --git a/cmd/frontend/graphqlbackend/batches.go b/cmd/frontend/graphqlbackend/batches.go index caeec0ed032e..121cc67c4587 100644 --- a/cmd/frontend/graphqlbackend/batches.go +++ b/cmd/frontend/graphqlbackend/batches.go @@ -686,10 +686,11 @@ type BatchWorkspaceFileResolver interface { Path() string Name() string IsDirectory() bool - Content(ctx context.Context) (string, error) + Content(ctx context.Context, args *GitTreeContentPageArgs) (string, error) ByteSize(ctx context.Context) (int32, error) + TotalLines(ctx context.Context) (int32, error) Binary(ctx context.Context) (bool, error) - RichHTML(ctx context.Context) (string, error) + RichHTML(ctx context.Context, args *GitTreeContentPageArgs) (string, error) URL(ctx context.Context) (string, error) CanonicalURL() string ExternalURLs(ctx context.Context) ([]*externallink.Resolver, error) diff --git a/cmd/frontend/graphqlbackend/batches.graphql b/cmd/frontend/graphqlbackend/batches.graphql index 5f759472ffad..465712e7214e 100644 --- a/cmd/frontend/graphqlbackend/batches.graphql +++ b/cmd/frontend/graphqlbackend/batches.graphql @@ -3516,12 +3516,25 @@ type BatchSpecWorkspaceFile implements File2 & Node { """ The content of this file. """ - content: String! + content( + """ + Return file content starting at line "startLine". A value <= 0 will be the start of the file. + """ + startLine: Int + """ + Return file content ending at line "endLine". A value < 0 or > totalLines will set endLine to the end of the file. + """ + endLine: Int + ): String! """ The file size in bytes. """ byteSize: Int! """ + Total line count for the Blob. Returns 0 for binary files. + """ + totalLines: Int! + """ Whether or not it is binary. """ binary: Boolean! @@ -3530,7 +3543,16 @@ type BatchSpecWorkspaceFile implements File2 & Node { rich file type. This HTML string is already escaped and thus is always safe to render. """ - richHTML: String! + richHTML( + """ + Return richHTML content starting at line "startLine". A value <= 0 will be the start of the file. + """ + startLine: Int + """ + Return richHTML content ending at line "endLine". A value < 0 or > totalLines will set endLine to the end of the file. + """ + endLine: Int + ): String! """ The URL to this file (using the input revision specifier, which may not be immutable). """ @@ -3561,5 +3583,15 @@ type BatchSpecWorkspaceFile implements File2 & Node { Specifies which format/highlighting technique to use. """ format: HighlightResponseFormat = HTML_HIGHLIGHT + """ + Return highlight content starting at line "startLine". A value <= 0 will be the start of the file. + Warning: Pagination only works with the HTML_PLAINTEXT format type at the moment. + """ + startLine: Int + """ + Return highlight content ending at line "endLine". A value < 0 or > totalLines will set endLine to the end of the file. + Warning: Pagination only works with the HTML_PLAINTEXT format type at the moment. + """ + endLine: Int ): HighlightedFile! } diff --git a/cmd/frontend/graphqlbackend/file.go b/cmd/frontend/graphqlbackend/file.go index 6e6d8d349942..bfc6c4ede7a6 100644 --- a/cmd/frontend/graphqlbackend/file.go +++ b/cmd/frontend/graphqlbackend/file.go @@ -13,10 +13,11 @@ type FileResolver interface { Path() string Name() string IsDirectory() bool - Content(ctx context.Context) (string, error) + Content(ctx context.Context, args *GitTreeContentPageArgs) (string, error) ByteSize(ctx context.Context) (int32, error) + TotalLines(ctx context.Context) (int32, error) Binary(ctx context.Context) (bool, error) - RichHTML(ctx context.Context) (string, error) + RichHTML(ctx context.Context, args *GitTreeContentPageArgs) (string, error) URL(ctx context.Context) (string, error) CanonicalURL() string ExternalURLs(ctx context.Context) ([]*externallink.Resolver, error) diff --git a/cmd/frontend/graphqlbackend/file_match.go b/cmd/frontend/graphqlbackend/file_match.go index bff2ba5e3937..2bc4773ea9d7 100644 --- a/cmd/frontend/graphqlbackend/file_match.go +++ b/cmd/frontend/graphqlbackend/file_match.go @@ -30,7 +30,11 @@ func (fm *FileMatchResolver) File() *GitTreeEntryResolver { // NOTE(sqs): Omits other commit fields to avoid needing to fetch them // (which would make it slow). This GitCommitResolver will return empty // values for all other fields. - return NewGitTreeEntryResolver(fm.db, gitserver.NewClient(fm.db), fm.Commit(), CreateFileInfo(fm.Path, false)) + opts := GitTreeEntryResolverOpts{ + commit: fm.Commit(), + stat: CreateFileInfo(fm.Path, false), + } + return NewGitTreeEntryResolver(fm.db, gitserver.NewClient(fm.db), opts) } func (fm *FileMatchResolver) Commit() *GitCommitResolver { diff --git a/cmd/frontend/graphqlbackend/git_commit.go b/cmd/frontend/graphqlbackend/git_commit.go index 458880aa6189..894204c13677 100644 --- a/cmd/frontend/graphqlbackend/git_commit.go +++ b/cmd/frontend/graphqlbackend/git_commit.go @@ -274,8 +274,11 @@ func (r *GitCommitResolver) path(ctx context.Context, path string, validate func if err := validate(stat); err != nil { return nil, err } - - return NewGitTreeEntryResolver(r.db, r.gitserverClient, r, stat), nil + opts := GitTreeEntryResolverOpts{ + commit: r, + stat: stat, + } + return NewGitTreeEntryResolver(r.db, r.gitserverClient, opts), nil } func (r *GitCommitResolver) FileNames(ctx context.Context) ([]string, error) { diff --git a/cmd/frontend/graphqlbackend/git_commit_test.go b/cmd/frontend/graphqlbackend/git_commit_test.go index 38141f21c2ad..8f18b9475411 100644 --- a/cmd/frontend/graphqlbackend/git_commit_test.go +++ b/cmd/frontend/graphqlbackend/git_commit_test.go @@ -49,7 +49,11 @@ func TestGitCommitResolver(t *testing.T) { commitResolver.inputRev = &inputRev require.Equal(t, "/xyz/-/commit/master%5E1", commitResolver.URL()) - treeResolver := NewGitTreeEntryResolver(db, client, commitResolver, CreateFileInfo("a/b", false)) + opts := GitTreeEntryResolverOpts{ + commit: commitResolver, + stat: CreateFileInfo("a/b", false), + } + treeResolver := NewGitTreeEntryResolver(db, client, opts) url, err := treeResolver.URL(ctx) require.Nil(t, err) require.Equal(t, "/xyz@master%5E1/-/blob/a/b", url) diff --git a/cmd/frontend/graphqlbackend/git_tree.go b/cmd/frontend/graphqlbackend/git_tree.go index b5cada745d30..a764b5769943 100644 --- a/cmd/frontend/graphqlbackend/git_tree.go +++ b/cmd/frontend/graphqlbackend/git_tree.go @@ -60,12 +60,17 @@ func (r *GitTreeEntryResolver) entries(ctx context.Context, args *gitTreeEntryCo l := make([]*GitTreeEntryResolver, 0, len(entries)) for _, entry := range entries { // Apply any additional filtering + if filter == nil || filter(entry) { - l = append(l, NewGitTreeEntryResolver(r.db, r.gitserverClient, r.commit, entry)) + opts := GitTreeEntryResolverOpts{ + commit: r.Commit(), + stat: entry, + } + l = append(l, NewGitTreeEntryResolver(r.db, r.gitserverClient, opts)) } } - // Update after filtering + // Update endLine filtering hasSingleChild := len(l) == 1 for i := range l { l[i].isSingleChild = &hasSingleChild diff --git a/cmd/frontend/graphqlbackend/git_tree_entry.go b/cmd/frontend/graphqlbackend/git_tree_entry.go index 95a626520e0f..413648d8585b 100644 --- a/cmd/frontend/graphqlbackend/git_tree_entry.go +++ b/cmd/frontend/graphqlbackend/git_tree_entry.go @@ -38,20 +38,34 @@ type GitTreeEntryResolver struct { gitserverClient gitserver.Client commit *GitCommitResolver - contentOnce sync.Once - content []byte - contentErr error - + contentOnce sync.Once + fullContentBytes []byte + contentErr error + cacheHighlighted bool // stat is this tree entry's file info. Its Name method must return the full path relative to // the root, not the basename. - stat fs.FileInfo - + stat fs.FileInfo isRecursive bool // whether entries is populated recursively (otherwise just current level of hierarchy) isSingleChild *bool // whether this is the single entry in its parent. Only set by the (&GitTreeEntryResolver) entries. } -func NewGitTreeEntryResolver(db database.DB, gitserverClient gitserver.Client, commit *GitCommitResolver, stat fs.FileInfo) *GitTreeEntryResolver { - return &GitTreeEntryResolver{db: db, commit: commit, stat: stat, gitserverClient: gitserverClient} +type GitTreeEntryResolverOpts struct { + commit *GitCommitResolver + stat fs.FileInfo +} + +type GitTreeContentPageArgs struct { + StartLine *int32 + EndLine *int32 +} + +func NewGitTreeEntryResolver(db database.DB, gitserverClient gitserver.Client, opts GitTreeEntryResolverOpts) *GitTreeEntryResolver { + return &GitTreeEntryResolver{ + db: db, + commit: opts.commit, + stat: opts.stat, + gitserverClient: gitserverClient, + } } func (r *GitTreeEntryResolver) Path() string { return r.stat.Name() } @@ -65,20 +79,33 @@ func (r *GitTreeEntryResolver) ToBatchSpecWorkspaceFile() (BatchWorkspaceFileRes return nil, false } +func (r *GitTreeEntryResolver) TotalLines(ctx context.Context) (int32, error) { + // If it is a binary, return 0 + binary, err := r.Binary(ctx) + if err != nil || binary { + return 0, err + } + + // We only care about the full content length here, so we just need content to be set. + content, err := r.Content(ctx, &GitTreeContentPageArgs{}) + if err != nil { + return 0, err + } + return int32(len(strings.Split(content, "\n"))), nil +} + func (r *GitTreeEntryResolver) ByteSize(ctx context.Context) (int32, error) { - content, err := r.Content(ctx) + // We only care about the full content length here, so we just need content to be set. + _, err := r.Content(ctx, &GitTreeContentPageArgs{}) if err != nil { return 0, err } - return int32(len([]byte(content))), nil + return int32(len(r.fullContentBytes)), nil } -func (r *GitTreeEntryResolver) Content(ctx context.Context) (string, error) { +func (r *GitTreeEntryResolver) Content(ctx context.Context, args *GitTreeContentPageArgs) (string, error) { r.contentOnce.Do(func() { - ctx, cancel := context.WithTimeout(ctx, 30*time.Second) - defer cancel() - - r.content, r.contentErr = r.gitserverClient.ReadFile( + r.fullContentBytes, r.contentErr = r.gitserverClient.ReadFile( ctx, authz.DefaultSubRepoPermsChecker, r.commit.repoResolver.RepoName(), @@ -87,11 +114,44 @@ func (r *GitTreeEntryResolver) Content(ctx context.Context) (string, error) { ) }) - return string(r.content), r.contentErr + return pageContent(strings.Split(string(r.fullContentBytes), "\n"), args.StartLine, args.EndLine), r.contentErr } -func (r *GitTreeEntryResolver) RichHTML(ctx context.Context) (string, error) { - content, err := r.Content(ctx) +func pageContent(content []string, startLine, endLine *int32) string { + totalContentLength := len(content) + startCursor := 0 + endCursor := totalContentLength + + // Any nil or illegal value for startLine or endLine gets set to either the start or + // end of the file respectively. + + // If startLine is set and is a legit value, set the cursor to point to it. + if startLine != nil && *startLine > 0 { + // The left index is inclusive, so we have to shift it back by 1 + startCursor = int(*startLine) - 1 + } + if startCursor >= totalContentLength { + startCursor = totalContentLength + } + + // If endLine is set and is a legit value, set the cursor to point to it. + if endLine != nil && *endLine >= 0 { + endCursor = int(*endLine) + } + if endCursor > totalContentLength { + endCursor = totalContentLength + } + + // Final failsafe in case someone is really messing around with this API. + if endCursor < startCursor { + return strings.Join(content[0:totalContentLength], "\n") + } + + return strings.Join(content[startCursor:endCursor], "\n") +} + +func (r *GitTreeEntryResolver) RichHTML(ctx context.Context, args *GitTreeContentPageArgs) (string, error) { + content, err := r.Content(ctx, args) if err != nil { return "", err } @@ -99,18 +159,26 @@ func (r *GitTreeEntryResolver) RichHTML(ctx context.Context) (string, error) { } func (r *GitTreeEntryResolver) Binary(ctx context.Context) (bool, error) { - content, err := r.Content(ctx) + // We only care about the full content length here, so we just need r.fullContentLines to be set. + _, err := r.Content(ctx, &GitTreeContentPageArgs{}) if err != nil { return false, err } - return highlight.IsBinary([]byte(content)), nil + return highlight.IsBinary(r.fullContentBytes), nil } func (r *GitTreeEntryResolver) Highlight(ctx context.Context, args *HighlightArgs) (*HighlightedFileResolver, error) { - content, err := r.Content(ctx) + // Currently, pagination + highlighting is not supported, throw out an error if it is attempted. + if (args.StartLine != nil || args.EndLine != nil) && args.Format != "HTML_PLAINTEXT" { + return nil, errors.New("pagination is not supported with formats other than HTML_PLAINTEXT, don't " + + "set startLine or endLine with other formats") + } + + content, err := r.Content(ctx, &GitTreeContentPageArgs{StartLine: args.StartLine, EndLine: args.EndLine}) if err != nil { return nil, err } + return highlightContent(ctx, args, content, r.Path(), highlight.Metadata{ RepoName: r.commit.repoResolver.Name(), Revision: string(r.commit.oid), @@ -327,7 +395,8 @@ func (r *GitTreeEntryResolver) SymbolInfo(ctx context.Context, args *symbolInfoA } func (r *GitTreeEntryResolver) LFS(ctx context.Context) (*lfsResolver, error) { - content, err := r.Content(ctx) + // We only care about the full content length here, so we just need content to be set. + content, err := r.Content(ctx, &GitTreeContentPageArgs{}) if err != nil { return nil, err } diff --git a/cmd/frontend/graphqlbackend/git_tree_entry_test.go b/cmd/frontend/graphqlbackend/git_tree_entry_test.go index e6869a2ccb77..e8775e56ba5f 100644 --- a/cmd/frontend/graphqlbackend/git_tree_entry_test.go +++ b/cmd/frontend/graphqlbackend/git_tree_entry_test.go @@ -2,6 +2,7 @@ package graphqlbackend import ( "context" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -16,12 +17,13 @@ import ( func TestGitTreeEntry_RawZipArchiveURL(t *testing.T) { db := database.NewMockDB() gitserverClient := gitserver.NewMockClient() - got := NewGitTreeEntryResolver(db, gitserverClient, - &GitCommitResolver{ + opts := GitTreeEntryResolverOpts{ + commit: &GitCommitResolver{ repoResolver: NewRepositoryResolver(db, gitserverClient, &types.Repo{Name: "my/repo"}), }, - CreateFileInfo("a/b", true)). - RawZipArchiveURL() + stat: CreateFileInfo("a/b", true), + } + got := NewGitTreeEntryResolver(db, gitserverClient, opts).RawZipArchiveURL() want := "http://example.com/my/repo/-/raw/a/b?format=zip" if got != want { t.Errorf("got %q, want %q", got, want) @@ -41,14 +43,15 @@ func TestGitTreeEntry_Content(t *testing.T) { } return []byte(wantContent), nil }) - - gitTree := NewGitTreeEntryResolver(db, gitserverClient, - &GitCommitResolver{ + opts := GitTreeEntryResolverOpts{ + commit: &GitCommitResolver{ repoResolver: NewRepositoryResolver(db, gitserverClient, &types.Repo{Name: "my/repo"}), }, - CreateFileInfo(wantPath, true)) + stat: CreateFileInfo(wantPath, true), + } + gitTree := NewGitTreeEntryResolver(db, gitserverClient, opts) - newFileContent, err := gitTree.Content(context.Background()) + newFileContent, err := gitTree.Content(context.Background(), &GitTreeContentPageArgs{}) if err != nil { t.Fatal(err) } @@ -66,3 +69,130 @@ func TestGitTreeEntry_Content(t *testing.T) { t.Fatalf("wrong file size, want=%d have=%d", want, have) } } + +func TestGitTreeEntry_ContentPagination(t *testing.T) { + wantPath := "foobar.md" + fullContent := `1 +2 +3 +4 +5 +6` + + db := database.NewMockDB() + gitserverClient := gitserver.NewMockClient() + + gitserverClient.ReadFileFunc.SetDefaultHook(func(_ context.Context, _ authz.SubRepoPermissionChecker, _ api.RepoName, _ api.CommitID, name string) ([]byte, error) { + if name != wantPath { + t.Fatalf("wrong name in ReadFile call. want=%q, have=%q", wantPath, name) + } + return []byte(fullContent), nil + }) + + tests := []struct { + startLine int32 + endLine int32 + wantContent string + }{ + { + startLine: 2, + endLine: 6, + wantContent: "2\n3\n4\n5\n6", + }, + { + startLine: 0, + endLine: 2, + wantContent: "1\n2", + }, + { + startLine: 0, + endLine: 0, + wantContent: "", + }, + { + startLine: 6, + endLine: 6, + wantContent: "6", + }, + { + startLine: -1, + endLine: -1, + wantContent: fullContent, + }, + { + startLine: 7, + endLine: 7, + wantContent: "", + }, + { + startLine: 5, + endLine: 2, + wantContent: fullContent, + }, + } + + for _, tc := range tests { + opts := GitTreeEntryResolverOpts{ + commit: &GitCommitResolver{ + repoResolver: NewRepositoryResolver(db, gitserverClient, &types.Repo{Name: "my/repo"}), + }, + stat: CreateFileInfo(wantPath, true), + } + gitTree := NewGitTreeEntryResolver(db, gitserverClient, opts) + + newFileContent, err := gitTree.Content(context.Background(), &GitTreeContentPageArgs{ + StartLine: &tc.startLine, + EndLine: &tc.endLine, + }) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(newFileContent, tc.wantContent); diff != "" { + t.Fatalf("wrong newFileContent: %s", diff) + } + + newByteSize, err := gitTree.ByteSize(context.Background()) + if err != nil { + t.Fatal(err) + } + + if have, want := newByteSize, int32(len([]byte(fullContent))); have != want { + t.Fatalf("wrong file size, want=%d have=%d", want, have) + } + + newTotalLines, err := gitTree.TotalLines(context.Background()) + if err != nil { + t.Fatal(err) + } + + if have, want := newTotalLines, int32(len(strings.Split(fullContent, "\n"))); have != want { + t.Fatalf("wrong file size, want=%d have=%d", want, have) + } + } + + // Testing default (nils) for pagination. + opts := GitTreeEntryResolverOpts{ + commit: &GitCommitResolver{ + repoResolver: NewRepositoryResolver(db, gitserverClient, &types.Repo{Name: "my/repo"}), + }, + stat: CreateFileInfo(wantPath, true), + } + gitTree := NewGitTreeEntryResolver(db, gitserverClient, opts) + + newFileContent, err := gitTree.Content(context.Background(), &GitTreeContentPageArgs{}) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(newFileContent, fullContent); diff != "" { + t.Fatalf("wrong newFileContent: %s", diff) + } + + newByteSize, err := gitTree.ByteSize(context.Background()) + if err != nil { + t.Fatal(err) + } + + if have, want := newByteSize, int32(len([]byte(fullContent))); have != want { + t.Fatalf("wrong file size, want=%d have=%d", want, have) + } +} diff --git a/cmd/frontend/graphqlbackend/highlight.go b/cmd/frontend/graphqlbackend/highlight.go index 8777bd3d9f75..0622342c7d3e 100644 --- a/cmd/frontend/graphqlbackend/highlight.go +++ b/cmd/frontend/graphqlbackend/highlight.go @@ -37,6 +37,8 @@ type HighlightArgs struct { IsLightTheme *bool HighlightLongLines bool Format string + StartLine *int32 + EndLine *int32 } type HighlightedFileResolver struct { diff --git a/cmd/frontend/graphqlbackend/preview_repository_comparison.go b/cmd/frontend/graphqlbackend/preview_repository_comparison.go index 2d518fb82e43..8d4c3d0bbe7a 100644 --- a/cmd/frontend/graphqlbackend/preview_repository_comparison.go +++ b/cmd/frontend/graphqlbackend/preview_repository_comparison.go @@ -142,7 +142,7 @@ func fileDiffVirtualFileContent(r *FileDiffResolver) FileContentFunc { var oldContent string if oldFile := r.OldFile(); oldFile != nil { var err error - oldContent, err = r.OldFile().Content(ctx) + oldContent, err = r.OldFile().Content(ctx, &GitTreeContentPageArgs{}) if err != nil { return } diff --git a/cmd/frontend/graphqlbackend/preview_repository_comparison_test.go b/cmd/frontend/graphqlbackend/preview_repository_comparison_test.go index 7773dfaf870e..35d0a25b4ddf 100644 --- a/cmd/frontend/graphqlbackend/preview_repository_comparison_test.go +++ b/cmd/frontend/graphqlbackend/preview_repository_comparison_test.go @@ -266,7 +266,7 @@ Line 9 Line 10 ` - haveContent, err := newFile.Content(ctx) + haveContent, err := newFile.Content(ctx, &GitTreeContentPageArgs{}) if err != nil { t.Fatal(err) } diff --git a/cmd/frontend/graphqlbackend/repository_comparison.go b/cmd/frontend/graphqlbackend/repository_comparison.go index 68281ba8701d..f45b1b0add6c 100644 --- a/cmd/frontend/graphqlbackend/repository_comparison.go +++ b/cmd/frontend/graphqlbackend/repository_comparison.go @@ -202,7 +202,11 @@ func (r *RepositoryComparisonResolver) FileDiffs(ctx context.Context, args *File // repositoryComparisonNewFile is the default NewFileFunc used by // RepositoryComparisonResolver to produce the new file in a FileDiffResolver. func repositoryComparisonNewFile(db database.DB, r *FileDiffResolver) FileResolver { - return NewGitTreeEntryResolver(db, r.gitserverClient, r.Head, CreateFileInfo(r.FileDiff.NewName, false)) + opts := GitTreeEntryResolverOpts{ + commit: r.Head, + stat: CreateFileInfo(r.FileDiff.NewName, false), + } + return NewGitTreeEntryResolver(db, r.gitserverClient, opts) } // computeRepositoryComparisonDiff returns a ComputeDiffFunc for the given @@ -437,7 +441,11 @@ func (r *FileDiffResolver) OldFile() FileResolver { if diffPathOrNull(r.FileDiff.OrigName) == nil { return nil } - return NewGitTreeEntryResolver(r.db, r.gitserverClient, r.Base, CreateFileInfo(r.FileDiff.OrigName, false)) + opts := GitTreeEntryResolverOpts{ + commit: r.Base, + stat: CreateFileInfo(r.FileDiff.OrigName, false), + } + return NewGitTreeEntryResolver(r.db, r.gitserverClient, opts) } func (r *FileDiffResolver) NewFile() FileResolver { @@ -490,7 +498,10 @@ func (r *fileDiffHighlighter) Highlight(ctx context.Context, args *HighlightArgs if file == nil { return nil, nil } - content, err := file.Content(ctx) + content, err := file.Content(ctx, &GitTreeContentPageArgs{ + StartLine: args.StartLine, + EndLine: args.EndLine, + }) if err != nil { return nil, err } diff --git a/cmd/frontend/graphqlbackend/repository_comparison_test.go b/cmd/frontend/graphqlbackend/repository_comparison_test.go index 6b620e5b1664..ae1d0e2adbc7 100644 --- a/cmd/frontend/graphqlbackend/repository_comparison_test.go +++ b/cmd/frontend/graphqlbackend/repository_comparison_test.go @@ -932,13 +932,13 @@ func TestFileDiffHighlighter(t *testing.T) { file1 := &dummyFileResolver{ path: "old.txt", - content: func(ctx context.Context) (string, error) { + content: func(ctx context.Context, args *GitTreeContentPageArgs) (string, error) { return "old1\nold2\nold3\n", nil }, } file2 := &dummyFileResolver{ path: "new.txt", - content: func(ctx context.Context) (string, error) { + content: func(ctx context.Context, args *GitTreeContentPageArgs) (string, error) { return "new1\nnew2\nnew3\n", nil }, } @@ -1002,29 +1002,36 @@ type dummyFileResolver struct { url string canonicalURL string - content func(context.Context) (string, error) + content func(context.Context, *GitTreeContentPageArgs) (string, error) } func (d *dummyFileResolver) Path() string { return d.path } func (d *dummyFileResolver) Name() string { return d.name } func (d *dummyFileResolver) IsDirectory() bool { return false } -func (d *dummyFileResolver) Content(ctx context.Context) (string, error) { - return d.content(ctx) +func (d *dummyFileResolver) Content(ctx context.Context, args *GitTreeContentPageArgs) (string, error) { + return d.content(ctx, args) } func (d *dummyFileResolver) ByteSize(ctx context.Context) (int32, error) { - content, err := d.content(ctx) + content, err := d.content(ctx, &GitTreeContentPageArgs{}) if err != nil { return 0, err } return int32(len([]byte(content))), nil } +func (d *dummyFileResolver) TotalLines(ctx context.Context) (int32, error) { + content, err := d.content(ctx, &GitTreeContentPageArgs{}) + if err != nil { + return 0, err + } + return int32(len(strings.Split(content, "\n"))), nil +} func (d *dummyFileResolver) Binary(ctx context.Context) (bool, error) { return false, nil } -func (d *dummyFileResolver) RichHTML(ctx context.Context) (string, error) { +func (d *dummyFileResolver) RichHTML(ctx context.Context, args *GitTreeContentPageArgs) (string, error) { return d.richHTML, nil } diff --git a/cmd/frontend/graphqlbackend/schema.graphql b/cmd/frontend/graphqlbackend/schema.graphql index dd8996c63e4f..14bbfae35749 100755 --- a/cmd/frontend/graphqlbackend/schema.graphql +++ b/cmd/frontend/graphqlbackend/schema.graphql @@ -4107,7 +4107,16 @@ type CodeIntelGitBlob { """ The content of this blob. """ - content: String! + content( + """ + Return file content starting at line "startLine". A value <= 0 will be the start of the file. + """ + startLine: Int + """ + Return file content ending at line "endLine". A value < 0 or > totalLines will set endLine to the end of the file. + """ + endLine: Int + ): String! } """ @@ -4931,7 +4940,16 @@ type CodeIntelGitTree { """ The content of this blob. """ - content: String! + content( + """ + Return file content starting at line "startLine". A value <= 0 will be the start of the file. + """ + startLine: Int + """ + Return file content ending at line "endLine". A value < 0 or > totalLines will set endLine to the end of the file. + """ + endLine: Int + ): String! } """ @@ -5103,12 +5121,25 @@ interface File2 { """ The content of this file. """ - content: String! + content( + """ + Return file content starting at line "startLine". A value <= 0 will be the start of the file. + """ + startLine: Int + """ + Return file content ending at line "endLine". A value < 0 or > totalLines will set endLine to the end of the file. + """ + endLine: Int + ): String! """ The file size in bytes. """ byteSize: Int! """ + Total line count for the file. Returns 0 for binary files. + """ + totalLines: Int! + """ Whether or not it is binary. """ binary: Boolean! @@ -5117,7 +5148,16 @@ interface File2 { rich file type. This HTML string is already escaped and thus is always safe to render. """ - richHTML: String! + richHTML( + """ + Return richHTML content starting at line "startLine". A value <= 0 will be the start of the file. + """ + startLine: Int + """ + Return richHTML content ending at line "endLine". A value < 0 or > totalLines will set endLine to the end of the file. + """ + endLine: Int + ): String! """ The URL to this file (using the input revision specifier, which may not be immutable). """ @@ -5148,6 +5188,16 @@ interface File2 { Specifies which format/highlighting technique to use. """ format: HighlightResponseFormat = HTML_HIGHLIGHT + """ + Return highlight content starting at line "startLine". A value <= 0 will be the start of the file. + Warning: Pagination only works with the HTML_PLAINTEXT format type at the moment. + """ + startLine: Int + """ + Return blob highlight ending at line "endLine". A value < 0 or > totalLines will set endLine to the end of the file. + Warning: Pagination only works with the HTML_PLAINTEXT format type at the moment. + """ + endLine: Int ): HighlightedFile! } @@ -5170,12 +5220,25 @@ type VirtualFile implements File2 { """ The content of this file. """ - content: String! + content( + """ + Return file content starting at line "startLine". A value <= 0 will be the start of the file. + """ + startLine: Int + """ + Return file content ending at line "endLine". A value < 0 or > totalLines will set endLine to the end of the file. + """ + endLine: Int + ): String! """ The file size in bytes. """ byteSize: Int! """ + Total line count for the file. Returns 0 for binary files. + """ + totalLines: Int! + """ Whether or not it is binary. """ binary: Boolean! @@ -5184,7 +5247,16 @@ type VirtualFile implements File2 { rich file type. This HTML string is already escaped and thus is always safe to render. """ - richHTML: String! + richHTML( + """ + Return richHTML content starting at line "startLine". A value <= 0 will be the start of the file. + """ + startLine: Int + """ + Return richHTML content ending at line "endLine". A value < 0 or > totalLines will set endLine to the end of the file. + """ + endLine: Int + ): String! """ Not implemented. """ @@ -5215,6 +5287,16 @@ type VirtualFile implements File2 { Specifies which format/highlighting technique to use. """ format: HighlightResponseFormat = HTML_HIGHLIGHT + """ + Return highlight content starting at line "startLine". A value <= 0 will be the start of the file. + Warning: Pagination only works with the HTML_PLAINTEXT format type at the moment. + """ + startLine: Int + """ + Return highlight content ending at line "endLine". A value < 0 or > totalLines will set endLine to the end of the file. + Warning: Pagination only works with the HTML_PLAINTEXT format type at the moment. + """ + endLine: Int ): HighlightedFile! } @@ -5263,12 +5345,25 @@ type GitBlob implements TreeEntry & File2 { """ The content of this blob. """ - content: String! + content( + """ + Return blob content starting at line "startLine". A value <= 0 will be the start of the file. + """ + startLine: Int + """ + Return blob content ending at line "endLine". A value < 0 or > totalLines will set endLine to the end of the file. + """ + endLine: Int + ): String! """ The file size in bytes. """ byteSize: Int! """ + Total line count for the Blob. Returns 0 for binary files. + """ + totalLines: Int! + """ Whether or not it is binary. """ binary: Boolean! @@ -5277,7 +5372,16 @@ type GitBlob implements TreeEntry & File2 { rich file type. This HTML string is already escaped and thus is always safe to render. """ - richHTML: String! + richHTML( + """ + Return richHTML content starting at line "startLine". A value <= 0 will be the start of the file. + """ + startLine: Int + """ + Return richHTML content ending at line "endLine". A value < 0 or > totalLines will set endLine to the end of the file. + """ + endLine: Int + ): String! """ The Git commit containing this blob. """ @@ -5320,6 +5424,16 @@ type GitBlob implements TreeEntry & File2 { Specifies which format/highlighting technique to use. """ format: HighlightResponseFormat = HTML_HIGHLIGHT + """ + Return highlight content starting at line "startLine". A value <= 0 will be the start of the file. + Warning: Pagination only works with the HTML_PLAINTEXT format type at the moment. + """ + startLine: Int + """ + Return highlight content ending at line "endLine". A value < 0 or > totalLines will set endLine to the end of the file. + Warning: Pagination only works with the HTML_PLAINTEXT format type at the moment. + """ + endLine: Int ): HighlightedFile! """ Submodule metadata if this tree points to a submodule diff --git a/cmd/frontend/graphqlbackend/symbols.go b/cmd/frontend/graphqlbackend/symbols.go index ead5d6e979ef..36327ace5bbc 100644 --- a/cmd/frontend/graphqlbackend/symbols.go +++ b/cmd/frontend/graphqlbackend/symbols.go @@ -120,8 +120,12 @@ func (r symbolResolver) Language() string { return r.Symbol.Language } func (r symbolResolver) Location() *locationResolver { stat := CreateFileInfo(r.Symbol.Path, false) sr := r.Symbol.Range() + opts := GitTreeEntryResolverOpts{ + commit: r.commit, + stat: stat, + } return &locationResolver{ - resource: NewGitTreeEntryResolver(r.db, gitserver.NewClient(r.db), r.commit, stat), + resource: NewGitTreeEntryResolver(r.db, gitserver.NewClient(r.db), opts), lspRange: &sr, } } diff --git a/cmd/frontend/graphqlbackend/virtual_file.go b/cmd/frontend/graphqlbackend/virtual_file.go index 0089f8619df4..9240d687642a 100644 --- a/cmd/frontend/graphqlbackend/virtual_file.go +++ b/cmd/frontend/graphqlbackend/virtual_file.go @@ -5,6 +5,7 @@ import ( "fmt" "io/fs" "path" + "strings" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" @@ -56,19 +57,32 @@ func (r *VirtualFileResolver) ExternalURLs(ctx context.Context) ([]*externallink } func (r *VirtualFileResolver) ByteSize(ctx context.Context) (int32, error) { - content, err := r.Content(ctx) + content, err := r.Content(ctx, &GitTreeContentPageArgs{}) if err != nil { return 0, err } return int32(len([]byte(content))), nil } -func (r *VirtualFileResolver) Content(ctx context.Context) (string, error) { +func (r *VirtualFileResolver) TotalLines(ctx context.Context) (int32, error) { + // If it is a binary, return 0 + binary, err := r.Binary(ctx) + if err != nil || binary { + return 0, err + } + content, err := r.Content(ctx, &GitTreeContentPageArgs{}) + if err != nil { + return 0, err + } + return int32(len(strings.Split(content, "\n"))), nil +} + +func (r *VirtualFileResolver) Content(ctx context.Context, args *GitTreeContentPageArgs) (string, error) { return r.fileContent(ctx) } -func (r *VirtualFileResolver) RichHTML(ctx context.Context) (string, error) { - content, err := r.Content(ctx) +func (r *VirtualFileResolver) RichHTML(ctx context.Context, args *GitTreeContentPageArgs) (string, error) { + content, err := r.Content(ctx, args) if err != nil { return "", err } @@ -76,7 +90,7 @@ func (r *VirtualFileResolver) RichHTML(ctx context.Context) (string, error) { } func (r *VirtualFileResolver) Binary(ctx context.Context) (bool, error) { - content, err := r.Content(ctx) + content, err := r.Content(ctx, &GitTreeContentPageArgs{}) if err != nil { return false, err } @@ -89,7 +103,7 @@ var highlightHistogram = promauto.NewHistogram(prometheus.HistogramOpts{ }) func (r *VirtualFileResolver) Highlight(ctx context.Context, args *HighlightArgs) (*HighlightedFileResolver, error) { - content, err := r.Content(ctx) + content, err := r.Content(ctx, &GitTreeContentPageArgs{StartLine: args.StartLine, EndLine: args.EndLine}) if err != nil { return nil, err } diff --git a/cmd/frontend/graphqlbackend/virtual_file_test.go b/cmd/frontend/graphqlbackend/virtual_file_test.go index cba7c6f58a00..a4d8fca9d81e 100644 --- a/cmd/frontend/graphqlbackend/virtual_file_test.go +++ b/cmd/frontend/graphqlbackend/virtual_file_test.go @@ -36,7 +36,7 @@ func TestVirtualFile(t *testing.T) { } }) t.Run("Content", func(t *testing.T) { - have, err := vfr.Content(context.Background()) + have, err := vfr.Content(context.Background(), &GitTreeContentPageArgs{}) if err != nil { t.Fatal(err) } @@ -54,7 +54,7 @@ func TestVirtualFile(t *testing.T) { } }) t.Run("RichHTML", func(t *testing.T) { - have, err := vfr.RichHTML(context.Background()) + have, err := vfr.RichHTML(context.Background(), &GitTreeContentPageArgs{}) if err != nil { t.Fatal(err) } diff --git a/enterprise/cmd/frontend/internal/batches/resolvers/batch_spec_workspace_file.go b/enterprise/cmd/frontend/internal/batches/resolvers/batch_spec_workspace_file.go index 3b302b1e751a..d4ce7bd79c6c 100644 --- a/enterprise/cmd/frontend/internal/batches/resolvers/batch_spec_workspace_file.go +++ b/enterprise/cmd/frontend/internal/batches/resolvers/batch_spec_workspace_file.go @@ -3,6 +3,7 @@ package resolvers import ( "context" "fmt" + "strings" "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/relay" @@ -84,7 +85,7 @@ func (r *batchSpecWorkspaceFileResolver) IsDirectory() bool { return false } -func (r *batchSpecWorkspaceFileResolver) Content(ctx context.Context) (string, error) { +func (r *batchSpecWorkspaceFileResolver) Content(ctx context.Context, args *graphqlbackend.GitTreeContentPageArgs) (string, error) { return "", errors.New("not implemented") } @@ -92,12 +93,21 @@ func (r *batchSpecWorkspaceFileResolver) ByteSize(ctx context.Context) (int32, e return int32(r.file.Size), nil } +func (r *batchSpecWorkspaceFileResolver) TotalLines(ctx context.Context) (int32, error) { + // If it is a binary, return 0 + binary, err := r.Binary(ctx) + if err != nil || binary { + return 0, err + } + return int32(len(strings.Split(string(r.file.Content), "\n"))), nil +} + func (r *batchSpecWorkspaceFileResolver) Binary(ctx context.Context) (bool, error) { vfr := r.createVirtualFile(r.file.Content, r.file.Path) return vfr.Binary(ctx) } -func (r *batchSpecWorkspaceFileResolver) RichHTML(ctx context.Context) (string, error) { +func (r *batchSpecWorkspaceFileResolver) RichHTML(ctx context.Context, args *graphqlbackend.GitTreeContentPageArgs) (string, error) { return "", errors.New("not implemented") } diff --git a/enterprise/cmd/frontend/internal/batches/resolvers/batch_spec_workspace_file_test.go b/enterprise/cmd/frontend/internal/batches/resolvers/batch_spec_workspace_file_test.go index 0458d862a86a..1d7d6f792e58 100644 --- a/enterprise/cmd/frontend/internal/batches/resolvers/batch_spec_workspace_file_test.go +++ b/enterprise/cmd/frontend/internal/batches/resolvers/batch_spec_workspace_file_test.go @@ -31,10 +31,13 @@ func (m *mockFileResolver) Binary(ctx context.Context) (bool, error) { func (m *mockFileResolver) ByteSize(ctx context.Context) (int32, error) { return 0, errors.New("not implemented") } -func (m *mockFileResolver) Content(ctx context.Context) (string, error) { +func (m *mockFileResolver) TotalLines(ctx context.Context) (int32, error) { + return 0, errors.New("not implemented") +} +func (m *mockFileResolver) Content(ctx context.Context, args *graphqlbackend.GitTreeContentPageArgs) (string, error) { return "", errors.New("not implemented") } -func (m *mockFileResolver) RichHTML(ctx context.Context) (string, error) { +func (m *mockFileResolver) RichHTML(ctx context.Context, args *graphqlbackend.GitTreeContentPageArgs) (string, error) { return "", errors.New("not implemented") } func (m *mockFileResolver) URL(ctx context.Context) (string, error) { @@ -158,7 +161,7 @@ func TestBatchSpecWorkspaceFileResolver(t *testing.T) { { name: "Content", getActual: func() (interface{}, error) { - return resolver.Content(context.Background()) + return resolver.Content(context.Background(), &graphqlbackend.GitTreeContentPageArgs{}) }, expected: "", expectedErr: errors.New("not implemented"), @@ -173,7 +176,7 @@ func TestBatchSpecWorkspaceFileResolver(t *testing.T) { { name: "RichHTML", getActual: func() (interface{}, error) { - return resolver.RichHTML(context.Background()) + return resolver.RichHTML(context.Background(), &graphqlbackend.GitTreeContentPageArgs{}) }, expected: "", expectedErr: errors.New("not implemented"), @@ -303,7 +306,7 @@ func TestBatchSpecWorkspaceFileResolver(t *testing.T) { { name: "Content", getActual: func() (interface{}, error) { - return resolver.Content(context.Background()) + return resolver.Content(context.Background(), &graphqlbackend.GitTreeContentPageArgs{}) }, expected: "", expectedErr: errors.New("not implemented"), @@ -318,7 +321,7 @@ func TestBatchSpecWorkspaceFileResolver(t *testing.T) { { name: "RichHTML", getActual: func() (interface{}, error) { - return resolver.RichHTML(context.Background()) + return resolver.RichHTML(context.Background(), &graphqlbackend.GitTreeContentPageArgs{}) }, expected: "", expectedErr: errors.New("not implemented"), diff --git a/enterprise/internal/codeintel/shared/resolvers/git_tree_entry_resolver.go b/enterprise/internal/codeintel/shared/resolvers/git_tree_entry_resolver.go index 29f5690af8c8..91f34b3cd028 100644 --- a/enterprise/internal/codeintel/shared/resolvers/git_tree_entry_resolver.go +++ b/enterprise/internal/codeintel/shared/resolvers/git_tree_entry_resolver.go @@ -54,14 +54,14 @@ func (r *GitTreeEntryResolver) ToGitBlob() (resolverstubs.GitTreeEntryResolver, // func (r *GitTreeEntryResolver) ToVirtualFile() (*virtualFileResolver, bool) { return nil, false } func (r *GitTreeEntryResolver) ByteSize(ctx context.Context) (int32, error) { - content, err := r.Content(ctx) + content, err := r.Content(ctx, &resolverstubs.GitTreeContentPageArgs{}) if err != nil { return 0, err } return int32(len([]byte(content))), nil } -func (r *GitTreeEntryResolver) Content(ctx context.Context) (string, error) { +func (r *GitTreeEntryResolver) Content(ctx context.Context, args *resolverstubs.GitTreeContentPageArgs) (string, error) { r.contentOnce.Do(func() { ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() diff --git a/internal/codeintel/resolvers/all.go b/internal/codeintel/resolvers/all.go index 9f6a99c27d35..24d7d94ca5a9 100644 --- a/internal/codeintel/resolvers/all.go +++ b/internal/codeintel/resolvers/all.go @@ -262,13 +262,18 @@ type PositionResolver interface { Character() int32 } +type GitTreeContentPageArgs struct { + StartLine *int32 + EndLine *int32 +} + type GitTreeEntryResolver interface { Path() string Name() string ToGitTree() (GitTreeEntryResolver, bool) ToGitBlob() (GitTreeEntryResolver, bool) ByteSize(ctx context.Context) (int32, error) - Content(ctx context.Context) (string, error) + Content(ctx context.Context, args *GitTreeContentPageArgs) (string, error) Commit() GitCommitResolver Repository() RepositoryResolver CanonicalURL() string From 7aee3047c997e675a846474c57b98cf1b22fa783 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Jan 2023 17:51:35 +0100 Subject: [PATCH 041/678] Parse GitHub PRs/issues in commit message (#46409) --- CHANGELOG.md | 1 + client/web/src/repo/blame/useBlameHunks.ts | 52 +++++++++---- client/web/src/repo/blob/BlameDecoration.tsx | 74 ++++++++++++++++--- client/web/src/repo/blob/Blob.tsx | 4 +- client/web/src/repo/blob/BlobPage.tsx | 4 +- client/web/src/repo/blob/CodeMirrorBlob.tsx | 8 +- .../blob/codemirror/blame-decorations.tsx | 35 +++++---- 7 files changed, 128 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab361121a87e..6fa88eb222cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ All notable changes to Sourcegraph are documented in this file. - Zoekt by default eagerly unmarshals the symbol index into memory. Previously we would unmarshal on every request for the purposes of symbol searches or ranking. This lead to pressure on the Go garbage collector. On sourcegraph.com we have noticed time spent in the garbage collector halved. In the unlikely event this leads to more OOMs in zoekt-webserver, you can disable by setting the environment variable `ZOEKT_ENABLE_LAZY_DOC_SECTIONS=t`. [zoekt#503](https://github.com/sourcegraph/zoekt/pull/503) - Removes the right side action sidebar that is shown on the code view page and moves the icons into the top nav. [#46339](https://github.com/sourcegraph/sourcegraph/pull/46339) - The `sourcegraph/prometheus` image no longer starts with `--web.enable-lifecycle --web.enable-admin-api` by default - these flags can be re-enabled by configuring `PROMETHEUS_ADDITIONAL_FLAGS` on the container. [#46393](https://github.com/sourcegraph/sourcegraph/pull/46393) +- Renders GitHub pull request references in git blame view. [#46409](https://github.com/sourcegraph/sourcegraph/pull/46409) ### Fixed diff --git a/client/web/src/repo/blame/useBlameHunks.ts b/client/web/src/repo/blame/useBlameHunks.ts index cab492ec17ef..54747742906b 100644 --- a/client/web/src/repo/blame/useBlameHunks.ts +++ b/client/web/src/repo/blame/useBlameHunks.ts @@ -14,6 +14,7 @@ import { useObservable } from '@sourcegraph/wildcard' import { requestGraphQL } from '../../backend/graphql' import { useFeatureFlag } from '../../featureFlags/useFeatureFlag' import { + ExternalServiceKind, FirstCommitDateResult, FirstCommitDateVariables, GitBlameResult, @@ -62,6 +63,12 @@ export interface BlameHunk { displayInfo: BlameHunkDisplayInfo } +export interface BlameHunkData { + current: BlameHunk[] | undefined + externalURLs: { url: string; serviceKind: ExternalServiceKind | null }[] | undefined + firstCommitDate: Date | undefined +} + const fetchBlameViaGraphQL = memoizeObservable( ({ repoName, @@ -73,11 +80,15 @@ const fetchBlameViaGraphQL = memoizeObservable( revision: string filePath: string sourcegraphURL: string - }): Observable<{ current: BlameHunk[] | undefined; firstCommitDate: Date | undefined }> => + }): Observable => requestGraphQL( gql` query GitBlame($repo: String!, $rev: String!, $path: String!) { repository(name: $repo) { + externalURLs { + url + serviceKind + } firstEverCommit { author { date @@ -121,14 +132,16 @@ const fetchBlameViaGraphQL = memoizeObservable( map(({ repository }) => { const hunks = repository?.commit?.blob?.blame const firstCommitDate = repository?.firstEverCommit?.author?.date + const externalURLs = repository?.externalURLs if (hunks) { return { current: hunks.map(blame => addDisplayInfoForHunk(blame, sourcegraphURL)), + externalURLs, firstCommitDate: firstCommitDate ? new Date(firstCommitDate) : undefined, } } - return { current: undefined, firstCommitDate: undefined } + return { current: undefined, externalURLs: undefined, firstCommitDate: undefined } }) ), makeRepoURI @@ -175,17 +188,19 @@ const fetchBlameViaStreaming = memoizeObservable( revision: string filePath: string sourcegraphURL: string - }): Observable<{ current: BlameHunk[] | undefined; firstCommitDate: Date | undefined }> => - new Observable<{ current: BlameHunk[] | undefined; firstCommitDate: Date | undefined }>(subscriber => { + }): Observable => + new Observable(subscriber => { let didEmitFirstCommitDate = false let firstCommitDate: Date | undefined + let externalURLs: BlameHunkData['externalURLs'] const assembledHunks: BlameHunk[] = [] const repoAndRevisionPath = `/${repoName}${revision ? `@${revision}` : ''}` Promise.all([ - fetchFirstCommitDate(repoName).then(date => { - firstCommitDate = date + fetchRepositoryData(repoName).then(res => { + firstCommitDate = res.firstCommitDate + externalURLs = res.externalURLs }), fetchEventSource(`/.api/blame${repoAndRevisionPath}/stream/${filePath}`, { method: 'GET', @@ -224,7 +239,7 @@ const fetchBlameViaStreaming = memoizeObservable( if (firstCommitDate !== undefined) { didEmitFirstCommitDate = true } - subscriber.next({ current: assembledHunks, firstCommitDate }) + subscriber.next({ current: assembledHunks, externalURLs, firstCommitDate }) } }, onerror(event) { @@ -236,7 +251,7 @@ const fetchBlameViaStreaming = memoizeObservable( () => { // This case can happen when the event source yields before the commit date is resolved if (!didEmitFirstCommitDate) { - subscriber.next({ current: assembledHunks, firstCommitDate }) + subscriber.next({ current: assembledHunks, externalURLs, firstCommitDate }) } subscriber.complete() @@ -249,7 +264,7 @@ const fetchBlameViaStreaming = memoizeObservable( makeRepoURI ) -async function fetchFirstCommitDate(repoName: string): Promise { +async function fetchRepositoryData(repoName: string): Promise> { return requestGraphQL( gql` query FirstCommitDate($repo: String!) { @@ -259,6 +274,10 @@ async function fetchFirstCommitDate(repoName: string): Promise date } } + externalURLs { + url + serviceKind + } } } `, @@ -266,8 +285,13 @@ async function fetchFirstCommitDate(repoName: string): Promise ) .pipe( map(dataOrThrowErrors), - map(({ repository }) => repository?.firstEverCommit?.author?.date), - map(date => (date ? new Date(date) : undefined)) + map(({ repository }) => { + const firstCommitDate = repository?.firstEverCommit?.author?.date + return { + externalURLs: repository?.externalURLs, + firstCommitDate: firstCommitDate ? new Date(firstCommitDate) : undefined, + } + }) ) .toPromise() } @@ -317,7 +341,7 @@ export const useBlameHunks = ( enableCodeMirror: boolean }, sourcegraphURL: string -): { current: BlameHunk[] | undefined; firstCommitDate: Date | undefined } => { +): BlameHunkData => { const [enableStreamingGitBlame, status] = useFeatureFlag('enable-streaming-git-blame') const [isBlameVisible] = useBlameVisibility() @@ -330,12 +354,12 @@ export const useBlameHunks = ( ? enableCodeMirror && enableStreamingGitBlame ? fetchBlameViaStreaming({ revision, repoName, filePath, sourcegraphURL }) : fetchBlameViaGraphQL({ revision, repoName, filePath, sourcegraphURL }) - : of({ current: undefined, firstCommitDate: undefined }), + : of({ current: undefined, externalURLs: undefined, firstCommitDate: undefined }), [shouldFetchBlame, enableCodeMirror, enableStreamingGitBlame, revision, repoName, filePath, sourcegraphURL] ) ) - return hunks || { current: undefined, firstCommitDate: undefined } + return hunks || { current: undefined, externalURLs: undefined, firstCommitDate: undefined } } const ONE_MONTH = 30 * 24 * 60 * 60 * 1000 diff --git a/client/web/src/repo/blob/BlameDecoration.tsx b/client/web/src/repo/blob/BlameDecoration.tsx index fb154f678535..1e89571d8c11 100644 --- a/client/web/src/repo/blob/BlameDecoration.tsx +++ b/client/web/src/repo/blob/BlameDecoration.tsx @@ -19,10 +19,11 @@ import { useObservable, } from '@sourcegraph/wildcard' +import { ExternalServiceKind } from '../../graphql-operations' import { eventLogger } from '../../tracking/eventLogger' import { UserAvatar } from '../../user/UserAvatar' import { replaceRevisionInURL } from '../../util/url' -import { BlameHunk } from '../blame/useBlameHunks' +import { BlameHunk, BlameHunkData } from '../blame/useBlameHunks' import { useBlameRecencyColor } from './BlameRecency' @@ -111,13 +112,14 @@ const usePopover = ({ export const BlameDecoration: React.FunctionComponent<{ line: number // 1-based line number blameHunk?: BlameHunk - firstCommitDate?: Date + firstCommitDate?: BlameHunkData['firstCommitDate'] + externalURLs?: BlameHunkData['externalURLs'] history: History onSelect?: (line: number) => void onDeselect?: (line: number) => void isLightTheme: boolean hideRecency: boolean -}> = ({ line, blameHunk, history, onSelect, onDeselect, firstCommitDate, isLightTheme, hideRecency }) => { +}> = ({ line, blameHunk, history, onSelect, onDeselect, firstCommitDate, externalURLs, isLightTheme, hideRecency }) => { const hunkStartLine = blameHunk?.startLine ?? line const id = hunkStartLine?.toString() || '' const onOpen = useCallback(() => { @@ -237,15 +239,8 @@ export const BlameDecoration: React.FunctionComponent<{ as={SourceCommitIcon} className={classNames('mr-2 flex-shrink-0', styles.icon)} /> - - {blameHunk.message} - + + {generateCommitMessageWithLinks(blameHunk, externalURLs)} {blameHunk.commit.parents.length > 0 && ( <> @@ -275,6 +270,61 @@ export const BlameDecoration: React.FunctionComponent<{ ) } +// This regex is supposed to match in the following cases: +// +// - Create search and search-ui packages (#29773) +// - Fix #123 for xyz +// +// However it is supposed not to mach in: +// +// - Something sourcegraph/other-repo#123 or so +// - 123#123 +const GH_ISSUE_NUMBER_IN_COMMIT = /([^\dA-Za-z](#\d+))/g + +const generateCommitMessageWithLinks = ( + blameHunk: BlameHunk, + externalURLs: BlameHunkData['externalURLs'] +): React.ReactNode => { + const commitLinkProps = { + to: blameHunk.displayInfo.linkURL, + target: '_blank', + rel: 'noreferrer noopener', + className: styles.link, + onClick: logCommitClick, + } + + const github = externalURLs ? externalURLs.find(url => url.serviceKind === ExternalServiceKind.GITHUB) : null + const message = blameHunk.message + const matches = [...message.matchAll(GH_ISSUE_NUMBER_IN_COMMIT)] + if (github && matches.length > 0) { + let remainingMessage = message + let skippedCharacters = 0 + const linkSegments: React.ReactNode[] = [] + + for (const match of matches) { + if (match.index === undefined) { + continue + } + const issueNumber = match[2] + const index = remainingMessage.indexOf(issueNumber, match.index - skippedCharacters) + const before = remainingMessage.slice(0, index) + + linkSegments.push({before}) + linkSegments.push({issueNumber}) + + const nextIndex = index + issueNumber.length + remainingMessage = remainingMessage.slice(index + issueNumber.length) + skippedCharacters += nextIndex + } + + linkSegments.push({remainingMessage}) + + return
{linkSegments}
+ } + + return {blameHunk.message} +} + const logCommitClick = (): void => { eventLogger.log('GitBlamePopupClicked', { target: 'commit' }, { target: 'commit' }) } diff --git a/client/web/src/repo/blob/Blob.tsx b/client/web/src/repo/blob/Blob.tsx index 4bfdfd8814b4..929b0aff6f47 100644 --- a/client/web/src/repo/blob/Blob.tsx +++ b/client/web/src/repo/blob/Blob.tsx @@ -67,7 +67,7 @@ import { Code, useObservable } from '@sourcegraph/wildcard' import { getHover, getDocumentHighlights } from '../../backend/features' import { WebHoverOverlay } from '../../components/shared' import { BlobStencilFields, ExternalLinkFields, Scalars } from '../../graphql-operations' -import { BlameHunk } from '../blame/useBlameHunks' +import { BlameHunkData } from '../blame/useBlameHunks' import { HoverThresholdProps } from '../RepoContainer' import { BlameColumn } from './BlameColumn' @@ -114,7 +114,7 @@ export interface BlobProps supportsFindImplementations?: boolean isBlameVisible?: boolean - blameHunks?: { current: BlameHunk[] | undefined; firstCommitDate: Date | undefined } + blameHunks?: BlameHunkData } export interface BlobInfo extends AbsoluteRepoFile, ModeSpec { diff --git a/client/web/src/repo/blob/BlobPage.tsx b/client/web/src/repo/blob/BlobPage.tsx index 7033c7c1cd7e..bb923a8225b5 100644 --- a/client/web/src/repo/blob/BlobPage.tsx +++ b/client/web/src/repo/blob/BlobPage.tsx @@ -327,7 +327,7 @@ export const BlobPage: React.FunctionComponent = props => { const wrapCodeSettings = useMemo(() => (wrapCode ? EditorView.lineWrapping : []), [wrapCode]) const blameDecorations = useMemo( - () => - createBlameDecorationsExtension( - !!isBlameVisible, - blameHunks?.current, - blameHunks?.firstCommitDate, - isLightTheme - ), + () => createBlameDecorationsExtension(!!isBlameVisible, blameHunks, isLightTheme), [isBlameVisible, blameHunks, isLightTheme] ) diff --git a/client/web/src/repo/blob/codemirror/blame-decorations.tsx b/client/web/src/repo/blob/codemirror/blame-decorations.tsx index 28f2e2aa101b..92524d382b88 100644 --- a/client/web/src/repo/blob/codemirror/blame-decorations.tsx +++ b/client/web/src/repo/blob/codemirror/blame-decorations.tsx @@ -22,7 +22,7 @@ import { createRoot, Root } from 'react-dom/client' import { createUpdateableField } from '@sourcegraph/shared/src/components/CodeMirrorEditor' -import { BlameHunk } from '../../blame/useBlameHunks' +import { BlameHunk, BlameHunkData } from '../../blame/useBlameHunks' import { BlameDecoration } from '../BlameDecoration' import { blobPropsFacet } from '.' @@ -61,7 +61,7 @@ class BlameDecorationWidget extends WidgetType { // We can not access the light theme and first commit date from the view // props because we need the widget to re-render when it updates. public readonly isLightTheme: boolean, - public readonly firstCommitDate: Date | undefined + public readonly blameHunkMetadata: Omit ) { super() const blobProps = this.view.state.facet(blobPropsFacet) @@ -86,7 +86,8 @@ class BlameDecorationWidget extends WidgetType { history={this.state.history} onSelect={this.selectRow} onDeselect={this.deselectRow} - firstCommitDate={this.firstCommitDate} + firstCommitDate={this.blameHunkMetadata.firstCommitDate} + externalURLs={this.blameHunkMetadata.externalURLs} isLightTheme={this.isLightTheme} hideRecency={false} /> @@ -119,7 +120,7 @@ class BlameDecorationWidget extends WidgetType { interface BlameDecorationsFacetProps { hunks: BlameHunk[] isLightTheme: boolean - firstCommitDate: Date | undefined + blameHunkMetadata: Omit } const showGitBlameDecorations = Facet.define({ combine: decorations => decorations[0], @@ -132,7 +133,7 @@ const showGitBlameDecorations = Facet.define | undefined constructor(view: EditorView) { this.decorations = this.computeDecorations(view, facet) @@ -142,19 +143,19 @@ const showGitBlameDecorations = Facet.define [ blameLineStyles({ isBlameVisible }), isBlameVisible ? showBlameGutter.of(isBlameVisible) : [], - blameHunks ? showGitBlameDecorations.of({ hunks: blameHunks, isLightTheme, firstCommitDate }) : [], + blameHunks?.current + ? showGitBlameDecorations.of({ + hunks: blameHunks.current, + isLightTheme, + blameHunkMetadata: { + firstCommitDate: blameHunks.firstCommitDate, + externalURLs: blameHunks.externalURLs, + }, + }) + : [], ] From 46cbcda2c23b0d81a09a1592dcfd99e8f24261ff Mon Sep 17 00:00:00 2001 From: Alex Ostrikov Date: Thu, 19 Jan 2023 21:16:32 +0400 Subject: [PATCH 042/678] permissions-center: use `NextSyncAt` for delay between GitHub webhook is received and sync is scheduled. (#46666) Test plan: Unit tests updated. --- .../cmd/frontend/internal/authz/webhooks/github.go | 14 ++++++-------- internal/authz/permssync/permssync.go | 2 ++ internal/authz/permssync/permssync_test.go | 9 +++++++-- internal/repoupdater/protocol/repoupdater.go | 1 + 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/enterprise/cmd/frontend/internal/authz/webhooks/github.go b/enterprise/cmd/frontend/internal/authz/webhooks/github.go index 1b0fab1199fb..e8478d7bbc2f 100644 --- a/enterprise/cmd/frontend/internal/authz/webhooks/github.go +++ b/enterprise/cmd/frontend/internal/authz/webhooks/github.go @@ -166,11 +166,10 @@ func (h *GitHubWebhook) getUserAndSyncPerms(ctx context.Context, db database.DB, return errors.Newf("no github external accounts found with account id %d", user.GetID()) } - // TODO: Here we should call another method to set `NextSyncAt` on the - // PermsSyncJob if that feature is enabled. permssync.SchedulePermsSync(ctx, h.logger, db, protocol.PermsSyncRequest{ - UserIDs: []int32{externalAccounts[0].UserID}, - Reason: reason, + UserIDs: []int32{externalAccounts[0].UserID}, + Reason: reason, + NextSyncAt: time.Now().Add(sleepTime), }) return err @@ -184,11 +183,10 @@ func (h *GitHubWebhook) getRepoAndSyncPerms(ctx context.Context, db database.DB, return err } - // TODO: Here we should call another method to set `NextSyncAt` on the - // PermsSyncJob if that feature is enabled. permssync.SchedulePermsSync(ctx, h.logger, db, protocol.PermsSyncRequest{ - RepoIDs: []api.RepoID{repo.ID}, - Reason: reason, + RepoIDs: []api.RepoID{repo.ID}, + Reason: reason, + NextSyncAt: time.Now().Add(sleepTime), }) return nil diff --git a/internal/authz/permssync/permssync.go b/internal/authz/permssync/permssync.go index fbefddccfa3f..23d126d4a264 100644 --- a/internal/authz/permssync/permssync.go +++ b/internal/authz/permssync/permssync.go @@ -82,6 +82,7 @@ func SchedulePermsSync(ctx context.Context, logger log.Logger, db database.DB, r InvalidateCaches: req.Options.InvalidateCaches, Reason: req.Reason, TriggeredByUserID: req.TriggeredByUserID, + NextSyncAt: req.NextSyncAt, } err := db.PermissionSyncJobs().CreateUserSyncJob(ctx, userID, opts) if err != nil { @@ -95,6 +96,7 @@ func SchedulePermsSync(ctx context.Context, logger log.Logger, db database.DB, r InvalidateCaches: req.Options.InvalidateCaches, Reason: req.Reason, TriggeredByUserID: req.TriggeredByUserID, + NextSyncAt: req.NextSyncAt, } err := db.PermissionSyncJobs().CreateRepoSyncJob(ctx, repoID, opts) if err != nil { diff --git a/internal/authz/permssync/permssync_test.go b/internal/authz/permssync/permssync_test.go index 48d0456c9cf2..f0f04c2ac004 100644 --- a/internal/authz/permssync/permssync_test.go +++ b/internal/authz/permssync/permssync_test.go @@ -3,6 +3,7 @@ package permssync import ( "context" "testing" + "time" "github.com/sourcegraph/log/logtest" "github.com/sourcegraph/sourcegraph/internal/api" @@ -26,7 +27,8 @@ func TestSchedulePermsSync_UserPermsTest(t *testing.T) { db.PermissionSyncJobsFunc.SetDefaultReturn(permsSyncStore) db.FeatureFlagsFunc.SetDefaultReturn(featureFlags) - request := protocol.PermsSyncRequest{UserIDs: []int32{1}, Reason: ReasonManualUserSync, TriggeredByUserID: int32(123)} + syncTime := time.Now().Add(13 * time.Second) + request := protocol.PermsSyncRequest{UserIDs: []int32{1}, Reason: ReasonManualUserSync, TriggeredByUserID: int32(123), NextSyncAt: syncTime} SchedulePermsSync(ctx, logger, db, request) assert.Len(t, permsSyncStore.CreateUserSyncJobFunc.History(), 1) assert.Empty(t, permsSyncStore.CreateRepoSyncJobFunc.History()) @@ -34,6 +36,7 @@ func TestSchedulePermsSync_UserPermsTest(t *testing.T) { assert.NotNil(t, permsSyncStore.CreateUserSyncJobFunc.History()[0].Arg2) assert.Equal(t, ReasonManualUserSync, permsSyncStore.CreateUserSyncJobFunc.History()[0].Arg2.Reason) assert.Equal(t, int32(123), permsSyncStore.CreateUserSyncJobFunc.History()[0].Arg2.TriggeredByUserID) + assert.Equal(t, syncTime, permsSyncStore.CreateUserSyncJobFunc.History()[0].Arg2.NextSyncAt) } func TestSchedulePermsSync_RepoPermsTest(t *testing.T) { @@ -51,7 +54,8 @@ func TestSchedulePermsSync_RepoPermsTest(t *testing.T) { db.PermissionSyncJobsFunc.SetDefaultReturn(permsSyncStore) db.FeatureFlagsFunc.SetDefaultReturn(featureFlags) - request := protocol.PermsSyncRequest{RepoIDs: []api.RepoID{1}, Reason: ReasonManualRepoSync} + syncTime := time.Now().Add(37 * time.Second) + request := protocol.PermsSyncRequest{RepoIDs: []api.RepoID{1}, Reason: ReasonManualRepoSync, NextSyncAt: syncTime} SchedulePermsSync(ctx, logger, db, request) assert.Len(t, permsSyncStore.CreateRepoSyncJobFunc.History(), 1) assert.Empty(t, permsSyncStore.CreateUserSyncJobFunc.History()) @@ -59,4 +63,5 @@ func TestSchedulePermsSync_RepoPermsTest(t *testing.T) { assert.NotNil(t, permsSyncStore.CreateRepoSyncJobFunc.History()[0].Arg1) assert.Equal(t, ReasonManualRepoSync, permsSyncStore.CreateRepoSyncJobFunc.History()[0].Arg2.Reason) assert.Equal(t, int32(0), permsSyncStore.CreateRepoSyncJobFunc.History()[0].Arg2.TriggeredByUserID) + assert.Equal(t, syncTime, permsSyncStore.CreateRepoSyncJobFunc.History()[0].Arg2.NextSyncAt) } diff --git a/internal/repoupdater/protocol/repoupdater.go b/internal/repoupdater/protocol/repoupdater.go index 3ccf78c70070..d6ddfea96b03 100644 --- a/internal/repoupdater/protocol/repoupdater.go +++ b/internal/repoupdater/protocol/repoupdater.go @@ -250,6 +250,7 @@ type PermsSyncRequest struct { Options authz.FetchPermsOptions `json:"options"` Reason string `json:"reason"` TriggeredByUserID int32 `json:"triggered_by_user_id"` + NextSyncAt time.Time `json:"next_sync_at"` } // PermsSyncResponse is a response to sync permissions. From e59a47767b46410ea36c6d9d4da13fea43e788ed Mon Sep 17 00:00:00 2001 From: Alex Ostrikov Date: Thu, 19 Jan 2023 21:26:47 +0400 Subject: [PATCH 043/678] permissions-center: implement perms sync deduplication logic. (#46656) Test plan: 1) sg migration up -> down -> up. 2) Database tests added. --- internal/database/mocks_temp.go | 125 +++++++++++ internal/database/permission_sync_jobs.go | 123 ++++++++++- .../database/permission_sync_jobs_test.go | 199 +++++++++++++++--- internal/database/schema.json | 53 +++-- internal/database/schema.md | 7 +- .../down.sql | 39 ++++ .../metadata.yaml | 2 + .../up.sql | 43 ++++ migrations/frontend/squashed.sql | 7 +- 9 files changed, 544 insertions(+), 54 deletions(-) create mode 100644 migrations/frontend/1674041632_add_constraints_to_permission_sync_jobs_table/down.sql create mode 100644 migrations/frontend/1674041632_add_constraints_to_permission_sync_jobs_table/metadata.yaml create mode 100644 migrations/frontend/1674041632_add_constraints_to_permission_sync_jobs_table/up.sql diff --git a/internal/database/mocks_temp.go b/internal/database/mocks_temp.go index 2386b5a99618..2f497d29ffad 100644 --- a/internal/database/mocks_temp.go +++ b/internal/database/mocks_temp.go @@ -35688,6 +35688,9 @@ func (c OutboundWebhookStoreWithFuncCall) Results() []interface{} { // github.com/sourcegraph/sourcegraph/internal/database) used for unit // testing. type MockPermissionSyncJobStore struct { + // CancelQueuedJobFunc is an instance of a mock function object + // controlling the behavior of the method CancelQueuedJob. + CancelQueuedJobFunc *PermissionSyncJobStoreCancelQueuedJobFunc // CreateRepoSyncJobFunc is an instance of a mock function object // controlling the behavior of the method CreateRepoSyncJob. CreateRepoSyncJobFunc *PermissionSyncJobStoreCreateRepoSyncJobFunc @@ -35716,6 +35719,11 @@ type MockPermissionSyncJobStore struct { // results, unless overwritten. func NewMockPermissionSyncJobStore() *MockPermissionSyncJobStore { return &MockPermissionSyncJobStore{ + CancelQueuedJobFunc: &PermissionSyncJobStoreCancelQueuedJobFunc{ + defaultHook: func(context.Context, int) (r0 error) { + return + }, + }, CreateRepoSyncJobFunc: &PermissionSyncJobStoreCreateRepoSyncJobFunc{ defaultHook: func(context.Context, api.RepoID, PermissionSyncJobOpts) (r0 error) { return @@ -35759,6 +35767,11 @@ func NewMockPermissionSyncJobStore() *MockPermissionSyncJobStore { // overwritten. func NewStrictMockPermissionSyncJobStore() *MockPermissionSyncJobStore { return &MockPermissionSyncJobStore{ + CancelQueuedJobFunc: &PermissionSyncJobStoreCancelQueuedJobFunc{ + defaultHook: func(context.Context, int) error { + panic("unexpected invocation of MockPermissionSyncJobStore.CancelQueuedJob") + }, + }, CreateRepoSyncJobFunc: &PermissionSyncJobStoreCreateRepoSyncJobFunc{ defaultHook: func(context.Context, api.RepoID, PermissionSyncJobOpts) error { panic("unexpected invocation of MockPermissionSyncJobStore.CreateRepoSyncJob") @@ -35802,6 +35815,9 @@ func NewStrictMockPermissionSyncJobStore() *MockPermissionSyncJobStore { // implementation, unless overwritten. func NewMockPermissionSyncJobStoreFrom(i PermissionSyncJobStore) *MockPermissionSyncJobStore { return &MockPermissionSyncJobStore{ + CancelQueuedJobFunc: &PermissionSyncJobStoreCancelQueuedJobFunc{ + defaultHook: i.CancelQueuedJob, + }, CreateRepoSyncJobFunc: &PermissionSyncJobStoreCreateRepoSyncJobFunc{ defaultHook: i.CreateRepoSyncJob, }, @@ -35826,6 +35842,115 @@ func NewMockPermissionSyncJobStoreFrom(i PermissionSyncJobStore) *MockPermission } } +// PermissionSyncJobStoreCancelQueuedJobFunc describes the behavior when the +// CancelQueuedJob method of the parent MockPermissionSyncJobStore instance +// is invoked. +type PermissionSyncJobStoreCancelQueuedJobFunc struct { + defaultHook func(context.Context, int) error + hooks []func(context.Context, int) error + history []PermissionSyncJobStoreCancelQueuedJobFuncCall + mutex sync.Mutex +} + +// CancelQueuedJob delegates to the next hook function in the queue and +// stores the parameter and result values of this invocation. +func (m *MockPermissionSyncJobStore) CancelQueuedJob(v0 context.Context, v1 int) error { + r0 := m.CancelQueuedJobFunc.nextHook()(v0, v1) + m.CancelQueuedJobFunc.appendCall(PermissionSyncJobStoreCancelQueuedJobFuncCall{v0, v1, r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the CancelQueuedJob +// method of the parent MockPermissionSyncJobStore instance is invoked and +// the hook queue is empty. +func (f *PermissionSyncJobStoreCancelQueuedJobFunc) SetDefaultHook(hook func(context.Context, int) error) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// CancelQueuedJob method of the parent MockPermissionSyncJobStore instance +// invokes the hook at the front of the queue and discards it. After the +// queue is empty, the default hook function is invoked for any future +// action. +func (f *PermissionSyncJobStoreCancelQueuedJobFunc) PushHook(hook func(context.Context, int) error) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *PermissionSyncJobStoreCancelQueuedJobFunc) SetDefaultReturn(r0 error) { + f.SetDefaultHook(func(context.Context, int) error { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *PermissionSyncJobStoreCancelQueuedJobFunc) PushReturn(r0 error) { + f.PushHook(func(context.Context, int) error { + return r0 + }) +} + +func (f *PermissionSyncJobStoreCancelQueuedJobFunc) nextHook() func(context.Context, int) error { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *PermissionSyncJobStoreCancelQueuedJobFunc) appendCall(r0 PermissionSyncJobStoreCancelQueuedJobFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of +// PermissionSyncJobStoreCancelQueuedJobFuncCall objects describing the +// invocations of this function. +func (f *PermissionSyncJobStoreCancelQueuedJobFunc) History() []PermissionSyncJobStoreCancelQueuedJobFuncCall { + f.mutex.Lock() + history := make([]PermissionSyncJobStoreCancelQueuedJobFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// PermissionSyncJobStoreCancelQueuedJobFuncCall is an object that describes +// an invocation of method CancelQueuedJob on an instance of +// MockPermissionSyncJobStore. +type PermissionSyncJobStoreCancelQueuedJobFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 int + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c PermissionSyncJobStoreCancelQueuedJobFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c PermissionSyncJobStoreCancelQueuedJobFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + // PermissionSyncJobStoreCreateRepoSyncJobFunc describes the behavior when // the CreateRepoSyncJob method of the parent MockPermissionSyncJobStore // instance is invoked. diff --git a/internal/database/permission_sync_jobs.go b/internal/database/permission_sync_jobs.go index 7ad14206f2a4..64be6a5de044 100644 --- a/internal/database/permission_sync_jobs.go +++ b/internal/database/permission_sync_jobs.go @@ -2,10 +2,14 @@ package database import ( "context" + "strconv" "time" "github.com/keegancsmith/sqlf" "github.com/lib/pq" + "github.com/sourcegraph/sourcegraph/internal/errcode" + "github.com/sourcegraph/sourcegraph/internal/timeutil" + "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/log" @@ -35,6 +39,7 @@ type PermissionSyncJobStore interface { CreateRepoSyncJob(ctx context.Context, repo api.RepoID, opts PermissionSyncJobOpts) error List(ctx context.Context, opts ListPermissionSyncJobOpts) ([]*PermissionSyncJob, error) + CancelQueuedJob(ctx context.Context, id int) error } type permissionSyncJobStore struct { @@ -112,10 +117,21 @@ VALUES ( %s, %s ) +ON CONFLICT DO NOTHING RETURNING %s ` +// createSyncJob inserts a postponed (`process_after IS NOT NULL`) sync job right +// away and checks new sync jobs without provided delay for duplicates. func (s *permissionSyncJobStore) createSyncJob(ctx context.Context, job *PermissionSyncJob) error { + if job.ProcessAfter.IsZero() { + // sync jobs without delay are checked for duplicates + return s.checkDuplicateAndCreateSyncJob(ctx, job) + } + return s.create(ctx, job) +} + +func (s *permissionSyncJobStore) create(ctx context.Context, job *PermissionSyncJob) error { q := sqlf.Sprintf( permissionSyncJobCreateQueryFmtstr, job.Reason, @@ -131,11 +147,97 @@ func (s *permissionSyncJobStore) createSyncJob(ctx context.Context, job *Permiss return scanPermissionSyncJob(job, s.QueryRow(ctx, q)) } +// checkDuplicateAndCreateSyncJob adds a new perms sync job with `process_after +// IS NULL` if there is no present duplicate of it. +// +// Duplicates are handled in this way: +// +// 1) If there is no existing job for given user/repo ID in a queued state, we +// insert right away. +// +// 2) If there is an existing job with lower priority, we cancel it and insert a +// new one with higher priority. +// +// 3) If there is an existing job with higher priority, we don't insert new job. +func (s *permissionSyncJobStore) checkDuplicateAndCreateSyncJob(ctx context.Context, job *PermissionSyncJob) (err error) { + tx, err := s.transact(ctx) + if err != nil { + return err + } + defer func() { + err = tx.Done(err) + }() + opts := ListPermissionSyncJobOpts{UserID: job.UserID, RepoID: job.RepositoryID, State: "queued", NotCanceled: true, NullProcessAfter: true} + syncJobs, err := tx.List(ctx, opts) + if err != nil { + return err + } + // Job doesn't exist -- create it + if len(syncJobs) == 0 { + return tx.create(ctx, job) + } + // Database constraint guarantees that we have at most 1 job with NULL + // `process_after` value. + existingJob := syncJobs[0] + + // Existing job with high priority should not be overridden. Existing low + // priority job shouldn't be overridden by another low priority job. + if existingJob.HighPriority || !job.HighPriority { + logField := "repositoryID" + id := strconv.Itoa(job.RepositoryID) + if job.RepositoryID == 0 { + logField = "userID" + id = strconv.Itoa(job.UserID) + } + s.logger.Debug( + "Permissions sync job is not added because a job with similar or higher priority already exists", + log.String(logField, id), + ) + return nil + } + + err = tx.CancelQueuedJob(ctx, existingJob.ID) + if err != nil && !errcode.IsNotFound(err) { + return err + } + return tx.create(ctx, job) +} + +type notFoundError struct{ error } + +func (e notFoundError) NotFound() bool { return true } + +func (s *permissionSyncJobStore) CancelQueuedJob(ctx context.Context, id int) error { + now := timeutil.Now() + q := sqlf.Sprintf(` +UPDATE permission_sync_jobs +SET cancel = TRUE, state = 'canceled', finished_at = %s +WHERE id = %s AND state = 'queued' AND cancel IS FALSE +`, now, id) + + res, err := s.ExecResult(ctx, q) + if err != nil { + return err + } + af, err := res.RowsAffected() + if err != nil { + return err + } + if af != 1 { + return notFoundError{errors.Newf("sync job with id %d not found", id)} + } + return nil +} + type ListPermissionSyncJobOpts struct { - ID int - UserID int - RepoID int - Reason string + ID int + UserID int + RepoID int + Reason string + State string + NullProcessAfter bool + NotNullProcessAfter bool + NotCanceled bool } func (opts ListPermissionSyncJobOpts) sqlConds() []*sqlf.Query { @@ -153,7 +255,18 @@ func (opts ListPermissionSyncJobOpts) sqlConds() []*sqlf.Query { if opts.Reason != "" { conds = append(conds, sqlf.Sprintf("reason = %s", opts.Reason)) } - + if opts.State != "" { + conds = append(conds, sqlf.Sprintf("state = %s", opts.State)) + } + if opts.NullProcessAfter { + conds = append(conds, sqlf.Sprintf("process_after IS NULL")) + } + if opts.NotNullProcessAfter { + conds = append(conds, sqlf.Sprintf("process_after IS NOT NULL")) + } + if opts.NotCanceled { + conds = append(conds, sqlf.Sprintf("cancel = false")) + } return conds } diff --git a/internal/database/permission_sync_jobs_test.go b/internal/database/permission_sync_jobs_test.go index 9167349d65bc..a74ac52a67b7 100644 --- a/internal/database/permission_sync_jobs_test.go +++ b/internal/database/permission_sync_jobs_test.go @@ -9,7 +9,9 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/sourcegraph/log/logtest" "github.com/sourcegraph/sourcegraph/internal/database/dbtest" + "github.com/sourcegraph/sourcegraph/internal/errcode" "github.com/sourcegraph/sourcegraph/internal/timeutil" + "github.com/stretchr/testify/assert" ) const ( @@ -29,40 +31,28 @@ func TestPermissionSyncJobs_CreateAndList(t *testing.T) { logger := logtest.Scoped(t) db := NewDB(logger, dbtest.NewDB(logger, t)) user, err := db.Users().Create(context.Background(), NewUser{Username: "horse"}) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } + assert.NoError(t, err) ctx := context.Background() store := PermissionSyncJobsWith(logger, db) jobs, err := store.List(ctx, ListPermissionSyncJobOpts{}) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if len(jobs) != 0 { - t.Fatalf("jobs returned even though database is empty") - } + assert.NoError(t, err) + assert.Len(t, jobs, 0, "jobs returned even though database is empty") opts := PermissionSyncJobOpts{HighPriority: true, InvalidateCaches: true, Reason: ReasonManualRepoSync, TriggeredByUserID: user.ID} - if err := store.CreateRepoSyncJob(ctx, 99, opts); err != nil { - t.Fatalf("unexpected error: %s", err) - } + err = store.CreateRepoSyncJob(ctx, 99, opts) + assert.NoError(t, err) nextSyncAt := clock.Now().Add(5 * time.Minute) opts = PermissionSyncJobOpts{HighPriority: false, InvalidateCaches: true, NextSyncAt: nextSyncAt, Reason: ReasonManualUserSync} - if err := store.CreateUserSyncJob(ctx, 77, opts); err != nil { - t.Fatalf("unexpected error: %s", err) - } + err = store.CreateUserSyncJob(ctx, 77, opts) + assert.NoError(t, err) jobs, err = store.List(ctx, ListPermissionSyncJobOpts{}) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } + assert.NoError(t, err) - if len(jobs) != 2 { - t.Fatalf("wrong number of jobs returned. want=%d, have=%d", 2, len(jobs)) - } + assert.Len(t, jobs, 2, "wrong number of jobs returned") wantJobs := []*PermissionSyncJob{ { @@ -87,9 +77,7 @@ func TestPermissionSyncJobs_CreateAndList(t *testing.T) { t.Fatalf("jobs[0] has wrong attributes: %s", diff) } for i, j := range jobs { - if j.QueuedAt.IsZero() { - t.Fatalf("job %d has no QueuedAt set", i) - } + assert.NotZerof(t, j.QueuedAt, "job %d has no QueuedAt set", i) } listTests := []struct { @@ -117,9 +105,7 @@ func TestPermissionSyncJobs_CreateAndList(t *testing.T) { for _, tt := range listTests { t.Run(tt.name, func(t *testing.T) { have, err := store.List(ctx, tt.opts) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } + assert.NoError(t, err) if len(have) != len(tt.wantJobs) { t.Fatalf("wrong number of jobs returned. want=%d, have=%d", len(tt.wantJobs), len(have)) } @@ -129,3 +115,162 @@ func TestPermissionSyncJobs_CreateAndList(t *testing.T) { }) } } + +func TestPermissionSyncJobs_Deduplication(t *testing.T) { + if testing.Short() { + t.Skip() + } + + clock := timeutil.NewFakeClock(time.Now(), 0) + + logger := logtest.Scoped(t) + db := NewDB(logger, dbtest.NewDB(logger, t)) + user1, err := db.Users().Create(context.Background(), NewUser{Username: "horse"}) + assert.NoError(t, err) + + user2, err := db.Users().Create(context.Background(), NewUser{Username: "graph"}) + assert.NoError(t, err) + ctx := context.Background() + + store := PermissionSyncJobsWith(logger, db) + + // 1) Insert low priority job without process_after for user1 + user1LowPrioJob := PermissionSyncJobOpts{Reason: ReasonManualUserSync, TriggeredByUserID: user1.ID} + err = store.CreateUserSyncJob(ctx, 1, user1LowPrioJob) + assert.NoError(t, err) + + allJobs, err := store.List(ctx, ListPermissionSyncJobOpts{}) + assert.NoError(t, err) + // check that we have 1 job with userID=1 + assert.Len(t, allJobs, 1) + assert.Equal(t, 1, allJobs[0].UserID) + + // 2) Insert low priority job without process_after for user2 + user2LowPrioJob := PermissionSyncJobOpts{Reason: ReasonManualUserSync, TriggeredByUserID: user2.ID} + err = store.CreateUserSyncJob(ctx, 2, user2LowPrioJob) + assert.NoError(t, err) + + allJobs, err = store.List(ctx, ListPermissionSyncJobOpts{}) + assert.NoError(t, err) + // check that we have 2 jobs including job for userID=2. job ID should match user ID + assert.Len(t, allJobs, 2) + assert.Equal(t, allJobs[0].ID, allJobs[0].UserID) + assert.Equal(t, allJobs[1].ID, allJobs[1].UserID) + + // 3) Another low priority job without process_after for user1 is dropped + err = store.CreateUserSyncJob(ctx, 1, user1LowPrioJob) + assert.NoError(t, err) + + allJobs, err = store.List(ctx, ListPermissionSyncJobOpts{}) + assert.NoError(t, err) + // check that we still have 2 jobs. Job ID should match user ID. + assert.Len(t, allJobs, 2) + assert.Equal(t, allJobs[0].ID, allJobs[0].UserID) + assert.Equal(t, allJobs[1].ID, allJobs[1].UserID) + + // 4) Insert some low priority jobs with process_after for both users. All of them should be inserted. + fiveMinutesLater := clock.Now().Add(5 * time.Minute) + tenMinutesLater := clock.Now().Add(10 * time.Minute) + user1LowPrioDelayedJob := PermissionSyncJobOpts{NextSyncAt: fiveMinutesLater, Reason: ReasonManualUserSync, TriggeredByUserID: user1.ID} + user2LowPrioDelayedJob := PermissionSyncJobOpts{NextSyncAt: tenMinutesLater, Reason: ReasonManualUserSync, TriggeredByUserID: user1.ID} + + err = store.CreateUserSyncJob(ctx, 1, user1LowPrioDelayedJob) + assert.NoError(t, err) + + err = store.CreateUserSyncJob(ctx, 2, user2LowPrioDelayedJob) + assert.NoError(t, err) + + allDelayedJobs, err := store.List(ctx, ListPermissionSyncJobOpts{NotNullProcessAfter: true}) + assert.NoError(t, err) + // check that we have 2 delayed jobs in total + assert.Len(t, allDelayedJobs, 2) + // userID of the job should be (jobID - 2) + assert.Equal(t, allDelayedJobs[0].UserID, allDelayedJobs[0].ID-2) + assert.Equal(t, allDelayedJobs[1].UserID, allDelayedJobs[1].ID-2) + + // 5) Insert *high* priority job without process_after for user1. Check that low priority job is canceled + user1HighPrioJob := PermissionSyncJobOpts{HighPriority: true, Reason: ReasonManualUserSync, TriggeredByUserID: user1.ID} + err = store.CreateUserSyncJob(ctx, 1, user1HighPrioJob) + assert.NoError(t, err) + + allUser1Jobs, err := store.List(ctx, ListPermissionSyncJobOpts{UserID: 1}) + assert.NoError(t, err) + // check that we have 3 jobs for userID=1 in total (low prio (canceled), delayed, high prio) + assert.Len(t, allUser1Jobs, 3) + // check that low prio job (ID=1) is canceled and others are not + for _, job := range allUser1Jobs { + if job.ID == 1 { + assert.True(t, job.Cancel) + } else { + assert.False(t, job.Cancel) + } + } + + // 6) Insert another low and high priority jobs without process_after for user1. + // Check that all of them are dropped since we already have a high prio job. + err = store.CreateUserSyncJob(ctx, 1, user1LowPrioJob) + assert.NoError(t, err) + + err = store.CreateUserSyncJob(ctx, 1, user1HighPrioJob) + assert.NoError(t, err) + + allUser1Jobs, err = store.List(ctx, ListPermissionSyncJobOpts{UserID: 1}) + assert.NoError(t, err) + // check that we still have 3 jobs for userID=1 in total (low prio (canceled), delayed, high prio) + assert.Len(t, allUser1Jobs, 3) + + // 7) Check that not "queued" jobs doesn't affect duplicates check: let's change high prio job to "processing" + // and insert one low prio after that. + result, err := db.ExecContext(ctx, "UPDATE permission_sync_jobs SET state='processing' WHERE id=5") + assert.NoError(t, err) + updatedRows, err := result.RowsAffected() + assert.NoError(t, err) + assert.Equal(t, int64(1), updatedRows) + + // Now we're good to insert new low prio job. + err = store.CreateUserSyncJob(ctx, 1, user1LowPrioJob) + assert.NoError(t, err) + + allUser1Jobs, err = store.List(ctx, ListPermissionSyncJobOpts{UserID: 1}) + assert.NoError(t, err) + // check that we now have 4 jobs for userID=1 in total (low prio (canceled), delayed, high prio (processing), NEW low prio) + assert.Len(t, allUser1Jobs, 4) +} + +func TestPermissionSyncJobs_CancelQueuedJob(t *testing.T) { + if testing.Short() { + t.Skip() + } + + logger := logtest.Scoped(t) + db := NewDB(logger, dbtest.NewDB(logger, t)) + ctx := context.Background() + + store := PermissionSyncJobsWith(logger, db) + + // Test that cancelling non-existent job errors out + err := store.CancelQueuedJob(ctx, 1) + assert.True(t, errcode.IsNotFound(err)) + + // Adding a job + err = store.CreateRepoSyncJob(ctx, 1, PermissionSyncJobOpts{Reason: ReasonManualUserSync}) + assert.NoError(t, err) + + // Cancelling a job should be successful now + err = store.CancelQueuedJob(ctx, 1) + assert.NoError(t, err) + + // Cancelling already cancelled job doesn't make sense and errors out as well + err = store.CancelQueuedJob(ctx, 1) + assert.True(t, errcode.IsNotFound(err)) + + // Adding another job and setting it to "processing" state + err = store.CreateRepoSyncJob(ctx, 1, PermissionSyncJobOpts{Reason: ReasonManualUserSync}) + assert.NoError(t, err) + _, err = db.ExecContext(ctx, "UPDATE permission_sync_jobs SET state='processing' WHERE id=2") + assert.NoError(t, err) + + // Cancelling it errors out because it is in a state different from "queued" + err = store.CancelQueuedJob(ctx, 2) + assert.True(t, errcode.IsNotFound(err)) +} diff --git a/internal/database/schema.json b/internal/database/schema.json index 305fda43d83c..a5526e5cbf3b 100755 --- a/internal/database/schema.json +++ b/internal/database/schema.json @@ -17016,7 +17016,7 @@ "Columns": [ { "Name": "cancel", - "Index": 13, + "Index": 14, "TypeName": "boolean", "IsNullable": false, "Default": "false", @@ -17029,7 +17029,7 @@ }, { "Name": "execution_logs", - "Index": 11, + "Index": 12, "TypeName": "json[]", "IsNullable": true, "Default": "", @@ -17042,7 +17042,7 @@ }, { "Name": "failure_message", - "Index": 3, + "Index": 4, "TypeName": "text", "IsNullable": true, "Default": "", @@ -17055,7 +17055,7 @@ }, { "Name": "finished_at", - "Index": 6, + "Index": 7, "TypeName": "timestamp with time zone", "IsNullable": true, "Default": "", @@ -17068,7 +17068,7 @@ }, { "Name": "high_priority", - "Index": 16, + "Index": 18, "TypeName": "boolean", "IsNullable": false, "Default": "false", @@ -17094,7 +17094,7 @@ }, { "Name": "invalidate_caches", - "Index": 17, + "Index": 19, "TypeName": "boolean", "IsNullable": false, "Default": "false", @@ -17107,7 +17107,7 @@ }, { "Name": "last_heartbeat_at", - "Index": 10, + "Index": 11, "TypeName": "timestamp with time zone", "IsNullable": true, "Default": "", @@ -17120,7 +17120,7 @@ }, { "Name": "num_failures", - "Index": 9, + "Index": 10, "TypeName": "integer", "IsNullable": false, "Default": "0", @@ -17133,7 +17133,7 @@ }, { "Name": "num_resets", - "Index": 8, + "Index": 9, "TypeName": "integer", "IsNullable": false, "Default": "0", @@ -17146,7 +17146,7 @@ }, { "Name": "process_after", - "Index": 7, + "Index": 8, "TypeName": "timestamp with time zone", "IsNullable": true, "Default": "", @@ -17159,7 +17159,7 @@ }, { "Name": "queued_at", - "Index": 4, + "Index": 5, "TypeName": "timestamp with time zone", "IsNullable": true, "Default": "now()", @@ -17172,9 +17172,9 @@ }, { "Name": "reason", - "Index": 18, + "Index": 3, "TypeName": "text", - "IsNullable": true, + "IsNullable": false, "Default": "", "CharacterMaximumLength": 0, "IsIdentity": false, @@ -17185,7 +17185,7 @@ }, { "Name": "repository_id", - "Index": 14, + "Index": 15, "TypeName": "integer", "IsNullable": true, "Default": "", @@ -17198,7 +17198,7 @@ }, { "Name": "started_at", - "Index": 5, + "Index": 6, "TypeName": "timestamp with time zone", "IsNullable": true, "Default": "", @@ -17224,7 +17224,7 @@ }, { "Name": "triggered_by_user_id", - "Index": 19, + "Index": 17, "TypeName": "integer", "IsNullable": true, "Default": "", @@ -17237,7 +17237,7 @@ }, { "Name": "user_id", - "Index": 15, + "Index": 16, "TypeName": "integer", "IsNullable": true, "Default": "", @@ -17250,7 +17250,7 @@ }, { "Name": "worker_hostname", - "Index": 12, + "Index": 13, "TypeName": "text", "IsNullable": false, "Default": "''::text", @@ -17273,6 +17273,16 @@ "ConstraintType": "p", "ConstraintDefinition": "PRIMARY KEY (id)" }, + { + "Name": "permission_sync_jobs_unique", + "IsPrimaryKey": false, + "IsUnique": true, + "IsExclusion": false, + "IsDeferrable": false, + "IndexDefinition": "CREATE UNIQUE INDEX permission_sync_jobs_unique ON permission_sync_jobs USING btree (high_priority, user_id, repository_id, cancel, process_after) WHERE state = 'queued'::text", + "ConstraintType": "", + "ConstraintDefinition": "" + }, { "Name": "permission_sync_jobs_process_after", "IsPrimaryKey": false, @@ -17315,6 +17325,13 @@ } ], "Constraints": [ + { + "Name": "permission_sync_jobs_for_repo_or_user", + "ConstraintType": "c", + "RefTableName": "", + "IsDeferrable": false, + "ConstraintDefinition": "CHECK ((user_id IS NULL) \u003c\u003e (repository_id IS NULL))" + }, { "Name": "permission_sync_jobs_triggered_by_user_id_fkey", "ConstraintType": "f", diff --git a/internal/database/schema.md b/internal/database/schema.md index a7d6c08a3d4c..74096ebfd9ba 100755 --- a/internal/database/schema.md +++ b/internal/database/schema.md @@ -2634,6 +2634,7 @@ Referenced by: ----------------------+--------------------------+-----------+----------+-------------------------------------------------- id | integer | | not null | nextval('permission_sync_jobs_id_seq'::regclass) state | text | | | 'queued'::text + reason | text | | not null | failure_message | text | | | queued_at | timestamp with time zone | | | now() started_at | timestamp with time zone | | | @@ -2647,16 +2648,18 @@ Referenced by: cancel | boolean | | not null | false repository_id | integer | | | user_id | integer | | | + triggered_by_user_id | integer | | | high_priority | boolean | | not null | false invalidate_caches | boolean | | not null | false - reason | text | | | - triggered_by_user_id | integer | | | Indexes: "permission_sync_jobs_pkey" PRIMARY KEY, btree (id) + "permission_sync_jobs_unique" UNIQUE, btree (high_priority, user_id, repository_id, cancel, process_after) WHERE state = 'queued'::text "permission_sync_jobs_process_after" btree (process_after) "permission_sync_jobs_repository_id" btree (repository_id) "permission_sync_jobs_state" btree (state) "permission_sync_jobs_user_id" btree (user_id) +Check constraints: + "permission_sync_jobs_for_repo_or_user" CHECK ((user_id IS NULL) <> (repository_id IS NULL)) Foreign-key constraints: "permission_sync_jobs_triggered_by_user_id_fkey" FOREIGN KEY (triggered_by_user_id) REFERENCES users(id) ON DELETE SET NULL DEFERRABLE diff --git a/migrations/frontend/1674041632_add_constraints_to_permission_sync_jobs_table/down.sql b/migrations/frontend/1674041632_add_constraints_to_permission_sync_jobs_table/down.sql new file mode 100644 index 000000000000..3185b2301d91 --- /dev/null +++ b/migrations/frontend/1674041632_add_constraints_to_permission_sync_jobs_table/down.sql @@ -0,0 +1,39 @@ +-- We are changing the schema and we don't expect any rows yet so we can just drop +-- the old table. +DROP TABLE IF EXISTS permission_sync_jobs; + +CREATE TABLE IF NOT EXISTS permission_sync_jobs +( + id SERIAL PRIMARY KEY, + state text DEFAULT 'queued', + failure_message text, + queued_at timestamp with time zone DEFAULT NOW(), + started_at timestamp with time zone, + finished_at timestamp with time zone, + process_after timestamp with time zone, + num_resets integer not null default 0, + num_failures integer not null default 0, + last_heartbeat_at timestamp with time zone, + execution_logs json[], + worker_hostname text not null default '', + cancel boolean not null default false, + + repository_id integer, + user_id integer, + + high_priority boolean not null default false, + invalidate_caches boolean not null default false +); + +CREATE INDEX IF NOT EXISTS permission_sync_jobs_state ON permission_sync_jobs (state); +CREATE INDEX IF NOT EXISTS permission_sync_jobs_process_after ON permission_sync_jobs (process_after); +CREATE INDEX IF NOT EXISTS permission_sync_jobs_repository_id ON permission_sync_jobs (repository_id); +CREATE INDEX IF NOT EXISTS permission_sync_jobs_user_id ON permission_sync_jobs (user_id); + +ALTER TABLE permission_sync_jobs + ADD COLUMN IF NOT EXISTS reason TEXT, + ADD COLUMN IF NOT EXISTS triggered_by_user_id INTEGER, + ADD FOREIGN KEY (triggered_by_user_id) REFERENCES users (id) ON DELETE SET NULL DEFERRABLE; + +COMMENT ON COLUMN permission_sync_jobs.reason IS 'Specifies why permissions sync job was triggered.'; +COMMENT ON COLUMN permission_sync_jobs.triggered_by_user_id IS 'Specifies an ID of a user who triggered a sync.'; diff --git a/migrations/frontend/1674041632_add_constraints_to_permission_sync_jobs_table/metadata.yaml b/migrations/frontend/1674041632_add_constraints_to_permission_sync_jobs_table/metadata.yaml new file mode 100644 index 000000000000..0aa542b8cea5 --- /dev/null +++ b/migrations/frontend/1674041632_add_constraints_to_permission_sync_jobs_table/metadata.yaml @@ -0,0 +1,2 @@ +name: add constraints to permission_sync_jobs table +parents: [1671159453, 1673897709] diff --git a/migrations/frontend/1674041632_add_constraints_to_permission_sync_jobs_table/up.sql b/migrations/frontend/1674041632_add_constraints_to_permission_sync_jobs_table/up.sql new file mode 100644 index 000000000000..26c4ffaf2136 --- /dev/null +++ b/migrations/frontend/1674041632_add_constraints_to_permission_sync_jobs_table/up.sql @@ -0,0 +1,43 @@ +-- We are changing the schema and we don't expect any rows yet so we can just drop +-- the old table. +DROP TABLE IF EXISTS permission_sync_jobs; + +CREATE TABLE permission_sync_jobs +( + id SERIAL PRIMARY KEY, + state TEXT DEFAULT 'queued', + reason TEXT NOT NULL, + failure_message TEXT, + queued_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + started_at TIMESTAMP WITH TIME ZONE, + finished_at TIMESTAMP WITH TIME ZONE, + process_after TIMESTAMP WITH TIME ZONE, + num_resets INTEGER NOT NULL DEFAULT 0, + num_failures INTEGER NOT NULL DEFAULT 0, + last_heartbeat_at TIMESTAMP WITH TIME ZONE, + execution_logs JSON[], + worker_hostname TEXT NOT NULL DEFAULT '', + cancel BOOLEAN NOT NULL DEFAULT FALSE, + + repository_id INTEGER, + user_id INTEGER, + triggered_by_user_id INTEGER REFERENCES users (id) ON DELETE SET NULL DEFERRABLE, + + high_priority BOOLEAN NOT NULL DEFAULT FALSE, + invalidate_caches BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT permission_sync_jobs_for_repo_or_user CHECK (((user_id IS NULL) <> (repository_id IS NULL))) +); + +CREATE INDEX IF NOT EXISTS permission_sync_jobs_state ON permission_sync_jobs (state); +CREATE INDEX IF NOT EXISTS permission_sync_jobs_process_after ON permission_sync_jobs (process_after); +CREATE INDEX IF NOT EXISTS permission_sync_jobs_repository_id ON permission_sync_jobs (repository_id); +CREATE INDEX IF NOT EXISTS permission_sync_jobs_user_id ON permission_sync_jobs (user_id); + +-- this index is used as a last resort if deduplication logic fails to work. +-- we should not enqueue more that one high priority immediate sync job (process_after IS NULL) for given repo/user. +CREATE UNIQUE INDEX IF NOT EXISTS permission_sync_jobs_unique ON permission_sync_jobs + USING btree (high_priority, user_id, repository_id, cancel, process_after) + WHERE (state = 'queued'); + +COMMENT ON COLUMN permission_sync_jobs.reason IS 'Specifies why permissions sync job was triggered.'; +COMMENT ON COLUMN permission_sync_jobs.triggered_by_user_id IS 'Specifies an ID of a user who triggered a sync.'; diff --git a/migrations/frontend/squashed.sql b/migrations/frontend/squashed.sql index 1d3f2ad939df..7a8cd1075ed7 100755 --- a/migrations/frontend/squashed.sql +++ b/migrations/frontend/squashed.sql @@ -3283,6 +3283,7 @@ CREATE VIEW outbound_webhooks_with_event_types AS CREATE TABLE permission_sync_jobs ( id integer NOT NULL, state text DEFAULT 'queued'::text, + reason text NOT NULL, failure_message text, queued_at timestamp with time zone DEFAULT now(), started_at timestamp with time zone, @@ -3296,10 +3297,10 @@ CREATE TABLE permission_sync_jobs ( cancel boolean DEFAULT false NOT NULL, repository_id integer, user_id integer, + triggered_by_user_id integer, high_priority boolean DEFAULT false NOT NULL, invalidate_caches boolean DEFAULT false NOT NULL, - reason text, - triggered_by_user_id integer + CONSTRAINT permission_sync_jobs_for_repo_or_user CHECK (((user_id IS NULL) <> (repository_id IS NULL))) ); COMMENT ON COLUMN permission_sync_jobs.reason IS 'Specifies why permissions sync job was triggered.'; @@ -4884,6 +4885,8 @@ CREATE INDEX permission_sync_jobs_repository_id ON permission_sync_jobs USING bt CREATE INDEX permission_sync_jobs_state ON permission_sync_jobs USING btree (state); +CREATE UNIQUE INDEX permission_sync_jobs_unique ON permission_sync_jobs USING btree (high_priority, user_id, repository_id, cancel, process_after) WHERE (state = 'queued'::text); + CREATE INDEX permission_sync_jobs_user_id ON permission_sync_jobs USING btree (user_id); CREATE UNIQUE INDEX permissions_unique_namespace_action ON permissions USING btree (namespace, action); From 83ff57b36ed59ecf7762bb4051efccbdeac500ec Mon Sep 17 00:00:00 2001 From: Indradhanush Gupta Date: Thu, 19 Jan 2023 23:02:19 +0530 Subject: [PATCH 044/678] graphqlbackend: Add API for site config history (#46475) Co-authored-by: Thorsten Ball Co-authored-by: Alex Ostrikov --- cmd/frontend/graphqlbackend/node.go | 5 + cmd/frontend/graphqlbackend/schema.graphql | 77 +++ cmd/frontend/graphqlbackend/site.go | 17 + .../graphqlbackend/site_config_change.go | 61 ++ .../site_config_change_connection.go | 142 +++++ .../site_config_change_connection_test.go | 603 ++++++++++++++++++ cmd/frontend/graphqlbackend/site_test.go | 175 +++++ .../graphqlbackend/webhook_logs_test.go | 1 + internal/database/helpers.go | 18 + 9 files changed, 1099 insertions(+) create mode 100644 cmd/frontend/graphqlbackend/site_config_change.go create mode 100644 cmd/frontend/graphqlbackend/site_config_change_connection.go create mode 100644 cmd/frontend/graphqlbackend/site_config_change_connection_test.go create mode 100644 cmd/frontend/graphqlbackend/site_test.go diff --git a/cmd/frontend/graphqlbackend/node.go b/cmd/frontend/graphqlbackend/node.go index 13267c019dc7..2ce1991e9dfb 100644 --- a/cmd/frontend/graphqlbackend/node.go +++ b/cmd/frontend/graphqlbackend/node.go @@ -218,6 +218,11 @@ func (r *NodeResolver) ToSite() (*siteResolver, bool) { return n, ok } +func (r *NodeResolver) ToSiteConfigurationChange() (*SiteConfigurationChangeResolver, bool) { + n, ok := r.Node.(*SiteConfigurationChangeResolver) + return n, ok +} + func (r *NodeResolver) ToLSIFUpload() (resolverstubs.LSIFUploadResolver, bool) { n, ok := r.Node.(resolverstubs.LSIFUploadResolver) return n, ok diff --git a/cmd/frontend/graphqlbackend/schema.graphql b/cmd/frontend/graphqlbackend/schema.graphql index 14bbfae35749..50dd0296361f 100755 --- a/cmd/frontend/graphqlbackend/schema.graphql +++ b/cmd/frontend/graphqlbackend/schema.graphql @@ -6687,6 +6687,83 @@ type SiteConfiguration { on the configuration (that can't be expressed in the JSON Schema). """ validationMessages: [String!]! + """ + EXPERIMENTAL: A list of diffs to depict what changed since the previous version of this + configuration. + Only site admins may perform this query. + """ + history( + """ + The number of nodes to return starting from the beginning (oldest). + Note: Use either first or last (see below) in the query. Setting both will + return an error. + """ + first: Int + """ + The number of nodes to return starting from the end (latest). + Note: Use either last or first (see above) in the query. Setting both will + return an error. + """ + last: Int + """ + Opaque pagination cursor to be used when paginating forwards that may be also used + in conjunction with "first" to return the first N nodes. + """ + after: String + """ + Opaque pagination cursor to be used when paginating backwards that may be + also used in conjunction with "last" to return the last N nodes. + """ + before: String + ): SiteConfigurationChangeConnection +} + +""" +A list of site config diffs. +""" +type SiteConfigurationChangeConnection implements Connection { + """ + A list of diffs in the site config + """ + nodes: [SiteConfigurationChange!]! + """ + The total number of diffs in the connection. + """ + totalCount: Int! + """ + Pagination information. + """ + pageInfo: ConnectionPageInfo! +} + +""" +A diff representing the change in the site config compared to the previous version. +""" +type SiteConfigurationChange implements Node { + """ + The ID of the site config in the history. + """ + id: ID! + """ + The user who made this change. If empty, it indicates that either the + author's information is not available or the change in the site config was applied + via an internal process (example: site startup or SITE_CONFIG_FILE being reloaded). + """ + author: User + """ + The diff string when diffed against the site config at previousID. + """ + diff: String! + """ + The timestamp when this change in the site config was applied. + """ + createdAt: DateTime! + """ + The timestamp when this change in the site config was modified. Usually + this should be the same as createdAt as entries in the site config history are + considered immutable. + """ + updatedAt: DateTime! } """ diff --git a/cmd/frontend/graphqlbackend/site.go b/cmd/frontend/graphqlbackend/site.go index 88ce83ad474c..097dd6b778f8 100644 --- a/cmd/frontend/graphqlbackend/site.go +++ b/cmd/frontend/graphqlbackend/site.go @@ -9,6 +9,7 @@ import ( "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/relay" + "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil" "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/siteid" "github.com/sourcegraph/sourcegraph/internal/actor" "github.com/sourcegraph/sourcegraph/internal/api" @@ -157,6 +158,22 @@ func (r *siteConfigurationResolver) ValidationMessages(ctx context.Context) ([]s return conf.ValidateSite(string(contents)) } +func (r *siteConfigurationResolver) History(ctx context.Context, args *graphqlutil.ConnectionResolverArgs) (*graphqlutil.ConnectionResolver[SiteConfigurationChangeResolver], error) { + // 🚨 SECURITY: The site configuration contains secret tokens and credentials, + // so only admins may view the history. + if err := auth.CheckCurrentUserIsSiteAdmin(ctx, r.db); err != nil { + return nil, err + } + + connectionStore := SiteConfigurationChangeConnectionStore{db: r.db} + + return graphqlutil.NewConnectionResolver[SiteConfigurationChangeResolver]( + &connectionStore, + args, + nil, + ) +} + func (r *schemaResolver) UpdateSiteConfiguration(ctx context.Context, args *struct { LastID int32 Input string diff --git a/cmd/frontend/graphqlbackend/site_config_change.go b/cmd/frontend/graphqlbackend/site_config_change.go new file mode 100644 index 000000000000..74c168313d1b --- /dev/null +++ b/cmd/frontend/graphqlbackend/site_config_change.go @@ -0,0 +1,61 @@ +package graphqlbackend + +import ( + "context" + + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/relay" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/gqlutil" +) + +const siteConfigurationChangeKind = "SiteConfigurationChange" + +type SiteConfigurationChangeResolver struct { + db database.DB + siteConfig *database.SiteConfig + previousSiteConfig *database.SiteConfig +} + +func (r SiteConfigurationChangeResolver) ID() graphql.ID { + return marshalSiteConfigurationChangeID(r.siteConfig.ID) +} + +// One line wrapper to be able to use in tests as well. +func marshalSiteConfigurationChangeID(id int32) graphql.ID { + return relay.MarshalID(siteConfigurationChangeKind, &id) +} + +func (r SiteConfigurationChangeResolver) Author(ctx context.Context) (*UserResolver, error) { + if r.siteConfig.AuthorUserID == 0 { + return nil, nil + } + + user, err := UserByIDInt32(ctx, r.db, r.siteConfig.AuthorUserID) + if err != nil { + return nil, err + } + + return user, nil +} + +// TODO: Implement this. +func (r SiteConfigurationChangeResolver) Diff() string { + // TODO: We will do something like this, but for now return an empty string to not leak secrets + // until we have implemented redaction. + // + // if r.previousSiteConfig == nil { + // return "" + // } + + // return cmp.Diff(r.siteConfig.Contents, r.previousSiteConfig.Contents) + return "" +} + +func (r SiteConfigurationChangeResolver) CreatedAt() gqlutil.DateTime { + return gqlutil.DateTime{Time: r.siteConfig.CreatedAt} +} + +func (r SiteConfigurationChangeResolver) UpdatedAt() gqlutil.DateTime { + return gqlutil.DateTime{Time: r.siteConfig.UpdatedAt} +} diff --git a/cmd/frontend/graphqlbackend/site_config_change_connection.go b/cmd/frontend/graphqlbackend/site_config_change_connection.go new file mode 100644 index 000000000000..add83f100a70 --- /dev/null +++ b/cmd/frontend/graphqlbackend/site_config_change_connection.go @@ -0,0 +1,142 @@ +package graphqlbackend + +import ( + "context" + + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/relay" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +type SiteConfigurationChangeConnectionStore struct { + db database.DB +} + +func (s *SiteConfigurationChangeConnectionStore) ComputeTotal(ctx context.Context) (*int32, error) { + count, err := s.db.Conf().GetSiteConfigCount(ctx) + c := int32(count) + return &c, err +} + +func (s *SiteConfigurationChangeConnectionStore) ComputeNodes(ctx context.Context, args *database.PaginationArgs) ([]*SiteConfigurationChangeResolver, error) { + if args == nil { + return nil, errors.New("pagination args cannot be nil") + } + + // NOTE: Do not modify "args" in-place because it is used by the caller of ComputeNodes to + // determine next/previous page. Instead, dereference the values from args first (if + // they're non-nil) and then assign them address of the new variables. + paginationArgs := args.Clone() + isModifiedPaginationArgs := modifyArgs(paginationArgs) + + history, err := s.db.Conf().ListSiteConfigs(ctx, paginationArgs) + if err != nil { + return []*SiteConfigurationChangeResolver{}, err + } + + totalFetched := len(history) + if totalFetched == 0 { + return []*SiteConfigurationChangeResolver{}, nil + } + + resolvers := []*SiteConfigurationChangeResolver{} + if paginationArgs.First != nil { + resolvers = generateResolversForFirst(history, s.db) + } else if paginationArgs.Last != nil { + resolvers = generateResolversForLast(history, s.db) + } + + if isModifiedPaginationArgs { + if paginationArgs.Last != nil { + resolvers = resolvers[1:] + } else if paginationArgs.First != nil && totalFetched == *paginationArgs.First { + resolvers = resolvers[:len(resolvers)-1] + } + } + + return resolvers, nil +} + +func (s *SiteConfigurationChangeConnectionStore) MarshalCursor(node *SiteConfigurationChangeResolver) (*string, error) { + cursor := string(node.ID()) + return &cursor, nil +} + +func (s *SiteConfigurationChangeConnectionStore) UnmarshalCursor(cursor string) (*int, error) { + var id int + err := relay.UnmarshalSpec(graphql.ID(cursor), &id) + return &id, err +} + +// modifyArgs will fetch one more than the originally requested number of items because we need one +// older item to get the diff of the oldes item in the list. +// +// A separate function so that this can be tested in isolation. +func modifyArgs(args *database.PaginationArgs) bool { + var modified bool + if args.First != nil { + *args.First += 1 + modified = true + } else if args.Last != nil && args.Before != nil { + if *args.Before > 0 { + modified = true + *args.Last += 1 + *args.Before -= 1 + } + } + + return modified +} + +func generateResolversForFirst(history []*database.SiteConfig, db database.DB) []*SiteConfigurationChangeResolver { + // If First is used then "history" is in descending order: 5, 4, 3, 2, 1. So look ahead for + // the "previousSiteConfig", but also only if we're not at the end of the slice yet. + // + // "previousSiteConfig" for the last item in "history" will be nil and that is okay, because + // we will truncate it from the end result being returned. The user did not request this. + // _We_ fetched an extra item to determine the "previousSiteConfig" of all the items. + resolvers := []*SiteConfigurationChangeResolver{} + totalFetched := len(history) + + for i := 0; i < totalFetched; i++ { + var previousSiteConfig *database.SiteConfig + if i < totalFetched-1 { + previousSiteConfig = history[i+1] + } + + resolvers = append(resolvers, &SiteConfigurationChangeResolver{ + db: db, + siteConfig: history[i], + previousSiteConfig: previousSiteConfig, + }) + } + + return resolvers +} + +func generateResolversForLast(history []*database.SiteConfig, db database.DB) []*SiteConfigurationChangeResolver { + // If Last is used then history is in ascending order: 1, 2, 3, 4, 5. So look behind for the + // "previousSiteConfig", but also only if we're not at the start of the slice. + // + // "previousSiteConfig" will be nil for the first item in history in this case and that is okay, + // because we will truncate it from the end result being returned. The user did not request + // this. _We_ fetched an extra item to determine the "previousSiteConfig" of all the items. + resolvers := []*SiteConfigurationChangeResolver{} + totalFetched := len(history) + + for i := 0; i < totalFetched; i++ { + var previousSiteConfig *database.SiteConfig + if i > 0 { + previousSiteConfig = history[i-1] + } + + resolvers = append(resolvers, &SiteConfigurationChangeResolver{ + db: db, + siteConfig: history[i], + previousSiteConfig: previousSiteConfig, + }) + } + + return resolvers +} diff --git a/cmd/frontend/graphqlbackend/site_config_change_connection_test.go b/cmd/frontend/graphqlbackend/site_config_change_connection_test.go new file mode 100644 index 000000000000..19778da3d47e --- /dev/null +++ b/cmd/frontend/graphqlbackend/site_config_change_connection_test.go @@ -0,0 +1,603 @@ +package graphqlbackend + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/sourcegraph/log" + "github.com/sourcegraph/sourcegraph/internal/actor" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/database/dbtest" + "github.com/sourcegraph/sourcegraph/internal/types" +) + +type siteConfigStubs struct { + db database.DB + users []*types.User + siteConfigs []*database.SiteConfig +} + +func setupSiteConfigStubs(t *testing.T) *siteConfigStubs { + logger := log.NoOp() + db := database.NewDB(logger, dbtest.NewDB(logger, t)) + ctx := context.Background() + + usersToCreate := []database.NewUser{ + {Username: "foo", DisplayName: "foo user"}, + {Username: "bar", DisplayName: "bar user"}, + } + + var users []*types.User + for _, input := range usersToCreate { + user, err := db.Users().Create(ctx, input) + if err != nil { + t.Fatal(err) + } + + if err := db.Users().SetIsSiteAdmin(ctx, user.ID, true); err != nil { + t.Fatal(err) + } + + users = append(users, user) + } + + conf := db.Conf() + siteConfigsToCreate := []*database.SiteConfig{ + { + Contents: ` +{ + "auth.Providers": [] +}`, + }, + { + AuthorUserID: 2, + // A new line is added. + Contents: ` +{ + "disableAutoGitUpdates": true, + "auth.Providers": [] +}`, + }, + { + AuthorUserID: 1, + // Existing line is changed. + Contents: ` +{ + "disableAutoGitUpdates": false, + "auth.Providers": [] +}`, + }, + { + AuthorUserID: 1, + // Existing line is removed. + Contents: ` +{ + "auth.Providers": [] +}`, + }, + } + + lastID := int32(0) + // This will create 5 entries, because the first time conf.SiteCreateIfupToDate is called it + // will create two entries in the DB. + for _, input := range siteConfigsToCreate { + siteConfig, err := conf.SiteCreateIfUpToDate(ctx, int32Ptr(lastID), input.AuthorUserID, input.Contents, false) + if err != nil { + t.Fatal(err) + } + + lastID = siteConfig.ID + } + + return &siteConfigStubs{ + db: db, + users: users, + // siteConfigs: siteConfigs, + } +} + +func TestSiteConfigConnection(t *testing.T) { + stubs := setupSiteConfigStubs(t) + + // Create a context with an admin user as the actor. + context := actor.WithActor(context.Background(), &actor.Actor{UID: 1}) + + RunTests(t, []*Test{ + { + Schema: mustParseGraphQLSchema(t, stubs.db), + Label: "Get first 2 site configuration history", + Context: context, + Query: ` + { + site { + id + configuration { + id + history(first: 2){ + totalCount + nodes{ + id + author{ + id, + username, + displayName + } + } + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + } + } + } + } + `, + ExpectedResult: fmt.Sprintf(` + { + "site": { + "id": "U2l0ZToic2l0ZSI=", + "configuration": { + "id": 5, + "history": { + "totalCount": 5, + "nodes": [ + { + "id": %[1]q, + "author": { + "id": "VXNlcjox", + "username": "foo", + "displayName": "foo user" + } + }, + { + "id": %[2]q, + "author": { + "id": "VXNlcjox", + "username": "foo", + "displayName": "foo user" + } + } + ], + "pageInfo": { + "hasNextPage": true, + "hasPreviousPage": false, + "endCursor": %[2]q, + "startCursor": %[1]q + } + } + } + } + } + `, marshalSiteConfigurationChangeID(5), marshalSiteConfigurationChangeID(4)), + }, + { + Schema: mustParseGraphQLSchema(t, stubs.db), + Label: "Get last 2 site configuration history", + Context: context, + Query: ` + { + site { + id + configuration { + id + history(last: 3){ + totalCount + nodes{ + id + author{ + id, + username, + displayName + } + } + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + } + } + } + } + `, + ExpectedResult: fmt.Sprintf(` + { + "site": { + "id": "U2l0ZToic2l0ZSI=", + "configuration": { + "id": 5, + "history": { + "totalCount": 5, + "nodes": [ + { + "id": %[1]q, + "author": { + "id": "VXNlcjoy", + "username": "bar", + "displayName": "bar user" + } + }, + { + "id": %[2]q, + "author": null + }, + { + "id": %[3]q, + "author": null + } + ], + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": true, + "endCursor": %[3]q, + "startCursor": %[1]q + } + } + } + } + } + `, marshalSiteConfigurationChangeID(3), marshalSiteConfigurationChangeID(2), marshalSiteConfigurationChangeID(1)), + }, + { + Schema: mustParseGraphQLSchema(t, stubs.db), + Label: "Get first 2 site configuration history based on an offset", + Context: context, + Query: fmt.Sprintf(` + { + site { + id + configuration { + id + history(first: 2, after: %q){ + totalCount + nodes{ + id + author{ + id, + username, + displayName + } + } + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + } + } + } + } + `, marshalSiteConfigurationChangeID(5)), + ExpectedResult: fmt.Sprintf(` + { + "site": { + "id": "U2l0ZToic2l0ZSI=", + "configuration": { + "id": 5, + "history": { + "totalCount": 5, + "nodes": [ + { + "id": %[1]q, + "author": { + "id": "VXNlcjox", + "username": "foo", + "displayName": "foo user" + } + }, + { + "id": %[2]q, + "author": { + "id": "VXNlcjoy", + "username": "bar", + "displayName": "bar user" + } + } + ], + "pageInfo": { + "hasNextPage": true, + "hasPreviousPage": true, + "endCursor": %[2]q, + "startCursor": %[1]q + } + } + } + } + } + `, marshalSiteConfigurationChangeID(4), marshalSiteConfigurationChangeID(3)), + }, + { + Schema: mustParseGraphQLSchema(t, stubs.db), + Label: "Get last 2 site configuration history based on an offset", + Context: context, + Query: fmt.Sprintf(` + { + site { + id + configuration { + id + history(last: 2, before: %q){ + totalCount + nodes{ + id + author{ + id, + username, + displayName + } + } + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + } + } + } + } + `, marshalSiteConfigurationChangeID(1)), + ExpectedResult: fmt.Sprintf(` + { + "site": { + "id": "U2l0ZToic2l0ZSI=", + "configuration": { + "id": 5, + "history": { + "totalCount": 5, + "nodes": [ + { + "id": %[1]q, + "author": { + "id": "VXNlcjoy", + "username": "bar", + "displayName": "bar user" + } + }, + { + "id": %[2]q, + "author": null + } + ], + "pageInfo": { + "hasNextPage": true, + "hasPreviousPage": true, + "endCursor": %[2]q, + "startCursor": %[1]q + } + } + } + } + } + `, marshalSiteConfigurationChangeID(3), marshalSiteConfigurationChangeID(2)), + }, + }) +} + +func TestSiteConfigurationChangeConnectionStoreComputeNodes(t *testing.T) { + stubs := setupSiteConfigStubs(t) + + ctx := context.Background() + store := SiteConfigurationChangeConnectionStore{db: stubs.db} + + if _, err := store.ComputeNodes(ctx, nil); err == nil { + t.Fatalf("expected error but got nil") + } + + testCases := []struct { + name string + paginationArgs *database.PaginationArgs + expectedSiteConfigIDs []int32 + // value of 0 in expectedPreviousSIteConfigIDs means nil in the test assertion. + expectedPreviousSiteConfigIDs []int32 + }{ + { + name: "first: 2", + paginationArgs: &database.PaginationArgs{ + First: intPtr(2), + }, + expectedSiteConfigIDs: []int32{5, 4}, + expectedPreviousSiteConfigIDs: []int32{4, 3}, + }, + { + name: "first: 5 (exact number of items that exist in the database)", + paginationArgs: &database.PaginationArgs{ + First: intPtr(5), + }, + expectedSiteConfigIDs: []int32{5, 4, 3, 2, 1}, + expectedPreviousSiteConfigIDs: []int32{4, 3, 2, 1, 0}, + }, + { + name: "first: 20 (more items than what exists in the database)", + paginationArgs: &database.PaginationArgs{ + First: intPtr(20), + }, + expectedSiteConfigIDs: []int32{5, 4, 3, 2, 1}, + expectedPreviousSiteConfigIDs: []int32{4, 3, 2, 1, 0}, + }, + { + name: "last: 2", + paginationArgs: &database.PaginationArgs{ + Last: intPtr(2), + }, + expectedSiteConfigIDs: []int32{1, 2}, + expectedPreviousSiteConfigIDs: []int32{0, 1}, + }, + { + name: "last: 5 (exact number of items that exist in the database)", + paginationArgs: &database.PaginationArgs{ + Last: intPtr(5), + }, + expectedSiteConfigIDs: []int32{1, 2, 3, 4, 5}, + expectedPreviousSiteConfigIDs: []int32{0, 1, 2, 3, 4}, + }, + { + name: "last: 20 (more items than what exists in the database)", + paginationArgs: &database.PaginationArgs{ + Last: intPtr(20), + }, + expectedSiteConfigIDs: []int32{1, 2, 3, 4, 5}, + expectedPreviousSiteConfigIDs: []int32{0, 1, 2, 3, 4}, + }, + { + name: "first: 2, after: 4", + paginationArgs: &database.PaginationArgs{ + First: intPtr(2), + After: intPtr(4), + }, + expectedSiteConfigIDs: []int32{3, 2}, + expectedPreviousSiteConfigIDs: []int32{2, 1}, + }, + { + name: "first: 10, after: 4", + paginationArgs: &database.PaginationArgs{ + First: intPtr(10), + After: intPtr(4), + }, + expectedSiteConfigIDs: []int32{3, 2, 1}, + expectedPreviousSiteConfigIDs: []int32{2, 1, 0}, + }, + { + name: "first: 2, after: 1", + paginationArgs: &database.PaginationArgs{ + First: intPtr(2), + After: intPtr(1), + }, + expectedSiteConfigIDs: []int32{}, + expectedPreviousSiteConfigIDs: []int32{}, + }, + { + name: "last: 2, before: 2", + paginationArgs: &database.PaginationArgs{ + Last: intPtr(2), + Before: intPtr(2), + }, + expectedSiteConfigIDs: []int32{3, 4}, + expectedPreviousSiteConfigIDs: []int32{2, 3}, + }, + { + name: "last: 10, before: 2", + paginationArgs: &database.PaginationArgs{ + Last: intPtr(10), + Before: intPtr(2), + }, + expectedSiteConfigIDs: []int32{3, 4, 5}, + expectedPreviousSiteConfigIDs: []int32{2, 3, 4}, + }, + { + name: "last: 2, before: 5", + paginationArgs: &database.PaginationArgs{ + Last: intPtr(2), + Before: intPtr(5), + }, + expectedSiteConfigIDs: []int32{}, + expectedPreviousSiteConfigIDs: []int32{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + siteConfigChangeResolvers, err := store.ComputeNodes(ctx, tc.paginationArgs) + if err != nil { + t.Errorf("expected nil, but got error: %v", err) + } + + gotLength := len(siteConfigChangeResolvers) + expectedLength := len(tc.expectedSiteConfigIDs) + if gotLength != expectedLength { + t.Fatalf("mismatched number of SiteConfigurationChangeResolvers, expected %d, got %d", expectedLength, gotLength) + } + + gotIDs := make([]int32, gotLength) + for i, got := range siteConfigChangeResolvers { + gotIDs[i] = got.siteConfig.ID + } + + if diff := cmp.Diff(tc.expectedSiteConfigIDs, gotIDs); diff != "" { + t.Errorf("mismatched siteConfig.ID, diff %v", diff) + } + + if len(tc.expectedPreviousSiteConfigIDs) == 0 { + return + } + + gotPreviousSiteConfigIDs := make([]int32, gotLength) + for i, got := range siteConfigChangeResolvers { + if got.previousSiteConfig == nil { + gotPreviousSiteConfigIDs[i] = 0 + } else { + gotPreviousSiteConfigIDs[i] = got.previousSiteConfig.ID + } + } + + if diff := cmp.Diff(tc.expectedPreviousSiteConfigIDs, gotPreviousSiteConfigIDs); diff != "" { + t.Errorf("mismatched siteConfig.ID, diff %v", diff) + } + }) + } +} + +func TestModifyArgs(t *testing.T) { + testCases := []struct { + name string + args *database.PaginationArgs + expectedArgs *database.PaginationArgs + expectedModified bool + }{ + { + name: "first: 5 (first page)", + args: &database.PaginationArgs{First: intPtr(5)}, + expectedArgs: &database.PaginationArgs{First: intPtr(6)}, + expectedModified: true, + }, + { + name: "first: 5, after: 10 (next page)", + args: &database.PaginationArgs{First: intPtr(5), After: intPtr(10)}, + expectedArgs: &database.PaginationArgs{First: intPtr(6), After: intPtr(10)}, + expectedModified: true, + }, + { + name: "last: 5 (last page)", + args: &database.PaginationArgs{Last: intPtr(5)}, + expectedArgs: &database.PaginationArgs{Last: intPtr(5)}, + expectedModified: false, + }, + { + name: "last: 5, before: 10 (previous page)", + args: &database.PaginationArgs{Last: intPtr(5), Before: intPtr(10)}, + expectedArgs: &database.PaginationArgs{Last: intPtr(6), Before: intPtr(9)}, + expectedModified: true, + }, + { + name: "last: 5, before: 1 (edge case)", + args: &database.PaginationArgs{Last: intPtr(5), Before: intPtr(1)}, + expectedArgs: &database.PaginationArgs{Last: intPtr(6), Before: intPtr(0)}, + expectedModified: true, + }, + { + name: "last: 5, before: 0 (same as last page but a mathematical edge case)", + args: &database.PaginationArgs{Last: intPtr(5), Before: intPtr(0)}, + expectedArgs: &database.PaginationArgs{Last: intPtr(5), Before: intPtr(0)}, + expectedModified: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + modified := modifyArgs(tc.args) + if modified != tc.expectedModified { + t.Errorf("Expected modified to be %v, but got %v", modified, tc.expectedModified) + } + + if diff := cmp.Diff(tc.args, tc.expectedArgs); diff != "" { + t.Errorf("Mismatch in modified args: %v", diff) + } + }) + } +} diff --git a/cmd/frontend/graphqlbackend/site_test.go b/cmd/frontend/graphqlbackend/site_test.go new file mode 100644 index 000000000000..d37953c83964 --- /dev/null +++ b/cmd/frontend/graphqlbackend/site_test.go @@ -0,0 +1,175 @@ +package graphqlbackend + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil" + "github.com/sourcegraph/sourcegraph/internal/actor" + "github.com/sourcegraph/sourcegraph/internal/auth" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/gitserver" + "github.com/sourcegraph/sourcegraph/internal/types" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +func TestSiteConfiguration(t *testing.T) { + t.Run("authenticated as non-admin", func(t *testing.T) { + users := database.NewMockUserStore() + users.GetByCurrentAuthUserFunc.SetDefaultReturn(&types.User{}, nil) + db := database.NewMockDB() + db.UsersFunc.SetDefaultReturn(users) + + ctx := actor.WithActor(context.Background(), &actor.Actor{UID: 1}) + _, err := newSchemaResolver(db, gitserver.NewClient(db)).Site().Configuration(ctx) + + if err == nil || !errors.Is(err, auth.ErrMustBeSiteAdmin) { + t.Fatalf("err: want %q but got %v", auth.ErrMustBeSiteAdmin, err) + } + }) +} + +func TestSiteConfigurationHistory(t *testing.T) { + stubs := setupSiteConfigStubs(t) + + ctx := actor.WithActor(context.Background(), &actor.Actor{UID: stubs.users[0].ID}) + schemaResolver, err := newSchemaResolver(stubs.db, gitserver.NewClient(stubs.db)).Site().Configuration(ctx) + if err != nil { + t.Fatalf("failed to create schemaResolver: %v", err) + } + + testCases := []struct { + name string + args *graphqlutil.ConnectionResolverArgs + expectedSiteConfigIDs []int32 + }{ + { + name: "first: 2", + args: &graphqlutil.ConnectionResolverArgs{First: int32Ptr(2)}, + expectedSiteConfigIDs: []int32{5, 4}, + }, + { + name: "first: 5 (exact number of items that exist in the database)", + args: &graphqlutil.ConnectionResolverArgs{First: int32Ptr(5)}, + expectedSiteConfigIDs: []int32{5, 4, 3, 2, 1}, + }, + { + name: "first: 20 (more items than what exists in the database)", + args: &graphqlutil.ConnectionResolverArgs{First: int32Ptr(20)}, + expectedSiteConfigIDs: []int32{5, 4, 3, 2, 1}, + }, + { + name: "last: 2", + args: &graphqlutil.ConnectionResolverArgs{Last: int32Ptr(2)}, + expectedSiteConfigIDs: []int32{2, 1}, + }, + { + name: "last: 5 (exact number of items that exist in the database)", + args: &graphqlutil.ConnectionResolverArgs{Last: int32Ptr(5)}, + expectedSiteConfigIDs: []int32{5, 4, 3, 2, 1}, + }, + { + name: "last: 20 (more items than what exists in the database)", + args: &graphqlutil.ConnectionResolverArgs{Last: int32Ptr(5)}, + expectedSiteConfigIDs: []int32{5, 4, 3, 2, 1}, + }, + { + name: "first: 2, after: 4", + args: &graphqlutil.ConnectionResolverArgs{ + First: int32Ptr(2), + After: stringPtr(string(marshalSiteConfigurationChangeID(4))), + }, + expectedSiteConfigIDs: []int32{3, 2}, + }, + { + name: "first: 10, after: 4 (overflow)", + args: &graphqlutil.ConnectionResolverArgs{ + First: int32Ptr(10), + After: stringPtr(string(marshalSiteConfigurationChangeID(4))), + }, + expectedSiteConfigIDs: []int32{3, 2, 1}, + }, + { + name: "first: 10, after: 6 (same as get all items, but latest ID in DB is 5)", + args: &graphqlutil.ConnectionResolverArgs{ + First: int32Ptr(10), + After: stringPtr(string(marshalSiteConfigurationChangeID(6))), + }, + expectedSiteConfigIDs: []int32{5, 4, 3, 2, 1}, + }, + { + name: "first: 10, after: 1 (beyond the last cursor in DB which is 1)", + args: &graphqlutil.ConnectionResolverArgs{ + First: int32Ptr(10), + After: stringPtr(string(marshalSiteConfigurationChangeID(1))), + }, + expectedSiteConfigIDs: []int32{}, + }, + { + name: "last: 2, before: 1", + args: &graphqlutil.ConnectionResolverArgs{ + Last: int32Ptr(2), + Before: stringPtr(string(marshalSiteConfigurationChangeID(1))), + }, + expectedSiteConfigIDs: []int32{3, 2}, + }, + { + name: "last: 10, before: 1 (overflow)", + args: &graphqlutil.ConnectionResolverArgs{ + Last: int32Ptr(10), + Before: stringPtr(string(marshalSiteConfigurationChangeID(1))), + }, + expectedSiteConfigIDs: []int32{5, 4, 3, 2}, + }, + { + name: "last: 10, before: 0 (same as get all items, but oldest ID in DB is 1)", + args: &graphqlutil.ConnectionResolverArgs{ + Last: int32Ptr(10), + Before: stringPtr(string(marshalSiteConfigurationChangeID(0))), + }, + expectedSiteConfigIDs: []int32{5, 4, 3, 2, 1}, + }, + { + name: "last: 10, before: 6 (beyond the latest cursor in DB which is 5)", + args: &graphqlutil.ConnectionResolverArgs{ + Last: int32Ptr(10), + Before: stringPtr(string(marshalSiteConfigurationChangeID(6))), + }, + expectedSiteConfigIDs: []int32{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + connectionResolver, err := schemaResolver.History(ctx, tc.args) + if err != nil { + t.Fatalf("failed to get history: %v", err) + } + + siteConfigChangeResolvers, err := connectionResolver.Nodes(ctx) + if err != nil { + t.Fatalf("failed to get nodes: %v", err) + } + + siteConfigChangeResolverIDs := make([]int32, len(siteConfigChangeResolvers)) + for i, s := range siteConfigChangeResolvers { + siteConfigChangeResolverIDs[i] = s.siteConfig.ID + } + + if len(siteConfigChangeResolvers) != len(tc.expectedSiteConfigIDs) { + diff := cmp.Diff(tc.expectedSiteConfigIDs, siteConfigChangeResolverIDs) + t.Fatalf(`mismatched number of resolvers, expected %d, got %d\n +diff in IDs: %s,\n +`, len(tc.expectedSiteConfigIDs), len(siteConfigChangeResolvers), diff) + } + + for i, resolver := range siteConfigChangeResolvers { + if resolver.siteConfig.ID != tc.expectedSiteConfigIDs[i] { + t.Errorf("position %d: expected siteConfig.ID %d, but got %d", i, tc.expectedSiteConfigIDs[i], resolver.siteConfig.ID) + } + } + }) + } + +} diff --git a/cmd/frontend/graphqlbackend/webhook_logs_test.go b/cmd/frontend/graphqlbackend/webhook_logs_test.go index 926038617e82..d68381d3f01b 100644 --- a/cmd/frontend/graphqlbackend/webhook_logs_test.go +++ b/cmd/frontend/graphqlbackend/webhook_logs_test.go @@ -420,6 +420,7 @@ func TestListWebhookLogs(t *testing.T) { } func boolPtr(v bool) *bool { return &v } +func intPtr(v int) *int { return &v } func int32Ptr(v int32) *int32 { return &v } func int64Ptr(v int64) *int64 { return &v } func stringPtr(v string) *string { return &v } diff --git a/internal/database/helpers.go b/internal/database/helpers.go index ec631d998d4d..90897ba645f2 100644 --- a/internal/database/helpers.go +++ b/internal/database/helpers.go @@ -110,3 +110,21 @@ func (p *PaginationArgs) SQL() (*QueryArgs, error) { return queryArgs, nil } + +// Clone (aka deepcopy) returns a new PaginationArgs object with the same values as "p". +func (p *PaginationArgs) Clone() *PaginationArgs { + copyIntPtr := func(n *int) *int { + if n == nil { + return nil + } + + c := *n + return &c + } + return &PaginationArgs{ + First: copyIntPtr(p.First), + Last: copyIntPtr(p.Last), + After: copyIntPtr(p.After), + Before: copyIntPtr(p.Before), + } +} From ea411e00f794cc5f9ae3c0253252ae9d1fa5a2cc Mon Sep 17 00:00:00 2001 From: Jason Hawk Harris Date: Thu, 19 Jan 2023 11:33:08 -0600 Subject: [PATCH 045/678] Add note about NO_PROXY (#46677) also adds not about safely removing minio env variables. --- doc/admin/how-to/blobstore_update_notes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/admin/how-to/blobstore_update_notes.md b/doc/admin/how-to/blobstore_update_notes.md index 831a8298cd11..cbd09cef41ec 100644 --- a/doc/admin/how-to/blobstore_update_notes.md +++ b/doc/admin/how-to/blobstore_update_notes.md @@ -8,7 +8,9 @@ Sourcegraph is committed to only distributing open-source software that is permi As a result, Sourcegraph v4.2.1+ and v4.3+ no longer use or distribute _any_ minio or AGPL-licensed software. -If your Sourcegraph instance is already configured to use [external object storage](../external_services/object_storage.md), or you use `DISABLE_MINIO=true` in `sourcegraph/server` deployments, then this change should not affect you (there would already be no Minio software running as part of Sourcegraph.) +If your Sourcegraph instance is already configured to use [external object storage](../external_services/object_storage.md), or you use `DISABLE_MINIO=true` in `sourcegraph/server` deployments, then this change should not affect you (there would already be no Minio software running as part of Sourcegraph). It is also safe to remove any minio specific env variables from your deployment. + +**If you are on running a proxy with the `NO_PROXY` env variable, you'll need to make sure that minio is removed from this list, and blobstore is added.** If you have any questions or concerns, please reach out to us via support@sourcegraph.com and we'd be happy to help. From ad3cd860bacb999ea123936d568ae2219a69155c Mon Sep 17 00:00:00 2001 From: adeola <67931373+adeola-ak@users.noreply.github.com> Date: Thu, 19 Jan 2023 13:05:35 -0500 Subject: [PATCH 046/678] batches: use user name/e-mail with unauthored batch specs (#46385) use user name/e-mail with unauthored batch specs --- CHANGELOG.md | 2 +- .../workers/batch_spec_workspace_creator.go | 8 +- .../ci/integration/executors/tester/main.go | 8 +- .../internal/batches/store/author/author.go | 38 ++++++++ .../batches/store/author/author_test.go | 93 +++++++++++++++++++ .../store/worker_workspace_execution.go | 7 ++ lib/batches/changeset_specs.go | 30 ++++-- lib/batches/changeset_specs_test.go | 17 +++- lib/batches/execution/cache/cache.go | 4 +- 9 files changed, 187 insertions(+), 20 deletions(-) create mode 100644 enterprise/internal/batches/store/author/author.go create mode 100644 enterprise/internal/batches/store/author/author_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa88eb222cc..6d5c26d87b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ All notable changes to Sourcegraph are documented in this file. ### Added -- +- - The default author and email for changesets will now be pulled from user account details when possible. [#46385](https://github.com/sourcegraph/sourcegraph/pull/46385) ### Changed diff --git a/enterprise/cmd/worker/internal/batches/workers/batch_spec_workspace_creator.go b/enterprise/cmd/worker/internal/batches/workers/batch_spec_workspace_creator.go index d8f27d36c361..52a5ef8802be 100644 --- a/enterprise/cmd/worker/internal/batches/workers/batch_spec_workspace_creator.go +++ b/enterprise/cmd/worker/internal/batches/workers/batch_spec_workspace_creator.go @@ -14,6 +14,7 @@ import ( "github.com/sourcegraph/sourcegraph/enterprise/internal/batches/service" "github.com/sourcegraph/sourcegraph/enterprise/internal/batches/store" + "github.com/sourcegraph/sourcegraph/enterprise/internal/batches/store/author" btypes "github.com/sourcegraph/sourcegraph/enterprise/internal/batches/types" "github.com/sourcegraph/sourcegraph/internal/actor" "github.com/sourcegraph/sourcegraph/internal/api" @@ -220,6 +221,11 @@ func (r *batchSpecWorkspaceCreator) process( usedCacheEntries := []int64{} changesetsByWorkspace := make(map[*btypes.BatchSpecWorkspace][]*btypes.ChangesetSpec) + author, err := author.GetChangesetAuthorForUser(ctx, database.UsersWith(r.logger, r.store), spec.UserID) + if err != nil { + return err + } + // Check for an existing cache entry for each of the workspaces. for _, workspace := range cacheKeyWorkspaces { for _, ck := range workspace.stepCacheKeys { @@ -275,7 +281,7 @@ func (r *batchSpecWorkspaceCreator) process( workspace.dbWorkspace.CachedResultFound = true - rawSpecs, err := cache.ChangesetSpecsFromCache(spec.Spec, workspace.repo, *res.Value, workspace.dbWorkspace.Path, true) + rawSpecs, err := cache.ChangesetSpecsFromCache(spec.Spec, workspace.repo, *res.Value, workspace.dbWorkspace.Path, true, author) if err != nil { return err } diff --git a/enterprise/dev/ci/integration/executors/tester/main.go b/enterprise/dev/ci/integration/executors/tester/main.go index ed63eb0728ff..9781ec927fbe 100644 --- a/enterprise/dev/ci/integration/executors/tester/main.go +++ b/enterprise/dev/ci/integration/executors/tester/main.go @@ -98,8 +98,8 @@ func main() { Title: "Hello World", Body: "My first batch change!", CommitMessage: "Append Hello World to all README.md files", - CommitAuthorName: "Sourcegraph", - CommitAuthorEmail: "batch-changes@sourcegraph.com", + CommitAuthorName: "sourcegraph", + CommitAuthorEmail: "sourcegraph@sourcegraph.com", Diff: []byte(expectedDiff), }, }, @@ -209,8 +209,8 @@ var expectedState = gqltestutil.BatchSpecDeep{ Subject: "Append Hello World to all README.md files", Body: "", Author: gqltestutil.ChangesetSpecCommitAuthor{ - Name: "Sourcegraph", - Email: "batch-changes@sourcegraph.com", + Name: "sourcegraph", + Email: "sourcegraph@sourcegraph.com", }, }, }, diff --git a/enterprise/internal/batches/store/author/author.go b/enterprise/internal/batches/store/author/author.go new file mode 100644 index 000000000000..6023174988fe --- /dev/null +++ b/enterprise/internal/batches/store/author/author.go @@ -0,0 +1,38 @@ +package author + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/errcode" + "github.com/sourcegraph/sourcegraph/lib/batches" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +func GetChangesetAuthorForUser(ctx context.Context, userStore database.UserStore, userID int32) (author *batches.ChangesetSpecAuthor, err error) { + + userEmailStore := database.UserEmailsWith(userStore) + + email, _, err := userEmailStore.GetPrimaryEmail(ctx, userID) + if errcode.IsNotFound(err) { + // No match just means there's no author, so we'll return nil. It's not + // an error, though. + return nil, nil + } else if err != nil { + return nil, errors.Wrap(err, "getting user e-mail") + } + + author = &batches.ChangesetSpecAuthor{Email: email} + + user, err := userStore.GetByID(ctx, userID) + if err != nil { + return nil, errors.Wrap(err, "getting user") + } + if user.DisplayName != "" { + author.Name = user.DisplayName + } else { + author.Name = user.Username + } + + return author, nil +} diff --git a/enterprise/internal/batches/store/author/author_test.go b/enterprise/internal/batches/store/author/author_test.go new file mode 100644 index 000000000000..b242fb0509d9 --- /dev/null +++ b/enterprise/internal/batches/store/author/author_test.go @@ -0,0 +1,93 @@ +package author + +import ( + "context" + "testing" + + "github.com/sourcegraph/log/logtest" + "github.com/stretchr/testify/assert" + + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/database/dbtest" +) + +func TestGetChangesetAuthorForUser(t *testing.T) { + + logger := logtest.Scoped(t) + ctx := context.Background() + db := database.NewDB(logger, dbtest.NewDB(logger, t)) + + userStore := db.Users() + + t.Run("User ID doesnt exist", func(t *testing.T) { + author, err := GetChangesetAuthorForUser(ctx, userStore, 0) + if err != nil { + t.Fatal(err) + } + if author != nil { + t.Fatalf("got non-nil author email when author doesnt exist: %v", author) + } + }) + + t.Run("User exists but doesn't have an email", func(t *testing.T) { + + user, err := userStore.Create(ctx, database.NewUser{ + Username: "mary", + }) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + + author, err := GetChangesetAuthorForUser(ctx, userStore, user.ID) + if err != nil { + t.Fatal(err) + } + if author != nil { + t.Fatalf("got non-nil author email when user doesnt have an email: %v", author) + } + }) + + t.Run("User exists and has an e-mail but doesn't have a display name", func(t *testing.T) { + + user, err := userStore.Create(ctx, database.NewUser{ + Username: "jane", + Email: "jane1@doe.com", + EmailIsVerified: true, + }) + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + author, err := GetChangesetAuthorForUser(ctx, userStore, user.ID) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, author.Name, user.Username) + }) + + t.Run("User exists", func(t *testing.T) { + + user, err := userStore.Create(ctx, database.NewUser{ + Username: "johnny", + Email: "john@test.com", + EmailIsVerified: true, + DisplayName: "John Tester", + }) + + userEmail := "john@test.com" + + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + author, err := GetChangesetAuthorForUser(ctx, userStore, user.ID) + if err != nil { + t.Fatal(err) + } + if author.Email != userEmail { + t.Fatalf("found incorrect email: %v", author) + } + + if author.Name != user.DisplayName { + t.Fatalf("found incorrect name: %v", author) + } + }) +} diff --git a/enterprise/internal/batches/store/worker_workspace_execution.go b/enterprise/internal/batches/store/worker_workspace_execution.go index 079370726cb7..af5256ce9afc 100644 --- a/enterprise/internal/batches/store/worker_workspace_execution.go +++ b/enterprise/internal/batches/store/worker_workspace_execution.go @@ -11,6 +11,7 @@ import ( "github.com/sourcegraph/log" + "github.com/sourcegraph/sourcegraph/enterprise/internal/batches/store/author" btypes "github.com/sourcegraph/sourcegraph/enterprise/internal/batches/types" "github.com/sourcegraph/sourcegraph/internal/actor" "github.com/sourcegraph/sourcegraph/internal/database" @@ -211,6 +212,11 @@ func (s *batchSpecWorkspaceExecutionWorkerStore) MarkComplete(ctx context.Contex } } + author, err := author.GetChangesetAuthorForUser(ctx, database.UsersWith(s.logger, s), batchSpec.UserID) + if err != nil { + return false, errors.Wrap(err, "creating changeset author") + } + rawSpecs, err := cache.ChangesetSpecsFromCache( batchSpec.Spec, batcheslib.Repository{ @@ -223,6 +229,7 @@ func (s *batchSpecWorkspaceExecutionWorkerStore) MarkComplete(ctx context.Contex latestStepResult.Value, workspace.Path, true, + author, ) if err != nil { return false, errors.Wrap(err, "failed to build changeset specs from cache") diff --git a/lib/batches/changeset_specs.go b/lib/batches/changeset_specs.go index c3927993cd1d..9d3e38f34d3a 100644 --- a/lib/batches/changeset_specs.go +++ b/lib/batches/changeset_specs.go @@ -37,7 +37,12 @@ type ChangesetSpecInput struct { Result execution.AfterStepResult } -func BuildChangesetSpecs(input *ChangesetSpecInput, binaryDiffs bool) ([]*ChangesetSpec, error) { +type ChangesetSpecAuthor struct { + Name string + Email string +} + +func BuildChangesetSpecs(input *ChangesetSpecInput, binaryDiffs bool, fallbackAuthor *ChangesetSpecAuthor) ([]*ChangesetSpec, error) { tmplCtx := &template.ChangesetTemplateContext{ BatchChangeAttributes: *input.BatchChangeAttributes, Steps: template.StepsContext{ @@ -52,20 +57,25 @@ func BuildChangesetSpecs(input *ChangesetSpecInput, binaryDiffs bool) ([]*Change }, } - var authorName string - var authorEmail string + var author ChangesetSpecAuthor if input.Template.Commit.Author == nil { - // user did not provide author info, so use defaults - authorName = "Sourcegraph" - authorEmail = "batch-changes@sourcegraph.com" + if fallbackAuthor != nil { + author = *fallbackAuthor + } else { + // user did not provide author info, so use defaults + author = ChangesetSpecAuthor{ + Name: "Sourcegraph", + Email: "batch-changes@sourcegraph.com", + } + } } else { var err error - authorName, err = template.RenderChangesetTemplateField("authorName", input.Template.Commit.Author.Name, tmplCtx) + author.Name, err = template.RenderChangesetTemplateField("authorName", input.Template.Commit.Author.Name, tmplCtx) if err != nil { return nil, err } - authorEmail, err = template.RenderChangesetTemplateField("authorEmail", input.Template.Commit.Author.Email, tmplCtx) + author.Email, err = template.RenderChangesetTemplateField("authorEmail", input.Template.Commit.Author.Email, tmplCtx) if err != nil { return nil, err } @@ -118,8 +128,8 @@ func BuildChangesetSpecs(input *ChangesetSpecInput, binaryDiffs bool) ([]*Change { Version: version, Message: message, - AuthorName: authorName, - AuthorEmail: authorEmail, + AuthorName: author.Name, + AuthorEmail: author.Email, Diff: diff, }, }, diff --git a/lib/batches/changeset_specs_test.go b/lib/batches/changeset_specs_test.go index 6213579f2fea..7b570fb2dc45 100644 --- a/lib/batches/changeset_specs_test.go +++ b/lib/batches/changeset_specs_test.go @@ -94,7 +94,8 @@ func TestCreateChangesetSpecs(t *testing.T) { tests := []struct { name string - input *ChangesetSpecInput + input *ChangesetSpecInput + author *ChangesetSpecAuthor want []*ChangesetSpec wantErr string @@ -145,11 +146,23 @@ func TestCreateChangesetSpecs(t *testing.T) { }, wantErr: "", }, + { + name: "publish with fallback author", + input: defaultInput, + author: &ChangesetSpecAuthor{Name: "Sourcegrapher", Email: "sourcegrapher@sourcegraph.com"}, + want: []*ChangesetSpec{ + specWith(defaultChangesetSpec, func(s *ChangesetSpec) { + s.Commits[0].AuthorEmail = "sourcegrapher@sourcegraph.com" + s.Commits[0].AuthorName = "Sourcegrapher" + }), + }, + wantErr: "", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - have, err := BuildChangesetSpecs(tt.input, true) + have, err := BuildChangesetSpecs(tt.input, true, tt.author) if err != nil { if tt.wantErr != "" { if err.Error() != tt.wantErr { diff --git a/lib/batches/execution/cache/cache.go b/lib/batches/execution/cache/cache.go index edf976e41dfc..9392cd2c31b9 100644 --- a/lib/batches/execution/cache/cache.go +++ b/lib/batches/execution/cache/cache.go @@ -151,7 +151,7 @@ func KeyForWorkspace(batchChangeAttributes *template.BatchChangeAttributes, r ba } // ChangesetSpecsFromCache takes the execution.Result and generates all changeset specs from it. -func ChangesetSpecsFromCache(spec *batches.BatchSpec, r batches.Repository, result execution.AfterStepResult, path string, binaryDiffs bool) ([]*batches.ChangesetSpec, error) { +func ChangesetSpecsFromCache(spec *batches.BatchSpec, r batches.Repository, result execution.AfterStepResult, path string, binaryDiffs bool, fallbackAuthor *batches.ChangesetSpecAuthor) ([]*batches.ChangesetSpec, error) { if len(result.Diff) == 0 { return []*batches.ChangesetSpec{}, nil } @@ -170,5 +170,5 @@ func ChangesetSpecsFromCache(spec *batches.BatchSpec, r batches.Repository, resu Path: path, } - return batches.BuildChangesetSpecs(input, binaryDiffs) + return batches.BuildChangesetSpecs(input, binaryDiffs, fallbackAuthor) } From 04ed3c848c3c73ee95014332e297701847a87e5d Mon Sep 17 00:00:00 2001 From: Geoffrey Gilmore Date: Thu, 19 Jan 2023 19:08:03 +0100 Subject: [PATCH 047/678] symbols: squirrel: remove /debugLocalCodeIntel endpoint (#46670) --- cmd/symbols/internal/api/handler_cgo.go | 1 - cmd/symbols/internal/api/handler_nocgo.go | 5 - cmd/symbols/squirrel/http_handlers.go | 122 ---------------------- 3 files changed, 128 deletions(-) diff --git a/cmd/symbols/internal/api/handler_cgo.go b/cmd/symbols/internal/api/handler_cgo.go index a637fd3d665c..f37fcbd8fb15 100644 --- a/cmd/symbols/internal/api/handler_cgo.go +++ b/cmd/symbols/internal/api/handler_cgo.go @@ -18,6 +18,5 @@ func addHandlers( readFileFunc func(context.Context, internaltypes.RepoCommitPath) ([]byte, error), ) { mux.HandleFunc("/localCodeIntel", squirrel.LocalCodeIntelHandler(readFileFunc)) - mux.HandleFunc("/debugLocalCodeIntel", squirrel.DebugLocalCodeIntelHandler) mux.HandleFunc("/symbolInfo", squirrel.NewSymbolInfoHandler(searchFunc, readFileFunc)) } diff --git a/cmd/symbols/internal/api/handler_nocgo.go b/cmd/symbols/internal/api/handler_nocgo.go index 05035c796a77..f65496fc6ef6 100644 --- a/cmd/symbols/internal/api/handler_nocgo.go +++ b/cmd/symbols/internal/api/handler_nocgo.go @@ -25,14 +25,9 @@ func addHandlers( } mux.HandleFunc("/localCodeIntel", jsonResponseHandler(internaltypes.LocalCodeIntelPayload{Symbols: []internaltypes.Symbol{}})) - mux.HandleFunc("/debugLocalCodeIntel", notEnabledHandler) mux.HandleFunc("/symbolInfo", jsonResponseHandler(internaltypes.SymbolInfo{})) } -func notEnabledHandler(w http.ResponseWriter, r *http.Request) { - http.Error(w, "feature not enabled in this build", http.StatusNotImplemented) -} - func jsonResponseHandler(v any) http.HandlerFunc { data, _ := json.Marshal(v) return func(w http.ResponseWriter, r *http.Request) { diff --git a/cmd/symbols/squirrel/http_handlers.go b/cmd/symbols/squirrel/http_handlers.go index 907e4d9feaae..5cc50641cee3 100644 --- a/cmd/symbols/squirrel/http_handlers.go +++ b/cmd/symbols/squirrel/http_handlers.go @@ -2,10 +2,8 @@ package squirrel import ( "bytes" - "context" "encoding/json" "fmt" - "html" "io" "math/rand" "net/http" @@ -13,8 +11,6 @@ import ( "strings" "github.com/inconshreveable/log15" - sitter "github.com/smacker/go-tree-sitter" - symbolsTypes "github.com/sourcegraph/sourcegraph/cmd/symbols/types" "github.com/sourcegraph/sourcegraph/internal/types" "github.com/sourcegraph/sourcegraph/lib/errors" @@ -131,124 +127,6 @@ func NewSymbolInfoHandler(symbolSearch symbolsTypes.SearchFunc, readFile readFil } } -// Response to /debugLocalCodeIntel. -func DebugLocalCodeIntelHandler(w http.ResponseWriter, r *http.Request) { - // Read ?ext= from the request. - ext := r.URL.Query().Get("ext") - if ext == "" { - http.Error(w, "missing ?ext= query parameter", http.StatusBadRequest) - return - } - - w.Header().Set("Content-Type", "text/html") - - path := types.RepoCommitPath{ - Repo: "foo", - Commit: "bar", - Path: "example." + ext, - } - - fileToRead := "/tmp/squirrel-example.txt" - readFile := func(ctx context.Context, args types.RepoCommitPath) ([]byte, error) { - return os.ReadFile("/tmp/squirrel-example.txt") - } - - squirrel := New(readFile, nil) - defer squirrel.Close() - - rangeToSymbolIx := map[types.Range]int{} - symbolIxToColor := map[int]string{} - payload, err := squirrel.localCodeIntel(r.Context(), path) - if err != nil { - fmt.Fprintf(w, "failed to generate local code intel payload: %s\n\n", err) - } else { - for ix := range payload.Symbols { - nonRed := []int{100, 120, 140, 160, 180, 200, 220, 240, 260} - symbolIxToColor[ix] = fmt.Sprintf("hsla(%d, 100%%, 50%%, 0.5)", sample(nonRed)) - } - - for ix, symbol := range payload.Symbols { - rangeToSymbolIx[symbol.Def] = ix - for _, ref := range symbol.Refs { - rangeToSymbolIx[ref] = ix - } - } - } - - node, err := squirrel.parse(r.Context(), path) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - fmt.Fprintf(w, ` - - `) - - fmt.Fprintf(w, "

Parsing as %s from file on disk %s

\n", ext, fileToRead) - - var nodeToHtml func(*sitter.Node, string) string - nodeToHtml = func(n *sitter.Node, stack string) string { - - thisStack := stack + ">" + n.Type() - - // Pick color - color := "" - if n.Type() == "ERROR" { - color = "hsla(0, 100%, 50%, 0.2)" - } else if ix, ok := rangeToSymbolIx[nodeToRange(n)]; ok { - if c, ok := symbolIxToColor[ix]; ok { - color = c - } - } else { - color = "hsla(0, 0%, 0%, 0.0)" - } - - // Tooltip - title := fmt.Sprintf("%s %d:%d-%d:%d", thisStack, n.StartPoint().Row, n.StartPoint().Column, n.EndPoint().Row, n.EndPoint().Column) - - if n.ChildCount() == 0 { - - // Base case: no children - - // Render - return fmt.Sprintf( - `%s`, - color, - title, - html.EscapeString(string(node.Contents[n.StartByte():n.EndByte()])), - ) - } else { - - // Recursive case: with children - - // Concatenate children - b := n.StartByte() - inner := &strings.Builder{} - for i := 0; i < int(n.ChildCount()); i++ { - inner.WriteString(html.EscapeString(string(node.Contents[b:n.Child(i).StartByte()]))) - inner.WriteString(nodeToHtml(n.Child(i), thisStack)) - b = n.Child(i).EndByte() - } - inner.WriteString(html.EscapeString(string(node.Contents[b:n.EndByte()]))) - - // Render - return fmt.Sprintf( - `%s`, - color, - title, - inner.String(), - ) - } - } - - fmt.Fprint(w, "
"+nodeToHtml(node.Node, "")+"
") -} - func sample[T any](xs []T) T { return xs[rand.Intn(len(xs))] } From 4b23cb2eb3b25425e5917c5d6fe21f47a4b4697c Mon Sep 17 00:00:00 2001 From: Cesar Jimenez Date: Thu, 19 Jan 2023 11:37:57 -0700 Subject: [PATCH 048/678] [hotfix]: adding logs to git tree resolver (#46682) --- .../shared/resolvers/git_tree_entry_resolver.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/enterprise/internal/codeintel/shared/resolvers/git_tree_entry_resolver.go b/enterprise/internal/codeintel/shared/resolvers/git_tree_entry_resolver.go index 91f34b3cd028..68adb5e0620a 100644 --- a/enterprise/internal/codeintel/shared/resolvers/git_tree_entry_resolver.go +++ b/enterprise/internal/codeintel/shared/resolvers/git_tree_entry_resolver.go @@ -36,10 +36,12 @@ type GitTreeEntryResolver struct { // stat is this tree entry's file info. Its Name method must return the full path relative to // the root, not the basename. stat fs.FileInfo + + logger log.Logger } func NewGitTreeEntryResolver(db database.DB, commit *GitCommitResolver, stat fs.FileInfo) *GitTreeEntryResolver { - return &GitTreeEntryResolver{db: db, commit: commit, stat: stat} + return &GitTreeEntryResolver{db: db, commit: commit, stat: stat, logger: log.Scoped("git tree entry resolver", "")} } func (r *GitTreeEntryResolver) Path() string { return r.stat.Name() } func (r *GitTreeEntryResolver) Name() string { return path.Base(r.stat.Name()) } @@ -47,6 +49,7 @@ func (r *GitTreeEntryResolver) Name() string { return path.Base(r.stat.Name()) } func (r *GitTreeEntryResolver) ToGitTree() (resolverstubs.GitTreeEntryResolver, bool) { return r, r.IsDirectory() } + func (r *GitTreeEntryResolver) ToGitBlob() (resolverstubs.GitTreeEntryResolver, bool) { return r, !r.IsDirectory() } @@ -103,8 +106,17 @@ func (r *GitTreeEntryResolver) URL(ctx context.Context) (string, error) { } func (r *GitTreeEntryResolver) Submodule() resolverstubs.GitSubmoduleResolver { + if r == nil { + r.logger.Error("git tree entry resolver is nil", log.Error(errors.New("git tree entry resolver is nil"))) + return nil + } + + if r.stat == nil { + r.logger.Error("stat is nil", log.Error(errors.New("stat is nil"))) + return nil + } + if submoduleInfo, ok := r.stat.Sys().(gitdomain.Submodule); ok { - // return &gitSubmoduleResolver{submodule: submoduleInfo} return NewGitSubmoduleResolver(submoduleInfo) } return nil From 02c985e3d77dd857f51056b1c45429c18c446abf Mon Sep 17 00:00:00 2001 From: Chris Warwick Date: Thu, 19 Jan 2023 14:11:00 -0500 Subject: [PATCH 049/678] insights: update insights dev docs (#46645) Co-authored-by: leo --- .../insights/backend.md | 150 +- .../insights/diagrams/architecture.excalidraw | 4243 ++++++++++++++--- .../insights/diagrams/architecture.svg | 575 +-- .../internal/insights/pipeline/readme.md | 33 - 4 files changed, 3647 insertions(+), 1354 deletions(-) delete mode 100644 enterprise/internal/insights/pipeline/readme.md diff --git a/doc/dev/background-information/insights/backend.md b/doc/dev/background-information/insights/backend.md index 984f04974c82..03be88e901e5 100644 --- a/doc/dev/background-information/insights/backend.md +++ b/doc/dev/background-information/insights/backend.md @@ -22,10 +22,10 @@ * Supports running search- and compute- based insights over all indexable repositories on the Sourcegraph installation. * Is backed by a separate Postgres instance. See the [database section](#database) below for more information. -* Optimizes unnecessary search queries by using an index of commits to query only for time periods that have had at least one commit. +* Optimizes unnecessary search queries by using commit history to query only for time periods that have had at least one commit. * Supports filtering: * By repository regexp - * By search context + * By repositories included/excluded by a search context * By filter options: name, result count, date added, and number of data series * Provides permissions restrictions by filtering of repositories that are not visible to the user at query time. @@ -37,7 +37,6 @@ The following architecture diagram shows how the backend fits into the two Sourc [![Architecture diagram](diagrams/architecture.svg)](https://raw.githubusercontent.com/sourcegraph/sourcegraph/main/doc/dev/background-information/insights/diagrams/architecture.svg) -Note: this architecture diagram is outdated at the time of writing in July 2022 and does not reflect the current state of Code Insights. ## Feature Flags Code Insights ships with an "escape hatch" feature flag that will completely disable the dependency on the Code Insights DB (named `codeinsights-db`). This feature flag is implemented as an environment variable that if set true `DISABLE_CODE_INSIGHTS=true` will disable the dependency and will not start the Code Insights background workers or GraphQL resolvers. This variable must be set on both the `worker` and `frontend` services to remove the dependency. If the flag is not set on both services, the `codeinsights-db` dependency will be required. @@ -62,7 +61,7 @@ Historically, Code Insights used a [TimescaleDB](https://www.timescale.com) data some of the timeseries query features, as well as the hypertable. Many of these are behind a proprietary license that would have required non-trivial work to bundle with Sourcegraph. -As of Sourcegraph 3.38, Code Insights no longer uses TimescaleDB and has moved to a standard vanilla Postgres image. The Code Insights database is still separate from the main Sourcegraph database. +As of Sourcegraph 3.38, Code Insights no longer uses TimescaleDB and has moved to a standard vanilla Postgres image. ## Insight Metadata Code Insights data is stored entirely in the `codeinsights-db` database, and exposed through a GraphQL API. Settings are deprecated as a storage @@ -77,13 +76,13 @@ At the moment we support four types of insights: * Search insights * Capture-group insights -* Language usage insights (over a list of repositories only) +* Language usage insights (over a single repository only) * Group-by insights ([experimental as of 3.42](https://github.com/sourcegraph/sourcegraph/blob/main/CHANGELOG.md#3420)) These can all be created from the UI and get resolved through the [GraphQL API](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+file:graphqlbackend/insights+createLineChartSearchInsight&patternType=lucky). #### Unique ID -An Insight View is defined to have a globally unique referencable ID. Each ID is [generated when the view is created](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+file:%5Eenterprise/internal/insights/resolvers/insight_view_resolvers%5C.go+UniqueID:%5Cs*ksuid.New%28%29.String%28%29%2C&patternType=regexp). +An Insight View is defined to have a globally unique referenceable ID. Each ID is [generated when the view is created](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+file:%5Eenterprise/internal/insights/resolvers/insight_view_resolvers%5C.go+UniqueID:%5Cs*ksuid.New%28%29.String%28%29%2C&patternType=regexp). [Read more about Insight Views](./insight_view.md) @@ -93,7 +92,7 @@ Data series defined with a repository scope used to be executed just-in-time, wi As of Sourcegraph 3.42, all data series behave the same, in that they are all recorded in the background. Data series are [uniquely identified by a randomly generated unique ID](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+file:%5Eenterprise/internal/insights/resolvers/insight_view_resolvers%5C.go+SeriesID:%5Cs*ksuid.New%28%29.String%28%29%2C&patternType=regexp). -Data series are also identified by a [compound key](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+file:%5Eenterprise/internal/insights/store/insight_store%5C.go+MatchSeriesArgs&patternType=lucky) that is used to preserve data series that have already been calculated. This will effectively share this data series among all users if the compound key matches. +Data series are also identified by a [compound key](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+file:%5Eenterprise/internal/insights/store/insight_store%5C.go+MatchSeriesArgs&patternType=lucky) that is used to preserve data series that have already been calculated. This will effectively share this data series among all users if the compound key matches. Series data sharing only applies to series run over all repositories. Data series are defined with a [recording interval](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+file:%5Eenterprise/internal/insights/types/types%5C.go+SampleInterval&patternType=lucky) that will define the frequency of samples that are taken for the series. @@ -105,39 +104,26 @@ A standard search series will execute Sourcegraph searches, and tabulate the cou was to be able to derive the series themselves from the results; that is to say a result of `(result: 1.17, count: 5) (result: 1.13, count: 3)` would generate two individual time series, one for each unique result. -We support this behavior by tabulating the results of a `compute` [search](https://sourcegraph.com/github.com/sourcegraph/sourcegraph/-/blob/enterprise/internal/insights/query/graphql_compute.go), and dynamically modifying the series that are returned. +### (1) Capturing historical data (backfilling) -### (2) The _insight enqueuer_ (indexed recorder) detects the new insight - -The _insight enqueuer_ is a background goroutine running in the `worker` service of Sourcegraph ([code](https://sourcegraph.com/github.com/sourcegraph/sourcegraph@55be9054a2609e06a1d916cc2f782827421dd2a3/-/blob/enterprise/internal/insights/background/insight_enqueuer.go?L27:6)), which runs all background goroutines for Sourcegraph - so long as `DISABLE_CODE_INSIGHTS=true` is not set on the `worker` container/process. - -Its job is to periodically schedule a recording of 'current' values for Insights by enqueuing a recording using a global query. This only requires a single query per insight regardless of the number of repositories, -and will return results for all the matched repositories. Each repository will still be recorded individually. These queries are placed on the same queue as historical queries (`insights_query_runner_jobs`) and can -be identified by the lack of a revision and repo filter on the query string. - -For example, `insights` might be an global recording, where `insights repo:^codehost\.com/myorg/somerepo@ref$` would be a historical recording for a specific repo / revision. -You can find these search queries for queued jobs on the (primary postgres) table `insights_query_runner_jobs.search_query` +If we only record data starting when the series were created, it would take months or longer for users to get any value out of insights. This introduces the need for us to backfill data by running search queries that answer "how many results existed in the past?" so we can populate historical data. -Insight recordings are scheduled using the database field (codeinsights-db) `insight_series.next_recording_after`, and will only be taken if the field time is less than the execution time of the job. -Recordings are scheduled to occur one interval (per series definition) following the execution time. For example, if a recording was taken at `2021-08-27T15:29:00.000Z` -with an interval definition of 1 day, the next recording will be scheduled for `2021-08-28T15:29:00.000Z`. The first recording after insight creation will occur on the same interval. +Backfilling relies on two background goroutines _[New Backfill](https://sourcegraph.com/github.com/sourcegraph/sourcegraph@175655dc14f60fb2e6387ec65e13ac8662114cec/-/blob/enterprise/internal/insights/scheduler/backfill_state_new_handler.go?L88:96&popover=pinned#tab=references)_ and _[In Progress Backfill](https://sourcegraph.com/github.com/sourcegraph/sourcegraph@175655dc14f60fb2e6387ec65e13ac8662114cec/-/blob/enterprise/internal/insights/scheduler/backfill_state_inprogress_handler.go?L123:29&popover=pinned#tab=references)_. -### (3) The historical data enqueuer (historical recorder) gets to work +When an insight is created a new Backfill record is created for each series in the `new` state. -If we only record data starting when the series were created, it would take months or longer for users to get any value out of insights. This introduces the need for us to backfill data by running search queries that answer "how many results existed in the past?" so we can populate historical data. +The _New Backfill_ processes backfills in the `new` state and determines which repositories are needed along with an estimated cost which determines the order in which it will be backfilled. It then moves the backfill record to the `in progress` state. -Similar to the _insight enqueuer_, the _historical insight enqueuer_ is a background goroutine ([code](https://sourcegraph.com/github.com/sourcegraph/sourcegraph@55be9054a2609e06a1d916cc2f782827421dd2a3/-/blob/enterprise/internal/insights/background/historical_enqueuer.go?L75:6)) which locates and enqueues work to populate historical data points. - -The most naive implementation of backfilling is as follows: -``` -For each relevant repository: - For each relevant time point: - Execute a search at the most recent revision -``` +The _In Progress Backfill_ processes backfills in the `in progress` state by iterating over the repositories and completing the following: + 1. Determines the revisions to search for a specific date/time + 2. Execute the searches + 3. Record the results in the database + +This process will repeat until all repositories for the series have been searched, checking at a [configurable interval of time](https://sourcegraph.com/github.com/sourcegraph/sourcegraph@175655dc14f60fb2e6387ec65e13ac8662114cec/-/blob/schema/schema.go?L2393:2&popover=pinned) to ensure no higher priority work has arrived. -Naively implemented, the historical backfiller would take a long time on any reasonably sized Sourcegraph installation. As an optimization, -the backfiller will only query for data frames that have recorded changes in each repository. This is accomplished by looking -at an index of commits and determining if that frame is eligible for removal. +Naively implemented, the backfiller would take a long time on any reasonably sized Sourcegraph installation. As an optimization, +the backfiller will only query for time periods that have recorded changes in each repository. This is accomplished by looking +at the repository's commits and determining if that time period is eligible for removal. Read more [below](#Backfill-compression) There is a rate limit associated with analyzing historical data frames. This limit can be configured using the site setting @@ -147,38 +133,35 @@ impact to `gitserver`. A likely safe starting point on most Sourcegraph installa #### Backfill compression Read more about the backfilling compression in the proposal [RFC 392](https://docs.google.com/document/d/1VDk5Buks48THxKPwB-b7F42q3tlKuJkmUmaCxv2oEzI/edit#heading=h.3babtpth82k2) -We maintain an index of commits (table `commit_index` in codeinsights-db) that are used to filter out repositories that do not need a search query. This index is -periodically [refreshed](https://sourcegraph.com/github.com/sourcegraph/sourcegraph@55be9054a2609e06a1d916cc2f782827421dd2a3/-/blob/enterprise/internal/insights/compression/worker.go?L41) -with changes since its previous refresh. Metadata for each repositories refresh is tracked in a table `commit_index_metadata`. - -To avoid race conditions with the index, data frames are only [filtered](https://sourcegraph.com/github.com/sourcegraph/sourcegraph@55be9054a2609e06a1d916cc2f782827421dd2a3/-/blob/enterprise/internal/insights/compression/compression.go?L84) -out if the `commit_index_metadata.last_updated_at` is greater than the data point we are attempting to compress. Additionally, if the index does not contain enough history (the commit -falls before the oldest commit in the index), all of the data frames prior to the history will be uncompressed. - -Currently, we only generate 12 months of history for this commit index to keep it reasonably sized. We do not currently do any pruning, but is an area that will need development in the future. +We query gitserver for commits and use that to filter out repositories and/or time periods that do not need any search queries. #### Detecting if an insight is _complete_ Given the large possible cardinality of required queries to backfill an insight, it is clear this process can take some time. Through dogfooding we have found on a Sourcegraph installation with ~36,000 repositories, we can expect to backfill an average insight in 20-30 minutes. The actual benchmarks of how long this will take vary greatly depending on the commit patterns and size of the Installation. -One important piece of information that needs to be surfaced to users is the answer to the question `is my insight still processing?` -This is a non-trivial question to answer: -1. Work is processed asynchronously, so querying the state of the queue is necessary -2. Iterating many thousands of repositories can result in some transient errors causing individual repositories to fail, and ultimately not be included in the queue [issue](https://github.com/sourcegraph/sourcegraph/issues/23844) +One important piece of information that needs to be surfaced to users is the answer to the question `is my insight still processing?`, this can be determined my examining the Backfill records for all of the series contained in an insight. When all backfills have reached a terminal state the processing is complete. + +`When will my insight finish processing?` is a non-trivial question to answer because the processing of a series may be paused for an indefinite amount of time if a new insight with a higher priority is created. -As a temporary measure to try and answer this question with some degree of accuracy, the historical backfiller applies the following semantic: -Flag an insight as `completed backfill` if the insight was able to complete one full iteration of all repositories without any `hard` errors (such as low level DB errors, etc). -This flag is represented as the database field `insight_series.backfill_queued_at` and is [set](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24%4055be905+StampBackfill&patternType=literal) at the end of the complete repository iteration. +### (2) The _insight enqueuer_ (indexed recorder) detects existing insights that need new data + +The _insight enqueuer_ is a background goroutine running in the `worker` service of Sourcegraph ([code](https://sourcegraph.com/github.com/sourcegraph/sourcegraph@55be9054a2609e06a1d916cc2f782827421dd2a3/-/blob/enterprise/internal/insights/background/insight_enqueuer.go?L27:6)), which runs all background goroutines for Sourcegraph - so long as `DISABLE_CODE_INSIGHTS=true` is not set on the `worker` container/process. -This semantic does not fully capture all possible states. For example, if a repository encounters a `soft` error (unable to fetch git metadata, for example) -it will be skipped and ultimately not populate in the data series. Improving this is an area of design and work in FY23. +Its job is to periodically schedule a recording of 'current' values for Insights by enqueuing a recording using a global query. This only requires a single global query per insight regardless of the number of repositories, +and will return results for all the matched repositories. Each matched repository will still be recorded individually. + +You can find these search queries for queued jobs on the (primary postgres) table `insights_query_runner_jobs.search_query` + +Insight recordings are scheduled using the database field (codeinsights-db) `insight_series.next_recording_after` or `insight_series.next_snapshot_after`, and will only be taken if the field time is less than the execution time of the job. +Recordings are scheduled to occur one interval (per series definition) following the execution time. For example, if a recording was taken at `2021-08-27T15:29:00.000Z` +with an interval definition of 1 day, the next recording will be scheduled for `2021-08-28T15:29:00.000Z`. The first recording after insight creation will occur on the same interval. Snapshots are scheduled to occur daily and are removed each time a new snapshot is captured. -### (4) The queryrunner worker gets work and runs the search query +### (3) The queryrunner worker gets work and runs the search query The queryrunner ([code](https://sourcegraph.com/github.com/sourcegraph/sourcegraph@55be9054a2609e06a1d916cc2f782827421dd2a3/-/blob/enterprise/internal/insights/background/queryrunner/worker.go)) is a background goroutine running in the `worker` service of Sourcegraph ([code](https://sourcegraph.com/github.com/sourcegraph/sourcegraph@55be9054a2609e06a1d916cc2f782827421dd2a3/-/blob/enterprise/internal/insights/background/queryrunner/worker.go?L42:6)), it is responsible for: -1. Dequeueing search queries that have been queued by the either the current, snapshot, or historical recorder. Queries are stored with a `priority` field that +1. Dequeueing search queries that have been queued by the `insight_enqueuer`. Queries are stored with a `priority` field that [dequeues](https://sourcegraph.com/github.com/sourcegraph/sourcegraph@55be905/-/blob/enterprise/internal/insights/background/queryrunner/worker.go?L134) queries in ascending priority order (0 is higher priority than 100). 2. Executing a search against Sourcegraph with the provided query. These queries are executed against the `internal` API, meaning they are *unauthorized* and can see all results. This allows us to build global results and filter based on user permissions at query time. 3. Aggregating the search results, per repository and storing them in the `series_points` table. @@ -190,16 +173,17 @@ the desired concurrency factor. With `insights.query.worker.concurrency=1` queri There is a rate limit associated with the query worker. This limit is shared across all concurrent handlers and can be configured using the site setting `insights.query.worker.rateLimit`. This value to set will depend on the size and scale of the Sourcegraph -installations `Searcher` service. +installations `Searcher` service. This rate limit is shared with `In Progress Backfiller`. + ### (5) Query-time and rendering! The webapp frontend invokes a GraphQL API which is served by the Sourcegraph `frontend` monolith backend service in order to query information about backend insights. ([code](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+file:enterprise/+lang:go+InsightConnectionResolver&patternType=literal)) 1. A GraphQL resolver `insightViewResolver` returns all the distinct data series in a single insight (UI panel) ([code](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+type+insightViewResolver+struct&patternType=literal)) -2. A [resolver is selected](https://sourcegraph.com/github.com/sourcegraph/sourcegraph/-/blob/enterprise/internal/insights/resolvers/insight_view_resolvers.go?L98-118) depending on the type of series, and whether or not dynamic search results need to be expanded. +2. A [resolver is selected](https://sourcegraph.com/github.com/sourcegraph/sourcegraph@175655dc14f60fb2e6387ec65e13ac8662114cec/-/blob/enterprise/internal/insights/resolvers/insight_view_resolvers.go?L141:31&popover=pinned) depending on the type of series, and whether or not dynamic search results need to be expanded. 3. A GraphQL resolver ultimately provides data points for a single series of data ([code](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+file:enterprise/+file:resolver+lang:go+Points%28&patternType=literal)) -4. The _series points resolver_ merely queries the _insights store_ for the data points it needs, and the store itself merely runs SQL queries against the database to get the datapoints ([code](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+file:enterprise/+file:store+lang:go+SeriesPoints%28&patternType=literal)) +4. The _series points resolver_ merely queries the _insights store_ for the data points it needs, and the store itself merely runs SQL queries against the database to get the data points ([code](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+file:enterprise/+file:store+lang:go+SeriesPoints%28&patternType=literal)) Note: There are other better developer docs which explain the general reasoning for why we have a "store" abstraction. Insights usage of it is pretty minimal, we mostly follow it to separate SQL operations from GraphQL resolver code and to remain consistent with the rest of Sourcegraph's architecture. @@ -224,7 +208,7 @@ This may not be suitable for Sourcegraph installations with highly controlled re The code insights time series are currently stored entirely within Postgres. As a design, insight data is stored as a full vector of match results per unique time point. This means that for some time `T`, all of the unique timeseries that fall under -one insight series can be aggregated to form the total result. Given that the processing system will execute every query at-least once, the possiblity of duplicates +one insight series can be aggregated to form the total result. Given that the processing system will execute every query at-least once, the possibility of duplicates exist within a unique timeseries. A simple deduplication is performed at query time. Read more about the [history](https://github.com/sourcegraph/sourcegraph/issues/23690) of this format. @@ -238,31 +222,12 @@ sg start enterprise-codeinsights Insights can then be [created either via the locally running webapp](../../../code_insights/how-tos/creating_a_custom_dashboard_of_code_insights.md), or [created via the GraphQL API](../../../api/graphql/managing-code-insights-with-api.md). -If you've created an insight that needs to generate series data on the backend, be aware of [the time interval](https://sourcegraph.com/github.com/sourcegraph/sourcegraph/-/blob/enterprise/internal/insights/background/historical_enqueuer.go?L122) at which these series will be picked up for backfilling. You may want to restart the service so that the new series will be picked up right away for processing. - ## Debugging This being a pretty complex, high cardinality, and slow-moving system - debugging can be tricky. In this section, I'll cover useful tips I have for debugging the system when developing it or otherwise using it. -### Accessing the Code Insights database - -#### Dev and docker compose deployments - -``` -docker exec -it codeinsights-db psql -U postgres -``` - -#### Kubernetes deployments - -``` -kubectl exec -it deployment/codeinsights-db -- psql -U postgres -``` - -* If trying to access Sourcegraph.com's DB: `kubectl -n prod exec -it deployment/codeinsights-db -- psql -U postgres` -* If trying to access k8s.sgdev.org's DB: `kubectl -n dogfood-k8s exec -it deployment/codeinsights-db -- psql -U postgres` - ### `sg insights` tool The `sg insights` tool is a developer-built tool to perform some common database queries used when debugging insight issues. @@ -296,9 +261,7 @@ Logs can be inspected from Grafana with a query like: ### Inspecting the Code Insights database -Read the [initial schema migration](https://github.com/sourcegraph/sourcegraph/blob/main/migrations/codeinsights/1000000001/up.sql) which contains all of the tables we create in Postgres and describes them in detail. This will explain the general layout of the database schema, etc. - -The most important table in the insights database is `series_points`, that's where the actual data is stored. +The table `series_points` in the insights database is the table where the actual search results data is stored. #### Querying data @@ -339,34 +302,9 @@ UNION SELECT id FROM repo_names WHERE name='github.com/gorilla/mux-original'; ``` -##### Inserting a data point - -You can insert a data point using e.g.: - -```sql -INSERT INTO series_points( - series_id, - time, - value, - metadata_id, - repo_id, - repo_name_id, - original_repo_name_id -) VALUES( - "my unique test series ID", - now(), - 0.5, - (SELECT id FROM metadata WHERE metadata = '{"hello": "world", "languages": ["Go", "Python", "Java"]}'), - 2, - (SELECT id FROM repo_names WHERE name = 'github.com/gorilla/mux-renamed'), - (SELECT id FROM repo_names WHERE name = 'github.com/gorilla/mux-original') -); -``` - -You can omit all of the `*repo*` fields (nullable) if you want to store a data point describing a global (associated with no repository) series of data. ## Creating DB migrations `migrations/codeinsights` in the root of this repository contains the migrations for the Code Insights database, they are executed when the frontend starts up (as is the same with e.g. codeintel DB migrations.) -Currently, the migration process blocks `frontend` and `worker` startup - which is one issue [we will need to solve](https://github.com/sourcegraph/sourcegraph/issues/18388). +Migrations can be created using [`sg`](../sg/reference.md#sg-migration-add) diff --git a/doc/dev/background-information/insights/diagrams/architecture.excalidraw b/doc/dev/background-information/insights/diagrams/architecture.excalidraw index 8db8bc277edf..3c766f8c89c9 100644 --- a/doc/dev/background-information/insights/diagrams/architecture.excalidraw +++ b/doc/dev/background-information/insights/diagrams/architecture.excalidraw @@ -5,8 +5,8 @@ "elements": [ { "type": "rectangle", - "version": 328, - "versionNonce": 1409136609, + "version": 581, + "versionNonce": 773338096, "isDeleted": false, "id": "757t64sQvuoL2kg95ggj8", "fillStyle": "hachure", @@ -15,8 +15,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1112.2105263157896, - "y": 155.52631578947364, + "x": 1489.4370888157894, + "y": 1199.7997532894733, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 251.00000000000003, @@ -24,16 +24,28 @@ "seed": 2122221822, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "7N9O4UYvb3trzfS-1WhiK", - "AjY--FnIsEH_RH8neZdWg", - "KKbSybPQ9XHbl7u01ubvh" - ] + "boundElements": [ + { + "type": "arrow", + "id": "7N9O4UYvb3trzfS-1WhiK" + }, + { + "type": "arrow", + "id": "AjY--FnIsEH_RH8neZdWg" + }, + { + "type": "arrow", + "id": "KKbSybPQ9XHbl7u01ubvh" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "text", - "version": 134, - "versionNonce": 1036186287, + "version": 387, + "versionNonce": 94978832, "isDeleted": false, "id": "Nc0YnZPTOySGOhmpeNpjp", "fillStyle": "hachure", @@ -42,8 +54,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1132.2105263157896, - "y": 179.02631578947364, + "x": 1509.4370888157894, + "y": 1223.2997532894733, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 211, @@ -51,18 +63,23 @@ "seed": 194629666, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "GraphQL API schema", "baseline": 20, "textAlign": "left", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "GraphQL API schema" }, { "type": "text", - "version": 476, - "versionNonce": 706614209, + "version": 729, + "versionNonce": 1993094640, "isDeleted": false, "id": "rRAGUA90gaugXWkfDXm11", "fillStyle": "hachure", @@ -71,8 +88,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1606.4210526315792, - "y": 293.57894736842104, + "x": 1983.647615131579, + "y": 1337.8523848684206, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 363, @@ -80,18 +97,23 @@ "seed": 1048712894, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "OSS GraphQL resolvers\n\"Insights not available in OSS\"", "baseline": 44, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "OSS GraphQL resolvers\n\"Insights not available in OSS\"" }, { "type": "rectangle", - "version": 510, - "versionNonce": 491701455, + "version": 763, + "versionNonce": 1403937040, "isDeleted": false, "id": "i3BuMRIOBe_19hJI91DfV", "fillStyle": "hachure", @@ -100,8 +122,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1591.4210526315792, - "y": 278.57894736842104, + "x": 1968.647615131579, + "y": 1322.8523848684206, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 393, @@ -109,18 +131,36 @@ "seed": 333248034, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "7N9O4UYvb3trzfS-1WhiK", - "J9lt97v6DJ9bxVEIbmjmx", - "B-o7BTvFlW-Q3QNgxxuJS", - "uV4A0Fp5-QI1BG0gkOoBE", - "YVPSmET7TbVynPjvhdUSL" - ] + "boundElements": [ + { + "type": "arrow", + "id": "7N9O4UYvb3trzfS-1WhiK" + }, + { + "type": "arrow", + "id": "J9lt97v6DJ9bxVEIbmjmx" + }, + { + "type": "arrow", + "id": "B-o7BTvFlW-Q3QNgxxuJS" + }, + { + "type": "arrow", + "id": "uV4A0Fp5-QI1BG0gkOoBE" + }, + { + "type": "arrow", + "id": "YVPSmET7TbVynPjvhdUSL" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "text", - "version": 321, - "versionNonce": 931656609, + "version": 574, + "versionNonce": 1303377904, "isDeleted": false, "id": "ztKSldwXr5KJRMoPk9bRK", "fillStyle": "hachure", @@ -129,8 +169,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1136.3684210526317, - "y": 418.89473684210526, + "x": 1513.5949835526314, + "y": 1463.1681743421047, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 328, @@ -138,18 +178,23 @@ "seed": 298870690, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Enterprise GraphQL resolvers", "baseline": 20, "textAlign": "left", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "Enterprise GraphQL resolvers" }, { "type": "rectangle", - "version": 349, - "versionNonce": 868574465, + "version": 602, + "versionNonce": 526368528, "isDeleted": false, "id": "kdqfC4iFcVRtqwUun5Eaf", "fillStyle": "hachure", @@ -158,8 +203,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1112.3684210526317, - "y": 405.89473684210526, + "x": 1489.5949835526314, + "y": 1450.1681743421047, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 378, @@ -167,22 +212,52 @@ "seed": 1151319586, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "AjY--FnIsEH_RH8neZdWg", - "xbjkvbQa1ZV0sIVqiYxPV", - "mjRzC9ucLmoNvN3m6lacw", - "Mu0ZCcmcW4RkURpLQ9DzL", - "o-u5q_29fU8DTydha1CS6", - "HGqemLiNbWvQ2m_d3rqL8", - "tNGtBSw12gQ-BIyd1Duuy", - "nGIZerkX-nuZP5ph84wzq", - "VJg36Parv4prCCSHUZc3U" - ] + "boundElements": [ + { + "type": "arrow", + "id": "AjY--FnIsEH_RH8neZdWg" + }, + { + "type": "arrow", + "id": "xbjkvbQa1ZV0sIVqiYxPV" + }, + { + "type": "arrow", + "id": "mjRzC9ucLmoNvN3m6lacw" + }, + { + "type": "arrow", + "id": "Mu0ZCcmcW4RkURpLQ9DzL" + }, + { + "type": "arrow", + "id": "o-u5q_29fU8DTydha1CS6" + }, + { + "type": "arrow", + "id": "HGqemLiNbWvQ2m_d3rqL8" + }, + { + "type": "arrow", + "id": "tNGtBSw12gQ-BIyd1Duuy" + }, + { + "type": "arrow", + "id": "nGIZerkX-nuZP5ph84wzq" + }, + { + "type": "arrow", + "id": "VJg36Parv4prCCSHUZc3U" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "text", - "version": 323, - "versionNonce": 1460027233, + "version": 576, + "versionNonce": 1456175600, "isDeleted": false, "id": "hLWjUmgRGYwTIOFlLzeyz", "fillStyle": "hachure", @@ -191,8 +266,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1157.9307999061698, - "y": 765.033075299085, + "x": 1535.1573624061696, + "y": 1809.3065127990847, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 176, @@ -200,20 +275,28 @@ "seed": 1244972130, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "BixWOkWrvvO6uxXJ6clF8" + "boundElements": [ + { + "type": "arrow", + "id": "BixWOkWrvvO6uxXJ6clF8" + } ], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "codeinsights-db\n(Postgres)", "baseline": 44, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "codeinsights-db\n(Postgres)" }, { "type": "diamond", - "version": 458, - "versionNonce": 112054337, + "version": 711, + "versionNonce": 1765104912, "isDeleted": false, "id": "F4xor4iMUYwNk1eWiE96P", "fillStyle": "hachure", @@ -222,8 +305,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1131.4307999061698, - "y": 676.533075299085, + "x": 1508.6573624061696, + "y": 1720.8065127990847, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 229, @@ -231,16 +314,28 @@ "seed": 571572514, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "AGBda_XPtse3jxYwQpX71", - "ZHgi360-kQlat5JRQ3CI3", - "oZ-FYC8NS_zJEK1zcW_sh" - ] + "boundElements": [ + { + "type": "arrow", + "id": "AGBda_XPtse3jxYwQpX71" + }, + { + "type": "arrow", + "id": "ZHgi360-kQlat5JRQ3CI3" + }, + { + "type": "arrow", + "id": "oZ-FYC8NS_zJEK1zcW_sh" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "text", - "version": 596, - "versionNonce": 1504291649, + "version": 849, + "versionNonce": 1965472752, "isDeleted": false, "id": "fB8hnRjwEBAj1eb54gN1I", "fillStyle": "hachure", @@ -249,8 +344,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1665.5350877192984, - "y": 422.05263157894734, + "x": 2042.7616502192982, + "y": 1466.3260690789468, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 223, @@ -258,18 +353,23 @@ "seed": 1835702370, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Insight Definitions", "baseline": 20, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "Insight Definitions" }, { "type": "rectangle", - "version": 574, - "versionNonce": 1867790607, + "version": 827, + "versionNonce": 1655126800, "isDeleted": false, "id": "3o8jhwih8PwQ9hnczwaLy", "fillStyle": "hachure", @@ -278,8 +378,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1594.0350877192984, - "y": 409.05263157894734, + "x": 1971.2616502192982, + "y": 1453.3260690789468, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 373.9999999999999, @@ -287,22 +387,52 @@ "seed": 1121057918, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "lO2Dx1FYaAgcEmPYoZ2pt", - "HGqemLiNbWvQ2m_d3rqL8", - "Fh1ESg-Q7e1r0U1vqFeKO", - "4VBjbrPO99JhZUjpe1xgi", - "uYs6SQeUIQFCxFHIEQLSN", - "kSII8z_7o6U45f0TTyTpv", - "ky7EQFlCZ2mIu7VUwvS3e", - "ZHgi360-kQlat5JRQ3CI3", - "f_I22dnuoi2salbROQ-_L" - ] + "boundElements": [ + { + "type": "arrow", + "id": "lO2Dx1FYaAgcEmPYoZ2pt" + }, + { + "type": "arrow", + "id": "HGqemLiNbWvQ2m_d3rqL8" + }, + { + "type": "arrow", + "id": "Fh1ESg-Q7e1r0U1vqFeKO" + }, + { + "type": "arrow", + "id": "4VBjbrPO99JhZUjpe1xgi" + }, + { + "type": "arrow", + "id": "uYs6SQeUIQFCxFHIEQLSN" + }, + { + "type": "arrow", + "id": "kSII8z_7o6U45f0TTyTpv" + }, + { + "type": "arrow", + "id": "ky7EQFlCZ2mIu7VUwvS3e" + }, + { + "type": "arrow", + "id": "ZHgi360-kQlat5JRQ3CI3" + }, + { + "type": "arrow", + "id": "f_I22dnuoi2salbROQ-_L" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "text", - "version": 278, - "versionNonce": 817142561, + "version": 531, + "versionNonce": 1791606256, "isDeleted": false, "id": "qwiuEpAxZlXnDT8i9HNdb", "fillStyle": "hachure", @@ -311,8 +441,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1123.8684210526317, - "y": 455.89473684210526, + "x": 1501.0949835526314, + "y": 1500.1681743421047, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 357, @@ -320,18 +450,23 @@ "seed": 1563189630, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 16, "fontFamily": 3, "text": "enterprise/internal/insights/resolvers", "baseline": 16, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "enterprise/internal/insights/resolvers" }, { "type": "text", - "version": 489, - "versionNonce": 1456399215, + "version": 742, + "versionNonce": 1311594768, "isDeleted": false, "id": "pPY8o3FB0jXr8qpENYXEa", "fillStyle": "hachure", @@ -340,8 +475,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1131.763157894737, - "y": 287.07894736842104, + "x": 1508.9897203947369, + "y": 1331.3523848684206, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 363, @@ -349,18 +484,23 @@ "seed": 1868760226, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "OSS GraphQL resolvers interface", "baseline": 20, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "OSS GraphQL resolvers interface" }, { "type": "text", - "version": 194, - "versionNonce": 1821806337, + "version": 447, + "versionNonce": 2025619440, "isDeleted": false, "id": "Vxih14Z7ZQI79dFcSBU_r", "fillStyle": "hachure", @@ -369,8 +509,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1131.263157894737, - "y": 324.57894736842104, + "x": 1508.4897203947369, + "y": 1368.8523848684206, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 366, @@ -378,18 +518,23 @@ "seed": 1717426466, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 16, "fontFamily": 3, "text": "cmd/frontend/graphqlbackend/insights.go", "baseline": 16, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "cmd/frontend/graphqlbackend/insights.go" }, { "type": "rectangle", - "version": 226, - "versionNonce": 958307727, + "version": 479, + "versionNonce": 1598011152, "isDeleted": false, "id": "o0sepXGt4fKDi4Zoqq58T", "fillStyle": "hachure", @@ -398,8 +543,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1115.263157894737, - "y": 276.57894736842104, + "x": 1492.4897203947369, + "y": 1320.8523848684206, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 395.00000000000006, @@ -407,22 +552,52 @@ "seed": 1319989118, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "KKbSybPQ9XHbl7u01ubvh", - "J9lt97v6DJ9bxVEIbmjmx", - "xbjkvbQa1ZV0sIVqiYxPV", - "mjRzC9ucLmoNvN3m6lacw", - "Mu0ZCcmcW4RkURpLQ9DzL", - "B-o7BTvFlW-Q3QNgxxuJS", - "uV4A0Fp5-QI1BG0gkOoBE", - "YVPSmET7TbVynPjvhdUSL", - "tNGtBSw12gQ-BIyd1Duuy" - ] + "boundElements": [ + { + "type": "arrow", + "id": "KKbSybPQ9XHbl7u01ubvh" + }, + { + "type": "arrow", + "id": "J9lt97v6DJ9bxVEIbmjmx" + }, + { + "type": "arrow", + "id": "xbjkvbQa1ZV0sIVqiYxPV" + }, + { + "type": "arrow", + "id": "mjRzC9ucLmoNvN3m6lacw" + }, + { + "type": "arrow", + "id": "Mu0ZCcmcW4RkURpLQ9DzL" + }, + { + "type": "arrow", + "id": "B-o7BTvFlW-Q3QNgxxuJS" + }, + { + "type": "arrow", + "id": "uV4A0Fp5-QI1BG0gkOoBE" + }, + { + "type": "arrow", + "id": "YVPSmET7TbVynPjvhdUSL" + }, + { + "type": "arrow", + "id": "tNGtBSw12gQ-BIyd1Duuy" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "arrow", - "version": 428, - "versionNonce": 1674512097, + "version": 1037, + "versionNonce": 2131165680, "isDeleted": false, "id": "KKbSybPQ9XHbl7u01ubvh", "fillStyle": "hachure", @@ -431,8 +606,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1237.9128290403494, - "y": 225.5263157894737, + "x": 1615.1393915403492, + "y": 1269.7997532894733, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 1.1898641037128073, @@ -440,16 +615,19 @@ "seed": 418616290, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "startBinding": { "elementId": "757t64sQvuoL2kg95ggj8", - "focus": 0.006517290636283955, - "gap": 2.000000000000014 + "focus": 0.00647206689684278, + "gap": 2 }, "endBinding": { "elementId": "o0sepXGt4fKDi4Zoqq58T", - "focus": -0.3636324752759167, - "gap": 9.089792232200267 + "focus": -0.36363247527591624, + "gap": 9.089792232200125 }, "lastCommittedPoint": null, "startArrowhead": null, @@ -467,8 +645,8 @@ }, { "type": "text", - "version": 254, - "versionNonce": 1236407215, + "version": 507, + "versionNonce": 441865488, "isDeleted": false, "id": "SWqZwUAIjBhj3_L4I2YyW", "fillStyle": "hachure", @@ -477,8 +655,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1198.184210526316, - "y": 545.3684210526316, + "x": 1575.4107730263158, + "y": 1589.6418585526312, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 176, @@ -486,18 +664,23 @@ "seed": 1431332706, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Series data", "baseline": 20, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "Series data" }, { "type": "text", - "version": 219, - "versionNonce": 1894613697, + "version": 472, + "versionNonce": 481379312, "isDeleted": false, "id": "1EznrDxP6yuSdOZ8AedsX", "fillStyle": "hachure", @@ -506,8 +689,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1130.684210526316, - "y": 584.3684210526316, + "x": 1507.9107730263158, + "y": 1628.6418585526312, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 319, @@ -515,18 +698,23 @@ "seed": 232858530, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 16, "fontFamily": 3, "text": "enterprise/internal/insights/store", "baseline": 16, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "enterprise/internal/insights/store" }, { "type": "rectangle", - "version": 253, - "versionNonce": 911141761, + "version": 506, + "versionNonce": 887698192, "isDeleted": false, "id": "4C63a8jstFovCFlcXH1go", "fillStyle": "hachure", @@ -535,8 +723,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1118.684210526316, - "y": 533.3684210526316, + "x": 1495.9107730263158, + "y": 1577.6418585526312, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 347, @@ -544,21 +732,48 @@ "seed": 323237054, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "AGBda_XPtse3jxYwQpX71", - "o-u5q_29fU8DTydha1CS6", - "nGIZerkX-nuZP5ph84wzq", - "D-aGC4EzCpWCj0Rogvok-", - "uLumB7sd5oaIkrCNFmc4b", - "BixWOkWrvvO6uxXJ6clF8", - "JyzJ3HAKOXhABckvRFBtL", - "dhppvnEuhCmfrBwwBpg1_" - ] + "boundElements": [ + { + "type": "arrow", + "id": "AGBda_XPtse3jxYwQpX71" + }, + { + "type": "arrow", + "id": "o-u5q_29fU8DTydha1CS6" + }, + { + "type": "arrow", + "id": "nGIZerkX-nuZP5ph84wzq" + }, + { + "type": "arrow", + "id": "D-aGC4EzCpWCj0Rogvok-" + }, + { + "type": "arrow", + "id": "uLumB7sd5oaIkrCNFmc4b" + }, + { + "type": "arrow", + "id": "BixWOkWrvvO6uxXJ6clF8" + }, + { + "type": "arrow", + "id": "JyzJ3HAKOXhABckvRFBtL" + }, + { + "type": "arrow", + "id": "dhppvnEuhCmfrBwwBpg1_" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "arrow", - "version": 856, - "versionNonce": 1888584353, + "version": 1465, + "versionNonce": 1338197488, "isDeleted": false, "id": "HGqemLiNbWvQ2m_d3rqL8", "fillStyle": "hachure", @@ -567,8 +782,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1499.945885841364, - "y": 446.2888242023153, + "x": 1877.1724483413639, + "y": 1490.5622617023148, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 86.42723004694835, @@ -576,16 +791,19 @@ "seed": 1823209854, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "startBinding": { "elementId": "kdqfC4iFcVRtqwUun5Eaf", - "gap": 9.577464788732403, - "focus": -0.03198423525845081 + "focus": -0.026034720732401314, + "gap": 9.577464788732414 }, "endBinding": { "elementId": "3o8jhwih8PwQ9hnczwaLy", - "gap": 7.661971830985919, - "focus": -0.13211499188354198 + "focus": -0.1321149918835334, + "gap": 7.661971830985976 }, "lastCommittedPoint": null, "startArrowhead": null, @@ -603,8 +821,8 @@ }, { "type": "text", - "version": 400, - "versionNonce": 1026962415, + "version": 643, + "versionNonce": 1970295056, "isDeleted": false, "id": "NUzZUYofYS1IbNjRUsWoO", "fillStyle": "hachure", @@ -613,8 +831,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1382.763498629139, - "y": 63.902597505507316, + "x": 1759.9900611291387, + "y": 1108.176035005507, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 361, @@ -622,18 +840,23 @@ "seed": 2024591998, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 28, "fontFamily": 3, "text": "\"Frontend\" service\n(Sourcegraph monolith)", "baseline": 62, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "\"Frontend\" service\n(Sourcegraph monolith)" }, { "type": "rectangle", - "version": 203, - "versionNonce": 2137687681, + "version": 456, + "versionNonce": 356845552, "isDeleted": false, "id": "9G0QPGUBYM-HQuseQbNyD", "fillStyle": "hachure", @@ -642,8 +865,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1090.1436155882034, - "y": 143.78281529135114, + "x": 1467.3701780882031, + "y": 1188.0562527913507, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 921.0526315789476, @@ -651,17 +874,32 @@ "seed": 120425598, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "D-aGC4EzCpWCj0Rogvok-", - "Fh1ESg-Q7e1r0U1vqFeKO", - "c28LwM1yzNMHGBRnXBT5z", - "uLumB7sd5oaIkrCNFmc4b" - ] + "boundElements": [ + { + "type": "arrow", + "id": "D-aGC4EzCpWCj0Rogvok-" + }, + { + "type": "arrow", + "id": "Fh1ESg-Q7e1r0U1vqFeKO" + }, + { + "type": "arrow", + "id": "c28LwM1yzNMHGBRnXBT5z" + }, + { + "type": "arrow", + "id": "uLumB7sd5oaIkrCNFmc4b" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "arrow", - "version": 53, - "versionNonce": 1732763151, + "version": 662, + "versionNonce": 325299984, "isDeleted": false, "id": "YVPSmET7TbVynPjvhdUSL", "fillStyle": "hachure", @@ -670,8 +908,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1513.3541419039925, - "y": 321.74470276866515, + "x": 1890.5807044039923, + "y": 1366.0181402686646, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 75.00000000000023, @@ -679,16 +917,19 @@ "seed": 1668503870, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "startBinding": { "elementId": "o0sepXGt4fKDi4Zoqq58T", - "focus": 0.2337591602607686, + "focus": 0.2337591602607645, "gap": 3.0909840092554077 }, "endBinding": { "elementId": "i3BuMRIOBe_19hJI91DfV", - "focus": 0.10944377338792276, - "gap": 3.0669107275864462 + "focus": 0.10944377338791995, + "gap": 3.06691072758656 }, "lastCommittedPoint": null, "startArrowhead": null, @@ -706,8 +947,8 @@ }, { "type": "arrow", - "version": 38, - "versionNonce": 2016939617, + "version": 647, + "versionNonce": 155093488, "isDeleted": false, "id": "tNGtBSw12gQ-BIyd1Duuy", "fillStyle": "hachure", @@ -716,8 +957,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1239.669931377677, - "y": 369.11312382129677, + "x": 1616.8964938776767, + "y": 1413.3865613212963, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 0, @@ -725,16 +966,19 @@ "seed": 933870626, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "startBinding": { "elementId": "o0sepXGt4fKDi4Zoqq58T", - "focus": 0.37009228616233014, + "focus": 0.37009228616233003, "gap": 10.534176452875727 }, "endBinding": { "elementId": "kdqfC4iFcVRtqwUun5Eaf", - "focus": -0.326447035317221, - "gap": 6.518455126071672 + "focus": -0.32644703531722097, + "gap": 6.518455126071558 }, "lastCommittedPoint": null, "startArrowhead": null, @@ -752,8 +996,8 @@ }, { "type": "arrow", - "version": 59, - "versionNonce": 1373586479, + "version": 668, + "versionNonce": 389762320, "isDeleted": false, "id": "nGIZerkX-nuZP5ph84wzq", "fillStyle": "hachure", @@ -762,8 +1006,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1242.5120366408348, - "y": 493.79733434761255, + "x": 1619.7385991408346, + "y": 1538.0707718476121, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 1.3157894736841627, @@ -771,16 +1015,19 @@ "seed": 539290722, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "startBinding": { "elementId": "kdqfC4iFcVRtqwUun5Eaf", "focus": 0.31901143305771457, - "gap": 6.902597505507288 + "gap": 6.902597505507401 }, "endBinding": { "elementId": "4C63a8jstFovCFlcXH1go", - "focus": -0.2640421033158665, - "gap": 7.992139336597916 + "focus": -0.26404210331586636, + "gap": 7.992139336598029 }, "lastCommittedPoint": null, "startArrowhead": null, @@ -798,8 +1045,8 @@ }, { "type": "text", - "version": 101, - "versionNonce": 1785930273, + "version": 354, + "versionNonce": 112708592, "isDeleted": false, "id": "lTFAqafiJIwRv1v0g_TTH", "fillStyle": "hachure", @@ -808,8 +1055,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1727.5721952475321, - "y": 801.6870113315333, + "x": 2104.798757747532, + "y": 1845.960448831533, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 129, @@ -817,18 +1064,23 @@ "seed": 603575678, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Main App DB\n(postgres)", "baseline": 44, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "Main App DB\n(postgres)" }, { "type": "arrow", - "version": 79, - "versionNonce": 1558505039, + "version": 688, + "versionNonce": 980364048, "isDeleted": false, "id": "BixWOkWrvvO6uxXJ6clF8", "fillStyle": "hachure", @@ -837,8 +1089,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1246.507133523623, - "y": 622.6931418247453, + "x": 1623.7336960236228, + "y": 1666.966579324745, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 0.8513780276500711, @@ -846,15 +1098,18 @@ "seed": 166996002, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "startBinding": { "elementId": "4C63a8jstFovCFlcXH1go", - "focus": 0.26451822191232477, + "focus": 0.26460114923910216, "gap": 5.324720772113778 }, "endBinding": { "elementId": "hLWjUmgRGYwTIOFlLzeyz", - "focus": 0.018622150638039077, + "focus": 0.018622150638039712, "gap": 9.006600141006402 }, "lastCommittedPoint": null, @@ -873,8 +1128,8 @@ }, { "type": "text", - "version": 516, - "versionNonce": 1462846575, + "version": 1058, + "versionNonce": 1203293680, "isDeleted": false, "id": "Qu8ToT5gC5xU0TsceRe_n", "fillStyle": "hachure", @@ -883,27 +1138,32 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 2194.0753324452508, + "x": 2095.8331449452508, "y": 65.5264751580787, "strokeColor": "#000000", "backgroundColor": "transparent", - "width": 607, - "height": 69, + "width": 1018, + "height": 67, "seed": 1366075490, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 28, "fontFamily": 3, - "text": "\"worker\" service\n(Sourcegraph repo background workers)", - "baseline": 62, + "text": "\"worker\" service\n(Sourcegraph background workers for adding new insight points)", + "baseline": 61, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "\"worker\" service\n(Sourcegraph background workers for adding new insight points)" }, { "type": "rectangle", - "version": 311, - "versionNonce": 1657191937, + "version": 637, + "versionNonce": 1198999824, "isDeleted": false, "id": "07hepXBHqk1CP7MlvC0h0", "fillStyle": "hachure", @@ -921,17 +1181,32 @@ "seed": 502227234, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "D-aGC4EzCpWCj0Rogvok-", - "Fh1ESg-Q7e1r0U1vqFeKO", - "c28LwM1yzNMHGBRnXBT5z", - "uLumB7sd5oaIkrCNFmc4b" - ] + "boundElements": [ + { + "type": "arrow", + "id": "D-aGC4EzCpWCj0Rogvok-" + }, + { + "type": "arrow", + "id": "Fh1ESg-Q7e1r0U1vqFeKO" + }, + { + "type": "arrow", + "id": "c28LwM1yzNMHGBRnXBT5z" + }, + { + "type": "arrow", + "id": "uLumB7sd5oaIkrCNFmc4b" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "text", - "version": 122, - "versionNonce": 546102927, + "version": 203, + "versionNonce": 88707056, "isDeleted": false, "id": "a4Sa_9u4vpYoBlLTijpct", "fillStyle": "hachure", @@ -949,18 +1224,23 @@ "seed": 424646562, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Insights background jobs", "baseline": 20, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "Insights background jobs" }, { "type": "rectangle", - "version": 85, - "versionNonce": 572610017, + "version": 166, + "versionNonce": 2043389712, "isDeleted": false, "id": "ntr3VWCiimw6A9PGzckXX", "fillStyle": "hachure", @@ -978,18 +1258,36 @@ "seed": 1161727486, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "mhaP-wCza_npDdAEH3IvG", - "ERHFq4qCo7Kg5qfbW2n66", - "V6IYfECn4SRBEW_5vz5ID", - "_xyMe07U-5sD-vOfkTC7F", - "YDHuFA0dR8PsY2YcFjyyR" - ] + "boundElements": [ + { + "type": "arrow", + "id": "mhaP-wCza_npDdAEH3IvG" + }, + { + "type": "arrow", + "id": "ERHFq4qCo7Kg5qfbW2n66" + }, + { + "type": "arrow", + "id": "V6IYfECn4SRBEW_5vz5ID" + }, + { + "type": "arrow", + "id": "_xyMe07U-5sD-vOfkTC7F" + }, + { + "type": "arrow", + "id": "YDHuFA0dR8PsY2YcFjyyR" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "text", - "version": 620, - "versionNonce": 196097199, + "version": 810, + "versionNonce": 393351664, "isDeleted": false, "id": "i6lWta0ioTSJz-lOdbmye", "fillStyle": "hachure", @@ -1007,21 +1305,32 @@ "seed": 2086780534, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "I3rdZPRt-qb8Ir5X0q-IN", - "8uvIH-0nn7R3HY1mBeEqX" + "boundElements": [ + { + "type": "arrow", + "id": "I3rdZPRt-qb8Ir5X0q-IN" + }, + { + "type": "arrow", + "id": "8uvIH-0nn7R3HY1mBeEqX" + } ], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Worker store\n\"query search\" jobs queue", "baseline": 44, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "Worker store\n\"query search\" jobs queue" }, { "type": "rectangle", - "version": 525, - "versionNonce": 1847456193, + "version": 715, + "versionNonce": 1575240976, "isDeleted": false, "id": "WPicCzg2CfEONxhK3bX1s", "fillStyle": "hachure", @@ -1039,28 +1348,76 @@ "seed": 861343350, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "fqTlxX3d6pECd39_ZOe_S", - "Ma7eb2hvlESCmgYDl5DGI", - "PHXjRA4_oe_2BAZpRQD1x", - "1V6bL1fKsDd3QBFVA0vJW", - "GdVfRvlF0-SQWdic8VPZu", - "9nw8DmtkvbslWWWLxlCkj", - "qVc66tRUosZulXFCBHEjV", - "Ul1vF1wkYluxymYg44QR8", - "7KoWPnNM90fiEjqnILQfl", - "XgqYhNgzHTAerlHepV6DQ", - "XR73REv1kPDhg7Pb9M-HF", - "rwlIO8iaZ_vglfFbQEdi0", - "LXI8B1QHLgj_S9O95y30L", - "OOo6noK3Yzq9wAnDi71CN", - "VqIz-hqk1a-AlrPp77Ui3" - ] + "boundElements": [ + { + "type": "arrow", + "id": "fqTlxX3d6pECd39_ZOe_S" + }, + { + "type": "arrow", + "id": "Ma7eb2hvlESCmgYDl5DGI" + }, + { + "type": "arrow", + "id": "PHXjRA4_oe_2BAZpRQD1x" + }, + { + "type": "arrow", + "id": "1V6bL1fKsDd3QBFVA0vJW" + }, + { + "type": "arrow", + "id": "GdVfRvlF0-SQWdic8VPZu" + }, + { + "type": "arrow", + "id": "9nw8DmtkvbslWWWLxlCkj" + }, + { + "type": "arrow", + "id": "qVc66tRUosZulXFCBHEjV" + }, + { + "type": "arrow", + "id": "Ul1vF1wkYluxymYg44QR8" + }, + { + "type": "arrow", + "id": "7KoWPnNM90fiEjqnILQfl" + }, + { + "type": "arrow", + "id": "XgqYhNgzHTAerlHepV6DQ" + }, + { + "type": "arrow", + "id": "XR73REv1kPDhg7Pb9M-HF" + }, + { + "type": "arrow", + "id": "rwlIO8iaZ_vglfFbQEdi0" + }, + { + "type": "arrow", + "id": "LXI8B1QHLgj_S9O95y30L" + }, + { + "type": "arrow", + "id": "OOo6noK3Yzq9wAnDi71CN" + }, + { + "type": "arrow", + "id": "VqIz-hqk1a-AlrPp77Ui3" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "text", - "version": 276, - "versionNonce": 4441807, + "version": 357, + "versionNonce": 356144112, "isDeleted": false, "id": "kdpmEOzV5mlRUj2XGBluk", "fillStyle": "hachure", @@ -1078,18 +1435,23 @@ "seed": 1020795702, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Query Runner Worker\n\"Query & insert search insights\"", "baseline": 44, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "Query Runner Worker\n\"Query & insert search insights\"" }, { "type": "rectangle", - "version": 216, - "versionNonce": 1809584545, + "version": 298, + "versionNonce": 825828112, "isDeleted": false, "id": "x0SUHWIGtnShRFKTALo-Q", "fillStyle": "hachure", @@ -1107,24 +1469,60 @@ "seed": 262598698, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "mhaP-wCza_npDdAEH3IvG", - "GdVfRvlF0-SQWdic8VPZu", - "W5kr90BAxpmq55kuX87XG", - "qVc66tRUosZulXFCBHEjV", - "KaoGpYt85VsZc8ubE7Kb7", - "7KoWPnNM90fiEjqnILQfl", - "XR73REv1kPDhg7Pb9M-HF", - "LXI8B1QHLgj_S9O95y30L", - "_xyMe07U-5sD-vOfkTC7F", - "sZBqICZReQDOlc26vfZUW", - "J6mGAvusPG43whVSuumX5" - ] + "boundElements": [ + { + "type": "arrow", + "id": "mhaP-wCza_npDdAEH3IvG" + }, + { + "type": "arrow", + "id": "GdVfRvlF0-SQWdic8VPZu" + }, + { + "type": "arrow", + "id": "W5kr90BAxpmq55kuX87XG" + }, + { + "type": "arrow", + "id": "qVc66tRUosZulXFCBHEjV" + }, + { + "type": "arrow", + "id": "KaoGpYt85VsZc8ubE7Kb7" + }, + { + "type": "arrow", + "id": "7KoWPnNM90fiEjqnILQfl" + }, + { + "type": "arrow", + "id": "XR73REv1kPDhg7Pb9M-HF" + }, + { + "type": "arrow", + "id": "LXI8B1QHLgj_S9O95y30L" + }, + { + "type": "arrow", + "id": "_xyMe07U-5sD-vOfkTC7F" + }, + { + "type": "arrow", + "id": "sZBqICZReQDOlc26vfZUW" + }, + { + "type": "arrow", + "id": "J6mGAvusPG43whVSuumX5" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "text", - "version": 545, - "versionNonce": 1558189295, + "version": 812, + "versionNonce": 1957320176, "isDeleted": false, "id": "4nKcaXTkrZYwNsE-SxMQx", "fillStyle": "hachure", @@ -1142,26 +1540,52 @@ "seed": 1881168682, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "4VBjbrPO99JhZUjpe1xgi", - "kSII8z_7o6U45f0TTyTpv", - "fqTlxX3d6pECd39_ZOe_S", - "Ma7eb2hvlESCmgYDl5DGI", - "GCm6wrA7MxeN0sdKOAhP5", - "I6oKx_w7WlSKZIOx6BenH", - "8uvIH-0nn7R3HY1mBeEqX" + "boundElements": [ + { + "type": "arrow", + "id": "4VBjbrPO99JhZUjpe1xgi" + }, + { + "type": "arrow", + "id": "kSII8z_7o6U45f0TTyTpv" + }, + { + "type": "arrow", + "id": "fqTlxX3d6pECd39_ZOe_S" + }, + { + "type": "arrow", + "id": "Ma7eb2hvlESCmgYDl5DGI" + }, + { + "type": "arrow", + "id": "GCm6wrA7MxeN0sdKOAhP5" + }, + { + "type": "arrow", + "id": "I6oKx_w7WlSKZIOx6BenH" + }, + { + "type": "arrow", + "id": "8uvIH-0nn7R3HY1mBeEqX" + } ], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Main App DB\n(Postgres)", "baseline": 44, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "Main App DB\n(Postgres)" }, { "type": "diamond", - "version": 638, - "versionNonce": 1888171393, + "version": 905, + "versionNonce": 1111638288, "isDeleted": false, "id": "VjzF4YslQqRvetgkrxY-y", "fillStyle": "hachure", @@ -1179,14 +1603,20 @@ "seed": 1694876790, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "lO2Dx1FYaAgcEmPYoZ2pt" - ] + "boundElements": [ + { + "type": "arrow", + "id": "lO2Dx1FYaAgcEmPYoZ2pt" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "text", - "version": 375, - "versionNonce": 420323087, + "version": 642, + "versionNonce": 717887472, "isDeleted": false, "id": "sAEwGILDzovcvE00infz2", "fillStyle": "hachure", @@ -1204,21 +1634,32 @@ "seed": 712992234, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "BixWOkWrvvO6uxXJ6clF8", - "bDQZmMnNN38SULllNNRUp" + "boundElements": [ + { + "type": "arrow", + "id": "BixWOkWrvvO6uxXJ6clF8" + }, + { + "type": "arrow", + "id": "bDQZmMnNN38SULllNNRUp" + } ], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "codeinsights-db\n(Postgres)", "baseline": 44, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "codeinsights-db\n(Postgres)" }, { "type": "diamond", - "version": 506, - "versionNonce": 2137709921, + "version": 773, + "versionNonce": 1350016784, "isDeleted": false, "id": "joqmZDtmjtpt2R1_IcMlM", "fillStyle": "hachure", @@ -1236,15 +1677,24 @@ "seed": 1275184566, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "AGBda_XPtse3jxYwQpX71", - "3QeYo9KF-zrpaqSvHQfBH" - ] + "boundElements": [ + { + "type": "arrow", + "id": "AGBda_XPtse3jxYwQpX71" + }, + { + "type": "arrow", + "id": "3QeYo9KF-zrpaqSvHQfBH" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "text", - "version": 444, - "versionNonce": 658826543, + "version": 525, + "versionNonce": 1268881904, "isDeleted": false, "id": "sgTPXCv349g7ibPPS5fyP", "fillStyle": "hachure", @@ -1262,18 +1712,23 @@ "seed": 100159210, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Query Runner Job Cleaner\n\"Delete old completed jobs\nfrom DB\"", "baseline": 69, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "Query Runner Job Cleaner\n\"Delete old completed jobs\nfrom DB\"" }, { "type": "rectangle", - "version": 271, - "versionNonce": 130713921, + "version": 354, + "versionNonce": 1817263376, "isDeleted": false, "id": "2fZCJklyGnIMLANt3uzgi", "fillStyle": "hachure", @@ -1291,24 +1746,60 @@ "seed": 799534262, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "mhaP-wCza_npDdAEH3IvG", - "ERHFq4qCo7Kg5qfbW2n66", - "I3rdZPRt-qb8Ir5X0q-IN", - "G-w8oscpqSO5CkGC1KuNW", - "9nw8DmtkvbslWWWLxlCkj", - "qVc66tRUosZulXFCBHEjV", - "oz7FIfR_rVuzraX9JZGcr", - "XgqYhNgzHTAerlHepV6DQ", - "OOo6noK3Yzq9wAnDi71CN", - "YDHuFA0dR8PsY2YcFjyyR", - "wbNfuvB7Xw0hvqavIeDXr" - ] + "boundElements": [ + { + "type": "arrow", + "id": "mhaP-wCza_npDdAEH3IvG" + }, + { + "type": "arrow", + "id": "ERHFq4qCo7Kg5qfbW2n66" + }, + { + "type": "arrow", + "id": "I3rdZPRt-qb8Ir5X0q-IN" + }, + { + "type": "arrow", + "id": "G-w8oscpqSO5CkGC1KuNW" + }, + { + "type": "arrow", + "id": "9nw8DmtkvbslWWWLxlCkj" + }, + { + "type": "arrow", + "id": "qVc66tRUosZulXFCBHEjV" + }, + { + "type": "arrow", + "id": "oz7FIfR_rVuzraX9JZGcr" + }, + { + "type": "arrow", + "id": "XgqYhNgzHTAerlHepV6DQ" + }, + { + "type": "arrow", + "id": "OOo6noK3Yzq9wAnDi71CN" + }, + { + "type": "arrow", + "id": "YDHuFA0dR8PsY2YcFjyyR" + }, + { + "type": "arrow", + "id": "wbNfuvB7Xw0hvqavIeDXr" + } + ], + "updated": 1674060475400, + "link": null, + "locked": false }, { "type": "text", - "version": 1079, - "versionNonce": 384722767, + "version": 1269, + "versionNonce": 708369392, "isDeleted": false, "id": "0CwTtpEfSBr84QQ9_LOs2", "fillStyle": "hachure", @@ -1326,21 +1817,32 @@ "seed": 926607530, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "I6oKx_w7WlSKZIOx6BenH", - "3QeYo9KF-zrpaqSvHQfBH" + "boundElements": [ + { + "type": "arrow", + "id": "I6oKx_w7WlSKZIOx6BenH" + }, + { + "type": "arrow", + "id": "3QeYo9KF-zrpaqSvHQfBH" + } ], + "updated": 1674060475400, + "link": null, + "locked": false, "fontSize": 16, "fontFamily": 3, "text": "Insight Definitions\nenterprise/.../insights/insight_store", "baseline": 35, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "Insight Definitions\nenterprise/.../insights/insight_store" }, { "type": "rectangle", - "version": 983, - "versionNonce": 1661581601, + "version": 1173, + "versionNonce": 1464277776, "isDeleted": false, "id": "-vBdHOqkOh_538xeTIlT3", "fillStyle": "hachure", @@ -1358,21 +1860,48 @@ "seed": 482660086, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "lO2Dx1FYaAgcEmPYoZ2pt", - "U1otXiU_ccINZO9N1RJpX", - "Fh1ESg-Q7e1r0U1vqFeKO", - "4VBjbrPO99JhZUjpe1xgi", - "uYs6SQeUIQFCxFHIEQLSN", - "kSII8z_7o6U45f0TTyTpv", - "Fqt__sgrp_HLOeovUwIJo", - "a_-ggtTwchHITIfYBR7el" - ] + "boundElements": [ + { + "type": "arrow", + "id": "lO2Dx1FYaAgcEmPYoZ2pt" + }, + { + "type": "arrow", + "id": "U1otXiU_ccINZO9N1RJpX" + }, + { + "type": "arrow", + "id": "Fh1ESg-Q7e1r0U1vqFeKO" + }, + { + "type": "arrow", + "id": "4VBjbrPO99JhZUjpe1xgi" + }, + { + "type": "arrow", + "id": "uYs6SQeUIQFCxFHIEQLSN" + }, + { + "type": "arrow", + "id": "kSII8z_7o6U45f0TTyTpv" + }, + { + "type": "arrow", + "id": "Fqt__sgrp_HLOeovUwIJo" + }, + { + "type": "arrow", + "id": "a_-ggtTwchHITIfYBR7el" + } + ], + "updated": 1674060475401, + "link": null, + "locked": false }, { "type": "text", - "version": 272, - "versionNonce": 1986176367, + "version": 462, + "versionNonce": 2023218672, "isDeleted": false, "id": "U-xuZTg_y-KYIwfEsF_nr", "fillStyle": "hachure", @@ -1390,18 +1919,23 @@ "seed": 961178102, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Insights Store", "baseline": 20, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "Insights Store" }, { "type": "text", - "version": 254, - "versionNonce": 311946497, + "version": 444, + "versionNonce": 1674656016, "isDeleted": false, "id": "voqAaouQEhzsZmGTDSOxZ", "fillStyle": "hachure", @@ -1419,18 +1953,23 @@ "seed": 643946602, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "fontSize": 16, "fontFamily": 3, "text": "enterprise/internal/insights/store", "baseline": 16, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "enterprise/internal/insights/store" }, { "type": "rectangle", - "version": 299, - "versionNonce": 1961500559, + "version": 492, + "versionNonce": 1741070320, "isDeleted": false, "id": "XOAb7y2WuZBPt9AI1xHBP", "fillStyle": "hachure", @@ -1448,23 +1987,56 @@ "seed": 1268103990, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "AGBda_XPtse3jxYwQpX71", - "o-u5q_29fU8DTydha1CS6", - "nGIZerkX-nuZP5ph84wzq", - "D-aGC4EzCpWCj0Rogvok-", - "uLumB7sd5oaIkrCNFmc4b", - "BixWOkWrvvO6uxXJ6clF8", - "bDQZmMnNN38SULllNNRUp", - "sZBqICZReQDOlc26vfZUW", - "wbNfuvB7Xw0hvqavIeDXr", - "J6mGAvusPG43whVSuumX5" - ] + "boundElements": [ + { + "type": "arrow", + "id": "AGBda_XPtse3jxYwQpX71" + }, + { + "type": "arrow", + "id": "o-u5q_29fU8DTydha1CS6" + }, + { + "type": "arrow", + "id": "nGIZerkX-nuZP5ph84wzq" + }, + { + "type": "arrow", + "id": "D-aGC4EzCpWCj0Rogvok-" + }, + { + "type": "arrow", + "id": "uLumB7sd5oaIkrCNFmc4b" + }, + { + "type": "arrow", + "id": "BixWOkWrvvO6uxXJ6clF8" + }, + { + "type": "arrow", + "id": "bDQZmMnNN38SULllNNRUp" + }, + { + "type": "arrow", + "id": "sZBqICZReQDOlc26vfZUW" + }, + { + "type": "arrow", + "id": "wbNfuvB7Xw0hvqavIeDXr" + }, + { + "type": "arrow", + "id": "J6mGAvusPG43whVSuumX5" + } + ], + "updated": 1674060475401, + "link": null, + "locked": false }, { "type": "arrow", - "version": 49, - "versionNonce": 1039443169, + "version": 720, + "versionNonce": 1432852240, "isDeleted": false, "id": "bDQZmMnNN38SULllNNRUp", "fillStyle": "hachure", @@ -1482,7 +2054,10 @@ "seed": 357210026, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "startBinding": { "elementId": "XOAb7y2WuZBPt9AI1xHBP", "focus": -0.02344558207887218, @@ -1509,8 +2084,8 @@ }, { "type": "text", - "version": 246, - "versionNonce": 1890541761, + "version": 368, + "versionNonce": 1204625904, "isDeleted": false, "id": "iedF9L0oQbUU8xdSKc9vB", "fillStyle": "hachure", @@ -1528,18 +2103,23 @@ "seed": 732096490, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Insight Enqueuer\n\"fetch insights, enqueue recording\"", "baseline": 44, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "Insight Enqueuer\n\"fetch insights, enqueue recording\"" }, { "type": "rectangle", - "version": 89, - "versionNonce": 1023723471, + "version": 200, + "versionNonce": 1082275088, "isDeleted": false, "id": "LL-KdkEpOKVDfNijJwADK", "fillStyle": "hachure", @@ -1557,21 +2137,48 @@ "seed": 679151530, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "BApdiDfEZe8VsIfIMR2Yi", - "f5HDastmpUENAnot5K4fZ", - "hIZb811VPQ5ChwLpY0vw9", - "3ugtdQsVT5c2_xsDhAdOP", - "Ul1vF1wkYluxymYg44QR8", - "a_-ggtTwchHITIfYBR7el", - "rwlIO8iaZ_vglfFbQEdi0", - "V6IYfECn4SRBEW_5vz5ID" - ] + "boundElements": [ + { + "type": "arrow", + "id": "BApdiDfEZe8VsIfIMR2Yi" + }, + { + "type": "arrow", + "id": "f5HDastmpUENAnot5K4fZ" + }, + { + "type": "arrow", + "id": "hIZb811VPQ5ChwLpY0vw9" + }, + { + "type": "arrow", + "id": "3ugtdQsVT5c2_xsDhAdOP" + }, + { + "type": "arrow", + "id": "Ul1vF1wkYluxymYg44QR8" + }, + { + "type": "arrow", + "id": "a_-ggtTwchHITIfYBR7el" + }, + { + "type": "arrow", + "id": "rwlIO8iaZ_vglfFbQEdi0" + }, + { + "type": "arrow", + "id": "V6IYfECn4SRBEW_5vz5ID" + } + ], + "updated": 1674060475401, + "link": null, + "locked": false }, { "type": "text", - "version": 89, - "versionNonce": 409444513, + "version": 170, + "versionNonce": 666067952, "isDeleted": false, "id": "LdttnxCoip4n34qBzz03y", "fillStyle": "hachure", @@ -1589,18 +2196,23 @@ "seed": 1566202870, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Frontend GraphQL Search API", "baseline": 20, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "Frontend GraphQL Search API" }, { "type": "rectangle", - "version": 39, - "versionNonce": 1004074479, + "version": 120, + "versionNonce": 1788416784, "isDeleted": false, "id": "UsQ5uQ49qy6i71GuOt7H8", "fillStyle": "hachure", @@ -1618,15 +2230,24 @@ "seed": 1374951850, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "UWa-1pvCQKNHqHGo0W5Ay", - "KaoGpYt85VsZc8ubE7Kb7" - ] + "boundElements": [ + { + "type": "arrow", + "id": "UWa-1pvCQKNHqHGo0W5Ay" + }, + { + "type": "arrow", + "id": "KaoGpYt85VsZc8ubE7Kb7" + } + ], + "updated": 1674060475401, + "link": null, + "locked": false }, { "type": "arrow", - "version": 83, - "versionNonce": 1120724097, + "version": 164, + "versionNonce": 1971414512, "isDeleted": false, "id": "KaoGpYt85VsZc8ubE7Kb7", "fillStyle": "hachure", @@ -1644,7 +2265,10 @@ "seed": 508546870, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "startBinding": { "elementId": "x0SUHWIGtnShRFKTALo-Q", "focus": -0.2262148575684279, @@ -1679,8 +2303,8 @@ }, { "type": "arrow", - "version": 137, - "versionNonce": 1212309519, + "version": 357, + "versionNonce": 707014928, "isDeleted": false, "id": "a_-ggtTwchHITIfYBR7el", "fillStyle": "hachure", @@ -1698,7 +2322,10 @@ "seed": 632304950, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "startBinding": { "elementId": "LL-KdkEpOKVDfNijJwADK", "focus": 0.3505169668180509, @@ -1733,8 +2360,8 @@ }, { "type": "arrow", - "version": 237, - "versionNonce": 463383649, + "version": 660, + "versionNonce": 507455472, "isDeleted": false, "id": "rwlIO8iaZ_vglfFbQEdi0", "fillStyle": "hachure", @@ -1752,7 +2379,10 @@ "seed": 2088396074, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "startBinding": { "elementId": "LL-KdkEpOKVDfNijJwADK", "focus": -0.7916666666666473, @@ -1803,8 +2433,8 @@ }, { "type": "arrow", - "version": 259, - "versionNonce": 1949926959, + "version": 449, + "versionNonce": 1184599824, "isDeleted": false, "id": "LXI8B1QHLgj_S9O95y30L", "fillStyle": "hachure", @@ -1822,7 +2452,10 @@ "seed": 696211702, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "startBinding": { "elementId": "x0SUHWIGtnShRFKTALo-Q", "focus": 0.41640552379786555, @@ -1877,8 +2510,8 @@ }, { "type": "arrow", - "version": 277, - "versionNonce": 715946049, + "version": 521, + "versionNonce": 771009008, "isDeleted": false, "id": "OOo6noK3Yzq9wAnDi71CN", "fillStyle": "hachure", @@ -1896,7 +2529,10 @@ "seed": 1658537642, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "startBinding": { "elementId": "2fZCJklyGnIMLANt3uzgi", "gap": 2.638888888882169, @@ -1947,8 +2583,8 @@ }, { "type": "arrow", - "version": 43, - "versionNonce": 2124867663, + "version": 714, + "versionNonce": 1484205328, "isDeleted": false, "id": "8uvIH-0nn7R3HY1mBeEqX", "fillStyle": "hachure", @@ -1966,7 +2602,10 @@ "seed": 246808682, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "startBinding": { "elementId": "i6lWta0ioTSJz-lOdbmye", "focus": 0.736066090401604, @@ -1993,8 +2632,8 @@ }, { "type": "arrow", - "version": 37, - "versionNonce": 1372964897, + "version": 148, + "versionNonce": 1873406960, "isDeleted": false, "id": "V6IYfECn4SRBEW_5vz5ID", "fillStyle": "hachure", @@ -2012,7 +2651,10 @@ "seed": 502894710, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "startBinding": { "elementId": "ntr3VWCiimw6A9PGzckXX", "focus": 0.7305649796638967, @@ -2043,8 +2685,8 @@ }, { "type": "arrow", - "version": 62, - "versionNonce": 895682159, + "version": 143, + "versionNonce": 690948880, "isDeleted": false, "id": "_xyMe07U-5sD-vOfkTC7F", "fillStyle": "hachure", @@ -2062,7 +2704,10 @@ "seed": 427801398, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "startBinding": { "elementId": "ntr3VWCiimw6A9PGzckXX", "focus": 0.7435754878434424, @@ -2093,8 +2738,8 @@ }, { "type": "arrow", - "version": 72, - "versionNonce": 185211905, + "version": 207, + "versionNonce": 224849392, "isDeleted": false, "id": "YDHuFA0dR8PsY2YcFjyyR", "fillStyle": "hachure", @@ -2112,7 +2757,10 @@ "seed": 269268330, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "startBinding": { "elementId": "ntr3VWCiimw6A9PGzckXX", "focus": 0.7467782688878561, @@ -2143,8 +2791,8 @@ }, { "type": "arrow", - "version": 223, - "versionNonce": 389439631, + "version": 1426, + "versionNonce": 322644240, "isDeleted": false, "id": "wbNfuvB7Xw0hvqavIeDXr", "fillStyle": "hachure", @@ -2162,7 +2810,10 @@ "seed": 2125428586, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "startBinding": { "elementId": "2fZCJklyGnIMLANt3uzgi", "gap": 1.2666666666666673, @@ -2209,8 +2860,8 @@ }, { "type": "arrow", - "version": 226, - "versionNonce": 1716057057, + "version": 1326, + "versionNonce": 808693744, "isDeleted": false, "id": "J6mGAvusPG43whVSuumX5", "fillStyle": "hachure", @@ -2228,7 +2879,10 @@ "seed": 1666715242, "groupIds": [], "strokeSharpness": "round", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "startBinding": { "elementId": "x0SUHWIGtnShRFKTALo-Q", "focus": 0.6192198451275001, @@ -2275,8 +2929,8 @@ }, { "type": "text", - "version": 303, - "versionNonce": 1928004705, + "version": 556, + "versionNonce": 1645446928, "isDeleted": false, "id": "LMQFsiOCpQ9wJLaPxI9te", "fillStyle": "hachure", @@ -2285,8 +2939,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1608.2105263157896, - "y": 452.5263157894737, + "x": 1985.4370888157894, + "y": 1496.7997532894733, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 347, @@ -2294,50 +2948,53 @@ "seed": 73571535, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "ZHgi360-kQlat5JRQ3CI3", - "oZ-FYC8NS_zJEK1zcW_sh" + "boundElements": [ + { + "type": "arrow", + "id": "ZHgi360-kQlat5JRQ3CI3" + }, + { + "type": "arrow", + "id": "oZ-FYC8NS_zJEK1zcW_sh" + } ], + "updated": 1674060475401, + "link": null, + "locked": false, "fontSize": 16, "fontFamily": 3, "text": "enterprise/.../insights/insight_store", "baseline": 16, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "enterprise/.../insights/insight_store" }, { - "id": "3QeYo9KF-zrpaqSvHQfBH", "type": "arrow", - "x": 2700.2016772645006, - "y": 765.5263157894736, - "width": 375.8233688836085, - "height": 134.66778206510264, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", + "version": 769, + "versionNonce": 538456560, + "isDeleted": false, + "id": "3QeYo9KF-zrpaqSvHQfBH", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 2700.2016772645006, + "y": 765.5263157894736, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 375.8233688836085, + "height": 134.66778206510264, + "seed": 1479586145, "groupIds": [], "strokeSharpness": "round", - "seed": 1479586145, - "version": 98, - "versionNonce": 355624815, - "isDeleted": false, - "boundElementIds": null, - "points": [ - [ - 0, - 0 - ], - [ - -375.8233688836085, - 134.66778206510264 - ] - ], - "lastCommittedPoint": null, + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "startBinding": { "elementId": "0CwTtpEfSBr84QQ9_LOs2", "focus": -0.5253485772956793, @@ -2348,13 +3005,24 @@ "focus": -0.18230315781637402, "gap": 10.227898064721686 }, + "lastCommittedPoint": null, "startArrowhead": null, - "endArrowhead": "arrow" + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -375.8233688836085, + 134.66778206510264 + ] + ] }, { "type": "rectangle", - "version": 231, - "versionNonce": 1528646401, + "version": 312, + "versionNonce": 842016016, "isDeleted": false, "id": "41d78JqMSz0n43e37qNk1", "fillStyle": "hachure", @@ -2372,112 +3040,153 @@ "seed": 844136545, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "mhaP-wCza_npDdAEH3IvG", - "GdVfRvlF0-SQWdic8VPZu", - "W5kr90BAxpmq55kuX87XG", - "qVc66tRUosZulXFCBHEjV", - "KaoGpYt85VsZc8ubE7Kb7", - "7KoWPnNM90fiEjqnILQfl", - "XR73REv1kPDhg7Pb9M-HF", - "LXI8B1QHLgj_S9O95y30L", - "_xyMe07U-5sD-vOfkTC7F", - "sZBqICZReQDOlc26vfZUW", - "J6mGAvusPG43whVSuumX5", - "VqIz-hqk1a-AlrPp77Ui3" - ] + "boundElements": [ + { + "type": "arrow", + "id": "mhaP-wCza_npDdAEH3IvG" + }, + { + "type": "arrow", + "id": "GdVfRvlF0-SQWdic8VPZu" + }, + { + "type": "arrow", + "id": "W5kr90BAxpmq55kuX87XG" + }, + { + "type": "arrow", + "id": "qVc66tRUosZulXFCBHEjV" + }, + { + "type": "arrow", + "id": "KaoGpYt85VsZc8ubE7Kb7" + }, + { + "type": "arrow", + "id": "7KoWPnNM90fiEjqnILQfl" + }, + { + "type": "arrow", + "id": "XR73REv1kPDhg7Pb9M-HF" + }, + { + "type": "arrow", + "id": "LXI8B1QHLgj_S9O95y30L" + }, + { + "type": "arrow", + "id": "_xyMe07U-5sD-vOfkTC7F" + }, + { + "type": "arrow", + "id": "sZBqICZReQDOlc26vfZUW" + }, + { + "type": "arrow", + "id": "J6mGAvusPG43whVSuumX5" + }, + { + "type": "arrow", + "id": "VqIz-hqk1a-AlrPp77Ui3" + } + ], + "updated": 1674060475401, + "link": null, + "locked": false }, { - "id": "tH6XvXiV-gMU9respPoyF", "type": "text", - "x": 2924.710526315789, - "y": 376.52631578947364, - "width": 176, - "height": 25, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", + "version": 128, + "versionNonce": 1679586288, + "isDeleted": false, + "id": "tH6XvXiV-gMU9respPoyF", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 2924.710526315789, + "y": 376.52631578947364, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 176, + "height": 25, + "seed": 1562549281, "groupIds": [], "strokeSharpness": "sharp", - "seed": 1562549281, - "version": 47, - "versionNonce": 1752277391, - "isDeleted": false, - "boundElementIds": null, - "text": "Worker Resetter", + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, + "text": "Worker Resetter", + "baseline": 20, "textAlign": "center", "verticalAlign": "middle", - "baseline": 20 + "containerId": null, + "originalText": "Worker Resetter" }, { - "id": "pPAeG8AaTeCXtJWdlnoRv", "type": "text", - "x": 2914.7105263157896, - "y": 407.52631578947364, - "width": 188, - "height": 20, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", + "version": 114, + "versionNonce": 1359545104, + "isDeleted": false, + "id": "pPAeG8AaTeCXtJWdlnoRv", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 2914.7105263157896, + "y": 407.52631578947364, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 188, + "height": 20, + "seed": 938733377, "groupIds": [], "strokeSharpness": "sharp", - "seed": 938733377, - "version": 33, - "versionNonce": 1243497185, - "isDeleted": false, - "boundElementIds": null, - "text": "\"reset stalled jobs\"", + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "fontSize": 16, "fontFamily": 3, + "text": "\"reset stalled jobs\"", + "baseline": 16, "textAlign": "left", "verticalAlign": "top", - "baseline": 16 + "containerId": null, + "originalText": "\"reset stalled jobs\"" }, { - "id": "VqIz-hqk1a-AlrPp77Ui3", "type": "arrow", - "x": 3015.7105263157896, - "y": 449.52631578947364, - "width": 23, - "height": 241, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", + "version": 312, + "versionNonce": 1648915952, + "isDeleted": false, + "id": "VqIz-hqk1a-AlrPp77Ui3", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 3015.7105263157896, + "y": 449.52631578947364, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 23, + "height": 241, + "seed": 1897476353, "groupIds": [], "strokeSharpness": "round", - "seed": 1897476353, - "version": 13, - "versionNonce": 537171887, - "isDeleted": false, - "boundElementIds": null, - "points": [ - [ - 0, - 0 - ], - [ - 23, - 241 - ] - ], - "lastCommittedPoint": null, + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "startBinding": { "elementId": "41d78JqMSz0n43e37qNk1", "focus": -0.003525091799267877, @@ -2488,13 +3197,24 @@ "focus": -0.004356711496293035, "gap": 10.305949748058026 }, + "lastCommittedPoint": null, "startArrowhead": null, - "endArrowhead": "arrow" + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 23, + 241 + ] + ] }, { "type": "diamond", - "version": 489, - "versionNonce": 1235094721, + "version": 742, + "versionNonce": 487989520, "isDeleted": false, "id": "KZkPAMPD04wahLpOne-zO", "fillStyle": "hachure", @@ -2503,8 +3223,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1681.055764411028, - "y": 711.9548872180453, + "x": 2058.2823269110277, + "y": 1756.2283247180449, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 229, @@ -2512,47 +3232,70 @@ "seed": 298940929, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "AGBda_XPtse3jxYwQpX71", - "ZHgi360-kQlat5JRQ3CI3", - "VJg36Parv4prCCSHUZc3U", - "JyzJ3HAKOXhABckvRFBtL", - "FDehLZll3rq8RPSgomCxq" - ] + "boundElements": [ + { + "type": "arrow", + "id": "AGBda_XPtse3jxYwQpX71" + }, + { + "type": "arrow", + "id": "ZHgi360-kQlat5JRQ3CI3" + }, + { + "type": "arrow", + "id": "VJg36Parv4prCCSHUZc3U" + }, + { + "type": "arrow", + "id": "JyzJ3HAKOXhABckvRFBtL" + }, + { + "type": "arrow", + "id": "FDehLZll3rq8RPSgomCxq" + } + ], + "updated": 1674060475401, + "link": null, + "locked": false }, { - "id": "xyNQXKOEegL0q3hETPZko", "type": "text", - "x": 1694.4446532999164, - "y": 584.2326649958231, - "width": 244, - "height": 39, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", + "version": 350, + "versionNonce": 2141468656, + "isDeleted": false, + "id": "xyNQXKOEegL0q3hETPZko", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 2071.6712157999164, + "y": 1628.5061024958227, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 244, + "height": 39, + "seed": 314310127, "groupIds": [], "strokeSharpness": "sharp", - "seed": 314310127, - "version": 97, - "versionNonce": 1465552257, - "isDeleted": false, - "boundElementIds": null, - "text": "Repository Permission Data\n", + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "fontSize": 16, "fontFamily": 3, + "text": "Repository Permission Data\n", + "baseline": 35, "textAlign": "left", "verticalAlign": "top", - "baseline": 35 + "containerId": null, + "originalText": "Repository Permission Data\n" }, { "type": "rectangle", - "version": 324, - "versionNonce": 2135413601, + "version": 577, + "versionNonce": 1708097296, "isDeleted": false, "id": "ODHl6PtX7vkcDHs6BaZkj", "fillStyle": "hachure", @@ -2561,8 +3304,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1649.8335421888055, - "y": 533.3437761069341, + "x": 2027.0601046888053, + "y": 1577.6172136069338, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 347, @@ -2570,22 +3313,52 @@ "seed": 728050959, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [ - "AGBda_XPtse3jxYwQpX71", - "o-u5q_29fU8DTydha1CS6", - "nGIZerkX-nuZP5ph84wzq", - "D-aGC4EzCpWCj0Rogvok-", - "uLumB7sd5oaIkrCNFmc4b", - "BixWOkWrvvO6uxXJ6clF8", - "JyzJ3HAKOXhABckvRFBtL", - "FDehLZll3rq8RPSgomCxq", - "dhppvnEuhCmfrBwwBpg1_" - ] + "boundElements": [ + { + "type": "arrow", + "id": "AGBda_XPtse3jxYwQpX71" + }, + { + "type": "arrow", + "id": "o-u5q_29fU8DTydha1CS6" + }, + { + "type": "arrow", + "id": "nGIZerkX-nuZP5ph84wzq" + }, + { + "type": "arrow", + "id": "D-aGC4EzCpWCj0Rogvok-" + }, + { + "type": "arrow", + "id": "uLumB7sd5oaIkrCNFmc4b" + }, + { + "type": "arrow", + "id": "BixWOkWrvvO6uxXJ6clF8" + }, + { + "type": "arrow", + "id": "JyzJ3HAKOXhABckvRFBtL" + }, + { + "type": "arrow", + "id": "FDehLZll3rq8RPSgomCxq" + }, + { + "type": "arrow", + "id": "dhppvnEuhCmfrBwwBpg1_" + } + ], + "updated": 1674060475401, + "link": null, + "locked": false }, { "type": "text", - "version": 328, - "versionNonce": 1420026177, + "version": 581, + "versionNonce": 1077393904, "isDeleted": false, "id": "fVNr_tIXO0AXlEnnb8Rys", "fillStyle": "hachure", @@ -2594,8 +3367,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1723.166875522139, - "y": 547.2882205513785, + "x": 2100.393438022139, + "y": 1591.5616580513781, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 187, @@ -2603,155 +3376,2313 @@ "seed": 2103379439, "groupIds": [], "strokeSharpness": "sharp", - "boundElementIds": [], + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, "fontSize": 20, "fontFamily": 3, "text": "InsightPermStore", "baseline": 20, "textAlign": "center", - "verticalAlign": "top" + "verticalAlign": "top", + "containerId": null, + "originalText": "InsightPermStore" }, { - "id": "FDehLZll3rq8RPSgomCxq", "type": "arrow", - "x": 1801.9182249381884, - "y": 618.3437761069341, - "width": 7.340072255148243, - "height": 93.1745092767261, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", + "version": 694, + "versionNonce": 1269636368, + "isDeleted": false, + "id": "FDehLZll3rq8RPSgomCxq", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 2179.1447874381884, + "y": 1662.6172136069338, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 7.340072255148243, + "height": 93.1745092767261, + "seed": 2016741729, "groupIds": [], "strokeSharpness": "round", - "seed": 2016741729, - "version": 85, - "versionNonce": 2063077665, - "isDeleted": false, - "boundElementIds": null, - "points": [ - [ - 0, + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, + "startBinding": { + "elementId": "ODHl6PtX7vkcDHs6BaZkj", + "focus": 0.10196263268821996, + "gap": 1 + }, + "endBinding": { + "elementId": "KZkPAMPD04wahLpOne-zO", + "focus": -0.08761616840218722, + "gap": 1 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -7.340072255148243, + 93.1745092767261 + ] + ] + }, + { + "type": "arrow", + "version": 620, + "versionNonce": 1728773104, + "isDeleted": false, + "id": "oZ-FYC8NS_zJEK1zcW_sh", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1991.6712157999166, + "y": 1530.7283247180449, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 272.2222222222222, + "height": 252.22222222222229, + "seed": 1257981185, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, + "startBinding": { + "elementId": "LMQFsiOCpQ9wJLaPxI9te", + "focus": 0.7674730945655628, + "gap": 13.928571428571558 + }, + "endBinding": { + "elementId": "F4xor4iMUYwNk1eWiE96P", + "focus": 0.3219316866776875, + "gap": 24.145997421380784 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -272.2222222222222, + 252.22222222222229 + ] + ] + }, + { + "type": "arrow", + "version": 620, + "versionNonce": 539425552, + "isDeleted": false, + "id": "dhppvnEuhCmfrBwwBpg1_", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1855.00454913325, + "y": 1626.2838802736005, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 165.55555555555543, + "height": 3.3333333333333712, + "seed": 1407983649, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1674060475401, + "link": null, + "locked": false, + "startBinding": { + "elementId": "4C63a8jstFovCFlcXH1go", + "focus": 0.2281393471631258, + "gap": 12.093776106934229 + }, + "endBinding": { + "elementId": "ODHl6PtX7vkcDHs6BaZkj", + "focus": 0.006392761427066181, + "gap": 6.499999999999659 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 165.55555555555543, + -3.3333333333333712 + ] + ] + }, + { + "type": "text", + "version": 1204, + "versionNonce": 1835582224, + "isDeleted": false, + "id": "RkS2oIb4gp3AB98NPePbn", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 881.4483735380118, + "y": 62.350022195845554, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 969, + "height": 67, + "seed": 337935856, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "fontSize": 28, + "fontFamily": 3, + "text": "\"worker\" service\n(Sourcegraph background workers for backfilling an insight)", + "baseline": 61, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "\"worker\" service\n(Sourcegraph background workers for backfilling an insight)" + }, + { + "type": "rectangle", + "version": 823, + "versionNonce": 214218224, + "isDeleted": false, + "id": "kkGW3TZK4mN3uGB_rpvpb", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 820.3864674707602, + "y": 142.14393225835806, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 1182.1637426900588, + "height": 673.301068763864, + "seed": 215914768, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "D-aGC4EzCpWCj0Rogvok-" + }, + { + "type": "arrow", + "id": "Fh1ESg-Q7e1r0U1vqFeKO" + }, + { + "type": "arrow", + "id": "c28LwM1yzNMHGBRnXBT5z" + }, + { + "type": "arrow", + "id": "uLumB7sd5oaIkrCNFmc4b" + } + ], + "updated": 1674060475402, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 332, + "versionNonce": 394125584, + "isDeleted": false, + "id": "tCZUyAN616qNab2apNTTs", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 854.7461165935665, + "y": 174.90557775140104, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 281, + "height": 25, + "seed": 683622384, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "Insights background jobs", + "baseline": 20, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Insights background jobs" + }, + { + "type": "rectangle", + "version": 299, + "versionNonce": 278427632, + "isDeleted": false, + "id": "SnOr5K9X_GNkGJqk90AaR", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 837.8572277046778, + "y": 159.7389110847344, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 313.88888888888914, + "height": 58.333333333333314, + "seed": 902362896, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "mhaP-wCza_npDdAEH3IvG" + }, + { + "type": "arrow", + "id": "ERHFq4qCo7Kg5qfbW2n66" + }, + { + "id": "2XlU_bX6zpwZoAAkJ03c7", + "type": "arrow" + }, + { + "id": "u9IXq-3dWfjfm75lm_S7l", + "type": "arrow" + }, + { + "id": "j2ETpxP6_pZBcF0FZ99Jq", + "type": "arrow" + } + ], + "updated": 1674060475402, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1003, + "versionNonce": 1220519696, + "isDeleted": false, + "id": "SfhT7kzIGVFRUz-_Gzkn_", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1732.8767493931146, + "y": 712.6558125752986, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 247, + "height": 48, + "seed": 548520432, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "I3rdZPRt-qb8Ir5X0q-IN" + }, + { + "id": "nlAp7H7RAmimAtDn19Vv9", + "type": "arrow" + }, + { + "id": "0jRdw5gg3Lea6ZI92PmNS", + "type": "arrow" + } + ], + "updated": 1674060706872, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "Worker store\n\"backfill\" jobs queue", + "baseline": 44, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Worker store\n\"backfill\" jobs queue" + }, + { + "type": "rectangle", + "version": 890, + "versionNonce": 681834992, + "isDeleted": false, + "id": "8K_4LINCvAB5tlBmBTfUD", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1720.8724091153372, + "y": 697.6558125752986, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 264.78211805555566, + "height": 80.27777777777771, + "seed": 1303876880, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "fqTlxX3d6pECd39_ZOe_S" + }, + { + "type": "arrow", + "id": "Ma7eb2hvlESCmgYDl5DGI" + }, + { + "type": "arrow", + "id": "PHXjRA4_oe_2BAZpRQD1x" + }, + { + "type": "arrow", + "id": "1V6bL1fKsDd3QBFVA0vJW" + }, + { + "type": "arrow", + "id": "GdVfRvlF0-SQWdic8VPZu" + }, + { + "type": "arrow", + "id": "9nw8DmtkvbslWWWLxlCkj" + }, + { + "type": "arrow", + "id": "qVc66tRUosZulXFCBHEjV" + }, + { + "type": "arrow", + "id": "Ul1vF1wkYluxymYg44QR8" + }, + { + "type": "arrow", + "id": "7KoWPnNM90fiEjqnILQfl" + }, + { + "type": "arrow", + "id": "XgqYhNgzHTAerlHepV6DQ" + }, + { + "type": "arrow", + "id": "XR73REv1kPDhg7Pb9M-HF" + }, + { + "id": "YDdWMJVVYCOcsX41N81II", + "type": "arrow" + }, + { + "id": "nVGVD1DtKacrq2BkylSd9", + "type": "arrow" + }, + { + "id": "iFK0huMD6KlzUhDdBld3d", + "type": "arrow" + }, + { + "id": "wgpG01hWl0JBGdlw8wPvd", + "type": "arrow" + }, + { + "id": "axWo3cC8VZgHVFTzLks0t", + "type": "arrow" + }, + { + "id": "y55MwITQpqrxzFBU-9SNI", + "type": "arrow" + }, + { + "id": "M5U6xKxJP3AcAwI0vu9oo", + "type": "arrow" + }, + { + "id": "0jRdw5gg3Lea6ZI92PmNS", + "type": "arrow" + }, + { + "id": "Bi0Y-qO3exeB83sxvNTqO", + "type": "arrow" + } + ], + "updated": 1674060475402, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 584, + "versionNonce": 522509584, + "isDeleted": false, + "id": "ux6JHmMH0XqSu2rhWqrJq", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 918.2291799486702, + "y": 462.54643757529834, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 388, + "height": 48, + "seed": 990118896, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "In Progress Backfill\n\"Query & insert results per repo\"", + "baseline": 44, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "In Progress Backfill\n\"Query & insert results per repo\"" + }, + { + "type": "rectangle", + "version": 502, + "versionNonce": 944259568, + "isDeleted": false, + "id": "XH76bht8oXQKGLEBQJgBf", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 906.4661591153372, + "y": 451.544701464187, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 393.05555555555566, + "height": 75, + "seed": 1780642576, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "mhaP-wCza_npDdAEH3IvG" + }, + { + "type": "arrow", + "id": "GdVfRvlF0-SQWdic8VPZu" + }, + { + "type": "arrow", + "id": "W5kr90BAxpmq55kuX87XG" + }, + { + "type": "arrow", + "id": "qVc66tRUosZulXFCBHEjV" + }, + { + "id": "pEX-swbMWpZ1IT0oEoIO0", + "type": "arrow" + }, + { + "type": "arrow", + "id": "7KoWPnNM90fiEjqnILQfl" + }, + { + "type": "arrow", + "id": "XR73REv1kPDhg7Pb9M-HF" + }, + { + "id": "nVGVD1DtKacrq2BkylSd9", + "type": "arrow" + }, + { + "id": "2XlU_bX6zpwZoAAkJ03c7", + "type": "arrow" + }, + { + "type": "arrow", + "id": "sZBqICZReQDOlc26vfZUW" + }, + { + "id": "zqlFEuuOVBVWb6TYnEhgm", + "type": "arrow" + }, + { + "id": "eg79mS17we_ZWTaATvOtb", + "type": "arrow" + } + ], + "updated": 1674060555599, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 872, + "versionNonce": 1849481488, + "isDeleted": false, + "id": "53LgwySUEXhvhBZLpu0iw", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1323.448444306713, + "y": 916.4682343831018, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 176, + "height": 49, + "seed": 1307035632, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "BixWOkWrvvO6uxXJ6clF8" + }, + { + "id": "aUh2D18gp2RlzVGV2xhcq", + "type": "arrow" + } + ], + "updated": 1674060475402, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "codeinsights-db\n(Postgres)", + "baseline": 44, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "codeinsights-db\n(Postgres)" + }, + { + "type": "diamond", + "version": 1007, + "versionNonce": 369615856, + "isDeleted": false, + "id": "udT3ExNG6f7gDPxeKgjhx", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1296.948444306713, + "y": 828.2338593831018, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 229, + "height": 229, + "seed": 994897680, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "AGBda_XPtse3jxYwQpX71" + }, + { + "id": "C2Fpu-CDw4s99m9heftdn", + "type": "arrow" + }, + { + "id": "nlAp7H7RAmimAtDn19Vv9", + "type": "arrow" + }, + { + "id": "aUh2D18gp2RlzVGV2xhcq", + "type": "arrow" + }, + { + "id": "5fkP6yokpWdna-XxM8lgd", + "type": "arrow" + }, + { + "id": "CjSUV0RGa6grxlOHJ2_ur", + "type": "arrow" + } + ], + "updated": 1674060711904, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1621, + "versionNonce": 478532880, + "isDeleted": false, + "id": "65EQwUObkhEz8WLF4Dd6G", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1437.5547007820069, + "y": 714.2989503676959, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 223, + "height": 48, + "seed": 95063024, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "I6oKx_w7WlSKZIOx6BenH" + }, + { + "id": "C2Fpu-CDw4s99m9heftdn", + "type": "arrow" + } + ], + "updated": 1674060475402, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "Insight Definitions\ninsight_store", + "baseline": 44, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Insight Definitions\ninsight_store" + }, + { + "type": "rectangle", + "version": 1491, + "versionNonce": 718865680, + "isDeleted": false, + "id": "efZG_NXhpdioWSiVzm4sU", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1413.1172007820069, + "y": 693.3848878676959, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 271.2187499999999, + "height": 86.00000000000003, + "seed": 1762159376, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "lO2Dx1FYaAgcEmPYoZ2pt" + }, + { + "type": "arrow", + "id": "U1otXiU_ccINZO9N1RJpX" + }, + { + "type": "arrow", + "id": "Fh1ESg-Q7e1r0U1vqFeKO" + }, + { + "type": "arrow", + "id": "4VBjbrPO99JhZUjpe1xgi" + }, + { + "type": "arrow", + "id": "uYs6SQeUIQFCxFHIEQLSN" + }, + { + "type": "arrow", + "id": "kSII8z_7o6U45f0TTyTpv" + }, + { + "type": "arrow", + "id": "Fqt__sgrp_HLOeovUwIJo" + }, + { + "id": "C2Fpu-CDw4s99m9heftdn", + "type": "arrow" + }, + { + "id": "6Y0ZoiREkUiAQDNeKcOi-", + "type": "arrow" + }, + { + "id": "SsKjOhIS5YFSATKVbdhkk", + "type": "arrow" + } + ], + "updated": 1674060507783, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 716, + "versionNonce": 249798640, + "isDeleted": false, + "id": "akya_N2AW0P2sYMULyVBD", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 891.8211938375607, + "y": 719.3780347975202, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 165, + "height": 24, + "seed": 1515425264, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1674060724156, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "Insights Store", + "baseline": 20, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Insights Store" + }, + { + "type": "rectangle", + "version": 751, + "versionNonce": 657020176, + "isDeleted": false, + "id": "y7q0tjwR8LxLuWU7KzCG4", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 856.3368188375607, + "y": 690.0889722975202, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 238.03125, + "height": 84, + "seed": 1988906992, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "AGBda_XPtse3jxYwQpX71" + }, + { + "type": "arrow", + "id": "o-u5q_29fU8DTydha1CS6" + }, + { + "type": "arrow", + "id": "nGIZerkX-nuZP5ph84wzq" + }, + { + "type": "arrow", + "id": "D-aGC4EzCpWCj0Rogvok-" + }, + { + "type": "arrow", + "id": "uLumB7sd5oaIkrCNFmc4b" + }, + { + "type": "arrow", + "id": "BixWOkWrvvO6uxXJ6clF8" + }, + { + "id": "aUh2D18gp2RlzVGV2xhcq", + "type": "arrow" + }, + { + "type": "arrow", + "id": "sZBqICZReQDOlc26vfZUW" + }, + { + "id": "dcvk4l8lJxpGUyJwAuWzd", + "type": "arrow" + }, + { + "id": "zqlFEuuOVBVWb6TYnEhgm", + "type": "arrow" + }, + { + "id": "CjSUV0RGa6grxlOHJ2_ur", + "type": "arrow" + } + ], + "updated": 1674060719724, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 804, + "versionNonce": 128813040, + "isDeleted": false, + "id": "aaeVRvNce-RfksoFbgdh8", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 956.2699785597824, + "y": 318.87543063085354, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 388, + "height": 48, + "seed": 1381814768, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "j2ETpxP6_pZBcF0FZ99Jq", + "type": "arrow" + } + ], + "updated": 1674060475402, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "Series New Backfill\n\"determine repos & estimate cost\"", + "baseline": 44, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Series New Backfill\n\"determine repos & estimate cost\"" + }, + { + "type": "rectangle", + "version": 384, + "versionNonce": 1658402576, + "isDeleted": false, + "id": "oDXArXKeR--Uo8Fv_eTex", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 913.304700782005, + "y": 304.9075486864091, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 466.6666666666665, + "height": 70.83333333333337, + "seed": 738559248, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "BApdiDfEZe8VsIfIMR2Yi" + }, + { + "type": "arrow", + "id": "f5HDastmpUENAnot5K4fZ" + }, + { + "type": "arrow", + "id": "hIZb811VPQ5ChwLpY0vw9" + }, + { + "type": "arrow", + "id": "3ugtdQsVT5c2_xsDhAdOP" + }, + { + "type": "arrow", + "id": "Ul1vF1wkYluxymYg44QR8" + }, + { + "id": "6Y0ZoiREkUiAQDNeKcOi-", + "type": "arrow" + }, + { + "id": "YDdWMJVVYCOcsX41N81II", + "type": "arrow" + }, + { + "id": "j2ETpxP6_pZBcF0FZ99Jq", + "type": "arrow" + }, + { + "id": "Bi0Y-qO3exeB83sxvNTqO", + "type": "arrow" + }, + { + "id": "SsKjOhIS5YFSATKVbdhkk", + "type": "arrow" + }, + { + "id": "75JM0P5M-qb4zo3QM5GEA", + "type": "arrow" + } + ], + "updated": 1674060571665, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 299, + "versionNonce": 446106096, + "isDeleted": false, + "id": "rFFK-WUiQMMaC3w5O5tbR", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1659.8767493931136, + "y": 172.37803479752023, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 316, + "height": 25, + "seed": 617419760, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "Frontend GraphQL Search API", + "baseline": 20, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Frontend GraphQL Search API" + }, + { + "type": "rectangle", + "version": 250, + "versionNonce": 1168821520, + "isDeleted": false, + "id": "dRQ_vTmtQf1VvB14Q5qcA", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1649.821193837558, + "y": 164.3224792419647, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 337.5, + "height": 40.2777777777778, + "seed": 691168016, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "UWa-1pvCQKNHqHGo0W5Ay" + }, + { + "id": "pEX-swbMWpZ1IT0oEoIO0", + "type": "arrow" + } + ], + "updated": 1674060475402, + "link": null, + "locked": false + }, + { + "type": "arrow", + "version": 626, + "versionNonce": 1493191664, + "isDeleted": false, + "id": "pEX-swbMWpZ1IT0oEoIO0", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1153.0124904408972, + "y": 450.155812575302, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 495.41981450776984, + "height": 266.3888888888928, + "seed": 65345008, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "startBinding": { + "elementId": "XH76bht8oXQKGLEBQJgBf", + "focus": -0.22621485756842843, + "gap": 1.3888888888850488 + }, + "endBinding": { + "elementId": "dRQ_vTmtQf1VvB14Q5qcA", + "focus": 0.38348082595870564, + "gap": 1.3888888888909605 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 374.58648117443636, + -120.55555555555952 + ], + [ + 391.2531478411033, + -259.4444444444484 + ], + [ + 495.41981450776984, + -266.3888888888928 + ] + ] + }, + { + "type": "arrow", + "version": 2012, + "versionNonce": 943207696, + "isDeleted": false, + "id": "6Y0ZoiREkUiAQDNeKcOi-", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1310.902107021283, + "y": 488.3233472975245, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 165.0614479273845, + "height": 188.99142433180486, + "seed": 1762885904, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1674060644446, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": { + "elementId": "efZG_NXhpdioWSiVzm4sU", + "focus": -0.4202445599144381, + "gap": 16.070116238366495 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 165.0614479273845, + 40.58660816182555 + ], + [ + 156.30634936097908, + 188.99142433180486 + ] + ] + }, + { + "type": "arrow", + "version": 1441, + "versionNonce": 1074186512, + "isDeleted": false, + "id": "nVGVD1DtKacrq2BkylSd9", + "fillStyle": "hachure", + "strokeWidth": 4, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1316.802078475052, + "y": 492.7278611864124, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 474.64111705703226, + "height": 188.33085317459995, + "seed": 186703632, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "startBinding": { + "elementId": "XH76bht8oXQKGLEBQJgBf", + "focus": -0.9557469528609506, + "gap": 17.28036380415915 + }, + "endBinding": { + "elementId": "8K_4LINCvAB5tlBmBTfUD", + "focus": -0.42026201485860554, + "gap": 16.59709821428629 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 109.45889968329493, + 15.24305555555236 + ], + [ + 243.79265219563285, + 31.374131944441274 + ], + [ + 462.1170684171734, + 39.19965277777453 + ], + [ + 474.64111705703226, + 188.33085317459995 + ] + ] + }, + { + "type": "arrow", + "version": 1291, + "versionNonce": 879437584, + "isDeleted": false, + "id": "nlAp7H7RAmimAtDn19Vv9", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1724.570749785338, + "y": 765.4914952888078, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 191.80910844340133, + "height": 150.64108895443155, + "seed": 2144501008, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "startBinding": { + "elementId": "SfhT7kzIGVFRUz-_Gzkn_", + "focus": 0.6166108006165942, + "gap": 9.611111111110745 + }, + "endBinding": { + "elementId": "udT3ExNG6f7gDPxeKgjhx", + "focus": 0.5997768654823261, + "gap": 23.627599864772577 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -191.80910844340133, + 150.64108895443155 + ] + ] + }, + { + "type": "arrow", + "version": 986, + "versionNonce": 2022347248, + "isDeleted": false, + "id": "j2ETpxP6_pZBcF0FZ99Jq", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 877.9114716153334, + "y": 227.33289590863137, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 29.733198713939373, + "height": 116.02170138888403, + "seed": 2053872624, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "startBinding": { + "elementId": "SnOr5K9X_GNkGJqk90AaR", + "focus": 0.7183587360624016, + "gap": 9.260651490563646 + }, + "endBinding": { + "elementId": "oDXArXKeR--Uo8Fv_eTex", + "focus": -0.9368486303053645, + "gap": 11.429995730510086 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -5.769965277777828, + 82.60243055555554 + ], + [ + 23.963233436161545, + 116.02170138888403 + ] + ] + }, + { + "type": "arrow", + "version": 603, + "versionNonce": 412337424, + "isDeleted": false, + "id": "2XlU_bX6zpwZoAAkJ03c7", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 877.5989716153335, + "y": 218.48914590863137, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 90.12818403240067, + "height": 231.66666666666333, + "seed": 1755339536, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "startBinding": { + "elementId": "SnOr5K9X_GNkGJqk90AaR", + "focus": 0.7435754878434437, + "gap": 1 + }, + "endBinding": { + "elementId": "XH76bht8oXQKGLEBQJgBf", + "focus": -0.4174252275682769, + "gap": 1.3888888888923248 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -1.3888888888886868, + 141.66666666666669 + ], + [ + 88.73929514351198, + 231.66666666666333 + ] + ] + }, + { + "type": "arrow", + "version": 2413, + "versionNonce": 982402032, + "isDeleted": false, + "id": "zqlFEuuOVBVWb6TYnEhgm", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1062.6198049486675, + "y": 536.6144306489912, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 60.552656843117916, + "height": 148.25231942630683, + "seed": 1514758128, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1674060719724, + "link": null, + "locked": false, + "startBinding": { + "elementId": "XH76bht8oXQKGLEBQJgBf", + "focus": 0.18335135891815238, + "gap": 10.069729184804146 + }, + "endBinding": { + "elementId": "y7q0tjwR8LxLuWU7KzCG4", + "focus": 0.03432198421780368, + "gap": 5.222222222222172 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -27.212673611111313, + 76.55726500747687 + ], + [ + -60.552656843117916, + 148.25231942630683 + ] + ] + }, + { + "type": "arrow", + "version": 1775, + "versionNonce": 923910416, + "isDeleted": false, + "id": "C2Fpu-CDw4s99m9heftdn", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1541.1492110867448, + "y": 780.4904878272405, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 78.45007644801467, + "height": 84.52962973017952, + "seed": 1366349584, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "startBinding": { + "elementId": "efZG_NXhpdioWSiVzm4sU", + "gap": 1.1055999595446337, + "focus": -0.18909981789729122 + }, + "endBinding": { + "elementId": "udT3ExNG6f7gDPxeKgjhx", + "gap": 10.22789806472187, + "focus": -0.18230315781637202 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -78.45007644801467, + 84.52962973017952 + ] + ] + }, + { + "type": "rectangle", + "version": 526, + "versionNonce": 1286772720, + "isDeleted": false, + "id": "1wXkG2q9wokqIL1b9RSXM", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1568.3995396307728, + "y": 407.2483003272405, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 393.05555555555566, + "height": 75, + "seed": 1542971888, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "mhaP-wCza_npDdAEH3IvG" + }, + { + "type": "arrow", + "id": "GdVfRvlF0-SQWdic8VPZu" + }, + { + "type": "arrow", + "id": "W5kr90BAxpmq55kuX87XG" + }, + { + "type": "arrow", + "id": "qVc66tRUosZulXFCBHEjV" + }, + { + "id": "pEX-swbMWpZ1IT0oEoIO0", + "type": "arrow" + }, + { + "type": "arrow", + "id": "7KoWPnNM90fiEjqnILQfl" + }, + { + "type": "arrow", + "id": "XR73REv1kPDhg7Pb9M-HF" + }, + { + "id": "nVGVD1DtKacrq2BkylSd9", + "type": "arrow" + }, + { + "id": "2XlU_bX6zpwZoAAkJ03c7", + "type": "arrow" + }, + { + "type": "arrow", + "id": "sZBqICZReQDOlc26vfZUW" + }, + { + "id": "zqlFEuuOVBVWb6TYnEhgm", + "type": "arrow" + }, + { + "id": "wgpG01hWl0JBGdlw8wPvd", + "type": "arrow" + }, + { + "id": "axWo3cC8VZgHVFTzLks0t", + "type": "arrow" + }, + { + "id": "0jRdw5gg3Lea6ZI92PmNS", + "type": "arrow" + } + ], + "updated": 1674060475402, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 359, + "versionNonce": 2136416016, + "isDeleted": false, + "id": "V56BpOrVOWz_2ciPMyR13", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1596.9273174085502, + "y": 416.7483003272405, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 341, + "height": 24, + "seed": 1768125712, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "In Progress Backfill Resetter", + "baseline": 20, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "In Progress Backfill Resetter" + }, + { + "type": "text", + "version": 358, + "versionNonce": 477764080, + "isDeleted": false, + "id": "jKNnAQLY-ZrfGYlkco0RJ", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1668.927317408551, + "y": 447.7483003272405, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 188, + "height": 20, + "seed": 1753621488, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "\"reset stalled jobs\"", + "baseline": 16, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "\"reset stalled jobs\"" + }, + { + "type": "rectangle", + "version": 647, + "versionNonce": 1275157488, + "isDeleted": false, + "id": "WYK8QcTkqoqM-dZzsSUp4", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1569.8311860380118, + "y": 322.40912828947387, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 393.05555555555566, + "height": 75, + "seed": 985873904, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "mhaP-wCza_npDdAEH3IvG" + }, + { + "type": "arrow", + "id": "GdVfRvlF0-SQWdic8VPZu" + }, + { + "type": "arrow", + "id": "W5kr90BAxpmq55kuX87XG" + }, + { + "type": "arrow", + "id": "qVc66tRUosZulXFCBHEjV" + }, + { + "id": "pEX-swbMWpZ1IT0oEoIO0", + "type": "arrow" + }, + { + "type": "arrow", + "id": "7KoWPnNM90fiEjqnILQfl" + }, + { + "type": "arrow", + "id": "XR73REv1kPDhg7Pb9M-HF" + }, + { + "id": "nVGVD1DtKacrq2BkylSd9", + "type": "arrow" + }, + { + "id": "2XlU_bX6zpwZoAAkJ03c7", + "type": "arrow" + }, + { + "type": "arrow", + "id": "sZBqICZReQDOlc26vfZUW" + }, + { + "id": "zqlFEuuOVBVWb6TYnEhgm", + "type": "arrow" + }, + { + "id": "wgpG01hWl0JBGdlw8wPvd", + "type": "arrow" + }, + { + "id": "NBWPFap83xLJwVDbqf-SG", + "type": "arrow" + }, + { + "id": "y55MwITQpqrxzFBU-9SNI", + "type": "arrow" + }, + { + "id": "M5U6xKxJP3AcAwI0vu9oo", + "type": "arrow" + } + ], + "updated": 1674060475402, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 471, + "versionNonce": 1950784272, + "isDeleted": false, + "id": "CiHO7uPKu7cMc5yeL5ABn", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1645.3589638157891, + "y": 331.90912828947387, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 247, + "height": 24, + "seed": 881580304, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "New Backfill Resetter", + "baseline": 20, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "New Backfill Resetter" + }, + { + "type": "text", + "version": 478, + "versionNonce": 1297112560, + "isDeleted": false, + "id": "ORT7y6ShTgnDvSCFkm9NR", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1670.35896381579, + "y": 362.90912828947387, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 188, + "height": 20, + "seed": 226885616, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 3, + "text": "\"reset stalled jobs\"", + "baseline": 16, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "\"reset stalled jobs\"" + }, + { + "id": "M5U6xKxJP3AcAwI0vu9oo", + "type": "arrow", + "x": 1968.8745888157896, + "y": 356.65131578947387, + "width": 124.05515050473196, + "height": 329.9296874999999, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 4, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 447209232, + "version": 600, + "versionNonce": 1631314928, + "isDeleted": false, + "boundElements": null, + "updated": 1674060475402, + "link": null, + "locked": false, + "points": [ + [ + 0, 0 ], [ - -7.340072255148243, - 93.1745092767261 + 23.984375, + 18.25 + ], + [ + 12.2578125, + 232.53125 + ], + [ + -89.4296875, + 253.3984374999999 + ], + [ + -100.07077550473196, + 329.9296874999999 ] ], "lastCommittedPoint": null, "startBinding": { - "elementId": "ODHl6PtX7vkcDHs6BaZkj", - "focus": 0.10198374448133453, - "gap": 1 + "elementId": "WYK8QcTkqoqM-dZzsSUp4", + "focus": 0.12113419357689176, + "gap": 5.987847222222172 }, "endBinding": { - "elementId": "KZkPAMPD04wahLpOne-zO", - "focus": -0.08761616840219316, - "gap": 1 + "elementId": "8K_4LINCvAB5tlBmBTfUD", + "focus": 0.061023005207626074, + "gap": 11.07480928582487 }, "startArrowhead": null, "endArrowhead": "arrow" }, { - "id": "oZ-FYC8NS_zJEK1zcW_sh", + "id": "0jRdw5gg3Lea6ZI92PmNS", "type": "arrow", - "x": 1614.4446532999168, - "y": 486.4548872180452, - "width": 272.2222222222222, - "height": 252.22222222222229, + "x": 1962.2964638157896, + "y": 444.76850328947387, + "width": 81.515625, + "height": 248.4531249999999, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "dotted", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", - "seed": 1257981185, - "version": 11, - "versionNonce": 1216261679, + "seed": 786000368, + "version": 585, + "versionNonce": 1272484624, "isDeleted": false, - "boundElementIds": null, + "boundElements": null, + "updated": 1674060475402, + "link": null, + "locked": false, "points": [ [ 0, 0 ], [ - -272.2222222222222, - 252.22222222222229 + 36.375, + 2.6328125 + ], + [ + 15.125, + 157.1796874999999 + ], + [ + -45.140625, + 167.59375 + ], + [ + -39.792014379488364, + 248.4531249999999 ] ], "lastCommittedPoint": null, "startBinding": { - "elementId": "LMQFsiOCpQ9wJLaPxI9te", - "focus": 0.7674730945655645, - "gap": 13.928571428571502 + "elementId": "1wXkG2q9wokqIL1b9RSXM", + "focus": -0.2757936123063957, + "gap": 1 }, "endBinding": { - "elementId": "F4xor4iMUYwNk1eWiE96P", - "focus": 0.32193168667768846, - "gap": 24.145997421380713 + "elementId": "8K_4LINCvAB5tlBmBTfUD", + "focus": 0.5345533413493034, + "gap": 4.43418428582487 }, "startArrowhead": null, "endArrowhead": "arrow" }, { - "id": "dhppvnEuhCmfrBwwBpg1_", "type": "arrow", - "x": 1477.7779866332503, - "y": 582.0104427736007, - "width": 165.55555555555543, - "height": 3.3333333333333712, + "version": 1970, + "versionNonce": 1254060528, + "isDeleted": false, + "id": "Bi0Y-qO3exeB83sxvNTqO", + "fillStyle": "hachure", + "strokeWidth": 4, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1393.5344990372737, + "y": 341.8726079521739, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 340.15674205703226, + "height": 345.57304067459995, + "seed": 543504368, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1674060662864, + "link": null, + "locked": false, + "startBinding": { + "elementId": "oDXArXKeR--Uo8Fv_eTex", + "focus": -0.07841005033999157, + "gap": 13.563131588602118 + }, + "endBinding": { + "elementId": "8K_4LINCvAB5tlBmBTfUD", + "focus": -0.8327079296882266, + "gap": 10.21016394852478 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 133.42764968329493, + 16.82899305555236 + ], + [ + 136.94890219563285, + 176.45225694444127 + ], + [ + 322.4764434171734, + 186.80902777777453 + ], + [ + 340.15674205703226, + 345.57304067459995 + ] + ] + }, + { + "type": "rectangle", + "version": 925, + "versionNonce": 516576528, + "isDeleted": false, + "id": "BJYUFMfc7zW05h_JU8-77", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, "angle": 0, + "x": 1144.6558388157898, + "y": 691.0341282894739, "strokeColor": "#000000", "backgroundColor": "transparent", + "width": 212.75, + "height": 84, + "seed": 129433360, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "AGBda_XPtse3jxYwQpX71" + }, + { + "type": "arrow", + "id": "o-u5q_29fU8DTydha1CS6" + }, + { + "type": "arrow", + "id": "nGIZerkX-nuZP5ph84wzq" + }, + { + "type": "arrow", + "id": "D-aGC4EzCpWCj0Rogvok-" + }, + { + "type": "arrow", + "id": "uLumB7sd5oaIkrCNFmc4b" + }, + { + "type": "arrow", + "id": "BixWOkWrvvO6uxXJ6clF8" + }, + { + "id": "aUh2D18gp2RlzVGV2xhcq", + "type": "arrow" + }, + { + "type": "arrow", + "id": "sZBqICZReQDOlc26vfZUW" + }, + { + "id": "dcvk4l8lJxpGUyJwAuWzd", + "type": "arrow" + }, + { + "id": "zqlFEuuOVBVWb6TYnEhgm", + "type": "arrow" + }, + { + "id": "eg79mS17we_ZWTaATvOtb", + "type": "arrow" + }, + { + "id": "5fkP6yokpWdna-XxM8lgd", + "type": "arrow" + } + ], + "updated": 1674060702837, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 777, + "versionNonce": 1873258480, + "isDeleted": false, + "id": "ZzGPkiUcBfqFMksVlcIHm", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1168.6636513157898, + "y": 725.6435032894739, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 165, + "height": 24, + "seed": 1067857680, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1674060475402, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 3, + "text": "Backfill Store", + "baseline": 20, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Backfill Store" + }, + { + "id": "eg79mS17we_ZWTaATvOtb", + "type": "arrow", + "x": 1202.2964638157898, + "y": 543.3778782894738, + "width": 22.9140625, + "height": 144.8671875, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 4, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, "groupIds": [], "strokeSharpness": "round", - "seed": 1407983649, - "version": 11, - "versionNonce": 1258834191, + "seed": 2292496, + "version": 466, + "versionNonce": 2022926096, "isDeleted": false, - "boundElementIds": null, + "boundElements": null, + "updated": 1674060616799, + "link": null, + "locked": false, "points": [ [ 0, 0 ], [ - 165.55555555555543, - -3.3333333333333712 + -16.765625, + 68.71875 + ], + [ + -22.9140625, + 144.8671875 ] ], "lastCommittedPoint": null, "startBinding": { - "elementId": "4C63a8jstFovCFlcXH1go", - "focus": 0.22813934716312106, - "gap": 12.093776106934229 + "elementId": "XH76bht8oXQKGLEBQJgBf", + "focus": -0.5472587376712125, + "gap": 16.833176825286728 }, "endBinding": { - "elementId": "ODHl6PtX7vkcDHs6BaZkj", - "focus": 0.0063927614270635705, - "gap": 6.499999999999773 + "elementId": "BJYUFMfc7zW05h_JU8-77", + "focus": -0.6856831460303964, + "gap": 2.7890625000001137 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "75JM0P5M-qb4zo3QM5GEA", + "type": "arrow", + "x": 1394.9683388157898, + "y": 346.81537828947387, + "width": 160.96875, + "height": 344.3203124999999, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 4, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1848840464, + "version": 1017, + "versionNonce": 1744688112, + "isDeleted": false, + "boundElements": null, + "updated": 1674060678135, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 9.53125, + 104.9296875 + ], + [ + -18.5078125, + 236.5468749999999 + ], + [ + -66.96875, + 244.5624999999999 + ], + [ + -144.265625, + 258.7734374999999 + ], + [ + -151.4375, + 344.3203124999999 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "oDXArXKeR--Uo8Fv_eTex", + "focus": -1.0473061521822076, + "gap": 14.996971367118249 }, + "endBinding": null, "startArrowhead": null, "endArrowhead": "arrow" + }, + { + "type": "arrow", + "version": 1943, + "versionNonce": 1410361328, + "isDeleted": false, + "id": "5fkP6yokpWdna-XxM8lgd", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1258.412127039797, + "y": 789.8318134243841, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 87.81554855198533, + "height": 62.64681723017952, + "seed": 982261520, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1674060742857, + "link": null, + "locked": false, + "startBinding": { + "elementId": "BJYUFMfc7zW05h_JU8-77", + "focus": 0.1517977301951125, + "gap": 14.797685134910239 + }, + "endBinding": { + "elementId": "udT3ExNG6f7gDPxeKgjhx", + "focus": 0.3818980161972752, + "gap": 28.974405658159526 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 87.81554855198533, + 62.64681723017952 + ] + ] + }, + { + "type": "arrow", + "version": 1560, + "versionNonce": 472496112, + "isDeleted": false, + "id": "CjSUV0RGa6grxlOHJ2_ur", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1054.4830717191394, + "y": 778.7760838122579, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 233.40976358035937, + "height": 128.43661667648337, + "seed": 1102007568, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1674060748240, + "link": null, + "locked": false, + "startBinding": { + "elementId": "y7q0tjwR8LxLuWU7KzCG4", + "focus": 0.029253953605859077, + "gap": 4.687111514737694 + }, + "endBinding": { + "elementId": "udT3ExNG6f7gDPxeKgjhx", + "focus": -0.28355324928944176, + "gap": 31.520534866582494 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 233.40976358035937, + 128.43661667648337 + ] + ] } ], "appState": { "gridSize": null, "viewBackgroundColor": "#ffffff" - } -} + }, + "files": {} +} \ No newline at end of file diff --git a/doc/dev/background-information/insights/diagrams/architecture.svg b/doc/dev/background-information/insights/diagrams/architecture.svg index 182f996c97db..2beabe153ce4 100644 --- a/doc/dev/background-information/insights/diagrams/architecture.svg +++ b/doc/dev/background-information/insights/diagrams/architecture.svg @@ -1,559 +1,16 @@ - - - - - - - - - - - GraphQL API schema - OSS GraphQL resolvers"Insights not available in OSS" - - - - Enterprise GraphQL resolvers - - - - Main App DB(Postgres) - - - - codeinsights-db(Postgres) - - - - User/org/global settings store"insights definitions" - - - - enterprise/internal/insights/resolvers - OSS GraphQL resolvers interface - cmd/frontend/graphqlbackend/insights.go - - - - - - - - - - - - - - - Insights Store - enterprise/internal/insights/store - - - - - - - - - - - - - - - "Frontend" service(Sourcegraph monolith) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "repo-updater" service(Sourcegraph repo background workers) - - - - Insights background jobs - - - - Worker store"query search" jobs queue - - - - Query Runner Worker"Query & insert search insights" - - - - Main App DB(Postgres) - - - - codeinsights-db(Postgres) - - - - Query Runner Job Cleaner"Delete old completed jobsfrom DB" - - - - User/org/global settings store"insights definitions" - - - - Insights Store - enterprise/internal/insights/store - - - - - - - - - - - - - - - - - - - - - - - - - - Insight Enqueuer"enumerate all insights, enqueue work" - - - - Frontend GraphQL Search API - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + GraphQL API schemaOSS GraphQL resolvers"Insights not available in OSS"Enterprise GraphQL resolverscodeinsights-db(Postgres)Insight Definitionsenterprise/internal/insights/resolversOSS GraphQL resolvers interfacecmd/frontend/graphqlbackend/insights.goSeries dataenterprise/internal/insights/store"Frontend" service(Sourcegraph monolith)Main App DB(postgres)"worker" service(Sourcegraph background workers for adding new insight points)Insights background jobsWorker store"query search" jobs queueQuery Runner Worker"Query & insert search insights"Main App DB(Postgres)codeinsights-db(Postgres)Query Runner Job Cleaner"Delete old completed jobsfrom DB"Insight Definitionsenterprise/.../insights/insight_storeInsights Storeenterprise/internal/insights/storeInsight Enqueuer"fetch insights, enqueue recording"Frontend GraphQL Search APIenterprise/.../insights/insight_storeWorker Resetter"reset stalled jobs"Repository Permission DataInsightPermStore"worker" service(Sourcegraph background workers for backfilling an insight)Insights background jobsWorker store"backfill" jobs queueIn Progress Backfill"Query & insert results per repo"codeinsights-db(Postgres)Insight Definitionsinsight_storeInsights StoreSeries New Backfill"determine repos & estimate cost"Frontend GraphQL Search APIIn Progress Backfill Resetter"reset stalled jobs"New Backfill Resetter"reset stalled jobs"Backfill Store \ No newline at end of file diff --git a/enterprise/internal/insights/pipeline/readme.md b/enterprise/internal/insights/pipeline/readme.md deleted file mode 100644 index 927606a77580..000000000000 --- a/enterprise/internal/insights/pipeline/readme.md +++ /dev/null @@ -1,33 +0,0 @@ -This is an exploration into reimagining what the backfilling or potentially even all query running could look like for insights. - -Below is a diagram to explain the flow this approach takes. - -```mermaid -flowchart TB - -subgraph backfiller - -subgraph BuildSearchJob[Build Search Jobs] - GetTimeIntervals[Get Time Intervals] --> CompressTimeIntervals[Run Compression] - subgraph FindRevisionBuildSearch[Find revision & build search job] - Worker1[Worker 1] - Workern[Worker n] - end - CompressTimeIntervals --> FindRevisionBuildSearch -end - - - -subgraph RunSearchJobs[Run Search Jobs] - subgraph SearchRunners[Search Runners] - SearchWorker1[Worker 1] - SearchWorkern[Worker n] - end -end - -FindRevisionBuildSearch --> RunSearchJobs -RunSearchJobs --> SaveResults[Save Results] - -end - -``` From be4f4409a3a6b5b88a2188e1b03b3f57d47f1c54 Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Thu, 19 Jan 2023 17:35:39 -0700 Subject: [PATCH 050/678] Sourcegraph App (single-binary branch) (#46547) * internal: add service and singleprogram packages * sg.config.yaml: add single-binary build targets * internal/env: add a function for clearing environ cache * internal/{workerutil,metrics}: add a hack to allow running 2 executors in the same process * internal/conf: add single-program deploy type * internal/singleprogram: clarify security * cmd/sourcegraph-oss: add initial single-binary main (will not build yet) * enterprise/cmd/sourcegraph: initial enterprise single-binary * Add multi-platform builds for single-program * single-binary: correctly build JS artifacts into binary * license_finder licenses add github.com/xi2/xz "Public domain" * internal/service/svcmain: correctly initialize logger for DeprecatedSingleServiceMain * worker: refactor to new service pattern * cmd/github-proxy: refactor to use new service pattern * symbols: refactor to use new service pattern * gitserver: refactor to user new service pattern * searcher: refactor to use new service pattern * gitserver: refactor to use new service pattern * repo-updater: refactor to use new service pattern * frontend: refactor to use new service pattern * executor: refactor to use new service pattern * internal/symbols: use new LoadConfig pattern * precise-code-intel-worker: refactor to use new service pattern * internal/symbols: load config for tests * cmd/repo-updater: remove LoadConfig approach * cmd/symbols: workaround env var conflict with searcher * executor: internal: add workaround to allow running 2 instances in same process * executors: add EXECUTOR_QUEUE_DISABLE_ACCESS_TOKEN for single-binary and dev deployments only * single-binary: use EXECUTOR_QUEUE_DISABLE_ACCESS_TOKEN * extsvc/github: fix default value for single-program deploy type * single-binary: stop relying on a local ctags image * single-binary: use unix sockets for postgres * release App snapshots in CI when pushed to app/release-snapshot branch * internal/service/svcmain: update TODO comment * executor: correct DEPLOY_TYPE check * dev/check: allow single-binary to import dbconn * executor: remove accidental reliance on dbconn package * executor: improve error logging when running commands (#46546) * executor: improve error logging when running commands * executor: do not attempt std config validation running e.g. install cmd * executor: do not pull in the conf package / frontend reliance * ci: executors: correct site config for passwordless auth * server: fix bug where github-proxy would try to be a conf server * CI: executors: fix integration test passwordless auth * executors: allow passwordless auth in sourcegraph/server for testing * repo-updater: fix enterprise init (caused regression in repository syncing) Signed-off-by: Stephen Gutekanst Co-authored-by: Peter Guy Co-authored-by: Quinn Slack --- .gitignore | 5 + cmd/frontend/internal/cli/config.go | 10 +- cmd/frontend/internal/cli/serve_cmd.go | 45 +---- cmd/frontend/internal/highlight/highlight.go | 14 +- cmd/frontend/main.go | 21 +-- cmd/frontend/shared/frontend.go | 27 --- cmd/frontend/shared/service.go | 39 +++++ cmd/github-proxy/github-proxy.go | 6 +- cmd/github-proxy/shared/service.go | 22 +++ cmd/github-proxy/shared/shared.go | 32 +--- cmd/gitserver/main.go | 7 +- cmd/gitserver/shared/service.go | 24 +++ cmd/gitserver/shared/shared.go | 78 ++++----- cmd/repo-updater/main.go | 12 +- cmd/repo-updater/shared/main.go | 67 ++------ cmd/repo-updater/shared/service.go | 44 +++++ cmd/searcher/main.go | 7 +- cmd/searcher/shared/service.go | 22 +++ cmd/searcher/shared/shared.go | 46 +---- cmd/sourcegraph-oss/main.go | 29 ++++ cmd/sourcegraph-oss/osscmd/osscmd.go | 30 ++++ cmd/symbols/main.go | 3 +- cmd/symbols/shared/main.go | 50 ++---- cmd/symbols/shared/service.go | 25 +++ cmd/symbols/shared/sqlite.go | 8 +- cmd/symbols/types/types.go | 28 ++- cmd/worker/main.go | 24 +-- cmd/worker/shared/config.go | 3 + cmd/worker/shared/main.go | 62 +++---- cmd/worker/shared/service.go | 24 +++ dev/check/go-dbconn-import.sh | 3 + dev/ci/runtype/runtype.go | 15 +- dev/ci/runtype/runtype_test.go | 6 + doc/dependency_decisions.yml | 7 + .../background-information/ci/reference.md | 9 + .../background-information/sg/reference.md | 4 + .../internal/command/observability.go | 12 +- enterprise/cmd/executor/internal/run/util.go | 36 ++-- enterprise/cmd/executor/main.go | 5 +- enterprise/cmd/executor/shared/service.go | 47 +++++ enterprise/cmd/executor/shared/shared.go | 27 +-- .../cmd/frontend/internal/codeintel/init.go | 2 +- .../frontend/internal/executorqueue/init.go | 36 +++- .../internal/executorqueue/queuehandler.go | 7 +- .../executorqueue/queuehandler_test.go | 2 +- .../executorqueue/queues/batches/queue.go | 2 +- .../executorqueue/queues/codeintel/queue.go | 4 +- .../queues/codeintel/transform.go | 7 +- .../queues/codeintel/transform_test.go | 12 +- enterprise/cmd/frontend/main.go | 16 +- enterprise/cmd/frontend/shared/service.go | 31 ++++ enterprise/cmd/gitserver/main.go | 11 +- enterprise/cmd/gitserver/shared/service.go | 25 +++ enterprise/cmd/gitserver/shared/shared.go | 2 +- .../cmd/precise-code-intel-worker/main.go | 5 +- .../shared/service.go | 28 +++ .../shared/shared.go | 61 ++----- enterprise/cmd/repo-updater/main.go | 10 +- enterprise/cmd/repo-updater/shared/service.go | 8 + enterprise/cmd/sourcegraph/README.md | 45 +++++ .../enterprisecmd/enterprisecmd.go | 30 ++++ .../enterprisecmd/executorcmd/executorcmd.go | 21 +++ enterprise/cmd/sourcegraph/main.go | 33 ++++ enterprise/cmd/symbols/main.go | 6 +- enterprise/cmd/symbols/shared/service.go | 28 +++ enterprise/cmd/symbols/shared/setup.go | 14 +- enterprise/cmd/worker/main.go | 47 +---- enterprise/cmd/worker/shared/service.go | 27 +++ enterprise/cmd/worker/shared/shared.go | 24 ++- enterprise/dev/app/goreleaser.yaml | 132 +++++++++++++++ enterprise/dev/app/release.sh | 88 ++++++++++ .../executors/config/site-config.json | 1 - .../integration/executors/docker-compose.yml | 1 + .../dev/ci/integration/executors/run.sh | 2 +- enterprise/dev/ci/internal/ci/operations.go | 19 +++ enterprise/dev/ci/internal/ci/pipeline.go | 4 + enterprise/dev/ci/scripts/release-app.sh | 23 +++ go.mod | 4 + go.sum | 4 + internal/conf/computed.go | 4 +- internal/conf/conf.go | 11 ++ internal/conf/confdefaults/confdefaults.go | 15 ++ internal/conf/deploy/deploytype.go | 14 +- internal/env/env.go | 11 ++ internal/extsvc/github/common.go | 11 +- internal/metrics/operation.go | 10 +- internal/service/service.go | 34 ++++ internal/service/svcmain/svcmain.go | 155 +++++++++++++++++ internal/singleprogram/postgresql.go | 141 +++++++++++++++ internal/singleprogram/singleprogram.go | 160 ++++++++++++++++++ internal/symbols/client.go | 23 +-- internal/symbols/client_test.go | 4 + internal/workerutil/observability.go | 5 +- sg.config.yaml | 81 ++++++++- 94 files changed, 1890 insertions(+), 596 deletions(-) delete mode 100644 cmd/frontend/shared/frontend.go create mode 100644 cmd/frontend/shared/service.go create mode 100644 cmd/github-proxy/shared/service.go create mode 100644 cmd/gitserver/shared/service.go create mode 100644 cmd/repo-updater/shared/service.go create mode 100644 cmd/searcher/shared/service.go create mode 100644 cmd/sourcegraph-oss/main.go create mode 100644 cmd/sourcegraph-oss/osscmd/osscmd.go create mode 100644 cmd/symbols/shared/service.go create mode 100644 cmd/worker/shared/service.go create mode 100644 enterprise/cmd/executor/shared/service.go create mode 100644 enterprise/cmd/frontend/shared/service.go create mode 100644 enterprise/cmd/gitserver/shared/service.go create mode 100644 enterprise/cmd/precise-code-intel-worker/shared/service.go create mode 100644 enterprise/cmd/repo-updater/shared/service.go create mode 100644 enterprise/cmd/sourcegraph/README.md create mode 100644 enterprise/cmd/sourcegraph/enterprisecmd/enterprisecmd.go create mode 100644 enterprise/cmd/sourcegraph/enterprisecmd/executorcmd/executorcmd.go create mode 100644 enterprise/cmd/sourcegraph/main.go create mode 100644 enterprise/cmd/symbols/shared/service.go create mode 100644 enterprise/cmd/worker/shared/service.go create mode 100644 enterprise/dev/app/goreleaser.yaml create mode 100755 enterprise/dev/app/release.sh create mode 100755 enterprise/dev/ci/scripts/release-app.sh create mode 100644 internal/service/service.go create mode 100644 internal/service/svcmain/svcmain.go create mode 100644 internal/singleprogram/postgresql.go create mode 100644 internal/singleprogram/singleprogram.go diff --git a/.gitignore b/.gitignore index 4e4e8e8708f3..73684663a2fd 100644 --- a/.gitignore +++ b/.gitignore @@ -184,3 +184,8 @@ go.work.sum # SCIP index.scip + +# Buildkite helper and cache files +/an +/tr +/cache-*.tar diff --git a/cmd/frontend/internal/cli/config.go b/cmd/frontend/internal/cli/config.go index eeea749a74d9..772ef6a45d5c 100644 --- a/cmd/frontend/internal/cli/config.go +++ b/cmd/frontend/internal/cli/config.go @@ -18,6 +18,7 @@ import ( "github.com/sourcegraph/log" "github.com/sourcegraph/sourcegraph/cmd/frontend/envvar" + "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/highlight" "github.com/sourcegraph/sourcegraph/internal/actor" "github.com/sourcegraph/sourcegraph/internal/api" "github.com/sourcegraph/sourcegraph/internal/conf" @@ -29,6 +30,7 @@ import ( "github.com/sourcegraph/sourcegraph/internal/env" "github.com/sourcegraph/sourcegraph/internal/extsvc" "github.com/sourcegraph/sourcegraph/internal/jsonc" + "github.com/sourcegraph/sourcegraph/internal/symbols" "github.com/sourcegraph/sourcegraph/internal/types" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/schema" @@ -551,7 +553,7 @@ func serviceConnections(logger log.Logger) conftypes.ServiceConnections { } var ( - searcherURL = env.Get("SEARCHER_URL", "k8s+http://searcher:3181", "searcher server URL") + searcherURL string searcherURLsOnce sync.Once searcherURLs *endpoint.Map @@ -572,6 +574,12 @@ var ( }() ) +func LoadConfig() { + searcherURL = env.Get("SEARCHER_URL", "k8s+http://searcher:3181", "searcher server URL") + highlight.LoadConfig() + symbols.LoadConfig() +} + func computeSearcherEndpoints() *endpoint.Map { searcherURLsOnce.Do(func() { if len(strings.Fields(searcherURL)) == 0 { diff --git a/cmd/frontend/internal/cli/serve_cmd.go b/cmd/frontend/internal/cli/serve_cmd.go index 4a91c63bca04..0298b03ade10 100644 --- a/cmd/frontend/internal/cli/serve_cmd.go +++ b/cmd/frontend/internal/cli/serve_cmd.go @@ -8,10 +8,8 @@ import ( "net/http" "os" "strconv" - "strings" "time" - "github.com/getsentry/sentry-go" "github.com/graph-gophers/graphql-go" "github.com/keegancsmith/tmpfriend" sglog "github.com/sourcegraph/log" @@ -26,7 +24,6 @@ import ( "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/app/ui" "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/app/updatecheck" "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/bg" - "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/cli/loghandlers" "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/httpapi" "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/siteid" oce "github.com/sourcegraph/sourcegraph/cmd/frontend/oneclickexport" @@ -36,20 +33,16 @@ import ( "github.com/sourcegraph/sourcegraph/internal/conf/deploy" "github.com/sourcegraph/sourcegraph/internal/database" connections "github.com/sourcegraph/sourcegraph/internal/database/connections/live" - "github.com/sourcegraph/sourcegraph/internal/debugserver" "github.com/sourcegraph/sourcegraph/internal/encryption/keyring" "github.com/sourcegraph/sourcegraph/internal/env" "github.com/sourcegraph/sourcegraph/internal/gitserver" "github.com/sourcegraph/sourcegraph/internal/goroutine" - "github.com/sourcegraph/sourcegraph/internal/hostname" "github.com/sourcegraph/sourcegraph/internal/httpserver" - "github.com/sourcegraph/sourcegraph/internal/logging" "github.com/sourcegraph/sourcegraph/internal/observation" "github.com/sourcegraph/sourcegraph/internal/oobmigration" - "github.com/sourcegraph/sourcegraph/internal/profiler" "github.com/sourcegraph/sourcegraph/internal/redispool" + "github.com/sourcegraph/sourcegraph/internal/service" "github.com/sourcegraph/sourcegraph/internal/sysreq" - "github.com/sourcegraph/sourcegraph/internal/tracer" "github.com/sourcegraph/sourcegraph/internal/users" "github.com/sourcegraph/sourcegraph/internal/version" "github.com/sourcegraph/sourcegraph/internal/version/upgradestore" @@ -93,27 +86,11 @@ func InitDB(logger sglog.Logger) (*sql.DB, error) { return sqlDB, nil } -// Main is the main entrypoint for the frontend server program. -func Main(enterpriseSetupHook func(database.DB, conftypes.UnifiedWatchable) enterprise.Services) error { - ctx := context.Background() - - log.SetFlags(0) - log.SetPrefix("") - - liblog := sglog.Init(sglog.Resource{ - Name: env.MyName, - Version: version.Version(), - InstanceID: hostname.Get(), - }, sglog.NewSentrySinkWith( - sglog.SentrySink{ - ClientOptions: sentry.ClientOptions{SampleRate: 0.2}, - }, - )) // Experimental: DevX is observing how sampling affects the errors signal - defer liblog.Sync() +type SetupFunc func(database.DB, conftypes.UnifiedWatchable) enterprise.Services - logger := sglog.Scoped("server", "the frontend server program") - ready := make(chan struct{}) - go debugserver.NewServerRoutine(ready).Start() +// Main is the main entrypoint for the frontend server program. +func Main(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, enterpriseSetupHook SetupFunc) error { + logger := observationCtx.Logger sqlDB, err := InitDB(logger) if err != nil { @@ -121,8 +98,6 @@ func Main(enterpriseSetupHook func(database.DB, conftypes.UnifiedWatchable) ente } db := database.NewDB(logger, sqlDB) - observationCtx := observation.NewContext(logger) - if os.Getenv("SRC_DISABLE_OOBMIGRATION_VALIDATION") != "" { logger.Warn("Skipping out-of-band migrations check") } else { @@ -142,9 +117,7 @@ func Main(enterpriseSetupHook func(database.DB, conftypes.UnifiedWatchable) ente return errors.Wrap(err, "failed to apply site config overrides") } globals.ConfigurationServerFrontendOnly = conf.InitConfigurationServerFrontendOnly(newConfigurationSource(logger, db)) - conf.Init() conf.MustValidateDefaults() - go conf.Watch(liblog.Update(conf.GetLogSinks)) // now we can init the keyring, as it depends on site config if err := keyring.Init(ctx); err != nil { @@ -161,12 +134,6 @@ func Main(enterpriseSetupHook func(database.DB, conftypes.UnifiedWatchable) ente return errors.Wrap(err, "failed to override external service config") } - // Filter trace logs - d, _ := time.ParseDuration(traceThreshold) - logging.Init(logging.Filter(loghandlers.Trace(strings.Fields(traceFields), d))) //nolint:staticcheck // Deprecated, but logs unmigrated to sourcegraph/log look really bad without this. - tracer.Init(sglog.Scoped("tracer", "internal tracer package"), conf.DefaultClient()) - profiler.Init() - // Run enterprise setup hook enterprise := enterpriseSetupHook(db, conf.DefaultClient()) @@ -283,7 +250,7 @@ func Main(enterpriseSetupHook func(database.DB, conftypes.UnifiedWatchable) ente println(fmt.Sprintf("\n\n%s\n\n", logoColor)) } logger.Info(fmt.Sprintf("✱ Sourcegraph is ready at: %s", globals.ExternalURL())) - close(ready) + ready() goroutine.MonitorBackgroundRoutines(context.Background(), routines...) return nil diff --git a/cmd/frontend/internal/highlight/highlight.go b/cmd/frontend/internal/highlight/highlight.go index d0c093eb12b9..3bfa682a438c 100644 --- a/cmd/frontend/internal/highlight/highlight.go +++ b/cmd/frontend/internal/highlight/highlight.go @@ -34,10 +34,12 @@ import ( "github.com/sourcegraph/sourcegraph/lib/errors" ) -var ( - syntectServer = env.Get("SRC_SYNTECT_SERVER", "http://syntect-server:9238", "syntect_server HTTP(s) address") - client *gosyntect.Client -) +func LoadConfig() { + syntectServer := env.Get("SRC_SYNTECT_SERVER", "http://syntect-server:9238", "syntect_server HTTP(s) address") + client = gosyntect.New(syntectServer) +} + +var client *gosyntect.Client var ( highlightOpOnce sync.Once @@ -63,10 +65,6 @@ func getHighlightOp() *observation.Operation { return highlightOp } -func init() { - client = gosyntect.New(syntectServer) -} - // IsBinary is a helper to tell if the content of a file is binary or not. // TODO(tjdevries): This doesn't make sense to be here, IMO func IsBinary(content []byte) bool { diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go index e43e47ecddb2..7242abb00f4c 100644 --- a/cmd/frontend/main.go +++ b/cmd/frontend/main.go @@ -1,26 +1,11 @@ +// Command frontend is a service that serves the web frontend and API. package main import ( - "github.com/sourcegraph/sourcegraph/cmd/frontend/enterprise" "github.com/sourcegraph/sourcegraph/cmd/frontend/shared" - "github.com/sourcegraph/sourcegraph/internal/authz" - "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" - "github.com/sourcegraph/sourcegraph/internal/database" - "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/cmd/sourcegraph-oss/osscmd" ) -// Note: All frontend code should be added to shared.Main, not here. See that -// function for details. - func main() { - // Set dummy authz provider to unblock channel for checking permissions in GraphQL APIs. - // See https://github.com/sourcegraph/sourcegraph/issues/3847 for details. - authz.SetProviders(true, []authz.Provider{}) - - env.Lock() - env.HandleHelpFlag() - - shared.Main(func(_ database.DB, _ conftypes.UnifiedWatchable) enterprise.Services { - return enterprise.DefaultServices() - }) + osscmd.DeprecatedSingleServiceMainOSS(shared.Service) } diff --git a/cmd/frontend/shared/frontend.go b/cmd/frontend/shared/frontend.go deleted file mode 100644 index 6525058d34f8..000000000000 --- a/cmd/frontend/shared/frontend.go +++ /dev/null @@ -1,27 +0,0 @@ -// Package shared contains the frontend command implementation shared -package shared - -import ( - "fmt" - "os" - - "github.com/sourcegraph/sourcegraph/cmd/frontend/enterprise" - "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/cli" - "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" - "github.com/sourcegraph/sourcegraph/internal/database" - - _ "github.com/sourcegraph/sourcegraph/cmd/frontend/registry/api" -) - -// Main is the main function that runs the frontend process. -// -// It is exposed as function in a package so that it can be called by other -// main package implementations such as Sourcegraph Enterprise, which import -// proprietary/private code. -func Main(enterpriseSetupHook func(database.DB, conftypes.UnifiedWatchable) enterprise.Services) { - err := cli.Main(enterpriseSetupHook) - if err != nil { - fmt.Fprintln(os.Stderr, "fatal:", err) - os.Exit(1) - } -} diff --git a/cmd/frontend/shared/service.go b/cmd/frontend/shared/service.go new file mode 100644 index 000000000000..53cd74aeedf3 --- /dev/null +++ b/cmd/frontend/shared/service.go @@ -0,0 +1,39 @@ +// Package shared contains the frontend command implementation shared +package shared + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/cmd/frontend/enterprise" + "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/cli" + "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" +) + +type svc struct{} + +func (svc) Name() string { return "frontend" } + +func (svc) Configure() (env.Config, []debugserver.Endpoint) { + CLILoadConfig() + return nil, nil +} + +func (svc) Start(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, config env.Config) error { + ossSetupHook := func(_ database.DB, _ conftypes.UnifiedWatchable) enterprise.Services { + return enterprise.DefaultServices() + } + return CLIMain(ctx, observationCtx, ready, ossSetupHook) +} + +var Service service.Service = svc{} + +// Reexported to get around `internal` package. +var ( + CLILoadConfig = cli.LoadConfig + CLIMain = cli.Main +) diff --git a/cmd/github-proxy/github-proxy.go b/cmd/github-proxy/github-proxy.go index 1e86b9223dca..9d74902c8371 100644 --- a/cmd/github-proxy/github-proxy.go +++ b/cmd/github-proxy/github-proxy.go @@ -2,11 +2,9 @@ package main import ( "github.com/sourcegraph/sourcegraph/cmd/github-proxy/shared" - "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/cmd/sourcegraph-oss/osscmd" ) func main() { - env.Lock() - env.HandleHelpFlag() - shared.Main() + osscmd.DeprecatedSingleServiceMainOSS(shared.Service) } diff --git a/cmd/github-proxy/shared/service.go b/cmd/github-proxy/shared/service.go new file mode 100644 index 000000000000..8a347c4d9def --- /dev/null +++ b/cmd/github-proxy/shared/service.go @@ -0,0 +1,22 @@ +package shared + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" +) + +type svc struct{} + +func (svc) Name() string { return "github-proxy" } + +func (svc) Configure() (env.Config, []debugserver.Endpoint) { return nil, nil } + +func (svc) Start(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, _ env.Config) error { + return Main(ctx, observationCtx, ready) +} + +var Service service.Service = svc{} diff --git a/cmd/github-proxy/shared/shared.go b/cmd/github-proxy/shared/shared.go index 171853c814f7..3820c9bb1bd4 100644 --- a/cmd/github-proxy/shared/shared.go +++ b/cmd/github-proxy/shared/shared.go @@ -15,7 +15,6 @@ import ( "syscall" "time" - "github.com/getsentry/sentry-go" "github.com/gorilla/handlers" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" @@ -24,14 +23,12 @@ import ( "github.com/sourcegraph/log" "github.com/sourcegraph/sourcegraph/internal/conf" - "github.com/sourcegraph/sourcegraph/internal/debugserver" "github.com/sourcegraph/sourcegraph/internal/env" "github.com/sourcegraph/sourcegraph/internal/goroutine" - "github.com/sourcegraph/sourcegraph/internal/hostname" "github.com/sourcegraph/sourcegraph/internal/instrumentation" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" "github.com/sourcegraph/sourcegraph/internal/trace" - "github.com/sourcegraph/sourcegraph/internal/tracer" - "github.com/sourcegraph/sourcegraph/internal/version" ) var logRequests, _ = strconv.ParseBool(env.Get("LOG_REQUESTS", "", "log HTTP requests")) @@ -56,28 +53,11 @@ var hopHeaders = map[string]struct{}{ "Upgrade": {}, } -func Main() { - liblog := log.Init(log.Resource{ - Name: env.MyName, - Version: version.Version(), - InstanceID: hostname.Get(), - }, log.NewSentrySinkWith( - log.SentrySink{ - ClientOptions: sentry.ClientOptions{SampleRate: 0.2}, - }, - )) // Experimental: DevX is observing how sampling affects the errors signal - - defer liblog.Sync() - conf.Init() - go conf.Watch(liblog.Update(conf.GetLogSinks)) - tracer.Init(log.Scoped("tracer", "internal tracer package"), conf.DefaultClient()) +func Main(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc) error { + logger := observationCtx.Logger // Ready immediately - ready := make(chan struct{}) - close(ready) - go debugserver.NewServerRoutine(ready).Start() - - logger := log.Scoped("server", "the github-proxy service") + ready() p := &githubProxy{ logger: logger, @@ -129,6 +109,8 @@ func Main() { if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Fatal(err.Error()) } + + return nil } func instrumentHandler(r prometheus.Registerer, h http.Handler) http.Handler { diff --git a/cmd/gitserver/main.go b/cmd/gitserver/main.go index 3f6d675f9f43..53f5ab463c7d 100644 --- a/cmd/gitserver/main.go +++ b/cmd/gitserver/main.go @@ -3,12 +3,9 @@ package main // import "github.com/sourcegraph/sourcegraph/cmd/gitserver" import ( "github.com/sourcegraph/sourcegraph/cmd/gitserver/shared" - "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/cmd/sourcegraph-oss/osscmd" ) func main() { - env.Lock() - env.HandleHelpFlag() - - shared.Main(nil) + osscmd.DeprecatedSingleServiceMainOSS(shared.Service) } diff --git a/cmd/gitserver/shared/service.go b/cmd/gitserver/shared/service.go new file mode 100644 index 000000000000..454bbc9b5215 --- /dev/null +++ b/cmd/gitserver/shared/service.go @@ -0,0 +1,24 @@ +package shared + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" +) + +type svc struct{} + +func (svc) Name() string { return "gitserver" } + +func (svc) Configure() (env.Config, []debugserver.Endpoint) { + return LoadConfig(), nil +} + +func (svc) Start(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, config env.Config) error { + return Main(ctx, observationCtx, ready, config.(*Config), nil) +} + +var Service service.Service = svc{} diff --git a/cmd/gitserver/shared/shared.go b/cmd/gitserver/shared/shared.go index 60308f61af32..a705556ba7be 100644 --- a/cmd/gitserver/shared/shared.go +++ b/cmd/gitserver/shared/shared.go @@ -14,7 +14,6 @@ import ( "syscall" "time" - "github.com/getsentry/sentry-go" jsoniter "github.com/json-iterator/go" "github.com/sourcegraph/log" "github.com/tidwall/gjson" @@ -30,7 +29,6 @@ import ( "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" "github.com/sourcegraph/sourcegraph/internal/database" connections "github.com/sourcegraph/sourcegraph/internal/database/connections/live" - "github.com/sourcegraph/sourcegraph/internal/debugserver" "github.com/sourcegraph/sourcegraph/internal/encryption/keyring" "github.com/sourcegraph/sourcegraph/internal/env" "github.com/sourcegraph/sourcegraph/internal/extsvc" @@ -45,22 +43,18 @@ import ( "github.com/sourcegraph/sourcegraph/internal/httpcli" "github.com/sourcegraph/sourcegraph/internal/instrumentation" "github.com/sourcegraph/sourcegraph/internal/jsonc" - "github.com/sourcegraph/sourcegraph/internal/logging" "github.com/sourcegraph/sourcegraph/internal/observation" - "github.com/sourcegraph/sourcegraph/internal/profiler" "github.com/sourcegraph/sourcegraph/internal/ratelimit" "github.com/sourcegraph/sourcegraph/internal/repos" "github.com/sourcegraph/sourcegraph/internal/requestclient" + "github.com/sourcegraph/sourcegraph/internal/service" "github.com/sourcegraph/sourcegraph/internal/trace" - "github.com/sourcegraph/sourcegraph/internal/tracer" "github.com/sourcegraph/sourcegraph/internal/types" - "github.com/sourcegraph/sourcegraph/internal/version" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/schema" ) var ( - reposDir = env.Get("SRC_REPOS_DIR", "/data/repos", "Root dir containing repos.") // Align these variables with the 'disk_space_remaining' alerts in monitoring wantPctFree = env.MustGetInt("SRC_REPOS_DESIRED_PERCENT_FREE", 10, "Target percentage of free space on disk.") @@ -77,48 +71,42 @@ var ( type EnterpriseInit func(db database.DB) -func Main(enterpriseInit EnterpriseInit) { - ctx := context.Background() +type Config struct { + env.BaseConfig - logging.Init() //nolint:staticcheck // Deprecated, but logs unmigrated to sourcegraph/log look really bad without this. - - liblog := log.Init(log.Resource{ - Name: env.MyName, - Version: version.Version(), - InstanceID: hostname.Get(), - }, log.NewSentrySinkWith( - log.SentrySink{ - ClientOptions: sentry.ClientOptions{SampleRate: 0.2}, - }, - )) - defer liblog.Sync() + ReposDir string +} - conf.Init() - go conf.Watch(liblog.Update(conf.GetLogSinks)) +func (c *Config) Load() { + c.ReposDir = c.Get("SRC_REPOS_DIR", "/data/repos", "Root dir containing repos.") +} - tracer.Init(log.Scoped("tracer", "internal tracer package"), conf.DefaultClient()) - profiler.Init() +func LoadConfig() *Config { + var config Config + config.Load() + return &config +} - logger := log.Scoped("server", "the gitserver service") - observationCtx := observation.NewContext(logger) +func Main(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, config *Config, enterpriseInit EnterpriseInit) error { + logger := observationCtx.Logger - if reposDir == "" { - logger.Fatal("SRC_REPOS_DIR is required") + if config.ReposDir == "" { + return errors.New("SRC_REPOS_DIR is required") } - if err := os.MkdirAll(reposDir, os.ModePerm); err != nil { - logger.Fatal("failed to create SRC_REPOS_DIR", log.Error(err)) + if err := os.MkdirAll(config.ReposDir, os.ModePerm); err != nil { + return errors.Wrap(err, "creating SRC_REPOS_DIR") } wantPctFree2, err := getPercent(wantPctFree) if err != nil { - logger.Fatal("SRC_REPOS_DESIRED_PERCENT_FREE is out of range", log.Error(err)) + return errors.Wrap(err, "SRC_REPOS_DESIRED_PERCENT_FREE is out of range") } sqlDB, err := getDB(observationCtx) if err != nil { - logger.Fatal("failed to initialize database stores", log.Error(err)) + return errors.Wrap(err, "initializing database stores") } - db := database.NewDB(logger, sqlDB) + db := database.NewDB(observationCtx.Logger, sqlDB) repoStore := db.Repos() dependenciesSvc := dependencies.NewService(observationCtx, db) @@ -126,7 +114,7 @@ func Main(enterpriseInit EnterpriseInit) { err = keyring.Init(ctx) if err != nil { - logger.Fatal("failed to initialise keyring", log.Error(err)) + return errors.Wrap(err, "initializing keyring") } if enterpriseInit != nil { @@ -134,19 +122,19 @@ func Main(enterpriseInit EnterpriseInit) { } if err != nil { - logger.Fatal("Failed to create sub-repo client", log.Error(err)) + return errors.Wrap(err, "creating sub-repo client") } gitserver := server.Server{ Logger: logger, ObservationCtx: observationCtx, - ReposDir: reposDir, + ReposDir: config.ReposDir, DesiredPercentFree: wantPctFree2, GetRemoteURLFunc: func(ctx context.Context, repo api.RepoName) (string, error) { return getRemoteURLFunc(ctx, externalServiceStore, repoStore, nil, repo) }, GetVCSSyncer: func(ctx context.Context, repo api.RepoName) (server.VCSSyncer, error) { - return getVCSSyncer(ctx, externalServiceStore, repoStore, dependenciesSvc, repo, reposDir) + return getVCSSyncer(ctx, externalServiceStore, repoStore, dependenciesSvc, repo, config.ReposDir) }, Hostname: hostname.Get(), DB: db, @@ -157,11 +145,11 @@ func Main(enterpriseInit EnterpriseInit) { gitserver.RegisterMetrics(observationCtx, db) if tmpDir, err := gitserver.SetupAndClearTmp(); err != nil { - logger.Fatal("failed to setup temporary directory", log.Error(err)) + return errors.Wrap(err, "failed to setup temporary directory") } else if err := os.Setenv("TMP_DIR", tmpDir); err != nil { // Additionally, set TMP_DIR so other temporary files we may accidentally // create are on the faster RepoDir mount. - logger.Fatal("Setting TMP_DIR", log.Error(err)) + return errors.Wrap(err, "setting TMP_DIR") } // Create Handler now since it also initializes state @@ -172,10 +160,6 @@ func Main(enterpriseInit EnterpriseInit) { handler = trace.HTTPMiddleware(logger, handler, conf.DefaultClient()) handler = instrumentation.HTTPMiddleware("", handler) - // Ready immediately - ready := make(chan struct{}) - close(ready) - ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -185,8 +169,10 @@ func Main(enterpriseInit EnterpriseInit) { logger.Warn("error performing initial rate limit sync", log.Error(err)) } + // Ready immediately + ready() + go syncRateLimiters(ctx, logger, externalServiceStore, rateLimitSyncerLimitPerSecond) - go debugserver.NewServerRoutine(ready).Start() go gitserver.Janitor(actor.WithInternalActor(ctx), janitorInterval) go gitserver.SyncRepoState(syncRepoStateInterval, syncRepoStateBatchSize, syncRepoStateUpdatePerSecond) @@ -239,6 +225,8 @@ func Main(enterpriseInit EnterpriseInit) { // The most important thing this does is kill all our clones. If we just // shutdown they will be orphaned and continue running. gitserver.Stop() + + return nil } func configureFusionClient(conn schema.PerforceConnection) server.FusionConfig { diff --git a/cmd/repo-updater/main.go b/cmd/repo-updater/main.go index d68377039e56..44c36216703e 100644 --- a/cmd/repo-updater/main.go +++ b/cmd/repo-updater/main.go @@ -4,17 +4,9 @@ package main import ( "github.com/sourcegraph/sourcegraph/cmd/repo-updater/shared" - "github.com/sourcegraph/sourcegraph/internal/authz" - "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/cmd/sourcegraph-oss/osscmd" ) func main() { - env.Lock() - env.HandleHelpFlag() - - // Set dummy authz provider to unblock channel for checking permissions in GraphQL APIs. - // See https://github.com/sourcegraph/sourcegraph/issues/3847 for details. - authz.SetProviders(true, []authz.Provider{}) - - shared.Main(nil) + osscmd.DeprecatedSingleServiceMainOSS(shared.Service) } diff --git a/cmd/repo-updater/shared/main.go b/cmd/repo-updater/shared/main.go index 92b17bf65a62..c54db2ec8765 100644 --- a/cmd/repo-updater/shared/main.go +++ b/cmd/repo-updater/shared/main.go @@ -11,7 +11,6 @@ import ( "strconv" "time" - "github.com/getsentry/sentry-go" "github.com/graph-gophers/graphql-go/relay" "github.com/prometheus/client_golang/prometheus" "github.com/sourcegraph/log" @@ -36,19 +35,16 @@ import ( "github.com/sourcegraph/sourcegraph/internal/env" "github.com/sourcegraph/sourcegraph/internal/extsvc" "github.com/sourcegraph/sourcegraph/internal/goroutine" - "github.com/sourcegraph/sourcegraph/internal/hostname" "github.com/sourcegraph/sourcegraph/internal/httpcli" "github.com/sourcegraph/sourcegraph/internal/httpserver" "github.com/sourcegraph/sourcegraph/internal/instrumentation" - "github.com/sourcegraph/sourcegraph/internal/logging" "github.com/sourcegraph/sourcegraph/internal/observation" - "github.com/sourcegraph/sourcegraph/internal/profiler" "github.com/sourcegraph/sourcegraph/internal/ratelimit" "github.com/sourcegraph/sourcegraph/internal/repos" + "github.com/sourcegraph/sourcegraph/internal/service" "github.com/sourcegraph/sourcegraph/internal/trace" - "github.com/sourcegraph/sourcegraph/internal/tracer" "github.com/sourcegraph/sourcegraph/internal/types" - "github.com/sourcegraph/sourcegraph/internal/version" + "github.com/sourcegraph/sourcegraph/lib/errors" ) const port = "3182" @@ -78,44 +74,16 @@ type LazyDebugserverEndpoint struct { manualPurgeEndpoint http.HandlerFunc } -func Main(enterpriseInit EnterpriseInit) { +func Main(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, debugserverEndpoints *LazyDebugserverEndpoint, enterpriseInit EnterpriseInit) error { // NOTE: Internal actor is required to have full visibility of the repo table // (i.e. bypass repository authorization). - ctx := actor.WithInternalActor(context.Background()) + ctx = actor.WithInternalActor(ctx) - logging.Init() //nolint:staticcheck // Deprecated, but logs unmigrated to sourcegraph/log look really bad without this. - - liblog := log.Init(log.Resource{ - Name: env.MyName, - Version: version.Version(), - InstanceID: hostname.Get(), - }, log.NewSentrySinkWith( - log.SentrySink{ - ClientOptions: sentry.ClientOptions{SampleRate: 0.2}, - }, - )) // Experimental: DevX is observing how sampling affects the errors signal - defer liblog.Sync() - - conf.Init() - go conf.Watch(liblog.Update(conf.GetLogSinks)) - - tracer.Init(log.Scoped("tracer", "internal tracer package"), conf.DefaultClient()) - profiler.Init() - - logger := log.Scoped("service", "repo-updater service") - observationCtx := observation.NewContext(logger) - - // Signals health of startup - ready := make(chan struct{}) - - // Start debug server - debugserverEndpoints := LazyDebugserverEndpoint{} - debugServerRoutine := createDebugServerRoutine(ready, &debugserverEndpoints) - go debugServerRoutine.Start() + logger := observationCtx.Logger clock := func() time.Time { return time.Now().UTC() } if err := keyring.Init(ctx); err != nil { - logger.Fatal("error initialising encryption keyring", log.Error(err)) + return errors.Wrap(err, "initializing encryption keyring") } dsn := conf.GetServiceConnectionValueAndRestartOnChange(func(serviceConnections conftypes.ServiceConnections) string { @@ -123,7 +91,7 @@ func Main(enterpriseInit EnterpriseInit) { }) sqlDB, err := connections.EnsureNewFrontendDB(observationCtx, dsn, "repo-updater") if err != nil { - logger.Fatal("failed to initialize database store", log.Error(err)) + return errors.Wrap(err, "initializing database store") } db := database.NewDB(logger, sqlDB) @@ -252,7 +220,7 @@ func Main(enterpriseInit EnterpriseInit) { // the debugserver constructed at the top of this function. This ensures we don't // have a race between becoming ready and a debugserver request failing directly // after being unblocked. - close(ready) + ready() // NOTE: Internal actor is required to have full visibility of the repo table // (i.e. bypass repository authorization). @@ -269,12 +237,13 @@ func Main(enterpriseInit EnterpriseInit) { trace.HTTPMiddleware(logger, authzBypass(handler), conf.DefaultClient())), }) goroutine.MonitorBackgroundRoutines(ctx, httpSrv) + + return nil } -func createDebugServerRoutine(ready chan struct{}, debugserverEndpoints *LazyDebugserverEndpoint) goroutine.BackgroundRoutine { - return debugserver.NewServerRoutine( - ready, - debugserver.Endpoint{ +func createDebugServerEndpoints(ready chan struct{}, debugserverEndpoints *LazyDebugserverEndpoint) []debugserver.Endpoint { + return []debugserver.Endpoint{ + { Name: "Repo Updater State", Path: "/repo-updater-state", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -284,7 +253,7 @@ func createDebugServerRoutine(ready chan struct{}, debugserverEndpoints *LazyDeb debugserverEndpoints.repoUpdaterStateEndpoint(w, r) }), }, - debugserver.Endpoint{ + { Name: "List Authz Providers", Path: "/list-authz-providers", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -294,7 +263,7 @@ func createDebugServerRoutine(ready chan struct{}, debugserverEndpoints *LazyDeb debugserverEndpoints.listAuthzProvidersEndpoint(w, r) }), }, - debugserver.Endpoint{ + { Name: "Gitserver Repo Status", Path: "/gitserver-repo-status", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -302,7 +271,7 @@ func createDebugServerRoutine(ready chan struct{}, debugserverEndpoints *LazyDeb debugserverEndpoints.gitserverReposStatusEndpoint(w, r) }), }, - debugserver.Endpoint{ + { Name: "Rate Limiter State", Path: "/rate-limiter-state", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -310,7 +279,7 @@ func createDebugServerRoutine(ready chan struct{}, debugserverEndpoints *LazyDeb debugserverEndpoints.rateLimiterStateEndpoint(w, r) }), }, - debugserver.Endpoint{ + { Name: "Manual Repo Purge", Path: "/manual-purge", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -318,7 +287,7 @@ func createDebugServerRoutine(ready chan struct{}, debugserverEndpoints *LazyDeb debugserverEndpoints.manualPurgeEndpoint(w, r) }), }, - ) + } } func gitserverReposStatusHandler(db database.DB) http.HandlerFunc { diff --git a/cmd/repo-updater/shared/service.go b/cmd/repo-updater/shared/service.go new file mode 100644 index 000000000000..3738548379f4 --- /dev/null +++ b/cmd/repo-updater/shared/service.go @@ -0,0 +1,44 @@ +package shared + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" +) + +type svc struct { + enterpriseInit EnterpriseInit + + ready chan struct{} + debugserverEndpoints LazyDebugserverEndpoint +} + +func (svc) Name() string { return "repo-updater" } + +func (s *svc) Configure() (env.Config, []debugserver.Endpoint) { + // Signals health of startup. + s.ready = make(chan struct{}) + + return nil, createDebugServerEndpoints(s.ready, &s.debugserverEndpoints) +} + +func (s *svc) Start(ctx context.Context, observationCtx *observation.Context, signalReadyToParent service.ReadyFunc, _ env.Config) error { + // This service's debugserver endpoints should start responding when this service is ready (and + // not ewait for *all* services to be ready). Therefore, we need to track whether we are ready + // separately. + ready := service.ReadyFunc(func() { + close(s.ready) + signalReadyToParent() + }) + + return Main(ctx, observationCtx, ready, &s.debugserverEndpoints, s.enterpriseInit) +} + +var Service service.Service = NewServiceWithEnterpriseInit(nil) + +func NewServiceWithEnterpriseInit(enterpriseInit EnterpriseInit) *svc { + return &svc{enterpriseInit: enterpriseInit} +} diff --git a/cmd/searcher/main.go b/cmd/searcher/main.go index 59aed76c7cda..99d7e39b72d9 100644 --- a/cmd/searcher/main.go +++ b/cmd/searcher/main.go @@ -4,12 +4,9 @@ package main import ( "github.com/sourcegraph/sourcegraph/cmd/searcher/shared" - "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/cmd/sourcegraph-oss/osscmd" ) func main() { - env.Lock() - env.HandleHelpFlag() - - shared.Main() + osscmd.DeprecatedSingleServiceMainOSS(shared.Service) } diff --git a/cmd/searcher/shared/service.go b/cmd/searcher/shared/service.go new file mode 100644 index 000000000000..631ac0c4ca0e --- /dev/null +++ b/cmd/searcher/shared/service.go @@ -0,0 +1,22 @@ +package shared + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" +) + +type svc struct{} + +func (svc) Name() string { return "searcher" } + +func (svc) Configure() (env.Config, []debugserver.Endpoint) { return nil, nil } + +func (svc) Start(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, _ env.Config) error { + return Start(ctx, observationCtx, ready) +} + +var Service service.Service = svc{} diff --git a/cmd/searcher/shared/shared.go b/cmd/searcher/shared/shared.go index 9d894bd9d8f2..91c4e6dd6bc8 100644 --- a/cmd/searcher/shared/shared.go +++ b/cmd/searcher/shared/shared.go @@ -5,7 +5,6 @@ package shared import ( "context" "io" - stdlog "log" "net" "net/http" "os" @@ -17,7 +16,6 @@ import ( "golang.org/x/sync/errgroup" - "github.com/getsentry/sentry-go" "github.com/keegancsmith/tmpfriend" "github.com/sourcegraph/log" @@ -28,20 +26,15 @@ import ( "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" "github.com/sourcegraph/sourcegraph/internal/database" connections "github.com/sourcegraph/sourcegraph/internal/database/connections/live" - "github.com/sourcegraph/sourcegraph/internal/debugserver" "github.com/sourcegraph/sourcegraph/internal/env" "github.com/sourcegraph/sourcegraph/internal/gitserver" "github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain" "github.com/sourcegraph/sourcegraph/internal/goroutine" - "github.com/sourcegraph/sourcegraph/internal/hostname" "github.com/sourcegraph/sourcegraph/internal/instrumentation" - "github.com/sourcegraph/sourcegraph/internal/logging" "github.com/sourcegraph/sourcegraph/internal/observation" - "github.com/sourcegraph/sourcegraph/internal/profiler" sharedsearch "github.com/sourcegraph/sourcegraph/internal/search" + "github.com/sourcegraph/sourcegraph/internal/service" "github.com/sourcegraph/sourcegraph/internal/trace" - "github.com/sourcegraph/sourcegraph/internal/tracer" - "github.com/sourcegraph/sourcegraph/internal/version" "github.com/sourcegraph/sourcegraph/lib/errors" ) @@ -116,11 +109,11 @@ func setupTmpDir() error { return nil } -func run(logger log.Logger) error { +func Start(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc) error { + logger := observationCtx.Logger + // Ready immediately - ready := make(chan struct{}) - close(ready) - go debugserver.NewServerRoutine(ready).Start() + ready() var cacheSizeBytes int64 if i, err := strconv.ParseInt(cacheSizeMB, 10, 64); err != nil { @@ -198,7 +191,7 @@ func run(logger log.Logger) error { handler = trace.HTTPMiddleware(logger, handler, conf.DefaultClient()) handler = instrumentation.HTTPMiddleware("", handler) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(ctx) defer cancel() g, ctx := errgroup.WithContext(ctx) @@ -241,30 +234,3 @@ func run(logger log.Logger) error { return g.Wait() } - -func Main() { - stdlog.SetFlags(0) - logging.Init() //nolint:staticcheck // Deprecated, but logs unmigrated to sourcegraph/log look really bad without this. - liblog := log.Init(log.Resource{ - Name: env.MyName, - Version: version.Version(), - InstanceID: hostname.Get(), - }, log.NewSentrySinkWith( - log.SentrySink{ - ClientOptions: sentry.ClientOptions{SampleRate: 0.2}, - }, - )) // Experimental: DevX is observing how sampling affects the errors signal - defer liblog.Sync() - - conf.Init() - go conf.Watch(liblog.Update(conf.GetLogSinks)) - tracer.Init(log.Scoped("tracer", "internal tracer package"), conf.DefaultClient()) - profiler.Init() - - logger := log.Scoped("server", "the searcher service") - - err := run(logger) - if err != nil { - logger.Fatal("searcher failed", log.Error(err)) - } -} diff --git a/cmd/sourcegraph-oss/main.go b/cmd/sourcegraph-oss/main.go new file mode 100644 index 000000000000..6c18aac01e40 --- /dev/null +++ b/cmd/sourcegraph-oss/main.go @@ -0,0 +1,29 @@ +// Command sourcegraph-oss is a single program that runs all of Sourcegraph (OSS variant). +package main + +import ( + frontend_shared "github.com/sourcegraph/sourcegraph/cmd/frontend/shared" + githubproxy_shared "github.com/sourcegraph/sourcegraph/cmd/github-proxy/shared" + gitserver_shared "github.com/sourcegraph/sourcegraph/cmd/gitserver/shared" + repoupdater_shared "github.com/sourcegraph/sourcegraph/cmd/repo-updater/shared" + searcher_shared "github.com/sourcegraph/sourcegraph/cmd/searcher/shared" + "github.com/sourcegraph/sourcegraph/cmd/sourcegraph-oss/osscmd" + symbols_shared "github.com/sourcegraph/sourcegraph/cmd/symbols/shared" + worker_shared "github.com/sourcegraph/sourcegraph/cmd/worker/shared" + "github.com/sourcegraph/sourcegraph/internal/service" +) + +// services is a list of services to run in the OSS build. +var services = []service.Service{ + frontend_shared.Service, + gitserver_shared.Service, + repoupdater_shared.Service, + searcher_shared.Service, + symbols_shared.Service, + worker_shared.Service, + githubproxy_shared.Service, +} + +func main() { + osscmd.MainOSS(services) +} diff --git a/cmd/sourcegraph-oss/osscmd/osscmd.go b/cmd/sourcegraph-oss/osscmd/osscmd.go new file mode 100644 index 000000000000..92f59baaf08e --- /dev/null +++ b/cmd/sourcegraph-oss/osscmd/osscmd.go @@ -0,0 +1,30 @@ +// Package osscmd defines entrypoint functions for the OSS build of Sourcegraph's single-program +// distribution. It is invoked by all OSS commands' main functions. +package osscmd + +import ( + "github.com/sourcegraph/sourcegraph/internal/authz" + "github.com/sourcegraph/sourcegraph/internal/service" + "github.com/sourcegraph/sourcegraph/internal/service/svcmain" +) + +var config = svcmain.Config{ + AfterConfigure: func() { + // Set dummy authz provider to unblock channel for checking permissions in GraphQL APIs. + // See https://github.com/sourcegraph/sourcegraph/issues/3847 for details. + authz.SetProviders(true, []authz.Provider{}) + }, +} + +// Main is called from the `main` function of the `sourcegraph-oss` command. +func MainOSS(services []service.Service) { + svcmain.Main(services, config) +} + +// DeprecatedSingleServiceMainOSS is called from the `main` function of a command in the OSS build +// to start a single service (such as frontend or gitserver). +// +// DEPRECATED: See svcmain.DeprecatedSingleServiceMain documentation for more info. +func DeprecatedSingleServiceMainOSS(service service.Service) { + svcmain.DeprecatedSingleServiceMain(service, config, true, true) +} diff --git a/cmd/symbols/main.go b/cmd/symbols/main.go index bcdbabff357b..215ede97c237 100644 --- a/cmd/symbols/main.go +++ b/cmd/symbols/main.go @@ -3,9 +3,10 @@ package main import ( + "github.com/sourcegraph/sourcegraph/cmd/sourcegraph-oss/osscmd" "github.com/sourcegraph/sourcegraph/cmd/symbols/shared" ) func main() { - shared.Main(shared.SetupSqlite) + osscmd.DeprecatedSingleServiceMainOSS(shared.Service) } diff --git a/cmd/symbols/shared/main.go b/cmd/symbols/shared/main.go index 1980478a2dfc..6fd6aaf25bad 100644 --- a/cmd/symbols/shared/main.go +++ b/cmd/symbols/shared/main.go @@ -9,8 +9,6 @@ import ( "strconv" "time" - "github.com/getsentry/sentry-go" - "github.com/sourcegraph/log" "github.com/sourcegraph/sourcegraph/cmd/symbols/fetcher" @@ -23,58 +21,36 @@ import ( "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" "github.com/sourcegraph/sourcegraph/internal/database" connections "github.com/sourcegraph/sourcegraph/internal/database/connections/live" - "github.com/sourcegraph/sourcegraph/internal/debugserver" "github.com/sourcegraph/sourcegraph/internal/env" "github.com/sourcegraph/sourcegraph/internal/goroutine" "github.com/sourcegraph/sourcegraph/internal/honey" - "github.com/sourcegraph/sourcegraph/internal/hostname" "github.com/sourcegraph/sourcegraph/internal/httpserver" "github.com/sourcegraph/sourcegraph/internal/instrumentation" - "github.com/sourcegraph/sourcegraph/internal/logging" "github.com/sourcegraph/sourcegraph/internal/observation" - "github.com/sourcegraph/sourcegraph/internal/profiler" + "github.com/sourcegraph/sourcegraph/internal/service" "github.com/sourcegraph/sourcegraph/internal/trace" - "github.com/sourcegraph/sourcegraph/internal/tracer" - "github.com/sourcegraph/sourcegraph/internal/version" + "github.com/sourcegraph/sourcegraph/lib/errors" ) var sanityCheck, _ = strconv.ParseBool(env.Get("SANITY_CHECK", "false", "check that go-sqlite3 works then exit 0 if it's ok or 1 if not")) var ( baseConfig = env.BaseConfig{} - RepositoryFetcherConfig = types.LoadRepositoryFetcherConfig(baseConfig) - CtagsConfig = types.LoadCtagsConfig(baseConfig) + RepositoryFetcherConfig types.RepositoryFetcherConfig + CtagsConfig types.CtagsConfig ) const addr = ":3184" type SetupFunc func(observationCtx *observation.Context, db database.DB, gitserverClient gitserver.GitserverClient, repositoryFetcher fetcher.RepositoryFetcher) (types.SearchFunc, func(http.ResponseWriter, *http.Request), []goroutine.BackgroundRoutine, string, error) -func Main(setup SetupFunc) { - // Initialization - env.HandleHelpFlag() - logging.Init() //nolint:staticcheck // Deprecated, but logs unmigrated to sourcegraph/log look really bad without this. - liblog := log.Init(log.Resource{ - Name: env.MyName, - Version: version.Version(), - InstanceID: hostname.Get(), - }, log.NewSentrySinkWith( - log.SentrySink{ - ClientOptions: sentry.ClientOptions{SampleRate: 0.2}, - }, - )) // Experimental: DevX is observing how sampling affects the errors signal - defer liblog.Sync() - - conf.Init() - go conf.Watch(liblog.Update(conf.GetLogSinks)) - tracer.Init(log.Scoped("tracer", "internal tracer package"), conf.DefaultClient()) - profiler.Init() +func Main(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, setup SetupFunc) error { + logger := observationCtx.Logger routines := []goroutine.BackgroundRoutine{} // Initialize tracing/metrics - logger := log.Scoped("service", "the symbols service") - observationCtx := observation.NewContext(logger, observation.Honeycomb(&honey.Dataset{ + observationCtx = observation.NewContext(logger, observation.Honeycomb(&honey.Dataset{ Name: "codeintel-symbols", SampleRate: 20, })) @@ -104,14 +80,10 @@ func Main(setup SetupFunc) { repositoryFetcher := fetcher.NewRepositoryFetcher(observationCtx, gitserverClient, RepositoryFetcherConfig.MaxTotalPathsLength, int64(RepositoryFetcherConfig.MaxFileSizeKb)*1000) searchFunc, handleStatus, newRoutines, ctagsBinary, err := setup(observationCtx, db, gitserverClient, repositoryFetcher) if err != nil { - logger.Fatal("Failed to set up", log.Error(err)) + return errors.Wrap(err, "failed to set up") } routines = append(routines, newRoutines...) - // Start debug server - ready := make(chan struct{}) - go debugserver.NewServerRoutine(ready).Start() - // Create HTTP server handler := api.NewHandler(searchFunc, gitserverClient.ReadFile, handleStatus, ctagsBinary) handler = handlePanic(logger, handler) @@ -126,8 +98,10 @@ func Main(setup SetupFunc) { routines = append(routines, server) // Mark health server as ready and go! - close(ready) - goroutine.MonitorBackgroundRoutines(context.Background(), routines...) + ready() + goroutine.MonitorBackgroundRoutines(ctx, routines...) + + return nil } func mustInitializeFrontendDB(observationCtx *observation.Context) *sql.DB { diff --git a/cmd/symbols/shared/service.go b/cmd/symbols/shared/service.go new file mode 100644 index 000000000000..745552383d7d --- /dev/null +++ b/cmd/symbols/shared/service.go @@ -0,0 +1,25 @@ +package shared + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" +) + +type svc struct{} + +func (svc) Name() string { return "symbols" } + +func (svc) Configure() (env.Config, []debugserver.Endpoint) { + LoadConfig() + return nil, nil +} + +func (svc) Start(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, config env.Config) error { + return Main(ctx, observationCtx, ready, SetupSqlite) +} + +var Service service.Service = svc{} diff --git a/cmd/symbols/shared/sqlite.go b/cmd/symbols/shared/sqlite.go index 514dd9b2dc22..8ecf41ede59c 100644 --- a/cmd/symbols/shared/sqlite.go +++ b/cmd/symbols/shared/sqlite.go @@ -23,7 +23,13 @@ import ( "github.com/sourcegraph/sourcegraph/internal/observation" ) -var config = types.LoadSqliteConfig(baseConfig, CtagsConfig, RepositoryFetcherConfig) +func LoadConfig() { + RepositoryFetcherConfig = types.LoadRepositoryFetcherConfig(baseConfig) + CtagsConfig = types.LoadCtagsConfig(baseConfig) + config = types.LoadSqliteConfig(baseConfig, CtagsConfig, RepositoryFetcherConfig) +} + +var config types.SqliteConfig func SetupSqlite(observationCtx *observation.Context, db database.DB, gitserverClient gitserver.GitserverClient, repositoryFetcher fetcher.RepositoryFetcher) (types.SearchFunc, func(http.ResponseWriter, *http.Request), []goroutine.BackgroundRoutine, string, error) { logger := observationCtx.Logger.Scoped("sqlite.setup", "SQLite setup") diff --git a/cmd/symbols/types/types.go b/cmd/symbols/types/types.go index 9db4883dadbe..e4e085fb4682 100644 --- a/cmd/symbols/types/types.go +++ b/cmd/symbols/types/types.go @@ -24,11 +24,29 @@ type SqliteConfig struct { MaxConcurrentlyIndexing int } +func aliasEnvVar(oldName, newName string) { + if os.Getenv(newName) != "" { + return // prefer using the new name + } + oldValue := os.Getenv(oldName) + if oldValue == "" { + return // old name was not set + } + // New name not in use, but old name is, so update the env. + _ = os.Setenv(newName, oldValue) +} + func LoadSqliteConfig(baseConfig env.BaseConfig, ctags CtagsConfig, repositoryFetcher RepositoryFetcherConfig) SqliteConfig { + // Variable was renamed to have SYMBOLS_ prefix to avoid a conflict with the same env var name + // in searcher when running as a single binary. The old name is treated as an alias to prevent + // customer environments from breaking if they still use it, because we have no way of migrating + // environment variables today. + aliasEnvVar("CACHE_DIR", "SYMBOLS_CACHE_DIR") + return SqliteConfig{ Ctags: ctags, RepositoryFetcher: repositoryFetcher, - CacheDir: baseConfig.Get("CACHE_DIR", "/tmp/symbols-cache", "directory in which to store cached symbols"), + CacheDir: baseConfig.Get("SYMBOLS_CACHE_DIR", "/tmp/symbols-cache", "directory in which to store cached symbols"), CacheSizeMB: baseConfig.GetInt("SYMBOLS_CACHE_SIZE_MB", "100000", "maximum size of the disk cache (in megabytes)"), NumCtagsProcesses: baseConfig.GetInt("CTAGS_PROCESSES", strconv.Itoa(runtime.GOMAXPROCS(0)), "number of concurrent parser processes to run"), RequestBufferSize: baseConfig.GetInt("REQUEST_BUFFER_SIZE", "8192", "maximum size of buffered parser request channel"), @@ -78,8 +96,14 @@ type RepositoryFetcherConfig struct { } func LoadRepositoryFetcherConfig(baseConfig env.BaseConfig) RepositoryFetcherConfig { + // Variable was renamed to have SYMBOLS_ prefix to avoid a conflict with the same env var name + // in searcher when running as a single binary. The old name is treated as an alias to prevent + // customer environments from breaking if they still use it, because we have no way of migrating + // environment variables today. + aliasEnvVar("MAX_TOTAL_PATHS_LENGTH", "SYMBOLS_MAX_TOTAL_PATHS_LENGTH") + return RepositoryFetcherConfig{ - MaxTotalPathsLength: baseConfig.GetInt("MAX_TOTAL_PATHS_LENGTH", "100000", "maximum sum of lengths of all paths in a single call to git archive"), + MaxTotalPathsLength: baseConfig.GetInt("SYMBOLS_MAX_TOTAL_PATHS_LENGTH", "100000", "maximum sum of lengths of all paths in a single call to git archive"), MaxFileSizeKb: baseConfig.GetInt("MAX_FILE_SIZE_KB", "1000", "maximum file size in KB, the contents of bigger files are ignored"), } } diff --git a/cmd/worker/main.go b/cmd/worker/main.go index bd26bca04d1e..8bfc22236bb8 100644 --- a/cmd/worker/main.go +++ b/cmd/worker/main.go @@ -1,30 +1,10 @@ package main import ( - "os" - - "github.com/sourcegraph/log" - + "github.com/sourcegraph/sourcegraph/cmd/sourcegraph-oss/osscmd" "github.com/sourcegraph/sourcegraph/cmd/worker/shared" - "github.com/sourcegraph/sourcegraph/internal/authz" - "github.com/sourcegraph/sourcegraph/internal/env" - "github.com/sourcegraph/sourcegraph/internal/observation" - "github.com/sourcegraph/sourcegraph/internal/version" ) func main() { - liblog := log.Init(log.Resource{ - Name: env.MyName, - Version: version.Version(), - }) - defer liblog.Sync() - - logger := log.Scoped("worker", "worker oss edition") - observationCtx := observation.NewContext(logger) - - authz.SetProviders(true, []authz.Provider{}) - if err := shared.Start(observationCtx, nil, nil, nil); err != nil { - logger.Error(err.Error()) - os.Exit(1) - } + osscmd.DeprecatedSingleServiceMainOSS(shared.Service) } diff --git a/cmd/worker/shared/config.go b/cmd/worker/shared/config.go index 55070e8a83d9..eb0e534819e7 100644 --- a/cmd/worker/shared/config.go +++ b/cmd/worker/shared/config.go @@ -3,6 +3,7 @@ package shared import ( "strings" + "github.com/sourcegraph/sourcegraph/cmd/worker/job" "github.com/sourcegraph/sourcegraph/internal/env" "github.com/sourcegraph/sourcegraph/lib/errors" ) @@ -14,6 +15,8 @@ type Config struct { env.BaseConfig names []string + Jobs map[string]job.Job + JobAllowlist []string JobBlocklist []string } diff --git a/cmd/worker/shared/main.go b/cmd/worker/shared/main.go index 88104192de01..181fe7cbe6c7 100644 --- a/cmd/worker/shared/main.go +++ b/cmd/worker/shared/main.go @@ -10,7 +10,8 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" - "github.com/sourcegraph/log" + + "github.com/sourcegraph/sourcegraph/internal/env" "github.com/sourcegraph/sourcegraph/internal/goroutine/recorder" "github.com/sourcegraph/sourcegraph/cmd/worker/internal/codeintel" @@ -23,19 +24,15 @@ import ( "github.com/sourcegraph/sourcegraph/cmd/worker/internal/zoektrepos" "github.com/sourcegraph/sourcegraph/cmd/worker/job" workerdb "github.com/sourcegraph/sourcegraph/cmd/worker/shared/init/db" - "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/database" - "github.com/sourcegraph/sourcegraph/internal/debugserver" "github.com/sourcegraph/sourcegraph/internal/encryption/keyring" - "github.com/sourcegraph/sourcegraph/internal/env" "github.com/sourcegraph/sourcegraph/internal/goroutine" "github.com/sourcegraph/sourcegraph/internal/httpserver" - "github.com/sourcegraph/sourcegraph/internal/logging" "github.com/sourcegraph/sourcegraph/internal/observation" "github.com/sourcegraph/sourcegraph/internal/oobmigration" "github.com/sourcegraph/sourcegraph/internal/oobmigration/migrations" - "github.com/sourcegraph/sourcegraph/internal/profiler" - "github.com/sourcegraph/sourcegraph/internal/tracer" + "github.com/sourcegraph/sourcegraph/internal/service" + "github.com/sourcegraph/sourcegraph/internal/symbols" "github.com/sourcegraph/sourcegraph/lib/errors" ) @@ -48,8 +45,9 @@ type namedBackgroundRoutine struct { JobName string } -// Start runs the worker. -func Start(observationCtx *observation.Context, additionalJobs map[string]job.Job, registerEnterpriseMigrators oobmigration.RegisterMigratorsFunc, enterpriseInit EnterpriseInit) error { +func LoadConfig(additionalJobs map[string]job.Job, registerEnterpriseMigrators oobmigration.RegisterMigratorsFunc) *Config { + symbols.LoadConfig() + registerMigrators := oobmigration.ComposeRegisterMigratorsFuncs(migrations.RegisterOSSMigrators, registerEnterpriseMigrators) builtins := map[string]job.Job{ @@ -63,26 +61,31 @@ func Start(observationCtx *observation.Context, additionalJobs map[string]job.Jo "outbound-webhook-sender": outboundwebhooks.NewSender(), } - jobs := map[string]job.Job{} + var config Config + config.Jobs = map[string]job.Job{} + for name, job := range builtins { - jobs[name] = job + config.Jobs[name] = job } for name, job := range additionalJobs { - jobs[name] = job + config.Jobs[name] = job } // Setup environment variables - loadConfigs(jobs) + loadConfigs(config.Jobs) + + // Validate environment variables + if err := validateConfigs(config.Jobs); err != nil { + config.AddError(err) + } - env.Lock() - env.HandleHelpFlag() - conf.Init() - logging.Init() //nolint:staticcheck // Deprecated, but logs unmigrated to sourcegraph/log look really bad without this. - tracer.Init(log.Scoped("tracer", "internal tracer package"), conf.DefaultClient()) - profiler.Init() + return &config +} - if err := keyring.Init(context.Background()); err != nil { - return errors.Wrap(err, "Failed to intialise keyring") +// Start runs the worker. +func Start(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, config *Config, enterpriseInit EnterpriseInit) error { + if err := keyring.Init(ctx); err != nil { + return errors.Wrap(err, "initializing keyring") } if enterpriseInit != nil { @@ -94,23 +97,14 @@ func Start(observationCtx *observation.Context, additionalJobs map[string]job.Jo enterpriseInit(db) } - // Start debug server - ready := make(chan struct{}) - go debugserver.NewServerRoutine(ready).Start() - - // Validate environment variables - if err := validateConfigs(jobs); err != nil { - return err - } - // Emit metrics to help site admins detect instances that accidentally // omit a job from from the instance's deployment configuration. - emitJobCountMetrics(jobs) + emitJobCountMetrics(config.Jobs) // Create the background routines that the worker will monitor for its // lifetime. There may be a non-trivial startup time on this step as we // connect to external databases, wait for migrations, etc. - allRoutinesWithJobNames, err := createBackgroundRoutines(observationCtx, jobs) + allRoutinesWithJobNames, err := createBackgroundRoutines(observationCtx, config.Jobs) if err != nil { return err } @@ -138,7 +132,7 @@ func Start(observationCtx *observation.Context, additionalJobs map[string]job.Jo // We're all set up now // Respond positively to ready checks - close(ready) + ready() // This method blocks while the app is live - the following return is only to appease // the type checker. @@ -147,7 +141,7 @@ func Start(observationCtx *observation.Context, additionalJobs map[string]job.Jo allRoutines = append(allRoutines, r.Routine) } - goroutine.MonitorBackgroundRoutines(context.Background(), allRoutines...) + goroutine.MonitorBackgroundRoutines(ctx, allRoutines...) return nil } diff --git a/cmd/worker/shared/service.go b/cmd/worker/shared/service.go new file mode 100644 index 000000000000..c26e4031113c --- /dev/null +++ b/cmd/worker/shared/service.go @@ -0,0 +1,24 @@ +package shared + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" +) + +type svc struct{} + +func (svc) Name() string { return "worker" } + +func (svc) Configure() (env.Config, []debugserver.Endpoint) { + return LoadConfig(nil, nil), nil +} + +func (svc) Start(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, config env.Config) error { + return Start(ctx, observationCtx, ready, config.(*Config), nil) +} + +var Service service.Service = svc{} diff --git a/dev/check/go-dbconn-import.sh b/dev/check/go-dbconn-import.sh index c3bab06cc465..1b2ef53c091c 100755 --- a/dev/check/go-dbconn-import.sh +++ b/dev/check/go-dbconn-import.sh @@ -27,6 +27,9 @@ allowed_prefix=( github.com/sourcegraph/sourcegraph/cmd/symbols # Transitively depends on zoekt package which imports but does not use DB github.com/sourcegraph/sourcegraph/cmd/searcher + # Main entrypoints for running all services, so they must be allowed to import it. + github.com/sourcegraph/sourcegraph/cmd/sourcegraph-oss + github.com/sourcegraph/sourcegraph/enterprise/cmd/sourcegraph ) # Create regex ^(a|b|c) diff --git a/dev/ci/runtype/runtype.go b/dev/ci/runtype/runtype.go index 19b6c29459b4..a2362140705d 100644 --- a/dev/ci/runtype/runtype.go +++ b/dev/ci/runtype/runtype.go @@ -18,9 +18,10 @@ const ( // Nightly builds - must be first because they take precedence - ReleaseNightly // release branch nightly healthcheck builds - BextNightly // browser extension nightly build - VsceNightly // vs code extension nightly build + ReleaseNightly // release branch nightly healthcheck builds + BextNightly // browser extension nightly build + VsceNightly // vs code extension nightly build + AppSnapshotRelease // app snapshot build // Release branches @@ -107,6 +108,12 @@ func (t RunType) Matcher() *RunTypeMatcher { BranchExact: true, } + case AppSnapshotRelease: + return &RunTypeMatcher{ + Branch: "app/release-snapshot", + BranchExact: true, + } + case TaggedRelease: return &RunTypeMatcher{ TagPrefix: "v", @@ -175,6 +182,8 @@ func (t RunType) String() string { return "Browser extension nightly release build" case VsceNightly: return "VS Code extension nightly release build" + case AppSnapshotRelease: + return "App snapshot release" case TaggedRelease: return "Tagged release" case ReleaseBranch: diff --git a/dev/ci/runtype/runtype_test.go b/dev/ci/runtype/runtype_test.go index 2a054bae9783..9af449bc4746 100644 --- a/dev/ci/runtype/runtype_test.go +++ b/dev/ci/runtype/runtype_test.go @@ -66,6 +66,12 @@ func TestComputeRunType(t *testing.T) { branch: "vsce/release", }, want: VsceReleaseBranch, + }, { + name: "app release", + args: args{ + branch: "app/release-snapshot", + }, + want: AppSnapshotRelease, }, { name: "release nightly", args: args{ diff --git a/doc/dependency_decisions.yml b/doc/dependency_decisions.yml index 29eff83b98f5..e98fc0699819 100644 --- a/doc/dependency_decisions.yml +++ b/doc/dependency_decisions.yml @@ -452,3 +452,10 @@ :why: Inference broken, LICENSE file lives at https://www.npmjs.com/package/stdin#license :versions: [] :when: 2023-01-09 04:58:41.642155000 Z +- - :license + - github.com/xi2/xz + - Public domain + - :who: + :why: + :versions: [] + :when: 2023-01-12 01:20:17.504279000 Z diff --git a/doc/dev/background-information/ci/reference.md b/doc/dev/background-information/ci/reference.md index 7e878cda5e81..6bcc93af9272 100644 --- a/doc/dev/background-information/ci/reference.md +++ b/doc/dev/background-information/ci/reference.md @@ -133,6 +133,15 @@ Base pipeline (more steps might be included based on branch changes): - Stylelint (all) - Upload build trace +### App snapshot release + +The run type for branches matching `app/release-snapshot` (exact match). + +Base pipeline (more steps might be included based on branch changes): + +- App release +- Upload build trace + ### Tagged release The run type for tags starting with `v`. diff --git a/doc/dev/background-information/sg/reference.md b/doc/dev/background-information/sg/reference.md index f630075180a5..d4911750541c 100644 --- a/doc/dev/background-information/sg/reference.md +++ b/doc/dev/background-information/sg/reference.md @@ -37,10 +37,12 @@ Available comamndsets in `sg.config.yaml`: * enterprise-codeinsights * enterprise-codeintel 🧠 * enterprise-e2e +* enterprise-single-program * iam * monitoring * monitoring-alerts * oss +* oss-single-program * oss-web-standalone * oss-web-standalone-prod * otel @@ -122,6 +124,8 @@ Available commands in `sg.config.yaml`: * repo-updater * searcher * server: Run an all-in-one sourcegraph/server image +* sourcegraph-oss: Single program (Go static binary) distribution, OSS variant +* sourcegraph: Single program (Go static binary) distribution * storybook * symbols * syntax-highlighter diff --git a/enterprise/cmd/executor/internal/command/observability.go b/enterprise/cmd/executor/internal/command/observability.go index fb0f468d0261..8bc17413a73e 100644 --- a/enterprise/cmd/executor/internal/command/observability.go +++ b/enterprise/cmd/executor/internal/command/observability.go @@ -28,7 +28,7 @@ type Operations struct { } func NewOperations(observationCtx *observation.Context) *Operations { - metrics := metrics.NewREDMetrics( + redMetrics := metrics.NewREDMetrics( observationCtx.Registerer, "apiworker_command", metrics.WithLabels("op"), @@ -39,7 +39,7 @@ func NewOperations(observationCtx *observation.Context) *Operations { return observationCtx.Operation(observation.Op{ Name: fmt.Sprintf("apiworker.%s", opName), MetricLabelValues: []string{opName}, - Metrics: metrics, + Metrics: redMetrics, }) } @@ -47,13 +47,17 @@ func NewOperations(observationCtx *observation.Context) *Operations { Name: "src_executor_run_lock_wait_total", Help: "The number of milliseconds spent waiting for the run lock.", }) - observationCtx.Registerer.MustRegister(runLockWaitTotal) + // TODO(sqs): TODO(single-binary): We use IgnoreDuplicate here to allow running 2 executor instances in + // the same process, but ideally we shouldn't need IgnoreDuplicate as that is a bit of a hack. + runLockWaitTotal = metrics.MustRegisterIgnoreDuplicate(observationCtx.Registerer, runLockWaitTotal) runLockHeldTotal := prometheus.NewCounter(prometheus.CounterOpts{ Name: "src_executor_run_lock_held_total", Help: "The number of milliseconds spent holding the run lock.", }) - observationCtx.Registerer.MustRegister(runLockHeldTotal) + // TODO(sqs): TODO(single-binary): We use IgnoreDuplicate here to allow running 2 executor instances in + // the same process, but ideally we shouldn't need IgnoreDuplicate as that is a bit of a hack. + runLockHeldTotal = metrics.MustRegisterIgnoreDuplicate(observationCtx.Registerer, runLockHeldTotal) return &Operations{ SetupGitInit: op("setup.git.init"), diff --git a/enterprise/cmd/executor/internal/run/util.go b/enterprise/cmd/executor/internal/run/util.go index 596cb82bbd59..85d2cc279b0f 100644 --- a/enterprise/cmd/executor/internal/run/util.go +++ b/enterprise/cmd/executor/internal/run/util.go @@ -1,6 +1,7 @@ package run import ( + "bytes" "context" "fmt" "net/url" @@ -19,6 +20,7 @@ import ( "github.com/sourcegraph/sourcegraph/internal/observation" "github.com/sourcegraph/sourcegraph/internal/version" "github.com/sourcegraph/sourcegraph/internal/workerutil" + "github.com/sourcegraph/sourcegraph/lib/errors" ) func newQueueTelemetryOptions(ctx context.Context, useFirecracker bool, logger log.Logger) queue.TelemetryOptions { @@ -56,39 +58,39 @@ func newQueueTelemetryOptions(ctx context.Context, useFirecracker bool, logger l } func getGitVersion(ctx context.Context) (string, error) { - cmd := exec.CommandContext(ctx, "git", "version") - out, err := cmd.Output() + out, err := execOutput(ctx, "git", "version") if err != nil { return "", err } - return strings.TrimPrefix(strings.TrimSpace(string(out)), "git version "), nil + return strings.TrimPrefix(out, "git version "), nil } func getSrcVersion(ctx context.Context) (string, error) { - cmd := exec.CommandContext(ctx, "src", "version", "-client-only") - out, err := cmd.Output() + out, err := execOutput(ctx, "src", "version", "-client-only") if err != nil { return "", err } - return strings.TrimPrefix(strings.TrimSpace(string(out)), "Current version: "), nil + return strings.TrimPrefix(out, "Current version: "), nil } func getDockerVersion(ctx context.Context) (string, error) { - cmd := exec.CommandContext(ctx, "docker", "version", "-f", "{{.Server.Version}}") - out, err := cmd.Output() - if err != nil { - return "", err - } - return strings.TrimSpace(string(out)), nil + return execOutput(ctx, "docker", "version", "-f", "{{.Server.Version}}") } func getIgniteVersion(ctx context.Context) (string, error) { - cmd := exec.CommandContext(ctx, "ignite", "version", "-o", "short") - out, err := cmd.Output() - if err != nil { - return "", err + return execOutput(ctx, "ignite", "version", "-o", "short") +} + +func execOutput(ctx context.Context, name string, args ...string) (string, error) { + var buf bytes.Buffer + cmd := exec.CommandContext(ctx, name, args...) + cmd.Stderr = &buf + cmd.Stdout = &buf + if err := cmd.Run(); err != nil { + cmdLine := strings.Join(append([]string{name}, args...), " ") + return "", errors.Wrap(err, fmt.Sprintf("'%s': %s", cmdLine, buf.String())) } - return strings.TrimSpace(string(out)), nil + return strings.TrimSpace(buf.String()), nil } func apiWorkerOptions(c *config.Config, queueTelemetryOptions queue.TelemetryOptions) apiworker.Options { diff --git a/enterprise/cmd/executor/main.go b/enterprise/cmd/executor/main.go index d34a05dc2284..8672680d551d 100644 --- a/enterprise/cmd/executor/main.go +++ b/enterprise/cmd/executor/main.go @@ -1,9 +1,10 @@ package main import ( - shared "github.com/sourcegraph/sourcegraph/enterprise/cmd/executor/shared" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/executor/shared" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/sourcegraph/enterprisecmd/executorcmd" ) func main() { - shared.Main() + executorcmd.DeprecatedSingleServiceMainEnterprise(shared.Service) } diff --git a/enterprise/cmd/executor/shared/service.go b/enterprise/cmd/executor/shared/service.go new file mode 100644 index 000000000000..40d2a93a01b5 --- /dev/null +++ b/enterprise/cmd/executor/shared/service.go @@ -0,0 +1,47 @@ +package shared + +import ( + "context" + + "github.com/sourcegraph/log" + + "github.com/sourcegraph/sourcegraph/enterprise/cmd/executor/internal/config" + "github.com/sourcegraph/sourcegraph/internal/conf/deploy" + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" +) + +type svc struct{} + +func (svc) Name() string { return "executor" } + +func (svc) Configure() (env.Config, []debugserver.Endpoint) { + var config config.Config + config.Load() + return &config, nil +} + +func (svc) Start(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, cfg env.Config) error { + config := cfg.(*config.Config) + + // TODO(sqs) HACK(sqs): run executors for both queues + if deploy.IsDeployTypeSingleProgram(deploy.Type()) { + otherConfig := *config + if config.QueueName == "batches" { + otherConfig.QueueName = "codeintel" + } else { + otherConfig.QueueName = "batches" + } + go func() { + if err := Main(ctx, observationCtx, ready, &otherConfig); err != nil { + observationCtx.Logger.Fatal("executor for other queue failed", log.Error(err)) + } + }() + } + + return Main(ctx, observationCtx, ready, config) +} + +var Service service.Service = svc{} diff --git a/enterprise/cmd/executor/shared/shared.go b/enterprise/cmd/executor/shared/shared.go index 587cfb9fbc3c..1d6c618e7242 100644 --- a/enterprise/cmd/executor/shared/shared.go +++ b/enterprise/cmd/executor/shared/shared.go @@ -12,33 +12,19 @@ import ( "github.com/sourcegraph/sourcegraph/enterprise/cmd/executor/internal/config" "github.com/sourcegraph/sourcegraph/enterprise/cmd/executor/internal/run" "github.com/sourcegraph/sourcegraph/internal/env" - "github.com/sourcegraph/sourcegraph/internal/hostname" - "github.com/sourcegraph/sourcegraph/internal/logging" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" "github.com/sourcegraph/sourcegraph/internal/version" // This import is required to force a binary hash change when the src-cli version is bumped. _ "github.com/sourcegraph/sourcegraph/internal/src-cli" ) -func Main() { - cfg := &config.Config{} - cfg.Load() - - env.Lock() - - logging.Init() //nolint:staticcheck // Deprecated, but logs unmigrated to sourcegraph/log look really bad without this. - liblog := log.Init(log.Resource{ - Name: env.MyName, - Version: version.Version(), - InstanceID: hostname.Get(), - }) - defer liblog.Sync() - - logger := log.Scoped("executor", "the executor service polls the public Sourcegraph frontend API for work to perform") +func Main(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, cfg *config.Config) error { makeActionHandler := func(handler func(cliCtx *cli.Context, logger log.Logger, config *config.Config) error) func(*cli.Context) error { return func(ctx *cli.Context) error { - return handler(ctx, logger, cfg) + return handler(ctx, observationCtx.Logger, cfg) } } @@ -159,8 +145,5 @@ func Main() { }, } - if err := app.RunContext(context.Background(), os.Args); err != nil { - println(err.Error()) - os.Exit(1) - } + return app.RunContext(ctx, os.Args) } diff --git a/enterprise/cmd/frontend/internal/codeintel/init.go b/enterprise/cmd/frontend/internal/codeintel/init.go index f7f158c87f8a..00ef0f1f2e51 100644 --- a/enterprise/cmd/frontend/internal/codeintel/init.go +++ b/enterprise/cmd/frontend/internal/codeintel/init.go @@ -20,7 +20,7 @@ import ( "github.com/sourcegraph/sourcegraph/internal/observation" ) -func init() { +func LoadConfig() { ConfigInst.Load() } diff --git a/enterprise/cmd/frontend/internal/executorqueue/init.go b/enterprise/cmd/frontend/internal/executorqueue/init.go index 3eb8a1b94a7e..44615bbc719d 100644 --- a/enterprise/cmd/frontend/internal/executorqueue/init.go +++ b/enterprise/cmd/frontend/internal/executorqueue/init.go @@ -2,11 +2,14 @@ package executorqueue import ( "context" + "strconv" "github.com/sourcegraph/log" "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" + "github.com/sourcegraph/sourcegraph/internal/conf/deploy" "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/env" metricsstore "github.com/sourcegraph/sourcegraph/internal/metrics/store" "github.com/sourcegraph/sourcegraph/internal/observation" @@ -16,6 +19,16 @@ import ( codeintelqueue "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/executorqueue/queues/codeintel" ) +func queueDisableAccessTokenDefault() string { + isSingleProgram := deploy.IsDeployTypeSingleProgram(deploy.Type()) + if isSingleProgram { + return "true" + } + return "false" +} + +var queueDisableAccessToken = env.Get("EXECUTOR_QUEUE_DISABLE_ACCESS_TOKEN_INSECURE", queueDisableAccessTokenDefault(), "Disable usage of an access token between executors and Sourcegraph (DANGEROUS") + // Init initializes the executor endpoints required for use with the executor service. func Init( ctx context.Context, @@ -27,9 +40,30 @@ func Init( codeintelUploadHandler := enterpriseServices.NewCodeIntelUploadHandler(false) batchesWorkspaceFileGetHandler := enterpriseServices.BatchesChangesFileGetHandler batchesWorkspaceFileExistsHandler := enterpriseServices.BatchesChangesFileGetHandler - accessToken := func() string { return conf.SiteConfig().ExecutorsAccessToken } + logger := log.Scoped("executorqueue", "") + accessToken := func() (token string, accessTokenEnabled bool) { + token = conf.SiteConfig().ExecutorsAccessToken + wantDisableAccessToken, _ := strconv.ParseBool(queueDisableAccessToken) + + if wantDisableAccessToken { + isSingleProgram := deploy.IsDeployTypeSingleProgram(deploy.Type()) + isSingleDockerContainer := deploy.IsDeployTypeSingleDockerContainer(deploy.Type()) + allowedDeployType := isSingleProgram || isSingleDockerContainer || env.InsecureDev + if allowedDeployType && token == "" { + // Disable the access token. + return "", false + } + // Respect the access token. + logger.Warn("access token may only be disabled if executors.accessToken is empty in site config AND the deployment type is single-program, single-docker-container, or dev") + return token, true + } + + // Respect the access token. + return token, true + } + metricsStore := metricsstore.NewDistributedStore("executors:") executorStore := db.Executors() diff --git a/enterprise/cmd/frontend/internal/executorqueue/queuehandler.go b/enterprise/cmd/frontend/internal/executorqueue/queuehandler.go index a086663d235b..86e763189d0e 100644 --- a/enterprise/cmd/frontend/internal/executorqueue/queuehandler.go +++ b/enterprise/cmd/frontend/internal/executorqueue/queuehandler.go @@ -17,7 +17,7 @@ import ( metricsstore "github.com/sourcegraph/sourcegraph/internal/metrics/store" ) -func newExecutorQueueHandler(logger log.Logger, db database.DB, queueHandlers []handler.ExecutorHandler, accessToken func() string, uploadHandler http.Handler, batchesWorkspaceFileGetHandler http.Handler, batchesWorkspaceFileExistsHandler http.Handler) func() http.Handler { +func newExecutorQueueHandler(logger log.Logger, db database.DB, queueHandlers []handler.ExecutorHandler, accessToken func() (token string, enabled bool), uploadHandler http.Handler, batchesWorkspaceFileGetHandler http.Handler, batchesWorkspaceFileExistsHandler http.Handler) func() http.Handler { metricsStore := metricsstore.NewDistributedStore("executors:") executorStore := db.Executors() gitserverClient := gitserver.NewClient(db) @@ -54,9 +54,10 @@ func newExecutorQueueHandler(logger log.Logger, db database.DB, queueHandlers [] // with the correct "token-executor " value. This should only be used // for internal _services_, not users, in which a shared key exchange can be // done so safely. -func authMiddleware(accessToken func() string, next http.Handler) http.Handler { +func authMiddleware(accessToken func() (token string, enabled bool), next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if validateExecutorToken(w, r, accessToken()) { + token, tokenEnabled := accessToken() + if !tokenEnabled || validateExecutorToken(w, r, token) { next.ServeHTTP(w, r) } }) diff --git a/enterprise/cmd/frontend/internal/executorqueue/queuehandler_test.go b/enterprise/cmd/frontend/internal/executorqueue/queuehandler_test.go index 769a3cd14431..66c983913f35 100644 --- a/enterprise/cmd/frontend/internal/executorqueue/queuehandler_test.go +++ b/enterprise/cmd/frontend/internal/executorqueue/queuehandler_test.go @@ -12,7 +12,7 @@ func TestInternalProxyAuthTokenMiddleware(t *testing.T) { accessToken := "hunter2" ts := httptest.NewServer(authMiddleware( - func() string { return accessToken }, + func() (token string, tokenEnabled bool) { return accessToken, true }, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) }), diff --git a/enterprise/cmd/frontend/internal/executorqueue/queues/batches/queue.go b/enterprise/cmd/frontend/internal/executorqueue/queues/batches/queue.go index 64b64b2a7d13..db4f75ec59f7 100644 --- a/enterprise/cmd/frontend/internal/executorqueue/queues/batches/queue.go +++ b/enterprise/cmd/frontend/internal/executorqueue/queues/batches/queue.go @@ -13,7 +13,7 @@ import ( "github.com/sourcegraph/sourcegraph/internal/observation" ) -func QueueOptions(observationCtx *observation.Context, db database.DB, _ func() string) handler.QueueOptions[*btypes.BatchSpecWorkspaceExecutionJob] { +func QueueOptions(observationCtx *observation.Context, db database.DB, _ func() (token string, tokenEnabled bool)) handler.QueueOptions[*btypes.BatchSpecWorkspaceExecutionJob] { logger := log.Scoped("executor-queue.batches", "The executor queue handlers for the batches queue") recordTransformer := func(ctx context.Context, version string, record *btypes.BatchSpecWorkspaceExecutionJob, _ handler.ResourceMetadata) (apiclient.Job, error) { batchesStore := store.New(db, observationCtx, nil) diff --git a/enterprise/cmd/frontend/internal/executorqueue/queues/codeintel/queue.go b/enterprise/cmd/frontend/internal/executorqueue/queues/codeintel/queue.go index b6cc4af38b1a..1e98683bd39e 100644 --- a/enterprise/cmd/frontend/internal/executorqueue/queues/codeintel/queue.go +++ b/enterprise/cmd/frontend/internal/executorqueue/queues/codeintel/queue.go @@ -12,9 +12,9 @@ import ( "github.com/sourcegraph/sourcegraph/internal/workerutil/dbworker/store" ) -func QueueOptions(observationCtx *observation.Context, db database.DB, accessToken func() string) handler.QueueOptions[types.Index] { +func QueueOptions(observationCtx *observation.Context, db database.DB, accessToken func() (token string, tokenEnabled bool)) handler.QueueOptions[types.Index] { recordTransformer := func(ctx context.Context, _ string, record types.Index, resourceMetadata handler.ResourceMetadata) (apiclient.Job, error) { - return transformRecord(ctx, db, record, resourceMetadata, accessToken()) + return transformRecord(ctx, db, record, resourceMetadata, accessToken) } store := store.New(observationCtx, db.Handle(), autoindexing.IndexWorkerStoreOptions) diff --git a/enterprise/cmd/frontend/internal/executorqueue/queues/codeintel/transform.go b/enterprise/cmd/frontend/internal/executorqueue/queues/codeintel/transform.go index 22229426ed5d..3d81b2a51533 100644 --- a/enterprise/cmd/frontend/internal/executorqueue/queues/codeintel/transform.go +++ b/enterprise/cmd/frontend/internal/executorqueue/queues/codeintel/transform.go @@ -38,7 +38,7 @@ func (e *accessLogTransformer) Create(ctx context.Context, log *database.Executo return e.ExecutorSecretAccessLogCreator.Create(ctx, log) } -func transformRecord(ctx context.Context, db database.DB, index types.Index, resourceMetadata handler.ResourceMetadata, accessToken string) (apiclient.Job, error) { +func transformRecord(ctx context.Context, db database.DB, index types.Index, resourceMetadata handler.ResourceMetadata, accessToken func() (token string, tokenEnabled bool)) (apiclient.Job, error) { resourceEnvironment := makeResourceEnvironment(resourceMetadata) var secrets []*database.ExecutorSecret @@ -94,8 +94,9 @@ func transformRecord(ctx context.Context, db database.DB, index types.Index, res }) } + token, _ := accessToken() frontendURL := conf.ExecutorsFrontendURL() - authorizationHeader := makeAuthHeaderValue(accessToken) + authorizationHeader := makeAuthHeaderValue(token) redactedAuthorizationHeader := makeAuthHeaderValue("REDACTED") srcCliImage := fmt.Sprintf("%s:%s", conf.ExecutorsSrcCLIImage(), conf.ExecutorsSrcCLIImageTag()) @@ -145,7 +146,7 @@ func transformRecord(ctx context.Context, db database.DB, index types.Index, res // Authorization header to src-cli, which we trust not to ship the // values to a third party, but not to trust to ensure the values // are absent from the command's stdout or stderr streams. - accessToken: "PASSWORD_REMOVED", + token: "PASSWORD_REMOVED", } // 🚨 SECURITY: Catch uses of executor secrets from the executor secret store maps.Copy(allRedactedValues, redactedEnvVars) diff --git a/enterprise/cmd/frontend/internal/executorqueue/queues/codeintel/transform_test.go b/enterprise/cmd/frontend/internal/executorqueue/queues/codeintel/transform_test.go index 089977222fdf..24ee7058a78c 100644 --- a/enterprise/cmd/frontend/internal/executorqueue/queues/codeintel/transform_test.go +++ b/enterprise/cmd/frontend/internal/executorqueue/queues/codeintel/transform_test.go @@ -80,7 +80,8 @@ func TestTransformRecord(t *testing.T) { conf.Mock(nil) }) - job, err := transformRecord(context.Background(), db, index, testCase.resourceMetadata, "hunter2") + accessToken := func() (token string, tokenEnabled bool) { return "hunter2", true } + job, err := transformRecord(context.Background(), db, index, testCase.resourceMetadata, accessToken) if err != nil { t.Fatalf("unexpected error transforming record: %s", err) } @@ -176,7 +177,8 @@ func TestTransformRecordWithoutIndexer(t *testing.T) { conf.Mock(nil) }) - job, err := transformRecord(context.Background(), db, index, handler.ResourceMetadata{}, "hunter2") + accessToken := func() (token string, tokenEnabled bool) { return "hunter2", true } + job, err := transformRecord(context.Background(), db, index, handler.ResourceMetadata{}, accessToken) if err != nil { t.Fatalf("unexpected error transforming record: %s", err) } @@ -307,7 +309,8 @@ func TestTransformRecordWithSecrets(t *testing.T) { conf.Mock(nil) }) - job, err := transformRecord(context.Background(), db, index, testCase.resourceMetadata, "hunter2") + accessToken := func() (token string, tokenEnabled bool) { return "hunter2", true } + job, err := transformRecord(context.Background(), db, index, testCase.resourceMetadata, accessToken) if err != nil { t.Fatalf("unexpected error transforming record: %s", err) } @@ -391,7 +394,8 @@ func TestTransformRecordDockerAuthConfig(t *testing.T) { }, 0, nil) db.ExecutorSecretAccessLogsFunc.SetDefaultReturn(database.NewMockExecutorSecretAccessLogStore()) - job, err := transformRecord(context.Background(), db, types.Index{ID: 42}, handler.ResourceMetadata{}, "hunter2") + accessToken := func() (token string, tokenEnabled bool) { return "hunter2", true } + job, err := transformRecord(context.Background(), db, types.Index{ID: 42}, handler.ResourceMetadata{}, accessToken) if err != nil { t.Fatal(err) } diff --git a/enterprise/cmd/frontend/main.go b/enterprise/cmd/frontend/main.go index b0558c692e00..6d6d1845c6b5 100644 --- a/enterprise/cmd/frontend/main.go +++ b/enterprise/cmd/frontend/main.go @@ -2,20 +2,10 @@ package main import ( - shared "github.com/sourcegraph/sourcegraph/cmd/frontend/shared" - _ "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/registry" - shared_enterprise "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/shared" - "github.com/sourcegraph/sourcegraph/internal/env" - "github.com/sourcegraph/sourcegraph/internal/oobmigration" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/shared" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/sourcegraph/enterprisecmd" ) func main() { - env.Lock() - env.HandleHelpFlag() - - shared.Main(shared_enterprise.EnterpriseSetupHook) -} - -func init() { - oobmigration.ReturnEnterpriseMigrations = true + enterprisecmd.DeprecatedSingleServiceMainEnterprise(shared.Service) } diff --git a/enterprise/cmd/frontend/shared/service.go b/enterprise/cmd/frontend/shared/service.go new file mode 100644 index 000000000000..f8ac900cc7fb --- /dev/null +++ b/enterprise/cmd/frontend/shared/service.go @@ -0,0 +1,31 @@ +package shared + +import ( + "context" + + frontend_shared "github.com/sourcegraph/sourcegraph/cmd/frontend/shared" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/codeintel" + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" + + _ "github.com/sourcegraph/sourcegraph/cmd/frontend/registry/api" + _ "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/registry" +) + +type svc struct{} + +func (svc) Name() string { return "frontend" } + +func (svc) Configure() (env.Config, []debugserver.Endpoint) { + frontend_shared.CLILoadConfig() + codeintel.LoadConfig() + return nil, nil +} + +func (svc) Start(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, config env.Config) error { + return frontend_shared.CLIMain(ctx, observationCtx, ready, EnterpriseSetupHook) +} + +var Service service.Service = svc{} diff --git a/enterprise/cmd/gitserver/main.go b/enterprise/cmd/gitserver/main.go index 501ea25f2759..bbd8343069ab 100644 --- a/enterprise/cmd/gitserver/main.go +++ b/enterprise/cmd/gitserver/main.go @@ -1,14 +1,11 @@ +// Command frontend is the enterprise frontend program. package main import ( - "github.com/sourcegraph/sourcegraph/cmd/gitserver/shared" - enterprise_shared "github.com/sourcegraph/sourcegraph/enterprise/cmd/gitserver/shared" - "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/gitserver/shared" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/sourcegraph/enterprisecmd" ) func main() { - env.Lock() - env.HandleHelpFlag() - - shared.Main(enterprise_shared.EnterpriseInit) + enterprisecmd.DeprecatedSingleServiceMainEnterprise(shared.Service) } diff --git a/enterprise/cmd/gitserver/shared/service.go b/enterprise/cmd/gitserver/shared/service.go new file mode 100644 index 000000000000..554fca6d8605 --- /dev/null +++ b/enterprise/cmd/gitserver/shared/service.go @@ -0,0 +1,25 @@ +package shared + +import ( + "context" + + shared "github.com/sourcegraph/sourcegraph/cmd/gitserver/shared" + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" +) + +type svc struct{} + +func (svc) Name() string { return "gitserver" } + +func (svc) Configure() (env.Config, []debugserver.Endpoint) { + return shared.LoadConfig(), nil +} + +func (svc) Start(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, config env.Config) error { + return shared.Main(ctx, observationCtx, ready, config.(*shared.Config), enterpriseInit) +} + +var Service service.Service = svc{} diff --git a/enterprise/cmd/gitserver/shared/shared.go b/enterprise/cmd/gitserver/shared/shared.go index 7a561aca1543..53442853e2c2 100644 --- a/enterprise/cmd/gitserver/shared/shared.go +++ b/enterprise/cmd/gitserver/shared/shared.go @@ -13,7 +13,7 @@ import ( "github.com/sourcegraph/sourcegraph/internal/database" ) -func EnterpriseInit(db database.DB) { +func enterpriseInit(db database.DB) { logger := log.Scoped("enterprise", "gitserver enterprise edition") var err error authz.DefaultSubRepoPermsChecker, err = srp.NewSubRepoPermsClient(edb.NewEnterpriseDB(db).SubRepoPerms()) diff --git a/enterprise/cmd/precise-code-intel-worker/main.go b/enterprise/cmd/precise-code-intel-worker/main.go index ba263be24552..f553a12a6315 100644 --- a/enterprise/cmd/precise-code-intel-worker/main.go +++ b/enterprise/cmd/precise-code-intel-worker/main.go @@ -1,9 +1,10 @@ package main import ( - shared "github.com/sourcegraph/sourcegraph/enterprise/cmd/precise-code-intel-worker/shared" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/precise-code-intel-worker/shared" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/sourcegraph/enterprisecmd" ) func main() { - shared.Main() + enterprisecmd.DeprecatedSingleServiceMainEnterprise(shared.Service) } diff --git a/enterprise/cmd/precise-code-intel-worker/shared/service.go b/enterprise/cmd/precise-code-intel-worker/shared/service.go new file mode 100644 index 000000000000..dbb0b2285985 --- /dev/null +++ b/enterprise/cmd/precise-code-intel-worker/shared/service.go @@ -0,0 +1,28 @@ +package shared + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" + "github.com/sourcegraph/sourcegraph/internal/symbols" +) + +type svc struct{} + +func (svc) Name() string { return "precise-code-intel-worker" } + +func (svc) Configure() (env.Config, []debugserver.Endpoint) { + symbols.LoadConfig() + var config Config + config.Load() + return &config, nil +} + +func (svc) Start(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, config env.Config) error { + return Main(ctx, observationCtx, ready, *config.(*Config)) +} + +var Service service.Service = svc{} diff --git a/enterprise/cmd/precise-code-intel-worker/shared/shared.go b/enterprise/cmd/precise-code-intel-worker/shared/shared.go index 466efb554b1c..9cf09fca3631 100644 --- a/enterprise/cmd/precise-code-intel-worker/shared/shared.go +++ b/enterprise/cmd/precise-code-intel-worker/shared/shared.go @@ -21,59 +21,28 @@ import ( "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" "github.com/sourcegraph/sourcegraph/internal/database" connections "github.com/sourcegraph/sourcegraph/internal/database/connections/live" - "github.com/sourcegraph/sourcegraph/internal/debugserver" "github.com/sourcegraph/sourcegraph/internal/encryption/keyring" - "github.com/sourcegraph/sourcegraph/internal/env" "github.com/sourcegraph/sourcegraph/internal/goroutine" "github.com/sourcegraph/sourcegraph/internal/honey" - "github.com/sourcegraph/sourcegraph/internal/hostname" "github.com/sourcegraph/sourcegraph/internal/httpserver" - "github.com/sourcegraph/sourcegraph/internal/logging" "github.com/sourcegraph/sourcegraph/internal/observation" - "github.com/sourcegraph/sourcegraph/internal/profiler" - "github.com/sourcegraph/sourcegraph/internal/tracer" + "github.com/sourcegraph/sourcegraph/internal/service" "github.com/sourcegraph/sourcegraph/internal/uploadstore" - "github.com/sourcegraph/sourcegraph/internal/version" "github.com/sourcegraph/sourcegraph/lib/errors" ) const addr = ":3188" -func Main() { - config := &Config{} - config.Load() - - env.Lock() - env.HandleHelpFlag() - logging.Init() //nolint:staticcheck // Deprecated, but logs unmigrated to sourcegraph/log look really bad without this. - liblog := log.Init(log.Resource{ - Name: env.MyName, - Version: version.Version(), - InstanceID: hostname.Get(), - }) - defer liblog.Sync() +func Main(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, config Config) error { + logger := observationCtx.Logger // Initialize tracing/metrics - logger := log.Scoped("codeintel-worker", "The precise-code-intel-worker service converts LSIF upload file into Postgres data.") - observationCtx := observation.NewContext(logger, observation.Honeycomb(&honey.Dataset{ + observationCtx = observation.NewContext(logger, observation.Honeycomb(&honey.Dataset{ Name: "codeintel-worker", })) - conf.Init() - go conf.Watch(liblog.Update(conf.GetLogSinks)) - tracer.Init(logger.Scoped("tracer", "internal tracer package"), conf.DefaultClient()) - profiler.Init() - - if err := config.Validate(); err != nil { - logger.Error("Failed for load config", log.Error(err)) - } - - // Start debug server - ready := make(chan struct{}) - go debugserver.NewServerRoutine(ready).Start() - - if err := keyring.Init(context.Background()); err != nil { - logger.Fatal("Failed to intialise keyring", log.Error(err)) + if err := keyring.Init(ctx); err != nil { + return errors.Wrap(err, "initializing keyring") } // Connect to databases @@ -83,13 +52,13 @@ func Main() { // Migrations may take a while, but after they're done we'll immediately // spin up a server and can accept traffic. Inform external clients we'll // be ready for traffic. - close(ready) + ready() // Initialize sub-repo permissions client var err error authz.DefaultSubRepoPermsChecker, err = srp.NewSubRepoPermsClient(edb.NewEnterpriseDB(db).SubRepoPerms()) if err != nil { - logger.Fatal("Failed to create sub-repo client", log.Error(err)) + return errors.Wrap(err, "creating sub-repo client") } services, err := codeintel.NewServices(codeintel.ServiceDependencies{ @@ -98,16 +67,16 @@ func Main() { ObservationCtx: observationCtx, }) if err != nil { - logger.Fatal("Failed to create codeintel services", log.Error(err)) + return errors.Wrap(err, "creating codeintel services") } // Initialize stores - uploadStore, err := lsifuploadstore.New(context.Background(), observationCtx, config.LSIFUploadStoreConfig) + uploadStore, err := lsifuploadstore.New(ctx, observationCtx, config.LSIFUploadStoreConfig) if err != nil { - logger.Fatal("Failed to create upload store", log.Error(err)) + return errors.Wrap(err, "creating upload store") } - if err := initializeUploadStore(context.Background(), uploadStore); err != nil { - logger.Fatal("Failed to initialize upload store", log.Error(err)) + if err := initializeUploadStore(ctx, uploadStore); err != nil { + return errors.Wrap(err, "initializing upload store") } // Initialize worker @@ -130,7 +99,9 @@ func Main() { }) // Go! - goroutine.MonitorBackgroundRoutines(context.Background(), worker, server) + goroutine.MonitorBackgroundRoutines(ctx, worker, server) + + return nil } func mustInitializeDB(observationCtx *observation.Context) *sql.DB { diff --git a/enterprise/cmd/repo-updater/main.go b/enterprise/cmd/repo-updater/main.go index 072cb9e0f0b8..6a3e64785a20 100644 --- a/enterprise/cmd/repo-updater/main.go +++ b/enterprise/cmd/repo-updater/main.go @@ -1,14 +1,10 @@ package main import ( - "github.com/sourcegraph/sourcegraph/cmd/repo-updater/shared" - enterprise_shared "github.com/sourcegraph/sourcegraph/enterprise/cmd/repo-updater/shared" - "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/repo-updater/shared" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/sourcegraph/enterprisecmd" ) func main() { - env.Lock() - env.HandleHelpFlag() - - shared.Main(enterprise_shared.EnterpriseInit) + enterprisecmd.DeprecatedSingleServiceMainEnterprise(shared.Service) } diff --git a/enterprise/cmd/repo-updater/shared/service.go b/enterprise/cmd/repo-updater/shared/service.go new file mode 100644 index 000000000000..4b4be6f122a3 --- /dev/null +++ b/enterprise/cmd/repo-updater/shared/service.go @@ -0,0 +1,8 @@ +package shared + +import ( + shared "github.com/sourcegraph/sourcegraph/cmd/repo-updater/shared" + "github.com/sourcegraph/sourcegraph/internal/service" +) + +var Service service.Service = shared.NewServiceWithEnterpriseInit(EnterpriseInit) diff --git a/enterprise/cmd/sourcegraph/README.md b/enterprise/cmd/sourcegraph/README.md new file mode 100644 index 000000000000..49180ae2cbb7 --- /dev/null +++ b/enterprise/cmd/sourcegraph/README.md @@ -0,0 +1,45 @@ +# Sourcegraph App + +Sourcegraph App is a single-binary distribution of Sourcegraph that runs on your local machine. + +**Status:** alpha (only for internal use at Sourcegraph) + +## Development + +```shell +sg start enterprise-single-program +``` + +## Usage + +Sourcegraph App is in alpha (only for internal use at Sourcegraph). + +Check the **Sourcegraph App release** bot in [`#app`](https://app.slack.com/client/T02FSM7DL/C04F9E7GUDP) (in the Sourcegraph internal Slack) for the latest release information. + +## Build and release + +### Snapshot releases + +> Sourcegraph App is in internal alpha and only has snapshot releases. There are no versioned or tagged releases yet. + +To build and release a snapshot for other people to use, push a commit to the special `app/release-snapshot` branch: + +```shell +git push -f origin HEAD:app/release-snapshot +``` + +This runs the `../../dev/app/release.sh` script in CI, which uses [goreleaser](https://goreleaser.com/) to build for many platforms, package, and publish to the `sourcegraph-app-releases` Google Cloud Storage bucket. + +Check the build status in [Buildkite `app/release-snapshot` branch builds](https://buildkite.com/sourcegraph/sourcegraph/builds?branch=app%2Frelease-snapshot). + +### Local builds (without releasing) + +To build it locally for all platforms (without releasing, uploading, or publishing it anywhere), run: + +```shell +enterprise/dev/app/release.sh --snapshot +``` + +The builds are written to the `dist` directory. + +If you just need a local build for your current platform, run `sg start enterprise-single-program` (as mentioned in the [Development](#development) section) and then grab the `.bin/sourcegraph` binary. This binary does not have the web bundle (JavaScript/CSS) embedded into it. diff --git a/enterprise/cmd/sourcegraph/enterprisecmd/enterprisecmd.go b/enterprise/cmd/sourcegraph/enterprisecmd/enterprisecmd.go new file mode 100644 index 000000000000..f637a7d00a05 --- /dev/null +++ b/enterprise/cmd/sourcegraph/enterprisecmd/enterprisecmd.go @@ -0,0 +1,30 @@ +// Package enterprisecmd defines entrypoint functions for the enterprise (non-OSS) build of +// Sourcegraph's single-program distribution. It is invoked by all enterprise (non-OSS) commands' +// main functions. +package enterprisecmd + +import ( + "github.com/sourcegraph/sourcegraph/internal/oobmigration" + "github.com/sourcegraph/sourcegraph/internal/service" + "github.com/sourcegraph/sourcegraph/internal/service/svcmain" +) + +var config = svcmain.Config{} + +// MainEnterprise is called from the `main` function of the `sourcegraph` command. +func MainEnterprise(services []service.Service) { + svcmain.Main(services, config) +} + +// DeprecatedSingleServiceMainEnterprise is called from the `main` function of a command in the +// enterprise (non-OSS) build to start a single service (such as frontend or gitserver). +// +// DEPRECATED: See svcmain.DeprecatedSingleServiceMain documentation for more info. +func DeprecatedSingleServiceMainEnterprise(service service.Service) { + svcmain.DeprecatedSingleServiceMain(service, config, true, true) +} + +func init() { + // TODO(sqs): TODO(single-binary): could we move this out of init? + oobmigration.ReturnEnterpriseMigrations = true +} diff --git a/enterprise/cmd/sourcegraph/enterprisecmd/executorcmd/executorcmd.go b/enterprise/cmd/sourcegraph/enterprisecmd/executorcmd/executorcmd.go new file mode 100644 index 000000000000..893e67fbf7c2 --- /dev/null +++ b/enterprise/cmd/sourcegraph/enterprisecmd/executorcmd/executorcmd.go @@ -0,0 +1,21 @@ +// Package executorcmd similar to enterprisecmd, except that it has customizations specific to the +// executor command. The executor command (1) does not connect to a database, and so dbconn is a +// a forbidden import and (2) is not just a service (it has commands like `executor install all`) +// which means environment variable configuration is not always present, and as such that must not +// be enforced in a standard way like in our other service cmds. +package executorcmd + +import ( + "github.com/sourcegraph/sourcegraph/internal/service" + "github.com/sourcegraph/sourcegraph/internal/service/svcmain" +) + +var config = svcmain.Config{} + +// DeprecatedSingleServiceMainEnterprise is called from the `main` function of a command in the +// enterprise (non-OSS) build to start a single service (such as frontend or gitserver). +// +// DEPRECATED: See svcmain.DeprecatedSingleServiceMain documentation for more info. +func DeprecatedSingleServiceMainEnterprise(service service.Service) { + svcmain.DeprecatedSingleServiceMain(service, config, false, false) +} diff --git a/enterprise/cmd/sourcegraph/main.go b/enterprise/cmd/sourcegraph/main.go new file mode 100644 index 000000000000..df696f5226f8 --- /dev/null +++ b/enterprise/cmd/sourcegraph/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "github.com/sourcegraph/sourcegraph/enterprise/cmd/sourcegraph/enterprisecmd" + "github.com/sourcegraph/sourcegraph/internal/service" + + githubproxy_shared "github.com/sourcegraph/sourcegraph/cmd/github-proxy/shared" + searcher_shared "github.com/sourcegraph/sourcegraph/cmd/searcher/shared" + executor_shared "github.com/sourcegraph/sourcegraph/enterprise/cmd/executor/shared" + frontend_shared "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/shared" + gitserver_shared "github.com/sourcegraph/sourcegraph/enterprise/cmd/gitserver/shared" + precise_code_intel_worker_shared "github.com/sourcegraph/sourcegraph/enterprise/cmd/precise-code-intel-worker/shared" + repoupdater_shared "github.com/sourcegraph/sourcegraph/enterprise/cmd/repo-updater/shared" + symbols_shared "github.com/sourcegraph/sourcegraph/enterprise/cmd/symbols/shared" + worker_shared "github.com/sourcegraph/sourcegraph/enterprise/cmd/worker/shared" +) + +// services is a list of services to run in the enterprise build. +var services = []service.Service{ + frontend_shared.Service, + gitserver_shared.Service, + repoupdater_shared.Service, + searcher_shared.Service, + symbols_shared.Service, + worker_shared.Service, + githubproxy_shared.Service, + precise_code_intel_worker_shared.Service, + executor_shared.Service, +} + +func main() { + enterprisecmd.MainEnterprise(services) +} diff --git a/enterprise/cmd/symbols/main.go b/enterprise/cmd/symbols/main.go index b8d550a9b0c0..d9ba611c13b4 100644 --- a/enterprise/cmd/symbols/main.go +++ b/enterprise/cmd/symbols/main.go @@ -1,10 +1,10 @@ package main import ( - "github.com/sourcegraph/sourcegraph/cmd/symbols/shared" - enterpriseshared "github.com/sourcegraph/sourcegraph/enterprise/cmd/symbols/shared" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/sourcegraph/enterprisecmd" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/symbols/shared" ) func main() { - shared.Main(enterpriseshared.CreateSetup(shared.CtagsConfig, shared.RepositoryFetcherConfig)) + enterprisecmd.DeprecatedSingleServiceMainEnterprise(shared.Service) } diff --git a/enterprise/cmd/symbols/shared/service.go b/enterprise/cmd/symbols/shared/service.go new file mode 100644 index 000000000000..ad859823bc91 --- /dev/null +++ b/enterprise/cmd/symbols/shared/service.go @@ -0,0 +1,28 @@ +package shared + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" + + symbols_shared "github.com/sourcegraph/sourcegraph/cmd/symbols/shared" +) + +type svc struct{} + +func (svc) Name() string { return "symbols" } + +func (svc) Configure() (env.Config, []debugserver.Endpoint) { + symbols_shared.LoadConfig() + config := loadRockskipConfig(env.BaseConfig{}, symbols_shared.CtagsConfig, symbols_shared.RepositoryFetcherConfig) + return &config, nil +} + +func (svc) Start(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, config env.Config) error { + return symbols_shared.Main(ctx, observationCtx, ready, CreateSetup(*config.(*rockskipConfig))) +} + +var Service service.Service = svc{} diff --git a/enterprise/cmd/symbols/shared/setup.go b/enterprise/cmd/symbols/shared/setup.go index aa0f4c2553e8..24f4b25b0e50 100644 --- a/enterprise/cmd/symbols/shared/setup.go +++ b/enterprise/cmd/symbols/shared/setup.go @@ -3,7 +3,6 @@ package shared import ( "context" "database/sql" - stdlog "log" "net/http" "strings" @@ -36,13 +35,7 @@ var ( minRepoSizeMb = env.MustGetInt("ROCKSKIP_MIN_REPO_SIZE_MB", -1, "all repos that are at least this big will be indexed using Rockskip") ) -func CreateSetup(ctags types.CtagsConfig, repositoryFetcher types.RepositoryFetcherConfig) shared.SetupFunc { - baseConfig := env.BaseConfig{} - config := loadRockskipConfig(baseConfig, ctags, repositoryFetcher) - if err := baseConfig.Validate(); err != nil { - stdlog.Fatal("failed to load configuration:", err) - } - +func CreateSetup(config rockskipConfig) shared.SetupFunc { repoToSize := map[string]int64{} if useRockskip { @@ -99,6 +92,7 @@ func CreateSetup(ctags types.CtagsConfig, repositoryFetcher types.RepositoryFetc } type rockskipConfig struct { + env.BaseConfig Ctags types.CtagsConfig RepositoryFetcher types.RepositoryFetcherConfig MaxRepos int @@ -110,6 +104,10 @@ type rockskipConfig struct { SearchLastIndexedCommit bool } +func (c *rockskipConfig) Load() { + // TODO(sqs): TODO(single-binary): load rockskip config from here +} + func loadRockskipConfig(baseConfig env.BaseConfig, ctags types.CtagsConfig, repositoryFetcher types.RepositoryFetcherConfig) rockskipConfig { return rockskipConfig{ Ctags: ctags, diff --git a/enterprise/cmd/worker/main.go b/enterprise/cmd/worker/main.go index 65ce8e643689..1b3d946f000b 100644 --- a/enterprise/cmd/worker/main.go +++ b/enterprise/cmd/worker/main.go @@ -1,51 +1,10 @@ package main import ( - "github.com/sourcegraph/log" - - "github.com/sourcegraph/sourcegraph/cmd/worker/shared" - enterprise_shared "github.com/sourcegraph/sourcegraph/enterprise/cmd/worker/shared" - srp "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/subrepoperms" - edb "github.com/sourcegraph/sourcegraph/enterprise/internal/database" - "github.com/sourcegraph/sourcegraph/enterprise/internal/oobmigration/migrations" - "github.com/sourcegraph/sourcegraph/internal/authz" - "github.com/sourcegraph/sourcegraph/internal/database" - "github.com/sourcegraph/sourcegraph/internal/env" - "github.com/sourcegraph/sourcegraph/internal/observation" - "github.com/sourcegraph/sourcegraph/internal/oobmigration" - "github.com/sourcegraph/sourcegraph/internal/version" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/sourcegraph/enterprisecmd" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/worker/shared" ) -func getEnterpriseInit(logger log.Logger) func(database.DB) { - return func(ossDB database.DB) { - enterpriseDB := edb.NewEnterpriseDB(ossDB) - - var err error - authz.DefaultSubRepoPermsChecker, err = srp.NewSubRepoPermsClient(enterpriseDB.SubRepoPerms()) - if err != nil { - logger.Fatal("Failed to create sub-repo client", log.Error(err)) - } - } - -} - func main() { - liblog := log.Init(log.Resource{ - Name: env.MyName, - Version: version.Version(), - }) - defer liblog.Sync() - - logger := log.Scoped("worker", "worker enterprise edition") - observationCtx := observation.NewContext(logger) - - go enterprise_shared.SetAuthzProviders(observationCtx) - - if err := shared.Start(observationCtx, enterprise_shared.AdditionalJobs, migrations.RegisterEnterpriseMigrators, getEnterpriseInit(logger)); err != nil { - logger.Fatal(err.Error()) - } -} - -func init() { - oobmigration.ReturnEnterpriseMigrations = true + enterprisecmd.DeprecatedSingleServiceMainEnterprise(shared.Service) } diff --git a/enterprise/cmd/worker/shared/service.go b/enterprise/cmd/worker/shared/service.go new file mode 100644 index 000000000000..92ba5118aaef --- /dev/null +++ b/enterprise/cmd/worker/shared/service.go @@ -0,0 +1,27 @@ +package shared + +import ( + "context" + + shared "github.com/sourcegraph/sourcegraph/cmd/worker/shared" + "github.com/sourcegraph/sourcegraph/enterprise/internal/oobmigration/migrations" + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/service" +) + +type svc struct{} + +func (svc) Name() string { return "worker" } + +func (svc) Configure() (env.Config, []debugserver.Endpoint) { + return shared.LoadConfig(additionalJobs, migrations.RegisterEnterpriseMigrators), nil +} + +func (svc) Start(ctx context.Context, observationCtx *observation.Context, ready service.ReadyFunc, config env.Config) error { + go setAuthzProviders(ctx, observationCtx) + return shared.Start(ctx, observationCtx, ready, config.(*shared.Config), getEnterpriseInit(observationCtx.Logger)) +} + +var Service service.Service = svc{} diff --git a/enterprise/cmd/worker/shared/shared.go b/enterprise/cmd/worker/shared/shared.go index 7524cf008983..8fe054d87c1b 100644 --- a/enterprise/cmd/worker/shared/shared.go +++ b/enterprise/cmd/worker/shared/shared.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/sourcegraph/log" "github.com/sourcegraph/sourcegraph/cmd/frontend/globals" "github.com/sourcegraph/sourcegraph/cmd/worker/job" workerdb "github.com/sourcegraph/sourcegraph/cmd/worker/shared/init/db" @@ -16,13 +17,16 @@ import ( "github.com/sourcegraph/sourcegraph/enterprise/cmd/worker/internal/permissions" "github.com/sourcegraph/sourcegraph/enterprise/cmd/worker/internal/telemetry" eiauthz "github.com/sourcegraph/sourcegraph/enterprise/internal/authz" + srp "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/subrepoperms" + edb "github.com/sourcegraph/sourcegraph/enterprise/internal/database" "github.com/sourcegraph/sourcegraph/internal/authz" "github.com/sourcegraph/sourcegraph/internal/conf" + "github.com/sourcegraph/sourcegraph/internal/database" "github.com/sourcegraph/sourcegraph/internal/extsvc/versions" "github.com/sourcegraph/sourcegraph/internal/observation" ) -var AdditionalJobs = map[string]job.Job{ +var additionalJobs = map[string]job.Job{ "codehost-version-syncing": versions.NewSyncingJob(), "insights-job": workerinsights.NewInsightsJob(), "insights-query-runner-job": workerinsights.NewInsightsQueryRunnerJob(), @@ -61,9 +65,8 @@ var AdditionalJobs = map[string]job.Job{ // current actor stored in an operation's context, which is likely an internal actor for many of // the jobs configured in this service. This also enables repository update operations to fetch // permissions from code hosts. -func SetAuthzProviders(observationCtx *observation.Context) { +func setAuthzProviders(ctx context.Context, observationCtx *observation.Context) { observationCtx = observation.ContextWithLogger(observationCtx.Logger.Scoped("authz-provider", ""), observationCtx) - db, err := workerdb.InitDB(observationCtx) if err != nil { return @@ -72,10 +75,21 @@ func SetAuthzProviders(observationCtx *observation.Context) { // authz also relies on UserMappings being setup. globals.WatchPermissionsUserMapping() - ctx := context.Background() - for range time.NewTicker(eiauthz.RefreshInterval()).C { allowAccessByDefault, authzProviders, _, _, _ := eiauthz.ProvidersFromConfig(ctx, conf.Get(), db.ExternalServices(), db) authz.SetProviders(allowAccessByDefault, authzProviders) } } + +func getEnterpriseInit(logger log.Logger) func(database.DB) { + return func(ossDB database.DB) { + enterpriseDB := edb.NewEnterpriseDB(ossDB) + + var err error + authz.DefaultSubRepoPermsChecker, err = srp.NewSubRepoPermsClient(enterpriseDB.SubRepoPerms()) + if err != nil { + logger.Fatal("Failed to create sub-repo client", log.Error(err)) + } + } + +} diff --git a/enterprise/dev/app/goreleaser.yaml b/enterprise/dev/app/goreleaser.yaml new file mode 100644 index 000000000000..ca4aa6303cfc --- /dev/null +++ b/enterprise/dev/app/goreleaser.yaml @@ -0,0 +1,132 @@ +project_name: sourcegraph + +env: + - CGO_ENABLED=1 + +before: + hooks: + - go mod tidy + +builds: + - id: build_linux + goos: + - linux + goarch: + - amd64 + main: &gomain ./enterprise/cmd/sourcegraph + tags: &tags + - dist + ldflags: &ldflags + - -X github.com/sourcegraph/sourcegraph/internal/version.version={{.Version}} + - -X github.com/sourcegraph/sourcegraph/internal/version.timestamp={{.Timestamp}} + - -X github.com/sourcegraph/sourcegraph/internal/conf/deploy.forceType=single-program + flags: &goflags + - -trimpath + - -v + + - id: build_macos + goos: + - darwin + goarch: + - amd64 + - arm64 + env: + - CC=o64-clang + - CXX=o64-clang++ + main: *gomain + tags: *tags + ldflags: *ldflags + flags: *goflags + + # TODO(sqs): Windows builds are broken due to compilation errors in: github.com/sourcegraph/mountinfo, github.com/coreos/go-iptables, github.com/sourcegraph/zoekt + # + # - id: build_windows + # goos: + # - windows + # goarch: + # - amd64 + # env: + # - CC=x86_64-w64-mingw32-gcc + # - CXX=x86_64-w64-mingw32-g++ + # main: *gomain + # tags: *tags + # ldflags: *ldflags + # flags: *goflags + +universal_binaries: + - id: build_macos + +archives: + - id: zip_archives + builds: ['build_linux', 'build_macos'] + format: zip + # Just include the binary file. The only way to do this is to specify a glob that matches + # nothing. + files: ['NO_FILES_*'] + +checksum: + name_template: 'checksums.txt' + +changelog: + skip: true + +release: + github: + owner: sourcegraph + # TODO(sqs): use just sourcegraph/sourcegraph? + name: sourcegraph-app + draft: true + prerelease: auto + +blobs: + - provider: gs + bucket: sourcegraph-app-releases + folder: "{{.Version}}" + ids: + - build_linux + - build_macos + - zip_archives + - linux_packages + +nfpms: + - id: linux_packages + builds: ['build_linux'] + formats: + - deb + - rpm + dependencies: + - git + - redis + vendor: "sourcegraph" + homepage: "https://github.com/sourcegraph/sourcegraph" + maintainer: "dev@sourcegraph.com" + description: "Code intelligence and search" + license: "Sourcegraph Enterprise License (portions licensed under Apache 2)" + +brews: + - homepage: "https://github.com/sourcegraph/sourcegraph" + description: "Code intelligence and search" + license: "Sourcegraph Enterprise License (portions licensed under Apache 2)" + tap: + owner: sourcegraph + # TODO(sqs): use just sourcegraph/homebrew-sourcegraph + name: homebrew-sourcegraph-app + url_template: https://storage.googleapis.com/sourcegraph-app-releases/{{ .Tag }}/{{ .ArtifactName }} + commit_author: + name: sourcegraph-buildkite + email: 71296199+sourcegraph-buildkite@users.noreply.github.com + install: | + bin.install "sourcegraph" + test: | + system "#{bin}/sourcegraph --help" + dependencies: + - name: git + - name: redis + +announce: + slack: + enabled: true + message_template: | + New version `{{.Version}}` (): `brew install sourcegraph/sourcegraph-app/sourcegraph`, , , . Winners ship, shippers win! :ship: :ship: :ship: + +# TODO(sqs): add back `dockers`, `scoop`, `snapcraft` as needed diff --git a/enterprise/dev/app/release.sh b/enterprise/dev/app/release.sh new file mode 100755 index 000000000000..dc46c159b9db --- /dev/null +++ b/enterprise/dev/app/release.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +set -eu + +ROOTDIR="$(realpath "$(dirname "${BASH_SOURCE[0]}")"/../../..)" +GORELEASER_CROSS_VERSION=v1.19.5 +GCLOUD_APP_CREDENTIALS_FILE=${GCLOUD_APP_CREDENTIALS_FILE-$HOME/.config/gcloud/application_default_credentials.json} + +if [ -z "${SKIP_BUILD_WEB-}" ]; then + # Use esbuild because it's faster. This is just a personal preference by me (@sqs); if there is a + # good reason to change it, feel free to do so. + ENTERPRISE=1 DEV_WEB_BUILDER=esbuild pnpm run build-web +fi + +if [ -z "${GITHUB_TOKEN-}" ]; then + echo "Error: GITHUB_TOKEN must be set." + exit 1 +fi + +if [ ! -f "$GCLOUD_APP_CREDENTIALS_FILE" ]; then + echo "Error: no gcloud application default credentials found. To obtain these credentials, first run:" + echo + echo " gcloud auth application-default login" + echo + echo "Or set GCLOUD_APP_CREDENTIALS_FILE to a file containing the credentials." + exit 1 +fi + +if [ -z "${VERSION-}" ]; then + echo "Error: VERSION must be set." + exit 1 +fi + +# Manually set the version because `git describe` (which goreleaser otherwise uses) prints the wrong +# version number because of how we use release branches +# (https://github.com/sourcegraph/sourcegraph/issues/46404). +GORELEASER_CURRENT_TAG=$VERSION + +DOCKER_ARGS=() +if [ -z "${BUILDKITE-}" ]; then + DOCKER_VOLUME_SOURCE="$ROOTDIR" +else + # In Buildkite, we're running in a Docker container, so `docker run -v` needs to refer to a + # directory on our Docker host, not in our container. Use the /mnt/tmp directory, which is shared + # between `dind` (the Docker-in-Docker host) and our container. + TMPDIR=$(mktemp -d --tmpdir=/mnt/tmp -t sourcegraph.XXXXXXXX) + cleanup() { + rm -rf "$TMPDIR" + } + trap cleanup EXIT + + # goreleaser expects a tag that corresponds to the version. When running in local dev, you can + # pass --skip-validate to skip this check, but in CI we want to run the other validations (such as + # checking that the Git checkout is not dirty). + git tag "$VERSION" + + # Copy the ROOTDIR and GCLOUD_APP_CREDENTIALS_FILE to /mnt/tmp so they can be volume-mounted. + cp -R "$ROOTDIR" "$TMPDIR" + DOCKER_VOLUME_SOURCE="$TMPDIR/$(basename "$ROOTDIR")" + GCLOUD_APP_CREDENTIALS_TMP="$TMPDIR"/application_default_credentials.json + cp "$GCLOUD_APP_CREDENTIALS_FILE" "$GCLOUD_APP_CREDENTIALS_TMP" + GCLOUD_APP_CREDENTIALS_FILE="$GCLOUD_APP_CREDENTIALS_TMP" + + # In Buildkite, we need to mount /buildkite-git-references because our .git directory refers to + # it. TODO(sqs): This is probably slow and undoes the optimization gained by using `git clone + # --reference`. + mkdir "$TMPDIR"/buildkite-git-references + cp -R /buildkite-git-references/sourcegraph.reference "$TMPDIR"/buildkite-git-references + DOCKER_ARGS+=(-v "$TMPDIR"/buildkite-git-references:/buildkite-git-references) +fi + +GORELEASER_ARGS=() +if [ -z "${SLACK_APP_RELEASE_WEBHOOK-}" ]; then + GORELEASER_ARGS+=(--skip-announce) +else + DOCKER_ARGS+=(-e "SLACK_WEBHOOK=$SLACK_APP_RELEASE_WEBHOOK") +fi + +# shellcheck disable=SC2086 +exec docker run --rm \ + ${DOCKER_ARGS[*]} \ + -v "$DOCKER_VOLUME_SOURCE":/go/src/github.com/sourcegraph/sourcegraph \ + -w /go/src/github.com/sourcegraph/sourcegraph \ + -v "$GCLOUD_APP_CREDENTIALS_FILE":/root/.config/gcloud/application_default_credentials.json \ + -e "GITHUB_TOKEN=$GITHUB_TOKEN" \ + -e "GORELEASER_CURRENT_TAG=$GORELEASER_CURRENT_TAG" \ + goreleaser/goreleaser-cross:$GORELEASER_CROSS_VERSION \ + --config enterprise/dev/app/goreleaser.yaml --parallelism 1 --debug --rm-dist ${GORELEASER_ARGS[*]} "$@" diff --git a/enterprise/dev/ci/integration/executors/config/site-config.json b/enterprise/dev/ci/integration/executors/config/site-config.json index 205455950898..cd89c4873334 100644 --- a/enterprise/dev/ci/integration/executors/config/site-config.json +++ b/enterprise/dev/ci/integration/executors/config/site-config.json @@ -1,5 +1,4 @@ { - "executors.accessToken": "$EXECUTOR_FRONTEND_PASSWORD", "executors.batcheshelperImage": "us.gcr.io/sourcegraph-dev/batcheshelper", "executors.batcheshelperImageTag": "$CANDIDATE_VERSION", "auth.providers": [ diff --git a/enterprise/dev/ci/integration/executors/docker-compose.yml b/enterprise/dev/ci/integration/executors/docker-compose.yml index 9b0fb94917c1..9749ed75f833 100644 --- a/enterprise/dev/ci/integration/executors/docker-compose.yml +++ b/enterprise/dev/ci/integration/executors/docker-compose.yml @@ -33,6 +33,7 @@ services: SOURCEGRAPH_LICENSE_GENERATION_KEY: '${SOURCEGRAPH_LICENSE_GENERATION_KEY}' SITE_CONFIG_FILE: /e2e/site-config.json PGDATASOURCE: postgres://sg@postgres:5432/sg + EXECUTOR_QUEUE_DISABLE_ACCESS_TOKEN_INSECURE: 'true' volumes: - '${DATA}/config:/etc/sourcegraph' - '${DATA}/data:/var/opt/sourcegraph' diff --git a/enterprise/dev/ci/integration/executors/run.sh b/enterprise/dev/ci/integration/executors/run.sh index 1f10f3f2223b..2cf530f7289d 100755 --- a/enterprise/dev/ci/integration/executors/run.sh +++ b/enterprise/dev/ci/integration/executors/run.sh @@ -27,7 +27,7 @@ trap cleanup EXIT export POSTGRES_IMAGE="us.gcr.io/sourcegraph-dev/postgres-12-alpine:${CANDIDATE_VERSION}" export SERVER_IMAGE="us.gcr.io/sourcegraph-dev/server:${CANDIDATE_VERSION}" export EXECUTOR_IMAGE="us.gcr.io/sourcegraph-dev/executor:${CANDIDATE_VERSION}" -export EXECUTOR_FRONTEND_PASSWORD="hunter2hunter2hunter2" +export EXECUTOR_FRONTEND_PASSWORD=none export SOURCEGRAPH_LICENSE_GENERATION_KEY="${SOURCEGRAPH_LICENSE_GENERATION_KEY:-""}" export TMP_DIR export DATA diff --git a/enterprise/dev/ci/internal/ci/operations.go b/enterprise/dev/ci/internal/ci/operations.go index 003a014bf434..7e9e56a4e092 100644 --- a/enterprise/dev/ci/internal/ci/operations.go +++ b/enterprise/dev/ci/internal/ci/operations.go @@ -569,6 +569,25 @@ func addVsceReleaseSteps(pipeline *bk.Pipeline) { bk.Cmd("pnpm --filter @sourcegraph/vscode run release")) } +// Release a snapshot of App. +func addAppSnapshotReleaseSteps(c Config) operations.Operation { + // TODO(sqs): Use goreleaser-pro nightly feature? Blocked on + // https://github.com/goreleaser/goreleaser-cross/issues/22. + + // goreleaser requires that the version is semver-compatible + // (https://goreleaser.com/limitations/semver/). This is fine for now in alpha. + version := fmt.Sprintf("0.0.%d-snapshot+%s-%.6s", c.BuildNumber, c.Time.Format("20060102"), c.Commit) + + return func(pipeline *bk.Pipeline) { + // Release App (.zip/.deb/.rpm to Google Cloud Storage, new tap for Homebrew, etc.). + pipeline.AddStep(":desktop_computer: App release", + withPnpmCache(), + bk.Cmd("pnpm install --frozen-lockfile --fetch-timeout 60000"), + bk.Env("VERSION", version), + bk.Cmd("enterprise/dev/ci/scripts/release-app.sh")) + } +} + // Adds a Buildkite pipeline "Wait". func wait(pipeline *bk.Pipeline) { pipeline.AddWait() diff --git a/enterprise/dev/ci/internal/ci/pipeline.go b/enterprise/dev/ci/internal/ci/pipeline.go index 3ffabc8cd66f..b60d94080949 100644 --- a/enterprise/dev/ci/internal/ci/pipeline.go +++ b/enterprise/dev/ci/internal/ci/pipeline.go @@ -180,6 +180,10 @@ func GeneratePipeline(c Config) (*bk.Pipeline, error) { // addVsceIntegrationTests, ) + case runtype.AppSnapshotRelease: + // If this is an App snapshot build, release a snapshot. + ops = operations.NewSet(addAppSnapshotReleaseSteps(c)) + case runtype.ImagePatch: // only build image for the specified image in the branch name // see https://handbook.sourcegraph.com/engineering/deployments#building-docker-images-for-a-specific-branch diff --git a/enterprise/dev/ci/scripts/release-app.sh b/enterprise/dev/ci/scripts/release-app.sh new file mode 100755 index 000000000000..a50db4fca865 --- /dev/null +++ b/enterprise/dev/ci/scripts/release-app.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -eu + +# Calls the enterprise/dev/app/release.sh script with the right env vars. + +GITHUB_TOKEN=$(gcloud secrets versions access latest --secret=BUILDKITE_GITHUBDOTCOM_TOKEN --quiet --project=sourcegraph-ci) +export GITHUB_TOKEN + +# TODO(sqs): Make this non-optional (by removing ` || echo -n`) when https://github.com/sourcegraph/infrastructure/pull/4481 is merged. +SLACK_APP_RELEASE_WEBHOOK=$(gcloud secrets versions access latest --secret=SLACK_APP_RELEASE_WEBHOOK --quiet --project=sourcegraph-ci || echo -n) +export SLACK_APP_RELEASE_WEBHOOK + +TMPFILE=$(mktemp) +gcloud secrets versions access latest --secret=BUILDKITE_GCLOUD_SERVICE_ACCOUNT --quiet --project=sourcegraph-ci > "$TMPFILE" +export GCLOUD_APP_CREDENTIALS_FILE=$TMPFILE +cleanup() { + rm "$TMPFILE" +} +trap cleanup EXIT + +ROOTDIR="$(realpath "$(dirname "${BASH_SOURCE[0]}")"/../../../..)" +exec "$ROOTDIR"/enterprise/dev/app/release.sh diff --git a/go.mod b/go.mod index e8eec8d8a538..697b3fa3b0f7 100644 --- a/go.mod +++ b/go.mod @@ -205,6 +205,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/prometheus v0.40.5 // indirect github.com/sirupsen/logrus v1.9.0 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/zenazn/goji v1.0.1 // indirect go.opentelemetry.io/otel/metric v0.33.0 // indirect @@ -219,6 +220,7 @@ require ( github.com/coreos/go-iptables v0.6.0 github.com/crewjam/saml/samlidp v0.0.0-20221211125903-d951aa2d145a github.com/dcadenas/pagerank v0.0.0-20171013173705-af922e3ceea8 + github.com/fergusstrange/embedded-postgres v1.19.0 github.com/frankban/quicktest v1.14.3 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/go-github/v47 v47.1.0 @@ -232,6 +234,8 @@ require ( golang.org/x/exp v0.0.0-20221208152030-732eee02a75a ) +replace github.com/fergusstrange/embedded-postgres => github.com/sourcegraph/embedded-postgres v1.19.1-0.20230113234230-bb62ad58a1e1 + require ( github.com/sourcegraph/zoekt v0.0.0-20230112115613-e0cf62d238b9 github.com/stretchr/objx v0.5.0 // indirect diff --git a/go.sum b/go.sum index ff5262ffdfe6..2c89d347eff5 100644 --- a/go.sum +++ b/go.sum @@ -2084,6 +2084,8 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9 github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/conc v0.1.0 h1:9GeYVmWWa1jeOq3zGq17m10d9pjYZpiGTj/N4hQFl58= github.com/sourcegraph/conc v0.1.0/go.mod h1:sEXGtKMpRbfGhShfObhgMyxDpdu/5ABGrzSGYaigx5A= +github.com/sourcegraph/embedded-postgres v1.19.1-0.20230113234230-bb62ad58a1e1 h1:NbyS/m5kyBsaxynmY18st03pL9ZSOdEEC/B839vNNRA= +github.com/sourcegraph/embedded-postgres v1.19.1-0.20230113234230-bb62ad58a1e1/go.mod h1:0B+3bPsMvcNgR9nN+bdM2x9YaNYDnf3ksUqYp1OAub0= github.com/sourcegraph/go-ctags v0.0.0-20230111110657-c27675da7f71 h1:tsWE3F3StWvnwLnC4JWb0zX0UHY9GULQtu/aoQvLJvI= github.com/sourcegraph/go-ctags v0.0.0-20230111110657-c27675da7f71/go.mod h1:ZYjpRXoJrRlxjU9ZfpaUKJkk62AjhJPffN3rlw2aqxM= github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= @@ -2293,6 +2295,7 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xeonx/timeago v1.0.0-rc4 h1:9rRzv48GlJC0vm+iBpLcWAr8YbETyN9Vij+7h2ammz4= github.com/xeonx/timeago v1.0.0-rc4/go.mod h1:qDLrYEFynLO7y5Ho7w3GwgtYgpy5UfhcXIIQvMKVDkA= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= @@ -2428,6 +2431,7 @@ go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 go.uber.org/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk= go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= diff --git a/internal/conf/computed.go b/internal/conf/computed.go index a003f5bfa16c..ba791ffbf776 100644 --- a/internal/conf/computed.go +++ b/internal/conf/computed.go @@ -21,7 +21,7 @@ import ( func init() { deployType := deploy.Type() if !deploy.IsValidDeployType(deployType) { - log.Fatalf("The 'DEPLOY_TYPE' environment variable is invalid. Expected one of: %q, %q, %q, %q, %q, %q. Got: %q", deploy.Kubernetes, deploy.DockerCompose, deploy.PureDocker, deploy.SingleDocker, deploy.Dev, deploy.Helm, deployType) + log.Fatalf("The 'DEPLOY_TYPE' environment variable is invalid. Expected one of: %q, %q, %q, %q, %q, %q, %q. Got: %q", deploy.Kubernetes, deploy.DockerCompose, deploy.PureDocker, deploy.SingleDocker, deploy.Dev, deploy.Helm, deploy.SingleProgram, deployType) } confdefaults.Default = defaultConfigForDeployment() @@ -36,6 +36,8 @@ func defaultConfigForDeployment() conftypes.RawUnified { return confdefaults.DockerContainer case deploy.IsDeployTypeKubernetes(deployType), deploy.IsDeployTypeDockerCompose(deployType), deploy.IsDeployTypePureDocker(deployType): return confdefaults.KubernetesOrDockerComposeOrPureDocker + case deploy.IsDeployTypeSingleProgram(deployType): + return confdefaults.SingleProgram default: panic("deploy type did not register default configuration") } diff --git a/internal/conf/conf.go b/internal/conf/conf.go index 6389b4b347c2..c44c887218e4 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -14,6 +14,7 @@ import ( sglog "github.com/sourcegraph/log" "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" + "github.com/sourcegraph/sourcegraph/internal/conf/deploy" "github.com/sourcegraph/sourcegraph/internal/env" "github.com/sourcegraph/sourcegraph/schema" ) @@ -66,6 +67,12 @@ func getMode() configurationMode { } func getModeUncached() configurationMode { + if deploy.IsDeployTypeSingleProgram(deploy.Type()) { + // Single-program always uses the server mode because everything is running in the same + // process. + return modeServer + } + mode := os.Getenv("CONFIGURATION_MODE") switch mode { @@ -206,6 +213,10 @@ var siteConfigEscapeHatchPath = env.Get("SITE_CONFIG_ESCAPE_HATCH_PATH", "$HOME/ // cannot access the UI (for example by configuring auth in a way that locks them out) // they can simply edit this file in any of the frontend containers to undo the change. func startSiteConfigEscapeHatchWorker(c ConfigurationSource) { + if os.Getenv("NO_SITE_CONFIG_ESCAPE_HATCH") == "1" { + return + } + siteConfigEscapeHatchPath = os.ExpandEnv(siteConfigEscapeHatchPath) var ( diff --git a/internal/conf/confdefaults/confdefaults.go b/internal/conf/confdefaults/confdefaults.go index da5b1ce112ff..320ddf81cf31 100644 --- a/internal/conf/confdefaults/confdefaults.go +++ b/internal/conf/confdefaults/confdefaults.go @@ -71,6 +71,21 @@ var KubernetesOrDockerComposeOrPureDocker = conftypes.RawUnified{ }`, } +// SingleProgram is the default configuration for the single-program (Go static binary) +// distribution. +var SingleProgram = conftypes.RawUnified{ + Site: `{ + "auth.providers": [ + { "type": "builtin" } + ], + "externalURL": "http://localhost:3080", + + "codeIntelAutoIndexing.enabled": true, + "codeIntelAutoIndexing.allowGlobalPolicies": true, + "executors.frontendURL": "http://host.docker.internal:3080", +}`, +} + // Default is the default for *this* deployment type. It is populated by // pkg/conf at init time. var Default conftypes.RawUnified diff --git a/internal/conf/deploy/deploytype.go b/internal/conf/deploy/deploytype.go index 31d9d86ed0f1..84198dcd118c 100644 --- a/internal/conf/deploy/deploytype.go +++ b/internal/conf/deploy/deploytype.go @@ -11,12 +11,18 @@ const ( PureDocker = "pure-docker" Dev = "dev" Helm = "helm" + SingleProgram = "single-program" ) var mock string +var forceType string // force a deploy type (can be injected with `go build -ldflags "-X ..."`) + // Type tells the deployment type. func Type() string { + if forceType != "" { + return forceType + } if mock != "" { return mock } @@ -62,6 +68,11 @@ func IsDeployTypeSingleDockerContainer(deployType string) bool { return deployType == SingleDocker } +// IsDeployTypeSingleProgram tells if the given deployment type is a single Go program. +func IsDeployTypeSingleProgram(deployType string) bool { + return deployType == SingleProgram +} + // IsDev tells if the given deployment type is "dev". func IsDev(deployType string) bool { return deployType == Dev @@ -74,5 +85,6 @@ func IsValidDeployType(deployType string) bool { IsDeployTypeDockerCompose(deployType) || IsDeployTypePureDocker(deployType) || IsDeployTypeSingleDockerContainer(deployType) || - IsDev(deployType) + IsDev(deployType) || + IsDeployTypeSingleProgram(deployType) } diff --git a/internal/env/env.go b/internal/env/env.go index 218dc3da2814..19c81a22af91 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -218,3 +218,14 @@ func HandleHelpFlag() { } } } + +// HackClearEnvironCache can be used to clear the environ cache if os.Setenv was called and you want +// subsequent env.Get calls to return the new value. It is a hack but useful because some env.Get +// calls are hard to remove from static init time, and the ones we've moved to post-init we want to +// be able to use the default values we set in package singleprogram. +// +// TODO(sqs): TODO(single-binary): this indicates our initialization order could be better, hence this +// is labeled as a hack. +func HackClearEnvironCache() { + environ = nil +} diff --git a/internal/extsvc/github/common.go b/internal/extsvc/github/common.go index 63342a8eecc3..dddf133111a9 100644 --- a/internal/extsvc/github/common.go +++ b/internal/extsvc/github/common.go @@ -22,6 +22,7 @@ import ( "github.com/sourcegraph/sourcegraph/internal/api" "github.com/sourcegraph/sourcegraph/internal/conf" + "github.com/sourcegraph/sourcegraph/internal/conf/deploy" "github.com/sourcegraph/sourcegraph/internal/encryption" "github.com/sourcegraph/sourcegraph/internal/env" "github.com/sourcegraph/sourcegraph/internal/extsvc" @@ -1463,6 +1464,14 @@ func ExternalRepoSpec(repo *Repository, baseURL *url.URL) api.ExternalRepoSpec { } } +func githubBaseURLDefault() string { + isSingleProgram := deploy.IsDeployTypeSingleProgram(deploy.Type()) + if isSingleProgram { + return "" + } + return "http://github-proxy" +} + var ( gitHubDisable, _ = strconv.ParseBool(env.Get("SRC_GITHUB_DISABLE", "false", "disables communication with GitHub instances. Used to test GitHub service degradation")) @@ -1470,7 +1479,7 @@ var ( requestCounter = metrics.NewRequestMeter("github", "Total number of requests sent to the GitHub API.") // Get raw proxy URL at service startup, but only get parsed URL at runtime with getGithubProxyURL - githubProxyRawURL = env.Get("GITHUB_BASE_URL", "http://github-proxy", "base URL for GitHub.com API (used for github-proxy)") + githubProxyRawURL = env.Get("GITHUB_BASE_URL", githubBaseURLDefault(), "base URL for GitHub.com API (used for github-proxy)") ) func getGithubProxyURL() (*url.URL, bool) { diff --git a/internal/metrics/operation.go b/internal/metrics/operation.go index aeac012dff2b..1e2300f0057c 100644 --- a/internal/metrics/operation.go +++ b/internal/metrics/operation.go @@ -111,7 +111,7 @@ func NewREDMetrics(r prometheus.Registerer, metricPrefix string, fns ...REDMetri }, options.labels, ) - duration = mustRegisterIgnoreDuplicate(r, duration) + duration = MustRegisterIgnoreDuplicate(r, duration) count := prometheus.NewCounterVec( prometheus.CounterOpts{ @@ -122,7 +122,7 @@ func NewREDMetrics(r prometheus.Registerer, metricPrefix string, fns ...REDMetri }, options.labels, ) - count = mustRegisterIgnoreDuplicate(r, count) + count = MustRegisterIgnoreDuplicate(r, count) errors := prometheus.NewCounterVec( prometheus.CounterOpts{ @@ -133,7 +133,7 @@ func NewREDMetrics(r prometheus.Registerer, metricPrefix string, fns ...REDMetri }, options.labels, ) - errors = mustRegisterIgnoreDuplicate(r, errors) + errors = MustRegisterIgnoreDuplicate(r, errors) return &REDMetrics{ Duration: duration, @@ -142,10 +142,10 @@ func NewREDMetrics(r prometheus.Registerer, metricPrefix string, fns ...REDMetri } } -// mustRegisterIgnoreDuplicate is like registerer.MustRegister(collector), except that it returns +// MustRegisterIgnoreDuplicate is like registerer.MustRegister(collector), except that it returns // the already registered collector with the same ID if a duplicate collector is attempted to be // registered. -func mustRegisterIgnoreDuplicate[T prometheus.Collector](registerer prometheus.Registerer, collector T) T { +func MustRegisterIgnoreDuplicate[T prometheus.Collector](registerer prometheus.Registerer, collector T) T { if err := registerer.Register(collector); err != nil { if e, ok := err.(prometheus.AlreadyRegisteredError); ok { return e.ExistingCollector.(T) diff --git a/internal/service/service.go b/internal/service/service.go new file mode 100644 index 000000000000..836f73425ce5 --- /dev/null +++ b/internal/service/service.go @@ -0,0 +1,34 @@ +// Package service defines a service that runs as part of the Sourcegraph application. Examples +// include frontend, gitserver, and repo-updater. +package service + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/observation" +) + +// A Service provides independent functionality in the Sourcegraph application. Examples include +// frontend, gitserver, and repo-updater. A service may run in the same process as any other +// service, in a separate process, in a separate container, or on a separate host. +type Service interface { + // Name is the name of the service. + Name() string + + // Configure reads from env vars, runs very quickly, and has no side effects. All services' + // Configure methods are run before any service's Start method. + // + // The returned env.Config will be passed to the service's Start method. + // + // The returned debugserver endpoints will be added to the global debugserver. + Configure() (env.Config, []debugserver.Endpoint) + + // Start starts the service. + // TODO(sqs): TODO(single-binary): make it monitorable with goroutine.Whatever interfaces. + Start(context.Context, *observation.Context, ReadyFunc, env.Config) error +} + +// ReadyFunc is called in (Service).Start when the service is ready to start serving clients. +type ReadyFunc func() diff --git a/internal/service/svcmain/svcmain.go b/internal/service/svcmain/svcmain.go new file mode 100644 index 000000000000..dae9b2496693 --- /dev/null +++ b/internal/service/svcmain/svcmain.go @@ -0,0 +1,155 @@ +// Package svcmain runs one or more services. +package svcmain + +import ( + "context" + "sync" + + "github.com/getsentry/sentry-go" + "github.com/sourcegraph/log" + + "github.com/sourcegraph/sourcegraph/internal/conf" + "github.com/sourcegraph/sourcegraph/internal/debugserver" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/hostname" + "github.com/sourcegraph/sourcegraph/internal/logging" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/internal/profiler" + "github.com/sourcegraph/sourcegraph/internal/service" + "github.com/sourcegraph/sourcegraph/internal/singleprogram" + "github.com/sourcegraph/sourcegraph/internal/tracer" + "github.com/sourcegraph/sourcegraph/internal/version" +) + +type Config struct { + AfterConfigure func() // run after all services' Configure hooks are called +} + +// Main is called from the `main` function of the `sourcegraph-oss` and `sourcegraph` commands. +func Main(services []service.Service, config Config) { + liblog := log.Init(log.Resource{ + Name: env.MyName, + Version: version.Version(), + InstanceID: hostname.Get(), + }, + // Experimental: DevX is observing how sampling affects the errors signal. + log.NewSentrySinkWith( + log.SentrySink{ + ClientOptions: sentry.ClientOptions{SampleRate: 0.2}, + }, + ), + ) + logger := log.Scoped("sourcegraph", "Sourcegraph") + singleprogram.Init(logger) + run(liblog, logger, services, config, true, true) +} + +// DeprecatedSingleServiceMain is called from the `main` function of a command to start a single +// service (such as frontend or gitserver). +// +// DEPRECATED: Building per-service commands (i.e., a separate binary for frontend, gitserver, etc.) +// is deprecated. +func DeprecatedSingleServiceMain(svc service.Service, config Config, validateConfig, useConfPackage bool) { + liblog := log.Init(log.Resource{ + Name: env.MyName, + Version: version.Version(), + InstanceID: hostname.Get(), + }, + // Experimental: DevX is observing how sampling affects the errors signal. + log.NewSentrySinkWith( + log.SentrySink{ + ClientOptions: sentry.ClientOptions{SampleRate: 0.2}, + }, + ), + ) + logger := log.Scoped("sourcegraph", "Sourcegraph") + run(liblog, logger, []service.Service{svc}, config, validateConfig, useConfPackage) +} + +func run( + liblog *log.PostInitCallbacks, + logger log.Logger, + services []service.Service, + config Config, + validateConfig bool, + useConfPackage bool, +) { + defer liblog.Sync() + + // Initialize log15. Even though it's deprecated, it's still fairly widely used. + logging.Init() //nolint:staticcheck // Deprecated, but logs unmigrated to sourcegraph/log look really bad without this. + + if useConfPackage { + conf.Init() + go conf.Watch(liblog.Update(conf.GetLogSinks)) + tracer.Init(log.Scoped("tracer", "internal tracer package"), conf.DefaultClient()) + } + profiler.Init() + + obctx := observation.NewContext(logger) + ctx := context.Background() + + allReady := make(chan struct{}) + + // Run the services' Configure funcs before env vars are locked. + var ( + serviceConfigs = make([]env.Config, len(services)) + allDebugserverEndpoints []debugserver.Endpoint + ) + for i, s := range services { + var debugserverEndpoints []debugserver.Endpoint + serviceConfigs[i], debugserverEndpoints = s.Configure() + allDebugserverEndpoints = append(allDebugserverEndpoints, debugserverEndpoints...) + } + + // Validate each service's configuration. + // + // This cannot be done for executor, see the executorcmd package for details. + if validateConfig { + for i, c := range serviceConfigs { + if c == nil { + continue + } + if err := c.Validate(); err != nil { + logger.Fatal("invalid configuration", log.String("service", services[i].Name()), log.Error(err)) + } + } + } + + env.Lock() + env.HandleHelpFlag() + + if config.AfterConfigure != nil { + config.AfterConfigure() + } + + // Start the debug server. The ready boolean state it publishes will become true when *all* + // services report ready. + var allReadyWG sync.WaitGroup + go debugserver.NewServerRoutine(allReady, allDebugserverEndpoints...).Start() + + // Start the services. + for i := range services { + service := services[i] + serviceConfig := serviceConfigs[i] + allReadyWG.Add(1) + go func() { + // TODO(sqs): TODO(single-binary): Consider using the goroutine package and/or the errgroup package to report + // errors and listen to signals to initiate cleanup in a consistent way across all + // services. + obctx := observation.ScopedContext("", service.Name(), "", obctx) + err := service.Start(ctx, obctx, allReadyWG.Done, serviceConfig) + if err != nil { + logger.Fatal("failed to start service", log.String("service", service.Name()), log.Error(err)) + } + }() + } + + // Pass along the signal to the debugserver that all started services are ready. + go func() { + allReadyWG.Wait() + close(allReady) + }() + + select {} +} diff --git a/internal/singleprogram/postgresql.go b/internal/singleprogram/postgresql.go new file mode 100644 index 000000000000..10308fe8be3b --- /dev/null +++ b/internal/singleprogram/postgresql.go @@ -0,0 +1,141 @@ +package singleprogram + +import ( + "context" + "fmt" + golog "log" + "os" + "os/user" + "path/filepath" + "time" + + embeddedpostgres "github.com/fergusstrange/embedded-postgres" + + "github.com/sourcegraph/log" + + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/goroutine" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +var useEmbeddedPostgreSQL = env.MustGetBool("USE_EMBEDDED_POSTGRESQL", true, "use an embedded PostgreSQL server (to use an existing PostgreSQL server and database, set the PG* env vars)") + +type postgresqlEnvVars struct { + PGPORT, PGHOST, PGUSER, PGPASSWORD, PGDATABASE, PGSSLMODE, PGDATASOURCE string +} + +func initPostgreSQL(logger log.Logger, embeddedPostgreSQLRootDir string) error { + var vars *postgresqlEnvVars + if useEmbeddedPostgreSQL { + var err error + vars, err = startEmbeddedPostgreSQL(embeddedPostgreSQLRootDir) + if err != nil { + return err + } + os.Setenv("PGPORT", vars.PGPORT) + os.Setenv("PGHOST", vars.PGHOST) + os.Setenv("PGUSER", vars.PGUSER) + os.Setenv("PGPASSWORD", vars.PGPASSWORD) + os.Setenv("PGDATABASE", vars.PGDATABASE) + os.Setenv("PGSSLMODE", vars.PGSSLMODE) + os.Setenv("PGDATASOURCE", vars.PGDATASOURCE) + } else { + vars = &postgresqlEnvVars{ + PGPORT: os.Getenv("PGPORT"), + PGHOST: os.Getenv("PGHOST"), + PGUSER: os.Getenv("PGUSER"), + PGPASSWORD: os.Getenv("PGPASSWORD"), + PGDATABASE: os.Getenv("PGDATABASE"), + PGSSLMODE: os.Getenv("PGSSLMODE"), + PGDATASOURCE: os.Getenv("PGDATASOURCE"), + } + } + + useSinglePostgreSQLDatabase(logger, vars) + + // Migration on startup is ideal for the single-program deployment because there are no other + // simultaneously running services at startup that might interfere with a migration. + // + // TODO(sqs): TODO(single-binary): make this behavior more official and not just for "dev" + setDefaultEnv(logger, "SG_DEV_MIGRATE_ON_APPLICATION_STARTUP", "1") + + return nil +} + +func startEmbeddedPostgreSQL(pgRootDir string) (*postgresqlEnvVars, error) { + // Note: on macOS unix socket paths must be <103 bytes, so we place them in the home directory. + current, err := user.Current() + if err != nil { + return nil, errors.Wrap(err, "user.Current") + } + unixSocketDir := filepath.Join(current.HomeDir, ".sourcegraph-psql") + err = os.MkdirAll(unixSocketDir, os.ModePerm) + if err != nil { + return nil, errors.Wrap(err, "creating unix socket dir") + } + + vars := &postgresqlEnvVars{ + PGPORT: "", + PGHOST: unixSocketDir, + PGUSER: "sourcegraph", + PGPASSWORD: "", + PGDATABASE: "sourcegraph", + PGSSLMODE: "disable", + PGDATASOURCE: "postgresql:///sourcegraph?host=" + unixSocketDir, + } + + db := embeddedpostgres.NewDatabase( + embeddedpostgres.DefaultConfig(). + Version(embeddedpostgres.V14). + BinariesPath(filepath.Join(pgRootDir, "bin")). + DataPath(filepath.Join(pgRootDir, "data")). + RuntimePath(filepath.Join(pgRootDir, "runtime")). + Username(vars.PGUSER). + Database(vars.PGDATABASE). + UseUnixSocket(unixSocketDir). + Logger(golog.Writer()), + ) + if err := db.Start(); err != nil { + return nil, err + } + go goroutine.MonitorBackgroundRoutines(context.Background(), &embeddedPostgreSQLBackgroundJob{db}) + return vars, nil +} + +type embeddedPostgreSQLBackgroundJob struct { + db *embeddedpostgres.EmbeddedPostgres // must be already started +} + +func (db *embeddedPostgreSQLBackgroundJob) Start() { + // Noop. We start it synchronously on purpose because everything else following it requires it. +} + +func (db *embeddedPostgreSQLBackgroundJob) Stop() { + // Sleep a short amount of time to give other services time to write to the database during their cleanup. + time.Sleep(1000 * time.Millisecond) + if err := db.db.Stop(); err != nil { + fmt.Fprintln(os.Stderr, "error stopping embedded PostgreSQL:", err) + } +} + +func useSinglePostgreSQLDatabase(logger log.Logger, vars *postgresqlEnvVars) { + // Use a single PostgreSQL DB. + // + // For code intel: + setDefaultEnv(logger, "CODEINTEL_PGPORT", vars.PGPORT) + setDefaultEnv(logger, "CODEINTEL_PGHOST", vars.PGHOST) + setDefaultEnv(logger, "CODEINTEL_PGUSER", vars.PGUSER) + setDefaultEnv(logger, "CODEINTEL_PGPASSWORD", vars.PGPASSWORD) + setDefaultEnv(logger, "CODEINTEL_PGDATABASE", vars.PGDATABASE) + setDefaultEnv(logger, "CODEINTEL_PGSSLMODE", vars.PGSSLMODE) + setDefaultEnv(logger, "CODEINTEL_PGDATASOURCE", vars.PGDATASOURCE) + setDefaultEnv(logger, "CODEINTEL_PG_ALLOW_SINGLE_DB", "true") + // And for code insights. + setDefaultEnv(logger, "CODEINSIGHTS_PGPORT", vars.PGPORT) + setDefaultEnv(logger, "CODEINSIGHTS_PGHOST", vars.PGHOST) + setDefaultEnv(logger, "CODEINSIGHTS_PGUSER", vars.PGUSER) + setDefaultEnv(logger, "CODEINSIGHTS_PGPASSWORD", vars.PGPASSWORD) + setDefaultEnv(logger, "CODEINSIGHTS_PGDATABASE", vars.PGDATABASE) + setDefaultEnv(logger, "CODEINSIGHTS_PGSSLMODE", vars.PGSSLMODE) + setDefaultEnv(logger, "CODEINSIGHTS_PGDATASOURCE", vars.PGDATASOURCE) +} diff --git a/internal/singleprogram/singleprogram.go b/internal/singleprogram/singleprogram.go new file mode 100644 index 000000000000..5e019816b43a --- /dev/null +++ b/internal/singleprogram/singleprogram.go @@ -0,0 +1,160 @@ +// Package singleprogram contains runtime utilities for the single-program (Go static binary) +// distribution of Sourcegraph. +package singleprogram + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/sourcegraph/log" + + "github.com/sourcegraph/sourcegraph/internal/conf/confdefaults" + "github.com/sourcegraph/sourcegraph/internal/env" +) + +func Init(logger log.Logger) { + // TODO(sqs) TODO(single-binary): see the env.HackClearEnvironCache docstring, we should be able to remove this + // eventually. + env.HackClearEnvironCache() + + // INDEXED_SEARCH_SERVERS is empty (but defined) so that indexed search is disabled. + setDefaultEnv(logger, "INDEXED_SEARCH_SERVERS", "") + + // Need to set this to avoid trying to look up gitservers via k8s service discovery. + // TODO(sqs) TODO(single-binary): Make this not require the hostname. + hostname, err := os.Hostname() + if err != nil { + fmt.Fprintln(os.Stderr, "unable to determine hostname:", err) + os.Exit(1) + } + setDefaultEnv(logger, "SRC_GIT_SERVERS", hostname+":3178") + + setDefaultEnv(logger, "SYMBOLS_URL", "http://127.0.0.1:3184") + setDefaultEnv(logger, "SEARCHER_URL", "http://127.0.0.1:3181") + setDefaultEnv(logger, "REPO_UPDATER_URL", "http://127.0.0.1:3182") + + // The syntax-highlighter might not be running, but this is a better default than an internal + // hostname. + setDefaultEnv(logger, "SRC_SYNTECT_SERVER", "http://localhost:9238") + + // Jaeger might not be running, but this is a better default than an internal hostname. + // + // TODO(sqs) TODO(single-binary): this isnt taking effect + // + // setDefaultEnv(logger, "JAEGER_SERVER_URL", "http://localhost:16686") + + // The s3proxy blobstore does need to be running. TODO(sqs): TODO(single-binary): bundle this somehow? + setDefaultEnv(logger, "PRECISE_CODE_INTEL_UPLOAD_AWS_ENDPOINT", "http://localhost:9000") + setDefaultEnv(logger, "PRECISE_CODE_INTEL_UPLOAD_BACKEND", "blobstore") + + // Need to override this because without a host (eg ":3080") it listens only on localhost, which + // is not accessible from the containers + setDefaultEnv(logger, "SRC_HTTP_ADDR", "0.0.0.0:3080") + + // This defaults to an internal hostname. + setDefaultEnv(logger, "SRC_FRONTEND_INTERNAL", "localhost:3090") + + cacheDir, err := os.UserCacheDir() + if err == nil { + cacheDir = filepath.Join(cacheDir, "sourcegraph-sp") + err = os.MkdirAll(cacheDir, 0700) + } + if err != nil { + fmt.Fprintln(os.Stderr, "unable to make user cache directory:", err) + os.Exit(1) + } + + setDefaultEnv(logger, "SRC_REPOS_DIR", filepath.Join(cacheDir, "repos")) + setDefaultEnv(logger, "CACHE_DIR", filepath.Join(cacheDir, "cache")) + + configDir, err := os.UserConfigDir() + if err == nil { + configDir = filepath.Join(configDir, "sourcegraph-sp") + err = os.MkdirAll(configDir, 0700) + } + if err != nil { + fmt.Fprintln(os.Stderr, "unable to make user config directory:", err) + os.Exit(1) + } + + embeddedPostgreSQLRootDir := filepath.Join(configDir, "postgresql") + if err := initPostgreSQL(logger, embeddedPostgreSQLRootDir); err != nil { + fmt.Fprintln(os.Stderr, "unable to set up PostgreSQL:", err) + os.Exit(1) + } + + writeFileIfNotExists := func(path string, data []byte) { + var err error + if _, err = os.Stat(path); os.IsNotExist(err) { + err = os.WriteFile(path, data, 0600) + } + if err != nil { + fmt.Fprintf(os.Stderr, "unable to write file %s: %s\n", path, err) + os.Exit(1) + } + } + + siteConfigPath := filepath.Join(configDir, "site-config.json") + setDefaultEnv(logger, "SITE_CONFIG_FILE", siteConfigPath) + setDefaultEnv(logger, "SITE_CONFIG_ALLOW_EDITS", "true") + writeFileIfNotExists(siteConfigPath, []byte(confdefaults.SingleProgram.Site)) + + globalSettingsPath := filepath.Join(configDir, "global-settings.json") + setDefaultEnv(logger, "GLOBAL_SETTINGS_FILE", globalSettingsPath) + setDefaultEnv(logger, "GLOBAL_SETTINGS_ALLOW_EDITS", "true") + writeFileIfNotExists(globalSettingsPath, []byte("{}\n")) + + // Escape hatch isn't needed in local dev since the site config can always just be a file on disk. + setDefaultEnv(logger, "NO_SITE_CONFIG_ESCAPE_HATCH", "1") + + // We disable the use of executors passwords, because executors only listen on `localhost` this + // is safe to do. + setDefaultEnv(logger, "EXECUTOR_FRONTEND_URL", "http://localhost:3080") + setDefaultEnv(logger, "EXECUTOR_FRONTEND_PASSWORD", "none") + setDefaultEnv(logger, "EXECUTOR_QUEUE_DISABLE_ACCESS_TOKEN_INSECURE", "true") + + setDefaultEnv(logger, "EXECUTOR_USE_FIRECRACKER", "false") + // TODO(sqs): TODO(single-binary): Make it so we can run multiple executors in single-program mode. Right now, you + // need to change this to "batches" to use batch changes executors. + setDefaultEnv(logger, "EXECUTOR_QUEUE_NAME", "codeintel") + + writeFile := func(path string, data []byte, perm fs.FileMode) { + if err := os.WriteFile(path, data, perm); err != nil { + fmt.Fprintf(os.Stderr, "unable to write file %s: %s\n", path, err) + os.Exit(1) + } + } + + setDefaultEnv(logger, "CTAGS_PROCESSES", "2") + // Write script that invokes universal-ctags via Docker. + // TODO(sqs): TODO(single-binary): stop relying on a ctags Docker image + ctagsPath := filepath.Join(cacheDir, "universal-ctags-dev") + writeFile(ctagsPath, []byte(universalCtagsDevScript), 0700) + setDefaultEnv(logger, "CTAGS_COMMAND", ctagsPath) +} + +// universalCtagsDevScript is copied from cmd/symbols/universal-ctags-dev. +const universalCtagsDevScript = `#!/usr/bin/env bash + +# This script is a wrapper around universal-ctags. + +exec docker run --rm -i \ + -a stdin -a stdout -a stderr \ + --user guest \ + --name=universal-ctags-$$ \ + --entrypoint /usr/local/bin/universal-ctags \ + slimsag/ctags:latest@sha256:dd21503a3ae51524ab96edd5c0d0b8326d4baaf99b4238dfe8ec0232050af3c7 "$@" +` + +// setDefaultEnv will set the environment variable if it is not set. +func setDefaultEnv(logger log.Logger, k, v string) { + if _, ok := os.LookupEnv(k); ok { + return + } + err := os.Setenv(k, v) + if err != nil { + logger.Fatal("setting default env variable", log.Error(err)) + } +} diff --git a/internal/symbols/client.go b/internal/symbols/client.go index 1df8434beb53..7fe21207e1b7 100644 --- a/internal/symbols/client.go +++ b/internal/symbols/client.go @@ -32,7 +32,19 @@ import ( "github.com/sourcegraph/sourcegraph/lib/errors" ) -var symbolsURL = env.Get("SYMBOLS_URL", "k8s+http://symbols:3184", "symbols service URL") +func LoadConfig() { + symbolsURL := env.Get("SYMBOLS_URL", "k8s+http://symbols:3184", "symbols service URL") + DefaultClient = &Client{ + URL: symbolsURL, + HTTPClient: defaultDoer, + HTTPLimiter: parallel.NewRun(500), + SubRepoPermsChecker: func() authz.SubRepoPermissionChecker { return authz.DefaultSubRepoPermsChecker }, + } +} + +// DefaultClient is the default Client. Unless overwritten, it is connected to the server specified by the +// SYMBOLS_URL environment variable. +var DefaultClient *Client var defaultDoer = func() httpcli.Doer { d, err := httpcli.NewInternalClientFactory("symbols").Doer() @@ -42,15 +54,6 @@ var defaultDoer = func() httpcli.Doer { return d }() -// DefaultClient is the default Client. Unless overwritten, it is connected to the server specified by the -// SYMBOLS_URL environment variable. -var DefaultClient = &Client{ - URL: symbolsURL, - HTTPClient: defaultDoer, - HTTPLimiter: parallel.NewRun(500), - SubRepoPermsChecker: func() authz.SubRepoPermissionChecker { return authz.DefaultSubRepoPermsChecker }, -} - // Client is a symbols service client. type Client struct { // URL to symbols service. diff --git a/internal/symbols/client_test.go b/internal/symbols/client_test.go index 253cbe682f83..372b23f469d0 100644 --- a/internal/symbols/client_test.go +++ b/internal/symbols/client_test.go @@ -14,6 +14,10 @@ import ( "github.com/sourcegraph/sourcegraph/internal/types" ) +func init() { + LoadConfig() +} + func TestSearchWithFiltering(t *testing.T) { ctx := context.Background() fixture := search.SymbolsResponse{ diff --git a/internal/workerutil/observability.go b/internal/workerutil/observability.go index 74573e65b731..b381c5d4bd8a 100644 --- a/internal/workerutil/observability.go +++ b/internal/workerutil/observability.go @@ -93,7 +93,10 @@ func NewMetrics(observationCtx *observation.Context, prefix string, opts ...Obse Help: help, }, keys) - observationCtx.Registerer.MustRegister(gaugeVec) + // TODO(sqs): TODO(single-binary): Ideally we would be using MustRegister here, not the + // IgnoreDuplicate variant. This is a bit of a hack to allow 2 executor instances to run in a + // single binary deployment. + gaugeVec = metrics.MustRegisterIgnoreDuplicate(observationCtx.Registerer, gaugeVec) return gaugeVec.WithLabelValues(values...) } diff --git a/sg.config.yaml b/sg.config.yaml index a6b5f5cc445c..c0c058a805bf 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -99,8 +99,8 @@ env: # Required for frontend and executor to communicate EXECUTOR_FRONTEND_URL: http://localhost:3080 - # Must match the secret defined in the site config. - EXECUTOR_FRONTEND_PASSWORD: hunter2hunter2hunter2 + EXECUTOR_FRONTEND_PASSWORD: none + EXECUTOR_QUEUE_DISABLE_ACCESS_TOKEN_INSECURE: true # Disable firecracker inside executor in dev EXECUTOR_USE_FIRECRACKER: false @@ -817,6 +817,55 @@ commands: cmd: pnpm --filter @sourcegraph/browser dev install: pnpm install + sourcegraph: + description: Single program (Go static binary) distribution + cmd: | + unset SRC_GIT_SERVERS INDEXED_SEARCH_SERVERS + + # TODO: This should be fixed + export SOURCEGRAPH_LICENSE_GENERATION_KEY=$(cat ../dev-private/enterprise/dev/test-license-generation-key.pem) + # If EXTSVC_CONFIG_FILE is *unset*, set a default. + export EXTSVC_CONFIG_FILE=${EXTSVC_CONFIG_FILE-'../dev-private/enterprise/dev/external-services-config.json'} + + .bin/sourcegraph + install: | + if [ -n "$DELVE" ]; then + export GCFLAGS='all=-N -l' + fi + go build -gcflags="$GCFLAGS" -ldflags="-X github.com/sourcegraph/sourcegraph/internal/conf/deploy.forceType=single-program" -o .bin/sourcegraph github.com/sourcegraph/sourcegraph/enterprise/cmd/sourcegraph + checkBinary: .bin/sourcegraph + env: + USE_EMBEDDED_POSTGRESQL: false + ENTERPRISE: 1 + SITE_CONFIG_FILE: '../dev-private/enterprise/dev/site-config.json' + SITE_CONFIG_ESCAPE_HATCH_PATH: '$HOME/.sourcegraph/site-config.json' + WEBPACK_DEV_SERVER: 1 + watch: + - cmd + - enterprise + - internal + - lib + - schema + + sourcegraph-oss: + description: Single program (Go static binary) distribution, OSS variant + cmd: | + unset SRC_GIT_SERVERS INDEXED_SEARCH_SERVERS + .bin/sourcegraph-oss + install: | + if [ -n "$DELVE" ]; then + export GCFLAGS='all=-N -l' + fi + go build -gcflags="$GCFLAGS" -ldflags="-X github.com/sourcegraph/sourcegraph/internal/conf/deploy.forceType=single-program" -o .bin/sourcegraph-oss github.com/sourcegraph/sourcegraph/cmd/sourcegraph-oss + checkBinary: .bin/sourcegraph-oss + env: + USE_EMBEDDED_POSTGRESQL: false + WEBPACK_DEV_SERVER: 1 + watch: + - cmd + - internal + - schema + defaultCommandset: enterprise commandsets: oss: @@ -1071,6 +1120,34 @@ commandsets: - otel-collector - jaeger + enterprise-single-program: + requiresDevPrivate: true + checks: + - docker + - redis + - postgres + - git + commands: + - sourcegraph + - docsite + - web + - syntax-highlighter + - blobstore + - caddy + + oss-single-program: + checks: + - docker + - redis + - postgres + - git + commands: + - sourcegraph-oss + - docsite + - oss-web + - syntax-highlighter + - caddy + tests: # These can be run with `sg test [name]` backend: From dd358256dca4d93436ff06e97fe328432dc77f30 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Fri, 20 Jan 2023 10:57:11 +0200 Subject: [PATCH 051/678] go mod tidy (#46701) --- go.sum | 1 + 1 file changed, 1 insertion(+) diff --git a/go.sum b/go.sum index 2c89d347eff5..af3e3061c037 100644 --- a/go.sum +++ b/go.sum @@ -1591,6 +1591,7 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= From 45afb64ae1eba44a873a06a671a4ccf51b65f0d3 Mon Sep 17 00:00:00 2001 From: Alex Ostrikov Date: Fri, 20 Jan 2023 13:34:49 +0400 Subject: [PATCH 052/678] chore: update `explicit_permissions_bitbucket_projects_jobs_worker` description. (#46700) Test plan: N/A, not a functional change. --- .../cmd/worker/internal/permissions/bitbucket_projects.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise/cmd/worker/internal/permissions/bitbucket_projects.go b/enterprise/cmd/worker/internal/permissions/bitbucket_projects.go index c924a5a68a1a..71e765f61487 100644 --- a/enterprise/cmd/worker/internal/permissions/bitbucket_projects.go +++ b/enterprise/cmd/worker/internal/permissions/bitbucket_projects.go @@ -357,7 +357,7 @@ func newBitbucketProjectPermissionsWorker(ctx context.Context, observationCtx *o options := workerutil.WorkerOptions{ Name: "explicit_permissions_bitbucket_projects_jobs_worker", - Description: "reads the `explicit_permissions_bitbucket_projects_jobs` table and executes the jobs", + Description: "syncs Bitbucket Projects via Explicit Permissions API", NumHandlers: cfg.WorkerConcurrency, Interval: cfg.WorkerPollInterval, HeartbeatInterval: 15 * time.Second, From 4f2f4b2073fab2ff2f0bff4d289fba7ef1c4fb2f Mon Sep 17 00:00:00 2001 From: Naman Kumar Date: Fri, 20 Jan 2023 15:05:32 +0530 Subject: [PATCH 053/678] Add cursor based pagination to repositories page (#46624) * [Repositories] add cursor-based pagination Co-authored-by: Indradhanush Gupta Co-authored-by: Indradhanush Gupta --- .../FilteredConnection/FilteredConnection.tsx | 20 +- .../hooks/usePageSwitcherPagination.ts | 5 +- .../components/FilteredConnection/utils.ts | 6 +- .../site-admin/SiteAdminRepositoriesPage.tsx | 147 +++++- .../AnalyticsOverviewPage/queries.ts | 2 +- client/web/src/site-admin/backend.ts | 125 ++--- .../graphqlutil/connection_resolver.go | 35 +- .../graphqlutil/connection_resolver_test.go | 34 +- cmd/frontend/graphqlbackend/org.go | 6 +- cmd/frontend/graphqlbackend/repositories.go | 173 +++++-- .../graphqlbackend/repositories_test.go | 458 +++++++++++++----- cmd/frontend/graphqlbackend/repository.go | 8 + .../graphqlbackend/repository_contributors.go | 35 +- .../repository_contributors_test.go | 8 +- cmd/frontend/graphqlbackend/saved_searches.go | 7 +- cmd/frontend/graphqlbackend/schema.graphql | 33 +- .../site_config_change_connection.go | 30 +- .../site_config_change_connection_test.go | 43 +- cmd/frontend/graphqlbackend/site_test.go | 2 +- internal/database/conf_test.go | 17 +- internal/database/helpers.go | 109 ++++- internal/database/repos.go | 32 +- 22 files changed, 955 insertions(+), 380 deletions(-) diff --git a/client/web/src/components/FilteredConnection/FilteredConnection.tsx b/client/web/src/components/FilteredConnection/FilteredConnection.tsx index 6859634fd6b3..d9f451a9d91a 100644 --- a/client/web/src/components/FilteredConnection/FilteredConnection.tsx +++ b/client/web/src/components/FilteredConnection/FilteredConnection.tsx @@ -628,17 +628,19 @@ export class FilteredConnection< this.showMoreClicks.next() } - private buildArgs = (filterValues: Map): FilteredConnectionArgs => { - let args: FilteredConnectionArgs = {} - for (const key of filterValues.keys()) { - const value = filterValues.get(key) - if (value === undefined) { - continue - } - args = { ...args, ...value.args } + private buildArgs = buildFilterArgs +} + +export const buildFilterArgs = (filterValues: Map): FilteredConnectionArgs => { + let args: FilteredConnectionArgs = {} + for (const key of filterValues.keys()) { + const value = filterValues.get(key) + if (value === undefined) { + continue } - return args + args = { ...args, ...value.args } } + return args } /** diff --git a/client/web/src/components/FilteredConnection/hooks/usePageSwitcherPagination.ts b/client/web/src/components/FilteredConnection/hooks/usePageSwitcherPagination.ts index 9c5f14f081e3..087afc08cd3e 100644 --- a/client/web/src/components/FilteredConnection/hooks/usePageSwitcherPagination.ts +++ b/client/web/src/components/FilteredConnection/hooks/usePageSwitcherPagination.ts @@ -41,7 +41,7 @@ export interface UsePaginatedConnectionResult extend connection?: PaginatedConnection loading: boolean error?: ApolloError - refetch: () => any + refetch: (variables?: TVariables) => any } interface UsePaginatedConnectionConfig { @@ -53,6 +53,8 @@ interface UsePaginatedConnectionConfig { fetchPolicy?: WatchQueryFetchPolicy // Allows running an optional callback on any successful request onCompleted?: (data: TResult) => void + // Allows to provide polling interval to useQuery + pollInterval?: number } interface UsePaginatedConnectionParameters { @@ -98,6 +100,7 @@ export const usePageSwitcherPagination = { diff --git a/client/web/src/components/FilteredConnection/utils.ts b/client/web/src/components/FilteredConnection/utils.ts index ebaa9adf7a9e..acf9a893d548 100644 --- a/client/web/src/components/FilteredConnection/utils.ts +++ b/client/web/src/components/FilteredConnection/utils.ts @@ -65,7 +65,7 @@ export const hasNextPage = (connection: Connection): boolean => : typeof connection.totalCount === 'number' && connection.nodes.length < connection.totalCount export interface GetUrlQueryParameters { - first: { + first?: { actual: number default: number } @@ -93,7 +93,7 @@ export const getUrlQuery = ({ searchParameters.set(QUERY_KEY, query) } - if (first.actual !== first.default) { + if (!!first && first.actual !== first.default) { searchParameters.set('first', String(first.actual)) } @@ -111,7 +111,7 @@ export const getUrlQuery = ({ } } - if (visibleResultCount && visibleResultCount !== 0 && visibleResultCount !== first.actual) { + if (visibleResultCount && visibleResultCount !== 0 && visibleResultCount !== first?.actual) { searchParameters.set('visible', String(visibleResultCount)) } diff --git a/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx b/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx index d7e2efd63303..4bb77077dcb3 100644 --- a/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx +++ b/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx @@ -1,8 +1,8 @@ -import React, { useCallback, useEffect, useMemo } from 'react' +import React, { useState, useEffect, useMemo } from 'react' import { mdiCloudDownload, mdiCog, mdiBrain } from '@mdi/js' +import { isEqual } from 'lodash' import { RouteComponentProps } from 'react-router' -import { Observable } from 'rxjs' import { logger } from '@sourcegraph/common' import { useQuery } from '@sourcegraph/http-client' @@ -15,6 +15,7 @@ import { Container, H4, Icon, + Input, Link, LoadingSpinner, PageHeader, @@ -22,17 +23,22 @@ import { Tooltip, ErrorAlert, LinkOrSpan, + PageSwitcher, } from '@sourcegraph/wildcard' import { EXTERNAL_SERVICE_IDS_AND_NAMES } from '../components/externalServices/backend' import { - FilteredConnection, + buildFilterArgs, + FilterControl, + FilteredConnectionFilterValue, FilteredConnectionFilter, - FilteredConnectionQueryArguments, } from '../components/FilteredConnection' +import { usePageSwitcherPagination } from '../components/FilteredConnection/hooks/usePageSwitcherPagination' +import { getFilterFromURL, getUrlQuery } from '../components/FilteredConnection/utils' import { PageTitle } from '../components/PageTitle' import { RepositoriesResult, + RepositoriesVariables, RepositoryOrderBy, RepositoryStatsResult, ExternalServiceIDsAndNamesVariables, @@ -43,7 +49,7 @@ import { import { refreshSiteFlags } from '../site/backend' import { ValueLegendList, ValueLegendListProps } from './analytics/components/ValueLegendList' -import { fetchAllRepositoriesAndPollIfEmptyOrAnyCloning, REPOSITORY_STATS, REPO_PAGE_POLL_INTERVAL } from './backend' +import { REPOSITORY_STATS, REPO_PAGE_POLL_INTERVAL, REPOSITORIES_QUERY } from './backend' import { ExternalRepositoryIcon } from './components/ExternalRepositoryIcon' import { RepoMirrorInfo } from './components/RepoMirrorInfo' @@ -355,17 +361,79 @@ export const SiteAdminRepositoriesPage: React.FunctionComponent => - fetchAllRepositoriesAndPollIfEmptyOrAnyCloning(args), - [] + const [filterValues, setFilterValues] = useState>(() => + getFilterFromURL(new URLSearchParams(location.search), filters) ) + + useEffect(() => { + setFilterValues(getFilterFromURL(new URLSearchParams(location.search), filters)) + }, [filters, location.search]) + + const [searchQuery, setSearchQuery] = useState( + () => new URLSearchParams(location.search).get('query') || '' + ) + + useEffect(() => { + const searchFragment = getUrlQuery({ + query: searchQuery, + filters, + filterValues, + search: location.search, + }) + const searchFragmentParams = new URLSearchParams(searchFragment) + searchFragmentParams.sort() + + const oldParams = new URLSearchParams(location.search) + oldParams.sort() + + if (!isEqual(Array.from(searchFragmentParams), Array.from(oldParams))) { + history.replace({ + search: searchFragment, + hash: location.hash, + // Do not throw away flash messages + state: location.state, + }) + } + }, [filters, filterValues, searchQuery, location, history]) + + const variables = useMemo(() => { + const args = buildFilterArgs(filterValues) + + return { + ...args, + query: searchQuery, + indexed: args.indexed ?? true, + notIndexed: args.notIndexed ?? true, + failedFetch: args.failedFetch ?? false, + corrupted: args.corrupted ?? false, + cloneStatus: args.cloneStatus ?? null, + externalService: args.externalService ?? null, + } as RepositoriesVariables + }, [searchQuery, filterValues]) + + const { + connection, + loading: reposLoading, + error: reposError, + refetch, + ...paginationProps + } = usePageSwitcherPagination({ + query: REPOSITORIES_QUERY, + variables, + getConnection: ({ data }) => data?.repositories || undefined, + options: { pollInterval: 5000 }, + }) + + useEffect(() => { + refetch(variables) + }, [refetch, variables]) + const showRepositoriesAddedBanner = new URLSearchParams(location.search).has('repositoriesUpdated') const licenseInfo = window.context.licenseInfo - const error = repoStatsError || extSvcError - const loading = repoStatsLoading || extSvcLoading + const error = repoStatsError || extSvcError || reposError + const loading = repoStatsLoading || extSvcLoading || reposLoading return (
@@ -413,20 +481,49 @@ export const SiteAdminRepositoriesPage: React.FunctionComponent} {legends && } {extSvcs && ( - > - className="mb-0" - listClassName="list-group list-group-flush mb-0" - summaryClassName="mt-2" - withCenteredSummary={true} - noun="repository" - pluralNoun="repositories" - queryConnection={queryRepositories} - nodeComponent={RepositoryNode} - inputClassName="ml-2 flex-1" - filters={filters} - history={history} - location={location} - /> + <> +
+ + setFilterValues(values => { + const newValues = new Map(values) + newValues.set(filter.id, value) + return newValues + }) + } + /> + setSearchQuery(event.currentTarget.value)} + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck={false} + aria-label="Search repositories..." + variant="regular" + /> +
+
    + {(connection?.nodes || []).map(node => ( + + ))} +
+ + )}
diff --git a/client/web/src/site-admin/analytics/AnalyticsOverviewPage/queries.ts b/client/web/src/site-admin/analytics/AnalyticsOverviewPage/queries.ts index 7e644c4a7f11..b0092c7b54ad 100644 --- a/client/web/src/site-admin/analytics/AnalyticsOverviewPage/queries.ts +++ b/client/web/src/site-admin/analytics/AnalyticsOverviewPage/queries.ts @@ -20,7 +20,7 @@ export const OVERVIEW_STATISTICS = gql` totalCount } repositories { - totalCount(precise: true) + totalCount } repositoryStats { gitDirBytes diff --git a/client/web/src/site-admin/backend.ts b/client/web/src/site-admin/backend.ts index 929d483d642e..ef01e6f10699 100644 --- a/client/web/src/site-admin/backend.ts +++ b/client/web/src/site-admin/backend.ts @@ -3,7 +3,7 @@ import { parse as parseJSONC } from 'jsonc-parser' import { Observable } from 'rxjs' import { map, mapTo, tap } from 'rxjs/operators' -import { repeatUntil, resetAllMemoizationCaches } from '@sourcegraph/common' +import { resetAllMemoizationCaches } from '@sourcegraph/common' import { createInvalidGraphQLMutationResponseError, dataOrThrowErrors, gql, useQuery } from '@sourcegraph/http-client' import { Settings } from '@sourcegraph/shared/src/settings/settings' @@ -31,9 +31,6 @@ import { RandomizeUserPasswordResult, ReloadSiteResult, ReloadSiteVariables, - RepositoriesResult, - RepositoriesVariables, - RepositoryOrderBy, Scalars, ScheduleRepositoryPermissionsSyncResult, ScheduleRepositoryPermissionsSyncVariables, @@ -136,6 +133,7 @@ const siteAdminRepositoryFieldsFragment = gql` ${externalRepositoryFieldsFragment} fragment SiteAdminRepositoryFields on Repository { + __typename id name createdAt @@ -150,85 +148,54 @@ const siteAdminRepositoryFieldsFragment = gql` } } ` - -/** - * Fetches all repositories. - * - * @returns Observable that emits the list of repositories - */ -function fetchAllRepositories(args: Partial): Observable { - return requestGraphQL( - gql` - query Repositories( - $first: Int - $query: String - $indexed: Boolean - $notIndexed: Boolean - $failedFetch: Boolean - $corrupted: Boolean - $cloneStatus: CloneStatus - $orderBy: RepositoryOrderBy - $descending: Boolean - $externalService: ID - ) { - repositories( - first: $first - query: $query - indexed: $indexed - notIndexed: $notIndexed - failedFetch: $failedFetch - corrupted: $corrupted - cloneStatus: $cloneStatus - orderBy: $orderBy - descending: $descending - externalService: $externalService - ) { - nodes { - ...SiteAdminRepositoryFields - } - totalCount(precise: true) - pageInfo { - hasNextPage - } - } +export const REPOSITORIES_QUERY = gql` + query Repositories( + $first: Int + $last: Int + $after: String + $before: String + $query: String + $indexed: Boolean + $notIndexed: Boolean + $failedFetch: Boolean + $corrupted: Boolean + $cloneStatus: CloneStatus + $orderBy: RepositoryOrderBy + $descending: Boolean + $externalService: ID + ) { + repositories( + first: $first + last: $last + after: $after + before: $before + query: $query + indexed: $indexed + notIndexed: $notIndexed + failedFetch: $failedFetch + corrupted: $corrupted + cloneStatus: $cloneStatus + orderBy: $orderBy + descending: $descending + externalService: $externalService + ) { + nodes { + ...SiteAdminRepositoryFields + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor } - - ${siteAdminRepositoryFieldsFragment} - `, - { - indexed: args.indexed ?? true, - notIndexed: args.notIndexed ?? true, - failedFetch: args.failedFetch ?? false, - corrupted: args.corrupted ?? false, - first: args.first ?? null, - query: args.query ?? null, - cloneStatus: args.cloneStatus ?? null, - orderBy: args.orderBy ?? RepositoryOrderBy.REPOSITORY_NAME, - descending: args.descending ?? false, - externalService: args.externalService ?? null, } - ).pipe( - map(dataOrThrowErrors), - map(data => data.repositories) - ) -} + } -export const REPO_PAGE_POLL_INTERVAL = 5000 + ${siteAdminRepositoryFieldsFragment} +` -export function fetchAllRepositoriesAndPollIfEmptyOrAnyCloning( - args: Partial -): Observable { - return fetchAllRepositories(args).pipe( - // Poll every 5000ms if repositories are being cloned or the list is empty. - repeatUntil( - result => - result.nodes && - result.nodes.length > 0 && - result.nodes.every(nodes => !nodes.mirrorInfo.cloneInProgress && nodes.mirrorInfo.cloned), - { delay: REPO_PAGE_POLL_INTERVAL } - ) - ) -} +export const REPO_PAGE_POLL_INTERVAL = 5000 export const SLOW_REQUESTS = gql` query SlowRequests($after: String) { diff --git a/cmd/frontend/graphqlbackend/graphqlutil/connection_resolver.go b/cmd/frontend/graphqlbackend/graphqlutil/connection_resolver.go index 99364a4fd8c1..d6a2de27c30d 100644 --- a/cmd/frontend/graphqlbackend/graphqlutil/connection_resolver.go +++ b/cmd/frontend/graphqlbackend/graphqlutil/connection_resolver.go @@ -24,9 +24,9 @@ type ConnectionResolverStore[N any] interface { // ComputeNodes returns the list of nodes based on the pagination args. ComputeNodes(context.Context, *database.PaginationArgs) ([]*N, error) // MarshalCursor returns cursor for a node and is called for generating start and end cursors. - MarshalCursor(*N) (*string, error) + MarshalCursor(*N, database.OrderBy) (*string, error) // UnmarshalCursor returns node id from after/before cursor string. - UnmarshalCursor(string) (*int, error) + UnmarshalCursor(string, database.OrderBy) (*string, error) } type ConnectionResolverArgs struct { @@ -60,6 +60,10 @@ type ConnectionResolverOptions struct { // // Defaults to `true` when not set. Reverse *bool + // Columns to order by + OrderBy database.OrderBy + // Order direction + Ascending bool } // MaxPageSize returns the configured max page limit for the connection @@ -104,7 +108,10 @@ func (r *ConnectionResolver[N]) paginationArgs() (*database.PaginationArgs, erro return nil, nil } - paginationArgs := database.PaginationArgs{} + paginationArgs := database.PaginationArgs{ + OrderBy: r.options.OrderBy, + Ascending: r.options.Ascending, + } limit := r.pageSize() + 1 if r.args.First != nil { @@ -116,7 +123,7 @@ func (r *ConnectionResolver[N]) paginationArgs() (*database.PaginationArgs, erro } if r.args.After != nil { - after, err := r.store.UnmarshalCursor(*r.args.After) + after, err := r.store.UnmarshalCursor(*r.args.After, r.options.OrderBy) if err != nil { return nil, err } @@ -125,7 +132,7 @@ func (r *ConnectionResolver[N]) paginationArgs() (*database.PaginationArgs, erro } if r.args.Before != nil { - before, err := r.store.UnmarshalCursor(*r.args.Before) + before, err := r.store.UnmarshalCursor(*r.args.Before, r.options.OrderBy) if err != nil { return nil, err } @@ -146,11 +153,11 @@ func (r *ConnectionResolver[N]) TotalCount(ctx context.Context) (int32, error) { r.data.total, r.data.totalError = r.store.ComputeTotal(ctx) }) - if r.data.totalError != nil || r.data.total == nil { - return 0, r.data.totalError + if r.data.total != nil { + return *r.data.total, r.data.totalError } - return *r.data.total, r.data.totalError + return 0, r.data.totalError } // Nodes returns value for connection.Nodes and is called by the graphql api. @@ -208,6 +215,7 @@ func (r *ConnectionResolver[N]) PageInfo(ctx context.Context) (*ConnectionPageIn nodes: nodes, store: r.store, args: r.args, + orderBy: r.options.OrderBy, }, nil } @@ -217,6 +225,7 @@ type ConnectionPageInfo[N any] struct { nodes []*N store ConnectionResolverStore[N] args *ConnectionResolverArgs + orderBy database.OrderBy } // HasNextPage returns value for connection.pageInfo.hasNextPage and is called by the graphql api. @@ -251,7 +260,7 @@ func (p *ConnectionPageInfo[N]) EndCursor() (cursor *string, err error) { return nil, nil } - cursor, err = p.store.MarshalCursor(p.nodes[len(p.nodes)-1]) + cursor, err = p.store.MarshalCursor(p.nodes[len(p.nodes)-1], p.orderBy) return } @@ -262,7 +271,7 @@ func (p *ConnectionPageInfo[N]) StartCursor() (cursor *string, err error) { return nil, nil } - cursor, err = p.store.MarshalCursor(p.nodes[0]) + cursor, err = p.store.MarshalCursor(p.nodes[0], p.orderBy) return } @@ -270,7 +279,11 @@ func (p *ConnectionPageInfo[N]) StartCursor() (cursor *string, err error) { // NewConnectionResolver returns a new connection resolver built using the store and connection args. func NewConnectionResolver[N any](store ConnectionResolverStore[N], args *ConnectionResolverArgs, options *ConnectionResolverOptions) (*ConnectionResolver[N], error) { if options == nil { - options = &ConnectionResolverOptions{} + options = &ConnectionResolverOptions{OrderBy: database.OrderBy{{Field: "id"}}} + } + + if len(options.OrderBy) == 0 { + options.OrderBy = database.OrderBy{{Field: "id"}} } return &ConnectionResolver[N]{ diff --git a/cmd/frontend/graphqlbackend/graphqlutil/connection_resolver_test.go b/cmd/frontend/graphqlbackend/graphqlutil/connection_resolver_test.go index 95fd18075f2a..4b577e2d224b 100644 --- a/cmd/frontend/graphqlbackend/graphqlutil/connection_resolver_test.go +++ b/cmd/frontend/graphqlbackend/graphqlutil/connection_resolver_test.go @@ -3,7 +3,6 @@ package graphqlutil import ( "context" "fmt" - "strconv" "testing" "github.com/google/go-cmp/cmp" @@ -55,19 +54,14 @@ func (s *testConnectionStore) ComputeNodes(ctx context.Context, args *database.P return nodes, nil } -func (*testConnectionStore) MarshalCursor(n *testConnectionNode) (*string, error) { +func (*testConnectionStore) MarshalCursor(n *testConnectionNode, _ database.OrderBy) (*string, error) { cursor := string(n.ID()) return &cursor, nil } -func (*testConnectionStore) UnmarshalCursor(cursor string) (*int, error) { - id, err := strconv.Atoi(cursor) - if err != nil { - return nil, err - } - - return &id, nil +func (*testConnectionStore) UnmarshalCursor(cursor string, _ database.OrderBy) (*string, error) { + return &cursor, nil } func newInt(n int) *int { @@ -116,13 +110,13 @@ func withLastPA(last int, a *database.PaginationArgs) *database.PaginationArgs { return a } -func withAfterPA(after int, a *database.PaginationArgs) *database.PaginationArgs { +func withAfterPA(after string, a *database.PaginationArgs) *database.PaginationArgs { a.After = &after return a } -func withBeforePA(before int, a *database.PaginationArgs) *database.PaginationArgs { +func withBeforePA(before string, a *database.PaginationArgs) *database.PaginationArgs { a.Before = &before return a @@ -166,6 +160,14 @@ func testResolverNodesResponse(t *testing.T, resolver *ConnectionResolver[testCo } } +func buildPaginationArgs() *database.PaginationArgs { + args := database.PaginationArgs{ + OrderBy: database.OrderBy{{Field: "id"}}, + } + + return &args +} + func TestConnectionNodes(t *testing.T) { for _, test := range []struct { name string @@ -177,30 +179,30 @@ func TestConnectionNodes(t *testing.T) { { name: "default", connectionArgs: withFirstCA(5, &ConnectionResolverArgs{}), - wantPaginationArgs: withFirstPA(6, &database.PaginationArgs{}), + wantPaginationArgs: withFirstPA(6, buildPaginationArgs()), wantNodes: 2, }, { name: "last arg", - wantPaginationArgs: withLastPA(6, &database.PaginationArgs{}), + wantPaginationArgs: withLastPA(6, buildPaginationArgs()), connectionArgs: withLastCA(5, &ConnectionResolverArgs{}), wantNodes: 2, }, { name: "after arg", - wantPaginationArgs: withAfterPA(0, withFirstPA(6, &database.PaginationArgs{})), + wantPaginationArgs: withAfterPA("0", withFirstPA(6, buildPaginationArgs())), connectionArgs: withAfterCA("0", withFirstCA(5, &ConnectionResolverArgs{})), wantNodes: 2, }, { name: "before arg", - wantPaginationArgs: withBeforePA(0, withLastPA(6, &database.PaginationArgs{})), + wantPaginationArgs: withBeforePA("0", withLastPA(6, buildPaginationArgs())), connectionArgs: withBeforeCA("0", withLastCA(5, &ConnectionResolverArgs{})), wantNodes: 2, }, { name: "with limit", - wantPaginationArgs: withBeforePA(0, withLastPA(2, &database.PaginationArgs{})), + wantPaginationArgs: withBeforePA("0", withLastPA(2, buildPaginationArgs())), connectionArgs: withBeforeCA("0", withLastCA(1, &ConnectionResolverArgs{})), wantNodes: 1, }, diff --git a/cmd/frontend/graphqlbackend/org.go b/cmd/frontend/graphqlbackend/org.go index 2191565bd342..6d74f1d4a9fa 100644 --- a/cmd/frontend/graphqlbackend/org.go +++ b/cmd/frontend/graphqlbackend/org.go @@ -195,7 +195,7 @@ func (s *membersConnectionStore) ComputeNodes(ctx context.Context, args *databas return userResolvers, nil } -func (s *membersConnectionStore) MarshalCursor(node *UserResolver) (*string, error) { +func (s *membersConnectionStore) MarshalCursor(node *UserResolver, _ database.OrderBy) (*string, error) { if node == nil { return nil, errors.New(`node is nil`) } @@ -205,13 +205,13 @@ func (s *membersConnectionStore) MarshalCursor(node *UserResolver) (*string, err return &cursor, nil } -func (s *membersConnectionStore) UnmarshalCursor(cusror string) (*int, error) { +func (s *membersConnectionStore) UnmarshalCursor(cusror string, _ database.OrderBy) (*string, error) { nodeID, err := UnmarshalUserID(graphql.ID(cusror)) if err != nil { return nil, err } - id := int(nodeID) + id := string(nodeID) return &id, nil } diff --git a/cmd/frontend/graphqlbackend/repositories.go b/cmd/frontend/graphqlbackend/repositories.go index dfa4e0a3cbc7..6cf00d4589c2 100644 --- a/cmd/frontend/graphqlbackend/repositories.go +++ b/cmd/frontend/graphqlbackend/repositories.go @@ -2,6 +2,9 @@ package graphqlbackend import ( "context" + "fmt" + "strconv" + "strings" "sync" "time" @@ -19,7 +22,6 @@ import ( ) type repositoryArgs struct { - graphqlutil.ConnectionArgs Query *string // Search query Names *[]string @@ -36,42 +38,17 @@ type repositoryArgs struct { OrderBy string Descending bool - After *string + graphqlutil.ConnectionResolverArgs } func (args *repositoryArgs) toReposListOptions() (database.ReposListOptions, error) { - opt := database.ReposListOptions{ - OrderBy: database.RepoListOrderBy{{ - Field: ToDBRepoListColumn(args.OrderBy), - Descending: args.Descending, - }}, - } + opt := database.ReposListOptions{} if args.Names != nil { opt.Names = *args.Names } if args.Query != nil { opt.Query = *args.Query } - if args.After != nil { - cursor, err := UnmarshalRepositoryCursor(args.After) - if err != nil { - return opt, err - } - opt.Cursors = append(opt.Cursors, cursor) - } else { - cursor := types.Cursor{ - Column: string(ToDBRepoListColumn(args.OrderBy)), - } - - if args.Descending { - cursor.Direction = "prev" - } else { - cursor.Direction = "next" - } - - opt.Cursors = append(opt.Cursors, &cursor) - } - args.Set(&opt.LimitOffset) if args.CloneStatus != nil { opt.CloneStatus = types.ParseCloneStatusFromGraphQL(*args.CloneStatus) @@ -114,22 +91,153 @@ func (args *repositoryArgs) toReposListOptions() (database.ReposListOptions, err return opt, nil } -func (r *schemaResolver) Repositories(args *repositoryArgs) (*repositoryConnectionResolver, error) { +func (r *schemaResolver) Repositories(ctx context.Context, args *repositoryArgs) (*graphqlutil.ConnectionResolver[RepositoryResolver], error) { opt, err := args.toReposListOptions() - if err != nil { return nil, err } - return &repositoryConnectionResolver{ + connectionStore := &repositoriesConnectionStore{ + ctx: ctx, db: r.db, logger: r.logger.Scoped("repositoryConnectionResolver", "resolves connections to a repository"), opt: opt, indexed: args.Indexed, notIndexed: args.NotIndexed, - }, nil + } + + maxPageSize := 1000 + + // `REPOSITORY_NAME` is the enum value in the graphql schema. + orderBy := "REPOSITORY_NAME" + if args.OrderBy != "" { + orderBy = args.OrderBy + } + + connectionOptions := graphqlutil.ConnectionResolverOptions{ + MaxPageSize: &maxPageSize, + OrderBy: database.OrderBy{{Field: string(ToDBRepoListColumn(orderBy))}, {Field: "id"}}, + Ascending: !args.Descending, + } + + return graphqlutil.NewConnectionResolver[RepositoryResolver](connectionStore, &args.ConnectionResolverArgs, &connectionOptions) } +type repositoriesConnectionStore struct { + ctx context.Context + logger log.Logger + db database.DB + opt database.ReposListOptions + indexed bool + notIndexed bool +} + +func (s *repositoriesConnectionStore) MarshalCursor(node *RepositoryResolver, orderBy database.OrderBy) (*string, error) { + column := orderBy[0].Field + var value string + + switch database.RepoListColumn(column) { + case database.RepoListName: + value = node.Name() + case database.RepoListCreatedAt: + value = fmt.Sprintf("'%v'", node.RawCreatedAt()) + case database.RepoListSize: + size, err := node.DiskSizeBytes(s.ctx) + if err != nil { + return nil, err + } + value = strconv.FormatInt(int64(*size), 10) + default: + return nil, errors.New(fmt.Sprintf("invalid OrderBy.Field. Expected: one of (name, created_at, gr.repo_size_bytes). Actual: %s", column)) + } + + cursor := MarshalRepositoryCursor( + &types.Cursor{ + Column: column, + Value: fmt.Sprintf("%s@%d", value, node.IDInt32()), + }, + ) + + return &cursor, nil +} + +func (s *repositoriesConnectionStore) UnmarshalCursor(cursor string, orderBy database.OrderBy) (*string, error) { + repoCursor, err := UnmarshalRepositoryCursor(&cursor) + if err != nil { + return nil, err + } + + if len(orderBy) == 0 { + return nil, errors.New("no orderBy provided") + } + + column := orderBy[0].Field + if repoCursor.Column != column { + return nil, errors.New(fmt.Sprintf("Invalid cursor. Expected: %s Actual: %s", column, repoCursor.Column)) + } + + csv := "" + values := strings.Split(repoCursor.Value, "@") + if len(values) != 2 { + return nil, errors.New(fmt.Sprintf("Invalid cursor. Expected Value: <%s>@ Actual Value: %s", column, repoCursor.Value)) + } + + switch database.RepoListColumn(column) { + case database.RepoListName: + csv = fmt.Sprintf("'%v', %v", values[0], values[1]) + case database.RepoListCreatedAt: + csv = fmt.Sprintf("%v, %v", values[0], values[1]) + case database.RepoListSize: + csv = fmt.Sprintf("%v, %v", values[0], values[1]) + default: + return nil, errors.New("Invalid OrderBy Field.") + } + + return &csv, err +} + +func i32ptr(v int32) *int32 { return &v } + +func (r *repositoriesConnectionStore) ComputeTotal(ctx context.Context) (countptr *int32, err error) { + // 🚨 SECURITY: Only site admins can list all repos, because a total repository + // count does not respect repository permissions. + if err := auth.CheckCurrentUserIsSiteAdmin(ctx, r.db); err != nil { + return i32ptr(int32(0)), nil + } + + // Counting repositories is slow on Sourcegraph.com. Don't wait very long for an exact count. + if envvar.SourcegraphDotComMode() { + return i32ptr(int32(0)), nil + } + + count, err := r.db.Repos().Count(ctx, r.opt) + return i32ptr(int32(count)), err +} + +func (r *repositoriesConnectionStore) ComputeNodes(ctx context.Context, args *database.PaginationArgs) ([]*RepositoryResolver, error) { + opt := r.opt + opt.PaginationArgs = args + + client := gitserver.NewClient(r.db) + repos, err := backend.NewRepos(r.logger, r.db, client).List(ctx, opt) + if err != nil { + return nil, err + } + + resolvers := make([]*RepositoryResolver, 0, len(repos)) + for _, repo := range repos { + resolvers = append(resolvers, NewRepositoryResolver(r.db, client, repo)) + } + + return resolvers, nil +} + +// NOTE(naman): The old resolver `RepositoryConnectionResolver` defined below is +// deprecated and replaced by `graphqlutil.ConnectionResolver` above which implements +// proper cursor-based pagination and do not support `precise` argument for totalCount. +// The old resolver is still being used by `AuthorizedUserRepositories` API, therefore +// the code is not removed yet. + type TotalCountArgs struct { Precise bool } @@ -256,7 +364,6 @@ func (r *repositoryConnectionResolver) TotalCount(ctx context.Context, args *Tot }() } - i32ptr := func(v int32) *int32 { return &v } count, err := r.db.Repos().Count(ctx, r.opt) return i32ptr(int32(count)), err } diff --git a/cmd/frontend/graphqlbackend/repositories_test.go b/cmd/frontend/graphqlbackend/repositories_test.go index 9b3a62094a11..2c63a1af3d2e 100644 --- a/cmd/frontend/graphqlbackend/repositories_test.go +++ b/cmd/frontend/graphqlbackend/repositories_test.go @@ -16,18 +16,39 @@ import ( "github.com/sourcegraph/sourcegraph/internal/actor" "github.com/sourcegraph/sourcegraph/internal/api" - "github.com/sourcegraph/sourcegraph/internal/auth" "github.com/sourcegraph/sourcegraph/internal/database" "github.com/sourcegraph/sourcegraph/internal/database/dbtest" "github.com/sourcegraph/sourcegraph/internal/types" "github.com/sourcegraph/sourcegraph/lib/errors" ) +func buildCursor(node *types.Repo) *string { + cursor := MarshalRepositoryCursor( + &types.Cursor{ + Column: "name", + Value: fmt.Sprintf("%s@%d", node.Name, node.ID), + }, + ) + + return &cursor +} + +func buildCursorBySize(node *types.Repo, size int64) *string { + cursor := MarshalRepositoryCursor( + &types.Cursor{ + Column: "gr.repo_size_bytes", + Value: fmt.Sprintf("%d@%d", size, node.ID), + }, + ) + + return &cursor +} + func TestRepositoriesCloneStatusFiltering(t *testing.T) { mockRepos := []*types.Repo{ - {Name: "repo1"}, // not_cloned - {Name: "repo2"}, // cloning - {Name: "repo3"}, // cloned + {ID: 1, Name: "repo1"}, // not_cloned + {ID: 2, Name: "repo2"}, // cloning + {ID: 3, Name: "repo3"}, // cloned } repos := database.NewMockRepoStore() @@ -69,7 +90,7 @@ func TestRepositoriesCloneStatusFiltering(t *testing.T) { Schema: schema, Query: ` { - repositories { + repositories(first: 3) { nodes { name } totalCount pageInfo { hasNextPage } @@ -84,18 +105,11 @@ func TestRepositoriesCloneStatusFiltering(t *testing.T) { { "name": "repo2" }, { "name": "repo3" } ], - "totalCount": null, + "totalCount": 0, "pageInfo": {"hasNextPage": false} } } `, - ExpectedErrors: []*gqlerrors.QueryError{ - { - Path: []any{"repositories", "totalCount"}, - Message: auth.ErrMustBeSiteAdmin.Error(), - ResolverError: auth.ErrMustBeSiteAdmin, - }, - }, }, }) }) @@ -108,7 +122,7 @@ func TestRepositoriesCloneStatusFiltering(t *testing.T) { Schema: schema, Query: ` { - repositories { + repositories(first: 3) { nodes { name } totalCount pageInfo { hasNextPage } @@ -136,7 +150,7 @@ func TestRepositoriesCloneStatusFiltering(t *testing.T) { // when setting them explicitly Query: ` { - repositories(cloned: true, notCloned: true) { + repositories(first: 3, cloned: true, notCloned: true) { nodes { name } totalCount pageInfo { hasNextPage } @@ -183,7 +197,7 @@ func TestRepositoriesCloneStatusFiltering(t *testing.T) { Schema: schema, Query: ` { - repositories(cloned: false) { + repositories(first: 3, cloned: false) { nodes { name } pageInfo { hasNextPage } } @@ -205,7 +219,7 @@ func TestRepositoriesCloneStatusFiltering(t *testing.T) { Schema: schema, Query: ` { - repositories(notCloned: false) { + repositories(first: 3, notCloned: false) { nodes { name } pageInfo { hasNextPage } } @@ -226,7 +240,7 @@ func TestRepositoriesCloneStatusFiltering(t *testing.T) { Schema: schema, Query: ` { - repositories(notCloned: false, cloned: false) { + repositories(first: 3, notCloned: false, cloned: false) { nodes { name } pageInfo { hasNextPage } } @@ -245,7 +259,7 @@ func TestRepositoriesCloneStatusFiltering(t *testing.T) { Schema: schema, Query: ` { - repositories(cloneStatus: CLONED) { + repositories(first: 3, cloneStatus: CLONED) { nodes { name } pageInfo { hasNextPage } } @@ -266,7 +280,7 @@ func TestRepositoriesCloneStatusFiltering(t *testing.T) { Schema: schema, Query: ` { - repositories(cloneStatus: CLONING) { + repositories(first: 3, cloneStatus: CLONING) { nodes { name } pageInfo { hasNextPage } } @@ -287,7 +301,7 @@ func TestRepositoriesCloneStatusFiltering(t *testing.T) { Schema: schema, Query: ` { - repositories(cloneStatus: NOT_CLONED) { + repositories(first: 3, cloneStatus: NOT_CLONED) { nodes { name } pageInfo { hasNextPage } } @@ -355,7 +369,7 @@ func TestRepositoriesIndexingFiltering(t *testing.T) { Schema: schema, Query: ` { - repositories { + repositories(first: 5) { nodes { name } totalCount pageInfo { hasNextPage } @@ -384,7 +398,7 @@ func TestRepositoriesIndexingFiltering(t *testing.T) { // when setting them explicitly Query: ` { - repositories(indexed: true, notIndexed: true) { + repositories(first: 5, indexed: true, notIndexed: true) { nodes { name } totalCount pageInfo { hasNextPage } @@ -410,7 +424,7 @@ func TestRepositoriesIndexingFiltering(t *testing.T) { Schema: schema, Query: ` { - repositories(indexed: false) { + repositories(first: 5, indexed: false) { nodes { name } totalCount pageInfo { hasNextPage } @@ -434,7 +448,7 @@ func TestRepositoriesIndexingFiltering(t *testing.T) { Schema: schema, Query: ` { - repositories(notIndexed: false) { + repositories(first: 5, notIndexed: false) { nodes { name } totalCount pageInfo { hasNextPage } @@ -458,7 +472,7 @@ func TestRepositoriesIndexingFiltering(t *testing.T) { Schema: schema, Query: ` { - repositories(notIndexed: false, indexed: false) { + repositories(first: 5, notIndexed: false, indexed: false) { nodes { name } totalCount pageInfo { hasNextPage } @@ -506,18 +520,18 @@ func TestRepositories_CursorPagination(t *testing.T) { RunTest(t, &Test{ Schema: mustParseGraphQLSchema(t, db), Query: buildQuery(1, ""), - ExpectedResult: ` + ExpectedResult: fmt.Sprintf(` { "repositories": { "nodes": [{ "name": "repo1" }], "pageInfo": { - "endCursor": "UmVwb3NpdG9yeUN1cnNvcjp7IkNvbHVtbiI6Im5hbWUiLCJWYWx1ZSI6InJlcG8yIiwiRGlyZWN0aW9uIjoibmV4dCJ9" + "endCursor": "%s" } } } - `, + `, *buildCursor(mockRepos[0])), }) }) @@ -526,19 +540,19 @@ func TestRepositories_CursorPagination(t *testing.T) { RunTest(t, &Test{ Schema: mustParseGraphQLSchema(t, db), - Query: buildQuery(1, "UmVwb3NpdG9yeUN1cnNvcjp7IkNvbHVtbiI6Im5hbWUiLCJWYWx1ZSI6InJlcG8yIiwiRGlyZWN0aW9uIjoibmV4dCJ9"), - ExpectedResult: ` + Query: buildQuery(1, *buildCursor(mockRepos[0])), + ExpectedResult: fmt.Sprintf(` { "repositories": { "nodes": [{ "name": "repo2" }], "pageInfo": { - "endCursor": "UmVwb3NpdG9yeUN1cnNvcjp7IkNvbHVtbiI6Im5hbWUiLCJWYWx1ZSI6InJlcG8zIiwiRGlyZWN0aW9uIjoibmV4dCJ9" + "endCursor": "%s" } } } - `, + `, *buildCursor(mockRepos[1])), }) }) @@ -547,19 +561,19 @@ func TestRepositories_CursorPagination(t *testing.T) { RunTest(t, &Test{ Schema: mustParseGraphQLSchema(t, db), - Query: buildQuery(1, "UmVwb3NpdG9yeUN1cnNvcjp7IkNvbHVtbiI6Im5hbWUiLCJWYWx1ZSI6InJlcG8yIiwiRGlyZWN0aW9uIjoicHJldiJ9"), - ExpectedResult: ` + Query: buildQuery(1, *buildCursor(mockRepos[0])), + ExpectedResult: fmt.Sprintf(` { "repositories": { "nodes": [{ "name": "repo2" }], "pageInfo": { - "endCursor": "UmVwb3NpdG9yeUN1cnNvcjp7IkNvbHVtbiI6Im5hbWUiLCJWYWx1ZSI6InJlcG8zIiwiRGlyZWN0aW9uIjoicHJldiJ9" + "endCursor": "%s" } } } - `, + `, *buildCursor(mockRepos[1])), }) }) @@ -569,7 +583,7 @@ func TestRepositories_CursorPagination(t *testing.T) { RunTest(t, &Test{ Schema: mustParseGraphQLSchema(t, db), Query: buildQuery(3, ""), - ExpectedResult: ` + ExpectedResult: fmt.Sprintf(` { "repositories": { "nodes": [{ @@ -580,11 +594,11 @@ func TestRepositories_CursorPagination(t *testing.T) { "name": "repo3" }], "pageInfo": { - "endCursor": null + "endCursor": "%s" } } } - `, + `, *buildCursor(mockRepos[2])), }) }) @@ -616,7 +630,12 @@ func TestRepositories_CursorPagination(t *testing.T) { ExpectedResult: "null", ExpectedErrors: []*gqlerrors.QueryError{ { - Path: []any{"repositories"}, + Path: []any{"repositories", "nodes"}, + Message: `cannot unmarshal repository cursor type: ""`, + ResolverError: errors.New(`cannot unmarshal repository cursor type: ""`), + }, + { + Path: []any{"repositories", "pageInfo"}, Message: `cannot unmarshal repository cursor type: ""`, ResolverError: errors.New(`cannot unmarshal repository cursor type: ""`), }, @@ -689,114 +708,295 @@ func TestRepositories_Integration(t *testing.T) { ctx = actor.WithActor(ctx, actor.FromUser(admin.ID)) tests := []repositoriesQueryTest{ - // no args + // first { - wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, - wantTotalCount: 8, + args: "first: 2", + wantRepos: []string{"repo1", "repo2"}, + wantTotalCount: 8, + wantNextPage: true, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[0].repo), + wantEndCursor: buildCursor(repos[1].repo), }, - // first + // second page with first, after args + { + args: fmt.Sprintf(`first: 2, after: "%s"`, *buildCursor(repos[0].repo)), + wantRepos: []string{"repo2", "repo3"}, + wantTotalCount: 8, + wantNextPage: true, + wantPreviousPage: true, + wantStartCursor: buildCursor(repos[1].repo), + wantEndCursor: buildCursor(repos[2].repo), + }, + // last page with first, after args + { + args: fmt.Sprintf(`first: 2, after: "%s"`, *buildCursor(repos[5].repo)), + wantRepos: []string{"repo7", "repo8"}, + wantTotalCount: 8, + wantNextPage: false, + wantPreviousPage: true, + wantStartCursor: buildCursor(repos[6].repo), + wantEndCursor: buildCursor(repos[7].repo), + }, + // last + { + args: "last: 2", + wantRepos: []string{"repo7", "repo8"}, + wantTotalCount: 8, + wantNextPage: false, + wantPreviousPage: true, + wantStartCursor: buildCursor(repos[6].repo), + wantEndCursor: buildCursor(repos[7].repo), + }, + // second last page with last, before args + { + args: fmt.Sprintf(`last: 2, before: "%s"`, *buildCursor(repos[6].repo)), + wantRepos: []string{"repo5", "repo6"}, + wantTotalCount: 8, + wantNextPage: true, + wantPreviousPage: true, + wantStartCursor: buildCursor(repos[4].repo), + wantEndCursor: buildCursor(repos[5].repo), + }, + // back to first page with last, before args + { + args: fmt.Sprintf(`last: 2, before: "%s"`, *buildCursor(repos[2].repo)), + wantRepos: []string{"repo1", "repo2"}, + wantTotalCount: 8, + wantNextPage: true, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[0].repo), + wantEndCursor: buildCursor(repos[1].repo), + }, + // descending first + { + args: "first: 2, descending: true", + wantRepos: []string{"repo8", "repo7"}, + wantTotalCount: 8, + wantNextPage: true, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[7].repo), + wantEndCursor: buildCursor(repos[6].repo), + }, + // descending second page with first, after args + { + args: fmt.Sprintf(`first: 2, descending: true, after: "%s"`, *buildCursor(repos[6].repo)), + wantRepos: []string{"repo6", "repo5"}, + wantTotalCount: 8, + wantNextPage: true, + wantPreviousPage: true, + wantStartCursor: buildCursor(repos[5].repo), + wantEndCursor: buildCursor(repos[4].repo), + }, + // descending last page with first, after args + { + args: fmt.Sprintf(`first: 2, descending: true, after: "%s"`, *buildCursor(repos[2].repo)), + wantRepos: []string{"repo2", "repo1"}, + wantTotalCount: 8, + wantNextPage: false, + wantPreviousPage: true, + wantStartCursor: buildCursor(repos[1].repo), + wantEndCursor: buildCursor(repos[0].repo), + }, + // descending last { - args: "first: 2", - wantRepos: []string{"repo1", "repo2"}, - wantTotalCount: 8, + args: "last: 2, descending: true", + wantRepos: []string{"repo2", "repo1"}, + wantTotalCount: 8, + wantNextPage: false, + wantPreviousPage: true, + wantStartCursor: buildCursor(repos[1].repo), + wantEndCursor: buildCursor(repos[0].repo), + }, + // descending second last page with last, before args + { + args: fmt.Sprintf(`last: 2, descending: true, before: "%s"`, *buildCursor(repos[3].repo)), + wantRepos: []string{"repo6", "repo5"}, + wantTotalCount: 8, + wantNextPage: true, + wantPreviousPage: true, + wantStartCursor: buildCursor(repos[5].repo), + wantEndCursor: buildCursor(repos[4].repo), + }, + // descending back to first page with last, before args + { + args: fmt.Sprintf(`last: 2, descending: true, before: "%s"`, *buildCursor(repos[5].repo)), + wantRepos: []string{"repo8", "repo7"}, + wantTotalCount: 8, + wantNextPage: true, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[7].repo), + wantEndCursor: buildCursor(repos[6].repo), }, // cloned { // cloned only says whether to "Include cloned repositories.", it doesn't exclude non-cloned. - args: "cloned: true", - wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, - wantTotalCount: 8, + args: "first: 10, cloned: true", + wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, + wantTotalCount: 8, + wantNextPage: false, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[0].repo), + wantEndCursor: buildCursor(repos[7].repo), }, { - args: "cloned: false", - wantRepos: []string{"repo1", "repo2", "repo3", "repo4"}, - wantTotalCount: 4, + args: "first: 10, cloned: false", + wantRepos: []string{"repo1", "repo2", "repo3", "repo4"}, + wantTotalCount: 4, + wantNextPage: false, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[0].repo), + wantEndCursor: buildCursor(repos[3].repo), }, { - args: "cloned: false, first: 2", - wantRepos: []string{"repo1", "repo2"}, - wantTotalCount: 4, + args: "cloned: false, first: 2", + wantRepos: []string{"repo1", "repo2"}, + wantTotalCount: 4, + wantNextPage: true, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[0].repo), + wantEndCursor: buildCursor(repos[1].repo), }, // notCloned { - args: "notCloned: true", - wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, - wantTotalCount: 8, + args: "first: 10, notCloned: true", + wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, + wantTotalCount: 8, + wantNextPage: false, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[0].repo), + wantEndCursor: buildCursor(repos[7].repo), }, { - args: "notCloned: false", - wantRepos: []string{"repo5", "repo6", "repo7", "repo8"}, - wantTotalCount: 4, + args: "first: 10, notCloned: false", + wantRepos: []string{"repo5", "repo6", "repo7", "repo8"}, + wantTotalCount: 4, + wantNextPage: false, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[4].repo), + wantEndCursor: buildCursor(repos[7].repo), }, // failedFetch { - args: "failedFetch: true", - wantRepos: []string{"repo2", "repo4", "repo6"}, - wantTotalCount: 3, + args: "first: 10, failedFetch: true", + wantRepos: []string{"repo2", "repo4", "repo6"}, + wantTotalCount: 3, + wantNextPage: false, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[1].repo), + wantEndCursor: buildCursor(repos[5].repo), }, { - args: "failedFetch: true, first: 2", - wantRepos: []string{"repo2", "repo4"}, - wantTotalCount: 3, + args: "failedFetch: true, first: 2", + wantRepos: []string{"repo2", "repo4"}, + wantTotalCount: 3, + wantNextPage: true, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[1].repo), + wantEndCursor: buildCursor(repos[3].repo), }, { - args: "failedFetch: false", - wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, - wantTotalCount: 8, + args: "first: 10, failedFetch: false", + wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, + wantTotalCount: 8, + wantNextPage: false, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[0].repo), + wantEndCursor: buildCursor(repos[7].repo), }, // cloneStatus { - args: "cloneStatus:NOT_CLONED", - wantRepos: []string{"repo1", "repo2"}, - wantTotalCount: 2, + args: "first: 10, cloneStatus:NOT_CLONED", + wantRepos: []string{"repo1", "repo2"}, + wantTotalCount: 2, + wantNextPage: false, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[0].repo), + wantEndCursor: buildCursor(repos[1].repo), }, { - args: "cloneStatus:CLONING", - wantRepos: []string{"repo3", "repo4"}, - wantTotalCount: 2, + args: "first: 10, cloneStatus:CLONING", + wantRepos: []string{"repo3", "repo4"}, + wantTotalCount: 2, + wantNextPage: false, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[2].repo), + wantEndCursor: buildCursor(repos[3].repo), }, { - args: "cloneStatus:CLONED", - wantRepos: []string{"repo5", "repo6", "repo7", "repo8"}, - wantTotalCount: 4, + args: "first: 10, cloneStatus:CLONED", + wantRepos: []string{"repo5", "repo6", "repo7", "repo8"}, + wantTotalCount: 4, + wantNextPage: false, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[4].repo), + wantEndCursor: buildCursor(repos[7].repo), }, { - args: "cloneStatus:NOT_CLONED, first: 1", - wantRepos: []string{"repo1"}, - wantTotalCount: 2, + args: "cloneStatus:NOT_CLONED, first: 1", + wantRepos: []string{"repo1"}, + wantTotalCount: 2, + wantNextPage: true, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[0].repo), + wantEndCursor: buildCursor(repos[0].repo), }, // indexed { // indexed only says whether to "Include indexed repositories.", it doesn't exclude non-indexed. - args: "indexed: true", - wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, - wantTotalCount: 8, + args: "first: 10, indexed: true", + wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, + wantTotalCount: 8, + wantNextPage: false, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[0].repo), + wantEndCursor: buildCursor(repos[7].repo), }, { - args: "indexed: false", - wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7"}, - wantTotalCount: 7, + args: "first: 10, indexed: false", + wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7"}, + wantTotalCount: 7, + wantNextPage: false, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[0].repo), + wantEndCursor: buildCursor(repos[6].repo), }, { - args: "indexed: false, first: 2", - wantRepos: []string{"repo1", "repo2"}, - wantTotalCount: 7, + args: "indexed: false, first: 2", + wantRepos: []string{"repo1", "repo2"}, + wantTotalCount: 7, + wantNextPage: true, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[0].repo), + wantEndCursor: buildCursor(repos[1].repo), }, // notIndexed { - args: "notIndexed: true", - wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, - wantTotalCount: 8, + args: "first: 10, notIndexed: true", + wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, + wantTotalCount: 8, + wantNextPage: false, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[0].repo), + wantEndCursor: buildCursor(repos[7].repo), }, { - args: "notIndexed: false", - wantRepos: []string{"repo8"}, - wantTotalCount: 1, + args: "first: 10, notIndexed: false", + wantRepos: []string{"repo8"}, + wantTotalCount: 1, + wantNextPage: false, + wantPreviousPage: false, + wantStartCursor: buildCursor(repos[7].repo), + wantEndCursor: buildCursor(repos[7].repo), }, { - args: "orderBy:SIZE, descending:false, first: 5", - wantRepos: []string{"repo6", "repo1", "repo2", "repo3", "repo4"}, - wantTotalCount: 8, + args: "orderBy:SIZE, descending:false, first: 5", + wantRepos: []string{"repo6", "repo1", "repo2", "repo3", "repo4"}, + wantTotalCount: 8, + wantNextPage: true, + wantPreviousPage: false, + wantStartCursor: buildCursorBySize(repos[5].repo, repos[5].size), + wantEndCursor: buildCursorBySize(repos[3].repo, repos[3].size), }, } @@ -809,12 +1009,13 @@ func TestRepositories_Integration(t *testing.T) { } type repositoriesQueryTest struct { - args string - - wantRepos []string - - wantNoTotalCount bool + args string + wantRepos []string wantTotalCount int + wantEndCursor *string + wantStartCursor *string + wantNextPage bool + wantPreviousPage bool } func runRepositoriesQuery(t *testing.T, ctx context.Context, schema *graphql.Schema, want repositoriesQueryTest) { @@ -824,9 +1025,17 @@ func runRepositoriesQuery(t *testing.T, ctx context.Context, schema *graphql.Sch Name string `json:"name"` } + type pageInfo struct { + HasNextPage bool `json:"hasNextPage"` + HasPreviousPage bool `json:"hasPreviousPage"` + StartCursor *string `json:"startCursor"` + EndCursor *string `json:"endCursor"` + } + type repositories struct { - Nodes []node `json:"nodes"` - TotalCount *int `json:"totalCount"` + Nodes []node `json:"nodes"` + TotalCount *int `json:"totalCount"` + PageInfo pageInfo `json:"pageInfo"` } type expected struct { @@ -842,24 +1051,35 @@ func runRepositoriesQuery(t *testing.T, ctx context.Context, schema *graphql.Sch Repositories: repositories{ Nodes: nodes, TotalCount: &want.wantTotalCount, + PageInfo: pageInfo{ + HasNextPage: want.wantNextPage, + HasPreviousPage: want.wantPreviousPage, + StartCursor: want.wantStartCursor, + EndCursor: want.wantEndCursor, + }, }, } - if want.wantNoTotalCount { - ex.Repositories.TotalCount = nil - } - marshaled, err := json.Marshal(ex) if err != nil { t.Fatalf("failed to marshal expected repositories query result: %s", err) } - var query string - if want.args != "" { - query = fmt.Sprintf(`{ repositories(%s) { nodes { name } totalCount } } `, want.args) - } else { - query = `{ repositories { nodes { name } totalCount } }` - } + query := fmt.Sprintf(` + { + repositories(%s) { + nodes { + name + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + }`, want.args) RunTest(t, &Test{ Context: ctx, diff --git a/cmd/frontend/graphqlbackend/repository.go b/cmd/frontend/graphqlbackend/repository.go index c517f96cf426..24887816bbbe 100644 --- a/cmd/frontend/graphqlbackend/repository.go +++ b/cmd/frontend/graphqlbackend/repository.go @@ -292,6 +292,14 @@ func (r *RepositoryResolver) CreatedAt() gqlutil.DateTime { return gqlutil.DateTime{Time: time.Now()} } +func (r *RepositoryResolver) RawCreatedAt() string { + if r.innerRepo == nil { + return "" + } + + return r.innerRepo.CreatedAt.Format(time.RFC3339) +} + func (r *RepositoryResolver) UpdatedAt() *gqlutil.DateTime { return nil } diff --git a/cmd/frontend/graphqlbackend/repository_contributors.go b/cmd/frontend/graphqlbackend/repository_contributors.go index 357840102a33..9d22935ae267 100644 --- a/cmd/frontend/graphqlbackend/repository_contributors.go +++ b/cmd/frontend/graphqlbackend/repository_contributors.go @@ -23,20 +23,17 @@ func (r *RepositoryResolver) Contributors(args *struct { repositoryContributorsArgs graphqlutil.ConnectionResolverArgs }) (*graphqlutil.ConnectionResolver[repositoryContributorResolver], error) { - connectionArgs := &graphqlutil.ConnectionResolverArgs{ - First: args.First, - Last: args.Last, - After: args.After, - Before: args.Before, - } connectionStore := &repositoryContributorConnectionStore{ db: r.db, args: &args.repositoryContributorsArgs, - connectionArgs: connectionArgs, + connectionArgs: &args.ConnectionResolverArgs, repo: r, } reverse := false - return graphqlutil.NewConnectionResolver[repositoryContributorResolver](connectionStore, connectionArgs, &graphqlutil.ConnectionResolverOptions{Reverse: &reverse}) + connectionOptions := graphqlutil.ConnectionResolverOptions{ + Reverse: &reverse, + } + return graphqlutil.NewConnectionResolver[repositoryContributorResolver](connectionStore, &args.ConnectionResolverArgs, &connectionOptions) } type repositoryContributorConnectionStore struct { @@ -52,17 +49,13 @@ type repositoryContributorConnectionStore struct { err error } -func (s *repositoryContributorConnectionStore) MarshalCursor(node *repositoryContributorResolver) (*string, error) { +func (s *repositoryContributorConnectionStore) MarshalCursor(node *repositoryContributorResolver, _ database.OrderBy) (*string, error) { position := strconv.Itoa(node.index) return &position, nil } -func (s *repositoryContributorConnectionStore) UnmarshalCursor(cursor string) (*int, error) { - position, err := strconv.Atoi(cursor) - if err != nil { - return nil, err - } - return &position, nil +func (s *repositoryContributorConnectionStore) UnmarshalCursor(cursor string, _ database.OrderBy) (*string, error) { + return &cursor, nil } func (s *repositoryContributorConnectionStore) ComputeTotal(ctx context.Context) (*int32, error) { @@ -123,13 +116,21 @@ func OffsetBasedCursorSlice[T any](nodes []T, args *database.PaginationArgs) ([] totalFloat := float64(len(nodes)) if args.First != nil { if args.After != nil { - start = int(math.Min(float64(*args.After)+1, totalFloat)) + after, err := strconv.Atoi(*args.After) + if err != nil { + return nil, 0, err + } + start = int(math.Min(float64(after)+1, totalFloat)) } end = int(math.Min(float64(start+*args.First), totalFloat)) } else if args.Last != nil { end = int(totalFloat) if args.Before != nil { - end = int(math.Max(float64(*args.Before), 0)) + before, err := strconv.Atoi(*args.Before) + if err != nil { + return nil, 0, err + } + end = int(math.Max(float64(before), 0)) } start = int(math.Max(float64(end-*args.Last), 0)) } else { diff --git a/cmd/frontend/graphqlbackend/repository_contributors_test.go b/cmd/frontend/graphqlbackend/repository_contributors_test.go index 4d273fb91624..2bd66b9049c6 100644 --- a/cmd/frontend/graphqlbackend/repository_contributors_test.go +++ b/cmd/frontend/graphqlbackend/repository_contributors_test.go @@ -11,9 +11,9 @@ import ( func TestOffsetBasedCursorSlice(t *testing.T) { slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} - int1 := 1 int2 := 2 - int8 := 8 + string1 := "1" + string8 := "8" testCases := []struct { name string @@ -27,7 +27,7 @@ func TestOffsetBasedCursorSlice(t *testing.T) { }, { "next page", - &database.PaginationArgs{First: &int2, After: &int1}, + &database.PaginationArgs{First: &int2, After: &string1}, autogold.Want("first two items", []int{3, 4}), }, { @@ -37,7 +37,7 @@ func TestOffsetBasedCursorSlice(t *testing.T) { }, { "previous page", - &database.PaginationArgs{Last: &int2, Before: &int8}, + &database.PaginationArgs{Last: &int2, Before: &string8}, autogold.Want("first two items", []int{7, 8}), }, } diff --git a/cmd/frontend/graphqlbackend/saved_searches.go b/cmd/frontend/graphqlbackend/saved_searches.go index a7f386ddd613..9ab197ee0143 100644 --- a/cmd/frontend/graphqlbackend/saved_searches.go +++ b/cmd/frontend/graphqlbackend/saved_searches.go @@ -2,6 +2,7 @@ package graphqlbackend import ( "context" + "strconv" "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/relay" @@ -150,19 +151,19 @@ type savedSearchesConnectionStore struct { orgID *int32 } -func (s *savedSearchesConnectionStore) MarshalCursor(node *savedSearchResolver) (*string, error) { +func (s *savedSearchesConnectionStore) MarshalCursor(node *savedSearchResolver, _ database.OrderBy) (*string, error) { cursor := string(node.ID()) return &cursor, nil } -func (s *savedSearchesConnectionStore) UnmarshalCursor(cursor string) (*int, error) { +func (s *savedSearchesConnectionStore) UnmarshalCursor(cursor string, _ database.OrderBy) (*string, error) { nodeID, err := unmarshalSavedSearchID(graphql.ID(cursor)) if err != nil { return nil, err } - id := int(nodeID) + id := strconv.Itoa(int(nodeID)) return &id, nil } diff --git a/cmd/frontend/graphqlbackend/schema.graphql b/cmd/frontend/graphqlbackend/schema.graphql index 50dd0296361f..c9c0f269beb6 100755 --- a/cmd/frontend/graphqlbackend/schema.graphql +++ b/cmd/frontend/graphqlbackend/schema.graphql @@ -1287,6 +1287,10 @@ type Query { """ first: Int """ + Returns the last n repositories from the list. + """ + last: Int + """ Return repositories whose names match the query. """ query: String @@ -1295,6 +1299,10 @@ type Query { """ after: String """ + An opaque cursor that is used for pagination. + """ + before: String + """ Return repositories whose names are in the list. """ names: [String!] @@ -1338,7 +1346,7 @@ type Query { Sort direction. """ descending: Boolean = false - ): RepositoryConnection! + ): NewRepositoryConnection! """ Looks up a Phabricator repository by name. @@ -3077,9 +3085,30 @@ type ExternalServiceSyncJob implements Node { """ reposUnmodified: Int! } - """ A list of repositories. + +The old `RepositoryConnection` is deprecated and is replaced by +this new connection which support proper cursor based pagination. +The new connection does not include `precise` argument for totalCount. +""" +type NewRepositoryConnection { + """ + A list of repositories. + """ + nodes: [Repository!]! + """ + The total count of repositories in the connection. + """ + totalCount: Int! + """ + Pagination information. + """ + pageInfo: ConnectionPageInfo! +} + +""" +Deprecated! A list of repositories. """ type RepositoryConnection { """ diff --git a/cmd/frontend/graphqlbackend/site_config_change_connection.go b/cmd/frontend/graphqlbackend/site_config_change_connection.go index add83f100a70..ebdc91444e37 100644 --- a/cmd/frontend/graphqlbackend/site_config_change_connection.go +++ b/cmd/frontend/graphqlbackend/site_config_change_connection.go @@ -2,6 +2,7 @@ package graphqlbackend import ( "context" + "strconv" "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/relay" @@ -28,7 +29,10 @@ func (s *SiteConfigurationChangeConnectionStore) ComputeNodes(ctx context.Contex // determine next/previous page. Instead, dereference the values from args first (if // they're non-nil) and then assign them address of the new variables. paginationArgs := args.Clone() - isModifiedPaginationArgs := modifyArgs(paginationArgs) + isModifiedPaginationArgs, err := modifyArgs(paginationArgs) + if err != nil { + return []*SiteConfigurationChangeResolver{}, err + } history, err := s.db.Conf().ListSiteConfigs(ctx, paginationArgs) if err != nil { @@ -58,35 +62,45 @@ func (s *SiteConfigurationChangeConnectionStore) ComputeNodes(ctx context.Contex return resolvers, nil } -func (s *SiteConfigurationChangeConnectionStore) MarshalCursor(node *SiteConfigurationChangeResolver) (*string, error) { +func (s *SiteConfigurationChangeConnectionStore) MarshalCursor(node *SiteConfigurationChangeResolver, _ database.OrderBy) (*string, error) { cursor := string(node.ID()) return &cursor, nil } -func (s *SiteConfigurationChangeConnectionStore) UnmarshalCursor(cursor string) (*int, error) { +func (s *SiteConfigurationChangeConnectionStore) UnmarshalCursor(cursor string, _ database.OrderBy) (*string, error) { var id int err := relay.UnmarshalSpec(graphql.ID(cursor), &id) - return &id, err + if err != nil { + return nil, err + } + + idStr := strconv.Itoa(id) + return &idStr, err } // modifyArgs will fetch one more than the originally requested number of items because we need one // older item to get the diff of the oldes item in the list. // // A separate function so that this can be tested in isolation. -func modifyArgs(args *database.PaginationArgs) bool { +func modifyArgs(args *database.PaginationArgs) (bool, error) { var modified bool if args.First != nil { *args.First += 1 modified = true } else if args.Last != nil && args.Before != nil { - if *args.Before > 0 { + before, err := strconv.Atoi(*args.Before) + if err != nil { + return false, err + } + + if before > 0 { modified = true *args.Last += 1 - *args.Before -= 1 + *args.Before = strconv.Itoa(before - 1) } } - return modified + return modified, nil } func generateResolversForFirst(history []*database.SiteConfig, db database.DB) []*SiteConfigurationChangeResolver { diff --git a/cmd/frontend/graphqlbackend/site_config_change_connection_test.go b/cmd/frontend/graphqlbackend/site_config_change_connection_test.go index 19778da3d47e..52e87c7c65d4 100644 --- a/cmd/frontend/graphqlbackend/site_config_change_connection_test.go +++ b/cmd/frontend/graphqlbackend/site_config_change_connection_test.go @@ -3,6 +3,7 @@ package graphqlbackend import ( "context" "fmt" + "strconv" "testing" "github.com/google/go-cmp/cmp" @@ -19,6 +20,12 @@ type siteConfigStubs struct { siteConfigs []*database.SiteConfig } +func toStringPtr(n int) *string { + str := strconv.Itoa(n) + + return &str +} + func setupSiteConfigStubs(t *testing.T) *siteConfigStubs { logger := log.NoOp() db := database.NewDB(logger, dbtest.NewDB(logger, t)) @@ -176,7 +183,7 @@ func TestSiteConfigConnection(t *testing.T) { }, { Schema: mustParseGraphQLSchema(t, stubs.db), - Label: "Get last 2 site configuration history", + Label: "Get last 3 site configuration history", Context: context, Query: ` { @@ -449,7 +456,7 @@ func TestSiteConfigurationChangeConnectionStoreComputeNodes(t *testing.T) { name: "first: 2, after: 4", paginationArgs: &database.PaginationArgs{ First: intPtr(2), - After: intPtr(4), + After: toStringPtr(4), }, expectedSiteConfigIDs: []int32{3, 2}, expectedPreviousSiteConfigIDs: []int32{2, 1}, @@ -458,7 +465,7 @@ func TestSiteConfigurationChangeConnectionStoreComputeNodes(t *testing.T) { name: "first: 10, after: 4", paginationArgs: &database.PaginationArgs{ First: intPtr(10), - After: intPtr(4), + After: toStringPtr(4), }, expectedSiteConfigIDs: []int32{3, 2, 1}, expectedPreviousSiteConfigIDs: []int32{2, 1, 0}, @@ -467,7 +474,7 @@ func TestSiteConfigurationChangeConnectionStoreComputeNodes(t *testing.T) { name: "first: 2, after: 1", paginationArgs: &database.PaginationArgs{ First: intPtr(2), - After: intPtr(1), + After: toStringPtr(1), }, expectedSiteConfigIDs: []int32{}, expectedPreviousSiteConfigIDs: []int32{}, @@ -476,7 +483,7 @@ func TestSiteConfigurationChangeConnectionStoreComputeNodes(t *testing.T) { name: "last: 2, before: 2", paginationArgs: &database.PaginationArgs{ Last: intPtr(2), - Before: intPtr(2), + Before: toStringPtr(2), }, expectedSiteConfigIDs: []int32{3, 4}, expectedPreviousSiteConfigIDs: []int32{2, 3}, @@ -485,7 +492,7 @@ func TestSiteConfigurationChangeConnectionStoreComputeNodes(t *testing.T) { name: "last: 10, before: 2", paginationArgs: &database.PaginationArgs{ Last: intPtr(10), - Before: intPtr(2), + Before: toStringPtr(2), }, expectedSiteConfigIDs: []int32{3, 4, 5}, expectedPreviousSiteConfigIDs: []int32{2, 3, 4}, @@ -494,7 +501,7 @@ func TestSiteConfigurationChangeConnectionStoreComputeNodes(t *testing.T) { name: "last: 2, before: 5", paginationArgs: &database.PaginationArgs{ Last: intPtr(2), - Before: intPtr(5), + Before: toStringPtr(5), }, expectedSiteConfigIDs: []int32{}, expectedPreviousSiteConfigIDs: []int32{}, @@ -558,8 +565,8 @@ func TestModifyArgs(t *testing.T) { }, { name: "first: 5, after: 10 (next page)", - args: &database.PaginationArgs{First: intPtr(5), After: intPtr(10)}, - expectedArgs: &database.PaginationArgs{First: intPtr(6), After: intPtr(10)}, + args: &database.PaginationArgs{First: intPtr(5), After: toStringPtr(10)}, + expectedArgs: &database.PaginationArgs{First: intPtr(6), After: toStringPtr(10)}, expectedModified: true, }, { @@ -570,27 +577,31 @@ func TestModifyArgs(t *testing.T) { }, { name: "last: 5, before: 10 (previous page)", - args: &database.PaginationArgs{Last: intPtr(5), Before: intPtr(10)}, - expectedArgs: &database.PaginationArgs{Last: intPtr(6), Before: intPtr(9)}, + args: &database.PaginationArgs{Last: intPtr(5), Before: toStringPtr(10)}, + expectedArgs: &database.PaginationArgs{Last: intPtr(6), Before: toStringPtr(9)}, expectedModified: true, }, { name: "last: 5, before: 1 (edge case)", - args: &database.PaginationArgs{Last: intPtr(5), Before: intPtr(1)}, - expectedArgs: &database.PaginationArgs{Last: intPtr(6), Before: intPtr(0)}, + args: &database.PaginationArgs{Last: intPtr(5), Before: toStringPtr(1)}, + expectedArgs: &database.PaginationArgs{Last: intPtr(6), Before: toStringPtr(0)}, expectedModified: true, }, { name: "last: 5, before: 0 (same as last page but a mathematical edge case)", - args: &database.PaginationArgs{Last: intPtr(5), Before: intPtr(0)}, - expectedArgs: &database.PaginationArgs{Last: intPtr(5), Before: intPtr(0)}, + args: &database.PaginationArgs{Last: intPtr(5), Before: toStringPtr(0)}, + expectedArgs: &database.PaginationArgs{Last: intPtr(5), Before: toStringPtr(0)}, expectedModified: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - modified := modifyArgs(tc.args) + modified, err := modifyArgs(tc.args) + if err != nil { + t.Fatal(err) + } + if modified != tc.expectedModified { t.Errorf("Expected modified to be %v, but got %v", modified, tc.expectedModified) } diff --git a/cmd/frontend/graphqlbackend/site_test.go b/cmd/frontend/graphqlbackend/site_test.go index d37953c83964..43d3a0ba24fa 100644 --- a/cmd/frontend/graphqlbackend/site_test.go +++ b/cmd/frontend/graphqlbackend/site_test.go @@ -71,7 +71,7 @@ func TestSiteConfigurationHistory(t *testing.T) { }, { name: "last: 20 (more items than what exists in the database)", - args: &graphqlutil.ConnectionResolverArgs{Last: int32Ptr(5)}, + args: &graphqlutil.ConnectionResolverArgs{Last: int32Ptr(20)}, expectedSiteConfigIDs: []int32{5, 4, 3, 2, 1}, }, { diff --git a/internal/database/conf_test.go b/internal/database/conf_test.go index 2b216511c418..44faca5ede56 100644 --- a/internal/database/conf_test.go +++ b/internal/database/conf_test.go @@ -286,6 +286,7 @@ func TestGetSiteConfigCount(t *testing.T) { func TestListSiteConfigs(t *testing.T) { toIntPtr := func(n int) *int { return &n } + toStringPtr := func(n string) *string { return &n } if testing.Short() { t.Skip() @@ -357,7 +358,7 @@ func TestListSiteConfigs(t *testing.T) { name: "first: 2, after: 3", listOptions: &PaginationArgs{ First: toIntPtr(2), - After: toIntPtr(3), + After: toStringPtr("3"), }, expectedIDs: []int32{2, 1}, }, @@ -365,7 +366,7 @@ func TestListSiteConfigs(t *testing.T) { name: "first: 5, after: 3 (overflow)", listOptions: &PaginationArgs{ First: toIntPtr(5), - After: toIntPtr(3), + After: toStringPtr("3"), }, expectedIDs: []int32{2, 1}, }, @@ -373,7 +374,7 @@ func TestListSiteConfigs(t *testing.T) { name: "last: 2, after: 4", listOptions: &PaginationArgs{ Last: toIntPtr(2), - After: toIntPtr(4), + After: toStringPtr("4"), }, expectedIDs: []int32{1, 2}, }, @@ -381,7 +382,7 @@ func TestListSiteConfigs(t *testing.T) { name: "last: 5, after: 4 (overflow)", listOptions: &PaginationArgs{ Last: toIntPtr(5), - After: toIntPtr(4), + After: toStringPtr("4"), }, expectedIDs: []int32{1, 2, 3}, }, @@ -389,7 +390,7 @@ func TestListSiteConfigs(t *testing.T) { name: "first: 2, before: 1", listOptions: &PaginationArgs{ First: toIntPtr(2), - Before: toIntPtr(1), + Before: toStringPtr("1"), }, expectedIDs: []int32{4, 3}, }, @@ -397,7 +398,7 @@ func TestListSiteConfigs(t *testing.T) { name: "first: 5, before: 1 (overflow)", listOptions: &PaginationArgs{ First: toIntPtr(5), - Before: toIntPtr(1), + Before: toStringPtr("1"), }, expectedIDs: []int32{4, 3, 2}, }, @@ -405,7 +406,7 @@ func TestListSiteConfigs(t *testing.T) { name: "last: 2, before: 1", listOptions: &PaginationArgs{ Last: toIntPtr(2), - Before: toIntPtr(1), + Before: toStringPtr("1"), }, expectedIDs: []int32{2, 3}, }, @@ -413,7 +414,7 @@ func TestListSiteConfigs(t *testing.T) { name: "last: 5, before: 1 (overflow)", listOptions: &PaginationArgs{ Last: toIntPtr(5), - Before: toIntPtr(1), + Before: toStringPtr("1"), }, expectedIDs: []int32{2, 3, 4}, }, diff --git a/internal/database/helpers.go b/internal/database/helpers.go index 90897ba645f2..540318c608ac 100644 --- a/internal/database/helpers.go +++ b/internal/database/helpers.go @@ -1,7 +1,9 @@ package database import ( + "fmt" "strconv" + "strings" "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/relay" @@ -77,32 +79,102 @@ func (a *QueryArgs) AppendAllToQuery(query *sqlf.Query) *sqlf.Query { return query } +type OrderBy []OrderByOption + +func (o OrderBy) Columns() []string { + columns := []string{} + + for _, orderOption := range o { + columns = append(columns, orderOption.Field) + } + + return columns +} + +func (o OrderBy) SQL(ascending bool) *sqlf.Query { + columns := []*sqlf.Query{} + + for _, orderOption := range o { + columns = append(columns, orderOption.SQL(ascending)) + } + + return sqlf.Join(columns, ", ") +} + +type OrderByOption struct { + Field string + Nulls string +} + +func (o OrderByOption) SQL(ascending bool) *sqlf.Query { + var sb strings.Builder + + sb.WriteString(o.Field) + + if ascending { + sb.WriteString(" ASC") + } else { + sb.WriteString(" DESC") + } + + if o.Nulls == "FIRST" || o.Nulls == "LAST" { + sb.WriteString(" NULLS " + o.Nulls) + } + + return sqlf.Sprintf(sb.String()) +} + type PaginationArgs struct { First *int Last *int - After *int - Before *int + After *string + Before *string + + // TODDO(naman): explain default + OrderBy OrderBy + Ascending bool } func (p *PaginationArgs) SQL() (*QueryArgs, error) { queryArgs := &QueryArgs{} var conditions []*sqlf.Query + + orderBy := p.OrderBy + if len(orderBy) < 1 { + orderBy = OrderBy{{Field: "id"}} + } + + orderByColumns := orderBy.Columns() + if p.After != nil { - conditions = append(conditions, sqlf.Sprintf("id < %v", p.After)) + columnsStr := strings.Join(orderByColumns, ", ") + condition := fmt.Sprintf("(%s) >", columnsStr) + if !p.Ascending { + condition = fmt.Sprintf("(%s) <", columnsStr) + } + + conditions = append(conditions, sqlf.Sprintf(fmt.Sprintf(condition+" (%s)", *p.After))) } if p.Before != nil { - conditions = append(conditions, sqlf.Sprintf("id > %v", p.Before)) + columnsStr := strings.Join(orderByColumns, ", ") + condition := fmt.Sprintf("(%s) <", columnsStr) + if !p.Ascending { + condition = fmt.Sprintf("(%s) >", columnsStr) + } + + conditions = append(conditions, sqlf.Sprintf(fmt.Sprintf(condition+" (%s)", *p.Before))) } + if len(conditions) > 0 { queryArgs.Where = sqlf.Sprintf("%v", sqlf.Join(conditions, "AND ")) } if p.First != nil { - queryArgs.Order = sqlf.Sprintf("id DESC") + queryArgs.Order = orderBy.SQL(p.Ascending) queryArgs.Limit = sqlf.Sprintf("LIMIT %d", *p.First) } else if p.Last != nil { - queryArgs.Order = sqlf.Sprintf("id ASC") + queryArgs.Order = orderBy.SQL(!p.Ascending) queryArgs.Limit = sqlf.Sprintf("LIMIT %d", *p.Last) } else { return nil, errors.New("First or Last must be set") @@ -111,20 +183,21 @@ func (p *PaginationArgs) SQL() (*QueryArgs, error) { return queryArgs, nil } +func copyPtr[T any](n *T) *T { + if n == nil { + return nil + } + + c := *n + return &c +} + // Clone (aka deepcopy) returns a new PaginationArgs object with the same values as "p". func (p *PaginationArgs) Clone() *PaginationArgs { - copyIntPtr := func(n *int) *int { - if n == nil { - return nil - } - - c := *n - return &c - } return &PaginationArgs{ - First: copyIntPtr(p.First), - Last: copyIntPtr(p.Last), - After: copyIntPtr(p.After), - Before: copyIntPtr(p.Before), + First: copyPtr[int](p.First), + Last: copyPtr[int](p.Last), + After: copyPtr[string](p.After), + Before: copyPtr[string](p.Before), } } diff --git a/internal/database/repos.go b/internal/database/repos.go index d8c0757317f3..4a8ed75a4d22 100644 --- a/internal/database/repos.go +++ b/internal/database/repos.go @@ -739,6 +739,9 @@ type ReposListOptions struct { // and if it doesn't end up being used this is wasted compute. ExcludeSources bool + // cursor-based pagination args + PaginationArgs *PaginationArgs + *LimitOffset } @@ -930,6 +933,22 @@ func (s *repoStore) list(ctx context.Context, tr *trace.Trace, opt ReposListOpti func (s *repoStore) listSQL(ctx context.Context, tr *trace.Trace, opt ReposListOptions) (*sqlf.Query, error) { var ctes, joins, where []*sqlf.Query + querySuffix := sqlf.Sprintf("%s %s", opt.OrderBy.SQL(), opt.LimitOffset.SQL()) + + if opt.PaginationArgs != nil { + p, err := opt.PaginationArgs.SQL() + if err != nil { + return nil, err + } + + if p.Where != nil { + where = append(where, p.Where) + } + + querySuffix = p.AppendOrderToQuery(&sqlf.Query{}) + querySuffix = p.AppendLimitToQuery(querySuffix) + } + // Cursor-based pagination requires parsing a handful of extra fields, which // may result in additional query conditions. if len(opt.Cursors) > 0 { @@ -1115,7 +1134,7 @@ func (s *repoStore) listSQL(ctx context.Context, tr *trace.Trace, opt ReposListO } if opt.NoCloned || opt.OnlyCloned || opt.FailedFetch || opt.OnlyCorrupted || opt.joinGitserverRepos || - opt.CloneStatus != types.CloneStatusUnknown || containsSizeField(opt.OrderBy) { + opt.CloneStatus != types.CloneStatusUnknown || containsSizeField(opt.OrderBy) || (opt.PaginationArgs != nil && containsOrderBySizeField(opt.PaginationArgs.OrderBy)) { joins = append(joins, sqlf.Sprintf("JOIN gitserver_repos gr ON gr.repo_id = repo.id")) } if opt.OnlyIndexed || opt.NoIndexed { @@ -1172,8 +1191,6 @@ func (s *repoStore) listSQL(ctx context.Context, tr *trace.Trace, opt ReposListO queryPrefix = sqlf.Sprintf("WITH %s", sqlf.Join(ctes, ",\n")) } - querySuffix := sqlf.Sprintf("%s %s", opt.OrderBy.SQL(), opt.LimitOffset.SQL()) - columns := repoColumns if !opt.ExcludeSources { columns = append(columns, getSourcesByRepoQueryStr) @@ -1210,6 +1227,15 @@ func containsSizeField(orderBy RepoListOrderBy) bool { return false } +func containsOrderBySizeField(orderBy OrderBy) bool { + for _, field := range orderBy { + if field.Field == string(RepoListSize) { + return true + } + } + return false +} + const userReposCTEFmtstr = ` SELECT repo_id as id FROM external_service_repos WHERE user_id = %d ` From b724b071ca711158cadd3055fb79a35876d3f24b Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Jan 2023 10:52:35 +0100 Subject: [PATCH 054/678] Lock to the first visible line of code when switching git blame on/off (#46684) --- client/web/src/repo/blob/CodeMirrorBlob.tsx | 38 +++---------------- .../web/src/repo/blob/codemirror/lock-line.ts | 37 ++++++++++++++++++ 2 files changed, 43 insertions(+), 32 deletions(-) create mode 100644 client/web/src/repo/blob/codemirror/lock-line.ts diff --git a/client/web/src/repo/blob/CodeMirrorBlob.tsx b/client/web/src/repo/blob/CodeMirrorBlob.tsx index 562366f087d1..c7d68f6907f1 100644 --- a/client/web/src/repo/blob/CodeMirrorBlob.tsx +++ b/client/web/src/repo/blob/CodeMirrorBlob.tsx @@ -5,7 +5,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { openSearchPanel } from '@codemirror/search' -import { Compartment, EditorState, Extension, Line } from '@codemirror/state' +import { Compartment, EditorState, Extension } from '@codemirror/state' import { EditorView } from '@codemirror/view' import { isEqual } from 'lodash' @@ -26,7 +26,8 @@ import { blobPropsFacet } from './codemirror' import { createBlameDecorationsExtension } from './codemirror/blame-decorations' import { syntaxHighlight } from './codemirror/highlight' import { pin, updatePin } from './codemirror/hovercard' -import { selectableLineNumbers, SelectedLineRange, selectLines, shouldScrollIntoView } from './codemirror/linenumbers' +import { selectableLineNumbers, SelectedLineRange, selectLines } from './codemirror/linenumbers' +import { lockFirstVisibleLine } from './codemirror/lock-line' import { navigateToLineOnAnyClickExtension } from './codemirror/navigate-to-any-line-on-click' import { search } from './codemirror/search' import { sourcegraphExtensions } from './codemirror/sourcegraph-extensions' @@ -277,7 +278,8 @@ export const Blob: React.FunctionComponent = props => { // Update blame decorations useLayoutEffect(() => { if (editor) { - editor.dispatch({ effects: blameDecorationsCompartment.reconfigure(blameDecorations) }) + const effects = [blameDecorationsCompartment.reconfigure(blameDecorations), ...lockFirstVisibleLine(editor)] + editor.dispatch({ effects }) } // editor is not provided because this should only be triggered after the // editor was created (i.e. not on first render) @@ -297,14 +299,7 @@ export const Blob: React.FunctionComponent = props => { // Update line wrapping useEffect(() => { if (editor) { - const effects = [wrapCodeCompartment.reconfigure(wrapCodeSettings)] - const firstLine = firstVisibleLine(editor) - if (firstLine) { - // Avoid jumpy scrollbar when enabling line wrapping by forcing the - // scroll bar to preserve the top line number that's visible. - // Details https://github.com/sourcegraph/sourcegraph/issues/41413 - effects.push(EditorView.scrollIntoView(firstLine.from, { y: 'start' })) - } + const effects = [wrapCodeCompartment.reconfigure(wrapCodeSettings), ...lockFirstVisibleLine(editor)] editor.dispatch({ effects }) } // editor is not provided because this should only be triggered after the @@ -377,24 +372,3 @@ function useDistinctBlob(blobInfo: BlobInfo): BlobInfo { return blobRef.current }, [blobInfo]) } - -// Returns the first line that is visible in the editor. We can't directly use -// the viewport for this functionality because the viewport includes lines that -// are rendered but not visible. -function firstVisibleLine(view: EditorView): Line | undefined { - for (const { from, to } of view.visibleRanges) { - for (let pos = from; pos < to; ) { - const line = view.state.doc.lineAt(pos) - // This may be an inefficient way to detect the first visible line - // but it appears to work correctly and this is unlikely to be a - // performance bottleneck since we should only use need to compute - // this for infrequently used code-paths like when enabling/disabling - // line wrapping, or when lazy syntax highlighting gets loaded. - if (!shouldScrollIntoView(view, { line: line.number + 1 })) { - return line - } - pos = line.to + 1 - } - } - return undefined -} diff --git a/client/web/src/repo/blob/codemirror/lock-line.ts b/client/web/src/repo/blob/codemirror/lock-line.ts new file mode 100644 index 000000000000..662959dd6bf5 --- /dev/null +++ b/client/web/src/repo/blob/codemirror/lock-line.ts @@ -0,0 +1,37 @@ +import { Line, StateEffect } from '@codemirror/state' +import { EditorView } from '@codemirror/view' + +import { shouldScrollIntoView } from './linenumbers' + +// Avoid jumpy scrollbar when the line dimensions change by locking on to the +// first visible line. +// +// Details https://github.com/sourcegraph/sourcegraph/issues/41413 +export function lockFirstVisibleLine(view: EditorView): StateEffect[] { + const firstLine = firstVisibleLine(view) + if (firstLine) { + return [EditorView.scrollIntoView(firstLine.from, { y: 'start' })] + } + return [] +} + +// Returns the first line that is visible in the editor. We can't directly use +// the viewport for this functionality because the viewport includes lines that +// are rendered but not visible. +function firstVisibleLine(view: EditorView): Line | undefined { + for (const { from, to } of view.visibleRanges) { + for (let pos = from; pos < to; ) { + const line = view.state.doc.lineAt(pos) + // This may be an inefficient way to detect the first visible line + // but it appears to work correctly and this is unlikely to be a + // performance bottleneck since we should only use need to compute + // this for infrequently used code-paths like when enabling/disabling + // line wrapping, or when lazy syntax highlighting gets loaded. + if (!shouldScrollIntoView(view, { line: line.number + 1 })) { + return line + } + pos = line.to + 1 + } + } + return undefined +} From e1852a7dfbf19b432c6944608ba80b9ac4718e90 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Fri, 20 Jan 2023 12:12:14 +0200 Subject: [PATCH 055/678] Backstage: add scaffolding (#46634) * initial plugin scaffolding as generated by backstage * ignore backstage for prettier * update eslint-plugin-react * fix react backstage versioning issue * review feedback and licensing * add missing config for startup * ignore more licenses from deps pulled in by backstage * add node_modules to prettier ignore --- .prettierignore | 2 + .stylelintignore | 1 + client/eslint-plugin-wildcard/package.json | 2 +- client/plugin-backstage/.eslintrc.js | 1 + client/plugin-backstage/.gitignore | 1 + client/plugin-backstage/README.md | 13 + client/plugin-backstage/app-config.yaml | 104 + client/plugin-backstage/dev/index.tsx | 12 + .../dist-types/dev/index.d.ts | 1 + .../ExampleComponent/ExampleComponent.d.ts | 1 + .../ExampleComponent.test.d.ts | 1 + .../components/ExampleComponent/index.d.ts | 1 + .../ExampleFetchComponent.d.ts | 29 + .../ExampleFetchComponent.test.d.ts | 1 + .../ExampleFetchComponent/index.d.ts | 1 + .../dist-types/src/index.d.ts | 1 + .../dist-types/src/plugin.d.ts | 4 + .../dist-types/src/plugin.test.d.ts | 1 + .../dist-types/src/routes.d.ts | 1 + .../dist-types/src/setupTests.d.ts | 2 + client/plugin-backstage/package.json | 62 + .../ExampleComponent.test.tsx | 27 + .../ExampleComponent/ExampleComponent.tsx | 38 + .../src/components/ExampleComponent/index.ts | 1 + .../ExampleFetchComponent.test.tsx | 25 + .../ExampleFetchComponent.tsx | 90 + .../components/ExampleFetchComponent/index.ts | 1 + client/plugin-backstage/src/index.ts | 1 + client/plugin-backstage/src/plugin.test.ts | 7 + client/plugin-backstage/src/plugin.ts | 19 + client/plugin-backstage/src/routes.ts | 5 + client/plugin-backstage/src/setupTests.ts | 2 + client/plugin-backstage/tsconfig.json | 15 + doc/dependency_decisions.yml | 36 + pnpm-lock.yaml | 6342 ++++++++++++++--- 35 files changed, 5965 insertions(+), 886 deletions(-) create mode 100644 client/plugin-backstage/.eslintrc.js create mode 100644 client/plugin-backstage/.gitignore create mode 100644 client/plugin-backstage/README.md create mode 100644 client/plugin-backstage/app-config.yaml create mode 100644 client/plugin-backstage/dev/index.tsx create mode 100644 client/plugin-backstage/dist-types/dev/index.d.ts create mode 100644 client/plugin-backstage/dist-types/src/components/ExampleComponent/ExampleComponent.d.ts create mode 100644 client/plugin-backstage/dist-types/src/components/ExampleComponent/ExampleComponent.test.d.ts create mode 100644 client/plugin-backstage/dist-types/src/components/ExampleComponent/index.d.ts create mode 100644 client/plugin-backstage/dist-types/src/components/ExampleFetchComponent/ExampleFetchComponent.d.ts create mode 100644 client/plugin-backstage/dist-types/src/components/ExampleFetchComponent/ExampleFetchComponent.test.d.ts create mode 100644 client/plugin-backstage/dist-types/src/components/ExampleFetchComponent/index.d.ts create mode 100644 client/plugin-backstage/dist-types/src/index.d.ts create mode 100644 client/plugin-backstage/dist-types/src/plugin.d.ts create mode 100644 client/plugin-backstage/dist-types/src/plugin.test.d.ts create mode 100644 client/plugin-backstage/dist-types/src/routes.d.ts create mode 100644 client/plugin-backstage/dist-types/src/setupTests.d.ts create mode 100644 client/plugin-backstage/package.json create mode 100644 client/plugin-backstage/src/components/ExampleComponent/ExampleComponent.test.tsx create mode 100644 client/plugin-backstage/src/components/ExampleComponent/ExampleComponent.tsx create mode 100644 client/plugin-backstage/src/components/ExampleComponent/index.ts create mode 100644 client/plugin-backstage/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx create mode 100644 client/plugin-backstage/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx create mode 100644 client/plugin-backstage/src/components/ExampleFetchComponent/index.ts create mode 100644 client/plugin-backstage/src/index.ts create mode 100644 client/plugin-backstage/src/plugin.test.ts create mode 100644 client/plugin-backstage/src/plugin.ts create mode 100644 client/plugin-backstage/src/routes.ts create mode 100644 client/plugin-backstage/src/setupTests.ts create mode 100644 client/plugin-backstage/tsconfig.json diff --git a/.prettierignore b/.prettierignore index feb34b90d54d..1d053e886dea 100644 --- a/.prettierignore +++ b/.prettierignore @@ -41,3 +41,5 @@ client/jetbrains/.gradle code-intel-extensions.json .direnv pnpm-lock.yaml +client/plugin-backstage/ +node_modules/ diff --git a/.stylelintignore b/.stylelintignore index bb4bb19c8b66..5da33b1db8b4 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -5,3 +5,4 @@ **/*.svg node_modules/ __mocks__/ +client/plugin-backstage/ diff --git a/client/eslint-plugin-wildcard/package.json b/client/eslint-plugin-wildcard/package.json index 0d445bb1c7e5..8f4f417d589e 100644 --- a/client/eslint-plugin-wildcard/package.json +++ b/client/eslint-plugin-wildcard/package.json @@ -5,7 +5,7 @@ "description": "Custom rules and recommended config for consumers of the Wildcard component library", "main": "lib/index.js", "peerDependencies": { - "eslint-plugin-react": "^7.21.1" + "eslint-plugin-react": "^7.32.1" }, "license": "Apache-2.0" } diff --git a/client/plugin-backstage/.eslintrc.js b/client/plugin-backstage/.eslintrc.js new file mode 100644 index 000000000000..e2a53a6ad283 --- /dev/null +++ b/client/plugin-backstage/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/client/plugin-backstage/.gitignore b/client/plugin-backstage/.gitignore new file mode 100644 index 000000000000..3c3629e647f5 --- /dev/null +++ b/client/plugin-backstage/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/client/plugin-backstage/README.md b/client/plugin-backstage/README.md new file mode 100644 index 000000000000..dbd27a8c86a6 --- /dev/null +++ b/client/plugin-backstage/README.md @@ -0,0 +1,13 @@ +# sourcegraph + +Welcome to the sourcegraph plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/sourcegraph](http://localhost:3000/sourcegraph). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/client/plugin-backstage/app-config.yaml b/client/plugin-backstage/app-config.yaml new file mode 100644 index 000000000000..3b5de8baa30c --- /dev/null +++ b/client/plugin-backstage/app-config.yaml @@ -0,0 +1,104 @@ +app: + title: Scaffolded Backstage App + baseUrl: http://localhost:3000 + +organization: + name: My Company + +backend: + # Used for enabling authentication, secret is shared by all backend plugins + # See https://backstage.io/docs/tutorials/backend-to-backend-auth for + # information on the format + # auth: + # keys: + # - secret: ${BACKEND_SECRET} + baseUrl: http://localhost:7007 + listen: + port: 7007 + # Uncomment the following host directive to bind to specific interfaces + # host: 127.0.0.1 + csp: + connect-src: ["'self'", 'http:', 'https:'] + # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference + # Default Helmet Content-Security-Policy values can be removed by setting the key to false + cors: + origin: http://localhost:3000 + methods: [GET, HEAD, PATCH, POST, PUT, DELETE] + credentials: true + # This is for local development only, it is not recommended to use this in production + # The production database configuration is stored in app-config.production.yaml + database: + client: better-sqlite3 + connection: ':memory:' + cache: + store: memory + # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir + +integrations: + github: + - host: github.com + # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information + # about setting up the GitHub integration here: https://backstage.io/docs/getting-started/configuration#setting-up-a-github-integration + token: ${GITHUB_TOKEN} + ### Example for how to add your GitHub Enterprise instance using the API: + # - host: ghe.example.net + # apiBaseUrl: https://ghe.example.net/api/v3 + # token: ${GHE_TOKEN} + +proxy: + ### Example for how to add a proxy endpoint for the frontend. + ### A typical reason to do this is to handle HTTPS and CORS for internal services. + # '/test': + # target: 'https://example.com' + # changeOrigin: true + +# Reference documentation http://backstage.io/docs/features/techdocs/configuration +# Note: After experimenting with basic setup, use CI/CD to generate docs +# and an external cloud storage when deploying TechDocs for production use-case. +# https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach +techdocs: + builder: 'local' # Alternatives - 'external' + generator: + runIn: 'docker' # Alternatives - 'local' + publisher: + type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives. + +auth: + # see https://backstage.io/docs/auth/ to learn about auth providers + providers: {} + +scaffolder: + # see https://backstage.io/docs/features/software-templates/configuration for software template options + +catalog: + import: + entityFilename: catalog-info.yaml + pullRequestBranchName: backstage-integration + rules: + - allow: [Component, System, API, Resource, Location] + locations: + # Local example data, file locations are relative to the backend process, typically `packages/backend` + - type: file + target: ../../examples/entities.yaml + + # Local example template + - type: file + target: ../../examples/template/template.yaml + rules: + - allow: [Template] + + # Local example organizational data + - type: file + target: ../../examples/org.yaml + rules: + - allow: [User, Group] + + ## Uncomment these lines to add more example data + # - type: url + # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml + + ## Uncomment these lines to add an example org + # - type: url + # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml + # rules: + # - allow: [User, Group] diff --git a/client/plugin-backstage/dev/index.tsx b/client/plugin-backstage/dev/index.tsx new file mode 100644 index 000000000000..dc23569c9120 --- /dev/null +++ b/client/plugin-backstage/dev/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { sourcegraphPlugin, SourcegraphPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(sourcegraphPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/sourcegraph' + }) + .render(); diff --git a/client/plugin-backstage/dist-types/dev/index.d.ts b/client/plugin-backstage/dist-types/dev/index.d.ts new file mode 100644 index 000000000000..cb0ff5c3b541 --- /dev/null +++ b/client/plugin-backstage/dist-types/dev/index.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/client/plugin-backstage/dist-types/src/components/ExampleComponent/ExampleComponent.d.ts b/client/plugin-backstage/dist-types/src/components/ExampleComponent/ExampleComponent.d.ts new file mode 100644 index 000000000000..7baec3657f80 --- /dev/null +++ b/client/plugin-backstage/dist-types/src/components/ExampleComponent/ExampleComponent.d.ts @@ -0,0 +1 @@ +export declare const ExampleComponent: () => JSX.Element; diff --git a/client/plugin-backstage/dist-types/src/components/ExampleComponent/ExampleComponent.test.d.ts b/client/plugin-backstage/dist-types/src/components/ExampleComponent/ExampleComponent.test.d.ts new file mode 100644 index 000000000000..cb0ff5c3b541 --- /dev/null +++ b/client/plugin-backstage/dist-types/src/components/ExampleComponent/ExampleComponent.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/client/plugin-backstage/dist-types/src/components/ExampleComponent/index.d.ts b/client/plugin-backstage/dist-types/src/components/ExampleComponent/index.d.ts new file mode 100644 index 000000000000..8b8437521b3c --- /dev/null +++ b/client/plugin-backstage/dist-types/src/components/ExampleComponent/index.d.ts @@ -0,0 +1 @@ +export { ExampleComponent } from './ExampleComponent'; diff --git a/client/plugin-backstage/dist-types/src/components/ExampleFetchComponent/ExampleFetchComponent.d.ts b/client/plugin-backstage/dist-types/src/components/ExampleFetchComponent/ExampleFetchComponent.d.ts new file mode 100644 index 000000000000..49bf203eb489 --- /dev/null +++ b/client/plugin-backstage/dist-types/src/components/ExampleFetchComponent/ExampleFetchComponent.d.ts @@ -0,0 +1,29 @@ +type User = { + gender: string; + name: { + title: string; + first: string; + last: string; + }; + location: object; + email: string; + login: object; + dob: object; + registered: object; + phone: string; + cell: string; + id: { + name: string; + value: string; + }; + picture: { + medium: string; + }; + nat: string; +}; +type DenseTableProps = { + users: User[]; +}; +export declare const DenseTable: ({ users }: DenseTableProps) => JSX.Element; +export declare const ExampleFetchComponent: () => JSX.Element; +export {}; diff --git a/client/plugin-backstage/dist-types/src/components/ExampleFetchComponent/ExampleFetchComponent.test.d.ts b/client/plugin-backstage/dist-types/src/components/ExampleFetchComponent/ExampleFetchComponent.test.d.ts new file mode 100644 index 000000000000..cb0ff5c3b541 --- /dev/null +++ b/client/plugin-backstage/dist-types/src/components/ExampleFetchComponent/ExampleFetchComponent.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/client/plugin-backstage/dist-types/src/components/ExampleFetchComponent/index.d.ts b/client/plugin-backstage/dist-types/src/components/ExampleFetchComponent/index.d.ts new file mode 100644 index 000000000000..41a43e84f108 --- /dev/null +++ b/client/plugin-backstage/dist-types/src/components/ExampleFetchComponent/index.d.ts @@ -0,0 +1 @@ +export { ExampleFetchComponent } from './ExampleFetchComponent'; diff --git a/client/plugin-backstage/dist-types/src/index.d.ts b/client/plugin-backstage/dist-types/src/index.d.ts new file mode 100644 index 000000000000..9bab1918c1b6 --- /dev/null +++ b/client/plugin-backstage/dist-types/src/index.d.ts @@ -0,0 +1 @@ +export { sourcegraphPlugin, SourcegraphPage } from './plugin'; diff --git a/client/plugin-backstage/dist-types/src/plugin.d.ts b/client/plugin-backstage/dist-types/src/plugin.d.ts new file mode 100644 index 000000000000..486258189455 --- /dev/null +++ b/client/plugin-backstage/dist-types/src/plugin.d.ts @@ -0,0 +1,4 @@ +export declare const sourcegraphPlugin: import("@backstage/core-plugin-api").BackstagePlugin<{ + root: import("@backstage/core-plugin-api").RouteRef; +}, {}, {}>; +export declare const SourcegraphPage: () => JSX.Element; diff --git a/client/plugin-backstage/dist-types/src/plugin.test.d.ts b/client/plugin-backstage/dist-types/src/plugin.test.d.ts new file mode 100644 index 000000000000..cb0ff5c3b541 --- /dev/null +++ b/client/plugin-backstage/dist-types/src/plugin.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/client/plugin-backstage/dist-types/src/routes.d.ts b/client/plugin-backstage/dist-types/src/routes.d.ts new file mode 100644 index 000000000000..9603076fc428 --- /dev/null +++ b/client/plugin-backstage/dist-types/src/routes.d.ts @@ -0,0 +1 @@ +export declare const rootRouteRef: import("@backstage/core-plugin-api").RouteRef; diff --git a/client/plugin-backstage/dist-types/src/setupTests.d.ts b/client/plugin-backstage/dist-types/src/setupTests.d.ts new file mode 100644 index 000000000000..48c09b534631 --- /dev/null +++ b/client/plugin-backstage/dist-types/src/setupTests.d.ts @@ -0,0 +1,2 @@ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/client/plugin-backstage/package.json b/client/plugin-backstage/package.json new file mode 100644 index 000000000000..c2250c2ef162 --- /dev/null +++ b/client/plugin-backstage/package.json @@ -0,0 +1,62 @@ +{ + "name": "@sourcegraph/backstage-plugin", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "^0.12.3", + "@backstage/core-plugin-api": "^1.3.0", + "@backstage/theme": "^0.2.16", + "@internal/plugin-sourcegraph": "link:", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.7.0", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.22.0", + "@backstage/core-app-api": "^1.4.0", + "@backstage/dev-utils": "^1.0.11", + "@backstage/test-utils": "^1.2.3", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^0.49.0" + }, + "resolutions": { + "react": "^17", + "react-dom": "^17", + "@types/react": "^17", + "@types/react-dom": "^17" + }, + "files": [ + "dist" + ] +} diff --git a/client/plugin-backstage/src/components/ExampleComponent/ExampleComponent.test.tsx b/client/plugin-backstage/src/components/ExampleComponent/ExampleComponent.test.tsx new file mode 100644 index 000000000000..df7f89463cc8 --- /dev/null +++ b/client/plugin-backstage/src/components/ExampleComponent/ExampleComponent.test.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { ExampleComponent } from './ExampleComponent'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { screen } from '@testing-library/react'; +import { + setupRequestMockHandlers, + renderInTestApp, +} from "@backstage/test-utils"; + +describe('ExampleComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + setupRequestMockHandlers(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), + ); + }); + + it('should render', async () => { + await renderInTestApp(); + expect(screen.getByText('Welcome to sourcegraph!')).toBeInTheDocument(); + }); +}); diff --git a/client/plugin-backstage/src/components/ExampleComponent/ExampleComponent.tsx b/client/plugin-backstage/src/components/ExampleComponent/ExampleComponent.tsx new file mode 100644 index 000000000000..84e7068bb28c --- /dev/null +++ b/client/plugin-backstage/src/components/ExampleComponent/ExampleComponent.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Typography, Grid } from '@material-ui/core'; +import { + InfoCard, + Header, + Page, + Content, + ContentHeader, + HeaderLabel, + SupportButton, +} from '@backstage/core-components'; +import { ExampleFetchComponent } from '../ExampleFetchComponent'; + +export const ExampleComponent = () => ( + +
+ + +
+ + + A description of your plugin goes here. + + + + + + All content should be wrapped in a card like this. + + + + + + + + +
+); diff --git a/client/plugin-backstage/src/components/ExampleComponent/index.ts b/client/plugin-backstage/src/components/ExampleComponent/index.ts new file mode 100644 index 000000000000..8b8437521b3c --- /dev/null +++ b/client/plugin-backstage/src/components/ExampleComponent/index.ts @@ -0,0 +1 @@ +export { ExampleComponent } from './ExampleComponent'; diff --git a/client/plugin-backstage/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx b/client/plugin-backstage/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx new file mode 100644 index 000000000000..a553ecd0b0e7 --- /dev/null +++ b/client/plugin-backstage/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { ExampleFetchComponent } from './ExampleFetchComponent'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; + +describe('ExampleFetchComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + setupRequestMockHandlers(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('https://randomuser.me/*', (_, res, ctx) => + res(ctx.status(200), ctx.delay(2000), ctx.json({})), + ), + ); + }); + it('should render', async () => { + await render(); + expect(await screen.findByTestId('progress')).toBeInTheDocument(); + }); +}); diff --git a/client/plugin-backstage/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/client/plugin-backstage/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx new file mode 100644 index 000000000000..8790e3572dd1 --- /dev/null +++ b/client/plugin-backstage/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { Table, TableColumn, Progress } from '@backstage/core-components'; +import Alert from '@material-ui/lab/Alert'; +import useAsync from 'react-use/lib/useAsync'; + +const useStyles = makeStyles({ + avatar: { + height: 32, + width: 32, + borderRadius: '50%', + }, +}); + +type User = { + gender: string; // "male" + name: { + title: string; // "Mr", + first: string; // "Duane", + last: string; // "Reed" + }; + location: object; // {street: {number: 5060, name: "Hickory Creek Dr"}, city: "Albany", state: "New South Wales",…} + email: string; // "duane.reed@example.com" + login: object; // {uuid: "4b785022-9a23-4ab9-8a23-cb3fb43969a9", username: "blackdog796", password: "patch",…} + dob: object; // {date: "1983-06-22T12:30:23.016Z", age: 37} + registered: object; // {date: "2006-06-13T18:48:28.037Z", age: 14} + phone: string; // "07-2154-5651" + cell: string; // "0405-592-879" + id: { + name: string; // "TFN", + value: string; // "796260432" + }; + picture: { medium: string }; // {medium: "https://randomuser.me/api/portraits/men/95.jpg",…} + nat: string; // "AU" +}; + +type DenseTableProps = { + users: User[]; +}; + +export const DenseTable = ({ users }: DenseTableProps) => { + const classes = useStyles(); + + const columns: TableColumn[] = [ + { title: 'Avatar', field: 'avatar' }, + { title: 'Name', field: 'name' }, + { title: 'Email', field: 'email' }, + { title: 'Nationality', field: 'nationality' }, + ]; + + const data = users.map(user => { + return { + avatar: ( + {user.name.first} + ), + name: `${user.name.first} ${user.name.last}`, + email: user.email, + nationality: user.nat, + }; + }); + + return ( + + ); +}; + +export const ExampleFetchComponent = () => { + const { value, loading, error } = useAsync(async (): Promise => { + const response = await fetch('https://randomuser.me/api/?results=20'); + const data = await response.json(); + return data.results; + }, []); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + return ; +}; diff --git a/client/plugin-backstage/src/components/ExampleFetchComponent/index.ts b/client/plugin-backstage/src/components/ExampleFetchComponent/index.ts new file mode 100644 index 000000000000..41a43e84f108 --- /dev/null +++ b/client/plugin-backstage/src/components/ExampleFetchComponent/index.ts @@ -0,0 +1 @@ +export { ExampleFetchComponent } from './ExampleFetchComponent'; diff --git a/client/plugin-backstage/src/index.ts b/client/plugin-backstage/src/index.ts new file mode 100644 index 000000000000..9bab1918c1b6 --- /dev/null +++ b/client/plugin-backstage/src/index.ts @@ -0,0 +1 @@ +export { sourcegraphPlugin, SourcegraphPage } from './plugin'; diff --git a/client/plugin-backstage/src/plugin.test.ts b/client/plugin-backstage/src/plugin.test.ts new file mode 100644 index 000000000000..fc47805bfe06 --- /dev/null +++ b/client/plugin-backstage/src/plugin.test.ts @@ -0,0 +1,7 @@ +import { sourcegraphPlugin } from './plugin'; + +describe('sourcegraph', () => { + it('should export plugin', () => { + expect(sourcegraphPlugin).toBeDefined(); + }); +}); diff --git a/client/plugin-backstage/src/plugin.ts b/client/plugin-backstage/src/plugin.ts new file mode 100644 index 000000000000..2e39ff72a2c7 --- /dev/null +++ b/client/plugin-backstage/src/plugin.ts @@ -0,0 +1,19 @@ +import { createPlugin, createRoutableExtension } from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +export const sourcegraphPlugin = createPlugin({ + id: 'sourcegraph', + routes: { + root: rootRouteRef, + }, +}); + +export const SourcegraphPage = sourcegraphPlugin.provide( + createRoutableExtension({ + name: 'SourcegraphPage', + component: () => + import('./components/ExampleComponent').then(m => m.ExampleComponent), + mountPoint: rootRouteRef, + }), +); diff --git a/client/plugin-backstage/src/routes.ts b/client/plugin-backstage/src/routes.ts new file mode 100644 index 000000000000..32d630351911 --- /dev/null +++ b/client/plugin-backstage/src/routes.ts @@ -0,0 +1,5 @@ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'sourcegraph', +}); diff --git a/client/plugin-backstage/src/setupTests.ts b/client/plugin-backstage/src/setupTests.ts new file mode 100644 index 000000000000..48c09b534631 --- /dev/null +++ b/client/plugin-backstage/src/setupTests.ts @@ -0,0 +1,2 @@ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/client/plugin-backstage/tsconfig.json b/client/plugin-backstage/tsconfig.json new file mode 100644 index 000000000000..be0c8b8b18d0 --- /dev/null +++ b/client/plugin-backstage/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": [ + "src", + "dev", + "migrations" + ], + "exclude": [ + "node_modules" + ], + "compilerOptions": { + "outDir": "dist-types", + "rootDir": "." + } +} diff --git a/doc/dependency_decisions.yml b/doc/dependency_decisions.yml index e98fc0699819..2793dd0a9c2b 100644 --- a/doc/dependency_decisions.yml +++ b/doc/dependency_decisions.yml @@ -452,6 +452,42 @@ :why: Inference broken, LICENSE file lives at https://www.npmjs.com/package/stdin#license :versions: [] :when: 2023-01-09 04:58:41.642155000 Z +- - :ignore + - eslint-plugin-deprecation + - :who: + :why: dependency from backstage + :versions: [] + :when: 2023-01-19 20:03:11.764982000 Z +- - :ignore + - fast-shallow-equal + - :who: + :why: dependency from backstage + :versions: [] + :when: 2023-01-19 20:03:33.000495000 Z +- - :ignore + - pako + - :who: + :why: dependency from backstage + :versions: [] + :when: 2023-01-19 20:03:51.213298000 Z +- - :ignore + - react-universal-interface + - :who: + :why: dependency from backstage + :versions: [] + :when: 2023-01-19 20:04:06.690991000 Z +- - :ignore + - tosource + - :who: + :why: dependency from backstage + :versions: [] + :when: 2023-01-19 20:25:48.126482000 Z +- - :ignore + - rollup-plugin-dts + - :who: + :why: dependency from backstage + :versions: [] + :when: 2023-01-19 20:26:01.138835000 Z - - :license - github.com/xi2/xz - Public domain diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07006c6d02f5..86c472fb5986 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -441,7 +441,7 @@ importers: '@sentry/browser': 7.8.1 '@sourcegraph/extension-api-classes': 1.1.0_sohmsbidf4ixb2vnv3mpdkpiau '@sourcegraph/extension-api-types': link:client/extension-api-types - '@storybook/addon-controls': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/addon-controls': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@visx/annotation': 2.10.0_ef5jwxihqo6n7gxfmzogljlgcm '@visx/axis': 2.11.1_react@18.1.0 '@visx/event': 2.6.0 @@ -492,7 +492,7 @@ importers: js-base64: 3.7.2 js-cookie: 2.2.1 js-yaml: 4.1.0 - jsonc-parser: 3.0.0 + jsonc-parser: 3.2.0 linguist-languages: 7.14.0 lodash: 4.17.21 lru-cache: 7.14.0 @@ -590,22 +590,22 @@ importers: '@storybook/addon-a11y': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/addon-actions': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/addon-console': 1.2.3_473b6rbrlfbkiblbxydosemc4u - '@storybook/addon-docs': 6.5.14_eecksdwfpy5g4a5i23piqqf6v4 + '@storybook/addon-docs': 6.5.14_pj2nlckshw25raeqzlyf2nlzmi '@storybook/addon-links': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm - '@storybook/addon-storyshots': 6.5.14_pmiuyjzits6kkyuf4klcpozjy4 + '@storybook/addon-storyshots': 6.5.14_cisa4gtyltf7ep7ewbmdkskhri '@storybook/addon-storyshots-puppeteer': 6.5.14_5bskxofbdzb2fai5lt3jrwan6e '@storybook/addon-storysource': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/addon-toolbars': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/addons': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/api': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm - '@storybook/builder-webpack5': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/builder-webpack5': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/client-api': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/components': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm - '@storybook/core': 6.5.14_r3crpb7cqiqcwc63njxwfaet7a - '@storybook/core-common': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/core': 6.5.14_o2cbjy3xxqdj2tzzexfwchdcvq + '@storybook/core-common': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/core-events': 6.5.14 - '@storybook/manager-webpack5': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi - '@storybook/react': 6.5.14_6bdewul77sl3cpoopqo52yktc4 + '@storybook/manager-webpack5': 6.5.14_gfp4q6646gfovewejhfnlg2afq + '@storybook/react': 6.5.14_r4utchdsxdspndke4rlo2g3uya '@storybook/theming': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@terminus-term/to-string-loader': 1.1.7-beta.1 '@testing-library/dom': 8.13.0 @@ -615,7 +615,7 @@ importers: '@testing-library/user-event': 13.5.0_tlwynutqiyp5mns3woioasuxnq '@types/babel__core': 7.1.20 '@types/bloomfilter': 0.0.0 - '@types/case-sensitive-paths-webpack-plugin': 2.1.6_oo63t3us67g6w4zg54gqorhf2i + '@types/case-sensitive-paths-webpack-plugin': 2.1.6_jh3afgd4dccyz4n3zkk5zvv5cy '@types/chrome': 0.0.127 '@types/classnames': 2.2.10 '@types/command-exists': 1.2.0 @@ -672,13 +672,13 @@ importers: '@types/simmerjs': 0.5.1 '@types/sinon': 9.0.4 '@types/socket.io-client': 1.4.33 - '@types/speed-measure-webpack-plugin': 1.3.4_oo63t3us67g6w4zg54gqorhf2i + '@types/speed-measure-webpack-plugin': 1.3.4_jh3afgd4dccyz4n3zkk5zvv5cy '@types/svgo': 2.6.0 '@types/testing-library__jest-dom': 5.9.5 '@types/uuid': 8.0.1 '@types/vscode': 1.63.1 - '@types/webpack-bundle-analyzer': 4.6.0_oo63t3us67g6w4zg54gqorhf2i - '@types/webpack-stats-plugin': 0.3.2_oo63t3us67g6w4zg54gqorhf2i + '@types/webpack-bundle-analyzer': 4.6.0_jh3afgd4dccyz4n3zkk5zvv5cy + '@types/webpack-stats-plugin': 0.3.2_jh3afgd4dccyz4n3zkk5zvv5cy '@types/yauzl': 2.10.0 '@vscode/test-electron': 2.1.3 abort-controller: 3.0.0 @@ -702,10 +702,10 @@ importers: connect-history-api-fallback: 1.6.0 cross-env: 7.0.2 css-loader: 6.7.2_webpack@5.75.0 - css-minimizer-webpack-plugin: 4.2.2_j5vv3ucw6sjpjo7n5idphq5l2u + css-minimizer-webpack-plugin: 4.2.2_htvmhiqynazf46fjrszipnqp7a enhanced-resolve: 5.10.0 envalid: 7.3.1 - esbuild: 0.16.10 + esbuild: 0.16.17 eslint: 8.18.0 eslint-plugin-monorepo: 0.3.2_fsuobzodmui5yhj7deh6fdsp7i events: 3.3.0 @@ -713,7 +713,7 @@ importers: expect: 27.5.1 express: 4.18.2 express-static-gzip: 2.1.1 - glob: 7.1.7 + glob: 7.2.3 google-auth-library: 5.7.0 googleapis: 47.0.0 googleapis-common: 3.2.0 @@ -779,16 +779,16 @@ importers: string-width: 4.2.3 style-loader: 3.3.1_webpack@5.75.0 stylelint: 14.3.0 - svgo: 2.7.0 + svgo: 2.8.0 term-size: 2.2.0 - terser-webpack-plugin: 5.3.6_j5vv3ucw6sjpjo7n5idphq5l2u + terser-webpack-plugin: 5.3.6_htvmhiqynazf46fjrszipnqp7a ts-loader: 9.4.2_vfotqvx6lgcbf3upbs6hgaza4q ts-node: 10.9.1_wh55dwo6xja56jtfktvlrff6xu typed-scss-modules: 4.1.1_sass@1.32.4 typescript: 4.9.3 utc-version: 2.0.2 vsce: 2.7.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-bundle-analyzer: 4.7.0 webpack-cli: 5.0.1_y7ttplitmkohdpgkllksfboxwa webpack-dev-server: 4.11.1_rjsyjcrmk25kqsjzwkvj3a2evq @@ -872,9 +872,9 @@ importers: client/eslint-plugin-wildcard: specifiers: - eslint-plugin-react: ^7.21.1 + eslint-plugin-react: ^7.32.1 dependencies: - eslint-plugin-react: 7.21.4_eslint@7.32.0 + eslint-plugin-react: 7.32.1_eslint@8.18.0 client/extension-api: specifiers: @@ -930,6 +930,53 @@ importers: devDependencies: '@sourcegraph/build-config': link:../build-config + client/plugin-backstage: + specifiers: + '@backstage/cli': ^0.22.0 + '@backstage/core-app-api': ^1.4.0 + '@backstage/core-components': ^0.12.3 + '@backstage/core-plugin-api': ^1.3.0 + '@backstage/dev-utils': ^1.0.11 + '@backstage/test-utils': ^1.2.3 + '@backstage/theme': ^0.2.16 + '@internal/plugin-sourcegraph': 'link:' + '@material-ui/core': ^4.9.13 + '@material-ui/icons': ^4.9.1 + '@material-ui/lab': 4.0.0-alpha.57 + '@testing-library/jest-dom': ^5.10.1 + '@testing-library/react': ^12.1.3 + '@testing-library/user-event': ^14.0.0 + '@types/node': '*' + cross-fetch: ^3.1.5 + msw: ^0.49.0 + react: ^18.2.0 + react-dom: ^18.2.0 + react-router-dom: ^6.7.0 + react-use: ^17.2.4 + dependencies: + '@backstage/core-components': 0.12.3_fvbr2cjomnxtlkfwrrwqyod56q + '@backstage/core-plugin-api': 1.3.0_sueyyaiqx2a24dbpeicw7rfxlu + '@backstage/theme': 0.2.16_7ir5hbqdbfw4we5ecpxz77f25m + '@internal/plugin-sourcegraph': 'link:' + '@material-ui/core': 4.12.4_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/icons': 4.11.3_5glfapqkhejqpattrqvw65m2la + '@material-ui/lab': 4.0.0-alpha.57_5glfapqkhejqpattrqvw65m2la + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-router-dom: 6.7.0_biqbaboplfbrettd7655fr4n2y + react-use: 17.4.0_biqbaboplfbrettd7655fr4n2y + devDependencies: + '@backstage/cli': 0.22.1_veicm43jo6hfcg5mjykszuneye + '@backstage/core-app-api': 1.4.0_4tknkhmdkn726v6rgr7mg246jm + '@backstage/dev-utils': 1.0.11_asenqjccrkesmsfwbq3lggkll4 + '@backstage/test-utils': 1.2.4_e442r2zblqvic2logccjohmmdi + '@testing-library/jest-dom': 5.16.4 + '@testing-library/react': 12.1.5_biqbaboplfbrettd7655fr4n2y + '@testing-library/user-event': 14.4.3_tlwynutqiyp5mns3woioasuxnq + '@types/node': 18.8.3 + cross-fetch: 3.1.5 + msw: 0.49.2_typescript@4.9.3 + client/shared: specifiers: '@sourcegraph/build-config': workspace:* @@ -1108,7 +1155,7 @@ packages: chalk: 4.1.2 fb-watchman: 2.0.0 fbjs: 3.0.0 - glob: 7.1.7 + glob: 7.2.3 graphql: 15.4.0 immutable: 3.7.6 invariant: 2.2.4 @@ -1163,11 +1210,11 @@ packages: puppeteer: 13.7.0 dev: true - /@babel/code-frame/7.12.11: - resolution: {integrity: sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==} + /@babel/code-frame/7.0.0: + resolution: {integrity: sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==} dependencies: '@babel/highlight': 7.18.6 - dev: false + dev: true /@babel/code-frame/7.18.6: resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} @@ -2169,6 +2216,16 @@ packages: '@babel/core': 7.20.5 '@babel/helper-plugin-utils': 7.20.2 + /@babel/plugin-transform-react-constant-elements/7.20.2_@babel+core@7.20.5: + resolution: {integrity: sha512-KS/G8YI8uwMGKErLFOHS/ekhqdHhpEloxs43NecQHVgo2QuQSyJhGIY1fL8UGl9wy5ItVwwoUL4YxVqsplGq2g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + /@babel/plugin-transform-react-display-name/7.18.6_@babel+core@7.20.5: resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==} engines: {node: '>=6.9.0'} @@ -2562,6 +2619,559 @@ packages: '@babel/helper-validator-identifier': 7.19.1 to-fast-properties: 2.0.0 + /@backstage/app-defaults/1.1.0_fvbr2cjomnxtlkfwrrwqyod56q: + resolution: {integrity: sha512-WZKYTfvsFXiVvZt39loo2Q0t6CJ90gfUqtRtajUDcrksDSvbF84I2rTFVbmX6tp5qG6XVR+q6qrMBN4sGFeH5Q==} + peerDependencies: + react: ^16.13.1 || ^17.0.0 + react-dom: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + dependencies: + '@backstage/core-app-api': 1.4.0_4tknkhmdkn726v6rgr7mg246jm + '@backstage/core-components': 0.12.3_fvbr2cjomnxtlkfwrrwqyod56q + '@backstage/core-plugin-api': 1.3.0_sueyyaiqx2a24dbpeicw7rfxlu + '@backstage/plugin-permission-react': 0.4.9_4tknkhmdkn726v6rgr7mg246jm + '@backstage/theme': 0.2.16_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/core': 4.12.4_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/icons': 4.11.3_5glfapqkhejqpattrqvw65m2la + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-router-dom: 6.7.0_biqbaboplfbrettd7655fr4n2y + transitivePeerDependencies: + - '@date-io/core' + - '@types/react' + - encoding + - js-cookie + - react-native + - supports-color + dev: true + + /@backstage/catalog-client/1.3.0: + resolution: {integrity: sha512-lSbaa3Y/siMFpFT71QHtybPQsOZC7/AuLswc3vX4qDGfJuIel6DhkAoRIvJCFXUcQqpN4hOk69I4CnOtJvzftQ==} + dependencies: + '@backstage/catalog-model': 1.1.5 + '@backstage/errors': 1.1.4 + cross-fetch: 3.1.5 + transitivePeerDependencies: + - encoding + dev: true + + /@backstage/catalog-model/1.1.5: + resolution: {integrity: sha512-rE1KFhpLlli0A7marJpbd4MCGWG+5vIRj49XZBbEdhevl7HfCCJMAA6IKyj8U7LJjwRVygppSkc8V2yAyEU/2A==} + dependencies: + '@backstage/config': 1.0.6 + '@backstage/errors': 1.1.4 + '@backstage/types': 1.0.2 + ajv: 8.11.2 + json-schema: 0.4.0 + lodash: 4.17.21 + uuid: 8.3.2 + transitivePeerDependencies: + - encoding + dev: true + + /@backstage/cli-common/0.1.11: + resolution: {integrity: sha512-6gjYi2ndXUBVV6YNbiPJMHoPLROlikZ2nnKJrblnYhWZaKhKncXVxtjfCGPItTFPnIbW0oZu2Ue0Z/1VCyfOaQ==} + dev: true + + /@backstage/cli/0.22.1_veicm43jo6hfcg5mjykszuneye: + resolution: {integrity: sha512-rmw1E108OZLpDh0z1Waa3kP6ohNXjBIPvkFGU1oC2fgDQwxqTKOZGMbIbBG3eG5F0vLe5fMKdisaox8NAk1WBg==} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.21.2 + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + dependencies: + '@backstage/cli-common': 0.1.11 + '@backstage/config': 1.0.6 + '@backstage/config-loader': 1.1.8_@swc+core@1.3.27 + '@backstage/errors': 1.1.4 + '@backstage/release-manifests': 0.0.8 + '@backstage/types': 1.0.2 + '@manypkg/get-packages': 1.1.3 + '@octokit/request': 6.2.1 + '@pmmmwh/react-refresh-webpack-plugin': 0.5.10_e764yt37rymiplqgpb7fiwcihu + '@rollup/plugin-commonjs': 23.0.7_rollup@2.79.1 + '@rollup/plugin-json': 5.0.2_rollup@2.79.1 + '@rollup/plugin-node-resolve': 13.3.0_rollup@2.79.1 + '@rollup/plugin-yaml': 4.0.1_rollup@2.79.1 + '@spotify/eslint-config-base': 14.1.3_eslint@8.18.0 + '@spotify/eslint-config-react': 14.1.3_qzuhtewbpqc65tezqbik3lhy6a + '@spotify/eslint-config-typescript': 14.1.3_nii4tqejuio2ec2bwcqvyw5c3i + '@sucrase/webpack-loader': 2.0.0_sucrase@3.29.0 + '@svgr/plugin-jsx': 6.5.1_@svgr+core@6.5.1 + '@svgr/plugin-svgo': 6.5.1_@svgr+core@6.5.1 + '@svgr/rollup': 6.5.1 + '@svgr/webpack': 6.5.1 + '@swc/core': 1.3.27 + '@swc/helpers': 0.4.14 + '@swc/jest': 0.2.24_@swc+core@1.3.27 + '@types/jest': 29.2.5 + '@types/webpack-env': 1.18.0 + '@typescript-eslint/eslint-plugin': 5.24.0_el7ggvuufxftzwvu6wsrutnxdi + '@typescript-eslint/parser': 5.24.0_4htfruiy2bgjslzgmagy6rfrsq + '@yarnpkg/lockfile': 1.1.0 + '@yarnpkg/parsers': 3.0.0-rc.35 + bfj: 7.0.2 + buffer: 6.0.3 + chalk: 4.1.2 + chokidar: 3.5.3 + commander: 9.4.1 + css-loader: 6.7.2_webpack@5.75.0 + diff: 5.0.0 + esbuild: 0.16.17 + esbuild-loader: 2.21.0_webpack@5.75.0 + eslint: 8.18.0 + eslint-config-prettier: 8.6.0_eslint@8.18.0 + eslint-formatter-friendly: 7.0.0 + eslint-plugin-deprecation: 1.3.3_4htfruiy2bgjslzgmagy6rfrsq + eslint-plugin-import: 2.26.0_ibw5rofldwqdfqodgjfojvmexu + eslint-plugin-jest: 27.2.1_qgasbpcsrurabgy544gmfdiiuu + eslint-plugin-jsx-a11y: 6.5.1_eslint@8.18.0 + eslint-plugin-monorepo: 0.3.2_fsuobzodmui5yhj7deh6fdsp7i + eslint-plugin-react: 7.32.1_eslint@8.18.0 + eslint-plugin-react-hooks: 4.5.0_eslint@8.18.0 + eslint-webpack-plugin: 3.2.0_4cuzlhuyxkclhi6pwpzucithpm + express: 4.18.2 + fork-ts-checker-webpack-plugin: 7.3.0_vfotqvx6lgcbf3upbs6hgaza4q + fs-extra: 10.1.0 + glob: 7.2.3 + global-agent: 3.0.0 + handlebars: 4.7.7 + html-webpack-plugin: 5.5.0_webpack@5.75.0 + inquirer: 8.2.5 + jest: 29.3.1_@types+node@18.8.3 + jest-css-modules: 2.1.0 + jest-environment-jsdom: 29.3.1 + jest-runtime: 29.3.1 + json-schema: 0.4.0 + lodash: 4.17.21 + mini-css-extract-plugin: 2.7.2_webpack@5.75.0 + minimatch: 5.1.6 + node-fetch: 2.6.7 + node-libs-browser: 2.2.1 + npm-packlist: 5.1.3 + ora: 5.4.1 + postcss: 8.4.19 + process: 0.11.10 + react-dev-utils: 12.0.1_7nbrhxx5wbdrea3w4d3cjhku4y + react-refresh: 0.14.0 + recursive-readdir: 2.2.2 + replace-in-file: 6.3.5 + rollup: 2.79.1 + rollup-plugin-dts: 4.2.3_k35zwyycrckt5xfsejji7kbwn4 + rollup-plugin-esbuild: 4.10.3_uiao7appyg7pvh5lt4amcal6cy + rollup-plugin-postcss: 4.0.2_postcss@8.4.19 + rollup-pluginutils: 2.8.2 + run-script-webpack-plugin: 0.1.1 + semver: 7.3.8 + style-loader: 3.3.1_webpack@5.75.0 + sucrase: 3.29.0 + swc-loader: 0.2.3_jftlsdtjtdse3p3ijno5hy54ky + tar: 6.1.13 + terser-webpack-plugin: 5.3.6_v7bbwjudcxv2o5c5io5ciwfori + util: 0.12.5 + webpack: 5.75.0_cw4su4nzareykaft5p7arksy5i + webpack-dev-server: 4.11.1_webpack@5.75.0 + webpack-node-externals: 3.0.0 + yaml: 2.2.1 + yml-loader: 2.1.0 + yn: 4.0.0 + zod: 3.18.0 + transitivePeerDependencies: + - '@svgr/core' + - '@swc/wasm' + - '@types/node' + - '@types/webpack' + - bufferutil + - canvas + - debug + - encoding + - eslint-import-resolver-node + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - node-notifier + - sockjs-client + - supports-color + - ts-node + - type-fest + - typescript + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + - webpack-hot-middleware + - webpack-plugin-serve + dev: true + + /@backstage/config-loader/1.1.8_@swc+core@1.3.27: + resolution: {integrity: sha512-S6B6OWaojJf9vWi/wVV5Pr8zXc1meSWTgjaXX64pDzdDGDlzL+wARloObJvDvqCJyLolkQuW2TNZYZKLRc6Iqg==} + dependencies: + '@backstage/cli-common': 0.1.11 + '@backstage/config': 1.0.6 + '@backstage/errors': 1.1.4 + '@backstage/types': 1.0.2 + '@types/json-schema': 7.0.11 + ajv: 8.11.2 + chokidar: 3.5.3 + fs-extra: 10.1.0 + json-schema: 0.4.0 + json-schema-merge-allof: 0.8.1 + json-schema-traverse: 1.0.0 + node-fetch: 2.6.7 + typescript-json-schema: 0.55.0_@swc+core@1.3.27 + yaml: 2.2.1 + yup: 0.32.11 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - encoding + dev: true + + /@backstage/config/1.0.6: + resolution: {integrity: sha512-ZN3ABydLRZNTtL9FXPpvw678CJ/G2UtGHrX/Cq6Tfd9QJ6/wjMTagTe/KibxGh6lxIG+VGU+BOAqt6mHgMDopA==} + dependencies: + '@backstage/types': 1.0.2 + lodash: 4.17.21 + + /@backstage/core-app-api/1.4.0_4tknkhmdkn726v6rgr7mg246jm: + resolution: {integrity: sha512-qPicwwUYP3V0dW6/U9ChNaJWdK2AaQYg0bcGrIpJkHCQbEr6TM+TuEyiwub77OmsXp1uXF+4Stxacl5TCWVNFA==} + peerDependencies: + '@types/react': ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + dependencies: + '@backstage/config': 1.0.6 + '@backstage/core-plugin-api': 1.3.0_sueyyaiqx2a24dbpeicw7rfxlu + '@backstage/types': 1.0.2 + '@backstage/version-bridge': 1.0.3_vqvt52xx3h2ersphfxson777fi + '@types/prop-types': 15.7.5 + '@types/react': 17.0.43 + prop-types: 15.8.1 + react: 18.2.0 + react-router-dom: 6.7.0_biqbaboplfbrettd7655fr4n2y + react-use: 17.4.0_biqbaboplfbrettd7655fr4n2y + zen-observable: 0.10.0 + zod: 3.18.0 + transitivePeerDependencies: + - react-dom + dev: true + + /@backstage/core-components/0.12.3_fvbr2cjomnxtlkfwrrwqyod56q: + resolution: {integrity: sha512-R+tmEfxLYTblvURn50G43VCm69QTA381cvtn72S0PAx1XlRgAj/JKJRuT240IgKQXLsumiAbn1yRy2Pcjojsyw==} + peerDependencies: + '@types/react': ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-dom: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + dependencies: + '@backstage/config': 1.0.6 + '@backstage/core-plugin-api': 1.3.0_sueyyaiqx2a24dbpeicw7rfxlu + '@backstage/errors': 1.1.4 + '@backstage/theme': 0.2.16_7ir5hbqdbfw4we5ecpxz77f25m + '@backstage/version-bridge': 1.0.3_vqvt52xx3h2ersphfxson777fi + '@material-table/core': 3.2.5_zgbk4qsvidizqjnfqfynvk3e4e + '@material-ui/core': 4.12.4_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/icons': 4.11.3_5glfapqkhejqpattrqvw65m2la + '@material-ui/lab': 4.0.0-alpha.57_5glfapqkhejqpattrqvw65m2la + '@react-hookz/web': 20.1.0_biqbaboplfbrettd7655fr4n2y + '@types/react': 17.0.43 + '@types/react-sparklines': 1.7.2 + '@types/react-text-truncate': 0.14.1 + ansi-regex: 6.0.1 + classnames: 2.3.1 + d3-selection: 3.0.0 + d3-shape: 3.0.1 + d3-zoom: 3.0.0 + dagre: 0.8.5 + history: 4.5.1 + immer: 9.0.18 + lodash: 4.17.21 + pluralize: 8.0.0 + prop-types: 15.8.1 + qs: 6.11.0 + rc-progress: 3.4.1_biqbaboplfbrettd7655fr4n2y + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-helmet: 6.1.0_react@18.2.0 + react-hook-form: 7.42.1_react@18.2.0 + react-markdown: 8.0.5_vqvt52xx3h2ersphfxson777fi + react-router-dom: 6.7.0_biqbaboplfbrettd7655fr4n2y + react-sparklines: 1.7.0_biqbaboplfbrettd7655fr4n2y + react-syntax-highlighter: 15.5.0_react@18.2.0 + react-text-truncate: 0.19.0_biqbaboplfbrettd7655fr4n2y + react-use: 17.4.0_biqbaboplfbrettd7655fr4n2y + react-virtualized-auto-sizer: 1.0.7_biqbaboplfbrettd7655fr4n2y + react-window: 1.8.8_biqbaboplfbrettd7655fr4n2y + remark-gfm: 3.0.1 + zen-observable: 0.10.0 + zod: 3.18.0 + transitivePeerDependencies: + - '@date-io/core' + - encoding + - js-cookie + - react-native + - supports-color + + /@backstage/core-plugin-api/1.3.0_sueyyaiqx2a24dbpeicw7rfxlu: + resolution: {integrity: sha512-A7powQ0vVyMU2HWyp+hP0vJWjsaOy6KE4OjrAN6m5bakb90DzCYfcZsZbnbdioTUtBaolFLIiN+iX4UpJdhpkA==} + peerDependencies: + '@types/react': ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + dependencies: + '@backstage/config': 1.0.6 + '@backstage/types': 1.0.2 + '@backstage/version-bridge': 1.0.3_vqvt52xx3h2ersphfxson777fi + '@types/react': 17.0.43 + history: 4.5.1 + prop-types: 15.8.1 + react: 18.2.0 + react-router-dom: 6.7.0_biqbaboplfbrettd7655fr4n2y + zen-observable: 0.10.0 + + /@backstage/dev-utils/1.0.11_asenqjccrkesmsfwbq3lggkll4: + resolution: {integrity: sha512-dkQRgpAQeeQTUeMtDtXtmsLam2KtxlQkMeclQGKZ6rStqAvWW+aJhgY0KYyDVFCKFOHtsJJZcdrZ9A5upUXgQQ==} + peerDependencies: + '@types/react': ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-dom: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + dependencies: + '@backstage/app-defaults': 1.1.0_fvbr2cjomnxtlkfwrrwqyod56q + '@backstage/catalog-model': 1.1.5 + '@backstage/core-app-api': 1.4.0_4tknkhmdkn726v6rgr7mg246jm + '@backstage/core-components': 0.12.3_fvbr2cjomnxtlkfwrrwqyod56q + '@backstage/core-plugin-api': 1.3.0_sueyyaiqx2a24dbpeicw7rfxlu + '@backstage/integration-react': 1.1.9_fvbr2cjomnxtlkfwrrwqyod56q + '@backstage/plugin-catalog-react': 1.2.4_fvbr2cjomnxtlkfwrrwqyod56q + '@backstage/test-utils': 1.2.4_e442r2zblqvic2logccjohmmdi + '@backstage/theme': 0.2.16_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/core': 4.12.4_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/icons': 4.11.3_5glfapqkhejqpattrqvw65m2la + '@testing-library/jest-dom': 5.16.4 + '@testing-library/react': 12.1.5_biqbaboplfbrettd7655fr4n2y + '@testing-library/user-event': 14.4.3_tlwynutqiyp5mns3woioasuxnq + '@types/react': 17.0.43 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-router-dom: 6.7.0_biqbaboplfbrettd7655fr4n2y + react-use: 17.4.0_biqbaboplfbrettd7655fr4n2y + zen-observable: 0.10.0 + transitivePeerDependencies: + - '@date-io/core' + - '@testing-library/dom' + - encoding + - js-cookie + - react-native + - supports-color + dev: true + + /@backstage/errors/1.1.4: + resolution: {integrity: sha512-u0q0/UlG+WM4h67Owfpvc/yN8T1ivFOLDzzmogaFSwC5+R6sZRqYasmjeURtkJvw7aG9RpXYWY7CofCSg1E20Q==} + dependencies: + '@backstage/types': 1.0.2 + cross-fetch: 3.1.5 + serialize-error: 8.1.0 + transitivePeerDependencies: + - encoding + + /@backstage/integration-react/1.1.9_fvbr2cjomnxtlkfwrrwqyod56q: + resolution: {integrity: sha512-fhxEgnZbLHxvu3TAwi7L+c9hw/dx1H9HAqi/MTcOvqjDRVnMN/An0OfRzUCksJPs3HeniRZM7GzcuE215rb1nQ==} + peerDependencies: + react: ^16.13.1 || ^17.0.0 + dependencies: + '@backstage/config': 1.0.6 + '@backstage/core-components': 0.12.3_fvbr2cjomnxtlkfwrrwqyod56q + '@backstage/core-plugin-api': 1.3.0_sueyyaiqx2a24dbpeicw7rfxlu + '@backstage/integration': 1.4.2 + '@backstage/theme': 0.2.16_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/core': 4.12.4_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/icons': 4.11.3_5glfapqkhejqpattrqvw65m2la + '@material-ui/lab': 4.0.0-alpha.57_5glfapqkhejqpattrqvw65m2la + react: 18.2.0 + react-use: 17.4.0_biqbaboplfbrettd7655fr4n2y + transitivePeerDependencies: + - '@date-io/core' + - '@types/react' + - encoding + - js-cookie + - react-dom + - react-native + - react-router-dom + - supports-color + dev: true + + /@backstage/integration/1.4.2: + resolution: {integrity: sha512-vWclxqDvOYDPPBXOiaN5HTcGlWR/Mdk8etZu4u24DLlvqmKRVOG3UFajf1VoNcEZqtkN08QsfbhoiQHE4mmHxg==} + dependencies: + '@backstage/config': 1.0.6 + '@backstage/errors': 1.1.4 + '@octokit/auth-app': 4.0.6 + '@octokit/rest': 19.0.5 + cross-fetch: 3.1.5 + git-url-parse: 13.1.0 + lodash: 4.17.21 + luxon: 3.2.1 + transitivePeerDependencies: + - encoding + dev: true + + /@backstage/plugin-catalog-common/1.0.10: + resolution: {integrity: sha512-EFxOGd++iO8Nl2LmY7uxCbaLKrglKR0LMmz35n1u2wNRbS0dE92MHJCrKjnLbgRx7JBc6LbUE9tMF24AmDxqtw==} + dependencies: + '@backstage/catalog-model': 1.1.5 + '@backstage/plugin-permission-common': 0.7.3 + '@backstage/plugin-search-common': 1.2.1 + transitivePeerDependencies: + - encoding + dev: true + + /@backstage/plugin-catalog-react/1.2.4_fvbr2cjomnxtlkfwrrwqyod56q: + resolution: {integrity: sha512-u26FK7JvQXmhlpTYTxoHf969By/kmHRWv7W2wV+osJYGuRX5ckX1Bpf7dY+WodOMCScksNHgz1gyHbkLd1iXeQ==} + peerDependencies: + '@types/react': ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + dependencies: + '@backstage/catalog-client': 1.3.0 + '@backstage/catalog-model': 1.1.5 + '@backstage/core-components': 0.12.3_fvbr2cjomnxtlkfwrrwqyod56q + '@backstage/core-plugin-api': 1.3.0_sueyyaiqx2a24dbpeicw7rfxlu + '@backstage/errors': 1.1.4 + '@backstage/integration': 1.4.2 + '@backstage/plugin-catalog-common': 1.0.10 + '@backstage/plugin-permission-common': 0.7.3 + '@backstage/plugin-permission-react': 0.4.9_4tknkhmdkn726v6rgr7mg246jm + '@backstage/theme': 0.2.16_7ir5hbqdbfw4we5ecpxz77f25m + '@backstage/types': 1.0.2 + '@backstage/version-bridge': 1.0.3_vqvt52xx3h2ersphfxson777fi + '@material-ui/core': 4.12.4_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/icons': 4.11.3_5glfapqkhejqpattrqvw65m2la + '@material-ui/lab': 4.0.0-alpha.57_5glfapqkhejqpattrqvw65m2la + '@types/react': 17.0.43 + classnames: 2.3.1 + jwt-decode: 3.1.2 + lodash: 4.17.21 + material-ui-popup-state: 1.9.3_fvaebydoztlmlbmlu5u4butffa + qs: 6.11.0 + react: 18.2.0 + react-router-dom: 6.7.0_biqbaboplfbrettd7655fr4n2y + react-use: 17.4.0_biqbaboplfbrettd7655fr4n2y + yaml: 2.2.1 + zen-observable: 0.10.0 + transitivePeerDependencies: + - '@date-io/core' + - encoding + - js-cookie + - react-dom + - react-native + - supports-color + dev: true + + /@backstage/plugin-permission-common/0.7.3: + resolution: {integrity: sha512-27I9X/kj3xBe6Hg4wynoEzDYLa5jpC7PNh8cGok6RLdkMM4H2aXo8yEMOArxp+JYPhHzNW07ZhjkBF0PcEpAEQ==} + dependencies: + '@backstage/config': 1.0.6 + '@backstage/errors': 1.1.4 + '@backstage/types': 1.0.2 + cross-fetch: 3.1.5 + uuid: 8.3.2 + zod: 3.18.0 + transitivePeerDependencies: + - encoding + dev: true + + /@backstage/plugin-permission-react/0.4.9_4tknkhmdkn726v6rgr7mg246jm: + resolution: {integrity: sha512-zsmYs8tt1BThfYNIawcvXFZQzMXrw75JWwYbUYY+Af47SRBckKBON4mkQ4qFG0VsSGH4gr4cmoA0e3Uj8MtGiA==} + peerDependencies: + '@types/react': ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + dependencies: + '@backstage/config': 1.0.6 + '@backstage/core-plugin-api': 1.3.0_sueyyaiqx2a24dbpeicw7rfxlu + '@backstage/plugin-permission-common': 0.7.3 + '@types/react': 17.0.43 + cross-fetch: 3.1.5 + react: 18.2.0 + react-router-dom: 6.7.0_biqbaboplfbrettd7655fr4n2y + react-use: 17.4.0_biqbaboplfbrettd7655fr4n2y + swr: 2.0.0_react@18.2.0 + transitivePeerDependencies: + - encoding + - react-dom + dev: true + + /@backstage/plugin-search-common/1.2.1: + resolution: {integrity: sha512-ek8QPVcONoy6pp+jSG9BXGAJKHioRtjutyxWlpxynBZzan1SShBoZ0suNd8EwydTnf5Qz+0Mn8keTkbdxFMiJA==} + dependencies: + '@backstage/plugin-permission-common': 0.7.3 + '@backstage/types': 1.0.2 + transitivePeerDependencies: + - encoding + dev: true + + /@backstage/release-manifests/0.0.8: + resolution: {integrity: sha512-LUOCzV5Xm3+BEwE2ZZkqvKeTNwBed1wZYOW5c2XuyNGKznqKUyWX8LDIFvEqsGSKdEyIWWLqqyBrnept9upgxg==} + dependencies: + cross-fetch: 3.1.5 + transitivePeerDependencies: + - encoding + dev: true + + /@backstage/test-utils/1.2.4_e442r2zblqvic2logccjohmmdi: + resolution: {integrity: sha512-osv0NmIBKUna4YKU9ea26Y3twatgN/TXha/dhtjwYUZdUscjzc7vMAE4V8vfB5WYoQJO8fnnZxWO20FPCP2fSQ==} + peerDependencies: + '@types/react': ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-dom: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + dependencies: + '@backstage/config': 1.0.6 + '@backstage/core-app-api': 1.4.0_4tknkhmdkn726v6rgr7mg246jm + '@backstage/core-plugin-api': 1.3.0_sueyyaiqx2a24dbpeicw7rfxlu + '@backstage/plugin-permission-common': 0.7.3 + '@backstage/plugin-permission-react': 0.4.9_4tknkhmdkn726v6rgr7mg246jm + '@backstage/theme': 0.2.16_7ir5hbqdbfw4we5ecpxz77f25m + '@backstage/types': 1.0.2 + '@material-ui/core': 4.12.4_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/icons': 4.11.3_5glfapqkhejqpattrqvw65m2la + '@testing-library/jest-dom': 5.16.4 + '@testing-library/react': 12.1.5_biqbaboplfbrettd7655fr4n2y + '@testing-library/user-event': 14.4.3_tlwynutqiyp5mns3woioasuxnq + '@types/react': 17.0.43 + cross-fetch: 3.1.5 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-router-dom: 6.7.0_biqbaboplfbrettd7655fr4n2y + zen-observable: 0.10.0 + transitivePeerDependencies: + - '@testing-library/dom' + - encoding + dev: true + + /@backstage/theme/0.2.16_7ir5hbqdbfw4we5ecpxz77f25m: + resolution: {integrity: sha512-UDVqQhPunL3uDrhhKP72HlvEoG3rv2dspPxCEGqAAICBjXdLGh7CZ2qGlwdBxDjvCZ/tJcg9GNfpa6SOzdMJmA==} + dependencies: + '@material-ui/core': 4.12.4_7ir5hbqdbfw4we5ecpxz77f25m + transitivePeerDependencies: + - '@types/react' + - react + - react-dom + + /@backstage/types/1.0.2: + resolution: {integrity: sha512-wE4AAP3je00UlVNV5faIto414aOUNv30CmvNmxgImNKelPRYJsMEicM9slwkrNMyFLqTMITeXJvQvMofUk3Wxg==} + + /@backstage/version-bridge/1.0.3_vqvt52xx3h2ersphfxson777fi: + resolution: {integrity: sha512-b+r0LKjciyVgrw/nrEEqY/zxMwT4GzGjoQUY3YgmtNuUeSheVHf2uwq++nsSfy3ZXZwvyTX0uR3c2YDB3q/Sig==} + peerDependencies: + '@types/react': ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + dependencies: + '@types/react': 17.0.43 + react: 18.2.0 + /@base2/pretty-print-object/1.0.1: resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==} dev: true @@ -2587,6 +3197,10 @@ packages: deprecated: Potential XSS vulnerability patched in v6.0.0. dev: true + /@changesets/types/4.1.0: + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + dev: true + /@cnakazawa/watch/1.0.3: resolution: {integrity: sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA==} engines: {node: '>=0.1.95'} @@ -2728,208 +3342,205 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true - /@discoveryjs/json-ext/0.5.7: - resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} - engines: {node: '>=10.0.0'} + /@date-io/core/1.3.13: + resolution: {integrity: sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==} + + /@date-io/date-fns/1.3.13_date-fns@2.16.1: + resolution: {integrity: sha512-yXxGzcRUPcogiMj58wVgFjc9qUYrCnnU9eLcyNbsQCmae4jPuZCDoIBR21j8ZURsM7GRtU62VOw5yNd4dDHunA==} + peerDependencies: + date-fns: ^2.0.0 + dependencies: + '@date-io/core': 1.3.13 + date-fns: 2.16.1 + + /@discoveryjs/json-ext/0.5.7: + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} /@discoveryjs/natural-compare/1.1.0: resolution: {integrity: sha512-yuctPJs5lRXoI8LkpVZGAV6n+DKOuEsfpfcIDQ8ZjWHwazqk1QjBc4jMlof0UlZHyUqv4dwsOTooMiAmtzvwXA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} dev: true - /@esbuild/android-arm/0.16.10: - resolution: {integrity: sha512-RmJjQTRrO6VwUWDrzTBLmV4OJZTarYsiepLGlF2rYTVB701hSorPywPGvP6d8HCuuRibyXa5JX4s3jN2kHEtjQ==} + /@emotion/hash/0.8.0: + resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} + + /@esbuild/android-arm/0.16.17: + resolution: {integrity: sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==} engines: {node: '>=12'} cpu: [arm] os: [android] requiresBuild: true optional: true - /@esbuild/android-arm64/0.16.10: - resolution: {integrity: sha512-47Y+NwVKTldTlDhSgJHZ/RpvBQMUDG7eKihqaF/u6g7s0ZPz4J1vy8A3rwnnUOF2CuDn7w7Gj/QcMoWz3U3SJw==} + /@esbuild/android-arm64/0.16.17: + resolution: {integrity: sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==} engines: {node: '>=12'} cpu: [arm64] os: [android] requiresBuild: true optional: true - /@esbuild/android-x64/0.16.10: - resolution: {integrity: sha512-C4PfnrBMcuAcOurQzpF1tTtZz94IXO5JmICJJ3NFJRHbXXsQUg9RFG45KvydKqtFfBaFLCHpduUkUfXwIvGnRg==} + /@esbuild/android-x64/0.16.17: + resolution: {integrity: sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==} engines: {node: '>=12'} cpu: [x64] os: [android] requiresBuild: true optional: true - /@esbuild/darwin-arm64/0.16.10: - resolution: {integrity: sha512-bH/bpFwldyOKdi9HSLCLhhKeVgRYr9KblchwXgY2NeUHBB/BzTUHtUSBgGBmpydB1/4E37m+ggXXfSrnD7/E7g==} + /@esbuild/darwin-arm64/0.16.17: + resolution: {integrity: sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] requiresBuild: true optional: true - /@esbuild/darwin-x64/0.16.10: - resolution: {integrity: sha512-OXt7ijoLuy+AjDSKQWu+KdDFMBbdeaL6wtgMKtDUXKWHiAMKHan5+R1QAG6HD4+K0nnOvEJXKHeA9QhXNAjOTQ==} + /@esbuild/darwin-x64/0.16.17: + resolution: {integrity: sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==} engines: {node: '>=12'} cpu: [x64] os: [darwin] requiresBuild: true optional: true - /@esbuild/freebsd-arm64/0.16.10: - resolution: {integrity: sha512-shSQX/3GHuspE3Uxtq5kcFG/zqC+VuMnJkqV7LczO41cIe6CQaXHD3QdMLA4ziRq/m0vZo7JdterlgbmgNIAlQ==} + /@esbuild/freebsd-arm64/0.16.17: + resolution: {integrity: sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] requiresBuild: true optional: true - /@esbuild/freebsd-x64/0.16.10: - resolution: {integrity: sha512-5YVc1zdeaJGASijZmTzSO4h6uKzsQGG3pkjI6fuXvolhm3hVRhZwnHJkforaZLmzvNv5Tb7a3QL2FAVmrgySIA==} + /@esbuild/freebsd-x64/0.16.17: + resolution: {integrity: sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] requiresBuild: true optional: true - /@esbuild/linux-arm/0.16.10: - resolution: {integrity: sha512-c360287ZWI2miBnvIj23bPyVctgzeMT2kQKR+x94pVqIN44h3GF8VMEs1SFPH1UgyDr3yBbx3vowDS1SVhyVhA==} + /@esbuild/linux-arm/0.16.17: + resolution: {integrity: sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==} engines: {node: '>=12'} cpu: [arm] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-arm64/0.16.10: - resolution: {integrity: sha512-2aqeNVxIaRfPcIaMZIFoblLh588sWyCbmj1HHCCs9WmeNWm+EIN0SmvsmPvTa/TsNZFKnxTcvkX2eszTcCqIrA==} + /@esbuild/linux-arm64/0.16.17: + resolution: {integrity: sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==} engines: {node: '>=12'} cpu: [arm64] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-ia32/0.16.10: - resolution: {integrity: sha512-sqMIEWeyrLGU7J5RB5fTkLRIFwsgsQ7ieWXlDLEmC2HblPYGb3AucD7inw2OrKFpRPKsec1l+lssiM3+NV5aOw==} + /@esbuild/linux-ia32/0.16.17: + resolution: {integrity: sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-loong64/0.16.10: - resolution: {integrity: sha512-O7Pd5hLEtTg37NC73pfhUOGTjx/+aXu5YoSq3ahCxcN7Bcr2F47mv+kG5t840thnsEzrv0oB70+LJu3gUgchvg==} + /@esbuild/linux-loong64/0.16.17: + resolution: {integrity: sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==} engines: {node: '>=12'} cpu: [loong64] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-mips64el/0.16.10: - resolution: {integrity: sha512-FN8mZOH7531iPHM0kaFhAOqqNHoAb6r/YHW2ZIxNi0a85UBi2DO4Vuyn7t1p4UN8a4LoAnLOT1PqNgHkgBJgbA==} + /@esbuild/linux-mips64el/0.16.17: + resolution: {integrity: sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-ppc64/0.16.10: - resolution: {integrity: sha512-Dg9RiqdvHOAWnOKIOTsIx8dFX9EDlY2IbPEY7YFzchrCiTZmMkD7jWA9UdZbNUygPjdmQBVPRCrLydReFlX9yg==} + /@esbuild/linux-ppc64/0.16.17: + resolution: {integrity: sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-riscv64/0.16.10: - resolution: {integrity: sha512-XMqtpjwzbmlar0BJIxmzu/RZ7EWlfVfH68Vadrva0Wj5UKOdKvqskuev2jY2oPV3aoQUyXwnMbMrFmloO2GfAw==} + /@esbuild/linux-riscv64/0.16.17: + resolution: {integrity: sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-s390x/0.16.10: - resolution: {integrity: sha512-fu7XtnoeRNFMx8DjK3gPWpFBDM2u5ba+FYwg27SjMJwKvJr4bDyKz5c+FLXLUSSAkMAt/UL+cUbEbra+rYtUgw==} + /@esbuild/linux-s390x/0.16.17: + resolution: {integrity: sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==} engines: {node: '>=12'} cpu: [s390x] os: [linux] requiresBuild: true optional: true - /@esbuild/linux-x64/0.16.10: - resolution: {integrity: sha512-61lcjVC/RldNNMUzQQdyCWjCxp9YLEQgIxErxU9XluX7juBdGKb0pvddS0vPNuCvotRbzijZ1pzII+26haWzbA==} + /@esbuild/linux-x64/0.16.17: + resolution: {integrity: sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==} engines: {node: '>=12'} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@esbuild/netbsd-x64/0.16.10: - resolution: {integrity: sha512-JeZXCX3viSA9j4HqSoygjssdqYdfHd6yCFWyfSekLbz4Ef+D2EjvsN02ZQPwYl5a5gg/ehdHgegHhlfOFP0HCA==} + /@esbuild/netbsd-x64/0.16.17: + resolution: {integrity: sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] requiresBuild: true optional: true - /@esbuild/openbsd-x64/0.16.10: - resolution: {integrity: sha512-3qpxQKuEVIIg8SebpXsp82OBrqjPV/OwNWmG+TnZDr3VGyChNnGMHccC1xkbxCHDQNnnXjxhMQNyHmdFJbmbRA==} + /@esbuild/openbsd-x64/0.16.17: + resolution: {integrity: sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] requiresBuild: true optional: true - /@esbuild/sunos-x64/0.16.10: - resolution: {integrity: sha512-z+q0xZ+et/7etz7WoMyXTHZ1rB8PMSNp/FOqURLJLOPb3GWJ2aj4oCqFCjPwEbW1rsT7JPpxeH/DwGAWk/I1Bg==} + /@esbuild/sunos-x64/0.16.17: + resolution: {integrity: sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==} engines: {node: '>=12'} cpu: [x64] os: [sunos] requiresBuild: true optional: true - /@esbuild/win32-arm64/0.16.10: - resolution: {integrity: sha512-+YYu5sbQ9npkNT9Dec+tn1F/kjg6SMgr6bfi/6FpXYZvCRfu2YFPZGb+3x8K30s8eRxFpoG4sGhiSUkr1xbHEw==} + /@esbuild/win32-arm64/0.16.17: + resolution: {integrity: sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==} engines: {node: '>=12'} cpu: [arm64] os: [win32] requiresBuild: true optional: true - /@esbuild/win32-ia32/0.16.10: - resolution: {integrity: sha512-Aw7Fupk7XNehR1ftHGYwUteyJ2q+em/aE+fVU3YMTBN2V5A7Z4aVCSV+SvCp9HIIHZavPFBpbdP3VfjQpdf6Xg==} + /@esbuild/win32-ia32/0.16.17: + resolution: {integrity: sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==} engines: {node: '>=12'} cpu: [ia32] os: [win32] requiresBuild: true optional: true - /@esbuild/win32-x64/0.16.10: - resolution: {integrity: sha512-qddWullt3sC1EIpfHvCRBq3H4g3L86DZpD6n8k2XFjFVyp01D++uNbN1hT/JRsHxTbyyemZcpwL5aRlJwc/zFw==} + /@esbuild/win32-x64/0.16.17: + resolution: {integrity: sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==} engines: {node: '>=12'} cpu: [x64] os: [win32] requiresBuild: true optional: true - /@eslint/eslintrc/0.4.3: - resolution: {integrity: sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 7.3.1 - globals: 13.16.0 - ignore: 4.0.6 - import-fresh: 3.3.0 - js-yaml: 3.14.1 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: false - /@eslint/eslintrc/1.3.0: resolution: {integrity: sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3384,7 +3995,7 @@ packages: '@graphql-tools/utils': 9.1.3_graphql@15.4.0 graphql: 15.4.0 is-glob: 4.0.3 - micromatch: 4.0.4 + micromatch: 4.0.5 tslib: 2.1.0 unixify: 1.0.0 transitivePeerDependencies: @@ -3674,17 +4285,6 @@ packages: '@hapi/hoek': 9.3.0 dev: false - /@humanwhocodes/config-array/0.5.0: - resolution: {integrity: sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: false - /@humanwhocodes/config-array/0.9.5: resolution: {integrity: sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==} engines: {node: '>=10.10.0'} @@ -3743,6 +4343,18 @@ packages: slash: 3.0.0 dev: true + /@jest/console/29.3.1: + resolution: {integrity: sha512-IRE6GD47KwcqA09RIWrabKdHPiKDGgtAL31xDxbi/RjQMsr+lY+ppxmHwY0dUEV3qvvxZzoe5Hl0RXZJOjQNUg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.3.1 + '@types/node': 18.8.3 + chalk: 4.1.2 + jest-message-util: 29.3.1 + jest-util: 29.3.1 + slash: 3.0.0 + dev: true + /@jest/core/28.1.3_ts-node@10.9.1: resolution: {integrity: sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -3776,7 +4388,7 @@ packages: jest-util: 28.1.3 jest-validate: 28.1.3 jest-watcher: 28.1.3 - micromatch: 4.0.4 + micromatch: 4.0.5 pretty-format: 28.1.3 rimraf: 3.0.2 slash: 3.0.0 @@ -3786,12 +4398,53 @@ packages: - ts-node dev: true + /@jest/core/29.3.1: + resolution: {integrity: sha512-0ohVjjRex985w5MmO5L3u5GR1O30DexhBSpuwx2P+9ftyqHdJXnk7IUWiP80oHMvt7ubHCJHxV0a0vlKVuZirw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.3.1 + '@jest/reporters': 29.3.1 + '@jest/test-result': 29.3.1 + '@jest/transform': 29.3.1 + '@jest/types': 29.3.1 + '@types/node': 18.8.3 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.3.1 + exit: 0.1.2 + graceful-fs: 4.2.10 + jest-changed-files: 29.2.0 + jest-config: 29.3.1_@types+node@18.8.3 + jest-haste-map: 29.3.1 + jest-message-util: 29.3.1 + jest-regex-util: 29.2.0 + jest-resolve: 29.3.1 + jest-resolve-dependencies: 29.3.1 + jest-runner: 29.3.1 + jest-runtime: 29.3.1 + jest-snapshot: 29.3.1 + jest-util: 29.3.1 + jest-validate: 29.3.1 + jest-watcher: 29.3.1 + micromatch: 4.0.5 + pretty-format: 29.3.1 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + /@jest/create-cache-key-function/27.5.1: resolution: {integrity: sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - dev: false /@jest/environment/28.1.3: resolution: {integrity: sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==} @@ -3803,6 +4456,16 @@ packages: jest-mock: 28.1.3 dev: true + /@jest/environment/29.3.1: + resolution: {integrity: sha512-pMmvfOPmoa1c1QpfFW0nXYtNLpofqo4BrCIk6f2kW4JFeNlHV2t3vd+3iDLf31e2ot2Mec0uqZfmI+U0K2CFag==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.3.1 + '@jest/types': 29.3.1 + '@types/node': 18.8.3 + jest-mock: 29.3.1 + dev: true + /@jest/expect-utils/28.1.3: resolution: {integrity: sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -3810,6 +4473,13 @@ packages: jest-get-type: 28.0.2 dev: true + /@jest/expect-utils/29.3.1: + resolution: {integrity: sha512-wlrznINZI5sMjwvUoLVk617ll/UYfGIZNxmbU+Pa7wmkL4vYzhV9R2pwVqUh4NWWuLQWkI8+8mOkxs//prKQ3g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.2.0 + dev: true + /@jest/expect/28.1.3: resolution: {integrity: sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -3820,6 +4490,16 @@ packages: - supports-color dev: true + /@jest/expect/29.3.1: + resolution: {integrity: sha512-QivM7GlSHSsIAWzgfyP8dgeExPRZ9BIe2LsdPyEhCGkZkoyA+kGsoIzbKAfZCvvRzfZioKwPtCZIt5SaoxYCvg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.3.1 + jest-snapshot: 29.3.1 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/fake-timers/28.1.3: resolution: {integrity: sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -3832,6 +4512,18 @@ packages: jest-util: 28.1.3 dev: true + /@jest/fake-timers/29.3.1: + resolution: {integrity: sha512-iHTL/XpnDlFki9Tq0Q1GGuVeQ8BHZGIYsvCO5eN/O/oJaRzofG9Xndd9HuSDBI/0ZS79pg0iwn07OMTQ7ngF2A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.3.1 + '@sinonjs/fake-timers': 9.1.2 + '@types/node': 18.8.3 + jest-message-util: 29.3.1 + jest-mock: 29.3.1 + jest-util: 29.3.1 + dev: true + /@jest/globals/28.1.3: resolution: {integrity: sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -3843,6 +4535,18 @@ packages: - supports-color dev: true + /@jest/globals/29.3.1: + resolution: {integrity: sha512-cTicd134vOcwO59OPaB6AmdHQMCtWOe+/DitpTZVxWgMJ+YvXL1HNAmPyiGbSHmF/mXVBkvlm8YYtQhyHPnV6Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.3.1 + '@jest/expect': 29.3.1 + '@jest/types': 29.3.1 + jest-mock: 29.3.1 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/reporters/28.1.3: resolution: {integrity: sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -3862,7 +4566,7 @@ packages: chalk: 4.1.2 collect-v8-coverage: 1.0.0 exit: 0.1.2 - glob: 7.1.7 + glob: 7.2.3 graceful-fs: 4.2.10 istanbul-lib-coverage: 3.2.0 istanbul-lib-instrument: 5.1.0 @@ -3881,6 +4585,43 @@ packages: - supports-color dev: true + /@jest/reporters/29.3.1: + resolution: {integrity: sha512-GhBu3YFuDrcAYW/UESz1JphEAbvUjaY2vShRZRoRY1mxpCMB3yGSJ4j9n0GxVlEOdCf7qjvUfBCrTUUqhVfbRA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.3.1 + '@jest/test-result': 29.3.1 + '@jest/transform': 29.3.1 + '@jest/types': 29.3.1 + '@jridgewell/trace-mapping': 0.3.17 + '@types/node': 18.8.3 + chalk: 4.1.2 + collect-v8-coverage: 1.0.0 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.10 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-instrument: 5.1.0 + istanbul-lib-report: 3.0.0 + istanbul-lib-source-maps: 4.0.0 + istanbul-reports: 3.1.4 + jest-message-util: 29.3.1 + jest-util: 29.3.1 + jest-worker: 29.3.1 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.0.1 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/schemas/28.1.3: resolution: {integrity: sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -3904,6 +4645,15 @@ packages: graceful-fs: 4.2.10 dev: true + /@jest/source-map/29.2.0: + resolution: {integrity: sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.17 + callsites: 3.1.0 + graceful-fs: 4.2.10 + dev: true + /@jest/test-result/28.1.3: resolution: {integrity: sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -3914,6 +4664,16 @@ packages: collect-v8-coverage: 1.0.0 dev: true + /@jest/test-result/29.3.1: + resolution: {integrity: sha512-qeLa6qc0ddB0kuOZyZIhfN5q0e2htngokyTWsGriedsDhItisW7SDYZ7ceOe57Ii03sL988/03wAcBh3TChMGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.3.1 + '@jest/types': 29.3.1 + '@types/istanbul-lib-coverage': 2.0.1 + collect-v8-coverage: 1.0.0 + dev: true + /@jest/test-sequencer/28.1.3: resolution: {integrity: sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -3924,6 +4684,16 @@ packages: slash: 3.0.0 dev: true + /@jest/test-sequencer/29.3.1: + resolution: {integrity: sha512-IqYvLbieTv20ArgKoAMyhLHNrVHJfzO6ARZAbQRlY4UGWfdDnLlZEF0BvKOMd77uIiIjSZRwq3Jb3Fa3I8+2UA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.3.1 + graceful-fs: 4.2.10 + jest-haste-map: 29.3.1 + slash: 3.0.0 + dev: true + /@jest/transform/26.6.2: resolution: {integrity: sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==} engines: {node: '>= 10.14.2'} @@ -3938,7 +4708,7 @@ packages: jest-haste-map: 26.6.2 jest-regex-util: 26.0.0 jest-util: 26.6.2 - micromatch: 4.0.4 + micromatch: 4.0.5 pirates: 4.0.5 slash: 3.0.0 source-map: 0.6.1 @@ -3962,7 +4732,30 @@ packages: jest-haste-map: 28.1.3 jest-regex-util: 28.0.2 jest-util: 28.1.3 - micromatch: 4.0.4 + micromatch: 4.0.5 + pirates: 4.0.5 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/transform/29.3.1: + resolution: {integrity: sha512-8wmCFBTVGYqFNLWfcOWoVuMuKYPUBTnTMDkdvFtAYELwDOl9RGwOsvQWGPFxDJ8AWY9xM/8xCXdqmPK3+Q5Lug==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.20.5 + '@jest/types': 29.3.1 + '@jridgewell/trace-mapping': 0.3.17 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.10 + jest-haste-map: 29.3.1 + jest-regex-util: 29.2.0 + jest-util: 29.3.1 + micromatch: 4.0.5 pirates: 4.0.5 slash: 3.0.0 write-file-atomic: 4.0.2 @@ -4164,82 +4957,296 @@ packages: fs-extra: 8.1.0 dev: true - /@mdi/js/7.1.96: - resolution: {integrity: sha512-wlrJs6Ryhaa5CqhK3FjTfMRnb/s7HeLkKMFqwQySkK86cdN1TGdzpSM3O4tsmzCA1dYBeTbXvOwSE/Y42cUrvA==} - dev: false - - /@mdx-js/mdx/1.6.22: - resolution: {integrity: sha512-AMxuLxPz2j5/6TpF/XSdKpQP1NlG0z11dFOlq+2IP/lSgl11GY8ji6S/rgsViN/L0BDvHvUMruRb7ub+24LUYA==} + /@manypkg/get-packages/1.1.3: + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} dependencies: - '@babel/core': 7.12.9 - '@babel/plugin-syntax-jsx': 7.12.1_@babel+core@7.12.9 - '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.12.9 - '@mdx-js/util': 1.6.22 - babel-plugin-apply-mdx-type-prop: 1.6.22_@babel+core@7.12.9 - babel-plugin-extract-import-names: 1.6.22 - camelcase-css: 2.0.1 - detab: 2.0.4 - hast-util-raw: 6.0.1 - lodash.uniq: 4.5.0 - mdast-util-to-hast: 10.0.1 - remark-footnotes: 2.0.0 - remark-mdx: 1.6.22 - remark-parse: 8.0.3 - remark-squeeze-paragraphs: 4.0.0 - style-to-object: 0.3.0 - unified: 9.2.0 - unist-builder: 2.0.3 - unist-util-visit: 2.0.3 - transitivePeerDependencies: - - supports-color + '@babel/runtime': 7.20.6 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 dev: true - /@mdx-js/react/1.6.22_react@18.1.0: - resolution: {integrity: sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg==} + /@material-table/core/3.2.5_zgbk4qsvidizqjnfqfynvk3e4e: + resolution: {integrity: sha512-TmVN/In15faabezW3COb4Ve5+YhqxFEQnf2Q2Cz3FVXXCFqJvtu3pkRLi+7N9UJ5bvistszz6wfHeiZZY1Rf9Q==} peerDependencies: - react: ^16.13.1 || ^17.0.0 - dependencies: - react: 18.1.0 - dev: true - - /@mdx-js/util/1.6.22: - resolution: {integrity: sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==} - dev: true - - /@mermaid-js/mermaid-cli/8.14.0: - resolution: {integrity: sha512-NHuFVPINakXJlAX0DHl3Bvcrz664ZblHfvB7M2X9fwTZNMZzoFTO2k0Q79Rh9QTmZTmmMjj0JmKMg7LiP+pFCA==} - hasBin: true + '@date-io/core': ^1.3.13 + '@material-ui/core': ^4.11.2 + react: '>=16.8.0' + react-dom: '>=16.8.0' dependencies: - chalk: 4.1.2 - commander: 9.4.1 - mermaid: 8.13.5 - puppeteer: 13.7.0 + '@babel/runtime': 7.20.6 + '@date-io/core': 1.3.13 + '@date-io/date-fns': 1.3.13_date-fns@2.16.1 + '@material-ui/core': 4.12.4_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/pickers': 3.3.10_mg7pc472ejgkw4p6x3x7vycany + '@material-ui/styles': 4.11.5_7ir5hbqdbfw4we5ecpxz77f25m + classnames: 2.3.1 + date-fns: 2.16.1 + debounce: 1.2.1 + fast-deep-equal: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + react-beautiful-dnd: 13.1.1_biqbaboplfbrettd7655fr4n2y + react-dom: 18.2.0_react@18.2.0 + react-double-scrollbar: 0.0.15_react@18.2.0 + uuid: 3.4.0 transitivePeerDependencies: - - bufferutil - - encoding - - supports-color - - utf-8-validate - dev: true - - /@microsoft/fast-element/1.7.0: - resolution: {integrity: sha512-2qpAWuiOSdQfH/XdO8ZtVhlvQVCjHlojWUPoGbvHJDizBccZib+4uGReG87RIBp2Fi0s7ngYPRUioS1Lr+Xe0A==} - dev: false + - '@types/react' + - react-native - /@microsoft/fast-foundation/2.32.2: - resolution: {integrity: sha512-xbagdbnNyKGg3Uvx+llTgN6C47FYYN1Wo5bRL8r3QcNalBkgsA/4T5fZFOoEypxUPzRDlEH/A81G1c8CJwHdLw==} + /@material-ui/core/4.12.4_7ir5hbqdbfw4we5ecpxz77f25m: + resolution: {integrity: sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==} + engines: {node: '>=8.0.0'} + deprecated: Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5. + peerDependencies: + '@types/react': ^16.8.6 || ^17.0.0 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + peerDependenciesMeta: + '@types/react': + optional: true dependencies: - '@microsoft/fast-element': 1.7.0 - '@microsoft/fast-web-utilities': 5.1.0 - tabbable: 5.2.1 - tslib: 2.1.0 - dev: false + '@babel/runtime': 7.20.6 + '@material-ui/styles': 4.11.5_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/system': 4.12.2_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/types': 5.1.0_@types+react@17.0.43 + '@material-ui/utils': 4.11.3_biqbaboplfbrettd7655fr4n2y + '@types/react': 17.0.43 + '@types/react-transition-group': 4.4.5 + clsx: 1.2.1 + hoist-non-react-statics: 3.3.2 + popper.js: 1.16.1-lts + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-is: 17.0.2 + react-transition-group: 4.4.5_biqbaboplfbrettd7655fr4n2y - /@microsoft/fast-react-wrapper/0.1.25_react@18.1.0: - resolution: {integrity: sha512-0qWU/pUY31/YKLgHzgn0+1Ye5klvTrp4/9X6PGzQk5GAU2jjN9dv1Atzsf5bz+BdkE3oWfBhiV6UOJvA/YbnmA==} + /@material-ui/icons/4.11.3_5glfapqkhejqpattrqvw65m2la: + resolution: {integrity: sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==} + engines: {node: '>=8.0.0'} peerDependencies: - react: '>=16.9.0' - dependencies: - '@microsoft/fast-element': 1.7.0 + '@material-ui/core': ^4.0.0 + '@types/react': ^16.8.6 || ^17.0.0 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.20.6 + '@material-ui/core': 4.12.4_7ir5hbqdbfw4we5ecpxz77f25m + '@types/react': 17.0.43 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + + /@material-ui/lab/4.0.0-alpha.57_5glfapqkhejqpattrqvw65m2la: + resolution: {integrity: sha512-qo/IuIQOmEKtzmRD2E4Aa6DB4A87kmY6h0uYhjUmrrgmEAgbbw9etXpWPVXuRK6AGIQCjFzV6WO2i21m1R4FCw==} + engines: {node: '>=8.0.0'} + deprecated: Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5. + peerDependencies: + '@material-ui/core': ^4.9.10 + '@types/react': ^16.8.6 || ^17.0.0 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.20.6 + '@material-ui/core': 4.12.4_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/utils': 4.11.3_biqbaboplfbrettd7655fr4n2y + '@types/react': 17.0.43 + clsx: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-is: 17.0.2 + + /@material-ui/pickers/3.3.10_mg7pc472ejgkw4p6x3x7vycany: + resolution: {integrity: sha512-hS4pxwn1ZGXVkmgD4tpFpaumUaAg2ZzbTrxltfC5yPw4BJV+mGkfnQOB4VpWEYZw2jv65Z0wLwDE/piQiPPZ3w==} + deprecated: Material UI Pickers v3 doesn't receive active development since January 2020. See the guide https://mui.com/material-ui/guides/pickers-migration/ to upgrade. + peerDependencies: + '@date-io/core': ^1.3.6 + '@material-ui/core': ^4.0.0 + prop-types: ^15.6.0 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + dependencies: + '@babel/runtime': 7.20.6 + '@date-io/core': 1.3.13 + '@material-ui/core': 4.12.4_7ir5hbqdbfw4we5ecpxz77f25m + '@types/styled-jsx': 2.2.9 + clsx: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-transition-group: 4.4.5_biqbaboplfbrettd7655fr4n2y + rifm: 0.7.0_react@18.2.0 + + /@material-ui/styles/4.11.5_7ir5hbqdbfw4we5ecpxz77f25m: + resolution: {integrity: sha512-o/41ot5JJiUsIETME9wVLAJrmIWL3j0R0Bj2kCOLbSfqEkKf0fmaPt+5vtblUh5eXr2S+J/8J3DaCb10+CzPGA==} + engines: {node: '>=8.0.0'} + deprecated: Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5. + peerDependencies: + '@types/react': ^16.8.6 || ^17.0.0 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.20.6 + '@emotion/hash': 0.8.0 + '@material-ui/types': 5.1.0_@types+react@17.0.43 + '@material-ui/utils': 4.11.3_biqbaboplfbrettd7655fr4n2y + '@types/react': 17.0.43 + clsx: 1.2.1 + csstype: 2.6.21 + hoist-non-react-statics: 3.3.2 + jss: 10.9.2 + jss-plugin-camel-case: 10.9.2 + jss-plugin-default-unit: 10.9.2 + jss-plugin-global: 10.9.2 + jss-plugin-nested: 10.9.2 + jss-plugin-props-sort: 10.9.2 + jss-plugin-rule-value-function: 10.9.2 + jss-plugin-vendor-prefixer: 10.9.2 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + + /@material-ui/system/4.12.2_7ir5hbqdbfw4we5ecpxz77f25m: + resolution: {integrity: sha512-6CSKu2MtmiJgcCGf6nBQpM8fLkuB9F55EKfbdTC80NND5wpTmKzwdhLYLH3zL4cLlK0gVaaltW7/wMuyTnN0Lw==} + engines: {node: '>=8.0.0'} + peerDependencies: + '@types/react': ^16.8.6 || ^17.0.0 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.20.6 + '@material-ui/utils': 4.11.3_biqbaboplfbrettd7655fr4n2y + '@types/react': 17.0.43 + csstype: 2.6.21 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + + /@material-ui/types/5.1.0_@types+react@17.0.43: + resolution: {integrity: sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==} + peerDependencies: + '@types/react': '*' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 17.0.43 + + /@material-ui/types/6.0.2_@types+react@17.0.43: + resolution: {integrity: sha512-/XUca4wUb9pWimLLdM1PE8KS8rTbDEGohSGkGtk3WST7lm23m+8RYv9uOmrvOg/VSsl4bMiOv4t2/LCb+RLbTg==} + peerDependencies: + '@types/react': '*' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 17.0.43 + dev: true + + /@material-ui/utils/4.11.3_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg==} + engines: {node: '>=8.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + dependencies: + '@babel/runtime': 7.20.6 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-is: 17.0.2 + + /@mdi/js/7.1.96: + resolution: {integrity: sha512-wlrJs6Ryhaa5CqhK3FjTfMRnb/s7HeLkKMFqwQySkK86cdN1TGdzpSM3O4tsmzCA1dYBeTbXvOwSE/Y42cUrvA==} + dev: false + + /@mdx-js/mdx/1.6.22: + resolution: {integrity: sha512-AMxuLxPz2j5/6TpF/XSdKpQP1NlG0z11dFOlq+2IP/lSgl11GY8ji6S/rgsViN/L0BDvHvUMruRb7ub+24LUYA==} + dependencies: + '@babel/core': 7.12.9 + '@babel/plugin-syntax-jsx': 7.12.1_@babel+core@7.12.9 + '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.12.9 + '@mdx-js/util': 1.6.22 + babel-plugin-apply-mdx-type-prop: 1.6.22_@babel+core@7.12.9 + babel-plugin-extract-import-names: 1.6.22 + camelcase-css: 2.0.1 + detab: 2.0.4 + hast-util-raw: 6.0.1 + lodash.uniq: 4.5.0 + mdast-util-to-hast: 10.0.1 + remark-footnotes: 2.0.0 + remark-mdx: 1.6.22 + remark-parse: 8.0.3 + remark-squeeze-paragraphs: 4.0.0 + style-to-object: 0.3.0 + unified: 9.2.0 + unist-builder: 2.0.3 + unist-util-visit: 2.0.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@mdx-js/react/1.6.22_react@18.1.0: + resolution: {integrity: sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg==} + peerDependencies: + react: ^16.13.1 || ^17.0.0 + dependencies: + react: 18.1.0 + dev: true + + /@mdx-js/util/1.6.22: + resolution: {integrity: sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==} + dev: true + + /@mermaid-js/mermaid-cli/8.14.0: + resolution: {integrity: sha512-NHuFVPINakXJlAX0DHl3Bvcrz664ZblHfvB7M2X9fwTZNMZzoFTO2k0Q79Rh9QTmZTmmMjj0JmKMg7LiP+pFCA==} + hasBin: true + dependencies: + chalk: 4.1.2 + commander: 9.4.1 + mermaid: 8.13.5 + puppeteer: 13.7.0 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: true + + /@microsoft/fast-element/1.7.0: + resolution: {integrity: sha512-2qpAWuiOSdQfH/XdO8ZtVhlvQVCjHlojWUPoGbvHJDizBccZib+4uGReG87RIBp2Fi0s7ngYPRUioS1Lr+Xe0A==} + dev: false + + /@microsoft/fast-foundation/2.32.2: + resolution: {integrity: sha512-xbagdbnNyKGg3Uvx+llTgN6C47FYYN1Wo5bRL8r3QcNalBkgsA/4T5fZFOoEypxUPzRDlEH/A81G1c8CJwHdLw==} + dependencies: + '@microsoft/fast-element': 1.7.0 + '@microsoft/fast-web-utilities': 5.1.0 + tabbable: 5.2.1 + tslib: 2.1.0 + dev: false + + /@microsoft/fast-react-wrapper/0.1.25_react@18.1.0: + resolution: {integrity: sha512-0qWU/pUY31/YKLgHzgn0+1Ye5klvTrp4/9X6PGzQk5GAU2jjN9dv1Atzsf5bz+BdkE3oWfBhiV6UOJvA/YbnmA==} + peerDependencies: + react: '>=16.9.0' + dependencies: + '@microsoft/fast-element': 1.7.0 '@microsoft/fast-foundation': 2.32.2 react: 18.1.0 dev: false @@ -4262,6 +5269,30 @@ packages: glob-to-regexp: 0.3.0 dev: true + /@mswjs/cookies/0.2.2: + resolution: {integrity: sha512-mlN83YSrcFgk7Dm1Mys40DLssI1KdJji2CMKN8eOlBqsTADYzj2+jWzsANsUTFbxDMWPD5e9bfA1RGqBpS3O1g==} + engines: {node: '>=14'} + dependencies: + '@types/set-cookie-parser': 2.4.2 + set-cookie-parser: 2.5.1 + dev: true + + /@mswjs/interceptors/0.17.6: + resolution: {integrity: sha512-201pBIWehTURb6q8Gheu4Zhvd3Ox1U4BJq5KiOQsYzkWyfiOG4pwcz5hPZIEryztgrf8/sdwABpvY757xMmfrQ==} + engines: {node: '>=14'} + dependencies: + '@open-draft/until': 1.0.3 + '@types/debug': 4.1.7 + '@xmldom/xmldom': 0.8.6 + debug: 4.3.4 + headers-polyfill: 3.1.2 + outvariant: 1.3.0 + strict-event-emitter: 0.2.8 + web-encoding: 1.1.5 + transitivePeerDependencies: + - supports-color + dev: true + /@n1ru4l/push-pull-async-iterable-iterator/3.2.0: resolution: {integrity: sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q==} engines: {node: '>=12'} @@ -4307,9 +5338,9 @@ packages: dependencies: '@octokit/auth-app': 4.0.6 '@octokit/auth-unauthenticated': 3.0.2 - '@octokit/core': 4.0.5 + '@octokit/core': 4.1.0 '@octokit/oauth-app': 4.1.0 - '@octokit/plugin-paginate-rest': 4.3.1_@octokit+core@4.0.5 + '@octokit/plugin-paginate-rest': 4.3.1_@octokit+core@4.1.0 '@octokit/types': 7.5.1 '@octokit/webhooks': 10.1.5 transitivePeerDependencies: @@ -4390,15 +5421,15 @@ packages: '@octokit/types': 7.5.1 dev: true - /@octokit/core/4.0.5: - resolution: {integrity: sha512-4R3HeHTYVHCfzSAi0C6pbGXV8UDI5Rk+k3G7kLVNckswN9mvpOzW9oENfjfH3nEmzg8y3AmKmzs8Sg6pLCeOCA==} + /@octokit/core/4.1.0: + resolution: {integrity: sha512-Czz/59VefU+kKDy+ZfDwtOIYIkFjExOKf+HA92aiTZJ6EfWpFzYQWw0l54ji8bVmyhc+mGaLUbSUmXazG7z5OQ==} engines: {node: '>= 14'} dependencies: '@octokit/auth-token': 3.0.1 '@octokit/graphql': 5.0.1 '@octokit/request': 6.2.1 '@octokit/request-error': 3.0.1 - '@octokit/types': 7.5.1 + '@octokit/types': 8.1.1 before-after-hook: 2.2.2 universal-user-agent: 6.0.0 transitivePeerDependencies: @@ -4440,7 +5471,7 @@ packages: '@octokit/auth-oauth-app': 5.0.3 '@octokit/auth-oauth-user': 2.0.3 '@octokit/auth-unauthenticated': 3.0.2 - '@octokit/core': 4.0.5 + '@octokit/core': 4.1.0 '@octokit/oauth-authorization-url': 5.0.0 '@octokit/oauth-methods': 2.0.3 '@types/aws-lambda': 8.10.106 @@ -4476,24 +5507,46 @@ packages: resolution: {integrity: sha512-4EuKSk3N95UBWFau3Bz9b3pheQ8jQYbKmBL5+GSuY8YDPDwu03J4BjI+66yNi8aaX/3h1qDpb0mbBkLdr+cfGQ==} dev: true - /@octokit/plugin-paginate-rest/4.3.1_@octokit+core@4.0.5: + /@octokit/openapi-types/14.0.0: + resolution: {integrity: sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw==} + dev: true + + /@octokit/plugin-paginate-rest/4.3.1_@octokit+core@4.1.0: resolution: {integrity: sha512-h8KKxESmSFTcXX409CAxlaOYscEDvN2KGQRsLCGT1NSqRW+D6EXLVQ8vuHhFznS9MuH9QYw1GfsUN30bg8hjVA==} engines: {node: '>= 14'} peerDependencies: '@octokit/core': '>=4' dependencies: - '@octokit/core': 4.0.5 + '@octokit/core': 4.1.0 '@octokit/types': 7.5.1 dev: true - /@octokit/plugin-rest-endpoint-methods/6.6.2_@octokit+core@4.0.5: - resolution: {integrity: sha512-n9dL5KMpz9qVFSNdcVWC8ZPbl68QbTk7+CMPXCXqaMZOLn1n1YuoSFFCy84Ge0fx333fUqpnBHv8BFjwGtUQkA==} + /@octokit/plugin-paginate-rest/5.0.1_@octokit+core@4.1.0: + resolution: {integrity: sha512-7A+rEkS70pH36Z6JivSlR7Zqepz3KVucEFVDnSrgHXzG7WLAzYwcHZbKdfTXHwuTHbkT1vKvz7dHl1+HNf6Qyw==} engines: {node: '>= 14'} + peerDependencies: + '@octokit/core': '>=4' + dependencies: + '@octokit/core': 4.1.0 + '@octokit/types': 8.1.1 + dev: true + + /@octokit/plugin-request-log/1.0.4_@octokit+core@4.1.0: + resolution: {integrity: sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==} peerDependencies: '@octokit/core': '>=3' dependencies: - '@octokit/core': 4.0.5 - '@octokit/types': 7.5.1 + '@octokit/core': 4.1.0 + dev: true + + /@octokit/plugin-rest-endpoint-methods/6.7.0_@octokit+core@4.1.0: + resolution: {integrity: sha512-orxQ0fAHA7IpYhG2flD2AygztPlGYNAdlzYz8yrD8NDgelPfOYoRPROfEyIe035PlxvbYrgkfUZIhSBKju/Cvw==} + engines: {node: '>= 14'} + peerDependencies: + '@octokit/core': '>=3' + dependencies: + '@octokit/core': 4.1.0 + '@octokit/types': 8.1.1 deprecation: 2.3.1 dev: true @@ -4504,13 +5557,13 @@ packages: bottleneck: 2.19.5 dev: true - /@octokit/plugin-throttling/4.3.0_@octokit+core@4.0.5: + /@octokit/plugin-throttling/4.3.0_@octokit+core@4.1.0: resolution: {integrity: sha512-4H0HlCQoJMkLputwruBvMNl4tBRiDHMkMZQyyR8Nh0KwLFrbMW0vuQjwSNPqTqMhiQ5JpP2XKPifhHr8gIs2jQ==} engines: {node: '>= 14'} peerDependencies: '@octokit/core': ^4.0.0 dependencies: - '@octokit/core': 4.0.5 + '@octokit/core': 4.1.0 '@octokit/types': 7.5.1 bottleneck: 2.19.5 dev: true @@ -4586,6 +5639,18 @@ packages: - encoding dev: true + /@octokit/rest/19.0.5: + resolution: {integrity: sha512-+4qdrUFq2lk7Va+Qff3ofREQWGBeoTKNqlJO+FGjFP35ZahP+nBenhZiGdu8USSgmq4Ky3IJ/i4u0xbLqHaeow==} + engines: {node: '>= 14'} + dependencies: + '@octokit/core': 4.1.0 + '@octokit/plugin-paginate-rest': 5.0.1_@octokit+core@4.1.0 + '@octokit/plugin-request-log': 1.0.4_@octokit+core@4.1.0 + '@octokit/plugin-rest-endpoint-methods': 6.7.0_@octokit+core@4.1.0 + transitivePeerDependencies: + - encoding + dev: true + /@octokit/types/2.0.1: resolution: {integrity: sha512-YDYgV6nCzdGdOm7wy43Ce8SQ3M5DMKegB8E5sTB/1xrxOdo2yS/KgUgML2N2ZGD621mkbdrAglwTyA4NDOlFFA==} dependencies: @@ -4610,6 +5675,12 @@ packages: '@octokit/openapi-types': 13.13.1 dev: true + /@octokit/types/8.1.1: + resolution: {integrity: sha512-7tjk+6DyhYAmei8FOEwPfGKc0VE1x56CKPJ+eE44zhDbOyMT+9yan8apfQFxo8oEFsy+0O7PiBtH8w0Yo0Y9Kw==} + dependencies: + '@octokit/openapi-types': 14.0.0 + dev: true + /@octokit/webhooks-methods/3.0.0: resolution: {integrity: sha512-FAIyAchH9JUKXugKMC17ERAXM/56vVJekwXOON46pmUDYfU7uXB4cFY8yc8nYr5ABqVI7KjRKfFt3mZF7OcyUA==} engines: {node: '>= 14'} @@ -4629,6 +5700,10 @@ packages: aggregate-error: 3.1.0 dev: true + /@open-draft/until/1.0.3: + resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==} + dev: true + /@opentelemetry/api-metrics/0.31.0: resolution: {integrity: sha512-PcL1x0kZtMie7NsNy67OyMvzLEXqf3xd0TZJKHHPMGTe89oMpNVrD1zJB1kZcwXOxLlHHb6tz21G3vvXPdXyZg==} engines: {node: '>=14'} @@ -4957,7 +6032,7 @@ packages: cross-spawn: 7.0.3 extract-zip: 2.0.1 fast-glob: 3.2.11 - micromatch: 4.0.4 + micromatch: 4.0.5 mime-types: 2.1.35 path-to-regexp: 6.2.0 rimraf: 3.0.2 @@ -5013,6 +6088,46 @@ packages: typescript: 4.9.3 dev: true + /@pmmmwh/react-refresh-webpack-plugin/0.5.10_e764yt37rymiplqgpb7fiwcihu: + resolution: {integrity: sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==} + engines: {node: '>= 10.13'} + peerDependencies: + '@types/webpack': 4.x || 5.x + react-refresh: '>=0.10.0 <1.0.0' + sockjs-client: ^1.4.0 + type-fest: '>=0.17.0 <4.0.0' + webpack: '>=4.43.0 <6.0.0' + webpack-dev-server: 3.x || 4.x + webpack-hot-middleware: 2.x + webpack-plugin-serve: 0.x || 1.x + peerDependenciesMeta: + '@types/webpack': + optional: true + sockjs-client: + optional: true + type-fest: + optional: true + webpack-dev-server: + optional: true + webpack-hot-middleware: + optional: true + webpack-plugin-serve: + optional: true + dependencies: + ansi-html-community: 0.0.8 + common-path-prefix: 3.0.0 + core-js-pure: 3.26.1 + error-stack-parser: 2.0.6 + find-up: 5.0.0 + html-entities: 2.3.2 + loader-utils: 2.0.4 + react-refresh: 0.14.0 + schema-utils: 3.1.1 + source-map: 0.7.3 + webpack: 5.75.0_cw4su4nzareykaft5p7arksy5i + webpack-dev-server: 4.11.1_webpack@5.75.0 + dev: true + /@pmmmwh/react-refresh-webpack-plugin/0.5.10_j3lj322d4usblreb4zevy6mwsq: resolution: {integrity: sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==} engines: {node: '>= 10.13'} @@ -5049,7 +6164,7 @@ packages: react-refresh: 0.10.0 schema-utils: 3.1.1 source-map: 0.7.3 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-dev-server: 4.11.1_rjsyjcrmk25kqsjzwkvj3a2evq dev: true @@ -5089,7 +6204,7 @@ packages: react-refresh: 0.11.0 schema-utils: 3.1.1 source-map: 0.7.3 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-dev-server: 4.11.1_rjsyjcrmk25kqsjzwkvj3a2evq dev: true @@ -5346,7 +6461,7 @@ packages: bowser: 2.9.0 fast-json-stable-stringify: 2.1.0 lodash-es: 4.17.21 - set-cookie-parser: 2.4.5 + set-cookie-parser: 2.5.1 utf8-byte-length: 1.0.4 dev: true @@ -5558,13 +6673,30 @@ packages: '@babel/runtime': 7.20.6 dev: false + /@react-hookz/deep-equal/1.0.4: + resolution: {integrity: sha512-N56fTrAPUDz/R423pag+n6TXWbvlBZDtTehaGFjK0InmN+V2OFWLE/WmORhmn6Ce7dlwH5+tQN1LJFw3ngTJVg==} + + /@react-hookz/web/20.1.0_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-60H9KAQ8QF4lEEY2VujTaDTEb5tzHFVQ+pq4kV5zPHMzDVoaBQbiWcZrCKpFaVHzBP/nPvDEfXICZzga0aIIzg==} + peerDependencies: + js-cookie: ^3.0.1 + react: ^16.8 || ^17 || ^18 + react-dom: ^16.8 || ^17 || ^18 + peerDependenciesMeta: + js-cookie: + optional: true + dependencies: + '@react-hookz/deep-equal': 1.0.4 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + /@react-native-community/cli-clean/9.2.1: resolution: {integrity: sha512-dyNWFrqRe31UEvNO+OFWmQ4hmqA07bR9Ief/6NnGwx67IO9q83D5PEAf/o96ML6jhSbDwCmpPKhPwwBbsyM3mQ==} dependencies: '@react-native-community/cli-tools': 9.2.1 chalk: 4.1.2 execa: 1.0.0 - prompts: 2.4.0 + prompts: 2.4.2 transitivePeerDependencies: - encoding dev: false @@ -5575,7 +6707,7 @@ packages: '@react-native-community/cli-tools': 9.2.1 cosmiconfig: 5.2.1 deepmerge: 3.3.0 - glob: 7.1.7 + glob: 7.2.3 joi: 17.7.0 transitivePeerDependencies: - encoding @@ -5603,7 +6735,7 @@ packages: ip: 1.1.8 node-stream-zip: 1.15.0 ora: 5.4.1 - prompts: 2.4.0 + prompts: 2.4.2 semver: 6.3.0 strip-ansi: 5.2.0 sudo-prompt: 9.2.1 @@ -5631,7 +6763,7 @@ packages: chalk: 4.1.2 execa: 1.0.0 fs-extra: 8.1.0 - glob: 7.1.7 + glob: 7.2.3 logkitty: 0.7.1 slash: 3.0.0 transitivePeerDependencies: @@ -5644,7 +6776,7 @@ packages: '@react-native-community/cli-tools': 9.2.1 chalk: 4.1.2 execa: 1.0.0 - glob: 7.1.7 + glob: 7.2.3 ora: 5.4.1 transitivePeerDependencies: - encoding @@ -5732,7 +6864,7 @@ packages: find-up: 4.1.0 fs-extra: 8.1.0 graceful-fs: 4.2.10 - prompts: 2.4.0 + prompts: 2.4.2 semver: 6.3.0 transitivePeerDependencies: - '@babel/core' @@ -5908,25 +7040,125 @@ packages: zustand: 3.7.2_react@18.1.0 dev: false + /@remix-run/router/1.3.0: + resolution: {integrity: sha512-nwQoYb3m4DDpHTeOwpJEuDt8lWVcujhYYSFGLluC+9es2PyLjm+jjq3IeRBQbwBtPLJE/lkuHuGHr8uQLgmJRA==} + engines: {node: '>=14'} + /@repeaterjs/repeater/3.0.4: resolution: {integrity: sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==} dev: true - /@sentry/browser/7.8.1: - resolution: {integrity: sha512-9JuagYqHyaZu/4RqyxrAgEHo71oV592XBuUKC33gajCVKWbyG3mNqudSMoHtdM1DrV9REZ4Elha7zFaE2cJX6g==} - engines: {node: '>=8'} + /@rollup/plugin-commonjs/23.0.7_rollup@2.79.1: + resolution: {integrity: sha512-hsSD5Qzyuat/swzrExGG5l7EuIlPhwTsT7KwKbSCQzIcJWjRxiimi/0tyMYY2bByitNb3i1p+6JWEDGa0NvT0Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.68.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true dependencies: - '@sentry/core': 7.8.1 - '@sentry/types': 7.8.1 - '@sentry/utils': 7.8.1 - tslib: 2.1.0 - dev: false + '@rollup/pluginutils': 5.0.2_rollup@2.79.1 + commondir: 1.0.1 + estree-walker: 2.0.2 + glob: 8.1.0 + is-reference: 1.2.1 + magic-string: 0.27.0 + rollup: 2.79.1 + dev: true - /@sentry/cli/1.74.6: - resolution: {integrity: sha512-pJ7JJgozyjKZSTjOGi86chIngZMLUlYt2HOog+OJn+WGvqEkVymu8m462j1DiXAnex9NspB4zLLNuZ/R6rTQHg==} - engines: {node: '>= 8'} - hasBin: true - requiresBuild: true + /@rollup/plugin-json/5.0.2_rollup@2.79.1: + resolution: {integrity: sha512-D1CoOT2wPvadWLhVcmpkDnesTzjhNIQRWLsc3fA49IFOP2Y84cFOOJ+nKGYedvXHKUsPeq07HR4hXpBBr+CHlA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.2_rollup@2.79.1 + rollup: 2.79.1 + dev: true + + /@rollup/plugin-node-resolve/13.3.0_rollup@2.79.1: + resolution: {integrity: sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==} + engines: {node: '>= 10.0.0'} + peerDependencies: + rollup: ^2.42.0 + dependencies: + '@rollup/pluginutils': 3.1.0_rollup@2.79.1 + '@types/resolve': 1.17.1 + deepmerge: 4.2.2 + is-builtin-module: 3.1.0 + is-module: 1.0.0 + resolve: 1.22.0 + rollup: 2.79.1 + dev: true + + /@rollup/plugin-yaml/4.0.1_rollup@2.79.1: + resolution: {integrity: sha512-eyftkLWrwaGhgad+gXmisPYXeW3hP1s+lz63mgbur+F/8aKZhPG1Bf8RFNnz0Vhnf3uBimFebZBDwwz6X4KqUQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.2_rollup@2.79.1 + js-yaml: 4.1.0 + rollup: 2.79.1 + tosource: 2.0.0-alpha.3 + dev: true + + /@rollup/pluginutils/3.1.0_rollup@2.79.1: + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.1 + dev: true + + /@rollup/pluginutils/4.2.1: + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + + /@rollup/pluginutils/5.0.2_rollup@2.79.1: + resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.0 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 2.79.1 + dev: true + + /@sentry/browser/7.8.1: + resolution: {integrity: sha512-9JuagYqHyaZu/4RqyxrAgEHo71oV592XBuUKC33gajCVKWbyG3mNqudSMoHtdM1DrV9REZ4Elha7zFaE2cJX6g==} + engines: {node: '>=8'} + dependencies: + '@sentry/core': 7.8.1 + '@sentry/types': 7.8.1 + '@sentry/utils': 7.8.1 + tslib: 2.1.0 + dev: false + + /@sentry/cli/1.74.6: + resolution: {integrity: sha512-pJ7JJgozyjKZSTjOGi86chIngZMLUlYt2HOog+OJn+WGvqEkVymu8m462j1DiXAnex9NspB4zLLNuZ/R6rTQHg==} + engines: {node: '>= 8'} + hasBin: true + requiresBuild: true dependencies: https-proxy-agent: 5.0.1 mkdirp: 0.5.5 @@ -6140,7 +7372,7 @@ packages: eslint-plugin-jest-dom: 3.6.5_eslint@8.18.0 eslint-plugin-jsdoc: 30.7.8_eslint@8.18.0 eslint-plugin-jsx-a11y: 6.5.1_eslint@8.18.0 - eslint-plugin-react: 7.21.4_eslint@8.18.0 + eslint-plugin-react: 7.32.1_eslint@8.18.0 eslint-plugin-react-hooks: 4.5.0_eslint@8.18.0 eslint-plugin-rxjs: 5.0.2_4htfruiy2bgjslzgmagy6rfrsq eslint-plugin-unicorn: 42.0.0_eslint@8.18.0 @@ -6252,6 +7484,43 @@ packages: resolution: {integrity: sha512-G/xsejsR84G5dj3kHJ7svKBo9E5tWl96rUHKP94Y2UDtA7BzUhAYbieM+b9ZUpIRt66h3+MlYbG5HK4UI2zDzw==} dev: true + /@spotify/eslint-config-base/14.1.3_eslint@8.18.0: + resolution: {integrity: sha512-Uy2yDkUF7yHjSUB+/b3QO3zLwvU94UkIAFYb6l+kJMhgHHgR8pUpPZdyYY+ThLnMpcOJZKd60quwIx/vyvdvEA==} + engines: {node: '>=14.17.0'} + peerDependencies: + eslint: '>=7.x' + dependencies: + eslint: 8.18.0 + dev: true + + /@spotify/eslint-config-react/14.1.3_qzuhtewbpqc65tezqbik3lhy6a: + resolution: {integrity: sha512-ZvTc9eIAuGvj1nfJhIMb5LbX6DbGXUBcLsB7B+DnaL0BB1e6WmtaDxJJ4QyPpLkLSu5fTTlziej8Jov1k4gCFg==} + engines: {node: '>=14.17.0'} + peerDependencies: + eslint: '>=8.x' + eslint-plugin-jsx-a11y: 6.x + eslint-plugin-react: '>=7.7.0 <8' + eslint-plugin-react-hooks: ^4.0.0 + dependencies: + eslint: 8.18.0 + eslint-plugin-jsx-a11y: 6.5.1_eslint@8.18.0 + eslint-plugin-react: 7.32.1_eslint@8.18.0 + eslint-plugin-react-hooks: 4.5.0_eslint@8.18.0 + dev: true + + /@spotify/eslint-config-typescript/14.1.3_nii4tqejuio2ec2bwcqvyw5c3i: + resolution: {integrity: sha512-dqHuWIyq73J3QkDAO0DIokouex7e+xMpx2XeNmaa4DmLyU7AoYUqd6LEoLdq8w5cJouuVHHNxBOQEw9TAH5zhw==} + engines: {node: '>=14.17.0'} + peerDependencies: + '@typescript-eslint/eslint-plugin': '>=5' + '@typescript-eslint/parser': '>=5' + eslint: '>=8.x' + dependencies: + '@typescript-eslint/eslint-plugin': 5.24.0_el7ggvuufxftzwvu6wsrutnxdi + '@typescript-eslint/parser': 5.24.0_4htfruiy2bgjslzgmagy6rfrsq + eslint: 8.18.0 + dev: true + /@statoscope/cli/5.24.0: resolution: {integrity: sha512-9o6Y+aykee3g6MoumTIn3mOIYOTDl7KQSISttQYepZCNUxu6cHhO52Cr7jWYPo0tI4DxZeQbeq8EmlsgnFSHvA==} engines: {node: '>=12.0.0'} @@ -6400,7 +7669,7 @@ packages: '@statoscope/webpack-stats-extension-package-info': 5.24.0_webpack@5.75.0 '@statoscope/webpack-ui': 5.24.0 open: 8.4.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /@statoscope/webpack-stats-extension-compressed/5.24.0_webpack@5.75.0: @@ -6411,7 +7680,7 @@ packages: '@statoscope/stats': 5.14.1 '@statoscope/stats-extension-compressed': 5.24.0 '@statoscope/webpack-model': 5.24.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /@statoscope/webpack-stats-extension-package-info/5.24.0_webpack@5.75.0: @@ -6422,7 +7691,7 @@ packages: '@statoscope/stats': 5.14.1 '@statoscope/stats-extension-package-info': 5.24.0 '@statoscope/webpack-model': 5.24.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /@statoscope/webpack-ui/5.24.0: @@ -6505,7 +7774,7 @@ packages: global: 4.4.0 dev: true - /@storybook/addon-controls/6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi: + /@storybook/addon-controls/6.5.14_gfp4q6646gfovewejhfnlg2afq: resolution: {integrity: sha512-p16k/69GjwVtnpEiz0fmb1qoqp/H2d5aaSGDt7VleeXsdhs4Kh0kJyxfLpekHmlzT+5IkO08Nm/U8tJOHbw4Hw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6520,7 +7789,7 @@ packages: '@storybook/api': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/client-logger': 6.5.14 '@storybook/components': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm - '@storybook/core-common': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/core-common': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/csf': 0.0.2--canary.4566f4d.1 '@storybook/node-logger': 6.5.14 '@storybook/store': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm @@ -6541,7 +7810,7 @@ packages: - webpack-cli dev: false - /@storybook/addon-docs/6.5.14_eecksdwfpy5g4a5i23piqqf6v4: + /@storybook/addon-docs/6.5.14_pj2nlckshw25raeqzlyf2nlzmi: resolution: {integrity: sha512-gapuzDY+dqgS4/Ap9zj5L76OSExBYtVNYej9xTiF+v0Gh4/kty9FIGlVWiqskffOmixL4nlyImpfsSH8V0JnCw==} peerDependencies: '@storybook/mdx2-csf': ^0.0.3 @@ -6562,7 +7831,7 @@ packages: '@storybook/addons': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/api': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/components': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm - '@storybook/core-common': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/core-common': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/core-events': 6.5.14 '@storybook/csf': 0.0.2--canary.4566f4d.1 '@storybook/docs-tools': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm @@ -6635,7 +7904,7 @@ packages: optional: true dependencies: '@axe-core/puppeteer': 4.4.2_puppeteer@13.7.0 - '@storybook/addon-storyshots': 6.5.14_pmiuyjzits6kkyuf4klcpozjy4 + '@storybook/addon-storyshots': 6.5.14_cisa4gtyltf7ep7ewbmdkskhri '@storybook/csf': 0.0.2--canary.4566f4d.1 '@storybook/node-logger': 6.5.14 '@types/jest-image-snapshot': 4.3.0 @@ -6647,7 +7916,7 @@ packages: - jest dev: true - /@storybook/addon-storyshots/6.5.14_pmiuyjzits6kkyuf4klcpozjy4: + /@storybook/addon-storyshots/6.5.14_cisa4gtyltf7ep7ewbmdkskhri: resolution: {integrity: sha512-BSYt+GyMeTlxCwMNVwsmfetKjeIZVVRFdhvtyTuSf9MvikBq+SXw6IihkeWbX2g6pssCz9Wc+s6rRK/HJpqTlA==} peerDependencies: '@angular/core': '>=6.0.0' @@ -6702,16 +7971,16 @@ packages: '@storybook/addons': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/babel-plugin-require-context-hook': 1.0.1 '@storybook/client-api': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm - '@storybook/core': 6.5.14_r3crpb7cqiqcwc63njxwfaet7a + '@storybook/core': 6.5.14_o2cbjy3xxqdj2tzzexfwchdcvq '@storybook/core-client': 6.5.14_77mmp7l52573w63t23u7ainpmu - '@storybook/core-common': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/core-common': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/csf': 0.0.2--canary.4566f4d.1 - '@storybook/react': 6.5.14_6bdewul77sl3cpoopqo52yktc4 + '@storybook/react': 6.5.14_r4utchdsxdspndke4rlo2g3uya '@types/glob': 7.1.3 - '@types/jest': 26.0.20 + '@types/jest': 26.0.24 '@types/jest-specific-snapshot': 0.5.4 core-js: 3.22.8 - glob: 7.1.7 + glob: 7.2.3 global: 4.4.0 jest: 28.1.3_sgfsbmxe5qwkqmsj7h2d7flbny jest-specific-snapshot: 4.0.0_jest@28.1.3 @@ -6763,7 +8032,7 @@ packages: '@storybook/source-loader': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/theming': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm core-js: 3.22.8 - estraverse: 5.2.0 + estraverse: 5.3.0 loader-utils: 2.0.4 prop-types: 15.8.1 react: 18.1.0 @@ -6844,7 +8113,7 @@ packages: resolution: {integrity: sha512-WM4vjgSVi8epvGiYfru7BtC3f0tGwNs7QK3Uc4xQn4t5hHQvISnCqbNrHdDYmNW56Do+bBztE8SwP6NGUvd7ww==} dev: true - /@storybook/builder-webpack4/6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi: + /@storybook/builder-webpack4/6.5.14_gfp4q6646gfovewejhfnlg2afq: resolution: {integrity: sha512-0pv8BlsMeiP9VYU2CbCZaa3yXDt1ssb8OeTRDbFC0uFFb3eqslsH68I7XsC8ap/dr0RZR0Edtw0OW3HhkjUXXw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6862,7 +8131,7 @@ packages: '@storybook/client-api': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/client-logger': 6.5.14 '@storybook/components': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm - '@storybook/core-common': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/core-common': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/core-events': 6.5.14 '@storybook/node-logger': 6.5.14 '@storybook/preview-web': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm @@ -6872,7 +8141,7 @@ packages: '@storybook/theming': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/ui': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@types/node': 16.11.36 - '@types/webpack': 5.28.0_oo63t3us67g6w4zg54gqorhf2i + '@types/webpack': 5.28.0_jh3afgd4dccyz4n3zkk5zvv5cy autoprefixer: 9.8.6 babel-loader: 8.2.5_ztqwsvkb6z73luspkai6ilstpu case-sensitive-paths-webpack-plugin: 2.4.0 @@ -6881,10 +8150,10 @@ packages: file-loader: 6.2.0_webpack@5.75.0 find-up: 5.0.0 fork-ts-checker-webpack-plugin: 4.1.6_7nbrhxx5wbdrea3w4d3cjhku4y - glob: 7.1.7 - glob-promise: 3.4.0_glob@7.1.7 + glob: 7.2.3 + glob-promise: 3.4.0_glob@7.2.3 global: 4.4.0 - html-webpack-plugin: 4.5.2_ovbyeo7usi33ah22pzu3dmmfoi + html-webpack-plugin: 4.5.2_nljosxlhezzfayhg2olmp6suom pnp-webpack-plugin: 1.6.4_typescript@4.9.3 postcss: 7.0.39 postcss-flexbugs-fixes: 4.2.1 @@ -6899,7 +8168,7 @@ packages: typescript: 4.9.3 url-loader: 4.1.1_p5dl6emkcwslbw72e37w4ug7em util-deprecate: 1.0.2 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-dev-middleware: 3.7.3_webpack@5.75.0 webpack-filter-warnings-plugin: 1.2.1_webpack@5.75.0 webpack-hot-middleware: 2.25.1 @@ -6915,7 +8184,7 @@ packages: - webpack-cli dev: true - /@storybook/builder-webpack5/6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi: + /@storybook/builder-webpack5/6.5.14_gfp4q6646gfovewejhfnlg2afq: resolution: {integrity: sha512-Ukj7Wwxz/3mKn5TI5mkm2mIm583LxOz78ZrpcOgI+vpjeRlMFXmGGEb68R47SiCdZoVCfIeCXXXzBd6Q6As6QQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -6933,7 +8202,7 @@ packages: '@storybook/client-api': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/client-logger': 6.5.14 '@storybook/components': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm - '@storybook/core-common': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/core-common': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/core-events': 6.5.14 '@storybook/node-logger': 6.5.14 '@storybook/preview-web': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm @@ -6948,9 +8217,9 @@ packages: case-sensitive-paths-webpack-plugin: 2.4.0 core-js: 3.22.8 css-loader: 5.2.6_webpack@5.75.0 - fork-ts-checker-webpack-plugin: 6.2.4_7nbrhxx5wbdrea3w4d3cjhku4y - glob: 7.1.7 - glob-promise: 3.4.0_glob@7.1.7 + fork-ts-checker-webpack-plugin: 6.5.2_7nbrhxx5wbdrea3w4d3cjhku4y + glob: 7.2.3 + glob-promise: 3.4.0_glob@7.2.3 html-webpack-plugin: 5.5.0_webpack@5.75.0 path-browserify: 1.0.1 process: 0.11.10 @@ -6958,11 +8227,11 @@ packages: react-dom: 18.1.0_react@18.1.0 stable: 0.1.8 style-loader: 2.0.0_webpack@5.75.0 - terser-webpack-plugin: 5.3.6_j5vv3ucw6sjpjo7n5idphq5l2u + terser-webpack-plugin: 5.3.6_htvmhiqynazf46fjrszipnqp7a ts-dedent: 2.0.0 typescript: 4.9.3 util-deprecate: 1.0.2 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-dev-middleware: 4.3.0_webpack@5.75.0 webpack-hot-middleware: 2.25.1 webpack-virtual-modules: 0.4.3 @@ -7092,10 +8361,10 @@ packages: typescript: 4.9.3 unfetch: 4.2.0 util-deprecate: 1.0.2 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true - /@storybook/core-common/6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi: + /@storybook/core-common/6.5.14_gfp4q6646gfovewejhfnlg2afq: resolution: {integrity: sha512-MrxhYXYrtN6z/+tydjPkCIwDQm5q8Jx+w4TPdLKBZu7vzfp6T3sT12Ym96j9MJ42CvE4vSDl/Njbw6C0D+yEVw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -7139,9 +8408,9 @@ packages: express: 4.18.2 file-system-cache: 1.0.5 find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 6.2.4_7nbrhxx5wbdrea3w4d3cjhku4y + fork-ts-checker-webpack-plugin: 6.5.2_7nbrhxx5wbdrea3w4d3cjhku4y fs-extra: 9.0.1 - glob: 7.1.7 + glob: 7.2.3 handlebars: 4.7.7 interpret: 2.2.0 json5: 2.2.1 @@ -7157,7 +8426,7 @@ packages: ts-dedent: 2.0.0 typescript: 4.9.3 util-deprecate: 1.0.2 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy transitivePeerDependencies: - '@swc/core' - esbuild @@ -7172,7 +8441,7 @@ packages: dependencies: core-js: 3.22.8 - /@storybook/core-server/6.5.14_vmkgtd5h7tg6n22u3xfio4t2bq: + /@storybook/core-server/6.5.14_atn4omggkvboprklebowgbukgq: resolution: {integrity: sha512-+Z3lHEsDpiBXt6xBwU5AVBoEkicndnHoiLwhEGPkfixy7POYEEny3cm54tteVxV8O5AHMwsHs54/QD+hHxAXnQ==} peerDependencies: '@storybook/builder-webpack5': '*' @@ -7189,23 +8458,23 @@ packages: optional: true dependencies: '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-webpack4': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi - '@storybook/builder-webpack5': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/builder-webpack4': 6.5.14_gfp4q6646gfovewejhfnlg2afq + '@storybook/builder-webpack5': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/core-client': 6.5.14_77mmp7l52573w63t23u7ainpmu - '@storybook/core-common': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/core-common': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/core-events': 6.5.14 '@storybook/csf': 0.0.2--canary.4566f4d.1 '@storybook/csf-tools': 6.5.14 - '@storybook/manager-webpack4': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi - '@storybook/manager-webpack5': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/manager-webpack4': 6.5.14_gfp4q6646gfovewejhfnlg2afq + '@storybook/manager-webpack5': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/node-logger': 6.5.14 '@storybook/semver': 7.3.2 '@storybook/store': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm - '@storybook/telemetry': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/telemetry': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@types/node': 16.11.36 '@types/node-fetch': 2.5.10 '@types/pretty-hrtime': 1.0.0 - '@types/webpack': 5.28.0_oo63t3us67g6w4zg54gqorhf2i + '@types/webpack': 5.28.0_jh3afgd4dccyz4n3zkk5zvv5cy better-opn: 2.1.1 boxen: 5.1.2 chalk: 4.1.2 @@ -7224,7 +8493,7 @@ packages: node-fetch: 2.6.7 open: 8.4.0 pretty-hrtime: 1.0.3 - prompts: 2.4.0 + prompts: 2.4.2 react: 18.1.0 react-dom: 18.1.0_react@18.1.0 regenerator-runtime: 0.13.11 @@ -7235,7 +8504,7 @@ packages: typescript: 4.9.3 util-deprecate: 1.0.2 watchpack: 2.4.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy ws: 8.11.0 x-default-browser: 0.4.0 transitivePeerDependencies: @@ -7253,7 +8522,7 @@ packages: - webpack-cli dev: true - /@storybook/core/6.5.14_r3crpb7cqiqcwc63njxwfaet7a: + /@storybook/core/6.5.14_o2cbjy3xxqdj2tzzexfwchdcvq: resolution: {integrity: sha512-5rjwZXk++NkKWCmHt/CC+h2L4ZbOYkLJpMmaB97CwgQCA6kaF8xuJqlAwG72VUH3oV+6RntW02X6/ypgX1atPw==} peerDependencies: '@storybook/builder-webpack5': '*' @@ -7270,14 +8539,14 @@ packages: typescript: optional: true dependencies: - '@storybook/builder-webpack5': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/builder-webpack5': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/core-client': 6.5.14_77mmp7l52573w63t23u7ainpmu - '@storybook/core-server': 6.5.14_vmkgtd5h7tg6n22u3xfio4t2bq - '@storybook/manager-webpack5': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/core-server': 6.5.14_atn4omggkvboprklebowgbukgq + '@storybook/manager-webpack5': 6.5.14_gfp4q6646gfovewejhfnlg2afq react: 18.1.0 react-dom: 18.1.0_react@18.1.0 typescript: 4.9.3 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy transitivePeerDependencies: - '@storybook/mdx2-csf' - '@swc/core' @@ -7340,7 +8609,7 @@ packages: - supports-color dev: true - /@storybook/manager-webpack4/6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi: + /@storybook/manager-webpack4/6.5.14_gfp4q6646gfovewejhfnlg2afq: resolution: {integrity: sha512-ixfJuaG0eiOlxn4i+LJNRUZkm+3WMsiaGUm0hw2XHF0pW3cBIA/+HyzkEwVh/fROHbsOERTkjNl0Ygl12Imw9w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -7355,12 +8624,12 @@ packages: '@babel/preset-react': 7.18.6_@babel+core@7.20.5 '@storybook/addons': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/core-client': 6.5.14_77mmp7l52573w63t23u7ainpmu - '@storybook/core-common': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/core-common': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/node-logger': 6.5.14 '@storybook/theming': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/ui': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@types/node': 16.11.36 - '@types/webpack': 5.28.0_oo63t3us67g6w4zg54gqorhf2i + '@types/webpack': 5.28.0_jh3afgd4dccyz4n3zkk5zvv5cy babel-loader: 8.2.5_ztqwsvkb6z73luspkai6ilstpu case-sensitive-paths-webpack-plugin: 2.4.0 chalk: 4.1.2 @@ -7370,7 +8639,7 @@ packages: file-loader: 6.2.0_webpack@5.75.0 find-up: 5.0.0 fs-extra: 9.0.1 - html-webpack-plugin: 4.5.2_ovbyeo7usi33ah22pzu3dmmfoi + html-webpack-plugin: 4.5.2_nljosxlhezzfayhg2olmp6suom node-fetch: 2.6.7 pnp-webpack-plugin: 1.6.4_typescript@4.9.3 react: 18.1.0 @@ -7385,7 +8654,7 @@ packages: typescript: 4.9.3 url-loader: 4.1.1_p5dl6emkcwslbw72e37w4ug7em util-deprecate: 1.0.2 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-dev-middleware: 3.7.3_webpack@5.75.0 webpack-virtual-modules: 0.2.2 transitivePeerDependencies: @@ -7400,7 +8669,7 @@ packages: - webpack-cli dev: true - /@storybook/manager-webpack5/6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi: + /@storybook/manager-webpack5/6.5.14_gfp4q6646gfovewejhfnlg2afq: resolution: {integrity: sha512-Z9uXhaBPpUhbLEYkZVm95vKSmyxXk+DLqa1apAQEmHz3EBMTNk/2n0aZnNnsspYzjNP6wvXWT0sGyXG6yhX2cw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -7415,7 +8684,7 @@ packages: '@babel/preset-react': 7.18.6_@babel+core@7.20.5 '@storybook/addons': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/core-client': 6.5.14_77mmp7l52573w63t23u7ainpmu - '@storybook/core-common': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/core-common': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/node-logger': 6.5.14 '@storybook/theming': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm '@storybook/ui': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm @@ -7438,11 +8707,11 @@ packages: resolve-from: 5.0.0 style-loader: 2.0.0_webpack@5.75.0 telejson: 6.0.8 - terser-webpack-plugin: 5.3.6_j5vv3ucw6sjpjo7n5idphq5l2u + terser-webpack-plugin: 5.3.6_htvmhiqynazf46fjrszipnqp7a ts-dedent: 2.0.0 typescript: 4.9.3 util-deprecate: 1.0.2 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-dev-middleware: 4.3.0_webpack@5.75.0 webpack-virtual-modules: 0.4.3 transitivePeerDependencies: @@ -7526,16 +8795,16 @@ packages: endent: 2.0.1 find-cache-dir: 3.3.2 flat-cache: 3.0.4 - micromatch: 4.0.4 + micromatch: 4.0.5 react-docgen-typescript: 2.2.2_typescript@4.9.3 tslib: 2.1.0 typescript: 4.9.3 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy transitivePeerDependencies: - supports-color dev: true - /@storybook/react/6.5.14_6bdewul77sl3cpoopqo52yktc4: + /@storybook/react/6.5.14_r4utchdsxdspndke4rlo2g3uya: resolution: {integrity: sha512-SL0P5czN3g/IZAYw8ur9I/O8MPZI7Lyd46Pw+B1f7+Ou8eLmhqa8Uc8+3fU6v7ohtUDwsBiTsg3TAfTVEPog4A==} engines: {node: '>=10.13.0'} hasBin: true @@ -7568,13 +8837,13 @@ packages: '@babel/preset-react': 7.18.6_@babel+core@7.20.5 '@pmmmwh/react-refresh-webpack-plugin': 0.5.10_unmakpayn7vcxadrrsbqlrpehy '@storybook/addons': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm - '@storybook/builder-webpack5': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/builder-webpack5': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/client-logger': 6.5.14 - '@storybook/core': 6.5.14_r3crpb7cqiqcwc63njxwfaet7a - '@storybook/core-common': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/core': 6.5.14_o2cbjy3xxqdj2tzzexfwchdcvq + '@storybook/core-common': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/csf': 0.0.2--canary.4566f4d.1 '@storybook/docs-tools': 6.5.14_ef5jwxihqo6n7gxfmzogljlgcm - '@storybook/manager-webpack5': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/manager-webpack5': 6.5.14_gfp4q6646gfovewejhfnlg2afq '@storybook/node-logger': 6.5.14 '@storybook/react-docgen-typescript-plugin': 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0_vfotqvx6lgcbf3upbs6hgaza4q '@storybook/semver': 7.3.2 @@ -7604,7 +8873,7 @@ packages: ts-dedent: 2.0.0 typescript: 4.9.3 util-deprecate: 1.0.2 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy transitivePeerDependencies: - '@storybook/mdx2-csf' - '@swc/core' @@ -7658,7 +8927,7 @@ packages: '@storybook/client-logger': 6.5.14 '@storybook/csf': 0.0.2--canary.4566f4d.1 core-js: 3.22.8 - estraverse: 5.2.0 + estraverse: 5.3.0 global: 4.4.0 loader-utils: 2.0.4 lodash: 4.17.21 @@ -7692,11 +8961,11 @@ packages: ts-dedent: 2.0.0 util-deprecate: 1.0.2 - /@storybook/telemetry/6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi: + /@storybook/telemetry/6.5.14_gfp4q6646gfovewejhfnlg2afq: resolution: {integrity: sha512-AVSw7WyKHrVbXMSZZ0fvg3oAb8xAS7OrmNU6++yUfbuqpF0JNtNkNnRSaJ4Nh7Vujzloy5jYhbpfY44nb/hsCw==} dependencies: '@storybook/client-logger': 6.5.14 - '@storybook/core-common': 6.5.14_jfkrx6ea7vesi2ooe5fwqjwmwi + '@storybook/core-common': 6.5.14_gfp4q6646gfovewejhfnlg2afq chalk: 4.1.2 core-js: 3.22.8 detect-package-manager: 2.0.1 @@ -7758,152 +9027,478 @@ packages: resolve-from: 5.0.0 dev: true - /@szmarczak/http-timer/1.1.2: - resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} - engines: {node: '>=6'} + /@sucrase/webpack-loader/2.0.0_sucrase@3.29.0: + resolution: {integrity: sha512-KUfWr83g70Qm+ZqjGL+M4tX01taDP3BldQcI6NSMlDf7WTDfuo0RvLlS0ekF6dPVslNyZhbFFBy2OBTB6Sa6+Q==} + peerDependencies: + sucrase: ^3 dependencies: - defer-to-connect: 1.0.2 + loader-utils: 1.4.0 + sucrase: 3.29.0 dev: true - /@szmarczak/http-timer/4.0.5: - resolution: {integrity: sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==} + /@svgr/babel-plugin-add-jsx-attribute/6.5.1_@babel+core@7.20.5: + resolution: {integrity: sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==} engines: {node: '>=10'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - defer-to-connect: 2.0.1 - dev: false + '@babel/core': 7.20.5 + dev: true - /@terminus-term/to-string-loader/1.1.7-beta.1: - resolution: {integrity: sha512-mYUDUYkEKpr/mS4LucALv4QKHsF8xWXcYChQdN2nZIXCoXJoBQFsQPSzdcAeCzbl/XDsyop/mI5vIA34RnDd0Q==} + /@svgr/babel-plugin-remove-jsx-attribute/6.5.0_@babel+core@7.20.5: + resolution: {integrity: sha512-8zYdkym7qNyfXpWvu4yq46k41pyNM9SOstoWhKlm+IfdCE1DdnRKeMUPsWIEO/DEkaWxJ8T9esNdG3QwQ93jBA==} + engines: {node: '>=10'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - loader-utils: 1.4.0 + '@babel/core': 7.20.5 dev: true - /@testing-library/dom/7.30.0: - resolution: {integrity: sha512-v4GzWtltaiDE0yRikLlcLAfEiiK8+ptu6OuuIebm9GdC2XlZTNDPGEfM2UkEtnH7hr9TRq2sivT5EA9P1Oy7bw==} + /@svgr/babel-plugin-remove-jsx-empty-expression/6.5.0_@babel+core@7.20.5: + resolution: {integrity: sha512-NFdxMq3xA42Kb1UbzCVxplUc0iqSyM9X8kopImvFnB+uSDdzIHOdbs1op8ofAvVRtbg4oZiyRl3fTYeKcOe9Iw==} engines: {node: '>=10'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/code-frame': 7.18.6 - '@babel/runtime': 7.20.6 - '@types/aria-query': 4.2.1 - aria-query: 4.2.2 - chalk: 4.1.2 - dom-accessibility-api: 0.5.13 - lz-string: 1.4.4 - pretty-format: 26.6.2 + '@babel/core': 7.20.5 dev: true - /@testing-library/dom/8.13.0: - resolution: {integrity: sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==} - engines: {node: '>=12'} + /@svgr/babel-plugin-replace-jsx-attribute-value/6.5.1_@babel+core@7.20.5: + resolution: {integrity: sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg==} + engines: {node: '>=10'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/code-frame': 7.18.6 - '@babel/runtime': 7.20.6 - '@types/aria-query': 4.2.1 - aria-query: 5.0.0 - chalk: 4.1.2 - dom-accessibility-api: 0.5.13 - lz-string: 1.4.4 - pretty-format: 27.5.1 + '@babel/core': 7.20.5 dev: true - /@testing-library/jest-dom/5.16.4: - resolution: {integrity: sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==} - engines: {node: '>=8', npm: '>=6', yarn: '>=1'} + /@svgr/babel-plugin-svg-dynamic-title/6.5.1_@babel+core@7.20.5: + resolution: {integrity: sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw==} + engines: {node: '>=10'} + peerDependencies: + '@babel/core': ^7.0.0-0 dependencies: - '@babel/runtime': 7.20.6 - '@types/testing-library__jest-dom': 5.9.5 - aria-query: 5.0.0 - chalk: 3.0.0 - css: 3.0.0 - css.escape: 1.5.1 - dom-accessibility-api: 0.5.13 - lodash: 4.17.21 - redent: 3.0.0 + '@babel/core': 7.20.5 dev: true - /@testing-library/react-hooks/8.0.0_i6xcbgvvylvakhe2evaee3dlf4: - resolution: {integrity: sha512-uZqcgtcUUtw7Z9N32W13qQhVAD+Xki2hxbTR461MKax8T6Jr8nsUvZB+vcBTkzY2nFvsUet434CsgF0ncW2yFw==} - engines: {node: '>=12'} + /@svgr/babel-plugin-svg-em-dimensions/6.5.1_@babel+core@7.20.5: + resolution: {integrity: sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA==} + engines: {node: '>=10'} peerDependencies: - '@types/react': ^16.9.0 || ^17.0.0 - react: ^16.9.0 || ^17.0.0 - react-dom: ^16.9.0 || ^17.0.0 - react-test-renderer: ^16.9.0 || ^17.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - react-dom: - optional: true - react-test-renderer: - optional: true + '@babel/core': ^7.0.0-0 dependencies: - '@babel/runtime': 7.20.6 - '@types/react': 18.0.8 - react: 18.1.0 - react-dom: 18.1.0_react@18.1.0 - react-error-boundary: 3.1.4_react@18.1.0 + '@babel/core': 7.20.5 dev: true - /@testing-library/react/13.4.0_ef5jwxihqo6n7gxfmzogljlgcm: - resolution: {integrity: sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==} - engines: {node: '>=12'} + /@svgr/babel-plugin-transform-react-native-svg/6.5.1_@babel+core@7.20.5: + resolution: {integrity: sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg==} + engines: {node: '>=10'} peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 + '@babel/core': ^7.0.0-0 dependencies: - '@babel/runtime': 7.20.6 - '@testing-library/dom': 8.13.0 - '@types/react-dom': 18.0.2 - react: 18.1.0 - react-dom: 18.1.0_react@18.1.0 + '@babel/core': 7.20.5 dev: true - /@testing-library/user-event/13.5.0_tlwynutqiyp5mns3woioasuxnq: - resolution: {integrity: sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==} - engines: {node: '>=10', npm: '>=6'} + /@svgr/babel-plugin-transform-svg-component/6.5.1_@babel+core@7.20.5: + resolution: {integrity: sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ==} + engines: {node: '>=12'} peerDependencies: - '@testing-library/dom': '>=7.21.4' + '@babel/core': ^7.0.0-0 dependencies: - '@babel/runtime': 7.20.6 - '@testing-library/dom': 8.13.0 + '@babel/core': 7.20.5 dev: true - /@tootallnate/once/1.1.2: - resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} - engines: {node: '>= 6'} + /@svgr/babel-preset/6.5.1_@babel+core@7.20.5: + resolution: {integrity: sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw==} + engines: {node: '>=10'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.5 + '@svgr/babel-plugin-add-jsx-attribute': 6.5.1_@babel+core@7.20.5 + '@svgr/babel-plugin-remove-jsx-attribute': 6.5.0_@babel+core@7.20.5 + '@svgr/babel-plugin-remove-jsx-empty-expression': 6.5.0_@babel+core@7.20.5 + '@svgr/babel-plugin-replace-jsx-attribute-value': 6.5.1_@babel+core@7.20.5 + '@svgr/babel-plugin-svg-dynamic-title': 6.5.1_@babel+core@7.20.5 + '@svgr/babel-plugin-svg-em-dimensions': 6.5.1_@babel+core@7.20.5 + '@svgr/babel-plugin-transform-react-native-svg': 6.5.1_@babel+core@7.20.5 + '@svgr/babel-plugin-transform-svg-component': 6.5.1_@babel+core@7.20.5 + dev: true + + /@svgr/core/6.5.1: + resolution: {integrity: sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.20.5 + '@svgr/babel-preset': 6.5.1_@babel+core@7.20.5 + '@svgr/plugin-jsx': 6.5.1_@svgr+core@6.5.1 + camelcase: 6.3.0 + cosmiconfig: 7.0.1 + transitivePeerDependencies: + - supports-color dev: true - /@tootallnate/once/2.0.0: - resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} - engines: {node: '>= 10'} - - /@trysound/sax/0.2.0: - resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} - engines: {node: '>=10.13.0'} + /@svgr/hast-util-to-babel-ast/6.5.1: + resolution: {integrity: sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw==} + engines: {node: '>=10'} + dependencies: + '@babel/types': 7.20.7 + entities: 4.4.0 dev: true - /@tsconfig/node10/1.0.8: - resolution: {integrity: sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==} + /@svgr/plugin-jsx/6.5.1_@svgr+core@6.5.1: + resolution: {integrity: sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw==} + engines: {node: '>=10'} + peerDependencies: + '@svgr/core': ^6.0.0 + dependencies: + '@babel/core': 7.20.5 + '@svgr/babel-preset': 6.5.1_@babel+core@7.20.5 + '@svgr/core': 6.5.1 + '@svgr/hast-util-to-babel-ast': 6.5.1 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color dev: true - /@tsconfig/node12/1.0.9: - resolution: {integrity: sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==} + /@svgr/plugin-svgo/6.5.1_@svgr+core@6.5.1: + resolution: {integrity: sha512-omvZKf8ixP9z6GWgwbtmP9qQMPX4ODXi+wzbVZgomNFsUIlHA1sf4fThdwTWSsZGgvGAG6yE+b/F5gWUkcZ/iQ==} + engines: {node: '>=10'} + peerDependencies: + '@svgr/core': '*' + dependencies: + '@svgr/core': 6.5.1 + cosmiconfig: 7.0.1 + deepmerge: 4.2.2 + svgo: 2.8.0 dev: true - /@tsconfig/node14/1.0.1: - resolution: {integrity: sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==} + /@svgr/rollup/6.5.1: + resolution: {integrity: sha512-GeUfq0grJfpcn2jRWRaZ4npn27nnWK21vUj6MqDqknuJnEqGADcZZjO9wrUAaPLr3InAnQi0Z7nwiNUdzkaj6A==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.20.5 + '@babel/plugin-transform-react-constant-elements': 7.20.2_@babel+core@7.20.5 + '@babel/preset-env': 7.20.2_@babel+core@7.20.5 + '@babel/preset-react': 7.18.6_@babel+core@7.20.5 + '@babel/preset-typescript': 7.18.6_@babel+core@7.20.5 + '@rollup/pluginutils': 4.2.1 + '@svgr/core': 6.5.1 + '@svgr/plugin-jsx': 6.5.1_@svgr+core@6.5.1 + '@svgr/plugin-svgo': 6.5.1_@svgr+core@6.5.1 + transitivePeerDependencies: + - supports-color dev: true - /@tsconfig/node16/1.0.2: - resolution: {integrity: sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==} + /@svgr/webpack/6.5.1: + resolution: {integrity: sha512-cQ/AsnBkXPkEK8cLbv4Dm7JGXq2XrumKnL1dRpJD9rIO2fTIlJI9a1uCciYG1F2aUsox/hJQyNGbt3soDxSRkA==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.20.5 + '@babel/plugin-transform-react-constant-elements': 7.20.2_@babel+core@7.20.5 + '@babel/preset-env': 7.20.2_@babel+core@7.20.5 + '@babel/preset-react': 7.18.6_@babel+core@7.20.5 + '@babel/preset-typescript': 7.18.6_@babel+core@7.20.5 + '@svgr/core': 6.5.1 + '@svgr/plugin-jsx': 6.5.1_@svgr+core@6.5.1 + '@svgr/plugin-svgo': 6.5.1_@svgr+core@6.5.1 + transitivePeerDependencies: + - supports-color dev: true - /@types/archy/0.0.32: - resolution: {integrity: sha512-5ZZ5+YGmUE01yejiXsKnTcvhakMZ2UllZlMsQni53Doc1JWhe21ia8VntRoRD6fAEWw08JBh/z9qQHJ+//MrIg==} + /@swc/core-darwin-arm64/1.3.27: + resolution: {integrity: sha512-IKlxkhEy99CnP9nduaf5IJWIFcr6D5cZCjYmCs7nWkjMV+aAieyDO9AX4LT8AcHy6CF7ByOX7SKoqk+gVMAaKw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true dev: true + optional: true - /@types/aria-query/4.2.1: - resolution: {integrity: sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg==} + /@swc/core-darwin-x64/1.3.27: + resolution: {integrity: sha512-MtabZIhFf/dL3vs6UMbd+vJsjIkm2NaFqulGV0Jofy2bfVZPTj/b5pXeOlUsTWy7JcH1uixjdx4RvJRyvqJxQA==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm-gnueabihf/1.3.27: + resolution: {integrity: sha512-XELMoGcUTAkk+G4buwIIhu6AIr1U418Odt22HUW8+ZvV+Wty2ICgR/myOIhM3xMb6U2L8ay+evMqoVNMQ0RRTg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-gnu/1.3.27: + resolution: {integrity: sha512-O6vtT6bnrVR9PzEIuA5U7tIfYo7bv97H9K9Vqy2oyHNeGN0H36DKwS4UqPreHtziXNF5+7ubdUYUkrG/j8UnUQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-musl/1.3.27: + resolution: {integrity: sha512-Oa0E1i7dOTWpaEZumKoNbTE/Ap+da6nlhqKVUdYrFDrOBi25tz76SdxZIyvAszzmgY89b5yd1naourKmkPXpww==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-gnu/1.3.27: + resolution: {integrity: sha512-S3v9H8oL2a8Ur6AjQyhkC6HfBVPOxKMdBhcZmdNuVgEUHbHdbf/Lka85F9IOYXEarMn0FtQw3ywowS22O9L5Uw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-musl/1.3.27: + resolution: {integrity: sha512-6DDkdXlOADpwICFZTRphCR+cIeS8aEYh4NlyzBito0mOWwIIdfCgALzhkTQOzTOkcD42bP97CIoZ97hqV/puOg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-arm64-msvc/1.3.27: + resolution: {integrity: sha512-baxfH4AbEcaTNo08wxV0W6hiMXwVCxPS4qc0amHpXPti92unvSqeDR1W3C9GjHqzXlWtmCRsq8Ww1pal6ZVLrw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-ia32-msvc/1.3.27: + resolution: {integrity: sha512-7iLJnH71k5qCwxv9NcM/P7nIEzTsC7r1sIiQW6bu+CpC8qZvwl0PS+XvQRlLly2gCZM+Le98tksYG14MEh+Hrw==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-x64-msvc/1.3.27: + resolution: {integrity: sha512-mFM907PDw/jrQ44+TRjIVGEOy2Mu06mMMz0HPMFuRsBzl5t0Kajp3vmn8FkkpS9wH5982VPi6hPYVTb7QJo5Qg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core/1.3.27: + resolution: {integrity: sha512-praRNgpeYGvwDIm/Cl6JU+yHMvwVraL0U6ejMgGyzvpcm1FVsZd1/EYXGqzbBJ0ALv7Gx4eK56h4GnwV6d4L0w==} + engines: {node: '>=10'} + requiresBuild: true + optionalDependencies: + '@swc/core-darwin-arm64': 1.3.27 + '@swc/core-darwin-x64': 1.3.27 + '@swc/core-linux-arm-gnueabihf': 1.3.27 + '@swc/core-linux-arm64-gnu': 1.3.27 + '@swc/core-linux-arm64-musl': 1.3.27 + '@swc/core-linux-x64-gnu': 1.3.27 + '@swc/core-linux-x64-musl': 1.3.27 + '@swc/core-win32-arm64-msvc': 1.3.27 + '@swc/core-win32-ia32-msvc': 1.3.27 + '@swc/core-win32-x64-msvc': 1.3.27 + dev: true + + /@swc/helpers/0.4.14: + resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==} + dependencies: + tslib: 2.1.0 + dev: true + + /@swc/jest/0.2.24_@swc+core@1.3.27: + resolution: {integrity: sha512-fwgxQbM1wXzyKzl1+IW0aGrRvAA8k0Y3NxFhKigbPjOJ4mCKnWEcNX9HQS3gshflcxq8YKhadabGUVfdwjCr6Q==} + engines: {npm: '>= 7.0.0'} + peerDependencies: + '@swc/core': '*' + dependencies: + '@jest/create-cache-key-function': 27.5.1 + '@swc/core': 1.3.27 + jsonc-parser: 3.2.0 + dev: true + + /@szmarczak/http-timer/1.1.2: + resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} + engines: {node: '>=6'} + dependencies: + defer-to-connect: 1.0.2 + dev: true + + /@szmarczak/http-timer/4.0.5: + resolution: {integrity: sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==} + engines: {node: '>=10'} + dependencies: + defer-to-connect: 2.0.1 + dev: false + + /@terminus-term/to-string-loader/1.1.7-beta.1: + resolution: {integrity: sha512-mYUDUYkEKpr/mS4LucALv4QKHsF8xWXcYChQdN2nZIXCoXJoBQFsQPSzdcAeCzbl/XDsyop/mI5vIA34RnDd0Q==} + dependencies: + loader-utils: 1.4.0 + dev: true + + /@testing-library/dom/7.30.0: + resolution: {integrity: sha512-v4GzWtltaiDE0yRikLlcLAfEiiK8+ptu6OuuIebm9GdC2XlZTNDPGEfM2UkEtnH7hr9TRq2sivT5EA9P1Oy7bw==} + engines: {node: '>=10'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/runtime': 7.20.6 + '@types/aria-query': 4.2.1 + aria-query: 4.2.2 + chalk: 4.1.2 + dom-accessibility-api: 0.5.13 + lz-string: 1.4.4 + pretty-format: 26.6.2 + dev: true + + /@testing-library/dom/8.13.0: + resolution: {integrity: sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==} + engines: {node: '>=12'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/runtime': 7.20.6 + '@types/aria-query': 4.2.1 + aria-query: 5.0.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.13 + lz-string: 1.4.4 + pretty-format: 27.5.1 + dev: true + + /@testing-library/jest-dom/5.16.4: + resolution: {integrity: sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==} + engines: {node: '>=8', npm: '>=6', yarn: '>=1'} + dependencies: + '@babel/runtime': 7.20.6 + '@types/testing-library__jest-dom': 5.9.5 + aria-query: 5.0.0 + chalk: 3.0.0 + css: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.5.13 + lodash: 4.17.21 + redent: 3.0.0 + dev: true + + /@testing-library/react-hooks/8.0.0_i6xcbgvvylvakhe2evaee3dlf4: + resolution: {integrity: sha512-uZqcgtcUUtw7Z9N32W13qQhVAD+Xki2hxbTR461MKax8T6Jr8nsUvZB+vcBTkzY2nFvsUet434CsgF0ncW2yFw==} + engines: {node: '>=12'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 + react: ^16.9.0 || ^17.0.0 + react-dom: ^16.9.0 || ^17.0.0 + react-test-renderer: ^16.9.0 || ^17.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-dom: + optional: true + react-test-renderer: + optional: true + dependencies: + '@babel/runtime': 7.20.6 + '@types/react': 18.0.8 + react: 18.1.0 + react-dom: 18.1.0_react@18.1.0 + react-error-boundary: 3.1.4_react@18.1.0 + dev: true + + /@testing-library/react/12.1.5_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg==} + engines: {node: '>=12'} + peerDependencies: + react: <18.0.0 + react-dom: <18.0.0 + dependencies: + '@babel/runtime': 7.20.6 + '@testing-library/dom': 8.13.0 + '@types/react-dom': 17.0.18 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: true + + /@testing-library/react/13.4.0_ef5jwxihqo6n7gxfmzogljlgcm: + resolution: {integrity: sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==} + engines: {node: '>=12'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@babel/runtime': 7.20.6 + '@testing-library/dom': 8.13.0 + '@types/react-dom': 18.0.2 + react: 18.1.0 + react-dom: 18.1.0_react@18.1.0 + dev: true + + /@testing-library/user-event/13.5.0_tlwynutqiyp5mns3woioasuxnq: + resolution: {integrity: sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + dependencies: + '@babel/runtime': 7.20.6 + '@testing-library/dom': 8.13.0 + dev: true + + /@testing-library/user-event/14.4.3_tlwynutqiyp5mns3woioasuxnq: + resolution: {integrity: sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + dependencies: + '@testing-library/dom': 8.13.0 + dev: true + + /@tootallnate/once/1.1.2: + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + dev: true + + /@tootallnate/once/2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + /@trysound/sax/0.2.0: + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + dev: true + + /@tsconfig/node10/1.0.8: + resolution: {integrity: sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==} + dev: true + + /@tsconfig/node12/1.0.9: + resolution: {integrity: sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==} + dev: true + + /@tsconfig/node14/1.0.1: + resolution: {integrity: sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==} + dev: true + + /@tsconfig/node16/1.0.2: + resolution: {integrity: sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==} + dev: true + + /@types/archy/0.0.32: + resolution: {integrity: sha512-5ZZ5+YGmUE01yejiXsKnTcvhakMZ2UllZlMsQni53Doc1JWhe21ia8VntRoRD6fAEWw08JBh/z9qQHJ+//MrIg==} + dev: true + + /@types/aria-query/4.2.1: + resolution: {integrity: sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg==} dev: true /@types/aws-lambda/8.10.106: @@ -7967,10 +9562,10 @@ packages: '@types/responselike': 1.0.0 dev: false - /@types/case-sensitive-paths-webpack-plugin/2.1.6_oo63t3us67g6w4zg54gqorhf2i: + /@types/case-sensitive-paths-webpack-plugin/2.1.6_jh3afgd4dccyz4n3zkk5zvv5cy: resolution: {integrity: sha512-1bk/krfgJ2bVPUusnmvWHg8Xwr/4I29yFxvZBFi5FZOshQzfcZ7XdutFHpYMs1w5RD319pjJbDk7J2ibWSW6QQ==} dependencies: - '@types/webpack': 5.28.0_oo63t3us67g6w4zg54gqorhf2i + '@types/webpack': 5.28.0_jh3afgd4dccyz4n3zkk5zvv5cy transitivePeerDependencies: - '@swc/core' - esbuild @@ -8069,6 +9664,11 @@ packages: /@types/d3-voronoi/1.1.9: resolution: {integrity: sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ==} + /@types/debug/4.1.7: + resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} + dependencies: + '@types/ms': 0.7.31 + /@types/escape-html/1.0.1: resolution: {integrity: sha512-4mI1FuUUZiuT95fSVqvZxp/ssQK9zsa86S43h9x3zPOSU9BBJ+BfDkXwuaU7BfsD+e7U0/cUUfJFk3iW2M4okA==} dev: true @@ -8076,18 +9676,25 @@ packages: /@types/eslint-scope/3.7.3: resolution: {integrity: sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==} dependencies: - '@types/eslint': 7.2.13 - '@types/estree': 0.0.51 + '@types/eslint': 8.4.10 + '@types/estree': 1.0.0 - /@types/eslint/7.2.13: - resolution: {integrity: sha512-LKmQCWAlnVHvvXq4oasNUMTJJb2GwSyTY8+1C7OH5ILR8mPLaljv1jxL1bXW3xB3jFbQxTKxJAvI8PyjB09aBg==} + /@types/eslint/8.4.10: + resolution: {integrity: sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==} dependencies: - '@types/estree': 0.0.51 + '@types/estree': 1.0.0 '@types/json-schema': 7.0.11 + /@types/estree/0.0.39: + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + dev: true + /@types/estree/0.0.51: resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} + /@types/estree/1.0.0: + resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} + /@types/events/1.2.0: resolution: {integrity: sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==} dev: true @@ -8167,7 +9774,6 @@ packages: resolution: {integrity: sha512-viwwrB+6xGzw+G1eWpF9geV3fnsDgXqHG+cqgiHrvQfDUW5hzhCyV7Sy3UJxhfRFBsgky2SSW33qi/YrIkjX5Q==} dependencies: '@types/unist': 2.0.3 - dev: true /@types/highlight.js/9.12.4: resolution: {integrity: sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==} @@ -8177,6 +9783,12 @@ packages: resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} dev: true + /@types/hoist-non-react-statics/3.3.1: + resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==} + dependencies: + '@types/react': 18.0.8 + hoist-non-react-statics: 3.3.2 + /@types/html-minifier-terser/5.1.1: resolution: {integrity: sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA==} dev: true @@ -8219,7 +9831,7 @@ packages: /@types/jest-image-snapshot/4.3.0: resolution: {integrity: sha512-gb6zF1ICfvzBsQYMTq2qFhhiI46Cab/t5PtLK4Z3mpbyQoyKI2HgCFDi71iE7rjs6TrIPnsf2GXw+mnGvZSgrA==} dependencies: - '@types/jest': 28.1.0 + '@types/jest': 29.2.5 '@types/pixelmatch': 5.2.3 ssim.js: 3.3.2 dev: true @@ -8227,11 +9839,11 @@ packages: /@types/jest-specific-snapshot/0.5.4: resolution: {integrity: sha512-1qISn4fH8wkOOPFEx+uWRRjw6m/pP/It3OHLm8Ee1KQpO7Z9ZGYDtWPU5AgK05UXsNTAgOK+dPQvJKGdy9E/1g==} dependencies: - '@types/jest': 28.1.0 + '@types/jest': 29.2.5 dev: true - /@types/jest/26.0.20: - resolution: {integrity: sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA==} + /@types/jest/26.0.24: + resolution: {integrity: sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==} dependencies: jest-diff: 26.6.2 pretty-format: 26.6.2 @@ -8244,8 +9856,18 @@ packages: pretty-format: 27.5.1 dev: true + /@types/jest/29.2.5: + resolution: {integrity: sha512-H2cSxkKgVmqNHXP7TC2L/WUorrZu8ZigyRywfVzv6EyBlxj39n4C00hjXYQWsbwqgElaj/CiAeSRmk5GoaKTgw==} + dependencies: + expect: 29.3.1 + pretty-format: 29.3.1 + dev: true + /@types/js-cookie/2.2.6: resolution: {integrity: sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==} + + /@types/js-levenshtein/1.1.1: + resolution: {integrity: sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g==} dev: true /@types/js-yaml/4.0.3: @@ -8272,6 +9894,14 @@ packages: '@types/tough-cookie': 2.3.5 dev: true + /@types/jsdom/20.0.1: + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + dependencies: + '@types/node': 18.8.3 + '@types/tough-cookie': 2.3.5 + parse5: 7.1.2 + dev: true + /@types/json-schema/7.0.11: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} @@ -8317,7 +9947,6 @@ packages: resolution: {integrity: sha512-SXPBMnFVQg1s00dlMCc/jCdvPqdE4mXaMMCeRlxLDmTAEoegHT53xKtkDnzDTOcmMHUfcjyf36/YYZ6SxRdnsw==} dependencies: '@types/unist': 2.0.3 - dev: true /@types/mime-types/2.1.0: resolution: {integrity: sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM=} @@ -8348,6 +9977,9 @@ packages: resolution: {integrity: sha1-qvOIoerTsPXtbcFhGVbqe0ClfTw=} dev: true + /@types/ms/0.7.31: + resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} + /@types/mz/2.7.3: resolution: {integrity: sha512-Zp1NUJ4Alh3gaun0a5rkF3DL7b2j1WB6rPPI5h+CJ98sQnxe9qwskClvupz/4bqChGR3L/BRhTjlaOwR+uiZJg==} dependencies: @@ -8428,8 +10060,8 @@ packages: /@types/pretty-hrtime/1.0.0: resolution: {integrity: sha512-xl+5r2rcrxdLViAYkkiLMYsoUs3qEyrAnHFyEzYysgRxdVp3WbhysxIvJIxZp9FvZ2CYezh0TaHZorivH+voOQ==} - /@types/prop-types/15.5.6: - resolution: {integrity: sha512-ZBFR7TROLVzCkswA3Fmqq+IIJt62/T7aY/Dmz+QkU7CaW2QFqAitCE8Ups7IzmGhcN1YWMBT4Qcoc07jU9hOJQ==} + /@types/prop-types/15.7.5: + resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} /@types/puppeteer/5.4.5: resolution: {integrity: sha512-lxCjpDEY+DZ66+W3x5Af4oHnEmUXt0HuaRzkBGE2UZiZEp/V1d3StpLPlmNVu/ea091bdNmVPl44lu8Wy/0ZCA==} @@ -8459,6 +10091,12 @@ packages: '@types/react': 18.0.8 dev: true + /@types/react-dom/17.0.18: + resolution: {integrity: sha512-rLVtIfbwyur2iFKykP2w0pl/1unw26b5td16d5xMgp7/yjTHomkyxPYChFoCr/FtEX1lN9wY6lFj1qvKdS5kDw==} + dependencies: + '@types/react': 17.0.43 + dev: true + /@types/react-dom/18.0.2: resolution: {integrity: sha512-UxeS+Wtj5bvLRREz9tIgsK4ntCuLDo0EcAcACgw3E+9wE8ePDr9uQpq53MfcyxyIS55xJ+0B6mDS8c4qkkHLBg==} dependencies: @@ -8483,6 +10121,14 @@ packages: '@types/react': 18.0.8 dev: false + /@types/react-redux/7.1.25: + resolution: {integrity: sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==} + dependencies: + '@types/hoist-non-react-statics': 3.3.1 + '@types/react': 18.0.8 + hoist-non-react-statics: 3.3.2 + redux: 4.2.0 + /@types/react-resizable/3.0.2: resolution: {integrity: sha512-4rHjZDQmSpFqRlNzlcnF5tpOG5fBcMuDlvD+qT3XHAJLKGx/FC3iDQ9li9tHW53ecWwZzHTPCGvz5vNWQN+v/Q==} dependencies: @@ -8504,18 +10150,32 @@ packages: '@types/react': 18.0.8 dev: true + /@types/react-sparklines/1.7.2: + resolution: {integrity: sha512-N1GwO7Ri5C5fE8+CxhiDntuSw1qYdGytBuedKrCxWpaojXm4WnfygbdBdc5sXGX7feMxDXBy9MNhxoUTwrMl4A==} + dependencies: + '@types/react': 18.0.8 + + /@types/react-text-truncate/0.14.1: + resolution: {integrity: sha512-yCtOOOJzrsfWF6TbnTDZz0gM5JYOxJmewExaTJTv01E7yrmpkNcmVny2fAtsNgSFCp8k2VgCePBoIvFBpKyEOw==} + dependencies: + '@types/react': 18.0.8 + + /@types/react-transition-group/4.4.5: + resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==} + dependencies: + '@types/react': 18.0.8 + /@types/react/17.0.43: resolution: {integrity: sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A==} dependencies: - '@types/prop-types': 15.5.6 + '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.2 csstype: 3.1.0 - dev: false /@types/react/18.0.8: resolution: {integrity: sha512-+j2hk9BzCOrrOSJASi5XiOyBbERk9jG5O73Ya4M0env5Ixi6vUNli4qy994AINcEF+1IEHISYFfIT4zwr++LKw==} dependencies: - '@types/prop-types': 15.5.6 + '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.2 csstype: 3.1.0 @@ -8530,6 +10190,12 @@ packages: resolution: {integrity: sha512-rPvqs+1hL/5hbES/0HTdUu4lvNmneiwKwccbWe7HGLWbnsLdqKnQHyWLg4Pj0AMO7PLHCwBM1Cs8orChdkDONg==} dev: true + /@types/resolve/1.17.1: + resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} + dependencies: + '@types/node': 18.8.3 + dev: true + /@types/responselike/1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: @@ -8581,6 +10247,12 @@ packages: '@types/mime': 2.0.0 '@types/node': 18.8.3 + /@types/set-cookie-parser/2.4.2: + resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} + dependencies: + '@types/node': 18.8.3 + dev: true + /@types/shelljs/0.8.8: resolution: {integrity: sha512-lD3LWdg6j8r0VRBFahJVaxoW0SIcswxKaFUrmKl33RJVeeoNYQAz4uqCJ5Z6v4oIBOsC5GozX+I5SorIKiTcQA==} dependencies: @@ -8623,10 +10295,10 @@ packages: dependencies: '@types/node': 18.8.3 - /@types/speed-measure-webpack-plugin/1.3.4_oo63t3us67g6w4zg54gqorhf2i: + /@types/speed-measure-webpack-plugin/1.3.4_jh3afgd4dccyz4n3zkk5zvv5cy: resolution: {integrity: sha512-bTV+ZctiMPqufZXEnfkNL4DgXzgkq0AnN2hnwqfEZn5ZqqxbGi55Rtp3vrnr8U5jgFJoYFONQCCkGPWsLOT2Sg==} dependencies: - '@types/webpack': 5.28.0_oo63t3us67g6w4zg54gqorhf2i + '@types/webpack': 5.28.0_jh3afgd4dccyz4n3zkk5zvv5cy transitivePeerDependencies: - '@swc/core' - esbuild @@ -8638,6 +10310,11 @@ packages: resolution: {integrity: sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==} dev: true + /@types/styled-jsx/2.2.9: + resolution: {integrity: sha512-W/iTlIkGEyTBGTEvZCey8EgQlQ5l0DwMqi3iOXlLs2kyBwYTXHKEiU6IZ5EwoRwngL8/dGYuzezSup89ttVHLw==} + dependencies: + '@types/react': 18.0.8 + /@types/svgo/2.6.0: resolution: {integrity: sha512-VSdhb3KTOglle1SLQD4+TB6ezj/MS3rN98gOUkXzbTUhG8VjFKHXN3OVgEFlTnW5fYBxt+lzZlD3PFqkwMj36Q==} dependencies: @@ -8651,7 +10328,7 @@ packages: /@types/testing-library__jest-dom/5.9.5: resolution: {integrity: sha512-ggn3ws+yRbOHog9GxnXiEZ/35Mow6YtPZpd7Z5mKDeZS/o7zx3yAle0ov/wjhVB5QT4N2Dt+GNoGCdqkBGCajQ==} dependencies: - '@types/jest': 28.1.0 + '@types/jest': 29.2.5 dev: true /@types/tough-cookie/2.3.5: @@ -8671,7 +10348,6 @@ packages: /@types/unist/2.0.3: resolution: {integrity: sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==} - dev: true /@types/uuid/8.0.1: resolution: {integrity: sha512-2kE8rEFgJpbBAPw5JghccEevQb0XVU0tewF/8h7wPQTeCtoJ6h8qmBIwuzUVm2MutmzC/cpCkwxudixoNYDp1A==} @@ -8696,12 +10372,12 @@ packages: resolution: {integrity: sha512-Z+ZqjRcnGfHP86dvx/BtSwWyZPKQ/LBdmAVImY82TphyjOw2KgTKcp7Nx92oNwCTsHzlshwexAG/WiY2JuUm3g==} dev: true - /@types/webpack-bundle-analyzer/4.6.0_oo63t3us67g6w4zg54gqorhf2i: + /@types/webpack-bundle-analyzer/4.6.0_jh3afgd4dccyz4n3zkk5zvv5cy: resolution: {integrity: sha512-XeQmQCCXdZdap+A/60UKmxW5Mz31Vp9uieGlHB3T4z/o2OLVLtTI3bvTuS6A2OWd/rbAAQiGGWIEFQACu16szA==} dependencies: '@types/node': 18.8.3 - tapable: 2.2.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + tapable: 2.2.1 + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy transitivePeerDependencies: - '@swc/core' - esbuild @@ -8712,10 +10388,10 @@ packages: /@types/webpack-env/1.18.0: resolution: {integrity: sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg==} - /@types/webpack-stats-plugin/0.3.2_oo63t3us67g6w4zg54gqorhf2i: + /@types/webpack-stats-plugin/0.3.2_jh3afgd4dccyz4n3zkk5zvv5cy: resolution: {integrity: sha512-wvIJrybgcuNZ+5sr99RAqr1FmC9tRjAyws7x3jPFHZxtyO+c5n/VbFZATKW5Wec3LOpEuLSFlfrmFF8TALAmwQ==} dependencies: - '@types/webpack': 5.28.0_oo63t3us67g6w4zg54gqorhf2i + '@types/webpack': 5.28.0_jh3afgd4dccyz4n3zkk5zvv5cy transitivePeerDependencies: - '@swc/core' - esbuild @@ -8723,12 +10399,12 @@ packages: - webpack-cli dev: true - /@types/webpack/5.28.0_oo63t3us67g6w4zg54gqorhf2i: + /@types/webpack/5.28.0_jh3afgd4dccyz4n3zkk5zvv5cy: resolution: {integrity: sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w==} dependencies: '@types/node': 18.8.3 - tapable: 2.2.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + tapable: 2.2.1 + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy transitivePeerDependencies: - '@swc/core' - esbuild @@ -9278,7 +10954,7 @@ packages: webpack: 5.x.x webpack-cli: 5.x.x dependencies: - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-cli: 5.0.1_y7ttplitmkohdpgkllksfboxwa /@webpack-cli/info/2.0.1_rjsyjcrmk25kqsjzwkvj3a2evq: @@ -9288,7 +10964,7 @@ packages: webpack: 5.x.x webpack-cli: 5.x.x dependencies: - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-cli: 5.0.1_y7ttplitmkohdpgkllksfboxwa /@webpack-cli/serve/2.0.1_ewykyfxtgmraekx43xa23ld4wa: @@ -9302,7 +10978,7 @@ packages: webpack-dev-server: optional: true dependencies: - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-cli: 5.0.1_y7ttplitmkohdpgkllksfboxwa webpack-dev-server: 4.11.1_rjsyjcrmk25kqsjzwkvj3a2evq @@ -9343,12 +11019,32 @@ packages: dependencies: tslib: 2.1.0 + /@xmldom/xmldom/0.8.6: + resolution: {integrity: sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg==} + engines: {node: '>=10.0.0'} + dev: true + + /@xobotyi/scrollbar-width/1.9.5: + resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} + /@xtuc/ieee754/1.2.0: resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} /@xtuc/long/4.2.2: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + /@yarnpkg/lockfile/1.1.0: + resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} + dev: true + + /@yarnpkg/parsers/3.0.0-rc.35: + resolution: {integrity: sha512-J6ySgEdQUqAmlttvZOoXOEsrDTAnHyR/MtEvuAG5a+gwKY/2Cc7xn4CWcpgfuwkp+0a4vXmt2BDwzacDoGDN1g==} + engines: {node: '>=14.15.0'} + dependencies: + js-yaml: 3.14.1 + tslib: 2.1.0 + dev: true + /@zkochan/js-yaml/0.0.6: resolution: {integrity: sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg==} hasBin: true @@ -9371,6 +11067,12 @@ packages: isexe: 2.0.0 dev: true + /@zxing/text-encoding/0.9.0: + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + requiresBuild: true + dev: true + optional: true + /abab/2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} dev: true @@ -9403,12 +11105,19 @@ packages: acorn-walk: 7.2.0 dev: true - /acorn-import-assertions/1.8.0_acorn@8.8.0: + /acorn-globals/7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + dependencies: + acorn: 8.8.1 + acorn-walk: 8.2.0 + dev: true + + /acorn-import-assertions/1.8.0_acorn@8.8.1: resolution: {integrity: sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==} peerDependencies: acorn: ^8 dependencies: - acorn: 8.8.0 + acorn: 8.8.1 /acorn-jsx/5.3.2_acorn@7.4.1: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -9416,13 +11125,14 @@ packages: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: acorn: 7.4.1 + dev: true - /acorn-jsx/5.3.2_acorn@8.8.0: + /acorn-jsx/5.3.2_acorn@8.8.1: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.8.0 + acorn: 8.8.1 /acorn-walk/7.2.0: resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} @@ -9437,9 +11147,10 @@ packages: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} engines: {node: '>=0.4.0'} hasBin: true + dev: true - /acorn/8.8.0: - resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==} + /acorn/8.8.1: + resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} engines: {node: '>=0.4.0'} hasBin: true @@ -9474,20 +11185,20 @@ packages: /airbnb-js-shims/2.2.1: resolution: {integrity: sha512-wJNXPH66U2xjgo1Zwyjf9EydvJ2Si94+vSdk6EERcBfB2VZkeltpqIats0cqIZMLCXP3zcyaUKGYQeIBT6XjsQ==} dependencies: - array-includes: 3.1.5 + array-includes: 3.1.6 array.prototype.flat: 1.3.0 - array.prototype.flatmap: 1.2.4 + array.prototype.flatmap: 1.3.1 es5-shim: 4.5.13 es6-shim: 0.35.5 function.prototype.name: 1.1.5 - globalthis: 1.0.1 - object.entries: 1.1.2 - object.fromentries: 2.0.2 + globalthis: 1.0.3 + object.entries: 1.1.6 + object.fromentries: 2.0.6 object.getownpropertydescriptors: 2.1.4 - object.values: 1.1.5 + object.values: 1.1.6 promise.allsettled: 1.0.2 promise.prototype.finally: 3.1.0 - string.prototype.matchall: 4.0.2 + string.prototype.matchall: 4.0.8 string.prototype.padend: 3.0.0 string.prototype.padstart: 3.0.0 symbol.prototype.description: 1.0.0 @@ -9563,6 +11274,7 @@ packages: /ansi-colors/4.1.1: resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} engines: {node: '>=6'} + dev: true /ansi-escapes/3.2.0: resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} @@ -9613,6 +11325,10 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + /ansi-regex/6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + /ansi-styles/2.2.1: resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} engines: {node: '>=0.10.0'} @@ -9783,13 +11499,13 @@ packages: /array-flatten/2.1.2: resolution: {integrity: sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==} - /array-includes/3.1.5: - resolution: {integrity: sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==} + /array-includes/3.1.6: + resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.1 + es-abstract: 1.21.1 get-intrinsic: 1.1.3 is-string: 1.0.7 @@ -9853,25 +11569,25 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.1 + es-abstract: 1.21.1 es-shim-unscopables: 1.0.0 dev: true - /array.prototype.flatmap/1.2.4: - resolution: {integrity: sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==} + /array.prototype.flatmap/1.3.1: + resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.1 - function-bind: 1.1.1 + es-abstract: 1.21.1 + es-shim-unscopables: 1.0.0 /array.prototype.map/1.0.2: resolution: {integrity: sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==} engines: {node: '>= 0.4'} dependencies: define-properties: 1.1.4 - es-abstract: 1.20.1 + es-abstract: 1.21.1 es-array-method-boxes-properly: 1.0.0 is-string: 1.0.7 dev: true @@ -9882,11 +11598,20 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.1 + es-abstract: 1.21.1 es-array-method-boxes-properly: 1.0.0 is-string: 1.0.7 dev: true + /array.prototype.tosorted/1.1.1: + resolution: {integrity: sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.21.1 + es-shim-unscopables: 1.0.0 + get-intrinsic: 1.1.3 + /arrify/1.0.1: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} @@ -9900,6 +11625,15 @@ packages: /asap/2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + /asn1.js/5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + dependencies: + bn.js: 4.12.0 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + dev: true + /asn1js/3.0.5: resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} engines: {node: '>=12.0.0'} @@ -9909,6 +11643,13 @@ packages: tslib: 2.1.0 dev: true + /assert/1.5.0: + resolution: {integrity: sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==} + dependencies: + object-assign: 4.1.1 + util: 0.10.3 + dev: true + /assign-symbols/1.0.0: resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} engines: {node: '>=0.10.0'} @@ -9936,6 +11677,7 @@ packages: /astral-regex/2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} + dev: true /async-done/1.3.1: resolution: {integrity: sha512-R1BaUeJ4PMoLNJuk+0tLJgjmEqVsdN118+Z8O+alhnQDQgy0kmD5Mqi0DNEmMx2LM0Ed5yekKu+ZXYvIHceicg==} @@ -10019,7 +11761,6 @@ packages: /available-typed-arrays/1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} - dev: false /axe-core/4.2.1: resolution: {integrity: sha512-evY7DN8qSIbsW2H/TWQ1bX3sXN1d4MNb5Vb4n7BzPuCwRHdkZ1H2eNLuSh73EoQqkGKUtju2G2HCcjCfhvZIAA==} @@ -10076,6 +11817,24 @@ packages: - supports-color dev: true + /babel-jest/29.3.1_@babel+core@7.20.5: + resolution: {integrity: sha512-aard+xnMoxgjwV70t0L6wkW/3HQQtV+O0PEimxKgzNqCJnbYmroPojdP2tqKSOAt8QAKV/uSZU8851M7B5+fcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.20.5 + '@jest/transform': 29.3.1 + '@types/babel__core': 7.1.20 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.2.0_@babel+core@7.20.5 + chalk: 4.1.2 + graceful-fs: 4.2.10 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /babel-loader/8.2.5_ztqwsvkb6z73luspkai6ilstpu: resolution: {integrity: sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==} engines: {node: '>= 8.9'} @@ -10088,7 +11847,7 @@ packages: loader-utils: 2.0.4 make-dir: 3.1.0 schema-utils: 2.7.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy /babel-loader/9.1.0_ztqwsvkb6z73luspkai6ilstpu: resolution: {integrity: sha512-Antt61KJPinUMwHwIIz9T5zfMgevnfZkEVWYDWlG888fgdvRRGD0JTuf/fFozQnfT+uq64sk1bmdHDy/mOEWnA==} @@ -10100,7 +11859,7 @@ packages: '@babel/core': 7.20.5 find-cache-dir: 3.3.2 schema-utils: 4.0.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /babel-plugin-add-react-displayname/0.0.5: @@ -10146,12 +11905,22 @@ packages: '@types/babel__traverse': 7.0.15 dev: true + /babel-plugin-jest-hoist/29.2.0: + resolution: {integrity: sha512-TnspP2WNiR3GLfCsUNHqeXw0RoQ2f9U5hQ5L3XFpwuO8htQmSrhh8qsB6vi5Yi8+kuynN1yjDjQsPfkebmB6ZA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.18.10 + '@babel/types': 7.20.7 + '@types/babel__core': 7.1.20 + '@types/babel__traverse': 7.0.15 + dev: true + /babel-plugin-lodash/3.3.4: resolution: {integrity: sha512-yDZLjK7TCkWl1gpBeBGmuaDIFhZKmkoL+Cu2MUUjv5VxUZx/z7tBGBCBcQs5RI1Bkz5LLmNdjx7paOyQtMovyg==} dependencies: '@babel/helper-module-imports': 7.18.6 '@babel/types': 7.20.7 - glob: 7.1.7 + glob: 7.2.3 lodash: 4.17.21 require-package-name: 2.0.1 dev: true @@ -10296,6 +12065,17 @@ packages: babel-preset-current-node-syntax: 1.0.1_@babel+core@7.20.5 dev: true + /babel-preset-jest/29.2.0_@babel+core@7.20.5: + resolution: {integrity: sha512-z9JmMJppMxNv8N7fNRHvhMg9cvIkMxQBXgFkane3yKVEvEOP+kB50lk8DFRvF9PGqbyXxlmebKWhuDORO8RgdA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.5 + babel-plugin-jest-hoist: 29.2.0 + babel-preset-current-node-syntax: 1.0.1_@babel+core@7.20.5 + dev: true + /bach/1.2.0: resolution: {integrity: sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg==} engines: {node: '>= 0.10'} @@ -10321,6 +12101,9 @@ packages: resolution: {integrity: sha512-1X8CnjFVQ+a+KW36uBNMTU5s8+v5FzeqrP7hTG5aTb4aPreSbZJlhwPon9VKMuEVgV++JM+SQrALY3kr7eswdg==} dev: true + /bail/2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + /balanced-match/0.4.2: resolution: {integrity: sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg==} dev: false @@ -10364,7 +12147,7 @@ packages: dev: true /batch/0.6.1: - resolution: {integrity: sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=} + resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} /before-after-hook/2.2.2: resolution: {integrity: sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==} @@ -10385,6 +12168,16 @@ packages: open: 7.3.0 dev: true + /bfj/7.0.2: + resolution: {integrity: sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==} + engines: {node: '>= 8.0.0'} + dependencies: + bluebird: 3.7.2 + check-types: 11.2.2 + hoopy: 0.1.4 + tryer: 1.0.1 + dev: true + /big-integer/1.6.51: resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} engines: {node: '>=0.6'} @@ -10443,6 +12236,14 @@ packages: resolution: {integrity: sha512-j4nzWIqEFpLSbdhUApHRGDwfXbV8ALhqOn+FY5L6XBdKPAXU9BpGgFSbDsgqogfqPPR9R2WooseWCsfhfEC6uQ==} dev: true + /bn.js/4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + dev: true + + /bn.js/5.2.1: + resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} + dev: true + /body-parser/1.20.1: resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -10481,6 +12282,10 @@ packages: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} dev: true + /boolean/3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + dev: true + /bottleneck/2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} dev: true @@ -10544,6 +12349,12 @@ packages: balanced-match: 1.0.0 concat-map: 0.0.1 + /brace-expansion/2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.0 + dev: true + /braces/2.3.2: resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} engines: {node: '>=0.10.0'} @@ -10567,6 +12378,10 @@ packages: dependencies: fill-range: 7.0.1 + /brorand/1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + dev: true + /browser-assert/1.2.1: resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==} dev: true @@ -10579,6 +12394,61 @@ packages: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} dev: true + /browserify-aes/1.2.0: + resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} + dependencies: + buffer-xor: 1.0.3 + cipher-base: 1.0.4 + create-hash: 1.2.0 + evp_bytestokey: 1.0.3 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /browserify-cipher/1.0.1: + resolution: {integrity: sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==} + dependencies: + browserify-aes: 1.2.0 + browserify-des: 1.0.2 + evp_bytestokey: 1.0.3 + dev: true + + /browserify-des/1.0.2: + resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==} + dependencies: + cipher-base: 1.0.4 + des.js: 1.0.1 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /browserify-rsa/4.1.0: + resolution: {integrity: sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==} + dependencies: + bn.js: 5.2.1 + randombytes: 2.1.0 + dev: true + + /browserify-sign/4.2.1: + resolution: {integrity: sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==} + dependencies: + bn.js: 5.2.1 + browserify-rsa: 4.1.0 + create-hash: 1.2.0 + create-hmac: 1.1.7 + elliptic: 6.5.4 + inherits: 2.0.4 + parse-asn1: 5.1.6 + readable-stream: 3.6.0 + safe-buffer: 5.2.1 + dev: true + + /browserify-zlib/0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + dependencies: + pako: 1.0.11 + dev: true + /browserslist/4.21.4: resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -10603,7 +12473,7 @@ packages: dev: true /buffer-equal-constant-time/1.0.1: - resolution: {integrity: sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=} + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} dev: true /buffer-equal/1.0.0: @@ -10619,6 +12489,18 @@ packages: engines: {node: '>=0.10'} dev: true + /buffer-xor/1.0.3: + resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} + dev: true + + /buffer/4.9.2: + resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + isarray: 1.0.0 + dev: true + /buffer/5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -10644,7 +12526,6 @@ packages: /builtin-status-codes/3.0.0: resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} - dev: false /bundlesize2/0.0.31: resolution: {integrity: sha512-MdzJW/u+n+0jH0Uz78g8WENHAW7QNUdLD/c8aLuPB/aCIwt52zMJ4fc2fBU2y1K2iMwE/9+JoR8ojsAF0r0Xjw==} @@ -10656,7 +12537,7 @@ packages: commander: 5.1.0 cosmiconfig: 5.2.1 figures: 3.2.0 - glob: 7.1.7 + glob: 7.2.3 gzip-size: 5.1.1 node-fetch: 2.6.7 plur: 4.0.0 @@ -10691,7 +12572,7 @@ packages: '@npmcli/move-file': 1.0.1 chownr: 2.0.0 fs-minipass: 2.1.0 - glob: 7.1.7 + glob: 7.2.3 infer-owner: 1.0.4 lru-cache: 6.0.0 minipass: 3.3.5 @@ -10703,7 +12584,7 @@ packages: promise-inflight: 1.0.1 rimraf: 3.0.2 ssri: 8.0.1 - tar: 6.1.11 + tar: 6.1.13 unique-filename: 1.1.1 transitivePeerDependencies: - bluebird @@ -10890,6 +12771,9 @@ packages: resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==} dev: true + /ccount/2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + /chainsaw/0.1.0: resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} dependencies: @@ -10914,9 +12798,17 @@ packages: escape-string-regexp: 1.0.5 supports-color: 5.5.0 - /chalk/3.0.0: - resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} - engines: {node: '>=8'} + /chalk/3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chalk/4.1.1: + resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==} + engines: {node: '>=10'} dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 @@ -10983,15 +12875,15 @@ packages: /character-entities-legacy/1.1.2: resolution: {integrity: sha512-9NB2VbXtXYWdXzqrvAHykE/f0QJxzaKIpZ5QzNZrrgQ7Iyxr2vnfS8fCBNVW9nUEZE0lo57nxKRqnzY/dKrwlA==} - dev: true /character-entities/1.2.2: resolution: {integrity: sha512-sMoHX6/nBiy3KKfC78dnEalnpn0Az0oSNvqUWYTtYrhRI5iUIYsROU48G+E+kMFQzqXaJ8kHJZ85n7y6/PHgwQ==} - dev: true + + /character-entities/2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} /character-reference-invalid/1.1.2: resolution: {integrity: sha512-7I/xceXfKyUJmSAn/jw8ve/9DyOP7XxufNYLI9Px7CmsKgEUaZLUTax6nZxGQtaoiZCjpu6cHPj20xC/vqRReQ==} - dev: true /chardet/0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} @@ -11001,6 +12893,10 @@ packages: resolution: {integrity: sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=} dev: true + /check-types/11.2.2: + resolution: {integrity: sha512-HBiYvXvn9Z70Z88XKjz3AEKd4HJhBXsa3j7xFnITAzoS8+q6eIGi8qDB8FKPBAjtuxjI/zFpwuiCb8oDtKOYrA==} + dev: true + /cheerio-select/2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} dependencies: @@ -11021,7 +12917,7 @@ packages: domhandler: 5.0.3 domutils: 3.0.1 htmlparser2: 8.0.1 - parse5: 7.0.0 + parse5: 7.1.2 parse5-htmlparser2-tree-adapter: 7.0.0 dev: true @@ -11165,6 +13061,13 @@ packages: /ci-info/3.3.1: resolution: {integrity: sha512-SXgeMX9VwDe7iFFaEWkA5AstuER9YKqy4EhHqr4DVqkwmD9rpVimkMKWHdjn30Ja45txyjhSn63lVX69eVCckg==} + /cipher-base/1.0.4: + resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + /cjs-module-lexer/1.2.2: resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} dev: true @@ -11180,7 +13083,6 @@ packages: /classnames/2.3.1: resolution: {integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==} - dev: false /clean-css/4.2.3: resolution: {integrity: sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==} @@ -11381,7 +13283,6 @@ packages: /clsx/1.2.1: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} engines: {node: '>=6'} - dev: false /co/4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} @@ -11455,7 +13356,7 @@ packages: color-name: 1.1.4 /color-name/1.1.3: - resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=} + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} /color-name/1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -11511,7 +13412,9 @@ packages: resolution: {integrity: sha512-Cg90/fcK93n0ecgYTAz1jaA3zvnQ0ExlmKY1rdbyHqAx6BHxwoJc+J7HDu0iuQ7ixEs1qaa+WyQ6oeuBpYP1iA==} dependencies: trim: 0.0.1 - dev: true + + /comma-separated-tokens/2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} /command-exists/1.2.9: resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} @@ -11600,7 +13503,7 @@ packages: dependencies: schema-utils: 4.0.0 serialize-javascript: 6.0.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /compression/1.7.4: @@ -11617,6 +13520,23 @@ packages: transitivePeerDependencies: - supports-color + /compute-gcd/1.2.1: + resolution: {integrity: sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==} + dependencies: + validate.io-array: 1.0.6 + validate.io-function: 1.0.2 + validate.io-integer-array: 1.0.0 + dev: true + + /compute-lcm/1.1.2: + resolution: {integrity: sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==} + dependencies: + compute-gcd: 1.2.1 + validate.io-array: 1.0.6 + validate.io-function: 1.0.2 + validate.io-integer-array: 1.0.0 + dev: true + /compute-scroll-into-view/1.0.17: resolution: {integrity: sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==} dev: false @@ -11639,6 +13559,12 @@ packages: typedarray: 0.0.6 dev: true + /concat-with-sourcemaps/1.1.0: + resolution: {integrity: sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==} + dependencies: + source-map: 0.6.1 + dev: true + /configstore/4.0.0: resolution: {integrity: sha512-CmquAXFBocrzaSM8mtGPMM/HiWmyIpr4CcJl/rgY2uCObZ/S7cKU0silxslqJejl+t/T9HS8E0PUNQD81JGUEQ==} engines: {node: '>=6'} @@ -11684,6 +13610,10 @@ packages: - supports-color dev: false + /console-browserify/1.2.0: + resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} + dev: true + /console-control-strings/1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} @@ -11695,6 +13625,10 @@ packages: upper-case: 2.0.2 dev: true + /constants-browserify/1.0.0: + resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} + dev: true + /content-disposition/0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -11710,8 +13644,12 @@ packages: dependencies: safe-buffer: 5.1.2 + /convert-source-map/2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + /cookie-signature/1.0.6: - resolution: {integrity: sha1-4wOogrNCzD7oylE6eZmXNNqzriw=} + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} /cookie/0.3.1: resolution: {integrity: sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==} @@ -11871,6 +13809,13 @@ packages: - supports-color dev: true + /create-ecdh/4.0.4: + resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} + dependencies: + bn.js: 4.12.0 + elliptic: 6.5.4 + dev: true + /create-error-class/3.0.2: resolution: {integrity: sha512-gYTKKexFO3kh200H1Nit76sRwRtOY32vQd3jpAQKpLtZqyNsSQNfI4N7o3eP2wUjV35pTWKRYqFUDBvUha/Pkw==} engines: {node: '>=0.10.0'} @@ -11878,6 +13823,27 @@ packages: capture-stack-trace: 1.0.1 dev: true + /create-hash/1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} + dependencies: + cipher-base: 1.0.4 + inherits: 2.0.4 + md5.js: 1.3.5 + ripemd160: 2.0.2 + sha.js: 2.4.11 + dev: true + + /create-hmac/1.1.7: + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + dependencies: + cipher-base: 1.0.4 + create-hash: 1.2.0 + inherits: 2.0.4 + ripemd160: 2.0.2 + safe-buffer: 5.2.1 + sha.js: 2.4.11 + dev: true + /create-require/1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true @@ -11900,7 +13866,6 @@ packages: node-fetch: 2.6.7 transitivePeerDependencies: - encoding - dev: true /cross-spawn/5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -11932,6 +13897,22 @@ packages: resolution: {integrity: sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=} dev: true + /crypto-browserify/3.12.0: + resolution: {integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==} + dependencies: + browserify-cipher: 1.0.1 + browserify-sign: 4.2.1 + create-ecdh: 4.0.4 + create-hash: 1.2.0 + create-hmac: 1.1.7 + diffie-hellman: 5.0.3 + inherits: 2.0.4 + pbkdf2: 3.1.2 + public-encrypt: 4.0.3 + randombytes: 2.1.0 + randomfill: 1.0.4 + dev: true + /crypto-random-string/1.0.0: resolution: {integrity: sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==} engines: {node: '>=4'} @@ -11946,6 +13927,11 @@ packages: resolution: {integrity: sha512-TcB+ZH9wZBG314jAUpKHPl1oYbRJV+nAT2YwZ9y4fmUN0FkEJa8e/hKZoOgzLYp1Z/CJdFhbhhGIGh0XG8W54Q==} dev: true + /css-box-model/1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + dependencies: + tiny-invariant: 1.3.1 + /css-color-names/0.0.4: resolution: {integrity: sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==} dev: true @@ -11958,6 +13944,11 @@ packages: timsort: 0.3.0 dev: true + /css-in-js-utils/3.1.0: + resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + dependencies: + hyphenate-style-name: 1.0.4 + /css-loader/3.6.0_webpack@5.75.0: resolution: {integrity: sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==} engines: {node: '>= 8.9.0'} @@ -11977,7 +13968,7 @@ packages: postcss-value-parser: 4.2.0 schema-utils: 2.7.0 semver: 6.3.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /css-loader/5.2.6_webpack@5.75.0: @@ -11996,7 +13987,7 @@ packages: postcss-value-parser: 4.2.0 schema-utils: 3.1.1 semver: 7.3.8 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /css-loader/6.7.2_webpack@5.75.0: @@ -12013,10 +14004,10 @@ packages: postcss-modules-values: 4.0.0_postcss@8.4.19 postcss-value-parser: 4.2.0 semver: 7.3.8 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true - /css-minimizer-webpack-plugin/4.2.2_j5vv3ucw6sjpjo7n5idphq5l2u: + /css-minimizer-webpack-plugin/4.2.2_htvmhiqynazf46fjrszipnqp7a: resolution: {integrity: sha512-s3Of/4jKfw1Hj9CxEO1E5oXhQAxlayuHO2y/ML+C6I9sQ7FdzfEV6QgMLN3vI+qFsjJGIAFLKtQK7t8BOXAIyA==} engines: {node: '>= 14.15.0'} peerDependencies: @@ -12042,13 +14033,13 @@ packages: optional: true dependencies: cssnano: 4.1.10 - esbuild: 0.16.10 + esbuild: 0.16.17 jest-worker: 29.3.1 postcss: 8.4.19 schema-utils: 4.0.0 serialize-javascript: 6.0.0 source-map: 0.6.1 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /css-modules-loader-core/1.1.0: @@ -12116,7 +14107,12 @@ packages: dependencies: mdn-data: 2.0.14 source-map: 0.6.1 - dev: true + + /css-vendor/2.0.8: + resolution: {integrity: sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==} + dependencies: + '@babel/runtime': 7.20.6 + is-in-browser: 1.1.3 /css-what/3.4.2: resolution: {integrity: sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==} @@ -12259,6 +14255,9 @@ packages: cssom: 0.3.8 dev: true + /csstype/2.6.21: + resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} + /csstype/3.1.0: resolution: {integrity: sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==} @@ -12345,7 +14344,6 @@ packages: /d3-color/3.0.1: resolution: {integrity: sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw==} engines: {node: '>=12'} - dev: true /d3-contour/1.3.2: resolution: {integrity: sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==} @@ -12374,7 +14372,6 @@ packages: /d3-dispatch/3.0.1: resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} engines: {node: '>=12'} - dev: true /d3-drag/1.2.5: resolution: {integrity: sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==} @@ -12389,7 +14386,6 @@ packages: dependencies: d3-dispatch: 3.0.1 d3-selection: 3.0.0 - dev: true /d3-dsv/1.2.0: resolution: {integrity: sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==} @@ -12417,7 +14413,6 @@ packages: /d3-ease/3.0.1: resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} engines: {node: '>=12'} - dev: true /d3-fetch/1.2.0: resolution: {integrity: sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==} @@ -12494,7 +14489,6 @@ packages: engines: {node: '>=12'} dependencies: d3-color: 3.0.1 - dev: true /d3-path/1.0.9: resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} @@ -12502,7 +14496,6 @@ packages: /d3-path/3.0.1: resolution: {integrity: sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==} engines: {node: '>=12'} - dev: true /d3-polygon/1.0.6: resolution: {integrity: sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==} @@ -12584,7 +14577,6 @@ packages: /d3-selection/3.0.0: resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} engines: {node: '>=12'} - dev: true /d3-shape/1.3.7: resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} @@ -12596,7 +14588,6 @@ packages: engines: {node: '>=12'} dependencies: d3-path: 3.0.1 - dev: true /d3-time-format/2.1.3: resolution: {integrity: sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==} @@ -12639,7 +14630,6 @@ packages: /d3-timer/3.0.1: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} - dev: true /d3-transition/1.3.2: resolution: {integrity: sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==} @@ -12664,7 +14654,6 @@ packages: d3-interpolate: 3.0.1 d3-selection: 3.0.0 d3-timer: 3.0.1 - dev: true /d3-voronoi/1.1.4: resolution: {integrity: sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==} @@ -12688,7 +14677,6 @@ packages: d3-interpolate: 3.0.1 d3-selection: 3.0.0 d3-transition: 3.0.1_d3-selection@3.0.0 - dev: true /d3/5.16.0: resolution: {integrity: sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==} @@ -12776,7 +14764,6 @@ packages: dependencies: graphlib: 2.1.8 lodash: 4.17.21 - dev: true /damerau-levenshtein/1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -12812,7 +14799,6 @@ packages: /date-fns/2.16.1: resolution: {integrity: sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==} engines: {node: '>=0.11'} - dev: false /dayjs/1.11.7: resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==} @@ -12900,6 +14886,11 @@ packages: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} dev: true + /decode-named-character-reference/1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + dependencies: + character-entities: 2.0.2 + /decode-uri-component/0.2.0: resolution: {integrity: sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==} engines: {node: '>=0.10'} @@ -13087,7 +15078,13 @@ packages: /dequal/2.0.2: resolution: {integrity: sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==} engines: {node: '>=6'} - dev: false + + /des.js/1.0.1: + resolution: {integrity: sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: true /destroy/1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} @@ -13138,6 +15135,17 @@ packages: execa: 5.1.1 dev: true + /detect-port-alt/1.1.6: + resolution: {integrity: sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==} + engines: {node: '>= 4.2.1'} + hasBin: true + dependencies: + address: 1.1.2 + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + dev: true + /detect-port/1.3.0: resolution: {integrity: sha512-E+B1gzkl2gqxt1IhUzwjrxBKRqx1UzC3WLONHinn8S3T6lwV/agVCyitiFOsGJ/eYuEUBvD71MZHy3Pv1G9doQ==} engines: {node: '>= 4.2.1'} @@ -13175,6 +15183,11 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dev: true + /diff-sequences/29.3.1: + resolution: {integrity: sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /diff/4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -13183,6 +15196,13 @@ packages: /diff/5.0.0: resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} engines: {node: '>=0.3.1'} + + /diffie-hellman/5.0.3: + resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} + dependencies: + bn.js: 4.12.0 + miller-rabin: 4.0.1 + randombytes: 2.1.0 dev: true /dir-glob/2.2.2: @@ -13236,6 +15256,12 @@ packages: '@babel/runtime': 7.20.6 dev: false + /dom-helpers/5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dependencies: + '@babel/runtime': 7.20.6 + csstype: 3.1.0 + /dom-serializer/0.2.2: resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} dependencies: @@ -13255,12 +15281,17 @@ packages: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 - entities: 4.3.1 + entities: 4.4.0 dev: true /dom-walk/0.1.1: resolution: {integrity: sha512-8CGZnLAdYN/o0SHjlP3nLvliHpi2f/prVU63/Hc4DTDpBgsNVAJekegjFtxfZ7NTUEDzHUByjX1gT3eYakIKqg==} + /domain-browser/1.2.0: + resolution: {integrity: sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==} + engines: {node: '>=0.4', npm: '>=1.2'} + dev: true + /domelementtype/1.3.1: resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} dev: true @@ -13418,11 +15449,28 @@ packages: batch-processor: 1.0.0 dev: true + /elliptic/6.5.4: + resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: true + /emittery/0.10.2: resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} engines: {node: '>=12'} dev: true + /emittery/0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + dev: true + /emoji-regex/7.0.3: resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==} dev: true @@ -13506,13 +15554,14 @@ packages: engines: {node: '>=10.13.0'} dependencies: graceful-fs: 4.2.10 - tapable: 2.2.0 + tapable: 2.2.1 /enquirer/2.3.6: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} engines: {node: '>=8.6'} dependencies: ansi-colors: 4.1.1 + dev: true /entities/1.1.2: resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} @@ -13521,8 +15570,8 @@ packages: /entities/2.1.0: resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==} - /entities/4.3.1: - resolution: {integrity: sha512-o4q/dYJlmyjP2zfnaWDUC6A3BQFmVTX+tZPezK7k0GLSU9QYCauscf5Y+qcEPzKL+EixVouYDgLQK5H9GrLpkg==} + /entities/4.4.0: + resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} engines: {node: '>=0.12'} dev: true @@ -13553,7 +15602,7 @@ packages: /error-stack-parser/2.0.6: resolution: {integrity: sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==} dependencies: - stackframe: 1.2.0 + stackframe: 1.3.4 /errorhandler/1.5.1: resolution: {integrity: sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==} @@ -13563,33 +15612,43 @@ packages: escape-html: 1.0.3 dev: false - /es-abstract/1.20.1: - resolution: {integrity: sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==} + /es-abstract/1.21.1: + resolution: {integrity: sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==} engines: {node: '>= 0.4'} dependencies: + available-typed-arrays: 1.0.5 call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 es-to-primitive: 1.2.1 function-bind: 1.1.1 function.prototype.name: 1.1.5 get-intrinsic: 1.1.3 get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 has: 1.0.3 has-property-descriptors: 1.0.0 + has-proto: 1.0.1 has-symbols: 1.0.3 - internal-slot: 1.0.3 - is-callable: 1.2.4 + internal-slot: 1.0.4 + is-array-buffer: 3.0.1 + is-callable: 1.2.7 is-negative-zero: 2.0.2 is-regex: 1.1.4 is-shared-array-buffer: 1.0.2 is-string: 1.0.7 + is-typed-array: 1.1.10 is-weakref: 1.0.2 - object-inspect: 1.12.0 + object-inspect: 1.12.3 object-keys: 1.1.1 - object.assign: 4.1.2 + object.assign: 4.1.4 regexp.prototype.flags: 1.4.3 - string.prototype.trimend: 1.0.5 - string.prototype.trimstart: 1.0.5 + safe-regex-test: 1.0.0 + string.prototype.trimend: 1.0.6 + string.prototype.trimstart: 1.0.6 + typed-array-length: 1.0.4 unbox-primitive: 1.0.2 + which-typed-array: 1.1.9 /es-array-method-boxes-properly/1.0.0: resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} @@ -13598,7 +15657,7 @@ packages: /es-get-iterator/1.1.0: resolution: {integrity: sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==} dependencies: - es-abstract: 1.20.1 + es-abstract: 1.21.1 has-symbols: 1.0.3 is-arguments: 1.1.0 is-map: 2.0.1 @@ -13610,17 +15669,24 @@ packages: /es-module-lexer/0.9.3: resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==} + /es-set-tostringtag/2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.1.3 + has: 1.0.3 + has-tostringtag: 1.0.0 + /es-shim-unscopables/1.0.0: resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} dependencies: has: 1.0.3 - dev: true /es-to-primitive/1.2.1: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} dependencies: - is-callable: 1.2.4 + is-callable: 1.2.7 is-date-object: 1.0.2 is-symbol: 1.0.3 @@ -13679,34 +15745,48 @@ packages: es6-symbol: 3.1.3 dev: true - /esbuild/0.16.10: - resolution: {integrity: sha512-z5dIViHoVnw2l+NCJ3zj5behdXjYvXne9gL18OOivCadXDUhyDkeSvEtLcGVAJW2fNmh33TDUpsi704XYlDodw==} + /esbuild-loader/2.21.0_webpack@5.75.0: + resolution: {integrity: sha512-k7ijTkCT43YBSZ6+fBCW1Gin7s46RrJ0VQaM8qA7lq7W+OLsGgtLyFV8470FzYi/4TeDexniTBTPTwZUnXXR5g==} + peerDependencies: + webpack: ^4.40.0 || ^5.0.0 + dependencies: + esbuild: 0.16.17 + joycon: 3.1.1 + json5: 2.2.1 + loader-utils: 2.0.4 + tapable: 2.2.1 + webpack: 5.75.0_cw4su4nzareykaft5p7arksy5i + webpack-sources: 1.4.3 + dev: true + + /esbuild/0.16.17: + resolution: {integrity: sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==} engines: {node: '>=12'} hasBin: true requiresBuild: true optionalDependencies: - '@esbuild/android-arm': 0.16.10 - '@esbuild/android-arm64': 0.16.10 - '@esbuild/android-x64': 0.16.10 - '@esbuild/darwin-arm64': 0.16.10 - '@esbuild/darwin-x64': 0.16.10 - '@esbuild/freebsd-arm64': 0.16.10 - '@esbuild/freebsd-x64': 0.16.10 - '@esbuild/linux-arm': 0.16.10 - '@esbuild/linux-arm64': 0.16.10 - '@esbuild/linux-ia32': 0.16.10 - '@esbuild/linux-loong64': 0.16.10 - '@esbuild/linux-mips64el': 0.16.10 - '@esbuild/linux-ppc64': 0.16.10 - '@esbuild/linux-riscv64': 0.16.10 - '@esbuild/linux-s390x': 0.16.10 - '@esbuild/linux-x64': 0.16.10 - '@esbuild/netbsd-x64': 0.16.10 - '@esbuild/openbsd-x64': 0.16.10 - '@esbuild/sunos-x64': 0.16.10 - '@esbuild/win32-arm64': 0.16.10 - '@esbuild/win32-ia32': 0.16.10 - '@esbuild/win32-x64': 0.16.10 + '@esbuild/android-arm': 0.16.17 + '@esbuild/android-arm64': 0.16.17 + '@esbuild/android-x64': 0.16.17 + '@esbuild/darwin-arm64': 0.16.17 + '@esbuild/darwin-x64': 0.16.17 + '@esbuild/freebsd-arm64': 0.16.17 + '@esbuild/freebsd-x64': 0.16.17 + '@esbuild/linux-arm': 0.16.17 + '@esbuild/linux-arm64': 0.16.17 + '@esbuild/linux-ia32': 0.16.17 + '@esbuild/linux-loong64': 0.16.17 + '@esbuild/linux-mips64el': 0.16.17 + '@esbuild/linux-ppc64': 0.16.17 + '@esbuild/linux-riscv64': 0.16.17 + '@esbuild/linux-s390x': 0.16.17 + '@esbuild/linux-x64': 0.16.17 + '@esbuild/netbsd-x64': 0.16.17 + '@esbuild/openbsd-x64': 0.16.17 + '@esbuild/sunos-x64': 0.16.17 + '@esbuild/win32-arm64': 0.16.17 + '@esbuild/win32-ia32': 0.16.17 + '@esbuild/win32-x64': 0.16.17 /escalade/3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} @@ -13733,6 +15813,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + /escape-string-regexp/5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + /escodegen/1.14.3: resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==} engines: {node: '>=4.0'} @@ -13752,7 +15836,7 @@ packages: hasBin: true dependencies: esprima: 4.0.1 - estraverse: 5.2.0 + estraverse: 5.3.0 esutils: 2.0.3 optionator: 0.8.3 optionalDependencies: @@ -13769,6 +15853,15 @@ packages: get-stdin: 6.0.0 dev: true + /eslint-config-prettier/8.6.0_eslint@8.18.0: + resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.18.0 + dev: true + /eslint-etc/5.1.0_4htfruiy2bgjslzgmagy6rfrsq: resolution: {integrity: sha512-Rmjl01h5smi5cbsFne2xpTuch2xNnwXiX2lbS4HttXUN5FwXKAwG1UEFBVGO1nC091YO/QyVahyfNPJSX2ae+g==} peerDependencies: @@ -13784,6 +15877,17 @@ packages: - supports-color dev: true + /eslint-formatter-friendly/7.0.0: + resolution: {integrity: sha512-WXg2D5kMHcRxIZA3ulxdevi8/BGTXu72pfOO5vXHqcAfClfIWDSlOljROjCSOCcKvilgmHz1jDWbvFCZHjMQ5w==} + engines: {node: '>=0.10.0'} + dependencies: + '@babel/code-frame': 7.0.0 + chalk: 2.4.2 + extend: 3.0.2 + strip-ansi: 5.2.0 + text-table: 0.2.0 + dev: true + /eslint-import-resolver-node/0.3.6: resolution: {integrity: sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==} dependencies: @@ -13851,6 +15955,21 @@ packages: requireindex: 1.2.0 dev: true + /eslint-plugin-deprecation/1.3.3_4htfruiy2bgjslzgmagy6rfrsq: + resolution: {integrity: sha512-Bbkv6ZN2cCthVXz/oZKPwsSY5S/CbgTLRG4Q2s2gpPpgNsT0uJ0dB5oLNiWzFYY8AgKX4ULxXFG1l/rDav9QFA==} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: ^3.7.5 || ^4.0.0 + dependencies: + '@typescript-eslint/experimental-utils': 5.4.0_4htfruiy2bgjslzgmagy6rfrsq + eslint: 8.18.0 + tslib: 2.1.0 + tsutils: 3.21.0_typescript@4.9.3 + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-plugin-etc/2.0.2_4htfruiy2bgjslzgmagy6rfrsq: resolution: {integrity: sha512-g3b95LCdTCwZA8On9EICYL8m1NMWaiGfmNUd/ftZTeGZDXrwujKXUr+unYzqKjKFo1EbqJ31vt+Dqzrdm/sUcw==} peerDependencies: @@ -13880,7 +15999,7 @@ packages: optional: true dependencies: '@typescript-eslint/parser': 5.24.0_4htfruiy2bgjslzgmagy6rfrsq - array-includes: 3.1.5 + array-includes: 3.1.6 array.prototype.flat: 1.3.0 debug: 2.6.9 doctrine: 2.1.0 @@ -13888,10 +16007,10 @@ packages: eslint-import-resolver-node: 0.3.6 eslint-module-utils: 2.7.3_bi6p7cyrvxrvblpcgmvuwzeqom has: 1.0.3 - is-core-module: 2.8.1 + is-core-module: 2.11.0 is-glob: 4.0.3 minimatch: 3.1.2 - object.values: 1.1.5 + object.values: 1.1.6 resolve: 1.22.0 tsconfig-paths: 3.14.1 transitivePeerDependencies: @@ -13912,6 +16031,28 @@ packages: requireindex: 1.2.0 dev: true + /eslint-plugin-jest/27.2.1_qgasbpcsrurabgy544gmfdiiuu: + resolution: {integrity: sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.0.0 + eslint: ^7.0.0 || ^8.0.0 + jest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + jest: + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 5.24.0_el7ggvuufxftzwvu6wsrutnxdi + '@typescript-eslint/utils': 5.24.0_4htfruiy2bgjslzgmagy6rfrsq + eslint: 8.18.0 + jest: 29.3.1_@types+node@18.8.3 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /eslint-plugin-jsdoc/30.7.8_eslint@8.18.0: resolution: {integrity: sha512-OWm2AYvXjCl7nRbpcw5xisfSVkpVAyp4lGqL9T+DeK4kaPm6ecnmTc/G5s1PtcRrwbaI8bIWGzwScqv5CdGyxA==} engines: {node: '>=10'} @@ -13938,7 +16079,7 @@ packages: dependencies: '@babel/runtime': 7.20.6 aria-query: 4.2.2 - array-includes: 3.1.5 + array-includes: 3.1.6 ast-types-flow: 0.0.7 axe-core: 4.4.2 axobject-query: 2.2.0 @@ -13978,45 +16119,28 @@ packages: eslint: 8.18.0 dev: true - /eslint-plugin-react/7.21.4_eslint@7.32.0: - resolution: {integrity: sha512-uHeQ8A0hg0ltNDXFu3qSfFqTNPXm1XithH6/SY318UX76CMj7Q599qWpgmMhVQyvhq36pm7qvoN3pb6/3jsTFg==} + /eslint-plugin-react/7.32.1_eslint@8.18.0: + resolution: {integrity: sha512-vOjdgyd0ZHBXNsmvU+785xY8Bfe57EFbTYYk8XrROzWpr9QBvpjITvAXt9xqcE6+8cjR/g1+mfumPToxsl1www==} engines: {node: '>=4'} peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 - dependencies: - array-includes: 3.1.5 - array.prototype.flatmap: 1.2.4 - doctrine: 2.1.0 - eslint: 7.32.0 - has: 1.0.3 - jsx-ast-utils: 3.3.0 - object.entries: 1.1.2 - object.fromentries: 2.0.2 - object.values: 1.1.5 - prop-types: 15.8.1 - resolve: 1.22.0 - string.prototype.matchall: 4.0.2 - dev: false - - /eslint-plugin-react/7.21.4_eslint@8.18.0: - resolution: {integrity: sha512-uHeQ8A0hg0ltNDXFu3qSfFqTNPXm1XithH6/SY318UX76CMj7Q599qWpgmMhVQyvhq36pm7qvoN3pb6/3jsTFg==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 dependencies: - array-includes: 3.1.5 - array.prototype.flatmap: 1.2.4 + array-includes: 3.1.6 + array.prototype.flatmap: 1.3.1 + array.prototype.tosorted: 1.1.1 doctrine: 2.1.0 eslint: 8.18.0 - has: 1.0.3 + estraverse: 5.3.0 jsx-ast-utils: 3.3.0 - object.entries: 1.1.2 - object.fromentries: 2.0.2 - object.values: 1.1.5 + minimatch: 3.1.2 + object.entries: 1.1.6 + object.fromentries: 2.0.6 + object.hasown: 1.1.2 + object.values: 1.1.6 prop-types: 15.8.1 - resolve: 1.22.0 - string.prototype.matchall: 4.0.2 - dev: true + resolve: 2.0.0-next.4 + semver: 6.3.0 + string.prototype.matchall: 4.0.8 /eslint-plugin-rxjs/5.0.2_4htfruiy2bgjslzgmagy6rfrsq: resolution: {integrity: sha512-Q2wsEHWInhZ3uz5df+YbD4g/NPQqAeYHjJuEsxqgVS+XAsYCuVE2pj9kADdMFy4GsQy2jt7KP+TOrnq1i6bI5Q==} @@ -14094,85 +16218,40 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: esrecurse: 4.3.0 - estraverse: 5.2.0 - - /eslint-utils/2.1.0: - resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} - engines: {node: '>=6'} - dependencies: - eslint-visitor-keys: 1.3.0 - dev: false + estraverse: 5.3.0 /eslint-utils/3.0.0_eslint@8.18.0: - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - dependencies: - eslint: 8.18.0 - eslint-visitor-keys: 2.0.0 - - /eslint-visitor-keys/1.3.0: - resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} - engines: {node: '>=4'} - dev: false - - /eslint-visitor-keys/2.0.0: - resolution: {integrity: sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==} - engines: {node: '>=10'} - - /eslint-visitor-keys/3.3.0: - resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - /eslint/7.32.0: - resolution: {integrity: sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==} - engines: {node: ^10.12.0 || >=12.0.0} - hasBin: true - dependencies: - '@babel/code-frame': 7.12.11 - '@eslint/eslintrc': 0.4.3 - '@humanwhocodes/config-array': 0.5.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 - enquirer: 2.3.6 - escape-string-regexp: 4.0.0 - eslint-scope: 5.1.1 - eslint-utils: 2.1.0 - eslint-visitor-keys: 2.0.0 - espree: 7.3.1 - esquery: 1.4.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - functional-red-black-tree: 1.0.1 - glob-parent: 5.1.2 - globals: 13.16.0 - ignore: 4.0.6 - import-fresh: 3.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - js-yaml: 3.14.1 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.1 - progress: 2.0.3 - regexpp: 3.2.0 - semver: 7.3.8 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - table: 6.8.0 - text-table: 0.2.0 - v8-compile-cache: 2.3.0 - transitivePeerDependencies: - - supports-color - dev: false + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.18.0 + eslint-visitor-keys: 2.0.0 + + /eslint-visitor-keys/2.0.0: + resolution: {integrity: sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==} + engines: {node: '>=10'} + + /eslint-visitor-keys/3.3.0: + resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + /eslint-webpack-plugin/3.2.0_4cuzlhuyxkclhi6pwpzucithpm: + resolution: {integrity: sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + webpack: ^5.0.0 + dependencies: + '@types/eslint': 8.4.10 + eslint: 8.18.0 + jest-worker: 28.1.3 + micromatch: 4.0.5 + normalize-path: 3.0.0 + schema-utils: 4.0.0 + webpack: 5.75.0_cw4su4nzareykaft5p7arksy5i + dev: true /eslint/8.18.0: resolution: {integrity: sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==} @@ -14217,21 +16296,12 @@ packages: transitivePeerDependencies: - supports-color - /espree/7.3.1: - resolution: {integrity: sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - acorn: 7.4.1 - acorn-jsx: 5.3.2_acorn@7.4.1 - eslint-visitor-keys: 1.3.0 - dev: false - /espree/9.3.2: resolution: {integrity: sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.8.0 - acorn-jsx: 5.3.2_acorn@8.8.0 + acorn: 8.8.1 + acorn-jsx: 5.3.2_acorn@8.8.1 eslint-visitor-keys: 3.3.0 /esprima/4.0.1: @@ -14243,22 +16313,34 @@ packages: resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} engines: {node: '>=0.10'} dependencies: - estraverse: 5.2.0 + estraverse: 5.3.0 /esrecurse/4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} dependencies: - estraverse: 5.2.0 + estraverse: 5.3.0 /estraverse/4.3.0: resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} engines: {node: '>=4.0'} - /estraverse/5.2.0: - resolution: {integrity: sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==} + /estraverse/5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + /estree-walker/0.6.1: + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + dev: true + + /estree-walker/1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + dev: true + + /estree-walker/2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + /esutils/2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -14296,6 +16378,13 @@ packages: original: 1.0.2 dev: false + /evp_bytestokey/1.0.3: + resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} + dependencies: + md5.js: 1.3.5 + safe-buffer: 5.2.1 + dev: true + /exec-sh/0.3.2: resolution: {integrity: sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg==} dev: true @@ -14419,6 +16508,17 @@ packages: jest-util: 28.1.3 dev: true + /expect/29.3.1: + resolution: {integrity: sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.3.1 + jest-get-type: 29.2.0 + jest-matcher-utils: 29.3.1 + jest-message-util: 29.3.1 + jest-util: 29.3.1 + dev: true + /express-static-gzip/2.1.1: resolution: {integrity: sha512-J+xSzdr5lj1cIuZey0ac6nUv22VE7GrdwTERqE8DsrqSXLm1zjeYWTVbK37t8exGwobxBXeWU2bM7eSMjBR4YA==} dependencies: @@ -14490,7 +16590,6 @@ packages: /extend/3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - dev: true /external-editor/3.0.3: resolution: {integrity: sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==} @@ -14584,7 +16683,7 @@ packages: '@nodelib/fs.walk': 1.2.4 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.4 + micromatch: 4.0.5 dev: true /fast-json-parse/1.0.3: @@ -14597,10 +16696,16 @@ packages: /fast-levenshtein/2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + /fast-loops/1.1.3: + resolution: {integrity: sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==} + /fast-safe-stringify/2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} dev: true + /fast-shallow-equal/1.0.0: + resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} + /fast-text-encoding/1.0.0: resolution: {integrity: sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==} dev: true @@ -14608,6 +16713,9 @@ packages: /fastest-levenshtein/1.0.12: resolution: {integrity: sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==} + /fastest-stable-stringify/2.0.2: + resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} + /fastparse/1.1.2: resolution: {integrity: sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==} dev: true @@ -14622,7 +16730,6 @@ packages: resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} dependencies: format: 0.2.2 - dev: true /faye-websocket/0.11.3: resolution: {integrity: sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==} @@ -14691,7 +16798,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.1.1 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /file-system-cache/1.0.5: @@ -14712,6 +16819,11 @@ packages: engines: {node: '>= 6'} dev: true + /filesize/8.0.7: + resolution: {integrity: sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==} + engines: {node: '>= 0.4.0'} + dev: true + /fill-range/4.0.0: resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} engines: {node: '>=0.10.0'} @@ -14897,8 +17009,7 @@ packages: /for-each/0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: - is-callable: 1.2.4 - dev: false + is-callable: 1.2.7 /for-in/1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} @@ -14941,14 +17052,14 @@ packages: semver: 5.7.1 tapable: 1.1.3 typescript: 4.9.3 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy worker-rpc: 0.1.1 transitivePeerDependencies: - supports-color dev: true - /fork-ts-checker-webpack-plugin/6.2.4_7nbrhxx5wbdrea3w4d3cjhku4y: - resolution: {integrity: sha512-M0+lELATBqzf+tubq9PcRfJm4/wW172P7cAoCs1OpgfRICtEuJ/JUMuUm0H9JAZ57lLaN1gj38uxNktDhscYhA==} + /fork-ts-checker-webpack-plugin/6.5.2_7nbrhxx5wbdrea3w4d3cjhku4y: + resolution: {integrity: sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==} engines: {node: '>=10', yarn: '>=1.0.0'} peerDependencies: eslint: '>= 6' @@ -14969,13 +17080,41 @@ packages: deepmerge: 4.2.2 eslint: 8.18.0 fs-extra: 9.0.1 + glob: 7.2.3 memfs: 3.4.12 minimatch: 3.1.2 schema-utils: 2.7.0 semver: 7.3.8 tapable: 1.1.3 typescript: 4.9.3 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy + + /fork-ts-checker-webpack-plugin/7.3.0_vfotqvx6lgcbf3upbs6hgaza4q: + resolution: {integrity: sha512-IN+XTzusCjR5VgntYFgxbxVx3WraPRnKehBFrf00cMSrtUuW9MsG9dhL6MWpY6MkjC3wVwoujfCDgZZCQwbswA==} + engines: {node: '>=12.13.0', yarn: '>=1.0.0'} + peerDependencies: + typescript: '>3.6.0' + vue-template-compiler: '*' + webpack: ^5.11.0 + peerDependenciesMeta: + vue-template-compiler: + optional: true + dependencies: + '@babel/code-frame': 7.18.6 + chalk: 4.1.2 + chokidar: 3.5.3 + cosmiconfig: 7.0.1 + deepmerge: 4.2.2 + fs-extra: 10.1.0 + memfs: 3.4.12 + minimatch: 3.1.2 + node-abort-controller: 3.0.1 + schema-utils: 3.1.1 + semver: 7.3.8 + tapable: 2.2.1 + typescript: 4.9.3 + webpack: 5.75.0_cw4su4nzareykaft5p7arksy5i + dev: true /form-data-encoder/1.7.2: resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} @@ -15011,7 +17150,6 @@ packages: /format/0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} - dev: true /formdata-node/4.4.1: resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} @@ -15045,7 +17183,7 @@ packages: map-cache: 0.2.2 /fresh/0.5.2: - resolution: {integrity: sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=} + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} /fromentries/1.3.2: @@ -15073,6 +17211,15 @@ packages: klaw: 1.3.1 dev: false + /fs-extra/10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.10 + jsonfile: 6.0.1 + universalify: 2.0.0 + dev: true + /fs-extra/8.1.0: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} @@ -15157,7 +17304,7 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.1 + es-abstract: 1.21.1 functions-have-names: 1.2.3 /functional-red-black-tree/1.0.1: @@ -15341,6 +17488,19 @@ packages: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} engines: {node: '>=0.10.0'} + /git-up/7.0.0: + resolution: {integrity: sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==} + dependencies: + is-ssh: 1.4.0 + parse-url: 8.1.0 + dev: true + + /git-url-parse/13.1.0: + resolution: {integrity: sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==} + dependencies: + git-up: 7.0.0 + dev: true + /github-from-package/0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} dev: true @@ -15368,14 +17528,14 @@ packages: dependencies: is-glob: 4.0.3 - /glob-promise/3.4.0_glob@7.1.7: + /glob-promise/3.4.0_glob@7.2.3: resolution: {integrity: sha512-q08RJ6O+eJn+dVanerAndJwIcumgbDdYiUT7zFQl3Wm1xD6fBKtah7H8ZJChj4wP+8C+QfeVy8xautR7rdmKEw==} engines: {node: '>=4'} peerDependencies: glob: '*' dependencies: '@types/glob': 7.1.3 - glob: 7.1.7 + glob: 7.2.3 dev: true /glob-stream/6.1.0: @@ -15383,7 +17543,7 @@ packages: engines: {node: '>= 0.10'} dependencies: extend: 3.0.2 - glob: 7.1.7 + glob: 7.2.3 glob-parent: 3.1.0 is-negated-glob: 1.0.0 ordered-read-streams: 1.0.1 @@ -15435,6 +17595,40 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 + dev: true + + /glob/7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + /glob/8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + dev: true + + /global-agent/3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.3.8 + serialize-error: 7.0.1 + dev: true /global-dirs/0.1.1: resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} @@ -15502,12 +17696,11 @@ packages: dependencies: type-fest: 0.20.2 - /globalthis/1.0.1: - resolution: {integrity: sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw==} + /globalthis/1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} engines: {node: '>= 0.4'} dependencies: define-properties: 1.1.4 - dev: true /globby/11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} @@ -15527,7 +17720,7 @@ packages: dependencies: array-union: 1.0.2 dir-glob: 2.2.2 - glob: 7.1.7 + glob: 7.2.3 ignore: 3.3.10 pify: 3.0.0 slash: 1.0.0 @@ -15541,7 +17734,7 @@ packages: array-union: 1.0.2 dir-glob: 2.2.2 fast-glob: 2.2.7 - glob: 7.1.7 + glob: 7.2.3 ignore: 4.0.6 pify: 4.0.1 slash: 2.0.0 @@ -15619,7 +17812,6 @@ packages: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.1.3 - dev: false /got/11.8.5: resolution: {integrity: sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==} @@ -15716,7 +17908,6 @@ packages: resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==} dependencies: lodash: 4.17.21 - dev: true /graphql-config/4.3.6_jpmwp2tmp44rtdds36wqrbd4rq: resolution: {integrity: sha512-i7mAPwc0LAZPnYu2bI8B6yXU5820Wy/ArvmOseDLZIu0OU1UTULEuexHo6ZcHXeT9NvGGaUPQZm8NV3z79YydA==} @@ -15781,7 +17972,7 @@ packages: columnify: 1.6.0 commander: 3.0.2 cosmiconfig: 5.2.1 - glob: 7.1.7 + glob: 7.2.3 graphql: 15.4.0 dev: true @@ -15954,6 +18145,10 @@ packages: dependencies: get-intrinsic: 1.1.3 + /has-proto/1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + /has-symbols/1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} @@ -16005,6 +18200,22 @@ packages: dependencies: function-bind: 1.1.1 + /hash-base/3.1.0: + resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} + engines: {node: '>=4'} + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.0 + safe-buffer: 5.2.1 + dev: true + + /hash.js/1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: true + /hasha/5.2.0: resolution: {integrity: sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==} engines: {node: '>=8'} @@ -16038,7 +18249,6 @@ packages: /hast-util-parse-selector/2.2.5: resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} - dev: true /hast-util-raw/6.0.1: resolution: {integrity: sha512-ZMuiYA+UF7BXBtsTBNcLBF5HzXzkyE6MLzJnL605LKE8GJylNjGc4jjxazAHUtcwT5/CEt6afRKViYB4X66dig==} @@ -16065,6 +18275,9 @@ packages: zwitch: 1.0.5 dev: true + /hast-util-whitespace/2.0.1: + resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} + /hastscript/6.0.0: resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} dependencies: @@ -16073,7 +18286,6 @@ packages: hast-util-parse-selector: 2.2.5 property-information: 5.6.0 space-separated-tokens: 1.1.2 - dev: true /he/1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} @@ -16087,6 +18299,10 @@ packages: tslib: 2.1.0 dev: true + /headers-polyfill/3.1.2: + resolution: {integrity: sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA==} + dev: true + /hermes-estree/0.8.0: resolution: {integrity: sha512-W6JDAOLZ5pMPMjEiQGLCXSSV7pIBEgRR5zGkxgmzGSXHOxqV5dC/M1Zevqpbm9TZDE5tu358qZf8Vkzmsc+u7Q==} dev: false @@ -16128,7 +18344,14 @@ packages: resolve-pathname: 2.2.0 value-equal: 0.2.1 warning: 3.0.0 - dev: false + + /hmac-drbg/1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: true /hoist-non-react-statics/3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -16142,6 +18365,11 @@ packages: parse-passwd: 1.0.0 dev: true + /hoopy/0.1.4: + resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==} + engines: {node: '>= 6.0.0'} + dev: true + /hosted-git-info/2.8.5: resolution: {integrity: sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==} dev: true @@ -16242,10 +18470,10 @@ packages: webpack: ^5.0.0 dependencies: html-webpack-plugin: 5.5.0_webpack@5.75.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true - /html-webpack-plugin/4.5.2_ovbyeo7usi33ah22pzu3dmmfoi: + /html-webpack-plugin/4.5.2_nljosxlhezzfayhg2olmp6suom: resolution: {integrity: sha512-q5oYdzjKUIPQVjOosjgvCHQOv9Ett9CYYHlgvJeXG0qQvdSojnBq4vAdQBwn1+yGveAwHCoe/rMR86ozX3+c2A==} engines: {node: '>=6.9'} peerDependencies: @@ -16253,14 +18481,14 @@ packages: dependencies: '@types/html-minifier-terser': 5.1.1 '@types/tapable': 1.0.7 - '@types/webpack': 5.28.0_oo63t3us67g6w4zg54gqorhf2i + '@types/webpack': 5.28.0_jh3afgd4dccyz4n3zkk5zvv5cy html-minifier-terser: 5.1.1 loader-utils: 1.4.0 lodash: 4.17.21 pretty-error: 2.1.1 tapable: 1.1.3 util.promisify: 1.0.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy transitivePeerDependencies: - '@swc/core' - esbuild @@ -16278,8 +18506,8 @@ packages: html-minifier-terser: 6.1.0 lodash: 4.17.21 pretty-error: 4.0.0 - tapable: 2.2.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + tapable: 2.2.1 + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /htmlparser2/6.1.0: @@ -16296,7 +18524,7 @@ packages: domelementtype: 2.3.0 domhandler: 5.0.3 domutils: 3.0.1 - entities: 4.3.1 + entities: 4.4.0 dev: true /http-cache-semantics/4.1.0: @@ -16369,7 +18597,7 @@ packages: http-proxy: 1.18.1 is-glob: 4.0.3 is-plain-obj: 3.0.0 - micromatch: 4.0.4 + micromatch: 4.0.5 transitivePeerDependencies: - debug dev: true @@ -16388,7 +18616,7 @@ packages: http-proxy: 1.18.1 is-glob: 4.0.3 is-plain-obj: 3.0.0 - micromatch: 4.0.4 + micromatch: 4.0.5 transitivePeerDependencies: - debug @@ -16416,7 +18644,6 @@ packages: /https-browserify/1.0.0: resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} - dev: false /https-proxy-agent/3.0.1: resolution: {integrity: sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==} @@ -16445,6 +18672,9 @@ packages: resolution: {integrity: sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g==} dev: true + /hyphenate-style-name/1.0.4: + resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==} + /iconv-lite/0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -16488,6 +18718,13 @@ packages: /ieee754/1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + /ignore-walk/5.0.1: + resolution: {integrity: sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + minimatch: 5.1.6 + dev: true + /ignore/3.3.10: resolution: {integrity: sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==} dev: true @@ -16495,6 +18732,7 @@ packages: /ignore/4.0.6: resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==} engines: {node: '>= 4'} + dev: true /ignore/5.2.0: resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} @@ -16518,11 +18756,21 @@ packages: resolution: {integrity: sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==} dev: true + /immer/9.0.18: + resolution: {integrity: sha512-eAPNpsj7Ax1q6Y/3lm2PmlwRcFzpON7HSNQ3ru5WQH1/PSpnyed/HpNOELl2CxLKoj4r+bAHgdyKqW5gc2Se1A==} + /immutable/3.7.6: resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==} engines: {node: '>=0.8.0'} dev: true + /import-cwd/3.0.0: + resolution: {integrity: sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==} + engines: {node: '>=8'} + dependencies: + import-from: 3.0.0 + dev: true + /import-fresh/2.0.0: resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} engines: {node: '>=4'} @@ -16537,6 +18785,13 @@ packages: parent-module: 1.0.1 resolve-from: 4.0.0 + /import-from/3.0.0: + resolution: {integrity: sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: true + /import-from/4.0.0: resolution: {integrity: sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==} engines: {node: '>=12.2'} @@ -16610,7 +18865,12 @@ packages: /inline-style-parser/0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} - dev: true + + /inline-style-prefixer/6.0.4: + resolution: {integrity: sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg==} + dependencies: + css-in-js-utils: 3.1.0 + fast-loops: 1.1.3 /inquirer/6.5.2: resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==} @@ -16652,8 +18912,8 @@ packages: wrap-ansi: 7.0.0 dev: true - /internal-slot/1.0.3: - resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} + /internal-slot/1.0.4: + resolution: {integrity: sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==} engines: {node: '>= 0.4'} dependencies: get-intrinsic: 1.1.3 @@ -16753,14 +19013,12 @@ packages: /is-alphabetical/1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} - dev: true /is-alphanumerical/1.0.2: resolution: {integrity: sha512-pyfU/0kHdISIgslFfZN9nfY1Gk3MquQgUm1mJTjdkEPpkAKNWuBTSqFwewOpR7N351VkErCiyV71zX7mlQQqsg==} dependencies: is-alphabetical: 1.0.4 is-decimal: 1.0.4 - dev: true /is-arguments/1.1.0: resolution: {integrity: sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==} @@ -16768,6 +19026,13 @@ packages: dependencies: call-bind: 1.0.2 + /is-array-buffer/3.0.1: + resolution: {integrity: sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.3 + is-typed-array: 1.1.10 + /is-arrayish/0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -16803,7 +19068,6 @@ packages: /is-buffer/2.0.5: resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} engines: {node: '>=4'} - dev: true /is-builtin-module/3.1.0: resolution: {integrity: sha512-OV7JjAgOTfAFJmHZLvpSTb4qi0nIILDV1gWPYDnDJUTNFM5aGlRAhk4QcT8i7TuAleeEV5Fdkqn3t4mS+Q11fg==} @@ -16812,8 +19076,8 @@ packages: builtin-modules: 3.3.0 dev: true - /is-callable/1.2.4: - resolution: {integrity: sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==} + /is-callable/1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} /is-ci/2.0.0: @@ -16834,8 +19098,8 @@ packages: rgba-regex: 1.0.0 dev: true - /is-core-module/2.8.1: - resolution: {integrity: sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==} + /is-core-module/2.11.0: + resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} dependencies: has: 1.0.3 @@ -16857,7 +19121,6 @@ packages: /is-decimal/1.0.4: resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} - dev: true /is-descriptor/0.1.6: resolution: {integrity: sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==} @@ -16940,7 +19203,6 @@ packages: engines: {node: '>= 0.4'} dependencies: has-tostringtag: 1.0.0 - dev: false /is-glob/3.1.0: resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==} @@ -16957,7 +19219,9 @@ packages: /is-hexadecimal/1.0.2: resolution: {integrity: sha512-but/G3sapV3MNyqiDBLrOi4x8uCIw0RY3o/Vb5GT0sMFHrVV7731wFSVy41T5FO1og7G0gXLJh0MkgPRouko/A==} - dev: true + + /is-in-browser/1.1.3: + resolution: {integrity: sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==} /is-installed-globally/0.1.0: resolution: {integrity: sha512-ERNhMg+i/XgDwPIPF3u24qpajVreaiSuvpb1Uu0jugw7KKcxGyCX8cgp8P5fwTmAuXku6beDHHECdKArjlg7tw==} @@ -16989,6 +19253,10 @@ packages: resolution: {integrity: sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==} dev: true + /is-module/1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + dev: true + /is-negated-glob/1.0.0: resolution: {integrity: sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==} engines: {node: '>=0.10.0'} @@ -16998,6 +19266,10 @@ packages: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} + /is-node-process/1.0.1: + resolution: {integrity: sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ==} + dev: true + /is-npm/3.0.0: resolution: {integrity: sha512-wsigDr1Kkschp2opC4G3yA6r9EgVA6NjRpWzIi9axXqeIaAATPRJc4uLujXe3Nd9uO8KoDyA4MD6aZSeXTADhA==} engines: {node: '>=8'} @@ -17067,6 +19339,10 @@ packages: resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} engines: {node: '>=10'} + /is-plain-obj/4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + /is-plain-object/2.0.4: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} @@ -17095,6 +19371,12 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-reference/1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + dependencies: + '@types/estree': 1.0.0 + dev: true + /is-regex/1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -17123,6 +19405,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-root/2.1.0: + resolution: {integrity: sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==} + engines: {node: '>=6'} + dev: true + /is-set/2.0.1: resolution: {integrity: sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==} dev: true @@ -17132,6 +19419,12 @@ packages: dependencies: call-bind: 1.0.2 + /is-ssh/1.4.0: + resolution: {integrity: sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==} + dependencies: + protocols: 2.0.1 + dev: true + /is-stream/1.1.0: resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} engines: {node: '>=0.10.0'} @@ -17161,7 +19454,6 @@ packages: for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 - dev: false /is-typedarray/1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} @@ -17403,6 +19695,14 @@ packages: p-limit: 3.1.0 dev: true + /jest-changed-files/29.2.0: + resolution: {integrity: sha512-qPVmLLyBmvF5HJrY7krDisx6Voi8DmlV3GZYX0aFNbaQsZeoz1hfxcCMbqDGuQCxU1dJy9eYc2xscE8QrCCYaA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + p-limit: 3.1.0 + dev: true + /jest-circus/28.1.3: resolution: {integrity: sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -17430,6 +19730,33 @@ packages: - supports-color dev: true + /jest-circus/29.3.1: + resolution: {integrity: sha512-wpr26sEvwb3qQQbdlmei+gzp6yoSSoSL6GsLPxnuayZSMrSd5Ka7IjAvatpIernBvT2+Ic6RLTg+jSebScmasg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.3.1 + '@jest/expect': 29.3.1 + '@jest/test-result': 29.3.1 + '@jest/types': 29.3.1 + '@types/node': 18.8.3 + chalk: 4.1.2 + co: 4.6.0 + dedent: 0.7.0 + is-generator-fn: 2.0.0 + jest-each: 29.3.1 + jest-matcher-utils: 29.3.1 + jest-message-util: 29.3.1 + jest-runtime: 29.3.1 + jest-snapshot: 29.3.1 + jest-util: 29.3.1 + p-limit: 3.1.0 + pretty-format: 29.3.1 + slash: 3.0.0 + stack-utils: 2.0.5 + transitivePeerDependencies: + - supports-color + dev: true + /jest-cli/28.1.3_sgfsbmxe5qwkqmsj7h2d7flbny: resolution: {integrity: sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -17450,7 +19777,35 @@ packages: jest-config: 28.1.3_sgfsbmxe5qwkqmsj7h2d7flbny jest-util: 28.1.3 jest-validate: 28.1.3 - prompts: 2.4.0 + prompts: 2.4.2 + yargs: 17.6.2 + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + dev: true + + /jest-cli/29.3.1_@types+node@18.8.3: + resolution: {integrity: sha512-TO/ewvwyvPOiBBuWZ0gm04z3WWP8TIK8acgPzE4IxgsLKQgb377NYGrQLc3Wl/7ndWzIH2CDNNsUjGxwLL43VQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.3.1 + '@jest/test-result': 29.3.1 + '@jest/types': 29.3.1 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.10 + import-local: 3.0.2 + jest-config: 29.3.1_@types+node@18.8.3 + jest-util: 29.3.1 + jest-validate: 29.3.1 + prompts: 2.4.2 yargs: 17.6.2 transitivePeerDependencies: - '@types/node' @@ -17478,7 +19833,7 @@ packages: chalk: 4.1.2 ci-info: 3.3.1 deepmerge: 4.2.2 - glob: 7.1.7 + glob: 7.2.3 graceful-fs: 4.2.10 jest-circus: 28.1.3 jest-environment-node: 28.1.3 @@ -17488,7 +19843,7 @@ packages: jest-runner: 28.1.3 jest-util: 28.1.3 jest-validate: 28.1.3 - micromatch: 4.0.4 + micromatch: 4.0.5 parse-json: 5.2.0 pretty-format: 28.1.3 slash: 3.0.0 @@ -17518,7 +19873,7 @@ packages: chalk: 4.1.2 ci-info: 3.3.1 deepmerge: 4.2.2 - glob: 7.1.7 + glob: 7.2.3 graceful-fs: 4.2.10 jest-circus: 28.1.3 jest-environment-node: 28.1.3 @@ -17528,16 +19883,61 @@ packages: jest-runner: 28.1.3 jest-util: 28.1.3 jest-validate: 28.1.3 - micromatch: 4.0.4 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 28.1.3 + slash: 3.0.0 + strip-json-comments: 3.1.1 + ts-node: 10.9.1_wh55dwo6xja56jtfktvlrff6xu + transitivePeerDependencies: + - supports-color + dev: true + + /jest-config/29.3.1_@types+node@18.8.3: + resolution: {integrity: sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.20.5 + '@jest/test-sequencer': 29.3.1 + '@jest/types': 29.3.1 + '@types/node': 18.8.3 + babel-jest: 29.3.1_@babel+core@7.20.5 + chalk: 4.1.2 + ci-info: 3.3.1 + deepmerge: 4.2.2 + glob: 7.2.3 + graceful-fs: 4.2.10 + jest-circus: 29.3.1 + jest-environment-node: 29.3.1 + jest-get-type: 29.2.0 + jest-regex-util: 29.2.0 + jest-resolve: 29.3.1 + jest-runner: 29.3.1 + jest-util: 29.3.1 + jest-validate: 29.3.1 + micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 28.1.3 + pretty-format: 29.3.1 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1_wh55dwo6xja56jtfktvlrff6xu transitivePeerDependencies: - supports-color dev: true + /jest-css-modules/2.1.0: + resolution: {integrity: sha512-my3Scnt6l2tOll/eGwNZeh1KLAFkNzdl4MyZRdpl46GO6/93JcKKdTjNqK6Nokg8A8rT84MFLOpY1pzqKBEqMw==} + dependencies: + identity-obj-proxy: 3.0.0 + dev: true + /jest-diff/26.6.2: resolution: {integrity: sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==} engines: {node: '>= 10.14.2'} @@ -17568,6 +19968,16 @@ packages: pretty-format: 28.1.3 dev: true + /jest-diff/29.3.1: + resolution: {integrity: sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.3.1 + jest-get-type: 29.2.0 + pretty-format: 29.3.1 + dev: true + /jest-docblock/28.1.1: resolution: {integrity: sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -17575,6 +19985,13 @@ packages: detect-newline: 3.1.0 dev: true + /jest-docblock/29.2.0: + resolution: {integrity: sha512-bkxUsxTgWQGbXV5IENmfiIuqZhJcyvF7tU4zJ/7ioTutdz4ToB5Yx6JOFBpgI+TphRY4lhOyCWGNH/QFQh5T6A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + dev: true + /jest-each/28.1.3: resolution: {integrity: sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -17586,6 +20003,17 @@ packages: pretty-format: 28.1.3 dev: true + /jest-each/29.3.1: + resolution: {integrity: sha512-qrZH7PmFB9rEzCSl00BWjZYuS1BSOH8lLuC0azQE9lQrAx3PWGKHTDudQiOSwIy5dGAJh7KA0ScYlCP7JxvFYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.3.1 + chalk: 4.1.2 + jest-get-type: 29.2.0 + jest-util: 29.3.1 + pretty-format: 29.3.1 + dev: true + /jest-environment-jsdom/28.1.3: resolution: {integrity: sha512-HnlGUmZRdxfCByd3GM2F100DgQOajUBzEitjGqIREcb45kGjZvRrKUdlaF6escXBdcXNl0OBh+1ZrfeZT3GnAg==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -17605,6 +20033,29 @@ packages: - utf-8-validate dev: true + /jest-environment-jsdom/29.3.1: + resolution: {integrity: sha512-G46nKgiez2Gy4zvYNhayfMEAFlVHhWfncqvqS6yCd0i+a4NsSUD2WtrKSaYQrYiLQaupHXxCRi8xxVL2M9PbhA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + '@jest/environment': 29.3.1 + '@jest/fake-timers': 29.3.1 + '@jest/types': 29.3.1 + '@types/jsdom': 20.0.1 + '@types/node': 18.8.3 + jest-mock: 29.3.1 + jest-util: 29.3.1 + jsdom: 20.0.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + /jest-environment-node/28.1.3: resolution: {integrity: sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -17617,6 +20068,18 @@ packages: jest-util: 28.1.3 dev: true + /jest-environment-node/29.3.1: + resolution: {integrity: sha512-xm2THL18Xf5sIHoU7OThBPtuH6Lerd+Y1NLYiZJlkE3hbE+7N7r8uvHIl/FkZ5ymKXJe/11SQuf3fv4v6rUMag==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.3.1 + '@jest/fake-timers': 29.3.1 + '@jest/types': 29.3.1 + '@types/node': 18.8.3 + jest-mock: 29.3.1 + jest-util: 29.3.1 + dev: true + /jest-fetch-mock/3.0.3: resolution: {integrity: sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==} dependencies: @@ -17640,6 +20103,11 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dev: true + /jest-get-type/29.2.0: + resolution: {integrity: sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /jest-haste-map/26.6.2: resolution: {integrity: sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==} engines: {node: '>= 10.14.2'} @@ -17654,7 +20122,7 @@ packages: jest-serializer: 26.6.2 jest-util: 26.6.2 jest-worker: 26.6.2 - micromatch: 4.0.4 + micromatch: 4.0.5 sane: 4.1.0 walker: 1.0.8 optionalDependencies: @@ -17676,7 +20144,26 @@ packages: jest-regex-util: 28.0.2 jest-util: 28.1.3 jest-worker: 28.1.3 - micromatch: 4.0.4 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /jest-haste-map/29.3.1: + resolution: {integrity: sha512-/FFtvoG1xjbbPXQLFef+WSU4yrc0fc0Dds6aRPBojUid7qlPqZvxdUBA03HW0fnVHXVCnCdkuoghYItKNzc/0A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.3.1 + '@types/graceful-fs': 4.1.5 + '@types/node': 18.8.3 + anymatch: 3.1.3 + fb-watchman: 2.0.0 + graceful-fs: 4.2.10 + jest-regex-util: 29.2.0 + jest-util: 29.3.1 + jest-worker: 29.3.1 + micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: fsevents: 2.3.2 @@ -17718,6 +20205,14 @@ packages: pretty-format: 28.1.3 dev: true + /jest-leak-detector/29.3.1: + resolution: {integrity: sha512-3DA/VVXj4zFOPagGkuqHnSQf1GZBmmlagpguxEERO6Pla2g84Q1MaVIB3YMxgUaFIaYag8ZnTyQgiZ35YEqAQA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.2.0 + pretty-format: 29.3.1 + dev: true + /jest-matcher-utils/26.6.2: resolution: {integrity: sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==} engines: {node: '>= 10.14.2'} @@ -17748,6 +20243,16 @@ packages: pretty-format: 28.1.3 dev: true + /jest-matcher-utils/29.3.1: + resolution: {integrity: sha512-fkRMZUAScup3txIKfMe3AIZZmPEjWEdsPJFK3AIy5qRohWqQFg1qrmKfYXR9qEkNc7OdAu2N4KPHibEmy4HPeQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.3.1 + jest-get-type: 29.2.0 + pretty-format: 29.3.1 + dev: true + /jest-message-util/26.6.2: resolution: {integrity: sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==} engines: {node: '>= 10.14.2'} @@ -17757,7 +20262,7 @@ packages: '@types/stack-utils': 2.0.0 chalk: 4.1.2 graceful-fs: 4.2.10 - micromatch: 4.0.4 + micromatch: 4.0.5 pretty-format: 26.6.2 slash: 3.0.0 stack-utils: 2.0.5 @@ -17772,7 +20277,7 @@ packages: '@types/stack-utils': 2.0.0 chalk: 4.1.2 graceful-fs: 4.2.10 - micromatch: 4.0.4 + micromatch: 4.0.5 pretty-format: 27.5.1 slash: 3.0.0 stack-utils: 2.0.5 @@ -17787,12 +20292,27 @@ packages: '@types/stack-utils': 2.0.0 chalk: 4.1.2 graceful-fs: 4.2.10 - micromatch: 4.0.4 + micromatch: 4.0.5 pretty-format: 28.1.3 slash: 3.0.0 stack-utils: 2.0.5 dev: true + /jest-message-util/29.3.1: + resolution: {integrity: sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.18.6 + '@jest/types': 29.3.1 + '@types/stack-utils': 2.0.0 + chalk: 4.1.2 + graceful-fs: 4.2.10 + micromatch: 4.0.5 + pretty-format: 29.3.1 + slash: 3.0.0 + stack-utils: 2.0.5 + dev: true + /jest-mock-extended/2.0.2-beta2_l4uz7kl2eeclic7mumfbeqbwgi: resolution: {integrity: sha512-56zcpgRPs3YxQP0ejcaaNFxUinPyRxQCbuk7GGORZqEbAFuQVXWAAtru2tI1N4qcLBoDWEJ/hwUxwbEGY5hdyw==} peerDependencies: @@ -17812,6 +20332,15 @@ packages: '@types/node': 18.8.3 dev: true + /jest-mock/29.3.1: + resolution: {integrity: sha512-H8/qFDtDVMFvFP4X8NuOT3XRDzOUTz+FeACjufHzsOIBAxivLqkB1PoLCaJx9iPPQ8dZThHPp/G3WRWyMgA3JA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.3.1 + '@types/node': 18.8.3 + jest-util: 29.3.1 + dev: true + /jest-pnp-resolver/1.2.2_jest-resolve@26.6.2: resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} engines: {node: '>=6'} @@ -17836,6 +20365,18 @@ packages: jest-resolve: 28.1.3 dev: true + /jest-pnp-resolver/1.2.2_jest-resolve@29.3.1: + resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.3.1 + dev: true + /jest-regex-util/26.0.0: resolution: {integrity: sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==} engines: {node: '>= 10.14.2'} @@ -17851,6 +20392,11 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dev: true + /jest-regex-util/29.2.0: + resolution: {integrity: sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /jest-resolve-dependencies/28.1.3: resolution: {integrity: sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -17861,6 +20407,16 @@ packages: - supports-color dev: true + /jest-resolve-dependencies/29.3.1: + resolution: {integrity: sha512-Vk0cYq0byRw2WluNmNWGqPeRnZ3p3hHmjJMp2dyyZeYIfiBskwq4rpiuGFR6QGAdbj58WC7HN4hQHjf2mpvrLA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.2.0 + jest-snapshot: 29.3.1 + transitivePeerDependencies: + - supports-color + dev: true + /jest-resolve/26.6.2: resolution: {integrity: sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==} engines: {node: '>= 10.14.2'} @@ -17890,6 +20446,21 @@ packages: slash: 3.0.0 dev: true + /jest-resolve/29.3.1: + resolution: {integrity: sha512-amXJgH/Ng712w3Uz5gqzFBBjxV8WFLSmNjoreBGMqxgCz5cH7swmBZzgBaCIOsvb0NbpJ0vgaSFdJqMdT+rADw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.10 + jest-haste-map: 29.3.1 + jest-pnp-resolver: 1.2.2_jest-resolve@29.3.1 + jest-util: 29.3.1 + jest-validate: 29.3.1 + resolve: 1.22.0 + resolve.exports: 1.1.0 + slash: 3.0.0 + dev: true + /jest-runner/28.1.3: resolution: {integrity: sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -17919,6 +20490,35 @@ packages: - supports-color dev: true + /jest-runner/29.3.1: + resolution: {integrity: sha512-oFvcwRNrKMtE6u9+AQPMATxFcTySyKfLhvso7Sdk/rNpbhg4g2GAGCopiInk1OP4q6gz3n6MajW4+fnHWlU3bA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.3.1 + '@jest/environment': 29.3.1 + '@jest/test-result': 29.3.1 + '@jest/transform': 29.3.1 + '@jest/types': 29.3.1 + '@types/node': 18.8.3 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.10 + jest-docblock: 29.2.0 + jest-environment-node: 29.3.1 + jest-haste-map: 29.3.1 + jest-leak-detector: 29.3.1 + jest-message-util: 29.3.1 + jest-resolve: 29.3.1 + jest-runtime: 29.3.1 + jest-util: 29.3.1 + jest-watcher: 29.3.1 + jest-worker: 29.3.1 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + dev: true + /jest-runtime/28.1.3: resolution: {integrity: sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -17934,7 +20534,7 @@ packages: cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.0 execa: 5.1.1 - glob: 7.1.7 + glob: 7.2.3 graceful-fs: 4.2.10 jest-haste-map: 28.1.3 jest-message-util: 28.1.3 @@ -17949,6 +20549,36 @@ packages: - supports-color dev: true + /jest-runtime/29.3.1: + resolution: {integrity: sha512-jLzkIxIqXwBEOZx7wx9OO9sxoZmgT2NhmQKzHQm1xwR1kNW/dn0OjxR424VwHHf1SPN6Qwlb5pp1oGCeFTQ62A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.3.1 + '@jest/fake-timers': 29.3.1 + '@jest/globals': 29.3.1 + '@jest/source-map': 29.2.0 + '@jest/test-result': 29.3.1 + '@jest/transform': 29.3.1 + '@jest/types': 29.3.1 + '@types/node': 18.8.3 + chalk: 4.1.2 + cjs-module-lexer: 1.2.2 + collect-v8-coverage: 1.0.0 + glob: 7.2.3 + graceful-fs: 4.2.10 + jest-haste-map: 29.3.1 + jest-message-util: 29.3.1 + jest-mock: 29.3.1 + jest-regex-util: 29.2.0 + jest-resolve: 29.3.1 + jest-snapshot: 29.3.1 + jest-util: 29.3.1 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /jest-serializer/26.6.2: resolution: {integrity: sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==} engines: {node: '>= 10.14.2'} @@ -18020,6 +20650,38 @@ packages: - supports-color dev: true + /jest-snapshot/29.3.1: + resolution: {integrity: sha512-+3JOc+s28upYLI2OJM4PWRGK9AgpsMs/ekNryUV0yMBClT9B1DF2u2qay8YxcQd338PPYSFNb0lsar1B49sLDA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.20.5 + '@babel/generator': 7.20.5 + '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.20.5 + '@babel/plugin-syntax-typescript': 7.20.0_@babel+core@7.20.5 + '@babel/traverse': 7.20.5 + '@babel/types': 7.20.7 + '@jest/expect-utils': 29.3.1 + '@jest/transform': 29.3.1 + '@jest/types': 29.3.1 + '@types/babel__traverse': 7.0.15 + '@types/prettier': 2.7.2 + babel-preset-current-node-syntax: 1.0.1_@babel+core@7.20.5 + chalk: 4.1.2 + expect: 29.3.1 + graceful-fs: 4.2.10 + jest-diff: 29.3.1 + jest-get-type: 29.2.0 + jest-haste-map: 29.3.1 + jest-matcher-utils: 29.3.1 + jest-message-util: 29.3.1 + jest-util: 29.3.1 + natural-compare: 1.4.0 + pretty-format: 29.3.1 + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + dev: true + /jest-specific-snapshot/4.0.0_jest@28.1.3: resolution: {integrity: sha512-YdW5P/MVwOizWR0MJwURxdrjdXvdG2MMpXKVGr7dZ2YrBmE6E6Ab74UL3DOYmGmzaCnNAW1CL02pY5MTHE3ulQ==} peerDependencies: @@ -18040,7 +20702,7 @@ packages: chalk: 4.1.2 graceful-fs: 4.2.10 is-ci: 2.0.0 - micromatch: 4.0.4 + micromatch: 4.0.5 dev: true /jest-util/27.5.1: @@ -18103,6 +20765,18 @@ packages: pretty-format: 28.1.3 dev: true + /jest-validate/29.3.1: + resolution: {integrity: sha512-N9Lr3oYR2Mpzuelp1F8negJR3YE+L1ebk1rYA5qYo9TTY3f9OWdptLoNSPP9itOCBIRBqjt/S5XHlzYglLN67g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.3.1 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.2.0 + leven: 3.1.0 + pretty-format: 29.3.1 + dev: true + /jest-watcher/28.1.3: resolution: {integrity: sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -18117,6 +20791,20 @@ packages: string-length: 4.0.2 dev: true + /jest-watcher/29.3.1: + resolution: {integrity: sha512-RspXG2BQFDsZSRKGCT/NiNa8RkQ1iKAjrO0//soTMWx/QUt+OcxMqMSBxz23PYGqUuWm2+m2mNNsmj0eIoOaFg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.3.1 + '@jest/types': 29.3.1 + '@types/node': 18.8.3 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.3.1 + string-length: 4.0.2 + dev: true + /jest-worker/26.6.2: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} @@ -18173,6 +20861,26 @@ packages: - ts-node dev: true + /jest/29.3.1_@types+node@18.8.3: + resolution: {integrity: sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.3.1 + '@jest/types': 29.3.1 + import-local: 3.0.2 + jest-cli: 29.3.1_@types+node@18.8.3 + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + dev: true + /joi/17.7.0: resolution: {integrity: sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==} dependencies: @@ -18190,6 +20898,11 @@ packages: '@discoveryjs/natural-compare': 1.1.0 dev: true + /joycon/3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + dev: true + /jpeg-js/0.4.4: resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} dev: true @@ -18204,7 +20917,11 @@ packages: /js-cookie/2.2.1: resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} - dev: false + + /js-levenshtein/1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + dev: true /js-library-detector/6.4.0: resolution: {integrity: sha512-NB2sYpmgqiTd7PNNhgp6bnEZmjvTUdAbzxABvYXWLpTL/t158T6mPnD8uYNd0FDP73YWyMrTYDvPxqdvCTbv2g==} @@ -18288,7 +21005,7 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.8.0 + acorn: 8.8.1 acorn-globals: 6.0.0 cssom: 0.4.4 cssstyle: 2.3.0 @@ -18301,11 +21018,11 @@ packages: http-proxy-agent: 4.0.1 https-proxy-agent: 5.0.1 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.0 + nwsapi: 2.2.2 parse5: 6.0.1 saxes: 5.0.1 symbol-tree: 3.2.4 - tough-cookie: 4.0.0 + tough-cookie: 4.1.2 w3c-hr-time: 1.0.2 w3c-xmlserializer: 2.0.0 webidl-conversions: 6.1.0 @@ -18320,9 +21037,51 @@ packages: - utf-8-validate dev: true - /jsdom/19.0.0: - resolution: {integrity: sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==} - engines: {node: '>=12'} + /jsdom/19.0.0: + resolution: {integrity: sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==} + engines: {node: '>=12'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + acorn: 8.8.1 + acorn-globals: 6.0.0 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.4.3 + domexception: 4.0.0 + escodegen: 2.0.0 + form-data: 4.0.0 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.2 + parse5: 6.0.1 + saxes: 5.0.1 + symbol-tree: 3.2.4 + tough-cookie: 4.1.2 + w3c-hr-time: 1.0.2 + w3c-xmlserializer: 3.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 10.0.0 + ws: 8.11.0 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /jsdom/20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} peerDependencies: canvas: ^2.5.0 peerDependenciesMeta: @@ -18330,8 +21089,8 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.8.0 - acorn-globals: 6.0.0 + acorn: 8.8.1 + acorn-globals: 7.0.1 cssom: 0.5.0 cssstyle: 2.3.0 data-urls: 3.0.2 @@ -18343,17 +21102,16 @@ packages: http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.0 - parse5: 6.0.1 - saxes: 5.0.1 + nwsapi: 2.2.2 + parse5: 7.1.2 + saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 4.0.0 - w3c-hr-time: 1.0.2 - w3c-xmlserializer: 3.0.0 + tough-cookie: 4.1.2 + w3c-xmlserializer: 4.0.0 webidl-conversions: 7.0.0 whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 - whatwg-url: 10.0.0 + whatwg-url: 11.0.0 ws: 8.11.0 xml-name-validator: 4.0.0 transitivePeerDependencies: @@ -18391,6 +21149,21 @@ packages: /json-parse-even-better-errors/2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + /json-schema-compare/0.2.2: + resolution: {integrity: sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==} + dependencies: + lodash: 4.17.21 + dev: true + + /json-schema-merge-allof/0.8.1: + resolution: {integrity: sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==} + engines: {node: '>=12.0.0'} + dependencies: + compute-lcm: 1.1.2 + json-schema-compare: 0.2.2 + lodash: 4.17.21 + dev: true + /json-schema-ref-parser/9.0.6: resolution: {integrity: sha512-z0JGv7rRD3CnJbZY/qCpscyArdtLJhr/wRBmFUdoZ8xMjsFyNdILSprG2degqRLjBjyhZHAEBpGOxniO9rKTxA==} engines: {node: '>=10'} @@ -18408,8 +21181,8 @@ packages: '@types/lodash': 4.14.182 '@types/prettier': 2.7.2 cli-color: 2.0.0 - glob: 7.1.7 - glob-promise: 3.4.0_glob@7.1.7 + glob: 7.2.3 + glob-promise: 3.4.0_glob@7.2.3 is-glob: 4.0.3 json-schema-ref-parser: 9.0.6 json-stringify-safe: 5.0.1 @@ -18427,6 +21200,10 @@ packages: /json-schema-traverse/1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + /json-schema/0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + dev: true + /json-stable-stringify-without-jsonify/1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -18460,9 +21237,8 @@ packages: engines: {node: '>=6'} hasBin: true - /jsonc-parser/3.0.0: - resolution: {integrity: sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==} - dev: false + /jsonc-parser/3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} /jsonfile/2.4.0: resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==} @@ -18501,12 +21277,66 @@ packages: semver: 5.7.1 dev: true + /jss-plugin-camel-case/10.9.2: + resolution: {integrity: sha512-wgBPlL3WS0WDJ1lPJcgjux/SHnDuu7opmgQKSraKs4z8dCCyYMx9IDPFKBXQ8Q5dVYij1FFV0WdxyhuOOAXuTg==} + dependencies: + '@babel/runtime': 7.20.6 + hyphenate-style-name: 1.0.4 + jss: 10.9.2 + + /jss-plugin-default-unit/10.9.2: + resolution: {integrity: sha512-pYg0QX3bBEFtTnmeSI3l7ad1vtHU42YEEpgW7pmIh+9pkWNWb5dwS/4onSfAaI0kq+dOZHzz4dWe+8vWnanoSg==} + dependencies: + '@babel/runtime': 7.20.6 + jss: 10.9.2 + + /jss-plugin-global/10.9.2: + resolution: {integrity: sha512-GcX0aE8Ef6AtlasVrafg1DItlL/tWHoC4cGir4r3gegbWwF5ZOBYhx04gurPvWHC8F873aEGqge7C17xpwmp2g==} + dependencies: + '@babel/runtime': 7.20.6 + jss: 10.9.2 + + /jss-plugin-nested/10.9.2: + resolution: {integrity: sha512-VgiOWIC6bvgDaAL97XCxGD0BxOKM0K0zeB/ECyNaVF6FqvdGB9KBBWRdy2STYAss4VVA7i5TbxFZN+WSX1kfQA==} + dependencies: + '@babel/runtime': 7.20.6 + jss: 10.9.2 + tiny-warning: 1.0.3 + + /jss-plugin-props-sort/10.9.2: + resolution: {integrity: sha512-AP1AyUTbi2szylgr+O0OB7gkIxEGzySLITZ2GpsaoX72YMCGI2jYAc+WUhPfvUnZYiauF4zTnN4V4TGuvFjJlw==} + dependencies: + '@babel/runtime': 7.20.6 + jss: 10.9.2 + + /jss-plugin-rule-value-function/10.9.2: + resolution: {integrity: sha512-vf5ms8zvLFMub6swbNxvzsurHfUZ5Shy5aJB2gIpY6WNA3uLinEcxYyraQXItRHi5ivXGqYciFDRM2ZoVoRZ4Q==} + dependencies: + '@babel/runtime': 7.20.6 + jss: 10.9.2 + tiny-warning: 1.0.3 + + /jss-plugin-vendor-prefixer/10.9.2: + resolution: {integrity: sha512-SxcEoH+Rttf9fEv6KkiPzLdXRmI6waOTcMkbbEFgdZLDYNIP9UKNHFy6thhbRKqv0XMQZdrEsbDyV464zE/dUA==} + dependencies: + '@babel/runtime': 7.20.6 + css-vendor: 2.0.8 + jss: 10.9.2 + + /jss/10.9.2: + resolution: {integrity: sha512-b8G6rWpYLR4teTUbGd4I4EsnWjg7MN0Q5bSsjKhVkJVjhQDy2KzkbD2AW3TuT0RYZVmZZHKIrXDn6kjU14qkUg==} + dependencies: + '@babel/runtime': 7.20.6 + csstype: 3.1.0 + is-in-browser: 1.1.3 + tiny-warning: 1.0.3 + /jsx-ast-utils/3.3.0: resolution: {integrity: sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==} engines: {node: '>=4.0'} dependencies: - array-includes: 3.1.5 - object.assign: 4.1.2 + array-includes: 3.1.6 + object.assign: 4.1.4 /junk/1.0.3: resolution: {integrity: sha512-3KF80UaaSSxo8jVnRYtMKNGFOoVPBdkkVPsw+Ad0y4oxKXPduS6G6iHkrf69yJVff/VAaYXkV42rtZ7daJxU3w==} @@ -18541,6 +21371,10 @@ packages: safe-buffer: 5.2.1 dev: true + /jwt-decode/3.1.2: + resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} + dev: true + /keytar/7.9.0: resolution: {integrity: sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==} requiresBuild: true @@ -18594,6 +21428,10 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + /kleur/4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + /klona/2.0.5: resolution: {integrity: sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==} engines: {node: '>= 8'} @@ -18787,6 +21625,11 @@ packages: - utf-8-validate dev: true + /lilconfig/2.0.6: + resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==} + engines: {node: '>=10'} + dev: true + /lines-and-columns/1.1.6: resolution: {integrity: sha512-8ZmlJFVK9iCmtLz19HpSsR8HaAMWBT284VMNednLwlIMDP2hJDCIhUp0IZ2xUcZ+Ob6BM0VvCSJwzASDM45NLQ==} @@ -19005,6 +21848,7 @@ packages: /lodash.truncate/4.4.2: resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + dev: true /lodash.uniq/4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} @@ -19046,6 +21890,9 @@ packages: yargs: 15.4.1 dev: false + /longest-streak/3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + /lookup-closest-locale/6.0.4: resolution: {integrity: sha512-bWoFbSGe6f1GvMGzj17LrwMX4FhDXDwZyH04ySVCPbtOJADcSRguZNKewoJ3Ful/MOxD/wRHvFPadk/kYZUbuQ==} dev: true @@ -19090,7 +21937,6 @@ packages: dependencies: fault: 1.0.4 highlight.js: 10.7.3 - dev: true /lru-cache/4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -19126,6 +21972,11 @@ packages: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} dev: true + /luxon/3.2.1: + resolution: {integrity: sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==} + engines: {node: '>=12'} + dev: true + /lz-string/1.4.4: resolution: {integrity: sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==} hasBin: true @@ -19136,6 +21987,20 @@ packages: engines: {node: '>=6'} dev: true + /magic-string/0.26.7: + resolution: {integrity: sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==} + engines: {node: '>=12'} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /magic-string/0.27.0: + resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + /make-dir/1.3.0: resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==} engines: {node: '>=4'} @@ -19216,6 +22081,9 @@ packages: mdurl: 1.0.1 uc.micro: 1.0.5 + /markdown-table/3.0.3: + resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} + /marked/1.0.0: resolution: {integrity: sha512-Wo+L1pWTVibfrSr+TTtMuiMfNzmZWiOPeO7rZsQUY5bgsxpHesBEcIWJloWVTFnrMXnf/TL30eTFSGJddmQAng==} engines: {node: '>= 8.16.2'} @@ -19244,6 +22112,29 @@ packages: - supports-color dev: true + /matcher/3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 4.0.0 + dev: true + + /material-ui-popup-state/1.9.3_fvaebydoztlmlbmlu5u4butffa: + resolution: {integrity: sha512-+Ete5Tzw5rXlYfmqptOS8kBUH8vnK5OJsd6IQ7SHtLjU0PsvsmM73M/k8ot0xkX4RmPGuNRsFbK3mlCe/ClQuw==} + peerDependencies: + '@material-ui/core': ^4.0.0 || ^5.0.0-beta + react: ^15.0.0 || ^16.0.0 || ^17.0.0 + dependencies: + '@babel/runtime': 7.20.6 + '@material-ui/core': 4.12.4_7ir5hbqdbfw4we5ecpxz77f25m + '@material-ui/types': 6.0.2_@types+react@17.0.43 + classnames: 2.3.1 + prop-types: 15.8.1 + react: 18.2.0 + transitivePeerDependencies: + - '@types/react' + dev: true + /math-expression-evaluator/1.2.17: resolution: {integrity: sha512-NE0er6hC8jGXQ8ANbZvtovNS4jQDaZlJZkajBYbCsk+nktzTUfS67dTzrxY92iJ3LCGks4IQeNVdUbjCa8vhHg==} dev: false @@ -19252,6 +22143,14 @@ packages: resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} dev: true + /md5.js/1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + /md5/2.3.0: resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} dependencies: @@ -19272,6 +22171,94 @@ packages: unist-util-visit: 2.0.3 dev: true + /mdast-util-definitions/5.1.1: + resolution: {integrity: sha512-rQ+Gv7mHttxHOBx2dkF4HWTg+EE+UR78ptQWDylzPKaQuVGdG4HIoY3SrS/pCp80nZ04greFvXbVFHT+uf0JVQ==} + dependencies: + '@types/mdast': 3.0.3 + '@types/unist': 2.0.3 + unist-util-visit: 4.1.1 + + /mdast-util-find-and-replace/2.2.1: + resolution: {integrity: sha512-SobxkQXFAdd4b5WmEakmkVoh18icjQRxGy5OWTCzgsLRm1Fu/KCtwD1HIQSsmq5ZRjVH0Ehwg6/Fn3xIUk+nKw==} + dependencies: + escape-string-regexp: 5.0.0 + unist-util-is: 5.1.1 + unist-util-visit-parents: 5.1.1 + + /mdast-util-from-markdown/1.2.0: + resolution: {integrity: sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==} + dependencies: + '@types/mdast': 3.0.3 + '@types/unist': 2.0.3 + decode-named-character-reference: 1.0.2 + mdast-util-to-string: 3.1.0 + micromark: 3.1.0 + micromark-util-decode-numeric-character-reference: 1.0.0 + micromark-util-decode-string: 1.0.2 + micromark-util-normalize-identifier: 1.0.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + unist-util-stringify-position: 3.0.2 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + + /mdast-util-gfm-autolink-literal/1.0.2: + resolution: {integrity: sha512-FzopkOd4xTTBeGXhXSBU0OCDDh5lUj2rd+HQqG92Ld+jL4lpUfgX2AT2OHAVP9aEeDKp7G92fuooSZcYJA3cRg==} + dependencies: + '@types/mdast': 3.0.3 + ccount: 2.0.1 + mdast-util-find-and-replace: 2.2.1 + micromark-util-character: 1.1.0 + + /mdast-util-gfm-footnote/1.0.1: + resolution: {integrity: sha512-p+PrYlkw9DeCRkTVw1duWqPRHX6Ywh2BNKJQcZbCwAuP/59B0Lk9kakuAd7KbQprVO4GzdW8eS5++A9PUSqIyw==} + dependencies: + '@types/mdast': 3.0.3 + mdast-util-to-markdown: 1.5.0 + micromark-util-normalize-identifier: 1.0.0 + + /mdast-util-gfm-strikethrough/1.0.2: + resolution: {integrity: sha512-T/4DVHXcujH6jx1yqpcAYYwd+z5lAYMw4Ls6yhTfbMMtCt0PHY4gEfhW9+lKsLBtyhUGKRIzcUA2FATVqnvPDA==} + dependencies: + '@types/mdast': 3.0.3 + mdast-util-to-markdown: 1.5.0 + + /mdast-util-gfm-table/1.0.6: + resolution: {integrity: sha512-uHR+fqFq3IvB3Rd4+kzXW8dmpxUhvgCQZep6KdjsLK4O6meK5dYZEayLtIxNus1XO3gfjfcIFe8a7L0HZRGgag==} + dependencies: + '@types/mdast': 3.0.3 + markdown-table: 3.0.3 + mdast-util-from-markdown: 1.2.0 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + + /mdast-util-gfm-task-list-item/1.0.1: + resolution: {integrity: sha512-KZ4KLmPdABXOsfnM6JHUIjxEvcx2ulk656Z/4Balw071/5qgnhz+H1uGtf2zIGnrnvDC8xR4Fj9uKbjAFGNIeA==} + dependencies: + '@types/mdast': 3.0.3 + mdast-util-to-markdown: 1.5.0 + + /mdast-util-gfm/2.0.1: + resolution: {integrity: sha512-42yHBbfWIFisaAfV1eixlabbsa6q7vHeSPY+cg+BBjX51M8xhgMacqH9g6TftB/9+YkcI0ooV4ncfrJslzm/RQ==} + dependencies: + mdast-util-from-markdown: 1.2.0 + mdast-util-gfm-autolink-literal: 1.0.2 + mdast-util-gfm-footnote: 1.0.1 + mdast-util-gfm-strikethrough: 1.0.2 + mdast-util-gfm-table: 1.0.6 + mdast-util-gfm-task-list-item: 1.0.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + + /mdast-util-phrasing/3.0.0: + resolution: {integrity: sha512-S+QYsDRLkGi8U7o5JF1agKa/sdP+CNGXXLqC17pdTVL8FHHgQEiwFGa9yE5aYtUxNiFGYoaDy9V1kC85Sz86Gg==} + dependencies: + '@types/mdast': 3.0.3 + unist-util-is: 5.1.1 + /mdast-util-to-hast/10.0.1: resolution: {integrity: sha512-BW3LM9SEMnjf4HXXVApZMt8gLQWVNXc3jryK0nJu/rOXPOnlkUjmdkDlmxMirpbU9ILncGFIwLH/ubnWBbcdgA==} dependencies: @@ -19285,10 +22272,38 @@ packages: unist-util-visit: 2.0.3 dev: true + /mdast-util-to-hast/12.2.5: + resolution: {integrity: sha512-EFNhT35ZR/VZ85/EedDdCNTq0oFM+NM/+qBomVGQ0+Lcg0nhI8xIwmdCzNMlVlCJNXRprpobtKP/IUh8cfz6zQ==} + dependencies: + '@types/hast': 2.3.1 + '@types/mdast': 3.0.3 + mdast-util-definitions: 5.1.1 + micromark-util-sanitize-uri: 1.1.0 + trim-lines: 3.0.1 + unist-builder: 3.0.0 + unist-util-generated: 2.0.0 + unist-util-position: 4.0.3 + unist-util-visit: 4.1.1 + + /mdast-util-to-markdown/1.5.0: + resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==} + dependencies: + '@types/mdast': 3.0.3 + '@types/unist': 2.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 3.0.0 + mdast-util-to-string: 3.1.0 + micromark-util-decode-string: 1.0.2 + unist-util-visit: 4.1.1 + zwitch: 2.0.4 + /mdast-util-to-string/1.1.0: resolution: {integrity: sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==} dev: true + /mdast-util-to-string/3.1.0: + resolution: {integrity: sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==} + /mdi-react/8.1.0_react@18.1.0: resolution: {integrity: sha512-MK/u2TbzyTW61DTzqPdewFjwt/joHndswlEpLRIHXhzbs7E//wncS1C4LVy/U6MLUVTjEc/9Vm70Y7qRFN0StA==} peerDependencies: @@ -19299,7 +22314,6 @@ packages: /mdn-data/2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} - dev: true /mdn-data/2.0.4: resolution: {integrity: sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==} @@ -19335,9 +22349,8 @@ packages: dependencies: fs-monkey: 1.0.3 - /memoize-one/5.0.4: - resolution: {integrity: sha512-P0z5IeAH6qHHGkJIXWw0xC2HNEgkx/9uWWBQw64FJj3/ol14VYdfVGWWr0fXfjhhv3TKVIqUq65os6O4GUNksA==} - dev: false + /memoize-one/5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} /memoizee/0.4.14: resolution: {integrity: sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==} @@ -19403,7 +22416,7 @@ packages: dev: false /merge-descriptors/1.0.1: - resolution: {integrity: sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=} + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} /merge-stream/2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -19508,7 +22521,7 @@ packages: jest-serializer: 27.5.1 jest-util: 27.5.1 jest-worker: 27.5.1 - micromatch: 4.0.4 + micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: fsevents: 2.3.2 @@ -19747,6 +22760,227 @@ packages: resolution: {integrity: sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==} dev: true + /micromark-core-commonmark/1.0.6: + resolution: {integrity: sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==} + dependencies: + decode-named-character-reference: 1.0.2 + micromark-factory-destination: 1.0.0 + micromark-factory-label: 1.0.2 + micromark-factory-space: 1.0.0 + micromark-factory-title: 1.0.2 + micromark-factory-whitespace: 1.0.0 + micromark-util-character: 1.1.0 + micromark-util-chunked: 1.0.0 + micromark-util-classify-character: 1.0.0 + micromark-util-html-tag-name: 1.1.0 + micromark-util-normalize-identifier: 1.0.0 + micromark-util-resolve-all: 1.0.0 + micromark-util-subtokenize: 1.0.2 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + + /micromark-extension-gfm-autolink-literal/1.0.3: + resolution: {integrity: sha512-i3dmvU0htawfWED8aHMMAzAVp/F0Z+0bPh3YrbTPPL1v4YAlCZpy5rBO5p0LPYiZo0zFVkoYh7vDU7yQSiCMjg==} + dependencies: + micromark-util-character: 1.1.0 + micromark-util-sanitize-uri: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + + /micromark-extension-gfm-footnote/1.0.4: + resolution: {integrity: sha512-E/fmPmDqLiMUP8mLJ8NbJWJ4bTw6tS+FEQS8CcuDtZpILuOb2kjLqPEeAePF1djXROHXChM/wPJw0iS4kHCcIg==} + dependencies: + micromark-core-commonmark: 1.0.6 + micromark-factory-space: 1.0.0 + micromark-util-character: 1.1.0 + micromark-util-normalize-identifier: 1.0.0 + micromark-util-sanitize-uri: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + + /micromark-extension-gfm-strikethrough/1.0.4: + resolution: {integrity: sha512-/vjHU/lalmjZCT5xt7CcHVJGq8sYRm80z24qAKXzaHzem/xsDYb2yLL+NNVbYvmpLx3O7SYPuGL5pzusL9CLIQ==} + dependencies: + micromark-util-chunked: 1.0.0 + micromark-util-classify-character: 1.0.0 + micromark-util-resolve-all: 1.0.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + + /micromark-extension-gfm-table/1.0.5: + resolution: {integrity: sha512-xAZ8J1X9W9K3JTJTUL7G6wSKhp2ZYHrFk5qJgY/4B33scJzE2kpfRL6oiw/veJTbt7jiM/1rngLlOKPWr1G+vg==} + dependencies: + micromark-factory-space: 1.0.0 + micromark-util-character: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + + /micromark-extension-gfm-tagfilter/1.0.1: + resolution: {integrity: sha512-Ty6psLAcAjboRa/UKUbbUcwjVAv5plxmpUTy2XC/3nJFL37eHej8jrHrRzkqcpipJliuBH30DTs7+3wqNcQUVA==} + dependencies: + micromark-util-types: 1.0.2 + + /micromark-extension-gfm-task-list-item/1.0.3: + resolution: {integrity: sha512-PpysK2S1Q/5VXi72IIapbi/jliaiOFzv7THH4amwXeYXLq3l1uo8/2Be0Ac1rEwK20MQEsGH2ltAZLNY2KI/0Q==} + dependencies: + micromark-factory-space: 1.0.0 + micromark-util-character: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + + /micromark-extension-gfm/2.0.1: + resolution: {integrity: sha512-p2sGjajLa0iYiGQdT0oelahRYtMWvLjy8J9LOCxzIQsllMCGLbsLW+Nc+N4vi02jcRJvedVJ68cjelKIO6bpDA==} + dependencies: + micromark-extension-gfm-autolink-literal: 1.0.3 + micromark-extension-gfm-footnote: 1.0.4 + micromark-extension-gfm-strikethrough: 1.0.4 + micromark-extension-gfm-table: 1.0.5 + micromark-extension-gfm-tagfilter: 1.0.1 + micromark-extension-gfm-task-list-item: 1.0.3 + micromark-util-combine-extensions: 1.0.0 + micromark-util-types: 1.0.2 + + /micromark-factory-destination/1.0.0: + resolution: {integrity: sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==} + dependencies: + micromark-util-character: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + + /micromark-factory-label/1.0.2: + resolution: {integrity: sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==} + dependencies: + micromark-util-character: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + + /micromark-factory-space/1.0.0: + resolution: {integrity: sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==} + dependencies: + micromark-util-character: 1.1.0 + micromark-util-types: 1.0.2 + + /micromark-factory-title/1.0.2: + resolution: {integrity: sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==} + dependencies: + micromark-factory-space: 1.0.0 + micromark-util-character: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + + /micromark-factory-whitespace/1.0.0: + resolution: {integrity: sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==} + dependencies: + micromark-factory-space: 1.0.0 + micromark-util-character: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + + /micromark-util-character/1.1.0: + resolution: {integrity: sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==} + dependencies: + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + + /micromark-util-chunked/1.0.0: + resolution: {integrity: sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==} + dependencies: + micromark-util-symbol: 1.0.1 + + /micromark-util-classify-character/1.0.0: + resolution: {integrity: sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==} + dependencies: + micromark-util-character: 1.1.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + + /micromark-util-combine-extensions/1.0.0: + resolution: {integrity: sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==} + dependencies: + micromark-util-chunked: 1.0.0 + micromark-util-types: 1.0.2 + + /micromark-util-decode-numeric-character-reference/1.0.0: + resolution: {integrity: sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==} + dependencies: + micromark-util-symbol: 1.0.1 + + /micromark-util-decode-string/1.0.2: + resolution: {integrity: sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==} + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 1.1.0 + micromark-util-decode-numeric-character-reference: 1.0.0 + micromark-util-symbol: 1.0.1 + + /micromark-util-encode/1.0.1: + resolution: {integrity: sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==} + + /micromark-util-html-tag-name/1.1.0: + resolution: {integrity: sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==} + + /micromark-util-normalize-identifier/1.0.0: + resolution: {integrity: sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==} + dependencies: + micromark-util-symbol: 1.0.1 + + /micromark-util-resolve-all/1.0.0: + resolution: {integrity: sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==} + dependencies: + micromark-util-types: 1.0.2 + + /micromark-util-sanitize-uri/1.1.0: + resolution: {integrity: sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==} + dependencies: + micromark-util-character: 1.1.0 + micromark-util-encode: 1.0.1 + micromark-util-symbol: 1.0.1 + + /micromark-util-subtokenize/1.0.2: + resolution: {integrity: sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==} + dependencies: + micromark-util-chunked: 1.0.0 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + + /micromark-util-symbol/1.0.1: + resolution: {integrity: sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==} + + /micromark-util-types/1.0.2: + resolution: {integrity: sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==} + + /micromark/3.1.0: + resolution: {integrity: sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==} + dependencies: + '@types/debug': 4.1.7 + debug: 4.3.4 + decode-named-character-reference: 1.0.2 + micromark-core-commonmark: 1.0.6 + micromark-factory-space: 1.0.0 + micromark-util-character: 1.1.0 + micromark-util-chunked: 1.0.0 + micromark-util-combine-extensions: 1.0.0 + micromark-util-decode-numeric-character-reference: 1.0.0 + micromark-util-encode: 1.0.1 + micromark-util-normalize-identifier: 1.0.0 + micromark-util-resolve-all: 1.0.0 + micromark-util-sanitize-uri: 1.1.0 + micromark-util-subtokenize: 1.0.2 + micromark-util-symbol: 1.0.1 + micromark-util-types: 1.0.2 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + /micromatch/3.1.10: resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} engines: {node: '>=0.10.0'} @@ -19767,13 +23001,21 @@ packages: transitivePeerDependencies: - supports-color - /micromatch/4.0.4: - resolution: {integrity: sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==} + /micromatch/4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} dependencies: braces: 3.0.2 picomatch: 2.3.1 + /miller-rabin/4.0.1: + resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} + hasBin: true + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + dev: true + /mime-db/1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -19846,12 +23088,16 @@ packages: webpack: ^5.0.0 dependencies: schema-utils: 4.0.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /minimalistic-assert/1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + /minimalistic-crypto-utils/1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + dev: true + /minimatch/3.0.4: resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} dependencies: @@ -19870,6 +23116,13 @@ packages: brace-expansion: 1.1.11 dev: true + /minimatch/5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist-options/4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} engines: {node: '>= 6'} @@ -19910,6 +23163,13 @@ packages: yallist: 4.0.0 dev: true + /minipass/4.0.0: + resolution: {integrity: sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: true + /minizlib/2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} @@ -20001,7 +23261,7 @@ packages: dependencies: loader-utils: 2.0.4 monaco-editor: 0.24.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /monaco-editor/0.24.0: @@ -20041,6 +23301,10 @@ packages: - supports-color dev: true + /mri/1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + /ms/2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -20054,6 +23318,42 @@ packages: /ms/2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /msw/0.49.2_typescript@4.9.3: + resolution: {integrity: sha512-70/E10f+POE2UmMw16v8PnKatpZplpkUwVRLBqiIdimpgaC3le7y2yOq9JmOrL15jpwWM5wAcPTOj0f550LI3g==} + engines: {node: '>=14'} + hasBin: true + requiresBuild: true + peerDependencies: + typescript: '>= 4.4.x <= 4.9.x' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@mswjs/cookies': 0.2.2 + '@mswjs/interceptors': 0.17.6 + '@open-draft/until': 1.0.3 + '@types/cookie': 0.4.1 + '@types/js-levenshtein': 1.1.1 + chalk: 4.1.1 + chokidar: 3.5.3 + cookie: 0.4.2 + graphql: 15.4.0 + headers-polyfill: 3.1.2 + inquirer: 8.2.5 + is-node-process: 1.0.1 + js-levenshtein: 1.1.6 + node-fetch: 2.6.7 + outvariant: 1.3.0 + path-to-regexp: 6.2.0 + strict-event-emitter: 0.2.8 + type-fest: 2.19.0 + typescript: 4.9.3 + yargs: 17.6.2 + transitivePeerDependencies: + - encoding + - supports-color + dev: true + /multicast-dns/7.2.5: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true @@ -20088,8 +23388,25 @@ packages: dev: true optional: true - /nanocolors/0.1.12: - resolution: {integrity: sha512-2nMHqg1x5PU+unxX7PGY7AuYxl2qDx7PSrTRjizr8sxdd3l/3hBuWWaki62qmtYm2U5i4Z5E7GbjlyDFhs9/EQ==} + /nano-css/5.3.5_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-vSB9X12bbNu4ALBu7nigJgRViZ6ja3OU7CeuiV1zMIbXOdmkLahgtPmh3GBOlDxbKY0CitqlPdOReGlBLSp+yg==} + peerDependencies: + react: '*' + react-dom: '*' + dependencies: + css-tree: 1.1.3 + csstype: 3.1.0 + fastest-stable-stringify: 2.0.2 + inline-style-prefixer: 6.0.4 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + rtl-css-js: 1.16.1 + sourcemap-codec: 1.4.8 + stacktrace-js: 2.0.2 + stylis: 4.0.13 + + /nanoclone/0.2.1: + resolution: {integrity: sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==} dev: true /nanoid/3.1.20: @@ -20201,6 +23518,10 @@ packages: semver: 7.3.8 dev: true + /node-abort-controller/3.0.1: + resolution: {integrity: sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw==} + dev: true + /node-addon-api/4.3.0: resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} dev: true @@ -20239,6 +23560,34 @@ packages: /node-int64/0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + /node-libs-browser/2.2.1: + resolution: {integrity: sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==} + dependencies: + assert: 1.5.0 + browserify-zlib: 0.2.0 + buffer: 4.9.2 + console-browserify: 1.2.0 + constants-browserify: 1.0.0 + crypto-browserify: 3.12.0 + domain-browser: 1.2.0 + events: 3.3.0 + https-browserify: 1.0.0 + os-browserify: 0.3.0 + path-browserify: 0.0.1 + process: 0.11.10 + punycode: 1.3.2 + querystring-es3: 0.2.1 + readable-stream: 2.3.7 + stream-browserify: 2.0.2 + stream-http: 2.8.3 + string_decoder: 1.1.1 + timers-browserify: 2.0.12 + tty-browserify: 0.0.0 + url: 0.11.0 + util: 0.11.1 + vm-browserify: 1.1.2 + dev: true + /node-preload/0.2.1: resolution: {integrity: sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==} engines: {node: '>=8'} @@ -20323,10 +23672,33 @@ packages: once: 1.4.0 dev: true + /npm-bundled/2.0.1: + resolution: {integrity: sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + npm-normalize-package-bin: 2.0.0 + dev: true + /npm-normalize-package-bin/1.0.1: resolution: {integrity: sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==} dev: true + /npm-normalize-package-bin/2.0.0: + resolution: {integrity: sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dev: true + + /npm-packlist/5.1.3: + resolution: {integrity: sha512-263/0NGrn32YFYi4J533qzrQ/krmmrWwhKkzwTuM4f/07ug51odoaNjUexxO4vxlzURHcmYMH1QjvHjsNDKLVg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + hasBin: true + dependencies: + glob: 8.1.0 + ignore-walk: 5.0.1 + npm-bundled: 2.0.1 + npm-normalize-package-bin: 2.0.0 + dev: true + /npm-run-path/2.0.2: resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} engines: {node: '>=4'} @@ -20380,8 +23752,8 @@ packages: engines: {node: '>=0.10.0'} dev: true - /nwsapi/2.2.0: - resolution: {integrity: sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==} + /nwsapi/2.2.2: + resolution: {integrity: sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==} dev: true /nyc/15.1.0: @@ -20398,7 +23770,7 @@ packages: find-up: 4.1.0 foreground-child: 2.0.0 get-package-type: 0.1.0 - glob: 7.1.7 + glob: 7.2.3 istanbul-lib-coverage: 3.2.0 istanbul-lib-hook: 3.0.0 istanbul-lib-instrument: 4.0.3 @@ -20441,8 +23813,8 @@ packages: define-property: 0.2.5 kind-of: 3.2.2 - /object-inspect/1.12.0: - resolution: {integrity: sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==} + /object-inspect/1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} /object-keys/1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} @@ -20454,8 +23826,8 @@ packages: dependencies: isobject: 3.0.1 - /object.assign/4.1.2: - resolution: {integrity: sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==} + /object.assign/4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 @@ -20473,22 +23845,21 @@ packages: isobject: 3.0.1 dev: true - /object.entries/1.1.2: - resolution: {integrity: sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==} + /object.entries/1.1.6: + resolution: {integrity: sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==} engines: {node: '>= 0.4'} dependencies: + call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.1 - has: 1.0.3 + es-abstract: 1.21.1 - /object.fromentries/2.0.2: - resolution: {integrity: sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==} + /object.fromentries/2.0.6: + resolution: {integrity: sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==} engines: {node: '>= 0.4'} dependencies: + call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.1 - function-bind: 1.1.1 - has: 1.0.3 + es-abstract: 1.21.1 /object.getownpropertydescriptors/2.1.4: resolution: {integrity: sha512-sccv3L/pMModT6dJAYF3fzGMVcb38ysQ0tEE6ixv2yXJDtEIPph268OlAdJj5/qZMZDq2g/jqvwppt36uS/uQQ==} @@ -20497,9 +23868,15 @@ packages: array.prototype.reduce: 1.0.4 call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.1 + es-abstract: 1.21.1 dev: true + /object.hasown/1.1.2: + resolution: {integrity: sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==} + dependencies: + define-properties: 1.1.4 + es-abstract: 1.21.1 + /object.map/1.0.1: resolution: {integrity: sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==} engines: {node: '>=0.10.0'} @@ -20522,13 +23899,13 @@ packages: make-iterator: 1.0.1 dev: true - /object.values/1.1.5: - resolution: {integrity: sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==} + /object.values/1.1.6: + resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.1 + es-abstract: 1.21.1 /objectorarray/1.0.4: resolution: {integrity: sha512-91k8bjcldstRz1bG6zJo8lWD7c6QXcB4nTDUqiEvIL1xAsLoZlOOZZG+nd6YPz+V7zY1580J4Xxh1vZtyv4i/w==} @@ -20546,12 +23923,12 @@ packages: engines: {node: '>= 14'} dependencies: '@octokit/app': 13.0.9 - '@octokit/core': 4.0.5 + '@octokit/core': 4.1.0 '@octokit/oauth-app': 4.1.0 - '@octokit/plugin-paginate-rest': 4.3.1_@octokit+core@4.0.5 - '@octokit/plugin-rest-endpoint-methods': 6.6.2_@octokit+core@4.0.5 + '@octokit/plugin-paginate-rest': 4.3.1_@octokit+core@4.1.0 + '@octokit/plugin-rest-endpoint-methods': 6.7.0_@octokit+core@4.1.0 '@octokit/plugin-retry': 3.0.9 - '@octokit/plugin-throttling': 4.3.0_@octokit+core@4.0.5 + '@octokit/plugin-throttling': 4.3.0_@octokit+core@4.1.0 '@octokit/types': 7.5.1 transitivePeerDependencies: - encoding @@ -20695,6 +24072,10 @@ packages: url-parse: 1.5.10 dev: false + /os-browserify/0.3.0: + resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} + dev: true + /os-homedir/1.0.2: resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} engines: {node: '>=0.10.0'} @@ -20735,6 +24116,10 @@ packages: os-tmpdir: 1.0.2 dev: true + /outvariant/1.3.0: + resolution: {integrity: sha512-yeWM9k6UPfG/nzxdaPlJkB2p08hCg4xP6Lx99F+vP8YF7xyZVfTmJjrrNalkmzudD4WFvNLVudQikqUmF8zhVQ==} + dev: true + /p-all/2.1.0: resolution: {integrity: sha512-HbZxz5FONzz/z2gJfk6bFca0BCiSRF8jU3yCsWOen/vR6lZjfPOu/e7L3uFzTW1i0H8TlC3vqQstEJPQL4/uLA==} engines: {node: '>=6'} @@ -20925,6 +24310,10 @@ packages: semver: 6.3.0 dev: true + /pako/1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + dev: true + /param-case/3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: @@ -20938,6 +24327,16 @@ packages: dependencies: callsites: 3.1.0 + /parse-asn1/5.1.6: + resolution: {integrity: sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==} + dependencies: + asn1.js: 5.4.1 + browserify-aes: 1.2.0 + evp_bytestokey: 1.0.3 + pbkdf2: 3.1.2 + safe-buffer: 5.2.1 + dev: true + /parse-cache-control/1.0.1: resolution: {integrity: sha1-juqz5U+laSD+Fro493+iGqzC104=} dev: true @@ -20951,7 +24350,6 @@ packages: is-alphanumerical: 1.0.2 is-decimal: 1.0.4 is-hexadecimal: 1.0.2 - dev: true /parse-filepath/1.0.2: resolution: {integrity: sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==} @@ -21000,6 +24398,12 @@ packages: engines: {node: '>=0.10.0'} dev: true + /parse-path/7.0.0: + resolution: {integrity: sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==} + dependencies: + protocols: 2.0.1 + dev: true + /parse-semver/1.1.1: resolution: {integrity: sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==} dependencies: @@ -21010,11 +24414,17 @@ packages: resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} dev: false + /parse-url/8.1.0: + resolution: {integrity: sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==} + dependencies: + parse-path: 7.0.0 + dev: true + /parse5-htmlparser2-tree-adapter/7.0.0: resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} dependencies: domhandler: 5.0.3 - parse5: 7.0.0 + parse5: 7.1.2 dev: true /parse5/4.0.0: @@ -21025,10 +24435,10 @@ packages: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} dev: true - /parse5/7.0.0: - resolution: {integrity: sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==} + /parse5/7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: - entities: 4.3.1 + entities: 4.4.0 dev: true /parseurl/1.3.3: @@ -21046,6 +24456,10 @@ packages: resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} engines: {node: '>=0.10.0'} + /path-browserify/0.0.1: + resolution: {integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==} + dev: true + /path-browserify/1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -21060,6 +24474,10 @@ packages: resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==} dev: true + /path-equal/1.2.5: + resolution: {integrity: sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==} + dev: true + /path-exists/2.1.0: resolution: {integrity: sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==} engines: {node: '>=0.10.0'} @@ -21154,6 +24572,17 @@ packages: util: 0.10.3 dev: true + /pbkdf2/3.1.2: + resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} + engines: {node: '>=0.12'} + dependencies: + create-hash: 1.2.0 + create-hmac: 1.1.7 + ripemd160: 2.0.2 + safe-buffer: 5.2.1 + sha.js: 2.4.11 + dev: true + /pend/1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} dev: true @@ -21187,6 +24616,11 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + /pify/5.0.0: + resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} + engines: {node: '>=10'} + dev: true + /pinkie-promise/2.0.1: resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} engines: {node: '>=0.10.0'} @@ -21236,6 +24670,13 @@ packages: dependencies: find-up: 5.0.0 + /pkg-up/3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + dependencies: + find-up: 3.0.0 + dev: true + /plur/4.0.0: resolution: {integrity: sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==} engines: {node: '>=10'} @@ -21246,7 +24687,6 @@ packages: /pluralize/8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} - dev: true /pngjs/3.4.0: resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} @@ -21295,6 +24735,9 @@ packages: '@babel/runtime': 7.20.6 dev: true + /popper.js/1.16.1-lts: + resolution: {integrity: sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==} + /posix-character-classes/0.1.1: resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} engines: {node: '>=0.10.0'} @@ -21383,6 +24826,23 @@ packages: postcss: 6.0.1 dev: false + /postcss-load-config/3.1.4_postcss@8.4.19: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.0.6 + postcss: 8.4.19 + yaml: 1.10.2 + dev: true + /postcss-loader/4.2.0_is25vgl4syps62ryudctlza7cy: resolution: {integrity: sha512-mqgScxHqbiz1yxbnNcPdKYo/6aVt+XExURmEbQlviFVWogDbM4AJ0A/B+ZBpYsJrTRxKw7HyRazg9x0Q9SWwLA==} engines: {node: '>= 10.13.0'} @@ -21396,7 +24856,7 @@ packages: postcss: 7.0.39 schema-utils: 3.1.1 semver: 7.3.8 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /postcss-loader/7.0.2_upg3rk2kpasnbk27hkqapxaxfq: @@ -21410,7 +24870,7 @@ packages: klona: 2.0.5 postcss: 8.4.19 semver: 7.3.8 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /postcss-media-query-parser/0.2.3: @@ -21579,6 +25039,22 @@ packages: postcss: 8.4.19 dev: true + /postcss-modules/4.3.1_postcss@8.4.19: + resolution: {integrity: sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==} + peerDependencies: + postcss: ^8.0.0 + dependencies: + generic-names: 4.0.0 + icss-replace-symbols: 1.1.0 + lodash.camelcase: 4.3.0 + postcss: 8.4.19 + postcss-modules-extract-imports: 3.0.0_postcss@8.4.19 + postcss-modules-local-by-default: 4.0.0_postcss@8.4.19 + postcss-modules-scope: 3.0.0_postcss@8.4.19 + postcss-modules-values: 4.0.0_postcss@8.4.19 + string-hash: 1.1.3 + dev: true + /postcss-modules/6.0.0_postcss@8.4.19: resolution: {integrity: sha512-7DGfnlyi/ju82BRzTIjWS5C4Tafmzl3R79YP/PASiocj+aa6yYphHhhKUOEoXQToId5rgyFgJ88+ccOUydjBXQ==} peerDependencies: @@ -21911,6 +25387,15 @@ packages: react-is: 18.2.0 dev: true + /pretty-format/29.3.1: + resolution: {integrity: sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.0.0 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + /pretty-format/3.8.0: resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} dev: true @@ -21922,7 +25407,6 @@ packages: /prismjs/1.27.0: resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} engines: {node: '>=6'} - dev: true /process-nextick-args/1.0.7: resolution: {integrity: sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw==} @@ -21945,6 +25429,7 @@ packages: /progress/2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + dev: true /promise-inflight/1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} @@ -21965,7 +25450,7 @@ packages: dependencies: array.prototype.map: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.1 + es-abstract: 1.21.1 function-bind: 1.1.1 iterate-value: 1.0.2 dev: true @@ -21975,10 +25460,15 @@ packages: engines: {node: '>= 0.4'} dependencies: define-properties: 1.1.4 - es-abstract: 1.20.1 + es-abstract: 1.21.1 function-bind: 1.1.1 dev: true + /promise.series/0.2.0: + resolution: {integrity: sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==} + engines: {node: '>=0.12'} + dev: true + /promise/7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} dependencies: @@ -21999,8 +25489,8 @@ packages: sisteransi: 1.0.5 dev: true - /prompts/2.4.0: - resolution: {integrity: sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==} + /prompts/2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} dependencies: kleur: 3.0.3 @@ -22013,10 +25503,20 @@ packages: object-assign: 4.1.1 react-is: 16.13.1 + /property-expr/2.0.5: + resolution: {integrity: sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==} + dev: true + /property-information/5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} dependencies: xtend: 4.0.2 + + /property-information/6.2.0: + resolution: {integrity: sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==} + + /protocols/2.0.1: + resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} dev: true /proxy-addr/2.0.7: @@ -22063,6 +25563,17 @@ packages: resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==} dev: true + /public-encrypt/4.0.3: + resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} + dependencies: + bn.js: 4.12.0 + browserify-rsa: 4.1.0 + create-hash: 1.2.0 + parse-asn1: 5.1.6 + randombytes: 2.1.0 + safe-buffer: 5.2.1 + dev: true + /pump/2.0.1: resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} dependencies: @@ -22086,7 +25597,6 @@ packages: /punycode/1.3.2: resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} - dev: false /punycode/2.1.1: resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} @@ -22151,6 +25661,11 @@ packages: engines: {node: '>=0.6'} dev: true + /querystring-es3/0.2.1: + resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==} + engines: {node: '>=0.4.x'} + dev: true + /querystring/0.2.0: resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} engines: {node: '>=0.4.x'} @@ -22175,6 +25690,9 @@ packages: engines: {node: '>=10'} dev: false + /raf-schd/4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + /raf/3.4.1: resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} dependencies: @@ -22189,6 +25707,13 @@ packages: dependencies: safe-buffer: 5.2.1 + /randomfill/1.0.4: + resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==} + dependencies: + randombytes: 2.1.0 + safe-buffer: 5.2.1 + dev: true + /range-parser/1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -22223,9 +25748,32 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.1.1 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true + /rc-progress/3.4.1_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-eAFDHXlk8aWpoXl0llrenPMt9qKHQXphxcVsnKs0FHC6eCSk1ebJtyaVjJUzKe0233ogiLDeEFK1Uihz3s67hw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + dependencies: + '@babel/runtime': 7.20.6 + classnames: 2.3.1 + rc-util: 5.27.1_biqbaboplfbrettd7655fr4n2y + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + + /rc-util/5.27.1_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-PsjHA+f+KBCz+YTZxrl3ukJU5RoNKoe3KSNMh0xGiISbR67NaM9E9BiMjCwxa3AcCUOg/rZ+V0ZKLSimAA+e3w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + dependencies: + '@babel/runtime': 7.20.6 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-is: 16.13.1 + /rc/1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -22236,6 +25784,24 @@ packages: strip-json-comments: 2.0.1 dev: true + /react-beautiful-dnd/13.1.1_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} + peerDependencies: + react: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.20.6 + css-box-model: 1.2.1 + memoize-one: 5.2.1 + raf-schd: 4.0.3 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-redux: 7.2.9_biqbaboplfbrettd7655fr4n2y + redux: 4.2.0 + use-memo-one: 1.1.3_react@18.2.0 + transitivePeerDependencies: + - react-native + /react-calendar/3.7.0_ef5jwxihqo6n7gxfmzogljlgcm: resolution: {integrity: sha512-zkK95zWLWLC6w3O7p3SHx/FJXEyyD2UMd4jr3CrKD+G73N+G5vEwrXxYQCNivIPoFNBjqoyYYGlkHA+TBDPLCw==} peerDependencies: @@ -22267,6 +25833,48 @@ packages: react: 18.1.0 dev: false + /react-dev-utils/12.0.1_7nbrhxx5wbdrea3w4d3cjhku4y: + resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=2.7' + webpack: '>=4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@babel/code-frame': 7.18.6 + address: 1.1.2 + browserslist: 4.21.4 + chalk: 4.1.2 + cross-spawn: 7.0.3 + detect-port-alt: 1.1.6 + escape-string-regexp: 4.0.0 + filesize: 8.0.7 + find-up: 5.0.0 + fork-ts-checker-webpack-plugin: 6.5.2_7nbrhxx5wbdrea3w4d3cjhku4y + global-modules: 2.0.0 + globby: 11.1.0 + gzip-size: 6.0.0 + immer: 9.0.18 + is-root: 2.1.0 + loader-utils: 3.2.1 + open: 8.4.0 + pkg-up: 3.1.0 + prompts: 2.4.2 + react-error-overlay: 6.0.11 + recursive-readdir: 2.2.2 + shell-quote: 1.7.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + typescript: 4.9.3 + webpack: 5.75.0_cw4su4nzareykaft5p7arksy5i + transitivePeerDependencies: + - eslint + - supports-color + - vue-template-compiler + dev: true + /react-devtools-core/4.24.0: resolution: {integrity: sha512-Rw7FzYOOzcfyUPaAm9P3g0tFdGqGq2LLiAI+wjYcp6CsF3DeeMrRS3HZAho4s273C29G/DJhx0e8BpRE/QZNGg==} dependencies: @@ -22311,6 +25919,23 @@ packages: react: 18.1.0 scheduler: 0.22.0 + /react-dom/18.2.0_react@18.2.0: + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + + /react-double-scrollbar/0.0.15_react@18.2.0: + resolution: {integrity: sha512-dLz3/WBIpgFnzFY0Kb4aIYBMT2BWomHuW2DH6/9jXfS6/zxRRBUFQ04My4HIB7Ma7QoRBpcy8NtkPeFgcGBpgg==} + engines: {node: '>=0.12.0'} + peerDependencies: + react: '>= 0.14.7' + dependencies: + react: 18.2.0 + /react-draggable/4.4.3_ef5jwxihqo6n7gxfmzogljlgcm: resolution: {integrity: sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==} peerDependencies: @@ -22346,6 +25971,13 @@ packages: react: 18.1.0 dev: true + /react-error-overlay/6.0.11: + resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==} + dev: true + + /react-fast-compare/3.2.0: + resolution: {integrity: sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==} + /react-focus-lock/2.7.1_ci6b7qrbzfuzg4ahcdqxg6om2y: resolution: {integrity: sha512-ImSeVmcrLKNMqzUsIdqOkXwTVltj79OPu43oT8tVun7eIckA4VdM7UmYUFo3H/UC2nRVgagMZGFnAOQEDiDYcA==} peerDependencies: @@ -22377,6 +26009,25 @@ packages: react-resizable: 3.0.4_ef5jwxihqo6n7gxfmzogljlgcm dev: false + /react-helmet/6.1.0_react@18.2.0: + resolution: {integrity: sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==} + peerDependencies: + react: '>=16.3.0' + dependencies: + object-assign: 4.1.1 + prop-types: 15.8.1 + react: 18.2.0 + react-fast-compare: 3.2.0 + react-side-effect: 2.1.2_react@18.2.0 + + /react-hook-form/7.42.1_react@18.2.0: + resolution: {integrity: sha512-2UIGqwMZksd5HS55crTT1ATLTr0rAI4jS7yVuqTaoRVDhY2Qc4IyjskCmpnmdYqUNOYFy04vW253tb2JRVh+IQ==} + engines: {node: '>=12.22.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + react: 18.2.0 + /react-inspector/5.1.1_react@18.1.0: resolution: {integrity: sha512-GURDaYzoLbW8pMGXwYPDBIv6nqei4kK7LPRZ9q9HCZF54wqXz/dnylBp/kfE9XmekBhHvLDdcYeyIwSrvtOiWg==} peerDependencies: @@ -22415,6 +26066,32 @@ packages: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} dev: false + /react-markdown/8.0.5_vqvt52xx3h2ersphfxson777fi: + resolution: {integrity: sha512-jGJolWWmOWAvzf+xMdB9zwStViODyyFQhNB/bwCerbBKmrTmgmA599CGiOlP58OId1IMoIRsA8UdI1Lod4zb5A==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + dependencies: + '@types/hast': 2.3.1 + '@types/prop-types': 15.7.5 + '@types/react': 17.0.43 + '@types/unist': 2.0.3 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 2.0.1 + prop-types: 15.8.1 + property-information: 6.2.0 + react: 18.2.0 + react-is: 18.2.0 + remark-parse: 10.0.1 + remark-rehype: 10.1.0 + space-separated-tokens: 2.0.2 + style-to-object: 0.4.1 + unified: 10.1.2 + unist-util-visit: 4.1.1 + vfile: 5.3.6 + transitivePeerDependencies: + - supports-color + /react-native-codegen/0.70.6_@babel+preset-env@7.20.2: resolution: {integrity: sha512-kdwIhH2hi+cFnG5Nb8Ji2JwmcCxnaOOo9440ov7XDzSvGfmUStnCzl+MCW8jLjqHcE4icT7N9y+xx4f50vfBTw==} dependencies: @@ -22451,7 +26128,7 @@ packages: event-target-shim: 5.0.1 invariant: 2.2.4 jsc-android: 250230.2.1 - memoize-one: 5.0.4 + memoize-one: 5.2.1 metro-react-native-babel-transformer: 0.72.3_@babel+core@7.20.5 metro-runtime: 0.72.3 metro-source-map: 0.72.3 @@ -22504,6 +26181,27 @@ packages: scheduler: 0.21.0 dev: false + /react-redux/7.2.9_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.20.6 + '@types/react-redux': 7.1.25 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-is: 17.0.2 + /react-refresh/0.10.0: resolution: {integrity: sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ==} engines: {node: '>=0.10.0'} @@ -22514,6 +26212,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /react-refresh/0.14.0: + resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} + engines: {node: '>=0.10.0'} + dev: true + /react-refresh/0.4.3: resolution: {integrity: sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA==} engines: {node: '>=0.10.0'} @@ -22603,10 +26306,22 @@ packages: prop-types: 15.8.1 react: 18.1.0 react-router: 5.2.0_react@18.1.0 - tiny-invariant: 1.0.3 + tiny-invariant: 1.3.1 tiny-warning: 1.0.3 dev: false + /react-router-dom/6.7.0_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-jQtXUJyhso3kFw430+0SPCbmCmY1/kJv8iRffGHwHy3CkoomGxeYzMkmeSPYo6Egzh3FKJZRAL22yg5p2tXtfg==} + engines: {node: '>=14'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@remix-run/router': 1.3.0 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-router: 6.7.0_react@18.2.0 + /react-router/5.2.0_react@18.1.0: resolution: {integrity: sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==} peerDependencies: @@ -22621,7 +26336,7 @@ packages: prop-types: 15.8.1 react: 18.1.0 react-is: 16.13.1 - tiny-invariant: 1.0.3 + tiny-invariant: 1.3.1 tiny-warning: 1.0.3 dev: false @@ -22634,6 +26349,15 @@ packages: react: 18.1.0 dev: false + /react-router/6.7.0_react@18.2.0: + resolution: {integrity: sha512-KNWlG622ddq29MAM159uUsNMdbX8USruoKnwMMQcs/QWZgFUayICSn2oB7reHce1zPj6CG18kfkZIunSSRyGHg==} + engines: {node: '>=14'} + peerDependencies: + react: '>=16.8' + dependencies: + '@remix-run/router': 1.3.0 + react: 18.2.0 + /react-shallow-renderer/16.15.0_react@18.1.0: resolution: {integrity: sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==} peerDependencies: @@ -22644,6 +26368,13 @@ packages: react-is: 18.2.0 dev: false + /react-side-effect/2.1.2_react@18.2.0: + resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==} + peerDependencies: + react: ^16.3.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + /react-sizeme/3.0.1_ef5jwxihqo6n7gxfmzogljlgcm: resolution: {integrity: sha512-9Hf1NLgSbny1bha77l9HwvwwxQUJxFUqi44Ih+y3evA+PezBpGdCGlnvye6avss2cIgs9PgdYgMnfuzJWn/RUw==} peerDependencies: @@ -22672,6 +26403,16 @@ packages: react-transition-group: 2.9.0_ef5jwxihqo6n7gxfmzogljlgcm dev: false + /react-sparklines/1.7.0_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-bJFt9K4c5Z0k44G8KtxIhbG+iyxrKjBZhdW6afP+R7EnIq+iKjbWbEFISrf3WKNFsda+C46XAfnX0StS5fbDcg==} + peerDependencies: + react: '*' + react-dom: '*' + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + /react-spring/9.4.2_ssv3vkwxg74iun7wj74vdfwzhu: resolution: {integrity: sha512-mK9xdq1kAhbe5YpP4EG2IzRa2C1M1UfR/MO1f83PE+IpHwCm1nGQhteF3MGyX6I3wnkoBWTXbY6n4443Dp52Og==} dependencies: @@ -22732,6 +26473,18 @@ packages: refractor: 3.6.0 dev: true + /react-syntax-highlighter/15.5.0_react@18.2.0: + resolution: {integrity: sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==} + peerDependencies: + react: '>= 0.14.0' + dependencies: + '@babel/runtime': 7.20.6 + highlight.js: 10.7.3 + lowlight: 1.20.0 + prismjs: 1.27.0 + react: 18.2.0 + refractor: 3.6.0 + /react-test-renderer/16.14.0_react@18.1.0: resolution: {integrity: sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==} peerDependencies: @@ -22744,6 +26497,16 @@ packages: scheduler: 0.19.1 dev: true + /react-text-truncate/0.19.0_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-QxHpZABfGG0Z3WEYbRTZ+rXdZn50Zvp+sWZXgVAd7FCKAMzv/kcwctTpNmWgXDTpAoHhMjOVwmgRtX3x5yeF4w==} + peerDependencies: + react: ^15.4.1 || ^16.0.0 || ^17.0.0 || || ^18.0.0 + react-dom: ^15.4.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + /react-transition-group/2.9.0_ef5jwxihqo6n7gxfmzogljlgcm: resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==} peerDependencies: @@ -22758,16 +26521,71 @@ packages: react-lifecycles-compat: 3.0.4 dev: false + /react-transition-group/4.4.5_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + dependencies: + '@babel/runtime': 7.20.6 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + + /react-universal-interface/0.6.2_react@18.2.0+tslib@2.1.0: + resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} + peerDependencies: + react: '*' + tslib: '*' + dependencies: + react: 18.2.0 + tslib: 2.1.0 + /react-use-measure/2.1.1_ef5jwxihqo6n7gxfmzogljlgcm: resolution: {integrity: sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==} peerDependencies: react: '>=16.13' react-dom: '>=16.13' dependencies: - debounce: 1.2.1 - react: 18.1.0 - react-dom: 18.1.0_react@18.1.0 - dev: false + debounce: 1.2.1 + react: 18.1.0 + react-dom: 18.1.0_react@18.1.0 + dev: false + + /react-use/17.4.0_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-TgbNTCA33Wl7xzIJegn1HndB4qTS9u03QUwyNycUnXaweZkE4Kq2SB+Yoxx8qbshkZGYBDvUXbXWRUmQDcZZ/Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@types/js-cookie': 2.2.6 + '@xobotyi/scrollbar-width': 1.9.5 + copy-to-clipboard: 3.3.1 + fast-deep-equal: 3.1.3 + fast-shallow-equal: 1.0.0 + js-cookie: 2.2.1 + nano-css: 5.3.5_biqbaboplfbrettd7655fr4n2y + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-universal-interface: 0.6.2_react@18.2.0+tslib@2.1.0 + resize-observer-polyfill: 1.5.1 + screenfull: 5.2.0 + set-harmonic-interval: 1.0.1 + throttle-debounce: 3.0.1 + ts-easing: 0.2.0 + tslib: 2.1.0 + + /react-virtualized-auto-sizer/1.0.7_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc + dependencies: + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 /react-visibility-sensor/5.1.1_ef5jwxihqo6n7gxfmzogljlgcm: resolution: {integrity: sha512-cTUHqIK+zDYpeK19rzW6zF9YfT4486TIgizZW53wEZ+/GPBbK7cNS0EHyJVyHYacwFEvvHLEKfgJndbemWhB/w==} @@ -22780,6 +26598,18 @@ packages: react-dom: 18.1.0_react@18.1.0 dev: false + /react-window/1.8.8_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.20.6 + memoize-one: 5.2.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + /react-zdog/1.0.11_qvb5bem4xlk5xwkq2cfcylelxe: resolution: {integrity: sha512-L6/8Zi+Nf+faNMsSZ31HLmLlu6jcbs/jqqFvme7CFnYjAeYfhJ4HyuHKd7Pu/zk9tegv6FaJj1v+hmUwUpKLQw==} peerDependencies: @@ -22804,6 +26634,12 @@ packages: dependencies: loose-envify: 1.4.0 + /react/18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + /read-installed/4.0.3: resolution: {integrity: sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==} dependencies: @@ -22820,7 +26656,7 @@ packages: /read-package-json/2.1.1: resolution: {integrity: sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A==} dependencies: - glob: 7.1.7 + glob: 7.2.3 json-parse-better-errors: 1.0.2 normalize-package-data: 2.5.0 npm-normalize-package-bin: 1.0.1 @@ -22864,6 +26700,16 @@ packages: type-fest: 0.6.0 dev: true + /read-yaml-file/1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + dependencies: + graceful-fs: 4.2.10 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + dev: true + /read-yaml-file/2.1.0: resolution: {integrity: sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==} engines: {node: '>=10.13'} @@ -23032,13 +26878,17 @@ packages: balanced-match: 1.0.0 dev: false + /redux/4.2.0: + resolution: {integrity: sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==} + dependencies: + '@babel/runtime': 7.20.6 + /refractor/3.6.0: resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} dependencies: hastscript: 6.0.0 parse-entities: 2.0.0 prismjs: 1.27.0 - dev: true /regenerate-unicode-properties/10.1.0: resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} @@ -23157,6 +27007,16 @@ packages: resolution: {integrity: sha512-3Clt8ZMH75Ayjp9q4CorNeyjwIxHFcTkaektplKGl2A1jNGEUey8cKL0ZC5vJwfcD5GFGsNLImLG/NGzWIzoMQ==} dev: true + /remark-gfm/3.0.1: + resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==} + dependencies: + '@types/mdast': 3.0.3 + mdast-util-gfm: 2.0.1 + micromark-extension-gfm: 2.0.1 + unified: 10.1.2 + transitivePeerDependencies: + - supports-color + /remark-mdx/1.6.22: resolution: {integrity: sha512-phMHBJgeV76uyFkH4rvzCftLfKCr2RZuF+/gmVcaKrpsihyzmhXjA0BEMDaPTXG5y8qZOKPVo83NAOX01LPnOQ==} dependencies: @@ -23172,6 +27032,15 @@ packages: - supports-color dev: true + /remark-parse/10.0.1: + resolution: {integrity: sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==} + dependencies: + '@types/mdast': 3.0.3 + mdast-util-from-markdown: 1.2.0 + unified: 10.1.2 + transitivePeerDependencies: + - supports-color + /remark-parse/8.0.3: resolution: {integrity: sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q==} dependencies: @@ -23193,6 +27062,14 @@ packages: xtend: 4.0.2 dev: true + /remark-rehype/10.1.0: + resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==} + dependencies: + '@types/hast': 2.3.1 + '@types/mdast': 3.0.3 + mdast-util-to-hast: 12.2.5 + unified: 10.1.2 + /remark-slug/6.1.0: resolution: {integrity: sha512-oGCxDF9deA8phWvxFuyr3oSJsdyUAxMFbA0mZ7Y1Sas+emILtO+e5WutF9564gDsEN4IXaQXm5pFo6MLH+YmwQ==} dependencies: @@ -23285,6 +27162,16 @@ packages: remove-trailing-separator: 1.1.0 dev: true + /replace-in-file/6.3.5: + resolution: {integrity: sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + chalk: 4.1.2 + glob: 7.2.3 + yargs: 17.6.2 + dev: true + /require-directory/2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -23328,7 +27215,6 @@ packages: /resize-observer-polyfill/1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} - dev: false /resolve-alpn/1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -23369,7 +27255,6 @@ packages: /resolve-pathname/2.2.0: resolution: {integrity: sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==} - dev: false /resolve-url/0.2.1: resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} @@ -23384,7 +27269,15 @@ packages: resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==} hasBin: true dependencies: - is-core-module: 2.8.1 + is-core-module: 2.11.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + /resolve/2.0.0-next.4: + resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} + hasBin: true + dependencies: + is-core-module: 2.11.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -23452,6 +27345,14 @@ packages: resolution: {integrity: sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==} dev: true + /rifm/0.7.0_react@18.2.0: + resolution: {integrity: sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==} + peerDependencies: + react: '>=16.8' + dependencies: + '@babel/runtime': 7.20.6 + react: 18.2.0 + /right-pad/1.0.1: resolution: {integrity: sha512-bYBjgxmkvTAfgIYy328fmkwhp39v8lwVgWhhrzxPV3yHtcSqyYKe9/XOhvW48UFjATg3VuJbpsp5822ACNvkmw==} engines: {node: '>= 0.10'} @@ -23466,13 +27367,32 @@ packages: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} hasBin: true dependencies: - glob: 7.1.7 + glob: 7.2.3 /rimraf/3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true dependencies: - glob: 7.1.7 + glob: 7.2.3 + + /ripemd160/2.0.2: + resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + dev: true + + /roarr/2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + dependencies: + boolean: 3.2.0 + detect-node: 2.0.5 + globalthis: 1.0.3 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.2 + dev: true /robots-parser/2.3.0: resolution: {integrity: sha512-RvuCITckrHM9k8DxCCU9rqWpuuKRfVX9iHG751dC3/EdERxp9gJATxYYdYOT3L0T+TAT4+27lENisk/VbHm47A==} @@ -23482,6 +27402,76 @@ packages: resolution: {integrity: sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==} dev: true + /rollup-plugin-dts/4.2.3_k35zwyycrckt5xfsejji7kbwn4: + resolution: {integrity: sha512-jlcpItqM2efqfIiKzDB/IKOS9E9fDvbkJSGw5GtK/PqPGS9eC3R3JKyw2VvpTktZA+TNgJRMu1NTv244aTUzzQ==} + engines: {node: '>=v12.22.12'} + peerDependencies: + rollup: ^2.55 + typescript: ^4.1 + dependencies: + magic-string: 0.26.7 + rollup: 2.79.1 + typescript: 4.9.3 + optionalDependencies: + '@babel/code-frame': 7.18.6 + dev: true + + /rollup-plugin-esbuild/4.10.3_uiao7appyg7pvh5lt4amcal6cy: + resolution: {integrity: sha512-RILwUCgnCL5vo8vyZ/ZpwcqRuE5KmLizEv6BujBQfgXFZ6ggcS0FiYvQN+gsTJfWCMaU37l0Fosh4eEufyO97Q==} + engines: {node: '>=12'} + peerDependencies: + esbuild: '>=0.10.1' + rollup: ^1.20.0 || ^2.0.0 + dependencies: + '@rollup/pluginutils': 4.2.1 + debug: 4.3.4 + es-module-lexer: 0.9.3 + esbuild: 0.16.17 + joycon: 3.1.1 + jsonc-parser: 3.2.0 + rollup: 2.79.1 + transitivePeerDependencies: + - supports-color + dev: true + + /rollup-plugin-postcss/4.0.2_postcss@8.4.19: + resolution: {integrity: sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==} + engines: {node: '>=10'} + peerDependencies: + postcss: 8.x + dependencies: + chalk: 4.1.2 + concat-with-sourcemaps: 1.1.0 + cssnano: 4.1.10 + import-cwd: 3.0.0 + p-queue: 6.6.2 + pify: 5.0.0 + postcss: 8.4.19 + postcss-load-config: 3.1.4_postcss@8.4.19 + postcss-modules: 4.3.1_postcss@8.4.19 + promise.series: 0.2.0 + resolve: 1.22.0 + rollup-pluginutils: 2.8.2 + safe-identifier: 0.4.2 + style-inject: 0.3.0 + transitivePeerDependencies: + - ts-node + dev: true + + /rollup-pluginutils/2.8.2: + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} + dependencies: + estree-walker: 0.6.1 + dev: true + + /rollup/2.79.1: + resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + /route-recognizer/0.3.4: resolution: {integrity: sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==} dev: true @@ -23491,6 +27481,11 @@ packages: engines: {node: 0.12.* || 4.* || 6.* || >= 7.*} dev: true + /rtl-css-js/1.16.1: + resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} + dependencies: + '@babel/runtime': 7.20.6 + /run-async/2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -23500,6 +27495,11 @@ packages: resolution: {integrity: sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==} dev: true + /run-script-webpack-plugin/0.1.1: + resolution: {integrity: sha512-PrxBRLv1K9itDKMlootSCyGhdTU+KbKGJ2wF6/k0eyo6M0YGPC58HYbS/J/QsDiwM0t7G99WcuCqto0J7omOXA==} + engines: {node: '>=14'} + dev: true + /rw/1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} dev: true @@ -23531,6 +27531,12 @@ packages: tslib: 2.1.0 dev: true + /sade/1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + dependencies: + mri: 1.2.0 + /safe-buffer/5.1.1: resolution: {integrity: sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==} dev: true @@ -23550,6 +27556,17 @@ packages: path-name: 1.0.0 dev: true + /safe-identifier/0.4.2: + resolution: {integrity: sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==} + dev: true + + /safe-regex-test/1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.3 + is-regex: 1.1.4 + /safe-regex/1.1.0: resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} dependencies: @@ -23561,6 +27578,11 @@ packages: regexp-tree: 0.1.24 dev: true + /safe-stable-stringify/2.4.2: + resolution: {integrity: sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==} + engines: {node: '>=10'} + dev: true + /safer-buffer/2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -23617,7 +27639,7 @@ packages: klona: 2.0.5 neo-async: 2.6.2 sass: 1.32.4 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /sass/1.32.4: @@ -23639,6 +27661,13 @@ packages: xmlchars: 2.2.0 dev: true + /saxes/6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: true + /scheduler/0.13.3: resolution: {integrity: sha512-UxN5QRYWtpR1egNWzJcVLk8jlegxAugswQc984lD3kU7NuobsO37/sRfbpTdBjtnD5TBNFA2Q2oLV5+UmPSmEQ==} dependencies: @@ -23671,6 +27700,11 @@ packages: dependencies: loose-envify: 1.4.0 + /scheduler/0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + /schema-utils/2.7.0: resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==} engines: {node: '>= 8.9.0'} @@ -23696,6 +27730,10 @@ packages: ajv-formats: 2.1.1_ajv@8.11.2 ajv-keywords: 5.1.0_ajv@8.11.2 + /screenfull/5.2.0: + resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} + engines: {node: '>=0.10.0'} + /scuid/1.1.0: resolution: {integrity: sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==} dev: true @@ -23709,6 +27747,10 @@ packages: dependencies: node-forge: 1.3.1 + /semver-compare/1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + dev: true + /semver-diff/2.1.0: resolution: {integrity: sha512-gL8F8L4ORwsS0+iQ34yCYv///jsOq0ZL7WP55d1HnJ32o7tyFYEFQZQA22mrLIacZdU6xecaBBZ+uEiffGNyXw==} engines: {node: '>=0.10.0'} @@ -23778,6 +27820,19 @@ packages: engines: {node: '>=0.10.0'} dev: false + /serialize-error/7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + dependencies: + type-fest: 0.13.1 + dev: true + + /serialize-error/8.1.0: + resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==} + engines: {node: '>=10'} + dependencies: + type-fest: 0.20.2 + /serialize-javascript/5.0.1: resolution: {integrity: sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==} dependencies: @@ -23828,10 +27883,14 @@ packages: /set-blocking/2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - /set-cookie-parser/2.4.5: - resolution: {integrity: sha512-LkSDwseogN5l6TerqGzFzL9mUDTxSq3hX2b5AaynjC1nSCNWiDypEgHatfc0v6KcnfgV3/6F6h4ABh6igjzlQQ==} + /set-cookie-parser/2.5.1: + resolution: {integrity: sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==} dev: true + /set-harmonic-interval/1.0.1: + resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==} + engines: {node: '>=6.9'} + /set-value/2.0.1: resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} engines: {node: '>=0.10.0'} @@ -23859,6 +27918,14 @@ packages: /setprototypeof/1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + /sha.js/2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + /shallow-clone/3.0.1: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} @@ -23897,7 +27964,7 @@ packages: engines: {node: '>=4'} hasBin: true dependencies: - glob: 7.1.7 + glob: 7.2.3 interpret: 1.2.0 rechoir: 0.6.2 dev: true @@ -23911,7 +27978,7 @@ packages: dependencies: call-bind: 1.0.2 get-intrinsic: 1.1.3 - object-inspect: 1.12.0 + object-inspect: 1.12.3 /signal-exit/3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -24039,6 +28106,7 @@ packages: ansi-styles: 4.3.0 astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + dev: true /slide/1.1.6: resolution: {integrity: sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==} @@ -24217,6 +28285,10 @@ packages: resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} deprecated: See https://github.com/lydell/source-map-url#deprecated + /source-map/0.5.6: + resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==} + engines: {node: '>=0.10.0'} + /source-map/0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} @@ -24237,11 +28309,17 @@ packages: graphql: 15.4.0 dev: true + /sourcemap-codec/1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + /space-separated-tokens/1.1.2: resolution: {integrity: sha512-G3jprCEw+xFEs0ORweLmblJ3XLymGGr6hxZYTYZjIlvDti9vOBUjRQa1Rzjt012aRrocKstHwdNi+F7HguPsEA==} dependencies: trim: 0.0.1 - dev: true + + /space-separated-tokens/2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} /sparkles/1.0.1: resolution: {integrity: sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==} @@ -24338,7 +28416,7 @@ packages: webpack: ^1 || ^2 || ^3 || ^4 || ^5 dependencies: chalk: 4.1.2 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /speedline-core/1.4.3: @@ -24371,6 +28449,10 @@ packages: /sprintf-js/1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + /sprintf-js/1.1.2: + resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==} + dev: true + /ssim.js/3.3.2: resolution: {integrity: sha512-MUVEvxBwOT1xMtOEmLF7NxtbwS5JuOnbr1Lj0sa/rXEh/75Ao7AjdQDjO6Tidl97Ml6O6Br8q7rx6MDvDU7pRg==} dev: true @@ -24386,6 +28468,11 @@ packages: resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + /stack-generator/2.0.10: + resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + dependencies: + stackframe: 1.3.4 + /stack-trace/0.0.10: resolution: {integrity: sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=} dev: true @@ -24397,8 +28484,21 @@ packages: escape-string-regexp: 2.0.0 dev: true - /stackframe/1.2.0: - resolution: {integrity: sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==} + /stackframe/1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + /stacktrace-gps/3.1.2: + resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==} + dependencies: + source-map: 0.5.6 + stackframe: 1.3.4 + + /stacktrace-js/2.0.2: + resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + dependencies: + error-stack-parser: 2.0.6 + stack-generator: 2.0.10 + stacktrace-gps: 3.1.2 /stacktrace-parser/0.1.10: resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==} @@ -24464,6 +28564,13 @@ packages: react-dom: 18.1.0_react@18.1.0 dev: true + /stream-browserify/2.0.2: + resolution: {integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==} + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.7 + dev: true + /stream-browserify/3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} dependencies: @@ -24475,6 +28582,16 @@ packages: resolution: {integrity: sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==} dev: true + /stream-http/2.8.3: + resolution: {integrity: sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==} + dependencies: + builtin-status-codes: 3.0.0 + inherits: 2.0.4 + readable-stream: 2.3.7 + to-arraybuffer: 1.0.1 + xtend: 4.0.2 + dev: true + /stream-http/3.2.0: resolution: {integrity: sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==} dependencies: @@ -24493,6 +28610,12 @@ packages: engines: {node: '>=10.0.0'} dev: true + /strict-event-emitter/0.2.8: + resolution: {integrity: sha512-KDf/ujU8Zud3YaLtMCcTI4xkZlZVIYxTLr+XIULexP+77EEVWixeXroLUXQXiVtH4XH2W7jr/3PT1v3zBuvc3A==} + dependencies: + events: 3.3.0 + dev: true + /string-env-interpolation/1.0.1: resolution: {integrity: sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==} dev: true @@ -24547,13 +28670,15 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - /string.prototype.matchall/4.0.2: - resolution: {integrity: sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==} + /string.prototype.matchall/4.0.8: + resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} dependencies: + call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.1 + es-abstract: 1.21.1 + get-intrinsic: 1.1.3 has-symbols: 1.0.3 - internal-slot: 1.0.3 + internal-slot: 1.0.4 regexp.prototype.flags: 1.4.3 side-channel: 1.0.4 @@ -24562,7 +28687,7 @@ packages: engines: {node: '>= 0.4'} dependencies: define-properties: 1.1.4 - es-abstract: 1.20.1 + es-abstract: 1.21.1 function-bind: 1.1.1 dev: true @@ -24571,23 +28696,23 @@ packages: engines: {node: '>= 0.4'} dependencies: define-properties: 1.1.4 - es-abstract: 1.20.1 + es-abstract: 1.21.1 function-bind: 1.1.1 dev: true - /string.prototype.trimend/1.0.5: - resolution: {integrity: sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==} + /string.prototype.trimend/1.0.6: + resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.1 + es-abstract: 1.21.1 - /string.prototype.trimstart/1.0.5: - resolution: {integrity: sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==} + /string.prototype.trimstart/1.0.6: + resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.1 + es-abstract: 1.21.1 /string_decoder/0.10.31: resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} @@ -24676,6 +28801,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + /style-inject/0.3.0: + resolution: {integrity: sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==} + dev: true + /style-loader/1.3.0_webpack@5.75.0: resolution: {integrity: sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q==} engines: {node: '>= 8.9.0'} @@ -24684,7 +28813,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 2.7.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /style-loader/2.0.0_webpack@5.75.0: @@ -24695,7 +28824,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.1.1 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /style-loader/3.3.1_webpack@5.75.0: @@ -24704,7 +28833,7 @@ packages: peerDependencies: webpack: ^5.0.0 dependencies: - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /style-mod/4.0.0: @@ -24721,6 +28850,11 @@ packages: inline-style-parser: 0.1.1 dev: true + /style-to-object/0.4.1: + resolution: {integrity: sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==} + dependencies: + inline-style-parser: 0.1.1 + /stylehacks/4.0.3: resolution: {integrity: sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==} engines: {node: '>=6.9.0'} @@ -24820,7 +28954,7 @@ packages: known-css-properties: 0.24.0 mathml-tag-names: 2.1.3 meow: 9.0.0 - micromatch: 4.0.4 + micromatch: 4.0.5 normalize-path: 3.0.0 normalize-selector: 0.2.0 picocolors: 1.0.0 @@ -24846,6 +28980,18 @@ packages: /stylis/4.0.13: resolution: {integrity: sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==} + + /sucrase/3.29.0: + resolution: {integrity: sha512-bZPAuGA5SdFHuzqIhTAqt9fvNEo9rESqXIG3oiKdF8K4UmkQxC4KlNL3lVyAErXp+mPvUqZ5l13qx6TrDIGf3A==} + engines: {node: '>=8'} + hasBin: true + dependencies: + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.1.6 + mz: 2.7.0 + pirates: 4.0.5 + ts-interface-checker: 0.1.13 dev: true /sudo-prompt/9.2.1: @@ -24940,6 +29086,10 @@ packages: es6-symbol: 3.1.3 dev: true + /svg-parser/2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + dev: true + /svg-tags/1.0.0: resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} dev: true @@ -24958,15 +29108,15 @@ packages: csso: 4.2.0 js-yaml: 3.14.1 mkdirp: 0.5.5 - object.values: 1.1.5 + object.values: 1.1.6 sax: 1.2.4 stable: 0.1.8 unquote: 1.1.1 util.promisify: 1.0.0 dev: true - /svgo/2.7.0: - resolution: {integrity: sha512-aDLsGkre4fTDCWvolyW+fs8ZJFABpzLXbtdK1y71CKnHzAnpDxKXPj2mNKj+pyOXUCzFHzuxRJ94XOFygOWV3w==} + /svgo/2.8.0: + resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} engines: {node: '>=10.13.0'} hasBin: true dependencies: @@ -24975,7 +29125,7 @@ packages: css-select: 4.1.3 css-tree: 1.1.3 csso: 4.2.0 - nanocolors: 0.1.12 + picocolors: 1.0.0 stable: 0.1.8 dev: true @@ -24985,6 +29135,26 @@ packages: tslib: 2.1.0 dev: true + /swc-loader/0.2.3_jftlsdtjtdse3p3ijno5hy54ky: + resolution: {integrity: sha512-D1p6XXURfSPleZZA/Lipb3A8pZ17fP4NObZvFCDjK/OKljroqDpPmsBdTraWhVBqUNpcWBQY1imWdoPScRlQ7A==} + peerDependencies: + '@swc/core': ^1.2.147 + webpack: '>=2' + dependencies: + '@swc/core': 1.3.27 + webpack: 5.75.0_cw4su4nzareykaft5p7arksy5i + dev: true + + /swr/2.0.0_react@18.2.0: + resolution: {integrity: sha512-IhUx5yPkX+Fut3h0SqZycnaNLXLXsb2ECFq0Y29cxnK7d8r7auY2JWNbCW3IX+EqXUg3rwNJFlhrw5Ye/b6k7w==} + engines: {pnpm: '7'} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + use-sync-external-store: 1.2.0_react@18.2.0 + dev: true + /symbol-observable/4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -25020,6 +29190,7 @@ packages: slice-ansi: 4.0.0 string-width: 4.2.3 strip-ansi: 6.0.1 + dev: true /tagged-template-noop/2.1.1: resolution: {integrity: sha512-diZ004cBHKVueqSr5p+/EPZhofCBRW7w7zZL71FcK8x+209BbMw77ICrP9AWXpVjPyyyIqRYYFAM4Wjk2HNWQg==} @@ -25030,8 +29201,8 @@ packages: resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} engines: {node: '>=6'} - /tapable/2.2.0: - resolution: {integrity: sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==} + /tapable/2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} /tar-fs/2.1.1: @@ -25054,13 +29225,13 @@ packages: readable-stream: 3.6.0 dev: true - /tar/6.1.11: - resolution: {integrity: sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==} - engines: {node: '>= 10'} + /tar/6.1.13: + resolution: {integrity: sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==} + engines: {node: '>=10'} dependencies: chownr: 2.0.0 fs-minipass: 2.1.0 - minipass: 3.3.5 + minipass: 4.0.0 minizlib: 2.1.2 mkdirp: 1.0.4 yallist: 4.0.0 @@ -25127,13 +29298,37 @@ packages: serialize-javascript: 5.0.1 source-map: 0.6.1 terser: 5.16.1 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-sources: 1.4.3 transitivePeerDependencies: - bluebird dev: true - /terser-webpack-plugin/5.3.6_j5vv3ucw6sjpjo7n5idphq5l2u: + /terser-webpack-plugin/5.3.6_htvmhiqynazf46fjrszipnqp7a: + resolution: {integrity: sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.17 + esbuild: 0.16.17 + jest-worker: 27.5.1 + schema-utils: 3.1.1 + serialize-javascript: 6.0.0 + terser: 5.16.1 + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy + + /terser-webpack-plugin/5.3.6_v7bbwjudcxv2o5c5io5ciwfori: resolution: {integrity: sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==} engines: {node: '>= 10.13.0'} peerDependencies: @@ -25150,19 +29345,21 @@ packages: optional: true dependencies: '@jridgewell/trace-mapping': 0.3.17 - esbuild: 0.16.10 + '@swc/core': 1.3.27 + esbuild: 0.16.17 jest-worker: 27.5.1 schema-utils: 3.1.1 serialize-javascript: 6.0.0 terser: 5.16.1 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_cw4su4nzareykaft5p7arksy5i + dev: true /terser/4.8.1: resolution: {integrity: sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - acorn: 8.8.0 + acorn: 8.8.1 commander: 2.20.3 source-map: 0.6.1 source-map-support: 0.5.21 @@ -25174,7 +29371,7 @@ packages: hasBin: true dependencies: '@jridgewell/source-map': 0.3.2 - acorn: 8.8.0 + acorn: 8.8.1 commander: 2.20.3 source-map-support: 0.5.21 @@ -25183,7 +29380,7 @@ packages: engines: {node: '>=8'} dependencies: '@istanbuljs/schema': 0.1.2 - glob: 7.1.7 + glob: 7.2.3 minimatch: 3.1.2 dev: true @@ -25218,7 +29415,6 @@ packages: /throttle-debounce/3.0.1: resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} engines: {node: '>=10'} - dev: true /through/2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -25256,6 +29452,13 @@ packages: engines: {node: '>=0.10.0'} dev: true + /timers-browserify/2.0.12: + resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} + engines: {node: '>=0.6.0'} + dependencies: + setimmediate: 1.0.5 + dev: true + /timers-ext/0.1.7: resolution: {integrity: sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==} dependencies: @@ -25267,13 +29470,11 @@ packages: resolution: {integrity: sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==} dev: true - /tiny-invariant/1.0.3: - resolution: {integrity: sha512-ytQx8T4DL8PjlX53yYzcIC0WhIZbpR0p1qcYjw2pHu3w6UtgWwFJQ/02cnhOnBBhlFx/edUIfcagCaQSe3KMWg==} - dev: false + /tiny-invariant/1.3.1: + resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} /tiny-warning/1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} - dev: false /title-case/3.0.3: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} @@ -25313,6 +29514,10 @@ packages: is-negated-glob: 1.0.0 dev: true + /to-arraybuffer/1.0.1: + resolution: {integrity: sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==} + dev: true + /to-fast-properties/2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -25364,17 +29569,27 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + /toposort/2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + dev: true + + /tosource/2.0.0-alpha.3: + resolution: {integrity: sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==} + engines: {node: '>=10'} + dev: true + /totalist/1.1.0: resolution: {integrity: sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==} engines: {node: '>=6'} - /tough-cookie/4.0.0: - resolution: {integrity: sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==} + /tough-cookie/4.1.2: + resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} engines: {node: '>=6'} dependencies: psl: 1.8.0 punycode: 2.1.1 - universalify: 0.1.2 + universalify: 0.2.0 + url-parse: 1.5.10 dev: true /tr46/0.0.3: @@ -25412,6 +29627,9 @@ packages: resolution: {integrity: sha1-tH77DRpfKlaoXMRc6lJWUek0BM8=} dev: true + /trim-lines/3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + /trim-newlines/1.0.0: resolution: {integrity: sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==} engines: {node: '>=0.10.0'} @@ -25428,16 +29646,25 @@ packages: /trim/0.0.1: resolution: {integrity: sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ==} - dev: true /trough/1.0.3: resolution: {integrity: sha512-fwkLWH+DimvA4YCy+/nvJd61nWQQ2liO/nF/RjkTpiOGi+zxZzVkhb1mvbHIIW4b/8nDsYI8uTmAlc0nNkRMOw==} dev: true + /trough/2.1.0: + resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} + + /tryer/1.0.1: + resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} + dev: true + /ts-dedent/2.0.0: resolution: {integrity: sha512-DfxKjSFQfw9+uf7N9Cy8Ebx9fv5fquK4hZ6SD3Rzr+1jKP6AVA6H8+B5457ZpUs0JKsGpGqIevbpZ9DMQJDp1A==} engines: {node: '>=6.10'} + /ts-easing/0.2.0: + resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} + /ts-essentials/7.0.3_typescript@4.9.3: resolution: {integrity: sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==} peerDependencies: @@ -25446,6 +29673,10 @@ packages: typescript: 4.9.3 dev: true + /ts-interface-checker/0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + /ts-invariant/0.9.4: resolution: {integrity: sha512-63jtX/ZSwnUNi/WhXjnK8kz4cHHpYS60AnmA6ixz17l7E12a5puCWFlNpkne5Rl0J8TBPVHpGjsj4fxs8ObVLQ==} engines: {node: '>=8'} @@ -25465,16 +29696,48 @@ packages: dependencies: chalk: 4.1.2 enhanced-resolve: 5.10.0 - micromatch: 4.0.4 + micromatch: 4.0.5 semver: 7.3.8 typescript: 4.9.3 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /ts-log/2.2.3: resolution: {integrity: sha512-XvB+OdKSJ708Dmf9ore4Uf/q62AYDTzFcAdxc8KNML1mmAWywRFVt/dn1KYJH8Agt5UJNujfM3znU5PxgAzA2w==} dev: true + /ts-node/10.9.1_wcgzs6xccsdl4rtr2s3c2cdazy: + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@swc/core': 1.3.27 + '@tsconfig/node10': 1.0.8 + '@tsconfig/node12': 1.0.9 + '@tsconfig/node14': 1.0.1 + '@tsconfig/node16': 1.0.2 + '@types/node': 16.11.36 + acorn: 8.8.1 + acorn-walk: 8.2.0 + arg: 4.1.0 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.8.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /ts-node/10.9.1_wh55dwo6xja56jtfktvlrff6xu: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true @@ -25495,7 +29758,7 @@ packages: '@tsconfig/node14': 1.0.1 '@tsconfig/node16': 1.0.2 '@types/node': 13.13.5 - acorn: 8.8.0 + acorn: 8.8.1 acorn-walk: 8.2.0 arg: 4.1.0 create-require: 1.1.1 @@ -25553,6 +29816,10 @@ packages: typescript: 4.9.3 dev: true + /tty-browserify/0.0.0: + resolution: {integrity: sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==} + dev: true + /tunnel-agent/0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: @@ -25582,6 +29849,11 @@ packages: engines: {node: '>=4'} dev: true + /type-fest/0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + dev: true + /type-fest/0.18.1: resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} engines: {node: '>=10'} @@ -25616,6 +29888,11 @@ packages: engines: {node: '>=8'} dev: true + /type-fest/2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + dev: true + /type-is/1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -25631,6 +29908,13 @@ packages: resolution: {integrity: sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==} dev: true + /typed-array-length/1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.10 + /typed-rest-client/1.8.6: resolution: {integrity: sha512-xcQpTEAJw2DP7GqVNECh4dD+riS+C1qndXLfBCJ3xk0kqprtGN491P5KlmrDbKdtuW8NEcP/5ChxiJI3S9WYTA==} dependencies: @@ -25655,7 +29939,7 @@ packages: chalk: 3.0.0 chokidar: 3.5.3 css-modules-loader-core: 1.1.0 - glob: 7.1.7 + glob: 7.2.3 param-case: 3.0.4 path: 0.12.7 reserved-words: 0.1.2 @@ -25701,6 +29985,29 @@ packages: typescript: 4.9.3 dev: true + /typescript-json-schema/0.55.0_@swc+core@1.3.27: + resolution: {integrity: sha512-BXaivYecUdiXWWNiUqXgY6A9cMWerwmhtO+lQE7tDZGs7Mf38sORDeQZugfYOZOHPZ9ulsD+w0LWjFDOQoXcwg==} + hasBin: true + dependencies: + '@types/json-schema': 7.0.11 + '@types/node': 16.11.36 + glob: 7.2.3 + path-equal: 1.2.5 + safe-stable-stringify: 2.4.2 + ts-node: 10.9.1_wcgzs6xccsdl4rtr2s3c2cdazy + typescript: 4.8.4 + yargs: 17.6.2 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + dev: true + + /typescript/4.8.4: + resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + /typescript/4.9.3: resolution: {integrity: sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==} engines: {node: '>=4.2.0'} @@ -25819,6 +30126,17 @@ packages: resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} + /unified/10.1.2: + resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} + dependencies: + '@types/unist': 2.0.3 + bail: 2.0.2 + extend: 3.0.2 + is-buffer: 2.0.5 + is-plain-obj: 4.1.0 + trough: 2.1.0 + vfile: 5.3.6 + /unified/9.2.0: resolution: {integrity: sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==} dependencies: @@ -25885,18 +30203,34 @@ packages: resolution: {integrity: sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==} dev: true + /unist-builder/3.0.0: + resolution: {integrity: sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==} + dependencies: + '@types/unist': 2.0.3 + /unist-util-generated/1.1.6: resolution: {integrity: sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==} dev: true + /unist-util-generated/2.0.0: + resolution: {integrity: sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw==} + /unist-util-is/4.0.2: resolution: {integrity: sha512-Ofx8uf6haexJwI1gxWMGg6I/dLnF2yE+KibhD3/diOqY2TinLcqHXCV6OI5gFVn3xQqDH+u0M625pfKwIwgBKQ==} dev: true + /unist-util-is/5.1.1: + resolution: {integrity: sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==} + /unist-util-position/3.1.0: resolution: {integrity: sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==} dev: true + /unist-util-position/4.0.3: + resolution: {integrity: sha512-p/5EMGIa1qwbXjA+QgcBXaPWjSnZfQ2Sc3yBEEfgPwsEmJd8Qh+DSk3LGnmOM4S1bY2C0AjmMnB8RuEYxpPwXQ==} + dependencies: + '@types/unist': 2.0.3 + /unist-util-remove-position/2.0.1: resolution: {integrity: sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==} dependencies: @@ -25915,6 +30249,11 @@ packages: '@types/unist': 2.0.3 dev: true + /unist-util-stringify-position/3.0.2: + resolution: {integrity: sha512-7A6eiDCs9UtjcwZOcCpM4aPII3bAAGv13E96IkawkOAW0OhH+yRxtY0lzo8KiHpzEMfH7Q+FizUmwp8Iqy5EWg==} + dependencies: + '@types/unist': 2.0.3 + /unist-util-visit-parents/3.1.1: resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} dependencies: @@ -25922,6 +30261,12 @@ packages: unist-util-is: 4.0.2 dev: true + /unist-util-visit-parents/5.1.1: + resolution: {integrity: sha512-gks4baapT/kNRaWxuGkl5BIhoanZo7sC/cUT/JToSRNL1dYoXRFl75d++NkjYk4TAu2uv2Px+l8guMajogeuiw==} + dependencies: + '@types/unist': 2.0.3 + unist-util-is: 5.1.1 + /unist-util-visit/2.0.3: resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==} dependencies: @@ -25930,6 +30275,13 @@ packages: unist-util-visit-parents: 3.1.1 dev: true + /unist-util-visit/4.1.1: + resolution: {integrity: sha512-n9KN3WV9k4h1DxYR1LoajgN93wpEi/7ZplVe02IoB4gH5ctI1AaF2670BLHQYbwj+pY83gFtyeySFiyMHJklrg==} + dependencies: + '@types/unist': 2.0.3 + unist-util-is: 5.1.1 + unist-util-visit-parents: 5.1.1 + /universal-github-app-jwt/1.1.0: resolution: {integrity: sha512-3b+ocAjjz4JTyqaOT+NNBd5BtTuvJTxWElIoeHSVelUV9J3Jp7avmQTdLKCaoqi/5Ox2o/q+VK19TJ233rVXVQ==} dependencies: @@ -25951,10 +30303,20 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + /universalify/0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: true + /universalify/1.0.0: resolution: {integrity: sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==} engines: {node: '>= 10.0.0'} + /universalify/2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: true + /unixify/1.0.0: resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} engines: {node: '>=0.10.0'} @@ -26096,7 +30458,7 @@ packages: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.1.1 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /url-parse-lax/1.0.0: @@ -26128,7 +30490,6 @@ packages: dependencies: punycode: 1.3.2 querystring: 0.2.0 - dev: false /urljoin/0.1.5: resolution: {integrity: sha512-OSGi+PS3zxk8XfQ+7buaupOdrW9P9p+V9rjxGzJaYEYDe/B2rv3WJCupq5LNERW4w4kWxsduUUrhCxZZiQ2udw==} @@ -26171,6 +30532,13 @@ packages: react: 18.1.0 dev: false + /use-memo-one/1.1.3_react@18.2.0: + resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + /use-resize-observer/9.0.2_ef5jwxihqo6n7gxfmzogljlgcm: resolution: {integrity: sha512-JOzsmF3/IDmtjG7OE5qXOP69LEpBpwhpLSiT1XgSr+uFRX0ftJHQnDaP7Xq+uhbljLYkJt67sqsbnyXBjiY8ig==} peerDependencies: @@ -26201,6 +30569,14 @@ packages: react: 18.1.0 dev: false + /use-sync-external-store/1.2.0_react@18.2.0: + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: true + /use/3.1.1: resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} engines: {node: '>=0.10.0'} @@ -26235,6 +30611,12 @@ packages: inherits: 2.0.1 dev: true + /util/0.11.1: + resolution: {integrity: sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==} + dependencies: + inherits: 2.0.3 + dev: true + /util/0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} dependencies: @@ -26243,7 +30625,6 @@ packages: is-generator-function: 1.0.10 is-typed-array: 1.1.10 which-typed-array: 1.1.9 - dev: false /utila/0.4.0: resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} @@ -26255,7 +30636,7 @@ packages: dev: false /utils-merge/1.0.1: - resolution: {integrity: sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=} + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} /uuid-browser/3.1.0: @@ -26272,12 +30653,21 @@ packages: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. hasBin: true - dev: true /uuid/8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true + /uvu/0.5.6: + resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + dequal: 2.0.2 + diff: 5.0.0 + kleur: 4.1.5 + sade: 1.8.1 + /v8-compile-cache-lib/3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} dev: true @@ -26308,9 +30698,33 @@ packages: spdx-expression-parse: 3.0.1 dev: true + /validate.io-array/1.0.6: + resolution: {integrity: sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==} + dev: true + + /validate.io-function/1.0.2: + resolution: {integrity: sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==} + dev: true + + /validate.io-integer-array/1.0.0: + resolution: {integrity: sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==} + dependencies: + validate.io-array: 1.0.6 + validate.io-integer: 1.0.5 + dev: true + + /validate.io-integer/1.0.5: + resolution: {integrity: sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==} + dependencies: + validate.io-number: 1.0.3 + dev: true + + /validate.io-number/1.0.3: + resolution: {integrity: sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==} + dev: true + /value-equal/0.2.1: resolution: {integrity: sha512-yRL36Xb2K/HmFT5Fe3M86S7mu4+a12/3l7uytUh6eNPPjP77ldPBvsAvmnWff39sXn55naRMZN8LZWRO8PWaeQ==} - dev: false /value-or-function/3.0.0: resolution: {integrity: sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==} @@ -26341,6 +30755,12 @@ packages: unist-util-stringify-position: 2.0.3 dev: true + /vfile-message/3.1.3: + resolution: {integrity: sha512-0yaU+rj2gKAyEk12ffdSbBfjnnj+b1zqTBv3OQCTn8yEB02bsPizwdBPrLJjHnK+cU9EMMcUnNv938XcZIkmdA==} + dependencies: + '@types/unist': 2.0.3 + unist-util-stringify-position: 3.0.2 + /vfile/4.1.0: resolution: {integrity: sha512-BaTPalregj++64xbGK6uIlsurN3BCRNM/P2Pg8HezlGzKd1O9PrwIac6bd9Pdx2uTb0QHoioZ+rXKolbVXEgJg==} dependencies: @@ -26351,6 +30771,14 @@ packages: vfile-message: 2.0.4 dev: true + /vfile/5.3.6: + resolution: {integrity: sha512-ADBsmerdGBs2WYckrLBEmuETSPyTD4TuLxTrw0DvjirxW1ra4ZwkbzG8ndsv3Q57smvHxo677MHaQrY9yxH8cA==} + dependencies: + '@types/unist': 2.0.3 + is-buffer: 2.0.5 + unist-util-stringify-position: 3.0.2 + vfile-message: 3.1.3 + /vinyl-fs/3.0.3: resolution: {integrity: sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==} engines: {node: '>= 0.10'} @@ -26361,7 +30789,7 @@ packages: is-valid-glob: 1.0.0 lazystream: 1.0.0 lead: 1.0.0 - object.assign: 4.1.2 + object.assign: 4.1.4 pumpify: 1.5.1 readable-stream: 2.3.7 remove-bom-buffer: 3.0.0 @@ -26403,12 +30831,16 @@ packages: resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} dev: false + /vm-browserify/1.1.2: + resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + dev: true + /vm2/3.9.10: resolution: {integrity: sha512-AuECTSvwu2OHLAZYhG716YzwodKCIJxB6u1zG7PgSQwIgAlEaoXH52bxdcvT8GkGjnYK7r7yWDW0m0sOsPuBjQ==} engines: {node: '>=6.0'} hasBin: true dependencies: - acorn: 8.8.0 + acorn: 8.8.1 acorn-walk: 8.2.0 dev: true @@ -26422,7 +30854,7 @@ packages: chalk: 2.4.2 cheerio: 1.0.0-rc.12 commander: 6.2.1 - glob: 7.1.7 + glob: 7.2.3 hosted-git-info: 4.1.0 keytar: 7.9.0 leven: 3.1.0 @@ -26473,6 +30905,13 @@ packages: xml-name-validator: 4.0.0 dev: true + /w3c-xmlserializer/4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + dependencies: + xml-name-validator: 4.0.0 + dev: true + /walker/1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: @@ -26482,7 +30921,6 @@ packages: resolution: {integrity: sha512-jMBt6pUrKn5I+OGgtQ4YZLdhIeJmObddh6CsibPxyQ5yPZm1XExSyzC1LCNX7BzhxWgiHmizBWJTHJIjMjTQYQ==} dependencies: loose-envify: 1.4.0 - dev: false /watchpack/2.4.0: resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} @@ -26501,6 +30939,14 @@ packages: dependencies: defaults: 1.0.3 + /web-encoding/1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + dev: true + /web-namespaces/1.1.4: resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==} dev: true @@ -26568,7 +31014,7 @@ packages: engines: {node: '>= 10.13.0'} hasBin: true dependencies: - acorn: 8.8.0 + acorn: 8.8.1 acorn-walk: 8.2.0 chalk: 4.1.2 commander: 7.2.0 @@ -26610,7 +31056,7 @@ packages: import-local: 3.0.2 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-bundle-analyzer: 4.7.0 webpack-dev-server: 4.11.1_rjsyjcrmk25kqsjzwkvj3a2evq webpack-merge: 5.7.3 @@ -26625,7 +31071,7 @@ packages: mime: 2.6.0 mkdirp: 0.5.5 range-parser: 1.2.1 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-log: 2.0.0 dev: true @@ -26641,7 +31087,7 @@ packages: mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 3.1.1 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /webpack-dev-middleware/5.3.3_webpack@5.75.0: @@ -26655,7 +31101,7 @@ packages: mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.0.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy /webpack-dev-server/4.11.1_rjsyjcrmk25kqsjzwkvj3a2evq: resolution: {integrity: sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==} @@ -26695,7 +31141,7 @@ packages: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-cli: 5.0.1_y7ttplitmkohdpgkllksfboxwa webpack-dev-middleware: 5.3.3_webpack@5.75.0 ws: 8.11.0 @@ -26705,13 +31151,61 @@ packages: - supports-color - utf-8-validate + /webpack-dev-server/4.11.1_webpack@5.75.0: + resolution: {integrity: sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==} + engines: {node: '>= 12.13.0'} + hasBin: true + peerDependencies: + webpack: ^4.37.0 || ^5.0.0 + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/bonjour': 3.5.10 + '@types/connect-history-api-fallback': 1.3.5 + '@types/express': 4.17.14 + '@types/serve-index': 1.9.1 + '@types/serve-static': 1.15.0 + '@types/sockjs': 0.3.33 + '@types/ws': 8.5.3 + ansi-html-community: 0.0.8 + bonjour-service: 1.0.14 + chokidar: 3.5.3 + colorette: 2.0.19 + compression: 1.7.4 + connect-history-api-fallback: 2.0.0 + default-gateway: 6.0.3 + express: 4.18.2 + graceful-fs: 4.2.10 + html-entities: 2.3.2 + http-proxy-middleware: 2.0.6_@types+express@4.17.14 + ipaddr.js: 2.0.1 + open: 8.4.0 + p-retry: 4.6.0 + rimraf: 3.0.2 + schema-utils: 4.0.0 + selfsigned: 2.1.1 + serve-index: 1.9.1 + sockjs: 0.3.24 + spdy: 4.0.2 + webpack: 5.75.0_cw4su4nzareykaft5p7arksy5i + webpack-dev-middleware: 5.3.3_webpack@5.75.0 + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + dev: true + /webpack-filter-warnings-plugin/1.2.1_webpack@5.75.0: resolution: {integrity: sha512-Ez6ytc9IseDMLPo0qCuNNYzgtUl8NovOqjIq4uAU8LTD4uoa1w1KpZyyzFtLTEMZpkkOkLfL9eN+KGYdk1Qtwg==} engines: {node: '>= 4.3 < 5.0.0 || >= 5.10'} peerDependencies: webpack: ^2.0.0 || ^3.0.0 || ^4.0.0 dependencies: - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /webpack-hot-middleware/2.25.1: @@ -26737,8 +31231,8 @@ packages: peerDependencies: webpack: ^5.47.0 dependencies: - tapable: 2.2.0 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + tapable: 2.2.1 + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy webpack-sources: 2.3.0 dev: true @@ -26749,6 +31243,11 @@ packages: clone-deep: 4.0.1 wildcard: 2.0.0 + /webpack-node-externals/3.0.0: + resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} + engines: {node: '>=6'} + dev: true + /webpack-sources/1.4.3: resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==} dependencies: @@ -26784,7 +31283,47 @@ packages: resolution: {integrity: sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw==} dev: true - /webpack/5.75.0_oo63t3us67g6w4zg54gqorhf2i: + /webpack/5.75.0_cw4su4nzareykaft5p7arksy5i: + resolution: {integrity: sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.3 + '@types/estree': 0.0.51 + '@webassemblyjs/ast': 1.11.1 + '@webassemblyjs/wasm-edit': 1.11.1 + '@webassemblyjs/wasm-parser': 1.11.1 + acorn: 8.8.1 + acorn-import-assertions: 1.8.0_acorn@8.8.1 + browserslist: 4.21.4 + chrome-trace-event: 1.0.2 + enhanced-resolve: 5.10.0 + es-module-lexer: 0.9.3 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.10 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.2.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.1.1 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.6_v7bbwjudcxv2o5c5io5ciwfori + watchpack: 2.4.0 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + dev: true + + /webpack/5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy: resolution: {integrity: sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==} engines: {node: '>=10.13.0'} hasBin: true @@ -26799,8 +31338,8 @@ packages: '@webassemblyjs/ast': 1.11.1 '@webassemblyjs/wasm-edit': 1.11.1 '@webassemblyjs/wasm-parser': 1.11.1 - acorn: 8.8.0 - acorn-import-assertions: 1.8.0_acorn@8.8.0 + acorn: 8.8.1 + acorn-import-assertions: 1.8.0_acorn@8.8.1 browserslist: 4.21.4 chrome-trace-event: 1.0.2 enhanced-resolve: 5.10.0 @@ -26814,8 +31353,8 @@ packages: mime-types: 2.1.35 neo-async: 2.6.2 schema-utils: 3.1.1 - tapable: 2.2.0 - terser-webpack-plugin: 5.3.6_j5vv3ucw6sjpjo7n5idphq5l2u + tapable: 2.2.1 + terser-webpack-plugin: 5.3.6_htvmhiqynazf46fjrszipnqp7a watchpack: 2.4.0 webpack-cli: 5.0.1_y7ttplitmkohdpgkllksfboxwa webpack-sources: 3.2.3 @@ -26918,7 +31457,6 @@ packages: gopd: 1.0.1 has-tostringtag: 1.0.0 is-typed-array: 1.1.10 - dev: false /which/1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} @@ -26995,7 +31533,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.1.1 - webpack: 5.75.0_oo63t3us67g6w4zg54gqorhf2i + webpack: 5.75.0_jh3afgd4dccyz4n3zkk5zvv5cy dev: true /worker-rpc/0.1.1: @@ -27447,15 +31985,40 @@ packages: buffer-crc32: 0.2.13 dev: true + /yml-loader/2.1.0: + resolution: {integrity: sha512-mo42d5FQWlXxpyTEpYywPu1LzK3F69pPPCOB8WKgJi8s+aqaogQP7XnXTjSobbKzzlZ/wXm7kg9CkP4x4ZnVMw==} + dependencies: + js-yaml: 3.14.1 + loader-utils: 1.4.0 + dev: true + /yn/3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} dev: true + /yn/4.0.0: + resolution: {integrity: sha512-huWiiCS4TxKc4SfgmTwW1K7JmXPPAmuXWYy4j9qjQo4+27Kni8mGhAAi1cloRWmBe2EqcLgt3IGqQoRL/MtPgg==} + engines: {node: '>=10'} + dev: true + /yocto-queue/0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + /yup/0.32.11: + resolution: {integrity: sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==} + engines: {node: '>=10'} + dependencies: + '@babel/runtime': 7.20.6 + '@types/lodash': 4.14.182 + lodash: 4.17.21 + lodash-es: 4.17.21 + nanoclone: 0.2.1 + property-expr: 2.0.5 + toposort: 2.0.2 + dev: true + /zdog/1.1.3: resolution: {integrity: sha512-raRj6r0gPzopFm5XWBJZr/NuV4EEnT4iE+U3dp5FV5pCb588Gmm3zLIp/j9yqqcMiHH8VNQlerLTgOqL7krh6w==} dev: false @@ -27465,9 +32028,15 @@ packages: dependencies: zen-observable: 0.8.15 + /zen-observable/0.10.0: + resolution: {integrity: sha512-iI3lT0iojZhKwT5DaFy2Ce42n3yFcLdFyOh01G7H0flMY60P8MJuVFEoJoNwXlmAyQ45GrjL6AcZmmlv8A5rbw==} + /zen-observable/0.8.15: resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==} + /zod/3.18.0: + resolution: {integrity: sha512-gwTm8RfUCe8l9rDwN5r2A17DkAa8Ez4Yl4yXqc5VqeGaXaJahzYYXbTwvhroZi0SNBqTwh/bKm2N0mpCzuw4bA==} + /zone.js/0.11.6: resolution: {integrity: sha512-umJqFtKyZlPli669gB1gOrRE9hxUUGkZr7mo878z+NEBJZZixJkKeVYfnoLa7g25SseUDc92OZrMKKHySyJrFg==} dependencies: @@ -27490,6 +32059,9 @@ packages: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} dev: true + /zwitch/2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + github.com/atlassian/tether/bf85430889b5231fbe5b383416cce6281225bf06: resolution: {tarball: https://codeload.github.com/atlassian/tether/tar.gz/bf85430889b5231fbe5b383416cce6281225bf06} name: tether From a6ff0684ba1576d8eb699b886e10a8df9aed50d5 Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Fri, 20 Jan 2023 12:14:19 +0200 Subject: [PATCH 056/678] rcache: allow negative max sizes in FIFOList (#46702) This is the same as using a value of zero. This is fixes a potential footgun when accidently passing in a negative value. Additionally another PR wants to use negative values as a signal to disable a feature. Test Plan: added a test --- internal/rcache/fifo_list.go | 16 +++++++++++----- internal/rcache/fifo_list_test.go | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/internal/rcache/fifo_list.go b/internal/rcache/fifo_list.go index 636cda760e42..19b3a36d9379 100644 --- a/internal/rcache/fifo_list.go +++ b/internal/rcache/fifo_list.go @@ -12,15 +12,17 @@ import ( // FIFOList holds the most recently inserted items, discarding older ones if the total item count goes over the configured size. type FIFOList struct { key string - maxSize *atomic.Int64 + maxSize atomic.Int64 // invariant: non-negative integer } // NewFIFOList returns a FIFOList, storing only a fixed amount of elements, discarding old ones if needed. func NewFIFOList(key string, size int) *FIFOList { - return &FIFOList{ - key: key, - maxSize: atomic.NewInt64(int64(size)), + l := &FIFOList{ + key: key, } + // SetMaxSize will adjust size to be a non-negative integer. + l.SetMaxSize(size) + return l } // Insert b in the cache and drops the oldest inserted item if the size exceeds the configured limit. @@ -65,11 +67,15 @@ func (l *FIFOList) MaxSize() int { return int(l.maxSize.Load()) } -// SetMaxSize will change the size we truncate at. +// SetMaxSize will change the size we truncate at. If maxSize is <= 0 the list +// will remain empty. // // Note: this won't cause truncation to happen, instead truncation is done on // the next insert. func (l *FIFOList) SetMaxSize(maxSize int) { + if maxSize < 0 { + maxSize = 0 + } l.maxSize.Store(int64(maxSize)) } diff --git a/internal/rcache/fifo_list_test.go b/internal/rcache/fifo_list_test.go index 506a382c06b2..958536d3533c 100644 --- a/internal/rcache/fifo_list_test.go +++ b/internal/rcache/fifo_list_test.go @@ -42,6 +42,12 @@ func Test_FIFOList_All_OK(t *testing.T) { inserts: bytes("a1", "a2", "a3"), want: bytes(), }, + { + key: "f", + size: -1, + inserts: bytes("a1", "a2", "a3"), + want: bytes(), + }, } for _, c := range cases { @@ -123,6 +129,14 @@ func Test_FIFOList_Slice_OK(t *testing.T) { from: 2, to: -1, }, + { + key: "f", + size: -1, + inserts: bytes("a1", "a2", "a3"), + want: bytes(), + from: 0, + to: -1, + }, } for _, c := range cases { From 4338f056b2db85a5f08be97f291246d41b3d20ee Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 20 Jan 2023 11:38:26 +0100 Subject: [PATCH 057/678] graphqlbackend test: rename repos to be 0-indexed (#46703) Noticed this yesterday. We use `repo[0]` further down when asserting `StartCursor` and `endCursor` but `"repo1"` in `wantRepos`, which is a bit confusing. This basically changes tests from this args: "first: 2, indexed: false", wantRepos: []string{"repo1", "repo2"}, wantStartCursor: buildCursor(repos[0].repo), // [...] to this: args: "first: 2, indexed: false", wantRepos: []string{"repo0", "repo1"}, wantStartCursor: buildCursor(repos[0].repo), // [...] I think that's a bit nicer. --- .../graphqlbackend/repositories_test.go | 77 +++++++++---------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/cmd/frontend/graphqlbackend/repositories_test.go b/cmd/frontend/graphqlbackend/repositories_test.go index 2c63a1af3d2e..379e3366bbb3 100644 --- a/cmd/frontend/graphqlbackend/repositories_test.go +++ b/cmd/frontend/graphqlbackend/repositories_test.go @@ -662,14 +662,14 @@ func TestRepositories_Integration(t *testing.T) { indexed bool lastError string }{ - {repo: &types.Repo{Name: "repo1"}, size: 20, cloneStatus: types.CloneStatusNotCloned}, - {repo: &types.Repo{Name: "repo2"}, size: 30, cloneStatus: types.CloneStatusNotCloned, lastError: "repo2 error"}, - {repo: &types.Repo{Name: "repo3"}, size: 40, cloneStatus: types.CloneStatusCloning}, - {repo: &types.Repo{Name: "repo4"}, size: 50, cloneStatus: types.CloneStatusCloning, lastError: "repo4 error"}, - {repo: &types.Repo{Name: "repo5"}, size: 60, cloneStatus: types.CloneStatusCloned}, - {repo: &types.Repo{Name: "repo6"}, size: 10, cloneStatus: types.CloneStatusCloned, lastError: "repo6 error"}, - {repo: &types.Repo{Name: "repo7"}, size: 70, cloneStatus: types.CloneStatusCloned, indexed: false}, - {repo: &types.Repo{Name: "repo8"}, size: 80, cloneStatus: types.CloneStatusCloned, indexed: true}, + {repo: &types.Repo{Name: "repo0"}, size: 20, cloneStatus: types.CloneStatusNotCloned}, + {repo: &types.Repo{Name: "repo1"}, size: 30, cloneStatus: types.CloneStatusNotCloned, lastError: "repo1 error"}, + {repo: &types.Repo{Name: "repo2"}, size: 40, cloneStatus: types.CloneStatusCloning}, + {repo: &types.Repo{Name: "repo3"}, size: 50, cloneStatus: types.CloneStatusCloning, lastError: "repo3 error"}, + {repo: &types.Repo{Name: "repo4"}, size: 60, cloneStatus: types.CloneStatusCloned}, + {repo: &types.Repo{Name: "repo5"}, size: 10, cloneStatus: types.CloneStatusCloned, lastError: "repo5 error"}, + {repo: &types.Repo{Name: "repo6"}, size: 70, cloneStatus: types.CloneStatusCloned, indexed: false}, + {repo: &types.Repo{Name: "repo7"}, size: 80, cloneStatus: types.CloneStatusCloned, indexed: true}, } for _, rsc := range repos { @@ -711,7 +711,7 @@ func TestRepositories_Integration(t *testing.T) { // first { args: "first: 2", - wantRepos: []string{"repo1", "repo2"}, + wantRepos: []string{"repo0", "repo1"}, wantTotalCount: 8, wantNextPage: true, wantPreviousPage: false, @@ -721,7 +721,7 @@ func TestRepositories_Integration(t *testing.T) { // second page with first, after args { args: fmt.Sprintf(`first: 2, after: "%s"`, *buildCursor(repos[0].repo)), - wantRepos: []string{"repo2", "repo3"}, + wantRepos: []string{"repo1", "repo2"}, wantTotalCount: 8, wantNextPage: true, wantPreviousPage: true, @@ -731,7 +731,7 @@ func TestRepositories_Integration(t *testing.T) { // last page with first, after args { args: fmt.Sprintf(`first: 2, after: "%s"`, *buildCursor(repos[5].repo)), - wantRepos: []string{"repo7", "repo8"}, + wantRepos: []string{"repo6", "repo7"}, wantTotalCount: 8, wantNextPage: false, wantPreviousPage: true, @@ -741,7 +741,7 @@ func TestRepositories_Integration(t *testing.T) { // last { args: "last: 2", - wantRepos: []string{"repo7", "repo8"}, + wantRepos: []string{"repo6", "repo7"}, wantTotalCount: 8, wantNextPage: false, wantPreviousPage: true, @@ -751,7 +751,7 @@ func TestRepositories_Integration(t *testing.T) { // second last page with last, before args { args: fmt.Sprintf(`last: 2, before: "%s"`, *buildCursor(repos[6].repo)), - wantRepos: []string{"repo5", "repo6"}, + wantRepos: []string{"repo4", "repo5"}, wantTotalCount: 8, wantNextPage: true, wantPreviousPage: true, @@ -761,7 +761,7 @@ func TestRepositories_Integration(t *testing.T) { // back to first page with last, before args { args: fmt.Sprintf(`last: 2, before: "%s"`, *buildCursor(repos[2].repo)), - wantRepos: []string{"repo1", "repo2"}, + wantRepos: []string{"repo0", "repo1"}, wantTotalCount: 8, wantNextPage: true, wantPreviousPage: false, @@ -771,7 +771,7 @@ func TestRepositories_Integration(t *testing.T) { // descending first { args: "first: 2, descending: true", - wantRepos: []string{"repo8", "repo7"}, + wantRepos: []string{"repo7", "repo6"}, wantTotalCount: 8, wantNextPage: true, wantPreviousPage: false, @@ -781,7 +781,7 @@ func TestRepositories_Integration(t *testing.T) { // descending second page with first, after args { args: fmt.Sprintf(`first: 2, descending: true, after: "%s"`, *buildCursor(repos[6].repo)), - wantRepos: []string{"repo6", "repo5"}, + wantRepos: []string{"repo5", "repo4"}, wantTotalCount: 8, wantNextPage: true, wantPreviousPage: true, @@ -791,7 +791,7 @@ func TestRepositories_Integration(t *testing.T) { // descending last page with first, after args { args: fmt.Sprintf(`first: 2, descending: true, after: "%s"`, *buildCursor(repos[2].repo)), - wantRepos: []string{"repo2", "repo1"}, + wantRepos: []string{"repo1", "repo0"}, wantTotalCount: 8, wantNextPage: false, wantPreviousPage: true, @@ -801,7 +801,7 @@ func TestRepositories_Integration(t *testing.T) { // descending last { args: "last: 2, descending: true", - wantRepos: []string{"repo2", "repo1"}, + wantRepos: []string{"repo1", "repo0"}, wantTotalCount: 8, wantNextPage: false, wantPreviousPage: true, @@ -811,7 +811,7 @@ func TestRepositories_Integration(t *testing.T) { // descending second last page with last, before args { args: fmt.Sprintf(`last: 2, descending: true, before: "%s"`, *buildCursor(repos[3].repo)), - wantRepos: []string{"repo6", "repo5"}, + wantRepos: []string{"repo5", "repo4"}, wantTotalCount: 8, wantNextPage: true, wantPreviousPage: true, @@ -821,7 +821,7 @@ func TestRepositories_Integration(t *testing.T) { // descending back to first page with last, before args { args: fmt.Sprintf(`last: 2, descending: true, before: "%s"`, *buildCursor(repos[5].repo)), - wantRepos: []string{"repo8", "repo7"}, + wantRepos: []string{"repo7", "repo6"}, wantTotalCount: 8, wantNextPage: true, wantPreviousPage: false, @@ -832,7 +832,7 @@ func TestRepositories_Integration(t *testing.T) { { // cloned only says whether to "Include cloned repositories.", it doesn't exclude non-cloned. args: "first: 10, cloned: true", - wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, + wantRepos: []string{"repo0", "repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7"}, wantTotalCount: 8, wantNextPage: false, wantPreviousPage: false, @@ -841,7 +841,7 @@ func TestRepositories_Integration(t *testing.T) { }, { args: "first: 10, cloned: false", - wantRepos: []string{"repo1", "repo2", "repo3", "repo4"}, + wantRepos: []string{"repo0", "repo1", "repo2", "repo3"}, wantTotalCount: 4, wantNextPage: false, wantPreviousPage: false, @@ -850,7 +850,7 @@ func TestRepositories_Integration(t *testing.T) { }, { args: "cloned: false, first: 2", - wantRepos: []string{"repo1", "repo2"}, + wantRepos: []string{"repo0", "repo1"}, wantTotalCount: 4, wantNextPage: true, wantPreviousPage: false, @@ -860,7 +860,7 @@ func TestRepositories_Integration(t *testing.T) { // notCloned { args: "first: 10, notCloned: true", - wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, + wantRepos: []string{"repo0", "repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7"}, wantTotalCount: 8, wantNextPage: false, wantPreviousPage: false, @@ -869,7 +869,7 @@ func TestRepositories_Integration(t *testing.T) { }, { args: "first: 10, notCloned: false", - wantRepos: []string{"repo5", "repo6", "repo7", "repo8"}, + wantRepos: []string{"repo4", "repo5", "repo6", "repo7"}, wantTotalCount: 4, wantNextPage: false, wantPreviousPage: false, @@ -879,7 +879,7 @@ func TestRepositories_Integration(t *testing.T) { // failedFetch { args: "first: 10, failedFetch: true", - wantRepos: []string{"repo2", "repo4", "repo6"}, + wantRepos: []string{"repo1", "repo3", "repo5"}, wantTotalCount: 3, wantNextPage: false, wantPreviousPage: false, @@ -888,7 +888,7 @@ func TestRepositories_Integration(t *testing.T) { }, { args: "failedFetch: true, first: 2", - wantRepos: []string{"repo2", "repo4"}, + wantRepos: []string{"repo1", "repo3"}, wantTotalCount: 3, wantNextPage: true, wantPreviousPage: false, @@ -897,7 +897,7 @@ func TestRepositories_Integration(t *testing.T) { }, { args: "first: 10, failedFetch: false", - wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, + wantRepos: []string{"repo0", "repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7"}, wantTotalCount: 8, wantNextPage: false, wantPreviousPage: false, @@ -907,7 +907,7 @@ func TestRepositories_Integration(t *testing.T) { // cloneStatus { args: "first: 10, cloneStatus:NOT_CLONED", - wantRepos: []string{"repo1", "repo2"}, + wantRepos: []string{"repo0", "repo1"}, wantTotalCount: 2, wantNextPage: false, wantPreviousPage: false, @@ -916,7 +916,7 @@ func TestRepositories_Integration(t *testing.T) { }, { args: "first: 10, cloneStatus:CLONING", - wantRepos: []string{"repo3", "repo4"}, + wantRepos: []string{"repo2", "repo3"}, wantTotalCount: 2, wantNextPage: false, wantPreviousPage: false, @@ -925,7 +925,7 @@ func TestRepositories_Integration(t *testing.T) { }, { args: "first: 10, cloneStatus:CLONED", - wantRepos: []string{"repo5", "repo6", "repo7", "repo8"}, + wantRepos: []string{"repo4", "repo5", "repo6", "repo7"}, wantTotalCount: 4, wantNextPage: false, wantPreviousPage: false, @@ -934,7 +934,7 @@ func TestRepositories_Integration(t *testing.T) { }, { args: "cloneStatus:NOT_CLONED, first: 1", - wantRepos: []string{"repo1"}, + wantRepos: []string{"repo0"}, wantTotalCount: 2, wantNextPage: true, wantPreviousPage: false, @@ -945,7 +945,7 @@ func TestRepositories_Integration(t *testing.T) { { // indexed only says whether to "Include indexed repositories.", it doesn't exclude non-indexed. args: "first: 10, indexed: true", - wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, + wantRepos: []string{"repo0", "repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7"}, wantTotalCount: 8, wantNextPage: false, wantPreviousPage: false, @@ -954,7 +954,7 @@ func TestRepositories_Integration(t *testing.T) { }, { args: "first: 10, indexed: false", - wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7"}, + wantRepos: []string{"repo0", "repo1", "repo2", "repo3", "repo4", "repo5", "repo6"}, wantTotalCount: 7, wantNextPage: false, wantPreviousPage: false, @@ -963,7 +963,7 @@ func TestRepositories_Integration(t *testing.T) { }, { args: "indexed: false, first: 2", - wantRepos: []string{"repo1", "repo2"}, + wantRepos: []string{"repo0", "repo1"}, wantTotalCount: 7, wantNextPage: true, wantPreviousPage: false, @@ -973,7 +973,7 @@ func TestRepositories_Integration(t *testing.T) { // notIndexed { args: "first: 10, notIndexed: true", - wantRepos: []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7", "repo8"}, + wantRepos: []string{"repo0", "repo1", "repo2", "repo3", "repo4", "repo5", "repo6", "repo7"}, wantTotalCount: 8, wantNextPage: false, wantPreviousPage: false, @@ -982,7 +982,7 @@ func TestRepositories_Integration(t *testing.T) { }, { args: "first: 10, notIndexed: false", - wantRepos: []string{"repo8"}, + wantRepos: []string{"repo7"}, wantTotalCount: 1, wantNextPage: false, wantPreviousPage: false, @@ -991,7 +991,7 @@ func TestRepositories_Integration(t *testing.T) { }, { args: "orderBy:SIZE, descending:false, first: 5", - wantRepos: []string{"repo6", "repo1", "repo2", "repo3", "repo4"}, + wantRepos: []string{"repo5", "repo0", "repo1", "repo2", "repo3"}, wantTotalCount: 8, wantNextPage: true, wantPreviousPage: false, @@ -1005,7 +1005,6 @@ func TestRepositories_Integration(t *testing.T) { runRepositoriesQuery(t, ctx, schema, tt) }) } - } type repositoriesQueryTest struct { From 471793dde546e99596c7221926a6ee97a4d5033e Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Fri, 20 Jan 2023 02:57:15 -0800 Subject: [PATCH 058/678] syncjobs: use rcache.FIFOList (#46676) --- CHANGELOG.md | 1 + .../authz/resolvers/permissions_sync_jobs.go | 2 +- .../internal/authz/resolvers/resolver.go | 10 ++++- .../internal/authz/resolvers/resolver_test.go | 24 +++++++++- .../internal/authz/syncjobs/mocks_test.go | 24 +++++----- .../internal/authz/syncjobs/records_reader.go | 45 +++++++++---------- .../authz/syncjobs/records_reader_test.go | 14 ++---- .../internal/authz/syncjobs/records_store.go | 42 +++++++---------- .../authz/syncjobs/records_store_test.go | 18 +++++--- schema/schema.go | 6 +-- schema/site.schema.json | 6 +-- 11 files changed, 104 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d5c26d87b61..4c8c24ba3dbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ All notable changes to Sourcegraph are documented in this file. - Zoekt by default eagerly unmarshals the symbol index into memory. Previously we would unmarshal on every request for the purposes of symbol searches or ranking. This lead to pressure on the Go garbage collector. On sourcegraph.com we have noticed time spent in the garbage collector halved. In the unlikely event this leads to more OOMs in zoekt-webserver, you can disable by setting the environment variable `ZOEKT_ENABLE_LAZY_DOC_SECTIONS=t`. [zoekt#503](https://github.com/sourcegraph/zoekt/pull/503) - Removes the right side action sidebar that is shown on the code view page and moves the icons into the top nav. [#46339](https://github.com/sourcegraph/sourcegraph/pull/46339) - The `sourcegraph/prometheus` image no longer starts with `--web.enable-lifecycle --web.enable-admin-api` by default - these flags can be re-enabled by configuring `PROMETHEUS_ADDITIONAL_FLAGS` on the container. [#46393](https://github.com/sourcegraph/sourcegraph/pull/46393) +- The experimental setting `authz.syncJobsRecordsTTL` has been changed to `authz.syncJobsRecordsLimit` - records are no longer retained based on age, but based on this size cap. [#46676](https://github.com/sourcegraph/sourcegraph/pull/46676) - Renders GitHub pull request references in git blame view. [#46409](https://github.com/sourcegraph/sourcegraph/pull/46409) ### Fixed diff --git a/enterprise/cmd/frontend/internal/authz/resolvers/permissions_sync_jobs.go b/enterprise/cmd/frontend/internal/authz/resolvers/permissions_sync_jobs.go index 73dc4d04d4e2..5f1a55d5830c 100644 --- a/enterprise/cmd/frontend/internal/authz/resolvers/permissions_sync_jobs.go +++ b/enterprise/cmd/frontend/internal/authz/resolvers/permissions_sync_jobs.go @@ -46,7 +46,7 @@ func getPermissionsSyncJobByIDFunc(r *Resolver) graphqlbackend.NodeByIDFunc { if err != nil { return nil, errors.Wrap(err, "unmarshal ID") } - status, err := r.syncJobsRecords.Get(time.Unix(0, unixNano)) + status, err := r.syncJobsRecords.Get(ctx, time.Unix(0, unixNano)) if err != nil { return nil, errors.Wrap(err, "node with ID not found - it may have expired") } diff --git a/enterprise/cmd/frontend/internal/authz/resolvers/resolver.go b/enterprise/cmd/frontend/internal/authz/resolvers/resolver.go index 31894b3bca6a..9b9abd8ff3a2 100644 --- a/enterprise/cmd/frontend/internal/authz/resolvers/resolver.go +++ b/enterprise/cmd/frontend/internal/authz/resolvers/resolver.go @@ -34,7 +34,7 @@ type Resolver struct { logger log.Logger db edb.EnterpriseDB syncJobsRecords interface { - Get(timestamp time.Time) (*syncjobs.Status, error) + Get(ctx context.Context, timestamp time.Time) (*syncjobs.Status, error) GetAll(ctx context.Context, first int) ([]syncjobs.Status, error) } } @@ -54,11 +54,14 @@ func (r *Resolver) checkLicense(feature licensing.Feature) error { return nil } +// syncJobRecordsReadLimit caps syncJobsRecords retrieval to 500 items +const syncJobRecordsReadLimit = 500 + func NewResolver(observationCtx *observation.Context, db database.DB, clock func() time.Time) graphqlbackend.AuthzResolver { return &Resolver{ logger: observationCtx.Logger.Scoped("authz.Resolver", ""), db: edb.NewEnterpriseDB(db), - syncJobsRecords: syncjobs.NewRecordsReader(), + syncJobsRecords: syncjobs.NewRecordsReader(syncJobRecordsReadLimit), } } @@ -636,6 +639,9 @@ func (r *Resolver) PermissionsSyncJobs(ctx context.Context, args *graphqlbackend if args.First == 0 { return nil, errors.Newf("expected non-zero 'first', got %d", args.First) } + if args.First > syncJobRecordsReadLimit { + return nil, errors.Newf("cannot retrieve more than %d records", syncJobRecordsReadLimit) + } records, err := r.syncJobsRecords.GetAll(ctx, int(args.First)) if err != nil { diff --git a/enterprise/cmd/frontend/internal/authz/resolvers/resolver_test.go b/enterprise/cmd/frontend/internal/authz/resolvers/resolver_test.go index f20f1c136bc2..986ba12283e3 100644 --- a/enterprise/cmd/frontend/internal/authz/resolvers/resolver_test.go +++ b/enterprise/cmd/frontend/internal/authz/resolvers/resolver_test.go @@ -1696,7 +1696,7 @@ query { type mockRecordsReader []syncjobs.Status -func (m mockRecordsReader) Get(t time.Time) (*syncjobs.Status, error) { +func (m mockRecordsReader) Get(ctx context.Context, t time.Time) (*syncjobs.Status, error) { for _, r := range m { if r.Completed.Equal(t) { return &r, nil @@ -1809,6 +1809,28 @@ query { }}) }) + t.Run("too many entries requested", func(t *testing.T) { + graphqlbackend.RunTests(t, []*graphqlbackend.Test{{ + Context: ctx, + Schema: parsedSchema, + Query: ` +query { + permissionsSyncJobs(first:999) { + totalCount + pageInfo { hasNextPage } + nodes { + id + } + } +}`, + ExpectedResult: "null", + ExpectedErrors: []*gqlerrors.QueryError{{ + Message: "cannot retrieve more than 500 records", + Path: []any{"permissionsSyncJobs"}, + }}, + }}) + }) + t.Run("get by node ID", func(t *testing.T) { graphqlbackend.RunTests(t, []*graphqlbackend.Test{{ Context: ctx, diff --git a/enterprise/internal/authz/syncjobs/mocks_test.go b/enterprise/internal/authz/syncjobs/mocks_test.go index 15545fd50482..e949d1a307f8 100644 --- a/enterprise/internal/authz/syncjobs/mocks_test.go +++ b/enterprise/internal/authz/syncjobs/mocks_test.go @@ -14,20 +14,22 @@ type confWatcher struct { func (c *confWatcher) Watch(fn func()) { c.update = fn } func (c *confWatcher) SiteConfig() schema.SiteConfiguration { return c.conf } -type memCache map[string]string - -func (m memCache) Set(k string, v []byte) { m[k] = string(v) } +type memCache struct { + // retain in []string for ease of autogold testing + values []string +} -func (m memCache) ListKeys(context.Context) (keys []string, err error) { - for k := range m { - keys = append(keys, k) - } - return +func (m *memCache) Insert(v []byte) error { + m.values = append(m.values, string(v)) + return nil } -func (m memCache) GetMulti(keys ...string) (vals [][]byte) { - for _, k := range keys { - vals = append(vals, []byte(m[k])) +// no-op +func (m *memCache) SetMaxSize(int) {} + +func (m *memCache) Slice(ctx context.Context, from, to int) (vals [][]byte, err error) { + for _, v := range m.values { + vals = append(vals, []byte(v)) } return } diff --git a/enterprise/internal/authz/syncjobs/records_reader.go b/enterprise/internal/authz/syncjobs/records_reader.go index d8dedabc8097..e4c3e86f9921 100644 --- a/enterprise/internal/authz/syncjobs/records_reader.go +++ b/enterprise/internal/authz/syncjobs/records_reader.go @@ -3,8 +3,6 @@ package syncjobs import ( "context" "encoding/json" - "sort" - "strconv" "time" "github.com/sourcegraph/sourcegraph/internal/rcache" @@ -14,51 +12,49 @@ import ( type recordsReader struct { // readOnlyCache is a replaceable abstraction over rcache.Cache. readOnlyCache interface { - ListKeys(ctx context.Context) ([]string, error) - GetMulti(keys ...string) [][]byte + Slice(ctx context.Context, from, to int) ([][]byte, error) } } -func NewRecordsReader() *recordsReader { +func NewRecordsReader(limit int) *recordsReader { return &recordsReader{ - readOnlyCache: rcache.New(syncJobsRecordsPrefix), + // The cache is read-only in recordsReader, so the limit doesn't affect + // the contents of the list - it doesn't need to align with the actual + // limit of the list. + readOnlyCache: rcache.NewFIFOList(syncJobsRecordsKey, limit), } } -// Get retrieves a record by timestamp. -func (r *recordsReader) Get(timestamp time.Time) (*Status, error) { - res := r.readOnlyCache.GetMulti(strconv.FormatInt(timestamp.UTC().UnixNano(), 10)) - if len(res) == 0 || len(res[0]) == 0 { - return nil, errors.New("record not found") +func (r *recordsReader) Get(ctx context.Context, timestamp time.Time) (*Status, error) { + items, err := r.GetAll(ctx, -1) + if err != nil { + return nil, errors.Wrap(err, "list jobs") } - var s Status - if err := json.Unmarshal(res[0], &s); err != nil { - return nil, errors.Wrap(err, "invalid record") + for _, i := range items { + if i.Completed.Equal(timestamp) { + return &i, nil + } } - return &s, nil + return nil, errors.New("job not found") } // GetAll retrieves the first n records, with the most recent records first. func (r *recordsReader) GetAll(ctx context.Context, first int) ([]Status, error) { - keys, err := r.readOnlyCache.ListKeys(ctx) + items, err := r.readOnlyCache.Slice(ctx, 0, first) if err != nil { return nil, errors.Wrap(err, "list jobs") } - // keys are timestamps - sort.Strings(keys) - switch { case first <= 0: return []Status{}, nil - case first < len(keys): - keys = keys[:first] + case first < len(items): + items = items[:first] } // get values - vals := r.readOnlyCache.GetMulti(keys...) - records := make([]Status, 0, len(vals)) - for _, v := range vals { + records := make([]Status, 0, len(items)) + for _, v := range items { var j Status if err := json.Unmarshal(v, &j); err != nil { continue // discard @@ -66,5 +62,6 @@ func (r *recordsReader) GetAll(ctx context.Context, first int) ([]Status, error) records = append(records, j) } + // records are already ~sorted return records, nil } diff --git a/enterprise/internal/authz/syncjobs/records_reader_test.go b/enterprise/internal/authz/syncjobs/records_reader_test.go index 2ee5d224c7e6..0f8b6a5d792b 100644 --- a/enterprise/internal/authz/syncjobs/records_reader_test.go +++ b/enterprise/internal/authz/syncjobs/records_reader_test.go @@ -6,12 +6,13 @@ import ( "github.com/sourcegraph/log/logtest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/sourcegraph/sourcegraph/lib/errors" ) func TestSyncJobRecordsRead(t *testing.T) { - c := memCache{} + c := &memCache{} // Write multiple records s := NewRecordsStore(logtest.Scoped(t)) @@ -30,13 +31,13 @@ func TestSyncJobRecordsRead(t *testing.T) { }}, nil) // set up reader - r := NewRecordsReader() + r := NewRecordsReader(100) r.readOnlyCache = c t.Run("read limited", func(t *testing.T) { results, err := r.GetAll(context.Background(), 1) assert.NoError(t, err) - assert.Len(t, results, 1) + require.Len(t, results, 1) first := results[0] assert.Equal(t, "repo", first.JobType) @@ -55,12 +56,5 @@ func TestSyncJobRecordsRead(t *testing.T) { third := results[2] assert.True(t, first.Completed.Before(second.Completed)) assert.True(t, second.Completed.Before(third.Completed)) - - t.Run("read single", func(t *testing.T) { - s, err := r.Get(second.Completed) - assert.NoError(t, err) - assert.Equal(t, second, *s) - }) }) - } diff --git a/enterprise/internal/authz/syncjobs/records_store.go b/enterprise/internal/authz/syncjobs/records_store.go index 72a141c2ba4b..181897523aa6 100644 --- a/enterprise/internal/authz/syncjobs/records_store.go +++ b/enterprise/internal/authz/syncjobs/records_store.go @@ -2,7 +2,6 @@ package syncjobs import ( "encoding/json" - "strconv" "sync" "time" @@ -12,11 +11,10 @@ import ( "github.com/sourcegraph/sourcegraph/internal/rcache" ) -// keep in sync with consumer in enterprise/cmd/frontend/internal/authz/resolvers/resolver.go -const syncJobsRecordsPrefix = "authz/sync-job-records" +const syncJobsRecordsKey = "authz/sync-job-records" // default documented in site.schema.json -const defaultSyncJobsRecordsTTLMinutes = 5 +const defaultSyncJobsRecordsLimit = 100 // RecordsStore is used to record the results of recent permissions syncing jobs for // diagnostic purposes. @@ -25,38 +23,33 @@ type RecordsStore struct { now func() time.Time mux sync.Mutex - // cache is a replaceable abstraction over rcache.Cache. - cache interface{ Set(key string, v []byte) } + // cache is a replaceable abstraction over rcache.FIFOList. + cache interface { + Insert(v []byte) error + SetMaxSize(int) + } } -type noopCache struct{} - -func (noopCache) Set(string, []byte) {} - func NewRecordsStore(logger log.Logger) *RecordsStore { return &RecordsStore{ logger: logger, - cache: noopCache{}, + cache: rcache.NewFIFOList(syncJobsRecordsKey, defaultSyncJobsRecordsLimit), now: time.Now, } } func (r *RecordsStore) Watch(c conftypes.WatchableSiteConfig) { c.Watch(func() { - r.mux.Lock() - defer r.mux.Unlock() - - ttlMinutes := c.SiteConfig().AuthzSyncJobsRecordsTTL - if ttlMinutes == 0 { - ttlMinutes = defaultSyncJobsRecordsTTLMinutes + recordsLimit := c.SiteConfig().AuthzSyncJobsRecordsLimit + if recordsLimit == 0 { + recordsLimit = defaultSyncJobsRecordsLimit } - if ttlMinutes > 0 { - ttlSeconds := ttlMinutes * 60 - r.cache = rcache.NewWithTTL(syncJobsRecordsPrefix, ttlSeconds) - r.logger.Debug("enabled records store cache", log.Int("ttlSeconds", ttlSeconds)) + // Setting cache size to <=0 disables it + r.cache.SetMaxSize(recordsLimit) + if recordsLimit > 0 { + r.logger.Debug("enabled records store cache", log.Int("limit", recordsLimit)) } else { - r.cache = noopCache{} r.logger.Debug("disabled records store cache") } }) @@ -66,9 +59,6 @@ func (r *RecordsStore) Watch(c conftypes.WatchableSiteConfig) { func (r *RecordsStore) Record(jobType string, jobID int32, providerStates []ProviderStatus, err error) { completed := r.now() - r.mux.Lock() - defer r.mux.Unlock() - record := Status{ JobType: jobType, JobID: jobID, @@ -90,5 +80,5 @@ func (r *RecordsStore) Record(jobType string, jobID int32, providerStates []Prov } // Key by timestamp for sorting - r.cache.Set(strconv.FormatInt(record.Completed.UTC().UnixNano(), 10), val) + r.cache.Insert(val) } diff --git a/enterprise/internal/authz/syncjobs/records_store_test.go b/enterprise/internal/authz/syncjobs/records_store_test.go index db87eccf7e59..9032b60903d9 100644 --- a/enterprise/internal/authz/syncjobs/records_store_test.go +++ b/enterprise/internal/authz/syncjobs/records_store_test.go @@ -17,11 +17,11 @@ func TestSyncJobsRecordsStoreWatch(t *testing.T) { s := NewRecordsStore(logtest.Scoped(t)) // assert default - assert.IsType(t, noopCache{}, s.cache) + assert.Equal(t, defaultSyncJobsRecordsLimit, s.cache.(*rcache.FIFOList).MaxSize()) cw := confWatcher{ conf: schema.SiteConfiguration{ - AuthzSyncJobsRecordsTTL: 5, + AuthzSyncJobsRecordsLimit: 5, }, } @@ -32,7 +32,7 @@ func TestSyncJobsRecordsStoreWatch(t *testing.T) { cw.update() // assert updated - assert.Equal(t, 5*time.Minute, s.cache.(*rcache.Cache).TTL()) + assert.Equal(t, 5, s.cache.(*rcache.FIFOList).MaxSize()) } func TestSyncJobRecordsRecord(t *testing.T) { @@ -45,17 +45,21 @@ func TestSyncJobRecordsRecord(t *testing.T) { now: func() time.Time { return mockTime }, } t.Run("success", func(t *testing.T) { - c := memCache{} + c := &memCache{} s.cache = c s.Record("repo", 12, []ProviderStatus{}, nil) - autogold.Want("record_success", memCache{"1136214245000000000": `{"job_type":"repo","job_id":12,"completed":"2006-01-02T15:04:05Z","status":"SUCCESS","message":"","providers":[]}`}). + autogold.Want("record_success", &memCache{values: []string{ + `{"job_type":"repo","job_id":12,"completed":"2006-01-02T15:04:05Z","status":"SUCCESS","message":"","providers":[]}`, + }}). Equal(t, c) }) t.Run("error", func(t *testing.T) { - c := memCache{} + c := &memCache{} s.cache = c s.Record("repo", 12, []ProviderStatus{}, errors.New("oh no")) - autogold.Want("record_error", memCache{"1136214245000000000": `{"job_type":"repo","job_id":12,"completed":"2006-01-02T15:04:05Z","status":"ERROR","message":"oh no","providers":[]}`}). + autogold.Want("record_error", &memCache{values: []string{ + `{"job_type":"repo","job_id":12,"completed":"2006-01-02T15:04:05Z","status":"ERROR","message":"oh no","providers":[]}`, + }}). Equal(t, c) }) } diff --git a/schema/schema.go b/schema/schema.go index 93a4a844c749..1357cb42642a 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -2288,8 +2288,8 @@ type SiteConfiguration struct { AuthzEnforceForSiteAdmins bool `json:"authz.enforceForSiteAdmins,omitempty"` // AuthzRefreshInterval description: Time interval (in seconds) of how often each component picks up authorization changes in external services. AuthzRefreshInterval int `json:"authz.refreshInterval,omitempty"` - // AuthzSyncJobsRecordsTTL description: EXPERIMENTAL: Time interval (in minutes) of how long to keep sync job records for. Set to a negative value to disable. - AuthzSyncJobsRecordsTTL int `json:"authz.syncJobsRecordsTTL,omitempty"` + // AuthzSyncJobsRecordsLimit description: EXPERIMENTAL: Number of sync job records to retain. Set to a negative value to disable sync jobs records entirely. + AuthzSyncJobsRecordsLimit int `json:"authz.syncJobsRecordsLimit,omitempty"` // BatchChangesChangesetsRetention description: How long changesets will be retained after they have been detached from a batch change. BatchChangesChangesetsRetention string `json:"batchChanges.changesetsRetention,omitempty"` // BatchChangesDisableWebhooksWarning description: Hides Batch Changes warnings about webhooks not being configured. @@ -2516,7 +2516,7 @@ func (v *SiteConfiguration) UnmarshalJSON(data []byte) error { delete(m, "auth.userOrgMap") delete(m, "authz.enforceForSiteAdmins") delete(m, "authz.refreshInterval") - delete(m, "authz.syncJobsRecordsTTL") + delete(m, "authz.syncJobsRecordsLimit") delete(m, "batchChanges.changesetsRetention") delete(m, "batchChanges.disableWebhooksWarning") delete(m, "batchChanges.enabled") diff --git a/schema/site.schema.json b/schema/site.schema.json index ec6be0de5c9c..05fc1f2a8df9 100644 --- a/schema/site.schema.json +++ b/schema/site.schema.json @@ -881,10 +881,10 @@ "type": "integer", "default": 5 }, - "authz.syncJobsRecordsTTL": { - "description": "EXPERIMENTAL: Time interval (in minutes) of how long to keep sync job records for. Set to a negative value to disable.", + "authz.syncJobsRecordsLimit": { + "description": "EXPERIMENTAL: Number of sync job records to retain. Set to a negative value to disable sync jobs records entirely.", "type": "integer", - "default": 5 + "default": 100 }, "externalService.userMode": { "description": "Enable to allow users to add external services for public and private repositories to the Sourcegraph instance.", From 6b2a6f911f8f395d30539f9fa45e32c478cf151a Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Fri, 20 Jan 2023 13:13:10 +0200 Subject: [PATCH 059/678] rcache: remove unused *Multi and ListKeys (#46577) This is no longer used. We removed it to reduce the API surface of rcache. Test Plan: go test --- internal/rcache/rcache.go | 118 --------------------------------- internal/rcache/rcache_test.go | 87 ++++-------------------- 2 files changed, 13 insertions(+), 192 deletions(-) diff --git a/internal/rcache/rcache.go b/internal/rcache/rcache.go index a54190c07b83..c87aa6240b94 100644 --- a/internal/rcache/rcache.go +++ b/internal/rcache/rcache.go @@ -1,7 +1,6 @@ package rcache import ( - "context" "fmt" "os" "time" @@ -11,7 +10,6 @@ import ( "github.com/inconshreveable/log15" "github.com/sourcegraph/sourcegraph/internal/redispool" - "github.com/sourcegraph/sourcegraph/lib/errors" ) // dataVersion is used for releases that change type structure for @@ -52,69 +50,6 @@ func NewWithTTL(keyPrefix string, ttlSeconds int) *Cache { func (r *Cache) TTL() time.Duration { return time.Duration(r.ttlSeconds) * time.Second } -func (r *Cache) GetMulti(keys ...string) [][]byte { - c := poolGet() - defer c.Close() - - if len(keys) == 0 { - return nil - } - rkeys := make([]any, len(keys)) - for i, key := range keys { - rkeys[i] = r.rkeyPrefix() + key - } - - vals, err := redis.Values(c.Do("MGET", rkeys...)) - if err != nil && err != redis.ErrNil { - log15.Warn("failed to execute redis command", "cmd", "MGET", "error", err) - } - - strVals := make([][]byte, len(vals)) - for i, val := range vals { - // MGET returns nil as not found. - if val == nil { - continue - } - - b, err := redis.Bytes(val, nil) - if err != nil { - log15.Warn("failed to parse bytes from Redis value", "value", val) - continue - } - strVals[i] = b - } - return strVals -} - -func (r *Cache) SetMulti(keyvals ...[2]string) { - c := poolGet() - defer c.Close() - - if len(keyvals) == 0 { - return - } - - for _, kv := range keyvals { - k, v := kv[0], kv[1] - if !utf8.Valid([]byte(k)) { - log15.Error("rcache: keys must be valid utf8", "key", []byte(k)) - continue - } - if r.ttlSeconds == 0 { - if err := c.Send("SET", r.rkeyPrefix()+k, []byte(v)); err != nil { - log15.Warn("failed to write redis command to client output buffer", "cmd", "SET", "error", err) - } - } else { - if err := c.Send("SETEX", r.rkeyPrefix()+k, r.ttlSeconds, []byte(v)); err != nil { - log15.Warn("failed to write redis command to client output buffer", "cmd", "SETEX", "error", err) - } - } - } - if err := c.Flush(); err != nil { - log15.Warn("failed to flush Redis client", "error", err) - } -} - // Get implements httpcache.Cache.Get func (r *Cache) Get(key string) ([]byte, bool) { c := poolGet() @@ -243,13 +178,6 @@ func (r *Cache) LTrimList(key string, count int) error { return err } -// DeleteMulti deletes the given keys. -func (r *Cache) DeleteMulti(keys ...string) { - for _, key := range keys { - r.Delete(key) - } -} - // Delete implements httpcache.Cache.Delete func (r *Cache) Delete(key string) { c := poolGet() @@ -263,52 +191,6 @@ func (r *Cache) Delete(key string) { } } -// ListKeys lists all keys associated with this cache. -// Use with care if you have long TTLs or no TTL configured. -func (r *Cache) ListKeys(ctx context.Context) (results []string, err error) { - var c redis.Conn - c, err = poolGetContext(ctx) - if err != nil { - return nil, errors.Wrap(err, "get redis conn") - } - defer func(c redis.Conn) { - if tempErr := c.Close(); err == nil { - err = tempErr - } - }(c) - - cursor := 0 - for { - select { - case <-ctx.Done(): - return results, ctx.Err() - default: - } - - res, err := redis.Values( - c.Do("SCAN", cursor, - "MATCH", r.rkeyPrefix()+"*", - "COUNT", 100), - ) - if err != nil { - return results, errors.Wrap(err, "redis scan") - } - - cursor, _ = redis.Int(res[0], nil) - keys, _ := redis.Strings(res[1], nil) - for i, k := range keys { - keys[i] = k[len(r.rkeyPrefix()):] - } - - results = append(results, keys...) - - if cursor == 0 { - break - } - } - return -} - // rkeyPrefix generates the actual key prefix we use on redis. func (r *Cache) rkeyPrefix() string { return fmt.Sprintf("%s:%s:", globalPrefix, r.keyPrefix) diff --git a/internal/rcache/rcache_test.go b/internal/rcache/rcache_test.go index 3d135b657616..9e798db4fc74 100644 --- a/internal/rcache/rcache_test.go +++ b/internal/rcache/rcache_test.go @@ -1,7 +1,6 @@ package rcache import ( - "context" "reflect" "strconv" "testing" @@ -94,59 +93,6 @@ func TestCache_simple(t *testing.T) { } } -func TestCache_multi(t *testing.T) { - SetupForTest(t) - - c := New("some_prefix") - vals := c.GetMulti("k0", "k1", "k2") - if got, exp := vals, [][]byte{nil, nil, nil}; !reflect.DeepEqual(exp, got) { - t.Errorf("Expected %v on initial fetch, got %v", exp, got) - } - - c.Set("k0", []byte("b")) - if got, exp := c.GetMulti("k0"), bytes("b"); !reflect.DeepEqual(exp, got) { - t.Errorf("Expected %v, but got %v", exp, got) - } - - c.SetMulti([2]string{"k0", "a"}) - if got, exp := c.GetMulti("k0"), bytes("a"); !reflect.DeepEqual(exp, got) { - t.Errorf("Expected %v, but got %v", exp, got) - } - - c.SetMulti([2]string{"k0", "a"}, [2]string{"k1", "b"}) - if got, exp := c.GetMulti("k0"), bytes("a"); !reflect.DeepEqual(exp, got) { - t.Errorf("Expected %v, but got %v", exp, got) - } - if got, exp := c.GetMulti("k1"), bytes("b"); !reflect.DeepEqual(exp, got) { - t.Errorf("Expected %v, but got %v", exp, got) - } - if got, exp := c.GetMulti("k0", "k1"), bytes("a", "b"); !reflect.DeepEqual(exp, got) { - t.Errorf("Expected %v, but got %v", exp, got) - } - if got, exp := c.GetMulti("k1", "k0"), bytes("b", "a"); !reflect.DeepEqual(exp, got) { - t.Errorf("Expected %v, but got %v", exp, got) - } - - c.SetMulti([2]string{"k0", "x"}, [2]string{"k1", "y"}, [2]string{"k2", "z"}) - if got, exp := c.GetMulti("k0", "k1", "k2"), bytes("x", "y", "z"); !reflect.DeepEqual(exp, got) { - t.Errorf("Expected %v, but got %v", exp, got) - } - got, exist := c.Get("k0") - if exp := "x"; !exist || string(got) != exp { - t.Errorf("Expected %v, but got %v", exp, string(got)) - } - - c.Delete("k0") - if got, exp := c.GetMulti("k0", "k1", "k2"), [][]byte{nil, []byte("y"), []byte("z")}; !reflect.DeepEqual(exp, got) { - t.Errorf("Expected %v, but got %v", exp, got) - } - - c.DeleteMulti("k1", "k2") - if got, exp := c.GetMulti("k0", "k1", "k2"), [][]byte{nil, nil, nil}; !reflect.DeepEqual(exp, got) { - t.Errorf("Expected %v, but got %v", exp, got) - } -} - func TestCache_deleteAllKeysWithPrefix(t *testing.T) { SetupForTest(t) @@ -167,7 +113,7 @@ func TestCache_deleteAllKeysWithPrefix(t *testing.T) { bKeys = append(bKeys, key) } - c.SetMulti([2]string{key, strconv.Itoa(i)}) + c.Set(key, []byte(strconv.Itoa(i))) } conn := poolGet() @@ -178,12 +124,22 @@ func TestCache_deleteAllKeysWithPrefix(t *testing.T) { t.Error(err) } - vals := c.GetMulti(aKeys...) + getMulti := func(keys ...string) [][]byte { + t.Helper() + var vals [][]byte + for _, k := range keys { + v, _ := c.Get(k) + vals = append(vals, v) + } + return vals + } + + vals := getMulti(aKeys...) if got, exp := vals, [][]byte{nil, nil, nil, nil, nil}; !reflect.DeepEqual(exp, got) { t.Errorf("Expected %v, but got %v", exp, got) } - vals = c.GetMulti(bKeys...) + vals = getMulti(bKeys...) if got, exp := vals, bytes("1", "3", "5", "7", "9"); !reflect.DeepEqual(exp, got) { t.Errorf("Expected %v, but got %v", exp, got) } @@ -266,23 +222,6 @@ func TestCache_SetWithTTL(t *testing.T) { } } -func TestCache_ListKeys(t *testing.T) { - SetupForTest(t) - - c := NewWithTTL("some_prefix", 1) - c.SetMulti( - [2]string{"foobar", "123"}, - [2]string{"bazbar", "456"}, - [2]string{"barfoo", "234"}, - ) - - keys, err := c.ListKeys(context.Background()) - assert.NoError(t, err) - for _, k := range []string{"foobar", "bazbar", "barfoo"} { - assert.Contains(t, keys, k) - } -} - func TestCache_LTrimList(t *testing.T) { SetupForTest(t) From 4a7fae11bb11ca9e7f777a87c6cb43dc9c51c27c Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Fri, 20 Jan 2023 13:24:31 +0200 Subject: [PATCH 060/678] redispool: replace Expire with SetEx (#46704) SetEx is used by rcache and we can update the only call site of expire with SetEx. Test Plan: go test --- internal/adminanalytics/cache.go | 6 +----- internal/redispool/keyvalue.go | 11 +++++------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/internal/adminanalytics/cache.go b/internal/adminanalytics/cache.go index b255445c7594..6fe9ff5cb77c 100644 --- a/internal/adminanalytics/cache.go +++ b/internal/adminanalytics/cache.go @@ -54,15 +54,11 @@ func setDataToCache(key string, data string, expireSeconds int) (bool, error) { return true, nil } - if err := store.Set(scopeKey+key, data); err != nil { - return false, err - } - if expireSeconds == 0 { expireSeconds = 24 * 60 * 60 // 1 day } - if err := store.Expire(scopeKey+key, expireSeconds); err != nil { + if err := store.SetEx(scopeKey+key, expireSeconds, data); err != nil { return false, err } diff --git a/internal/redispool/keyvalue.go b/internal/redispool/keyvalue.go index b3a8af5634c8..cc3845d97cad 100644 --- a/internal/redispool/keyvalue.go +++ b/internal/redispool/keyvalue.go @@ -22,6 +22,7 @@ type KeyValue interface { Get(key string) Value GetSet(key string, value any) Value Set(key string, value any) error + SetEx(key string, ttlSeconds int, value any) error Del(key string) error HGet(key, field string) Value @@ -32,8 +33,6 @@ type KeyValue interface { LLen(key string) (int, error) LRange(key string, start, stop int) Value - Expire(key string, seconds int) error - // WithContext will return a KeyValue that should respect ctx for all // blocking operations. WithContext(ctx context.Context) KeyValue @@ -102,6 +101,10 @@ func (r *redisKeyValue) Set(key string, val any) error { return r.do("SET", r.prefix+key, val).err } +func (r *redisKeyValue) SetEx(key string, ttlSeconds int, val any) error { + return r.do("SET", r.prefix+key, ttlSeconds, val).err +} + func (r *redisKeyValue) Del(key string) error { return r.do("DEL", r.prefix+key).err } @@ -128,10 +131,6 @@ func (r *redisKeyValue) LRange(key string, start, stop int) Value { return r.do("LRANGE", r.prefix+key, start, stop) } -func (r *redisKeyValue) Expire(key string, seconds int) error { - return r.do("EXPIRE", r.prefix+key, seconds).err -} - func (r *redisKeyValue) WithContext(ctx context.Context) KeyValue { return &redisKeyValue{ pool: r.pool, From dac4e1a7033d90c02b9d47465806c3f3b6f1dbaa Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 20 Jan 2023 13:01:22 +0100 Subject: [PATCH 061/678] authz: change `ValidateConnection` to return `error` instead of `[]string` (#46424) --- .../cmd/frontend/internal/authz/init.go | 5 +- .../internal/authz/perms_syncer_test.go | 2 +- enterprise/internal/authz/authz_test.go | 2 +- .../internal/authz/bitbucketcloud/provider.go | 7 +-- .../authz/bitbucketserver/provider.go | 6 +- .../authz/bitbucketserver/provider_test.go | 32 +++++----- enterprise/internal/authz/gerrit/gerrit.go | 14 ++--- .../internal/authz/gerrit/gerrit_test.go | 20 ++++--- enterprise/internal/authz/github/github.go | 59 ++++++++----------- .../internal/authz/github/github_test.go | 26 ++++---- enterprise/internal/authz/gitlab/oauth.go | 2 +- enterprise/internal/authz/gitlab/sudo.go | 11 ++-- .../internal/authz/perforce/perforce.go | 6 +- internal/authz/iface.go | 2 +- internal/database/repos_perm_test.go | 2 +- 15 files changed, 94 insertions(+), 102 deletions(-) diff --git a/enterprise/cmd/frontend/internal/authz/init.go b/enterprise/cmd/frontend/internal/authz/init.go index 0413cf955e80..9bb369ff2a73 100644 --- a/enterprise/cmd/frontend/internal/authz/init.go +++ b/enterprise/cmd/frontend/internal/authz/init.go @@ -62,10 +62,11 @@ func Init( // Add connection validation issue for _, p := range providers { - for _, problem := range p.ValidateConnection(ctx) { - warnings = append(warnings, fmt.Sprintf("%s provider %q: %s", p.ServiceType(), p.ServiceID(), problem)) + if err := p.ValidateConnection(ctx); err != nil { + warnings = append(warnings, fmt.Sprintf("%s provider %q: %s", p.ServiceType(), p.ServiceID(), err)) } } + problems = append(problems, conf.NewExternalServiceProblems(warnings...)...) return problems }) diff --git a/enterprise/cmd/repo-updater/internal/authz/perms_syncer_test.go b/enterprise/cmd/repo-updater/internal/authz/perms_syncer_test.go index 6379c8c5a187..bc3c59bc3e1b 100644 --- a/enterprise/cmd/repo-updater/internal/authz/perms_syncer_test.go +++ b/enterprise/cmd/repo-updater/internal/authz/perms_syncer_test.go @@ -93,7 +93,7 @@ func (p *mockProvider) ServiceType() string { return p.serviceType } func (p *mockProvider) ServiceID() string { return p.serviceID } func (p *mockProvider) URN() string { return extsvc.URN(p.serviceType, p.id) } -func (*mockProvider) ValidateConnection(context.Context) []string { return nil } +func (*mockProvider) ValidateConnection(context.Context) error { return nil } func (p *mockProvider) FetchUserPerms(ctx context.Context, acct *extsvc.Account, opts authz.FetchPermsOptions) (*authz.ExternalUserPermissions, error) { return p.fetchUserPerms(ctx, acct) diff --git a/enterprise/internal/authz/authz_test.go b/enterprise/internal/authz/authz_test.go index 907d2c356a12..13c1ae8882dd 100644 --- a/enterprise/internal/authz/authz_test.go +++ b/enterprise/internal/authz/authz_test.go @@ -50,7 +50,7 @@ func (m gitlabAuthzProviderParams) URN() string { panic("should never be called") } -func (m gitlabAuthzProviderParams) ValidateConnection(context.Context) []string { return nil } +func (m gitlabAuthzProviderParams) ValidateConnection(context.Context) error { return nil } func (m gitlabAuthzProviderParams) FetchUserPerms(context.Context, *extsvc.Account, authz.FetchPermsOptions) (*authz.ExternalUserPermissions, error) { panic("should never be called") diff --git a/enterprise/internal/authz/bitbucketcloud/provider.go b/enterprise/internal/authz/bitbucketcloud/provider.go index 9e8562dd08c1..cdc6b2293c53 100644 --- a/enterprise/internal/authz/bitbucketcloud/provider.go +++ b/enterprise/internal/authz/bitbucketcloud/provider.go @@ -60,12 +60,9 @@ func NewProvider(db database.DB, conn *types.BitbucketCloudConnection, opts Prov // ValidateConnection validates that the Provider has access to the Bitbucket Cloud API // with the credentials it was configured with. -func (p *Provider) ValidateConnection(ctx context.Context) []string { +func (p *Provider) ValidateConnection(ctx context.Context) error { _, err := p.client.CurrentUser(ctx) - if err != nil { - return []string{err.Error()} - } - return []string{} + return err } func (p *Provider) URN() string { diff --git a/enterprise/internal/authz/bitbucketserver/provider.go b/enterprise/internal/authz/bitbucketserver/provider.go index 78aacb4833bd..67f1ad8b01fe 100644 --- a/enterprise/internal/authz/bitbucketserver/provider.go +++ b/enterprise/internal/authz/bitbucketserver/provider.go @@ -50,17 +50,17 @@ func NewProvider(cli *bitbucketserver.Client, urn string, pluginPerm bool) *Prov // ValidateConnection validates that the Provider has access to the Bitbucket Server API // with the OAuth credentials it was configured with. -func (p *Provider) ValidateConnection(ctx context.Context) []string { +func (p *Provider) ValidateConnection(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() username, err := p.client.Username() if err != nil { - return []string{err.Error()} + return err } if _, err := p.client.UserPermissions(ctx, username); err != nil { - return []string{err.Error()} + return err } return nil diff --git a/enterprise/internal/authz/bitbucketserver/provider_test.go b/enterprise/internal/authz/bitbucketserver/provider_test.go index 7346b89debc0..396a535119b5 100644 --- a/enterprise/internal/authz/bitbucketserver/provider_test.go +++ b/enterprise/internal/authz/bitbucketserver/provider_test.go @@ -31,19 +31,17 @@ func TestProvider_ValidateConnection(t *testing.T) { } for _, tc := range []struct { - name string - client func(*bitbucketserver.Client) - problems []string + name string + client func(*bitbucketserver.Client) + wantErr string }{ { name: "no-problems-when-authenticated-as-admin", }, { - name: "problems-when-authenticated-as-non-admin", - client: func(c *bitbucketserver.Client) { c.Auth = &auth.BasicAuth{} }, - problems: []string{ - `Bitbucket API HTTP error: code=401 url="${INSTANCEURL}/rest/api/1.0/admin/permissions/users?filter=" body="{\"errors\":[{\"context\":null,\"message\":\"You are not permitted to access this resource\",\"exceptionName\":\"com.atlassian.bitbucket.AuthorisationException\"}]}"`, - }, + name: "problems-when-authenticated-as-non-admin", + client: func(c *bitbucketserver.Client) { c.Auth = &auth.BasicAuth{} }, + wantErr: `Bitbucket API HTTP error: code=401 url="${INSTANCEURL}/rest/api/1.0/admin/permissions/users?filter=" body="{\"errors\":[{\"context\":null,\"message\":\"You are not permitted to access this resource\",\"exceptionName\":\"com.atlassian.bitbucket.AuthorisationException\"}]}"`, }, } { t.Run(tc.name, func(t *testing.T) { @@ -55,13 +53,19 @@ func TestProvider_ValidateConnection(t *testing.T) { tc.client(p.client) } - for i := range tc.problems { - tc.problems[i] = strings.ReplaceAll(tc.problems[i], "${INSTANCEURL}", instanceURL) - } + tc.wantErr = strings.ReplaceAll(tc.wantErr, "${INSTANCEURL}", instanceURL) - problems := p.ValidateConnection(context.Background()) - if have, want := problems, tc.problems; !reflect.DeepEqual(have, want) { - t.Error(cmp.Diff(have, want)) + err := p.ValidateConnection(context.Background()) + if tc.wantErr == "" && err != nil { + t.Fatalf("unexpected error: %s", err) + } + if tc.wantErr != "" { + if err == nil { + t.Fatal("expected error, but got none") + } + if have, want := err.Error(), tc.wantErr; !reflect.DeepEqual(have, want) { + t.Error(cmp.Diff(have, want)) + } } }) } diff --git a/enterprise/internal/authz/gerrit/gerrit.go b/enterprise/internal/authz/gerrit/gerrit.go index 159f95bd71c7..7dc1ca37514c 100644 --- a/enterprise/internal/authz/gerrit/gerrit.go +++ b/enterprise/internal/authz/gerrit/gerrit.go @@ -3,7 +3,6 @@ package gerrit import ( "context" "encoding/json" - "fmt" "net/url" jsoniter "github.com/json-iterator/go" @@ -134,20 +133,15 @@ func (p Provider) URN() string { // ValidateConnection validates the connection to the Gerrit code host. // Currently, this is done by querying for the Administrators group and validating that the // group returned is valid, hence meaning that the given credentials have Admin permissions. -func (p Provider) ValidateConnection(ctx context.Context) (warnings []string) { - +func (p Provider) ValidateConnection(ctx context.Context) error { adminGroup, err := p.client.GetGroup(ctx, adminGroupName) if err != nil { - return []string{ - fmt.Sprintf("Unable to get %s group: %v", adminGroupName, err), - } + return errors.Newf("Unable to get %s group: %s", adminGroupName, err) } if adminGroup.ID == "" || adminGroup.Name != adminGroupName || adminGroup.CreatedOn == "" { - return []string{ - fmt.Sprintf("Gerrit credentials not sufficent enough to query %s group", adminGroupName), - } + return errors.Newf("Gerrit credentials not sufficent enough to query %s group", adminGroupName) } - return []string{} + return nil } diff --git a/enterprise/internal/authz/gerrit/gerrit_test.go b/enterprise/internal/authz/gerrit/gerrit_test.go index 9cf5ff323588..b70cd3bbfed5 100644 --- a/enterprise/internal/authz/gerrit/gerrit_test.go +++ b/enterprise/internal/authz/gerrit/gerrit_test.go @@ -72,9 +72,9 @@ func TestProvider_FetchAccount(t *testing.T) { func TestProvider_ValidateConnection(t *testing.T) { testCases := []struct { - name string - client mockClient - warnings []string + name string + client mockClient + wantErr string }{ { name: "GetGroup fails", @@ -83,7 +83,7 @@ func TestProvider_ValidateConnection(t *testing.T) { return gerrit.Group{}, errors.New("fake error") }, }, - warnings: []string{fmt.Sprintf("Unable to get %s group: %v", adminGroupName, errors.New("fake error"))}, + wantErr: fmt.Sprintf("Unable to get %s group: %v", adminGroupName, errors.New("fake error")), }, { name: "no access to admin group", @@ -94,7 +94,7 @@ func TestProvider_ValidateConnection(t *testing.T) { }, nil }, }, - warnings: []string{fmt.Sprintf("Gerrit credentials not sufficent enough to query %s group", adminGroupName)}, + wantErr: fmt.Sprintf("Gerrit credentials not sufficent enough to query %s group", adminGroupName), }, { name: "admin group is valid", @@ -107,14 +107,18 @@ func TestProvider_ValidateConnection(t *testing.T) { }, nil }, }, - warnings: []string{}, + wantErr: "", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { p := NewTestProvider(&tc.client) - warnings := p.ValidateConnection(context.Background()) - if diff := cmp.Diff(warnings, tc.warnings); diff != "" { + err := p.ValidateConnection(context.Background()) + errMessage := "" + if err != nil { + errMessage = err.Error() + } + if diff := cmp.Diff(errMessage, tc.wantErr); diff != "" { t.Fatalf("warnings did not match: %s", diff) } diff --git a/enterprise/internal/authz/github/github.go b/enterprise/internal/authz/github/github.go index 4a328472a387..279b100b02d5 100644 --- a/enterprise/internal/authz/github/github.go +++ b/enterprise/internal/authz/github/github.go @@ -102,24 +102,20 @@ func (p *Provider) ServiceType() string { return p.codeHost.ServiceType } -func (p *Provider) ValidateConnection(ctx context.Context) []string { - required := p.requiredAuthScopes() - if len(required) == 0 { - return []string{} +func (p *Provider) ValidateConnection(ctx context.Context) error { + required, ok := p.requiredAuthScopes() + if !ok { + return nil } client, err := p.client() if err != nil { - return []string{ - fmt.Sprintf("Unable to get client: %v", err), - } + return errors.Wrap(err, "unable to get client") } scopes, err := client.GetAuthenticatedOAuthScopes(ctx) if err != nil { - return []string{ - fmt.Sprintf("Additional OAuth scopes are required, but failed to get available scopes: %+v", err), - } + return errors.Wrap(err, "additional OAuth scopes are required, but failed to get available scopes") } gotScopes := make(map[string]struct{}) @@ -127,22 +123,19 @@ func (p *Provider) ValidateConnection(ctx context.Context) []string { gotScopes[gotScope] = struct{}{} } - var problems []string // check if required scopes are satisfied - for _, requiredScope := range required { - satisfiesScope := false - for _, s := range requiredScope.oneOf { - if _, found := gotScopes[s]; found { - satisfiesScope = true - break - } - } - if !satisfiesScope { - problems = append(problems, requiredScope.message) + satisfiesScope := false + for _, s := range required.oneOf { + if _, found := gotScopes[s]; found { + satisfiesScope = true + break } } + if !satisfiesScope { + return errors.New(required.message) + } - return problems + return nil } type requiredAuthScope struct { @@ -152,20 +145,18 @@ type requiredAuthScope struct { message string } -func (p *Provider) requiredAuthScopes() []requiredAuthScope { - scopes := []requiredAuthScope{} - - if p.groupsCache != nil { - // Needs extra scope to pull group permissions - scopes = append(scopes, requiredAuthScope{ - oneOf: []string{"read:org", "write:org", "admin:org"}, - message: "Scope `read:org`, `write:org`, or `admin:org` is required to enable `authorization.groupsCacheTTL` - " + - "please provide a `token` with the required scopes, or try updating the [**site configuration**](/site-admin/configuration)'s " + - "corresponding entry in [`auth.providers`](https://docs.sourcegraph.com/admin/auth) to enable `allowGroupsPermissionsSync`.", - }) +func (p *Provider) requiredAuthScopes() (requiredAuthScope, bool) { + if p.groupsCache == nil { + return requiredAuthScope{}, false } - return scopes + // Needs extra scope to pull group permissions + return requiredAuthScope{ + oneOf: []string{"read:org", "write:org", "admin:org"}, + message: "Scope `read:org`, `write:org`, or `admin:org` is required to enable `authorization.groupsCacheTTL` - " + + "please provide a `token` with the required scopes, or try updating the [**site configuration**](/site-admin/configuration)'s " + + "corresponding entry in [`auth.providers`](https://docs.sourcegraph.com/admin/auth) to enable `allowGroupsPermissionsSync`.", + }, true } // fetchUserPermsByToken fetches all the private repo ids that the token can access. diff --git a/enterprise/internal/authz/github/github_test.go b/enterprise/internal/authz/github/github_test.go index 53fac1cc39d7..9d53a689518a 100644 --- a/enterprise/internal/authz/github/github_test.go +++ b/enterprise/internal/authz/github/github_test.go @@ -1206,8 +1206,8 @@ func TestProvider_ValidateConnection(t *testing.T) { GitHubURL: mustURL(t, "https://github.com"), GroupsCacheTTL: -1, }) - problems := p.ValidateConnection(context.Background()) - if len(problems) > 0 { + err := p.ValidateConnection(context.Background()) + if err != nil { t.Fatal("expected validate to pass") } }) @@ -1225,12 +1225,12 @@ func TestProvider_ValidateConnection(t *testing.T) { return nil, errors.New("scopes error") }) p.client = mockClientFunc(mockClient) - problems := p.ValidateConnection(context.Background()) - if len(problems) != 1 { + err := p.ValidateConnection(context.Background()) + if err == nil { t.Fatal("expected 1 problem") } - if !strings.Contains(problems[0], "scopes error") { - t.Fatalf("unexpected problem: %q", problems[0]) + if !strings.Contains(err.Error(), "scopes error") { + t.Fatalf("unexpected problem: %q", err.Error()) } }) @@ -1241,12 +1241,12 @@ func TestProvider_ValidateConnection(t *testing.T) { return []string{}, nil }) p.client = mockClientFunc(mockClient) - problems := p.ValidateConnection(context.Background()) - if len(problems) != 1 { - t.Fatal("expected 1 problem") + err := p.ValidateConnection(context.Background()) + if err == nil { + t.Fatal("expected error") } - if !strings.Contains(problems[0], "read:org") { - t.Fatalf("unexpected problem: %q", problems[0]) + if !strings.Contains(err.Error(), "read:org") { + t.Fatalf("unexpected problem: %q", err.Error()) } }) @@ -1262,8 +1262,8 @@ func TestProvider_ValidateConnection(t *testing.T) { return testCase, nil }) p.client = mockClientFunc(mockClient) - problems := p.ValidateConnection(context.Background()) - if len(problems) != 0 { + err := p.ValidateConnection(context.Background()) + if err != nil { t.Fatalf("expected validate to pass for scopes=%+v", testCase) } } diff --git a/enterprise/internal/authz/gitlab/oauth.go b/enterprise/internal/authz/gitlab/oauth.go index c75d8e495260..e8611ef837f6 100644 --- a/enterprise/internal/authz/gitlab/oauth.go +++ b/enterprise/internal/authz/gitlab/oauth.go @@ -61,7 +61,7 @@ func newOAuthProvider(op OAuthProviderOp, cli httpcli.Doer) *OAuthProvider { } } -func (p *OAuthProvider) ValidateConnection(context.Context) (problems []string) { +func (p *OAuthProvider) ValidateConnection(context.Context) error { return nil } diff --git a/enterprise/internal/authz/gitlab/sudo.go b/enterprise/internal/authz/gitlab/sudo.go index 0807606cbc15..ec04a6ab069e 100644 --- a/enterprise/internal/authz/gitlab/sudo.go +++ b/enterprise/internal/authz/gitlab/sudo.go @@ -75,17 +75,18 @@ func newSudoProvider(op SudoProviderOp, cli httpcli.Doer) *SudoProvider { } } -func (p *SudoProvider) ValidateConnection(ctx context.Context) (problems []string) { +func (p *SudoProvider) ValidateConnection(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if _, _, err := p.clientProvider.GetPATClient(p.sudoToken, "1").ListProjects(ctx, "projects"); err != nil { if err == ctx.Err() { - problems = append(problems, fmt.Sprintf("GitLab API did not respond within 5s (%s)", err.Error())) - } else if !gitlab.IsNotFound(err) { - problems = append(problems, "access token did not have sufficient privileges, requires scopes \"sudo\" and \"api\"") + return errors.Wrap(err, "GitLab API did not respond within 5s") + } + if !gitlab.IsNotFound(err) { + return errors.New("access token did not have sufficient privileges, requires scopes \"sudo\" and \"api\"") } } - return problems + return nil } func (p *SudoProvider) URN() string { diff --git a/enterprise/internal/authz/perforce/perforce.go b/enterprise/internal/authz/perforce/perforce.go index e82760b948fa..3bf391896e2c 100644 --- a/enterprise/internal/authz/perforce/perforce.go +++ b/enterprise/internal/authz/perforce/perforce.go @@ -378,7 +378,7 @@ func (p *Provider) URN() string { return p.urn } -func (p *Provider) ValidateConnection(ctx context.Context) (problems []string) { +func (p *Provider) ValidateConnection(ctx context.Context) error { // Validate the user has "super" access with "-u" option, see https://www.perforce.com/perforce/r12.1/manuals/cmdref/protects.html rc, _, err := p.p4Execer.P4Exec(ctx, p.host, p.user, p.password, "protects", "-u", p.user) if err == nil { @@ -387,9 +387,9 @@ func (p *Provider) ValidateConnection(ctx context.Context) (problems []string) { } if strings.Contains(err.Error(), "You don't have permission for this operation.") { - return []string{"the user does not have super access"} + return errors.New("the user does not have super access") } - return []string{"validate user access level: " + err.Error()} + return errors.Wrap(err, "invalid user access level") } func scanEmail(s *bufio.Scanner) (string, string, bool) { diff --git a/internal/authz/iface.go b/internal/authz/iface.go index 5b86c24e88e9..4770b072eebf 100644 --- a/internal/authz/iface.go +++ b/internal/authz/iface.go @@ -135,7 +135,7 @@ type Provider interface { // ValidateConnection checks that the configuration and credentials of the authz provider // can establish a valid connection with the provider, and returns warnings based on any // issues it finds. - ValidateConnection(ctx context.Context) (warnings []string) + ValidateConnection(ctx context.Context) error } // ErrUnauthenticated indicates an unauthenticated request. diff --git a/internal/database/repos_perm_test.go b/internal/database/repos_perm_test.go index de4afea4ca4a..790a9798a4af 100644 --- a/internal/database/repos_perm_test.go +++ b/internal/database/repos_perm_test.go @@ -37,7 +37,7 @@ func (p *fakeProvider) ServiceType() string { return p.codeHost.ServiceType } func (p *fakeProvider) ServiceID() string { return p.codeHost.ServiceID } func (p *fakeProvider) URN() string { return extsvc.URN(p.codeHost.ServiceType, 0) } -func (p *fakeProvider) ValidateConnection(context.Context) (problems []string) { return nil } +func (p *fakeProvider) ValidateConnection(context.Context) error { return nil } func (p *fakeProvider) FetchUserPerms(context.Context, *extsvc.Account, authz.FetchPermsOptions) (*authz.ExternalUserPermissions, error) { return nil, nil From fdc37a68a97285da075d72e197cbe52db5e15a7b Mon Sep 17 00:00:00 2001 From: Alex Ostrikov Date: Fri, 20 Jan 2023 16:19:09 +0400 Subject: [PATCH 062/678] permissions-center: rename `NextSyncAt` to `ProcessAfter` for consistency. (#46699) Test plan: Existing tests. --- .../internal/authz/webhooks/github.go | 12 ++-- .../internal/authz/perms_syncer.go | 62 +++++++++---------- internal/authz/permssync/permssync.go | 4 +- internal/authz/permssync/permssync_test.go | 8 +-- internal/database/permission_sync_jobs.go | 10 +-- .../database/permission_sync_jobs_test.go | 10 +-- internal/repoupdater/protocol/repoupdater.go | 2 +- 7 files changed, 54 insertions(+), 54 deletions(-) diff --git a/enterprise/cmd/frontend/internal/authz/webhooks/github.go b/enterprise/cmd/frontend/internal/authz/webhooks/github.go index e8478d7bbc2f..737c03817290 100644 --- a/enterprise/cmd/frontend/internal/authz/webhooks/github.go +++ b/enterprise/cmd/frontend/internal/authz/webhooks/github.go @@ -167,9 +167,9 @@ func (h *GitHubWebhook) getUserAndSyncPerms(ctx context.Context, db database.DB, } permssync.SchedulePermsSync(ctx, h.logger, db, protocol.PermsSyncRequest{ - UserIDs: []int32{externalAccounts[0].UserID}, - Reason: reason, - NextSyncAt: time.Now().Add(sleepTime), + UserIDs: []int32{externalAccounts[0].UserID}, + Reason: reason, + ProcessAfter: time.Now().Add(sleepTime), }) return err @@ -184,9 +184,9 @@ func (h *GitHubWebhook) getRepoAndSyncPerms(ctx context.Context, db database.DB, } permssync.SchedulePermsSync(ctx, h.logger, db, protocol.PermsSyncRequest{ - RepoIDs: []api.RepoID{repo.ID}, - Reason: reason, - NextSyncAt: time.Now().Add(sleepTime), + RepoIDs: []api.RepoID{repo.ID}, + Reason: reason, + ProcessAfter: time.Now().Add(sleepTime), }) return nil diff --git a/enterprise/cmd/repo-updater/internal/authz/perms_syncer.go b/enterprise/cmd/repo-updater/internal/authz/perms_syncer.go index ea2e504644a5..a6b6a2b072cf 100644 --- a/enterprise/cmd/repo-updater/internal/authz/perms_syncer.go +++ b/enterprise/cmd/repo-updater/internal/authz/perms_syncer.go @@ -128,7 +128,7 @@ func (s *PermsSyncer) ScheduleUsers(ctx context.Context, opts authz.FetchPermsOp priority: priorityHigh, userID: userIDs[i], options: opts, - // NOTE: Have nextSyncAt with zero value (i.e. not set) gives it higher priority, + // NOTE: Have processAfter with zero value (i.e. not set) gives it higher priority, // as the request is most likely triggered by a user action from OSS namespace. } } @@ -153,7 +153,7 @@ func (s *PermsSyncer) scheduleUsers(ctx context.Context, users ...scheduledUser) Type: requestTypeUser, ID: u.userID, Options: u.options, - NextSyncAt: u.nextSyncAt, + NextSyncAt: u.processAfter, NoPerms: u.noPerms, }) logger.Debug("queue.enqueued", log.Int32("userID", u.userID), log.Bool("updated", updated)) @@ -178,7 +178,7 @@ func (s *PermsSyncer) ScheduleRepos(ctx context.Context, repoIDs ...api.RepoID) repositories[i] = scheduledRepo{ priority: priorityHigh, repoID: repoIDs[i], - // NOTE: Have nextSyncAt with zero value (i.e. not set) gives it higher priority, + // NOTE: Have processAfter with zero value (i.e. not set) gives it higher priority, // as the request is most likely triggered by a user action from OSS namespace. } } @@ -203,7 +203,7 @@ func (s *PermsSyncer) scheduleRepos(ctx context.Context, repos ...scheduledRepo) Priority: r.priority, Type: requestTypeRepo, ID: int32(r.repoID), - NextSyncAt: r.nextSyncAt, + NextSyncAt: r.processAfter, NoPerms: r.noPerms, }) logger.Debug("queue.enqueued", log.Int32("repoID", int32(r.repoID)), log.Bool("updated", updated)) @@ -595,7 +595,7 @@ func (s *PermsSyncer) syncUserPerms(ctx context.Context, userID int32, noPerms b Type: authz.PermRepos, IDs: map[int32]struct{}{}, } - s.permsStore.LoadUserPermissions(ctx, oldPerms) + _ = s.permsStore.LoadUserPermissions(ctx, oldPerms) // Save new permissions to database p := &authz.UserPermissions{ @@ -774,7 +774,7 @@ func (s *PermsSyncer) syncRepoPerms(ctx context.Context, repoID api.RepoID, noPe Perm: authz.Read, UserIDs: map[int32]struct{}{}, } - s.permsStore.LoadRepoPermissions(ctx, oldPerms) + _ = s.permsStore.LoadRepoPermissions(ctx, oldPerms) // Save permissions to database p := &authz.RepoPermissions{ @@ -1003,7 +1003,7 @@ func (s *PermsSyncer) scheduleUsersWithNoPerms(ctx context.Context) ([]scheduled users[i] = scheduledUser{ priority: priorityLow, userID: id, - // NOTE: Have nextSyncAt with zero value (i.e. not set) gives it higher priority. + // NOTE: Have processAfter with zero value (i.e. not set) gives it higher priority. noPerms: true, } } @@ -1019,20 +1019,20 @@ func (s *PermsSyncer) scheduleReposWithNoPerms(ctx context.Context) ([]scheduled } metricsNoPerms.WithLabelValues("repo").Set(float64(len(ids))) - repos := make([]scheduledRepo, len(ids)) + repositories := make([]scheduledRepo, len(ids)) for i, id := range ids { - repos[i] = scheduledRepo{ + repositories[i] = scheduledRepo{ priority: priorityLow, repoID: id, - // NOTE: Have nextSyncAt with zero value (i.e. not set) gives it higher priority. + // NOTE: Have processAfter with zero value (i.e. not set) gives it higher priority. noPerms: true, } } - return repos, nil + return repositories, nil } -// scheduleUsersWithOldestPerms returns computed schedules for users who have oldest -// permissions in database and capped results by the limit. +// scheduleUsersWithOldestPerms returns computed schedules for users who have the +// oldest permissions in database and capped results by the limit. func (s *PermsSyncer) scheduleUsersWithOldestPerms(ctx context.Context, limit int, age time.Duration) ([]scheduledUser, error) { results, err := s.permsStore.UserIDsWithOldestPerms(ctx, limit, age) if err != nil { @@ -1042,9 +1042,9 @@ func (s *PermsSyncer) scheduleUsersWithOldestPerms(ctx context.Context, limit in users := make([]scheduledUser, 0, len(results)) for id, t := range results { users = append(users, scheduledUser{ - priority: priorityLow, - userID: id, - nextSyncAt: t, + priority: priorityLow, + userID: id, + processAfter: t, }) } return users, nil @@ -1058,15 +1058,15 @@ func (s *PermsSyncer) scheduleReposWithOldestPerms(ctx context.Context, limit in return nil, err } - repos := make([]scheduledRepo, 0, len(results)) + repositories := make([]scheduledRepo, 0, len(results)) for id, t := range results { - repos = append(repos, scheduledRepo{ - priority: priorityLow, - repoID: id, - nextSyncAt: t, + repositories = append(repositories, scheduledRepo{ + priority: priorityLow, + repoID: id, + processAfter: t, }) } - return repos, nil + return repositories, nil } // schedule contains information for scheduling users and repositories. @@ -1077,10 +1077,10 @@ type schedule struct { // scheduledUser contains information for scheduling a user. type scheduledUser struct { - priority priority - userID int32 - options authz.FetchPermsOptions - nextSyncAt time.Time + priority priority + userID int32 + options authz.FetchPermsOptions + processAfter time.Time // Whether the user has no permissions when scheduled. Currently used to // accept partial results from authz provider in case of error. @@ -1089,9 +1089,9 @@ type scheduledUser struct { // scheduledRepo contains for scheduling a repository. type scheduledRepo struct { - priority priority - repoID api.RepoID - nextSyncAt time.Time + priority priority + repoID api.RepoID + processAfter time.Time // Whether the repository has no permissions when scheduled. Currently used // to accept partial results from authz provider in case of error. @@ -1200,7 +1200,7 @@ func (s *PermsSyncer) runSchedule(ctx context.Context) { if u.noPerms { reason = permssync.ReasonUserNoPermissions } - opts := database.PermissionSyncJobOpts{NextSyncAt: u.nextSyncAt, Reason: reason} + opts := database.PermissionSyncJobOpts{ProcessAfter: u.processAfter, Reason: reason} if err := store.CreateUserSyncJob(ctx, u.userID, opts); err != nil { logger.Error("failed to create user sync job", log.Error(err)) continue @@ -1212,7 +1212,7 @@ func (s *PermsSyncer) runSchedule(ctx context.Context) { if r.noPerms { reason = permssync.ReasonRepoNoPermissions } - opts := database.PermissionSyncJobOpts{NextSyncAt: r.nextSyncAt, Reason: reason} + opts := database.PermissionSyncJobOpts{ProcessAfter: r.processAfter, Reason: reason} if err := store.CreateRepoSyncJob(ctx, r.repoID, opts); err != nil { logger.Error("failed to create repo sync job", log.Error(err)) continue diff --git a/internal/authz/permssync/permssync.go b/internal/authz/permssync/permssync.go index 23d126d4a264..c65cb685c127 100644 --- a/internal/authz/permssync/permssync.go +++ b/internal/authz/permssync/permssync.go @@ -82,7 +82,7 @@ func SchedulePermsSync(ctx context.Context, logger log.Logger, db database.DB, r InvalidateCaches: req.Options.InvalidateCaches, Reason: req.Reason, TriggeredByUserID: req.TriggeredByUserID, - NextSyncAt: req.NextSyncAt, + ProcessAfter: req.ProcessAfter, } err := db.PermissionSyncJobs().CreateUserSyncJob(ctx, userID, opts) if err != nil { @@ -96,7 +96,7 @@ func SchedulePermsSync(ctx context.Context, logger log.Logger, db database.DB, r InvalidateCaches: req.Options.InvalidateCaches, Reason: req.Reason, TriggeredByUserID: req.TriggeredByUserID, - NextSyncAt: req.NextSyncAt, + ProcessAfter: req.ProcessAfter, } err := db.PermissionSyncJobs().CreateRepoSyncJob(ctx, repoID, opts) if err != nil { diff --git a/internal/authz/permssync/permssync_test.go b/internal/authz/permssync/permssync_test.go index f0f04c2ac004..7512789a8df6 100644 --- a/internal/authz/permssync/permssync_test.go +++ b/internal/authz/permssync/permssync_test.go @@ -28,7 +28,7 @@ func TestSchedulePermsSync_UserPermsTest(t *testing.T) { db.FeatureFlagsFunc.SetDefaultReturn(featureFlags) syncTime := time.Now().Add(13 * time.Second) - request := protocol.PermsSyncRequest{UserIDs: []int32{1}, Reason: ReasonManualUserSync, TriggeredByUserID: int32(123), NextSyncAt: syncTime} + request := protocol.PermsSyncRequest{UserIDs: []int32{1}, Reason: ReasonManualUserSync, TriggeredByUserID: int32(123), ProcessAfter: syncTime} SchedulePermsSync(ctx, logger, db, request) assert.Len(t, permsSyncStore.CreateUserSyncJobFunc.History(), 1) assert.Empty(t, permsSyncStore.CreateRepoSyncJobFunc.History()) @@ -36,7 +36,7 @@ func TestSchedulePermsSync_UserPermsTest(t *testing.T) { assert.NotNil(t, permsSyncStore.CreateUserSyncJobFunc.History()[0].Arg2) assert.Equal(t, ReasonManualUserSync, permsSyncStore.CreateUserSyncJobFunc.History()[0].Arg2.Reason) assert.Equal(t, int32(123), permsSyncStore.CreateUserSyncJobFunc.History()[0].Arg2.TriggeredByUserID) - assert.Equal(t, syncTime, permsSyncStore.CreateUserSyncJobFunc.History()[0].Arg2.NextSyncAt) + assert.Equal(t, syncTime, permsSyncStore.CreateUserSyncJobFunc.History()[0].Arg2.ProcessAfter) } func TestSchedulePermsSync_RepoPermsTest(t *testing.T) { @@ -55,7 +55,7 @@ func TestSchedulePermsSync_RepoPermsTest(t *testing.T) { db.FeatureFlagsFunc.SetDefaultReturn(featureFlags) syncTime := time.Now().Add(37 * time.Second) - request := protocol.PermsSyncRequest{RepoIDs: []api.RepoID{1}, Reason: ReasonManualRepoSync, NextSyncAt: syncTime} + request := protocol.PermsSyncRequest{RepoIDs: []api.RepoID{1}, Reason: ReasonManualRepoSync, ProcessAfter: syncTime} SchedulePermsSync(ctx, logger, db, request) assert.Len(t, permsSyncStore.CreateRepoSyncJobFunc.History(), 1) assert.Empty(t, permsSyncStore.CreateUserSyncJobFunc.History()) @@ -63,5 +63,5 @@ func TestSchedulePermsSync_RepoPermsTest(t *testing.T) { assert.NotNil(t, permsSyncStore.CreateRepoSyncJobFunc.History()[0].Arg1) assert.Equal(t, ReasonManualRepoSync, permsSyncStore.CreateRepoSyncJobFunc.History()[0].Arg2.Reason) assert.Equal(t, int32(0), permsSyncStore.CreateRepoSyncJobFunc.History()[0].Arg2.TriggeredByUserID) - assert.Equal(t, syncTime, permsSyncStore.CreateRepoSyncJobFunc.History()[0].Arg2.NextSyncAt) + assert.Equal(t, syncTime, permsSyncStore.CreateRepoSyncJobFunc.History()[0].Arg2.ProcessAfter) } diff --git a/internal/database/permission_sync_jobs.go b/internal/database/permission_sync_jobs.go index 64be6a5de044..b34467bdea7e 100644 --- a/internal/database/permission_sync_jobs.go +++ b/internal/database/permission_sync_jobs.go @@ -23,7 +23,7 @@ import ( type PermissionSyncJobOpts struct { HighPriority bool InvalidateCaches bool - NextSyncAt time.Time + ProcessAfter time.Time Reason string TriggeredByUserID int32 } @@ -78,8 +78,8 @@ func (s *permissionSyncJobStore) CreateUserSyncJob(ctx context.Context, user int Reason: opts.Reason, TriggeredByUserID: opts.TriggeredByUserID, } - if !opts.NextSyncAt.IsZero() { - job.ProcessAfter = opts.NextSyncAt + if !opts.ProcessAfter.IsZero() { + job.ProcessAfter = opts.ProcessAfter } return s.createSyncJob(ctx, job) } @@ -92,8 +92,8 @@ func (s *permissionSyncJobStore) CreateRepoSyncJob(ctx context.Context, repo api Reason: opts.Reason, TriggeredByUserID: opts.TriggeredByUserID, } - if !opts.NextSyncAt.IsZero() { - job.ProcessAfter = opts.NextSyncAt + if !opts.ProcessAfter.IsZero() { + job.ProcessAfter = opts.ProcessAfter } return s.createSyncJob(ctx, job) } diff --git a/internal/database/permission_sync_jobs_test.go b/internal/database/permission_sync_jobs_test.go index a74ac52a67b7..cecf11e719cb 100644 --- a/internal/database/permission_sync_jobs_test.go +++ b/internal/database/permission_sync_jobs_test.go @@ -44,8 +44,8 @@ func TestPermissionSyncJobs_CreateAndList(t *testing.T) { err = store.CreateRepoSyncJob(ctx, 99, opts) assert.NoError(t, err) - nextSyncAt := clock.Now().Add(5 * time.Minute) - opts = PermissionSyncJobOpts{HighPriority: false, InvalidateCaches: true, NextSyncAt: nextSyncAt, Reason: ReasonManualUserSync} + processAfter := clock.Now().Add(5 * time.Minute) + opts = PermissionSyncJobOpts{HighPriority: false, InvalidateCaches: true, ProcessAfter: processAfter, Reason: ReasonManualUserSync} err = store.CreateUserSyncJob(ctx, 77, opts) assert.NoError(t, err) @@ -69,7 +69,7 @@ func TestPermissionSyncJobs_CreateAndList(t *testing.T) { State: "queued", UserID: 77, InvalidateCaches: true, - ProcessAfter: nextSyncAt, + ProcessAfter: processAfter, Reason: ReasonManualUserSync, }, } @@ -171,8 +171,8 @@ func TestPermissionSyncJobs_Deduplication(t *testing.T) { // 4) Insert some low priority jobs with process_after for both users. All of them should be inserted. fiveMinutesLater := clock.Now().Add(5 * time.Minute) tenMinutesLater := clock.Now().Add(10 * time.Minute) - user1LowPrioDelayedJob := PermissionSyncJobOpts{NextSyncAt: fiveMinutesLater, Reason: ReasonManualUserSync, TriggeredByUserID: user1.ID} - user2LowPrioDelayedJob := PermissionSyncJobOpts{NextSyncAt: tenMinutesLater, Reason: ReasonManualUserSync, TriggeredByUserID: user1.ID} + user1LowPrioDelayedJob := PermissionSyncJobOpts{ProcessAfter: fiveMinutesLater, Reason: ReasonManualUserSync, TriggeredByUserID: user1.ID} + user2LowPrioDelayedJob := PermissionSyncJobOpts{ProcessAfter: tenMinutesLater, Reason: ReasonManualUserSync, TriggeredByUserID: user1.ID} err = store.CreateUserSyncJob(ctx, 1, user1LowPrioDelayedJob) assert.NoError(t, err) diff --git a/internal/repoupdater/protocol/repoupdater.go b/internal/repoupdater/protocol/repoupdater.go index d6ddfea96b03..9fb70cdc4f18 100644 --- a/internal/repoupdater/protocol/repoupdater.go +++ b/internal/repoupdater/protocol/repoupdater.go @@ -250,7 +250,7 @@ type PermsSyncRequest struct { Options authz.FetchPermsOptions `json:"options"` Reason string `json:"reason"` TriggeredByUserID int32 `json:"triggered_by_user_id"` - NextSyncAt time.Time `json:"next_sync_at"` + ProcessAfter time.Time `json:"process_after"` } // PermsSyncResponse is a response to sync permissions. From a96f3d3ae29a47d262fd0f5aa46bfe14354dc245 Mon Sep 17 00:00:00 2001 From: Alex Ostrikov Date: Fri, 20 Jan 2023 16:32:56 +0400 Subject: [PATCH 063/678] permissions-center: make tests fail-fast by changing `assert` to `require`. (#46698) Also make grammar consistent in test comments! Test plan: Tests should still pass. --- .../database/permission_sync_jobs_test.go | 148 +++++++++--------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/internal/database/permission_sync_jobs_test.go b/internal/database/permission_sync_jobs_test.go index cecf11e719cb..3cd71596c0f0 100644 --- a/internal/database/permission_sync_jobs_test.go +++ b/internal/database/permission_sync_jobs_test.go @@ -11,7 +11,7 @@ import ( "github.com/sourcegraph/sourcegraph/internal/database/dbtest" "github.com/sourcegraph/sourcegraph/internal/errcode" "github.com/sourcegraph/sourcegraph/internal/timeutil" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -31,28 +31,28 @@ func TestPermissionSyncJobs_CreateAndList(t *testing.T) { logger := logtest.Scoped(t) db := NewDB(logger, dbtest.NewDB(logger, t)) user, err := db.Users().Create(context.Background(), NewUser{Username: "horse"}) - assert.NoError(t, err) + require.NoError(t, err) ctx := context.Background() store := PermissionSyncJobsWith(logger, db) jobs, err := store.List(ctx, ListPermissionSyncJobOpts{}) - assert.NoError(t, err) - assert.Len(t, jobs, 0, "jobs returned even though database is empty") + require.NoError(t, err) + require.Len(t, jobs, 0, "jobs returned even though database is empty") opts := PermissionSyncJobOpts{HighPriority: true, InvalidateCaches: true, Reason: ReasonManualRepoSync, TriggeredByUserID: user.ID} err = store.CreateRepoSyncJob(ctx, 99, opts) - assert.NoError(t, err) + require.NoError(t, err) processAfter := clock.Now().Add(5 * time.Minute) opts = PermissionSyncJobOpts{HighPriority: false, InvalidateCaches: true, ProcessAfter: processAfter, Reason: ReasonManualUserSync} err = store.CreateUserSyncJob(ctx, 77, opts) - assert.NoError(t, err) + require.NoError(t, err) jobs, err = store.List(ctx, ListPermissionSyncJobOpts{}) - assert.NoError(t, err) + require.NoError(t, err) - assert.Len(t, jobs, 2, "wrong number of jobs returned") + require.Len(t, jobs, 2, "wrong number of jobs returned") wantJobs := []*PermissionSyncJob{ { @@ -77,7 +77,7 @@ func TestPermissionSyncJobs_CreateAndList(t *testing.T) { t.Fatalf("jobs[0] has wrong attributes: %s", diff) } for i, j := range jobs { - assert.NotZerof(t, j.QueuedAt, "job %d has no QueuedAt set", i) + require.NotZerof(t, j.QueuedAt, "job %d has no QueuedAt set", i) } listTests := []struct { @@ -105,7 +105,7 @@ func TestPermissionSyncJobs_CreateAndList(t *testing.T) { for _, tt := range listTests { t.Run(tt.name, func(t *testing.T) { have, err := store.List(ctx, tt.opts) - assert.NoError(t, err) + require.NoError(t, err) if len(have) != len(tt.wantJobs) { t.Fatalf("wrong number of jobs returned. want=%d, have=%d", len(tt.wantJobs), len(have)) } @@ -126,47 +126,47 @@ func TestPermissionSyncJobs_Deduplication(t *testing.T) { logger := logtest.Scoped(t) db := NewDB(logger, dbtest.NewDB(logger, t)) user1, err := db.Users().Create(context.Background(), NewUser{Username: "horse"}) - assert.NoError(t, err) + require.NoError(t, err) user2, err := db.Users().Create(context.Background(), NewUser{Username: "graph"}) - assert.NoError(t, err) + require.NoError(t, err) ctx := context.Background() store := PermissionSyncJobsWith(logger, db) - // 1) Insert low priority job without process_after for user1 + // 1) Insert low priority job without process_after for user1. user1LowPrioJob := PermissionSyncJobOpts{Reason: ReasonManualUserSync, TriggeredByUserID: user1.ID} err = store.CreateUserSyncJob(ctx, 1, user1LowPrioJob) - assert.NoError(t, err) + require.NoError(t, err) allJobs, err := store.List(ctx, ListPermissionSyncJobOpts{}) - assert.NoError(t, err) - // check that we have 1 job with userID=1 - assert.Len(t, allJobs, 1) - assert.Equal(t, 1, allJobs[0].UserID) + require.NoError(t, err) + // Check that we have 1 job with userID=1. + require.Len(t, allJobs, 1) + require.Equal(t, 1, allJobs[0].UserID) - // 2) Insert low priority job without process_after for user2 + // 2) Insert low priority job without process_after for user2. user2LowPrioJob := PermissionSyncJobOpts{Reason: ReasonManualUserSync, TriggeredByUserID: user2.ID} err = store.CreateUserSyncJob(ctx, 2, user2LowPrioJob) - assert.NoError(t, err) + require.NoError(t, err) allJobs, err = store.List(ctx, ListPermissionSyncJobOpts{}) - assert.NoError(t, err) - // check that we have 2 jobs including job for userID=2. job ID should match user ID - assert.Len(t, allJobs, 2) - assert.Equal(t, allJobs[0].ID, allJobs[0].UserID) - assert.Equal(t, allJobs[1].ID, allJobs[1].UserID) + require.NoError(t, err) + // Check that we have 2 jobs including job for userID=2. Job ID should match user ID. + require.Len(t, allJobs, 2) + require.Equal(t, allJobs[0].ID, allJobs[0].UserID) + require.Equal(t, allJobs[1].ID, allJobs[1].UserID) - // 3) Another low priority job without process_after for user1 is dropped + // 3) Another low priority job without process_after for user1 is dropped. err = store.CreateUserSyncJob(ctx, 1, user1LowPrioJob) - assert.NoError(t, err) + require.NoError(t, err) allJobs, err = store.List(ctx, ListPermissionSyncJobOpts{}) - assert.NoError(t, err) - // check that we still have 2 jobs. Job ID should match user ID. - assert.Len(t, allJobs, 2) - assert.Equal(t, allJobs[0].ID, allJobs[0].UserID) - assert.Equal(t, allJobs[1].ID, allJobs[1].UserID) + require.NoError(t, err) + // Check that we still have 2 jobs. Job ID should match user ID. + require.Len(t, allJobs, 2) + require.Equal(t, allJobs[0].ID, allJobs[0].UserID) + require.Equal(t, allJobs[1].ID, allJobs[1].UserID) // 4) Insert some low priority jobs with process_after for both users. All of them should be inserted. fiveMinutesLater := clock.Now().Add(5 * time.Minute) @@ -175,66 +175,66 @@ func TestPermissionSyncJobs_Deduplication(t *testing.T) { user2LowPrioDelayedJob := PermissionSyncJobOpts{ProcessAfter: tenMinutesLater, Reason: ReasonManualUserSync, TriggeredByUserID: user1.ID} err = store.CreateUserSyncJob(ctx, 1, user1LowPrioDelayedJob) - assert.NoError(t, err) + require.NoError(t, err) err = store.CreateUserSyncJob(ctx, 2, user2LowPrioDelayedJob) - assert.NoError(t, err) + require.NoError(t, err) allDelayedJobs, err := store.List(ctx, ListPermissionSyncJobOpts{NotNullProcessAfter: true}) - assert.NoError(t, err) - // check that we have 2 delayed jobs in total - assert.Len(t, allDelayedJobs, 2) - // userID of the job should be (jobID - 2) - assert.Equal(t, allDelayedJobs[0].UserID, allDelayedJobs[0].ID-2) - assert.Equal(t, allDelayedJobs[1].UserID, allDelayedJobs[1].ID-2) - - // 5) Insert *high* priority job without process_after for user1. Check that low priority job is canceled + require.NoError(t, err) + // Check that we have 2 delayed jobs in total. + require.Len(t, allDelayedJobs, 2) + // UserID of the job should be (jobID - 2). + require.Equal(t, allDelayedJobs[0].UserID, allDelayedJobs[0].ID-2) + require.Equal(t, allDelayedJobs[1].UserID, allDelayedJobs[1].ID-2) + + // 5) Insert *high* priority job without process_after for user1. Check that low priority job is canceled. user1HighPrioJob := PermissionSyncJobOpts{HighPriority: true, Reason: ReasonManualUserSync, TriggeredByUserID: user1.ID} err = store.CreateUserSyncJob(ctx, 1, user1HighPrioJob) - assert.NoError(t, err) + require.NoError(t, err) allUser1Jobs, err := store.List(ctx, ListPermissionSyncJobOpts{UserID: 1}) - assert.NoError(t, err) - // check that we have 3 jobs for userID=1 in total (low prio (canceled), delayed, high prio) - assert.Len(t, allUser1Jobs, 3) - // check that low prio job (ID=1) is canceled and others are not + require.NoError(t, err) + // Check that we have 3 jobs for userID=1 in total (low prio (canceled), delayed, high prio). + require.Len(t, allUser1Jobs, 3) + // Check that low prio job (ID=1) is canceled and others are not. for _, job := range allUser1Jobs { if job.ID == 1 { - assert.True(t, job.Cancel) + require.True(t, job.Cancel) } else { - assert.False(t, job.Cancel) + require.False(t, job.Cancel) } } // 6) Insert another low and high priority jobs without process_after for user1. // Check that all of them are dropped since we already have a high prio job. err = store.CreateUserSyncJob(ctx, 1, user1LowPrioJob) - assert.NoError(t, err) + require.NoError(t, err) err = store.CreateUserSyncJob(ctx, 1, user1HighPrioJob) - assert.NoError(t, err) + require.NoError(t, err) allUser1Jobs, err = store.List(ctx, ListPermissionSyncJobOpts{UserID: 1}) - assert.NoError(t, err) - // check that we still have 3 jobs for userID=1 in total (low prio (canceled), delayed, high prio) - assert.Len(t, allUser1Jobs, 3) + require.NoError(t, err) + // Check that we still have 3 jobs for userID=1 in total (low prio (canceled), delayed, high prio). + require.Len(t, allUser1Jobs, 3) // 7) Check that not "queued" jobs doesn't affect duplicates check: let's change high prio job to "processing" // and insert one low prio after that. result, err := db.ExecContext(ctx, "UPDATE permission_sync_jobs SET state='processing' WHERE id=5") - assert.NoError(t, err) + require.NoError(t, err) updatedRows, err := result.RowsAffected() - assert.NoError(t, err) - assert.Equal(t, int64(1), updatedRows) + require.NoError(t, err) + require.Equal(t, int64(1), updatedRows) // Now we're good to insert new low prio job. err = store.CreateUserSyncJob(ctx, 1, user1LowPrioJob) - assert.NoError(t, err) + require.NoError(t, err) allUser1Jobs, err = store.List(ctx, ListPermissionSyncJobOpts{UserID: 1}) - assert.NoError(t, err) - // check that we now have 4 jobs for userID=1 in total (low prio (canceled), delayed, high prio (processing), NEW low prio) - assert.Len(t, allUser1Jobs, 4) + require.NoError(t, err) + // Check that we now have 4 jobs for userID=1 in total (low prio (canceled), delayed, high prio (processing), NEW low prio). + require.Len(t, allUser1Jobs, 4) } func TestPermissionSyncJobs_CancelQueuedJob(t *testing.T) { @@ -248,29 +248,29 @@ func TestPermissionSyncJobs_CancelQueuedJob(t *testing.T) { store := PermissionSyncJobsWith(logger, db) - // Test that cancelling non-existent job errors out + // Test that cancelling non-existent job errors out. err := store.CancelQueuedJob(ctx, 1) - assert.True(t, errcode.IsNotFound(err)) + require.True(t, errcode.IsNotFound(err)) - // Adding a job + // Adding a job. err = store.CreateRepoSyncJob(ctx, 1, PermissionSyncJobOpts{Reason: ReasonManualUserSync}) - assert.NoError(t, err) + require.NoError(t, err) - // Cancelling a job should be successful now + // Cancelling a job should be successful now. err = store.CancelQueuedJob(ctx, 1) - assert.NoError(t, err) + require.NoError(t, err) - // Cancelling already cancelled job doesn't make sense and errors out as well + // Cancelling already cancelled job doesn't make sense and errors out as well. err = store.CancelQueuedJob(ctx, 1) - assert.True(t, errcode.IsNotFound(err)) + require.True(t, errcode.IsNotFound(err)) - // Adding another job and setting it to "processing" state + // Adding another job and setting it to "processing" state. err = store.CreateRepoSyncJob(ctx, 1, PermissionSyncJobOpts{Reason: ReasonManualUserSync}) - assert.NoError(t, err) + require.NoError(t, err) _, err = db.ExecContext(ctx, "UPDATE permission_sync_jobs SET state='processing' WHERE id=2") - assert.NoError(t, err) + require.NoError(t, err) - // Cancelling it errors out because it is in a state different from "queued" + // Cancelling it errors out because it is in a state different from "queued". err = store.CancelQueuedJob(ctx, 2) - assert.True(t, errcode.IsNotFound(err)) + require.True(t, errcode.IsNotFound(err)) } From e65cb6f328055c18aa520017aac0bb26f47757cc Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Fri, 20 Jan 2023 15:38:21 +0200 Subject: [PATCH 064/678] adminanalytics: remove unread boolean return in cache calls (#46705) The boolean was never read. Additionally it's value was always err != nil. Test Plan: go test --- internal/adminanalytics/cache.go | 18 +++++++----------- internal/adminanalytics/codeintelbylanguage.go | 2 +- .../adminanalytics/codeinteltoprepositories.go | 2 +- internal/adminanalytics/fetcher.go | 4 ++-- internal/adminanalytics/repos.go | 2 +- internal/adminanalytics/users.go | 4 ++-- internal/adminanalytics/utils.go | 2 +- 7 files changed, 15 insertions(+), 19 deletions(-) diff --git a/internal/adminanalytics/cache.go b/internal/adminanalytics/cache.go index 6fe9ff5cb77c..a95ac6fa615b 100644 --- a/internal/adminanalytics/cache.go +++ b/internal/adminanalytics/cache.go @@ -49,35 +49,31 @@ func getItemFromCache[T interface{}](cacheKey string) (*T, error) { return &summary, nil } -func setDataToCache(key string, data string, expireSeconds int) (bool, error) { +func setDataToCache(key string, data string, expireSeconds int) error { if cacheDisabledInTest { - return true, nil + return nil } if expireSeconds == 0 { expireSeconds = 24 * 60 * 60 // 1 day } - if err := store.SetEx(scopeKey+key, expireSeconds, data); err != nil { - return false, err - } - - return true, nil + return store.SetEx(scopeKey+key, expireSeconds, data) } -func setArrayToCache[T interface{}](cacheKey string, nodes []*T) (bool, error) { +func setArrayToCache[T interface{}](cacheKey string, nodes []*T) error { data, err := json.Marshal(nodes) if err != nil { - return false, err + return err } return setDataToCache(cacheKey, string(data), 0) } -func setItemToCache[T interface{}](cacheKey string, summary *T) (bool, error) { +func setItemToCache[T interface{}](cacheKey string, summary *T) error { data, err := json.Marshal(summary) if err != nil { - return false, err + return err } return setDataToCache(cacheKey, string(data), 0) diff --git a/internal/adminanalytics/codeintelbylanguage.go b/internal/adminanalytics/codeintelbylanguage.go index 4d1dc4c28469..82af06df60df 100644 --- a/internal/adminanalytics/codeintelbylanguage.go +++ b/internal/adminanalytics/codeintelbylanguage.go @@ -69,7 +69,7 @@ func GetCodeIntelByLanguage(ctx context.Context, db database.DB, cache bool, dat items = append(items, &item) } - if _, err := setArrayToCache(cacheKey, items); err != nil { + if err := setArrayToCache(cacheKey, items); err != nil { return nil, err } diff --git a/internal/adminanalytics/codeinteltoprepositories.go b/internal/adminanalytics/codeinteltoprepositories.go index c1d519414303..2c7bac3c835e 100644 --- a/internal/adminanalytics/codeinteltoprepositories.go +++ b/internal/adminanalytics/codeinteltoprepositories.go @@ -112,7 +112,7 @@ func GetCodeIntelTopRepositories(ctx context.Context, db database.DB, cache bool items = append(items, &item) } - if _, err := setArrayToCache(cacheKey, items); err != nil { + if err := setArrayToCache(cacheKey, items); err != nil { return nil, err } diff --git a/internal/adminanalytics/fetcher.go b/internal/adminanalytics/fetcher.go index 496ae216224c..c3e761ff04fa 100644 --- a/internal/adminanalytics/fetcher.go +++ b/internal/adminanalytics/fetcher.go @@ -106,7 +106,7 @@ func (f *AnalyticsFetcher) Nodes(ctx context.Context) ([]*AnalyticsNode, error) allNodes = append(allNodes, node) } - if _, err := setArrayToCache(cacheKey, allNodes); err != nil { + if err := setArrayToCache(cacheKey, allNodes); err != nil { return nil, err } @@ -151,7 +151,7 @@ func (f *AnalyticsFetcher) Summary(ctx context.Context) (*AnalyticsSummary, erro summary := &AnalyticsSummary{data} - if _, err := setItemToCache(cacheKey, summary); err != nil { + if err := setItemToCache(cacheKey, summary); err != nil { return nil, err } diff --git a/internal/adminanalytics/repos.go b/internal/adminanalytics/repos.go index e22ec697095d..3925e72ae61e 100644 --- a/internal/adminanalytics/repos.go +++ b/internal/adminanalytics/repos.go @@ -37,7 +37,7 @@ func (r *Repos) Summary(ctx context.Context) (*ReposSummary, error) { summary := &ReposSummary{data} - if _, err := setItemToCache(cacheKey, summary); err != nil { + if err := setItemToCache(cacheKey, summary); err != nil { return nil, err } diff --git a/internal/adminanalytics/users.go b/internal/adminanalytics/users.go index f628c86a008c..9e5422d9044a 100644 --- a/internal/adminanalytics/users.go +++ b/internal/adminanalytics/users.go @@ -113,7 +113,7 @@ func (f *Users) Frequencies(ctx context.Context) ([]*UsersFrequencyNode, error) nodes = append(nodes, &UsersFrequencyNode{data}) } - if _, err := setArrayToCache(cacheKey, nodes); err != nil { + if err := setArrayToCache(cacheKey, nodes); err != nil { return nil, err } @@ -187,7 +187,7 @@ func (f *Users) MonthlyActiveUsers(ctx context.Context) ([]*MonthlyActiveUsersRo nodes = append(nodes, &MonthlyActiveUsersRow{data}) } - if _, err := setArrayToCache(cacheKey, nodes); err != nil { + if err := setArrayToCache(cacheKey, nodes); err != nil { return nil, err } diff --git a/internal/adminanalytics/utils.go b/internal/adminanalytics/utils.go index db3fcda5a30c..af2a08d2908d 100644 --- a/internal/adminanalytics/utils.go +++ b/internal/adminanalytics/utils.go @@ -100,7 +100,7 @@ func getSgEmpUserIDs(ctx context.Context, db database.DB, cache bool) ([]*int32, return ids, err } - if _, err := setDataToCache(employeeUserIdsCacheKey, string(cacheData), employeeUserIdsCacheExpirySeconds); err != nil { + if err := setDataToCache(employeeUserIdsCacheKey, string(cacheData), employeeUserIdsCacheExpirySeconds); err != nil { return ids, err } From 685b6268cb9165ab8762b5aed94bf7e44d0a02ea Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Fri, 20 Jan 2023 16:01:00 +0200 Subject: [PATCH 065/678] redispool: correctly call and test SetEx (#46711) Look at that delicious comment about intentionally not testing expire. Test Plan: go test --- internal/redispool/keyvalue.go | 2 +- internal/redispool/keyvalue_test.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/redispool/keyvalue.go b/internal/redispool/keyvalue.go index cc3845d97cad..019f0fbb6fb8 100644 --- a/internal/redispool/keyvalue.go +++ b/internal/redispool/keyvalue.go @@ -102,7 +102,7 @@ func (r *redisKeyValue) Set(key string, val any) error { } func (r *redisKeyValue) SetEx(key string, ttlSeconds int, val any) error { - return r.do("SET", r.prefix+key, ttlSeconds, val).err + return r.do("SETEX", r.prefix+key, ttlSeconds, val).err } func (r *redisKeyValue) Del(key string) error { diff --git a/internal/redispool/keyvalue_test.go b/internal/redispool/keyvalue_test.go index 6188ff81b9ef..ca633fbc9c25 100644 --- a/internal/redispool/keyvalue_test.go +++ b/internal/redispool/keyvalue_test.go @@ -170,7 +170,12 @@ func testKeyValue(t *testing.T, kv redispool.KeyValue) { assertEqual(kv.LRange("list", 0, 4), bytes("1", "2", "3")) assertListLen("list", 3) - // We intentionally do not test EXPIRE since I don't like sleeps in tests. + // SetEx + assertWorks(kv.SetEx("expires", 60, "1")) + assertEqual(kv.Get("expires"), "1") + assertWorks(kv.SetEx("expires", 1, "2")) + time.Sleep(1100 * time.Millisecond) + assertEqual(kv.Get("expires"), nil) } // Mostly copy-pasta from rache. Will clean up later as the relationship From d83e67512402304cb879412ebd1eb89d5ea40758 Mon Sep 17 00:00:00 2001 From: Bolaji Olajide <25608335+BolajiOlajide@users.noreply.github.com> Date: Fri, 20 Jan 2023 15:02:58 +0100 Subject: [PATCH 066/678] update permission notify list (#46712) --- internal/database/CODENOTIFY | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/database/CODENOTIFY b/internal/database/CODENOTIFY index a93d0d9e9de9..102da6b30209 100644 --- a/internal/database/CODENOTIFY +++ b/internal/database/CODENOTIFY @@ -3,6 +3,7 @@ external* @eseliger namespaces* @eseliger repos* @eseliger @unknwon -permission* @BolajiOlajide -user_role* @BolajiOlajide -role* @BolajiOlajide +permissions* @BolajiOlajide +user_roles* @BolajiOlajide +roles* @BolajiOlajide +role_permissions* @BolajiOlajide From 56a19bc209a6edc9b1d345b6f83ad4e81c5846c8 Mon Sep 17 00:00:00 2001 From: Vova Kulikov Date: Fri, 20 Jan 2023 12:45:35 -0300 Subject: [PATCH 067/678] Code Insights: Add num samples display option (#46653) * Add numSamples to core types * Add num samples field to the drilldown UI * Align all insights view query * Fix creation UI pages types * Fix integration tests * Fix storybook stories and unit tests mocks * Create insight with numSamples null by default * Fix wording for the display options and its layout for small filters panel size * Add note to about new settings in CHANGELOG.md * Fix CHANGELOG.md * Hide num samples settings for compute powered insight * Fix drill down story * default to max possible value --- CHANGELOG.md | 5 +- .../SmartInsightsViewGrid.story.tsx | 19 +--- .../backend-insight/BackendInsight.story.tsx | 36 +++---- .../backend-insight/BackendInsight.tsx | 20 ++-- .../DrillDownFilters.story.tsx | 4 +- .../DrillDownInsightFilters.story.tsx | 8 +- .../DrillDownInsightFilters.tsx | 27 ++++- .../drill-down-filters/utils.test.ts | 31 ------ .../drill-down-filters/utils.ts | 81 +++------------ .../drill-down-filters-panel/index.ts | 2 - .../DrillDownFiltersPopover.tsx | 3 + .../backend-insight/components/index.ts | 1 - .../SortFilterSeriesPanel.module.scss | 8 +- .../SortFilterSeriesPanel.story.tsx | 5 +- .../SortFilterSeriesPanel.tsx | 75 ++++++++++---- .../web/src/enterprise/insights/constants.ts | 1 + .../deserialization/create-insight-view.ts | 98 ++----------------- .../deserialization/field-parsers.ts | 82 ++++++++++++++++ .../backend/gql-backend/gql/GetInsights.ts | 10 +- .../methods/create-insight/serializators.ts | 48 +++++---- .../methods/update-insight/serializators.ts | 48 +++++---- .../hooks/use-save-insight-as-new-view.ts | 4 +- .../insights/core/types/insight/common.ts | 68 ++++--------- .../insights/core/types/insight/index.ts | 11 +-- .../insight/types/capture-group-insight.ts | 7 +- .../types/insight/types/compute-insight.ts | 3 +- .../types/insight/types/lang-stat-insight.ts | 3 +- .../types/insight/types/search-insight.ts | 6 +- .../insights/pages/all-insights-view/query.ts | 10 +- .../utils/capture-group-insight-sanitizer.ts | 7 +- .../compute/utils/insight-sanitaizer.ts | 6 +- .../lang-stats/utils/insight-sanitizer.ts | 4 +- .../search-insight/utils/insight-sanitizer.ts | 10 +- .../components/EditCaptureGroupInsight.tsx | 7 +- .../components/EditSearchInsight.tsx | 1 - .../StandaloneBackendInsight.tsx | 8 +- .../insights/create-insights.test.ts | 10 +- .../insights/drill-down-filters.test.ts | 4 +- .../insights/edit-search-insights.test.ts | 1 + .../insights/fixtures/dashboards.ts | 44 ++------- .../insights/fixtures/insights-metadata.ts | 10 +- .../insights/single-insight-page.test.ts | 1 + .../resolvers/insight_view_resolvers.go | 2 +- 43 files changed, 348 insertions(+), 491 deletions(-) delete mode 100644 client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/drill-down-filters/utils.test.ts create mode 100644 client/web/src/enterprise/insights/core/backend/gql-backend/deserialization/field-parsers.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c8c24ba3dbe..d457ec65bea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ All notable changes to Sourcegraph are documented in this file. ### Added -- - The default author and email for changesets will now be pulled from user account details when possible. [#46385](https://github.com/sourcegraph/sourcegraph/pull/46385) +- The default author and email for changesets will now be pulled from user account details when possible. [#46385](https://github.com/sourcegraph/sourcegraph/pull/46385) +- Code Insights has a new display option: "Max number of series points to display". This setting controls the number of data points you see per series on an insight. [#46653](https://github.com/sourcegraph/sourcegraph/pull/46653) ### Changed @@ -25,7 +26,7 @@ All notable changes to Sourcegraph are documented in this file. ### Fixed -- +- Fixed a bug where saving default Sort & Limit filters in Code Insights did not persist [#46653](https://github.com/sourcegraph/sourcegraph/pull/46653) ### Removed diff --git a/client/web/src/enterprise/insights/components/insights-view-grid/SmartInsightsViewGrid.story.tsx b/client/web/src/enterprise/insights/components/insights-view-grid/SmartInsightsViewGrid.story.tsx index 789ca4a22981..f663bcffda56 100644 --- a/client/web/src/enterprise/insights/components/insights-view-grid/SmartInsightsViewGrid.story.tsx +++ b/client/web/src/enterprise/insights/components/insights-view-grid/SmartInsightsViewGrid.story.tsx @@ -11,7 +11,7 @@ import { SeriesSortDirection, SeriesSortMode, } from '../../../../graphql-operations' -import { InsightExecutionType, InsightType, BackendInsight } from '../../core' +import { InsightType, BackendInsight } from '../../core' import { GET_INSIGHT_VIEW_GQL } from '../../core/backend/gql-backend' import { SmartInsightsViewGrid } from './SmartInsightsViewGrid' @@ -34,7 +34,8 @@ const DEFAULT_FILTERS = { includeRepoRegexp: '', context: '', seriesDisplayOptions: { - limit: '20', + limit: 20, + numSamples: null, sortOptions: { direction: SeriesSortDirection.DESC, mode: SeriesSortMode.RESULT_COUNT, @@ -45,7 +46,6 @@ const DEFAULT_FILTERS = { const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ { id: 'searchInsights.insight.Backend_1', - executionType: InsightExecutionType.Backend, repositories: [], type: InsightType.SearchBased, title: 'Backend insight #1', @@ -54,12 +54,10 @@ const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ filters: DEFAULT_FILTERS, dashboardReferenceCount: 0, isFrozen: false, - seriesDisplayOptions: {}, dashboards: [], }, { id: 'searchInsights.insight.Backend_2', - executionType: InsightExecutionType.Backend, repositories: [], type: InsightType.SearchBased, title: 'Backend insight #2', @@ -71,12 +69,10 @@ const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ filters: DEFAULT_FILTERS, dashboardReferenceCount: 0, isFrozen: false, - seriesDisplayOptions: {}, dashboards: [], }, { id: 'searchInsights.insight.Backend_3', - executionType: InsightExecutionType.Backend, repositories: [], type: InsightType.SearchBased, title: 'Backend insight #3', @@ -89,12 +85,10 @@ const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ filters: DEFAULT_FILTERS, dashboardReferenceCount: 0, isFrozen: false, - seriesDisplayOptions: {}, dashboards: [], }, { id: 'searchInsights.insight.Backend_4', - executionType: InsightExecutionType.Backend, repositories: [], type: InsightType.SearchBased, title: 'Backend insight #4', @@ -103,12 +97,10 @@ const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ filters: DEFAULT_FILTERS, dashboardReferenceCount: 0, isFrozen: false, - seriesDisplayOptions: {}, dashboards: [], }, { id: 'searchInsights.insight.Backend_5', - executionType: InsightExecutionType.Backend, repositories: [], type: InsightType.CaptureGroup, title: 'Backend insight #5', @@ -117,12 +109,10 @@ const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ filters: DEFAULT_FILTERS, dashboardReferenceCount: 0, isFrozen: false, - seriesDisplayOptions: {}, dashboards: [], }, { id: 'searchInsights.insight.Backend_6', - executionType: InsightExecutionType.Backend, repositories: [], type: InsightType.SearchBased, title: 'Backend insight #6', @@ -131,12 +121,10 @@ const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ filters: DEFAULT_FILTERS, dashboardReferenceCount: 0, isFrozen: false, - seriesDisplayOptions: {}, dashboards: [], }, { id: 'searchInsights.insight.Backend_7', - executionType: InsightExecutionType.Backend, repositories: [], type: InsightType.SearchBased, title: 'Backend insight #7', @@ -145,7 +133,6 @@ const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ filters: DEFAULT_FILTERS, dashboardReferenceCount: 0, isFrozen: false, - seriesDisplayOptions: {}, dashboards: [], }, ] diff --git a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/BackendInsight.story.tsx b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/BackendInsight.story.tsx index a0de326e53a2..5c0f49151f5d 100644 --- a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/BackendInsight.story.tsx +++ b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/BackendInsight.story.tsx @@ -9,13 +9,7 @@ import { H2 } from '@sourcegraph/wildcard' import { WebStory } from '../../../../../../components/WebStory' import { GetInsightViewResult, SeriesSortDirection, SeriesSortMode } from '../../../../../../graphql-operations' -import { - SeriesChartContent, - SearchBasedInsight, - CaptureGroupInsight, - InsightExecutionType, - InsightType, -} from '../../../../core' +import { SeriesChartContent, SearchBasedInsight, CaptureGroupInsight, InsightType } from '../../../../core' import { GET_INSIGHT_VIEW_GQL } from '../../../../core/backend/gql-backend' import { InsightInProcessError } from '../../../../core/backend/utils/errors' @@ -42,14 +36,14 @@ const INSIGHT_CONFIGURATION_MOCK: SearchBasedInsight = { { id: 'series_002', query: '', name: 'B metric', stroke: 'var(--warning)' }, ], type: InsightType.SearchBased, - executionType: InsightExecutionType.Backend, step: { weeks: 2 }, filters: { excludeRepoRegexp: '', includeRepoRegexp: '', context: '', seriesDisplayOptions: { - limit: '20', + numSamples: 12, + limit: 20, sortOptions: { direction: SeriesSortDirection.DESC, mode: SeriesSortMode.RESULT_COUNT, @@ -58,13 +52,6 @@ const INSIGHT_CONFIGURATION_MOCK: SearchBasedInsight = { }, dashboardReferenceCount: 0, isFrozen: false, - seriesDisplayOptions: { - limit: 20, - sortOptions: { - direction: SeriesSortDirection.DESC, - mode: SeriesSortMode.RESULT_COUNT, - }, - }, dashboards: [], } @@ -175,6 +162,7 @@ const mockInsightAPIResponse = ({ filters: { includeRepoRegex: '', excludeRepoRegex: '', searchContexts: [''] }, seriesDisplayOptions: { limit: 20, + numSamples: 12, sortOptions: { direction: SeriesSortDirection.DESC, mode: SeriesSortMode.RESULT_COUNT, @@ -196,6 +184,7 @@ const mockInsightAPIResponse = ({ filters: { includeRepoRegex: '', excludeRepoRegex: '', searchContexts: [''] }, seriesDisplayOptions: { limit: 20, + numSamples: 12, sortOptions: { direction: SeriesSortDirection.DESC, mode: SeriesSortMode.RESULT_COUNT, @@ -253,7 +242,6 @@ const TestBackendInsight: React.FunctionComponent = filters: { includeRepoRegex: '', excludeRepoRegex: '', searchContexts: [''] }, seriesDisplayOptions: { limit: 20, + numSamples: 12, sortOptions: { direction: SeriesSortDirection.DESC, mode: SeriesSortMode.RESULT_COUNT, @@ -946,6 +937,7 @@ const BACKEND_INSIGHT_TERRAFORM_AWS_VERSIONS_MOCK: MockedResponse(( searchContexts: [debouncedFilters.context], }, seriesDisplayOptions: { - limit: parseSeriesLimit(debouncedFilters.seriesDisplayOptions.limit), + numSamples: debouncedFilters.seriesDisplayOptions.numSamples, + limit: debouncedFilters.seriesDisplayOptions.limit, sortOptions: debouncedFilters.seriesDisplayOptions.sortOptions, }, }, @@ -114,11 +111,7 @@ export const BackendInsightView = forwardRef(( } async function handleFilterSave(filters: InsightFilters): Promise { - const seriesDisplayOptions: SeriesDisplayOptionsInput = { - limit: parseSeriesLimit(filters.seriesDisplayOptions.limit), - sortOptions: filters.seriesDisplayOptions.sortOptions, - } - const insightWithNewFilters = { ...insight, filters, seriesDisplayOptions } + const insightWithNewFilters = { ...insight, filters } await updateInsight({ insightId: insight.id, nextInsightData: insightWithNewFilters }).toPromise() @@ -192,6 +185,9 @@ export const BackendInsightView = forwardRef(( anchor={cardElementRef} initialFiltersValue={filters} originalFiltersValue={originalInsightFilters} + // It doesn't make sense to have max series per point for compute insights + // because there is always only one point per series + isNumSamplesFilterAvailable={!isComputeInsight(insight)} onFilterChange={setFilters} onFilterSave={handleFilterSave} onInsightCreate={handleInsightFilterCreation} diff --git a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/DrillDownFilters.story.tsx b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/DrillDownFilters.story.tsx index 6ca9db59357f..26d2874bec83 100644 --- a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/DrillDownFilters.story.tsx +++ b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/DrillDownFilters.story.tsx @@ -21,7 +21,8 @@ export const DrillDownPopover: Story = () => { includeRepoRegexp: '', context: '', seriesDisplayOptions: { - limit: '20', + limit: 20, + numSamples: null, sortOptions: { direction: SeriesSortDirection.DESC, mode: SeriesSortMode.RESULT_COUNT, @@ -35,6 +36,7 @@ export const DrillDownPopover: Story = () => { anchor={exampleReference} initialFiltersValue={initialFiltersValue} originalFiltersValue={initialFiltersValue} + isNumSamplesFilterAvailable={true} onFilterChange={log('onFilterChange')} onFilterSave={log('onFilterSave')} onInsightCreate={log('onInsightCreate')} diff --git a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/drill-down-filters/DrillDownInsightFilters.story.tsx b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/drill-down-filters/DrillDownInsightFilters.story.tsx index c9574410e0a7..4de48cab0396 100644 --- a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/drill-down-filters/DrillDownInsightFilters.story.tsx +++ b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/drill-down-filters/DrillDownInsightFilters.story.tsx @@ -87,7 +87,8 @@ const ORIGINAL_FILTERS: InsightFilters = { excludeRepoRegexp: '', context: '', seriesDisplayOptions: { - limit: '20', + limit: 20, + numSamples: null, sortOptions: { direction: SeriesSortDirection.DESC, mode: SeriesSortMode.RESULT_COUNT, @@ -100,7 +101,8 @@ const FILTERS: InsightFilters = { excludeRepoRegexp: 'hello world loooong loooooooooooooong repo filter regular expressssssion', context: '', seriesDisplayOptions: { - limit: '20', + limit: 20, + numSamples: null, sortOptions: { direction: SeriesSortDirection.DESC, mode: SeriesSortMode.RESULT_COUNT, @@ -113,6 +115,7 @@ export const DrillDownFiltersShowcase: Story = () => ( { originalValues, className, visualMode, + isNumSamplesFilterAvailable, onFiltersChange, onFilterSave, onCreateInsightRequest, @@ -145,6 +153,7 @@ export const DrillDownInsightFilters: FunctionComponent + + + ))} + + ) +} + +function useMutableRefValue(value: T): MutableRefObject { + const valueRef = useRef(value) + valueRef.current = value + + return valueRef +} + +const REPOSITORIES_COUNT_GQL = gql` + query InsightRepositoriesCount($query: String!) { + previewRepositoriesFromQuery(query: $query) { + numberOfRepositories + } + } +` + +interface RepositoriesCountProps { + repoQuery: useFieldAPI + className?: string +} + +function RepositoriesCount(props: RepositoriesCountProps): ReactElement { + const { repoQuery, className } = props + + const query = useDebounce(!repoQuery.input.disabled ? repoQuery.input.value.query : '', 500) + + const { data } = useQuery( + REPOSITORIES_COUNT_GQL, + { skip: repoQuery.input.disabled, variables: { query } } + ) + + const repositoriesNumber = !repoQuery.input.disabled + ? data?.previewRepositoriesFromQuery.numberOfRepositories ?? 0 + : 0 + + return ( + + Repositories count: {repositoriesNumber} + + ) +} diff --git a/client/web/src/enterprise/insights/components/creation-ui/insight-repo-section/use-repo-fields.ts b/client/web/src/enterprise/insights/components/creation-ui/insight-repo-section/use-repo-fields.ts new file mode 100644 index 000000000000..ee4e9f5e5c6a --- /dev/null +++ b/client/web/src/enterprise/insights/components/creation-ui/insight-repo-section/use-repo-fields.ts @@ -0,0 +1,126 @@ +import { useMemo } from 'react' + +import { ApolloClient, gql, useApolloClient } from '@apollo/client' + +import { QueryState } from '@sourcegraph/shared/src/search' + +import { ValidateInsightRepoQueryResult, ValidateInsightRepoQueryVariables } from '../../../../../graphql-operations' +import { useExperimentalFeatures } from '../../../../../stores' +import { RepoMode } from '../../../pages/insights/creation/search-insight/types' +import { AsyncValidator, useField, useFieldAPI, ValidationResult } from '../../form' +import { FormAPI } from '../../form/hooks/useForm' +import { insightRepositoriesAsyncValidator, insightRepositoriesValidator } from '../validators/validators' + +interface RepositoriesFields { + /** + * [Experimental] Repositories UI can work in different modes when we have + * two repo UI fields version of the creation UI. This field controls the + * current mode + */ + repoMode: RepoMode + + /** + * Search-powered query, this is used to gather different repositories though + * search API instead of having strict list of repo URLs. + */ + repoQuery: QueryState + + /** Repositories which to be used to get the info for code insights */ + repositories: string +} + +interface Input { + formApi: FormAPI +} + +interface Fields { + repoMode: useFieldAPI + repoQuery: useFieldAPI + repositories: useFieldAPI +} + +export function useRepoFields(props: Input): Fields { + const { formApi } = props + + const apolloClient = useApolloClient() + const repoFieldVariation = useExperimentalFeatures(features => features.codeInsightsRepoUI) + + const isSingleSearchQueryRepo = repoFieldVariation === 'single-search-query' + const isSearchQueryORUrlsList = repoFieldVariation === 'search-query-or-strict-list' + + const repoMode = useField({ + formApi, + name: 'repoMode', + }) + + const isSearchQueryMode = repoMode.meta.value === 'search-query' + const isURLsListMode = repoMode.meta.value === 'urls-list' + + // Search query field is required only if it's only one option for the filling in + // repositories info (in case of "single-search-query" UI variation) or when + // we are in the "search-query" repo mode (in case of "search-query-or-strict-list" UI variation) + const isRepoQueryRequired = isSingleSearchQueryRepo || isSearchQueryMode + + // Repo urls list field is required only if we are in the "search-query-or-strict-list" UI variation, + // and we picked urls-list repo mode in the UI. In all other cases this field nighter rendered nor + // required + const isRepoURLsListRequired = isSearchQueryORUrlsList && isURLsListMode + + const validateRepoQuerySyntax = useMemo(() => createValidateRepoQuerySyntax(apolloClient), [apolloClient]) + + const repoQuery = useField({ + formApi, + name: 'repoQuery', + disabled: !isSearchQueryMode, + validators: { + sync: isRepoQueryRequired ? validateRepoQuery : undefined, + async: isRepoQueryRequired ? validateRepoQuerySyntax : undefined, + }, + }) + + const repositories = useField({ + formApi, + name: 'repositories', + disabled: !isURLsListMode, + required: isRepoURLsListRequired, + validators: { + // Turn off any validations for the repositories' field in we are in all repos mode + sync: isRepoURLsListRequired ? insightRepositoriesValidator : undefined, + async: isRepoURLsListRequired ? insightRepositoriesAsyncValidator : undefined, + }, + }) + + return { repoMode, repoQuery, repositories } +} + +function validateRepoQuery(value?: QueryState): ValidationResult { + if (value && value.query.trim() === '') { + return 'Search repositories query is a required filed, please fill in the field.' + } +} + +const VALIDATE_REPO_QUERY_GQL = gql` + query ValidateInsightRepoQuery($query: String!) { + validateScopedInsightQuery(query: $query) { + isValid + invalidReason + } + } +` + +function createValidateRepoQuerySyntax(apolloClient: ApolloClient): AsyncValidator { + return async (value?: QueryState): Promise> => { + if (!value) { + return + } + + const { data } = await apolloClient.query({ + query: VALIDATE_REPO_QUERY_GQL, + variables: { query: value.query }, + }) + + if (data.validateScopedInsightQuery.invalidReason) { + return data.validateScopedInsightQuery.invalidReason + } + } +} diff --git a/client/web/src/enterprise/insights/components/creation-ui/sanitizers/index.ts b/client/web/src/enterprise/insights/components/creation-ui/sanitizers/index.ts index c84bdaaedf91..f4fa28a0083a 100644 --- a/client/web/src/enterprise/insights/components/creation-ui/sanitizers/index.ts +++ b/client/web/src/enterprise/insights/components/creation-ui/sanitizers/index.ts @@ -1,2 +1,2 @@ -export { getSanitizedRepositories } from './repositories' +export { getSanitizedRepositories, getSanitizedRepositoryScope } from './repositories' export { getSanitizedSeries } from './series' diff --git a/client/web/src/enterprise/insights/components/creation-ui/sanitizers/repositories.ts b/client/web/src/enterprise/insights/components/creation-ui/sanitizers/repositories.ts index 98b95225f0ba..20af1ffec657 100644 --- a/client/web/src/enterprise/insights/components/creation-ui/sanitizers/repositories.ts +++ b/client/web/src/enterprise/insights/components/creation-ui/sanitizers/repositories.ts @@ -1,3 +1,5 @@ +import { RepositoryScopeInput } from '@sourcegraph/shared/src/graphql-operations' + /** * Returns parsed by string repositories list. * @@ -9,3 +11,19 @@ export function getSanitizedRepositories(rawRepositories: string): string[] { .split(/\s*,\s*/) .filter(repo => repo) } + +/** + * Returns parsed by string repositories list. + * + * @param rawRepositories - string with repositories split by commas + */ +export function getSanitizedRepositoryScope( + rawRepositories: string, + repoQuery: string | undefined, + repoMode: string +): RepositoryScopeInput { + return { + repositories: repoMode === 'urls-list' ? getSanitizedRepositories(rawRepositories) : [], + repositoryCriteria: repoMode === 'search-query' ? repoQuery : null, + } +} diff --git a/client/web/src/enterprise/insights/components/creation-ui/validators/validators.ts b/client/web/src/enterprise/insights/components/creation-ui/validators/validators.ts index 7515fde9cfbc..b8c0983da4db 100644 --- a/client/web/src/enterprise/insights/components/creation-ui/validators/validators.ts +++ b/client/web/src/enterprise/insights/components/creation-ui/validators/validators.ts @@ -21,7 +21,7 @@ export const insightTitleValidator = createRequiredValidator('Title is a require * we have a creation UI insight sandbox demo widget. */ export const insightRepositoriesValidator: Validator = value => { - if (value !== undefined && dedupeWhitespace(value).trim() === '') { + if (value !== undefined && dedupeWhitespace(value) === '') { return 'Repositories is a required field.' } diff --git a/client/web/src/enterprise/insights/components/form/getDefaultInputProps.ts b/client/web/src/enterprise/insights/components/form/getDefaultInputProps.ts index b7ccd0387893..ce83fca461fb 100644 --- a/client/web/src/enterprise/insights/components/form/getDefaultInputProps.ts +++ b/client/web/src/enterprise/insights/components/form/getDefaultInputProps.ts @@ -2,8 +2,9 @@ import { InputStatus, InputProps } from '@sourcegraph/wildcard' import { useFieldAPI } from './hooks' -function getDefaultInputStatus({ meta }: useFieldAPI): InputStatus { - const isValidated = meta.initialValue || meta.touched +export function getDefaultInputStatus({ meta }: useFieldAPI, getValue?: (value: T) => unknown): InputStatus { + const initialValue = getValue ? getValue(meta.initialValue) : meta.initialValue + const isValidated = initialValue || meta.touched if (meta.validState === 'CHECKING') { return InputStatus.loading @@ -20,7 +21,7 @@ function getDefaultInputStatus({ meta }: useFieldAPI): InputStatus { return InputStatus.initial } -function getDefaultInputError({ meta }: useFieldAPI): InputProps['error'] { +export function getDefaultInputError({ meta }: useFieldAPI): InputProps['error'] { return (meta.touched && meta.error) || undefined } diff --git a/client/web/src/enterprise/insights/components/form/hooks/useField.ts b/client/web/src/enterprise/insights/components/form/hooks/useField.ts index 88585f1fb867..f0e38abe20fb 100644 --- a/client/web/src/enterprise/insights/components/form/hooks/useField.ts +++ b/client/web/src/enterprise/insights/components/form/hooks/useField.ts @@ -71,7 +71,7 @@ export type UseFieldProps = { export function useField( props: UseFieldProps ): useFieldAPI { - const { formApi, name, validators, onChange = noop, ...inputProps } = props + const { formApi, name, validators, onChange = noop, disabled, ...inputProps } = props const { submitted, touched: formTouched } = formApi const { sync: syncValidator, async: asyncValidator } = validators ?? {} @@ -99,7 +99,7 @@ export function useField // If we got error from native attr validation (required, pattern, type) // we still run validator in order to get some custom error message for - // standard validation error if validator doesn't provide message we fallback + // standard validation error if validator doesn't provide message we fall back // on standard validationMessage string [1] (ex. Please fill in input.) const nativeErrorMessage = inputElement?.validationMessage ?? '' const customValidationResult = syncValidator ? syncValidator(state.value, validity) : undefined @@ -143,6 +143,17 @@ export function useField })) } + // Hide any validation errors by default if the field is in the disabled + // state. + if (disabled && !syncValidator && !asyncValidator) { + return setState(state => ({ + ...state, + validState: 'NOT_VALIDATED', + error: '', + validity, + })) + } + return setState(state => ({ ...state, validState: 'VALID' as const, @@ -150,7 +161,7 @@ export function useField errorContext: customValidationContext, validity, })) - }, [state.value, syncValidator, startAsyncValidation, asyncValidator, cancelAsyncValidation, setState]) + }, [state.value, syncValidator, startAsyncValidation, asyncValidator, cancelAsyncValidation, setState, disabled]) const handleBlur = useCallback(() => setState(state => ({ ...state, touched: true })), [setState]) const handleChange = useCallback( @@ -170,6 +181,7 @@ export function useField value: state.value, onBlur: handleBlur, onChange: handleChange, + disabled, ...inputProps, }, meta: { diff --git a/client/web/src/enterprise/insights/components/form/hooks/useForm.ts b/client/web/src/enterprise/insights/components/form/hooks/useForm.ts index 666a4896a527..4e99862ba59e 100644 --- a/client/web/src/enterprise/insights/components/form/hooks/useForm.ts +++ b/client/web/src/enterprise/insights/components/form/hooks/useForm.ts @@ -321,7 +321,7 @@ export function useForm(props: UseFormProps(':invalid:not(fieldset)')?.focus() + formElement.querySelector(':invalid:not(fieldset) [aria-invalid="true"]')?.focus() } }, } diff --git a/client/web/src/enterprise/insights/components/form/index.ts b/client/web/src/enterprise/insights/components/form/index.ts index e7bb554ec753..ede5bf2df7e3 100644 --- a/client/web/src/enterprise/insights/components/form/index.ts +++ b/client/web/src/enterprise/insights/components/form/index.ts @@ -1,5 +1,5 @@ // helpers for form-field's setup -export { getDefaultInputProps } from './getDefaultInputProps' +export { getDefaultInputProps, getDefaultInputError, getDefaultInputStatus } from './getDefaultInputProps' // form components export { RepositoryField } from './repositories-field/RepositoryField' @@ -7,5 +7,6 @@ export { RepositoriesField } from './repositories-field/RepositoriesField' export { InsightQueryInput } from './query-input/InsightQueryInput' export { FormRadioInput } from './form-radio-input/FormRadioInput' export { FormGroup } from './form-group/FormGroup' +export { MonacoField } from './monaco-field' export * from './hooks' diff --git a/client/web/src/enterprise/insights/components/form/monaco-field/MonacoField.module.scss b/client/web/src/enterprise/insights/components/form/monaco-field/MonacoField.module.scss index 4e4dcdd78256..2067095371fe 100644 --- a/client/web/src/enterprise/insights/components/form/monaco-field/MonacoField.module.scss +++ b/client/web/src/enterprise/insights/components/form/monaco-field/MonacoField.module.scss @@ -2,17 +2,23 @@ display: flex; min-width: 0; position: relative; - padding: 0.375rem 0.75rem !important; + padding: 0.5rem 0.75rem !important; background-image: none !important; .monaco-field { - background-position: right 0.75rem top 0.175rem !important; + background-position: right 0.75rem top 0.1rem !important; } } .focus-container { height: auto; + // Spread standard input paddings in order to fix visually problem + // with codemirror editor on the code insight creation UI pages. + // See https://github.com/sourcegraph/sourcegraph/issues/37785 + padding-top: 0.5rem; + padding-bottom: 0.5rem; + &:focus-within, &:focus { border: 1px solid var(--input-focus-border-color); @@ -69,6 +75,13 @@ :global(.cm-editor) { flex: 1; } + + // Fix loading spinner layout position (since line height has non default value + // loading layout requires layout change) + // stylelint-disable-next-line selector-class-pattern + :global(.cm-sg-loading-spinner-container) { + align-self: center; + } } .editor { diff --git a/client/web/src/enterprise/insights/components/form/monaco-field/MonacoField.story.tsx b/client/web/src/enterprise/insights/components/form/monaco-field/MonacoField.story.tsx index 14688494fc03..db338def6608 100644 --- a/client/web/src/enterprise/insights/components/form/monaco-field/MonacoField.story.tsx +++ b/client/web/src/enterprise/insights/components/form/monaco-field/MonacoField.story.tsx @@ -20,20 +20,20 @@ before:2021-12-23T00:00:00+03:00 database.NewDB export const SimpleMonacoField = () => (
- + - + - + - + - + - + - + ) } diff --git a/client/web/src/enterprise/insights/components/form/query-input/InsightQueryInput.module.scss b/client/web/src/enterprise/insights/components/form/query-input/InsightQueryInput.module.scss index d41b9990ce0e..d2f2ded426ef 100644 --- a/client/web/src/enterprise/insights/components/form/query-input/InsightQueryInput.module.scss +++ b/client/web/src/enterprise/insights/components/form/query-input/InsightQueryInput.module.scss @@ -5,20 +5,8 @@ } .input-wrapper { - // Spread standard input paddings in order to fix visually problem - // with codemirror editor on the code insight creation UI pages. - // See https://github.com/sourcegraph/sourcegraph/issues/37785 - padding-top: 0.5rem; - padding-bottom: 0.5rem; border-top-right-radius: 0; border-bottom-right-radius: 0; - - // Fix loading spinner layout position (since line height has non default value - // loading layout requires layout change) - // stylelint-disable-next-line selector-class-pattern - :global(.cm-sg-loading-spinner-container) { - align-self: center; - } } .regex-button { diff --git a/client/web/src/enterprise/insights/components/form/query-input/InsightQueryInput.tsx b/client/web/src/enterprise/insights/components/form/query-input/InsightQueryInput.tsx index b3b4e5148117..afd39a3da8e2 100644 --- a/client/web/src/enterprise/insights/components/form/query-input/InsightQueryInput.tsx +++ b/client/web/src/enterprise/insights/components/form/query-input/InsightQueryInput.tsx @@ -1,8 +1,10 @@ -import { forwardRef } from 'react' +import { forwardRef, InputHTMLAttributes, PropsWithChildren } from 'react' import classNames from 'classnames' +import LinkExternalIcon from 'mdi-react/OpenInNewIcon' import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations' +import { QueryChangeSource, QueryState } from '@sourcegraph/shared/src/search' import type { MonacoFieldProps } from '../monaco-field' import * as Monaco from '../monaco-field' @@ -11,33 +13,65 @@ import { generateRepoFiltersQuery } from './utils/generate-repo-filters-query' import styles from './InsightQueryInput.module.scss' -export interface InsightQueryInputProps extends MonacoFieldProps { +type NativeInputProps = Omit, 'onChange' | 'onBlur'> +type MonacoPublicProps = Omit + +export interface InsightQueryInputProps extends MonacoPublicProps, NativeInputProps { + value: string patternType: SearchPatternType repositories?: string + onChange: (value: string) => void } -export const InsightQueryInput = forwardRef((props, reference) => { - const { children, patternType, repositories = '', ...otherProps } = props - const previewQuery = `${generateRepoFiltersQuery(repositories)} ${props.value}`.trim() +export const InsightQueryInput = forwardRef>( + (props, reference) => { + const { + value, + patternType, + repositories = '', + 'aria-invalid': ariaInvalid, + onChange, + children, + ...otherProps + } = props + const previewQuery = `${generateRepoFiltersQuery(repositories)} ${props.value}`.trim() + + const handleOnChange = (queryState: QueryState): void => { + if (queryState.query !== value) { + onChange(queryState.query) + } + } + + return ( +
+ {children ? ( + + - return ( -
- {children ? ( - + {children} + + ) : ( + )} - {children} - - ) : ( - - )} - - -
- ) -}) + + Preview results + +
+ ) + } +) diff --git a/client/web/src/enterprise/insights/components/insights-view-grid/SmartInsightsViewGrid.story.tsx b/client/web/src/enterprise/insights/components/insights-view-grid/SmartInsightsViewGrid.story.tsx index f663bcffda56..5d515519cf29 100644 --- a/client/web/src/enterprise/insights/components/insights-view-grid/SmartInsightsViewGrid.story.tsx +++ b/client/web/src/enterprise/insights/components/insights-view-grid/SmartInsightsViewGrid.story.tsx @@ -47,6 +47,7 @@ const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ { id: 'searchInsights.insight.Backend_1', repositories: [], + repoQuery: '', type: InsightType.SearchBased, title: 'Backend insight #1', series: [{ id: '001', query: 'test_query', stroke: 'blue', name: 'series A' }], @@ -59,6 +60,7 @@ const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ { id: 'searchInsights.insight.Backend_2', repositories: [], + repoQuery: '', type: InsightType.SearchBased, title: 'Backend insight #2', series: [ @@ -74,6 +76,7 @@ const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ { id: 'searchInsights.insight.Backend_3', repositories: [], + repoQuery: '', type: InsightType.SearchBased, title: 'Backend insight #3', series: [ @@ -90,6 +93,7 @@ const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ { id: 'searchInsights.insight.Backend_4', repositories: [], + repoQuery: '', type: InsightType.SearchBased, title: 'Backend insight #4', series: [{ id: '001', query: 'test_query', stroke: 'blue', name: 'series A' }], @@ -102,6 +106,7 @@ const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ { id: 'searchInsights.insight.Backend_5', repositories: [], + repoQuery: '', type: InsightType.CaptureGroup, title: 'Backend insight #5', query: '', @@ -114,6 +119,7 @@ const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ { id: 'searchInsights.insight.Backend_6', repositories: [], + repoQuery: '', type: InsightType.SearchBased, title: 'Backend insight #6', series: [{ id: '001', query: 'test_query', stroke: 'blue', name: 'series A' }], @@ -126,6 +132,7 @@ const INSIGHT_CONFIGURATIONS: BackendInsight[] = [ { id: 'searchInsights.insight.Backend_7', repositories: [], + repoQuery: '', type: InsightType.SearchBased, title: 'Backend insight #7', series: [{ id: '001', query: 'test_query', stroke: 'red', name: 'series A' }], diff --git a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/BackendInsight.story.tsx b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/BackendInsight.story.tsx index 5c0f49151f5d..a955f01aa63b 100644 --- a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/BackendInsight.story.tsx +++ b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/BackendInsight.story.tsx @@ -31,6 +31,7 @@ const INSIGHT_CONFIGURATION_MOCK: SearchBasedInsight = { id: 'searchInsights.insight.mock_backend_insight_id', title: 'Backend Insight Mock', repositories: [], + repoQuery: '', series: [ { id: 'series_001', query: '', name: 'A metric', stroke: 'var(--warning)' }, { id: 'series_002', query: '', name: 'B metric', stroke: 'var(--warning)' }, @@ -266,6 +267,7 @@ const COMPONENT_MIGRATION_INSIGHT_CONFIGURATION: SearchBasedInsight = { dashboardReferenceCount: 0, isFrozen: false, repositories: [], + repoQuery: '', dashboards: [], } @@ -295,6 +297,7 @@ const DATA_FETCHING_INSIGHT_CONFIGURATION: SearchBasedInsight = { dashboardReferenceCount: 0, isFrozen: false, repositories: [], + repoQuery: '', dashboards: [], } @@ -304,6 +307,7 @@ const TERRAFORM_INSIGHT_CONFIGURATION: CaptureGroupInsight = { title: 'Backend Insight Mock', step: { weeks: 2 }, repositories: [], + repoQuery: '', query: '', filters: { excludeRepoRegexp: '', diff --git a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/drill-down-input/DrillDownInput.tsx b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/drill-down-input/DrillDownInput.tsx index 56b879bd675d..74406167e0f6 100644 --- a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/drill-down-input/DrillDownInput.tsx +++ b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/drill-down-input/DrillDownInput.tsx @@ -2,7 +2,7 @@ import React, { forwardRef, InputHTMLAttributes, PropsWithChildren, Ref } from ' import classNames from 'classnames' -import { Button, Input, InputProps, Label } from '@sourcegraph/wildcard' +import { Button, Input, Label, InputProps } from '@sourcegraph/wildcard' import { TruncatedText } from '../../../../../../trancated-text/TruncatedText' diff --git a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/search-context/DrillDownSearchContextFilter.tsx b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/search-context/DrillDownSearchContextFilter.tsx index 8807bebb9faf..1acf81838cfd 100644 --- a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/search-context/DrillDownSearchContextFilter.tsx +++ b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/search-context/DrillDownSearchContextFilter.tsx @@ -6,7 +6,7 @@ import classNames from 'classnames' import { noop } from 'lodash' import { isDefined } from '@sourcegraph/common' -import { InputProps, Link, LoadingSpinner, useDebounce, ErrorAlert } from '@sourcegraph/wildcard' +import { Link, LoadingSpinner, useDebounce, ErrorAlert, InputProps } from '@sourcegraph/wildcard' import { GetSearchContextsResult } from '../../../../../../../../../graphql-operations' import { TruncatedText } from '../../../../../../trancated-text/TruncatedText' diff --git a/client/web/src/enterprise/insights/core/backend/gql-backend/deserialization/create-insight-view.ts b/client/web/src/enterprise/insights/core/backend/gql-backend/deserialization/create-insight-view.ts index 3a61fcf92ebb..1c0ae82613f3 100644 --- a/client/web/src/enterprise/insights/core/backend/gql-backend/deserialization/create-insight-view.ts +++ b/client/web/src/enterprise/insights/core/backend/gql-backend/deserialization/create-insight-view.ts @@ -29,7 +29,7 @@ export const createInsightView = (insight: InsightViewNode): Insight => { const { defaultFilters, defaultSeriesDisplayOptions } = insight // We do not support different time scope for different series at the moment const step = getDurationFromStep(insight.dataSeriesDefinitions[0].timeScope) - const { repositories } = getInsightRepositories(insight.repositoryDefinition) + const { repositories, repoSearch } = getInsightRepositories(insight.repositoryDefinition) const filters = getParsedFilters(defaultFilters, defaultSeriesDisplayOptions) if (isCaptureGroupInsight) { @@ -40,6 +40,7 @@ export const createInsightView = (insight: InsightViewNode): Insight => { ...baseInsight, type: InsightType.CaptureGroup, repositories, + repoQuery: repoSearch, query, step, filters, @@ -78,6 +79,7 @@ export const createInsightView = (insight: InsightViewNode): Insight => { ...baseInsight, type: InsightType.SearchBased, repositories, + repoQuery: repoSearch, series, step, filters, diff --git a/client/web/src/enterprise/insights/core/backend/gql-backend/gql/GetInsights.ts b/client/web/src/enterprise/insights/core/backend/gql-backend/gql/GetInsights.ts index fa171656409d..c4462dab0d53 100644 --- a/client/web/src/enterprise/insights/core/backend/gql-backend/gql/GetInsights.ts +++ b/client/web/src/enterprise/insights/core/backend/gql-backend/gql/GetInsights.ts @@ -62,6 +62,16 @@ export const INSIGHT_VIEW_FRAGMENT = gql` searchContexts } dashboardReferenceCount + repositoryDefinition { + __typename + ... on RepositorySearchScope { + search + } + + ... on InsightRepositoryScope { + repositories + } + } dashboards { nodes { id diff --git a/client/web/src/enterprise/insights/core/backend/gql-backend/methods/create-insight/serializators.ts b/client/web/src/enterprise/insights/core/backend/gql-backend/methods/create-insight/serializators.ts index e67d03255ed4..d26c119950c7 100644 --- a/client/web/src/enterprise/insights/core/backend/gql-backend/methods/create-insight/serializators.ts +++ b/client/web/src/enterprise/insights/core/backend/gql-backend/methods/create-insight/serializators.ts @@ -43,6 +43,10 @@ export function getCaptureGroupInsightCreateInput( const [unit, value] = getStepInterval(step) const input: LineChartSearchInsightInput = { + repositoryScope: { + repositories: insight.repositories, + repositoryCriteria: insight.repoQuery, + }, dataSeries: [ { query: insight.query, @@ -74,17 +78,20 @@ export function getSearchInsightCreateInput( insight: MinimalSearchBasedInsightData, dashboardId: string | null ): LineChartSearchInsightInput { - const { step, repositories, filters, title } = insight + const { step, repositories, repoQuery, filters, title } = insight const [unit, value] = getStepInterval(step) const input: LineChartSearchInsightInput = { + repositoryScope: { + repositories, + repositoryCriteria: repoQuery, + }, dataSeries: insight.series.map(series => ({ query: series.query, options: { label: series.name, lineColor: series.stroke, }, - repositoryScope: { repositories }, timeScope: { stepInterval: { unit, value } }, })), options: { title }, @@ -132,17 +139,17 @@ export function getComputeInsightCreateInput( insight: MinimalComputeInsightData, dashboardId: string | null ): LineChartSearchInsightInput { - const { repositories, filters, groupBy, title } = insight + const { repositories, filters, groupBy, title, series } = insight const input: LineChartSearchInsightInput = { - dataSeries: insight.series.map(series => ({ + repositoryScope: { repositories }, + dataSeries: series.map(series => ({ query: series.query, options: { label: series.name, lineColor: series.stroke, }, - repositoryScope: { repositories }, - timeScope: { stepInterval: { unit: TimeIntervalStepUnit.WEEK, value: 2 } }, groupBy, + timeScope: { stepInterval: { unit: TimeIntervalStepUnit.WEEK, value: 2 } }, generatedFromCaptureGroups: true, })), options: { title }, diff --git a/client/web/src/enterprise/insights/core/backend/gql-backend/methods/update-insight/serializators.ts b/client/web/src/enterprise/insights/core/backend/gql-backend/methods/update-insight/serializators.ts index 0a45f71fae81..e6a8a460fd7e 100644 --- a/client/web/src/enterprise/insights/core/backend/gql-backend/methods/update-insight/serializators.ts +++ b/client/web/src/enterprise/insights/core/backend/gql-backend/methods/update-insight/serializators.ts @@ -13,11 +13,15 @@ import { import { getStepInterval } from '../../utils/get-step-interval' export function getSearchInsightUpdateInput(insight: MinimalSearchBasedInsightData): UpdateLineChartSearchInsightInput { - const { title, repositories, series, filters, step } = insight + const { title, repositories, repoQuery, series, filters, step } = insight const [unit, value] = getStepInterval(step) return { + repositoryScope: { + repositories, + repositoryCriteria: repoQuery, + }, dataSeries: series.map(series => ({ seriesId: series.id, query: series.query, @@ -25,7 +29,6 @@ export function getSearchInsightUpdateInput(insight: MinimalSearchBasedInsightDa label: series.name, lineColor: series.stroke, }, - repositoryScope: { repositories }, timeScope: { stepInterval: { unit, value } }, })), presentationOptions: { title }, @@ -43,15 +46,15 @@ export function getSearchInsightUpdateInput(insight: MinimalSearchBasedInsightDa export function getCaptureGroupInsightUpdateInput( insight: MinimalCaptureGroupInsightData ): UpdateLineChartSearchInsightInput { - const { step, filters, query, title, repositories } = insight + const { step, filters, query, title, repositories, repoQuery } = insight const [unit, value] = getStepInterval(step) return { + repositoryScope: { repositories, repositoryCriteria: repoQuery }, dataSeries: [ { query, options: {}, - repositoryScope: { repositories }, timeScope: { stepInterval: { unit, value } }, generatedFromCaptureGroups: true, }, diff --git a/client/web/src/enterprise/insights/core/backend/gql-backend/utils/insight-polling.ts b/client/web/src/enterprise/insights/core/backend/gql-backend/utils/insight-polling.ts index c19a89faedc8..5c6eb435fb98 100644 --- a/client/web/src/enterprise/insights/core/backend/gql-backend/utils/insight-polling.ts +++ b/client/web/src/enterprise/insights/core/backend/gql-backend/utils/insight-polling.ts @@ -1,4 +1,5 @@ import { BackendInsight } from '../../..' + const ALL_REPOS_POLL_INTERVAL = 30000 const SOME_REPOS_POLL_INTERVAL = 2000 diff --git a/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-compute-insight.ts b/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-compute-insight.ts index 04d52b439427..dfb34c184e8e 100644 --- a/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-compute-insight.ts +++ b/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-compute-insight.ts @@ -35,7 +35,7 @@ export function useLivePreviewComputeInsight(props: Props): Result ({ ...srs, groupBy, diff --git a/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-series-insight.ts b/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-series-insight.ts index 6774e7a7f9de..a43820b26675 100644 --- a/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-series-insight.ts +++ b/client/web/src/enterprise/insights/core/hooks/live-preview-insight/use-live-preview-series-insight.ts @@ -4,6 +4,7 @@ import { ApolloError, gql, useQuery } from '@apollo/client' import { Duration } from 'date-fns' import { HTTPStatusError } from '@sourcegraph/http-client' +import { RepositoryScopeInput } from '@sourcegraph/shared/src/graphql-operations' import { Series } from '@sourcegraph/wildcard' import { @@ -37,7 +38,7 @@ export interface SeriesWithStroke extends SearchSeriesPreviewInput { interface Props { skip: boolean step: Duration - repositories: string[] + repoScope: RepositoryScopeInput series: SeriesWithStroke[] } @@ -54,7 +55,7 @@ interface Result { * instead, it's calculated on the fly in query time on the backend. */ export function useLivePreviewSeriesInsight(props: Props): Result[]> { - const { skip, repositories, step, series } = props + const { skip, repoScope, step, series } = props const [unit, value] = getStepInterval(step) const { data, loading, error, refetch } = useQuery( @@ -69,7 +70,7 @@ export function useLivePreviewSeriesInsight(props: Props): Result[ generatedFromCaptureGroups: srs.generatedFromCaptureGroups, groupBy: srs.groupBy, })), - repositoryScope: { repositories }, + repositoryScope: repoScope, timeScope: { stepInterval: { unit, value: +value } }, }, }, @@ -81,12 +82,12 @@ export function useLivePreviewSeriesInsight(props: Props): Result[ return createPreviewSeriesContent({ response: data, originalSeries: series, - repositories, + repositories: repoScope.repositories, }) } return null - }, [data, repositories, series]) + }, [data, repoScope, series]) if (loading) { return { state: { status: LivePreviewStatus.Loading }, refetch } diff --git a/client/web/src/enterprise/insights/core/types/insight/types/capture-group-insight.ts b/client/web/src/enterprise/insights/core/types/insight/types/capture-group-insight.ts index 2b1b0c6928c2..1dc178e4bf6e 100644 --- a/client/web/src/enterprise/insights/core/types/insight/types/capture-group-insight.ts +++ b/client/web/src/enterprise/insights/core/types/insight/types/capture-group-insight.ts @@ -8,6 +8,8 @@ export interface CaptureGroupInsight extends BaseInsight { /** Capture group regexp query string */ query: string + repoQuery: string + /** * List of repositories that are used to collect data by query regexp field */ diff --git a/client/web/src/enterprise/insights/core/types/insight/types/search-insight.ts b/client/web/src/enterprise/insights/core/types/insight/types/search-insight.ts index 8aaae65836c6..e0b3470f6bae 100644 --- a/client/web/src/enterprise/insights/core/types/insight/types/search-insight.ts +++ b/client/web/src/enterprise/insights/core/types/insight/types/search-insight.ts @@ -5,6 +5,7 @@ import { BaseInsight, InsightFilters, InsightType } from '../common' export interface SearchBasedInsight extends BaseInsight { type: InsightType.SearchBased repositories: string[] + repoQuery: string filters: InsightFilters series: SearchBasedInsightSeries[] step: Duration diff --git a/client/web/src/enterprise/insights/pages/all-insights-view/query.ts b/client/web/src/enterprise/insights/pages/all-insights-view/query.ts index b07d9de6d193..56b92444dc60 100644 --- a/client/web/src/enterprise/insights/pages/all-insights-view/query.ts +++ b/client/web/src/enterprise/insights/pages/all-insights-view/query.ts @@ -45,6 +45,16 @@ export const GET_ALL_INSIGHT_CONFIGURATIONS = gql` searchContexts } dashboardReferenceCount + repositoryDefinition { + __typename + ... on RepositorySearchScope { + search + } + + ... on InsightRepositoryScope { + repositories + } + } dashboards { nodes { id diff --git a/client/web/src/enterprise/insights/pages/insights/creation/LineChartLivePreview.tsx b/client/web/src/enterprise/insights/pages/insights/creation/LineChartLivePreview.tsx index cd875e2b6243..7c234bfa2c14 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/LineChartLivePreview.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/LineChartLivePreview.tsx @@ -1,6 +1,6 @@ import { FC, HTMLAttributes } from 'react' -import { useDeepMemo, Text, Series, useDebounce, ErrorAlert } from '@sourcegraph/wildcard' +import { useDeepMemo, Series, useDebounce, ErrorAlert } from '@sourcegraph/wildcard' import { useSeriesToggle } from '../../../../../insights/utils/use-series-toggle' import { @@ -13,10 +13,10 @@ import { LivePreviewBlurBackdrop, LivePreviewBanner, LivePreviewLegend, - getSanitizedRepositories, + getSanitizedRepositoryScope, SERIES_MOCK_CHART, } from '../../../components' -import { useLivePreviewSeriesInsight, LivePreviewStatus } from '../../../core/hooks/live-preview-insight' +import { useLivePreviewSeriesInsight, LivePreviewStatus } from '../../../core' import { getSanitizedCaptureQuery } from './capture-group/utils/capture-group-insight-sanitizer' import { InsightStep } from './search-insight' @@ -31,19 +31,20 @@ export interface LivePreviewSeries { interface LineChartLivePreviewProps extends HTMLAttributes { disabled: boolean repositories: string + repoQuery: string | undefined + repoMode: string stepValue: string step: InsightStep - isAllReposMode: boolean series: LivePreviewSeries[] } export const LineChartLivePreview: FC = props => { - const { disabled, repositories, stepValue, step, series, isAllReposMode, ...attributes } = props + const { disabled, repositories, repoQuery, repoMode, stepValue, step, series, ...attributes } = props const seriesToggleState = useSeriesToggle() const settings = useDebounce( useDeepMemo({ - repositories: getSanitizedRepositories(repositories), + repoScope: getSanitizedRepositoryScope(repositories, repoQuery, repoMode), step: { [step]: stepValue }, series: series.map(srs => { const sanitizer = srs.generatedFromCaptureGroup @@ -70,7 +71,7 @@ export const LineChartLivePreview: FC = props => { ) } diff --git a/client/web/src/enterprise/insights/pages/insights/creation/capture-group/components/CaptureGoupCreationForm.tsx b/client/web/src/enterprise/insights/pages/insights/creation/capture-group/components/CaptureGoupCreationForm.tsx index a71286bf018e..e9d680d2aa26 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/capture-group/components/CaptureGoupCreationForm.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/capture-group/components/CaptureGoupCreationForm.tsx @@ -2,7 +2,7 @@ import { FC, FormHTMLAttributes, ReactNode } from 'react' import classNames from 'classnames' -import { Card, Checkbox, Input, Label, Link } from '@sourcegraph/wildcard' +import { Card, Input, Label, Link } from '@sourcegraph/wildcard' import { CodeInsightTimeStepPicker, @@ -11,9 +11,9 @@ import { getDefaultInputProps, useFieldAPI, Form, - RepositoriesField, LimitedAccessLabel, SubmissionErrors, + RepoSettingSection, } from '../../../../../components' import { useUiFeatures } from '../../../../../hooks' import { CaptureGroupFormFields } from '../types' @@ -27,7 +27,8 @@ interface CaptureGroupCreationFormProps extends Omit title: useFieldAPI repositories: useFieldAPI - allReposMode: useFieldAPI + repoQuery: useFieldAPI + repoMode: useFieldAPI step: useFieldAPI stepValue: useFieldAPI query: useFieldAPI @@ -49,8 +50,9 @@ export const CaptureGroupCreationForm: FC = props const { form, title, + repoMode, + repoQuery, repositories, - allReposMode, query, step, stepValue, @@ -70,43 +72,7 @@ export const CaptureGroupCreationForm: FC = props return ( // eslint-disable-next-line react/forbid-elements
- - - - - - - This feature is actively in development. Read about the{' '} - - limitations here. - - - +
@@ -205,7 +171,7 @@ export const CaptureGroupCreationForm: FC = props errorInputState={stepValue.meta.touched && stepValue.meta.validState === 'INVALID'} stepType={step.input.value} onStepTypeChange={step.input.onChange} - numberOfPoints={allReposMode.input.value ? 12 : 7} + numberOfPoints={7} /> diff --git a/client/web/src/enterprise/insights/pages/insights/creation/capture-group/components/CaptureGroupCreationContent.tsx b/client/web/src/enterprise/insights/pages/insights/creation/capture-group/components/CaptureGroupCreationContent.tsx index 2cad36d544c5..0bc637ed2f8e 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/capture-group/components/CaptureGroupCreationContent.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/capture-group/components/CaptureGroupCreationContent.tsx @@ -2,6 +2,7 @@ import { FC, ReactNode } from 'react' import { noop } from 'lodash' +import { useExperimentalFeatures } from '../../../../../../../stores' import { CreationUiLayout, CreationUIForm, @@ -10,8 +11,7 @@ import { FormChangeEvent, SubmissionErrors, useForm, - insightRepositoriesValidator, - insightRepositoriesAsyncValidator, + useRepoFields, } from '../../../../../components' import { LineChartLivePreview } from '../../LineChartLivePreview' import { CaptureGroupFormFields } from '../types' @@ -25,7 +25,8 @@ const INITIAL_VALUES: CaptureGroupFormFields = { title: '', step: 'months', stepValue: '2', - allRepos: false, + repoMode: 'search-query', + repoQuery: { query: '' }, dashboardReferenceCount: 0, } @@ -42,41 +43,29 @@ interface CaptureGroupCreationContentProps { export const CaptureGroupCreationContent: FC = props => { const { touched, initialValues = {}, className, children, onSubmit, onChange = noop } = props + const repoFieldVariation = useExperimentalFeatures(features => features.codeInsightsRepoUI) + const isSearchQueryORUrlsList = repoFieldVariation === 'search-query-or-strict-list' + + // Enforce "search-query" initial value if we're in the single search query UI mode + const fixedInitialValues = isSearchQueryORUrlsList + ? { ...INITIAL_VALUES, ...initialValues } + : { ...INITIAL_VALUES, ...initialValues, repoMode: 'search-query' as const } + const form = useForm({ - initialValues: { ...INITIAL_VALUES, ...initialValues }, + initialValues: fixedInitialValues, touched, onSubmit, onChange, }) + const { repoMode, repoQuery, repositories } = useRepoFields({ formApi: form.formAPI }) + const title = useField({ name: 'title', formApi: form.formAPI, validators: { sync: TITLE_VALIDATORS }, }) - const allReposMode = useField({ - name: 'allRepos', - formApi: form.formAPI, - onChange: (checked: boolean) => { - // Reset form values in case if All repos mode was activated - if (checked) { - repositories.input.onChange('') - } - }, - }) - - const repositories = useField({ - name: 'repositories', - formApi: form.formAPI, - validators: { - // Turn off any validations for the repositories' field in we are in all repos mode - sync: !allReposMode.input.value ? insightRepositoriesValidator : undefined, - async: !allReposMode.input.value ? insightRepositoriesAsyncValidator : undefined, - }, - disabled: allReposMode.input.value, - }) - const query = useField({ name: 'groupSearchQuery', formApi: form.formAPI, @@ -96,6 +85,7 @@ export const CaptureGroupCreationContent: FC = const handleFormReset = (): void => { title.input.onChange('') + repoQuery.input.onChange({ query: '' }) repositories.input.onChange('') query.input.onChange('') step.input.onChange('months') @@ -106,14 +96,15 @@ export const CaptureGroupCreationContent: FC = } const hasFilledValue = - form.values.title !== '' || form.values.repositories !== '' || form.values.groupSearchQuery !== '' + form.values.title !== '' || + form.values.repositories !== '' || + form.values.repoQuery.query !== '' || + form.values.groupSearchQuery !== '' const areAllFieldsForPreviewValid = - repositories.meta.validState === 'VALID' && + (repositories.meta.validState === 'VALID' || repoQuery.meta.validState === 'VALID') && stepValue.meta.validState === 'VALID' && - query.meta.validState === 'VALID' && - // For all repos mode we are not able to show the live preview chart - !allReposMode.input.value + query.meta.validState === 'VALID' return ( @@ -122,12 +113,13 @@ export const CaptureGroupCreationContent: FC = as={CaptureGroupCreationForm} form={form} title={title} + repoMode={repoMode} + repoQuery={repoQuery} repositories={repositories} step={step} stepValue={stepValue} query={query} isFormClearActive={hasFilledValue} - allReposMode={allReposMode} dashboardReferenceCount={initialValues.dashboardReferenceCount} onFormReset={handleFormReset} > @@ -137,8 +129,9 @@ export const CaptureGroupCreationContent: FC = { const encodedSearchInsightParameters = encodeCaptureInsightURL({ repositories: 'github.com/sourcegraph/sourcegraph, github.com/example/example', title: 'Insight title', - allRepos: true, groupSearchQuery: 'file:go\\.mod$ go\\s*(\\d\\.\\d+) patterntype:regexp', }) expect(decodeCaptureInsightURL(encodedSearchInsightParameters)).toStrictEqual({ repositories: 'github.com/sourcegraph/sourcegraph, github.com/example/example', + repoMode: 'urls-list', + repoQuery: { query: '' }, title: 'Insight title', - allRepos: true, groupSearchQuery: 'file:go\\.mod$ go\\s*(\\d\\.\\d+) patterntype:regexp', }) }) diff --git a/client/web/src/enterprise/insights/pages/insights/creation/capture-group/utils/capture-insigh-url-parsers/capture-insight-url-parsers.ts b/client/web/src/enterprise/insights/pages/insights/creation/capture-group/utils/capture-insigh-url-parsers/capture-insight-url-parsers.ts index 8769a6c2da83..b7cc8f6d1c83 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/capture-group/utils/capture-insigh-url-parsers/capture-insight-url-parsers.ts +++ b/client/web/src/enterprise/insights/pages/insights/creation/capture-group/utils/capture-insigh-url-parsers/capture-insight-url-parsers.ts @@ -1,6 +1,10 @@ import { CaptureGroupFormFields } from '../../types' -export type CaptureInsightUrlValues = Omit +type UnsupportedFields = 'step' | 'stepValue' | 'repoQuery' | 'repoMode' + +export type CaptureInsightUrlValues = Omit & { + repoQuery: string +} export function encodeCaptureInsightURL(values: Partial): string { const parameters = new URLSearchParams() @@ -10,6 +14,7 @@ export function encodeCaptureInsightURL(values: Partial const fields = values as CaptureInsightUrlValues switch (key) { + case 'repoQuery': case 'groupSearchQuery': { parameters.set(key, encodeURIComponent(fields[key].toString())) break @@ -27,17 +32,18 @@ export function decodeCaptureInsightURL(queryParameters: string): Partial = pro const { licensed, insight } = useUiFeatures() const creationPermission = useObservable(useMemo(() => insight.getCreationPermissions(), [insight])) - const { initialValues, loading, setLocalStorageFormValues } = useSearchInsightInitialValues() + const { initialValues, setLocalStorageFormValues } = useSearchInsightInitialValues() useEffect(() => { telemetryService.logViewEvent('CodeInsightsSearchBasedCreationPage') @@ -99,59 +99,43 @@ export const SearchInsightCreationPage: FC = pro - {loading && ( - // loading state for 1 click creation insight values resolve operation -
- Resolving search query -
- )} - - { - // If we have a query in URL we should be sure that we have initial values - // from URL query based insight. If we don't have query in URl we can render - // page without resolving URL query based insight values. - !loading && ( - <> - - Search-based code insights analyze your code based on any search query.{' '} - - Learn more. - - - } - /> - - - {form => ( - - )} - - - ) - } + + Search-based code insights analyze your code based on any search query.{' '} + + Learn more. + + + } + /> + + + {form => ( + + )} +
) } diff --git a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/components/SearchInsightCreationContent.tsx b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/components/SearchInsightCreationContent.tsx index 31e9483852f0..c407b942bbfc 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/components/SearchInsightCreationContent.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/components/SearchInsightCreationContent.tsx @@ -36,10 +36,11 @@ export const SearchInsightCreationContent: FC form: { values, formAPI, handleSubmit }, title, repositories, + repoQuery, + repoMode, series, step, stepValue, - allReposMode, } = useInsightCreationForm({ touched, initialValue, @@ -51,6 +52,7 @@ export const SearchInsightCreationContent: FC // TODO [VK] Change useForm API in order to implement form.reset method. title.input.onChange('') repositories.input.onChange('') + repoQuery.input.onChange({ query: '' }) // Focus first element of the form repositories.input.ref.current?.focus() series.input.onChange([createDefaultEditSeries({ edit: true })]) @@ -61,15 +63,14 @@ export const SearchInsightCreationContent: FC // If some fields that needed to run live preview are invalid // we should disable live chart preview const allFieldsForPreviewAreValid = - repositories.meta.validState === 'VALID' && + (repositories.meta.validState === 'VALID' || repoQuery.meta.validState === 'VALID') && (series.meta.validState === 'VALID' || series.input.value.some(series => series.valid)) && - stepValue.meta.validState === 'VALID' && - // For the "all repositories" mode we are not able to show the live preview chart - !allReposMode.input.value + stepValue.meta.validState === 'VALID' const hasFilledValue = values.series?.some(line => line.name !== '' || line.query !== '') || values.repositories !== '' || + values.repoQuery.query !== '' || values.title !== '' return ( @@ -83,7 +84,8 @@ export const SearchInsightCreationContent: FC submitted={formAPI.submitted} title={title} repositories={repositories} - allReposMode={allReposMode} + repoQuery={repoQuery} + repoMode={repoMode} series={series} step={step} stepValue={stepValue} @@ -98,7 +100,8 @@ export const SearchInsightCreationContent: FC as={LineChartLivePreview} disabled={!allFieldsForPreviewAreValid} repositories={repositories.meta.value} - isAllReposMode={allReposMode.input.value} + repoMode={repoMode.meta.value} + repoQuery={repoQuery.meta.value.query} series={seriesToPreview(series.input.value)} step={step.meta.value} stepValue={stepValue.meta.value} diff --git a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/components/SearchInsightCreationForm.tsx b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/components/SearchInsightCreationForm.tsx index 80c424c5efa3..604d9351cfcf 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/components/SearchInsightCreationForm.tsx +++ b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/components/SearchInsightCreationForm.tsx @@ -1,16 +1,16 @@ import { FC, FormEventHandler, ReactNode, FormHTMLAttributes } from 'react' -import { Checkbox, Input, Link } from '@sourcegraph/wildcard' +import { Input } from '@sourcegraph/wildcard' import { FormSeries, CodeInsightDashboardsVisibility, CodeInsightTimeStepPicker, - RepositoriesField, FormGroup, getDefaultInputProps, useFieldAPI, SubmissionErrors, + RepoSettingSection, } from '../../../../../components' import { useUiFeatures } from '../../../../../hooks' import { CreateInsightFormFields } from '../types' @@ -25,7 +25,8 @@ interface CreationSearchInsightFormProps extends Omit repositories: useFieldAPI - allReposMode: useFieldAPI + repoQuery: useFieldAPI + repoMode: useFieldAPI series: useFieldAPI step: useFieldAPI @@ -53,7 +54,8 @@ export const SearchInsightCreationForm: FC = pro submitted, title, repositories, - allReposMode, + repoQuery, + repoMode, series, stepValue, step, @@ -69,47 +71,9 @@ export const SearchInsightCreationForm: FC = pro return ( // eslint-disable-next-line react/forbid-elements - - + - - - - This feature is actively in development. Read about the{' '} - - limitations here. - - - -
-
+
= pro errorInputState={stepValue.meta.touched && stepValue.meta.validState === 'INVALID'} stepType={step.input.value} onStepTypeChange={step.input.onChange} - numberOfPoints={allReposMode.input.value ? 12 : 7} + numberOfPoints={7} /> diff --git a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/components/use-insight-creation-form.ts b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/components/use-insight-creation-form.ts index 73cf9adc8996..8ec4a95caa4e 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/components/use-insight-creation-form.ts +++ b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/components/use-insight-creation-form.ts @@ -1,19 +1,21 @@ +import { QueryState } from '@sourcegraph/shared/src/search' + +import { useExperimentalFeatures } from '../../../../../../../stores' import { - useField, - useFieldAPI, + createDefaultEditSeries, + EditableDataSeries, Form, FormChangeEvent, + insightSeriesValidator, + insightStepValueValidator, + insightTitleValidator, SubmissionErrors, + useField, + useFieldAPI, useForm, - EditableDataSeries, - insightTitleValidator, - insightRepositoriesValidator, - insightRepositoriesAsyncValidator, - insightStepValueValidator, - insightSeriesValidator, - createDefaultEditSeries, + useRepoFields, } from '../../../../../components' -import { CreateInsightFormFields, InsightStep } from '../types' +import { CreateInsightFormFields, InsightStep, RepoMode } from '../types' export const INITIAL_INSIGHT_VALUES: CreateInsightFormFields = { // If user opens the creation form to create insight @@ -24,7 +26,8 @@ export const INITIAL_INSIGHT_VALUES: CreateInsightFormFields = { stepValue: '2', title: '', repositories: '', - allRepos: false, + repoMode: 'search-query', + repoQuery: { query: '' }, dashboardReferenceCount: 0, } @@ -39,10 +42,11 @@ export interface InsightCreationForm { form: Form title: useFieldAPI repositories: useFieldAPI + repoQuery: useFieldAPI + repoMode: useFieldAPI series: useFieldAPI step: useFieldAPI stepValue: useFieldAPI - allReposMode: useFieldAPI } /** @@ -52,28 +56,22 @@ export interface InsightCreationForm { export function useInsightCreationForm(props: UseInsightCreationFormProps): InsightCreationForm { const { touched, initialValue = {}, onSubmit, onChange } = props + const repoFieldVariation = useExperimentalFeatures(features => features.codeInsightsRepoUI) + const isSearchQueryORUrlsList = repoFieldVariation === 'search-query-or-strict-list' + + // Enforce "search-query" initial value if we're in the single search query UI mode + const initialValues = isSearchQueryORUrlsList + ? { ...INITIAL_INSIGHT_VALUES, ...initialValue } + : { ...INITIAL_INSIGHT_VALUES, ...initialValue, repoMode: 'search-query' as const } + const form = useForm({ - initialValues: { - ...INITIAL_INSIGHT_VALUES, - ...initialValue, - }, + initialValues, onSubmit, onChange, touched, }) - const allReposMode = useField({ - name: 'allRepos', - formApi: form.formAPI, - onChange: (checked: boolean) => { - // Reset form values in case if All repos mode was activated - if (checked) { - repositories.input.onChange('') - } - }, - }) - - const isAllReposMode = allReposMode.input.value + const { repoMode, repoQuery, repositories } = useRepoFields({ formApi: form.formAPI }) const title = useField({ name: 'title', @@ -81,17 +79,6 @@ export function useInsightCreationForm(props: UseInsightCreationFormProps): Insi validators: { sync: insightTitleValidator }, }) - const repositories = useField({ - name: 'repositories', - formApi: form.formAPI, - validators: { - // Turn off any validations for the repositories' field in we are in all repos mode - sync: !isAllReposMode ? insightRepositoriesValidator : undefined, - async: !isAllReposMode ? insightRepositoriesAsyncValidator : undefined, - }, - disabled: isAllReposMode, - }) - const series = useField({ name: 'series', formApi: form.formAPI, @@ -102,22 +89,21 @@ export function useInsightCreationForm(props: UseInsightCreationFormProps): Insi name: 'step', formApi: form.formAPI, }) + const stepValue = useField({ name: 'stepValue', formApi: form.formAPI, - validators: { - // Turn off any validations if we are in all repos mode - sync: !isAllReposMode ? insightStepValueValidator : undefined, - }, + validators: { sync: insightStepValueValidator }, }) return { form, title, repositories, + repoQuery, + repoMode, series, step, stepValue, - allReposMode, } } diff --git a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/types.ts b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/types.ts index 210081bc6e2a..dcac491086ca 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/types.ts +++ b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/types.ts @@ -1,6 +1,9 @@ -import { EditableDataSeries } from '../../../../components/creation-ui/form-series/types' +import { QueryState } from '@sourcegraph/shared/src/search' + +import { EditableDataSeries } from '../../../../components' export type InsightStep = 'hours' | 'days' | 'weeks' | 'months' | 'years' +export type RepoMode = 'search-query' | 'urls-list' export interface CreateInsightFormFields { /** Code Insight series setting (name of line, line query, color) */ @@ -12,18 +15,25 @@ export interface CreateInsightFormFields { /** Repositories which to be used to get the info for code insights */ repositories: string + /** + * [Experimental] Repositories UI can work in different modes when we have + * two repo UI fields version of the creation UI. This field controls the + * current mode + */ + repoMode: RepoMode + + /** + * Search-powered query, this is used to gather different repositories though + * search API instead of having strict list of repo URLs. + */ + repoQuery: QueryState + /** Setting for set chart step - how often do we collect data. */ step: InsightStep /** Value for insight step setting */ stepValue: string - /** - * This setting stands for turning on/off all repos mode that means this insight - * will be run over all repos on BE (BE insight) - */ - allRepos: boolean - /** The total number of dashboards on which this insight is referenced. */ dashboardReferenceCount: number } diff --git a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/insight-sanitizer.ts b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/insight-sanitizer.ts index 962ac454dead..9d5b036fc3a5 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/insight-sanitizer.ts +++ b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/insight-sanitizer.ts @@ -9,34 +9,11 @@ import { CreateInsightFormFields } from '../types' * presented in user/org settings. */ export function getSanitizedSearchInsight(rawInsight: CreateInsightFormFields): MinimalSearchBasedInsightData { - if (rawInsight.allRepos) { - return { - repositories: [], - type: InsightType.SearchBased, - title: rawInsight.title, - series: getSanitizedSeries(rawInsight.series), - step: { [rawInsight.step]: +rawInsight.stepValue }, - dashboards: [], - filters: { - excludeRepoRegexp: '', - includeRepoRegexp: '', - context: '', - seriesDisplayOptions: { - limit: MAX_NUMBER_OF_SERIES, - numSamples: null, - sortOptions: { - direction: SeriesSortDirection.DESC, - mode: SeriesSortMode.RESULT_COUNT, - }, - }, - }, - } - } - return { type: InsightType.SearchBased, title: rawInsight.title, - repositories: getSanitizedRepositories(rawInsight.repositories), + repoQuery: rawInsight.repoMode === 'search-query' ? rawInsight.repoQuery.query : '', + repositories: rawInsight.repoMode === 'urls-list' ? getSanitizedRepositories(rawInsight.repositories) : [], series: getSanitizedSeries(rawInsight.series), step: { [rawInsight.step]: +rawInsight.stepValue }, dashboards: [], diff --git a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/search-insight-url-parsers/search-insight-url-parsers.test.ts b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/search-insight-url-parsers/search-insight-url-parsers.test.ts index ebd444b2b961..7f281c2bd291 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/search-insight-url-parsers/search-insight-url-parsers.test.ts +++ b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/search-insight-url-parsers/search-insight-url-parsers.test.ts @@ -9,7 +9,7 @@ describe('decodeSearchInsightUrl', () => { test('should return a valid search insight initial values object', () => { const queryString = encodeURIComponent( - `?repositories=github.com/sourcegraph/sourcegraph, github.com/example/example&title=Insight title&allRepos=true&series=${JSON.stringify( + `?repositories=github.com/sourcegraph/sourcegraph, github.com/example/example&title=Insight title&series=${JSON.stringify( [ { id: 1, @@ -23,9 +23,10 @@ describe('decodeSearchInsightUrl', () => { ) expect(decodeSearchInsightUrl(queryString)).toStrictEqual({ + repoMode: 'urls-list', + repoQuery: { query: '' }, repositories: 'github.com/sourcegraph/sourcegraph, github.com/example/example', title: 'Insight title', - allRepos: true, series: [ { id: 1, edit: false, valid: true, autofocus: false, name: 'series 1', query: 'test1', stroke: 'red' }, { id: 2, edit: false, valid: true, autofocus: false, name: 'series 2', query: 'test2', stroke: 'blue' }, @@ -39,7 +40,6 @@ describe('encodeSearchInsightUrl', () => { const encodedSearchInsightParameters = encodeSearchInsightUrl({ repositories: 'github.com/sourcegraph/sourcegraph, github.com/example/example', title: 'Insight title', - allRepos: true, series: [ { id: '1', name: 'series 1', query: 'test1', stroke: 'red' }, { id: '2', name: 'series 2', query: 'test2', stroke: 'blue' }, @@ -47,9 +47,10 @@ describe('encodeSearchInsightUrl', () => { }) expect(decodeSearchInsightUrl(encodedSearchInsightParameters)).toStrictEqual({ + repoMode: 'urls-list', + repoQuery: { query: '' }, repositories: 'github.com/sourcegraph/sourcegraph, github.com/example/example', title: 'Insight title', - allRepos: true, series: [ { id: '1', diff --git a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/search-insight-url-parsers/search-insight-url-parsers.ts b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/search-insight-url-parsers/search-insight-url-parsers.ts index 7bb46d0842db..5447b7db05a3 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/search-insight-url-parsers/search-insight-url-parsers.ts +++ b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/search-insight-url-parsers/search-insight-url-parsers.ts @@ -6,18 +6,19 @@ export function decodeSearchInsightUrl(queryParameters: string): Partial createDefaultEditSeries({ ...series, edit: false, valid: true })) - const allRepos = searchParameter.get('allRepos') - if (repositories || title || editableSeries.length > 0 || allRepos) { + if (repoQuery || repositories || title || editableSeries.length > 0) { return { title: title ?? '', + repoQuery: { query: repoQuery ?? '' }, repositories: repositories ?? '', - allRepos: !!allRepos, series: editableSeries, + repoMode: repoQuery ? 'search-query' : 'urls-list', } } @@ -27,9 +28,10 @@ export function decodeSearchInsightUrl(queryParameters: string): Partial { + repoQuery: string series: (Omit & { id?: string | number })[] } @@ -42,10 +44,9 @@ export function encodeSearchInsightUrl(values: Partial): switch (key) { case 'title': - case 'repositories': - case 'allRepos': { - parameters.set(key, fields[key].toString()) - + case 'repoQuery': + case 'repositories': { + parameters.set(key, encodeURIComponent(fields[key].toString())) break } case 'series': { diff --git a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/use-initial-values.ts b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/use-initial-values.ts index 2eaa87c8ebdc..6f1a60ee49f3 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/use-initial-values.ts +++ b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/use-initial-values.ts @@ -2,7 +2,6 @@ import { useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { isErrorLike } from '@sourcegraph/common' import { useLocalStorage } from '@sourcegraph/wildcard' import { CreateInsightFormFields } from '../types' @@ -12,7 +11,6 @@ import { useURLQueryInsight } from './use-url-query-insight/use-url-query-insigh export interface UseInitialValuesResult { initialValues: Partial - loading: boolean setLocalStorageFormValues: (values: CreateInsightFormFields | undefined) => void } @@ -21,7 +19,7 @@ export function useSearchInsightInitialValues(): UseInitialValuesResult { // Search insight creation UI form can take values from URL query param in order // to support 1-click creation insight flow for the search result page. - const { hasQueryInsight, data: urlQueryInsightValues } = useURLQueryInsight(search) + const initialValuesFromURLParam = useURLQueryInsight(search) const urlParsedInsightValues = useMemo(() => decodeSearchInsightUrl(search), [search]) @@ -37,10 +35,9 @@ export function useSearchInsightInitialValues(): UseInitialValuesResult { ) // [1] "query" query parameter has a higher priority - if (hasQueryInsight) { + if (initialValuesFromURLParam) { return { - initialValues: !isErrorLike(urlQueryInsightValues) ? urlQueryInsightValues ?? {} : {}, - loading: urlQueryInsightValues === undefined, + initialValues: initialValuesFromURLParam, setLocalStorageFormValues, } } @@ -49,7 +46,6 @@ export function useSearchInsightInitialValues(): UseInitialValuesResult { if (urlParsedInsightValues) { return { initialValues: urlParsedInsightValues, - loading: false, setLocalStorageFormValues, } } @@ -57,7 +53,6 @@ export function useSearchInsightInitialValues(): UseInitialValuesResult { // [3] Fallback on localstorage saved insight values return { initialValues: localStorageFormValues ?? {}, - loading: false, setLocalStorageFormValues, } } diff --git a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/use-url-query-insight/use-url-query-insight.test.ts b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/use-url-query-insight/use-url-query-insight.test.ts index 7129122fd68b..e234366f993f 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/use-url-query-insight/use-url-query-insight.test.ts +++ b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/use-url-query-insight/use-url-query-insight.test.ts @@ -5,7 +5,7 @@ describe('getInsightDataFromQuery', () => { const result = getInsightDataFromQuery('') expect(result).toStrictEqual({ - repositories: [], + repoQuery: '', seriesQuery: '', }) }) @@ -17,7 +17,7 @@ describe('getInsightDataFromQuery', () => { const result = getInsightDataFromQuery(queryString) expect(result).toStrictEqual({ - repositories: ['^github\\.com/sourcegraph/sourcegraph$'], + repoQuery: 'context:global repo:^github\\.com/sourcegraph/sourcegraph$', seriesQuery: 'test patterntype:literal', }) }) @@ -28,7 +28,7 @@ describe('getInsightDataFromQuery', () => { const result = getInsightDataFromQuery(queryString) expect(result).toStrictEqual({ - repositories: ['^github\\.com/sourcegraph/sourcegraph$'], + repoQuery: 'repo:^github\\.com/sourcegraph/sourcegraph$', seriesQuery: 'test patterntype:literal', }) }) @@ -40,7 +40,8 @@ describe('getInsightDataFromQuery', () => { const result = getInsightDataFromQuery(queryString) expect(result).toStrictEqual({ - repositories: ['^github\\.com/sourcegraph/sourcegraph$', '^github\\.com/sourcegraph/about'], + repoQuery: + 'context:global repo:^github\\.com/sourcegraph/sourcegraph$ repo:^github\\.com/sourcegraph/about', seriesQuery: 'test patterntype:literal', }) }) @@ -52,7 +53,7 @@ describe('getInsightDataFromQuery', () => { const result = getInsightDataFromQuery(queryString) expect(result).toStrictEqual({ - repositories: ['^github\\.com/sourcegraph/sourcegraph$|^github\\.com/sourcegraph/about'], + repoQuery: 'context:global repo:^github\\.com/sourcegraph/sourcegraph$|^github\\.com/sourcegraph/about', seriesQuery: 'test patterntype:literal', }) }) @@ -64,7 +65,7 @@ describe('getInsightDataFromQuery', () => { const result = getInsightDataFromQuery(queryString) expect(result).toStrictEqual({ - repositories: ['^github\\.com/sourcegraph/sourcegraph$'], + repoQuery: 'context:global repo:^github\\.com/sourcegraph/sourcegraph$', seriesQuery: '"repo: " patterntype:literal', }) }) diff --git a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/use-url-query-insight/use-url-query-insight.ts b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/use-url-query-insight/use-url-query-insight.ts index 6bbd0994a119..1da7e3ec13df 100644 --- a/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/use-url-query-insight/use-url-query-insight.ts +++ b/client/web/src/enterprise/insights/pages/insights/creation/search-insight/utils/use-url-query-insight/use-url-query-insight.ts @@ -1,86 +1,46 @@ -import { useEffect, useMemo, useState } from 'react' +import { useMemo } from 'react' -import { gql, useLazyQuery } from '@apollo/client' - -import { asError, ErrorLike, dedupeWhitespace } from '@sourcegraph/common' +import { dedupeWhitespace } from '@sourcegraph/common' import { FilterType } from '@sourcegraph/shared/src/search/query/filters' import { stringHuman } from '@sourcegraph/shared/src/search/query/printer' import { scanSearchQuery } from '@sourcegraph/shared/src/search/query/scanner' import { isFilterType, isRepoFilter } from '@sourcegraph/shared/src/search/query/validate' -import { SearchRepositoriesResult, SearchRepositoriesVariables } from '../../../../../../../../graphql-operations' import { createDefaultEditSeries } from '../../../../../../components' import { CreateInsightFormFields } from '../../types' -const GET_SEARCH_REPOSITORIES = gql` - query SearchRepositories($query: String) { - search(query: $query) { - results { - repositories { - name - } - } - } - } -` - -export interface UseURLQueryInsightResult { - /** - * Insight data. undefined in case if we are in a loading state or - * URL doesn't have query param. - * */ - data: Partial | ErrorLike | undefined - - /** Whether the search query param is presented in URL. */ - hasQueryInsight: boolean -} +export type UseURLQueryInsightResult = Partial | null /** * Returns initial values for the search insight from query param. */ export function useURLQueryInsight(queryParameters: string): UseURLQueryInsightResult { - const [insightValues, setInsightValues] = useState | ErrorLike | undefined>() - - const [getResolvedSearchRepositories] = useLazyQuery( - GET_SEARCH_REPOSITORIES - ) const query = useMemo(() => new URLSearchParams(queryParameters).get('query'), [queryParameters]) - useEffect(() => { - if (query === null) { - return + return useMemo(() => { + if (!query) { + return null } - const { seriesQuery, repositories } = getInsightDataFromQuery(query) + const { seriesQuery, repoQuery } = getInsightDataFromQuery(query) - // If search query doesn't have repo we should run async repositories resolve - // step to avoid case then run search with query without repo: filter we get - // all indexed repositories. - if (repositories.length > 0) { - getResolvedSearchRepositories({ variables: { query } }) - .then(({ data }) => { - const repositories = data?.search?.results.repositories ?? [] - setInsightValues( - createInsightFormFields( - seriesQuery, - repositories.map(repo => repo.name) - ) - ) - }) - .catch(error => setInsightValues(asError(error))) - } else { - setInsightValues(createInsightFormFields(seriesQuery, repositories)) + return { + series: [ + createDefaultEditSeries({ + edit: true, + valid: true, + name: 'Search series #1', + query: seriesQuery ?? '', + }), + ], + repoMode: repoQuery ? 'search-query' : 'urls-list', + repoQuery: { query: repoQuery }, } - }, [getResolvedSearchRepositories, query]) - - return { - hasQueryInsight: query !== null, - data: query !== null ? insightValues : undefined, - } + }, [query]) } export interface InsightData { - repositories: string[] + repoQuery: string seriesQuery: string } @@ -97,23 +57,17 @@ export function getInsightDataFromQuery(searchQuery: string | null): InsightData if (!searchQuery || sequence.type === 'error') { return { seriesQuery: '', - repositories: [], + repoQuery: '', } } const tokens = Array.isArray(sequence.term) ? sequence.term : [sequence.term] - const repositories = [] - - // Find all repo: filters and get their values for insight repositories field - for (const token of tokens) { - if (isRepoFilter(token)) { - const repoValue = token.value?.value - if (repoValue) { - repositories.push(repoValue) - } - } - } + // Generate a string query from tokens with repo: and context filters only + // in order to put this in the repo query field + const tokensWithRepoFiltersAndContext = tokens.filter( + token => isRepoFilter(token) || isFilterType(token, FilterType.context) || token.type === 'whitespace' + ) // Generate a string query from tokens without repo: filters for the insight // query field. @@ -121,24 +75,8 @@ export function getInsightDataFromQuery(searchQuery: string | null): InsightData token => !isRepoFilter(token) && !isFilterType(token, FilterType.context) ) - const humanReadableQueryString = stringHuman(tokensWithoutRepoFiltersAndContext) - - return { - seriesQuery: dedupeWhitespace(humanReadableQueryString.trim()), - repositories, - } -} - -function createInsightFormFields(seriesQuery: string, repositories: string[] = []): Partial { return { - series: [ - createDefaultEditSeries({ - edit: true, - valid: true, - name: 'Search series #1', - query: seriesQuery ?? '', - }), - ], - repositories: repositories.join(', '), + seriesQuery: dedupeWhitespace(stringHuman(tokensWithoutRepoFiltersAndContext)), + repoQuery: dedupeWhitespace(stringHuman(tokensWithRepoFiltersAndContext)), } } diff --git a/client/web/src/enterprise/insights/pages/insights/edit-insight/components/EditCaptureGroupInsight.tsx b/client/web/src/enterprise/insights/pages/insights/edit-insight/components/EditCaptureGroupInsight.tsx index dfa5312c02ba..adb4035be8fa 100644 --- a/client/web/src/enterprise/insights/pages/insights/edit-insight/components/EditCaptureGroupInsight.tsx +++ b/client/web/src/enterprise/insights/pages/insights/edit-insight/components/EditCaptureGroupInsight.tsx @@ -23,18 +23,22 @@ interface EditCaptureGroupInsightProps { export const EditCaptureGroupInsight: FC = props => { const { insight, licensed, isEditAvailable, onSubmit, onCancel } = props - const insightFormValues = useMemo( - () => ({ + const insightFormValues = useMemo(() => { + const isAllReposInsight = insight.repoQuery === '' && insight.repositories.length === 0 + const repoQuery = isAllReposInsight ? 'repo:.*' : insight.repoQuery + + return { title: insight.title, + repoMode: repoQuery ? 'search-query' : 'urls-list', + repoQuery: { query: repoQuery }, repositories: insight.repositories.join(', '), groupSearchQuery: insight.query, stepValue: Object.values(insight.step)[0]?.toString() ?? '3', step: Object.keys(insight.step)[0] as InsightStep, allRepos: insight.repositories.length === 0, dashboardReferenceCount: insight.dashboardReferenceCount, - }), - [insight] - ) + } + }, [insight]) const handleSubmit = (values: CaptureGroupFormFields): SubmissionErrors | Promise | void => { const sanitizedInsight = getSanitizedCaptureGroupInsight(values) diff --git a/client/web/src/enterprise/insights/pages/insights/edit-insight/components/EditSearchInsight.tsx b/client/web/src/enterprise/insights/pages/insights/edit-insight/components/EditSearchInsight.tsx index 18238dc125c8..222444805433 100644 --- a/client/web/src/enterprise/insights/pages/insights/edit-insight/components/EditSearchInsight.tsx +++ b/client/web/src/enterprise/insights/pages/insights/edit-insight/components/EditSearchInsight.tsx @@ -23,18 +23,22 @@ interface EditSearchBasedInsightProps { export const EditSearchBasedInsight: FC = props => { const { insight, licensed, isEditAvailable, onSubmit, onCancel } = props - const insightFormValues = useMemo( - () => ({ + const insightFormValues = useMemo(() => { + const isAllReposInsight = insight.repoQuery === '' && insight.repositories.length === 0 + const repoQuery = isAllReposInsight ? 'repo:.*' : insight.repoQuery + + return { title: insight.title, + repoMode: repoQuery ? 'search-query' : 'urls-list', + repoQuery: { query: repoQuery }, repositories: insight.repositories.join(', '), series: insight.series.map(line => createDefaultEditSeries({ ...line, valid: true })), stepValue: Object.values(insight.step)[0]?.toString() ?? '3', step: Object.keys(insight.step)[0] as InsightStep, allRepos: insight.repositories.length === 0, dashboardReferenceCount: insight.dashboardReferenceCount, - }), - [insight] - ) + } + }, [insight]) const handleSubmit = (values: CreateInsightFormFields): SubmissionErrors | Promise | void => { const sanitizedInsight = getSanitizedSearchInsight(values) diff --git a/client/web/src/enterprise/insights/pages/landing/getting-started/components/code-insights-examples/CodeInsightsExamples.tsx b/client/web/src/enterprise/insights/pages/landing/getting-started/components/code-insights-examples/CodeInsightsExamples.tsx index 6ff8e907542a..aa914a3bc523 100644 --- a/client/web/src/enterprise/insights/pages/landing/getting-started/components/code-insights-examples/CodeInsightsExamples.tsx +++ b/client/web/src/enterprise/insights/pages/landing/getting-started/components/code-insights-examples/CodeInsightsExamples.tsx @@ -24,7 +24,7 @@ const SEARCH_INSIGHT_CREATION_UI_URL_PARAMETERS = encodeSearchInsightUrl({ const CAPTURE_GROUP_INSIGHT_CREATION_UI_URL_PARAMETERS = encodeCaptureInsightURL({ title: ALPINE_VERSIONS_INSIGHT.title, - allRepos: true, + repoQuery: 'repo:.*', groupSearchQuery: ALPINE_VERSIONS_INSIGHT.groupSearch, }) diff --git a/client/web/src/enterprise/insights/pages/landing/getting-started/components/code-insights-templates/constants.ts b/client/web/src/enterprise/insights/pages/landing/getting-started/components/code-insights-templates/constants.ts index 45a3340382ad..2e8b20412553 100644 --- a/client/web/src/enterprise/insights/pages/landing/getting-started/components/code-insights-templates/constants.ts +++ b/client/web/src/enterprise/insights/pages/landing/getting-started/components/code-insights-templates/constants.ts @@ -33,7 +33,7 @@ const TERRAFORM_VERSIONS: Template = { description: 'Detect and track which Terraform versions are present or most popular in your codebase', templateValues: { title: 'Terraform versions', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: '1.1.0', @@ -55,7 +55,7 @@ const CSS_MODULES_MIGRATION: Template = { description: 'Tracking migration from global CSS to CSS modules', templateValues: { title: 'Global CSS to CSS modules', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Global CSS', @@ -77,7 +77,7 @@ const LOG4J_FIXED_VERSIONS: Template = { description: 'Confirm that vulnerable versions of log4j are removed and only fixed versions appear', templateValues: { title: 'Vulnerable and fixed Log4j versions', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Vulnerable', @@ -100,7 +100,7 @@ const YARN_ADOPTION: Template = { 'Are more repos increasingly using yarn? Track yarn adoption across teams and groups in your organization', templateValues: { title: 'Yarn adoption', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Yarn', @@ -117,7 +117,7 @@ const JAVA_VERSIONS: Template = { description: 'Detect and track which Java versions are most popular in your codebase', templateValues: { title: 'Java versions', - allRepos: true, + repoQuery: 'repo:.*', groupSearchQuery: 'file:pom\\.xml$ (.*)', }, } @@ -128,7 +128,7 @@ const LINTER_OVERRIDE_RULES: Template = { description: 'A code health indicator for how many linter override rules exist', templateValues: { title: 'Linter override rules', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Rule overrides', @@ -145,7 +145,7 @@ const TS_JS_USAGE: Template = { description: 'Track the growth of certain languages by file count', templateValues: { title: 'Language use over time', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'TypeScript', @@ -167,7 +167,7 @@ const CONFIG_OR_DOC_FILE: Template = { description: 'How many repos contain a config or docs file in a specific directory', templateValues: { title: 'Config or docs file', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Repositories with doc', @@ -184,7 +184,7 @@ const ALLOW_DENY_LIST_TRACKING: Template = { description: 'How the switch from files containing “blacklist/whitelist” to “denylist/allowlist” is progressing', templateValues: { title: '“blacklist/whitelist” to “denylist/allowlist”', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'blacklist/whitelist', @@ -206,7 +206,7 @@ const PYTHON_2_3: Template = { description: 'How far along is the Python major version migration', templateValues: { title: 'Python 2 to Python 3', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Python 3', @@ -265,7 +265,7 @@ const FREQUENTLY_USED_DATABASE: Template = { description: 'Which databases we are calling or writing to most often', templateValues: { title: 'Frequently used databases', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Redis', @@ -287,7 +287,7 @@ const LARGE_PACKAGE_USAGE: Template = { description: 'Understand if a growing number of repos import a large/expensive package', templateValues: { title: 'Large or expensive package usage', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Repositories with large package usage', @@ -304,7 +304,7 @@ const REACT_COMPONENT_LIB_USAGE: Template = { description: 'How many places are importing components from a library', templateValues: { title: 'React Component use', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Library imports', @@ -321,7 +321,7 @@ const CI_TOOLING: Template = { description: 'How many repos are using our CI system', templateValues: { title: 'CI tooling adoption', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Repo with CircleCI config', @@ -338,7 +338,7 @@ const CSS_CLASS: Template = { description: 'The removal of all deprecated CSS class', templateValues: { title: 'CSS class', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Deprecated CSS class', @@ -355,7 +355,7 @@ const ICON_OR_IMAGE: Template = { description: 'The removal of all deprecated icon or image instances', templateValues: { title: 'Icon or image', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Deprecated logo', @@ -373,7 +373,7 @@ const STRUCTURAL_CODE_PATTERN: Template = { "Deprecating a structural code pattern in favor of a safer pattern, like how many tries don't have catches", templateValues: { title: 'Structural code pattern', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Try catch', @@ -390,7 +390,7 @@ const TOOLING_MIGRATION: Template = { description: 'The progress of deprecating tooling you’re moving off of', templateValues: { title: 'Tooling', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Deprecated logger', @@ -407,7 +407,7 @@ const VAR_KEYWORDS: Template = { description: 'Number of var keywords in the code basee (ES5 depreciation)', templateValues: { title: 'Var keywords', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'var statements', @@ -424,7 +424,7 @@ const TESTING_LIBRARIES: Template = { description: 'Which React test libraries are being consolidated', templateValues: { title: 'Consolidation of Testing Libraries', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: '@testing-library', @@ -446,7 +446,7 @@ const LICENSE_TYPES: Template = { description: 'See the breakdown of licenses from package.json files', templateValues: { title: 'License types in the codebase', - allRepos: true, + repoQuery: 'repo:.*', groupSearchQuery: 'file:package.json "license":\\s"(.*)"', }, } @@ -457,7 +457,7 @@ const ALL_LOG4J_VERSIONS: Template = { description: 'Which log4j versions are present, including vulnerable versions', templateValues: { title: 'All log4j versions', - allRepos: true, + repoQuery: 'repo:.*', groupSearchQuery: 'lang:gradle org\\.apache\\.logging\\.log4j[\'"] 2\\.([0-9]+)\\.', }, } @@ -468,7 +468,7 @@ const PYTHON_VERSIONS: Template = { description: 'Which python versions are in use or haven’t been updated', templateValues: { title: 'Python versions', - allRepos: true, + repoQuery: 'repo:.*', groupSearchQuery: '#!/usr/bin/env python([0-9]\\.[0-9]+)', }, } @@ -479,7 +479,7 @@ const NODEJS_VERSIONS: Template = { description: 'Which node.js versions are present based on nvm files', templateValues: { title: 'Node.js versions', - allRepos: true, + repoQuery: 'repo:.*', groupSearchQuery: 'nvm\\suse\\s([0-9]+\\.[0-9]+)', }, } @@ -490,7 +490,7 @@ const CSS_COLORS: Template = { description: 'What CSS colors are present or most popular', templateValues: { title: 'CSS Colors', - allRepos: true, + repoQuery: 'repo:.*', groupSearchQuery: 'color:#([0-9a-fA-f]{3,6})', }, } @@ -501,7 +501,7 @@ const CHECKOV_SKIP_TYPES: Template = { description: 'See the most common reasons for why secuirty checks in checkov are skipped', templateValues: { title: 'Types of checkov skips', - allRepos: true, + repoQuery: 'repo:.*', groupSearchQuery: 'patterntype:regexp file:.tf #checkov:skip=(.*)', }, } @@ -512,7 +512,7 @@ const TODOS: Template = { description: 'How many TODOs are in a specific part of the codebase (or all of it)', templateValues: { title: 'TODOs', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'TODOs', @@ -529,7 +529,7 @@ const REVERT_COMMITS: Template = { description: 'How frequently there are commits with “revert” in the commit message', templateValues: { title: 'Commits with “revert”', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Reverts', @@ -546,7 +546,7 @@ const DEPRECATED_CALLS: Template = { description: 'How many times deprecated calls are used', templateValues: { title: 'Deprecated calls', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: '@deprecated', @@ -563,7 +563,7 @@ const STORYBOOK_TESTS: Template = { description: 'How many tests for Storybook exist', templateValues: { title: 'Storybook tests', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Stories', @@ -580,7 +580,7 @@ const REPOS_WITH_README: Template = { description: "How many repos do or don't have READMEs", templateValues: { title: 'Repos with Documentation', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'with readme', @@ -602,7 +602,7 @@ const OWNERSHIP_TRACKING: Template = { description: "How many repos do or don't have CODEOWNERS files", templateValues: { title: 'Ownership via CODEOWNERS files', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'with readme', @@ -625,7 +625,7 @@ const VULNERABLE_OPEN_SOURCE: Template = { 'Confirm that a vulnerable open source library has been fully removed, or see the speed of the deprecation', templateValues: { title: 'Vulnerable open source library', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'vulnerableLibrary@14.3.9', @@ -642,7 +642,7 @@ const API_KEYS_DETECTION: Template = { description: 'How quickly we notice and remove API keys when they are committed', templateValues: { title: 'API keys', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'API key', @@ -659,7 +659,7 @@ const SKIPPED_TESTS: Template = { description: 'See how many tests have skip conditions', templateValues: { title: 'How many tests are skipped', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Skipped tests', @@ -676,7 +676,7 @@ const TEST_AMOUNT_AND_TYPES: Template = { description: 'See what types of tests are most common and total counts', templateValues: { title: 'Tests amount and types', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'e2e tests', @@ -703,7 +703,7 @@ const TS_VS_GO: Template = { description: 'Are there more Typescript or more Go files', templateValues: { title: 'Typescript vs. Go', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'TypeScript', @@ -725,7 +725,7 @@ const IOS_APP_SCREENS: Template = { description: 'What number of iOS app screens are in the entire app', templateValues: { title: 'iOS app screens', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Screens', @@ -742,7 +742,7 @@ const ADOPTING_NEW_API: Template = { description: 'Which teams or repos have adopted a new API so far', templateValues: { title: 'Adopting new API by Team', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Mobile team', @@ -764,7 +764,7 @@ const PROBLEMATIC_API_BY_TEAM: Template = { description: 'Which teams have the most usage of a problematic API', templateValues: { title: 'Problematic API by Team', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'Mobile team', @@ -786,7 +786,7 @@ const DATA_FETCHING_GQL: Template = { description: 'What GraphQL operations are being called often', templateValues: { title: 'Data fetching from GraphQL', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'requestGraphQL', @@ -813,7 +813,7 @@ const GO_STATIC_CHECK_SA6005: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Inefficient string comparison with strings.ToLower or strings.ToUpper', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'SA6005', @@ -835,7 +835,7 @@ const GO_STATIC_CHECK_S1002: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Omit comparison with boolean constant', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1002', @@ -852,7 +852,7 @@ const GO_STATIC_CHECK_S1003: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Replace call to strings.Index with strings.Contains', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1003', @@ -874,7 +874,7 @@ const GO_STATIC_CHECK_S1004: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Replace call to bytes.Compare with bytes.Equal', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1004', @@ -896,7 +896,7 @@ const GO_STATIC_CHECK_S1005: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Drop unnecessary use of the blank identifier', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1005', @@ -918,7 +918,7 @@ const GO_STATIC_CHECK_S1006: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Use for { ... } for infinite loops', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1006', @@ -940,7 +940,7 @@ const GO_STATIC_CHECK_S1010: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Omit default slice index', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S10010', @@ -962,7 +962,7 @@ const GO_STATIC_CHECK_S1012: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Replace time.Now().Sub(x) with time.Since(x)', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S10012', @@ -987,7 +987,7 @@ const GO_STATIC_CHECK_S1019: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Simplify make call by omitting redundant arguments', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S10019', @@ -1009,7 +1009,7 @@ const GO_STATIC_CHECK_S1020: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Omit redundant nil check in type assertion', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1020', @@ -1031,7 +1031,7 @@ const GO_STATIC_CHECK_S1023: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Omit redundant control flow', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1023', @@ -1053,7 +1053,7 @@ const GO_STATIC_CHECK_S1024: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Replace x.Sub(time.Now()) with time.Until(x)', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1024', @@ -1075,7 +1075,7 @@ const GO_STATIC_CHECK_S1025: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Don’t use fmt.Sprintf("%s", x) unnecessarily', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1025', @@ -1097,7 +1097,7 @@ const GO_STATIC_CHECK_S1028: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Simplify error construction with fmt.Errorf', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1028', @@ -1119,7 +1119,7 @@ const GO_STATIC_CHECK_S1029: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Range over the string directly', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1029', @@ -1141,7 +1141,7 @@ const GO_STATIC_CHECK_S1032: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Use sort.Ints(x), sort.Float64s(x), and sort.Strings(x)', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1032', @@ -1158,7 +1158,7 @@ const GO_STATIC_CHECK_S1035: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Redundant call to net/http.CanonicalHeaderKey in method call on net/http.Header', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1035', @@ -1182,7 +1182,7 @@ const GO_STATIC_CHECK_S1037: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Redundant call to net/http.CanonicalHeaderKey in method call on net/http.Header', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1035', @@ -1204,7 +1204,7 @@ const GO_STATIC_CHECK_S1038: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] - Unnecessarily complex way of printing formatted string', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1038', @@ -1226,7 +1226,7 @@ const GO_STATIC_CHECK_S1039: Template = { description: 'Code search turned code checker', templateValues: { title: '[quickfix] Unnecessary use of fmt.Sprint', - allRepos: true, + repoQuery: 'repo:.*', series: [ { name: 'S1039', diff --git a/client/web/src/enterprise/insights/pages/landing/getting-started/components/dynamic-code-insight-example/DynamicInsightPreview.tsx b/client/web/src/enterprise/insights/pages/landing/getting-started/components/dynamic-code-insight-example/DynamicInsightPreview.tsx index 4e397ead5f2d..c407a027a530 100644 --- a/client/web/src/enterprise/insights/pages/landing/getting-started/components/dynamic-code-insight-example/DynamicInsightPreview.tsx +++ b/client/web/src/enterprise/insights/pages/landing/getting-started/components/dynamic-code-insight-example/DynamicInsightPreview.tsx @@ -48,7 +48,7 @@ export const DynamicInsightPreview: FC = props => { const settings = useDebounce( useDeepMemo({ series: createExampleDataSeries(query), - repositories: getSanitizedRepositories(repositories), + repoScope: { repositories: getSanitizedRepositories(repositories) }, step: { months: 2 }, disabled, }), diff --git a/client/web/src/integration/insights/create-insights.test.ts b/client/web/src/integration/insights/create-insights.test.ts index 30bc1bb373fa..1d26241645df 100644 --- a/client/web/src/integration/insights/create-insights.test.ts +++ b/client/web/src/integration/insights/create-insights.test.ts @@ -163,6 +163,11 @@ describe('Code insight create insight page', () => { dashboardReferenceCount: 0, dashboards: { nodes: [] }, + repositoryDefinition: { + repositories: ['github.com/sourcegraph/sourcegraph'], + __typename: 'InsightRepositoryScope', + }, + presentation: { __typename: 'LineChartInsightViewPresentation', title: 'Test insight title', @@ -181,10 +186,6 @@ describe('Code insight create insight page', () => { }, ], }, - repositoryDefinition: { - repositories: ['github.com/sourcegraph/sourcegraph'], - __typename: 'InsightRepositoryScope', - }, dataSeriesDefinitions: [ { seriesId: '1', diff --git a/client/web/src/integration/insights/edit-search-insights.test.ts b/client/web/src/integration/insights/edit-search-insights.test.ts index 7d84a66cbace..9786c2fc1bbc 100644 --- a/client/web/src/integration/insights/edit-search-insights.test.ts +++ b/client/web/src/integration/insights/edit-search-insights.test.ts @@ -188,6 +188,10 @@ describe('Code insight edit insight page', () => { // Check that new org settings config has edited insight assert.deepStrictEqual(editInsightMutationVariables, { input: { + repositoryScope: { + repositories: ['github.com/sourcegraph/sourcegraph', 'github.com/sourcegraph/about'], + repositoryCriteria: '', + }, dataSeries: [ { seriesId: '001', @@ -196,9 +200,6 @@ describe('Code insight edit insight page', () => { label: 'test edited series title', lineColor: 'var(--oc-cyan-7)', }, - repositoryScope: { - repositories: ['github.com/sourcegraph/sourcegraph', 'github.com/sourcegraph/about'], - }, timeScope: { stepInterval: { unit: 'DAY', @@ -213,9 +214,6 @@ describe('Code insight edit insight page', () => { label: 'new test series title', lineColor: 'var(--oc-grape-7)', }, - repositoryScope: { - repositories: ['github.com/sourcegraph/sourcegraph', 'github.com/sourcegraph/about'], - }, timeScope: { stepInterval: { unit: 'DAY', diff --git a/client/web/src/integration/insights/fixtures/insights-metadata.ts b/client/web/src/integration/insights/fixtures/insights-metadata.ts index 690388af6886..5bba982e783a 100644 --- a/client/web/src/integration/insights/fixtures/insights-metadata.ts +++ b/client/web/src/integration/insights/fixtures/insights-metadata.ts @@ -27,7 +27,10 @@ export const createJITMigrationToGQLInsightMetadataFixture = (options: InsightOp excludeRepoRegex: '', }, dashboards: { nodes: [] }, - + repositoryDefinition: { + __typename: 'InsightRepositoryScope', + repositories: ['github.com/sourcegraph/sourcegraph'], + }, presentation: { __typename: 'LineChartInsightViewPresentation', title: 'Migration to new GraphQL TS types', @@ -46,10 +49,6 @@ export const createJITMigrationToGQLInsightMetadataFixture = (options: InsightOp }, ], }, - repositoryDefinition: { - __typename: 'InsightRepositoryScope', - repositories: ['github.com/sourcegraph/sourcegraph'], - }, dataSeriesDefinitions: [ { __typename: 'SearchInsightDataSeriesDefinition', @@ -94,6 +93,10 @@ export const STORYBOOK_GROWTH_INSIGHT_METADATA_FIXTURE: InsightViewNode = { excludeRepoRegex: '', searchContexts: [], }, + repositoryDefinition: { + __typename: 'InsightRepositoryScope', + repositories: ['github.com/sourcegraph/sourcegraph'], + }, presentation: { __typename: 'LineChartInsightViewPresentation', title: 'Team head count', @@ -106,10 +109,6 @@ export const STORYBOOK_GROWTH_INSIGHT_METADATA_FIXTURE: InsightViewNode = { }, ], }, - repositoryDefinition: { - __typename: 'InsightRepositoryScope', - repositories: ['github.com/sourcegraph/sourcegraph'], - }, dataSeriesDefinitions: [ { __typename: 'SearchInsightDataSeriesDefinition', diff --git a/client/web/src/stores/experimentalFeatures.ts b/client/web/src/stores/experimentalFeatures.ts index 6617eda1392c..fd456dffad7b 100644 --- a/client/web/src/stores/experimentalFeatures.ts +++ b/client/web/src/stores/experimentalFeatures.ts @@ -16,7 +16,7 @@ const defaultSettings: SettingsExperimentalFeatures = { showCodeMonitoringLogs: true, codeInsightsCompute: false, editor: 'codemirror6', - codeInsightsRepoUI: 'single-search-query', + codeInsightsRepoUI: 'search-query-or-strict-list', applySearchQuerySuggestionOnEnter: false, } diff --git a/client/wildcard/src/components/Combobox/Combobox.tsx b/client/wildcard/src/components/Combobox/Combobox.tsx index 20e2655420b8..956cf051742d 100644 --- a/client/wildcard/src/components/Combobox/Combobox.tsx +++ b/client/wildcard/src/components/Combobox/Combobox.tsx @@ -9,6 +9,7 @@ import { useRef, useEffect, useLayoutEffect, + InputHTMLAttributes, } from 'react' import { @@ -78,7 +79,10 @@ export const Combobox = forwardRef((props, ref) => { ) }) as ForwardReferenceComponent<'div', ComboboxProps> -interface ComboboxInputProps extends ReachComboboxInputProps, Omit {} +interface ComboboxInputProps + extends ReachComboboxInputProps, + Omit, 'value'>, + InputProps {} /** * Combobox Input wrapper over Reach UI combobox input component. We wrap this component diff --git a/client/wildcard/src/components/Form/Input/Input.module.scss b/client/wildcard/src/components/Form/Input/Input.module.scss index f99746b31a1a..cf4532c8fd1b 100644 --- a/client/wildcard/src/components/Form/Input/Input.module.scss +++ b/client/wildcard/src/components/Form/Input/Input.module.scss @@ -1,3 +1,7 @@ +.label { + width: 100%; +} + .input-loading { padding-right: 2rem; } @@ -5,3 +9,15 @@ .loader-input { display: flex; } + +.description-block { + // Compensate visual list padding in order to + // adjust list points with other element vertical + // rhythm + margin: 0 0 0 0.25rem; + + ul { + margin: 0; + padding-left: 1rem; + } +} diff --git a/client/wildcard/src/components/Form/Input/Input.story.tsx b/client/wildcard/src/components/Form/Input/Input.story.tsx index dad924901d02..c79044ccd906 100644 --- a/client/wildcard/src/components/Form/Input/Input.story.tsx +++ b/client/wildcard/src/components/Form/Input/Input.story.tsx @@ -4,7 +4,7 @@ import { Meta } from '@storybook/react' import { BrandedStory } from '../../../stories/BrandedStory' -import { Input } from './Input' +import { Input, InputDescription, InputElement, InputErrorMessage, InputStatus, Label } from './Input' const Story: Meta = { title: 'wildcard/Input', @@ -81,6 +81,25 @@ export const Simple = () => { placeholder="testing this one" variant="small" /> + +
+ + + + +
    +
  • Hint: you can use regular expressions within each of the available filters
  • +
  • + Datapoints will be automatically backfilled using the list of repositories resulting from + today’s search. Future data points will use the list refreshed for every snapshot. +
  • +
+
+
) } diff --git a/client/wildcard/src/components/Form/Input/Input.test.tsx b/client/wildcard/src/components/Form/Input/Input.test.tsx index b95179ac8075..7727e482a5cf 100644 --- a/client/wildcard/src/components/Form/Input/Input.test.tsx +++ b/client/wildcard/src/components/Form/Input/Input.test.tsx @@ -50,40 +50,7 @@ describe('Input', () => { /> ) - expect(container.firstChild).toMatchInlineSnapshot(` -
+ + + + + + + + + + + @@ -122,7 +133,7 @@ Tier 1 code hosts are our highest level of support for code hosts. When leveragi #### Status definitions -An code host status is: +A code host status is: - 🟢 _Generally Available:_ Available as a normal product feature up to 100k repositories. - 🟡 _Partially available:_ Available, but may be limited in some significant ways (either missing or buggy functionality). If you plan to leverage this, please contact your Customer Engineer. @@ -142,6 +153,7 @@ We recognize there are other code hosts including CVS, Azure Dev Ops, SVN, and m - [GitLab](gitlab.md) - [Bitbucket Cloud](bitbucket_cloud.md) - [Bitbucket Server / Bitbucket Data Center](bitbucket_server.md) +- [Gerrit](gerrit.md) - [Other Git code hosts (using a Git URL)](other.md) - [Non-Git code hosts](non-git.md) - [Perforce](../repo/perforce.md) diff --git a/doc/admin/external_service/other.md b/doc/admin/external_service/other.md index f94b46187325..c4e6a54deee9 100644 --- a/doc/admin/external_service/other.md +++ b/doc/admin/external_service/other.md @@ -13,21 +13,21 @@ To connect generic Git host to Sourcegraph: >NOTE: Repository access over SSH is not yet supported on [Sourcegraph Cloud](../../cloud/index.md). -If your code host serves git repositories over SSH (e.g. Gerrit), make sure your Sourcegraph instance can connect to your code host over SSH: +If your code host serves git repositories over SSH, make sure your Sourcegraph instance can connect to your code host over SSH: ``` docker exec $CONTAINER ssh -p $PORT $USER@$HOSTNAME ``` - $CONTAINER is the name or ID of your sourcegraph/server container -- $PORT is the port on which your code host's git server is listening for connections (Gerrit defaults to `29418`) +- $PORT is the port on which your code host's git server is listening for connections - $USER is your user on your code host (Gerrit defaults to `admin`) -- $HOSTNAME is the hostname of your code host from within the sourcegraph/server container (e.g. `gerrit.example.com`) +- $HOSTNAME is the hostname of your code host from within the sourcegraph/server container (e.g. `githost.example.com`) -Here's an example for Gerrit: +Here's an example: ``` -docker exec sourcegraph ssh -p 29418 admin@gerrit.example.com +docker exec sourcegraph ssh -p 29418 admin@githost.example.com ``` The `url` field is then @@ -36,15 +36,15 @@ The `url` field is then "url": "ssh://$USER@$HOSTNAME:$PORT"` ``` -Here's an example for Gerrit: +Here's an example: ```json - "url": "ssh://admin@gerrit.example.com:29418", + "url": "ssh://admin@githost.example.com:29418", ``` ## Adding repositories -For Gerrit, elements of the `repos` field are the same as the repository names. For example, a repository at https://gerrit.example.com/admin/repos/gorilla/mux will be `"gorilla/mux"` in the `repos` field. +Elements of the `repos` field are the same as the repository names. For example, a repository at https://githost.example.com/admin/repos/gorilla/mux will be `"gorilla/mux"` in the `repos` field. Repositories must be listed individually: diff --git a/doc/admin/repo/permissions.md b/doc/admin/repo/permissions.md index 5340f1b6736a..a01da2101f01 100644 --- a/doc/admin/repo/permissions.md +++ b/doc/admin/repo/permissions.md @@ -5,6 +5,7 @@ Sourcegraph can be configured to enforce repository permissions from code hosts. - [GitHub / GitHub Enterprise](#github) - [GitLab](#gitlab) - [Bitbucket Server / Bitbucket Data Center](#bitbucket-server-bitbucket-data-center) +- [Gerrit](#gerrit) - [Unified SSO](https://unknwon.io/posts/200915_setup-sourcegraph-gitlab-keycloak/) - [Explicit permissions API](#explicit-permissions-api) @@ -262,6 +263,24 @@ By installing the [Bitbucket Server plugin](../../../integration/bitbucket_serve
+## Gerrit + +Prerequisite: [Add Gerrit as an authentication provider](../auth/index.md#gerrit). + +Then, [add or edit a Gerrit connection](../external_service/gerrit.md) and include the `authorization` field: + +```json +{ + // The Gerrit URL used to set up the Gerrit authentication provider must match this URL. + "url": "https://gerrit.example.com", + "username": "", + "password": "", + "authorization": {} +} +``` + +
+ ## Background permissions syncing Sourcegraph 3.17+ diff --git a/enterprise/cmd/frontend/internal/auth/gerrit/config.go b/enterprise/cmd/frontend/internal/auth/gerrit/config.go new file mode 100644 index 000000000000..a49b5372f689 --- /dev/null +++ b/enterprise/cmd/frontend/internal/auth/gerrit/config.go @@ -0,0 +1,94 @@ +package gerrit + +import ( + "context" + "fmt" + + "github.com/sourcegraph/sourcegraph/cmd/frontend/auth/providers" + "github.com/sourcegraph/sourcegraph/internal/conf" + "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" + "github.com/sourcegraph/sourcegraph/internal/extsvc" + "github.com/sourcegraph/sourcegraph/internal/extsvc/gerrit" + "github.com/sourcegraph/sourcegraph/schema" +) + +func Init() { + const pkgName = "gerrit" + conf.ContributeValidator(func(cfg conftypes.SiteConfigQuerier) conf.Problems { + _, problems := parseConfig(cfg) + return problems + }) + + go conf.Watch(func() { + newProviders, _ := parseConfig(conf.Get()) + newProviderList := make([]providers.Provider, len(newProviders)) + for i := range newProviders { + newProviderList[i] = &newProviders[i] + } + providers.Update(pkgName, newProviderList) + }) +} + +type Provider struct { + ServiceID string + ServiceType string +} + +func parseConfig(cfg conftypes.SiteConfigQuerier) (ps []Provider, problems conf.Problems) { + seen := make(map[string]struct{}) + for _, pr := range cfg.SiteConfig().AuthProviders { + if pr.Gerrit == nil { + continue + } + + provider := parseProvider(pr.Gerrit) + if _, ok := seen[provider.ServiceID]; !ok { + ps = append(ps, provider) + seen[provider.ServiceID] = struct{}{} + } else { + problems = append(problems, conf.NewSiteProblem(fmt.Sprintf("Cannot have more than one auth provider with url %q", provider.ServiceID))) + } + } + + return ps, problems +} + +func parseProvider(p *schema.GerritAuthProvider) Provider { + return Provider{ + ServiceID: p.Url, + ServiceType: p.Type, + } +} + +func (p *Provider) ConfigID() providers.ConfigID { + return providers.ConfigID{ + Type: extsvc.TypeGerrit, + ID: p.ServiceID, + } +} + +func (p *Provider) Config() schema.AuthProviders { + return schema.AuthProviders{ + Gerrit: &schema.GerritAuthProvider{ + Type: p.ServiceType, + Url: p.ServiceID, + }, + } +} + +func (p *Provider) CachedInfo() *providers.Info { + return &providers.Info{ + ServiceID: p.ServiceID, + ClientID: "", + DisplayName: "Gerrit", + AuthenticationURL: p.ServiceID, + } +} + +func (p *Provider) Refresh(ctx context.Context) error { + return nil +} + +func (p *Provider) ExternalAccountInfo(ctx context.Context, account extsvc.Account) (*extsvc.PublicAccountData, error) { + return gerrit.GetPublicExternalAccountData(ctx, &account.AccountData) +} diff --git a/enterprise/cmd/frontend/internal/auth/gerrit/config_test.go b/enterprise/cmd/frontend/internal/auth/gerrit/config_test.go new file mode 100644 index 000000000000..f6f4c91d0ef0 --- /dev/null +++ b/enterprise/cmd/frontend/internal/auth/gerrit/config_test.go @@ -0,0 +1,102 @@ +package gerrit + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/sourcegraph/sourcegraph/internal/conf" + "github.com/sourcegraph/sourcegraph/internal/extsvc" + "github.com/sourcegraph/sourcegraph/schema" +) + +func TestParseConfig(t *testing.T) { + testCases := map[string]struct { + cfg *conf.Unified + wantProviders []Provider + wantProblems []string + }{ + "no configs": { + cfg: &conf.Unified{}, + wantProviders: []Provider(nil), + }, + "1 gerrit config": { + cfg: &conf.Unified{SiteConfiguration: schema.SiteConfiguration{ + AuthProviders: []schema.AuthProviders{{ + Gerrit: &schema.GerritAuthProvider{ + Url: "https://gerrit.example.com", + Type: extsvc.TypeGerrit, + }, + }}, + }}, + wantProviders: []Provider{{ + ServiceID: "https://gerrit.example.com", + ServiceType: extsvc.TypeGerrit, + }}, + }, + "2 gerrit configs with same URL causes conflict": { + cfg: &conf.Unified{SiteConfiguration: schema.SiteConfiguration{ + AuthProviders: []schema.AuthProviders{ + { + Gerrit: &schema.GerritAuthProvider{ + Url: "https://gerrit.example.com", + Type: extsvc.TypeGerrit, + }, + }, + { + Gerrit: &schema.GerritAuthProvider{ + Url: "https://gerrit.example.com", + Type: extsvc.TypeGerrit, + }, + }, + }, + }}, + wantProviders: []Provider{{ + ServiceID: "https://gerrit.example.com", + ServiceType: extsvc.TypeGerrit, + }}, + wantProblems: []string{ + `Cannot have more than one auth provider with url "https://gerrit.example.com"`, + }, + }, + "2 gerrit configs with different URLs is okay": { + cfg: &conf.Unified{SiteConfiguration: schema.SiteConfiguration{ + AuthProviders: []schema.AuthProviders{ + { + Gerrit: &schema.GerritAuthProvider{ + Url: "https://gerrit.example.com", + Type: extsvc.TypeGerrit, + }, + }, + { + Gerrit: &schema.GerritAuthProvider{ + Url: "https://gerrit.different.com", + Type: extsvc.TypeGerrit, + }, + }, + }, + }}, + wantProviders: []Provider{ + { + ServiceID: "https://gerrit.example.com", + ServiceType: extsvc.TypeGerrit, + }, + { + ServiceID: "https://gerrit.different.com", + ServiceType: extsvc.TypeGerrit, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + gotProviders, gotProblems := parseConfig(tc.cfg) + if diff := cmp.Diff(tc.wantProviders, gotProviders); diff != "" { + t.Errorf("providers mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tc.wantProblems, gotProblems.Messages()); diff != "" { + t.Errorf("problems mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/enterprise/cmd/frontend/internal/auth/init.go b/enterprise/cmd/frontend/internal/auth/init.go index 6abfb125c332..0d89223c1780 100644 --- a/enterprise/cmd/frontend/internal/auth/init.go +++ b/enterprise/cmd/frontend/internal/auth/init.go @@ -13,6 +13,7 @@ import ( "github.com/sourcegraph/sourcegraph/cmd/frontend/external/app" "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/bitbucketcloudoauth" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/gerrit" "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/githuboauth" "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/gitlaboauth" "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/httpheader" @@ -35,6 +36,7 @@ func Init(logger log.Logger, db database.DB) { githuboauth.Init(logger, db) gitlaboauth.Init(logger, db) bitbucketcloudoauth.Init(logger, db) + gerrit.Init() // Register enterprise auth middleware auth.RegisterMiddlewares( diff --git a/enterprise/internal/authz/authz.go b/enterprise/internal/authz/authz.go index 60cf58fb4d1e..395bdc3243dd 100644 --- a/enterprise/internal/authz/authz.go +++ b/enterprise/internal/authz/authz.go @@ -11,6 +11,7 @@ import ( "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/bitbucketcloud" "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/bitbucketserver" + "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/gerrit" "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/github" "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/gitlab" "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/perforce" @@ -63,6 +64,7 @@ func ProvidersFromConfig( extsvc.KindBitbucketServer, extsvc.KindBitbucketCloud, extsvc.KindPerforce, + extsvc.KindGerrit, }, LimitOffset: &database.LimitOffset{ Limit: 500, // The number is randomly chosen @@ -75,6 +77,7 @@ func ProvidersFromConfig( bitbucketServerConns []*types.BitbucketServerConnection perforceConns []*types.PerforceConnection bitbucketCloudConns []*types.BitbucketCloudConnection + gerritConns []*types.GerritConnection ) for { svcs, err := store.List(ctx, opt) @@ -129,6 +132,11 @@ func ProvidersFromConfig( URN: svc.URN(), PerforceConnection: c, }) + case *schema.GerritConnection: + gerritConns = append(gerritConns, &types.GerritConnection{ + URN: svc.URN(), + GerritConnection: c, + }) default: logger.Error("ProvidersFromConfig", log.Error(errors.Errorf("unexpected connection type: %T", cfg))) continue @@ -140,69 +148,36 @@ func ProvidersFromConfig( } } - if len(gitHubConns) > 0 { - enableGithubInternalRepoVisibility := false - ef := cfg.SiteConfig().ExperimentalFeatures - if ef != nil { - enableGithubInternalRepoVisibility = ef.EnableGithubInternalRepoVisibility - } - - ghProviders, ghProblems, ghWarnings, ghInvalidConnections := github.NewAuthzProviders(db, gitHubConns, cfg.SiteConfig().AuthProviders, enableGithubInternalRepoVisibility) - providers = append(providers, ghProviders...) - seriousProblems = append(seriousProblems, ghProblems...) - warnings = append(warnings, ghWarnings...) - invalidConnections = append(invalidConnections, ghInvalidConnections...) - } - - if len(gitLabConns) > 0 { - glProviders, glProblems, glWarnings, glInvalidConnections := gitlab.NewAuthzProviders(db, cfg.SiteConfig(), gitLabConns) - providers = append(providers, glProviders...) - seriousProblems = append(seriousProblems, glProblems...) - warnings = append(warnings, glWarnings...) - invalidConnections = append(invalidConnections, glInvalidConnections...) + enableGithubInternalRepoVisibility := false + ef := cfg.SiteConfig().ExperimentalFeatures + if ef != nil { + enableGithubInternalRepoVisibility = ef.EnableGithubInternalRepoVisibility } - if len(bitbucketServerConns) > 0 { - bbsProviders, bbsProblems, bbsWarnings, bbsInvalidConnections := bitbucketserver.NewAuthzProviders(bitbucketServerConns) - providers = append(providers, bbsProviders...) - seriousProblems = append(seriousProblems, bbsProblems...) - warnings = append(warnings, bbsWarnings...) - invalidConnections = append(invalidConnections, bbsInvalidConnections...) - } - - if len(perforceConns) > 0 { - pfProviders, pfProblems, pfWarnings, pfInvalidConnections := perforce.NewAuthzProviders(perforceConns) - providers = append(providers, pfProviders...) - seriousProblems = append(seriousProblems, pfProblems...) - warnings = append(warnings, pfWarnings...) - invalidConnections = append(invalidConnections, pfInvalidConnections...) - } - - if len(bitbucketCloudConns) > 0 { - bbcloudProviders, bbcloudProblems, bbcloudWarnings, bbcloudInvalidConnections := bitbucketcloud.NewAuthzProviders(db, bitbucketCloudConns, cfg.SiteConfig().AuthProviders) - providers = append(providers, bbcloudProviders...) - seriousProblems = append(seriousProblems, bbcloudProblems...) - warnings = append(warnings, bbcloudWarnings...) - invalidConnections = append(invalidConnections, bbcloudInvalidConnections...) - } + initResult := github.NewAuthzProviders(db, gitHubConns, cfg.SiteConfig().AuthProviders, enableGithubInternalRepoVisibility) + initResult.Append(gitlab.NewAuthzProviders(db, cfg.SiteConfig(), gitLabConns)) + initResult.Append(bitbucketserver.NewAuthzProviders(bitbucketServerConns)) + initResult.Append(perforce.NewAuthzProviders(perforceConns)) + initResult.Append(bitbucketcloud.NewAuthzProviders(db, bitbucketCloudConns, cfg.SiteConfig().AuthProviders)) + initResult.Append(gerrit.NewAuthzProviders(gerritConns, cfg.SiteConfig().AuthProviders)) // 🚨 SECURITY: Warn the admin when both code host authz provider and the permissions user mapping are configured. if cfg.SiteConfig().PermissionsUserMapping != nil && cfg.SiteConfig().PermissionsUserMapping.Enabled { allowAccessByDefault = false - if len(providers) > 0 { - serviceTypes := make([]string, len(providers)) - for i := range providers { - serviceTypes[i] = strconv.Quote(providers[i].ServiceType()) + if len(initResult.Providers) > 0 { + serviceTypes := make([]string, len(initResult.Providers)) + for i := range initResult.Providers { + serviceTypes[i] = strconv.Quote(initResult.Providers[i].ServiceType()) } msg := fmt.Sprintf( "The permissions user mapping (site configuration `permissions.userMapping`) cannot be enabled when %s authorization providers are in use. Blocking access to all repositories until the conflict is resolved.", strings.Join(serviceTypes, ", ")) - seriousProblems = append(seriousProblems, msg) + initResult.Problems = append(initResult.Problems, msg) } } - return allowAccessByDefault, providers, seriousProblems, warnings, invalidConnections + return allowAccessByDefault, initResult.Providers, initResult.Problems, initResult.Warnings, initResult.InvalidConnections } func RefreshInterval() time.Duration { diff --git a/enterprise/internal/authz/authz_test.go b/enterprise/internal/authz/authz_test.go index 13c1ae8882dd..ac04f7a45d95 100644 --- a/enterprise/internal/authz/authz_test.go +++ b/enterprise/internal/authz/authz_test.go @@ -489,7 +489,7 @@ func TestAuthzProvidersFromConfig(t *testing.T) { Config: extsvc.NewUnencryptedConfig(mustMarshalJSONString(bbs)), }) } - case extsvc.KindGitHub, extsvc.KindPerforce, extsvc.KindBitbucketCloud: + case extsvc.KindGitHub, extsvc.KindPerforce, extsvc.KindBitbucketCloud, extsvc.KindGerrit: default: return nil, errors.Errorf("unexpected kind: %s", kind) } @@ -522,6 +522,7 @@ func TestAuthzProvidersEnabledACLsDisabled(t *testing.T) { githubConnections []*schema.GitHubConnection perforceConnections []*schema.PerforceConnection bitbucketCloudConnections []*schema.BitbucketCloudConnection + gerritConnections []*schema.GerritConnection expInvalidConnections []string expSeriousProblems []string @@ -615,6 +616,20 @@ func TestAuthzProvidersEnabledACLsDisabled(t *testing.T) { expSeriousProblems: []string{"failed"}, expInvalidConnections: []string{"bitbucketCloud"}, }, + { + description: "Gerrit connection with authz enabled but missing license for ACLs", + cfg: conf.Unified{}, + gerritConnections: []*schema.GerritConnection{ + { + Authorization: &schema.GerritAuthorization{}, + Url: "https://gerrit.sgdev.org", + Username: "admin", + Password: "secret-password", + }, + }, + expSeriousProblems: []string{"failed"}, + expInvalidConnections: []string{"gerrit"}, + }, { description: "Perforce connection with authz enabled but missing license for ACLs", cfg: conf.Unified{}, @@ -676,6 +691,13 @@ func TestAuthzProvidersEnabledACLsDisabled(t *testing.T) { Config: extsvc.NewUnencryptedConfig(mustMarshalJSONString(bbcloud)), }) } + case extsvc.KindGerrit: + for _, g := range test.gerritConnections { + svcs = append(svcs, &types.ExternalService{ + Kind: kind, + Config: extsvc.NewUnencryptedConfig(mustMarshalJSONString(g)), + }) + } case extsvc.KindPerforce: for _, pf := range test.perforceConnections { svcs = append(svcs, &types.ExternalService{ diff --git a/enterprise/internal/authz/bitbucketcloud/authz.go b/enterprise/internal/authz/bitbucketcloud/authz.go index 932b7fe29210..8593ce1a3677 100644 --- a/enterprise/internal/authz/bitbucketcloud/authz.go +++ b/enterprise/internal/authz/bitbucketcloud/authz.go @@ -6,6 +6,7 @@ import ( "github.com/sourcegraph/sourcegraph/enterprise/internal/licensing" + atypes "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/types" "github.com/sourcegraph/sourcegraph/internal/authz" "github.com/sourcegraph/sourcegraph/internal/database" "github.com/sourcegraph/sourcegraph/internal/extsvc" @@ -23,7 +24,8 @@ import ( // This constructor does not and should not directly check connectivity to external services - if // desired, callers should use `(*Provider).ValidateConnection` directly to get warnings related // to connection issues. -func NewAuthzProviders(db database.DB, conns []*types.BitbucketCloudConnection, authProviders []schema.AuthProviders) (ps []authz.Provider, problems []string, warnings []string, invalidConnections []string) { +func NewAuthzProviders(db database.DB, conns []*types.BitbucketCloudConnection, authProviders []schema.AuthProviders) *atypes.ProviderInitResult { + initResults := &atypes.ProviderInitResult{} bbcloudAuthProviders := make(map[string]*schema.BitbucketCloudAuthProvider) for _, p := range authProviders { if p.Bitbucketcloud != nil { @@ -44,15 +46,15 @@ func NewAuthzProviders(db database.DB, conns []*types.BitbucketCloudConnection, for _, c := range conns { p, err := newAuthzProvider(db, c) if err != nil { - invalidConnections = append(invalidConnections, extsvc.TypeBitbucketCloud) - problems = append(problems, err.Error()) + initResults.InvalidConnections = append(initResults.InvalidConnections, extsvc.TypeBitbucketCloud) + initResults.Problems = append(initResults.Problems, err.Error()) } if p == nil { continue } if _, exists := bbcloudAuthProviders[p.ServiceID()]; !exists { - warnings = append(warnings, + initResults.Warnings = append(initResults.Warnings, fmt.Sprintf("Bitbucket Cloud config for %[1]s has `authorization` enabled, "+ "but no authentication provider matching %[1]q was found. "+ "Check the [**site configuration**](/site-admin/configuration) to "+ @@ -60,10 +62,10 @@ func NewAuthzProviders(db database.DB, conns []*types.BitbucketCloudConnection, p.ServiceID())) } - ps = append(ps, p) + initResults.Providers = append(initResults.Providers, p) } - return ps, problems, warnings, invalidConnections + return initResults } func newAuthzProvider( diff --git a/enterprise/internal/authz/bitbucketcloud/authz_test.go b/enterprise/internal/authz/bitbucketcloud/authz_test.go index 3fa6a1567f10..8d536d34fbca 100644 --- a/enterprise/internal/authz/bitbucketcloud/authz_test.go +++ b/enterprise/internal/authz/bitbucketcloud/authz_test.go @@ -16,7 +16,7 @@ func TestNewAuthzProviders(t *testing.T) { db := database.NewMockDB() db.ExternalServicesFunc.SetDefaultReturn(database.NewMockExternalServiceStore()) t.Run("no authorization", func(t *testing.T) { - providers, problems, warnings, invalidConnections := NewAuthzProviders( + initResults := NewAuthzProviders( db, []*types.BitbucketCloudConnection{{ BitbucketCloudConnection: &schema.BitbucketCloudConnection{ @@ -28,15 +28,15 @@ func TestNewAuthzProviders(t *testing.T) { assert := assert.New(t) - assert.Len(providers, 0, "unexpected a providers: %+v", providers) - assert.Len(problems, 0, "unexpected problems: %+v", problems) - assert.Len(warnings, 0, "unexpected warnings: %+v", warnings) - assert.Len(invalidConnections, 0, "unexpected invalidConnections: %+v", invalidConnections) + assert.Len(initResults.Providers, 0, "unexpected a providers: %+v", initResults.Providers) + assert.Len(initResults.Problems, 0, "unexpected problems: %+v", initResults.Problems) + assert.Len(initResults.Warnings, 0, "unexpected warnings: %+v", initResults.Warnings) + assert.Len(initResults.InvalidConnections, 0, "unexpected invalidConnections: %+v", initResults.InvalidConnections) }) t.Run("no matching auth provider", func(t *testing.T) { licensing.MockCheckFeatureError("") - providers, problems, warnings, invalidConnections := NewAuthzProviders( + initResults := NewAuthzProviders( db, []*types.BitbucketCloudConnection{ { @@ -49,20 +49,20 @@ func TestNewAuthzProviders(t *testing.T) { []schema.AuthProviders{{Bitbucketcloud: &schema.BitbucketCloudAuthProvider{}}}, ) - require.Len(t, providers, 1, "expect exactly one provider") - assert.NotNil(t, providers[0]) + require.Len(t, initResults.Providers, 1, "expect exactly one provider") + assert.NotNil(t, initResults.Providers[0]) - assert.Empty(t, problems) - assert.Empty(t, invalidConnections) + assert.Empty(t, initResults.Problems) + assert.Empty(t, initResults.InvalidConnections) - require.Len(t, warnings, 1, "expect exactly one warning") - assert.Contains(t, warnings[0], "no authentication provider") + require.Len(t, initResults.Warnings, 1, "expect exactly one warning") + assert.Contains(t, initResults.Warnings[0], "no authentication provider") }) t.Run("matching auth provider found", func(t *testing.T) { t.Run("default case", func(t *testing.T) { licensing.MockCheckFeatureError("") - providers, problems, warnings, invalidConnections := NewAuthzProviders( + initResults := NewAuthzProviders( db, []*types.BitbucketCloudConnection{ { @@ -75,17 +75,17 @@ func TestNewAuthzProviders(t *testing.T) { []schema.AuthProviders{{Bitbucketcloud: &schema.BitbucketCloudAuthProvider{}}}, ) - require.Len(t, providers, 1, "expect exactly one provider") - assert.NotNil(t, providers[0]) + require.Len(t, initResults.Providers, 1, "expect exactly one provider") + assert.NotNil(t, initResults.Providers[0]) - assert.Empty(t, problems) - assert.Empty(t, warnings) - assert.Empty(t, invalidConnections) + assert.Empty(t, initResults.Problems) + assert.Empty(t, initResults.Warnings) + assert.Empty(t, initResults.InvalidConnections) }) t.Run("license does not have ACLs feature", func(t *testing.T) { licensing.MockCheckFeatureError("failed") - providers, problems, warnings, invalidConnections := NewAuthzProviders( + initResults := NewAuthzProviders( db, []*types.BitbucketCloudConnection{ { @@ -100,10 +100,10 @@ func TestNewAuthzProviders(t *testing.T) { expectedError := []string{"failed"} expInvalidConnectionErr := []string{"bitbucketCloud"} - assert.Equal(t, expectedError, problems) - assert.Equal(t, expInvalidConnectionErr, invalidConnections) - assert.Empty(t, providers) - assert.Empty(t, warnings) + assert.Equal(t, expectedError, initResults.Problems) + assert.Equal(t, expInvalidConnectionErr, initResults.InvalidConnections) + assert.Empty(t, initResults.Providers) + assert.Empty(t, initResults.Warnings) }) }) } diff --git a/enterprise/internal/authz/bitbucketserver/authz.go b/enterprise/internal/authz/bitbucketserver/authz.go index 8058a1baa8af..1eab0a376ea5 100644 --- a/enterprise/internal/authz/bitbucketserver/authz.go +++ b/enterprise/internal/authz/bitbucketserver/authz.go @@ -1,6 +1,7 @@ package bitbucketserver import ( + atypes "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/types" "github.com/sourcegraph/sourcegraph/enterprise/internal/licensing" "github.com/sourcegraph/sourcegraph/internal/authz" "github.com/sourcegraph/sourcegraph/internal/conf" @@ -22,20 +23,21 @@ import ( // to connection issues. func NewAuthzProviders( conns []*types.BitbucketServerConnection, -) (ps []authz.Provider, problems []string, warnings []string, invalidConnections []string) { +) *atypes.ProviderInitResult { + initResults := &atypes.ProviderInitResult{} // Authorization (i.e., permissions) providers for _, c := range conns { pluginPerm := conf.BitbucketServerPluginPerm() || (c.Plugin != nil && c.Plugin.Permissions == "enabled") p, err := newAuthzProvider(c, pluginPerm) if err != nil { - invalidConnections = append(invalidConnections, extsvc.TypeBitbucketServer) - problems = append(problems, err.Error()) + initResults.InvalidConnections = append(initResults.InvalidConnections, extsvc.TypeBitbucketServer) + initResults.Problems = append(initResults.Problems, err.Error()) } else if p != nil { - ps = append(ps, p) + initResults.Providers = append(initResults.Providers, p) } } - return ps, problems, warnings, invalidConnections + return initResults } func newAuthzProvider( diff --git a/enterprise/internal/authz/gerrit/authz.go b/enterprise/internal/authz/gerrit/authz.go index 94a9db93d0c3..1e7b50f8126b 100644 --- a/enterprise/internal/authz/gerrit/authz.go +++ b/enterprise/internal/authz/gerrit/authz.go @@ -1,17 +1,61 @@ package gerrit import ( - "github.com/sourcegraph/sourcegraph/internal/authz" + "fmt" + "net/url" + + atypes "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/types" + "github.com/sourcegraph/sourcegraph/enterprise/internal/licensing" + "github.com/sourcegraph/sourcegraph/internal/extsvc" "github.com/sourcegraph/sourcegraph/internal/types" + "github.com/sourcegraph/sourcegraph/schema" ) // NewAuthzProviders returns the set of Gerrit authz providers derived from the connections. -func NewAuthzProviders(conns []*types.GerritConnection) (ps []authz.Provider) { +func NewAuthzProviders(conns []*types.GerritConnection, authProviders []schema.AuthProviders) *atypes.ProviderInitResult { + initResults := &atypes.ProviderInitResult{} + gerritAuthProviders := make(map[string]*schema.GerritAuthProvider) + for _, p := range authProviders { + if p.Gerrit == nil { + continue + } + + gerritURL, err := url.Parse(p.Gerrit.Url) + if err != nil { + continue + } + + // Use normalised base URL as ID. + gerritAuthProviders[extsvc.NormalizeBaseURL(gerritURL).String()] = p.Gerrit + } + for _, c := range conns { - p, _ := NewProvider(c) + if c.Authorization == nil { + // No authorization required + continue + } + if err := licensing.Check(licensing.FeatureACLs); err != nil { + initResults.InvalidConnections = append(initResults.InvalidConnections, extsvc.TypeGerrit) + initResults.Problems = append(initResults.Problems, err.Error()) + continue + } + p, err := NewProvider(c) + if err != nil { + initResults.InvalidConnections = append(initResults.InvalidConnections, extsvc.TypeGerrit) + initResults.Problems = append(initResults.Problems, err.Error()) + } if p != nil { - ps = append(ps, p) + initResults.Providers = append(initResults.Providers, p) + + if _, exists := gerritAuthProviders[p.ServiceID()]; !exists { + initResults.Warnings = append(initResults.Warnings, + fmt.Sprintf("Gerrit config for %[1]s has `authorization` enabled, "+ + "but no authentication provider matching %[1]q was found. "+ + "Check the [**site configuration**](/site-admin/configuration) to "+ + "verify an entry in [`auth.providers`](https://docs.sourcegraph.com/admin/auth) exists for %[1]s.", + p.ServiceID())) + } } } - return ps + return initResults } diff --git a/enterprise/internal/authz/gerrit/client.go b/enterprise/internal/authz/gerrit/client.go index 14392f6bcebc..ef60aa9fe401 100644 --- a/enterprise/internal/authz/gerrit/client.go +++ b/enterprise/internal/authz/gerrit/client.go @@ -2,14 +2,17 @@ package gerrit import ( "context" + "net/url" + "github.com/sourcegraph/sourcegraph/internal/extsvc/auth" "github.com/sourcegraph/sourcegraph/internal/extsvc/gerrit" + "github.com/sourcegraph/sourcegraph/internal/httpcli" ) type client interface { - ListAccountsByEmail(ctx context.Context, email string) (gerrit.ListAccountsResponse, error) - ListAccountsByUsername(ctx context.Context, username string) (gerrit.ListAccountsResponse, error) + ListProjects(ctx context.Context, opts gerrit.ListProjectsArgs) (gerrit.ListProjectsResponse, bool, error) GetGroup(ctx context.Context, groupName string) (gerrit.Group, error) + WithAuthenticator(a auth.Authenticator) client } var _ client = (*ClientAdapter)(nil) @@ -19,24 +22,31 @@ type ClientAdapter struct { *gerrit.Client } -type mockClient struct { - mockListAccountsByEmail func(ctx context.Context, email string) (gerrit.ListAccountsResponse, error) - mockListAccountsByUsername func(ctx context.Context, username string) (gerrit.ListAccountsResponse, error) - mockGetGroup func(ctx context.Context, groupName string) (gerrit.Group, error) +// NewClient creates a new Gerrit client and wraps it in a ClientAdapter. +func NewClient(urn string, baseURL *url.URL, creds *gerrit.AccountCredentials, httpClient httpcli.Doer) (client, error) { + c, err := gerrit.NewClient(urn, baseURL, creds, httpClient) + if err != nil { + return nil, err + } + return &ClientAdapter{c}, nil } -func (m *mockClient) ListAccountsByEmail(ctx context.Context, email string) (gerrit.ListAccountsResponse, error) { - if m.mockListAccountsByEmail != nil { - return m.mockListAccountsByEmail(ctx, email) - } - return nil, nil +// WithAuthenticator returns a new ClientAdapter with the given authenticator. +func (m *ClientAdapter) WithAuthenticator(a auth.Authenticator) client { + return &ClientAdapter{m.Client.WithAuthenticator(a)} } -func (m *mockClient) ListAccountsByUsername(ctx context.Context, username string) (gerrit.ListAccountsResponse, error) { - if m.mockListAccountsByUsername != nil { - return m.mockListAccountsByUsername(ctx, username) +type mockClient struct { + mockListProjects func(ctx context.Context, opts gerrit.ListProjectsArgs) (gerrit.ListProjectsResponse, bool, error) + mockGetGroup func(ctx context.Context, groupName string) (gerrit.Group, error) +} + +func (m *mockClient) ListProjects(ctx context.Context, opts gerrit.ListProjectsArgs) (gerrit.ListProjectsResponse, bool, error) { + if m.mockListProjects != nil { + return m.mockListProjects(ctx, opts) } - return nil, nil + + return nil, false, nil } func (m *mockClient) GetGroup(ctx context.Context, groupName string) (gerrit.Group, error) { @@ -45,3 +55,7 @@ func (m *mockClient) GetGroup(ctx context.Context, groupName string) (gerrit.Gro } return gerrit.Group{}, nil } + +func (m *mockClient) WithAuthenticator(a auth.Authenticator) client { + return m +} diff --git a/enterprise/internal/authz/gerrit/gerrit.go b/enterprise/internal/authz/gerrit/gerrit.go index 7dc1ca37514c..27c929353d32 100644 --- a/enterprise/internal/authz/gerrit/gerrit.go +++ b/enterprise/internal/authz/gerrit/gerrit.go @@ -2,13 +2,11 @@ package gerrit import ( "context" - "encoding/json" "net/url" - jsoniter "github.com/json-iterator/go" - "github.com/sourcegraph/sourcegraph/internal/authz" "github.com/sourcegraph/sourcegraph/internal/extsvc" + "github.com/sourcegraph/sourcegraph/internal/extsvc/auth" "github.com/sourcegraph/sourcegraph/internal/extsvc/gerrit" "github.com/sourcegraph/sourcegraph/internal/types" "github.com/sourcegraph/sourcegraph/lib/errors" @@ -29,7 +27,10 @@ func NewProvider(conn *types.GerritConnection) (*Provider, error) { if err != nil { return nil, err } - gClient, err := gerrit.NewClient(conn.URN, conn.GerritConnection, nil) + gClient, err := NewClient(conn.URN, baseURL, &gerrit.AccountCredentials{ + Username: conn.Username, + Password: conn.Password, + }, nil) if err != nil { return nil, err } @@ -40,80 +41,57 @@ func NewProvider(conn *types.GerritConnection) (*Provider, error) { }, nil } +// FetchAccount is unused for Gerrit. Users need to provide their own account +// credentials instead. func (p Provider) FetchAccount(ctx context.Context, user *types.User, current []*extsvc.Account, verifiedEmails []string) (*extsvc.Account, error) { - // First try to fetch Gerrit account for this username - accts, err := p.client.ListAccountsByUsername(ctx, user.Username) + return nil, nil +} + +func (p Provider) FetchUserPerms(ctx context.Context, account *extsvc.Account, opts authz.FetchPermsOptions) (*authz.ExternalUserPermissions, error) { + if account == nil { + return nil, errors.New("no gerrit account provided") + } else if !extsvc.IsHostOfAccount(p.codeHost, account) { + return nil, errors.Errorf("not a code host of the account: want %q but have %q", + account.AccountSpec.ServiceID, p.codeHost.ServiceID) + } else if account.AccountData.Data == nil || account.AccountData.AuthData == nil { + return nil, errors.New("no account data") + } + + credentials, err := gerrit.GetExternalAccountCredentials(ctx, &account.AccountData) if err != nil { return nil, err } - // Check that this account from Gerrit correlates to a verified email - if acct, found, err := p.checkAccountsAgainstVerifiedEmails(accts, user, verifiedEmails); found && err == nil { - return acct, nil - } - // If no account was found via the user's Sourcegraph username, attempt to find an account via one of the verified emails. - for _, email := range verifiedEmails { - accts, err := p.client.ListAccountsByEmail(ctx, email) + client := p.client.WithAuthenticator(&auth.BasicAuth{ + Username: credentials.Username, + Password: credentials.Password, + }) + + queryArgs := gerrit.ListProjectsArgs{ + Cursor: &gerrit.Pagination{PerPage: 100, Page: 1}, + } + extIDs := []extsvc.RepoID{} + for { + projects, nextPage, err := client.ListProjects(ctx, queryArgs) if err != nil { return nil, err } - for _, acct := range accts { - return p.buildExtsvcAccount(acct, user, email) - } - } - return nil, nil -} + for _, project := range projects { + extIDs = append(extIDs, extsvc.RepoID(project.ID)) + } -func (p Provider) checkAccountsAgainstVerifiedEmails(accts gerrit.ListAccountsResponse, user *types.User, verifiedEmails []string) (*extsvc.Account, bool, error) { - if len(accts) == 0 { - return nil, false, nil - } - for _, email := range verifiedEmails { - for _, acct := range accts { - if acct.Email == email && acct.Username == user.Username { - foundAcct, err := p.buildExtsvcAccount(acct, user, email) - return foundAcct, true, err - } + if !nextPage { + break } + queryArgs.Cursor.Page++ } - return nil, false, nil -} -func (p Provider) buildExtsvcAccount(acct gerrit.Account, user *types.User, email string) (*extsvc.Account, error) { - acctData, err := marshalAccountData(acct.Username, acct.Email, acct.ID) - if err != nil { - return nil, errors.Wrap(err, "marshaling account data") - } - return &extsvc.Account{ - UserID: user.ID, - AccountSpec: extsvc.AccountSpec{ - ServiceType: p.codeHost.ServiceType, - ServiceID: p.codeHost.ServiceID, - AccountID: email, - }, - AccountData: extsvc.AccountData{ - Data: extsvc.NewUnencryptedData(acctData), - }, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, + return &authz.ExternalUserPermissions{ + Exacts: extIDs, }, nil } -func marshalAccountData(username, email string, acctID int32) (json.RawMessage, error) { - return jsoniter.Marshal( - gerrit.AccountData{ - Username: username, - Email: email, - AccountID: acctID, - }, - ) -} - -func (p Provider) FetchUserPerms(ctx context.Context, account *extsvc.Account, opts authz.FetchPermsOptions) (*authz.ExternalUserPermissions, error) { - return nil, &authz.ErrUnimplemented{Feature: "gerrit.FetchUserPerms"} -} - func (p Provider) FetchRepoPerms(ctx context.Context, repo *extsvc.Repository, opts authz.FetchPermsOptions) ([]extsvc.AccountID, error) { return nil, &authz.ErrUnimplemented{Feature: "gerrit.FetchRepoPerms"} } diff --git a/enterprise/internal/authz/gerrit/gerrit_test.go b/enterprise/internal/authz/gerrit/gerrit_test.go index b70cd3bbfed5..ea4913143476 100644 --- a/enterprise/internal/authz/gerrit/gerrit_test.go +++ b/enterprise/internal/authz/gerrit/gerrit_test.go @@ -8,68 +8,12 @@ import ( "github.com/google/go-cmp/cmp" + "github.com/sourcegraph/sourcegraph/internal/authz" "github.com/sourcegraph/sourcegraph/internal/extsvc" "github.com/sourcegraph/sourcegraph/internal/extsvc/gerrit" - "github.com/sourcegraph/sourcegraph/internal/types" "github.com/sourcegraph/sourcegraph/lib/errors" ) -func TestProvider_FetchAccount(t *testing.T) { - userEmail := "test-email@example.com" - userName := "test-user" - testCases := []struct { - name string - client mockClient - }{ - { - name: "no matching username but email match", - client: mockClient{ - mockListAccountsByEmail: func(ctx context.Context, email string) (gerrit.ListAccountsResponse, error) { - return []gerrit.Account{ - { - Email: userEmail, - }, - }, nil - }, - mockListAccountsByUsername: nil, - }, - }, - { - name: "username matches and email valid", - client: mockClient{ - mockListAccountsByEmail: nil, - mockListAccountsByUsername: func(ctx context.Context, username string) (gerrit.ListAccountsResponse, error) { - return []gerrit.Account{ - { - Email: userEmail, - Username: username, - }, - }, nil - }, - }, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - p := NewTestProvider(&tc.client) - user := types.User{ - Username: userName, - } - verifiedEmails := []string{ - userEmail, - } - acct, err := p.FetchAccount(context.Background(), &user, nil, verifiedEmails) - if err != nil { - t.Fatalf("error fetching account: %s", err) - } - if acct == nil { - t.Fatalf("account was nil") - } - // TODO: validate account - }) - } -} - func TestProvider_ValidateConnection(t *testing.T) { testCases := []struct { name string @@ -126,6 +70,101 @@ func TestProvider_ValidateConnection(t *testing.T) { } } +func TestProvider_FetchUserPerms(t *testing.T) { + accountData := extsvc.AccountData{} + err := gerrit.SetExternalAccountData(&accountData, &gerrit.Account{}, &gerrit.AccountCredentials{ + Username: "test-user", + Password: "test-password", + }) + if err != nil { + t.Fatal(err) + } + + mClient := mockClient{} + mClient.mockListProjects = func(ctx context.Context, opts gerrit.ListProjectsArgs) (gerrit.ListProjectsResponse, bool, error) { + resp := gerrit.ListProjectsResponse{ + "test-project": &gerrit.Project{ + ID: "test-project", + }, + } + + return resp, false, nil + } + + testCases := map[string]struct { + client mockClient + account *extsvc.Account + wantErr bool + wantPerms *authz.ExternalUserPermissions + }{ + "nil account gives error": { + client: mClient, + account: nil, + wantErr: true, + }, + "account of wrong service type gives error": { + client: mClient, + account: &extsvc.Account{ + AccountSpec: extsvc.AccountSpec{ + ServiceType: "github", + ServiceID: "https://gerrit.sgdev.org/", + }, + }, + wantErr: true, + }, + "account of wrong service id gives error": { + client: mClient, + account: &extsvc.Account{ + AccountSpec: extsvc.AccountSpec{ + ServiceType: "gerrit", + ServiceID: "https://github.sgdev.org/", + }, + }, + wantErr: true, + }, + "account with no data gives error": { + client: mClient, + account: &extsvc.Account{ + AccountSpec: extsvc.AccountSpec{ + ServiceType: "gerrit", + ServiceID: "https://gerrit.sgdev.org/", + }, + AccountData: extsvc.AccountData{}, + }, + wantErr: true, + }, + "correct account gives correct permissions": { + client: mClient, + account: &extsvc.Account{ + AccountSpec: extsvc.AccountSpec{ + ServiceType: "gerrit", + ServiceID: "https://gerrit.sgdev.org/", + }, + AccountData: accountData, + }, + wantPerms: &authz.ExternalUserPermissions{ + Exacts: []extsvc.RepoID{"test-project"}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + p := NewTestProvider(&tc.client) + perms, err := p.FetchUserPerms(context.Background(), tc.account, authz.FetchPermsOptions{}) + if err != nil && !tc.wantErr { + t.Fatalf("unexpected error: %s", err) + } + if err == nil && tc.wantErr { + t.Fatalf("expected error but got none") + } + if diff := cmp.Diff(perms, tc.wantPerms); diff != "" { + t.Fatalf("permissions did not match: %s", diff) + } + }) + } +} + func NewTestProvider(client client) *Provider { baseURL, _ := url.Parse("https://gerrit.sgdev.org") return &Provider{ diff --git a/enterprise/internal/authz/github/authz.go b/enterprise/internal/authz/github/authz.go index 3bac964631c0..a09d6517204f 100644 --- a/enterprise/internal/authz/github/authz.go +++ b/enterprise/internal/authz/github/authz.go @@ -6,8 +6,8 @@ import ( "strconv" "time" + atypes "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/types" "github.com/sourcegraph/sourcegraph/enterprise/internal/licensing" - "github.com/sourcegraph/sourcegraph/internal/authz" "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/database" "github.com/sourcegraph/sourcegraph/internal/extsvc" @@ -37,7 +37,8 @@ func NewAuthzProviders( conns []*ExternalConnection, authProviders []schema.AuthProviders, enableGithubInternalRepoVisibility bool, -) (ps []authz.Provider, problems []string, warnings []string, invalidConnections []string) { +) *atypes.ProviderInitResult { + initResults := &atypes.ProviderInitResult{} // Auth providers (i.e. login mechanisms) githubAuthProviders := make(map[string]*schema.GitHubAuthProvider) for _, p := range authProviders { @@ -60,8 +61,8 @@ func NewAuthzProviders( // Initialize authz (permissions) provider. p, err := newAuthzProvider(db, c) if err != nil { - invalidConnections = append(invalidConnections, extsvc.TypeGitHub) - problems = append(problems, err.Error()) + initResults.InvalidConnections = append(initResults.InvalidConnections, extsvc.TypeGitHub) + initResults.Problems = append(initResults.Problems, err.Error()) } if p == nil { continue @@ -75,7 +76,7 @@ func NewAuthzProviders( // Permissions require a corresponding GitHub OAuth provider. Without one, repos // with restricted permissions will not be visible to non-admins. if authProvider, exists := githubAuthProviders[p.ServiceID()]; !exists { - warnings = append(warnings, + initResults.Warnings = append(initResults.Warnings, fmt.Sprintf("GitHub config for %[1]s has `authorization` enabled, "+ "but no authentication provider matching %[1]q was found. "+ "Check the [**site configuration**](/site-admin/configuration) to "+ @@ -83,7 +84,7 @@ func NewAuthzProviders( p.ServiceID())) } else if p.groupsCache != nil && !authProvider.AllowGroupsPermissionsSync { // Groups permissions requires auth provider to request the correct scopes. - warnings = append(warnings, + initResults.Warnings = append(initResults.Warnings, fmt.Sprintf("GitHub config for %[1]s has `authorization.groupsCacheTTL` enabled, but "+ "the authentication provider matching %[1]q does not have `allowGroupsPermissionsSync` enabled. "+ "Update the [**site configuration**](/site-admin/configuration) in the appropriate entry "+ @@ -94,10 +95,10 @@ func NewAuthzProviders( } // Register this provider. - ps = append(ps, p) + initResults.Providers = append(initResults.Providers, p) } - return ps, problems, warnings, invalidConnections + return initResults } // newAuthzProvider instantiates a provider, or returns nil if authorization is disabled. diff --git a/enterprise/internal/authz/github/authz_test.go b/enterprise/internal/authz/github/authz_test.go index afa855cc567f..90ba531f2e7a 100644 --- a/enterprise/internal/authz/github/authz_test.go +++ b/enterprise/internal/authz/github/authz_test.go @@ -20,7 +20,7 @@ func TestNewAuthzProviders(t *testing.T) { db := database.NewMockDB() db.ExternalServicesFunc.SetDefaultReturn(database.NewMockExternalServiceStore()) t.Run("no authorization", func(t *testing.T) { - providers, problems, warnings, invalidConnections := NewAuthzProviders( + initResults := NewAuthzProviders( db, []*ExternalConnection{ { @@ -39,15 +39,15 @@ func TestNewAuthzProviders(t *testing.T) { assert := assert.New(t) - assert.Len(providers, 0, "unexpected a providers: %+v", providers) - assert.Len(problems, 0, "unexpected problems: %+v", problems) - assert.Len(warnings, 0, "unexpected warnings: %+v", warnings) - assert.Len(invalidConnections, 0, "unexpected invalidConnections: %+v", invalidConnections) + assert.Len(initResults.Providers, 0, "unexpected a providers: %+v", initResults.Providers) + assert.Len(initResults.Problems, 0, "unexpected problems: %+v", initResults.Problems) + assert.Len(initResults.Warnings, 0, "unexpected warnings: %+v", initResults.Warnings) + assert.Len(initResults.InvalidConnections, 0, "unexpected invalidConnections: %+v", initResults.InvalidConnections) }) t.Run("no matching auth provider", func(t *testing.T) { licensing.MockCheckFeatureError("") - providers, problems, warnings, invalidConnections := NewAuthzProviders( + initResults := NewAuthzProviders( db, []*ExternalConnection{ { @@ -68,20 +68,20 @@ func TestNewAuthzProviders(t *testing.T) { false, ) - require.Len(t, providers, 1, "expect exactly one provider") - assert.NotNil(t, providers[0]) + require.Len(t, initResults.Providers, 1, "expect exactly one provider") + assert.NotNil(t, initResults.Providers[0]) - assert.Empty(t, problems) - assert.Empty(t, invalidConnections) + assert.Empty(t, initResults.Problems) + assert.Empty(t, initResults.InvalidConnections) - require.Len(t, warnings, 1, "expect exactly one warning") - assert.Contains(t, warnings[0], "no authentication provider") + require.Len(t, initResults.Warnings, 1, "expect exactly one warning") + assert.Contains(t, initResults.Warnings[0], "no authentication provider") }) t.Run("matching auth provider found", func(t *testing.T) { t.Run("default case", func(t *testing.T) { licensing.MockCheckFeatureError("") - providers, problems, warnings, invalidConnections := NewAuthzProviders( + initResults := NewAuthzProviders( db, []*ExternalConnection{ { @@ -101,17 +101,17 @@ func TestNewAuthzProviders(t *testing.T) { false, ) - require.Len(t, providers, 1, "expect exactly one provider") - assert.NotNil(t, providers[0]) + require.Len(t, initResults.Providers, 1, "expect exactly one provider") + assert.NotNil(t, initResults.Providers[0]) - assert.Empty(t, problems) - assert.Empty(t, warnings) - assert.Empty(t, invalidConnections) + assert.Empty(t, initResults.Problems) + assert.Empty(t, initResults.Warnings) + assert.Empty(t, initResults.InvalidConnections) }) t.Run("license does not have ACLs feature", func(t *testing.T) { licensing.MockCheckFeatureError("failed") - providers, problems, warnings, invalidConnections := NewAuthzProviders( + initResults := NewAuthzProviders( db, []*ExternalConnection{ { @@ -132,15 +132,15 @@ func TestNewAuthzProviders(t *testing.T) { expectedError := []string{"failed"} expInvalidConnectionErr := []string{"github"} - assert.Equal(t, expectedError, problems) - assert.Equal(t, expInvalidConnectionErr, invalidConnections) - assert.Empty(t, providers) - assert.Empty(t, warnings) + assert.Equal(t, expectedError, initResults.Problems) + assert.Equal(t, expInvalidConnectionErr, initResults.InvalidConnections) + assert.Empty(t, initResults.Providers) + assert.Empty(t, initResults.Warnings) }) t.Run("groups cache enabled, but not allowGroupsPermissionsSync", func(t *testing.T) { licensing.MockCheckFeatureError("") - providers, problems, warnings, invalidConnections := NewAuthzProviders( + initResults := NewAuthzProviders( db, []*ExternalConnection{ { @@ -164,15 +164,15 @@ func TestNewAuthzProviders(t *testing.T) { false, ) - require.Len(t, providers, 1, "expect exactly one provider") - assert.NotNil(t, providers[0]) - assert.Nil(t, providers[0].(*Provider).groupsCache, "expect groups cache to be disabled") + require.Len(t, initResults.Providers, 1, "expect exactly one provider") + assert.NotNil(t, initResults.Providers[0]) + assert.Nil(t, initResults.Providers[0].(*Provider).groupsCache, "expect groups cache to be disabled") - assert.Empty(t, problems) + assert.Empty(t, initResults.Problems) - require.Len(t, warnings, 1, "expect exactly one warning") - assert.Contains(t, warnings[0], "allowGroupsPermissionsSync") - assert.Empty(t, invalidConnections) + require.Len(t, initResults.Warnings, 1, "expect exactly one warning") + assert.Contains(t, initResults.Warnings[0], "allowGroupsPermissionsSync") + assert.Empty(t, initResults.InvalidConnections) }) t.Run("groups cache and allowGroupsPermissionsSync enabled", func(t *testing.T) { @@ -181,7 +181,7 @@ func TestNewAuthzProviders(t *testing.T) { } db := database.NewMockDB() db.ExternalServicesFunc.SetDefaultReturn(database.NewMockExternalServiceStore()) - providers, problems, warnings, invalidConnections := NewAuthzProviders( + initResults := NewAuthzProviders( db, []*ExternalConnection{ { @@ -205,13 +205,13 @@ func TestNewAuthzProviders(t *testing.T) { false, ) - require.Len(t, providers, 1, "expect exactly one provider") - assert.NotNil(t, providers[0]) - assert.NotNil(t, providers[0].(*Provider).groupsCache, "expect groups cache to be enabled") + require.Len(t, initResults.Providers, 1, "expect exactly one provider") + assert.NotNil(t, initResults.Providers[0]) + assert.NotNil(t, initResults.Providers[0].(*Provider).groupsCache, "expect groups cache to be enabled") - assert.Empty(t, problems) - assert.Empty(t, warnings) - assert.Empty(t, invalidConnections) + assert.Empty(t, initResults.Problems) + assert.Empty(t, initResults.Warnings) + assert.Empty(t, initResults.InvalidConnections) }) t.Run("github app installation id available", func(t *testing.T) { @@ -232,7 +232,7 @@ func TestNewAuthzProviders(t *testing.T) { db := database.NewMockDB() db.ExternalServicesFunc.SetDefaultReturn(database.NewMockExternalServiceStore()) - providers, problems, warnings, invalidConnections := NewAuthzProviders( + initResults := NewAuthzProviders( db, []*ExternalConnection{ { @@ -260,12 +260,12 @@ func TestNewAuthzProviders(t *testing.T) { false, ) - require.Len(t, providers, 1, "expect exactly one provider") - assert.NotNil(t, providers[0]) + require.Len(t, initResults.Providers, 1, "expect exactly one provider") + assert.NotNil(t, initResults.Providers[0]) - assert.Empty(t, problems) - assert.Empty(t, warnings) - assert.Empty(t, invalidConnections) + assert.Empty(t, initResults.Problems) + assert.Empty(t, initResults.Warnings) + assert.Empty(t, initResults.InvalidConnections) }) }) } diff --git a/enterprise/internal/authz/gitlab/authz.go b/enterprise/internal/authz/gitlab/authz.go index fb10bf3a5927..6947853f7194 100644 --- a/enterprise/internal/authz/gitlab/authz.go +++ b/enterprise/internal/authz/gitlab/authz.go @@ -4,6 +4,7 @@ import ( "net/url" "github.com/sourcegraph/sourcegraph/cmd/frontend/auth/providers" + atypes "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/types" "github.com/sourcegraph/sourcegraph/enterprise/internal/licensing" "github.com/sourcegraph/sourcegraph/internal/authz" "github.com/sourcegraph/sourcegraph/internal/database" @@ -27,20 +28,20 @@ func NewAuthzProviders( db database.DB, cfg schema.SiteConfiguration, conns []*types.GitLabConnection, -) (ps []authz.Provider, problems []string, warnings []string, invalidConnections []string, -) { +) *atypes.ProviderInitResult { + initResults := &atypes.ProviderInitResult{} // Authorization (i.e., permissions) providers for _, c := range conns { p, err := newAuthzProvider(db, c.URN, c.Authorization, c.Url, c.Token, gitlab.TokenType(c.TokenType), cfg.AuthProviders) if err != nil { - invalidConnections = append(invalidConnections, extsvc.TypeGitLab) - problems = append(problems, err.Error()) + initResults.InvalidConnections = append(initResults.InvalidConnections, extsvc.TypeGitLab) + initResults.Problems = append(initResults.Problems, err.Error()) } else if p != nil { - ps = append(ps, p) + initResults.Providers = append(initResults.Providers, p) } } - return ps, problems, warnings, invalidConnections + return initResults } func newAuthzProvider(db database.DB, urn string, a *schema.GitLabAuthorization, instanceURL, token string, tokenType gitlab.TokenType, ps []schema.AuthProviders) (authz.Provider, error) { diff --git a/enterprise/internal/authz/perforce/authz.go b/enterprise/internal/authz/perforce/authz.go index a2da8fa237a9..d1f0498e5f10 100644 --- a/enterprise/internal/authz/perforce/authz.go +++ b/enterprise/internal/authz/perforce/authz.go @@ -7,6 +7,7 @@ import ( "github.com/sourcegraph/sourcegraph/enterprise/internal/licensing" + atypes "github.com/sourcegraph/sourcegraph/enterprise/internal/authz/types" "github.com/sourcegraph/sourcegraph/internal/authz" "github.com/sourcegraph/sourcegraph/internal/extsvc" "github.com/sourcegraph/sourcegraph/internal/types" @@ -22,18 +23,19 @@ import ( // This constructor does not and should not directly check connectivity to external services - if // desired, callers should use `(*Provider).ValidateConnection` directly to get warnings related // to connection issues. -func NewAuthzProviders(conns []*types.PerforceConnection) (ps []authz.Provider, problems []string, warnings []string, invalidConnections []string) { +func NewAuthzProviders(conns []*types.PerforceConnection) *atypes.ProviderInitResult { + initResults := &atypes.ProviderInitResult{} for _, c := range conns { p, err := newAuthzProvider(c.URN, c.Authorization, c.P4Port, c.P4User, c.P4Passwd, c.Depots) if err != nil { - invalidConnections = append(invalidConnections, extsvc.TypePerforce) - problems = append(problems, err.Error()) + initResults.InvalidConnections = append(initResults.InvalidConnections, extsvc.TypePerforce) + initResults.Problems = append(initResults.Problems, err.Error()) } else if p != nil { - ps = append(ps, p) + initResults.Providers = append(initResults.Providers, p) } } - return ps, problems, warnings, invalidConnections + return initResults } func newAuthzProvider( diff --git a/enterprise/internal/authz/types/types.go b/enterprise/internal/authz/types/types.go new file mode 100644 index 000000000000..a18497e89229 --- /dev/null +++ b/enterprise/internal/authz/types/types.go @@ -0,0 +1,17 @@ +package types + +import "github.com/sourcegraph/sourcegraph/internal/authz" + +type ProviderInitResult struct { + Providers []authz.Provider + Problems []string + Warnings []string + InvalidConnections []string +} + +func (r *ProviderInitResult) Append(res *ProviderInitResult) { + r.Providers = append(r.Providers, res.Providers...) + r.Problems = append(r.Problems, res.Problems...) + r.Warnings = append(r.Warnings, res.Warnings...) + r.InvalidConnections = append(r.InvalidConnections, res.InvalidConnections...) +} diff --git a/internal/extsvc/gerrit/account.go b/internal/extsvc/gerrit/account.go index 8cd9fad9868c..b72bc0d809d6 100644 --- a/internal/extsvc/gerrit/account.go +++ b/internal/extsvc/gerrit/account.go @@ -2,6 +2,8 @@ package gerrit import ( "context" + "encoding/json" + "net/url" "github.com/sourcegraph/sourcegraph/internal/encryption" "github.com/sourcegraph/sourcegraph/internal/extsvc" @@ -14,11 +16,58 @@ type AccountData struct { AccountID int32 `json:"account_id"` } +// AccountCredentials stores basic HTTP auth credentials for a Gerrit account. +type AccountCredentials struct { + Username string `json:"username"` + Password string `json:"password"` +} + // GetExternalAccountData extracts account data for the external account. -func GetExternalAccountData(ctx context.Context, data *extsvc.AccountData) (*AccountData, error) { - if data.Data == nil { - return nil, nil +func GetExternalAccountData(ctx context.Context, data *extsvc.AccountData) (usr *AccountData, err error) { + return encryption.DecryptJSON[AccountData](ctx, data.Data) +} + +// GetExternalAccountCredentials extracts the account credentials for the external account. +func GetExternalAccountCredentials(ctx context.Context, data *extsvc.AccountData) (*AccountCredentials, error) { + return encryption.DecryptJSON[AccountCredentials](ctx, data.AuthData) +} + +func GetPublicExternalAccountData(ctx context.Context, data *extsvc.AccountData) (*extsvc.PublicAccountData, error) { + usr, err := GetExternalAccountData(ctx, data) + if err != nil { + return nil, err } - return encryption.DecryptJSON[AccountData](ctx, data.Data) + return &extsvc.PublicAccountData{ + DisplayName: &usr.Username, + }, nil +} + +func SetExternalAccountData(data *extsvc.AccountData, usr *Account, creds *AccountCredentials) error { + serializedUser, err := json.Marshal(usr) + if err != nil { + return err + } + serializedCreds, err := json.Marshal(creds) + if err != nil { + return err + } + + data.Data = extsvc.NewUnencryptedData(serializedUser) + data.AuthData = extsvc.NewUnencryptedData(serializedCreds) + return nil +} + +var MockVerifyAccount func(context.Context, *url.URL, *AccountCredentials) (*Account, error) + +func VerifyAccount(ctx context.Context, u *url.URL, creds *AccountCredentials) (*Account, error) { + if MockVerifyAccount != nil { + return MockVerifyAccount(ctx, u, creds) + } + + client, err := NewClient("", u, creds, nil) + if err != nil { + return nil, err + } + return client.GetAuthenticatedUserAccount(ctx) } diff --git a/internal/extsvc/gerrit/client.go b/internal/extsvc/gerrit/client.go index 0a22905531c3..30ed688ad358 100644 --- a/internal/extsvc/gerrit/client.go +++ b/internal/extsvc/gerrit/client.go @@ -3,16 +3,17 @@ package gerrit import ( "context" - "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/url" + "strconv" + "github.com/sourcegraph/sourcegraph/internal/extsvc/auth" "github.com/sourcegraph/sourcegraph/internal/httpcli" "github.com/sourcegraph/sourcegraph/internal/ratelimit" - "github.com/sourcegraph/sourcegraph/schema" + "github.com/sourcegraph/sourcegraph/lib/errors" ) // Client access a Gerrit via the REST API. @@ -20,69 +21,65 @@ type Client struct { // HTTP Client used to communicate with the API httpClient httpcli.Doer - // Config is the code host connection config for this client - Config *schema.GerritConnection - // URL is the base URL of Gerrit. URL *url.URL // RateLimit is the self-imposed rate limiter (since Gerrit does not have a concept // of rate limiting in HTTP response headers). rateLimit *ratelimit.InstrumentedLimiter + + // Authenticator used to authenticate HTTP requests. + auther auth.Authenticator } // NewClient returns an authenticated Gerrit API client with -// the provided configuration. If a nil httpClient is provided, http.DefaultClient +// the provided configuration. If a nil httpClient is provided, httpcli.ExternalDoer // will be used. -func NewClient(urn string, config *schema.GerritConnection, httpClient httpcli.Doer) (*Client, error) { - u, err := url.Parse(config.Url) - if err != nil { - return nil, err - } - +func NewClient(urn string, url *url.URL, creds *AccountCredentials, httpClient httpcli.Doer) (*Client, error) { if httpClient == nil { httpClient = httpcli.ExternalDoer } + auther := &auth.BasicAuth{ + Username: creds.Username, + Password: creds.Password, + } + return &Client{ httpClient: httpClient, - Config: config, - URL: u, + URL: url, rateLimit: ratelimit.DefaultRegistry.Get(urn), + auther: auther, }, nil } -type ListAccountsResponse []Account - -func (c *Client) ListAccountsByEmail(ctx context.Context, email string) (ListAccountsResponse, error) { - qsAccounts := make(url.Values) - qsAccounts.Set("q", fmt.Sprintf("email:%s", email)) // TODO: what query should we run? - return c.listAccounts(ctx, qsAccounts) -} - -func (c *Client) ListAccountsByUsername(ctx context.Context, username string) (ListAccountsResponse, error) { - qsAccounts := make(url.Values) - qsAccounts.Set("q", fmt.Sprintf("username:%s", username)) // TODO: what query should we run? - return c.listAccounts(ctx, qsAccounts) +func (c *Client) WithAuthenticator(a auth.Authenticator) *Client { + return &Client{ + httpClient: c.httpClient, + URL: c.URL, + rateLimit: c.rateLimit, + auther: a, + } } -func (c *Client) listAccounts(ctx context.Context, qsAccounts url.Values) (ListAccountsResponse, error) { - qsAccounts.Set("o", "details") - - urlPath := "a/accounts/" - - uAllProjects := url.URL{Path: urlPath, RawQuery: qsAccounts.Encode()} - - reqAllAccounts, err := http.NewRequest("GET", uAllProjects.String(), nil) - +func (c *Client) GetAuthenticatedUserAccount(ctx context.Context) (*Account, error) { + req, err := http.NewRequest("GET", "a/accounts/self", nil) if err != nil { return nil, err } - respAllAccts := ListAccountsResponse{} - if _, err = c.do(ctx, reqAllAccounts, &respAllAccts); err != nil { - return respAllAccts, err + + var account Account + if _, err = c.do(ctx, req, &account); err != nil { + if httpErr := (&httpError{}); errors.As(err, &httpErr) { + if httpErr.Unauthorized() { + return nil, errors.New("Invalid username or password.") + } + } + + return nil, err } - return respAllAccts, nil + + return &account, nil } func (c *Client) GetGroup(ctx context.Context, groupName string) (Group, error) { @@ -105,73 +102,104 @@ func (c *Client) GetGroup(ctx context.Context, groupName string) (Group, error) // ListProjectsArgs defines options to be set on ListProjects method calls. type ListProjectsArgs struct { Cursor *Pagination + // If true, only fetches repositories with type CODE + OnlyCodeProjects bool } // ListProjectsResponse defines a response struct returned from ListProjects method calls. type ListProjectsResponse map[string]*Project -func (c *Client) ListProjects(ctx context.Context, opts ListProjectsArgs) (projects *ListProjectsResponse, nextPage bool, err error) { - +func (c *Client) listCodeProjects(ctx context.Context, cursor *Pagination) (ListProjectsResponse, bool, error) { // Unfortunately Gerrit APIs are quite limited and don't support pagination well. + // e.g. when we request a list of 100 CODE projects, 100 projects are fetched and + // only then filtered for CODE projects, possibly returning less than 100 projects. + // This means we cannot rely on the number of projects returned to determine if + // there are more projects to fetch. // Currently, if you want to only get CODE projects and want to know if there is another page - // to query for, the only way to do that is to query twice and compare the results. - qsAllProjects := make(url.Values) - qsCodeProjects := make(url.Values) + // to query for, the only way to do that is to query both CODE and ALL projects and compare + // the number of projects returned. - if opts.Cursor == nil { - opts.Cursor = &Pagination{PerPage: 100, Page: 1} - } + query := make(url.Values) + query.Set("n", strconv.Itoa(cursor.PerPage)) + query.Set("S", strconv.Itoa((cursor.Page-1)*cursor.PerPage)) + query.Set("type", "CODE") - // Number of results to return. - qsAllProjects.Set("n", fmt.Sprintf("%d", opts.Cursor.PerPage)) - qsCodeProjects.Set("n", fmt.Sprintf("%d", opts.Cursor.PerPage)) + uProjects := url.URL{Path: "a/projects/", RawQuery: query.Encode()} + req, err := http.NewRequest("GET", uProjects.String(), nil) + if err != nil { + return nil, false, err + } - // Skip the first S projects. - qsAllProjects.Set("S", fmt.Sprintf("%d", (opts.Cursor.Page-1)*opts.Cursor.PerPage)) - qsCodeProjects.Set("S", fmt.Sprintf("%d", (opts.Cursor.Page-1)*opts.Cursor.PerPage)) + var projects ListProjectsResponse + if _, err = c.do(ctx, req, &projects); err != nil { + return nil, false, err + } - // Set the desired project type to CODE (ALL/CODE/PERMISSIONS). - qsCodeProjects.Set("type", "CODE") + // If the number of projects returned is zero we cannot assume that there is no next page. + // We fetch the first project on the next page of ALL projects and check if that page is empty. + if len(projects) == 0 { + nextPageProject, _, err := c.listAllProjects(ctx, &Pagination{PerPage: 1, Skip: cursor.Page * cursor.PerPage}) + if err != nil { + return nil, false, err + } + if len(nextPageProject) == 0 { + return projects, false, nil + } + } - urlPath := "a/projects/" + // Otherwise we always assume that there is a next page. + return projects, true, nil +} - uAllProjects := url.URL{Path: urlPath, RawQuery: qsAllProjects.Encode()} +func (c *Client) listAllProjects(ctx context.Context, cursor *Pagination) (ListProjectsResponse, bool, error) { + query := make(url.Values) + query.Set("n", strconv.Itoa(cursor.PerPage)) + if cursor.Skip > 0 { + query.Set("S", strconv.Itoa(cursor.Skip)) + } else { + query.Set("S", strconv.Itoa((cursor.Page-1)*cursor.PerPage)) + } - reqAllProjects, err := http.NewRequest("GET", uAllProjects.String(), nil) + uProjects := url.URL{Path: "a/projects/", RawQuery: query.Encode()} + req, err := http.NewRequest("GET", uProjects.String(), nil) if err != nil { return nil, false, err } - var respAllProjects ListProjectsResponse - if _, err = c.do(ctx, reqAllProjects, &respAllProjects); err != nil { + var projects ListProjectsResponse + if _, err = c.do(ctx, req, &projects); err != nil { return nil, false, err } - uCodeProjects := url.URL{Path: urlPath, RawQuery: qsCodeProjects.Encode()} + // If the number of returned projects equal the number of requested projects, + // we assume that there is a next page. + return projects, len(projects) == cursor.PerPage, nil +} - reqCodeProjects, err := http.NewRequest("GET", uCodeProjects.String(), nil) - if err != nil { - return nil, false, err - } +// ListProjects fetches a list of CODE projects from Gerrit. +func (c *Client) ListProjects(ctx context.Context, opts ListProjectsArgs) (projects ListProjectsResponse, nextPage bool, err error) { - var respCodeProjects ListProjectsResponse - if _, err = c.do(ctx, reqCodeProjects, &respCodeProjects); err != nil { - return nil, false, err + if opts.Cursor == nil { + opts.Cursor = &Pagination{PerPage: 100, Page: 1} } - // If the amount of Projects we get back from AllProjects is greater than or equal to - // the amount we asked for in a page, then there is another page. - nextPage = len(respAllProjects) >= opts.Cursor.PerPage + if opts.OnlyCodeProjects { + return c.listCodeProjects(ctx, opts.Cursor) + } - return &respCodeProjects, nextPage, nil + return c.listAllProjects(ctx, opts.Cursor) } //nolint:unparam // http.Response is never used, but it makes sense API wise. func (c *Client) do(ctx context.Context, req *http.Request, result any) (*http.Response, error) { req.URL = c.URL.ResolveReference(req.URL) - // Add Basic Auth headers for authenticated requests. - req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(c.Config.Username+":"+c.Config.Password))) + // Authenticate request with auther + if c.auther != nil { + if err := c.auther.Authenticate(req); err != nil { + return nil, err + } + } if err := c.rateLimit.Wait(ctx); err != nil { return nil, err @@ -244,8 +272,10 @@ type Label struct { } type Pagination struct { - Page int `json:"page"` - PerPage int `json:"per_page"` + PerPage int + // Either Skip or Page should be set. If Skip is non-zero, it takes precedence. + Page int + Skip int } type httpError struct { diff --git a/internal/extsvc/gerrit/client_test.go b/internal/extsvc/gerrit/client_test.go index 841bd72c5f31..f71663b449c2 100644 --- a/internal/extsvc/gerrit/client_test.go +++ b/internal/extsvc/gerrit/client_test.go @@ -14,7 +14,6 @@ import ( "github.com/sourcegraph/sourcegraph/internal/httptestutil" "github.com/sourcegraph/sourcegraph/internal/lazyregexp" "github.com/sourcegraph/sourcegraph/internal/testutil" - "github.com/sourcegraph/sourcegraph/schema" ) var update = flag.Bool("update", false, "update testdata") @@ -60,11 +59,12 @@ func NewTestClient(t testing.TB, name string, update bool) (*Client, func()) { } hc = httpcli.GerritUnauthenticateMiddleware(hc) - c := &schema.GerritConnection{ - Url: "https://gerrit-review.googlesource.com", + u, err := url.Parse("https://gerrit-review.googlesource.com") + if err != nil { + t.Fatal(err) } - cli, err := NewClient("urn", c, hc) + cli, err := NewClient("urn", u, &AccountCredentials{}, hc) if err != nil { t.Fatal(err) } diff --git a/internal/extsvc/gerrit/externalaccount/externalaccount.go b/internal/extsvc/gerrit/externalaccount/externalaccount.go new file mode 100644 index 000000000000..f76cac1acf5e --- /dev/null +++ b/internal/extsvc/gerrit/externalaccount/externalaccount.go @@ -0,0 +1,47 @@ +package externalaccount + +import ( + "context" + "encoding/json" + "net/url" + "strconv" + + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/extsvc" + "github.com/sourcegraph/sourcegraph/internal/extsvc/gerrit" +) + +func AddGerritExternalAccount(ctx context.Context, db database.DB, userID int32, serviceID string, accountDetails string) (err error) { + var accountCredentials gerrit.AccountCredentials + err = json.Unmarshal([]byte(accountDetails), &accountCredentials) + if err != nil { + return err + } + + serviceURL, err := url.Parse(serviceID) + if err != nil { + return err + } + serviceURL = extsvc.NormalizeBaseURL(serviceURL) + + gerritAccount, err := gerrit.VerifyAccount(ctx, serviceURL, &accountCredentials) + if err != nil { + return err + } + + accountSpec := extsvc.AccountSpec{ + ServiceType: extsvc.TypeGerrit, + ServiceID: serviceID, + AccountID: strconv.Itoa(int(gerritAccount.ID)), + } + + accountData := extsvc.AccountData{} + if err = gerrit.SetExternalAccountData(&accountData, gerritAccount, &accountCredentials); err != nil { + return err + } + + if err = db.UserExternalAccounts().AssociateUserAndSave(ctx, userID, accountSpec, accountData); err != nil { + return err + } + return nil +} diff --git a/internal/extsvc/gerrit/testdata/golden/ListProjects.json b/internal/extsvc/gerrit/testdata/golden/ListProjects.json index 927e79501cae..54c7aa22eb86 100644 --- a/internal/extsvc/gerrit/testdata/golden/ListProjects.json +++ b/internal/extsvc/gerrit/testdata/golden/ListProjects.json @@ -1,4 +1,31 @@ { + "Core-Plugins": { + "description": "", + "id": "Core-Plugins", + "name": "", + "parent": "", + "state": "ACTIVE", + "branches": null, + "labels": null + }, + "Public-Plugins": { + "description": "", + "id": "Public-Plugins", + "name": "", + "parent": "", + "state": "ACTIVE", + "branches": null, + "labels": null + }, + "Public-Projects": { + "description": "", + "id": "Public-Projects", + "name": "", + "parent": "", + "state": "ACTIVE", + "branches": null, + "labels": null + }, "TestRepo": { "description": "", "id": "TestRepo", diff --git a/internal/extsvc/gerrit/testdata/vcr/ListProjects.yaml b/internal/extsvc/gerrit/testdata/vcr/ListProjects.yaml index 5efaae8ba4d6..36a0426332a4 100644 --- a/internal/extsvc/gerrit/testdata/vcr/ListProjects.yaml +++ b/internal/extsvc/gerrit/testdata/vcr/ListProjects.yaml @@ -20,54 +20,13 @@ interactions: Content-Disposition: - attachment Content-Security-Policy-Report-Only: - - 'script-src ''nonce-8hgH7U16LjDSnWVKQId8/g'' ''unsafe-inline'' ''strict-dynamic'' + - 'script-src ''nonce-LqYfkker0cyXan8LHVCJDw'' ''unsafe-inline'' ''strict-dynamic'' https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri https://csp.withgoogle.com/csp/gerritcodereview/1' Content-Type: - application/json; charset=utf-8 Date: - - Tue, 19 Apr 2022 17:08:30 GMT - Expires: - - Mon, 01 Jan 1990 00:00:00 GMT - Pragma: - - no-cache - Strict-Transport-Security: - - max-age=31536000; includeSubDomains; preload - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - "0" - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: {} - url: https://gerrit-review.googlesource.com/projects/?S=0&n=5&type=CODE - method: GET - response: - body: | - )]}' - {"TestRepo":{"id":"TestRepo","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/TestRepo/","target":"_blank"}]},"apps/analytics-etl":{"id":"apps%2Fanalytics-etl","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/apps/analytics-etl/","target":"_blank"}]}} - headers: - Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" - Cache-Control: - - no-cache, no-store, max-age=0, must-revalidate - Content-Disposition: - - attachment - Content-Security-Policy-Report-Only: - - 'script-src ''nonce-jxdm3cKVgIcc8qg5SF58fw'' ''unsafe-inline'' ''strict-dynamic'' - https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri - https://csp.withgoogle.com/csp/gerritcodereview/1' - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 19 Apr 2022 17:08:30 GMT + - Tue, 24 Jan 2023 12:14:13 GMT Expires: - Mon, 01 Jan 1990 00:00:00 GMT Pragma: diff --git a/internal/repos/gerrit.go b/internal/repos/gerrit.go index 66dec0007e48..032f328a0303 100644 --- a/internal/repos/gerrit.go +++ b/internal/repos/gerrit.go @@ -2,6 +2,7 @@ package repos import ( "context" + "net/url" "path" "sort" @@ -20,10 +21,12 @@ import ( // A GerritSource yields repositories from a single Gerrit connection configured // in Sourcegraph via the external services configuration. type GerritSource struct { - svc *types.ExternalService - cli *gerrit.Client - serviceID string - perPage int + svc *types.ExternalService + cli *gerrit.Client + serviceID string + perPage int + private bool + allowedProjects map[string]struct{} } // NewGerritSource returns a new GerritSource from the given external service. @@ -46,16 +49,31 @@ func NewGerritSource(ctx context.Context, svc *types.ExternalService, cf *httpcl return nil, err } - cli, err := gerrit.NewClient(svc.URN(), &c, httpCli) + u, err := url.Parse(c.Url) if err != nil { return nil, err } + cli, err := gerrit.NewClient(svc.URN(), u, &gerrit.AccountCredentials{ + Username: c.Username, + Password: c.Password, + }, httpCli) + if err != nil { + return nil, err + } + + allowedProjects := make(map[string]struct{}) + for _, project := range c.Projects { + allowedProjects[project] = struct{}{} + } + return &GerritSource{ - svc: svc, - cli: cli, - serviceID: extsvc.NormalizeBaseURL(cli.URL).String(), - perPage: 100, + svc: svc, + cli: cli, + allowedProjects: allowedProjects, + serviceID: extsvc.NormalizeBaseURL(cli.URL).String(), + perPage: 100, + private: c.Authorization != nil, }, nil } @@ -69,7 +87,8 @@ func (s *GerritSource) CheckConnection(ctx context.Context) error { // ListRepos returns all Gerrit repositories configured with this GerritSource's config. func (s *GerritSource) ListRepos(ctx context.Context, results chan SourceResult) { args := gerrit.ListProjectsArgs{ - Cursor: &gerrit.Pagination{PerPage: s.perPage, Page: 1}, + Cursor: &gerrit.Pagination{PerPage: s.perPage, Page: 1}, + OnlyCodeProjects: true, } for { @@ -80,17 +99,23 @@ func (s *GerritSource) ListRepos(ctx context.Context, results chan SourceResult) } // Unfortunately, because Gerrit API responds with a map, we have to sort it to maintain proper ordering - pageAsMap := map[string]*gerrit.Project(*page) - pageKeySlice := make([]string, 0, len(pageAsMap)) + pageKeySlice := make([]string, 0, len(page)) - for p := range pageAsMap { + for p := range page { pageKeySlice = append(pageKeySlice, p) } sort.Strings(pageKeySlice) for _, p := range pageKeySlice { - repo, err := s.makeRepo(p, pageAsMap[p]) + // Only check if the project is allowed if we have a list of allowed projects + if len(s.allowedProjects) != 0 { + if _, ok := s.allowedProjects[p]; !ok { + continue + } + } + + repo, err := s.makeRepo(p, page[p]) if err != nil { results <- SourceResult{Source: s, Err: err} return @@ -114,7 +139,7 @@ func (s *GerritSource) ExternalServices() types.ExternalServices { func (s *GerritSource) makeRepo(projectName string, p *gerrit.Project) (*types.Repo, error) { urn := s.svc.URN() - fullURL, err := urlx.Parse(s.cli.URL.String() + projectName) + fullURL, err := urlx.Parse(s.cli.URL.JoinPath(projectName).String()) if err != nil { return nil, err } @@ -137,5 +162,6 @@ func (s *GerritSource) makeRepo(projectName string, p *gerrit.Project) (*types.R }, }, Metadata: p, + Private: s.private, }, nil } diff --git a/internal/repos/gerrit_test.go b/internal/repos/gerrit_test.go index fe446422c224..d24431bfd334 100644 --- a/internal/repos/gerrit_test.go +++ b/internal/repos/gerrit_test.go @@ -2,6 +2,7 @@ package repos import ( "context" + "net/url" "testing" "github.com/sourcegraph/sourcegraph/internal/extsvc" @@ -9,32 +10,69 @@ import ( "github.com/sourcegraph/sourcegraph/internal/testutil" "github.com/sourcegraph/sourcegraph/internal/types" "github.com/sourcegraph/sourcegraph/schema" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGerritSource_ListRepos(t *testing.T) { - conf := &schema.GerritConnection{ - Url: "https://gerrit-review.googlesource.com", - } - cf, save := newClientFactory(t, t.Name(), httpcli.GerritUnauthenticateMiddleware) - defer save(t) - - svc := &types.ExternalService{ - Kind: extsvc.KindGerrit, - Config: extsvc.NewUnencryptedConfig(marshalJSON(t, conf)), - } - - ctx := context.Background() - src, err := NewGerritSource(ctx, svc, cf) - if err != nil { - t.Fatal(err) - } - - src.perPage = 25 - - repos, err := listAll(context.Background(), src) - if err != nil { - t.Fatal(err) - } - - testutil.AssertGolden(t, "testdata/sources/GERRIT/"+t.Name(), update(t.Name()), repos) + cfName := t.Name() + t.Run("no filtering", func(t *testing.T) { + conf := &schema.GerritConnection{ + Url: "https://gerrit-review.googlesource.com", + } + cf, save := newClientFactory(t, cfName, httpcli.GerritUnauthenticateMiddleware) + defer save(t) + + svc := &types.ExternalService{ + Kind: extsvc.KindGerrit, + Config: extsvc.NewUnencryptedConfig(marshalJSON(t, conf)), + } + + ctx := context.Background() + src, err := NewGerritSource(ctx, svc, cf) + require.NoError(t, err) + + src.perPage = 25 + + repos, err := listAll(ctx, src) + require.NoError(t, err) + + testutil.AssertGolden(t, "testdata/sources/GERRIT/"+t.Name(), update(t.Name()), repos) + }) + + t.Run("with filtering", func(t *testing.T) { + conf := &schema.GerritConnection{ + Url: "https://gerrit-review.googlesource.com", + Projects: []string{ + "apps/reviewit", + "buck", + }, + } + cf, save := newClientFactory(t, cfName, httpcli.GerritUnauthenticateMiddleware) + defer save(t) + + svc := &types.ExternalService{ + Kind: extsvc.KindGerrit, + Config: extsvc.NewUnencryptedConfig(marshalJSON(t, conf)), + } + + ctx := context.Background() + src, err := NewGerritSource(ctx, svc, cf) + require.NoError(t, err) + + src.perPage = 25 + + repos, err := listAll(ctx, src) + require.NoError(t, err) + + assert.Len(t, repos, 2) + repoNames := make([]string, 0, len(repos)) + for _, repo := range repos { + repoNames = append(repoNames, repo.ExternalRepo.ID) + } + assert.ElementsMatch(t, repoNames, []string{ + url.PathEscape("apps/reviewit"), + "buck", + }) + }) } diff --git a/internal/repos/testdata/sources/GERRIT/TestGerritSource_ListRepos b/internal/repos/testdata/sources/GERRIT/TestGerritSource_ListRepos/no_filtering similarity index 98% rename from internal/repos/testdata/sources/GERRIT/TestGerritSource_ListRepos rename to internal/repos/testdata/sources/GERRIT/TestGerritSource_ListRepos/no_filtering index 26dd08b2dca0..bafa1b7716b9 100644 --- a/internal/repos/testdata/sources/GERRIT/TestGerritSource_ListRepos +++ b/internal/repos/testdata/sources/GERRIT/TestGerritSource_ListRepos/no_filtering @@ -895,38 +895,6 @@ "labels": null } }, - { - "ID": 0, - "Name": "gerrit-review.googlesource.com/gwtjsonrpc", - "URI": "gerrit-review.googlesource.com/gwtjsonrpc", - "Description": "", - "Fork": false, - "Archived": false, - "Private": false, - "CreatedAt": "0001-01-01T00:00:00Z", - "UpdatedAt": "0001-01-01T00:00:00Z", - "DeletedAt": "0001-01-01T00:00:00Z", - "ExternalRepo": { - "ID": "gwtjsonrpc", - "ServiceType": "gerrit", - "ServiceID": "https://gerrit-review.googlesource.com/" - }, - "Sources": { - "extsvc:gerrit:0": { - "ID": "extsvc:gerrit:0", - "CloneURL": "https://gerrit-review.googlesource.com/gwtjsonrpc" - } - }, - "Metadata": { - "description": "", - "id": "gwtjsonrpc", - "name": "", - "parent": "", - "state": "ACTIVE", - "branches": null, - "labels": null - } - }, { "ID": 0, "Name": "gerrit-review.googlesource.com/gwtorm", @@ -1119,6 +1087,70 @@ "labels": null } }, + { + "ID": 0, + "Name": "gerrit-review.googlesource.com/luci-config", + "URI": "gerrit-review.googlesource.com/luci-config", + "Description": "", + "Fork": false, + "Archived": false, + "Private": false, + "CreatedAt": "0001-01-01T00:00:00Z", + "UpdatedAt": "0001-01-01T00:00:00Z", + "DeletedAt": "0001-01-01T00:00:00Z", + "ExternalRepo": { + "ID": "luci-config", + "ServiceType": "gerrit", + "ServiceID": "https://gerrit-review.googlesource.com/" + }, + "Sources": { + "extsvc:gerrit:0": { + "ID": "extsvc:gerrit:0", + "CloneURL": "https://gerrit-review.googlesource.com/luci-config" + } + }, + "Metadata": { + "description": "", + "id": "luci-config", + "name": "", + "parent": "", + "state": "ACTIVE", + "branches": null, + "labels": null + } + }, + { + "ID": 0, + "Name": "gerrit-review.googlesource.com/luci-test", + "URI": "gerrit-review.googlesource.com/luci-test", + "Description": "", + "Fork": false, + "Archived": false, + "Private": false, + "CreatedAt": "0001-01-01T00:00:00Z", + "UpdatedAt": "0001-01-01T00:00:00Z", + "DeletedAt": "0001-01-01T00:00:00Z", + "ExternalRepo": { + "ID": "luci-test", + "ServiceType": "gerrit", + "ServiceID": "https://gerrit-review.googlesource.com/" + }, + "Sources": { + "extsvc:gerrit:0": { + "ID": "extsvc:gerrit:0", + "CloneURL": "https://gerrit-review.googlesource.com/luci-test" + } + }, + "Metadata": { + "description": "", + "id": "luci-test", + "name": "", + "parent": "", + "state": "ACTIVE", + "branches": null, + "labels": null + } + }, { "ID": 0, "Name": "gerrit-review.googlesource.com/modules/cache-chroniclemap", @@ -2111,6 +2143,38 @@ "labels": null } }, + { + "ID": 0, + "Name": "gerrit-review.googlesource.com/plugins/checks-jenkins", + "URI": "gerrit-review.googlesource.com/plugins/checks-jenkins", + "Description": "", + "Fork": false, + "Archived": false, + "Private": false, + "CreatedAt": "0001-01-01T00:00:00Z", + "UpdatedAt": "0001-01-01T00:00:00Z", + "DeletedAt": "0001-01-01T00:00:00Z", + "ExternalRepo": { + "ID": "plugins%2Fchecks-jenkins", + "ServiceType": "gerrit", + "ServiceID": "https://gerrit-review.googlesource.com/" + }, + "Sources": { + "extsvc:gerrit:0": { + "ID": "extsvc:gerrit:0", + "CloneURL": "https://gerrit-review.googlesource.com/plugins/checks-jenkins" + } + }, + "Metadata": { + "description": "", + "id": "plugins%2Fchecks-jenkins", + "name": "", + "parent": "", + "state": "ACTIVE", + "branches": null, + "labels": null + } + }, { "ID": 0, "Name": "gerrit-review.googlesource.com/plugins/cloud-notifications", @@ -2719,6 +2783,38 @@ "labels": null } }, + { + "ID": 0, + "Name": "gerrit-review.googlesource.com/plugins/events-nats", + "URI": "gerrit-review.googlesource.com/plugins/events-nats", + "Description": "", + "Fork": false, + "Archived": false, + "Private": false, + "CreatedAt": "0001-01-01T00:00:00Z", + "UpdatedAt": "0001-01-01T00:00:00Z", + "DeletedAt": "0001-01-01T00:00:00Z", + "ExternalRepo": { + "ID": "plugins%2Fevents-nats", + "ServiceType": "gerrit", + "ServiceID": "https://gerrit-review.googlesource.com/" + }, + "Sources": { + "extsvc:gerrit:0": { + "ID": "extsvc:gerrit:0", + "CloneURL": "https://gerrit-review.googlesource.com/plugins/events-nats" + } + }, + "Metadata": { + "description": "", + "id": "plugins%2Fevents-nats", + "name": "", + "parent": "", + "state": "ACTIVE", + "branches": null, + "labels": null + } + }, { "ID": 0, "Name": "gerrit-review.googlesource.com/plugins/events-rabbitmq", @@ -2842,7 +2938,7 @@ "id": "plugins%2Ffind-owners", "name": "", "parent": "", - "state": "ACTIVE", + "state": "READ_ONLY", "branches": null, "labels": null } @@ -2943,6 +3039,38 @@ "labels": null } }, + { + "ID": 0, + "Name": "gerrit-review.googlesource.com/plugins/git-repo-metrics", + "URI": "gerrit-review.googlesource.com/plugins/git-repo-metrics", + "Description": "", + "Fork": false, + "Archived": false, + "Private": false, + "CreatedAt": "0001-01-01T00:00:00Z", + "UpdatedAt": "0001-01-01T00:00:00Z", + "DeletedAt": "0001-01-01T00:00:00Z", + "ExternalRepo": { + "ID": "plugins%2Fgit-repo-metrics", + "ServiceType": "gerrit", + "ServiceID": "https://gerrit-review.googlesource.com/" + }, + "Sources": { + "extsvc:gerrit:0": { + "ID": "extsvc:gerrit:0", + "CloneURL": "https://gerrit-review.googlesource.com/plugins/git-repo-metrics" + } + }, + "Metadata": { + "description": "", + "id": "plugins%2Fgit-repo-metrics", + "name": "", + "parent": "", + "state": "ACTIVE", + "branches": null, + "labels": null + } + }, { "ID": 0, "Name": "gerrit-review.googlesource.com/plugins/gitblit", @@ -4287,38 +4415,6 @@ "labels": null } }, - { - "ID": 0, - "Name": "gerrit-review.googlesource.com/plugins/manifest", - "URI": "gerrit-review.googlesource.com/plugins/manifest", - "Description": "", - "Fork": false, - "Archived": false, - "Private": false, - "CreatedAt": "0001-01-01T00:00:00Z", - "UpdatedAt": "0001-01-01T00:00:00Z", - "DeletedAt": "0001-01-01T00:00:00Z", - "ExternalRepo": { - "ID": "plugins%2Fmanifest", - "ServiceType": "gerrit", - "ServiceID": "https://gerrit-review.googlesource.com/" - }, - "Sources": { - "extsvc:gerrit:0": { - "ID": "extsvc:gerrit:0", - "CloneURL": "https://gerrit-review.googlesource.com/plugins/manifest" - } - }, - "Metadata": { - "description": "", - "id": "plugins%2Fmanifest", - "name": "", - "parent": "", - "state": "ACTIVE", - "branches": null, - "labels": null - } - }, { "ID": 0, "Name": "gerrit-review.googlesource.com/plugins/manifest-subscription", @@ -5951,38 +6047,6 @@ "labels": null } }, - { - "ID": 0, - "Name": "gerrit-review.googlesource.com/plugins/startblock", - "URI": "gerrit-review.googlesource.com/plugins/startblock", - "Description": "", - "Fork": false, - "Archived": false, - "Private": false, - "CreatedAt": "0001-01-01T00:00:00Z", - "UpdatedAt": "0001-01-01T00:00:00Z", - "DeletedAt": "0001-01-01T00:00:00Z", - "ExternalRepo": { - "ID": "plugins%2Fstartblock", - "ServiceType": "gerrit", - "ServiceID": "https://gerrit-review.googlesource.com/" - }, - "Sources": { - "extsvc:gerrit:0": { - "ID": "extsvc:gerrit:0", - "CloneURL": "https://gerrit-review.googlesource.com/plugins/startblock" - } - }, - "Metadata": { - "description": "", - "id": "plugins%2Fstartblock", - "name": "", - "parent": "", - "state": "ACTIVE", - "branches": null, - "labels": null - } - }, { "ID": 0, "Name": "gerrit-review.googlesource.com/plugins/supermanifest", @@ -6751,6 +6815,38 @@ "labels": null } }, + { + "ID": 0, + "Name": "gerrit-review.googlesource.com/summit/2022", + "URI": "gerrit-review.googlesource.com/summit/2022", + "Description": "", + "Fork": false, + "Archived": false, + "Private": false, + "CreatedAt": "0001-01-01T00:00:00Z", + "UpdatedAt": "0001-01-01T00:00:00Z", + "DeletedAt": "0001-01-01T00:00:00Z", + "ExternalRepo": { + "ID": "summit%2F2022", + "ServiceType": "gerrit", + "ServiceID": "https://gerrit-review.googlesource.com/" + }, + "Sources": { + "extsvc:gerrit:0": { + "ID": "extsvc:gerrit:0", + "CloneURL": "https://gerrit-review.googlesource.com/summit/2022" + } + }, + "Metadata": { + "description": "", + "id": "summit%2F2022", + "name": "", + "parent": "", + "state": "ACTIVE", + "branches": null, + "labels": null + } + }, { "ID": 0, "Name": "gerrit-review.googlesource.com/training/gerrit", diff --git a/internal/repos/testdata/sources/TestGerritSource_ListRepos.yaml b/internal/repos/testdata/sources/TestGerritSource_ListRepos.yaml index 72b1a42eb5c0..59522c0658ac 100644 --- a/internal/repos/testdata/sources/TestGerritSource_ListRepos.yaml +++ b/internal/repos/testdata/sources/TestGerritSource_ListRepos.yaml @@ -1,47 +1,6 @@ --- version: 1 interactions: -- request: - body: "" - form: {} - headers: {} - url: https://gerrit-review.googlesource.com/projects/?S=0&n=25 - method: GET - response: - body: | - )]}' - {"Core-Plugins":{"id":"Core-Plugins","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/Core-Plugins/","target":"_blank"}]},"Public-Plugins":{"id":"Public-Plugins","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/Public-Plugins/","target":"_blank"}]},"Public-Projects":{"id":"Public-Projects","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/Public-Projects/","target":"_blank"}]},"TestRepo":{"id":"TestRepo","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/TestRepo/","target":"_blank"}]},"apps/analytics-etl":{"id":"apps%2Fanalytics-etl","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/apps/analytics-etl/","target":"_blank"}]},"apps/kibana-dashboard":{"id":"apps%2Fkibana-dashboard","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/apps/kibana-dashboard/","target":"_blank"}]},"apps/reviewit":{"id":"apps%2Freviewit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/apps/reviewit/","target":"_blank"}]},"aws-gerrit":{"id":"aws-gerrit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/aws-gerrit/","target":"_blank"}]},"bazlets":{"id":"bazlets","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/bazlets/","target":"_blank"}]},"buck":{"id":"buck","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/buck/","target":"_blank"}]},"bucklets":{"id":"bucklets","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/bucklets/","target":"_blank"}]},"docker-gerrit":{"id":"docker-gerrit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/docker-gerrit/","target":"_blank"}]},"executablewar":{"id":"executablewar","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/executablewar/","target":"_blank"}]},"gcompute-tools":{"id":"gcompute-tools","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gcompute-tools/","target":"_blank"}]},"gerrit":{"id":"gerrit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit/","target":"_blank"}]},"gerrit-attic":{"id":"gerrit-attic","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-attic/","target":"_blank"}]},"gerrit-bug-reporter":{"id":"gerrit-bug-reporter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-bug-reporter/","target":"_blank"}]},"gerrit-ci-scripts":{"id":"gerrit-ci-scripts","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-ci-scripts/","target":"_blank"}]},"gerrit-fe-dev-helper":{"id":"gerrit-fe-dev-helper","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-fe-dev-helper/","target":"_blank"}]},"gerrit-installer":{"id":"gerrit-installer","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-installer/","target":"_blank"}]},"gerrit-linter":{"id":"gerrit-linter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-linter/","target":"_blank"}]},"gerrit-load-tests":{"id":"gerrit-load-tests","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-load-tests/","target":"_blank"}]},"gerrit-monitoring":{"id":"gerrit-monitoring","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-monitoring/","target":"_blank"}]},"gerrit-release-tools":{"id":"gerrit-release-tools","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-release-tools/","target":"_blank"}]},"gerrit-switch":{"id":"gerrit-switch","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-switch/","target":"_blank"}]}} - headers: - Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" - Cache-Control: - - no-cache, no-store, max-age=0, must-revalidate - Content-Disposition: - - attachment - Content-Security-Policy-Report-Only: - - 'script-src ''nonce-gKu3xhgRPVkXwLoz9uedzQ'' ''unsafe-inline'' ''strict-dynamic'' - https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri - https://csp.withgoogle.com/csp/gerritcodereview/1' - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 19 Apr 2022 17:13:52 GMT - Expires: - - Mon, 01 Jan 1990 00:00:00 GMT - Pragma: - - no-cache - Strict-Transport-Security: - - max-age=31536000; includeSubDomains; preload - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - "0" - status: 200 OK - code: 200 - duration: "" - request: body: "" form: {} @@ -54,61 +13,19 @@ interactions: {"TestRepo":{"id":"TestRepo","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/TestRepo/","target":"_blank"}]},"apps/analytics-etl":{"id":"apps%2Fanalytics-etl","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/apps/analytics-etl/","target":"_blank"}]},"apps/kibana-dashboard":{"id":"apps%2Fkibana-dashboard","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/apps/kibana-dashboard/","target":"_blank"}]},"apps/reviewit":{"id":"apps%2Freviewit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/apps/reviewit/","target":"_blank"}]},"aws-gerrit":{"id":"aws-gerrit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/aws-gerrit/","target":"_blank"}]},"bazlets":{"id":"bazlets","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/bazlets/","target":"_blank"}]},"buck":{"id":"buck","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/buck/","target":"_blank"}]},"bucklets":{"id":"bucklets","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/bucklets/","target":"_blank"}]},"docker-gerrit":{"id":"docker-gerrit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/docker-gerrit/","target":"_blank"}]},"executablewar":{"id":"executablewar","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/executablewar/","target":"_blank"}]},"gcompute-tools":{"id":"gcompute-tools","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gcompute-tools/","target":"_blank"}]},"gerrit":{"id":"gerrit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit/","target":"_blank"}]},"gerrit-attic":{"id":"gerrit-attic","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-attic/","target":"_blank"}]},"gerrit-bug-reporter":{"id":"gerrit-bug-reporter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-bug-reporter/","target":"_blank"}]},"gerrit-ci-scripts":{"id":"gerrit-ci-scripts","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-ci-scripts/","target":"_blank"}]},"gerrit-fe-dev-helper":{"id":"gerrit-fe-dev-helper","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-fe-dev-helper/","target":"_blank"}]},"gerrit-installer":{"id":"gerrit-installer","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-installer/","target":"_blank"}]},"gerrit-linter":{"id":"gerrit-linter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-linter/","target":"_blank"}]},"gerrit-load-tests":{"id":"gerrit-load-tests","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-load-tests/","target":"_blank"}]},"gerrit-monitoring":{"id":"gerrit-monitoring","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-monitoring/","target":"_blank"}]},"gerrit-release-tools":{"id":"gerrit-release-tools","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-release-tools/","target":"_blank"}]},"gerrit-switch":{"id":"gerrit-switch","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gerrit-switch/","target":"_blank"}]}} headers: Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Cache-Control: - no-cache, no-store, max-age=0, must-revalidate Content-Disposition: - attachment Content-Security-Policy-Report-Only: - - 'script-src ''nonce-R/syYOOvcp9SzM/ZwN4Iow'' ''unsafe-inline'' ''strict-dynamic'' + - 'script-src ''nonce-YqZvq-7aLipc99ovh3E2sA'' ''unsafe-inline'' ''strict-dynamic'' https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri https://csp.withgoogle.com/csp/gerritcodereview/1' Content-Type: - application/json; charset=utf-8 Date: - - Tue, 19 Apr 2022 17:13:52 GMT - Expires: - - Mon, 01 Jan 1990 00:00:00 GMT - Pragma: - - no-cache - Strict-Transport-Security: - - max-age=31536000; includeSubDomains; preload - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - "0" - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: {} - url: https://gerrit-review.googlesource.com/projects/?S=25&n=25 - method: GET - response: - body: | - )]}' - {"git-repo":{"id":"git-repo","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/git-repo/","target":"_blank"}]},"gitblit":{"id":"gitblit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gitblit/","target":"_blank"}]},"gitfs":{"id":"gitfs","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gitfs/","target":"_blank"}]},"gitiles":{"id":"gitiles","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gitiles/","target":"_blank"}]},"gs-maven-wagon":{"id":"gs-maven-wagon","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gs-maven-wagon/","target":"_blank"}]},"gwtexpui":{"id":"gwtexpui","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gwtexpui/","target":"_blank"}]},"gwtjsonrpc":{"id":"gwtjsonrpc","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gwtjsonrpc/","target":"_blank"}]},"gwtorm":{"id":"gwtorm","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gwtorm/","target":"_blank"}]},"homepage":{"id":"homepage","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/homepage/","target":"_blank"}]},"homepage-test":{"id":"homepage-test","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/homepage-test/","target":"_blank"}]},"java-prettify":{"id":"java-prettify","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/java-prettify/","target":"_blank"}]},"k8s-gerrit":{"id":"k8s-gerrit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/k8s-gerrit/","target":"_blank"}]},"libs/modules/repomanager/cassandra":{"id":"libs%2Fmodules%2Frepomanager%2Fcassandra","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/libs/modules/repomanager/cassandra/","target":"_blank"}]},"modules/cache-chroniclemap":{"id":"modules%2Fcache-chroniclemap","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/cache-chroniclemap/","target":"_blank"}]},"modules/cache-postgres":{"id":"modules%2Fcache-postgres","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/cache-postgres/","target":"_blank"}]},"modules/cached-refdb":{"id":"modules%2Fcached-refdb","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/cached-refdb/","target":"_blank"}]},"modules/events-broker":{"id":"modules%2Fevents-broker","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/events-broker/","target":"_blank"}]},"modules/git-refs-filter":{"id":"modules%2Fgit-refs-filter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/git-refs-filter/","target":"_blank"}]},"modules/index-elasticsearch":{"id":"modules%2Findex-elasticsearch","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/index-elasticsearch/","target":"_blank"}]},"modules/virtualhost":{"id":"modules%2Fvirtualhost","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/virtualhost/","target":"_blank"}]},"plugin-builder":{"id":"plugin-builder","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugin-builder/","target":"_blank"}]},"plugins/account":{"id":"plugins%2Faccount","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/account/","target":"_blank"}]},"plugins/admin-console":{"id":"plugins%2Fadmin-console","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/admin-console/","target":"_blank"}]},"plugins/analytics":{"id":"plugins%2Fanalytics","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/analytics/","target":"_blank"}]},"plugins/analytics-wizard":{"id":"plugins%2Fanalytics-wizard","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/analytics-wizard/","target":"_blank"}]}} - headers: - Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" - Cache-Control: - - no-cache, no-store, max-age=0, must-revalidate - Content-Disposition: - - attachment - Content-Security-Policy-Report-Only: - - 'script-src ''nonce-e0+I5s8haxG2+ADBVRpSmg'' ''unsafe-inline'' ''strict-dynamic'' - https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri - https://csp.withgoogle.com/csp/gerritcodereview/1' - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 19 Apr 2022 17:13:53 GMT + - Fri, 27 Jan 2023 13:00:53 GMT Expires: - Mon, 01 Jan 1990 00:00:00 GMT Pragma: @@ -133,64 +50,22 @@ interactions: response: body: | )]}' - {"git-repo":{"id":"git-repo","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/git-repo/","target":"_blank"}]},"gitblit":{"id":"gitblit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gitblit/","target":"_blank"}]},"gitfs":{"id":"gitfs","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gitfs/","target":"_blank"}]},"gitiles":{"id":"gitiles","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gitiles/","target":"_blank"}]},"gs-maven-wagon":{"id":"gs-maven-wagon","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gs-maven-wagon/","target":"_blank"}]},"gwtexpui":{"id":"gwtexpui","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gwtexpui/","target":"_blank"}]},"gwtjsonrpc":{"id":"gwtjsonrpc","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gwtjsonrpc/","target":"_blank"}]},"gwtorm":{"id":"gwtorm","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gwtorm/","target":"_blank"}]},"homepage":{"id":"homepage","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/homepage/","target":"_blank"}]},"homepage-test":{"id":"homepage-test","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/homepage-test/","target":"_blank"}]},"java-prettify":{"id":"java-prettify","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/java-prettify/","target":"_blank"}]},"k8s-gerrit":{"id":"k8s-gerrit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/k8s-gerrit/","target":"_blank"}]},"libs/modules/repomanager/cassandra":{"id":"libs%2Fmodules%2Frepomanager%2Fcassandra","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/libs/modules/repomanager/cassandra/","target":"_blank"}]},"modules/cache-chroniclemap":{"id":"modules%2Fcache-chroniclemap","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/cache-chroniclemap/","target":"_blank"}]},"modules/cache-postgres":{"id":"modules%2Fcache-postgres","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/cache-postgres/","target":"_blank"}]},"modules/cached-refdb":{"id":"modules%2Fcached-refdb","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/cached-refdb/","target":"_blank"}]},"modules/events-broker":{"id":"modules%2Fevents-broker","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/events-broker/","target":"_blank"}]},"modules/git-refs-filter":{"id":"modules%2Fgit-refs-filter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/git-refs-filter/","target":"_blank"}]},"modules/index-elasticsearch":{"id":"modules%2Findex-elasticsearch","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/index-elasticsearch/","target":"_blank"}]},"modules/virtualhost":{"id":"modules%2Fvirtualhost","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/virtualhost/","target":"_blank"}]},"plugin-builder":{"id":"plugin-builder","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugin-builder/","target":"_blank"}]},"plugins/account":{"id":"plugins%2Faccount","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/account/","target":"_blank"}]},"plugins/admin-console":{"id":"plugins%2Fadmin-console","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/admin-console/","target":"_blank"}]},"plugins/analytics":{"id":"plugins%2Fanalytics","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/analytics/","target":"_blank"}]},"plugins/analytics-wizard":{"id":"plugins%2Fanalytics-wizard","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/analytics-wizard/","target":"_blank"}]}} - headers: - Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" - Cache-Control: - - no-cache, no-store, max-age=0, must-revalidate - Content-Disposition: - - attachment - Content-Security-Policy-Report-Only: - - 'script-src ''nonce-YSUSOY5zLcVMRI2nQh3nBA'' ''unsafe-inline'' ''strict-dynamic'' - https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri - https://csp.withgoogle.com/csp/gerritcodereview/1' - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 19 Apr 2022 17:13:53 GMT - Expires: - - Mon, 01 Jan 1990 00:00:00 GMT - Pragma: - - no-cache - Strict-Transport-Security: - - max-age=31536000; includeSubDomains; preload - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - "0" - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: {} - url: https://gerrit-review.googlesource.com/projects/?S=50&n=25 - method: GET - response: - body: | - )]}' - {"plugins/approval-extension":{"id":"plugins%2Fapproval-extension","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/approval-extension/","target":"_blank"}]},"plugins/approver-annotator":{"id":"plugins%2Fapprover-annotator","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/approver-annotator/","target":"_blank"}]},"plugins/audit-sl4j":{"id":"plugins%2Faudit-sl4j","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/audit-sl4j/","target":"_blank"}]},"plugins/auth-htpasswd":{"id":"plugins%2Fauth-htpasswd","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/auth-htpasswd/","target":"_blank"}]},"plugins/auto-topic":{"id":"plugins%2Fauto-topic","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/auto-topic/","target":"_blank"}]},"plugins/automerger":{"id":"plugins%2Fautomerger","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/automerger/","target":"_blank"}]},"plugins/autosubmitter":{"id":"plugins%2Fautosubmitter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/autosubmitter/","target":"_blank"}]},"plugins/avatars-external":{"id":"plugins%2Favatars-external","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/avatars-external/","target":"_blank"}]},"plugins/avatars-gravatar":{"id":"plugins%2Favatars-gravatar","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/avatars-gravatar/","target":"_blank"}]},"plugins/avatars/external":{"id":"plugins%2Favatars%2Fexternal","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/avatars/external/","target":"_blank"}]},"plugins/avatars/gravatar":{"id":"plugins%2Favatars%2Fgravatar","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/avatars/gravatar/","target":"_blank"}]},"plugins/aws-dynamodb-refdb":{"id":"plugins%2Faws-dynamodb-refdb","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/aws-dynamodb-refdb/","target":"_blank"}]},"plugins/batch":{"id":"plugins%2Fbatch","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/batch/","target":"_blank"}]},"plugins/branch-network":{"id":"plugins%2Fbranch-network","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/branch-network/","target":"_blank"}]},"plugins/cfoauth":{"id":"plugins%2Fcfoauth","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/cfoauth/","target":"_blank"}]},"plugins/change-head":{"id":"plugins%2Fchange-head","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/change-head/","target":"_blank"}]},"plugins/change-labels":{"id":"plugins%2Fchange-labels","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/change-labels/","target":"_blank"}]},"plugins/changemessage":{"id":"plugins%2Fchangemessage","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/changemessage/","target":"_blank"}]},"plugins/checks":{"id":"plugins%2Fchecks","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/checks/","target":"_blank"}]},"plugins/cloud-notifications":{"id":"plugins%2Fcloud-notifications","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/cloud-notifications/","target":"_blank"}]},"plugins/code-coverage":{"id":"plugins%2Fcode-coverage","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/code-coverage/","target":"_blank"}]},"plugins/code-owners":{"id":"plugins%2Fcode-owners","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/code-owners/","target":"_blank"}]},"plugins/codemirror-editor":{"id":"plugins%2Fcodemirror-editor","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/codemirror-editor/","target":"_blank"}]},"plugins/commit-message-length-validator":{"id":"plugins%2Fcommit-message-length-validator","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/commit-message-length-validator/","target":"_blank"}]},"plugins/commit-validator-sample":{"id":"plugins%2Fcommit-validator-sample","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/commit-validator-sample/","target":"_blank"}]}} + {"git-repo":{"id":"git-repo","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/git-repo/","target":"_blank"}]},"gitblit":{"id":"gitblit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gitblit/","target":"_blank"}]},"gitfs":{"id":"gitfs","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gitfs/","target":"_blank"}]},"gitiles":{"id":"gitiles","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gitiles/","target":"_blank"}]},"gs-maven-wagon":{"id":"gs-maven-wagon","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gs-maven-wagon/","target":"_blank"}]},"gwtexpui":{"id":"gwtexpui","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gwtexpui/","target":"_blank"}]},"gwtorm":{"id":"gwtorm","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/gwtorm/","target":"_blank"}]},"homepage":{"id":"homepage","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/homepage/","target":"_blank"}]},"homepage-test":{"id":"homepage-test","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/homepage-test/","target":"_blank"}]},"java-prettify":{"id":"java-prettify","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/java-prettify/","target":"_blank"}]},"k8s-gerrit":{"id":"k8s-gerrit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/k8s-gerrit/","target":"_blank"}]},"libs/modules/repomanager/cassandra":{"id":"libs%2Fmodules%2Frepomanager%2Fcassandra","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/libs/modules/repomanager/cassandra/","target":"_blank"}]},"luci-config":{"id":"luci-config","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/luci-config/","target":"_blank"}]},"luci-test":{"id":"luci-test","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/luci-test/","target":"_blank"}]},"modules/cache-chroniclemap":{"id":"modules%2Fcache-chroniclemap","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/cache-chroniclemap/","target":"_blank"}]},"modules/cache-postgres":{"id":"modules%2Fcache-postgres","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/cache-postgres/","target":"_blank"}]},"modules/cached-refdb":{"id":"modules%2Fcached-refdb","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/cached-refdb/","target":"_blank"}]},"modules/events-broker":{"id":"modules%2Fevents-broker","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/events-broker/","target":"_blank"}]},"modules/git-refs-filter":{"id":"modules%2Fgit-refs-filter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/git-refs-filter/","target":"_blank"}]},"modules/index-elasticsearch":{"id":"modules%2Findex-elasticsearch","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/index-elasticsearch/","target":"_blank"}]},"modules/virtualhost":{"id":"modules%2Fvirtualhost","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/modules/virtualhost/","target":"_blank"}]},"plugin-builder":{"id":"plugin-builder","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugin-builder/","target":"_blank"}]},"plugins/account":{"id":"plugins%2Faccount","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/account/","target":"_blank"}]},"plugins/admin-console":{"id":"plugins%2Fadmin-console","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/admin-console/","target":"_blank"}]},"plugins/analytics":{"id":"plugins%2Fanalytics","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/analytics/","target":"_blank"}]}} headers: Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Cache-Control: - no-cache, no-store, max-age=0, must-revalidate Content-Disposition: - attachment Content-Security-Policy-Report-Only: - - 'script-src ''nonce-RbMEMmw9wn0Tq9WlH9hZmQ'' ''unsafe-inline'' ''strict-dynamic'' + - 'script-src ''nonce-d8P1q8Fo3eYniraL5bqraA'' ''unsafe-inline'' ''strict-dynamic'' https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri https://csp.withgoogle.com/csp/gerritcodereview/1' Content-Type: - application/json; charset=utf-8 Date: - - Tue, 19 Apr 2022 17:13:53 GMT + - Fri, 27 Jan 2023 13:00:55 GMT Expires: - Mon, 01 Jan 1990 00:00:00 GMT Pragma: @@ -215,64 +90,22 @@ interactions: response: body: | )]}' - {"plugins/approval-extension":{"id":"plugins%2Fapproval-extension","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/approval-extension/","target":"_blank"}]},"plugins/approver-annotator":{"id":"plugins%2Fapprover-annotator","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/approver-annotator/","target":"_blank"}]},"plugins/audit-sl4j":{"id":"plugins%2Faudit-sl4j","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/audit-sl4j/","target":"_blank"}]},"plugins/auth-htpasswd":{"id":"plugins%2Fauth-htpasswd","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/auth-htpasswd/","target":"_blank"}]},"plugins/auto-topic":{"id":"plugins%2Fauto-topic","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/auto-topic/","target":"_blank"}]},"plugins/automerger":{"id":"plugins%2Fautomerger","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/automerger/","target":"_blank"}]},"plugins/autosubmitter":{"id":"plugins%2Fautosubmitter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/autosubmitter/","target":"_blank"}]},"plugins/avatars-external":{"id":"plugins%2Favatars-external","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/avatars-external/","target":"_blank"}]},"plugins/avatars-gravatar":{"id":"plugins%2Favatars-gravatar","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/avatars-gravatar/","target":"_blank"}]},"plugins/avatars/external":{"id":"plugins%2Favatars%2Fexternal","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/avatars/external/","target":"_blank"}]},"plugins/avatars/gravatar":{"id":"plugins%2Favatars%2Fgravatar","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/avatars/gravatar/","target":"_blank"}]},"plugins/aws-dynamodb-refdb":{"id":"plugins%2Faws-dynamodb-refdb","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/aws-dynamodb-refdb/","target":"_blank"}]},"plugins/batch":{"id":"plugins%2Fbatch","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/batch/","target":"_blank"}]},"plugins/branch-network":{"id":"plugins%2Fbranch-network","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/branch-network/","target":"_blank"}]},"plugins/cfoauth":{"id":"plugins%2Fcfoauth","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/cfoauth/","target":"_blank"}]},"plugins/change-head":{"id":"plugins%2Fchange-head","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/change-head/","target":"_blank"}]},"plugins/change-labels":{"id":"plugins%2Fchange-labels","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/change-labels/","target":"_blank"}]},"plugins/changemessage":{"id":"plugins%2Fchangemessage","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/changemessage/","target":"_blank"}]},"plugins/checks":{"id":"plugins%2Fchecks","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/checks/","target":"_blank"}]},"plugins/cloud-notifications":{"id":"plugins%2Fcloud-notifications","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/cloud-notifications/","target":"_blank"}]},"plugins/code-coverage":{"id":"plugins%2Fcode-coverage","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/code-coverage/","target":"_blank"}]},"plugins/code-owners":{"id":"plugins%2Fcode-owners","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/code-owners/","target":"_blank"}]},"plugins/codemirror-editor":{"id":"plugins%2Fcodemirror-editor","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/codemirror-editor/","target":"_blank"}]},"plugins/commit-message-length-validator":{"id":"plugins%2Fcommit-message-length-validator","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/commit-message-length-validator/","target":"_blank"}]},"plugins/commit-validator-sample":{"id":"plugins%2Fcommit-validator-sample","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/commit-validator-sample/","target":"_blank"}]}} + {"plugins/analytics-wizard":{"id":"plugins%2Fanalytics-wizard","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/analytics-wizard/","target":"_blank"}]},"plugins/approval-extension":{"id":"plugins%2Fapproval-extension","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/approval-extension/","target":"_blank"}]},"plugins/approver-annotator":{"id":"plugins%2Fapprover-annotator","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/approver-annotator/","target":"_blank"}]},"plugins/audit-sl4j":{"id":"plugins%2Faudit-sl4j","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/audit-sl4j/","target":"_blank"}]},"plugins/auth-htpasswd":{"id":"plugins%2Fauth-htpasswd","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/auth-htpasswd/","target":"_blank"}]},"plugins/auto-topic":{"id":"plugins%2Fauto-topic","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/auto-topic/","target":"_blank"}]},"plugins/automerger":{"id":"plugins%2Fautomerger","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/automerger/","target":"_blank"}]},"plugins/autosubmitter":{"id":"plugins%2Fautosubmitter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/autosubmitter/","target":"_blank"}]},"plugins/avatars-external":{"id":"plugins%2Favatars-external","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/avatars-external/","target":"_blank"}]},"plugins/avatars-gravatar":{"id":"plugins%2Favatars-gravatar","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/avatars-gravatar/","target":"_blank"}]},"plugins/avatars/external":{"id":"plugins%2Favatars%2Fexternal","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/avatars/external/","target":"_blank"}]},"plugins/avatars/gravatar":{"id":"plugins%2Favatars%2Fgravatar","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/avatars/gravatar/","target":"_blank"}]},"plugins/aws-dynamodb-refdb":{"id":"plugins%2Faws-dynamodb-refdb","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/aws-dynamodb-refdb/","target":"_blank"}]},"plugins/batch":{"id":"plugins%2Fbatch","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/batch/","target":"_blank"}]},"plugins/branch-network":{"id":"plugins%2Fbranch-network","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/branch-network/","target":"_blank"}]},"plugins/cfoauth":{"id":"plugins%2Fcfoauth","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/cfoauth/","target":"_blank"}]},"plugins/change-head":{"id":"plugins%2Fchange-head","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/change-head/","target":"_blank"}]},"plugins/change-labels":{"id":"plugins%2Fchange-labels","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/change-labels/","target":"_blank"}]},"plugins/changemessage":{"id":"plugins%2Fchangemessage","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/changemessage/","target":"_blank"}]},"plugins/checks":{"id":"plugins%2Fchecks","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/checks/","target":"_blank"}]},"plugins/checks-jenkins":{"id":"plugins%2Fchecks-jenkins","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/checks-jenkins/","target":"_blank"}]},"plugins/cloud-notifications":{"id":"plugins%2Fcloud-notifications","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/cloud-notifications/","target":"_blank"}]},"plugins/code-coverage":{"id":"plugins%2Fcode-coverage","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/code-coverage/","target":"_blank"}]},"plugins/code-owners":{"id":"plugins%2Fcode-owners","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/code-owners/","target":"_blank"}]},"plugins/codemirror-editor":{"id":"plugins%2Fcodemirror-editor","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/codemirror-editor/","target":"_blank"}]}} headers: Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Cache-Control: - no-cache, no-store, max-age=0, must-revalidate Content-Disposition: - attachment Content-Security-Policy-Report-Only: - - 'script-src ''nonce-f5B17YjLUguF3LsnBk/+WA'' ''unsafe-inline'' ''strict-dynamic'' + - 'script-src ''nonce-j8zfTs6GJa1NrgLaog_OfA'' ''unsafe-inline'' ''strict-dynamic'' https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri https://csp.withgoogle.com/csp/gerritcodereview/1' Content-Type: - application/json; charset=utf-8 Date: - - Tue, 19 Apr 2022 17:13:54 GMT - Expires: - - Mon, 01 Jan 1990 00:00:00 GMT - Pragma: - - no-cache - Strict-Transport-Security: - - max-age=31536000; includeSubDomains; preload - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - "0" - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: {} - url: https://gerrit-review.googlesource.com/projects/?S=75&n=25 - method: GET - response: - body: | - )]}' - {"plugins/cookbook-plugin":{"id":"plugins%2Fcookbook-plugin","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/cookbook-plugin/","target":"_blank"}]},"plugins/copyright":{"id":"plugins%2Fcopyright","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/copyright/","target":"_blank"}]},"plugins/delete-project":{"id":"plugins%2Fdelete-project","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/delete-project/","target":"_blank"}]},"plugins/depends-on":{"id":"plugins%2Fdepends-on","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/depends-on/","target":"_blank"}]},"plugins/donation-button":{"id":"plugins%2Fdonation-button","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/donation-button/","target":"_blank"}]},"plugins/download-commands":{"id":"plugins%2Fdownload-commands","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/download-commands/","target":"_blank"}]},"plugins/egit":{"id":"plugins%2Fegit","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/egit/","target":"_blank"}]},"plugins/emoticons":{"id":"plugins%2Femoticons","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/emoticons/","target":"_blank"}]},"plugins/events":{"id":"plugins%2Fevents","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events/","target":"_blank"}]},"plugins/events-aws-kinesis":{"id":"plugins%2Fevents-aws-kinesis","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-aws-kinesis/","target":"_blank"}]},"plugins/events-gcloud-pubsub":{"id":"plugins%2Fevents-gcloud-pubsub","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-gcloud-pubsub/","target":"_blank"}]},"plugins/events-kafka":{"id":"plugins%2Fevents-kafka","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-kafka/","target":"_blank"}]},"plugins/events-log":{"id":"plugins%2Fevents-log","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-log/","target":"_blank"}]},"plugins/events-rabbitmq":{"id":"plugins%2Fevents-rabbitmq","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-rabbitmq/","target":"_blank"}]},"plugins/evict-cache":{"id":"plugins%2Fevict-cache","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/evict-cache/","target":"_blank"}]},"plugins/examples":{"id":"plugins%2Fexamples","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/examples/","target":"_blank"}]},"plugins/find-owners":{"id":"plugins%2Ffind-owners","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/find-owners/","target":"_blank"}]},"plugins/force-draft":{"id":"plugins%2Fforce-draft","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/force-draft/","target":"_blank"}]},"plugins/gc-conductor":{"id":"plugins%2Fgc-conductor","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gc-conductor/","target":"_blank"}]},"plugins/gerrit-support":{"id":"plugins%2Fgerrit-support","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gerrit-support/","target":"_blank"}]},"plugins/gitblit":{"id":"plugins%2Fgitblit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gitblit/","target":"_blank"}]},"plugins/gitgroups":{"id":"plugins%2Fgitgroups","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gitgroups/","target":"_blank"}]},"plugins/github":{"id":"plugins%2Fgithub","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github/","target":"_blank"}]},"plugins/github-groups":{"id":"plugins%2Fgithub-groups","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-groups/","target":"_blank"}]},"plugins/github-profile":{"id":"plugins%2Fgithub-profile","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-profile/","target":"_blank"}]}} - headers: - Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" - Cache-Control: - - no-cache, no-store, max-age=0, must-revalidate - Content-Disposition: - - attachment - Content-Security-Policy-Report-Only: - - 'script-src ''nonce-1kfNH8IER+NxHsJvq7rD7Q'' ''unsafe-inline'' ''strict-dynamic'' - https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri - https://csp.withgoogle.com/csp/gerritcodereview/1' - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 19 Apr 2022 17:13:54 GMT + - Fri, 27 Jan 2023 13:00:55 GMT Expires: - Mon, 01 Jan 1990 00:00:00 GMT Pragma: @@ -297,64 +130,22 @@ interactions: response: body: | )]}' - {"plugins/cookbook-plugin":{"id":"plugins%2Fcookbook-plugin","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/cookbook-plugin/","target":"_blank"}]},"plugins/copyright":{"id":"plugins%2Fcopyright","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/copyright/","target":"_blank"}]},"plugins/delete-project":{"id":"plugins%2Fdelete-project","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/delete-project/","target":"_blank"}]},"plugins/depends-on":{"id":"plugins%2Fdepends-on","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/depends-on/","target":"_blank"}]},"plugins/donation-button":{"id":"plugins%2Fdonation-button","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/donation-button/","target":"_blank"}]},"plugins/download-commands":{"id":"plugins%2Fdownload-commands","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/download-commands/","target":"_blank"}]},"plugins/egit":{"id":"plugins%2Fegit","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/egit/","target":"_blank"}]},"plugins/emoticons":{"id":"plugins%2Femoticons","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/emoticons/","target":"_blank"}]},"plugins/events":{"id":"plugins%2Fevents","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events/","target":"_blank"}]},"plugins/events-aws-kinesis":{"id":"plugins%2Fevents-aws-kinesis","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-aws-kinesis/","target":"_blank"}]},"plugins/events-gcloud-pubsub":{"id":"plugins%2Fevents-gcloud-pubsub","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-gcloud-pubsub/","target":"_blank"}]},"plugins/events-kafka":{"id":"plugins%2Fevents-kafka","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-kafka/","target":"_blank"}]},"plugins/events-log":{"id":"plugins%2Fevents-log","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-log/","target":"_blank"}]},"plugins/events-rabbitmq":{"id":"plugins%2Fevents-rabbitmq","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-rabbitmq/","target":"_blank"}]},"plugins/evict-cache":{"id":"plugins%2Fevict-cache","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/evict-cache/","target":"_blank"}]},"plugins/examples":{"id":"plugins%2Fexamples","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/examples/","target":"_blank"}]},"plugins/find-owners":{"id":"plugins%2Ffind-owners","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/find-owners/","target":"_blank"}]},"plugins/force-draft":{"id":"plugins%2Fforce-draft","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/force-draft/","target":"_blank"}]},"plugins/gc-conductor":{"id":"plugins%2Fgc-conductor","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gc-conductor/","target":"_blank"}]},"plugins/gerrit-support":{"id":"plugins%2Fgerrit-support","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gerrit-support/","target":"_blank"}]},"plugins/gitblit":{"id":"plugins%2Fgitblit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gitblit/","target":"_blank"}]},"plugins/gitgroups":{"id":"plugins%2Fgitgroups","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gitgroups/","target":"_blank"}]},"plugins/github":{"id":"plugins%2Fgithub","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github/","target":"_blank"}]},"plugins/github-groups":{"id":"plugins%2Fgithub-groups","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-groups/","target":"_blank"}]},"plugins/github-profile":{"id":"plugins%2Fgithub-profile","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-profile/","target":"_blank"}]}} - headers: - Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" - Cache-Control: - - no-cache, no-store, max-age=0, must-revalidate - Content-Disposition: - - attachment - Content-Security-Policy-Report-Only: - - 'script-src ''nonce-JsY38lbQiARG87pU9x/LqQ'' ''unsafe-inline'' ''strict-dynamic'' - https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri - https://csp.withgoogle.com/csp/gerritcodereview/1' - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 19 Apr 2022 17:13:54 GMT - Expires: - - Mon, 01 Jan 1990 00:00:00 GMT - Pragma: - - no-cache - Strict-Transport-Security: - - max-age=31536000; includeSubDomains; preload - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - "0" - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: {} - url: https://gerrit-review.googlesource.com/projects/?S=100&n=25 - method: GET - response: - body: | - )]}' - {"plugins/github-pullrequest":{"id":"plugins%2Fgithub-pullrequest","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-pullrequest/","target":"_blank"}]},"plugins/github-replication":{"id":"plugins%2Fgithub-replication","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-replication/","target":"_blank"}]},"plugins/github-webhooks":{"id":"plugins%2Fgithub-webhooks","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-webhooks/","target":"_blank"}]},"plugins/gitiles":{"id":"plugins%2Fgitiles","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gitiles/","target":"_blank"}]},"plugins/go-import":{"id":"plugins%2Fgo-import","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/go-import/","target":"_blank"}]},"plugins/google-apps-group":{"id":"plugins%2Fgoogle-apps-group","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/google-apps-group/","target":"_blank"}]},"plugins/healthcheck":{"id":"plugins%2Fhealthcheck","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/healthcheck/","target":"_blank"}]},"plugins/heartbeat":{"id":"plugins%2Fheartbeat","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/heartbeat/","target":"_blank"}]},"plugins/helloworld":{"id":"plugins%2Fhelloworld","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/helloworld/","target":"_blank"}]},"plugins/hide-actions":{"id":"plugins%2Fhide-actions","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hide-actions/","target":"_blank"}]},"plugins/high-availability":{"id":"plugins%2Fhigh-availability","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/high-availability/","target":"_blank"}]},"plugins/hooks":{"id":"plugins%2Fhooks","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks/","target":"_blank"}]},"plugins/hooks-audit":{"id":"plugins%2Fhooks-audit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-audit/","target":"_blank"}]},"plugins/hooks-bugzilla":{"id":"plugins%2Fhooks-bugzilla","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-bugzilla/","target":"_blank"}]},"plugins/hooks-its":{"id":"plugins%2Fhooks-its","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-its/","target":"_blank"}]},"plugins/hooks-jira":{"id":"plugins%2Fhooks-jira","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-jira/","target":"_blank"}]},"plugins/hooks-rtc":{"id":"plugins%2Fhooks-rtc","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-rtc/","target":"_blank"}]},"plugins/imagare":{"id":"plugins%2Fimagare","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/imagare/","target":"_blank"}]},"plugins/image-diff":{"id":"plugins%2Fimage-diff","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/image-diff/","target":"_blank"}]},"plugins/importer":{"id":"plugins%2Fimporter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/importer/","target":"_blank"}]},"plugins/its-base":{"id":"plugins%2Fits-base","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-base/","target":"_blank"}]},"plugins/its-bugzilla":{"id":"plugins%2Fits-bugzilla","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-bugzilla/","target":"_blank"}]},"plugins/its-github":{"id":"plugins%2Fits-github","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-github/","target":"_blank"}]},"plugins/its-jira":{"id":"plugins%2Fits-jira","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-jira/","target":"_blank"}]},"plugins/its-phabricator":{"id":"plugins%2Fits-phabricator","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-phabricator/","target":"_blank"}]}} + {"plugins/commit-message-length-validator":{"id":"plugins%2Fcommit-message-length-validator","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/commit-message-length-validator/","target":"_blank"}]},"plugins/commit-validator-sample":{"id":"plugins%2Fcommit-validator-sample","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/commit-validator-sample/","target":"_blank"}]},"plugins/cookbook-plugin":{"id":"plugins%2Fcookbook-plugin","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/cookbook-plugin/","target":"_blank"}]},"plugins/copyright":{"id":"plugins%2Fcopyright","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/copyright/","target":"_blank"}]},"plugins/delete-project":{"id":"plugins%2Fdelete-project","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/delete-project/","target":"_blank"}]},"plugins/depends-on":{"id":"plugins%2Fdepends-on","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/depends-on/","target":"_blank"}]},"plugins/donation-button":{"id":"plugins%2Fdonation-button","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/donation-button/","target":"_blank"}]},"plugins/download-commands":{"id":"plugins%2Fdownload-commands","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/download-commands/","target":"_blank"}]},"plugins/egit":{"id":"plugins%2Fegit","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/egit/","target":"_blank"}]},"plugins/emoticons":{"id":"plugins%2Femoticons","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/emoticons/","target":"_blank"}]},"plugins/events":{"id":"plugins%2Fevents","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events/","target":"_blank"}]},"plugins/events-aws-kinesis":{"id":"plugins%2Fevents-aws-kinesis","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-aws-kinesis/","target":"_blank"}]},"plugins/events-gcloud-pubsub":{"id":"plugins%2Fevents-gcloud-pubsub","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-gcloud-pubsub/","target":"_blank"}]},"plugins/events-kafka":{"id":"plugins%2Fevents-kafka","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-kafka/","target":"_blank"}]},"plugins/events-log":{"id":"plugins%2Fevents-log","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-log/","target":"_blank"}]},"plugins/events-nats":{"id":"plugins%2Fevents-nats","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-nats/","target":"_blank"}]},"plugins/events-rabbitmq":{"id":"plugins%2Fevents-rabbitmq","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/events-rabbitmq/","target":"_blank"}]},"plugins/evict-cache":{"id":"plugins%2Fevict-cache","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/evict-cache/","target":"_blank"}]},"plugins/examples":{"id":"plugins%2Fexamples","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/examples/","target":"_blank"}]},"plugins/find-owners":{"id":"plugins%2Ffind-owners","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/find-owners/","target":"_blank"}]},"plugins/force-draft":{"id":"plugins%2Fforce-draft","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/force-draft/","target":"_blank"}]},"plugins/gc-conductor":{"id":"plugins%2Fgc-conductor","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gc-conductor/","target":"_blank"}]},"plugins/gerrit-support":{"id":"plugins%2Fgerrit-support","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gerrit-support/","target":"_blank"}]},"plugins/git-repo-metrics":{"id":"plugins%2Fgit-repo-metrics","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/git-repo-metrics/","target":"_blank"}]},"plugins/gitblit":{"id":"plugins%2Fgitblit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gitblit/","target":"_blank"}]}} headers: Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Cache-Control: - no-cache, no-store, max-age=0, must-revalidate Content-Disposition: - attachment Content-Security-Policy-Report-Only: - - 'script-src ''nonce-g5Js9SarWd0KFinBS5L36w'' ''unsafe-inline'' ''strict-dynamic'' + - 'script-src ''nonce-_jF4bAKB-BcW1FZV-hAyzg'' ''unsafe-inline'' ''strict-dynamic'' https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri https://csp.withgoogle.com/csp/gerritcodereview/1' Content-Type: - application/json; charset=utf-8 Date: - - Tue, 19 Apr 2022 17:13:54 GMT + - Fri, 27 Jan 2023 13:00:56 GMT Expires: - Mon, 01 Jan 1990 00:00:00 GMT Pragma: @@ -379,64 +170,22 @@ interactions: response: body: | )]}' - {"plugins/github-pullrequest":{"id":"plugins%2Fgithub-pullrequest","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-pullrequest/","target":"_blank"}]},"plugins/github-replication":{"id":"plugins%2Fgithub-replication","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-replication/","target":"_blank"}]},"plugins/github-webhooks":{"id":"plugins%2Fgithub-webhooks","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-webhooks/","target":"_blank"}]},"plugins/gitiles":{"id":"plugins%2Fgitiles","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gitiles/","target":"_blank"}]},"plugins/go-import":{"id":"plugins%2Fgo-import","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/go-import/","target":"_blank"}]},"plugins/google-apps-group":{"id":"plugins%2Fgoogle-apps-group","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/google-apps-group/","target":"_blank"}]},"plugins/healthcheck":{"id":"plugins%2Fhealthcheck","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/healthcheck/","target":"_blank"}]},"plugins/heartbeat":{"id":"plugins%2Fheartbeat","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/heartbeat/","target":"_blank"}]},"plugins/helloworld":{"id":"plugins%2Fhelloworld","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/helloworld/","target":"_blank"}]},"plugins/hide-actions":{"id":"plugins%2Fhide-actions","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hide-actions/","target":"_blank"}]},"plugins/high-availability":{"id":"plugins%2Fhigh-availability","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/high-availability/","target":"_blank"}]},"plugins/hooks":{"id":"plugins%2Fhooks","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks/","target":"_blank"}]},"plugins/hooks-audit":{"id":"plugins%2Fhooks-audit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-audit/","target":"_blank"}]},"plugins/hooks-bugzilla":{"id":"plugins%2Fhooks-bugzilla","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-bugzilla/","target":"_blank"}]},"plugins/hooks-its":{"id":"plugins%2Fhooks-its","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-its/","target":"_blank"}]},"plugins/hooks-jira":{"id":"plugins%2Fhooks-jira","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-jira/","target":"_blank"}]},"plugins/hooks-rtc":{"id":"plugins%2Fhooks-rtc","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-rtc/","target":"_blank"}]},"plugins/imagare":{"id":"plugins%2Fimagare","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/imagare/","target":"_blank"}]},"plugins/image-diff":{"id":"plugins%2Fimage-diff","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/image-diff/","target":"_blank"}]},"plugins/importer":{"id":"plugins%2Fimporter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/importer/","target":"_blank"}]},"plugins/its-base":{"id":"plugins%2Fits-base","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-base/","target":"_blank"}]},"plugins/its-bugzilla":{"id":"plugins%2Fits-bugzilla","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-bugzilla/","target":"_blank"}]},"plugins/its-github":{"id":"plugins%2Fits-github","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-github/","target":"_blank"}]},"plugins/its-jira":{"id":"plugins%2Fits-jira","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-jira/","target":"_blank"}]},"plugins/its-phabricator":{"id":"plugins%2Fits-phabricator","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-phabricator/","target":"_blank"}]}} - headers: - Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" - Cache-Control: - - no-cache, no-store, max-age=0, must-revalidate - Content-Disposition: - - attachment - Content-Security-Policy-Report-Only: - - 'script-src ''nonce-mr2lw7MHsYFKcXi1tgb+7Q'' ''unsafe-inline'' ''strict-dynamic'' - https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri - https://csp.withgoogle.com/csp/gerritcodereview/1' - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 19 Apr 2022 17:13:55 GMT - Expires: - - Mon, 01 Jan 1990 00:00:00 GMT - Pragma: - - no-cache - Strict-Transport-Security: - - max-age=31536000; includeSubDomains; preload - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - "0" - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: {} - url: https://gerrit-review.googlesource.com/projects/?S=125&n=25 - method: GET - response: - body: | - )]}' - {"plugins/its-redmine":{"id":"plugins%2Fits-redmine","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-redmine/","target":"_blank"}]},"plugins/its-rtc":{"id":"plugins%2Fits-rtc","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-rtc/","target":"_blank"}]},"plugins/its-storyboard":{"id":"plugins%2Fits-storyboard","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-storyboard/","target":"_blank"}]},"plugins/javamelody":{"id":"plugins%2Fjavamelody","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/javamelody/","target":"_blank"}]},"plugins/kafka-events":{"id":"plugins%2Fkafka-events","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/kafka-events/","target":"_blank"}]},"plugins/labelui":{"id":"plugins%2Flabelui","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/labelui/","target":"_blank"}]},"plugins/lfs":{"id":"plugins%2Flfs","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/lfs/","target":"_blank"}]},"plugins/lfs-storage-fs":{"id":"plugins%2Flfs-storage-fs","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/lfs-storage-fs/","target":"_blank"}]},"plugins/lfs-storage-s3":{"id":"plugins%2Flfs-storage-s3","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/lfs-storage-s3/","target":"_blank"}]},"plugins/log-level":{"id":"plugins%2Flog-level","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/log-level/","target":"_blank"}]},"plugins/login-redirect":{"id":"plugins%2Flogin-redirect","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/login-redirect/","target":"_blank"}]},"plugins/maintainer":{"id":"plugins%2Fmaintainer","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/maintainer/","target":"_blank"}]},"plugins/manifest":{"id":"plugins%2Fmanifest","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/manifest/","target":"_blank"}]},"plugins/manifest-subscription":{"id":"plugins%2Fmanifest-subscription","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/manifest-subscription/","target":"_blank"}]},"plugins/menuextender":{"id":"plugins%2Fmenuextender","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/menuextender/","target":"_blank"}]},"plugins/messageoftheday":{"id":"plugins%2Fmessageoftheday","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/messageoftheday/","target":"_blank"}]},"plugins/metrics-reporter-cloudwatch":{"id":"plugins%2Fmetrics-reporter-cloudwatch","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-cloudwatch/","target":"_blank"}]},"plugins/metrics-reporter-elasticsearch":{"id":"plugins%2Fmetrics-reporter-elasticsearch","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-elasticsearch/","target":"_blank"}]},"plugins/metrics-reporter-graphite":{"id":"plugins%2Fmetrics-reporter-graphite","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-graphite/","target":"_blank"}]},"plugins/metrics-reporter-jmx":{"id":"plugins%2Fmetrics-reporter-jmx","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-jmx/","target":"_blank"}]},"plugins/metrics-reporter-prometheus":{"id":"plugins%2Fmetrics-reporter-prometheus","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-prometheus/","target":"_blank"}]},"plugins/motd":{"id":"plugins%2Fmotd","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/motd/","target":"_blank"}]},"plugins/multi-master":{"id":"plugins%2Fmulti-master","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/multi-master/","target":"_blank"}]},"plugins/multi-site":{"id":"plugins%2Fmulti-site","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/multi-site/","target":"_blank"}]},"plugins/oauth":{"id":"plugins%2Foauth","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/oauth/","target":"_blank"}]}} + {"plugins/gitgroups":{"id":"plugins%2Fgitgroups","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gitgroups/","target":"_blank"}]},"plugins/github":{"id":"plugins%2Fgithub","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github/","target":"_blank"}]},"plugins/github-groups":{"id":"plugins%2Fgithub-groups","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-groups/","target":"_blank"}]},"plugins/github-profile":{"id":"plugins%2Fgithub-profile","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-profile/","target":"_blank"}]},"plugins/github-pullrequest":{"id":"plugins%2Fgithub-pullrequest","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-pullrequest/","target":"_blank"}]},"plugins/github-replication":{"id":"plugins%2Fgithub-replication","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-replication/","target":"_blank"}]},"plugins/github-webhooks":{"id":"plugins%2Fgithub-webhooks","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/github-webhooks/","target":"_blank"}]},"plugins/gitiles":{"id":"plugins%2Fgitiles","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/gitiles/","target":"_blank"}]},"plugins/go-import":{"id":"plugins%2Fgo-import","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/go-import/","target":"_blank"}]},"plugins/google-apps-group":{"id":"plugins%2Fgoogle-apps-group","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/google-apps-group/","target":"_blank"}]},"plugins/healthcheck":{"id":"plugins%2Fhealthcheck","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/healthcheck/","target":"_blank"}]},"plugins/heartbeat":{"id":"plugins%2Fheartbeat","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/heartbeat/","target":"_blank"}]},"plugins/helloworld":{"id":"plugins%2Fhelloworld","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/helloworld/","target":"_blank"}]},"plugins/hide-actions":{"id":"plugins%2Fhide-actions","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hide-actions/","target":"_blank"}]},"plugins/high-availability":{"id":"plugins%2Fhigh-availability","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/high-availability/","target":"_blank"}]},"plugins/hooks":{"id":"plugins%2Fhooks","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks/","target":"_blank"}]},"plugins/hooks-audit":{"id":"plugins%2Fhooks-audit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-audit/","target":"_blank"}]},"plugins/hooks-bugzilla":{"id":"plugins%2Fhooks-bugzilla","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-bugzilla/","target":"_blank"}]},"plugins/hooks-its":{"id":"plugins%2Fhooks-its","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-its/","target":"_blank"}]},"plugins/hooks-jira":{"id":"plugins%2Fhooks-jira","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-jira/","target":"_blank"}]},"plugins/hooks-rtc":{"id":"plugins%2Fhooks-rtc","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/hooks-rtc/","target":"_blank"}]},"plugins/imagare":{"id":"plugins%2Fimagare","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/imagare/","target":"_blank"}]},"plugins/image-diff":{"id":"plugins%2Fimage-diff","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/image-diff/","target":"_blank"}]},"plugins/importer":{"id":"plugins%2Fimporter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/importer/","target":"_blank"}]},"plugins/its-base":{"id":"plugins%2Fits-base","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-base/","target":"_blank"}]}} headers: Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Cache-Control: - no-cache, no-store, max-age=0, must-revalidate Content-Disposition: - attachment Content-Security-Policy-Report-Only: - - 'script-src ''nonce-1ASgR5RQxpQHbZMBn6sywQ'' ''unsafe-inline'' ''strict-dynamic'' + - 'script-src ''nonce-698hhxWIxQ9w5YWX4xdJTg'' ''unsafe-inline'' ''strict-dynamic'' https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri https://csp.withgoogle.com/csp/gerritcodereview/1' Content-Type: - application/json; charset=utf-8 Date: - - Tue, 19 Apr 2022 17:13:55 GMT + - Fri, 27 Jan 2023 13:00:58 GMT Expires: - Mon, 01 Jan 1990 00:00:00 GMT Pragma: @@ -461,64 +210,22 @@ interactions: response: body: | )]}' - {"plugins/its-redmine":{"id":"plugins%2Fits-redmine","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-redmine/","target":"_blank"}]},"plugins/its-rtc":{"id":"plugins%2Fits-rtc","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-rtc/","target":"_blank"}]},"plugins/its-storyboard":{"id":"plugins%2Fits-storyboard","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-storyboard/","target":"_blank"}]},"plugins/javamelody":{"id":"plugins%2Fjavamelody","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/javamelody/","target":"_blank"}]},"plugins/kafka-events":{"id":"plugins%2Fkafka-events","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/kafka-events/","target":"_blank"}]},"plugins/labelui":{"id":"plugins%2Flabelui","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/labelui/","target":"_blank"}]},"plugins/lfs":{"id":"plugins%2Flfs","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/lfs/","target":"_blank"}]},"plugins/lfs-storage-fs":{"id":"plugins%2Flfs-storage-fs","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/lfs-storage-fs/","target":"_blank"}]},"plugins/lfs-storage-s3":{"id":"plugins%2Flfs-storage-s3","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/lfs-storage-s3/","target":"_blank"}]},"plugins/log-level":{"id":"plugins%2Flog-level","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/log-level/","target":"_blank"}]},"plugins/login-redirect":{"id":"plugins%2Flogin-redirect","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/login-redirect/","target":"_blank"}]},"plugins/maintainer":{"id":"plugins%2Fmaintainer","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/maintainer/","target":"_blank"}]},"plugins/manifest":{"id":"plugins%2Fmanifest","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/manifest/","target":"_blank"}]},"plugins/manifest-subscription":{"id":"plugins%2Fmanifest-subscription","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/manifest-subscription/","target":"_blank"}]},"plugins/menuextender":{"id":"plugins%2Fmenuextender","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/menuextender/","target":"_blank"}]},"plugins/messageoftheday":{"id":"plugins%2Fmessageoftheday","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/messageoftheday/","target":"_blank"}]},"plugins/metrics-reporter-cloudwatch":{"id":"plugins%2Fmetrics-reporter-cloudwatch","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-cloudwatch/","target":"_blank"}]},"plugins/metrics-reporter-elasticsearch":{"id":"plugins%2Fmetrics-reporter-elasticsearch","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-elasticsearch/","target":"_blank"}]},"plugins/metrics-reporter-graphite":{"id":"plugins%2Fmetrics-reporter-graphite","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-graphite/","target":"_blank"}]},"plugins/metrics-reporter-jmx":{"id":"plugins%2Fmetrics-reporter-jmx","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-jmx/","target":"_blank"}]},"plugins/metrics-reporter-prometheus":{"id":"plugins%2Fmetrics-reporter-prometheus","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-prometheus/","target":"_blank"}]},"plugins/motd":{"id":"plugins%2Fmotd","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/motd/","target":"_blank"}]},"plugins/multi-master":{"id":"plugins%2Fmulti-master","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/multi-master/","target":"_blank"}]},"plugins/multi-site":{"id":"plugins%2Fmulti-site","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/multi-site/","target":"_blank"}]},"plugins/oauth":{"id":"plugins%2Foauth","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/oauth/","target":"_blank"}]}} - headers: - Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" - Cache-Control: - - no-cache, no-store, max-age=0, must-revalidate - Content-Disposition: - - attachment - Content-Security-Policy-Report-Only: - - 'script-src ''nonce-6J6TyccZ6KOEiao77OgOlQ'' ''unsafe-inline'' ''strict-dynamic'' - https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri - https://csp.withgoogle.com/csp/gerritcodereview/1' - Content-Type: - - application/json; charset=utf-8 - Date: - - Tue, 19 Apr 2022 17:13:55 GMT - Expires: - - Mon, 01 Jan 1990 00:00:00 GMT - Pragma: - - no-cache - Strict-Transport-Security: - - max-age=31536000; includeSubDomains; preload - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - "0" - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: {} - url: https://gerrit-review.googlesource.com/projects/?S=150&n=25 - method: GET - response: - body: | - )]}' - {"plugins/out-of-the-box":{"id":"plugins%2Fout-of-the-box","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/out-of-the-box/","target":"_blank"}]},"plugins/owners":{"id":"plugins%2Fowners","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/owners/","target":"_blank"}]},"plugins/plugin-manager":{"id":"plugins%2Fplugin-manager","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/plugin-manager/","target":"_blank"}]},"plugins/project-download-commands":{"id":"plugins%2Fproject-download-commands","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/project-download-commands/","target":"_blank"}]},"plugins/project-group-structure":{"id":"plugins%2Fproject-group-structure","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/project-group-structure/","target":"_blank"}]},"plugins/prolog-submit-rules":{"id":"plugins%2Fprolog-submit-rules","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/prolog-submit-rules/","target":"_blank"}]},"plugins/pull-replication":{"id":"plugins%2Fpull-replication","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/pull-replication/","target":"_blank"}]},"plugins/push-pull-replication":{"id":"plugins%2Fpush-pull-replication","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/push-pull-replication/","target":"_blank"}]},"plugins/quickstart":{"id":"plugins%2Fquickstart","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/quickstart/","target":"_blank"}]},"plugins/quota":{"id":"plugins%2Fquota","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/quota/","target":"_blank"}]},"plugins/rabbitmq":{"id":"plugins%2Frabbitmq","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/rabbitmq/","target":"_blank"}]},"plugins/rate-limiter":{"id":"plugins%2Frate-limiter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/rate-limiter/","target":"_blank"}]},"plugins/readonly":{"id":"plugins%2Freadonly","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/readonly/","target":"_blank"}]},"plugins/ref-copy":{"id":"plugins%2Fref-copy","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/ref-copy/","target":"_blank"}]},"plugins/ref-protection":{"id":"plugins%2Fref-protection","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/ref-protection/","target":"_blank"}]},"plugins/reject-private-submit":{"id":"plugins%2Freject-private-submit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reject-private-submit/","target":"_blank"}]},"plugins/rename-project":{"id":"plugins%2Frename-project","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/rename-project/","target":"_blank"}]},"plugins/reparent":{"id":"plugins%2Freparent","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reparent/","target":"_blank"}]},"plugins/replication":{"id":"plugins%2Freplication","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/replication/","target":"_blank"}]},"plugins/replication-status":{"id":"plugins%2Freplication-status","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/replication-status/","target":"_blank"}]},"plugins/repository-usage":{"id":"plugins%2Frepository-usage","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/repository-usage/","target":"_blank"}]},"plugins/review-strategy":{"id":"plugins%2Freview-strategy","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/review-strategy/","target":"_blank"}]},"plugins/reviewassistant":{"id":"plugins%2Freviewassistant","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reviewassistant/","target":"_blank"}]},"plugins/reviewers":{"id":"plugins%2Freviewers","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reviewers/","target":"_blank"}]},"plugins/reviewers-by-blame":{"id":"plugins%2Freviewers-by-blame","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reviewers-by-blame/","target":"_blank"}]}} + {"plugins/its-bugzilla":{"id":"plugins%2Fits-bugzilla","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-bugzilla/","target":"_blank"}]},"plugins/its-github":{"id":"plugins%2Fits-github","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-github/","target":"_blank"}]},"plugins/its-jira":{"id":"plugins%2Fits-jira","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-jira/","target":"_blank"}]},"plugins/its-phabricator":{"id":"plugins%2Fits-phabricator","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-phabricator/","target":"_blank"}]},"plugins/its-redmine":{"id":"plugins%2Fits-redmine","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-redmine/","target":"_blank"}]},"plugins/its-rtc":{"id":"plugins%2Fits-rtc","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-rtc/","target":"_blank"}]},"plugins/its-storyboard":{"id":"plugins%2Fits-storyboard","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/its-storyboard/","target":"_blank"}]},"plugins/javamelody":{"id":"plugins%2Fjavamelody","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/javamelody/","target":"_blank"}]},"plugins/kafka-events":{"id":"plugins%2Fkafka-events","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/kafka-events/","target":"_blank"}]},"plugins/labelui":{"id":"plugins%2Flabelui","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/labelui/","target":"_blank"}]},"plugins/lfs":{"id":"plugins%2Flfs","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/lfs/","target":"_blank"}]},"plugins/lfs-storage-fs":{"id":"plugins%2Flfs-storage-fs","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/lfs-storage-fs/","target":"_blank"}]},"plugins/lfs-storage-s3":{"id":"plugins%2Flfs-storage-s3","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/lfs-storage-s3/","target":"_blank"}]},"plugins/log-level":{"id":"plugins%2Flog-level","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/log-level/","target":"_blank"}]},"plugins/login-redirect":{"id":"plugins%2Flogin-redirect","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/login-redirect/","target":"_blank"}]},"plugins/maintainer":{"id":"plugins%2Fmaintainer","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/maintainer/","target":"_blank"}]},"plugins/manifest-subscription":{"id":"plugins%2Fmanifest-subscription","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/manifest-subscription/","target":"_blank"}]},"plugins/menuextender":{"id":"plugins%2Fmenuextender","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/menuextender/","target":"_blank"}]},"plugins/messageoftheday":{"id":"plugins%2Fmessageoftheday","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/messageoftheday/","target":"_blank"}]},"plugins/metrics-reporter-cloudwatch":{"id":"plugins%2Fmetrics-reporter-cloudwatch","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-cloudwatch/","target":"_blank"}]},"plugins/metrics-reporter-elasticsearch":{"id":"plugins%2Fmetrics-reporter-elasticsearch","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-elasticsearch/","target":"_blank"}]},"plugins/metrics-reporter-graphite":{"id":"plugins%2Fmetrics-reporter-graphite","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-graphite/","target":"_blank"}]},"plugins/metrics-reporter-jmx":{"id":"plugins%2Fmetrics-reporter-jmx","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-jmx/","target":"_blank"}]},"plugins/metrics-reporter-prometheus":{"id":"plugins%2Fmetrics-reporter-prometheus","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/metrics-reporter-prometheus/","target":"_blank"}]},"plugins/motd":{"id":"plugins%2Fmotd","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/motd/","target":"_blank"}]}} headers: Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Cache-Control: - no-cache, no-store, max-age=0, must-revalidate Content-Disposition: - attachment Content-Security-Policy-Report-Only: - - 'script-src ''nonce-m1CX6gz0hqtAcbq8lgFA4Q'' ''unsafe-inline'' ''strict-dynamic'' + - 'script-src ''nonce-L-xv-wVA8Tk3-K86dwGVxA'' ''unsafe-inline'' ''strict-dynamic'' https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri https://csp.withgoogle.com/csp/gerritcodereview/1' Content-Type: - application/json; charset=utf-8 Date: - - Tue, 19 Apr 2022 17:13:56 GMT + - Fri, 27 Jan 2023 13:00:59 GMT Expires: - Mon, 01 Jan 1990 00:00:00 GMT Pragma: @@ -543,23 +250,22 @@ interactions: response: body: | )]}' - {"plugins/out-of-the-box":{"id":"plugins%2Fout-of-the-box","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/out-of-the-box/","target":"_blank"}]},"plugins/owners":{"id":"plugins%2Fowners","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/owners/","target":"_blank"}]},"plugins/plugin-manager":{"id":"plugins%2Fplugin-manager","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/plugin-manager/","target":"_blank"}]},"plugins/project-download-commands":{"id":"plugins%2Fproject-download-commands","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/project-download-commands/","target":"_blank"}]},"plugins/project-group-structure":{"id":"plugins%2Fproject-group-structure","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/project-group-structure/","target":"_blank"}]},"plugins/prolog-submit-rules":{"id":"plugins%2Fprolog-submit-rules","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/prolog-submit-rules/","target":"_blank"}]},"plugins/pull-replication":{"id":"plugins%2Fpull-replication","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/pull-replication/","target":"_blank"}]},"plugins/push-pull-replication":{"id":"plugins%2Fpush-pull-replication","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/push-pull-replication/","target":"_blank"}]},"plugins/quickstart":{"id":"plugins%2Fquickstart","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/quickstart/","target":"_blank"}]},"plugins/quota":{"id":"plugins%2Fquota","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/quota/","target":"_blank"}]},"plugins/rabbitmq":{"id":"plugins%2Frabbitmq","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/rabbitmq/","target":"_blank"}]},"plugins/rate-limiter":{"id":"plugins%2Frate-limiter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/rate-limiter/","target":"_blank"}]},"plugins/readonly":{"id":"plugins%2Freadonly","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/readonly/","target":"_blank"}]},"plugins/ref-copy":{"id":"plugins%2Fref-copy","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/ref-copy/","target":"_blank"}]},"plugins/ref-protection":{"id":"plugins%2Fref-protection","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/ref-protection/","target":"_blank"}]},"plugins/reject-private-submit":{"id":"plugins%2Freject-private-submit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reject-private-submit/","target":"_blank"}]},"plugins/rename-project":{"id":"plugins%2Frename-project","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/rename-project/","target":"_blank"}]},"plugins/reparent":{"id":"plugins%2Freparent","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reparent/","target":"_blank"}]},"plugins/replication":{"id":"plugins%2Freplication","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/replication/","target":"_blank"}]},"plugins/replication-status":{"id":"plugins%2Freplication-status","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/replication-status/","target":"_blank"}]},"plugins/repository-usage":{"id":"plugins%2Frepository-usage","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/repository-usage/","target":"_blank"}]},"plugins/review-strategy":{"id":"plugins%2Freview-strategy","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/review-strategy/","target":"_blank"}]},"plugins/reviewassistant":{"id":"plugins%2Freviewassistant","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reviewassistant/","target":"_blank"}]},"plugins/reviewers":{"id":"plugins%2Freviewers","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reviewers/","target":"_blank"}]},"plugins/reviewers-by-blame":{"id":"plugins%2Freviewers-by-blame","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reviewers-by-blame/","target":"_blank"}]}} + {"plugins/multi-master":{"id":"plugins%2Fmulti-master","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/multi-master/","target":"_blank"}]},"plugins/multi-site":{"id":"plugins%2Fmulti-site","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/multi-site/","target":"_blank"}]},"plugins/oauth":{"id":"plugins%2Foauth","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/oauth/","target":"_blank"}]},"plugins/out-of-the-box":{"id":"plugins%2Fout-of-the-box","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/out-of-the-box/","target":"_blank"}]},"plugins/owners":{"id":"plugins%2Fowners","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/owners/","target":"_blank"}]},"plugins/plugin-manager":{"id":"plugins%2Fplugin-manager","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/plugin-manager/","target":"_blank"}]},"plugins/project-download-commands":{"id":"plugins%2Fproject-download-commands","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/project-download-commands/","target":"_blank"}]},"plugins/project-group-structure":{"id":"plugins%2Fproject-group-structure","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/project-group-structure/","target":"_blank"}]},"plugins/prolog-submit-rules":{"id":"plugins%2Fprolog-submit-rules","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/prolog-submit-rules/","target":"_blank"}]},"plugins/pull-replication":{"id":"plugins%2Fpull-replication","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/pull-replication/","target":"_blank"}]},"plugins/push-pull-replication":{"id":"plugins%2Fpush-pull-replication","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/push-pull-replication/","target":"_blank"}]},"plugins/quickstart":{"id":"plugins%2Fquickstart","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/quickstart/","target":"_blank"}]},"plugins/quota":{"id":"plugins%2Fquota","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/quota/","target":"_blank"}]},"plugins/rabbitmq":{"id":"plugins%2Frabbitmq","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/rabbitmq/","target":"_blank"}]},"plugins/rate-limiter":{"id":"plugins%2Frate-limiter","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/rate-limiter/","target":"_blank"}]},"plugins/readonly":{"id":"plugins%2Freadonly","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/readonly/","target":"_blank"}]},"plugins/ref-copy":{"id":"plugins%2Fref-copy","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/ref-copy/","target":"_blank"}]},"plugins/ref-protection":{"id":"plugins%2Fref-protection","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/ref-protection/","target":"_blank"}]},"plugins/reject-private-submit":{"id":"plugins%2Freject-private-submit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reject-private-submit/","target":"_blank"}]},"plugins/rename-project":{"id":"plugins%2Frename-project","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/rename-project/","target":"_blank"}]},"plugins/reparent":{"id":"plugins%2Freparent","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reparent/","target":"_blank"}]},"plugins/replication":{"id":"plugins%2Freplication","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/replication/","target":"_blank"}]},"plugins/replication-status":{"id":"plugins%2Freplication-status","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/replication-status/","target":"_blank"}]},"plugins/repository-usage":{"id":"plugins%2Frepository-usage","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/repository-usage/","target":"_blank"}]},"plugins/review-strategy":{"id":"plugins%2Freview-strategy","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/review-strategy/","target":"_blank"}]}} headers: Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Cache-Control: - no-cache, no-store, max-age=0, must-revalidate Content-Disposition: - attachment Content-Security-Policy-Report-Only: - - 'script-src ''nonce-xk+LknuXXPHs11T1RSXN6w'' ''unsafe-inline'' ''strict-dynamic'' + - 'script-src ''nonce-y32JXCBvlHLH6l02aE5iXg'' ''unsafe-inline'' ''strict-dynamic'' https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri https://csp.withgoogle.com/csp/gerritcodereview/1' Content-Type: - application/json; charset=utf-8 Date: - - Tue, 19 Apr 2022 17:13:56 GMT + - Fri, 27 Jan 2023 13:01:02 GMT Expires: - Mon, 01 Jan 1990 00:00:00 GMT Pragma: @@ -579,28 +285,27 @@ interactions: body: "" form: {} headers: {} - url: https://gerrit-review.googlesource.com/projects/?S=175&n=25 + url: https://gerrit-review.googlesource.com/projects/?S=175&n=25&type=CODE method: GET response: body: | )]}' - {"plugins/reviewnotes":{"id":"plugins%2Freviewnotes","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reviewnotes/","target":"_blank"}]},"plugins/saml":{"id":"plugins%2Fsaml","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/saml/","target":"_blank"}]},"plugins/scripting-rules":{"id":"plugins%2Fscripting-rules","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/scripting-rules/","target":"_blank"}]},"plugins/scripting/groovy-provider":{"id":"plugins%2Fscripting%2Fgroovy-provider","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/scripting/groovy-provider/","target":"_blank"}]},"plugins/scripting/scala-provider":{"id":"plugins%2Fscripting%2Fscala-provider","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/scripting/scala-provider/","target":"_blank"}]},"plugins/scripts":{"id":"plugins%2Fscripts","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/scripts/","target":"_blank"}]},"plugins/secure-config":{"id":"plugins%2Fsecure-config","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/secure-config/","target":"_blank"}]},"plugins/server-config":{"id":"plugins%2Fserver-config","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/server-config/","target":"_blank"}]},"plugins/server-log-viewer":{"id":"plugins%2Fserver-log-viewer","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/server-log-viewer/","target":"_blank"}]},"plugins/serviceuser":{"id":"plugins%2Fserviceuser","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/serviceuser/","target":"_blank"}]},"plugins/shutdown":{"id":"plugins%2Fshutdown","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/shutdown/","target":"_blank"}]},"plugins/simple-submit-rules":{"id":"plugins%2Fsimple-submit-rules","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/simple-submit-rules/","target":"_blank"}]},"plugins/singleusergroup":{"id":"plugins%2Fsingleusergroup","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/singleusergroup/","target":"_blank"}]},"plugins/slack-integration":{"id":"plugins%2Fslack-integration","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/slack-integration/","target":"_blank"}]},"plugins/startblock":{"id":"plugins%2Fstartblock","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/startblock/","target":"_blank"}]},"plugins/supermanifest":{"id":"plugins%2Fsupermanifest","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/supermanifest/","target":"_blank"}]},"plugins/sync-events":{"id":"plugins%2Fsync-events","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/sync-events/","target":"_blank"}]},"plugins/sync-index":{"id":"plugins%2Fsync-index","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/sync-index/","target":"_blank"}]},"plugins/task":{"id":"plugins%2Ftask","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/task/","target":"_blank"}]},"plugins/uploadvalidator":{"id":"plugins%2Fuploadvalidator","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/uploadvalidator/","target":"_blank"}]},"plugins/verify-status":{"id":"plugins%2Fverify-status","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/verify-status/","target":"_blank"}]},"plugins/webhooks":{"id":"plugins%2Fwebhooks","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/webhooks/","target":"_blank"}]},"plugins/websession-broker":{"id":"plugins%2Fwebsession-broker","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/websession-broker/","target":"_blank"}]},"plugins/websession-flatfile":{"id":"plugins%2Fwebsession-flatfile","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/websession-flatfile/","target":"_blank"}]},"plugins/wip":{"id":"plugins%2Fwip","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/wip/","target":"_blank"}]}} + {"plugins/reviewassistant":{"id":"plugins%2Freviewassistant","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reviewassistant/","target":"_blank"}]},"plugins/reviewers":{"id":"plugins%2Freviewers","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reviewers/","target":"_blank"}]},"plugins/reviewers-by-blame":{"id":"plugins%2Freviewers-by-blame","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reviewers-by-blame/","target":"_blank"}]},"plugins/reviewnotes":{"id":"plugins%2Freviewnotes","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reviewnotes/","target":"_blank"}]},"plugins/saml":{"id":"plugins%2Fsaml","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/saml/","target":"_blank"}]},"plugins/scripting-rules":{"id":"plugins%2Fscripting-rules","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/scripting-rules/","target":"_blank"}]},"plugins/scripting/groovy-provider":{"id":"plugins%2Fscripting%2Fgroovy-provider","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/scripting/groovy-provider/","target":"_blank"}]},"plugins/scripting/scala-provider":{"id":"plugins%2Fscripting%2Fscala-provider","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/scripting/scala-provider/","target":"_blank"}]},"plugins/scripts":{"id":"plugins%2Fscripts","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/scripts/","target":"_blank"}]},"plugins/secure-config":{"id":"plugins%2Fsecure-config","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/secure-config/","target":"_blank"}]},"plugins/server-config":{"id":"plugins%2Fserver-config","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/server-config/","target":"_blank"}]},"plugins/server-log-viewer":{"id":"plugins%2Fserver-log-viewer","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/server-log-viewer/","target":"_blank"}]},"plugins/serviceuser":{"id":"plugins%2Fserviceuser","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/serviceuser/","target":"_blank"}]},"plugins/shutdown":{"id":"plugins%2Fshutdown","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/shutdown/","target":"_blank"}]},"plugins/simple-submit-rules":{"id":"plugins%2Fsimple-submit-rules","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/simple-submit-rules/","target":"_blank"}]},"plugins/singleusergroup":{"id":"plugins%2Fsingleusergroup","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/singleusergroup/","target":"_blank"}]},"plugins/slack-integration":{"id":"plugins%2Fslack-integration","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/slack-integration/","target":"_blank"}]},"plugins/supermanifest":{"id":"plugins%2Fsupermanifest","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/supermanifest/","target":"_blank"}]},"plugins/sync-events":{"id":"plugins%2Fsync-events","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/sync-events/","target":"_blank"}]},"plugins/sync-index":{"id":"plugins%2Fsync-index","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/sync-index/","target":"_blank"}]},"plugins/task":{"id":"plugins%2Ftask","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/task/","target":"_blank"}]},"plugins/uploadvalidator":{"id":"plugins%2Fuploadvalidator","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/uploadvalidator/","target":"_blank"}]},"plugins/verify-status":{"id":"plugins%2Fverify-status","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/verify-status/","target":"_blank"}]},"plugins/webhooks":{"id":"plugins%2Fwebhooks","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/webhooks/","target":"_blank"}]},"plugins/websession-broker":{"id":"plugins%2Fwebsession-broker","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/websession-broker/","target":"_blank"}]}} headers: Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Cache-Control: - no-cache, no-store, max-age=0, must-revalidate Content-Disposition: - attachment Content-Security-Policy-Report-Only: - - 'script-src ''nonce-wyjaMVEdIdILU+r5j2ojdw'' ''unsafe-inline'' ''strict-dynamic'' + - 'script-src ''nonce--qICSwb_5IH1YBloTUfnlQ'' ''unsafe-inline'' ''strict-dynamic'' https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri https://csp.withgoogle.com/csp/gerritcodereview/1' Content-Type: - application/json; charset=utf-8 Date: - - Tue, 19 Apr 2022 17:13:56 GMT + - Fri, 27 Jan 2023 13:01:03 GMT Expires: - Mon, 01 Jan 1990 00:00:00 GMT Pragma: @@ -620,28 +325,27 @@ interactions: body: "" form: {} headers: {} - url: https://gerrit-review.googlesource.com/projects/?S=175&n=25&type=CODE + url: https://gerrit-review.googlesource.com/projects/?S=200&n=25&type=CODE method: GET response: body: | )]}' - {"plugins/reviewnotes":{"id":"plugins%2Freviewnotes","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/reviewnotes/","target":"_blank"}]},"plugins/saml":{"id":"plugins%2Fsaml","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/saml/","target":"_blank"}]},"plugins/scripting-rules":{"id":"plugins%2Fscripting-rules","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/scripting-rules/","target":"_blank"}]},"plugins/scripting/groovy-provider":{"id":"plugins%2Fscripting%2Fgroovy-provider","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/scripting/groovy-provider/","target":"_blank"}]},"plugins/scripting/scala-provider":{"id":"plugins%2Fscripting%2Fscala-provider","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/scripting/scala-provider/","target":"_blank"}]},"plugins/scripts":{"id":"plugins%2Fscripts","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/scripts/","target":"_blank"}]},"plugins/secure-config":{"id":"plugins%2Fsecure-config","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/secure-config/","target":"_blank"}]},"plugins/server-config":{"id":"plugins%2Fserver-config","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/server-config/","target":"_blank"}]},"plugins/server-log-viewer":{"id":"plugins%2Fserver-log-viewer","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/server-log-viewer/","target":"_blank"}]},"plugins/serviceuser":{"id":"plugins%2Fserviceuser","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/serviceuser/","target":"_blank"}]},"plugins/shutdown":{"id":"plugins%2Fshutdown","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/shutdown/","target":"_blank"}]},"plugins/simple-submit-rules":{"id":"plugins%2Fsimple-submit-rules","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/simple-submit-rules/","target":"_blank"}]},"plugins/singleusergroup":{"id":"plugins%2Fsingleusergroup","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/singleusergroup/","target":"_blank"}]},"plugins/slack-integration":{"id":"plugins%2Fslack-integration","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/slack-integration/","target":"_blank"}]},"plugins/startblock":{"id":"plugins%2Fstartblock","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/startblock/","target":"_blank"}]},"plugins/supermanifest":{"id":"plugins%2Fsupermanifest","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/supermanifest/","target":"_blank"}]},"plugins/sync-events":{"id":"plugins%2Fsync-events","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/sync-events/","target":"_blank"}]},"plugins/sync-index":{"id":"plugins%2Fsync-index","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/sync-index/","target":"_blank"}]},"plugins/task":{"id":"plugins%2Ftask","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/task/","target":"_blank"}]},"plugins/uploadvalidator":{"id":"plugins%2Fuploadvalidator","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/uploadvalidator/","target":"_blank"}]},"plugins/verify-status":{"id":"plugins%2Fverify-status","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/verify-status/","target":"_blank"}]},"plugins/webhooks":{"id":"plugins%2Fwebhooks","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/webhooks/","target":"_blank"}]},"plugins/websession-broker":{"id":"plugins%2Fwebsession-broker","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/websession-broker/","target":"_blank"}]},"plugins/websession-flatfile":{"id":"plugins%2Fwebsession-flatfile","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/websession-flatfile/","target":"_blank"}]},"plugins/wip":{"id":"plugins%2Fwip","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/wip/","target":"_blank"}]}} + {"plugins/websession-flatfile":{"id":"plugins%2Fwebsession-flatfile","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/websession-flatfile/","target":"_blank"}]},"plugins/wip":{"id":"plugins%2Fwip","state":"READ_ONLY","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/wip/","target":"_blank"}]},"plugins/wmf-fixshadowuser":{"id":"plugins%2Fwmf-fixshadowuser","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/wmf-fixshadowuser/","target":"_blank"}]},"plugins/x-docs":{"id":"plugins%2Fx-docs","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/x-docs/","target":"_blank"}]},"plugins/zookeeper-refdb":{"id":"plugins%2Fzookeeper-refdb","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/zookeeper-refdb/","target":"_blank"}]},"plugins/zuul":{"id":"plugins%2Fzuul","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/zuul/","target":"_blank"}]},"plugins/zuul-results-summary":{"id":"plugins%2Fzuul-results-summary","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/zuul-results-summary/","target":"_blank"}]},"plugins/zuul-status":{"id":"plugins%2Fzuul-status","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/zuul-status/","target":"_blank"}]},"polymer-bridges":{"id":"polymer-bridges","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/polymer-bridges/","target":"_blank"}]},"prolog-cafe":{"id":"prolog-cafe","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/prolog-cafe/","target":"_blank"}]},"summit/2015":{"id":"summit%2F2015","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2015/","target":"_blank"}]},"summit/2016":{"id":"summit%2F2016","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2016/","target":"_blank"}]},"summit/2017":{"id":"summit%2F2017","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2017/","target":"_blank"}]},"summit/2018":{"id":"summit%2F2018","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2018/","target":"_blank"}]},"summit/2019":{"id":"summit%2F2019","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2019/","target":"_blank"}]},"summit/2021":{"id":"summit%2F2021","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2021/","target":"_blank"}]},"summit/2022":{"id":"summit%2F2022","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2022/","target":"_blank"}]},"training/gerrit":{"id":"training%2Fgerrit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/training/gerrit/","target":"_blank"}]},"training/sample":{"id":"training%2Fsample","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/training/sample/","target":"_blank"}]},"zoekt":{"id":"zoekt","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zoekt/","target":"_blank"}]},"zookeeper-refdb":{"id":"zookeeper-refdb","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zookeeper-refdb/","target":"_blank"}]},"zuul/config":{"id":"zuul%2Fconfig","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zuul/config/","target":"_blank"}]},"zuul/jobs":{"id":"zuul%2Fjobs","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zuul/jobs/","target":"_blank"}]},"zuul/ops":{"id":"zuul%2Fops","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zuul/ops/","target":"_blank"}]}} headers: Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Cache-Control: - no-cache, no-store, max-age=0, must-revalidate Content-Disposition: - attachment Content-Security-Policy-Report-Only: - - 'script-src ''nonce-l+cZs41uMEEPrL8Cn79xJg'' ''unsafe-inline'' ''strict-dynamic'' + - 'script-src ''nonce-Ey55fsyVPtyfGD8j5ZlK1Q'' ''unsafe-inline'' ''strict-dynamic'' https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri https://csp.withgoogle.com/csp/gerritcodereview/1' Content-Type: - application/json; charset=utf-8 Date: - - Tue, 19 Apr 2022 17:13:56 GMT + - Fri, 27 Jan 2023 13:01:04 GMT Expires: - Mon, 01 Jan 1990 00:00:00 GMT Pragma: @@ -661,28 +365,29 @@ interactions: body: "" form: {} headers: {} - url: https://gerrit-review.googlesource.com/projects/?S=200&n=25 + url: https://gerrit-review.googlesource.com/projects/?S=225&n=25&type=CODE method: GET response: body: | )]}' - {"plugins/wmf-fixshadowuser":{"id":"plugins%2Fwmf-fixshadowuser","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/wmf-fixshadowuser/","target":"_blank"}]},"plugins/x-docs":{"id":"plugins%2Fx-docs","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/x-docs/","target":"_blank"}]},"plugins/zookeeper-refdb":{"id":"plugins%2Fzookeeper-refdb","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/zookeeper-refdb/","target":"_blank"}]},"plugins/zuul":{"id":"plugins%2Fzuul","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/zuul/","target":"_blank"}]},"plugins/zuul-results-summary":{"id":"plugins%2Fzuul-results-summary","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/zuul-results-summary/","target":"_blank"}]},"plugins/zuul-status":{"id":"plugins%2Fzuul-status","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/zuul-status/","target":"_blank"}]},"polymer-bridges":{"id":"polymer-bridges","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/polymer-bridges/","target":"_blank"}]},"prolog-cafe":{"id":"prolog-cafe","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/prolog-cafe/","target":"_blank"}]},"summit/2015":{"id":"summit%2F2015","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2015/","target":"_blank"}]},"summit/2016":{"id":"summit%2F2016","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2016/","target":"_blank"}]},"summit/2017":{"id":"summit%2F2017","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2017/","target":"_blank"}]},"summit/2018":{"id":"summit%2F2018","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2018/","target":"_blank"}]},"summit/2019":{"id":"summit%2F2019","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2019/","target":"_blank"}]},"summit/2021":{"id":"summit%2F2021","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2021/","target":"_blank"}]},"training/gerrit":{"id":"training%2Fgerrit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/training/gerrit/","target":"_blank"}]},"training/sample":{"id":"training%2Fsample","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/training/sample/","target":"_blank"}]},"zoekt":{"id":"zoekt","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zoekt/","target":"_blank"}]},"zookeeper-refdb":{"id":"zookeeper-refdb","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zookeeper-refdb/","target":"_blank"}]},"zuul/config":{"id":"zuul%2Fconfig","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zuul/config/","target":"_blank"}]},"zuul/jobs":{"id":"zuul%2Fjobs","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zuul/jobs/","target":"_blank"}]},"zuul/ops":{"id":"zuul%2Fops","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zuul/ops/","target":"_blank"}]}} + {} headers: Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Cache-Control: - no-cache, no-store, max-age=0, must-revalidate Content-Disposition: - attachment + Content-Length: + - "8" Content-Security-Policy-Report-Only: - - 'script-src ''nonce-Y6BFYJaVARF8u3QzCDV5tQ'' ''unsafe-inline'' ''strict-dynamic'' + - 'script-src ''nonce-Ec8-Bazqoa2K-mnuMBefWw'' ''unsafe-inline'' ''strict-dynamic'' https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri https://csp.withgoogle.com/csp/gerritcodereview/1' Content-Type: - application/json; charset=utf-8 Date: - - Tue, 19 Apr 2022 17:13:56 GMT + - Fri, 27 Jan 2023 13:01:06 GMT Expires: - Mon, 01 Jan 1990 00:00:00 GMT Pragma: @@ -702,28 +407,29 @@ interactions: body: "" form: {} headers: {} - url: https://gerrit-review.googlesource.com/projects/?S=200&n=25&type=CODE + url: https://gerrit-review.googlesource.com/projects/?S=250&n=1 method: GET response: body: | )]}' - {"plugins/wmf-fixshadowuser":{"id":"plugins%2Fwmf-fixshadowuser","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/wmf-fixshadowuser/","target":"_blank"}]},"plugins/x-docs":{"id":"plugins%2Fx-docs","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/x-docs/","target":"_blank"}]},"plugins/zookeeper-refdb":{"id":"plugins%2Fzookeeper-refdb","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/zookeeper-refdb/","target":"_blank"}]},"plugins/zuul":{"id":"plugins%2Fzuul","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/zuul/","target":"_blank"}]},"plugins/zuul-results-summary":{"id":"plugins%2Fzuul-results-summary","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/zuul-results-summary/","target":"_blank"}]},"plugins/zuul-status":{"id":"plugins%2Fzuul-status","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/plugins/zuul-status/","target":"_blank"}]},"polymer-bridges":{"id":"polymer-bridges","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/polymer-bridges/","target":"_blank"}]},"prolog-cafe":{"id":"prolog-cafe","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/prolog-cafe/","target":"_blank"}]},"summit/2015":{"id":"summit%2F2015","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2015/","target":"_blank"}]},"summit/2016":{"id":"summit%2F2016","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2016/","target":"_blank"}]},"summit/2017":{"id":"summit%2F2017","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2017/","target":"_blank"}]},"summit/2018":{"id":"summit%2F2018","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2018/","target":"_blank"}]},"summit/2019":{"id":"summit%2F2019","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2019/","target":"_blank"}]},"summit/2021":{"id":"summit%2F2021","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/summit/2021/","target":"_blank"}]},"training/gerrit":{"id":"training%2Fgerrit","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/training/gerrit/","target":"_blank"}]},"training/sample":{"id":"training%2Fsample","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/training/sample/","target":"_blank"}]},"zoekt":{"id":"zoekt","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zoekt/","target":"_blank"}]},"zookeeper-refdb":{"id":"zookeeper-refdb","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zookeeper-refdb/","target":"_blank"}]},"zuul/config":{"id":"zuul%2Fconfig","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zuul/config/","target":"_blank"}]},"zuul/jobs":{"id":"zuul%2Fjobs","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zuul/jobs/","target":"_blank"}]},"zuul/ops":{"id":"zuul%2Fops","state":"ACTIVE","web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/zuul/ops/","target":"_blank"}]}} + {} headers: Alt-Svc: - - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43" + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Cache-Control: - no-cache, no-store, max-age=0, must-revalidate Content-Disposition: - attachment + Content-Length: + - "8" Content-Security-Policy-Report-Only: - - 'script-src ''nonce-uagiEOhZEYcj2CCDhUJ5Jw'' ''unsafe-inline'' ''strict-dynamic'' + - 'script-src ''nonce-4skeVHBp7IRtri5tnTCFsQ'' ''unsafe-inline'' ''strict-dynamic'' https: http: ''unsafe-eval'';object-src ''none'';base-uri ''self'';report-uri https://csp.withgoogle.com/csp/gerritcodereview/1' Content-Type: - application/json; charset=utf-8 Date: - - Tue, 19 Apr 2022 17:13:57 GMT + - Fri, 27 Jan 2023 13:01:06 GMT Expires: - Mon, 01 Jan 1990 00:00:00 GMT Pragma: diff --git a/internal/types/secret.go b/internal/types/secret.go index 083752371e97..66a51ef74b61 100644 --- a/internal/types/secret.go +++ b/internal/types/secret.go @@ -204,6 +204,12 @@ func (e *ExternalService) UnredactConfig(ctx context.Context, old *ExternalServi return errCodeHostIdentityChanged{"p4.port", "p4.passwd"} } es.unredactString(c.P4Passwd, o.P4Passwd, "p4.passwd") + case *schema.GerritConnection: + o := oldCfg.(*schema.GerritConnection) + es.unredactString(c.Password, o.Password, "password") + if c.Url != o.Url { + return errCodeHostIdentityChanged{"url", "password"} + } case *schema.GitoliteConnection: // Nothing to redact case *schema.GoModulesConnection: diff --git a/schema/gerrit.schema.json b/schema/gerrit.schema.json index b9f53398851a..165e029869d6 100644 --- a/schema/gerrit.schema.json +++ b/schema/gerrit.schema.json @@ -28,6 +28,26 @@ "description": "The password associated with the Gerrit username used for authentication.", "type": "string", "minLength": 1 + }, + "projects": { + "description": "An array of project strings specifying which Gerrit projects to mirror on Sourcegraph. If empty, all projects will be mirrored.", + "type": "array", + "items": { "type": "string" }, + "examples": [ + ["name", "owner/name"], + ["docs", "kubernetes/kubernetes", "golang/go", "facebook/react"] + ] + }, + "authorization": { + "title": "GerritAuthorization", + "description": "If non-null, enforces Gerrit repository permissions. This requires that there is an item in the [site configuration json](https://docs.sourcegraph.com/admin/config/site_config#auth-providers) `auth.providers` field, of type \"gerrit\" with the same `url` field as specified in this `GerritConnection`.", + "type": "object", + "properties": { + "identityProvider": { + "description": "The identity provider to use for user information. If not set, the `url` field is used.", + "type": "string" + } + } } } } diff --git a/schema/schema.go b/schema/schema.go index a9016cecffd7..366c783169ed 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -119,6 +119,7 @@ type AuthProviders struct { Github *GitHubAuthProvider Gitlab *GitLabAuthProvider Bitbucketcloud *BitbucketCloudAuthProvider + Gerrit *GerritAuthProvider } func (v AuthProviders) MarshalJSON() ([]byte, error) { @@ -143,6 +144,9 @@ func (v AuthProviders) MarshalJSON() ([]byte, error) { if v.Bitbucketcloud != nil { return json.Marshal(v.Bitbucketcloud) } + if v.Gerrit != nil { + return json.Marshal(v.Gerrit) + } return nil, errors.New("tagged union type must have exactly 1 non-nil field value") } func (v *AuthProviders) UnmarshalJSON(data []byte) error { @@ -157,6 +161,8 @@ func (v *AuthProviders) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &v.Bitbucketcloud) case "builtin": return json.Unmarshal(data, &v.Builtin) + case "gerrit": + return json.Unmarshal(data, &v.Gerrit) case "github": return json.Unmarshal(data, &v.Github) case "gitlab": @@ -168,7 +174,7 @@ func (v *AuthProviders) UnmarshalJSON(data []byte) error { case "saml": return json.Unmarshal(data, &v.Saml) } - return fmt.Errorf("tagged union type must have a %q property whose value is one of %s", "type", []string{"builtin", "saml", "openidconnect", "http-header", "github", "gitlab", "bitbucketcloud"}) + return fmt.Errorf("tagged union type must have a %q property whose value is one of %s", "type", []string{"builtin", "saml", "openidconnect", "http-header", "github", "gitlab", "bitbucketcloud", "gerrit"}) } // AzureDevOpsConnection description: Configuration for a connection to Azure DevOps. @@ -695,8 +701,6 @@ type ExperimentalFeatures struct { EnablePostSignupFlow bool `json:"enablePostSignupFlow,omitempty"` // EventLogging description: Enables user event logging inside of the Sourcegraph instance. This will allow admins to have greater visibility of user activity, such as frequently viewed pages, frequent searches, and more. These event logs (and any specific user actions) are only stored locally, and never leave this Sourcegraph instance. EventLogging string `json:"eventLogging,omitempty"` - // Gerrit description: Allow adding Gerrit code host connections - Gerrit string `json:"gerrit,omitempty"` // GitServerPinnedRepos description: List of repositories pinned to specific gitserver instances. The specified repositories will remain at their pinned servers on scaling the cluster. If the specified pinned server differs from the current server that stores the repository, then it must be re-cloned to the specified server. GitServerPinnedRepos map[string]string `json:"gitServerPinnedRepos,omitempty"` // GoPackages description: Allow adding Go package host connections @@ -782,7 +786,6 @@ func (v *ExperimentalFeatures) UnmarshalJSON(data []byte) error { delete(m, "enablePermissionsWebhooks") delete(m, "enablePostSignupFlow") delete(m, "eventLogging") - delete(m, "gerrit") delete(m, "gitServerPinnedRepos") delete(m, "goPackages") delete(m, "insightsAlternateLoadingStrategy") @@ -872,10 +875,27 @@ type FusionClient struct { Retries int `json:"retries,omitempty"` } +// GerritAuthProvider description: Gerrit auth provider +type GerritAuthProvider struct { + Type string `json:"type"` + // Url description: URL of the Gerrit instance, such as https://gerrit-review.googlesource.com or https://gerrit.example.com. + Url string `json:"url"` +} + +// GerritAuthorization description: If non-null, enforces Gerrit repository permissions. This requires that there is an item in the [site configuration json](https://docs.sourcegraph.com/admin/config/site_config#auth-providers) `auth.providers` field, of type "gerrit" with the same `url` field as specified in this `GerritConnection`. +type GerritAuthorization struct { + // IdentityProvider description: The identity provider to use for user information. If not set, the `url` field is used. + IdentityProvider string `json:"identityProvider,omitempty"` +} + // GerritConnection description: Configuration for a connection to Gerrit. type GerritConnection struct { + // Authorization description: If non-null, enforces Gerrit repository permissions. This requires that there is an item in the [site configuration json](https://docs.sourcegraph.com/admin/config/site_config#auth-providers) `auth.providers` field, of type "gerrit" with the same `url` field as specified in this `GerritConnection`. + Authorization *GerritAuthorization `json:"authorization,omitempty"` // Password description: The password associated with the Gerrit username used for authentication. Password string `json:"password"` + // Projects description: An array of project strings specifying which Gerrit projects to mirror on Sourcegraph. If empty, all projects will be mirrored. + Projects []string `json:"projects,omitempty"` // Url description: URL of a Gerrit instance, such as https://gerrit.example.com. Url string `json:"url"` // Username description: A username for authentication withe the Gerrit code host. diff --git a/schema/site.schema.json b/schema/site.schema.json index 766ac1568295..c990c7f73330 100644 --- a/schema/site.schema.json +++ b/schema/site.schema.json @@ -218,12 +218,6 @@ "enum": ["enabled", "disabled"], "default": "disabled" }, - "gerrit": { - "description": "Allow adding Gerrit code host connections", - "type": "string", - "enum": ["enabled", "disabled"], - "default": "disabled" - }, "azureDevOps": { "description": "Allow adding Azure DevOps code host connections", "type": "string", @@ -1594,7 +1588,7 @@ "properties": { "type": { "type": "string", - "enum": ["builtin", "saml", "openidconnect", "http-header", "github", "gitlab", "bitbucketcloud"] + "enum": ["builtin", "saml", "openidconnect", "http-header", "github", "gitlab", "bitbucketcloud", "gerrit"] } }, "oneOf": [ @@ -1604,7 +1598,8 @@ { "$ref": "#/definitions/HTTPHeaderAuthProvider" }, { "$ref": "#/definitions/GitHubAuthProvider" }, { "$ref": "#/definitions/GitLabAuthProvider" }, - { "$ref": "#/definitions/BitbucketCloudAuthProvider" } + { "$ref": "#/definitions/BitbucketCloudAuthProvider" }, + { "$ref": "#/definitions/GerritAuthProvider" } ], "!go": { "taggedUnionType": true @@ -2179,6 +2174,23 @@ } } }, + "GerritAuthProvider": { + "description": "Gerrit auth provider", + "type": "object", + "additionalProperties": false, + "required": ["type", "url"], + "properties": { + "type": { + "type": "string", + "const": "gerrit" + }, + "url": { + "type": "string", + "description": "URL of the Gerrit instance, such as https://gerrit-review.googlesource.com or https://gerrit.example.com.", + "default": "https://gerrit-review.googlesource.com/" + } + } + }, "AuthProviderCommon": { "$comment": "This schema is not used directly. The *AuthProvider schemas refer to its properties directly.", "description": "Common properties for authentication providers.", From 59ebb8cb082ef7fac2c19a7f880539d68bb2fa01 Mon Sep 17 00:00:00 2001 From: Jean-Hadrien Chabran Date: Fri, 27 Jan 2023 16:34:25 +0100 Subject: [PATCH 212/678] docs: fix a missing generated change (#47030) --- doc/dev/background-information/ci/reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/dev/background-information/ci/reference.md b/doc/dev/background-information/ci/reference.md index 623909ff601c..c5e1c78aa9d5 100644 --- a/doc/dev/background-information/ci/reference.md +++ b/doc/dev/background-information/ci/reference.md @@ -104,7 +104,7 @@ Base pipeline (more steps might be included based on branch changes): - **Metadata**: Pipeline metadata - Build //dev/sg //lib/... -- Build //monitoring/... +- Test //monitoring/... - Upload build trace ### Wolfi Exp Branch From b6e394c4de037295670fa830a5d97c62fcdc563d Mon Sep 17 00:00:00 2001 From: Will Dollman Date: Fri, 27 Jan 2023 16:04:21 +0000 Subject: [PATCH 213/678] Introduce package repository (#46942) * Add a package repository to the CI pipeline * Add melange configs to build our external dependencies --- .../dev/ci/internal/ci/wolfi_operations.go | 37 ++++++-- .../wolfi/{build.sh => build-base-image.sh} | 20 +++-- .../dev/ci/scripts/wolfi/build-package.sh | 33 ++++--- .../dev/ci/scripts/wolfi/build-repo-index.sh | 52 ++++++++++++ .../dev/ci/scripts/wolfi/upload-package.sh | 50 +++++++++++ wolfi-packages/comby.yaml | 37 ++++++++ wolfi-packages/{foobar.yaml => coursier.yaml} | 6 +- wolfi-packages/ctags.yaml | 48 +++++++++++ wolfi-packages/http-server-stabilizer.yaml | 35 ++++++++ wolfi-packages/local-build.sh | 19 +++++ wolfi-packages/p4-fusion.yaml | 85 +++++++++++++++++++ wolfi-packages/p4cli.yaml | 29 +++++++ wolfi-packages/syntect-server.yaml | 45 ++++++++++ wolfi-packages/temporary-keys/README.md | 2 + wolfi-packages/temporary-keys/melange.rsa | 51 +++++++++++ wolfi-packages/temporary-keys/melange.rsa.pub | 14 +++ 16 files changed, 539 insertions(+), 24 deletions(-) rename enterprise/dev/ci/scripts/wolfi/{build.sh => build-base-image.sh} (62%) create mode 100755 enterprise/dev/ci/scripts/wolfi/build-repo-index.sh create mode 100755 enterprise/dev/ci/scripts/wolfi/upload-package.sh create mode 100644 wolfi-packages/comby.yaml rename wolfi-packages/{foobar.yaml => coursier.yaml} (87%) create mode 100644 wolfi-packages/ctags.yaml create mode 100644 wolfi-packages/http-server-stabilizer.yaml create mode 100755 wolfi-packages/local-build.sh create mode 100644 wolfi-packages/p4-fusion.yaml create mode 100644 wolfi-packages/p4cli.yaml create mode 100644 wolfi-packages/syntect-server.yaml create mode 100644 wolfi-packages/temporary-keys/README.md create mode 100644 wolfi-packages/temporary-keys/melange.rsa create mode 100644 wolfi-packages/temporary-keys/melange.rsa.pub diff --git a/enterprise/dev/ci/internal/ci/wolfi_operations.go b/enterprise/dev/ci/internal/ci/wolfi_operations.go index 59b94b300487..13beb6dd0989 100644 --- a/enterprise/dev/ci/internal/ci/wolfi_operations.go +++ b/enterprise/dev/ci/internal/ci/wolfi_operations.go @@ -10,8 +10,8 @@ import ( "github.com/sourcegraph/sourcegraph/internal/lazyregexp" ) -var baseImageRegex = lazyregexp.New(`wolfi-images\/([^\/]+)\/\w+[.]yaml`) -var packageRegex = lazyregexp.New(`wolfi-packages\/(\w+)[.]yaml`) +var baseImageRegex = lazyregexp.New(`wolfi-images\/([^\/]+)\/[\w-]+[.]yaml`) +var packageRegex = lazyregexp.New(`wolfi-packages\/([\w-]+)[.]yaml`) func WolfiBaseImagesOperations(changedFiles []string) *operations.Set { // TODO: Should we require the image name, or the full path to the yaml file? @@ -35,24 +35,49 @@ func WolfiPackagesOperations(changedFiles []string) *operations.Set { ops := operations.NewSet() logger := log.Scoped("gen-pipeline", "generates the pipeline for ci") + var stepKeys []string for _, c := range changedFiles { match := packageRegex.FindStringSubmatch(c) if len(match) == 2 { - ops.Append(buildPackages(match[1])) + buildFunc, key := buildPackage(match[1]) + stepKeys = append(stepKeys, key) + ops.Append(buildFunc) } else { logger.Fatal(fmt.Sprintf("Unable to extract package name from '%s', matches were %+v\n", c, match)) } } + ops.Append(buildRepoIndex("main", stepKeys)) + return ops } -func buildPackages(target string) func(*bk.Pipeline) { +// Dependency tree between steps: +// (buildPackage[1], buildPackage[2], ...) <-- buildRepoIndex <-- (buildWolfi[1], buildWolfi[2], ...) + +func buildPackage(target string) (func(*bk.Pipeline), string) { + // TODO: Can this be sanitised? + stepKey := fmt.Sprintf("package-dependency-%s", target) + return func(pipeline *bk.Pipeline) { pipeline.AddStep(fmt.Sprintf(":package: Package dependency '%s'", target), bk.Cmd(fmt.Sprintf("./enterprise/dev/ci/scripts/wolfi/build-package.sh %s", target)), // We want to run on the bazel queue, so we have a pretty minimal agent. bk.Agent("queue", "bazel"), + bk.Key(stepKey), + ) + }, stepKey +} + +func buildRepoIndex(branch string, packageKeys []string) func(*bk.Pipeline) { + return func(pipeline *bk.Pipeline) { + pipeline.AddStep(fmt.Sprintf(":card_index_dividers: Build and sign repository index for branch '%s'", branch), + bk.Cmd(fmt.Sprintf("./enterprise/dev/ci/scripts/wolfi/build-repo-index.sh %s", branch)), + // We want to run on the bazel queue, so we have a pretty minimal agent. + bk.Agent("queue", "bazel"), + // Depend on all previous package building steps + bk.DependsOn(packageKeys...), + bk.Key("buildRepoIndex"), ) } } @@ -60,9 +85,11 @@ func buildPackages(target string) func(*bk.Pipeline) { func buildWolfi(target string) func(*bk.Pipeline) { return func(pipeline *bk.Pipeline) { pipeline.AddStep(fmt.Sprintf(":wolf: Build Wolfi base image '%s'", target), - bk.Cmd(fmt.Sprintf("./enterprise/dev/ci/scripts/wolfi/build.sh %s", target)), + bk.Cmd(fmt.Sprintf("./enterprise/dev/ci/scripts/wolfi/build-base-image.sh %s", target)), // We want to run on the bazel queue, so we have a pretty minimal agent. bk.Agent("queue", "bazel"), + // Wait for repo to be re-indexed as images may depend on new packages + bk.DependsOn("buildRepoIndex"), ) } } diff --git a/enterprise/dev/ci/scripts/wolfi/build.sh b/enterprise/dev/ci/scripts/wolfi/build-base-image.sh similarity index 62% rename from enterprise/dev/ci/scripts/wolfi/build.sh rename to enterprise/dev/ci/scripts/wolfi/build-base-image.sh index 946562b1c759..6e375e5d0c93 100755 --- a/enterprise/dev/ci/scripts/wolfi/build.sh +++ b/enterprise/dev/ci/scripts/wolfi/build-base-image.sh @@ -1,8 +1,9 @@ #!/bin/bash +set -euf -o pipefail + cd "$(dirname "${BASH_SOURCE[0]}")/../../../../.." -set -euf -o pipefail tmpdir=$(mktemp -d -t wolfi-bin.XXXXXXXX) function cleanup() { echo "Removing $tmpdir" @@ -10,18 +11,23 @@ function cleanup() { } trap cleanup EXIT +# TODO: Install these binaries as part of the buildkite base image ( cd "$tmpdir" mkdir bin - # Install apko - wget https://github.com/chainguard-dev/apko/releases/download/v0.6.0/apko_0.6.0_linux_amd64.tar.gz + # Install apko from Sourcegraph cache + # Source: https://github.com/chainguard-dev/apko/releases/download/v0.6.0/apko_0.6.0_linux_amd64.tar.gz + wget https://storage.googleapis.com/package-repository/ci-binaries/apko_0.6.0_linux_amd64.tar.gz tar zxf apko_0.6.0_linux_amd64.tar.gz mv apko_0.6.0_linux_amd64/apko bin/apko - # Install apk - wget https://gitlab.alpinelinux.org/alpine/apk-tools/-/package_files/62/download -O bin/apk - chmod +x bin/apk + # Install apk from Sourcegraph cache + # Source: https://gitlab.alpinelinux.org/api/v4/projects/5/packages/generic//v2.12.11/x86_64/apk.static + wget https://storage.googleapis.com/package-repository/ci-binaries/apk-v2.12.11.tar.gz + tar zxf apk-v2.12.11.tar.gz + chmod +x apk + mv apk bin/apk ) export PATH="$tmpdir/bin:$PATH" @@ -45,6 +51,7 @@ fi cd "wolfi-images/${name}" +# Build base image with apko echo " * Building apko base image '$name'" image_name="sourcegraph-wolfi/${name}-base" tarball="sourcegraph-wolfi-${name}-base.tar" @@ -53,6 +60,7 @@ apko build --debug apko.yaml \ "$tarball" || (echo "*** Build failed ***" && exit 1) +# Tag image and upload to GCP Artifact Registry docker load <"$tarball" docker tag "$image_name" "us.gcr.io/sourcegraph-dev/wolfi-${name}:latest" docker push "us.gcr.io/sourcegraph-dev/wolfi-${name}:latest" diff --git a/enterprise/dev/ci/scripts/wolfi/build-package.sh b/enterprise/dev/ci/scripts/wolfi/build-package.sh index 68fb6f658bca..4f9f2a95482c 100755 --- a/enterprise/dev/ci/scripts/wolfi/build-package.sh +++ b/enterprise/dev/ci/scripts/wolfi/build-package.sh @@ -1,8 +1,9 @@ #!/bin/bash +set -euf -o pipefail + cd "$(dirname "${BASH_SOURCE[0]}")/../../../../.." -set -euf -o pipefail tmpdir=$(mktemp -d -t melange-bin.XXXXXXXX) function cleanup() { echo "Removing $tmpdir" @@ -10,21 +11,28 @@ function cleanup() { } trap cleanup EXIT +# TODO: Install these binaries as part of the buildkite base image ( cd "$tmpdir" mkdir bin - # Install melange - wget https://github.com/chainguard-dev/melange/releases/download/v0.2.0/melange_0.2.0_linux_amd64.tar.gz + # Install melange from Sourcegraph cache + # Source: https://github.com/chainguard-dev/melange/releases/download/v0.2.0/melange_0.2.0_linux_amd64.tar.gz + wget https://storage.googleapis.com/package-repository/ci-binaries/melange_0.2.0_linux_amd64.tar.gz tar zxf melange_0.2.0_linux_amd64.tar.gz mv melange_0.2.0_linux_amd64/melange bin/melange - # Install apk - wget https://gitlab.alpinelinux.org/alpine/apk-tools/-/package_files/62/download -O bin/apk - chmod +x bin/apk + # Install apk from Sourcegraph cache + # Source: https://gitlab.alpinelinux.org/api/v4/projects/5/packages/generic//v2.12.11/x86_64/apk.static + wget https://storage.googleapis.com/package-repository/ci-binaries/apk-v2.12.11.tar.gz + tar zxf apk-v2.12.11.tar.gz + chmod +x apk + mv apk bin/apk # Fetch custom-built bubblewrap 0.7.0 (temporary, until https://github.com/sourcegraph/infrastructure/pull/4520 is merged) - wget https://dollman.org/files/bwrap + # Build from source + wget https://storage.googleapis.com/package-repository/ci-binaries/bwrap-0.7.0.tar.gz + tar zxf bwrap-0.7.0.tar.gz chmod +x bwrap mv bwrap bin/ ) @@ -38,7 +46,7 @@ fi name=${1%/} -cd "wolfi-packages" +pushd "wolfi-packages" if [ ! -e "${name}.yaml" ]; then echo "File '$name.yaml' does not exist" @@ -49,8 +57,13 @@ fi # bubblewrap release in buildkite-agent-stateless-bazel's Dockerfile, and ship it in /usr/local/bin echo " * Building melange package '$name'" -# TODO: Signing key -melange build "$name.yaml" --arch x86_64 + +# Build package +melange build "$name.yaml" --arch x86_64 --generate-index false # Upload package as build artifact buildkite-agent artifact upload packages/*/* + +# Upload package to repo +popd +./enterprise/dev/ci/scripts/wolfi/upload-package.sh diff --git a/enterprise/dev/ci/scripts/wolfi/build-repo-index.sh b/enterprise/dev/ci/scripts/wolfi/build-repo-index.sh new file mode 100755 index 000000000000..40405bef9e74 --- /dev/null +++ b/enterprise/dev/ci/scripts/wolfi/build-repo-index.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +set -eu -o pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/../../../../.." +# TODO: This must be replaced with a proper K8s managed secret +key_path=$(realpath ./wolfi-packages/temporary-keys/) + +# TODO: Manage these variables properly +GCP_PROJECT="sourcegraph-ci" +GCS_BUCKET="package-repository" +TARGET_ARCH="x86_64" +branch="main" + +tmpdir=$(mktemp -d -t melange-bin.XXXXXXXX) +function cleanup() { + echo "Removing $tmpdir" + rm -rf "$tmpdir" +} +trap cleanup EXIT + +# TODO: Install these binaries as part of the buildkite base image +( + cd "$tmpdir" + mkdir bin + + # Install melange from Sourcegraph cache + # Source: https://github.com/chainguard-dev/melange/releases/download/v0.2.0/melange_0.2.0_linux_amd64.tar.gz + wget https://storage.googleapis.com/package-repository/ci-binaries/melange_0.2.0_linux_amd64.tar.gz + tar zxf melange_0.2.0_linux_amd64.tar.gz + mv melange_0.2.0_linux_amd64/melange bin/melange +) + +export PATH="$tmpdir/bin:$PATH" + +apkindex_build_dir=$(mktemp -d -t apkindex-build.XXXXXXXX) +pushd "$apkindex_build_dir" + +# Fetch all APKINDEX fragments from bucket +gsutil -u "$GCP_PROJECT" -m cp "gs://$GCS_BUCKET/packages/$branch/$TARGET_ARCH/*.APKINDEX.fragment" ./ + +# Concat all fragments into a single APKINDEX and tar.gz it +touch placeholder.APKINDEX.fragment +cat ./*.APKINDEX.fragment >APKINDEX +touch DESCRIPTION +tar zcf APKINDEX.tar.gz APKINDEX DESCRIPTION + +# Sign index +melange sign-index --signing-key "$key_path/melange.rsa" APKINDEX.tar.gz + +# Upload signed APKINDEX archive +gsutil -u "$GCP_PROJECT" cp APKINDEX.tar.gz "gs://$GCS_BUCKET/packages/$branch/$TARGET_ARCH/" diff --git a/enterprise/dev/ci/scripts/wolfi/upload-package.sh b/enterprise/dev/ci/scripts/wolfi/upload-package.sh new file mode 100755 index 000000000000..7f318b5ce203 --- /dev/null +++ b/enterprise/dev/ci/scripts/wolfi/upload-package.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +set -eu -o pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/../../../../.." + +# TODO: Manage these variables properly +GCP_PROJECT="sourcegraph-ci" +GCS_BUCKET="package-repository" +TARGET_ARCH="x86_64" +branch="main" + +cd wolfi-packages/packages/$TARGET_ARCH + +# Use GCP tooling to upload new package to repo, ensuring it's on the right branch. +# Check that this exact package does not already exist in the repo - fail if so + +# TODO: Support branches for uploading +# TODO: Check for existing files only if we're on main - overwriting is permitted on branches + +echo " * Uploading package to repository" + +# List all .apk files under wolfi-packages/packages/$TARGET_ARCH/ +apks=(*.apk) +for apk in "${apks[@]}"; do + echo " * Processing $apk" + dest_path="gs://$GCS_BUCKET/packages/$branch/$TARGET_ARCH/" + echo " -> File path: $dest_path / $apk" + + # Generate index fragment for this package + melange index -o "$apk.APKINDEX.tar.gz" "$apk" + tar zxf "$apk.APKINDEX.tar.gz" + index_fragment="$apk.APKINDEX.fragment" + mv APKINDEX "$index_fragment" + echo " * Generated index fragment '$index_fragment" + + # Check if this version of the package already exists in bucket + echo " * Checking if this package version already exists in repo..." + if gsutil -q -u "$GCP_PROJECT" stat "$dest_path/$apk"; then + echo "$apk: A package with this version already exists, and cannot be overwritten." + echo "Resolve this issue by incrementing the \`epoch\` field in the package's YAML file." + # exit 1 + else + echo " * File does not exist, uploading..." + fi + + # TODO: Pass -n when on main to avoid accidental overwriting + echo " * Uploading package and index fragment to repo" + gsutil -u "$GCP_PROJECT" cp "$apk" "$index_fragment" "$dest_path" +done diff --git a/wolfi-packages/comby.yaml b/wolfi-packages/comby.yaml new file mode 100644 index 000000000000..d9112c72276c --- /dev/null +++ b/wolfi-packages/comby.yaml @@ -0,0 +1,37 @@ +package: + name: comby + version: 1.8.1 + epoch: 0 + description: "A code rewrite tool for structural search and replace that supports ~every language." + target-architecture: + - x86_64 + copyright: + - paths: + - "*" + attestation: 'Copyright 2019 Rijnard van Tonder' + license: 'Apache License 2.0' + dependencies: + runtime: + +environment: + contents: + repositories: + - https://packages.wolfi.dev/os + keyring: + - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub + packages: + - wolfi-base + - busybox + - ca-certificates-bundle + +pipeline: + - uses: fetch + with: + uri: https://github.com/comby-tools/comby/releases/download/${{package.version}}/comby-${{package.version}}-x86_64-linux.tar.gz + expected-sha256: ec0ca6477822154d71033e0b0a724c23a0608b99028ecab492bc9876ae8c458a + # TODO: Work out why we can't use fetch's extract: true + - runs: | + tar zxvf comby-${{package.version}}-x86_64-linux.tar.gz + - runs: | + mkdir -p ${{targets.destdir}}/usr/bin/ + cp comby-${{package.version}}-x86_64-linux ${{targets.destdir}}/usr/bin/comby diff --git a/wolfi-packages/foobar.yaml b/wolfi-packages/coursier.yaml similarity index 87% rename from wolfi-packages/foobar.yaml rename to wolfi-packages/coursier.yaml index e76d30c0b8a0..c61c446d21cc 100644 --- a/wolfi-packages/foobar.yaml +++ b/wolfi-packages/coursier.yaml @@ -1,4 +1,5 @@ -# Foobar package based on Coursier package +# Melange-based replacement for Coursier +# Previously packaged in the scip-java repo package: name: coursier @@ -10,8 +11,7 @@ package: copyright: - paths: - "*" - attestation: 'TODO' - license: 'TODO' + license: 'Apache License 2.0' dependencies: runtime: diff --git a/wolfi-packages/ctags.yaml b/wolfi-packages/ctags.yaml new file mode 100644 index 000000000000..ca9be2a7d660 --- /dev/null +++ b/wolfi-packages/ctags.yaml @@ -0,0 +1,48 @@ +# Melange-based replacement for https://sourcegraph.sourcegraph.com/github.com/sourcegraph/sourcegraph/-/blob/cmd/symbols/ctags-install-alpine.sh + +package: + name: ctags + version: f95bb3497f53748c2b6afc7f298cff218103ab90 + epoch: 0 + description: "A maintained ctags implementation" + target-architecture: + - x86_64 + copyright: + - paths: + - "*" + license: GPL-2.0 + dependencies: + runtime: + +environment: + contents: + repositories: + - https://packages.wolfi.dev/os + keyring: + - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub + packages: + - build-base + - autoconf + - automake + - pkgconf + - pkgconf-dev + - busybox + - ca-certificates-bundle + - jansson-dev + +pipeline: + - uses: fetch + with: + uri: https://codeload.github.com/universal-ctags/ctags/tar.gz/${{package.version}} + expected-sha256: 1cb9b090f5cefa7704032df4c6ea2aca09d0adf8b2739be0aa3d4aaa720d0079 + - name: Autogen + runs: | + ./autogen.sh + - uses: autoconf/configure + with: + opts: | + --program-prefix=universal- \ + --enable-json + - uses: autoconf/make + - uses: autoconf/make-install + - uses: strip diff --git a/wolfi-packages/http-server-stabilizer.yaml b/wolfi-packages/http-server-stabilizer.yaml new file mode 100644 index 000000000000..df024412187e --- /dev/null +++ b/wolfi-packages/http-server-stabilizer.yaml @@ -0,0 +1,35 @@ +# Melange-based replacement for https://sourcegraph.sourcegraph.com/github.com/sourcegraph/sourcegraph/-/blob/docker-images/syntax-highlighter/Dockerfile?L16 + +package: + name: http-server-stabilizer + version: 1.0.5 + epoch: 0 + description: "HTTP server stabilizer for unruly servers" + target-architecture: + - x86_64 + copyright: + - paths: + - "*" + attestation: 'Copyright 2018 Sourcegraph, Inc.' + license: 'Apache License 2.0' + dependencies: + runtime: + +environment: + contents: + repositories: + - https://packages.wolfi.dev/os + keyring: + - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub + packages: + - wolfi-base + +pipeline: + - uses: fetch + with: + uri: https://github.com/sourcegraph/http-server-stabilizer/archive/refs/tags/v${{package.version}}.tar.gz + expected-sha256: e568f2b407a09d288abfc41057e6b76a68d00f0cfe2d461d629263fa2fbba94e + - uses: go/build + with: + packages: main.go + output: http-server-stabilizer diff --git a/wolfi-packages/local-build.sh b/wolfi-packages/local-build.sh new file mode 100755 index 000000000000..b2167678c149 --- /dev/null +++ b/wolfi-packages/local-build.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -eu -o pipefail + +# This script can be used to quickly build packages locally when working on package configs in this directory. +# In production, packages are built using the CI pipeline. + +if [ $# -eq 0 ]; then + echo "No arguments supplied - provide the melange YAML file to build" + exit 0 +fi + +name=${1%/} +echo "Building package '$name'" + +# Mounting /tmp can be useful for debugging: -v "$HOME/tmp":/tmp \ +docker run --privileged \ + -v "$PWD":/work \ + cgr.dev/chainguard/melange build "$name" --arch x86_64 diff --git a/wolfi-packages/p4-fusion.yaml b/wolfi-packages/p4-fusion.yaml new file mode 100644 index 000000000000..deb69b24cb7d --- /dev/null +++ b/wolfi-packages/p4-fusion.yaml @@ -0,0 +1,85 @@ +# Melange-based replacement for https://sourcegraph.sourcegraph.com/github.com/sourcegraph/sourcegraph/-/blob/cmd/gitserver/p4-fusion-install-alpine.sh + +package: + name: p4-fusion + version: 1.12 + epoch: 0 + description: "A fast Perforce to Git conversion tool" + target-architecture: + - x86_64 + copyright: + - paths: + - "*" + attestation: 'Copyright (c) 2022 Salesforce, Inc.' + license: 'BSD 3-Clause License' + dependencies: + runtime: + - libstdc++ + +environment: + contents: + repositories: + - https://packages.wolfi.dev/os + keyring: + - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub + packages: + - build-base + - wget + - perl + - bash + - cmake + - busybox + - ca-certificates-bundle + +pipeline: + # Download p4-fusion + - uses: fetch + with: + uri: https://github.com/salesforce/p4-fusion/archive/refs/tags/v${{package.version}}.tar.gz + expected-sha256: f5cbca35880aad2e6fac05c669dab6c13c3389aa62cbc5d5ee2ce73e77bcfe78 + extract: false + - runs: | + mkdir p4-fusion-src + tar -C p4-fusion-src -xzf v${{package.version}}.tar.gz --strip 1 + + # Download OpenSSL + - uses: fetch + with: + uri: https://www.openssl.org/source/openssl-1.0.2t.tar.gz + expected-sha256: 14cb464efe7ac6b54799b34456bd69558a749a4931ecfd9cf9f71d7881cac7bc + extract: false + - runs: | + mkdir openssl-src + tar -C openssl-src -xzf openssl-1.0.2t.tar.gz --strip 1 + + # Download Helix Core C++ API + - uses: fetch + with: + uri: https://www.perforce.com/downloads/perforce/r22.1/bin.linux26x86_64/p4api.tgz + expected-sha256: 82db09791758516ba2561d75c744f190a9562c8be26dc2cb96aea537e2a451d3 + extract: false + - runs: | + mkdir -p p4-fusion-src/vendor/helix-core-api/linux + tar -C p4-fusion-src/vendor/helix-core-api/linux -xzf p4api.tgz --strip 1 + + # Build OpenSSL + - runs: | + cd openssl-src + ./config + - runs: | + cd openssl-src + make build_libs + - runs: | + cd openssl-src + make install + + # Build p4-fusion + - runs: | + cd p4-fusion-src + ./generate_cache.sh RelWithDebInfo + ./build.sh + + # Copy p4-fusion binary + - runs: | + mkdir -p ${{targets.destdir}}/usr/local/bin/ + cp p4-fusion-src/build/p4-fusion/p4-fusion ${{targets.destdir}}/usr/local/bin/ diff --git a/wolfi-packages/p4cli.yaml b/wolfi-packages/p4cli.yaml new file mode 100644 index 000000000000..d5805002e4e6 --- /dev/null +++ b/wolfi-packages/p4cli.yaml @@ -0,0 +1,29 @@ +package: + name: p4cli + version: 21.2 + epoch: 0 + description: "Command line interface for Perforce" + target-architecture: + - x86_64 + dependencies: + runtime: + +environment: + contents: + repositories: + - https://packages.wolfi.dev/os + keyring: + - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub + packages: + - wolfi-base + +pipeline: + - uses: fetch + with: + uri: http://cdist2.perforce.com/perforce/r${{package.version}}/bin.linux26x86_64/p4 + expected-sha256: 3462491b61ba9cf0c5d70511b7a3280cd7c154b49bfd8c669d5761953968c159 + extract: false + - runs: | + chmod +x p4 + mkdir -p ${{targets.destdir}}/usr/local/bin/ + cp p4 ${{targets.destdir}}/usr/local/bin/p4 diff --git a/wolfi-packages/syntect-server.yaml b/wolfi-packages/syntect-server.yaml new file mode 100644 index 000000000000..13b657f372b7 --- /dev/null +++ b/wolfi-packages/syntect-server.yaml @@ -0,0 +1,45 @@ +# Melange-based replacement for https://sourcegraph.sourcegraph.com/github.com/sourcegraph/sourcegraph/-/blob/docker-images/syntax-highlighter/Dockerfile?L2 + +package: + name: syntect-server + version: 4.3 + epoch: 0 + description: "HTTP server stabilizer for unruly servers" + target-architecture: + - x86_64 + copyright: + - paths: + - "*" + attestation: 'Copyright (c) 2017 Sourcegraph' + license: 'MIT License' + dependencies: + runtime: + +environment: + contents: + repositories: + - https://packages.wolfi.dev/os + keyring: + - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub + packages: + - wolfi-base + - build-base + - rust + +pipeline: + # Tie version of syntect-server to Sourcegraph release + - uses: git-checkout + with: + repository: https://github.com/sourcegraph/sourcegraph/ + branch: ${{package.version}} + destination: /sourcegraph + + - runs: | + cd /sourcegraph/docker-images/syntax-highlighter + cargo test --release --workspace + cargo rustc --release + + - runs: | + cd /sourcegraph/docker-images/syntax-highlighter + mkdir -p ${{targets.destdir}}/usr/local/bin/ + cp target/release/syntect_server ${{targets.destdir}}/usr/local/bin/syntax_highlighter diff --git a/wolfi-packages/temporary-keys/README.md b/wolfi-packages/temporary-keys/README.md new file mode 100644 index 000000000000..1297dee3371b --- /dev/null +++ b/wolfi-packages/temporary-keys/README.md @@ -0,0 +1,2 @@ +NOTE: These keys are ONLY being used for CI pipeline testing and will be rotated and switched to secure storage +before they're put to any production use! diff --git a/wolfi-packages/temporary-keys/melange.rsa b/wolfi-packages/temporary-keys/melange.rsa new file mode 100644 index 000000000000..081b472de4f5 --- /dev/null +++ b/wolfi-packages/temporary-keys/melange.rsa @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAokytwaFiijxjM3p8TH5OosXn3AjzTGFS4H5ggRI/EqRvG+6j +8dyaIc1h+fShgqbINeAS7J7Vm4JuSVvoYF8VHHMOcEGkm98ZhHeI17Zz59UVGPRi +4cEeoX5fLDfHD49Aq4KZcBjV9GotNYKLBNgp6fNxruMihEFi+/lLHb6oShzDglnI +cF1G1i1IO16BYDWjB5qSGpGwvKjVwBWwvokyFlTsZiTv1DpMNsCpgJtt9a6hR97Y +ZQYKW28y632+lwLoT/+ERDZXl+6qVGfbguUxwkDnRX71RBYgSpqgrgginjuCrK/O +UXWD/PPiD0sc5zuoLZ6+84B3F6AIVf1ETxmkVDySBkQa3qGCfWBt3ipkMh2IF5V0 +KttgNpUBdRuemXsmO6UKBtG+XVY2zEU9pguDj7UTkMPrlwZbT1XXzIPe+7xpSh1I +zNV6WWI0njeXHwPeYLAjRfqjp2h4SAp4vKGkNXRoetUalBvGoO5qDQ7mhJqWJeXJ +BBq+W2vhS8tromYAc5rGOl7AejMY618zAgpYzYLhN1OWFcOeLt8pdFpLg3lSaRxx +K33mG6jGX038S8tY+BrgkpOdtnqe+VRo1C1wpFY7zUrx21GMbY+QyBjdNgkDh5Ir +ckEan/mRWdne9VeOO7JEOIvaluOyNu6AK5th179mF9nqV488UNmKLXUnJFsCAwEA +AQKCAgBMK7yo0btTsX/FW0kXBXiWgFePN0wonsysu+NC8HNVpoLXEysyihx0nNXM +3/klPm1ci6uWDf2mnJJyL4ZiJH3d+kneeZBt70kkmI1K4ECJn8HlEl2OInrjxFGa +iRsNvGfXltW4fI99xI8vO/NO9LzHJhBGyica9y0joR6V+TM2hUVk2gpuYfiq8Fmk +M3h41POM0AieG55dDMg0/HkVE4LEZFsGnXNJoYq/b7CdwVTcJ3deKcJZt3oI66l0 +SHG4og6x7PQAp5h4n/Sk5JFrX3H4/0kLnsgxikjheqzKwNqudLOhpGkqZgLqJvQD +xXbtN1x4/LYVxkceeWcwJEt47Eno16J5bBC9S0CWLC3Dn8jo7Ruq37YPBXbq+c3D +dv4q8EOPimOW/VUYLv2STUHEZi5Ikfl8ApvHAdOfewmE1mjp7zQWt8SlWtnQhm4w +WLfU0QPhJ4X8c+vt/sTKQiDCkoLN3Sr4cBN+ElyhrBE9pW1qrghjtRuxmEIl3Aj8 +euBHFfOi7Lkpa6cPrdxIO/V+xV4boiboWImMkztR0Y+5MHAJ2wSobMXRtJI9Q2Bi +KlcuhgU1NlBm3ooofhihBRWJtoIBtpk/+zt7HBZ3tgwt82DxBeyiNcdt+mxz23YL +e9rU6P4zgDA4VQXpoPVlAS+latIxP8JaqLHH+SlEMr2IQvbdwQKCAQEAztWiGfIV +lj11B77P3MvAR+BEIleEFD4Mplh+21minekYtt9xlFyrh0BXW6k2SKEfgVvk0L/2 +UhLFuRLWEKvN0D7CLxiQuxYBjYN0v6tnGaURMwvcDrMQwK+pCScztz+O3rzqEV6N +L7/PJy5HAMU8Or0fRekrWJ6sQZlTAl7XgDnR2ZZNfUc0Ncd6sJYdwjBlI8i+ZMLK +ojVGonMrKqskbXMzRalil5OGOXjDZ2mnVYQyUzEmni3Br1d+/f9eCbfNnZ2Ndkz8 +FGsaAxW4IcsA+4z79JVEQ6rDP2FEwBiTK2AY24YxYkLUSVgW+vl6XCcyi46JWzfK +p0NF1/POj5ShKwKCAQEAyOD8uLLFHZkVW3bX/Z+QMmcOZgl4Fu9sQsh6oYkUo/Sc +cnW+w5kljEORNC/dJJ/lze0fUrQ6iK6ZtCHVCrsxDQ7/wAR6w1tGszG2kdZVA3hd +Sw99jFJRfDKdYgHPmdTTVFxVXNh9l9Mu23evbXhKdbH1rtgd+RufIirexMhC0GEz +NH5tUKpeRLrorn+jdHIbso30zQ0lXua54xwrloWzqo3/rWmyeWQhTF2xYn3cZdHh +7rk3ZjS4jL9Je9PjzbKLcUxhDVyXh/UzBJ3JPVfq65/cHNDK4WbBJT1TKHiF/xOl +6CPH9q/kPMakEPv5wN85eDGxD2GpXPN9EKeB/8wRkQKCAQEAp/slXgEYux5Kr/Gu +i9oG3dksPN/q6y3BxE/XJ3rS8YDgi5VJf38L6Bq/WDhDWBVTqxHg8hEVkm6gmsDL +jlqaGuj9eJZw5SDoPfBnn0srvs4q+9RD8sRHdNa1aDOocslx1UCEsXqjHAahzWZu +UBff5Ky4e2T1yVGFAPnvStuQFhnfbuH9KSPtKUhLQqOIo6/+VGOzDc6OF9NA/Kqg +glTgjuui1o7M/eHYf1CvEoviVTe6T4p5nLn4tdlP0CXYT/gxTDMrMssvZ73cMGkx +VHC06ZgFAQ1BldtYG196ILFUxUOUrKV98hnoo3ChqU94quNCz8kBkU3sjo+6Z7sh +JcdGZQKCAQBtQAfSf8+6sDYwX2tLEcv/zZLEJeQ9PQu5KoWfcwlZvkCT87vDb3g/ +V8QSCPIR6DC4lX0Sotiu257WnAvc4T/lJUIs8YK+2taRwLjViicEUyMSHqOefGq6 +zGBSHEAHHvushucaLtfnicCTNf2f4OtGJXpEFGAAymf60iwEBBJqeGK76wKS+4V3 +hdvkg6CnRSQRMSlxl/O6MGvqu7htDdxQJxhl/PVHFhESEmbV9TO34LUq+2rI2GWC +jsptYSklBzTVcr/Z34C+Pbn4icQX68flTDUPKvMcMaFcGoUunpXpy4rQvR0U6kaM +U1CJuDD2F7GO0B/HCMtutTJQq3mFYDwxAoIBAQDMCqfgqMjdTMRQUI0JWLw5M0un +eVYe20AcfaSa8tRHwEcMItoR/kfag074f70fvA5jFEzbarJtjLne8T0krIVzxUag +ST9oIoSxHYYupfSbitECwLn70peMzr53C6sT2+LJkiz2U169v7rl9DK90HBHQTKO +/XD8RgEW28khje/FHcvzGubjaiY0fXeE7slyxZ6yXulLCVSD+ZZgc6vq0UXF6CwH +oosKQqTZ38Walqb9wmLgn5/pn4RLqrTlUBF7PRqMTEC95V0hSb1eMfVNmwaCDNoT +eJSackPOYzX9xwP0+srwkUdWgOacVAx7mEIPKfyU01eg0o5A23O5J+oMwfPn +-----END RSA PRIVATE KEY----- diff --git a/wolfi-packages/temporary-keys/melange.rsa.pub b/wolfi-packages/temporary-keys/melange.rsa.pub new file mode 100644 index 000000000000..c2a3020d9f25 --- /dev/null +++ b/wolfi-packages/temporary-keys/melange.rsa.pub @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAokytwaFiijxjM3p8TH5O +osXn3AjzTGFS4H5ggRI/EqRvG+6j8dyaIc1h+fShgqbINeAS7J7Vm4JuSVvoYF8V +HHMOcEGkm98ZhHeI17Zz59UVGPRi4cEeoX5fLDfHD49Aq4KZcBjV9GotNYKLBNgp +6fNxruMihEFi+/lLHb6oShzDglnIcF1G1i1IO16BYDWjB5qSGpGwvKjVwBWwvoky +FlTsZiTv1DpMNsCpgJtt9a6hR97YZQYKW28y632+lwLoT/+ERDZXl+6qVGfbguUx +wkDnRX71RBYgSpqgrgginjuCrK/OUXWD/PPiD0sc5zuoLZ6+84B3F6AIVf1ETxmk +VDySBkQa3qGCfWBt3ipkMh2IF5V0KttgNpUBdRuemXsmO6UKBtG+XVY2zEU9pguD +j7UTkMPrlwZbT1XXzIPe+7xpSh1IzNV6WWI0njeXHwPeYLAjRfqjp2h4SAp4vKGk +NXRoetUalBvGoO5qDQ7mhJqWJeXJBBq+W2vhS8tromYAc5rGOl7AejMY618zAgpY +zYLhN1OWFcOeLt8pdFpLg3lSaRxxK33mG6jGX038S8tY+BrgkpOdtnqe+VRo1C1w +pFY7zUrx21GMbY+QyBjdNgkDh5IrckEan/mRWdne9VeOO7JEOIvaluOyNu6AK5th +179mF9nqV488UNmKLXUnJFsCAwEAAQ== +-----END PUBLIC KEY----- From 44c123ceea29b205261f1decbff75578644e07eb Mon Sep 17 00:00:00 2001 From: Cezary Bartoszuk Date: Fri, 27 Jan 2023 10:14:25 -0600 Subject: [PATCH 214/678] =?UTF-8?q?Ownership=20search=20=E2=9A=97=EF=B8=8F?= =?UTF-8?q?=20=20(#45973)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revive internal/search/codeownership from #43037 * Bring the rest of the CODEOWNERS search logic * Implement a service for looking up CODEOWNERS file * Add frontend support for new file filter Co-authored-by: Indradhanush Gupta Co-authored-by: Alex Ostrikov Co-authored-by: Erik Seliger --- .../input/codemirror/completion.test.ts | 5 +- .../results/sidebar/SearchReference.tsx | 6 + .../src/search/query/completion.test.ts | 36 ++- .../shared/src/search/query/decoratedToken.ts | 1 + client/shared/src/search/query/filters.ts | 7 + client/shared/src/search/query/hover.ts | 2 + client/shared/src/search/query/predicates.ts | 21 +- cmd/frontend/backend/own.go | 4 + cmd/frontend/backend/own_test.go | 4 +- internal/search/client/client.go | 1 + internal/search/codeownership/job.go | 154 ++++++++++ internal/search/codeownership/job_test.go | 285 ++++++++++++++++++ internal/search/codeownership/rules_cache.go | 50 +++ internal/search/job/jobutil/job.go | 7 + internal/search/query/predicate.go | 17 ++ internal/search/query/predicate_test.go | 31 ++ internal/search/query/types.go | 11 + internal/search/types.go | 4 + 18 files changed, 638 insertions(+), 8 deletions(-) create mode 100644 internal/search/codeownership/job.go create mode 100644 internal/search/codeownership/job_test.go create mode 100644 internal/search/codeownership/rules_cache.go diff --git a/client/branded/src/search-ui/input/codemirror/completion.test.ts b/client/branded/src/search-ui/input/codemirror/completion.test.ts index ac2f55f3a949..0bdab0886583 100644 --- a/client/branded/src/search-ui/input/codemirror/completion.test.ts +++ b/client/branded/src/search-ui/input/codemirror/completion.test.ts @@ -276,7 +276,10 @@ describe('codmirror completions', () => { }, ] as SearchMatch[] ) - )?.map(({ apply }) => apply) + ) + // Do not consider functions like has.owner or has.content. + ?.filter(({ apply }) => typeof apply === 'string') + .map(({ apply }) => apply) ).toStrictEqual(['^some/path/main\\.go$ ']) }) diff --git a/client/branded/src/search-ui/results/sidebar/SearchReference.tsx b/client/branded/src/search-ui/results/sidebar/SearchReference.tsx index 1f7d6e29d069..462541b3d5d6 100644 --- a/client/branded/src/search-ui/results/sidebar/SearchReference.tsx +++ b/client/branded/src/search-ui/results/sidebar/SearchReference.tsx @@ -142,6 +142,12 @@ To use this filter, the search query must contain \`type:diff\` or \`type:commit description: 'Search only inside files that contain content matching the provided regexp pattern.', examples: ['file:has.content(github.com/sourcegraph/sourcegraph)'], }, + { + ...createQueryExampleFromString('has.owner({name})'), + field: FilterType.file, + description: 'Search only inside files that are owned by the given owner.', + examples: ['file:has.owner(johndoe)'], + }, { ...createQueryExampleFromString('{yes/only}'), field: FilterType.fork, diff --git a/client/shared/src/search/query/completion.test.ts b/client/shared/src/search/query/completion.test.ts index e285f9ce0ad4..9012b3c7c834 100644 --- a/client/shared/src/search/query/completion.test.ts +++ b/client/shared/src/search/query/completion.test.ts @@ -3,7 +3,7 @@ import { isSearchMatchOfType, SearchMatch } from '../stream' import { FetchSuggestions, getCompletionItems } from './completion' import { POPULAR_LANGUAGES } from './languageFilter' -import { scanSearchQuery, ScanSuccess, ScanResult } from './scanner' +import { ScanResult, scanSearchQuery, ScanSuccess } from './scanner' import { Token } from './token' expect.addSnapshotSerializer({ @@ -321,7 +321,27 @@ describe('getCompletionItems()', () => { {} ) )?.suggestions.map(({ label, insertText }) => ({ label, insertText })) - ).toStrictEqual([{ label: 'connect.go', insertText: '^connect\\.go$ ' }]) + ).toStrictEqual([ + { + // eslint-disable-next-line no-template-curly-in-string + insertText: 'contains.content(${1:TODO}) ', + label: 'contains.content(...)', + }, + { + // eslint-disable-next-line no-template-curly-in-string + insertText: 'has.content(${1:TODO}) ', + label: 'has.content(...)', + }, + { + // eslint-disable-next-line no-template-curly-in-string + insertText: 'has.owner(${1}) ', + label: 'has.owner(...)', + }, + { + insertText: '^connect\\.go$ ', + label: 'connect.go', + }, + ]) }) test('sets current filter value as filterText', async () => { @@ -340,7 +360,7 @@ describe('getCompletionItems()', () => { {} ) )?.suggestions.map(({ filterText }) => filterText) - ).toStrictEqual(['^jsonrpc']) + ).toStrictEqual(['contains.content(...)', 'has.content(...)', 'has.owner(...)', '^jsonrpc']) }) test('includes file path in insertText when completing filter value', async () => { @@ -359,7 +379,15 @@ describe('getCompletionItems()', () => { {} ) )?.suggestions.map(({ insertText }) => insertText) - ).toStrictEqual(['^some/path/main\\.go$ ']) + ).toStrictEqual([ + // eslint-disable-next-line no-template-curly-in-string + 'contains.content(${1:TODO}) ', + // eslint-disable-next-line no-template-curly-in-string + 'has.content(${1:TODO}) ', + // eslint-disable-next-line no-template-curly-in-string + 'has.owner(${1}) ', + '^some/path/main\\.go$ ', + ]) }) test('escapes spaces in repo value', async () => { diff --git a/client/shared/src/search/query/decoratedToken.ts b/client/shared/src/search/query/decoratedToken.ts index 33e1bb783364..da4169478c35 100644 --- a/client/shared/src/search/query/decoratedToken.ts +++ b/client/shared/src/search/query/decoratedToken.ts @@ -1006,6 +1006,7 @@ const decoratePredicateBody = (path: string[], body: string, offset: number): De break } case 'has.tag': + case 'has.owner': case 'has.key': return [ { diff --git a/client/shared/src/search/query/filters.ts b/client/shared/src/search/query/filters.ts index 74355058c707..b749a5424628 100644 --- a/client/shared/src/search/query/filters.ts +++ b/client/shared/src/search/query/filters.ts @@ -236,6 +236,7 @@ export const FILTERS: Record & negatable: true, description: negated => `${negated ? 'Exclude' : 'Include only'} results from file paths matching the given search pattern.`, + discreteValues: () => [...predicateCompletion('file')], placeholder: 'regex', suggestions: 'path', }, @@ -414,6 +415,12 @@ export const validateFilter = ( // account for finite discrete values and exemption of checks. return { valid: true } } + if (typeAndDefinition.type === FilterType.file) { + // File filter is made exempt from checking discrete valid values, since a valid `contain` predicate + // has infinite valid discrete values. TODO(rvantonder): value validation should be separated to + // account for finite discrete values and exemption of checks. + return { valid: true } + } if (typeAndDefinition.type === FilterType.lang) { // Lang filter is exempt because our discrete completion values are only a subset of all valid // language values, which are captured by a Go library. The backend takes care of returning an diff --git a/client/shared/src/search/query/hover.ts b/client/shared/src/search/query/hover.ts index 56f71550ab90..07d815e22f86 100644 --- a/client/shared/src/search/query/hover.ts +++ b/client/shared/src/search/query/hover.ts @@ -178,6 +178,8 @@ const toPredicateHover = (token: MetaPredicate): string => { return '**Built-in predicate**. Search only inside repositories that are associated with the given key:value pair' case 'has.key': return '**Built-in predicate**. Search only inside repositories that are associated with the given key, regardless of its value' + case 'has.owner': + return '**Built-in predicate**. Search only inside files that are owned by the given person or team' } return '' } diff --git a/client/shared/src/search/query/predicates.ts b/client/shared/src/search/query/predicates.ts index 7b2359089391..4c56cf9e967b 100644 --- a/client/shared/src/search/query/predicates.ts +++ b/client/shared/src/search/query/predicates.ts @@ -53,7 +53,7 @@ export const PREDICATES: Access[] = [ }, { name: 'has', - fields: [{ name: 'content' }], + fields: [{ name: 'content' }, { name: 'owner' }], }, ], }, @@ -216,5 +216,24 @@ export const predicateCompletion = (field: string): Completion[] => { }, ] } + if (field === 'file') { + return [ + { + label: 'contains.content(...)', + insertText: 'contains.content(${1:TODO})', + asSnippet: true, + }, + { + label: 'has.content(...)', + insertText: 'has.content(${1:TODO})', + asSnippet: true, + }, + { + label: 'has.owner(...)', + insertText: 'has.owner(${1})', + asSnippet: true, + }, + ] + } return [] } diff --git a/cmd/frontend/backend/own.go b/cmd/frontend/backend/own.go index e97f0ae9f106..b58c595dcc12 100644 --- a/cmd/frontend/backend/own.go +++ b/cmd/frontend/backend/own.go @@ -3,6 +3,7 @@ package backend import ( "bytes" "context" + "os" "github.com/sourcegraph/sourcegraph/internal/api" "github.com/sourcegraph/sourcegraph/internal/authz" @@ -55,7 +56,10 @@ func (s ownService) OwnersFile(ctx context.Context, repoName api.RepoName, commi ) if content != nil && err == nil { return codeowners.Parse(bytes.NewReader(content)) + } else if os.IsNotExist(err) { + continue } + return nil, err } return nil, nil } diff --git a/cmd/frontend/backend/own_test.go b/cmd/frontend/backend/own_test.go index c9fc4a1c76e3..924ee20d02b2 100644 --- a/cmd/frontend/backend/own_test.go +++ b/cmd/frontend/backend/own_test.go @@ -2,6 +2,7 @@ package backend_test import ( "context" + "os" "testing" "github.com/stretchr/testify/assert" @@ -11,7 +12,6 @@ import ( "github.com/sourcegraph/sourcegraph/internal/api" "github.com/sourcegraph/sourcegraph/internal/authz" "github.com/sourcegraph/sourcegraph/internal/gitserver" - "github.com/sourcegraph/sourcegraph/lib/errors" codeownerspb "github.com/sourcegraph/sourcegraph/internal/own/codeowners/proto" ) @@ -28,7 +28,7 @@ type repoFiles map[repoPath]string func (fs repoFiles) ReadFile(_ context.Context, _ authz.SubRepoPermissionChecker, repoName api.RepoName, commitID api.CommitID, file string) ([]byte, error) { content, ok := fs[repoPath{Repo: repoName, CommitID: commitID, Path: file}] if !ok { - return nil, errors.New("file does not exist") + return nil, os.ErrNotExist } return []byte(content), nil } diff --git a/internal/search/client/client.go b/internal/search/client/client.go index c172c6e7298e..5115ffd1385d 100644 --- a/internal/search/client/client.go +++ b/internal/search/client/client.go @@ -290,6 +290,7 @@ func ToFeatures(flagSet *featureflag.FlagSet, logger log.Logger) *search.Feature return &search.Features{ ContentBasedLangFilters: flagSet.GetBoolOr("search-content-based-lang-detection", false), + CodeOwnershipFilters: flagSet.GetBoolOr("search-ownership", false), HybridSearch: flagSet.GetBoolOr("search-hybrid", true), // can remove flag in 4.5 Ranking: flagSet.GetBoolOr("search-ranking", false), Debug: flagSet.GetBoolOr("search-debug", false), diff --git a/internal/search/codeownership/job.go b/internal/search/codeownership/job.go new file mode 100644 index 000000000000..c59faa16987c --- /dev/null +++ b/internal/search/codeownership/job.go @@ -0,0 +1,154 @@ +package codeownership + +import ( + "context" + "strings" + "sync" + + otlog "github.com/opentracing/opentracing-go/log" + + "github.com/sourcegraph/sourcegraph/internal/gitserver" + "github.com/sourcegraph/sourcegraph/internal/search" + "github.com/sourcegraph/sourcegraph/internal/search/job" + "github.com/sourcegraph/sourcegraph/internal/search/result" + "github.com/sourcegraph/sourcegraph/internal/search/streaming" + "github.com/sourcegraph/sourcegraph/internal/trace" + "github.com/sourcegraph/sourcegraph/lib/errors" + + codeownerspb "github.com/sourcegraph/sourcegraph/internal/own/codeowners/proto" +) + +func New(child job.Job, includeOwners, excludeOwners []string) job.Job { + return &codeownershipJob{ + child: child, + includeOwners: includeOwners, + excludeOwners: excludeOwners, + } +} + +type codeownershipJob struct { + child job.Job + + includeOwners []string + excludeOwners []string +} + +func (s *codeownershipJob) Run(ctx context.Context, clients job.RuntimeClients, stream streaming.Sender) (alert *search.Alert, err error) { + _, ctx, stream, finish := job.StartSpan(ctx, stream, s) + defer finish(alert, err) + + var ( + mu sync.Mutex + errs error + ) + + rules := NewRulesCache() + + filteredStream := streaming.StreamFunc(func(event streaming.SearchEvent) { + var err error + event.Results, err = applyCodeOwnershipFiltering(ctx, clients.Gitserver, &rules, s.includeOwners, s.excludeOwners, event.Results) + if err != nil { + mu.Lock() + errs = errors.Append(errs, err) + mu.Unlock() + } + stream.Send(event) + }) + + alert, err = s.child.Run(ctx, clients, filteredStream) + if err != nil { + errs = errors.Append(errs, err) + } + return alert, errs +} + +func (s *codeownershipJob) Name() string { + return "CodeOwnershipFilterJob" +} + +func (s *codeownershipJob) Fields(v job.Verbosity) (res []otlog.Field) { + switch v { + case job.VerbosityMax: + fallthrough + case job.VerbosityBasic: + res = append(res, + trace.Strings("includeOwners", s.includeOwners), + trace.Strings("excludeOwners", s.excludeOwners), + ) + } + return res +} + +func (s *codeownershipJob) Children() []job.Describer { + return []job.Describer{s.child} +} + +func (s *codeownershipJob) MapChildren(fn job.MapFunc) job.Job { + cp := *s + cp.child = job.Map(s.child, fn) + return &cp +} + +func applyCodeOwnershipFiltering( + ctx context.Context, + gitserver gitserver.Client, + rules *RulesCache, + includeOwners, + excludeOwners []string, + matches []result.Match, +) ([]result.Match, error) { + var errs error + + filtered := matches[:0] + +matchesLoop: + for _, m := range matches { + // Code ownership is currently only implemented for files. + mm, ok := m.(*result.FileMatch) + if !ok { + continue + } + + file, err := rules.GetFromCacheOrFetch(ctx, gitserver, mm.Repo.Name, mm.CommitID) + if err != nil { + errs = errors.Append(errs, err) + } + owners := file.FindOwners(mm.File.Path) + for _, owner := range includeOwners { + if !containsOwner(owners, owner) { + continue matchesLoop + } + } + for _, notOwner := range excludeOwners { + if containsOwner(owners, notOwner) { + continue matchesLoop + } + } + + filtered = append(filtered, m) + } + + return filtered, errs +} + +// containsOwner searches within emails and handles in a case-insensitive +// manner. Empty string passed as search term means any, so the predicate +// returns true if there is at least one owner, and false otherwise. +func containsOwner(owners []*codeownerspb.Owner, owner string) bool { + if owner == "" { + return len(owners) > 0 + } + isHandle := strings.HasPrefix(owner, "@") + owner = strings.ToLower(strings.TrimPrefix(owner, "@")) + for _, o := range owners { + if strings.ToLower(o.Handle) == owner { + return true + } + // Prefixing the search term with `@` indicates intent to match a handle, + // so we do not match email in that case. + if !isHandle && (strings.ToLower(o.Email) == owner) { + return true + } + } + return false +} diff --git a/internal/search/codeownership/job_test.go b/internal/search/codeownership/job_test.go new file mode 100644 index 000000000000..6728b8893d8c --- /dev/null +++ b/internal/search/codeownership/job_test.go @@ -0,0 +1,285 @@ +package codeownership + +import ( + "context" + "strings" + "testing" + + "github.com/hexops/autogold" + + "github.com/sourcegraph/sourcegraph/internal/api" + "github.com/sourcegraph/sourcegraph/internal/authz" + "github.com/sourcegraph/sourcegraph/internal/gitserver" + "github.com/sourcegraph/sourcegraph/internal/search/result" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +func TestApplyCodeOwnershipFiltering(t *testing.T) { + type args struct { + includeOwners []string + excludeOwners []string + matches []result.Match + repoContent map[string]string + } + tests := []struct { + name string + args args + want autogold.Value + }{ + { + // TODO: We should display an error in search describing why the result is empty. + name: "filters all matches if we include an owner and have no code owners file", + args: args{ + includeOwners: []string{"@test"}, + excludeOwners: []string{}, + matches: []result.Match{ + &result.FileMatch{ + File: result.File{ + Path: "README.md", + }, + }, + }, + }, + want: autogold.Want("no results", []result.Match{}), + }, + { + name: "selects only results matching owners", + args: args{ + includeOwners: []string{"@test"}, + excludeOwners: []string{}, + matches: []result.Match{ + &result.FileMatch{ + File: result.File{ + Path: "README.md", + }, + }, + &result.FileMatch{ + File: result.File{ + Path: "package.json", + }, + }, + }, + repoContent: map[string]string{ + "CODEOWNERS": "README.md @test\n", + }, + }, + want: autogold.Want("results matching ownership", []result.Match{ + &result.FileMatch{ + File: result.File{ + Path: "README.md", + }, + }, + }), + }, + { + name: "match username without search term containing a leading @", + args: args{ + includeOwners: []string{"test"}, + excludeOwners: []string{}, + matches: []result.Match{ + &result.FileMatch{ + File: result.File{ + Path: "README.md", + }, + }, + }, + repoContent: map[string]string{ + "CODEOWNERS": "README.md @test\n", + }, + }, + want: autogold.Want("results matching ownership", []result.Match{ + &result.FileMatch{ + File: result.File{ + Path: "README.md", + }, + }, + }), + }, + { + name: "match on email", + args: args{ + includeOwners: []string{"test@example.com"}, + excludeOwners: []string{}, + matches: []result.Match{ + &result.FileMatch{ + File: result.File{ + Path: "README.md", + }, + }, + }, + repoContent: map[string]string{ + "CODEOWNERS": "README.md test@example.com\n", + }, + }, + want: autogold.Want("results matching ownership", []result.Match{ + &result.FileMatch{ + File: result.File{ + Path: "README.md", + }, + }, + }), + }, + { + name: "selects only results without excluded owners", + args: args{ + includeOwners: []string{}, + excludeOwners: []string{"@test"}, + matches: []result.Match{ + &result.FileMatch{ + File: result.File{ + Path: "README.md", + }, + }, + &result.FileMatch{ + File: result.File{ + Path: "package.json", + }, + }, + }, + repoContent: map[string]string{ + "CODEOWNERS": "README.md @test\n", + }, + }, + want: autogold.Want("results matching ownership", []result.Match{ + &result.FileMatch{ + File: result.File{ + Path: "package.json", + }, + }, + }), + }, + { + name: "do not match on email if search term includes leading @", + args: args{ + includeOwners: []string{"@test@example.com"}, + excludeOwners: []string{}, + matches: []result.Match{ + &result.FileMatch{ + File: result.File{ + Path: "README.md", + }, + }, + }, + repoContent: map[string]string{ + "CODEOWNERS": "README.md test@example.com\n", + }, + }, + want: autogold.Want("no matching results", []result.Match{}), + }, + { + name: "selects results with any owner assigned", + args: args{ + includeOwners: []string{""}, + excludeOwners: []string{}, + matches: []result.Match{ + &result.FileMatch{ + File: result.File{ + Path: "README.md", + }, + }, + &result.FileMatch{ + File: result.File{ + Path: "package.json", + }, + }, + &result.FileMatch{ + File: result.File{ + Path: "/test/AbstractFactoryTest.java", + }, + }, + &result.FileMatch{ + File: result.File{ + Path: "/test/fixture-data.json", + }, + }, + }, + repoContent: map[string]string{ + "CODEOWNERS": strings.Join([]string{ + "README.md @test", + "/test/* @example", + "/test/*.json", // explicitly unassigned ownership + }, "\n"), + }, + }, + want: autogold.Want("results matching ownership", []result.Match{ + &result.FileMatch{ + File: result.File{ + Path: "README.md", + }, + }, + &result.FileMatch{ + File: result.File{ + Path: "/test/AbstractFactoryTest.java", + }, + }, + }), + }, + { + name: "selects results without an owner", + args: args{ + includeOwners: []string{}, + excludeOwners: []string{""}, + matches: []result.Match{ + &result.FileMatch{ + File: result.File{ + Path: "README.md", + }, + }, + &result.FileMatch{ + File: result.File{ + Path: "package.json", + }, + }, + &result.FileMatch{ + File: result.File{ + Path: "/test/AbstractFactoryTest.java", + }, + }, + &result.FileMatch{ + File: result.File{ + Path: "/test/fixture-data.json", + }, + }, + }, + repoContent: map[string]string{ + "CODEOWNERS": strings.Join([]string{ + "README.md @test", + "/test/* @example", + "/test/*.json", // explicitly unassigned ownership + }, "\n"), + }, + }, + want: autogold.Want("results matching ownership", []result.Match{ + &result.FileMatch{ + File: result.File{ + Path: "package.json", + }, + }, + &result.FileMatch{ + File: result.File{ + Path: "/test/fixture-data.json", + }, + }, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + rules := NewRulesCache() + + gitserverClient := gitserver.NewMockClient() + gitserverClient.ReadFileFunc.SetDefaultHook(func(_ context.Context, _ authz.SubRepoPermissionChecker, _ api.RepoName, _ api.CommitID, file string) ([]byte, error) { + content, ok := tt.args.repoContent[file] + if !ok { + return nil, errors.New("file does not exist") + } + return []byte(content), nil + }) + + matches, _ := applyCodeOwnershipFiltering(ctx, gitserverClient, &rules, tt.args.includeOwners, tt.args.excludeOwners, tt.args.matches) + + tt.want.Equal(t, matches) + }) + } +} diff --git a/internal/search/codeownership/rules_cache.go b/internal/search/codeownership/rules_cache.go new file mode 100644 index 000000000000..4eb6f735c7a3 --- /dev/null +++ b/internal/search/codeownership/rules_cache.go @@ -0,0 +1,50 @@ +package codeownership + +import ( + "context" + "sync" + + "github.com/sourcegraph/sourcegraph/cmd/frontend/backend" + "github.com/sourcegraph/sourcegraph/internal/api" + "github.com/sourcegraph/sourcegraph/internal/gitserver" + + codeownerspb "github.com/sourcegraph/sourcegraph/internal/own/codeowners/proto" +) + +type RulesKey struct { + repoName api.RepoName + commitID api.CommitID +} + +type RulesCache struct { + rules map[RulesKey]*codeownerspb.File + + mu sync.RWMutex +} + +func NewRulesCache() RulesCache { + return RulesCache{rules: make(map[RulesKey]*codeownerspb.File)} +} + +func (c *RulesCache) GetFromCacheOrFetch(ctx context.Context, gitserver gitserver.Client, repoName api.RepoName, commitID api.CommitID) (*codeownerspb.File, error) { + c.mu.RLock() + key := RulesKey{repoName, commitID} + if _, ok := c.rules[key]; ok { + defer c.mu.RUnlock() + return c.rules[key], nil + } + c.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() + // Recheck condition. + if _, ok := c.rules[key]; !ok { + file, err := backend.NewOwnService(gitserver).OwnersFile(ctx, repoName, commitID) + if err != nil { + emptyRuleset := &codeownerspb.File{} + c.rules[key] = emptyRuleset + return emptyRuleset, err + } + c.rules[key] = file + } + return c.rules[key], nil +} diff --git a/internal/search/job/jobutil/job.go b/internal/search/job/jobutil/job.go index a97bebe33192..f9ae1bdaa7b1 100644 --- a/internal/search/job/jobutil/job.go +++ b/internal/search/job/jobutil/job.go @@ -11,6 +11,7 @@ import ( "github.com/sourcegraph/sourcegraph/internal/authz" "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/search" + codeownershipjob "github.com/sourcegraph/sourcegraph/internal/search/codeownership" "github.com/sourcegraph/sourcegraph/internal/search/commit" "github.com/sourcegraph/sourcegraph/internal/search/filter" "github.com/sourcegraph/sourcegraph/internal/search/job" @@ -184,6 +185,12 @@ func NewBasicJob(inputs *search.Inputs, b query.Basic) (job.Job, error) { } } + { // Apply code ownership post-search filter + if includeOwners, excludeOwners := b.FileHasOwner(); inputs.Features.CodeOwnershipFilters && (len(includeOwners) > 0 || len(excludeOwners) > 0) { + basicJob = codeownershipjob.New(basicJob, includeOwners, excludeOwners) + } + } + { // Apply selectors if v, _ := b.ToParseTree().StringValue(query.FieldSelect); v != "" { sp, _ := filter.SelectPathFromString(v) // Invariant: select already validated diff --git a/internal/search/query/predicate.go b/internal/search/query/predicate.go index f2e163571e54..38df6f3c7954 100644 --- a/internal/search/query/predicate.go +++ b/internal/search/query/predicate.go @@ -42,6 +42,7 @@ var DefaultPredicateRegistry = PredicateRegistry{ FieldFile: { "contains.content": func() Predicate { return &FileContainsContentPredicate{} }, "has.content": func() Predicate { return &FileContainsContentPredicate{} }, + "has.owner": func() Predicate { return &FileHasOwnerPredicate{} }, }, } @@ -341,3 +342,19 @@ func (f *FileContainsContentPredicate) Unmarshal(params string, negated bool) er func (f FileContainsContentPredicate) Field() string { return FieldFile } func (f FileContainsContentPredicate) Name() string { return "contains.content" } + +/* file:has.owner(pattern) */ + +type FileHasOwnerPredicate struct { + Owner string + Negated bool +} + +func (f *FileHasOwnerPredicate) Unmarshal(params string, negated bool) error { + f.Owner = params + f.Negated = negated + return nil +} + +func (f FileHasOwnerPredicate) Field() string { return FieldFile } +func (f FileHasOwnerPredicate) Name() string { return "has.owner" } diff --git a/internal/search/query/predicate_test.go b/internal/search/query/predicate_test.go index a0ff07372f65..1789972bb573 100644 --- a/internal/search/query/predicate_test.go +++ b/internal/search/query/predicate_test.go @@ -170,3 +170,34 @@ func TestRepoHasKVPPredicate(t *testing.T) { } }) } + +func TestFileHasOwnerPredicate(t *testing.T) { + t.Run("Unmarshal", func(t *testing.T) { + type test struct { + name string + params string + expected *FileHasOwnerPredicate + } + + valid := []test{ + {`just text`, `test`, &FileHasOwnerPredicate{Owner: "test"}}, + {`handle starting with @`, `@octo-org/octocats`, &FileHasOwnerPredicate{Owner: "@octo-org/octocats"}}, + {`email`, `test@example.com`, &FileHasOwnerPredicate{Owner: "test@example.com"}}, + {`empty`, ``, &FileHasOwnerPredicate{Owner: ""}}, + } + + for _, tc := range valid { + t.Run(tc.name, func(t *testing.T) { + p := &FileHasOwnerPredicate{} + err := p.Unmarshal(tc.params, false) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !reflect.DeepEqual(tc.expected, p) { + t.Fatalf("expected %#v, got %#v", tc.expected, p) + } + }) + } + }) +} diff --git a/internal/search/query/types.go b/internal/search/query/types.go index 273fcf95437f..f5dcd2f54337 100644 --- a/internal/search/query/types.go +++ b/internal/search/query/types.go @@ -458,6 +458,17 @@ func (p Parameters) RepoHasKVPs() (res []RepoKVPFilter) { return res } +func (p Parameters) FileHasOwner() (include, exclude []string) { + VisitTypedPredicate(toNodes(p), func(pred *FileHasOwnerPredicate) { + if pred.Negated { + exclude = append(exclude, pred.Owner) + } else { + include = append(include, pred.Owner) + } + }) + return include, exclude +} + // Exists returns whether a parameter exists in the query (whether negated or not). func (p Parameters) Exists(field string) bool { found := false diff --git a/internal/search/types.go b/internal/search/types.go index e69a3b7a425c..7324aedf9bf4 100644 --- a/internal/search/types.go +++ b/internal/search/types.go @@ -346,6 +346,10 @@ type Features struct { // Debug when true will set the Debug field on FileMatches. This may grow // from here. For now we treat this like a feature flag for convenience. Debug bool `json:"debug"` + + // CodeOwnershipFilters when true will enable searching through code ownership + // using `file:has.owner({owner})` filter. + CodeOwnershipFilters bool `json:"codeownersip"` } func (f *Features) String() string { From a51b140e1ee058cb61ee5a5a560828add1783454 Mon Sep 17 00:00:00 2001 From: Idan Varsano Date: Fri, 27 Jan 2023 11:54:18 -0500 Subject: [PATCH 215/678] Fix ADO connection check (#46968) fixing conenction check for ADO --- .../graphqlbackend/external_service.go | 1 + internal/extsvc/azuredevops/client.go | 61 ++++++++++++-- internal/extsvc/azuredevops/client_test.go | 18 ++++- .../golden/AzureServicesConnectionData.json | 5 ++ .../testdata/golden/ListProjects.json | 80 +++++++++++++++++++ .../testdata/vcr/AzureServicesProfile.yaml | 60 ++++++++++++++ .../vcr/ListRepositoriesByProjectOrOrg.yaml | 22 +++-- internal/repos/azuredevops.go | 9 ++- 8 files changed, 236 insertions(+), 20 deletions(-) create mode 100644 internal/extsvc/azuredevops/testdata/golden/AzureServicesConnectionData.json create mode 100644 internal/extsvc/azuredevops/testdata/vcr/AzureServicesProfile.yaml diff --git a/cmd/frontend/graphqlbackend/external_service.go b/cmd/frontend/graphqlbackend/external_service.go index 0777f2e7989a..47aaaef475f4 100644 --- a/cmd/frontend/graphqlbackend/external_service.go +++ b/cmd/frontend/graphqlbackend/external_service.go @@ -62,6 +62,7 @@ var availabilityCheck = map[string]bool{ extsvc.KindGitLab: true, extsvc.KindBitbucketServer: true, extsvc.KindBitbucketCloud: true, + extsvc.KindAzureDevOps: true, } func externalServiceByID(ctx context.Context, db database.DB, gqlID graphql.ID) (*externalServiceResolver, error) { diff --git a/internal/extsvc/azuredevops/client.go b/internal/extsvc/azuredevops/client.go index 4cf6473c1aae..dd15632cfc48 100644 --- a/internal/extsvc/azuredevops/client.go +++ b/internal/extsvc/azuredevops/client.go @@ -16,6 +16,12 @@ import ( "github.com/sourcegraph/sourcegraph/schema" ) +const ( + azureDevOpsServicesURL = "https://dev.azure.com/" + // TODO: @varsanojidan look into which API version/s we want to support. + apiVersion = "7.0" +) + // Client used to access an AzureDevOps code host via the REST API. type Client struct { // HTTP Client used to communicate with the API. @@ -65,12 +71,10 @@ type ListRepositoriesByProjectOrOrgArgs struct { } func (c *Client) ListRepositoriesByProjectOrOrg(ctx context.Context, opts ListRepositoriesByProjectOrOrgArgs) ([]Repository, error) { - qs := make(url.Values) + queryParams := make(url.Values) + queryParams.Set("api-version", apiVersion) - // TODO: @varsanojidan look into which API version/s we want to support. - qs.Set("api-version", "7.0") - - urlRepositoriesByProjects := url.URL{Path: fmt.Sprintf("%s/_apis/git/repositories", opts.ProjectOrOrgName), RawQuery: qs.Encode()} + urlRepositoriesByProjects := url.URL{Path: fmt.Sprintf("%s/_apis/git/repositories", opts.ProjectOrOrgName), RawQuery: queryParams.Encode()} req, err := http.NewRequest("GET", urlRepositoriesByProjects.String(), nil) if err != nil { @@ -78,16 +82,45 @@ func (c *Client) ListRepositoriesByProjectOrOrg(ctx context.Context, opts ListRe } var repos ListRepositoriesResponse - if _, err = c.do(ctx, req, &repos); err != nil { + if _, err = c.do(ctx, req, "", &repos); err != nil { return nil, err } return repos.Value, nil } +// AzureServicesProfile is used to return information about the authorized user, should only be used for Azure Services (https://dev.azure.com) +func (c *Client) AzureServicesProfile(ctx context.Context) (Profile, error) { + queryParams := make(url.Values) + + queryParams.Set("api-version", apiVersion) + + urlProfile := url.URL{Path: "/_apis/profile/profiles/me", RawQuery: queryParams.Encode()} + + req, err := http.NewRequest("GET", urlProfile.String(), nil) + if err != nil { + return Profile{}, err + } + + var p Profile + if _, err = c.do(ctx, req, "https://app.vssps.visualstudio.com", &p); err != nil { + return Profile{}, err + } + + return p, nil +} + //nolint:unparam // http.Response is never used, but it makes sense API wise. -func (c *Client) do(ctx context.Context, req *http.Request, result any) (*http.Response, error) { - req.URL = c.URL.ResolveReference(req.URL) +func (c *Client) do(ctx context.Context, req *http.Request, urlOverride string, result any) (*http.Response, error) { + var err error + u := c.URL + if urlOverride != "" { + u, err = url.Parse(urlOverride) + if err != nil { + return nil, err + } + } + req.URL = u.ResolveReference(req.URL) // Add Basic Auth headers for authenticated requests. c.auth.Authenticate(req) @@ -139,6 +172,12 @@ func (c *Client) WithAuthenticator(a auth.Authenticator) (*Client, error) { }, nil } +// IsAzureDevOpsServices returns true if the client is configured to Azure DevOps +// Services (https://dev.azure.com +func (c *Client) IsAzureDevOpsServices() bool { + return c.URL.String() == azureDevOpsServicesURL +} + type ListRepositoriesResponse struct { Value []Repository `json:"value"` Count int `json:"count"` @@ -163,6 +202,12 @@ type Project struct { Visibility string `json:"visibility"` } +type Profile struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + EmailAddress string `json:"emailAddress"` +} + type httpError struct { StatusCode int URL *url.URL diff --git a/internal/extsvc/azuredevops/client_test.go b/internal/extsvc/azuredevops/client_test.go index 456aa2458215..e46063266b58 100644 --- a/internal/extsvc/azuredevops/client_test.go +++ b/internal/extsvc/azuredevops/client_test.go @@ -20,15 +20,13 @@ var update = flag.Bool("update", false, "update testdata") func TestClient_ListRepositoriesByProjectOrOrg(t *testing.T) { cli, save := NewTestClient(t, "ListRepositoriesByProjectOrOrg", *update) - defer save() - - ctx := context.Background() + t.Cleanup(save) opts := ListRepositoriesByProjectOrOrgArgs{ ProjectOrOrgName: "sgtestazure", } - resp, err := cli.ListRepositoriesByProjectOrOrg(ctx, opts) + resp, err := cli.ListRepositoriesByProjectOrOrg(context.Background(), opts) if err != nil { t.Fatal(err) } @@ -36,6 +34,18 @@ func TestClient_ListRepositoriesByProjectOrOrg(t *testing.T) { testutil.AssertGolden(t, "testdata/golden/ListProjects.json", *update, resp) } +func TestClient_AzureServicesProfile(t *testing.T) { + cli, save := NewTestClient(t, "AzureServicesProfile", *update) + t.Cleanup(save) + + resp, err := cli.AzureServicesProfile(context.Background()) + if err != nil { + t.Fatal(err) + } + + testutil.AssertGolden(t, "testdata/golden/AzureServicesConnectionData.json", *update, resp) +} + // NewTestClient returns an azuredevops.Client that records its interactions // to testdata/vcr/. func NewTestClient(t testing.TB, name string, update bool) (*Client, func()) { diff --git a/internal/extsvc/azuredevops/testdata/golden/AzureServicesConnectionData.json b/internal/extsvc/azuredevops/testdata/golden/AzureServicesConnectionData.json new file mode 100644 index 000000000000..e7987060df99 --- /dev/null +++ b/internal/extsvc/azuredevops/testdata/golden/AzureServicesConnectionData.json @@ -0,0 +1,5 @@ +{ + "id": "testuserID", + "displayName": "testuser", + "emailAddress": "testuser@sourcegraph.com" + } \ No newline at end of file diff --git a/internal/extsvc/azuredevops/testdata/golden/ListProjects.json b/internal/extsvc/azuredevops/testdata/golden/ListProjects.json index e29a1fdae209..0c366e548a34 100644 --- a/internal/extsvc/azuredevops/testdata/golden/ListProjects.json +++ b/internal/extsvc/azuredevops/testdata/golden/ListProjects.json @@ -1,4 +1,20 @@ [ + { + "id": "a000eac7-cc32-4b55-b877-1915325444a7", + "name": "sg test with many spaces", + "remoteURL": "https://sgtestazure@dev.azure.com/sgtestazure/sg%20test%20with%20many%20%20%20%20%20%20%20%20%20%20%20spaces/_git/sg%20test%20with%20many%20%20%20%20%20%20%20%20%20%20%20spaces", + "url": "https://dev.azure.com/sgtestazure/e414d6eb-05c3-46ff-a6f7-ce278577b7c2/_apis/git/repositories/a000eac7-cc32-4b55-b877-1915325444a7", + "sshUrl": "git@ssh.dev.azure.com:v3/sgtestazure/sg%20test%20with%20many%20%20%20%20%20%20%20%20%20%20%20spaces/sg%20test%20with%20many%20%20%20%20%20%20%20%20%20%20%20spaces", + "webUrl": "https://dev.azure.com/sgtestazure/sg%20test%20with%20many%20%20%20%20%20%20%20%20%20%20%20spaces/_git/sg%20test%20with%20many%20%20%20%20%20%20%20%20%20%20%20spaces", + "isDisabled": false, + "project": { + "id": "e414d6eb-05c3-46ff-a6f7-ce278577b7c2", + "name": "sg test with many spaces", + "state": "wellFormed", + "revision": 27, + "visibility": "private" + } + }, { "id": "2128ab2d-8459-4359-9939-221e92f3aeb1", "name": "sgtestazure3", @@ -15,6 +31,54 @@ "visibility": "private" } }, + { + "id": "7a7a0959-9c79-47f5-9d42-319c735179ce", + "name": "sg test with spaces", + "remoteURL": "https://sgtestazure@dev.azure.com/sgtestazure/sg%20test%20with%20spaces/_git/sg%20test%20with%20spaces", + "url": "https://dev.azure.com/sgtestazure/8c1230da-3631-41e8-8df3-c94a705227d8/_apis/git/repositories/7a7a0959-9c79-47f5-9d42-319c735179ce", + "sshUrl": "git@ssh.dev.azure.com:v3/sgtestazure/sg%20test%20with%20spaces/sg%20test%20with%20spaces", + "webUrl": "https://dev.azure.com/sgtestazure/sg%20test%20with%20spaces/_git/sg%20test%20with%20spaces", + "isDisabled": false, + "project": { + "id": "8c1230da-3631-41e8-8df3-c94a705227d8", + "name": "sg test with spaces", + "state": "wellFormed", + "revision": 19, + "visibility": "private" + } + }, + { + "id": "00e0ad0a-66df-4dc2-bd1b-4c34e05b9225", + "name": "sg test with spaces 2", + "remoteURL": "https://sgtestazure@dev.azure.com/sgtestazure/sg%20test%20with%20spaces/_git/sg%20test%20with%20spaces%202", + "url": "https://dev.azure.com/sgtestazure/8c1230da-3631-41e8-8df3-c94a705227d8/_apis/git/repositories/00e0ad0a-66df-4dc2-bd1b-4c34e05b9225", + "sshUrl": "git@ssh.dev.azure.com:v3/sgtestazure/sg%20test%20with%20spaces/sg%20test%20with%20spaces%202", + "webUrl": "https://dev.azure.com/sgtestazure/sg%20test%20with%20spaces/_git/sg%20test%20with%20spaces%202", + "isDisabled": false, + "project": { + "id": "8c1230da-3631-41e8-8df3-c94a705227d8", + "name": "sg test with spaces", + "state": "wellFormed", + "revision": 19, + "visibility": "private" + } + }, + { + "id": "be5d3e06-8bfb-445a-9729-5136088a5aea", + "name": "src cli with spaces", + "remoteURL": "https://sgtestazure@dev.azure.com/sgtestazure/sg%20test%20with%20spaces/_git/src%20cli%20with%20spaces", + "url": "https://dev.azure.com/sgtestazure/8c1230da-3631-41e8-8df3-c94a705227d8/_apis/git/repositories/be5d3e06-8bfb-445a-9729-5136088a5aea", + "sshUrl": "git@ssh.dev.azure.com:v3/sgtestazure/sg%20test%20with%20spaces/src%20cli%20with%20spaces", + "webUrl": "https://dev.azure.com/sgtestazure/sg%20test%20with%20spaces/_git/src%20cli%20with%20spaces", + "isDisabled": false, + "project": { + "id": "8c1230da-3631-41e8-8df3-c94a705227d8", + "name": "sg test with spaces", + "state": "wellFormed", + "revision": 19, + "visibility": "private" + } + }, { "id": "d66c87ea-6548-4a67-9f1f-560528393e73", "name": "sgtestazure2", @@ -46,5 +110,21 @@ "revision": 11, "visibility": "private" } + }, + { + "id": "c33edfe3-79ca-4a18-b4b2-fffa4e458e64", + "name": "sgtestrepo12458", + "remoteURL": "https://sgtestazure@dev.azure.com/sgtestazure/sgmanyrepos/_git/sgtestrepo12458", + "url": "https://dev.azure.com/sgtestazure/5966469e-b7f4-4f02-ad3a-f6a881fc6f6d/_apis/git/repositories/c33edfe3-79ca-4a18-b4b2-fffa4e458e64", + "sshUrl": "git@ssh.dev.azure.com:v3/sgtestazure/sgmanyrepos/sgtestrepo12458", + "webUrl": "https://dev.azure.com/sgtestazure/sgmanyrepos/_git/sgtestrepo12458", + "isDisabled": false, + "project": { + "id": "5966469e-b7f4-4f02-ad3a-f6a881fc6f6d", + "name": "sgmanyrepos", + "state": "wellFormed", + "revision": 75, + "visibility": "private" + } } ] \ No newline at end of file diff --git a/internal/extsvc/azuredevops/testdata/vcr/AzureServicesProfile.yaml b/internal/extsvc/azuredevops/testdata/vcr/AzureServicesProfile.yaml new file mode 100644 index 000000000000..e48ca54978e1 --- /dev/null +++ b/internal/extsvc/azuredevops/testdata/vcr/AzureServicesProfile.yaml @@ -0,0 +1,60 @@ +--- +version: 1 +interactions: +- request: + body: "" + form: {} + headers: {} + url: https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=7.0 + method: GET + response: + body: '{"displayName":"testuser","publicAlias":"n/a","emailAddress":"testuser@sourcegraph.com","coreRevision":411938451,"timeStamp":"2023-01-20T19:20:51.6433333+00:00","id":"testuserID","revision":411938451}' + headers: + Access-Control-Expose-Headers: + - Request-Context + Activityid: + - f821b5ff-fbd9-40bc-840e-4ff946b7e4e4 + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Type: + - application/json; charset=utf-8; api-version=7.0 + Date: + - Wed, 25 Jan 2023 23:45:55 GMT + Etag: + - '"0"' + Expires: + - "-1" + Last-Modified: + - Fri, 20 Jan 2023 19:20:51 GMT + P3p: + - CP="CAO DSP COR ADMa DEV CONo TELo CUR PSA PSD TAI IVDo OUR SAMi BUS DEM NAV + STA UNI COM INT PHY ONL FIN PUR LOC CNT" + Pragma: + - no-cache + Request-Context: + - appId=cid-v1:20b3930f-73dc-453a-b660-e3891d782eef + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Vary: + - Accept-Encoding + X-Cache: + - CONFIG_NOCACHE + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Msedge-Ref: + - 'Ref A: 06AA28D1910B40148D6C42F9BB43B4BE Ref B: YTO01EDGE0721 Ref C: 2023-01-25T23:45:56Z' + X-Tfs-Processid: + - c90b9da1-6ace-4793-9002-24d86f225712 + X-Tfs-Session: + - f821b5ff-fbd9-40bc-840e-4ff946b7e4e4 + X-Vss-E2eid: + - f821b5ff-fbd9-40bc-840e-4ff946b7e4e4 + X-Vss-Senderdeploymentid: + - a5ca35eb-148e-4ccd-bbb3-d31576d75958 + X-Vss-Userdata: + - 473dec3e-03d7-6147-b106-0b0a7f766a92:idan.varsano@sourcegraph.com + status: 200 OK + code: 200 + duration: "" diff --git a/internal/extsvc/azuredevops/testdata/vcr/ListRepositoriesByProjectOrOrg.yaml b/internal/extsvc/azuredevops/testdata/vcr/ListRepositoriesByProjectOrOrg.yaml index 54cfdf699a3d..66310b83274c 100644 --- a/internal/extsvc/azuredevops/testdata/vcr/ListRepositoriesByProjectOrOrg.yaml +++ b/internal/extsvc/azuredevops/testdata/vcr/ListRepositoriesByProjectOrOrg.yaml @@ -8,18 +8,26 @@ interactions: url: https://dev.azure.com/sgtestazure/_apis/git/repositories?api-version=7.0 method: GET response: - body: '{"value":[{"id":"2128ab2d-8459-4359-9939-221e92f3aeb1","name":"sgtestazure3","url":"https://dev.azure.com/sgtestazure/dc493f7d-0b57-4de2-a59b-3f74ff3ea334/_apis/git/repositories/2128ab2d-8459-4359-9939-221e92f3aeb1","project":{"id":"dc493f7d-0b57-4de2-a59b-3f74ff3ea334","name":"sgtestazure","url":"https://dev.azure.com/sgtestazure/_apis/projects/dc493f7d-0b57-4de2-a59b-3f74ff3ea334","state":"wellFormed","revision":11,"visibility":"private","lastUpdateTime":"2023-01-20T19:22:47.403Z"},"defaultBranch":"refs/heads/master","size":1076199250,"remoteUrl":"https://sgtestazure@dev.azure.com/sgtestazure/sgtestazure/_git/sgtestazure3","sshUrl":"git@ssh.dev.azure.com:v3/sgtestazure/sgtestazure/sgtestazure3","webUrl":"https://dev.azure.com/sgtestazure/sgtestazure/_git/sgtestazure3","isDisabled":false,"isInMaintenance":false},{"id":"d66c87ea-6548-4a67-9f1f-560528393e73","name":"sgtestazure2","url":"https://dev.azure.com/sgtestazure/dc493f7d-0b57-4de2-a59b-3f74ff3ea334/_apis/git/repositories/d66c87ea-6548-4a67-9f1f-560528393e73","project":{"id":"dc493f7d-0b57-4de2-a59b-3f74ff3ea334","name":"sgtestazure","url":"https://dev.azure.com/sgtestazure/_apis/projects/dc493f7d-0b57-4de2-a59b-3f74ff3ea334","state":"wellFormed","revision":11,"visibility":"private","lastUpdateTime":"2023-01-20T19:22:47.403Z"},"defaultBranch":"refs/heads/main","size":726,"remoteUrl":"https://sgtestazure@dev.azure.com/sgtestazure/sgtestazure/_git/sgtestazure2","sshUrl":"git@ssh.dev.azure.com:v3/sgtestazure/sgtestazure/sgtestazure2","webUrl":"https://dev.azure.com/sgtestazure/sgtestazure/_git/sgtestazure2","isDisabled":false,"isInMaintenance":false},{"id":"c4d186ef-18a6-4de4-a610-aa9ebd4e1faa","name":"sgtestazure","url":"https://dev.azure.com/sgtestazure/dc493f7d-0b57-4de2-a59b-3f74ff3ea334/_apis/git/repositories/c4d186ef-18a6-4de4-a610-aa9ebd4e1faa","project":{"id":"dc493f7d-0b57-4de2-a59b-3f74ff3ea334","name":"sgtestazure","url":"https://dev.azure.com/sgtestazure/_apis/projects/dc493f7d-0b57-4de2-a59b-3f74ff3ea334","state":"wellFormed","revision":11,"visibility":"private","lastUpdateTime":"2023-01-20T19:22:47.403Z"},"defaultBranch":"refs/heads/master","size":3688420,"remoteUrl":"https://sgtestazure@dev.azure.com/sgtestazure/sgtestazure/_git/sgtestazure","sshUrl":"git@ssh.dev.azure.com:v3/sgtestazure/sgtestazure/sgtestazure","webUrl":"https://dev.azure.com/sgtestazure/sgtestazure/_git/sgtestazure","isDisabled":false,"isInMaintenance":false}],"count":3}' + body: '{"value":[{"id":"a000eac7-cc32-4b55-b877-1915325444a7","name":"sg test + with many spaces","url":"https://dev.azure.com/sgtestazure/e414d6eb-05c3-46ff-a6f7-ce278577b7c2/_apis/git/repositories/a000eac7-cc32-4b55-b877-1915325444a7","project":{"id":"e414d6eb-05c3-46ff-a6f7-ce278577b7c2","name":"sg + test with many spaces","url":"https://dev.azure.com/sgtestazure/_apis/projects/e414d6eb-05c3-46ff-a6f7-ce278577b7c2","state":"wellFormed","revision":27,"visibility":"private","lastUpdateTime":"2023-01-24T18:21:31.597Z"},"size":0,"remoteUrl":"https://sgtestazure@dev.azure.com/sgtestazure/sg%20test%20with%20many%20%20%20%20%20%20%20%20%20%20%20spaces/_git/sg%20test%20with%20many%20%20%20%20%20%20%20%20%20%20%20spaces","sshUrl":"git@ssh.dev.azure.com:v3/sgtestazure/sg%20test%20with%20many%20%20%20%20%20%20%20%20%20%20%20spaces/sg%20test%20with%20many%20%20%20%20%20%20%20%20%20%20%20spaces","webUrl":"https://dev.azure.com/sgtestazure/sg%20test%20with%20many%20%20%20%20%20%20%20%20%20%20%20spaces/_git/sg%20test%20with%20many%20%20%20%20%20%20%20%20%20%20%20spaces","isDisabled":false,"isInMaintenance":false},{"id":"2128ab2d-8459-4359-9939-221e92f3aeb1","name":"sgtestazure3","url":"https://dev.azure.com/sgtestazure/dc493f7d-0b57-4de2-a59b-3f74ff3ea334/_apis/git/repositories/2128ab2d-8459-4359-9939-221e92f3aeb1","project":{"id":"dc493f7d-0b57-4de2-a59b-3f74ff3ea334","name":"sgtestazure","url":"https://dev.azure.com/sgtestazure/_apis/projects/dc493f7d-0b57-4de2-a59b-3f74ff3ea334","state":"wellFormed","revision":11,"visibility":"private","lastUpdateTime":"2023-01-20T19:22:47.403Z"},"defaultBranch":"refs/heads/master","size":1076199250,"remoteUrl":"https://sgtestazure@dev.azure.com/sgtestazure/sgtestazure/_git/sgtestazure3","sshUrl":"git@ssh.dev.azure.com:v3/sgtestazure/sgtestazure/sgtestazure3","webUrl":"https://dev.azure.com/sgtestazure/sgtestazure/_git/sgtestazure3","isDisabled":false,"isInMaintenance":false},{"id":"7a7a0959-9c79-47f5-9d42-319c735179ce","name":"sg + test with spaces","url":"https://dev.azure.com/sgtestazure/8c1230da-3631-41e8-8df3-c94a705227d8/_apis/git/repositories/7a7a0959-9c79-47f5-9d42-319c735179ce","project":{"id":"8c1230da-3631-41e8-8df3-c94a705227d8","name":"sg + test with spaces","description":"test project with spaces","url":"https://dev.azure.com/sgtestazure/_apis/projects/8c1230da-3631-41e8-8df3-c94a705227d8","state":"wellFormed","revision":19,"visibility":"private","lastUpdateTime":"2023-01-24T18:08:42.87Z"},"size":0,"remoteUrl":"https://sgtestazure@dev.azure.com/sgtestazure/sg%20test%20with%20spaces/_git/sg%20test%20with%20spaces","sshUrl":"git@ssh.dev.azure.com:v3/sgtestazure/sg%20test%20with%20spaces/sg%20test%20with%20spaces","webUrl":"https://dev.azure.com/sgtestazure/sg%20test%20with%20spaces/_git/sg%20test%20with%20spaces","isDisabled":false,"isInMaintenance":false},{"id":"00e0ad0a-66df-4dc2-bd1b-4c34e05b9225","name":"sg + test with spaces 2","url":"https://dev.azure.com/sgtestazure/8c1230da-3631-41e8-8df3-c94a705227d8/_apis/git/repositories/00e0ad0a-66df-4dc2-bd1b-4c34e05b9225","project":{"id":"8c1230da-3631-41e8-8df3-c94a705227d8","name":"sg + test with spaces","description":"test project with spaces","url":"https://dev.azure.com/sgtestazure/_apis/projects/8c1230da-3631-41e8-8df3-c94a705227d8","state":"wellFormed","revision":19,"visibility":"private","lastUpdateTime":"2023-01-24T18:08:42.87Z"},"defaultBranch":"refs/heads/main","size":726,"remoteUrl":"https://sgtestazure@dev.azure.com/sgtestazure/sg%20test%20with%20spaces/_git/sg%20test%20with%20spaces%202","sshUrl":"git@ssh.dev.azure.com:v3/sgtestazure/sg%20test%20with%20spaces/sg%20test%20with%20spaces%202","webUrl":"https://dev.azure.com/sgtestazure/sg%20test%20with%20spaces/_git/sg%20test%20with%20spaces%202","isDisabled":false,"isInMaintenance":false},{"id":"be5d3e06-8bfb-445a-9729-5136088a5aea","name":"src + cli with spaces","url":"https://dev.azure.com/sgtestazure/8c1230da-3631-41e8-8df3-c94a705227d8/_apis/git/repositories/be5d3e06-8bfb-445a-9729-5136088a5aea","project":{"id":"8c1230da-3631-41e8-8df3-c94a705227d8","name":"sg + test with spaces","description":"test project with spaces","url":"https://dev.azure.com/sgtestazure/_apis/projects/8c1230da-3631-41e8-8df3-c94a705227d8","state":"wellFormed","revision":19,"visibility":"private","lastUpdateTime":"2023-01-24T18:08:42.87Z"},"defaultBranch":"refs/heads/master","size":3675486,"remoteUrl":"https://sgtestazure@dev.azure.com/sgtestazure/sg%20test%20with%20spaces/_git/src%20cli%20with%20spaces","sshUrl":"git@ssh.dev.azure.com:v3/sgtestazure/sg%20test%20with%20spaces/src%20cli%20with%20spaces","webUrl":"https://dev.azure.com/sgtestazure/sg%20test%20with%20spaces/_git/src%20cli%20with%20spaces","isDisabled":false,"isInMaintenance":false},{"id":"d66c87ea-6548-4a67-9f1f-560528393e73","name":"sgtestazure2","url":"https://dev.azure.com/sgtestazure/dc493f7d-0b57-4de2-a59b-3f74ff3ea334/_apis/git/repositories/d66c87ea-6548-4a67-9f1f-560528393e73","project":{"id":"dc493f7d-0b57-4de2-a59b-3f74ff3ea334","name":"sgtestazure","url":"https://dev.azure.com/sgtestazure/_apis/projects/dc493f7d-0b57-4de2-a59b-3f74ff3ea334","state":"wellFormed","revision":11,"visibility":"private","lastUpdateTime":"2023-01-20T19:22:47.403Z"},"defaultBranch":"refs/heads/main","size":726,"remoteUrl":"https://sgtestazure@dev.azure.com/sgtestazure/sgtestazure/_git/sgtestazure2","sshUrl":"git@ssh.dev.azure.com:v3/sgtestazure/sgtestazure/sgtestazure2","webUrl":"https://dev.azure.com/sgtestazure/sgtestazure/_git/sgtestazure2","isDisabled":false,"isInMaintenance":false},{"id":"c4d186ef-18a6-4de4-a610-aa9ebd4e1faa","name":"sgtestazure","url":"https://dev.azure.com/sgtestazure/dc493f7d-0b57-4de2-a59b-3f74ff3ea334/_apis/git/repositories/c4d186ef-18a6-4de4-a610-aa9ebd4e1faa","project":{"id":"dc493f7d-0b57-4de2-a59b-3f74ff3ea334","name":"sgtestazure","url":"https://dev.azure.com/sgtestazure/_apis/projects/dc493f7d-0b57-4de2-a59b-3f74ff3ea334","state":"wellFormed","revision":11,"visibility":"private","lastUpdateTime":"2023-01-20T19:22:47.403Z"},"defaultBranch":"refs/heads/master","size":3688420,"remoteUrl":"https://sgtestazure@dev.azure.com/sgtestazure/sgtestazure/_git/sgtestazure","sshUrl":"git@ssh.dev.azure.com:v3/sgtestazure/sgtestazure/sgtestazure","webUrl":"https://dev.azure.com/sgtestazure/sgtestazure/_git/sgtestazure","isDisabled":false,"isInMaintenance":false},{"id":"c33edfe3-79ca-4a18-b4b2-fffa4e458e64","name":"sgtestrepo12458","url":"https://dev.azure.com/sgtestazure/5966469e-b7f4-4f02-ad3a-f6a881fc6f6d/_apis/git/repositories/c33edfe3-79ca-4a18-b4b2-fffa4e458e64","project":{"id":"5966469e-b7f4-4f02-ad3a-f6a881fc6f6d","name":"sgmanyrepos","url":"https://dev.azure.com/sgtestazure/_apis/projects/5966469e-b7f4-4f02-ad3a-f6a881fc6f6d","state":"wellFormed","revision":75,"visibility":"private","lastUpdateTime":"2023-01-25T18:12:46.233Z"},"size":0,"remoteUrl":"https://sgtestazure@dev.azure.com/sgtestazure/sgmanyrepos/_git/sgtestrepo12458","sshUrl":"git@ssh.dev.azure.com:v3/sgtestazure/sgmanyrepos/sgtestrepo12458","webUrl":"https://dev.azure.com/sgtestazure/sgmanyrepos/_git/sgtestrepo12458","isDisabled":false,"isInMaintenance":false}],"count":8}' headers: Access-Control-Expose-Headers: - Request-Context Activityid: - - e414f49c-0872-49f3-9d18-76465ddd50be + - 4461bfe8-d360-40bc-b680-db7f68dbc42b Cache-Control: - no-cache, no-store, must-revalidate Content-Type: - application/json; charset=utf-8; api-version=7.0 Date: - - Mon, 23 Jan 2023 21:59:06 GMT + - Wed, 25 Jan 2023 23:47:01 GMT Expires: - "-1" P3p: @@ -38,13 +46,13 @@ interactions: X-Frame-Options: - SAMEORIGIN X-Msedge-Ref: - - 'Ref A: 02506A3264D642CDAE0C8EE26AD67596 Ref B: YTO01EDGE0715 Ref C: 2023-01-23T21:59:06Z' + - 'Ref A: 06DFD8BF241F478098B86BB0364658E4 Ref B: YTO01EDGE0715 Ref C: 2023-01-25T23:47:01Z' X-Tfs-Processid: - - fa11745b-7c39-4f23-92f9-452c246beedc + - 2fdeb1ed-7dc0-48f0-b8e9-369d20f0fad6 X-Tfs-Session: - - e414f49c-0872-49f3-9d18-76465ddd50be + - 4461bfe8-d360-40bc-b680-db7f68dbc42b X-Vss-E2eid: - - e414f49c-0872-49f3-9d18-76465ddd50be + - 4461bfe8-d360-40bc-b680-db7f68dbc42b X-Vss-Senderdeploymentid: - 4ff21e82-8865-0b2e-ffe8-9598818f8190 X-Vss-Userdata: diff --git a/internal/repos/azuredevops.go b/internal/repos/azuredevops.go index 383ad8dec471..7cf5e41eee52 100644 --- a/internal/repos/azuredevops.go +++ b/internal/repos/azuredevops.go @@ -79,7 +79,14 @@ func NewAzureDevOpsSource(ctx context.Context, logger log.Logger, svc *types.Ext // from the subsequent calls. This is going to be expanded as part of issue #44683 // to actually only return true if the source can serve requests. func (s *AzureDevOpsSource) CheckConnection(ctx context.Context) error { - return checkConnection("https://dev.azure.com") + if s.cli.IsAzureDevOpsServices() { + _, err := s.cli.AzureServicesProfile(ctx) + return err + } + // If this isn't Azure DevOps Services, i.e. not https://dev.azure.com, return + // ok but log a warning because it is not supported. + s.logger.Warn("connection check for Azure DevOps Server is not supported, skipping.") + return nil } // ListRepos returns all Azure DevOps repositories configured with this AzureDevOpsSource's config. From a047f7677f5c28ecf87e5d1c0a326b974ef93958 Mon Sep 17 00:00:00 2001 From: Geoffrey Gilmore Date: Fri, 27 Jan 2023 08:56:10 -0800 Subject: [PATCH 216/678] sg: implement buf format linter (#47002) --- dev/sg/buf/buf.go | 55 ++++++ dev/sg/generates.go | 2 +- dev/sg/internal/generate/proto/proto.go | 65 ++++---- dev/sg/linters/buf.go | 156 ++++++++++++++++++ dev/sg/linters/linters.go | 7 + .../background-information/ci/reference.md | 5 + .../background-information/sg/reference.md | 9 + enterprise/dev/ci/internal/ci/changed/diff.go | 24 +++ .../dev/ci/internal/ci/changed/diff_test.go | 27 ++- .../dev/ci/internal/ci/changed/linters.go | 1 + .../own/codeowners/proto/codeowners.proto | 108 ++++++------ 11 files changed, 366 insertions(+), 93 deletions(-) create mode 100644 dev/sg/buf/buf.go create mode 100644 dev/sg/linters/buf.go diff --git a/dev/sg/buf/buf.go b/dev/sg/buf/buf.go new file mode 100644 index 000000000000..8991d9300c45 --- /dev/null +++ b/dev/sg/buf/buf.go @@ -0,0 +1,55 @@ +// Package buf defines shared functionality and utilities for the buf cli. +package buf + +import ( + "context" + "os" + "path/filepath" + + "github.com/sourcegraph/run" + "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" + "github.com/sourcegraph/sourcegraph/dev/sg/root" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +var dependencies = []dependency{ + "github.com/bufbuild/buf/cmd/buf@v1.11.0", + "github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc@v1.5.1", + "google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1", +} + +// InstallDependencies installs the dependencies required to run the buf cli. +func InstallDependencies(ctx context.Context, out *std.Output) error { + rootDir, err := root.RepositoryRoot() + if err != nil { + return errors.Wrap(err, "finding repository root") + } + + gobin := filepath.Join(rootDir, ".bin") + + for _, d := range dependencies { + if err := d.install(ctx, gobin, out); err != nil { + return errors.Wrapf(err, "installing dependency %q", d) + } + } + + return nil +} + +type dependency string + +func (d dependency) String() string { return string(d) } + +func (d dependency) install(ctx context.Context, gobin string, out *std.Output) error { + err := run.Cmd(ctx, "go", "install", d.String()). + Environ(os.Environ()). + Env(map[string]string{ + "GOBIN": gobin, + }). + Run(). + StreamLines(out.Verbose) + if err != nil { + return errors.Wrapf(err, "go install %s returned an error", d) + } + return nil +} diff --git a/dev/sg/generates.go b/dev/sg/generates.go index 584f9e02f9e1..12c2833680dc 100644 --- a/dev/sg/generates.go +++ b/dev/sg/generates.go @@ -51,5 +51,5 @@ func generateGoRunner(ctx context.Context, args []string) *generate.Report { } func generateProtoRunner(ctx context.Context, args []string) *generate.Report { - return proto.Generate(ctx) + return proto.Generate(ctx, verbose) } diff --git a/dev/sg/internal/generate/proto/proto.go b/dev/sg/internal/generate/proto/proto.go index a5feb134c8b1..af79bf937754 100644 --- a/dev/sg/internal/generate/proto/proto.go +++ b/dev/sg/internal/generate/proto/proto.go @@ -10,19 +10,15 @@ import ( "time" "github.com/sourcegraph/run" + "github.com/sourcegraph/sourcegraph/dev/sg/buf" + "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" + "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/dev/sg/internal/generate" "github.com/sourcegraph/sourcegraph/dev/sg/root" - "github.com/sourcegraph/sourcegraph/lib/errors" ) -var dependencies = []dependency{ - "github.com/bufbuild/buf/cmd/buf@v1.11.0", - "github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc@v1.5.1", - "google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1", -} - -func Generate(ctx context.Context) *generate.Report { +func Generate(ctx context.Context, verboseOutput bool) *generate.Report { // Save working directory cwd, err := os.Getwd() if err != nil { @@ -41,13 +37,11 @@ func Generate(ctx context.Context) *generate.Report { if err != nil { return &generate.Report{Err: err} } - gobin := filepath.Join(rootDir, ".bin") - // Install dependencies into $ROOT/.bin - for _, d := range dependencies { - if err := d.install(ctx, gobin, &sb); err != nil { - return &generate.Report{Err: err} - } + output := std.NewOutput(&sb, verboseOutput) + err = buf.InstallDependencies(ctx, output) + if err != nil { + return &generate.Report{Output: sb.String(), Err: err} } // Run buf generate in every directory with buf.gen.yaml @@ -66,12 +60,20 @@ func Generate(ctx context.Context) *generate.Report { if err != nil { return &generate.Report{Err: err} } + + gobin := filepath.Join(rootDir, ".bin") for _, p := range bufGenFilePaths { dir := filepath.Dir(p) - os.Chdir(dir) - if err := runBufGenerate(ctx, gobin, &sb); err != nil { + err := os.Chdir(dir) + if err != nil { + err = errors.Wrapf(err, "changing directory to %q", dir) return &generate.Report{Err: err} } + + err = runBufGenerate(ctx, gobin, &sb) + if err != nil { + return &generate.Report{Output: sb.String(), Err: err} + } } return &generate.Report{ @@ -82,31 +84,20 @@ func Generate(ctx context.Context) *generate.Report { func FindGeneratedFiles(dir string) ([]string, error) { var paths []string - err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { - if strings.HasSuffix(path, ".pb.go") { - paths = append(paths, path) + + err := filepath.WalkDir(dir, root.SkipGitIgnoreWalkFunc(func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err } - return nil - }) - return paths, err -} -type dependency string + if !entry.IsDir() && strings.HasSuffix(path, ".pb.go") { + paths = append(paths, path) + } -func (d dependency) String() string { return string(d) } + return nil + })) -func (d dependency) install(ctx context.Context, gobin string, w io.Writer) error { - err := run.Cmd(ctx, "go", "install", d.String()). - Environ(os.Environ()). - Env(map[string]string{ - "GOBIN": gobin, - }). - Run(). - Stream(w) - if err != nil { - return errors.Wrapf(err, "go install %s returned an error", d) - } - return nil + return paths, err } func runBufGenerate(ctx context.Context, gobin string, w io.Writer) error { diff --git a/dev/sg/linters/buf.go b/dev/sg/linters/buf.go new file mode 100644 index 000000000000..be2f4a4dcf79 --- /dev/null +++ b/dev/sg/linters/buf.go @@ -0,0 +1,156 @@ +package linters + +import ( + "context" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/sourcegraph/run" + "github.com/sourcegraph/sourcegraph/dev/sg/buf" + "github.com/sourcegraph/sourcegraph/dev/sg/internal/check" + "github.com/sourcegraph/sourcegraph/dev/sg/internal/repo" + "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" + "github.com/sourcegraph/sourcegraph/dev/sg/root" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +var bufFormat = &linter{ + Name: "Buf Format", + Check: func(ctx context.Context, out *std.Output, args *repo.State) error { + cwd, err := os.Getwd() + if err != nil { + return errors.Wrap(err, "getting current working directory") + } + defer func() { + os.Chdir(cwd) + }() + + rootDir, err := root.RepositoryRoot() + if err != nil { + return errors.Wrap(err, "getting repository root") + } + + err = os.Chdir(rootDir) + if err != nil { + return errors.Wrap(err, "changing directory to repository root") + } + + err = buf.InstallDependencies(ctx, out) + if err != nil { + return errors.Wrap(err, "installing buf dependencies") + } + + protoFiles, err := findProtoFiles(rootDir) + if err != nil { + return errors.Wrapf(err, "finding .proto files") + } + + bufArgs := []string{ + "format", + "--diff", + "--exit-code", + } + + for _, file := range protoFiles { + bufArgs = append(bufArgs, "--path", file) + } + + gobin := filepath.Join(rootDir, ".bin") + return runBuf(ctx, gobin, out, bufArgs...) + }, + + Fix: func(ctx context.Context, cio check.IO, args *repo.State) error { + cwd, err := os.Getwd() + if err != nil { + return errors.Wrap(err, "getting current working directory") + } + defer func() { + os.Chdir(cwd) + }() + + rootDir, err := root.RepositoryRoot() + if err != nil { + return errors.Wrap(err, "getting repository root") + } + + err = os.Chdir(rootDir) + if err != nil { + return errors.Wrap(err, "changing directory to repository root") + } + + err = buf.InstallDependencies(ctx, cio.Output) + if err != nil { + return errors.Wrap(err, "installing buf dependencies") + } + + protoFiles, err := findProtoFiles(rootDir) + if err != nil { + return errors.Wrapf(err, "finding .proto files") + } + + bufArgs := []string{ + "format", + "--write", + } + + for _, file := range protoFiles { + bufArgs = append(bufArgs, "--path", file) + } + + gobin := filepath.Join(rootDir, ".bin") + return runBuf(ctx, gobin, cio.Output, bufArgs...) + }, +} + +func findProtoFiles(dir string) ([]string, error) { + var files []string + + err := filepath.WalkDir(dir, root.SkipGitIgnoreWalkFunc(func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + if filepath.Ext(path) == ".proto" { + relPath, err := filepath.Rel(dir, path) + if err != nil { + return errors.Wrapf(err, "getting relative path for .proto file %q (base %q)", path, dir) + } + + files = append(files, relPath) + } + + return nil + })) + + if err != nil { + return nil, errors.Wrapf(err, "walking %q to find proto files", dir) + } + + return files, nil +} + +func runBuf(ctx context.Context, gobin string, out *std.Output, parameters ...string) error { + bufPath := filepath.Join(gobin, "buf") + + arguments := []string{bufPath} + arguments = append(arguments, parameters...) + + err := root.Run(run.Cmd(ctx, arguments...). + Environ(os.Environ()). + Env(map[string]string{ + "GOBIN": gobin, + })). + StreamLines(out.Write) + if err != nil { + commandStr := fmt.Sprintf("buf %s", strings.Join(parameters, " ")) + return errors.Wrapf(err, "%q returned an error", commandStr) + } + return nil +} diff --git a/dev/sg/linters/linters.go b/dev/sg/linters/linters.go index 4c64ba5c8272..96021405f5b4 100644 --- a/dev/sg/linters/linters.go +++ b/dev/sg/linters/linters.go @@ -90,6 +90,13 @@ var Targets = []Target{ bashSyntax, }, }, + { + Name: "protobuf", + Description: "Check protobuf code for linting errors, formatting, etc", + Checks: []*linter{ + bufFormat, + }, + }, Formatting, } diff --git a/doc/dev/background-information/ci/reference.md b/doc/dev/background-information/ci/reference.md index c5e1c78aa9d5..7a2c39b05ef1 100644 --- a/doc/dev/background-information/ci/reference.md +++ b/doc/dev/background-information/ci/reference.md @@ -91,6 +91,11 @@ The default run type. - **Metadata**: Pipeline metadata - Upload build trace +- Pipeline for `Protobuf` changes: + - **Metadata**: Pipeline metadata + - **Linters and static analysis**: Run sg lint + - Upload build trace + ### Bazel Exp Branch The run type for branches matching `bzl/`. diff --git a/doc/dev/background-information/sg/reference.md b/doc/dev/background-information/sg/reference.md index 6a93783fd6ac..200a7256fc6e 100644 --- a/doc/dev/background-information/sg/reference.md +++ b/doc/dev/background-information/sg/reference.md @@ -457,6 +457,15 @@ Flags: Check shell code for linting errors, formatting, etc. +Flags: + +* `--feedback`: provide feedback about this command by opening up a GitHub discussion + +### sg lint protobuf + +Check protobuf code for linting errors, formatting, etc. + + Flags: * `--feedback`: provide feedback about this command by opening up a GitHub discussion diff --git a/enterprise/dev/ci/internal/ci/changed/diff.go b/enterprise/dev/ci/internal/ci/changed/diff.go index 5c7d553ca158..41b0eca228f2 100644 --- a/enterprise/dev/ci/internal/ci/changed/diff.go +++ b/enterprise/dev/ci/internal/ci/changed/diff.go @@ -27,6 +27,7 @@ const ( DockerImages WolfiPackages WolfiBaseImages + Protobuf // All indicates all changes should be considered included in this diff, except None. All @@ -191,7 +192,28 @@ func ParseDiff(files []string) (diff Diff, changedFiles ChangedFiles) { diff |= WolfiBaseImages changedFiles[WolfiBaseImages] = append(changedFiles[WolfiBaseImages], p) } + + // Affects Protobuf files and configuration + if strings.HasSuffix(p, ".proto") { + diff |= Protobuf + } + + // Affects generated Protobuf files + if strings.HasSuffix(p, "buf.gen.yaml") { + diff |= Protobuf + } + + // Affects configuration for Buf and associated linters + if strings.HasSuffix(p, "buf.yaml") { + diff |= Protobuf + } + + // Generated Go code from Protobuf definitions + if strings.HasSuffix(p, ".pb.go") { + diff |= Protobuf + } } + return } @@ -230,6 +252,8 @@ func (d Diff) String() string { return "WolfiPackages" case WolfiBaseImages: return "WolfiBaseImages" + case Protobuf: + return "Protobuf" case All: return "All" diff --git a/enterprise/dev/ci/internal/ci/changed/diff_test.go b/enterprise/dev/ci/internal/ci/changed/diff_test.go index 3acfbb74bda4..0f418c96b1df 100644 --- a/enterprise/dev/ci/internal/ci/changed/diff_test.go +++ b/enterprise/dev/ci/internal/ci/changed/diff_test.go @@ -73,7 +73,32 @@ func TestParseDiff(t *testing.T) { WolfiPackages: []string{"wolfi-packages/package-test.yaml"}, }, doNotWantAffects: []Diff{}, - }} + }, { + name: "Protobuf definitions", + files: []string{"cmd/searcher/messages.proto"}, + wantAffects: []Diff{Protobuf}, + wantChangedFiles: make(ChangedFiles), + doNotWantAffects: []Diff{}, + }, { + name: "Protobuf generated code", + files: []string{"cmd/searcher/messages.pb.go"}, + wantAffects: []Diff{Protobuf, Go}, + wantChangedFiles: make(ChangedFiles), + doNotWantAffects: []Diff{}, + }, { + name: "Buf CLI module configuration", + files: []string{"cmd/searcher/buf.yaml"}, + wantAffects: []Diff{Protobuf}, + wantChangedFiles: make(ChangedFiles), + doNotWantAffects: []Diff{}, + }, { + name: "Buf CLI generated code configuration", + files: []string{"cmd/searcher/buf.gen.yaml"}, + wantAffects: []Diff{Protobuf}, + wantChangedFiles: make(ChangedFiles), + doNotWantAffects: []Diff{}, + }, + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff, changedFiles := ParseDiff(tt.files) diff --git a/enterprise/dev/ci/internal/ci/changed/linters.go b/enterprise/dev/ci/internal/ci/changed/linters.go index 94bd44725ecb..cd25418bfb15 100644 --- a/enterprise/dev/ci/internal/ci/changed/linters.go +++ b/enterprise/dev/ci/internal/ci/changed/linters.go @@ -11,6 +11,7 @@ var diffsWithLinters = []Diff{ SVG, Client, Shell, + Protobuf, } // GetTargets evaluates the lint targets to run over the given CI diff. diff --git a/internal/own/codeowners/proto/codeowners.proto b/internal/own/codeowners/proto/codeowners.proto index 2e0ae9fac90a..7f2c554684d8 100644 --- a/internal/own/codeowners/proto/codeowners.proto +++ b/internal/own/codeowners/proto/codeowners.proto @@ -19,67 +19,67 @@ option go_package = "github.com/sourcegraph/sourcegraph/internal/own/codeowners/ // separately. That is, an owner is potentially extracted // for every section. message File { - repeated Rule rule = 1; + repeated Rule rule = 1; } // Rule associates a single pattern to match a path with an owner. message Rule { - // Patterns are familliar glob patterns that match file paths. - // * `filename` matches any file with that name, for example: - // * `/filename` and `/src/filename` match. - // * `directory/path/` matches any tree of subdirectories rooted - // at this pattern, for example: - // * `/src/directory/path/file` matches. - // * `/src/directory/path/another/directory/file` matches. - // * `directory/*` matches only files with specified parent, - // but not descendants, for example: - // * `/src/foo/bar/directory/file` matches. - // * `/src/foo/bar/directory/another/file` does not match. - // * Any of the above can be prefixed with `/`, which further - // filters the match, by requiring the file path match to be - // rooted at the directory root, for `/src/dir/*`: - // * `/src/dir/file` matches. - // * `/main/src/dir/file` does not match, as `src` is not top-level. - // * `/src/dir/another/file` does not match as `*` matches - // only files directly contained in specified directory. - // * In the above patterns `/**/` can be used to match any sub-path - // between two parts of a pattern. For example: `/docs/**/internal/` - // will match `/docs/foo/bar/internal/file`. - // * The file part of the pattern can use a `*` wildcard like so: - // `docs/*.md` will match `/src/docs/index.md` but not `/src/docs/index.js`. - // * In BITBUCKET plugin, patterns that serve to exclude ownership - // start with an exclamation mark `!/src/noownershere`. These are - // translated to a pattern without the `!` and now owners. - string pattern = 1; - // Owners list all the parties that claim ownership over files - // matched by a given pattern. - // This list may be empty. In such case it denotes an abandoned - // codebase, and can be used if there is an un-owned subdirectory - // within otherwise owned directory structure. - repeated Owner owner = 2; - // Optionally a rule can be associated with a section name. - // The name must be lowercase, as the names of sections in text - // representation of the codeowners file are case-insensitive. - // Each section represents a kind-of-ownership. That is, - // when evaluating an owner for a path, only one rule can apply - // for a path, but that is within the scope of a section. - // For instance a CODEOWNERS file could specify a [PM] section - // associating product managers with codebases. This rule set - // can be completely independent of the others. In that case, - // when evaluating owners, the result also contains a separate - // owners for the PM section. - string section_name = 3; + // Patterns are familliar glob patterns that match file paths. + // * `filename` matches any file with that name, for example: + // * `/filename` and `/src/filename` match. + // * `directory/path/` matches any tree of subdirectories rooted + // at this pattern, for example: + // * `/src/directory/path/file` matches. + // * `/src/directory/path/another/directory/file` matches. + // * `directory/*` matches only files with specified parent, + // but not descendants, for example: + // * `/src/foo/bar/directory/file` matches. + // * `/src/foo/bar/directory/another/file` does not match. + // * Any of the above can be prefixed with `/`, which further + // filters the match, by requiring the file path match to be + // rooted at the directory root, for `/src/dir/*`: + // * `/src/dir/file` matches. + // * `/main/src/dir/file` does not match, as `src` is not top-level. + // * `/src/dir/another/file` does not match as `*` matches + // only files directly contained in specified directory. + // * In the above patterns `/**/` can be used to match any sub-path + // between two parts of a pattern. For example: `/docs/**/internal/` + // will match `/docs/foo/bar/internal/file`. + // * The file part of the pattern can use a `*` wildcard like so: + // `docs/*.md` will match `/src/docs/index.md` but not `/src/docs/index.js`. + // * In BITBUCKET plugin, patterns that serve to exclude ownership + // start with an exclamation mark `!/src/noownershere`. These are + // translated to a pattern without the `!` and now owners. + string pattern = 1; + // Owners list all the parties that claim ownership over files + // matched by a given pattern. + // This list may be empty. In such case it denotes an abandoned + // codebase, and can be used if there is an un-owned subdirectory + // within otherwise owned directory structure. + repeated Owner owner = 2; + // Optionally a rule can be associated with a section name. + // The name must be lowercase, as the names of sections in text + // representation of the codeowners file are case-insensitive. + // Each section represents a kind-of-ownership. That is, + // when evaluating an owner for a path, only one rule can apply + // for a path, but that is within the scope of a section. + // For instance a CODEOWNERS file could specify a [PM] section + // associating product managers with codebases. This rule set + // can be completely independent of the others. In that case, + // when evaluating owners, the result also contains a separate + // owners for the PM section. + string section_name = 3; } // Owner is denoted by either a handle or an email. // We expect exactly one of the fields to be present. message Owner { - // Handle can refer to a user or a team defined externally. - // In the text config, a handle always starts with `@`. - // In can contain `/` to denote a sub-group. - // The string content of the handle stored here DOES NOT CONTAIN - // the initial `@` sign. - string handle = 1; - // E-mail can be used instead of a handle to denote an owner account. - string email = 2; + // Handle can refer to a user or a team defined externally. + // In the text config, a handle always starts with `@`. + // In can contain `/` to denote a sub-group. + // The string content of the handle stored here DOES NOT CONTAIN + // the initial `@` sign. + string handle = 1; + // E-mail can be used instead of a handle to denote an owner account. + string email = 2; } From bcf939bd5a1cb963acb092fec298ffc93e3c4ffb Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 27 Jan 2023 17:59:04 +0100 Subject: [PATCH 217/678] Fix initial page load for files in the project root (#47026) --- client/web/src/repo/RepoRevisionSidebarFileTree.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/web/src/repo/RepoRevisionSidebarFileTree.tsx b/client/web/src/repo/RepoRevisionSidebarFileTree.tsx index c48c969035ad..0a506472f4f0 100644 --- a/client/web/src/repo/RepoRevisionSidebarFileTree.tsx +++ b/client/web/src/repo/RepoRevisionSidebarFileTree.tsx @@ -83,9 +83,13 @@ export const RepoRevisionSidebarFileTree: React.FunctionComponent = props const { telemetryService, onExpandParent, alwaysLoadAncestors } = props // Ensure that the initial file path does not update when the props change - const [initialFilePath] = useState( - props.initialFilePathIsDirectory ? props.initialFilePath : dirname(props.initialFilePath) - ) + const [initialFilePath] = useState(() => { + let path = props.initialFilePathIsDirectory ? props.initialFilePath : dirname(props.initialFilePath) + if (path === '.') { + path = '' + } + return path + }) const [treeData, setTreeData] = useState(null) const navigate = useNavigate() From 69706b345c80e0e27201593467b0e48c8e60b18f Mon Sep 17 00:00:00 2001 From: Camden Cheek Date: Fri, 27 Jan 2023 10:06:00 -0700 Subject: [PATCH 218/678] fix go import path for searcher.proto (#47033) --- internal/searcher/proto/searcher.pb.go | 9 +++++---- internal/searcher/proto/searcher.proto | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/searcher/proto/searcher.pb.go b/internal/searcher/proto/searcher.pb.go index 907ad57ce1ba..27e40d596524 100644 --- a/internal/searcher/proto/searcher.pb.go +++ b/internal/searcher/proto/searcher.pb.go @@ -865,11 +865,12 @@ var file_searcher_proto_rawDesc = []byte{ 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x17, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, - 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x42, 0x38, 0x5a, - 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x6f, 0x75, 0x72, + 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x42, 0x3c, 0x5a, + 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x67, 0x72, 0x61, 0x70, 0x68, 0x2f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x67, 0x72, - 0x61, 0x70, 0x68, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x65, 0x72, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x70, 0x68, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x65, 0x61, + 0x72, 0x63, 0x68, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/internal/searcher/proto/searcher.proto b/internal/searcher/proto/searcher.proto index 91c469a628d0..27cabeb6e8e6 100644 --- a/internal/searcher/proto/searcher.proto +++ b/internal/searcher/proto/searcher.proto @@ -4,7 +4,7 @@ package searcher; import "google/protobuf/duration.proto"; -option go_package = "github.com/sourcegraph/sourcegraph/cmd/searcher/proto/"; +option go_package = "github.com/sourcegraph/sourcegraph/internal/searcher/proto"; // Searcher is internal interface for the searcher service. service Searcher { From 9534aa21ea1cff2122bd56b5f221d4b52031fb9c Mon Sep 17 00:00:00 2001 From: Beatrix <68532117+abeatrix@users.noreply.github.com> Date: Fri, 27 Jan 2023 09:29:19 -0800 Subject: [PATCH 219/678] ci: add deploy-sourcegraph-k8s to release pipeline (#46993) Add deploy-sourcegraph-k8s, the repo that hosts the new default k8s base cluster, to the current release pipeline --- dev/release/src/release.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/dev/release/src/release.ts b/dev/release/src/release.ts index c6d7e657d11d..20b7f7d0b537 100644 --- a/dev/release/src/release.ts +++ b/dev/release/src/release.ts @@ -572,6 +572,16 @@ cc @${config.captainGitHubUsername} edits: [`tools/update-docker-tags.sh ${release.version}`], ...prBodyAndDraftState([]), }, + { + owner: 'sourcegraph', + repo: 'deploy-sourcegraph-k8s', + base: `${release.major}.${release.minor}`, + head: `publish-${release.version}`, + commitMessage: defaultPRMessage, + title: defaultPRMessage, + edits: [`sg ops update-images -pin-tag ${release.version} base/`], + ...prBodyAndDraftState([]), + }, { owner: 'sourcegraph', repo: 'deploy-sourcegraph-docker', From 119ca1c34adeeff46d2da66687724e1c2cf87eeb Mon Sep 17 00:00:00 2001 From: Noah S-C Date: Fri, 27 Jan 2023 17:49:20 +0000 Subject: [PATCH 220/678] database: support choice between ordered and unordered maps in keyed collection scanner (reducer) (#47029) --- .../migrations/codeintel/scip_migrator.go | 11 +- go.mod | 3 + go.sum | 6 + internal/database/basestore/mocks_test.go | 502 ++++++++++++++++++ internal/database/basestore/rows.go | 11 +- .../database/basestore/scan_collections.go | 114 +++- .../basestore/scan_collections_test.go | 112 ++++ internal/database/encryption_tables.go | 4 +- internal/database/helpers.go | 8 +- mockgen.test.yaml | 4 + 10 files changed, 740 insertions(+), 35 deletions(-) create mode 100644 internal/database/basestore/mocks_test.go create mode 100644 internal/database/basestore/scan_collections_test.go diff --git a/enterprise/internal/oobmigration/migrations/codeintel/scip_migrator.go b/enterprise/internal/oobmigration/migrations/codeintel/scip_migrator.go index d310d8013d0c..0d8b45341f52 100644 --- a/enterprise/internal/oobmigration/migrations/codeintel/scip_migrator.go +++ b/enterprise/internal/oobmigration/migrations/codeintel/scip_migrator.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "crypto/sha256" - "database/sql" "fmt" "os" "sort" @@ -608,8 +607,10 @@ func (s *scipWriter) InsertDocument( return nil } -const DocumentsBatchSize = 256 -const MaxBatchPayloadSum = 1024 * 1024 * 32 +const ( + DocumentsBatchSize = 256 + MaxBatchPayloadSum = 1024 * 1024 * 32 +) func (s *scipWriter) flush(ctx context.Context) (err error) { documents := s.batch @@ -852,7 +853,7 @@ const deleteLSIFDataQuery = ` DELETE FROM %s WHERE dump_id = %s ` -func makeDocumentScanner(serializer *serializer) func(rows *sql.Rows, queryErr error) (map[string]DocumentData, error) { +func makeDocumentScanner(serializer *serializer) func(rows basestore.Rows, queryErr error) (map[string]DocumentData, error) { return basestore.NewMapScanner(func(s dbutil.Scanner) (string, DocumentData, error) { var path string var data MarshalledDocumentData @@ -869,7 +870,7 @@ func makeDocumentScanner(serializer *serializer) func(rows *sql.Rows, queryErr e }) } -func scanResultChunksIntoMap(serializer *serializer, f func(idx int, resultChunk ResultChunkData) error) func(rows *sql.Rows, queryErr error) error { +func scanResultChunksIntoMap(serializer *serializer, f func(idx int, resultChunk ResultChunkData) error) func(rows basestore.Rows, queryErr error) error { return basestore.NewCallbackScanner(func(s dbutil.Scanner) (bool, error) { var idx int var rawData []byte diff --git a/go.mod b/go.mod index 5236f27b7178..52ff5fca9904 100644 --- a/go.mod +++ b/go.mod @@ -191,6 +191,8 @@ require github.com/XSAM/otelsql v0.15.0 require ( cloud.google.com/go/compute/metadata v0.2.1 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/cloudflare/circl v1.3.0 // indirect github.com/cockroachdb/apd/v2 v2.0.1 // indirect github.com/dennwc/varint v1.0.0 // indirect @@ -234,6 +236,7 @@ require ( github.com/sourcegraph/conc v0.1.0 github.com/sourcegraph/mountinfo v0.0.0-20221027185101-272dd8baaf4a github.com/sourcegraph/sourcegraph/monitoring v0.0.0-20230124144931-b2d81b1accb6 + github.com/wk8/go-ordered-map/v2 v2.1.5 github.com/xanzy/go-gitlab v0.76.0 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87 go.opentelemetry.io/otel/exporters/jaeger v1.11.2 diff --git a/go.sum b/go.sum index a2b16bd51869..b1cac76465a5 100644 --- a/go.sum +++ b/go.sum @@ -357,6 +357,8 @@ github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -393,6 +395,8 @@ github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR github.com/bufbuild/buf v1.4.0 h1:GqE3a8CMmcFvWPzuY3Mahf9Kf3S9XgZ/ORpfYFzO+90= github.com/bufbuild/buf v1.4.0/go.mod h1:mwHG7klTHnX+rM/ym8LXGl7vYpVmnwT96xWoRB4H5QI= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= @@ -2267,6 +2271,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/wk8/go-ordered-map/v2 v2.1.5 h1:jLbYIFyWQMUwHLO20cImlCRBoNc5lp0nmE2dvwcxc7k= +github.com/wk8/go-ordered-map/v2 v2.1.5/go.mod h1:9Xvgm2mV2kSq2SAm0Y608tBmu8akTzI7c2bz7/G7ZN4= github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/go-gitlab v0.32.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= diff --git a/internal/database/basestore/mocks_test.go b/internal/database/basestore/mocks_test.go new file mode 100644 index 000000000000..314928c0a1eb --- /dev/null +++ b/internal/database/basestore/mocks_test.go @@ -0,0 +1,502 @@ +// Code generated by go-mockgen 1.3.7; DO NOT EDIT. +// +// This file was generated by running `sg generate` (or `go-mockgen`) at the root of +// this repository. To add additional mocks to this or another package, add a new entry +// to the mockgen.yaml file in the root of this repository. + +package basestore + +import "sync" + +// MockRows is a mock implementation of the Rows interface (from the package +// github.com/sourcegraph/sourcegraph/internal/database/basestore) used for +// unit testing. +type MockRows struct { + // CloseFunc is an instance of a mock function object controlling the + // behavior of the method Close. + CloseFunc *RowsCloseFunc + // ErrFunc is an instance of a mock function object controlling the + // behavior of the method Err. + ErrFunc *RowsErrFunc + // NextFunc is an instance of a mock function object controlling the + // behavior of the method Next. + NextFunc *RowsNextFunc + // ScanFunc is an instance of a mock function object controlling the + // behavior of the method Scan. + ScanFunc *RowsScanFunc +} + +// NewMockRows creates a new mock of the Rows interface. All methods return +// zero values for all results, unless overwritten. +func NewMockRows() *MockRows { + return &MockRows{ + CloseFunc: &RowsCloseFunc{ + defaultHook: func() (r0 error) { + return + }, + }, + ErrFunc: &RowsErrFunc{ + defaultHook: func() (r0 error) { + return + }, + }, + NextFunc: &RowsNextFunc{ + defaultHook: func() (r0 bool) { + return + }, + }, + ScanFunc: &RowsScanFunc{ + defaultHook: func(...interface{}) (r0 error) { + return + }, + }, + } +} + +// NewStrictMockRows creates a new mock of the Rows interface. All methods +// panic on invocation, unless overwritten. +func NewStrictMockRows() *MockRows { + return &MockRows{ + CloseFunc: &RowsCloseFunc{ + defaultHook: func() error { + panic("unexpected invocation of MockRows.Close") + }, + }, + ErrFunc: &RowsErrFunc{ + defaultHook: func() error { + panic("unexpected invocation of MockRows.Err") + }, + }, + NextFunc: &RowsNextFunc{ + defaultHook: func() bool { + panic("unexpected invocation of MockRows.Next") + }, + }, + ScanFunc: &RowsScanFunc{ + defaultHook: func(...interface{}) error { + panic("unexpected invocation of MockRows.Scan") + }, + }, + } +} + +// NewMockRowsFrom creates a new mock of the MockRows interface. All methods +// delegate to the given implementation, unless overwritten. +func NewMockRowsFrom(i Rows) *MockRows { + return &MockRows{ + CloseFunc: &RowsCloseFunc{ + defaultHook: i.Close, + }, + ErrFunc: &RowsErrFunc{ + defaultHook: i.Err, + }, + NextFunc: &RowsNextFunc{ + defaultHook: i.Next, + }, + ScanFunc: &RowsScanFunc{ + defaultHook: i.Scan, + }, + } +} + +// RowsCloseFunc describes the behavior when the Close method of the parent +// MockRows instance is invoked. +type RowsCloseFunc struct { + defaultHook func() error + hooks []func() error + history []RowsCloseFuncCall + mutex sync.Mutex +} + +// Close delegates to the next hook function in the queue and stores the +// parameter and result values of this invocation. +func (m *MockRows) Close() error { + r0 := m.CloseFunc.nextHook()() + m.CloseFunc.appendCall(RowsCloseFuncCall{r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the Close method of the +// parent MockRows instance is invoked and the hook queue is empty. +func (f *RowsCloseFunc) SetDefaultHook(hook func() error) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// Close method of the parent MockRows instance invokes the hook at the +// front of the queue and discards it. After the queue is empty, the default +// hook function is invoked for any future action. +func (f *RowsCloseFunc) PushHook(hook func() error) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *RowsCloseFunc) SetDefaultReturn(r0 error) { + f.SetDefaultHook(func() error { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *RowsCloseFunc) PushReturn(r0 error) { + f.PushHook(func() error { + return r0 + }) +} + +func (f *RowsCloseFunc) nextHook() func() error { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *RowsCloseFunc) appendCall(r0 RowsCloseFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of RowsCloseFuncCall objects describing the +// invocations of this function. +func (f *RowsCloseFunc) History() []RowsCloseFuncCall { + f.mutex.Lock() + history := make([]RowsCloseFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// RowsCloseFuncCall is an object that describes an invocation of method +// Close on an instance of MockRows. +type RowsCloseFuncCall struct { + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c RowsCloseFuncCall) Args() []interface{} { + return []interface{}{} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c RowsCloseFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + +// RowsErrFunc describes the behavior when the Err method of the parent +// MockRows instance is invoked. +type RowsErrFunc struct { + defaultHook func() error + hooks []func() error + history []RowsErrFuncCall + mutex sync.Mutex +} + +// Err delegates to the next hook function in the queue and stores the +// parameter and result values of this invocation. +func (m *MockRows) Err() error { + r0 := m.ErrFunc.nextHook()() + m.ErrFunc.appendCall(RowsErrFuncCall{r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the Err method of the +// parent MockRows instance is invoked and the hook queue is empty. +func (f *RowsErrFunc) SetDefaultHook(hook func() error) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// Err method of the parent MockRows instance invokes the hook at the front +// of the queue and discards it. After the queue is empty, the default hook +// function is invoked for any future action. +func (f *RowsErrFunc) PushHook(hook func() error) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *RowsErrFunc) SetDefaultReturn(r0 error) { + f.SetDefaultHook(func() error { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *RowsErrFunc) PushReturn(r0 error) { + f.PushHook(func() error { + return r0 + }) +} + +func (f *RowsErrFunc) nextHook() func() error { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *RowsErrFunc) appendCall(r0 RowsErrFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of RowsErrFuncCall objects describing the +// invocations of this function. +func (f *RowsErrFunc) History() []RowsErrFuncCall { + f.mutex.Lock() + history := make([]RowsErrFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// RowsErrFuncCall is an object that describes an invocation of method Err +// on an instance of MockRows. +type RowsErrFuncCall struct { + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c RowsErrFuncCall) Args() []interface{} { + return []interface{}{} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c RowsErrFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + +// RowsNextFunc describes the behavior when the Next method of the parent +// MockRows instance is invoked. +type RowsNextFunc struct { + defaultHook func() bool + hooks []func() bool + history []RowsNextFuncCall + mutex sync.Mutex +} + +// Next delegates to the next hook function in the queue and stores the +// parameter and result values of this invocation. +func (m *MockRows) Next() bool { + r0 := m.NextFunc.nextHook()() + m.NextFunc.appendCall(RowsNextFuncCall{r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the Next method of the +// parent MockRows instance is invoked and the hook queue is empty. +func (f *RowsNextFunc) SetDefaultHook(hook func() bool) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// Next method of the parent MockRows instance invokes the hook at the front +// of the queue and discards it. After the queue is empty, the default hook +// function is invoked for any future action. +func (f *RowsNextFunc) PushHook(hook func() bool) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *RowsNextFunc) SetDefaultReturn(r0 bool) { + f.SetDefaultHook(func() bool { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *RowsNextFunc) PushReturn(r0 bool) { + f.PushHook(func() bool { + return r0 + }) +} + +func (f *RowsNextFunc) nextHook() func() bool { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *RowsNextFunc) appendCall(r0 RowsNextFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of RowsNextFuncCall objects describing the +// invocations of this function. +func (f *RowsNextFunc) History() []RowsNextFuncCall { + f.mutex.Lock() + history := make([]RowsNextFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// RowsNextFuncCall is an object that describes an invocation of method Next +// on an instance of MockRows. +type RowsNextFuncCall struct { + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 bool +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c RowsNextFuncCall) Args() []interface{} { + return []interface{}{} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c RowsNextFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + +// RowsScanFunc describes the behavior when the Scan method of the parent +// MockRows instance is invoked. +type RowsScanFunc struct { + defaultHook func(...interface{}) error + hooks []func(...interface{}) error + history []RowsScanFuncCall + mutex sync.Mutex +} + +// Scan delegates to the next hook function in the queue and stores the +// parameter and result values of this invocation. +func (m *MockRows) Scan(v0 ...interface{}) error { + r0 := m.ScanFunc.nextHook()(v0...) + m.ScanFunc.appendCall(RowsScanFuncCall{v0, r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the Scan method of the +// parent MockRows instance is invoked and the hook queue is empty. +func (f *RowsScanFunc) SetDefaultHook(hook func(...interface{}) error) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// Scan method of the parent MockRows instance invokes the hook at the front +// of the queue and discards it. After the queue is empty, the default hook +// function is invoked for any future action. +func (f *RowsScanFunc) PushHook(hook func(...interface{}) error) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *RowsScanFunc) SetDefaultReturn(r0 error) { + f.SetDefaultHook(func(...interface{}) error { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *RowsScanFunc) PushReturn(r0 error) { + f.PushHook(func(...interface{}) error { + return r0 + }) +} + +func (f *RowsScanFunc) nextHook() func(...interface{}) error { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *RowsScanFunc) appendCall(r0 RowsScanFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of RowsScanFuncCall objects describing the +// invocations of this function. +func (f *RowsScanFunc) History() []RowsScanFuncCall { + f.mutex.Lock() + history := make([]RowsScanFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// RowsScanFuncCall is an object that describes an invocation of method Scan +// on an instance of MockRows. +type RowsScanFuncCall struct { + // Arg0 is a slice containing the values of the variadic arguments + // passed to this method invocation. + Arg0 []interface{} + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. The variadic slice argument is flattened in this array such +// that one positional argument and three variadic arguments would result in +// a slice of four, not two. +func (c RowsScanFuncCall) Args() []interface{} { + trailing := []interface{}{} + for _, val := range c.Arg0 { + trailing = append(trailing, val) + } + + return append([]interface{}{}, trailing...) +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c RowsScanFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} diff --git a/internal/database/basestore/rows.go b/internal/database/basestore/rows.go index 47435e844ded..1f80b4460931 100644 --- a/internal/database/basestore/rows.go +++ b/internal/database/basestore/rows.go @@ -1,11 +1,16 @@ package basestore import ( - "database/sql" - "github.com/sourcegraph/sourcegraph/lib/errors" ) +type Rows interface { + Next() bool + Close() error + Err() error + Scan(...interface{}) error +} + // CloseRows closes the given rows object. The resulting error is a multierror // containing the error parameter along with any errors that occur during scanning // or closing the rows object. The rows object is assumed to be non-nil. @@ -25,6 +30,6 @@ import ( // ensure that the rows are always properly handled. // // things, err := ScanThings(store.Query(ctx, query)) -func CloseRows(rows *sql.Rows, err error) error { +func CloseRows(rows Rows, err error) error { return errors.Append(err, rows.Close(), rows.Err()) } diff --git a/internal/database/basestore/scan_collections.go b/internal/database/basestore/scan_collections.go index e728c6fd45a9..6468e21c690e 100644 --- a/internal/database/basestore/scan_collections.go +++ b/internal/database/basestore/scan_collections.go @@ -1,7 +1,8 @@ package basestore import ( - "database/sql" + orderedmap "github.com/wk8/go-ordered-map/v2" + "golang.org/x/exp/maps" "github.com/sourcegraph/sourcegraph/internal/database/dbutil" ) @@ -9,8 +10,8 @@ import ( // NewCallbackScanner returns a basestore scanner function that invokes the given // function on every SQL row object in the given query result set. If the callback // function returns a false-valued flag, the remaining rows are discarded. -func NewCallbackScanner(f func(dbutil.Scanner) (bool, error)) func(rows *sql.Rows, queryErr error) error { - return func(rows *sql.Rows, queryErr error) (err error) { +func NewCallbackScanner(f func(dbutil.Scanner) (bool, error)) func(rows Rows, queryErr error) error { + return func(rows Rows, queryErr error) (err error) { if queryErr != nil { return queryErr } @@ -32,8 +33,8 @@ func NewCallbackScanner(f func(dbutil.Scanner) (bool, error)) func(rows *sql.Row // first value of a query result (assuming there is at most one value). // The given function is invoked with a SQL rows object to scan a single // value. -func NewFirstScanner[T any](f func(dbutil.Scanner) (T, error)) func(rows *sql.Rows, queryErr error) (T, bool, error) { - return func(rows *sql.Rows, queryErr error) (value T, called bool, _ error) { +func NewFirstScanner[T any](f func(dbutil.Scanner) (T, error)) func(rows Rows, queryErr error) (T, bool, error) { + return func(rows Rows, queryErr error) (value T, called bool, _ error) { scanner := func(s dbutil.Scanner) (_ bool, err error) { called = true value, err = f(s) @@ -48,8 +49,8 @@ func NewFirstScanner[T any](f func(dbutil.Scanner) (T, error)) func(rows *sql.Ro // NewSliceScanner returns a basestore scanner function that returns all // the values of a query result. The given function is invoked multiple // times with a SQL rows object to scan a single value. -func NewSliceScanner[T any](f func(dbutil.Scanner) (T, error)) func(rows *sql.Rows, queryErr error) ([]T, error) { - return func(rows *sql.Rows, queryErr error) (values []T, _ error) { +func NewSliceScanner[T any](f func(dbutil.Scanner) (T, error)) func(rows Rows, queryErr error) ([]T, error) { + return func(rows Rows, queryErr error) (values []T, _ error) { scanner := func(s dbutil.Scanner) (bool, error) { value, err := f(s) if err != nil { @@ -72,8 +73,8 @@ func NewSliceScanner[T any](f func(dbutil.Scanner) (T, error)) func(rows *sql.Ro // Example query that would avail of this function, where we want only 10 rows but still // the count of everything that would have been returned, without performing two separate queries: // SELECT u.id, COUNT(*) OVER() as count FROM users LIMIT 10 -func NewSliceWithCountScanner[T any](f func(dbutil.Scanner) (T, int, error)) func(rows *sql.Rows, queryErr error) ([]T, int, error) { - return func(rows *sql.Rows, queryErr error) (values []T, totalCount int, _ error) { +func NewSliceWithCountScanner[T any](f func(dbutil.Scanner) (T, int, error)) func(rows Rows, queryErr error) ([]T, int, error) { + return func(rows Rows, queryErr error) (values []T, totalCount int, _ error) { scanner := func(s dbutil.Scanner) (bool, error) { value, count, err := f(s) if err != nil { @@ -94,44 +95,52 @@ func NewSliceWithCountScanner[T any](f func(dbutil.Scanner) (T, int, error)) fun // query result organized as a map. The given function is invoked multiple times with a SQL rows // object to scan a single map value. The given reducer provides a way to customize how multiple // values are reduced into a collection. -func NewKeyedCollectionScanner[K comparable, V, Vs any]( +func NewKeyedCollectionScanner[Map keyedMap[K, Vs], K comparable, V, Vs any]( + values Map, scanPair func(dbutil.Scanner) (K, V, error), reducer CollectionReducer[V, Vs], -) func(rows *sql.Rows, queryErr error) (map[K]Vs, error) { - return func(rows *sql.Rows, queryErr error) (map[K]Vs, error) { - values := map[K]Vs{} +) func(rows Rows, queryErr error) error { + return func(rows Rows, queryErr error) error { scanner := func(s dbutil.Scanner) (bool, error) { key, value, err := scanPair(s) if err != nil { return false, err } - collection, ok := values[key] + collection, ok := values.Get(key) if !ok { collection = reducer.Create() } - values[key] = reducer.Reduce(collection, value) + values.Set(key, reducer.Reduce(collection, value)) return true, nil } err := NewCallbackScanner(scanner)(rows, queryErr) - return values, err + return err } } // NewMapScanner returns a basestore scanner function that returns the values of a // query result organized as a map. The given function is invoked multiple times with // a SQL rows object to scan a single map value. -func NewMapScanner[K comparable, V any](f func(dbutil.Scanner) (K, V, error)) func(rows *sql.Rows, queryErr error) (map[K]V, error) { - return NewKeyedCollectionScanner[K, V, V](f, SingleValueReducer[V]{}) +func NewMapScanner[K comparable, V any](f func(dbutil.Scanner) (K, V, error)) func(rows Rows, queryErr error) (map[K]V, error) { + return func(rows Rows, queryErr error) (map[K]V, error) { + m := &UnorderedMap[K, V]{m: make(map[K]V)} + err := NewKeyedCollectionScanner[*UnorderedMap[K, V], K, V, V](m, f, SingleValueReducer[V]{})(rows, queryErr) + return m.ToMap(), err + } } // NewMapSliceScanner returns a basestore scanner function that returns the values // of a query result organized as a map of slice values. The given function is invoked // multiple times with a SQL rows object to scan a single map key value. -func NewMapSliceScanner[K comparable, V any](f func(dbutil.Scanner) (K, V, error)) func(rows *sql.Rows, queryErr error) (map[K][]V, error) { - return NewKeyedCollectionScanner[K, V, []V](f, SliceReducer[V]{}) +func NewMapSliceScanner[K comparable, V any](f func(dbutil.Scanner) (K, V, error)) func(rows Rows, queryErr error) (map[K][]V, error) { + return func(rows Rows, queryErr error) (map[K][]V, error) { + m := &UnorderedMap[K, []V]{m: make(map[K][]V)} + err := NewKeyedCollectionScanner[*UnorderedMap[K, []V], K, V, []V](m, f, SliceReducer[V]{})(rows, queryErr) + return m.ToMap(), err + } } // CollectionReducer configures how scanners created by `NewKeyedCollectionScanner` will @@ -155,3 +164,68 @@ type SingleValueReducer[T any] struct{} func (r SingleValueReducer[T]) Create() (_ T) { return } func (r SingleValueReducer[T]) Reduce(collection T, value T) T { return value } + +type keyedMap[K comparable, V any] interface { + Get(K) (V, bool) + Set(K, V) + Len() int + Values() []V + ToMap() map[K]V +} + +type UnorderedMap[K comparable, V any] struct { + m map[K]V +} + +func (m UnorderedMap[K, V]) Get(key K) (V, bool) { + v, ok := m.m[key] + return v, ok +} + +func (m UnorderedMap[K, V]) Set(key K, val V) { + m.m[key] = val +} + +func (m UnorderedMap[K, V]) Len() int { + return len(m.m) +} + +func (m UnorderedMap[K, V]) Values() []V { + return maps.Values(m.m) +} + +func (m *UnorderedMap[K, V]) ToMap() map[K]V { + return m.m +} + +type OrderedMap[K comparable, V any] struct { + m *orderedmap.OrderedMap[K, V] +} + +func (m OrderedMap[K, V]) Get(key K) (V, bool) { + return m.m.Get(key) +} + +func (m OrderedMap[K, V]) Set(key K, val V) { + m.m.Set(key, val) +} + +func (m OrderedMap[K, V]) Len() int { + return m.m.Len() +} + +func (m OrderedMap[K, V]) Values() []V { + values := make([]V, 0, m.m.Len()) + for pair := m.m.Oldest(); pair != nil; pair = pair.Next() { + values = append(values, pair.Value) + } + return values +} + +func (m *OrderedMap[K, V]) ToMap() map[K]V { + ret := make(map[K]V, m.m.Len()) + for pair := m.m.Oldest(); pair != nil; pair = pair.Next() { + ret[pair.Key] = pair.Value + } + return ret +} diff --git a/internal/database/basestore/scan_collections_test.go b/internal/database/basestore/scan_collections_test.go new file mode 100644 index 000000000000..705457c46fdb --- /dev/null +++ b/internal/database/basestore/scan_collections_test.go @@ -0,0 +1,112 @@ +package basestore + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + orderedmap "github.com/wk8/go-ordered-map/v2" + + "github.com/sourcegraph/sourcegraph/internal/database/dbutil" +) + +type reducee struct { + ID int + Values []int +} + +type reduceeRow struct { + ID, Value int +} + +type testReducer struct{} + +func (d testReducer) Create() reducee { + return reducee{Values: make([]int, 0)} +} + +func (d testReducer) Reduce(collection reducee, value reduceeRow) reducee { + collection.ID = value.ID + collection.Values = append(collection.Values, value.Value) + return collection +} + +func Test_KeyedCollectionScannerOrdered(t *testing.T) { + data := []reduceeRow{ + { + ID: 0, + Value: 0, + }, + { + ID: 0, + Value: 1, + }, + { + ID: 0, + Value: 2, + }, + { + ID: 2, + Value: 0, + }, + { + ID: 1, + Value: 1, + }, + { + ID: 0, + Value: 3, + }, + { + ID: -1, + Value: -1, + }, + { + ID: 1, + Value: 0, + }, + } + offset := -1 + + rows := NewMockRows() + rows.NextFunc.SetDefaultHook(func() bool { + offset++ + return offset < len(data) + }) + rows.ScanFunc.SetDefaultHook(func(i ...interface{}) error { + *(i[0].(*int)) = data[offset].ID + *(i[1].(*int)) = data[offset].Value + return nil + }) + + m := &OrderedMap[int, reducee]{m: orderedmap.New[int, reducee]()} + NewKeyedCollectionScanner[*OrderedMap[int, reducee], int, reduceeRow, reducee](m, func(s dbutil.Scanner) (int, reduceeRow, error) { + var red reduceeRow + err := s.Scan(&red.ID, &red.Value) + return red.ID, red, err + }, testReducer{})(rows, nil) + + if m.Len() != 4 { + t.Errorf("unexpected map size: want=%d got=%d\n%v", 4, m.Len(), m.Values()) + } + + if diff := cmp.Diff([]reducee{ + { + ID: 0, + Values: []int{0, 1, 2, 3}, + }, + { + ID: 2, + Values: []int{0}, + }, + { + ID: 1, + Values: []int{1, 0}, + }, + { + ID: -1, + Values: []int{-1}, + }, + }, m.Values()); diff != "" { + t.Errorf("unexpected collection output (-want,+got):\n%s", diff) + } +} diff --git a/internal/database/encryption_tables.go b/internal/database/encryption_tables.go index 14e0ad488216..004ef9cac019 100644 --- a/internal/database/encryption_tables.go +++ b/internal/database/encryption_tables.go @@ -1,8 +1,6 @@ package database import ( - "database/sql" - "github.com/sourcegraph/sourcegraph/internal/database/basestore" "github.com/sourcegraph/sourcegraph/internal/database/dbutil" "github.com/sourcegraph/sourcegraph/internal/encryption" @@ -15,7 +13,7 @@ type EncryptionConfig struct { KeyIDFieldName string EncryptedFieldNames []string UpdateAsBytes bool - Scan func(*sql.Rows, error) (map[int]Encrypted, error) + Scan func(basestore.Rows, error) (map[int]Encrypted, error) Key func() encryption.Key Limit int } diff --git a/internal/database/helpers.go b/internal/database/helpers.go index 540318c608ac..c5eb9e3e4d04 100644 --- a/internal/database/helpers.go +++ b/internal/database/helpers.go @@ -195,9 +195,9 @@ func copyPtr[T any](n *T) *T { // Clone (aka deepcopy) returns a new PaginationArgs object with the same values as "p". func (p *PaginationArgs) Clone() *PaginationArgs { return &PaginationArgs{ - First: copyPtr[int](p.First), - Last: copyPtr[int](p.Last), - After: copyPtr[string](p.After), - Before: copyPtr[string](p.Before), + First: copyPtr(p.First), + Last: copyPtr(p.Last), + After: copyPtr(p.After), + Before: copyPtr(p.Before), } } diff --git a/mockgen.test.yaml b/mockgen.test.yaml index 254654202ba1..d0591052bf13 100644 --- a/mockgen.test.yaml +++ b/mockgen.test.yaml @@ -178,6 +178,10 @@ path: github.com/sourcegraph/sourcegraph/internal/database/migration/runner interfaces: - Store +- filename: internal/database/basestore/mocks_test.go + path: github.com/sourcegraph/sourcegraph/internal/database/basestore + interfaces: + - Rows - filename: internal/featureflag/mocks_test.go path: github.com/sourcegraph/sourcegraph/internal/featureflag interfaces: From c5bca867cbde6bced88e198837181d34f88aa072 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 27 Jan 2023 10:06:22 -0800 Subject: [PATCH 221/678] Remove old tutorials page (#47039) remove old tutorials page --- doc/tutorials/index.md | 93 ------------------------------------------ 1 file changed, 93 deletions(-) delete mode 100644 doc/tutorials/index.md diff --git a/doc/tutorials/index.md b/doc/tutorials/index.md deleted file mode 100644 index 461a5b860874..000000000000 --- a/doc/tutorials/index.md +++ /dev/null @@ -1,93 +0,0 @@ -# Sourcegraph User Tutorials - -> ⚠️ Note: Work in Progress. Full Content & Links Coming Soon. - - -## Find Your Way Around Sourcegraph -| Topic | Description | -| ----------- | ----------- | -| Home | TODO | -| Notebooks | TODO | -| Code Monitoring | TODO | -| Settings & Configuration | TODO | - -## Setting Up Your Sourcegraph User Environment -| Topic | Description | -| -------- | --------| -| Customizing your settings | TODO | -| Using Sourcegraph with your IDE | TODO | -| Using the Sourcegraph browser extension on your code | TODO |host -| Using the Sourcegraph CLI | TODO | -| Saving Searches | TODO | -| Search Contexts | TODO | - -## Get Started Searching -| Topic | Description | -| -------- | --------| -| Search features | TODO | -| Search types | TODO | -| Saved searches | TODO | -| Search contexts | TODO | -| Search Query Syntax | TODO | - -## More Advanced Searching -| Topic | Description | -| -------- | --------| -| Commits | TODO | -| Multi branch | TODO | -| AND/OR | TODO | -| Other more advanced filters (time, diff, author, etc) | TODO | -| Advanced Regex | TODO | -| Structural Search | TODO | - -## Search Scenarios -| Topic | Description | -| -------- | --------| -| How to Space | TODO | - -## Code Navigation -| Topic | Description | -| -------- | --------| -| Search vs Precise | TODO | -| Language Support | TODO | -| Definition & Implementation | TODO | -| References & dependencies | TODO | -| Symbol search | TODO | - - -## Search Notebooks -| Topic | Description | -| -------- | --------| -| Web-based vs File-based | Description | -| Blocks | Description | - - -## Code Insights -| Topic | Description | -| -------- | --------| -| Report Types | TODO | -| Filters | TODO | -| Dashboards | TODO | -| Access & Sharing | TODO | -| Link to how-to space | TODO | - - -## Batch Changes -| Topic | Description | -| -------- | --------| -| Workflow | TODO | -| Creating | TODO | -| Viewing | TODO | -| Publishing | TODO | -| Updating | TODO | -| Error Handling | TODO | -| Link to how-to space | TODO | - - -## Using GraphQL -| Topic | Description | -| -------- | --------| -| UI vs API | TODO | -| Access / permissions | TODO | -| Examples | TODO | - From 4265edc1420358c9a6619fe574d1e346fe37ee16 Mon Sep 17 00:00:00 2001 From: Geoffrey Gilmore Date: Fri, 27 Jan 2023 10:15:02 -0800 Subject: [PATCH 222/678] grpc: move symbols proto definitions to internal/symbols/proto (#47038) --- cmd/symbols/internal/api/handler.go | 2 +- cmd/symbols/internal/api/handler_cgo.go | 2 +- cmd/symbols/internal/api/handler_nocgo.go | 2 +- internal/symbols/client.go | 2 +- {cmd => internal}/symbols/proto/BUILD.bazel | 0 {cmd => internal}/symbols/proto/buf.gen.yaml | 0 {cmd => internal}/symbols/proto/conversion.go | 0 {cmd => internal}/symbols/proto/conversion_test.go | 0 internal/symbols/proto/doc.go | 2 ++ {cmd => internal}/symbols/proto/symbols.pb.go | 8 ++++---- {cmd => internal}/symbols/proto/symbols.proto | 2 +- {cmd => internal}/symbols/proto/symbols_grpc.pb.go | 0 12 files changed, 11 insertions(+), 9 deletions(-) rename {cmd => internal}/symbols/proto/BUILD.bazel (100%) rename {cmd => internal}/symbols/proto/buf.gen.yaml (100%) rename {cmd => internal}/symbols/proto/conversion.go (100%) rename {cmd => internal}/symbols/proto/conversion_test.go (100%) create mode 100644 internal/symbols/proto/doc.go rename {cmd => internal}/symbols/proto/symbols.pb.go (99%) rename {cmd => internal}/symbols/proto/symbols.proto (98%) rename {cmd => internal}/symbols/proto/symbols_grpc.pb.go (100%) diff --git a/cmd/symbols/internal/api/handler.go b/cmd/symbols/internal/api/handler.go index 04f232afd587..6b7ae65f6e54 100644 --- a/cmd/symbols/internal/api/handler.go +++ b/cmd/symbols/internal/api/handler.go @@ -8,11 +8,11 @@ import ( "github.com/sourcegraph/go-ctags" logger "github.com/sourcegraph/log" - "github.com/sourcegraph/sourcegraph/cmd/symbols/proto" "github.com/sourcegraph/sourcegraph/cmd/symbols/types" "github.com/sourcegraph/sourcegraph/internal/grpc/defaults" "github.com/sourcegraph/sourcegraph/internal/search" "github.com/sourcegraph/sourcegraph/internal/search/result" + "github.com/sourcegraph/sourcegraph/internal/symbols/proto" internaltypes "github.com/sourcegraph/sourcegraph/internal/types" "github.com/sourcegraph/sourcegraph/lib/errors" diff --git a/cmd/symbols/internal/api/handler_cgo.go b/cmd/symbols/internal/api/handler_cgo.go index 9b56fdbfbf06..3c64d9def49a 100644 --- a/cmd/symbols/internal/api/handler_cgo.go +++ b/cmd/symbols/internal/api/handler_cgo.go @@ -6,9 +6,9 @@ import ( "context" "net/http" - "github.com/sourcegraph/sourcegraph/cmd/symbols/proto" "github.com/sourcegraph/sourcegraph/cmd/symbols/squirrel" "github.com/sourcegraph/sourcegraph/cmd/symbols/types" + "github.com/sourcegraph/sourcegraph/internal/symbols/proto" internaltypes "github.com/sourcegraph/sourcegraph/internal/types" ) diff --git a/cmd/symbols/internal/api/handler_nocgo.go b/cmd/symbols/internal/api/handler_nocgo.go index e16a9d34fdd7..e7dc676992c5 100644 --- a/cmd/symbols/internal/api/handler_nocgo.go +++ b/cmd/symbols/internal/api/handler_nocgo.go @@ -7,9 +7,9 @@ import ( "encoding/json" "net/http" - "github.com/sourcegraph/sourcegraph/cmd/symbols/proto" "github.com/sourcegraph/sourcegraph/cmd/symbols/types" "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/symbols/proto" internaltypes "github.com/sourcegraph/sourcegraph/internal/types" ) diff --git a/internal/symbols/client.go b/internal/symbols/client.go index 49d56d43c8e3..4bf39b77be36 100644 --- a/internal/symbols/client.go +++ b/internal/symbols/client.go @@ -16,9 +16,9 @@ import ( "github.com/opentracing/opentracing-go/ext" otlog "github.com/opentracing/opentracing-go/log" "github.com/sourcegraph/go-ctags" - "github.com/sourcegraph/sourcegraph/cmd/symbols/proto" "github.com/sourcegraph/sourcegraph/internal/featureflag" "github.com/sourcegraph/sourcegraph/internal/grpc/defaults" + "github.com/sourcegraph/sourcegraph/internal/symbols/proto" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/protobuf/types/known/emptypb" diff --git a/cmd/symbols/proto/BUILD.bazel b/internal/symbols/proto/BUILD.bazel similarity index 100% rename from cmd/symbols/proto/BUILD.bazel rename to internal/symbols/proto/BUILD.bazel diff --git a/cmd/symbols/proto/buf.gen.yaml b/internal/symbols/proto/buf.gen.yaml similarity index 100% rename from cmd/symbols/proto/buf.gen.yaml rename to internal/symbols/proto/buf.gen.yaml diff --git a/cmd/symbols/proto/conversion.go b/internal/symbols/proto/conversion.go similarity index 100% rename from cmd/symbols/proto/conversion.go rename to internal/symbols/proto/conversion.go diff --git a/cmd/symbols/proto/conversion_test.go b/internal/symbols/proto/conversion_test.go similarity index 100% rename from cmd/symbols/proto/conversion_test.go rename to internal/symbols/proto/conversion_test.go diff --git a/internal/symbols/proto/doc.go b/internal/symbols/proto/doc.go new file mode 100644 index 000000000000..f159cc9ad056 --- /dev/null +++ b/internal/symbols/proto/doc.go @@ -0,0 +1,2 @@ +// Package proto contains protocol buffer definitions for the symbols service. +package proto diff --git a/cmd/symbols/proto/symbols.pb.go b/internal/symbols/proto/symbols.pb.go similarity index 99% rename from cmd/symbols/proto/symbols.pb.go rename to internal/symbols/proto/symbols.pb.go index 94a29c2fec29..a24ceafbb881 100644 --- a/cmd/symbols/proto/symbols.pb.go +++ b/internal/symbols/proto/symbols.pb.go @@ -1186,11 +1186,11 @@ var file_symbols_proto_rawDesc = []byte{ 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x00, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x22, 0x00, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x67, 0x72, 0x61, 0x70, 0x68, 0x2f, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x67, 0x72, 0x61, 0x70, 0x68, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x73, 0x79, 0x6d, - 0x62, 0x6f, 0x6c, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x72, 0x63, 0x65, 0x67, 0x72, 0x61, 0x70, 0x68, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2f, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/cmd/symbols/proto/symbols.proto b/internal/symbols/proto/symbols.proto similarity index 98% rename from cmd/symbols/proto/symbols.proto rename to internal/symbols/proto/symbols.proto index 7dd4e5b5ffb2..af80803db1ec 100644 --- a/cmd/symbols/proto/symbols.proto +++ b/internal/symbols/proto/symbols.proto @@ -5,7 +5,7 @@ package symbols.v1; import "google/protobuf/duration.proto"; import "google/protobuf/empty.proto"; -option go_package = "github.com/sourcegraph/sourcegraph/cmd/symbols/proto"; +option go_package = "github.com/sourcegraph/sourcegraph/internal/symbols/proto"; service Symbols { rpc Search(SearchRequest) returns (SymbolsResponse) {} diff --git a/cmd/symbols/proto/symbols_grpc.pb.go b/internal/symbols/proto/symbols_grpc.pb.go similarity index 100% rename from cmd/symbols/proto/symbols_grpc.pb.go rename to internal/symbols/proto/symbols_grpc.pb.go From 4c730bd4332347e14ff4517cc8bb90d62d2a0db1 Mon Sep 17 00:00:00 2001 From: Camden Cheek Date: Fri, 27 Jan 2023 11:50:31 -0700 Subject: [PATCH 223/678] sort the clashing slice for determinism (#47035) --- internal/search/repos/repos.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/search/repos/repos.go b/internal/search/repos/repos.go index c4d2803677af..f07d5bba6b90 100644 --- a/internal/search/repos/repos.go +++ b/internal/search/repos/repos.go @@ -911,7 +911,7 @@ func getRevsForMatchedRepo(repo api.RepoName, pats []patternRevspec) (matched [] clashing = append(clashing, rev) } // ensure that lists are always returned in sorted order. - slices.SortFunc(matched, query.RevisionSpecifier.Less) + slices.SortFunc(clashing, query.RevisionSpecifier.Less) return } From 459085c9ed48c6db1876e9c4cdaa1ad3452a1e45 Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Fri, 27 Jan 2023 10:54:09 -0800 Subject: [PATCH 224/678] Search: restore old syntax for repo:contains filters (#47007) In 4.0, we renamed the `repo.contains` predicates to be more consistent. At the time, we didn't think any users would be affected, so we cut over fully to the new syntax. But some users have scripts or batch changes that use the old syntax, which will break on 4.0 with no grace period to cut over. This change restores the old syntax alongside the new, to give users a time period to transition. We still plan to remove the old syntax in the future. The old syntax is only restored in the backend, it's not documented or part of autocomplete. --- CHANGELOG.md | 1 + dev/gqltest/search_test.go | 15 ++++ internal/search/query/predicate.go | 108 ++++++++++++++++++++++-- internal/search/query/predicate_test.go | 55 +++++++++++- internal/search/query/types.go | 9 ++ internal/search/query/visitor_test.go | 3 + 6 files changed, 184 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31c170738427..bfdeb625ee58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ All notable changes to Sourcegraph are documented in this file. ### Fixed - Fixed a bug where saving default Sort & Limit filters in Code Insights did not persist [#46653](https://github.com/sourcegraph/sourcegraph/pull/46653) +- Restored the old syntax for `repo:contains` filters that was previously removed in version 4.0.0. For now, both the old and new syntaxes are supported to allow for smooth upgrades. Users are encouraged to switch to the new syntax, since the old one may still be removed in a future version. ### Removed diff --git a/dev/gqltest/search_test.go b/dev/gqltest/search_test.go index 11483776d201..75347cc9470a 100644 --- a/dev/gqltest/search_test.go +++ b/dev/gqltest/search_test.go @@ -1116,6 +1116,11 @@ func testSearchClient(t *testing.T, client searchClient) { query: `repo:contains.file(path:go\.mod)`, counts: counts{Repo: 2}, }, + { + name: `repo contains file using deprecated syntax`, + query: `repo:contains.file(go\.mod)`, + counts: counts{Repo: 2}, + }, { name: `repo contains file but not content`, query: `repo:contains.path(go\.mod) -repo:contains.content(go-diff)`, @@ -1220,6 +1225,16 @@ func testSearchClient(t *testing.T, client searchClient) { query: `repo:contains.file(path:diff.pb.go) type:commit LSIF`, counts: counts{Commit: 2}, }, + { + name: `repo contains file using deprecated syntax`, + query: `repo:contains(file:go\.mod)`, + counts: counts{Repo: 2}, + }, + { + name: `repo contains content using deprecated syntax`, + query: `repo:contains(content:nextFileFirstLine)`, + counts: counts{Repo: 1}, + }, { name: `predicate logic does not conflict with unrecognized patterns`, query: `repo:sg(test)`, diff --git a/internal/search/query/predicate.go b/internal/search/query/predicate.go index 38df6f3c7954..727778ab380f 100644 --- a/internal/search/query/predicate.go +++ b/internal/search/query/predicate.go @@ -38,6 +38,9 @@ var DefaultPredicateRegistry = PredicateRegistry{ "has.tag": func() Predicate { return &RepoHasTagPredicate{} }, "has": func() Predicate { return &RepoHasKVPPredicate{} }, "has.key": func() Predicate { return &RepoHasKeyPredicate{} }, + + // Deprecated predicates + "contains": func() Predicate { return &RepoContainsPredicate{} }, }, FieldFile: { "contains.content": func() Predicate { return &FileContainsContentPredicate{} }, @@ -106,8 +109,9 @@ func (EmptyPredicate) Unmarshal(_ string, negated bool) error { return nil } -// RepoContainsFilePredicate represents the `repo:contains.file()` predicate, -// which filters to repos that contain a path and/or content +// RepoContainsFilePredicate represents the `repo:contains.file()` predicate, which filters to +// repos that contain a path and/or content. NOTE: this predicate still supports the deprecated +// syntax `repo:contains.file(name.go)` on a best-effort basis. type RepoContainsFilePredicate struct { Path string Content string @@ -120,19 +124,41 @@ func (f *RepoContainsFilePredicate) Unmarshal(params string, negated bool) error return err } - for _, node := range nodes { - if err := f.parseNode(node); err != nil { + if err := f.parseNodes(nodes); err != nil { + // If there's a parsing error, try falling back to the deprecated syntax `repo:contains.file(name.go)`. + // Only attempt to fall back if there is a single pattern node, to avoid being too lenient. + if len(nodes) != 1 { + return err + } + + pattern, ok := nodes[0].(Pattern) + if !ok { + return err + } + + if _, err := syntax.Parse(pattern.Value, syntax.Perl); err != nil { return err } + f.Path = pattern.Value } if f.Path == "" && f.Content == "" { return errors.New("one of path or content must be set") } + f.Negated = negated return nil } +func (f *RepoContainsFilePredicate) parseNodes(nodes []Node) error { + for _, node := range nodes { + if err := f.parseNode(node); err != nil { + return err + } + } + return nil +} + func (f *RepoContainsFilePredicate) parseNode(n Node) error { switch v := n.(type) { case Parameter: @@ -160,7 +186,7 @@ func (f *RepoContainsFilePredicate) parseNode(n Node) error { return errors.Errorf("unsupported option %q", v.Field) } case Pattern: - return errors.Errorf(`prepend 'path:' or 'content:' to "%s" to search repositories containing path or content respectively.`, v.Value) + return errors.Errorf(`prepend 'file:' or 'content:' to "%s" to search repositories containing files or content respectively.`, v.Value) case Operator: if v.Kind == Or { return errors.New("predicates do not currently support 'or' queries") @@ -319,6 +345,78 @@ func (p *RepoHasKeyPredicate) Unmarshal(params string, negated bool) (err error) func (p *RepoHasKeyPredicate) Field() string { return FieldRepo } func (p *RepoHasKeyPredicate) Name() string { return "has.key" } +// RepoContainsPredicate represents the `repo:contains(file:a content:b)` predicate. +// DEPRECATED: this syntax is deprecated in favor of `repo:contains.file`. +type RepoContainsPredicate struct { + File string + Content string + Negated bool +} + +func (f *RepoContainsPredicate) Unmarshal(params string, negated bool) error { + nodes, err := Parse(params, SearchTypeRegex) + if err != nil { + return err + } + for _, node := range nodes { + if err := f.parseNode(node); err != nil { + return err + } + } + + if f.File == "" && f.Content == "" { + return errors.New("one of file or content must be set") + } + f.Negated = negated + return nil +} + +func (f *RepoContainsPredicate) parseNode(n Node) error { + switch v := n.(type) { + case Parameter: + if v.Negated { + return errors.New("the repo:contains() predicate does not currently support negated values") + } + switch strings.ToLower(v.Field) { + case "file": + if f.File != "" { + return errors.New("cannot specify file multiple times") + } + if _, err := regexp.Compile(v.Value); err != nil { + return errors.Errorf("the repo:contains() predicate has invalid `file` argument: %w", err) + } + f.File = v.Value + case "content": + if f.Content != "" { + return errors.New("cannot specify content multiple times") + } + if _, err := regexp.Compile(v.Value); err != nil { + return errors.Errorf("the repo:contains() predicate has invalid `content` argument: %w", err) + } + f.Content = v.Value + default: + return errors.Errorf("unsupported option %q", v.Field) + } + case Pattern: + return errors.Errorf(`prepend 'file:' or 'content:' to "%s" to search repositories containing files or content respectively.`, v.Value) + case Operator: + if v.Kind == Or { + return errors.New("predicates do not currently support 'or' queries") + } + for _, operand := range v.Operands { + if err := f.parseNode(operand); err != nil { + return err + } + } + default: + return errors.Errorf("unsupported node type %T", n) + } + return nil +} + +func (f *RepoContainsPredicate) Field() string { return FieldRepo } +func (f *RepoContainsPredicate) Name() string { return "contains" } + /* file:contains.content(pattern) */ type FileContainsContentPredicate struct { diff --git a/internal/search/query/predicate_test.go b/internal/search/query/predicate_test.go index 1789972bb573..91e50f6ab947 100644 --- a/internal/search/query/predicate_test.go +++ b/internal/search/query/predicate_test.go @@ -19,6 +19,8 @@ func TestRepoContainsFilePredicate(t *testing.T) { {`content`, `content:test`, &RepoContainsFilePredicate{Content: "test"}}, {`path and content`, `path:test.go content:abc`, &RepoContainsFilePredicate{Path: "test.go", Content: "abc"}}, {`content and path`, `content:abc path:test.go`, &RepoContainsFilePredicate{Path: "test.go", Content: "abc"}}, + {`unnamed path`, `test.go`, &RepoContainsFilePredicate{Path: "test.go"}}, + {`unnamed path regex`, `test(a|b)*.go`, &RepoContainsFilePredicate{Path: "test(a|b)*.go"}}, } for _, tc := range valid { @@ -39,9 +41,9 @@ func TestRepoContainsFilePredicate(t *testing.T) { {`empty`, ``, nil}, {`negated path`, `-path:test`, nil}, {`negated content`, `-content:test`, nil}, - {`unsupported syntax`, `abc:test`, nil}, - {`unnamed content`, `test`, nil}, {`catch invalid content regexp`, `path:foo content:([)`, nil}, + {`unsupported syntax`, `content1 content2`, nil}, + {`invalid unnamed path`, `([)`, nil}, } for _, tc := range invalid { @@ -171,6 +173,55 @@ func TestRepoHasKVPPredicate(t *testing.T) { }) } +func TestRepoContainsPredicate(t *testing.T) { + t.Run("Unmarshal", func(t *testing.T) { + type test struct { + name string + params string + expected *RepoContainsPredicate + } + + valid := []test{ + {`path`, `file:test`, &RepoContainsPredicate{File: "test"}}, + {`path regex`, `file:test(a|b)*.go`, &RepoContainsPredicate{File: "test(a|b)*.go"}}, + {`content`, `content:test`, &RepoContainsPredicate{Content: "test"}}, + {`path and content`, `file:test.go content:abc`, &RepoContainsPredicate{File: "test.go", Content: "abc"}}, + {`content and path`, `content:abc file:test.go`, &RepoContainsPredicate{File: "test.go", Content: "abc"}}, + } + + for _, tc := range valid { + t.Run(tc.name, func(t *testing.T) { + p := &RepoContainsPredicate{} + err := p.Unmarshal(tc.params, false) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !reflect.DeepEqual(tc.expected, p) { + t.Fatalf("expected %#v, got %#v", tc.expected, p) + } + }) + } + + invalid := []test{ + {`empty`, ``, nil}, + {`negated path`, `-file:test`, nil}, + {`negated content`, `-content:test`, nil}, + {`catch invalid content regexp`, `file:foo content:([)`, nil}, + } + + for _, tc := range invalid { + t.Run(tc.name, func(t *testing.T) { + p := &RepoContainsPredicate{} + err := p.Unmarshal(tc.params, false) + if err == nil { + t.Fatal("expected error but got none") + } + }) + } + }) +} + func TestFileHasOwnerPredicate(t *testing.T) { t.Run("Unmarshal", func(t *testing.T) { type test struct { diff --git a/internal/search/query/types.go b/internal/search/query/types.go index f5dcd2f54337..9e2ff1c43a58 100644 --- a/internal/search/query/types.go +++ b/internal/search/query/types.go @@ -349,6 +349,7 @@ func (p Parameters) IncludeExcludeValues(field string) (include, exclude []strin // - repo:contains.file(path:foo content:bar) || repo:has.file(path:foo content:bar) // - repo:contains.path(foo) || repo:has.path(foo) // - repo:contains.content(c) || repo:has.content(c) +// - repo:contains(file:foo content:bar) // - repohasfile:f type RepoHasFileContentArgs struct { // At least one of these strings should be non-empty @@ -388,6 +389,14 @@ func (p Parameters) RepoHasFileContent() (res []RepoHasFileContentArgs) { }) }) + VisitTypedPredicate(nodes, func(pred *RepoContainsPredicate) { + res = append(res, RepoHasFileContentArgs{ + Path: pred.File, + Content: pred.Content, + Negated: pred.Negated, + }) + }) + return res } diff --git a/internal/search/query/visitor_test.go b/internal/search/query/visitor_test.go index f013ea795297..999bbbfe40c0 100644 --- a/internal/search/query/visitor_test.go +++ b/internal/search/query/visitor_test.go @@ -36,6 +36,9 @@ func TestVisitTypedPredicate(t *testing.T) { }, { "repo:test repo:has.file(path:test)", autogold.Want("one predicate", []*RepoContainsFilePredicate{{Path: "test"}}), + }, { + "repo:test repo:contains.file(test)", + autogold.Want("one predicate", []*RepoContainsFilePredicate{{Path: "test"}}), }} for _, tc := range cases { From b54fa464263d30bc4407aa68f8cdd6c2b435aa40 Mon Sep 17 00:00:00 2001 From: Erik Seliger Date: Fri, 27 Jan 2023 19:57:11 +0100 Subject: [PATCH 225/678] Propose teams GraphQL/DB schema and add a first cut of docs (#45817) This PR suggests an API schema, and how to use it via a small docs page, including a src-cli extension. We will add working examples for how to ingest data from GitHub and GitLab for reference implementations of customer internal systems. This aims to follow what has been described in RFC 772. Note that this PR doesn't yet address metadata on teams, this will be introduced in a separate PR. Teams will not be an enterprise extension, given how deeply integrated they have to become. Syncers and custom integration later on might be though. --- cmd/frontend/graphqlbackend/graphqlbackend.go | 4 + cmd/frontend/graphqlbackend/node.go | 5 + cmd/frontend/graphqlbackend/schema.graphql | 287 ++++++++++++++++++ cmd/frontend/graphqlbackend/teams.go | 102 +++++++ cmd/frontend/graphqlbackend/user.go | 8 + doc/admin/teams/teams.md | 63 ++++ 6 files changed, 469 insertions(+) create mode 100644 cmd/frontend/graphqlbackend/teams.go create mode 100644 doc/admin/teams/teams.md diff --git a/cmd/frontend/graphqlbackend/graphqlbackend.go b/cmd/frontend/graphqlbackend/graphqlbackend.go index b9d975c95c48..18e0ee127e0e 100644 --- a/cmd/frontend/graphqlbackend/graphqlbackend.go +++ b/cmd/frontend/graphqlbackend/graphqlbackend.go @@ -865,3 +865,7 @@ func (r *schemaResolver) CodeHostSyncDue(ctx context.Context, args *struct { } return r.db.ExternalServices().SyncDue(ctx, ids, time.Duration(args.Seconds)*time.Second) } + +func (r *schemaResolver) Teams(ctx context.Context, args *ListTeamsArgs) (*teamConnectionResolver, error) { + return &teamConnectionResolver{}, nil +} diff --git a/cmd/frontend/graphqlbackend/node.go b/cmd/frontend/graphqlbackend/node.go index 2ce1991e9dfb..cc0f1c4c5cb6 100644 --- a/cmd/frontend/graphqlbackend/node.go +++ b/cmd/frontend/graphqlbackend/node.go @@ -328,3 +328,8 @@ func (r *NodeResolver) ToOutboundWebhook() (OutboundWebhookResolver, bool) { n, ok := r.Node.(OutboundWebhookResolver) return n, ok } + +func (r *NodeResolver) ToTeam() (*teamResolver, bool) { + n, ok := r.Node.(*teamResolver) + return n, ok +} diff --git a/cmd/frontend/graphqlbackend/schema.graphql b/cmd/frontend/graphqlbackend/schema.graphql index bcd5bf5ddd5d..8a2f2a055839 100755 --- a/cmd/frontend/graphqlbackend/schema.graphql +++ b/cmd/frontend/graphqlbackend/schema.graphql @@ -8937,3 +8937,290 @@ type ConnectionPageInfo { """ hasPreviousPage: Boolean! } + +""" +A team is a grouping of users/persons into a common handle. Teams are commonly used to define +codeowners. +""" +type Team implements Node { + """ + The unique ID of the team. + """ + id: ID! + + """ + The name of the team. Needs to be globally unique across usernames, organization + names, and team names. Team names can use alphanumeric characters, - dash + and / forward slash. + """ + name: String! + + """ + URL to link to the teams profile page. + """ + url: String! + + """ + A human readable name substitute for the name. Null, if not defined. + """ + displayName: String + + """ + A team can be made read-only from the CLI instructing the UI to show a warning + banner that this is managed externally, and management features will only be + available to site-admins. It can also still be manipulated from the CLI. + """ + readonly: Boolean! + + """ + The teams direct members. That is members that are strictly part of this team, + but not members of child teams. Team membership is NOT inherited. + """ + members( + """ + Returns the first n team members from the list. + """ + first: Int + + """ + Opaque pagination cursor. + """ + after: String + + """ + Optionally apply a text search filter over the results. + """ + search: String + ): TeamMemberConnection! + + """ + Parent team can be null, if this is a root team. + """ + parentTeam: Team + + """ + The list of direct child teams. + """ + childTeams( + """ + Returns the first n teams from the list. + """ + first: Int + + """ + Opaque pagination cursor. + """ + after: String + + """ + Optionally apply a text search filter over the results. + """ + search: String + ): TeamConnection! + + """ + True, if the current user can modify this team. + """ + viewerCanAdminister: Boolean! +} + +""" +A list of teams. +""" +type TeamConnection { + """ + The total count of items in the connection. + """ + totalCount( + """ + If true, the total count of teams, including deeply nested child teams will + be returned. Note that those teams will NOT be part of the nodes. + """ + countDeeplyNestedTeams: Boolean = false + ): Int! + + """ + The pagination info for the connection. + """ + pageInfo: PageInfo! + + """ + The current page of teams in this connection. + """ + nodes: [Team!]! +} + +""" +A list of team members. +""" +type TeamMemberConnection { + """ + The total count of items in the connection. + """ + totalCount( + """ + If true, the total count of members of this team, including deeply nested + child teams members will be returned. Note that child team members will NOT + be part of the nodes. + """ + countDeeplyNestedTeamMembers: Boolean = false + ): Int! + + """ + The pagination info for the connection. + """ + pageInfo: PageInfo! + + """ + The current page of team members in this connection. + """ + nodes: [TeamMember!]! +} + +""" +A team member is an entity that can be associated to a team. + +For now, this will be User, and will be expanded to User | Person later. +""" +interface TeamMember { + """ + All the teams this TeamMember is a direct member of. + """ + teams( + """ + Returns the first n teams from the list. + """ + first: Int + + """ + Opaque pagination cursor. + """ + after: String + + """ + Optionally apply a text search filter over the results. + """ + search: String + ): TeamConnection! +} + +extend type User implements TeamMember { + """ + All the teams this user is a direct member of. + """ + teams( + """ + Returns the first n teams from the list. + """ + first: Int + + """ + Opaque pagination cursor. + """ + after: String + + """ + Optionally apply a text search filter over the results. + """ + search: String + ): TeamConnection! +} + +extend type Query { + """ + Get the global list of all root teams. (Those without a parent team). + """ + teams( + """ + Returns the first n teams from the list. + """ + first: Int + + """ + Opaque pagination cursor. + """ + after: String + """ + Search can be used to do a text-search over the team names. + """ + search: String + ): TeamConnection! +} + +extend type Mutation { + """ + Creates a team. The name must be unique, display name can be used to set a custom + display value for the team inside Sourcegraph. + + If readonly is true, the Sourcegraph UI will show a warning banner that this team + is managed externally, and it can only be modified by site-admins. + This is to prevent state drift from external systems that ingest team information into Sourcegraph. + Readonly can only be set by site-admins. + + Either parentTeam XOR parentTeamName can be specified to make the team a child + team of the given parent. Only members of the parent team or site-admis can create + a child team. + """ + createTeam( + name: String! + displayName: String + readonly: Boolean = false + parentTeam: ID + parentTeamName: String + ): Team! + + """ + Update an existing team. ID or Name must be specified, but not both. + + To unset the display name, pass an empty string. Null will make it ignore updates. + + Either parentTeam XOR parentTeamName can be specified to make the team a child + team of the given parent. + The user has to be a team-member of both the child and parent team for that, and + neither can be read-only. Site-admin can modify all teams without constraints. + """ + updateTeam(id: ID, name: String, displayName: String, parentTeam: ID, parentTeamName: String): Team! + + """ + Delete team deletes a team. ID or Name must be specified, but not both. + Must be team-member to delete. If the team is marked as read-only, must be site-admin. + """ + deleteTeam(id: ID, name: String): EmptyResponse + + """ + Add a list of team members to an existing team. + People that already are part of the team are ignored. + + Either team XOR teamName can be specified to specify the team. + Must be team member to add new team members, or site-admin. + + For now, members can only be the IDs of User entities in Sourcegraph. + Later, we will expand this to allow Persons as well. + """ + addTeamMembers(team: ID, teamName: String, members: [ID!]!): Team! + + """ + This is a convenience method to forcefully overwrite the full set of members + of a team. This is handy to sync external state without diffing the current + members vs the desired set of members. + + Either team XOR teamName can be specified to specify the team. + Must be team member to modify team members, or site-admin. + + For now, members can only be the IDs of User entities in Sourcegraph. + Later, we will expand this to allow Persons as well. + """ + setTeamMembers(team: ID, teamName: String, members: [ID!]!): Team! + + """ + This mutation removes team membership for the given team and set of members. + Members that weren't part of the team are ignored. + + Either team XOR teamName can be specified to specify the team. + Must be team member to remove team members, or site-admin. + + For now, members can only be the IDs of User entities in Sourcegraph. + Later, we will expand this to allow Persons as well. + """ + removeTeamMembers(team: ID, teamName: String, members: [ID!]!): Team! +} diff --git a/cmd/frontend/graphqlbackend/teams.go b/cmd/frontend/graphqlbackend/teams.go new file mode 100644 index 000000000000..32af2a3bf0e8 --- /dev/null +++ b/cmd/frontend/graphqlbackend/teams.go @@ -0,0 +1,102 @@ +package graphqlbackend + +import ( + "github.com/graph-gophers/graphql-go" + + "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil" +) + +type ListTeamsArgs struct { + First *int32 + After *string + Search *string +} + +type teamConnectionResolver struct{} + +func (r *teamConnectionResolver) TotalCount(args *struct{ CountDeeplyNestedTeams bool }) int32 { + return 0 +} +func (r *teamConnectionResolver) PageInfo() *graphqlutil.PageInfo { + return graphqlutil.HasNextPage(false) +} +func (r *teamConnectionResolver) Nodes() []*teamResolver { return nil } + +type teamResolver struct{} + +func (r *teamResolver) ID() graphql.ID { return "" } +func (r *teamResolver) Name() string { return "" } +func (r *teamResolver) URL() string { return "" } +func (r *teamResolver) DisplayName() *string { return nil } +func (r *teamResolver) Readonly() bool { return false } +func (r *teamResolver) ParentTeam() *teamResolver { return nil } +func (r *teamResolver) ViewerCanAdminister() bool { return false } +func (r *teamResolver) Members(args *ListTeamsArgs) *teamMemberConnection { + return &teamMemberConnection{} +} +func (r *teamResolver) ChildTeams(args *ListTeamsArgs) *teamConnectionResolver { + return &teamConnectionResolver{} +} + +type teamMemberConnection struct{} + +func (r *teamMemberConnection) TotalCount(args *struct{ CountDeeplyNestedTeamMembers bool }) int32 { + return 0 +} +func (r *teamMemberConnection) PageInfo() *graphqlutil.PageInfo { + return graphqlutil.HasNextPage(false) +} +func (r *teamMemberConnection) Nodes() []*UserResolver { + return nil +} + +type CreateTeamArgs struct { + Name string + DisplayName *string + ReadOnly bool + ParentTeam *graphql.ID + ParentTeamName *string +} + +func (r *schemaResolver) CreateTeam(args *CreateTeamArgs) *teamResolver { + return &teamResolver{} +} + +type UpdateTeamArgs struct { + ID *graphql.ID + Name *string + DisplayName *string + ParentTeam *graphql.ID + ParentTeamName *string +} + +func (r *schemaResolver) UpdateTeam(args *UpdateTeamArgs) *teamResolver { + return &teamResolver{} +} + +type DeleteTeamArgs struct { + ID *graphql.ID + Name *string +} + +func (r *schemaResolver) DeleteTeam(args *DeleteTeamArgs) *EmptyResponse { + return &EmptyResponse{} +} + +type TeamMembersArgs struct { + Team *graphql.ID + TeamName *string + Members []graphql.ID +} + +func (r *schemaResolver) AddTeamMembers(args *TeamMembersArgs) *teamResolver { + return &teamResolver{} +} + +func (r *schemaResolver) SetTeamMembers(args *TeamMembersArgs) *teamResolver { + return &teamResolver{} +} + +func (r *schemaResolver) RemoveTeamMembers(args *TeamMembersArgs) *teamResolver { + return &teamResolver{} +} diff --git a/cmd/frontend/graphqlbackend/user.go b/cmd/frontend/graphqlbackend/user.go index 59fa9841067b..a1bf20ce405d 100644 --- a/cmd/frontend/graphqlbackend/user.go +++ b/cmd/frontend/graphqlbackend/user.go @@ -512,3 +512,11 @@ func (r *UserResolver) Monitors(ctx context.Context, args *ListMonitorsArgs) (Mo } return EnterpriseResolvers.codeMonitorsResolver.Monitors(ctx, r.user.ID, args) } + +func (r *UserResolver) Teams(ctx context.Context, args *ListTeamsArgs) (*teamConnectionResolver, error) { + return &teamConnectionResolver{}, nil +} + +func (r *UserResolver) ToUser() (*UserResolver, bool) { + return r, true +} diff --git a/doc/admin/teams/teams.md b/doc/admin/teams/teams.md new file mode 100644 index 000000000000..7acd9f4c45cc --- /dev/null +++ b/doc/admin/teams/teams.md @@ -0,0 +1,63 @@ +# Modeling teams in Sourcegraph + +To model your internal team structure in Sourcegraph, you can utilize Sourcegraph teams. Teams are groupings of users into a common handle. Teams are structured as a tree, so teams can have child teams. + +Example: + +``` +engineering +├─ security +├─ code graph +│ ├─ Batch Changes +│ ├─ Code Insights +├─ source +│ ├─ Repo Management +│ ├─ IAM +product +``` + +Teams in Sourcegraph will be usable in Sourcegraph Own [other features in the future]. Teams can be code owners and will influence the Own experience. You can search for code owned by a specific team, and in the future advanced ownership analytics will be informed by given team structures. [TODO: Link to Own docs](./teams.md) + +## Configuring teams + +Teams can either be defined directly in Sourcegraph by hand, or be ingested from external systems into Sourcegraph using [src-cli](https://github.com/sourcegraph/src-cli). + +### From the UI + +Go to **site-admin>Teams**. On this page, click "Create a new team". The team has to at least be a unique name and can optionally take a display name. Additionally, you can define a teams parent team to build a tree structure as outlined above. + +After hitting create, you will be redirected to the team page where you can add Sourcegraph users as team members. + +> NOTE: Teams defined from src-cli using the `-readonly` flag cannot be modified from the UI to prevent state drift from external systems ingesting the data. + +### From the CLI + +If you prefer a command line based approach, or would like to integrate an external system of record for teams into Sourcegraph, [src-cli](https://github.com/sourcegraph/src-cli) provides commands to manage teams as well: + +``` +src teams create [-displayName=] [-readonly] +src teams delete +src teams list [-search=] +src teams add-member [...] +# Forcefully overwrites all members of a given team. +src teams set-members [...] +src teams remove-member [...] +``` + +## Common integrations + +### GitHub teams + +Using the GitHub CLI, you can ingest teams data from GitHub into Sourcegraph. You may want to run this process regularly. + +``` +TODO: Script here that scrapes the GitHub API for teams and converts them into Sourcegraph teams. +``` + +### GitLab teams + +Using the GitLab API, you can ingest teams data from GitLab into Sourcegraph. You may want to run this process regularly. + +``` +TODO: Script here that scrapes the GitLab API for teams and converts them into Sourcegraph teams. +``` From 7b0c74ce121ca92bae4d9c35b1753f8d52aa6fed Mon Sep 17 00:00:00 2001 From: Erik Seliger Date: Fri, 27 Jan 2023 20:24:15 +0100 Subject: [PATCH 226/678] Implement database stores for teams (#46936) This PR implements the first set of database methods we're sure we're going to need to implement the resolver layer for teams, and the relevant database schema changes. I've implemented the unique constraint across users, teams and orgs from the RFC as well. --- enterprise/internal/database/mocks_temp.go | 114 ++ internal/database/database.go | 5 + internal/database/mocks_temp.go | 1736 +++++++++++++++++ internal/database/orgs.go | 4 +- internal/database/schema.json | 288 ++- internal/database/schema.md | 51 +- internal/database/teams.go | 628 ++++++ internal/database/teams_test.go | 447 +++++ internal/types/types.go | 18 + migrations/frontend/1671463799_teams/down.sql | 9 + .../frontend/1671463799_teams/metadata.yaml | 2 + migrations/frontend/1671463799_teams/up.sql | 25 + migrations/frontend/squashed.sql | 59 +- mockgen.temp.yaml | 1 + 14 files changed, 3382 insertions(+), 5 deletions(-) create mode 100644 internal/database/teams.go create mode 100644 internal/database/teams_test.go create mode 100644 migrations/frontend/1671463799_teams/down.sql create mode 100644 migrations/frontend/1671463799_teams/metadata.yaml create mode 100644 migrations/frontend/1671463799_teams/up.sql diff --git a/enterprise/internal/database/mocks_temp.go b/enterprise/internal/database/mocks_temp.go index b6502d96cb4e..450014ba8d51 100644 --- a/enterprise/internal/database/mocks_temp.go +++ b/enterprise/internal/database/mocks_temp.go @@ -6937,6 +6937,9 @@ type MockEnterpriseDB struct { // SubRepoPermsFunc is an instance of a mock function object controlling // the behavior of the method SubRepoPerms. SubRepoPermsFunc *EnterpriseDBSubRepoPermsFunc + // TeamsFunc is an instance of a mock function object controlling the + // behavior of the method Teams. + TeamsFunc *EnterpriseDBTeamsFunc // TemporarySettingsFunc is an instance of a mock function object // controlling the behavior of the method TemporarySettings. TemporarySettingsFunc *EnterpriseDBTemporarySettingsFunc @@ -7181,6 +7184,11 @@ func NewMockEnterpriseDB() *MockEnterpriseDB { return }, }, + TeamsFunc: &EnterpriseDBTeamsFunc{ + defaultHook: func() (r0 database.TeamStore) { + return + }, + }, TemporarySettingsFunc: &EnterpriseDBTemporarySettingsFunc{ defaultHook: func() (r0 database.TemporarySettingsStore) { return @@ -7448,6 +7456,11 @@ func NewStrictMockEnterpriseDB() *MockEnterpriseDB { panic("unexpected invocation of MockEnterpriseDB.SubRepoPerms") }, }, + TeamsFunc: &EnterpriseDBTeamsFunc{ + defaultHook: func() database.TeamStore { + panic("unexpected invocation of MockEnterpriseDB.Teams") + }, + }, TemporarySettingsFunc: &EnterpriseDBTemporarySettingsFunc{ defaultHook: func() database.TemporarySettingsStore { panic("unexpected invocation of MockEnterpriseDB.TemporarySettings") @@ -7634,6 +7647,9 @@ func NewMockEnterpriseDBFrom(i EnterpriseDB) *MockEnterpriseDB { SubRepoPermsFunc: &EnterpriseDBSubRepoPermsFunc{ defaultHook: i.SubRepoPerms, }, + TeamsFunc: &EnterpriseDBTeamsFunc{ + defaultHook: i.Teams, + }, TemporarySettingsFunc: &EnterpriseDBTemporarySettingsFunc{ defaultHook: i.TemporarySettings, }, @@ -11816,6 +11832,104 @@ func (c EnterpriseDBSubRepoPermsFuncCall) Results() []interface{} { return []interface{}{c.Result0} } +// EnterpriseDBTeamsFunc describes the behavior when the Teams method of the +// parent MockEnterpriseDB instance is invoked. +type EnterpriseDBTeamsFunc struct { + defaultHook func() database.TeamStore + hooks []func() database.TeamStore + history []EnterpriseDBTeamsFuncCall + mutex sync.Mutex +} + +// Teams delegates to the next hook function in the queue and stores the +// parameter and result values of this invocation. +func (m *MockEnterpriseDB) Teams() database.TeamStore { + r0 := m.TeamsFunc.nextHook()() + m.TeamsFunc.appendCall(EnterpriseDBTeamsFuncCall{r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the Teams method of the +// parent MockEnterpriseDB instance is invoked and the hook queue is empty. +func (f *EnterpriseDBTeamsFunc) SetDefaultHook(hook func() database.TeamStore) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// Teams method of the parent MockEnterpriseDB instance invokes the hook at +// the front of the queue and discards it. After the queue is empty, the +// default hook function is invoked for any future action. +func (f *EnterpriseDBTeamsFunc) PushHook(hook func() database.TeamStore) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *EnterpriseDBTeamsFunc) SetDefaultReturn(r0 database.TeamStore) { + f.SetDefaultHook(func() database.TeamStore { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *EnterpriseDBTeamsFunc) PushReturn(r0 database.TeamStore) { + f.PushHook(func() database.TeamStore { + return r0 + }) +} + +func (f *EnterpriseDBTeamsFunc) nextHook() func() database.TeamStore { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *EnterpriseDBTeamsFunc) appendCall(r0 EnterpriseDBTeamsFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of EnterpriseDBTeamsFuncCall objects +// describing the invocations of this function. +func (f *EnterpriseDBTeamsFunc) History() []EnterpriseDBTeamsFuncCall { + f.mutex.Lock() + history := make([]EnterpriseDBTeamsFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// EnterpriseDBTeamsFuncCall is an object that describes an invocation of +// method Teams on an instance of MockEnterpriseDB. +type EnterpriseDBTeamsFuncCall struct { + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 database.TeamStore +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c EnterpriseDBTeamsFuncCall) Args() []interface{} { + return []interface{}{} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c EnterpriseDBTeamsFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + // EnterpriseDBTemporarySettingsFunc describes the behavior when the // TemporarySettings method of the parent MockEnterpriseDB instance is // invoked. diff --git a/internal/database/database.go b/internal/database/database.go index d08fad201bac..f066fb930a35 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -62,6 +62,7 @@ type DB interface { ExecutorSecrets(encryption.Key) ExecutorSecretStore ExecutorSecretAccessLogs() ExecutorSecretAccessLogStore ZoektRepos() ZoektReposStore + Teams() TeamStore Transact(context.Context) (DB, error) WithTransact(context.Context, func(tx DB) error) error @@ -285,3 +286,7 @@ func (d *db) ExecutorSecretAccessLogs() ExecutorSecretAccessLogStore { func (d *db) ZoektRepos() ZoektReposStore { return ZoektReposWith(d.Store) } + +func (d *db) Teams() TeamStore { + return TeamsWith(d.Store) +} diff --git a/internal/database/mocks_temp.go b/internal/database/mocks_temp.go index 2f06a8b24516..5282a8116392 100644 --- a/internal/database/mocks_temp.go +++ b/internal/database/mocks_temp.go @@ -3982,6 +3982,9 @@ type MockDB struct { // SettingsFunc is an instance of a mock function object controlling the // behavior of the method Settings. SettingsFunc *DBSettingsFunc + // TeamsFunc is an instance of a mock function object controlling the + // behavior of the method Teams. + TeamsFunc *DBTeamsFunc // TemporarySettingsFunc is an instance of a mock function object // controlling the behavior of the method TemporarySettings. TemporarySettingsFunc *DBTemporarySettingsFunc @@ -4211,6 +4214,11 @@ func NewMockDB() *MockDB { return }, }, + TeamsFunc: &DBTeamsFunc{ + defaultHook: func() (r0 TeamStore) { + return + }, + }, TemporarySettingsFunc: &DBTemporarySettingsFunc{ defaultHook: func() (r0 TemporarySettingsStore) { return @@ -4463,6 +4471,11 @@ func NewStrictMockDB() *MockDB { panic("unexpected invocation of MockDB.Settings") }, }, + TeamsFunc: &DBTeamsFunc{ + defaultHook: func() TeamStore { + panic("unexpected invocation of MockDB.Teams") + }, + }, TemporarySettingsFunc: &DBTemporarySettingsFunc{ defaultHook: func() TemporarySettingsStore { panic("unexpected invocation of MockDB.TemporarySettings") @@ -4639,6 +4652,9 @@ func NewMockDBFrom(i DB) *MockDB { SettingsFunc: &DBSettingsFunc{ defaultHook: i.Settings, }, + TeamsFunc: &DBTeamsFunc{ + defaultHook: i.Teams, + }, TemporarySettingsFunc: &DBTemporarySettingsFunc{ defaultHook: i.TemporarySettings, }, @@ -8486,6 +8502,104 @@ func (c DBSettingsFuncCall) Results() []interface{} { return []interface{}{c.Result0} } +// DBTeamsFunc describes the behavior when the Teams method of the parent +// MockDB instance is invoked. +type DBTeamsFunc struct { + defaultHook func() TeamStore + hooks []func() TeamStore + history []DBTeamsFuncCall + mutex sync.Mutex +} + +// Teams delegates to the next hook function in the queue and stores the +// parameter and result values of this invocation. +func (m *MockDB) Teams() TeamStore { + r0 := m.TeamsFunc.nextHook()() + m.TeamsFunc.appendCall(DBTeamsFuncCall{r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the Teams method of the +// parent MockDB instance is invoked and the hook queue is empty. +func (f *DBTeamsFunc) SetDefaultHook(hook func() TeamStore) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// Teams method of the parent MockDB instance invokes the hook at the front +// of the queue and discards it. After the queue is empty, the default hook +// function is invoked for any future action. +func (f *DBTeamsFunc) PushHook(hook func() TeamStore) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *DBTeamsFunc) SetDefaultReturn(r0 TeamStore) { + f.SetDefaultHook(func() TeamStore { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *DBTeamsFunc) PushReturn(r0 TeamStore) { + f.PushHook(func() TeamStore { + return r0 + }) +} + +func (f *DBTeamsFunc) nextHook() func() TeamStore { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *DBTeamsFunc) appendCall(r0 DBTeamsFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of DBTeamsFuncCall objects describing the +// invocations of this function. +func (f *DBTeamsFunc) History() []DBTeamsFuncCall { + f.mutex.Lock() + history := make([]DBTeamsFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// DBTeamsFuncCall is an object that describes an invocation of method Teams +// on an instance of MockDB. +type DBTeamsFuncCall struct { + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 TeamStore +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c DBTeamsFuncCall) Args() []interface{} { + return []interface{}{} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c DBTeamsFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + // DBTemporarySettingsFunc describes the behavior when the TemporarySettings // method of the parent MockDB instance is invoked. type DBTemporarySettingsFunc struct { @@ -45965,6 +46079,1628 @@ func (c SettingsStoreWithFuncCall) Results() []interface{} { return []interface{}{c.Result0} } +// MockTeamStore is a mock implementation of the TeamStore interface (from +// the package github.com/sourcegraph/sourcegraph/internal/database) used +// for unit testing. +type MockTeamStore struct { + // CountTeamMembersFunc is an instance of a mock function object + // controlling the behavior of the method CountTeamMembers. + CountTeamMembersFunc *TeamStoreCountTeamMembersFunc + // CountTeamsFunc is an instance of a mock function object controlling + // the behavior of the method CountTeams. + CountTeamsFunc *TeamStoreCountTeamsFunc + // CreateTeamFunc is an instance of a mock function object controlling + // the behavior of the method CreateTeam. + CreateTeamFunc *TeamStoreCreateTeamFunc + // CreateTeamMemberFunc is an instance of a mock function object + // controlling the behavior of the method CreateTeamMember. + CreateTeamMemberFunc *TeamStoreCreateTeamMemberFunc + // DeleteTeamFunc is an instance of a mock function object controlling + // the behavior of the method DeleteTeam. + DeleteTeamFunc *TeamStoreDeleteTeamFunc + // DeleteTeamMemberFunc is an instance of a mock function object + // controlling the behavior of the method DeleteTeamMember. + DeleteTeamMemberFunc *TeamStoreDeleteTeamMemberFunc + // DoneFunc is an instance of a mock function object controlling the + // behavior of the method Done. + DoneFunc *TeamStoreDoneFunc + // GetTeamByIDFunc is an instance of a mock function object controlling + // the behavior of the method GetTeamByID. + GetTeamByIDFunc *TeamStoreGetTeamByIDFunc + // GetTeamByNameFunc is an instance of a mock function object + // controlling the behavior of the method GetTeamByName. + GetTeamByNameFunc *TeamStoreGetTeamByNameFunc + // HandleFunc is an instance of a mock function object controlling the + // behavior of the method Handle. + HandleFunc *TeamStoreHandleFunc + // ListTeamMembersFunc is an instance of a mock function object + // controlling the behavior of the method ListTeamMembers. + ListTeamMembersFunc *TeamStoreListTeamMembersFunc + // ListTeamsFunc is an instance of a mock function object controlling + // the behavior of the method ListTeams. + ListTeamsFunc *TeamStoreListTeamsFunc + // UpdateTeamFunc is an instance of a mock function object controlling + // the behavior of the method UpdateTeam. + UpdateTeamFunc *TeamStoreUpdateTeamFunc +} + +// NewMockTeamStore creates a new mock of the TeamStore interface. All +// methods return zero values for all results, unless overwritten. +func NewMockTeamStore() *MockTeamStore { + return &MockTeamStore{ + CountTeamMembersFunc: &TeamStoreCountTeamMembersFunc{ + defaultHook: func(context.Context, ListTeamMembersOpts) (r0 int32, r1 error) { + return + }, + }, + CountTeamsFunc: &TeamStoreCountTeamsFunc{ + defaultHook: func(context.Context, ListTeamsOpts) (r0 int32, r1 error) { + return + }, + }, + CreateTeamFunc: &TeamStoreCreateTeamFunc{ + defaultHook: func(context.Context, *types.Team) (r0 error) { + return + }, + }, + CreateTeamMemberFunc: &TeamStoreCreateTeamMemberFunc{ + defaultHook: func(context.Context, ...*types.TeamMember) (r0 error) { + return + }, + }, + DeleteTeamFunc: &TeamStoreDeleteTeamFunc{ + defaultHook: func(context.Context, int32) (r0 error) { + return + }, + }, + DeleteTeamMemberFunc: &TeamStoreDeleteTeamMemberFunc{ + defaultHook: func(context.Context, ...*types.TeamMember) (r0 error) { + return + }, + }, + DoneFunc: &TeamStoreDoneFunc{ + defaultHook: func(error) (r0 error) { + return + }, + }, + GetTeamByIDFunc: &TeamStoreGetTeamByIDFunc{ + defaultHook: func(context.Context, int32) (r0 *types.Team, r1 error) { + return + }, + }, + GetTeamByNameFunc: &TeamStoreGetTeamByNameFunc{ + defaultHook: func(context.Context, string) (r0 *types.Team, r1 error) { + return + }, + }, + HandleFunc: &TeamStoreHandleFunc{ + defaultHook: func() (r0 basestore.TransactableHandle) { + return + }, + }, + ListTeamMembersFunc: &TeamStoreListTeamMembersFunc{ + defaultHook: func(context.Context, ListTeamMembersOpts) (r0 []*types.TeamMember, r1 *TeamMemberListCursor, r2 error) { + return + }, + }, + ListTeamsFunc: &TeamStoreListTeamsFunc{ + defaultHook: func(context.Context, ListTeamsOpts) (r0 []*types.Team, r1 int32, r2 error) { + return + }, + }, + UpdateTeamFunc: &TeamStoreUpdateTeamFunc{ + defaultHook: func(context.Context, *types.Team) (r0 error) { + return + }, + }, + } +} + +// NewStrictMockTeamStore creates a new mock of the TeamStore interface. All +// methods panic on invocation, unless overwritten. +func NewStrictMockTeamStore() *MockTeamStore { + return &MockTeamStore{ + CountTeamMembersFunc: &TeamStoreCountTeamMembersFunc{ + defaultHook: func(context.Context, ListTeamMembersOpts) (int32, error) { + panic("unexpected invocation of MockTeamStore.CountTeamMembers") + }, + }, + CountTeamsFunc: &TeamStoreCountTeamsFunc{ + defaultHook: func(context.Context, ListTeamsOpts) (int32, error) { + panic("unexpected invocation of MockTeamStore.CountTeams") + }, + }, + CreateTeamFunc: &TeamStoreCreateTeamFunc{ + defaultHook: func(context.Context, *types.Team) error { + panic("unexpected invocation of MockTeamStore.CreateTeam") + }, + }, + CreateTeamMemberFunc: &TeamStoreCreateTeamMemberFunc{ + defaultHook: func(context.Context, ...*types.TeamMember) error { + panic("unexpected invocation of MockTeamStore.CreateTeamMember") + }, + }, + DeleteTeamFunc: &TeamStoreDeleteTeamFunc{ + defaultHook: func(context.Context, int32) error { + panic("unexpected invocation of MockTeamStore.DeleteTeam") + }, + }, + DeleteTeamMemberFunc: &TeamStoreDeleteTeamMemberFunc{ + defaultHook: func(context.Context, ...*types.TeamMember) error { + panic("unexpected invocation of MockTeamStore.DeleteTeamMember") + }, + }, + DoneFunc: &TeamStoreDoneFunc{ + defaultHook: func(error) error { + panic("unexpected invocation of MockTeamStore.Done") + }, + }, + GetTeamByIDFunc: &TeamStoreGetTeamByIDFunc{ + defaultHook: func(context.Context, int32) (*types.Team, error) { + panic("unexpected invocation of MockTeamStore.GetTeamByID") + }, + }, + GetTeamByNameFunc: &TeamStoreGetTeamByNameFunc{ + defaultHook: func(context.Context, string) (*types.Team, error) { + panic("unexpected invocation of MockTeamStore.GetTeamByName") + }, + }, + HandleFunc: &TeamStoreHandleFunc{ + defaultHook: func() basestore.TransactableHandle { + panic("unexpected invocation of MockTeamStore.Handle") + }, + }, + ListTeamMembersFunc: &TeamStoreListTeamMembersFunc{ + defaultHook: func(context.Context, ListTeamMembersOpts) ([]*types.TeamMember, *TeamMemberListCursor, error) { + panic("unexpected invocation of MockTeamStore.ListTeamMembers") + }, + }, + ListTeamsFunc: &TeamStoreListTeamsFunc{ + defaultHook: func(context.Context, ListTeamsOpts) ([]*types.Team, int32, error) { + panic("unexpected invocation of MockTeamStore.ListTeams") + }, + }, + UpdateTeamFunc: &TeamStoreUpdateTeamFunc{ + defaultHook: func(context.Context, *types.Team) error { + panic("unexpected invocation of MockTeamStore.UpdateTeam") + }, + }, + } +} + +// NewMockTeamStoreFrom creates a new mock of the MockTeamStore interface. +// All methods delegate to the given implementation, unless overwritten. +func NewMockTeamStoreFrom(i TeamStore) *MockTeamStore { + return &MockTeamStore{ + CountTeamMembersFunc: &TeamStoreCountTeamMembersFunc{ + defaultHook: i.CountTeamMembers, + }, + CountTeamsFunc: &TeamStoreCountTeamsFunc{ + defaultHook: i.CountTeams, + }, + CreateTeamFunc: &TeamStoreCreateTeamFunc{ + defaultHook: i.CreateTeam, + }, + CreateTeamMemberFunc: &TeamStoreCreateTeamMemberFunc{ + defaultHook: i.CreateTeamMember, + }, + DeleteTeamFunc: &TeamStoreDeleteTeamFunc{ + defaultHook: i.DeleteTeam, + }, + DeleteTeamMemberFunc: &TeamStoreDeleteTeamMemberFunc{ + defaultHook: i.DeleteTeamMember, + }, + DoneFunc: &TeamStoreDoneFunc{ + defaultHook: i.Done, + }, + GetTeamByIDFunc: &TeamStoreGetTeamByIDFunc{ + defaultHook: i.GetTeamByID, + }, + GetTeamByNameFunc: &TeamStoreGetTeamByNameFunc{ + defaultHook: i.GetTeamByName, + }, + HandleFunc: &TeamStoreHandleFunc{ + defaultHook: i.Handle, + }, + ListTeamMembersFunc: &TeamStoreListTeamMembersFunc{ + defaultHook: i.ListTeamMembers, + }, + ListTeamsFunc: &TeamStoreListTeamsFunc{ + defaultHook: i.ListTeams, + }, + UpdateTeamFunc: &TeamStoreUpdateTeamFunc{ + defaultHook: i.UpdateTeam, + }, + } +} + +// TeamStoreCountTeamMembersFunc describes the behavior when the +// CountTeamMembers method of the parent MockTeamStore instance is invoked. +type TeamStoreCountTeamMembersFunc struct { + defaultHook func(context.Context, ListTeamMembersOpts) (int32, error) + hooks []func(context.Context, ListTeamMembersOpts) (int32, error) + history []TeamStoreCountTeamMembersFuncCall + mutex sync.Mutex +} + +// CountTeamMembers delegates to the next hook function in the queue and +// stores the parameter and result values of this invocation. +func (m *MockTeamStore) CountTeamMembers(v0 context.Context, v1 ListTeamMembersOpts) (int32, error) { + r0, r1 := m.CountTeamMembersFunc.nextHook()(v0, v1) + m.CountTeamMembersFunc.appendCall(TeamStoreCountTeamMembersFuncCall{v0, v1, r0, r1}) + return r0, r1 +} + +// SetDefaultHook sets function that is called when the CountTeamMembers +// method of the parent MockTeamStore instance is invoked and the hook queue +// is empty. +func (f *TeamStoreCountTeamMembersFunc) SetDefaultHook(hook func(context.Context, ListTeamMembersOpts) (int32, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// CountTeamMembers method of the parent MockTeamStore instance invokes the +// hook at the front of the queue and discards it. After the queue is empty, +// the default hook function is invoked for any future action. +func (f *TeamStoreCountTeamMembersFunc) PushHook(hook func(context.Context, ListTeamMembersOpts) (int32, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *TeamStoreCountTeamMembersFunc) SetDefaultReturn(r0 int32, r1 error) { + f.SetDefaultHook(func(context.Context, ListTeamMembersOpts) (int32, error) { + return r0, r1 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *TeamStoreCountTeamMembersFunc) PushReturn(r0 int32, r1 error) { + f.PushHook(func(context.Context, ListTeamMembersOpts) (int32, error) { + return r0, r1 + }) +} + +func (f *TeamStoreCountTeamMembersFunc) nextHook() func(context.Context, ListTeamMembersOpts) (int32, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *TeamStoreCountTeamMembersFunc) appendCall(r0 TeamStoreCountTeamMembersFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of TeamStoreCountTeamMembersFuncCall objects +// describing the invocations of this function. +func (f *TeamStoreCountTeamMembersFunc) History() []TeamStoreCountTeamMembersFuncCall { + f.mutex.Lock() + history := make([]TeamStoreCountTeamMembersFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// TeamStoreCountTeamMembersFuncCall is an object that describes an +// invocation of method CountTeamMembers on an instance of MockTeamStore. +type TeamStoreCountTeamMembersFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 ListTeamMembersOpts + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 int32 + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c TeamStoreCountTeamMembersFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c TeamStoreCountTeamMembersFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1} +} + +// TeamStoreCountTeamsFunc describes the behavior when the CountTeams method +// of the parent MockTeamStore instance is invoked. +type TeamStoreCountTeamsFunc struct { + defaultHook func(context.Context, ListTeamsOpts) (int32, error) + hooks []func(context.Context, ListTeamsOpts) (int32, error) + history []TeamStoreCountTeamsFuncCall + mutex sync.Mutex +} + +// CountTeams delegates to the next hook function in the queue and stores +// the parameter and result values of this invocation. +func (m *MockTeamStore) CountTeams(v0 context.Context, v1 ListTeamsOpts) (int32, error) { + r0, r1 := m.CountTeamsFunc.nextHook()(v0, v1) + m.CountTeamsFunc.appendCall(TeamStoreCountTeamsFuncCall{v0, v1, r0, r1}) + return r0, r1 +} + +// SetDefaultHook sets function that is called when the CountTeams method of +// the parent MockTeamStore instance is invoked and the hook queue is empty. +func (f *TeamStoreCountTeamsFunc) SetDefaultHook(hook func(context.Context, ListTeamsOpts) (int32, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// CountTeams method of the parent MockTeamStore instance invokes the hook +// at the front of the queue and discards it. After the queue is empty, the +// default hook function is invoked for any future action. +func (f *TeamStoreCountTeamsFunc) PushHook(hook func(context.Context, ListTeamsOpts) (int32, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *TeamStoreCountTeamsFunc) SetDefaultReturn(r0 int32, r1 error) { + f.SetDefaultHook(func(context.Context, ListTeamsOpts) (int32, error) { + return r0, r1 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *TeamStoreCountTeamsFunc) PushReturn(r0 int32, r1 error) { + f.PushHook(func(context.Context, ListTeamsOpts) (int32, error) { + return r0, r1 + }) +} + +func (f *TeamStoreCountTeamsFunc) nextHook() func(context.Context, ListTeamsOpts) (int32, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *TeamStoreCountTeamsFunc) appendCall(r0 TeamStoreCountTeamsFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of TeamStoreCountTeamsFuncCall objects +// describing the invocations of this function. +func (f *TeamStoreCountTeamsFunc) History() []TeamStoreCountTeamsFuncCall { + f.mutex.Lock() + history := make([]TeamStoreCountTeamsFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// TeamStoreCountTeamsFuncCall is an object that describes an invocation of +// method CountTeams on an instance of MockTeamStore. +type TeamStoreCountTeamsFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 ListTeamsOpts + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 int32 + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c TeamStoreCountTeamsFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c TeamStoreCountTeamsFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1} +} + +// TeamStoreCreateTeamFunc describes the behavior when the CreateTeam method +// of the parent MockTeamStore instance is invoked. +type TeamStoreCreateTeamFunc struct { + defaultHook func(context.Context, *types.Team) error + hooks []func(context.Context, *types.Team) error + history []TeamStoreCreateTeamFuncCall + mutex sync.Mutex +} + +// CreateTeam delegates to the next hook function in the queue and stores +// the parameter and result values of this invocation. +func (m *MockTeamStore) CreateTeam(v0 context.Context, v1 *types.Team) error { + r0 := m.CreateTeamFunc.nextHook()(v0, v1) + m.CreateTeamFunc.appendCall(TeamStoreCreateTeamFuncCall{v0, v1, r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the CreateTeam method of +// the parent MockTeamStore instance is invoked and the hook queue is empty. +func (f *TeamStoreCreateTeamFunc) SetDefaultHook(hook func(context.Context, *types.Team) error) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// CreateTeam method of the parent MockTeamStore instance invokes the hook +// at the front of the queue and discards it. After the queue is empty, the +// default hook function is invoked for any future action. +func (f *TeamStoreCreateTeamFunc) PushHook(hook func(context.Context, *types.Team) error) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *TeamStoreCreateTeamFunc) SetDefaultReturn(r0 error) { + f.SetDefaultHook(func(context.Context, *types.Team) error { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *TeamStoreCreateTeamFunc) PushReturn(r0 error) { + f.PushHook(func(context.Context, *types.Team) error { + return r0 + }) +} + +func (f *TeamStoreCreateTeamFunc) nextHook() func(context.Context, *types.Team) error { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *TeamStoreCreateTeamFunc) appendCall(r0 TeamStoreCreateTeamFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of TeamStoreCreateTeamFuncCall objects +// describing the invocations of this function. +func (f *TeamStoreCreateTeamFunc) History() []TeamStoreCreateTeamFuncCall { + f.mutex.Lock() + history := make([]TeamStoreCreateTeamFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// TeamStoreCreateTeamFuncCall is an object that describes an invocation of +// method CreateTeam on an instance of MockTeamStore. +type TeamStoreCreateTeamFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 *types.Team + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c TeamStoreCreateTeamFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c TeamStoreCreateTeamFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + +// TeamStoreCreateTeamMemberFunc describes the behavior when the +// CreateTeamMember method of the parent MockTeamStore instance is invoked. +type TeamStoreCreateTeamMemberFunc struct { + defaultHook func(context.Context, ...*types.TeamMember) error + hooks []func(context.Context, ...*types.TeamMember) error + history []TeamStoreCreateTeamMemberFuncCall + mutex sync.Mutex +} + +// CreateTeamMember delegates to the next hook function in the queue and +// stores the parameter and result values of this invocation. +func (m *MockTeamStore) CreateTeamMember(v0 context.Context, v1 ...*types.TeamMember) error { + r0 := m.CreateTeamMemberFunc.nextHook()(v0, v1...) + m.CreateTeamMemberFunc.appendCall(TeamStoreCreateTeamMemberFuncCall{v0, v1, r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the CreateTeamMember +// method of the parent MockTeamStore instance is invoked and the hook queue +// is empty. +func (f *TeamStoreCreateTeamMemberFunc) SetDefaultHook(hook func(context.Context, ...*types.TeamMember) error) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// CreateTeamMember method of the parent MockTeamStore instance invokes the +// hook at the front of the queue and discards it. After the queue is empty, +// the default hook function is invoked for any future action. +func (f *TeamStoreCreateTeamMemberFunc) PushHook(hook func(context.Context, ...*types.TeamMember) error) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *TeamStoreCreateTeamMemberFunc) SetDefaultReturn(r0 error) { + f.SetDefaultHook(func(context.Context, ...*types.TeamMember) error { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *TeamStoreCreateTeamMemberFunc) PushReturn(r0 error) { + f.PushHook(func(context.Context, ...*types.TeamMember) error { + return r0 + }) +} + +func (f *TeamStoreCreateTeamMemberFunc) nextHook() func(context.Context, ...*types.TeamMember) error { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *TeamStoreCreateTeamMemberFunc) appendCall(r0 TeamStoreCreateTeamMemberFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of TeamStoreCreateTeamMemberFuncCall objects +// describing the invocations of this function. +func (f *TeamStoreCreateTeamMemberFunc) History() []TeamStoreCreateTeamMemberFuncCall { + f.mutex.Lock() + history := make([]TeamStoreCreateTeamMemberFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// TeamStoreCreateTeamMemberFuncCall is an object that describes an +// invocation of method CreateTeamMember on an instance of MockTeamStore. +type TeamStoreCreateTeamMemberFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is a slice containing the values of the variadic arguments + // passed to this method invocation. + Arg1 []*types.TeamMember + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. The variadic slice argument is flattened in this array such +// that one positional argument and three variadic arguments would result in +// a slice of four, not two. +func (c TeamStoreCreateTeamMemberFuncCall) Args() []interface{} { + trailing := []interface{}{} + for _, val := range c.Arg1 { + trailing = append(trailing, val) + } + + return append([]interface{}{c.Arg0}, trailing...) +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c TeamStoreCreateTeamMemberFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + +// TeamStoreDeleteTeamFunc describes the behavior when the DeleteTeam method +// of the parent MockTeamStore instance is invoked. +type TeamStoreDeleteTeamFunc struct { + defaultHook func(context.Context, int32) error + hooks []func(context.Context, int32) error + history []TeamStoreDeleteTeamFuncCall + mutex sync.Mutex +} + +// DeleteTeam delegates to the next hook function in the queue and stores +// the parameter and result values of this invocation. +func (m *MockTeamStore) DeleteTeam(v0 context.Context, v1 int32) error { + r0 := m.DeleteTeamFunc.nextHook()(v0, v1) + m.DeleteTeamFunc.appendCall(TeamStoreDeleteTeamFuncCall{v0, v1, r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the DeleteTeam method of +// the parent MockTeamStore instance is invoked and the hook queue is empty. +func (f *TeamStoreDeleteTeamFunc) SetDefaultHook(hook func(context.Context, int32) error) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// DeleteTeam method of the parent MockTeamStore instance invokes the hook +// at the front of the queue and discards it. After the queue is empty, the +// default hook function is invoked for any future action. +func (f *TeamStoreDeleteTeamFunc) PushHook(hook func(context.Context, int32) error) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *TeamStoreDeleteTeamFunc) SetDefaultReturn(r0 error) { + f.SetDefaultHook(func(context.Context, int32) error { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *TeamStoreDeleteTeamFunc) PushReturn(r0 error) { + f.PushHook(func(context.Context, int32) error { + return r0 + }) +} + +func (f *TeamStoreDeleteTeamFunc) nextHook() func(context.Context, int32) error { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *TeamStoreDeleteTeamFunc) appendCall(r0 TeamStoreDeleteTeamFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of TeamStoreDeleteTeamFuncCall objects +// describing the invocations of this function. +func (f *TeamStoreDeleteTeamFunc) History() []TeamStoreDeleteTeamFuncCall { + f.mutex.Lock() + history := make([]TeamStoreDeleteTeamFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// TeamStoreDeleteTeamFuncCall is an object that describes an invocation of +// method DeleteTeam on an instance of MockTeamStore. +type TeamStoreDeleteTeamFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 int32 + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c TeamStoreDeleteTeamFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c TeamStoreDeleteTeamFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + +// TeamStoreDeleteTeamMemberFunc describes the behavior when the +// DeleteTeamMember method of the parent MockTeamStore instance is invoked. +type TeamStoreDeleteTeamMemberFunc struct { + defaultHook func(context.Context, ...*types.TeamMember) error + hooks []func(context.Context, ...*types.TeamMember) error + history []TeamStoreDeleteTeamMemberFuncCall + mutex sync.Mutex +} + +// DeleteTeamMember delegates to the next hook function in the queue and +// stores the parameter and result values of this invocation. +func (m *MockTeamStore) DeleteTeamMember(v0 context.Context, v1 ...*types.TeamMember) error { + r0 := m.DeleteTeamMemberFunc.nextHook()(v0, v1...) + m.DeleteTeamMemberFunc.appendCall(TeamStoreDeleteTeamMemberFuncCall{v0, v1, r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the DeleteTeamMember +// method of the parent MockTeamStore instance is invoked and the hook queue +// is empty. +func (f *TeamStoreDeleteTeamMemberFunc) SetDefaultHook(hook func(context.Context, ...*types.TeamMember) error) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// DeleteTeamMember method of the parent MockTeamStore instance invokes the +// hook at the front of the queue and discards it. After the queue is empty, +// the default hook function is invoked for any future action. +func (f *TeamStoreDeleteTeamMemberFunc) PushHook(hook func(context.Context, ...*types.TeamMember) error) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *TeamStoreDeleteTeamMemberFunc) SetDefaultReturn(r0 error) { + f.SetDefaultHook(func(context.Context, ...*types.TeamMember) error { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *TeamStoreDeleteTeamMemberFunc) PushReturn(r0 error) { + f.PushHook(func(context.Context, ...*types.TeamMember) error { + return r0 + }) +} + +func (f *TeamStoreDeleteTeamMemberFunc) nextHook() func(context.Context, ...*types.TeamMember) error { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *TeamStoreDeleteTeamMemberFunc) appendCall(r0 TeamStoreDeleteTeamMemberFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of TeamStoreDeleteTeamMemberFuncCall objects +// describing the invocations of this function. +func (f *TeamStoreDeleteTeamMemberFunc) History() []TeamStoreDeleteTeamMemberFuncCall { + f.mutex.Lock() + history := make([]TeamStoreDeleteTeamMemberFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// TeamStoreDeleteTeamMemberFuncCall is an object that describes an +// invocation of method DeleteTeamMember on an instance of MockTeamStore. +type TeamStoreDeleteTeamMemberFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is a slice containing the values of the variadic arguments + // passed to this method invocation. + Arg1 []*types.TeamMember + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. The variadic slice argument is flattened in this array such +// that one positional argument and three variadic arguments would result in +// a slice of four, not two. +func (c TeamStoreDeleteTeamMemberFuncCall) Args() []interface{} { + trailing := []interface{}{} + for _, val := range c.Arg1 { + trailing = append(trailing, val) + } + + return append([]interface{}{c.Arg0}, trailing...) +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c TeamStoreDeleteTeamMemberFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + +// TeamStoreDoneFunc describes the behavior when the Done method of the +// parent MockTeamStore instance is invoked. +type TeamStoreDoneFunc struct { + defaultHook func(error) error + hooks []func(error) error + history []TeamStoreDoneFuncCall + mutex sync.Mutex +} + +// Done delegates to the next hook function in the queue and stores the +// parameter and result values of this invocation. +func (m *MockTeamStore) Done(v0 error) error { + r0 := m.DoneFunc.nextHook()(v0) + m.DoneFunc.appendCall(TeamStoreDoneFuncCall{v0, r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the Done method of the +// parent MockTeamStore instance is invoked and the hook queue is empty. +func (f *TeamStoreDoneFunc) SetDefaultHook(hook func(error) error) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// Done method of the parent MockTeamStore instance invokes the hook at the +// front of the queue and discards it. After the queue is empty, the default +// hook function is invoked for any future action. +func (f *TeamStoreDoneFunc) PushHook(hook func(error) error) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *TeamStoreDoneFunc) SetDefaultReturn(r0 error) { + f.SetDefaultHook(func(error) error { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *TeamStoreDoneFunc) PushReturn(r0 error) { + f.PushHook(func(error) error { + return r0 + }) +} + +func (f *TeamStoreDoneFunc) nextHook() func(error) error { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *TeamStoreDoneFunc) appendCall(r0 TeamStoreDoneFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of TeamStoreDoneFuncCall objects describing +// the invocations of this function. +func (f *TeamStoreDoneFunc) History() []TeamStoreDoneFuncCall { + f.mutex.Lock() + history := make([]TeamStoreDoneFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// TeamStoreDoneFuncCall is an object that describes an invocation of method +// Done on an instance of MockTeamStore. +type TeamStoreDoneFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 error + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c TeamStoreDoneFuncCall) Args() []interface{} { + return []interface{}{c.Arg0} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c TeamStoreDoneFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + +// TeamStoreGetTeamByIDFunc describes the behavior when the GetTeamByID +// method of the parent MockTeamStore instance is invoked. +type TeamStoreGetTeamByIDFunc struct { + defaultHook func(context.Context, int32) (*types.Team, error) + hooks []func(context.Context, int32) (*types.Team, error) + history []TeamStoreGetTeamByIDFuncCall + mutex sync.Mutex +} + +// GetTeamByID delegates to the next hook function in the queue and stores +// the parameter and result values of this invocation. +func (m *MockTeamStore) GetTeamByID(v0 context.Context, v1 int32) (*types.Team, error) { + r0, r1 := m.GetTeamByIDFunc.nextHook()(v0, v1) + m.GetTeamByIDFunc.appendCall(TeamStoreGetTeamByIDFuncCall{v0, v1, r0, r1}) + return r0, r1 +} + +// SetDefaultHook sets function that is called when the GetTeamByID method +// of the parent MockTeamStore instance is invoked and the hook queue is +// empty. +func (f *TeamStoreGetTeamByIDFunc) SetDefaultHook(hook func(context.Context, int32) (*types.Team, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// GetTeamByID method of the parent MockTeamStore instance invokes the hook +// at the front of the queue and discards it. After the queue is empty, the +// default hook function is invoked for any future action. +func (f *TeamStoreGetTeamByIDFunc) PushHook(hook func(context.Context, int32) (*types.Team, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *TeamStoreGetTeamByIDFunc) SetDefaultReturn(r0 *types.Team, r1 error) { + f.SetDefaultHook(func(context.Context, int32) (*types.Team, error) { + return r0, r1 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *TeamStoreGetTeamByIDFunc) PushReturn(r0 *types.Team, r1 error) { + f.PushHook(func(context.Context, int32) (*types.Team, error) { + return r0, r1 + }) +} + +func (f *TeamStoreGetTeamByIDFunc) nextHook() func(context.Context, int32) (*types.Team, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *TeamStoreGetTeamByIDFunc) appendCall(r0 TeamStoreGetTeamByIDFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of TeamStoreGetTeamByIDFuncCall objects +// describing the invocations of this function. +func (f *TeamStoreGetTeamByIDFunc) History() []TeamStoreGetTeamByIDFuncCall { + f.mutex.Lock() + history := make([]TeamStoreGetTeamByIDFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// TeamStoreGetTeamByIDFuncCall is an object that describes an invocation of +// method GetTeamByID on an instance of MockTeamStore. +type TeamStoreGetTeamByIDFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 int32 + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 *types.Team + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c TeamStoreGetTeamByIDFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c TeamStoreGetTeamByIDFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1} +} + +// TeamStoreGetTeamByNameFunc describes the behavior when the GetTeamByName +// method of the parent MockTeamStore instance is invoked. +type TeamStoreGetTeamByNameFunc struct { + defaultHook func(context.Context, string) (*types.Team, error) + hooks []func(context.Context, string) (*types.Team, error) + history []TeamStoreGetTeamByNameFuncCall + mutex sync.Mutex +} + +// GetTeamByName delegates to the next hook function in the queue and stores +// the parameter and result values of this invocation. +func (m *MockTeamStore) GetTeamByName(v0 context.Context, v1 string) (*types.Team, error) { + r0, r1 := m.GetTeamByNameFunc.nextHook()(v0, v1) + m.GetTeamByNameFunc.appendCall(TeamStoreGetTeamByNameFuncCall{v0, v1, r0, r1}) + return r0, r1 +} + +// SetDefaultHook sets function that is called when the GetTeamByName method +// of the parent MockTeamStore instance is invoked and the hook queue is +// empty. +func (f *TeamStoreGetTeamByNameFunc) SetDefaultHook(hook func(context.Context, string) (*types.Team, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// GetTeamByName method of the parent MockTeamStore instance invokes the +// hook at the front of the queue and discards it. After the queue is empty, +// the default hook function is invoked for any future action. +func (f *TeamStoreGetTeamByNameFunc) PushHook(hook func(context.Context, string) (*types.Team, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *TeamStoreGetTeamByNameFunc) SetDefaultReturn(r0 *types.Team, r1 error) { + f.SetDefaultHook(func(context.Context, string) (*types.Team, error) { + return r0, r1 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *TeamStoreGetTeamByNameFunc) PushReturn(r0 *types.Team, r1 error) { + f.PushHook(func(context.Context, string) (*types.Team, error) { + return r0, r1 + }) +} + +func (f *TeamStoreGetTeamByNameFunc) nextHook() func(context.Context, string) (*types.Team, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *TeamStoreGetTeamByNameFunc) appendCall(r0 TeamStoreGetTeamByNameFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of TeamStoreGetTeamByNameFuncCall objects +// describing the invocations of this function. +func (f *TeamStoreGetTeamByNameFunc) History() []TeamStoreGetTeamByNameFuncCall { + f.mutex.Lock() + history := make([]TeamStoreGetTeamByNameFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// TeamStoreGetTeamByNameFuncCall is an object that describes an invocation +// of method GetTeamByName on an instance of MockTeamStore. +type TeamStoreGetTeamByNameFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 string + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 *types.Team + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c TeamStoreGetTeamByNameFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c TeamStoreGetTeamByNameFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1} +} + +// TeamStoreHandleFunc describes the behavior when the Handle method of the +// parent MockTeamStore instance is invoked. +type TeamStoreHandleFunc struct { + defaultHook func() basestore.TransactableHandle + hooks []func() basestore.TransactableHandle + history []TeamStoreHandleFuncCall + mutex sync.Mutex +} + +// Handle delegates to the next hook function in the queue and stores the +// parameter and result values of this invocation. +func (m *MockTeamStore) Handle() basestore.TransactableHandle { + r0 := m.HandleFunc.nextHook()() + m.HandleFunc.appendCall(TeamStoreHandleFuncCall{r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the Handle method of the +// parent MockTeamStore instance is invoked and the hook queue is empty. +func (f *TeamStoreHandleFunc) SetDefaultHook(hook func() basestore.TransactableHandle) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// Handle method of the parent MockTeamStore instance invokes the hook at +// the front of the queue and discards it. After the queue is empty, the +// default hook function is invoked for any future action. +func (f *TeamStoreHandleFunc) PushHook(hook func() basestore.TransactableHandle) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *TeamStoreHandleFunc) SetDefaultReturn(r0 basestore.TransactableHandle) { + f.SetDefaultHook(func() basestore.TransactableHandle { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *TeamStoreHandleFunc) PushReturn(r0 basestore.TransactableHandle) { + f.PushHook(func() basestore.TransactableHandle { + return r0 + }) +} + +func (f *TeamStoreHandleFunc) nextHook() func() basestore.TransactableHandle { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *TeamStoreHandleFunc) appendCall(r0 TeamStoreHandleFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of TeamStoreHandleFuncCall objects describing +// the invocations of this function. +func (f *TeamStoreHandleFunc) History() []TeamStoreHandleFuncCall { + f.mutex.Lock() + history := make([]TeamStoreHandleFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// TeamStoreHandleFuncCall is an object that describes an invocation of +// method Handle on an instance of MockTeamStore. +type TeamStoreHandleFuncCall struct { + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 basestore.TransactableHandle +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c TeamStoreHandleFuncCall) Args() []interface{} { + return []interface{}{} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c TeamStoreHandleFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + +// TeamStoreListTeamMembersFunc describes the behavior when the +// ListTeamMembers method of the parent MockTeamStore instance is invoked. +type TeamStoreListTeamMembersFunc struct { + defaultHook func(context.Context, ListTeamMembersOpts) ([]*types.TeamMember, *TeamMemberListCursor, error) + hooks []func(context.Context, ListTeamMembersOpts) ([]*types.TeamMember, *TeamMemberListCursor, error) + history []TeamStoreListTeamMembersFuncCall + mutex sync.Mutex +} + +// ListTeamMembers delegates to the next hook function in the queue and +// stores the parameter and result values of this invocation. +func (m *MockTeamStore) ListTeamMembers(v0 context.Context, v1 ListTeamMembersOpts) ([]*types.TeamMember, *TeamMemberListCursor, error) { + r0, r1, r2 := m.ListTeamMembersFunc.nextHook()(v0, v1) + m.ListTeamMembersFunc.appendCall(TeamStoreListTeamMembersFuncCall{v0, v1, r0, r1, r2}) + return r0, r1, r2 +} + +// SetDefaultHook sets function that is called when the ListTeamMembers +// method of the parent MockTeamStore instance is invoked and the hook queue +// is empty. +func (f *TeamStoreListTeamMembersFunc) SetDefaultHook(hook func(context.Context, ListTeamMembersOpts) ([]*types.TeamMember, *TeamMemberListCursor, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// ListTeamMembers method of the parent MockTeamStore instance invokes the +// hook at the front of the queue and discards it. After the queue is empty, +// the default hook function is invoked for any future action. +func (f *TeamStoreListTeamMembersFunc) PushHook(hook func(context.Context, ListTeamMembersOpts) ([]*types.TeamMember, *TeamMemberListCursor, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *TeamStoreListTeamMembersFunc) SetDefaultReturn(r0 []*types.TeamMember, r1 *TeamMemberListCursor, r2 error) { + f.SetDefaultHook(func(context.Context, ListTeamMembersOpts) ([]*types.TeamMember, *TeamMemberListCursor, error) { + return r0, r1, r2 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *TeamStoreListTeamMembersFunc) PushReturn(r0 []*types.TeamMember, r1 *TeamMemberListCursor, r2 error) { + f.PushHook(func(context.Context, ListTeamMembersOpts) ([]*types.TeamMember, *TeamMemberListCursor, error) { + return r0, r1, r2 + }) +} + +func (f *TeamStoreListTeamMembersFunc) nextHook() func(context.Context, ListTeamMembersOpts) ([]*types.TeamMember, *TeamMemberListCursor, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *TeamStoreListTeamMembersFunc) appendCall(r0 TeamStoreListTeamMembersFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of TeamStoreListTeamMembersFuncCall objects +// describing the invocations of this function. +func (f *TeamStoreListTeamMembersFunc) History() []TeamStoreListTeamMembersFuncCall { + f.mutex.Lock() + history := make([]TeamStoreListTeamMembersFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// TeamStoreListTeamMembersFuncCall is an object that describes an +// invocation of method ListTeamMembers on an instance of MockTeamStore. +type TeamStoreListTeamMembersFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 ListTeamMembersOpts + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 []*types.TeamMember + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 *TeamMemberListCursor + // Result2 is the value of the 3rd result returned from this method + // invocation. + Result2 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c TeamStoreListTeamMembersFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c TeamStoreListTeamMembersFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1, c.Result2} +} + +// TeamStoreListTeamsFunc describes the behavior when the ListTeams method +// of the parent MockTeamStore instance is invoked. +type TeamStoreListTeamsFunc struct { + defaultHook func(context.Context, ListTeamsOpts) ([]*types.Team, int32, error) + hooks []func(context.Context, ListTeamsOpts) ([]*types.Team, int32, error) + history []TeamStoreListTeamsFuncCall + mutex sync.Mutex +} + +// ListTeams delegates to the next hook function in the queue and stores the +// parameter and result values of this invocation. +func (m *MockTeamStore) ListTeams(v0 context.Context, v1 ListTeamsOpts) ([]*types.Team, int32, error) { + r0, r1, r2 := m.ListTeamsFunc.nextHook()(v0, v1) + m.ListTeamsFunc.appendCall(TeamStoreListTeamsFuncCall{v0, v1, r0, r1, r2}) + return r0, r1, r2 +} + +// SetDefaultHook sets function that is called when the ListTeams method of +// the parent MockTeamStore instance is invoked and the hook queue is empty. +func (f *TeamStoreListTeamsFunc) SetDefaultHook(hook func(context.Context, ListTeamsOpts) ([]*types.Team, int32, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// ListTeams method of the parent MockTeamStore instance invokes the hook at +// the front of the queue and discards it. After the queue is empty, the +// default hook function is invoked for any future action. +func (f *TeamStoreListTeamsFunc) PushHook(hook func(context.Context, ListTeamsOpts) ([]*types.Team, int32, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *TeamStoreListTeamsFunc) SetDefaultReturn(r0 []*types.Team, r1 int32, r2 error) { + f.SetDefaultHook(func(context.Context, ListTeamsOpts) ([]*types.Team, int32, error) { + return r0, r1, r2 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *TeamStoreListTeamsFunc) PushReturn(r0 []*types.Team, r1 int32, r2 error) { + f.PushHook(func(context.Context, ListTeamsOpts) ([]*types.Team, int32, error) { + return r0, r1, r2 + }) +} + +func (f *TeamStoreListTeamsFunc) nextHook() func(context.Context, ListTeamsOpts) ([]*types.Team, int32, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *TeamStoreListTeamsFunc) appendCall(r0 TeamStoreListTeamsFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of TeamStoreListTeamsFuncCall objects +// describing the invocations of this function. +func (f *TeamStoreListTeamsFunc) History() []TeamStoreListTeamsFuncCall { + f.mutex.Lock() + history := make([]TeamStoreListTeamsFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// TeamStoreListTeamsFuncCall is an object that describes an invocation of +// method ListTeams on an instance of MockTeamStore. +type TeamStoreListTeamsFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 ListTeamsOpts + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 []*types.Team + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 int32 + // Result2 is the value of the 3rd result returned from this method + // invocation. + Result2 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c TeamStoreListTeamsFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c TeamStoreListTeamsFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1, c.Result2} +} + +// TeamStoreUpdateTeamFunc describes the behavior when the UpdateTeam method +// of the parent MockTeamStore instance is invoked. +type TeamStoreUpdateTeamFunc struct { + defaultHook func(context.Context, *types.Team) error + hooks []func(context.Context, *types.Team) error + history []TeamStoreUpdateTeamFuncCall + mutex sync.Mutex +} + +// UpdateTeam delegates to the next hook function in the queue and stores +// the parameter and result values of this invocation. +func (m *MockTeamStore) UpdateTeam(v0 context.Context, v1 *types.Team) error { + r0 := m.UpdateTeamFunc.nextHook()(v0, v1) + m.UpdateTeamFunc.appendCall(TeamStoreUpdateTeamFuncCall{v0, v1, r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the UpdateTeam method of +// the parent MockTeamStore instance is invoked and the hook queue is empty. +func (f *TeamStoreUpdateTeamFunc) SetDefaultHook(hook func(context.Context, *types.Team) error) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// UpdateTeam method of the parent MockTeamStore instance invokes the hook +// at the front of the queue and discards it. After the queue is empty, the +// default hook function is invoked for any future action. +func (f *TeamStoreUpdateTeamFunc) PushHook(hook func(context.Context, *types.Team) error) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *TeamStoreUpdateTeamFunc) SetDefaultReturn(r0 error) { + f.SetDefaultHook(func(context.Context, *types.Team) error { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *TeamStoreUpdateTeamFunc) PushReturn(r0 error) { + f.PushHook(func(context.Context, *types.Team) error { + return r0 + }) +} + +func (f *TeamStoreUpdateTeamFunc) nextHook() func(context.Context, *types.Team) error { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *TeamStoreUpdateTeamFunc) appendCall(r0 TeamStoreUpdateTeamFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of TeamStoreUpdateTeamFuncCall objects +// describing the invocations of this function. +func (f *TeamStoreUpdateTeamFunc) History() []TeamStoreUpdateTeamFuncCall { + f.mutex.Lock() + history := make([]TeamStoreUpdateTeamFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// TeamStoreUpdateTeamFuncCall is an object that describes an invocation of +// method UpdateTeam on an instance of MockTeamStore. +type TeamStoreUpdateTeamFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 *types.Team + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c TeamStoreUpdateTeamFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c TeamStoreUpdateTeamFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + // MockTemporarySettingsStore is a mock implementation of the // TemporarySettingsStore interface (from the package // github.com/sourcegraph/sourcegraph/internal/database) used for unit diff --git a/internal/database/orgs.go b/internal/database/orgs.go index 6f3dd516cb7b..d08a1664e7cf 100644 --- a/internal/database/orgs.go +++ b/internal/database/orgs.go @@ -26,7 +26,7 @@ func (e *OrgNotFoundError) NotFound() bool { return true } -var errOrgNameAlreadyExists = errors.New("organization name is already taken (by a user or another organization)") +var errOrgNameAlreadyExists = errors.New("organization name is already taken (by a user, team, or another organization)") type OrgStore interface { AddOrgsOpenBetaStats(ctx context.Context, userID int32, data string) (string, error) @@ -229,7 +229,7 @@ func (o *orgStore) Create(ctx context.Context, name string, displayName *string) return nil, err } - // Reserve organization name in shared users+orgs namespace. + // Reserve organization name in shared users+orgs+teams namespace. if _, err := tx.Handle().ExecContext(ctx, "INSERT INTO names(name, org_id) VALUES($1, $2)", newOrg.Name, newOrg.ID); err != nil { return nil, errOrgNameAlreadyExists } diff --git a/internal/database/schema.json b/internal/database/schema.json index 68996d2e729d..6765558e729d 100755 --- a/internal/database/schema.json +++ b/internal/database/schema.json @@ -922,6 +922,15 @@ "Increment": 1, "CycleOption": "NO" }, + { + "Name": "teams_id_seq", + "TypeName": "integer", + "StartValue": 1, + "MinimumValue": 1, + "MaximumValue": 2147483647, + "Increment": 1, + "CycleOption": "NO" + }, { "Name": "temporary_settings_id_seq", "TypeName": "integer", @@ -14866,6 +14875,19 @@ "GenerationExpression": "", "Comment": "" }, + { + "Name": "team_id", + "Index": 4, + "TypeName": "integer", + "IsNullable": true, + "Default": "", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, { "Name": "user_id", "Index": 2, @@ -14898,7 +14920,7 @@ "ConstraintType": "c", "RefTableName": "", "IsDeferrable": false, - "ConstraintDefinition": "CHECK (user_id IS NOT NULL OR org_id IS NOT NULL)" + "ConstraintDefinition": "CHECK (user_id IS NOT NULL OR org_id IS NOT NULL OR team_id IS NOT NULL)" }, { "Name": "names_org_id_fkey", @@ -14907,6 +14929,13 @@ "IsDeferrable": false, "ConstraintDefinition": "FOREIGN KEY (org_id) REFERENCES orgs(id) ON UPDATE CASCADE ON DELETE CASCADE" }, + { + "Name": "names_team_id_fkey", + "ConstraintType": "f", + "RefTableName": "teams", + "IsDeferrable": false, + "ConstraintDefinition": "FOREIGN KEY (team_id) REFERENCES teams(id) ON UPDATE CASCADE ON DELETE CASCADE" + }, { "Name": "names_user_id_fkey", "ConstraintType": "f", @@ -20487,6 +20516,263 @@ ], "Triggers": [] }, + { + "Name": "team_members", + "Comment": "", + "Columns": [ + { + "Name": "created_at", + "Index": 3, + "TypeName": "timestamp with time zone", + "IsNullable": false, + "Default": "now()", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, + { + "Name": "team_id", + "Index": 1, + "TypeName": "integer", + "IsNullable": false, + "Default": "", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, + { + "Name": "updated_at", + "Index": 4, + "TypeName": "timestamp with time zone", + "IsNullable": false, + "Default": "now()", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, + { + "Name": "user_id", + "Index": 2, + "TypeName": "integer", + "IsNullable": false, + "Default": "", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + } + ], + "Indexes": [ + { + "Name": "team_members_team_id_user_id_key", + "IsPrimaryKey": true, + "IsUnique": true, + "IsExclusion": false, + "IsDeferrable": false, + "IndexDefinition": "CREATE UNIQUE INDEX team_members_team_id_user_id_key ON team_members USING btree (team_id, user_id)", + "ConstraintType": "p", + "ConstraintDefinition": "PRIMARY KEY (team_id, user_id)" + } + ], + "Constraints": [ + { + "Name": "team_members_team_id_fkey", + "ConstraintType": "f", + "RefTableName": "teams", + "IsDeferrable": false, + "ConstraintDefinition": "FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE" + }, + { + "Name": "team_members_user_id_fkey", + "ConstraintType": "f", + "RefTableName": "users", + "IsDeferrable": false, + "ConstraintDefinition": "FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE" + } + ], + "Triggers": [] + }, + { + "Name": "teams", + "Comment": "", + "Columns": [ + { + "Name": "created_at", + "Index": 7, + "TypeName": "timestamp with time zone", + "IsNullable": false, + "Default": "now()", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, + { + "Name": "creator_id", + "Index": 6, + "TypeName": "integer", + "IsNullable": false, + "Default": "", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, + { + "Name": "display_name", + "Index": 3, + "TypeName": "text", + "IsNullable": true, + "Default": "", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, + { + "Name": "id", + "Index": 1, + "TypeName": "integer", + "IsNullable": false, + "Default": "nextval('teams_id_seq'::regclass)", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, + { + "Name": "name", + "Index": 2, + "TypeName": "citext", + "IsNullable": false, + "Default": "", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, + { + "Name": "parent_team_id", + "Index": 5, + "TypeName": "integer", + "IsNullable": true, + "Default": "", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, + { + "Name": "readonly", + "Index": 4, + "TypeName": "boolean", + "IsNullable": false, + "Default": "false", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, + { + "Name": "updated_at", + "Index": 8, + "TypeName": "timestamp with time zone", + "IsNullable": false, + "Default": "now()", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + } + ], + "Indexes": [ + { + "Name": "teams_name", + "IsPrimaryKey": false, + "IsUnique": true, + "IsExclusion": false, + "IsDeferrable": false, + "IndexDefinition": "CREATE UNIQUE INDEX teams_name ON teams USING btree (name)", + "ConstraintType": "", + "ConstraintDefinition": "" + }, + { + "Name": "teams_pkey", + "IsPrimaryKey": true, + "IsUnique": true, + "IsExclusion": false, + "IsDeferrable": false, + "IndexDefinition": "CREATE UNIQUE INDEX teams_pkey ON teams USING btree (id)", + "ConstraintType": "p", + "ConstraintDefinition": "PRIMARY KEY (id)" + } + ], + "Constraints": [ + { + "Name": "teams_creator_id_fkey", + "ConstraintType": "f", + "RefTableName": "users", + "IsDeferrable": false, + "ConstraintDefinition": "FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL" + }, + { + "Name": "teams_display_name_max_length", + "ConstraintType": "c", + "RefTableName": "", + "IsDeferrable": false, + "ConstraintDefinition": "CHECK (char_length(display_name) \u003c= 255)" + }, + { + "Name": "teams_name_max_length", + "ConstraintType": "c", + "RefTableName": "", + "IsDeferrable": false, + "ConstraintDefinition": "CHECK (char_length(name::text) \u003c= 255)" + }, + { + "Name": "teams_name_valid_chars", + "ConstraintType": "c", + "RefTableName": "", + "IsDeferrable": false, + "ConstraintDefinition": "CHECK (name ~ '^[a-zA-Z0-9](?:[a-zA-Z0-9]|[-.](?=[a-zA-Z0-9]))*-?$'::citext)" + }, + { + "Name": "teams_parent_team_id_fkey", + "ConstraintType": "f", + "RefTableName": "teams", + "IsDeferrable": false, + "ConstraintDefinition": "FOREIGN KEY (parent_team_id) REFERENCES teams(id) ON DELETE CASCADE" + } + ], + "Triggers": [] + }, { "Name": "temporary_settings", "Comment": "Stores per-user temporary settings used in the UI, for example, which modals have been dimissed or what theme is preferred.", diff --git a/internal/database/schema.md b/internal/database/schema.md index c51ef9ba4d55..a3d16acd0739 100755 --- a/internal/database/schema.md +++ b/internal/database/schema.md @@ -2249,12 +2249,14 @@ Indexes: name | citext | | not null | user_id | integer | | | org_id | integer | | | + team_id | integer | | | Indexes: "names_pkey" PRIMARY KEY, btree (name) Check constraints: - "names_check" CHECK (user_id IS NOT NULL OR org_id IS NOT NULL) + "names_check" CHECK (user_id IS NOT NULL OR org_id IS NOT NULL OR team_id IS NOT NULL) Foreign-key constraints: "names_org_id_fkey" FOREIGN KEY (org_id) REFERENCES orgs(id) ON UPDATE CASCADE ON DELETE CASCADE + "names_team_id_fkey" FOREIGN KEY (team_id) REFERENCES teams(id) ON UPDATE CASCADE ON DELETE CASCADE "names_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE ``` @@ -3214,6 +3216,51 @@ Foreign-key constraints: ``` +# Table "public.team_members" +``` + Column | Type | Collation | Nullable | Default +------------+--------------------------+-----------+----------+--------- + team_id | integer | | not null | + user_id | integer | | not null | + created_at | timestamp with time zone | | not null | now() + updated_at | timestamp with time zone | | not null | now() +Indexes: + "team_members_team_id_user_id_key" PRIMARY KEY, btree (team_id, user_id) +Foreign-key constraints: + "team_members_team_id_fkey" FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE + "team_members_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + +``` + +# Table "public.teams" +``` + Column | Type | Collation | Nullable | Default +----------------+--------------------------+-----------+----------+----------------------------------- + id | integer | | not null | nextval('teams_id_seq'::regclass) + name | citext | | not null | + display_name | text | | | + readonly | boolean | | not null | false + parent_team_id | integer | | | + creator_id | integer | | not null | + created_at | timestamp with time zone | | not null | now() + updated_at | timestamp with time zone | | not null | now() +Indexes: + "teams_pkey" PRIMARY KEY, btree (id) + "teams_name" UNIQUE, btree (name) +Check constraints: + "teams_display_name_max_length" CHECK (char_length(display_name) <= 255) + "teams_name_max_length" CHECK (char_length(name::text) <= 255) + "teams_name_valid_chars" CHECK (name ~ '^[a-zA-Z0-9](?:[a-zA-Z0-9]|[-.](?=[a-zA-Z0-9]))*-?$'::citext) +Foreign-key constraints: + "teams_creator_id_fkey" FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL + "teams_parent_team_id_fkey" FOREIGN KEY (parent_team_id) REFERENCES teams(id) ON DELETE CASCADE +Referenced by: + TABLE "names" CONSTRAINT "names_team_id_fkey" FOREIGN KEY (team_id) REFERENCES teams(id) ON UPDATE CASCADE ON DELETE CASCADE + TABLE "team_members" CONSTRAINT "team_members_team_id_fkey" FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE + TABLE "teams" CONSTRAINT "teams_parent_team_id_fkey" FOREIGN KEY (parent_team_id) REFERENCES teams(id) ON DELETE CASCADE + +``` + # Table "public.temporary_settings" ``` Column | Type | Collation | Nullable | Default @@ -3459,6 +3506,8 @@ Referenced by: TABLE "settings" CONSTRAINT "settings_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT TABLE "sub_repo_permissions" CONSTRAINT "sub_repo_permissions_users_id_fk" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE TABLE "survey_responses" CONSTRAINT "survey_responses_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) + TABLE "team_members" CONSTRAINT "team_members_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + TABLE "teams" CONSTRAINT "teams_creator_id_fkey" FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL TABLE "temporary_settings" CONSTRAINT "temporary_settings_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE TABLE "user_credentials" CONSTRAINT "user_credentials_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE DEFERRABLE TABLE "user_emails" CONSTRAINT "user_emails_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) diff --git a/internal/database/teams.go b/internal/database/teams.go new file mode 100644 index 000000000000..f087b5feb347 --- /dev/null +++ b/internal/database/teams.go @@ -0,0 +1,628 @@ +package database + +import ( + "context" + "fmt" + + "github.com/jackc/pgconn" + "github.com/keegancsmith/sqlf" + + "github.com/sourcegraph/sourcegraph/internal/database/basestore" + "github.com/sourcegraph/sourcegraph/internal/database/batch" + "github.com/sourcegraph/sourcegraph/internal/database/dbutil" + "github.com/sourcegraph/sourcegraph/internal/timeutil" + "github.com/sourcegraph/sourcegraph/internal/types" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +type ListTeamsOpts struct { + *LimitOffset + + // Only return teams past this cursor. + Cursor int32 + // List teams of a specific parent team only. + WithParentID int32 + // Only return root teams (teams that have no parent). + // This is used on the main overview list of teams. + RootOnly bool + // Filter teams by search term. Currently, name and displayName are searchable. + Search string + // List teams that a specific user is a member of. + ForUserMember int32 +} + +func (opts ListTeamsOpts) SQL() (where, joins []*sqlf.Query) { + where = []*sqlf.Query{ + sqlf.Sprintf("teams.id >= %s", opts.Cursor), + } + joins = []*sqlf.Query{} + + if opts.WithParentID != 0 { + where = append(where, sqlf.Sprintf("teams.parent_team_id = %s", opts.WithParentID)) + } + if opts.RootOnly { + where = append(where, sqlf.Sprintf("teams.parent_team_id IS NULL")) + } + if opts.Search != "" { + term := "%" + opts.Search + "%" + where = append(where, sqlf.Sprintf("(teams.name ILIKE %s OR teams.display_name ILIKE %s)", term, term)) + } + if opts.ForUserMember != 0 { + joins = append(joins, sqlf.Sprintf("JOIN team_members ON team_members.team_id = teams.id")) + where = append(where, sqlf.Sprintf("team_members.user_id = %s", opts.ForUserMember)) + } + + return where, joins +} + +type TeamMemberListCursor struct { + TeamID int32 + UserID int32 +} + +type ListTeamMembersOpts struct { + *LimitOffset + + // Only return members past this cursor. + Cursor TeamMemberListCursor + // Required. Scopes the list operation to the given team. + TeamID int32 + // Filter members by search term. Currently, name and displayName of the users + // are searchable. + Search string +} + +func (opts ListTeamMembersOpts) SQL() (where, joins []*sqlf.Query) { + where = []*sqlf.Query{ + sqlf.Sprintf("team_members.team_id >= %s AND team_members.user_id >= %s", opts.Cursor.TeamID, opts.Cursor.UserID), + } + joins = []*sqlf.Query{} + + if opts.TeamID != 0 { + where = append(where, sqlf.Sprintf("team_members.team_id = %s", opts.TeamID)) + } + if opts.Search != "" { + joins = append(joins, sqlf.Sprintf("JOIN users ON users.id = team_members.user_id")) + term := "%" + opts.Search + "%" + where = append(where, sqlf.Sprintf("(users.username ILIKE %s OR users.display_name ILIKE %s)", term, term)) + } + + return where, joins +} + +// TeamNotFoundError is returned when a team cannot be found. +type TeamNotFoundError struct { + args any +} + +func (err TeamNotFoundError) Error() string { + return fmt.Sprintf("team not found: %v", err.args) +} + +func (TeamNotFoundError) NotFound() bool { + return true +} + +// ErrTeamNameAlreadyExists is returned when the team name is already in use, either +// by another team or another user/org. +var ErrTeamNameAlreadyExists = errors.New("team name is already taken (by a user, organization, or another team)") + +// TeamStore provides database methods for interacting with teams and their members. +type TeamStore interface { + basestore.ShareableStore + Done(error) error + + // GetTeamByID returns the given team by ID. If not found, a NotFounder error is returned. + GetTeamByID(ctx context.Context, id int32) (*types.Team, error) + // GetTeamByName returns the given team by name. If not found, a NotFounder error is returned. + GetTeamByName(ctx context.Context, name string) (*types.Team, error) + // ListTeams lists teams given the options. The matching teams, plus the next cursor are + // returned. + ListTeams(ctx context.Context, opts ListTeamsOpts) ([]*types.Team, int32, error) + // CountTeams counts teams given the options. + CountTeams(ctx context.Context, opts ListTeamsOpts) (int32, error) + // ListTeamMembers lists team members given the options. The matching teams, + // plus the next cursor are returned. + ListTeamMembers(ctx context.Context, opts ListTeamMembersOpts) ([]*types.TeamMember, *TeamMemberListCursor, error) + // CountTeamMembers counts teams given the options. + CountTeamMembers(ctx context.Context, opts ListTeamMembersOpts) (int32, error) + // CreateTeam creates the given team in the database. + CreateTeam(ctx context.Context, team *types.Team) error + // UpdateTeam updates the given team in the database. + UpdateTeam(ctx context.Context, team *types.Team) error + // DeleteTeam deletes the given team from the database. + DeleteTeam(ctx context.Context, team int32) error + // CreateTeamMember creates the team members in the database. If any of the inserts fail, + // all inserts are reverted. + CreateTeamMember(ctx context.Context, members ...*types.TeamMember) error + // DeleteTeam deletes the given team members from the database. + DeleteTeamMember(ctx context.Context, members ...*types.TeamMember) error +} + +type teamStore struct { + *basestore.Store +} + +// TeamsWith instantiates and returns a new TeamStore using the other store handle. +func TeamsWith(other basestore.ShareableStore) TeamStore { + return &teamStore{ + Store: basestore.NewWithHandle(other.Handle()), + } +} + +func (s *teamStore) With(other basestore.ShareableStore) TeamStore { + return &teamStore{ + Store: s.Store.With(other), + } +} + +func (s *teamStore) Transact(ctx context.Context) (TeamStore, error) { + txBase, err := s.Store.Transact(ctx) + return &teamStore{ + Store: txBase, + }, err +} + +func (s *teamStore) GetTeamByID(ctx context.Context, id int32) (*types.Team, error) { + conds := []*sqlf.Query{ + sqlf.Sprintf("teams.id = %s", id), + } + return s.getTeam(ctx, conds) +} + +func (s *teamStore) GetTeamByName(ctx context.Context, name string) (*types.Team, error) { + conds := []*sqlf.Query{ + sqlf.Sprintf("teams.name = %s", name), + } + return s.getTeam(ctx, conds) +} + +func (s *teamStore) getTeam(ctx context.Context, conds []*sqlf.Query) (*types.Team, error) { + q := sqlf.Sprintf(getTeamQueryFmtstr, sqlf.Join(teamColumns, ","), sqlf.Join(conds, "AND")) + + teams, err := scanTeams(s.Query(ctx, q)) + if err != nil { + return nil, err + } + + if len(teams) != 1 { + return nil, TeamNotFoundError{args: conds} + } + + return teams[0], nil +} + +const getTeamQueryFmtstr = ` +SELECT %s +FROM teams +WHERE + %s +LIMIT 1 +` + +func (s *teamStore) ListTeams(ctx context.Context, opts ListTeamsOpts) (_ []*types.Team, next int32, err error) { + conds, joins := opts.SQL() + + if opts.LimitOffset != nil && opts.Limit > 0 { + opts.Limit++ + } + + q := sqlf.Sprintf( + listTeamsQueryFmtstr, + sqlf.Join(teamColumns, ","), + sqlf.Join(joins, "\n"), + sqlf.Join(conds, "AND"), + opts.LimitOffset.SQL(), + ) + + teams, err := scanTeams(s.Query(ctx, q)) + if err != nil { + return nil, 0, err + } + + if opts.LimitOffset != nil && opts.Limit > 0 && len(teams) == opts.Limit { + next = teams[len(teams)-1].ID + teams = teams[:len(teams)-1] + } + + return teams, next, nil +} + +const listTeamsQueryFmtstr = ` +SELECT %s +FROM teams +%s +WHERE %s +ORDER BY + teams.id ASC +%s +` + +func (s *teamStore) CountTeams(ctx context.Context, opts ListTeamsOpts) (int32, error) { + // Disable cursor for counting. + opts.Cursor = 0 + conds, joins := opts.SQL() + + q := sqlf.Sprintf( + countTeamsQueryFmtstr, + sqlf.Join(joins, "\n"), + sqlf.Join(conds, "AND"), + ) + + count, _, err := basestore.ScanFirstInt(s.Query(ctx, q)) + return int32(count), err +} + +const countTeamsQueryFmtstr = ` +SELECT COUNT(*) +FROM teams +%s +WHERE %s +` + +func (s *teamStore) ListTeamMembers(ctx context.Context, opts ListTeamMembersOpts) (_ []*types.TeamMember, next *TeamMemberListCursor, err error) { + conds, joins := opts.SQL() + + if opts.LimitOffset != nil && opts.Limit > 0 { + opts.Limit++ + } + + q := sqlf.Sprintf( + listTeamMembersQueryFmtstr, + sqlf.Join(teamMemberColumns, ","), + sqlf.Join(joins, "\n"), + sqlf.Join(conds, "AND"), + opts.LimitOffset.SQL(), + ) + + tms, err := scanTeamMembers(s.Query(ctx, q)) + if err != nil { + return nil, nil, err + } + + if opts.LimitOffset != nil && opts.Limit > 0 && len(tms) == opts.Limit { + next = &TeamMemberListCursor{ + TeamID: tms[len(tms)-1].TeamID, + UserID: tms[len(tms)-1].UserID, + } + tms = tms[:len(tms)-1] + } + + return tms, next, nil +} + +const listTeamMembersQueryFmtstr = ` +SELECT %s +FROM team_members +%s +WHERE %s +ORDER BY + team_members.team_id ASC, team_members.user_id ASC +%s +` + +func (s *teamStore) CountTeamMembers(ctx context.Context, opts ListTeamMembersOpts) (int32, error) { + // Disable cursor for counting. + opts.Cursor = TeamMemberListCursor{} + conds, joins := opts.SQL() + + q := sqlf.Sprintf( + countTeamMembersQueryFmtstr, + sqlf.Join(joins, "\n"), + sqlf.Join(conds, "AND"), + ) + + count, _, err := basestore.ScanFirstInt(s.Query(ctx, q)) + return int32(count), err +} + +const countTeamMembersQueryFmtstr = ` +SELECT COUNT(*) +FROM team_members +%s +WHERE %s +` + +func (s *teamStore) CreateTeam(ctx context.Context, team *types.Team) (err error) { + tx, err := s.Transact(ctx) + if err != nil { + return err + } + defer func() { + err = tx.Done(err) + }() + + if team.CreatedAt.IsZero() { + team.CreatedAt = timeutil.Now() + } + + if team.UpdatedAt.IsZero() { + team.UpdatedAt = team.CreatedAt + } + + q := sqlf.Sprintf( + createTeamQueryFmtstr, + sqlf.Join(teamInsertColumns, ","), + team.Name, + dbutil.NewNullString(team.DisplayName), + team.ReadOnly, + dbutil.NewNullInt32(team.ParentTeamID), + dbutil.NewNullInt32(team.CreatorID), + team.CreatedAt, + team.UpdatedAt, + sqlf.Join(teamColumns, ","), + ) + + row := tx.Handle().QueryRowContext( + ctx, + q.Query(sqlf.PostgresBindVar), + q.Args()..., + ) + if err := row.Err(); err != nil { + var e *pgconn.PgError + if errors.As(err, &e) { + switch e.ConstraintName { + case "teams_name": + return ErrTeamNameAlreadyExists + case "orgs_name_max_length", "orgs_name_valid_chars": + return errors.Errorf("team name invalid: %s", e.ConstraintName) + case "orgs_display_name_max_length": + return errors.Errorf("team display name invalid: %s", e.ConstraintName) + } + } + + return err + } + + if err := scanTeam(row, team); err != nil { + return err + } + + q = sqlf.Sprintf(createTeamNameReservationQueryFmtstr, team.Name, team.ID) + + // Reserve team name in shared users+orgs+teams namespace. + if _, err := tx.Handle().ExecContext(ctx, q.Query(sqlf.PostgresBindVar), q.Args()...); err != nil { + var e *pgconn.PgError + if errors.As(err, &e) { + switch e.ConstraintName { + case "names_pkey": + return ErrTeamNameAlreadyExists + } + } + return err + } + + return nil +} + +const createTeamQueryFmtstr = ` +INSERT INTO teams +(%s) +VALUES (%s, %s, %s, %s, %s, %s, %s) +RETURNING %s +` + +const createTeamNameReservationQueryFmtstr = ` +INSERT INTO names + (name, team_id) +VALUES + (%s, %s) +` + +func (s *teamStore) UpdateTeam(ctx context.Context, team *types.Team) error { + team.UpdatedAt = timeutil.Now() + + conds := []*sqlf.Query{ + sqlf.Sprintf("id = %s", team.ID), + } + + q := sqlf.Sprintf( + updateTeamQueryFmtstr, + dbutil.NewNullString(team.DisplayName), + dbutil.NewNullInt32(team.ParentTeamID), + team.UpdatedAt, + sqlf.Join(conds, "AND"), + sqlf.Join(teamColumns, ","), + ) + + return scanTeam(s.QueryRow(ctx, q), team) +} + +const updateTeamQueryFmtstr = ` +UPDATE + teams +SET + display_name = %s, + parent_team_id = %s, + updated_at = %s +WHERE + %s +RETURNING + %s +` + +func (s *teamStore) DeleteTeam(ctx context.Context, team int32) (err error) { + tx, err := s.Transact(ctx) + if err != nil { + return err + } + defer func() { + err = tx.Done(err) + }() + + conds := []*sqlf.Query{ + sqlf.Sprintf("teams.id = %s", team), + } + + q := sqlf.Sprintf(deleteTeamQueryFmtstr, sqlf.Join(conds, "AND")) + + res, err := tx.Handle().ExecContext(ctx, q.Query(sqlf.PostgresBindVar), q.Args()...) + if err != nil { + return err + } + rows, err := res.RowsAffected() + if err != nil { + return err + } + if rows == 0 { + return TeamNotFoundError{args: conds} + } + + conds = []*sqlf.Query{ + sqlf.Sprintf("names.team_id = %s", team), + } + + q = sqlf.Sprintf(deleteTeamNameReservationQueryFmtstr, sqlf.Join(conds, "AND")) + + // Release the teams name so it can be used by another user, team or org. + _, err = tx.Handle().ExecContext(ctx, q.Query(sqlf.PostgresBindVar), q.Args()...) + return err +} + +const deleteTeamQueryFmtstr = ` +DELETE FROM + teams +WHERE %s +` + +const deleteTeamNameReservationQueryFmtstr = ` +DELETE FROM + names +WHERE %s +` + +func (s *teamStore) CreateTeamMember(ctx context.Context, members ...*types.TeamMember) error { + inserter := func(inserter *batch.Inserter) error { + for _, m := range members { + if m.CreatedAt.IsZero() { + m.CreatedAt = timeutil.Now() + } + + if m.UpdatedAt.IsZero() { + m.UpdatedAt = m.CreatedAt + } + + if err := inserter.Insert( + ctx, + m.TeamID, + m.UserID, + m.CreatedAt, + m.UpdatedAt, + ); err != nil { + return err + } + } + return nil + } + + i := -1 + return batch.WithInserterWithReturn( + ctx, + s.Handle(), + "team_members", + batch.MaxNumPostgresParameters, + teamMemberInsertColumns, + "", + teamMemberStringColumns, + func(sc dbutil.Scanner) error { + i++ + return scanTeamMember(sc, members[i]) + }, + inserter, + ) +} + +func (s *teamStore) DeleteTeamMember(ctx context.Context, members ...*types.TeamMember) error { + ms := []*sqlf.Query{} + for _, m := range members { + ms = append(ms, sqlf.Sprintf("(%s, %s)", m.TeamID, m.UserID)) + } + conds := []*sqlf.Query{ + sqlf.Sprintf("(team_id, user_id) IN (%s)", sqlf.Join(ms, ",")), + } + + q := sqlf.Sprintf(deleteTeamMemberQueryFmtstr, sqlf.Join(conds, "AND")) + return s.Exec(ctx, q) +} + +const deleteTeamMemberQueryFmtstr = ` +DELETE FROM + team_members +WHERE %s +` + +var teamColumns = []*sqlf.Query{ + sqlf.Sprintf("teams.id"), + sqlf.Sprintf("teams.name"), + sqlf.Sprintf("teams.display_name"), + sqlf.Sprintf("teams.readonly"), + sqlf.Sprintf("teams.parent_team_id"), + sqlf.Sprintf("teams.creator_id"), + sqlf.Sprintf("teams.created_at"), + sqlf.Sprintf("teams.updated_at"), +} + +var teamInsertColumns = []*sqlf.Query{ + sqlf.Sprintf("name"), + sqlf.Sprintf("display_name"), + sqlf.Sprintf("readonly"), + sqlf.Sprintf("parent_team_id"), + sqlf.Sprintf("creator_id"), + sqlf.Sprintf("created_at"), + sqlf.Sprintf("updated_at"), +} + +var teamMemberColumns = []*sqlf.Query{ + sqlf.Sprintf("team_members.team_id"), + sqlf.Sprintf("team_members.user_id"), + sqlf.Sprintf("team_members.created_at"), + sqlf.Sprintf("team_members.updated_at"), +} + +var teamMemberStringColumns = []string{ + "team_members.team_id", + "team_members.user_id", + "team_members.created_at", + "team_members.updated_at", +} + +var teamMemberInsertColumns = []string{ + "team_id", + "user_id", + "created_at", + "updated_at", +} + +var scanTeams = basestore.NewSliceScanner(func(s dbutil.Scanner) (*types.Team, error) { + var t types.Team + err := scanTeam(s, &t) + return &t, err +}) + +func scanTeam(sc dbutil.Scanner, t *types.Team) error { + return sc.Scan( + &t.ID, + &t.Name, + &dbutil.NullString{S: &t.DisplayName}, + &t.ReadOnly, + &dbutil.NullInt32{N: &t.ParentTeamID}, + &dbutil.NullInt32{N: &t.CreatorID}, + &t.CreatedAt, + &t.UpdatedAt, + ) +} + +var scanTeamMembers = basestore.NewSliceScanner(func(s dbutil.Scanner) (*types.TeamMember, error) { + var t types.TeamMember + err := scanTeamMember(s, &t) + return &t, err +}) + +func scanTeamMember(sc dbutil.Scanner, tm *types.TeamMember) error { + return sc.Scan( + &tm.TeamID, + &tm.UserID, + &tm.CreatedAt, + &tm.UpdatedAt, + ) +} diff --git a/internal/database/teams_test.go b/internal/database/teams_test.go new file mode 100644 index 000000000000..4ddb2f1a16c2 --- /dev/null +++ b/internal/database/teams_test.go @@ -0,0 +1,447 @@ +package database + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/sourcegraph/log/logtest" + "github.com/stretchr/testify/require" + + "github.com/sourcegraph/sourcegraph/internal/actor" + "github.com/sourcegraph/sourcegraph/internal/database/dbtest" + "github.com/sourcegraph/sourcegraph/internal/errcode" + "github.com/sourcegraph/sourcegraph/internal/types" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +func TestTeams_CreateUpdateDelete(t *testing.T) { + ctx := actor.WithInternalActor(context.Background()) + logger := logtest.NoOp(t) + db := NewDB(logger, dbtest.NewDB(logger, t)) + user, err := db.Users().Create(ctx, NewUser{Username: "johndoe"}) + if err != nil { + t.Fatal(err) + } + + store := db.Teams() + + team := &types.Team{ + Name: "own", + DisplayName: "Sourcegraph Own", + ReadOnly: true, + CreatorID: user.ID, + } + if err := store.CreateTeam(ctx, team); err != nil { + t.Fatal(err) + } + + member := &types.TeamMember{TeamID: team.ID, UserID: user.ID} + + t.Run("create/remove team member", func(t *testing.T) { + if err := store.CreateTeamMember(ctx, member); err != nil { + t.Fatal(err) + } + + // Should not allow a second insert + if err := store.CreateTeamMember(ctx, member); err == nil { + t.Fatal("no error for reinsert") + } + + if err := store.DeleteTeamMember(ctx, member); err != nil { + t.Fatal(err) + } + + // Should allow a second delete without side-effects + if err := store.DeleteTeamMember(ctx, member); err != nil { + t.Fatal(err) + } + }) + + t.Run("duplicate team names are forbidden", func(t *testing.T) { + err := store.CreateTeam(ctx, team) + if err == nil { + t.Fatal("got no error") + } + if !errors.Is(err, ErrTeamNameAlreadyExists) { + t.Fatalf("invalid err returned %v", err) + } + }) + + t.Run("duplicate names with users are forbidden", func(t *testing.T) { + tm := &types.Team{ + Name: user.Username, + CreatorID: user.ID, + } + err := store.CreateTeam(ctx, tm) + if err == nil { + t.Fatal("got no error") + } + if !errors.Is(err, ErrTeamNameAlreadyExists) { + t.Fatalf("invalid err returned %v", err) + } + }) + + t.Run("duplicate names with orgs are forbidden", func(t *testing.T) { + name := "theorg" + _, err := db.Orgs().Create(ctx, name, nil) + if err != nil { + t.Fatal(err) + } + + tm := &types.Team{ + Name: name, + CreatorID: user.ID, + } + err = store.CreateTeam(ctx, tm) + if err == nil { + t.Fatal("got no error") + } + if !errors.Is(err, ErrTeamNameAlreadyExists) { + t.Fatalf("invalid err returned %v", err) + } + }) + + t.Run("update", func(t *testing.T) { + otherTeam := &types.Team{Name: "own2", CreatorID: user.ID} + err := store.CreateTeam(ctx, otherTeam) + if err != nil { + t.Fatal(err) + } + team.DisplayName = "" + team.ParentTeamID = otherTeam.ID + if err := store.UpdateTeam(ctx, team); err != nil { + t.Fatal(err) + } + require.Equal(t, otherTeam.ID, team.ParentTeamID) + // Should be properly unset in the DB. + require.Equal(t, "", team.DisplayName) + }) + + t.Run("delete", func(t *testing.T) { + if err := store.DeleteTeam(ctx, team.ID); err != nil { + t.Fatal(err) + } + _, err = store.GetTeamByID(ctx, team.ID) + if err == nil { + t.Fatal("team not deleted") + } + var tnfe TeamNotFoundError + if !errors.As(err, &tnfe) { + t.Fatalf("invalid error returned, expected not found got %v", err) + } + + // Check that we cannot delete the team a second time without error. + err = store.DeleteTeam(ctx, team.ID) + if err == nil { + t.Fatal("team deleted twice") + } + if !errors.As(err, &tnfe) { + t.Fatalf("invalid error returned, expected not found got %v", err) + } + + // Check that we can create a new team with the same name now. + err := store.CreateTeam(ctx, team) + if err != nil { + t.Fatal(err) + } + }) +} + +func TestTeams_GetListCount(t *testing.T) { + internalCtx := actor.WithInternalActor(context.Background()) + logger := logtest.NoOp(t) + db := NewDB(logger, dbtest.NewDB(logger, t)) + johndoe, err := db.Users().Create(internalCtx, NewUser{Username: "johndoe"}) + if err != nil { + t.Fatal(err) + } + alice, err := db.Users().Create(internalCtx, NewUser{Username: "alice"}) + if err != nil { + t.Fatal(err) + } + + store := db.Teams() + + createTeam := func(team *types.Team, members ...int32) *types.Team { + team.CreatorID = johndoe.ID + if err := store.CreateTeam(internalCtx, team); err != nil { + t.Fatal(err) + } + for _, m := range members { + if err := store.CreateTeamMember(internalCtx, &types.TeamMember{TeamID: team.ID, UserID: m}); err != nil { + t.Fatal(err) + } + } + return team + } + + engineeringTeam := createTeam(&types.Team{Name: "engineering"}, johndoe.ID) + salesTeam := createTeam(&types.Team{Name: "sales"}) + supportTeam := createTeam(&types.Team{Name: "support"}, johndoe.ID) + ownTeam := createTeam(&types.Team{Name: "sgown", ParentTeamID: engineeringTeam.ID}, alice.ID) + batchesTeam := createTeam(&types.Team{Name: "batches", ParentTeamID: engineeringTeam.ID}, johndoe.ID, alice.ID) + + t.Run("GetByID", func(t *testing.T) { + for _, want := range []*types.Team{engineeringTeam, salesTeam, supportTeam, ownTeam, batchesTeam} { + have, err := store.GetTeamByID(internalCtx, want.ID) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(want, have); diff != "" { + t.Fatal(diff) + } + } + t.Run("not found error", func(t *testing.T) { + _, err := store.GetTeamByID(internalCtx, 100000) + if err == nil { + t.Fatal("no error for not found team") + } + var tnfe TeamNotFoundError + if !errors.As(err, &tnfe) { + t.Fatalf("invalid error returned, expected not found got %v", err) + } + }) + }) + + t.Run("GetByName", func(t *testing.T) { + for _, want := range []*types.Team{engineeringTeam, salesTeam, supportTeam, ownTeam, batchesTeam} { + have, err := store.GetTeamByName(internalCtx, want.Name) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(want, have); diff != "" { + t.Fatal(diff) + } + } + t.Run("not found error", func(t *testing.T) { + _, err := store.GetTeamByName(internalCtx, "definitelynotateam") + if err == nil { + t.Fatal("no error for not found team") + } + var tnfe TeamNotFoundError + if !errors.As(err, &tnfe) { + t.Fatalf("invalid error returned, expected not found got %v", err) + } + }) + }) + + t.Run("ListCountTeams", func(t *testing.T) { + allTeams := []*types.Team{engineeringTeam, salesTeam, supportTeam, ownTeam, batchesTeam} + + // Get all. + haveTeams, haveCursor, err := store.ListTeams(internalCtx, ListTeamsOpts{}) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(allTeams, haveTeams); diff != "" { + t.Fatal(diff) + } + + if haveCursor != 0 { + t.Fatal("incorrect cursor returned") + } + + // Test cursor pagination. + var lastCursor int32 + for i := 0; i < len(allTeams); i++ { + t.Run(fmt.Sprintf("List 1 %s", allTeams[i].Name), func(t *testing.T) { + opts := ListTeamsOpts{LimitOffset: &LimitOffset{Limit: 1}, Cursor: lastCursor} + teams, c, err := store.ListTeams(internalCtx, opts) + if err != nil { + t.Fatal(err) + } + lastCursor = c + + if diff := cmp.Diff(allTeams[i], teams[0]); diff != "" { + t.Fatal(diff) + } + }) + } + + // Test global count. + have, err := store.CountTeams(internalCtx, ListTeamsOpts{}) + if err != nil { + t.Fatal(err) + } + if have, want := have, int32(len(allTeams)); have != want { + t.Fatalf("incorrect number of teams returned have=%d want=%d", have, want) + } + + t.Run("WithParentID", func(t *testing.T) { + engineeringTeams := []*types.Team{ownTeam, batchesTeam} + + // Get all. + haveTeams, haveCursor, err := store.ListTeams(internalCtx, ListTeamsOpts{WithParentID: engineeringTeam.ID}) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(engineeringTeams, haveTeams); diff != "" { + t.Fatal(diff) + } + + if haveCursor != 0 { + t.Fatal("incorrect cursor returned") + } + }) + + t.Run("RootOnly", func(t *testing.T) { + rootTeams := []*types.Team{engineeringTeam, salesTeam, supportTeam} + + // Get all. + haveTeams, haveCursor, err := store.ListTeams(internalCtx, ListTeamsOpts{RootOnly: true}) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(rootTeams, haveTeams); diff != "" { + t.Fatal(diff) + } + + if haveCursor != 0 { + t.Fatal("incorrect cursor returned") + } + }) + + t.Run("Search", func(t *testing.T) { + for _, team := range allTeams { + opts := ListTeamsOpts{Search: team.Name[:3]} + teams, _, err := store.ListTeams(internalCtx, opts) + if err != nil { + t.Fatal(err) + } + + if len(teams) != 1 { + t.Fatalf("expected exactly 1 team, got %d", len(teams)) + } + + if diff := cmp.Diff(team, teams[0]); diff != "" { + t.Fatal(diff) + } + } + }) + + t.Run("ForUserMember", func(t *testing.T) { + johnTeams := []*types.Team{engineeringTeam, supportTeam, batchesTeam} + aliceTeams := []*types.Team{ownTeam, batchesTeam} + + t.Run("johndoe", func(t *testing.T) { + haveTeams, haveCursor, err := store.ListTeams(internalCtx, ListTeamsOpts{ForUserMember: johndoe.ID}) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(johnTeams, haveTeams); diff != "" { + t.Fatal(diff) + } + + if haveCursor != 0 { + t.Fatal("incorrect cursor returned") + } + }) + + t.Run("alice", func(t *testing.T) { + haveTeams, haveCursor, err := store.ListTeams(internalCtx, ListTeamsOpts{ForUserMember: alice.ID}) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(aliceTeams, haveTeams); diff != "" { + t.Fatal(diff) + } + + if haveCursor != 0 { + t.Fatal("incorrect cursor returned") + } + }) + }) + }) + + t.Run("ListCountTeamMembers", func(t *testing.T) { + allTeams := map[*types.Team][]int32{ + engineeringTeam: {johndoe.ID}, + salesTeam: {}, + batchesTeam: {johndoe.ID, alice.ID}, + } + + for team, wantMembers := range allTeams { + haveMemberTypes, haveCursor, err := store.ListTeamMembers(internalCtx, ListTeamMembersOpts{TeamID: team.ID}) + if err != nil { + t.Fatal(err) + } + + haveMembers := []int32{} + for _, member := range haveMemberTypes { + haveMembers = append(haveMembers, member.UserID) + } + + if diff := cmp.Diff(wantMembers, haveMembers); diff != "" { + t.Fatal(diff) + } + + if haveCursor != nil { + t.Fatal("incorrect cursor returned") + } + + have, err := store.CountTeamMembers(internalCtx, ListTeamMembersOpts{TeamID: team.ID}) + if err != nil { + t.Fatal(err) + } + if have, want := have, int32(len(wantMembers)); have != want { + t.Fatalf("incorrect number of teams returned have=%d want=%d", have, want) + } + + // Test cursor pagination. + var lastCursor TeamMemberListCursor + for i := 0; i < len(wantMembers); i++ { + t.Run(fmt.Sprintf("List 1 %s", team.Name), func(t *testing.T) { + opts := ListTeamMembersOpts{LimitOffset: &LimitOffset{Limit: 1}, Cursor: lastCursor, TeamID: team.ID} + members, c, err := store.ListTeamMembers(internalCtx, opts) + if err != nil { + t.Fatal(err) + } + if c != nil { + lastCursor = *c + } else { + lastCursor = TeamMemberListCursor{} + } + + if len(members) != 1 { + t.Fatalf("expected exactly 1 member, got %d", len(members)) + } + + if diff := cmp.Diff(wantMembers[i], members[0].UserID); diff != "" { + t.Fatal(diff) + } + }) + } + } + + t.Run("Search", func(t *testing.T) { + // Search for john in the team that contains both john and alice: batchesTeam + opts := ListTeamMembersOpts{TeamID: batchesTeam.ID, Search: johndoe.Username[:3]} + members, _, err := store.ListTeamMembers(internalCtx, opts) + if err != nil { + t.Fatal(err) + } + + if len(members) != 1 { + t.Fatalf("expected exactly 1 member, got %d", len(members)) + } + + if diff := cmp.Diff(johndoe.ID, members[0].UserID); diff != "" { + t.Fatal(diff) + } + }) + }) +} + +func TestTeamNotFoundError(t *testing.T) { + err := TeamNotFoundError{} + if have := errcode.IsNotFound(err); !have { + t.Error("TeamNotFoundError does not say it represents a not found error") + } +} diff --git a/internal/types/types.go b/internal/types/types.go index 4dd2056f0dff..972f2975e01d 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1794,3 +1794,21 @@ type SlowRequest struct { Query string `json:"query"` Filepath string `json:"filepath"` } + +type Team struct { + ID int32 + Name string + DisplayName string + ReadOnly bool + ParentTeamID int32 + CreatorID int32 + CreatedAt time.Time + UpdatedAt time.Time +} + +type TeamMember struct { + UserID int32 + TeamID int32 + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/migrations/frontend/1671463799_teams/down.sql b/migrations/frontend/1671463799_teams/down.sql new file mode 100644 index 000000000000..ff06fcfb11bd --- /dev/null +++ b/migrations/frontend/1671463799_teams/down.sql @@ -0,0 +1,9 @@ +ALTER TABLE names DROP CONSTRAINT IF EXISTS names_check; + +ALTER TABLE names DROP COLUMN IF EXISTS team_id; + +ALTER TABLE names ADD CONSTRAINT names_check CHECK (user_id IS NOT NULL OR org_id IS NOT NULL); + +DROP TABLE IF EXISTS team_members; + +DROP TABLE IF EXISTS teams; diff --git a/migrations/frontend/1671463799_teams/metadata.yaml b/migrations/frontend/1671463799_teams/metadata.yaml new file mode 100644 index 000000000000..d434c289fa21 --- /dev/null +++ b/migrations/frontend/1671463799_teams/metadata.yaml @@ -0,0 +1,2 @@ +name: Teams +parents: [1669645608, 1670870072, 1670600028] diff --git a/migrations/frontend/1671463799_teams/up.sql b/migrations/frontend/1671463799_teams/up.sql new file mode 100644 index 000000000000..8a4042d084da --- /dev/null +++ b/migrations/frontend/1671463799_teams/up.sql @@ -0,0 +1,25 @@ +CREATE TABLE IF NOT EXISTS teams ( + id SERIAL PRIMARY KEY, + name citext NOT NULL CONSTRAINT teams_name_max_length CHECK (char_length(name::text) <= 255) CONSTRAINT teams_name_valid_chars CHECK (name ~ '^[a-zA-Z0-9](?:[a-zA-Z0-9]|[-.](?=[a-zA-Z0-9]))*-?$'::citext), + display_name text CONSTRAINT teams_display_name_max_length CHECK (char_length(display_name) <= 255), + readonly boolean NOT NULL DEFAULT false, + parent_team_id integer REFERENCES teams(id) ON DELETE CASCADE, + creator_id integer NOT NULL REFERENCES users(id) ON DELETE SET NULL, + created_at timestamp with time zone NOT NULL DEFAULT now(), + updated_at timestamp with time zone NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX IF NOT EXISTS teams_name ON teams(name); + +CREATE TABLE IF NOT EXISTS team_members ( + team_id integer NOT NULL REFERENCES teams(id) ON DELETE CASCADE, + user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at timestamp with time zone NOT NULL DEFAULT now(), + updated_at timestamp with time zone NOT NULL DEFAULT now(), + CONSTRAINT team_members_team_id_user_id_key UNIQUE (team_id, user_id), + PRIMARY KEY(team_id, user_id) +); + +ALTER TABLE names ADD COLUMN IF NOT EXISTS team_id integer REFERENCES teams(id) ON UPDATE CASCADE ON DELETE CASCADE; +ALTER TABLE names DROP CONSTRAINT IF EXISTS names_check; +ALTER TABLE names ADD CONSTRAINT names_check CHECK (user_id IS NOT NULL OR org_id IS NOT NULL OR team_id IS NOT NULL); diff --git a/migrations/frontend/squashed.sql b/migrations/frontend/squashed.sql index 761fc343a20a..3953cb99c838 100755 --- a/migrations/frontend/squashed.sql +++ b/migrations/frontend/squashed.sql @@ -2952,7 +2952,8 @@ CREATE TABLE names ( name citext NOT NULL, user_id integer, org_id integer, - CONSTRAINT names_check CHECK (((user_id IS NOT NULL) OR (org_id IS NOT NULL))) + team_id integer, + CONSTRAINT names_check CHECK (((user_id IS NOT NULL) OR (org_id IS NOT NULL) OR (team_id IS NOT NULL))) ); CREATE TABLE namespace_permissions ( @@ -3765,6 +3766,37 @@ CREATE SEQUENCE survey_responses_id_seq ALTER SEQUENCE survey_responses_id_seq OWNED BY survey_responses.id; +CREATE TABLE team_members ( + team_id integer NOT NULL, + user_id integer NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + +CREATE TABLE teams ( + id integer NOT NULL, + name citext NOT NULL, + display_name text, + readonly boolean DEFAULT false NOT NULL, + parent_team_id integer, + creator_id integer NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT teams_display_name_max_length CHECK ((char_length(display_name) <= 255)), + CONSTRAINT teams_name_max_length CHECK ((char_length((name)::text) <= 255)), + CONSTRAINT teams_name_valid_chars CHECK ((name OPERATOR(~) '^[a-zA-Z0-9](?:[a-zA-Z0-9]|[-.](?=[a-zA-Z0-9]))*-?$'::citext)) +); + +CREATE SEQUENCE teams_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE teams_id_seq OWNED BY teams.id; + CREATE TABLE temporary_settings ( id integer NOT NULL, user_id integer NOT NULL, @@ -4142,6 +4174,8 @@ ALTER TABLE ONLY settings ALTER COLUMN id SET DEFAULT nextval('settings_id_seq': ALTER TABLE ONLY survey_responses ALTER COLUMN id SET DEFAULT nextval('survey_responses_id_seq'::regclass); +ALTER TABLE ONLY teams ALTER COLUMN id SET DEFAULT nextval('teams_id_seq'::regclass); + ALTER TABLE ONLY temporary_settings ALTER COLUMN id SET DEFAULT nextval('temporary_settings_id_seq'::regclass); ALTER TABLE ONLY user_credentials ALTER COLUMN id SET DEFAULT nextval('user_credentials_id_seq'::regclass); @@ -4513,6 +4547,12 @@ ALTER TABLE ONLY settings ALTER TABLE ONLY survey_responses ADD CONSTRAINT survey_responses_pkey PRIMARY KEY (id); +ALTER TABLE ONLY team_members + ADD CONSTRAINT team_members_team_id_user_id_key PRIMARY KEY (team_id, user_id); + +ALTER TABLE ONLY teams + ADD CONSTRAINT teams_pkey PRIMARY KEY (id); + ALTER TABLE ONLY temporary_settings ADD CONSTRAINT temporary_settings_pkey PRIMARY KEY (id); @@ -4941,6 +4981,8 @@ CREATE UNIQUE INDEX sub_repo_permissions_repo_id_user_id_version_uindex ON sub_r CREATE INDEX sub_repo_perms_user_id ON sub_repo_permissions USING btree (user_id); +CREATE UNIQUE INDEX teams_name ON teams USING btree (name); + CREATE INDEX user_credentials_credential_idx ON user_credentials USING btree (((encryption_key_id = ANY (ARRAY[''::text, 'previously-migrated'::text])))); CREATE UNIQUE INDEX user_emails_user_id_is_primary_idx ON user_emails USING btree (user_id, is_primary) WHERE (is_primary = true); @@ -5276,6 +5318,9 @@ ALTER TABLE ONLY lsif_uploads_reference_counts ALTER TABLE ONLY names ADD CONSTRAINT names_org_id_fkey FOREIGN KEY (org_id) REFERENCES orgs(id) ON UPDATE CASCADE ON DELETE CASCADE; +ALTER TABLE ONLY names + ADD CONSTRAINT names_team_id_fkey FOREIGN KEY (team_id) REFERENCES teams(id) ON UPDATE CASCADE ON DELETE CASCADE; + ALTER TABLE ONLY names ADD CONSTRAINT names_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE; @@ -5420,6 +5465,18 @@ ALTER TABLE ONLY sub_repo_permissions ALTER TABLE ONLY survey_responses ADD CONSTRAINT survey_responses_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); +ALTER TABLE ONLY team_members + ADD CONSTRAINT team_members_team_id_fkey FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE; + +ALTER TABLE ONLY team_members + ADD CONSTRAINT team_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + +ALTER TABLE ONLY teams + ADD CONSTRAINT teams_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL; + +ALTER TABLE ONLY teams + ADD CONSTRAINT teams_parent_team_id_fkey FOREIGN KEY (parent_team_id) REFERENCES teams(id) ON DELETE CASCADE; + ALTER TABLE ONLY temporary_settings ADD CONSTRAINT temporary_settings_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/mockgen.temp.yaml b/mockgen.temp.yaml index ebba18debc2a..0af261eb22e9 100644 --- a/mockgen.temp.yaml +++ b/mockgen.temp.yaml @@ -76,6 +76,7 @@ - ExecutorSecretAccessLogStore - ZoektReposStore - PermissionSyncJobStore + - TeamStore - filename: internal/gitserver/mocks_temp.go path: github.com/sourcegraph/sourcegraph/internal/gitserver interfaces: From fc5131c23bba44d55b4e99653d231ab4a0b05035 Mon Sep 17 00:00:00 2001 From: Erik Seliger Date: Fri, 27 Jan 2023 20:30:06 +0100 Subject: [PATCH 227/678] Bump github.com/rjeczalik/notify dependency (#47042) This fixes an issue that I saw locally: ``` # github.com/rjeczalik/notify cgo-gcc-prolog:217:2: warning: 'FSEventStreamScheduleWithRunLoop' is deprecated: first deprecated in macOS 13.0 - Use FSEventStreamSetDispatchQueue instead. [-Wdeprecated-declarations] /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/CoreServices.framework/Frameworks/FSEvents.framework/Headers/FSEvents.h:1138:1: note: 'FSEventStreamScheduleWithRunLoop' has been explicitly marked deprecated here ``` --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 52ff5fca9904..6983db2a969b 100644 --- a/go.mod +++ b/go.mod @@ -114,7 +114,7 @@ require ( github.com/prometheus/common v0.37.0 github.com/qustavo/sqlhooks/v2 v2.1.0 github.com/rafaeljusto/redigomock v2.4.0+incompatible - github.com/rjeczalik/notify v0.9.2 + github.com/rjeczalik/notify v0.9.3 github.com/russellhaering/gosaml2 v0.7.0 github.com/russellhaering/goxmldsig v1.2.0 github.com/schollz/progressbar/v3 v3.8.5 diff --git a/go.sum b/go.sum index b1cac76465a5..53c2e3599e6c 100644 --- a/go.sum +++ b/go.sum @@ -1982,8 +1982,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8= -github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= +github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= +github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= From c69207f871f35a8872dbfecb7d5490e76266bfb7 Mon Sep 17 00:00:00 2001 From: Eric Fritz Date: Fri, 27 Jan 2023 13:57:12 -0600 Subject: [PATCH 228/678] scip: Improve OOM conditions for LSIF migration (#47040) --- .../migrations/codeintel/scip_migrator.go | 27 +++++++++---------- .../codeintel/scip_migrator_test.go | 6 ++--- lib/codeintel/lsif/scip/document.go | 10 ++++++- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/enterprise/internal/oobmigration/migrations/codeintel/scip_migrator.go b/enterprise/internal/oobmigration/migrations/codeintel/scip_migrator.go index 0d8b45341f52..a572a5c63c80 100644 --- a/enterprise/internal/oobmigration/migrations/codeintel/scip_migrator.go +++ b/enterprise/internal/oobmigration/migrations/codeintel/scip_migrator.go @@ -80,14 +80,16 @@ func getEnv(name string, defaultValue int) int { var ( // NOTE: modified in tests scipMigratorConcurrencyLevel = getEnv("SCIP_MIGRATOR_CONCURRENCY_LEVEL", 1) - scipMigratorUploadBatchSize = getEnv("SCIP_MIGRATOR_UPLOAD_BATCH_SIZE", 32) - scipMigratorDocumentBatchSize = 64 - scipMigratorResultChunkDefaultCacheSize = 8192 + scipMigratorUploadReaderBatchSize = getEnv("SCIP_MIGRATOR_UPLOAD_BATCH_SIZE", 32) + scipMigratorResultChunkReaderCacheSize = 8192 + scipMigratorDocumentReaderBatchSize = 64 + scipMigratorDocumentWriterBatchSize = 256 + scipMigratorDocumentWriterMaxPayloadSum = 1024 * 1024 * 32 ) func (m *scipMigrator) Up(ctx context.Context) error { - ch := make(chan struct{}, scipMigratorUploadBatchSize) - for i := 0; i < scipMigratorUploadBatchSize; i++ { + ch := make(chan struct{}, scipMigratorUploadReaderBatchSize) + for i := 0; i < scipMigratorUploadReaderBatchSize; i++ { ch <- struct{}{} } close(ch) @@ -204,7 +206,7 @@ func migrateUpload( return nil } - resultChunkCacheSize := scipMigratorResultChunkDefaultCacheSize + resultChunkCacheSize := scipMigratorResultChunkReaderCacheSize if numResultChunks < resultChunkCacheSize { resultChunkCacheSize = numResultChunks } @@ -236,8 +238,8 @@ func migrateUpload( documentsByPath, err := scanDocuments(codeintelTx.Query(ctx, sqlf.Sprintf( scipMigratorScanDocumentsQuery, uploadID, - scipMigratorDocumentBatchSize, - page*scipMigratorDocumentBatchSize, + scipMigratorDocumentReaderBatchSize, + page*scipMigratorDocumentReaderBatchSize, ))) if err != nil { return err @@ -568,7 +570,7 @@ func (s *scipWriter) InsertDocument( path string, scipDocument *ogscip.Document, ) error { - if s.batchPayloadSum >= MaxBatchPayloadSum { + if s.batchPayloadSum >= scipMigratorDocumentWriterMaxPayloadSum { if err := s.flush(ctx); err != nil { return err } @@ -598,7 +600,7 @@ func (s *scipWriter) InsertDocument( }) s.batchPayloadSum += len(compressedPayload) - if len(s.batch) >= DocumentsBatchSize { + if len(s.batch) >= scipMigratorDocumentWriterBatchSize { if err := s.flush(ctx); err != nil { return err } @@ -607,11 +609,6 @@ func (s *scipWriter) InsertDocument( return nil } -const ( - DocumentsBatchSize = 256 - MaxBatchPayloadSum = 1024 * 1024 * 32 -) - func (s *scipWriter) flush(ctx context.Context) (err error) { documents := s.batch s.batch = nil diff --git a/enterprise/internal/oobmigration/migrations/codeintel/scip_migrator_test.go b/enterprise/internal/oobmigration/migrations/codeintel/scip_migrator_test.go index f74e0062d34f..d9c5fa43cca2 100644 --- a/enterprise/internal/oobmigration/migrations/codeintel/scip_migrator_test.go +++ b/enterprise/internal/oobmigration/migrations/codeintel/scip_migrator_test.go @@ -14,9 +14,9 @@ import ( ) func init() { - scipMigratorUploadBatchSize = 1 - scipMigratorDocumentBatchSize = 4 - scipMigratorResultChunkDefaultCacheSize = 16 + scipMigratorUploadReaderBatchSize = 1 + scipMigratorDocumentReaderBatchSize = 4 + scipMigratorResultChunkReaderCacheSize = 16 } func TestSCIPMigrator(t *testing.T) { diff --git a/lib/codeintel/lsif/scip/document.go b/lib/codeintel/lsif/scip/document.go index 469d42624a39..9ce4837110b3 100644 --- a/lib/codeintel/lsif/scip/document.go +++ b/lib/codeintel/lsif/scip/document.go @@ -129,6 +129,8 @@ type symbolMetadata struct { implementationRelationships []string } +const maxDefinitionsPerDefinitionResult = 16 + // convertRange converts an LSIF range into an equivalent set of SCIP occurrences. The output of this function // is a slice of occurrences, as multiple moniker names/relationships translate to distinct occurrence objects, // as well as a slice of additional symbol metadata that should be aggregated and persisted into the enclosing @@ -224,7 +226,13 @@ func convertRange( } else { role := scip.SymbolRole_UnspecifiedSymbolRole - for _, targetRangeID := range targetRangeFetcher(r.DefinitionResultID) { + targetRanges := targetRangeFetcher(r.DefinitionResultID) + sort.Slice(targetRanges, func(i, j int) bool { return targetRanges[i] < targetRanges[j] }) + if len(targetRanges) > maxDefinitionsPerDefinitionResult { + targetRanges = targetRanges[:maxDefinitionsPerDefinitionResult] + } + + for _, targetRangeID := range targetRanges { // Add reference to the defining range identifier addOccurrence(constructSymbolName(uploadID, targetRangeID), role) } From 8a43ea52956b0ba6ea8cad8c2b664bac23d28ab3 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 27 Jan 2023 12:04:33 -0800 Subject: [PATCH 229/678] Add tutorials page back (#47045) --- doc/tutorials/index.md | 93 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 doc/tutorials/index.md diff --git a/doc/tutorials/index.md b/doc/tutorials/index.md new file mode 100644 index 000000000000..461a5b860874 --- /dev/null +++ b/doc/tutorials/index.md @@ -0,0 +1,93 @@ +# Sourcegraph User Tutorials + +> ⚠️ Note: Work in Progress. Full Content & Links Coming Soon. + + +## Find Your Way Around Sourcegraph +| Topic | Description | +| ----------- | ----------- | +| Home | TODO | +| Notebooks | TODO | +| Code Monitoring | TODO | +| Settings & Configuration | TODO | + +## Setting Up Your Sourcegraph User Environment +| Topic | Description | +| -------- | --------| +| Customizing your settings | TODO | +| Using Sourcegraph with your IDE | TODO | +| Using the Sourcegraph browser extension on your code | TODO |host +| Using the Sourcegraph CLI | TODO | +| Saving Searches | TODO | +| Search Contexts | TODO | + +## Get Started Searching +| Topic | Description | +| -------- | --------| +| Search features | TODO | +| Search types | TODO | +| Saved searches | TODO | +| Search contexts | TODO | +| Search Query Syntax | TODO | + +## More Advanced Searching +| Topic | Description | +| -------- | --------| +| Commits | TODO | +| Multi branch | TODO | +| AND/OR | TODO | +| Other more advanced filters (time, diff, author, etc) | TODO | +| Advanced Regex | TODO | +| Structural Search | TODO | + +## Search Scenarios +| Topic | Description | +| -------- | --------| +| How to Space | TODO | + +## Code Navigation +| Topic | Description | +| -------- | --------| +| Search vs Precise | TODO | +| Language Support | TODO | +| Definition & Implementation | TODO | +| References & dependencies | TODO | +| Symbol search | TODO | + + +## Search Notebooks +| Topic | Description | +| -------- | --------| +| Web-based vs File-based | Description | +| Blocks | Description | + + +## Code Insights +| Topic | Description | +| -------- | --------| +| Report Types | TODO | +| Filters | TODO | +| Dashboards | TODO | +| Access & Sharing | TODO | +| Link to how-to space | TODO | + + +## Batch Changes +| Topic | Description | +| -------- | --------| +| Workflow | TODO | +| Creating | TODO | +| Viewing | TODO | +| Publishing | TODO | +| Updating | TODO | +| Error Handling | TODO | +| Link to how-to space | TODO | + + +## Using GraphQL +| Topic | Description | +| -------- | --------| +| UI vs API | TODO | +| Access / permissions | TODO | +| Examples | TODO | + From 0584e283e0bcdd79a5719eaa7b55bcc115686192 Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Fri, 27 Jan 2023 22:22:22 +0200 Subject: [PATCH 230/678] rcache: make it possible for redispool.Cache to change (#47031) This is needed to work around tricky we have around things being declared at init() time, which is fairly common with redis usages in our codebase. Also removed the no longer needed backwards.go. Test Plan: go test --- internal/rcache/BUILD.bazel | 1 - internal/rcache/backwards.go | 37 ---------------------------------- internal/rcache/fifo_list.go | 10 ++++----- internal/rcache/rcache.go | 37 +++++++++++++++++++++------------- internal/rcache/rcache_test.go | 7 ++++++- 5 files changed, 34 insertions(+), 58 deletions(-) delete mode 100644 internal/rcache/backwards.go diff --git a/internal/rcache/BUILD.bazel b/internal/rcache/BUILD.bazel index a6014412dae0..1b69c32e206a 100644 --- a/internal/rcache/BUILD.bazel +++ b/internal/rcache/BUILD.bazel @@ -3,7 +3,6 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "rcache", srcs = [ - "backwards.go", "fifo_list.go", "rcache.go", ], diff --git a/internal/rcache/backwards.go b/internal/rcache/backwards.go deleted file mode 100644 index a6601a1b7bf1..000000000000 --- a/internal/rcache/backwards.go +++ /dev/null @@ -1,37 +0,0 @@ -package rcache - -import ( - "time" - - "github.com/sourcegraph/sourcegraph/lib/errors" - - "github.com/gomodule/redigo/redis" -) - -// poolGet is temporary wrapper around getting a raw redis connection. It just -// fails if redis is disabled. We intend to remove this wrapper in the future -// and instead fallback to in-memory options for the Sourcegraph App. -func poolGet() redis.Conn { - pool, ok := pool.Pool() - if !ok { - return errorConn{err: errRedisDisable} - } - - return pool.Get() -} - -var errRedisDisable = errors.New("redis is disabled") - -// copy pasta from redigo/redis -type errorConn struct{ err error } - -func (ec errorConn) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err } -func (ec errorConn) DoWithTimeout(time.Duration, string, ...interface{}) (interface{}, error) { - return nil, ec.err -} -func (ec errorConn) Send(string, ...interface{}) error { return ec.err } -func (ec errorConn) Err() error { return ec.err } -func (ec errorConn) Close() error { return nil } -func (ec errorConn) Flush() error { return ec.err } -func (ec errorConn) Receive() (interface{}, error) { return nil, ec.err } -func (ec errorConn) ReceiveWithTimeout(time.Duration) (interface{}, error) { return nil, ec.err } diff --git a/internal/rcache/fifo_list.go b/internal/rcache/fifo_list.go index d79f7dc9572b..b8311b2b403b 100644 --- a/internal/rcache/fifo_list.go +++ b/internal/rcache/fifo_list.go @@ -43,19 +43,19 @@ func (l *FIFOList) Insert(b []byte) error { // disabling. maxSize := l.MaxSize() if maxSize == 0 { - if err := pool.LTrim(key, 0, 0); err != nil { + if err := kv().LTrim(key, 0, 0); err != nil { return errors.Wrap(err, "failed to execute redis command LTRIM") } return nil } // O(1) because we're just adding a single element. - if err := pool.LPush(key, b); err != nil { + if err := kv().LPush(key, b); err != nil { return errors.Wrap(err, "failed to execute redis command LPUSH") } // O(1) because the average case if just about dropping the last element. - if err := pool.LTrim(key, 0, maxSize-1); err != nil { + if err := kv().LTrim(key, 0, maxSize-1); err != nil { return errors.Wrap(err, "failed to execute redis command LTRIM") } return nil @@ -63,7 +63,7 @@ func (l *FIFOList) Insert(b []byte) error { func (l *FIFOList) Size() (int, error) { key := l.globalPrefixKey() - n, err := pool.LLen(key) + n, err := kv().LLen(key) if err != nil { return 0, errors.Wrap(err, "failed to execute redis command LLEN") } @@ -95,7 +95,7 @@ func (l *FIFOList) Slice(ctx context.Context, from, to int) ([][]byte, error) { } key := l.globalPrefixKey() - bs, err := pool.WithContext(ctx).LRange(key, from, to).ByteSlices() + bs, err := kv().WithContext(ctx).LRange(key, from, to).ByteSlices() if err != nil { // Return ctx error if it expired if ctx.Err() != nil { diff --git a/internal/rcache/rcache.go b/internal/rcache/rcache.go index 7a81886328a7..5bde3151160a 100644 --- a/internal/rcache/rcache.go +++ b/internal/rcache/rcache.go @@ -52,7 +52,7 @@ func (r *Cache) TTL() time.Duration { return time.Duration(r.ttlSeconds) * time. // Get implements httpcache.Cache.Get func (r *Cache) Get(key string) ([]byte, bool) { - b, err := pool.Get(r.rkeyPrefix() + key).Bytes() + b, err := kv().Get(r.rkeyPrefix() + key).Bytes() if err != nil && err != redis.ErrNil { log15.Warn("failed to execute redis command", "cmd", "GET", "error", err) } @@ -67,7 +67,7 @@ func (r *Cache) Set(key string, b []byte) { } if r.ttlSeconds == 0 { - err := pool.Set(r.rkeyPrefix()+key, b) + err := kv().Set(r.rkeyPrefix()+key, b) if err != nil { log15.Warn("failed to execute redis command", "cmd", "SET", "error", err) } @@ -81,14 +81,14 @@ func (r *Cache) SetWithTTL(key string, b []byte, ttl int) { log15.Error("rcache: keys must be valid utf8", "key", []byte(key)) } - err := pool.SetEx(r.rkeyPrefix()+key, ttl, b) + err := kv().SetEx(r.rkeyPrefix()+key, ttl, b) if err != nil { log15.Warn("failed to execute redis command", "cmd", "SETEX", "error", err) } } func (r *Cache) Increase(key string) { - err := pool.Incr(r.rkeyPrefix() + key) + err := kv().Incr(r.rkeyPrefix() + key) if err != nil { log15.Warn("failed to execute redis command", "cmd", "INCR", "error", err) return @@ -98,7 +98,7 @@ func (r *Cache) Increase(key string) { return } - err = pool.Expire(r.rkeyPrefix()+key, r.ttlSeconds) + err = kv().Expire(r.rkeyPrefix()+key, r.ttlSeconds) if err != nil { log15.Warn("failed to execute redis command", "cmd", "EXPIRE", "error", err) return @@ -106,7 +106,7 @@ func (r *Cache) Increase(key string) { } func (r *Cache) KeyTTL(key string) (int, bool) { - ttl, err := pool.TTL(r.rkeyPrefix() + key) + ttl, err := kv().TTL(r.rkeyPrefix() + key) if err != nil { log15.Warn("failed to execute redis command", "cmd", "TTL", "error", err) return -1, false @@ -124,22 +124,22 @@ func (r *Cache) FIFOList(key string, maxSize int) *FIFOList { // If the key already exists and is a different type, an error is returned. // If the hash key does not exist, it is created. If it exists, the value is overwritten. func (r *Cache) SetHashItem(key string, hashKey string, hashValue string) error { - return pool.HSet(r.rkeyPrefix()+key, hashKey, hashValue) + return kv().HSet(r.rkeyPrefix()+key, hashKey, hashValue) } // GetHashItem gets a key in a HASH. func (r *Cache) GetHashItem(key string, hashKey string) (string, error) { - return pool.HGet(r.rkeyPrefix()+key, hashKey).String() + return kv().HGet(r.rkeyPrefix()+key, hashKey).String() } // GetHashAll returns the members of the HASH stored at `key`, in no particular order. func (r *Cache) GetHashAll(key string) (map[string]string, error) { - return pool.HGetAll(r.rkeyPrefix() + key).StringMap() + return kv().HGetAll(r.rkeyPrefix() + key).StringMap() } // Delete implements httpcache.Cache.Delete func (r *Cache) Delete(key string) { - err := pool.Del(r.rkeyPrefix() + key) + err := kv().Del(r.rkeyPrefix() + key) if err != nil { log15.Warn("failed to execute redis command", "cmd", "DEL", "error", err) } @@ -162,7 +162,7 @@ type TB interface { func SetupForTest(t TB) { t.Helper() - pool = redispool.RedisKeyValue(&redis.Pool{ + pool := &redis.Pool{ MaxIdle: 3, IdleTimeout: 240 * time.Second, Dial: func() (redis.Conn, error) { @@ -172,10 +172,11 @@ func SetupForTest(t TB) { _, err := c.Do("PING") return err }, - }) + } + kvMock = redispool.RedisKeyValue(pool) globalPrefix = "__test__" + t.Name() - c := poolGet() + c := pool.Get() defer c.Close() // If we are not on CI, skip the test if our redis connection fails. @@ -222,7 +223,15 @@ return result return err } +var kvMock redispool.KeyValue + +func kv() redispool.KeyValue { + if kvMock != nil { + return kvMock + } + return redispool.Cache +} + var ( - pool = redispool.Cache globalPrefix = dataVersion ) diff --git a/internal/rcache/rcache_test.go b/internal/rcache/rcache_test.go index 3025f0bfb16f..610b82fb1b4c 100644 --- a/internal/rcache/rcache_test.go +++ b/internal/rcache/rcache_test.go @@ -116,7 +116,12 @@ func TestCache_deleteAllKeysWithPrefix(t *testing.T) { c.Set(key, []byte(strconv.Itoa(i))) } - conn := poolGet() + pool, ok := kv().Pool() + if !ok { + t.Fatal("need redis connection") + } + + conn := pool.Get() defer conn.Close() err := deleteAllKeysWithPrefix(conn, c.rkeyPrefix()+"a") From b801717a45573dc55bd303e57924a5755ebbb365 Mon Sep 17 00:00:00 2001 From: Vova Kulikov Date: Fri, 27 Jan 2023 17:30:37 -0300 Subject: [PATCH 231/678] Code Insights: Use download blob flow for the insight export data action (#47027) Use download blob flow for the insight export data action --- client/web/src/components/LoaderButton.tsx | 4 +- .../components/DownloadFileButton.tsx | 56 +++++++++++++++++++ .../modals/ExportInsightDataModal.tsx | 16 +++--- .../CodeInsightIndependentPageActions.tsx | 10 +++- 4 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 client/web/src/enterprise/insights/components/DownloadFileButton.tsx diff --git a/client/web/src/components/LoaderButton.tsx b/client/web/src/components/LoaderButton.tsx index 3b3dc2781b51..4aed1b5e6c17 100644 --- a/client/web/src/components/LoaderButton.tsx +++ b/client/web/src/components/LoaderButton.tsx @@ -4,13 +4,13 @@ import classNames from 'classnames' import { LoadingSpinner, Button, ButtonProps } from '@sourcegraph/wildcard' -interface Props extends ButtonProps { +export interface LoaderButtonProps extends ButtonProps { loading: boolean label: string alwaysShowLabel: boolean } -export const LoaderButton: React.FunctionComponent>> = ({ +export const LoaderButton: React.FunctionComponent>> = ({ loading, label, alwaysShowLabel, diff --git a/client/web/src/enterprise/insights/components/DownloadFileButton.tsx b/client/web/src/enterprise/insights/components/DownloadFileButton.tsx new file mode 100644 index 000000000000..84a8befde2cf --- /dev/null +++ b/client/web/src/enterprise/insights/components/DownloadFileButton.tsx @@ -0,0 +1,56 @@ +import { FC, MouseEvent, useState } from 'react' + +import { ButtonProps } from '@sourcegraph/wildcard' + +import { LoaderButton } from '../../../components/LoaderButton' + +interface DownloadFileButtonProps extends ButtonProps { + fileUrl: string + fileName: string + children?: string +} + +export const DownloadFileButton: FC = props => { + const { fileUrl, fileName, children, onClick, ...attributes } = props + + const [isLoading, setLoading] = useState(false) + + const handleClick = async (event: MouseEvent): Promise => { + setLoading(true) + + try { + const file = await fetch(fileUrl, { headers: window.context.xhrHeaders }) + const fileBlob = await file.blob() + const url = URL.createObjectURL(fileBlob) + + syntheticDownload(url, fileName) + + if (onClick) { + onClick(event) + } + } finally { + setLoading(false) + } + } + + return ( + + ) +} + +function syntheticDownload(url: string, name: string): void { + const element = document.createElement('a') + element.setAttribute('href', url) + element.setAttribute('download', name) + + document.body.append(element) + element.click() + + element.remove() +} diff --git a/client/web/src/enterprise/insights/components/modals/ExportInsightDataModal.tsx b/client/web/src/enterprise/insights/components/modals/ExportInsightDataModal.tsx index 88b0d43330bb..d2e5ae9101c5 100644 --- a/client/web/src/enterprise/insights/components/modals/ExportInsightDataModal.tsx +++ b/client/web/src/enterprise/insights/components/modals/ExportInsightDataModal.tsx @@ -1,6 +1,10 @@ import { FC } from 'react' -import { Button, Modal, Text, H2 } from '@sourcegraph/wildcard' +import { escapeRegExp } from 'lodash' + +import { Modal, Text, H2 } from '@sourcegraph/wildcard' + +import { DownloadFileButton } from '../DownloadFileButton' interface ExportInsightDataModalProps { insightId: string @@ -22,16 +26,14 @@ export const ExportInsightDataModal: FC = props => This will only include data that you are permitted to see.
- +
) diff --git a/client/web/src/enterprise/insights/pages/insights/insight/components/actions/CodeInsightIndependentPageActions.tsx b/client/web/src/enterprise/insights/pages/insights/insight/components/actions/CodeInsightIndependentPageActions.tsx index 59b9fd62b4a9..8cb9bae98074 100644 --- a/client/web/src/enterprise/insights/pages/insights/insight/components/actions/CodeInsightIndependentPageActions.tsx +++ b/client/web/src/enterprise/insights/pages/insights/insight/components/actions/CodeInsightIndependentPageActions.tsx @@ -1,11 +1,13 @@ import { FunctionComponent, useRef, useState } from 'react' import { mdiLinkVariant } from '@mdi/js' +import { escapeRegExp } from 'lodash' import { useHistory } from 'react-router' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' import { Button, Link, Icon, Tooltip } from '@sourcegraph/wildcard' +import { DownloadFileButton } from '../../../../../components/DownloadFileButton' import { ConfirmDeleteModal } from '../../../../../components/modals/ConfirmDeleteModal' import { Insight, isLangStatsInsight } from '../../../../../core' import { useCopyURLHandler } from '../../../../../hooks/use-copy-url-handler' @@ -48,9 +50,13 @@ export const CodeInsightIndependentPageActions: FunctionComponent = props
{!isLangStatsInsight(insight) && ( - + )} From 6e39de344c5e4b787352879cdb631a56638efb09 Mon Sep 17 00:00:00 2001 From: Eric Fritz Date: Fri, 27 Jan 2023 14:43:46 -0600 Subject: [PATCH 232/678] codeintel: Add additional fetch options for uploads/indexes (#47047) --- .../internal/store/store_indexes.go | 41 +++++++++++++------ .../internal/store/store_indexes_test.go | 39 +++++++++++------- .../codeintel/autoindexing/shared/types.go | 12 +++--- .../uploads/internal/store/store_uploads.go | 35 ++++++++++------ .../internal/store/store_uploads_test.go | 6 ++- .../codeintel/uploads/shared/types.go | 1 + 6 files changed, 88 insertions(+), 46 deletions(-) diff --git a/enterprise/internal/codeintel/autoindexing/internal/store/store_indexes.go b/enterprise/internal/codeintel/autoindexing/internal/store/store_indexes.go index c47b9bc064db..51027dbc0baf 100644 --- a/enterprise/internal/codeintel/autoindexing/internal/store/store_indexes.go +++ b/enterprise/internal/codeintel/autoindexing/internal/store/store_indexes.go @@ -2,6 +2,7 @@ package store import ( "context" + "sort" "time" "github.com/keegancsmith/sqlf" @@ -116,7 +117,13 @@ func (s *store) GetIndexes(ctx context.Context, opts shared.GetIndexesOptions) ( conds = append(conds, makeIndexSearchCondition(opts.Term)) } if opts.State != "" { - conds = append(conds, makeStateCondition(opts.State)) + opts.States = append(opts.States, opts.State) + } + if len(opts.States) > 0 { + conds = append(conds, makeStateCondition(opts.States)) + } + if opts.WithoutUpload { + conds = append(conds, sqlf.Sprintf("NOT EXISTS (SELECT 1 FROM lsif_uploads u2 WHERE u2.associated_index_id = u.id)")) } authzConds, err := database.AuthzQueryConds(ctx, database.NewDBWith(s.logger, tx.db)) @@ -187,7 +194,7 @@ func (s *store) DeleteIndexes(ctx context.Context, opts shared.DeleteIndexesOpti conds = append(conds, makeIndexSearchCondition(opts.Term)) } if opts.State != "" { - conds = append(conds, makeStateCondition(opts.State)) + conds = append(conds, makeStateCondition([]string{opts.State})) } authzConds, err := database.AuthzQueryConds(ctx, database.NewDBWith(s.logger, s.db)) @@ -237,7 +244,7 @@ func (s *store) ReindexIndexes(ctx context.Context, opts shared.ReindexIndexesOp conds = append(conds, makeIndexSearchCondition(opts.Term)) } if opts.State != "" { - conds = append(conds, makeStateCondition(opts.State)) + conds = append(conds, makeStateCondition([]string{opts.State})) } authzConds, err := database.AuthzQueryConds(ctx, database.NewDBWith(s.logger, s.db)) @@ -297,21 +304,29 @@ func makeIndexSearchCondition(term string) *sqlf.Query { } // makeStateCondition returns a disjunction of clauses comparing the upload against the target state. -func makeStateCondition(state string) *sqlf.Query { - states := make([]string, 0, 2) - if state == "errored" || state == "failed" { +func makeStateCondition(states []string) *sqlf.Query { + stateMap := make(map[string]struct{}, 2) + for _, state := range states { // Treat errored and failed states as equivalent - states = append(states, "errored", "failed") - } else { - states = append(states, state) + if state == "errored" || state == "failed" { + stateMap["errored"] = struct{}{} + stateMap["failed"] = struct{}{} + } else { + stateMap[state] = struct{}{} + } } - queries := make([]*sqlf.Query, 0, len(states)) - for _, state := range states { - queries = append(queries, sqlf.Sprintf("u.state = %s", state)) + orderedStates := make([]string, 0, len(stateMap)) + for state := range stateMap { + orderedStates = append(orderedStates, state) + } + sort.Strings(orderedStates) + + if len(orderedStates) == 1 { + return sqlf.Sprintf("u.state = %s", orderedStates[0]) } - return sqlf.Sprintf("(%s)", sqlf.Join(queries, " OR ")) + return sqlf.Sprintf("u.state = ANY(%s)", pq.Array(orderedStates)) } // GetIndexByID returns an index by its identifier and boolean flag indicating its existence. diff --git a/enterprise/internal/codeintel/autoindexing/internal/store/store_indexes_test.go b/enterprise/internal/codeintel/autoindexing/internal/store/store_indexes_test.go index 5d2ecbc09440..4c43fc913a13 100644 --- a/enterprise/internal/codeintel/autoindexing/internal/store/store_indexes_test.go +++ b/enterprise/internal/codeintel/autoindexing/internal/store/store_indexes_test.go @@ -5,6 +5,7 @@ import ( "fmt" "sort" "strconv" + "strings" "testing" "time" @@ -192,19 +193,23 @@ func TestGetIndexes(t *testing.T) { ) testCases := []struct { - repositoryID int - state string - term string - expectedIDs []int + repositoryID int + state string + states []string + term string + withoutUpload bool + expectedIDs []int }{ {expectedIDs: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}}, {repositoryID: 50, expectedIDs: []int{1, 2, 3, 5, 7, 8, 9, 10}}, {state: "completed", expectedIDs: []int{7, 8, 10}}, - {term: "003", expectedIDs: []int{1, 3, 5}}, // searches commits - {term: "333", expectedIDs: []int{1, 2, 3, 5}}, // searches commits and failure message - {term: "QuEuEd", expectedIDs: []int{1, 3, 4, 9}}, // searches text status - {term: "bAr", expectedIDs: []int{4, 6}}, // search repo names - {state: "failed", expectedIDs: []int{2}}, // treats errored/failed states equivalently + {term: "003", expectedIDs: []int{1, 3, 5}}, // searches commits + {term: "333", expectedIDs: []int{1, 2, 3, 5}}, // searches commits and failure message + {term: "QuEuEd", expectedIDs: []int{1, 3, 4, 9}}, // searches text status + {term: "bAr", expectedIDs: []int{4, 6}}, // search repo names + {state: "failed", expectedIDs: []int{2}}, // treats errored/failed states equivalently + {states: []string{"completed", "failed"}, expectedIDs: []int{2, 7, 8, 10}}, // searches multiple states + {withoutUpload: true, expectedIDs: []int{2, 4, 6, 7, 8, 9, 10}}, // anti-join with upload records } for _, testCase := range testCases { @@ -215,20 +220,24 @@ func TestGetIndexes(t *testing.T) { } name := fmt.Sprintf( - "repositoryID=%d state=%s term=%s offset=%d", + "repositoryID=%d state=%s states=%s term=%s without_upload=%v offset=%d", testCase.repositoryID, testCase.state, + strings.Join(testCase.states, ","), testCase.term, + testCase.withoutUpload, lo, ) t.Run(name, func(t *testing.T) { indexes, totalCount, err := store.GetIndexes(ctx, shared.GetIndexesOptions{ - RepositoryID: testCase.repositoryID, - State: testCase.state, - Term: testCase.term, - Limit: 3, - Offset: lo, + RepositoryID: testCase.repositoryID, + State: testCase.state, + States: testCase.states, + Term: testCase.term, + WithoutUpload: testCase.withoutUpload, + Limit: 3, + Offset: lo, }) if err != nil { t.Fatalf("unexpected error getting indexes for repo: %s", err) diff --git a/enterprise/internal/codeintel/autoindexing/shared/types.go b/enterprise/internal/codeintel/autoindexing/shared/types.go index 2632fef0d42a..60addf7e3e5e 100644 --- a/enterprise/internal/codeintel/autoindexing/shared/types.go +++ b/enterprise/internal/codeintel/autoindexing/shared/types.go @@ -5,11 +5,13 @@ import ( ) type GetIndexesOptions struct { - RepositoryID int - State string - Term string - Limit int - Offset int + RepositoryID int + State string + States []string + Term string + WithoutUpload bool + Limit int + Offset int } type SourcedCommits struct { diff --git a/enterprise/internal/codeintel/uploads/internal/store/store_uploads.go b/enterprise/internal/codeintel/uploads/internal/store/store_uploads.go index 990d59297f65..57dc68bfe0a3 100644 --- a/enterprise/internal/codeintel/uploads/internal/store/store_uploads.go +++ b/enterprise/internal/codeintel/uploads/internal/store/store_uploads.go @@ -1978,7 +1978,10 @@ func buildGetConditionsAndCte(opts shared.GetUploadsOptions) (*sqlf.Query, []*sq conds = append(conds, makeSearchCondition(opts.Term)) } if opts.State != "" { - conds = append(conds, makeStateCondition(opts.State)) + opts.States = append(opts.States, opts.State) + } + if len(opts.States) > 0 { + conds = append(conds, makeStateCondition(opts.States)) } else if !allowDeletedUploads { conds = append(conds, sqlf.Sprintf("u.state != 'deleted'")) } @@ -2096,7 +2099,7 @@ func buildDeleteConditions(opts shared.DeleteUploadsOptions) []*sqlf.Query { conds = append(conds, makeSearchCondition(opts.Term)) } if opts.State != "" { - conds = append(conds, makeStateCondition(opts.State)) + conds = append(conds, makeStateCondition([]string{opts.State})) } if opts.VisibleAtTip { conds = append(conds, sqlf.Sprintf("EXISTS ("+visibleAtTipSubselectQuery+")")) @@ -2126,21 +2129,29 @@ func makeSearchCondition(term string) *sqlf.Query { } // makeStateCondition returns a disjunction of clauses comparing the upload against the target state. -func makeStateCondition(state string) *sqlf.Query { - states := make([]string, 0, 2) - if state == "errored" || state == "failed" { +func makeStateCondition(states []string) *sqlf.Query { + stateMap := make(map[string]struct{}, 2) + for _, state := range states { // Treat errored and failed states as equivalent - states = append(states, "errored", "failed") - } else { - states = append(states, state) + if state == "errored" || state == "failed" { + stateMap["errored"] = struct{}{} + stateMap["failed"] = struct{}{} + } else { + stateMap[state] = struct{}{} + } } - queries := make([]*sqlf.Query, 0, len(states)) - for _, state := range states { - queries = append(queries, sqlf.Sprintf("u.state = %s", state)) + orderedStates := make([]string, 0, len(stateMap)) + for state := range stateMap { + orderedStates = append(orderedStates, state) + } + sort.Strings(orderedStates) + + if len(orderedStates) == 1 { + return sqlf.Sprintf("u.state = %s", orderedStates[0]) } - return sqlf.Sprintf("(%s)", sqlf.Join(queries, " OR ")) + return sqlf.Sprintf("u.state = ANY(%s)", pq.Array(orderedStates)) } func buildCTEPrefix(cteDefinitions []cteDefinition) *sqlf.Query { diff --git a/enterprise/internal/codeintel/uploads/internal/store/store_uploads_test.go b/enterprise/internal/codeintel/uploads/internal/store/store_uploads_test.go index 4fe31477b978..dcc06f23e031 100644 --- a/enterprise/internal/codeintel/uploads/internal/store/store_uploads_test.go +++ b/enterprise/internal/codeintel/uploads/internal/store/store_uploads_test.go @@ -111,6 +111,7 @@ func TestGetUploads(t *testing.T) { type testCase struct { repositoryID int state string + states []string term string visibleAtTip bool dependencyOf int @@ -149,13 +150,15 @@ func TestGetUploads(t *testing.T) { {dependentOf: 11, expectedIDs: []int{}}, {allowDeletedRepo: true, state: "deleted", expectedIDs: []int{12, 13, 14, 15}}, {allowDeletedRepo: true, state: "deleted", alllowDeletedUpload: true, expectedIDs: []int{12, 13, 14, 15, 16, 17}}, + {states: []string{"completed", "failed"}, expectedIDs: []int{2, 7, 8, 10, 11}}, } runTest := func(testCase testCase, lo, hi int) (errors int) { name := fmt.Sprintf( - "repositoryID=%d|state='%s'|term='%s'|visibleAtTip=%v|dependencyOf=%d|dependentOf=%d|offset=%d", + "repositoryID=%d|state='%s'|states='%s',term='%s'|visibleAtTip=%v|dependencyOf=%d|dependentOf=%d|offset=%d", testCase.repositoryID, testCase.state, + strings.Join(testCase.states, ","), testCase.term, testCase.visibleAtTip, testCase.dependencyOf, @@ -167,6 +170,7 @@ func TestGetUploads(t *testing.T) { uploads, totalCount, err := store.GetUploads(ctx, shared.GetUploadsOptions{ RepositoryID: testCase.repositoryID, State: testCase.state, + States: testCase.states, Term: testCase.term, VisibleAtTip: testCase.visibleAtTip, DependencyOf: testCase.dependencyOf, diff --git a/enterprise/internal/codeintel/uploads/shared/types.go b/enterprise/internal/codeintel/uploads/shared/types.go index 5e155728a287..a2919332c650 100644 --- a/enterprise/internal/codeintel/uploads/shared/types.go +++ b/enterprise/internal/codeintel/uploads/shared/types.go @@ -17,6 +17,7 @@ type SourcedCommits struct { type GetUploadsOptions struct { RepositoryID int State string + States []string Term string VisibleAtTip bool DependencyOf int From f39d9cc1f02c96e4fbbc347572fce04045aad624 Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Fri, 27 Jan 2023 13:15:33 -0800 Subject: [PATCH 233/678] set meta noindex on doc pages that are not for the current version (#47046) Fixes a bug where noindex was not being set correctly on doc pages on non-current versions. This bug was introduced when new code was added to the template to also noindex pages related to Code Insights for the early 2021 launch. That embargo is no longer needed, so we can remove that code altogether. After this change, visiting a page with an `@version` in the URL such as https://docs.sourcegraph.com/@3.1/dev/roadmap and inspecting the source should reveal a meta noindex tag. --- doc/_resources/templates/document.html | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/doc/_resources/templates/document.html b/doc/_resources/templates/document.html index b06ed01e989c..38aef1fd943c 100644 --- a/doc/_resources/templates/document.html +++ b/doc/_resources/templates/document.html @@ -58,13 +58,8 @@ {{end}} {{define "head"}} - {{ if .Content }} - {{ if contains .Content.Path "insights" }} - - - {{end}} - {{ else if .ContentVersion }} - + {{ if .ContentVersion }} + {{ end }} {{end}} From 3798bdf5eb8384e2efe1740b102f87a312dbc45e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juliana=20Pe=C3=B1a?= Date: Fri, 27 Jan 2023 14:36:41 -0800 Subject: [PATCH 234/678] docs: fix horizontal scrolling (#47006) --- doc/_resources/assets/docsite.css | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/_resources/assets/docsite.css b/doc/_resources/assets/docsite.css index 5d8a0d1b3394..5b5f13e66cb4 100644 --- a/doc/_resources/assets/docsite.css +++ b/doc/_resources/assets/docsite.css @@ -171,6 +171,7 @@ body > div#page { flex-direction: column; min-height: 100vh; justify-content: space-between; + overflow-x: hidden; /* Prevent horizontal scrolling */ } /* Responsive */ @media (max-width: 800px /* == var(--sidebar-breakpoint-width) */) { From 1e99a25c478693139d67b206b26acb89845c2e2d Mon Sep 17 00:00:00 2001 From: Eric Fritz Date: Fri, 27 Jan 2023 17:22:14 -0600 Subject: [PATCH 235/678] migrator: Fix drift schema resolution on downgrade command (#47055) --- cmd/migrator/shared/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/migrator/shared/main.go b/cmd/migrator/shared/main.go index 23bae8f155a1..1f17c6f2d968 100644 --- a/cmd/migrator/shared/main.go +++ b/cmd/migrator/shared/main.go @@ -75,7 +75,7 @@ func Start(logger log.Logger, registerEnterpriseMigrators registerMigratorsUsing cliutil.Drift(appName, newRunner, outputFactory, schemaFactories...), cliutil.AddLog(appName, newRunner, outputFactory), cliutil.Upgrade(appName, newRunnerWithSchemas, outputFactory, registerMigrators, schemaFactories...), - cliutil.Downgrade(appName, newRunnerWithSchemas, outputFactory, registerMigrators), + cliutil.Downgrade(appName, newRunnerWithSchemas, outputFactory, registerMigrators, schemaFactories...), cliutil.RunOutOfBandMigrations(appName, newRunner, outputFactory, registerMigrators), }, } From 4405a2c6ef1b8671de2149ed2b907c2e29b57b4e Mon Sep 17 00:00:00 2001 From: Felix Kling Date: Sat, 28 Jan 2023 21:09:11 +0100 Subject: [PATCH 236/678] experimental query input: Combine "add to query" and "go to" suggestions/actions (#46982) With this commit every suggestion can have two "actions": Return applies the first/default action, Shift+Return applies the alternative action. This allows us to use for examples repository suggestions for add the `repo:` filter + value or to directly naviagate to the repository. Additionally we showing an "info" footer to let the user know how they can activate each suggestion. **Note:** In this iteration we only support selecting either action via the keyboard. Using the mouse will always apply the first action. --- .../experimental/Suggestions.module.scss | 135 ++++++++----- .../input/experimental/Suggestions.tsx | 188 ++++++++++------- .../src/search-ui/input/experimental/index.ts | 12 +- .../input/experimental/optionRenderer.tsx | 47 +++++ .../experimental/suggestionsExtension.ts | 102 ++++++---- client/web/src/search/input/suggestions.ts | 191 ++++++++++-------- 6 files changed, 416 insertions(+), 259 deletions(-) create mode 100644 client/branded/src/search-ui/input/experimental/optionRenderer.tsx diff --git a/client/branded/src/search-ui/input/experimental/Suggestions.module.scss b/client/branded/src/search-ui/input/experimental/Suggestions.module.scss index 2870243b357c..f6e144dfc201 100644 --- a/client/branded/src/search-ui/input/experimental/Suggestions.module.scss +++ b/client/branded/src/search-ui/input/experimental/Suggestions.module.scss @@ -1,68 +1,90 @@ -.suggestions { - &[role='grid'] { - overflow-y: auto; - } +.container { + overflow-y: hidden; + display: flex; + flex-direction: column; - ul { - margin: 0; - padding: 0; - list-style: none; + .footer { + border-top: 1px solid var(--border-color); + padding: 0.5rem 1.25rem; + flex: 0 0 auto; + color: var(--text-muted); } - [role='rowgroup'] { - border-bottom: 1px solid var(--border-color); - padding: 0.75rem; - - &:first-of-type { - padding-top: 0; - } + .suggestions { + overflow-y: auto; - &:last-of-type { - border: none; + ul { + margin: 0; + padding: 0; + list-style: none; + flex: 1; } - // group header - [role='presentation'] { - color: var(--text-muted); - font-size: 0.75rem; - font-weight: 500; - margin-bottom: 0.25rem; - padding: 0 0.5rem; - } + [role='rowgroup'] { + border-bottom: 1px solid var(--border-color); + padding: 0.75rem; - [role='row'] { - display: flex; - align-items: center; - padding: 0.25rem 0.5rem; - border-radius: var(--border-radius); - font-family: var(--code-font-family); - font-size: 0.75rem; - min-height: 1.5rem; - - &[aria-selected='true'] { - background-color: var(--subtle-bg); - border-radius: 4px; + &:first-of-type { + padding-top: 0; } - &:hover { - background-color: var(--color-bg-2); - cursor: pointer; + &:last-of-type { + border: none; } - .match { - font-weight: bold; - } - - .description { - margin-left: 0.5rem; - color: var(--input-placeholder-color); + // group header + [role='presentation'] { + color: var(--text-muted); + font-size: 0.75rem; + font-weight: 500; + margin-bottom: 0.25rem; + padding: 0 0.5rem; } - .note { + [role='row'] { + display: flex; + align-items: center; + padding: 0.25rem 0.5rem; + border-radius: var(--border-radius); + font-family: var(--code-font-family); font-size: 0.75rem; - margin-left: auto; - color: var(--text-muted); - font-family: var(--font-family-base); + min-height: 1.5rem; + + &[aria-selected='true'] { + background-color: var(--subtle-bg); + border-radius: 4px; + } + + &:hover { + background-color: var(--color-bg-2); + cursor: pointer; + } + + .match { + font-weight: bold; + } + + .description { + margin-left: 0.5rem; + color: var(--input-placeholder-color); + } + + .note { + font-size: 0.75rem; + margin-left: auto; + color: var(--text-muted); + font-family: var(--font-family-base); + display: flex; + + > [role='gridcell'] { + padding: 0 0.5rem; + border-left: 1px solid var(--border-color); + + &:first-child { + border-left: 0; + } + } + } } } } @@ -72,15 +94,18 @@ display: flex; font-family: var(--code-font-family); font-size: 0.75rem; + + .separator { + color: var(--search-filter-keyword-color); + } +} + +.filter-field { color: var(--search-filter-keyword-color); background-color: var(--oc-blue-0); // border: 1px solid var(--oc-blue-1); border-radius: 3px; padding: 0; - - .separator { - color: var(--search-filter-keyword-color); - } } .icon { diff --git a/client/branded/src/search-ui/input/experimental/Suggestions.tsx b/client/branded/src/search-ui/input/experimental/Suggestions.tsx index a709e07f4a67..bdd872b8e80d 100644 --- a/client/branded/src/search-ui/input/experimental/Suggestions.tsx +++ b/client/branded/src/search-ui/input/experimental/Suggestions.tsx @@ -1,21 +1,23 @@ import React, { MouseEvent, useMemo, useState, useCallback, useLayoutEffect } from 'react' -import { Icon, useWindowSize } from '@sourcegraph/wildcard' +import { mdiInformationOutline } from '@mdi/js' +import classnames from 'classnames' -import { SyntaxHighlightedSearchQuery } from '../../components' +import { shortcutDisplayName } from '@sourcegraph/shared/src/keyboardShortcuts' +import { Icon, useWindowSize } from '@sourcegraph/wildcard' -import { Group, Option } from './suggestionsExtension' +import { Action, Group, Option } from './suggestionsExtension' import styles from './Suggestions.module.scss' -function getNote(option: Option): string { - switch (option.type) { +function getActionName(action: Action): string { + switch (action.type) { case 'completion': - return 'Add' - case 'target': - return option.note ?? 'Jump to' + return action.name ?? 'Add' + case 'goto': + return action.name ?? 'Go to' case 'command': - return option.note ?? '' + return action.name ?? 'Run' } } @@ -54,7 +56,9 @@ export const Suggestions: React.FunctionComponent = ({ // This is using an arbitrary 20px "margin" between the suggestions box // and the window border () => (container ? `${windowHeight - container.getBoundingClientRect().top - 20}px` : 'auto'), - [container, windowHeight] + // Recompute height when suggestions change + // eslint-disable-next-line react-hooks/exhaustive-deps + [container, windowHeight, results] ) const flattenedRows = useMemo(() => results.flatMap(group => group.options), [results]) const focusedItem = flattenedRows[activeRowIndex] @@ -74,80 +78,114 @@ export const Suggestions: React.FunctionComponent = ({
- {results.map((group, groupIndex) => - group.options.length > 0 ? ( -
    - - {group.options.map((option, rowIndex) => ( -
  • - {option.icon && ( -
    -
  • + ))} +
+ ) : null + )} +
+ {focusedItem &&
}
) } -export const FilterOption: React.FunctionComponent<{ option: Option }> = ({ option }) => ( - - {option.matches - ? [...option.value].map((char, index) => - option.matches!.has(index) ? ( - - {char} - - ) : ( - char - ) - ) - : option.value} - : - +const Footer: React.FunctionComponent<{ option: Option }> = ({ option }) => ( +
+ + {option.info?.(option)} + {!option.info && ( + <> + {' '} + {option.alternativeAction && ( + + )} + + )} + +
) -export const QueryOption: React.FunctionComponent<{ option: Option }> = ({ option }) => ( - +const ActionInfo: React.FunctionComponent<{ action: Action; shortcut: string }> = ({ action, shortcut }) => { + const displayName = shortcutDisplayName(shortcut) + switch (action.type) { + case 'completion': + return ( + <> + Press {displayName} to add to your query. + + ) + case 'goto': + return ( + <> + Press {displayName} to go to the suggestion. + + ) + case 'command': + return ( + <> + Press {displayName} to execute the command. + + ) + } +} + +export const HighlightedLabel: React.FunctionComponent<{ label: string; matches: Set }> = ({ + label, + matches, +}) => ( + <> + {[...label].map((char, index) => + matches.has(index) ? ( + + {char} + + ) : ( + char + ) + )} + ) diff --git a/client/branded/src/search-ui/input/experimental/index.ts b/client/branded/src/search-ui/input/experimental/index.ts index 6d507d72ed8d..4ef484746a5c 100644 --- a/client/branded/src/search-ui/input/experimental/index.ts +++ b/client/branded/src/search-ui/input/experimental/index.ts @@ -1,4 +1,12 @@ export { LazyCodeMirrorQueryInput } from './LazyCodeMirrorQueryInput' -export type { Group, Option, Completion, Target, Command, Source, SuggestionResult } from './suggestionsExtension' +export type { + Group, + Option, + CompletionAction, + GoToAction, + CommandAction, + Source, + SuggestionResult, +} from './suggestionsExtension' export { getEditorConfig } from './suggestionsExtension' -export { FilterOption, QueryOption } from './Suggestions' +export * from './optionRenderer' diff --git a/client/branded/src/search-ui/input/experimental/optionRenderer.tsx b/client/branded/src/search-ui/input/experimental/optionRenderer.tsx new file mode 100644 index 000000000000..37abc90dabbf --- /dev/null +++ b/client/branded/src/search-ui/input/experimental/optionRenderer.tsx @@ -0,0 +1,47 @@ +import classnames from 'classnames' + +import { SyntaxHighlightedSearchQuery } from '../../components' + +import { HighlightedLabel } from './Suggestions' +import { Option } from './suggestionsExtension' + +import styles from './Suggestions.module.scss' + +const FilterOption: React.FunctionComponent<{ option: Option }> = ({ option }) => ( + + {option.matches ? : option.label} + : + +) + +const FilterValueOption: React.FunctionComponent<{ option: Option }> = ({ option }) => { + const label = option.label + const separatorIndex = label.indexOf(':') + const field = label.slice(0, separatorIndex) + const value = label.slice(separatorIndex + 1) + + return ( + + + {option.matches ? : option.label} + : + + {option.matches ? : option.label} + + ) +} + +const QueryOption: React.FunctionComponent<{ option: Option }> = ({ option }) => ( + +) + +// Custom renderer for filter suggestions +export const filterRenderer = (option: Option): React.ReactElement => +export const filterValueRenderer = (option: Option): React.ReactElement => +// Custom renderer for (the current) query suggestions +export const queryRenderer = (option: Option): React.ReactElement => +export const submitQueryInfo = (): React.ReactElement => ( + <> + Press Return to submit your query. + +) diff --git a/client/branded/src/search-ui/input/experimental/suggestionsExtension.ts b/client/branded/src/search-ui/input/experimental/suggestionsExtension.ts index 46ae7ea603b8..303ca15514c6 100644 --- a/client/branded/src/search-ui/input/experimental/suggestionsExtension.ts +++ b/client/branded/src/search-ui/input/experimental/suggestionsExtension.ts @@ -51,41 +51,62 @@ export interface SuggestionResult { export type CustomRenderer = (option: Option) => React.ReactElement -export interface Command { - type: 'command' - value: string - apply: (view: EditorView) => void - matches?: Set - // svg path +export interface Option { + /** + * The label the input is matched against and shown in the UI. + */ + label: string + /** + * What to do when this option is applied (via Enter) + */ + action: Action + /** + * Options can have perform an alternative action when applied via + * Shift+Enter. + */ + alternativeAction?: Action + /** + * A short description of the option, shown next to the label. + */ + description?: string + /** + * The SVG path of the icon to use for this option. + */ icon?: string + /** + * If present the provided component will be used to render the label of the + * option. + */ render?: CustomRenderer - description?: string - note?: string + /** + * If present this component is rendered as footer. + */ + info?: CustomRenderer + /** + * A set of character indexes. If provided the characters of at these + * positions in the label will be highlighted as matches. + */ + matches?: Set +} + +export interface CommandAction { + type: 'command' + apply: (view: EditorView) => void + name?: string } -export interface Target { - type: 'target' - value: string +export interface GoToAction { + type: 'goto' url: string - matches?: Set - // svg path - icon?: string - render?: CustomRenderer - description?: string - note?: string + name?: string } -export interface Completion { +export interface CompletionAction { type: 'completion' from: number + name?: string to?: number - value: string insertValue?: string - matches?: Set - // svg path - icon?: string - render?: CustomRenderer - description?: string } -export type Option = Command | Target | Completion +export type Action = CommandAction | GoToAction | CompletionAction export interface Group { title: string @@ -97,7 +118,7 @@ class SuggestionView { private root: Root private onSelect = (option: Option): void => { - applyOption(this.view, option) + applyAction(this.view, option.action, option) // Query input looses focus when option is selected via // mousedown/click. This is a necessary hack to re-focus the query // input. @@ -452,21 +473,21 @@ function moveSelection(direction: 'forward' | 'backward'): CodeMirrorCommand { } } -function applyOption(view: EditorView, option: Option): void { - switch (option.type) { +function applyAction(view: EditorView, action: Action, option: Option): void { + switch (action.type) { case 'completion': { - const text = option.insertValue ?? option.value + const text = action.insertValue ?? option.label view.dispatch({ ...view.state.changeByRange(range => { if (range === view.state.selection.main) { return { changes: { - from: option.from, - to: option.to ?? view.state.selection.main.head, + from: action.from, + to: action.to ?? view.state.selection.main.head, insert: text, }, - range: EditorSelection.cursor(option.from + text.length), + range: EditorSelection.cursor(action.from + text.length), } } return { range } @@ -475,14 +496,14 @@ function applyOption(view: EditorView, option: Option): void { } break case 'command': - option.apply(view) + action.apply(view) break - case 'target': + case 'goto': { const history = view.state.facet(suggestionsConfig).history if (history) { - history.push(option.url) + history.push(action.url) } } break @@ -530,19 +551,16 @@ export const suggestionSource = Facet.define({ if (!state.open || !option) { return false } - applyOption(view, option) + applyAction(view, option.action, option) return true }, - }, - { - key: 'Tab', - run(view) { + shift(view) { const state = view.state.field(suggestionsStateField) const option = state.result.at(state.selectedOption) - if (!state.open || !option) { + if (!state.open || !option || !option.alternativeAction) { return false } - applyOption(view, option) + applyAction(view, option.alternativeAction, option) return true }, }, diff --git a/client/web/src/search/input/suggestions.ts b/client/web/src/search/input/suggestions.ts index cefdb1eb2813..f0f7d7d8a6e3 100644 --- a/client/web/src/search/input/suggestions.ts +++ b/client/web/src/search/input/suggestions.ts @@ -1,5 +1,3 @@ -import React from 'react' - import { EditorState } from '@codemirror/state' import { mdiFilterOutline, mdiTextSearchVariant, mdiSourceRepository, mdiStar, mdiFileOutline } from '@mdi/js' import { extendedMatch, Fzf, FzfOptions, FzfResultItem } from 'fzf' @@ -10,13 +8,13 @@ import { tokenAt, tokens as queryTokens } from '@sourcegraph/branded' import { Group, Option, - Target, - Completion, Source, - FilterOption, - QueryOption, getEditorConfig, SuggestionResult, + submitQueryInfo, + queryRenderer, + filterRenderer, + filterValueRenderer, } from '@sourcegraph/branded/src/search-ui/experimental' import { getParsedQuery } from '@sourcegraph/branded/src/search-ui/input/codemirror/parsedQuery' import { isDefined } from '@sourcegraph/common' @@ -52,11 +50,6 @@ type InternalSource = (params: const none: any[] = [] -// Custom renderer for filter suggestions -const filterRenderer = (option: Option): React.ReactElement => React.createElement(FilterOption, { option }) -// Custom renderer for (the current) query suggestions -const queryRenderer = (option: Option): React.ReactElement => React.createElement(QueryOption, { option }) - function starTiebraker(a: { item: { stars: number } }, b: { item: { stars: number } }): number { return b.item.stars - a.item.stars } @@ -125,37 +118,45 @@ interface File { } /** - * Converts a Repo value to a (jump) target suggestion. + * Converts a Repo value to a suggestion. */ -function toRepoTarget({ item, positions }: FzfResultItem): Target { - return { - type: 'target', - icon: mdiSourceRepository, - value: item.name, - url: `/${item.name}`, - matches: positions, - } +function toRepoSuggestion(result: FzfResultItem, from: number, to?: number): Option { + const option = toRepoCompletion(result, from, to, 'repo:') + option.action.name = 'Add' + option.alternativeAction = { + type: 'goto', + url: `/${result.item.name}`, + } + option.render = filterValueRenderer + return option } /** * Converts a Repo value to a completion suggestion. */ -function toRepoCompletion({ item, positions }: FzfResultItem, from: number, to?: number): Completion { +function toRepoCompletion( + { item, positions }: FzfResultItem, + from: number, + to?: number, + valuePrefix = '' +): Option { return { - type: 'completion', - icon: mdiSourceRepository, - value: item.name, - insertValue: regexInsertText(item.name, { globbing: false }) + ' ', + label: valuePrefix + item.name, matches: positions, - from, - to, + icon: mdiSourceRepository, + action: { + type: 'completion', + insertValue: valuePrefix + regexInsertText(item.name, { globbing: false }) + ' ', + from, + to, + }, } } /** * Converts a Context value to a completion suggestion. */ -function toContextCompletion({ item, positions }: FzfResultItem, from: number, to?: number): Completion { +function toContextCompletion({ item, positions }: FzfResultItem, from: number, to?: number): Option { let description = item.default ? 'Default' : '' if (item.description) { if (item.default) { @@ -165,65 +166,76 @@ function toContextCompletion({ item, positions }: FzfResultItem, from: } return { - type: 'completion', + label: item.spec, // Passing an empty string is a hack to draw an "empty" icon icon: item.starred ? mdiStar : ' ', - value: item.spec, - insertValue: item.spec + ' ', description, matches: positions, - from, - to, + action: { + type: 'completion', + insertValue: item.spec + ' ', + from, + to, + }, } } /** * Converts a filter to a completion suggestion. */ -function toFilterCompletion(filter: FilterType, from: number, to?: number): Completion { +function toFilterCompletion(filter: FilterType, from: number, to?: number): Option { const definition = FILTERS[filter] const description = typeof definition.description === 'function' ? definition.description(false) : definition.description return { - type: 'completion', + label: filter, icon: mdiFilterOutline, render: filterRenderer, - value: filter, - insertValue: filter + ':', description, - from, - to, + action: { + type: 'completion', + insertValue: filter + ':', + from, + to, + }, } } /** * Converts a File value to a completion suggestion. */ -function toFileCompletion({ item, positions }: FzfResultItem, from: number, to?: number): Completion { +function toFileCompletion( + { item, positions }: FzfResultItem, + from: number, + to?: number, + valuePrefix = '' +): Option { return { - type: 'completion', + label: valuePrefix + item.path, icon: mdiFileOutline, - value: item.path, - insertValue: regexInsertText(item.path, { globbing: false }) + ' ', description: item.repository, matches: positions, - from, - to, + action: { + type: 'completion', + insertValue: valuePrefix + regexInsertText(item.path, { globbing: false }) + ' ', + from, + to, + }, } } /** * Converts a File value to a (jump) target suggestion. */ -function toFileTarget({ item, positions }: FzfResultItem): Target { - return { - type: 'target', - icon: mdiFileOutline, - value: item.path, - description: item.repository, - url: item.url, - matches: positions, - } +function toFileSuggestion(result: FzfResultItem, from: number, to?: number): Option { + const option = toFileCompletion(result, from, to, 'file:') + option.action.name = 'Add' + option.alternativeAction = { + type: 'goto', + url: result.item.url, + } + option.render = filterValueRenderer + return option } /** @@ -235,19 +247,19 @@ const currentQuery: InternalSource = ({ token, input }) => { return null } - let value = input - let note = 'Search everywhere' + let label = input + let actionName = 'Search everywhere' const contextFilter = findFilter(input, FilterType.context, FilterKind.Global) if (contextFilter) { - value = omitFilter(input, contextFilter) + label = omitFilter(input, contextFilter) if (contextFilter.value?.value !== 'global') { - note = `Search '${contextFilter.value?.value ?? ''}'` + actionName = `Search '${contextFilter.value?.value ?? ''}'` } } - if (value.trim() === '') { + if (label.trim() === '') { return null } @@ -257,14 +269,17 @@ const currentQuery: InternalSource = ({ token, input }) => { title: '', options: [ { - type: 'command', icon: mdiTextSearchVariant, - value, - note, - apply: view => { - getEditorConfig(view.state).onSubmit() + label, + action: { + type: 'command', + name: actionName, + apply: view => { + getEditorConfig(view.state).onSubmit() + }, }, render: queryRenderer, + info: submitQueryInfo, }, ], }, @@ -320,6 +335,21 @@ const filterSuggestions: InternalSource = ({ tokens, token, position }) => { return options.length > 0 ? { result: [{ title: 'Narrow your search', options }] } : null } +const contextActions: Group = { + title: 'Actions', + options: [ + { + label: 'Manage contexts', + description: 'Add, edit, remove search contexts', + action: { + type: 'goto', + name: 'Go to /contexts', + url: '/contexts', + }, + }, + ], +} + /** * Returns static and dynamic completion suggestions for filters when completing * a filter value. @@ -377,18 +407,7 @@ function filterValueSuggestions(caches: Caches): InternalSource { title: 'Search contexts', options: entries.map(entry => toContextCompletion(entry, from, to)), }, - { - title: 'Actions', - options: [ - { - type: 'target', - value: 'Manage contexts', - description: 'Add, edit, remove search contexts', - note: 'Got to /contexts', - url: '/contexts', - }, - ], - }, + contextActions, ] }) default: { @@ -412,16 +431,18 @@ function staticFilterValueSuggestions(token?: Token): Group | null { } const value = token.value - let options: Completion[] = resolvedFilter.definition.discreteValues(token.value, false).map(value => ({ - type: 'completion', - from: token.value?.range.start ?? token.range.end, - to: token.value?.range.end, - value: value.label, - insertValue: (value.insertText ?? value.label) + ' ', + let options: Option[] = resolvedFilter.definition.discreteValues(token.value, false).map(value => ({ + label: value.label, + action: { + type: 'completion', + from: token.value?.range.start ?? token.range.end, + to: token.value?.range.end, + insertValue: (value.insertText ?? value.label) + ' ', + }, })) if (value && value.value !== '') { - const fzf = new Fzf(options, { selector: option => option.value }) + const fzf = new Fzf(options, { selector: option => option.label }) options = fzf.find(value.value).map(match => ({ ...match.item, matches: match.positions })) } @@ -446,7 +467,7 @@ function repoSuggestions(cache: Caches['repo']): InternalSource { results => [ { title: 'Repositories', - options: results.slice(0, 5).map(toRepoTarget), + options: results.slice(0, 5).map(result => toRepoSuggestion(result, token.range.start)), }, ], parsedQuery, @@ -484,7 +505,7 @@ function fileSuggestions(cache: Caches['file'], isSourcegraphDotCom?: boolean): results => [ { title: 'Files', - options: results.slice(0, 5).map(toFileTarget), + options: results.slice(0, 5).map(result => toFileSuggestion(result, token.range.start)), }, ], parsedQuery, From 316ccbac5727fab5122ef3c68e5c2a79bfc7d130 Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Sun, 29 Jan 2023 00:40:20 -0800 Subject: [PATCH 237/678] make DEV_WEB_BUILDER_OMIT_SLOW_DEPS work with pnpm (#47067) The DEV_WEB_BUILDER_OMIT_SLOW_DEPS env var is an experimental dev env var for faster builds that I sometimes use. When using it, the packageResolution plugin needs the same fix as the one for stylePlugin in https://github.com/sourcegraph/sourcegraph/pull/46452 to work with pnpm. This is a generally sensible fix that implements the correct and desirable behavior, not just a way to make a hack work. --- client/build-config/src/esbuild/packageResolutionPlugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/build-config/src/esbuild/packageResolutionPlugin.ts b/client/build-config/src/esbuild/packageResolutionPlugin.ts index 20556aae7cd6..a4990d169ce4 100644 --- a/client/build-config/src/esbuild/packageResolutionPlugin.ts +++ b/client/build-config/src/esbuild/packageResolutionPlugin.ts @@ -3,7 +3,7 @@ import fs from 'fs' import { CachedInputFileSystem, ResolverFactory } from 'enhanced-resolve' import * as esbuild from 'esbuild' -import { NODE_MODULES_PATH } from '../paths' +import { NODE_MODULES_PATH, WORKSPACE_NODE_MODULES_PATHS } from '../paths' interface Resolutions { [fromModule: string]: string @@ -22,7 +22,7 @@ export const packageResolutionPlugin = (resolutions: Resolutions): esbuild.Plugi fileSystem: new CachedInputFileSystem(fs, 4000), extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], symlinks: true, // Resolve workspace symlinks - modules: [NODE_MODULES_PATH], + modules: [NODE_MODULES_PATH, ...WORKSPACE_NODE_MODULES_PATHS], unsafeCache: true, }) From 9f4a52a9f7067c7c171574aa3b4b61c294263b4a Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Sun, 29 Jan 2023 08:37:55 -0800 Subject: [PATCH 238/678] build: upgrade bazel to 6.0.0 (#47049) --- .bazelversion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bazelversion b/.bazelversion index adfd8609ed01..09b254e90c61 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -6.0.0rc3 +6.0.0 From 5833620d05b83959a82d49fe8ea70f468e3edaf5 Mon Sep 17 00:00:00 2001 From: Indradhanush Gupta Date: Mon, 30 Jan 2023 11:35:55 +0530 Subject: [PATCH 239/678] internal/repos: Remove unused check connection code (#47073) --- internal/repos/check_connection.go | 116 ------------------------ internal/repos/check_connection_test.go | 93 ------------------- 2 files changed, 209 deletions(-) delete mode 100644 internal/repos/check_connection.go delete mode 100644 internal/repos/check_connection_test.go diff --git a/internal/repos/check_connection.go b/internal/repos/check_connection.go deleted file mode 100644 index 3a0392647be8..000000000000 --- a/internal/repos/check_connection.go +++ /dev/null @@ -1,116 +0,0 @@ -package repos - -import ( - "net" - "net/url" - "strings" - "time" - - "github.com/sourcegraph/sourcegraph/lib/errors" -) - -func getHostname(rawURL string) (string, error) { - u, err := url.Parse(rawURL) - if err != nil { - return "", errors.Wrap(err, "invalid or bad url for connection check") - } - - // Best effort at finding a hostname. For example if rawURL is sourcegraph.com, then u.Host is - // empty but Path is sourcegraph.com. Use that as a result. - // - // 👉 Also, we need to use u.Hostname() here because we want to strip any port numbers if they - // are present in u.Host. - hostname := u.Hostname() - if hostname == "" { - if u.Scheme != "" { - // rawURL is most likely something like "sourcegraph.com:80", read from u.Scheme. - hostname = u.Scheme - } else if u.Path != "" { - // rawURL is most likely something like "sourcegraph.com:80", read from u.Path. - hostname = u.Path - } else { - return "", errors.Newf("unsupported url format (%q) for connection check", rawURL) - } - } - - return hostname, nil -} - -// checkConnection parses the rawURL and makes a best effort attempt to obtain a hostname. It then -// performs an IP lookup on that hostname and returns a an error on failure. -// -// At the moment this function is only limited to doing IP lookups. We may want/have to expand this -// to support other code hosts or to add more checks (for example making a test API call to verify -// the authorization, etc). -func checkConnection(rawURL string) error { - if err := dnsLookup(rawURL); err != nil { - return errors.Wrap(err, "DNS lookup failed") - } - - if err := ping(rawURL); err != nil { - return errors.Wrap(err, "ping failed") - } - - return nil -} - -func dnsLookup(rawURL string) error { - hostname, err := getHostname(rawURL) - if err != nil { - return errors.Wrap(err, "getHostname failed") - } - - ips, err := net.LookupIP(hostname) - if err != nil { - return err - } - - if len(ips) == 0 { - return errors.Newf("no IP addresses found for hostname %q", hostname) - } - - return nil -} - -// ping attempts to connect to the given rawURL. Technically it is not exactly a ping request in the -// UNIX sense since it uses TCP instead of ICMP. But we use the name to signifiy the intent here, -// which is to check if we can connect to the URL. -func ping(rawURL string) error { - hostname, err := getHostname(rawURL) - if err != nil { - return errors.Wrap(err, "getHostname failed") - } - - baseURL := rawURL - var protocol string - if strings.Contains(rawURL, "://") { - parts := strings.Split(rawURL, "://") - // Technically we can never have this condition because of the getHostname check above which - // should detect any malformed URLs. But if I've learnt anything out of implementing the - // methods in this file, is that URL parsing in itself is very brittle so I don't want to - // take any chances with a panic here. - if len(parts) < 2 { - return errors.Newf("potentially malformed URL: %q", rawURL) - } - - protocol, baseURL = parts[0], parts[1] - } - - // Check if the URL includes a port. - _, port, err := net.SplitHostPort(baseURL) - if err != nil { - switch protocol { - // Assume HTTP if URL has no protocol or port. - case "", "http": - port = "80" - case "https": - port = "443" - default: - return errors.Wrap(err, "failed to get port for URL which is likely a non HTTP based URL") - } - } - - address := net.JoinHostPort(hostname, port) - _, err = net.DialTimeout("tcp", address, 2*time.Second) - return err -} diff --git a/internal/repos/check_connection_test.go b/internal/repos/check_connection_test.go deleted file mode 100644 index 64a93db9849a..000000000000 --- a/internal/repos/check_connection_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package repos - -import ( - "testing" -) - -func TestDnsLookup(t *testing.T) { - t.Run("bad URL", func(t *testing.T) { - if err := dnsLookup("foo"); err == nil { - t.Error("Expected error but got nil") - } - }) - - t.Run("good URL", func(t *testing.T) { - if err := dnsLookup("https://sourcegraph.com"); err != nil { - t.Errorf("Expected nil but got error: %v", err) - } - }) - - t.Run("good URL with port", func(t *testing.T) { - if err := dnsLookup("https://sourcegraph.com:80"); err != nil { - t.Errorf("Expected nil but got error: %v", err) - } - }) - - t.Run("good URL without protocol", func(t *testing.T) { - if err := dnsLookup("sourcegraph.com"); err != nil { - t.Errorf("Expected nil but got error: %v", err) - } - }) - - t.Run("good URL with port but without protocol", func(t *testing.T) { - if err := dnsLookup("sourcegraph.com:80"); err != nil { - t.Errorf("Expected nil but got error: %v", err) - } - }) - - t.Run("good URL with username:password", func(t *testing.T) { - if err := dnsLookup("https://username:password@sourcegraph.com"); err != nil { - t.Errorf("Expected nil but got error: %v", err) - } - }) -} - -func TestPing(t *testing.T) { - t.Run("bad URL", func(t *testing.T) { - if err := ping("foo"); err == nil { - t.Error("Expected error but got nil") - } - }) - - t.Run("bad URL with non HTTP protocol", func(t *testing.T) { - if err := ping("ftp://foo"); err == nil { - t.Error("Expected error but got nil") - } - }) - - t.Run("hostname and port", func(t *testing.T) { - if err := ping("sourcegraph.com:80"); err != nil { - t.Errorf("Expected nil but got error: %v", err) - } - }) - - t.Run("hostname without port", func(t *testing.T) { - if err := ping("ghe.sgdev.org"); err != nil { - t.Errorf("Expected nil but got error: %v", err) - } - }) - - t.Run("HTTP hostname", func(t *testing.T) { - if err := ping("http://ghe.sgdev.org"); err != nil { - t.Errorf("Expected nil but got error: %v", err) - } - }) - - t.Run("HTTP hostname and port", func(t *testing.T) { - if err := ping("http://ghe.sgdev.org:80"); err != nil { - t.Errorf("Expected nil but got error: %v", err) - } - }) - - t.Run("HTTPS hostname", func(t *testing.T) { - if err := ping("https://ghe.sgdev.org"); err != nil { - t.Errorf("Expected nil but got error: %v", err) - } - }) - - t.Run("HTTPS hostname and port", func(t *testing.T) { - if err := ping("https://ghe.sgdev.org:443"); err != nil { - t.Errorf("Expected nil but got error: %v", err) - } - }) -} From 782fa4e9f78c123889b3086fd9b336509dcd3d0e Mon Sep 17 00:00:00 2001 From: Taras Yemets Date: Mon, 30 Jan 2023 08:35:44 +0200 Subject: [PATCH 240/678] blob: focus selected occurrence (#46765) --- client/web/src/repo/blob/Blob.module.scss | 4 + client/web/src/repo/blob/CodeMirrorBlob.tsx | 28 +- .../{hover.ts => code-intel-tooltips.ts} | 505 ++++++++++-------- .../codemirror/token-selection/decorations.ts | 107 ++++ .../codemirror/token-selection/definition.ts | 95 ++-- .../token-selection/document-highlights.ts | 20 +- .../codemirror/token-selection/extension.ts | 31 +- .../codemirror/token-selection/keybindings.ts | 277 +++++++--- .../token-selection/modifier-click.ts | 17 +- .../codemirror/token-selection/selections.ts | 108 +--- .../codemirror/tooltips/LoadingTooltip.ts | 34 +- .../codemirror/tooltips/TemporaryTooltip.tsx | 18 +- 12 files changed, 734 insertions(+), 510 deletions(-) rename client/web/src/repo/blob/codemirror/token-selection/{hover.ts => code-intel-tooltips.ts} (57%) create mode 100644 client/web/src/repo/blob/codemirror/token-selection/decorations.ts diff --git a/client/web/src/repo/blob/Blob.module.scss b/client/web/src/repo/blob/Blob.module.scss index 55787589d1f7..602ba38e5537 100644 --- a/client/web/src/repo/blob/Blob.module.scss +++ b/client/web/src/repo/blob/Blob.module.scss @@ -106,3 +106,7 @@ cursor: pointer; background-color: var(--body-bg); } + +:global(.cm-editor:not(:focus-within) .focus-visible) { + box-shadow: none; +} diff --git a/client/web/src/repo/blob/CodeMirrorBlob.tsx b/client/web/src/repo/blob/CodeMirrorBlob.tsx index c7d68f6907f1..2fcfbe520ba0 100644 --- a/client/web/src/repo/blob/CodeMirrorBlob.tsx +++ b/client/web/src/repo/blob/CodeMirrorBlob.tsx @@ -29,10 +29,12 @@ import { pin, updatePin } from './codemirror/hovercard' import { selectableLineNumbers, SelectedLineRange, selectLines } from './codemirror/linenumbers' import { lockFirstVisibleLine } from './codemirror/lock-line' import { navigateToLineOnAnyClickExtension } from './codemirror/navigate-to-any-line-on-click' +import { occurrenceAtPosition, positionAtCmPosition } from './codemirror/occurrence-utils' import { search } from './codemirror/search' import { sourcegraphExtensions } from './codemirror/sourcegraph-extensions' +import { selectOccurrence } from './codemirror/token-selection/code-intel-tooltips' import { tokenSelectionExtension } from './codemirror/token-selection/extension' -import { selectionFromLocation, selectRange } from './codemirror/token-selection/selections' +import { selectionFromLocation } from './codemirror/token-selection/selections' import { tokensAsLinks } from './codemirror/tokens-as-links' import { isValidLineRange } from './codemirror/utils' import { setBlobEditView } from './use-blob-store' @@ -246,18 +248,22 @@ export const Blob: React.FunctionComponent = props => { return } - // Sync editor selection with the URL so that triggering + // Sync editor selection/focus with the URL so that triggering // `history.goBack/goForward()` works similar to the "Go back" // command in VS Code. - const { range } = selectionFromLocation(editor, historyRef.current.location) - if (range) { - selectRange(editor, range) - // Automatically focus the content DOM to enable keyboard - // navigation. Without this automatic focus, users need to click - // on the blob view with the mouse. - // NOTE: this focus statment does not seem to have an effect - // when using macOS VoiceOver. - editor.contentDOM.focus({ preventScroll: true }) + const { selection } = selectionFromLocation(editor, historyRef.current.location) + if (selection) { + const position = positionAtCmPosition(editor, selection.from) + const occurrence = occurrenceAtPosition(editor.state, position) + if (occurrence) { + selectOccurrence(editor, occurrence) + // Automatically focus the content DOM to enable keyboard + // navigation. Without this automatic focus, users need to click + // on the blob view with the mouse. + // NOTE: this focus statment does not seem to have an effect + // when using macOS VoiceOver. + editor.contentDOM.focus({ preventScroll: true }) + } } } // editor is not provided because this should only be triggered after the diff --git a/client/web/src/repo/blob/codemirror/token-selection/hover.ts b/client/web/src/repo/blob/codemirror/token-selection/code-intel-tooltips.ts similarity index 57% rename from client/web/src/repo/blob/codemirror/token-selection/hover.ts rename to client/web/src/repo/blob/codemirror/token-selection/code-intel-tooltips.ts index 84b7a85c752f..e9176d09f36c 100644 --- a/client/web/src/repo/blob/codemirror/token-selection/hover.ts +++ b/client/web/src/repo/blob/codemirror/token-selection/code-intel-tooltips.ts @@ -1,105 +1,140 @@ -import { countColumn, Extension, SelectionRange, StateEffect, StateField } from '@codemirror/state' -import { - closeHoverTooltips, - Decoration, - DecorationSet, - EditorView, - getTooltip, - PluginValue, - showTooltip, - Tooltip, - ViewPlugin, - ViewUpdate, -} from '@codemirror/view' -import { isEqual } from 'lodash' +import { countColumn, Extension, Prec, StateEffect, StateField } from '@codemirror/state' +import { EditorView, getTooltip, PluginValue, showTooltip, Tooltip, ViewPlugin, ViewUpdate } from '@codemirror/view' import { BehaviorSubject, from, fromEvent, of, Subject, Subscription } from 'rxjs' import { catchError, debounceTime, filter, map, scan, switchMap, tap } from 'rxjs/operators' -import { HoverMerged, TextDocumentPositionParameters } from '@sourcegraph/client-api' -import { formatSearchParameters, LineOrPositionOrRange } from '@sourcegraph/common' +import { HoverMerged, TextDocumentPositionParameters } from '@sourcegraph/client-api/src' +import { formatSearchParameters, LineOrPositionOrRange } from '@sourcegraph/common/src' import { getOrCreateCodeIntelAPI } from '@sourcegraph/shared/src/codeintel/api' import { Occurrence, Position } from '@sourcegraph/shared/src/codeintel/scip' -import { createUpdateableField } from '@sourcegraph/shared/src/components/CodeMirrorEditor' import { toURIWithPath } from '@sourcegraph/shared/src/util/url' -import { blobPropsFacet } from '..' -import { - computeMouseDirection, - HOVER_DEBOUNCE_TIME, - MOUSE_NO_BUTTON, - pin, - selectionHighlightDecoration, -} from '../hovercard' +import { computeMouseDirection, HOVER_DEBOUNCE_TIME, MOUSE_NO_BUTTON, pin } from '../hovercard' +import { blobPropsFacet } from '../index' import { isInteractiveOccurrence, + occurrenceAtMouseEvent, occurrenceAtPosition, positionAtCmPosition, rangeToCmSelection, } from '../occurrence-utils' import { CodeIntelTooltip, HoverResult } from '../tooltips/CodeIntelTooltip' -import { preciseOffsetAtCoords, preciseWordAtCoords, uiPositionToOffset } from '../utils' +import { positionToOffset, preciseOffsetAtCoords, uiPositionToOffset } from '../utils' -export function hoverExtension(): Extension { - return [hoverCache, hoveredOccurrenceField, hoverTooltip, hoverManager, tooltipStyles, hoverField, pinManager] -} -export const hoverCache = StateField.define>>({ - create: () => new Map(), - update: value => value, -}) +import { preloadDefinition } from './definition' +import { showDocumentHighlightsForOccurrence } from './document-highlights' -export const closeHover = (view: EditorView): void => - // Always emit `closeHoverTooltips` alongside `setHoverEffect.of(null)` to - // fix an issue where the tooltip could get stuck if you rapidly press Space - // before the tooltip finishes loading. - view.dispatch({ effects: [setHoverEffect.of(null), closeHoverTooltips] }) -export const showHover = (view: EditorView, tooltip: Tooltip): void => - view.dispatch({ effects: setHoverEffect.of(tooltip) }) - -// intentionally not exported because clients should use the close/open hover -// helpers above. -const setHoverEffect = StateEffect.define() -export const hoverField = StateField.define({ - create: () => null, - update(tooltip, transactions) { - if (transactions.docChanged || transactions.selection) { - // Close hover when the selection moves and when the document - // changes (although that should not happen because we only support - // read-only mode). - return null - } - for (const effect of transactions.effects) { - if (effect.is(setHoverEffect)) { - tooltip = effect.value - } - } - return tooltip +type CodeIntelTooltipTrigger = 'focus' | 'hover' | 'pin' +type CodeIntelTooltipState = { occurrence: Occurrence; tooltip: Tooltip | null } | null + +export const setFocusedOccurrence = StateEffect.define() +export const setFocusedOccurrenceTooltip = StateEffect.define() +const setPinnedCodeIntelTooltipState = StateEffect.define() +const setHoveredCodeIntelTooltipState = StateEffect.define() + +/** + * {@link StateField} storing focused (selected), hovered and pinned {@link Occurrence}s and {@link Tooltip}s associate with them. + */ +export const codeIntelTooltipsState = StateField.define>({ + create() { + return { hover: null, focus: null, pin: null } }, - provide: field => showTooltip.from(field), -}) -export const setHoveredOccurrenceEffect = StateEffect.define() -export const hoveredOccurrenceField = StateField.define({ - create: () => null, update(value, transaction) { for (const effect of transaction.effects) { - if (effect.is(setHoveredOccurrenceEffect)) { - value = effect.value + if (effect.is(setFocusedOccurrence)) { + return { + ...value, + focus: effect.value ? { occurrence: effect.value, tooltip: null } : null, + } + } + if (effect.is(setFocusedOccurrenceTooltip)) { + return { + ...value, + focus: value.focus?.occurrence ? { ...value.focus, tooltip: effect.value } : null, + } + } + + if (effect.is(setHoveredCodeIntelTooltipState)) { + return { ...value, hover: effect.value } + } + + if (effect.is(setPinnedCodeIntelTooltipState)) { + return { ...value, pin: effect.value } } } + return value }, + provide(field) { + return [ + showTooltip.computeN([field], state => Object.values(state.field(field)).map(val => val?.tooltip ?? null)), + + /** + * If there is a focused occurrence set editor's tabindex to -1, so that pressing Shift+Tab moves the focus + * outside the editor instead of focusing the editor itself. + * + * Explicitly define extension precedence to override the [default tabindex value](https://sourcegraph.com/github.com/sourcegraph/sourcegraph@728ea45d1cc063cd60cbd552e00929c09cb8ced8/-/blob/client/web/src/repo/blob/CodeMirrorBlob.tsx?L47&subtree=true). + */ + Prec.high( + EditorView.contentAttributes.compute([field], state => ({ + tabindex: state.field(field).focus?.occurrence ? '-1' : '0', + })) + ), + ] + }, }) -const getPinnedOccurrence = (view: EditorView, pin: LineOrPositionOrRange | null): Occurrence | null => { - if (!pin || !pin.line || !pin.character) { - return null +export const getCodeIntelTooltipState = ( + view: EditorView, + key: CodeIntelTooltipTrigger +): { occurrence: Occurrence; tooltip: Tooltip | null } | null => view.state.field(codeIntelTooltipsState)[key] + +const focusOccurrence = (view: EditorView, occurrence: Occurrence): void => { + const offset = positionToOffset(view.state.doc, occurrence.range.end) + if (offset !== null) { + const node = view.domAtPos(offset).node + const element = node instanceof HTMLElement ? node : node.parentElement + const lineEl = element?.matches('.cm-line') ? element : element?.closest('.cm-line') + const interactiveOccurrenceEl = lineEl?.querySelector('.interactive-occurrence') + if (interactiveOccurrenceEl) { + interactiveOccurrenceEl.focus() + } } - const offset = uiPositionToOffset(view.state.doc, { line: pin.line, character: pin.character }) - if (offset === null) { - return null +} + +const preloadHoverData = (view: EditorView, occurrence: Occurrence): void => { + if (!view.state.field(hoverCache).has(occurrence)) { + hoverAtOccurrence(view, occurrence).then( + () => {}, + () => {} + ) } - return occurrenceAtPosition(view.state, positionAtCmPosition(view, offset)) ?? null } +const warmupOccurrence = (view: EditorView, occurrence: Occurrence): void => { + preloadHoverData(view, occurrence) + preloadDefinition(view, occurrence) +} + +/** + * Sets given occurrence to {@link codeIntelTooltipsState}, syncs editor selection with occurrence range, + * fetches hover, definition data and document highlights for occurrence, and focuses the selected occurrence DOM node. + */ +export const selectOccurrence = (view: EditorView, occurrence: Occurrence): void => { + warmupOccurrence(view, occurrence) + view.dispatch({ + effects: setFocusedOccurrence.of(occurrence), + selection: rangeToCmSelection(view.state, occurrence.range), + }) + showDocumentHighlightsForOccurrence(view, occurrence) + focusOccurrence(view, occurrence) +} + +const hoverCache = StateField.define>>({ + create: () => new Map(), + update: value => value, +}) + export async function getHoverTooltip(view: EditorView, pos: number): Promise { const cmLine = view.state.doc.lineAt(pos) const line = cmLine.number - 1 @@ -131,80 +166,6 @@ export function hoverAtOccurrence(view: EditorView, occurrence: Occurrence): Pro return contents } -// Extension that automatically displays the code-intel popover when the URL has -// `popover=pinned`, and removed this URL parameter when the user clicks -// anywhere on the file to dismiss the pinned popover. -const pinManager = ViewPlugin.fromClass( - class implements PluginValue { - public decorations: DecorationSet - private nextPin: Subject - private subscription: Subscription - - constructor(view: EditorView) { - this.decorations = Decoration.none - this.nextPin = new BehaviorSubject(view.state.field(pin)) - this.subscription = this.nextPin - .pipe( - map(pin => { - const occurrence = getPinnedOccurrence(view, pin) - return occurrence ? rangeToCmSelection(view.state, occurrence.range) : null - }), - tap(range => { - if (!range) { - this.computeDecorations(null) - } - }), - switchMap(range => - range - ? from(getHoverTooltip(view, range.from)).pipe( - tap(tooltip => this.computeDecorations(tooltip ? range : null)) - ) - : of(null) - ) - ) - .subscribe(tooltip => - // Scheduling the update for the next loop is necessary at the - // moment because we are triggering this effect in response to an - // editor update (pin field change) and you cannot synchronously - // trigger an update from an update. - window.requestAnimationFrame(() => view.dispatch({ effects: setHoverEffect.of(tooltip) })) - ) - } - - public update(update: ViewUpdate): void { - if (update.startState.field(pin) !== update.state.field(pin)) { - this.nextPin.next(update.state.field(pin)) - } - - if (update.selectionSet && update.state.field(pin)) { - // Remove `popover=pinned` from the URL when the user updates the selection. - const history = update.state.facet(blobPropsFacet).history - const params = new URLSearchParams(history.location.search) - params.delete('popover') - window.requestAnimationFrame(() => - // Use `history.push` instead of `history.replace` in case - // the user accidentally clicked somewhere without intending to - // dismiss the popover. - history.push({ ...history.location, search: formatSearchParameters(params) }) - ) - } - } - - public destroy(): void { - this.subscription.unsubscribe() - } - - private computeDecorations(range: SelectionRange | null): void { - this.decorations = range - ? Decoration.set(selectionHighlightDecoration.range(range.from, range.to)) - : Decoration.none - } - }, - { - decorations: ({ decorations }) => decorations, - } -) - async function hoverRequest( view: EditorView, occurrence: Occurrence, @@ -235,58 +196,9 @@ function isPrecise(hover: HoverMerged | null): boolean { return false } -const tooltipStyles = EditorView.theme({ - // Tooltip styles is a combination of the default wildcard PopoverContent component (https://github.com/sourcegraph/sourcegraph/blob/5de30f6fa1c59d66341e4dfc0c374cab0ad17bff/client/wildcard/src/components/Popover/components/popover-content/PopoverContent.module.scss#L1-L10) - // and the floating tooltip-like storybook usage example (https://github.com/sourcegraph/sourcegraph/blob/5de30f6fa1c59d66341e4dfc0c374cab0ad17bff/client/wildcard/src/components/Popover/story/Popover.story.module.scss#L54-L62) - // ignoring the min/max width rules. - '.cm-tooltip.tmp-tooltip': { - fontSize: '0.875rem', - backgroundClip: 'padding-box', - backgroundColor: 'var(--dropdown-bg)', - border: '1px solid var(--dropdown-border-color)', - borderRadius: 'var(--popover-border-radius)', - color: 'var(--body-color)', - boxShadow: 'var(--dropdown-shadow)', - padding: '0.5rem', - }, - - '.cm-tooltip-above:not(.tmp-tooltip), .cm-tooltip-below:not(.tmp-tooltip)': { - border: 'unset', - }, - - '.cm-tooltip.cm-tooltip-above.tmp-tooltip .cm-tooltip-arrow:before': { - borderTopColor: 'var(--dropdown-border-color)', - }, - '.cm-tooltip.cm-tooltip-above.tmp-tooltip .cm-tooltip-arrow:after': { - borderTopColor: 'var(--dropdown-bg)', - }, -}) - -/** - * Field for storing visible hovered occurrence range and visible code-intel tooltip for this occurrence. - */ -const [hoverTooltip, setHoverTooltip] = createUpdateableField<{ - tooltip: Tooltip - range: { from: number; to: number } -} | null>(null, field => [ - // show code-intel tooltip - showTooltip.computeN([field], state => [state.field(field)?.tooltip ?? null]), - - // highlight occurrence with tooltip - EditorView.decorations.compute([field], state => { - const value = state.field(field) - - if (!value?.tooltip || !value?.range) { - return Decoration.none - } - - return Decoration.set(selectionHighlightDecoration.range(value.range.from, value.range.to)) - }), -]) - /** * Listens to mousemove events, determines whether the position under the mouse - * cursor is a valid {@link Occurrence}, fetches hover information as necessary and updates {@link hoverTooltip}. + * cursor is a valid {@link Occurrence}, fetches hover information as necessary and updates {@link codeIntelTooltipsState}. */ const hoverManager = ViewPlugin.fromClass( class HoverManager implements PluginValue { @@ -333,12 +245,15 @@ const hoverManager = ViewPlugin.fromClass( return true } - const currentTooltip = view.state.field(hoverTooltip) - if (!currentTooltip) { + const currentOccurrence = getCodeIntelTooltipState(view, 'hover')?.occurrence + if (!currentOccurrence) { return true } - return !isOffsetInHoverRange(offset, currentTooltip.range) + return !isOffsetInHoverRange( + offset, + rangeToCmSelection(this.view.state, currentOccurrence.range) + ) }), // To make it easier to reach the tooltip with the mouse, we determine @@ -355,7 +270,7 @@ const hoverManager = ViewPlugin.fromClass( }, next ) => { - const currentTooltip = view.state.field(hoverTooltip)?.tooltip + const currentTooltip = getCodeIntelTooltipState(view, 'hover')?.tooltip if (!currentTooltip) { return next } @@ -390,37 +305,64 @@ const hoverManager = ViewPlugin.fromClass( return of('HIDE' as const) } - return of(preciseWordAtCoords(this.view, position)).pipe( - // if the hovered token changed, hide the existing tooltip and proceed with fetching of the new one - tap(range => { - const currentRange = view.state.field(hoverTooltip)?.range - if (range && currentRange && !isEqual(currentRange, range)) { - setHoverTooltip(view, null) + const offset = preciseOffsetAtCoords(this.view, position) + if (!offset) { + return of(null) + } + const pos = positionAtCmPosition(view, offset) + return of(occurrenceAtPosition(this.view.state, pos)).pipe( + tap(occurrence => { + const current = getCodeIntelTooltipState(view, 'hover')?.occurrence + if (current && occurrence && current !== occurrence) { + view.dispatch({ + effects: setHoveredCodeIntelTooltipState.of(null), + }) } }), - switchMap(range => - range - ? from(getHoverTooltip(view, range.from)).pipe( - catchError(() => of(null)), - map(tooltip => (tooltip ? { tooltip, range } : null)) - ) - : of(null) - ), - map(tooltip => ({ position, tooltip })) + tap(occurrence => { + if (occurrence) { + preloadDefinition(view, occurrence) + } + }), + switchMap(occurrence => { + if (!occurrence) { + return of(null) + } + + const offset = positionToOffset(this.view.state.doc, occurrence.range.start) + if (offset === null) { + return of(null) + } + + return from(getHoverTooltip(view, offset)).pipe( + catchError(() => of(null)), + map(tooltip => (tooltip ? { tooltip, occurrence } : null)) + ) + }), + map(hover => ({ position, hover })) ) }) ) .subscribe(next => { if (next === 'HIDE') { - setHoverTooltip(view, null) + view.dispatch({ effects: setHoveredCodeIntelTooltipState.of(null) }) return } // We only change the tooltip when // a) There is a new tooltip at the position (tooltip !== null) // b) there is no tooltip and the mouse is moving away from the tooltip - if (next.tooltip || next.position.direction !== 'towards') { - setHoverTooltip(view, next.tooltip) + if (next?.hover || next?.position.direction !== 'towards') { + if (!next?.hover?.occurrence) { + view.dispatch({ + effects: setHoveredCodeIntelTooltipState.of(null), + }) + return + } + + view.dispatch({ + effects: setHoveredCodeIntelTooltipState.of(next.hover), + }) } }) ) @@ -435,3 +377,128 @@ const hoverManager = ViewPlugin.fromClass( function isOffsetInHoverRange(offset: number, range: { from: number; to: number }): boolean { return range.from <= offset && offset <= range.to } + +const getPinnedOccurrence = (view: EditorView, pin: LineOrPositionOrRange | null): Occurrence | null => { + if (!pin || !pin.line || !pin.character) { + return null + } + const offset = uiPositionToOffset(view.state.doc, { line: pin.line, character: pin.character }) + if (offset === null) { + return null + } + return occurrenceAtPosition(view.state, positionAtCmPosition(view, offset)) ?? null +} + +// Extension that automatically displays the code-intel popover when the URL has +// `popover=pinned`, and removed this URL parameter when the user clicks +// anywhere on the file to dismiss the pinned popover. +const pinManager = ViewPlugin.fromClass( + class implements PluginValue { + private nextPin: Subject + private subscription: Subscription + + constructor(view: EditorView) { + this.nextPin = new BehaviorSubject(view.state.field(pin)) + this.subscription = this.nextPin + .pipe( + map(pin => getPinnedOccurrence(view, pin)), + tap(occurrence => { + if (!occurrence) { + window.requestAnimationFrame(() => + view.dispatch({ effects: setPinnedCodeIntelTooltipState.of(null) }) + ) + } + }), + switchMap(occurrence => { + if (!occurrence) { + return of(null) + } + + return from(getHoverTooltip(view, rangeToCmSelection(view.state, occurrence.range).from)).pipe( + map(tooltip => ({ occurrence, tooltip })) + ) + }) + ) + .subscribe(pin => + // Scheduling the update for the next loop is necessary at the + // moment because we are triggering this effect in response to an + // editor update (pin field change) and you cannot synchronously + // trigger an update from an update. + window.requestAnimationFrame(() => + view.dispatch({ effects: setPinnedCodeIntelTooltipState.of(pin) }) + ) + ) + } + + public update(update: ViewUpdate): void { + if (update.startState.field(pin) !== update.state.field(pin)) { + this.nextPin.next(update.state.field(pin)) + } + + if (update.selectionSet && update.state.field(pin)) { + // Remove `popover=pinned` from the URL when the user updates the selection. + const history = update.state.facet(blobPropsFacet).history + const params = new URLSearchParams(history.location.search) + params.delete('popover') + window.requestAnimationFrame(() => + // Use `history.push` instead of `history.replace` in case + // the user accidentally clicked somewhere without intending to + // dismiss the popover. + history.push({ ...history.location, search: formatSearchParameters(params) }) + ) + } + } + + public destroy(): void { + this.subscription.unsubscribe() + } + } +) + +const tooltipStyles = EditorView.theme({ + // Tooltip styles is a combination of the default wildcard PopoverContent component (https://github.com/sourcegraph/sourcegraph/blob/5de30f6fa1c59d66341e4dfc0c374cab0ad17bff/client/wildcard/src/components/Popover/components/popover-content/PopoverContent.module.scss#L1-L10) + // and the floating tooltip-like storybook usage example (https://github.com/sourcegraph/sourcegraph/blob/5de30f6fa1c59d66341e4dfc0c374cab0ad17bff/client/wildcard/src/components/Popover/story/Popover.story.module.scss#L54-L62) + // ignoring the min/max width rules. + '.cm-tooltip.tmp-tooltip': { + fontSize: '0.875rem', + backgroundClip: 'padding-box', + backgroundColor: 'var(--dropdown-bg)', + border: '1px solid var(--dropdown-border-color)', + borderRadius: 'var(--popover-border-radius)', + color: 'var(--body-color)', + boxShadow: 'var(--dropdown-shadow)', + padding: '0.5rem', + }, + + '.cm-tooltip-above:not(.tmp-tooltip), .cm-tooltip-below:not(.tmp-tooltip)': { + border: 'unset', + }, + + '.cm-tooltip.cm-tooltip-above.tmp-tooltip .cm-tooltip-arrow:before': { + borderTopColor: 'var(--dropdown-border-color)', + }, + '.cm-tooltip.cm-tooltip-above.tmp-tooltip .cm-tooltip-arrow:after': { + borderTopColor: 'var(--dropdown-bg)', + }, +}) + +export function codeIntelTooltipsExtension(): Extension { + return [ + codeIntelTooltipsState, + hoverCache, + hoverManager, + pinManager, + tooltipStyles, + + EditorView.domEventHandlers({ + click(event, view) { + // Close selected (focused) code-intel tooltip on click outside. + const atEvent = occurrenceAtMouseEvent(view, event) + const current = getCodeIntelTooltipState(view, 'focus') + if (atEvent?.occurrence !== current?.occurrence && current?.tooltip instanceof CodeIntelTooltip) { + view.dispatch({ effects: setFocusedOccurrenceTooltip.of(null) }) + } + }, + }), + ] +} diff --git a/client/web/src/repo/blob/codemirror/token-selection/decorations.ts b/client/web/src/repo/blob/codemirror/token-selection/decorations.ts new file mode 100644 index 000000000000..c51e2d29eea1 --- /dev/null +++ b/client/web/src/repo/blob/codemirror/token-selection/decorations.ts @@ -0,0 +1,107 @@ +import { Extension, Range } from '@codemirror/state' +import { Decoration, EditorView } from '@codemirror/view' +import classNames from 'classnames' + +import { positionToOffset, sortRangeValuesByStart } from '../utils' + +import { codeIntelTooltipsState } from './code-intel-tooltips' +import { definitionUrlField } from './definition' +import { documentHighlightsField, findByOccurrence } from './document-highlights' +import { isModifierKeyHeld } from './modifier-click' + +function sortByFromPosition(ranges: Range[]): Range[] { + return ranges.sort((a, b) => a.from - b.from) +} + +/** + * Extension providing decorations for focused, hovered, pinned occurrences, and document highlights. + * We combine all of these into a single extension to avoid the focused element blur caused by its removal from the DOM. + */ +export function interactiveOccurrencesExtension(): Extension { + return [ + EditorView.decorations.compute( + [codeIntelTooltipsState, documentHighlightsField, definitionUrlField, isModifierKeyHeld], + state => { + const { focus, hover, pin } = state.field(codeIntelTooltipsState) + const decorations = [] + + if (focus) { + decorations.push({ + decoration: Decoration.mark({ + class: classNames( + 'interactive-occurrence', // used as interactive occurrence selector + 'focus-visible', // prevents code editor from blur when focused element inside it changes + 'sourcegraph-document-highlight' // highlights the selected (focused) occurrence + ), + attributes: { + // Selected (focused) occurrence is the only focusable element in the editor. + // This helps to maintain the focus position when editor is blurred and then focused again. + tabindex: '0', + }, + }), + range: focus.occurrence.range, + }) + + // Decorate selected (focused) occurrence document highlights. + const highlights = state.field(documentHighlightsField) + const focusedOccurrenceHighlight = findByOccurrence(highlights, focus.occurrence) + if (focusedOccurrenceHighlight) { + for (const highlight of sortRangeValuesByStart(highlights)) { + if (highlight === focusedOccurrenceHighlight) { + // Focused occurrence is already highlighted. + continue + } + + decorations.push({ + decoration: Decoration.mark({ + class: 'sourcegraph-document-highlight', + }), + range: highlight.range, + }) + } + } + } + + if (pin) { + decorations.push({ + decoration: Decoration.mark({ class: 'selection-highlight' }), + range: pin.occurrence.range, + }) + } + + if (hover) { + decorations.push({ + decoration: Decoration.mark({ + class: classNames('selection-highlight', { + // If the user is hovering over a selected (focused) occurrence with a definition holding the modifier key, + // add a class to make an occurrence to look like a link. + ['cm-token-selection-definition-ready']: + state.field(isModifierKeyHeld) && + state.field(definitionUrlField).get(hover.occurrence).hasOccurrence, + }), + }), + range: hover.occurrence.range, + }) + } + + const ranges = decorations.reduce((acc, { decoration, range }) => { + const from = positionToOffset(state.doc, range.start) + const to = positionToOffset(state.doc, range.end) + + if (from !== null && to !== null) { + acc.push(decoration.range(from, to)) + } + + return acc + }, [] as Range[]) + + return Decoration.set(sortByFromPosition(ranges)) + } + ), + EditorView.theme({ + '.cm-token-selection-definition-ready': { + textDecoration: 'underline', + }, + }), + ] +} diff --git a/client/web/src/repo/blob/codemirror/token-selection/definition.ts b/client/web/src/repo/blob/codemirror/token-selection/definition.ts index c901d0bd5a77..674a80ef6dac 100644 --- a/client/web/src/repo/blob/codemirror/token-selection/definition.ts +++ b/client/web/src/repo/blob/codemirror/token-selection/definition.ts @@ -1,5 +1,5 @@ -import { Facet, RangeSet, StateEffect, StateField } from '@codemirror/state' -import { Decoration, EditorView } from '@codemirror/view' +import { Extension, StateEffect, StateField } from '@codemirror/state' +import { EditorView } from '@codemirror/view' import * as H from 'history' import { TextDocumentPositionParameters } from '@sourcegraph/client-api' @@ -9,14 +9,18 @@ import { Occurrence, Position, Range } from '@sourcegraph/shared/src/codeintel/s import { BlobViewState, parseRepoURI, toPrettyBlobURL, toURIWithPath } from '@sourcegraph/shared/src/util/url' import { blobPropsFacet } from '..' -import { isInteractiveOccurrence, occurrenceAtMouseEvent, OccurrenceMap, rangeToCmSelection } from '../occurrence-utils' +import { + isInteractiveOccurrence, + occurrenceAtMouseEvent, + occurrenceAtPosition, + OccurrenceMap, +} from '../occurrence-utils' import { LoadingTooltip } from '../tooltips/LoadingTooltip' import { showTemporaryTooltip } from '../tooltips/TemporaryTooltip' import { preciseOffsetAtCoords } from '../utils' -import { hoveredOccurrenceField } from './hover' -import { isModifierKey, isModifierKeyHeld } from './modifier-click' -import { selectOccurrence, selectRange } from './selections' +import { getCodeIntelTooltipState, selectOccurrence, setFocusedOccurrenceTooltip } from './code-intel-tooltips' +import { isModifierKey } from './modifier-click' export interface DefinitionResult { handler: (position: Position) => void @@ -24,11 +28,9 @@ export interface DefinitionResult { locations: Location[] atTheDefinition?: boolean } -const definitionReady = Decoration.mark({ - class: 'cm-token-selection-definition-ready', -}) + const setDefinitionEffect = StateEffect.define>() -const definitionUrlField = StateField.define>({ +export const definitionUrlField = StateField.define>({ create: () => new OccurrenceMap(new Map(), 'empty-definition'), update(value, transaction) { for (const effect of transaction.effects) { @@ -45,34 +47,18 @@ export const definitionCache = StateField.define value, }) -export const underlinedDefinitionFacet = Facet.define({ - combine: props => props[0], - enables: () => [ - definitionUrlField, - EditorView.decorations.compute([definitionUrlField, hoveredOccurrenceField, isModifierKeyHeld], state => { - const occ = state.field(hoveredOccurrenceField) - const { value: url, hasOccurrence: hasDefinition } = state.field(definitionUrlField).get(occ) - if (occ && state.field(isModifierKeyHeld) && hasDefinition) { - const range = rangeToCmSelection(state, occ.range) - if (range.from === range.to) { - return RangeSet.empty - } - if (url) { - // Insert an HTML link to support Context-menu>Open-link-in-new-tab - const definitionURL = Decoration.mark({ - attributes: { - href: url, - }, - tagName: 'a', - }) - return RangeSet.of([definitionURL.range(range.from, range.to)]) - } - return RangeSet.of([definitionReady.range(range.from, range.to)]) - } - return RangeSet.empty - }), - ], -}) +export function definitionExtension(): Extension { + return [definitionCache, definitionUrlField] +} + +export function preloadDefinition(view: EditorView, occurrence: Occurrence): void { + if (!view.state.field(definitionCache).has(occurrence)) { + goToDefinitionAtOccurrence(view, occurrence).then( + () => {}, + () => {} + ) + } +} export function goToDefinitionOnMouseEvent( view: EditorView, @@ -85,17 +71,32 @@ export function goToDefinitionOnMouseEvent( } if (isInteractiveOccurrence(atEvent.occurrence)) { selectOccurrence(view, atEvent.occurrence) + + // Ensure editor remains focused for the keyboard navigation to work + view.contentDOM.focus() } if (!isModifierKey(event) && !options?.isLongClick) { return } - const spinner = new LoadingTooltip(view, preciseOffsetAtCoords(view, { x: event.clientX, y: event.clientY })) + + const offset = preciseOffsetAtCoords(view, { x: event.clientX, y: event.clientY }) + if (offset === null) { + return + } + + view.dispatch({ effects: setFocusedOccurrenceTooltip.of(new LoadingTooltip(offset)) }) goToDefinitionAtOccurrence(view, atEvent.occurrence) .then( ({ handler }) => handler(atEvent.position), () => {} ) - .finally(() => spinner.stop()) + .finally(() => { + // close loading tooltip if any + const current = getCodeIntelTooltipState(view, 'focus') + if (current?.tooltip instanceof LoadingTooltip && current?.occurrence === atEvent.occurrence) { + view.dispatch({ effects: setFocusedOccurrenceTooltip.of(null) }) + } + }) } export function goToDefinitionAtOccurrence(view: EditorView, occurrence: Occurrence): Promise { @@ -183,12 +184,6 @@ async function goToDefinition( previousURL?: string } const history = view.state.facet(blobPropsFacet).history as H.History - const selectionRange = Range.fromNumbers( - range.start.line, - range.start.character, - range.end.line, - range.end.character - ) const hrefFrom = locationToURL(locationFrom) // Don't push URLs into the history if the last goto-def // action was from the same URL same as this action. This @@ -200,7 +195,13 @@ async function goToDefinition( history.push(hrefFrom) } if (uri === params.textDocument.uri) { - selectRange(view, selectionRange) + const definitionOccurrence = occurrenceAtPosition( + view.state, + new Position(range.start.line, range.start.character) + ) + if (definitionOccurrence) { + selectOccurrence(view, definitionOccurrence) + } } if (shouldPushHistory) { history.push(hrefTo, { previousURL: hrefFrom }) diff --git a/client/web/src/repo/blob/codemirror/token-selection/document-highlights.ts b/client/web/src/repo/blob/codemirror/token-selection/document-highlights.ts index 4dd3c9381788..3d0c636de5ea 100644 --- a/client/web/src/repo/blob/codemirror/token-selection/document-highlights.ts +++ b/client/web/src/repo/blob/codemirror/token-selection/document-highlights.ts @@ -8,15 +8,12 @@ import { createUpdateableField } from '@sourcegraph/shared/src/components/CodeMi import { toURIWithPath } from '@sourcegraph/shared/src/util/url' import { blobPropsFacet } from '..' -import { showDocumentHighlights } from '../document-highlights' -export const documentHighlightCache = StateField.define>>({ +const documentHighlightCache = StateField.define>>({ create: () => new Map(), update: value => value, }) -const [documentHighlightsField, , setDocumentHighlights] = createUpdateableField([], field => - showDocumentHighlights.from(field) -) +export const [documentHighlightsField, , setDocumentHighlights] = createUpdateableField([]) async function getDocumentHighlights(view: EditorView, occurrence: Occurrence): Promise { const cache = view.state.field(documentHighlightCache) @@ -43,6 +40,19 @@ export function showDocumentHighlightsForOccurrence(view: EditorView, occurrence ) } +export function findByOccurrence( + highlights: DocumentHighlight[], + occurrence: Occurrence +): DocumentHighlight | undefined { + return highlights.find( + highlight => + occurrence.range.start.line === highlight.range.start.line && + occurrence.range.start.character === highlight.range.start.character && + occurrence.range.end.line === highlight.range.end.line && + occurrence.range.end.character === highlight.range.end.character + ) +} + export function documentHighlightsExtension(): Extension { return [documentHighlightCache, documentHighlightsField] } diff --git a/client/web/src/repo/blob/codemirror/token-selection/extension.ts b/client/web/src/repo/blob/codemirror/token-selection/extension.ts index 46bc2325b9b8..e87181dc0794 100644 --- a/client/web/src/repo/blob/codemirror/token-selection/extension.ts +++ b/client/web/src/repo/blob/codemirror/token-selection/extension.ts @@ -1,14 +1,15 @@ import { Extension } from '@codemirror/state' -import { EditorView, keymap } from '@codemirror/view' +import { EditorView } from '@codemirror/view' import { MOUSE_MAIN_BUTTON } from '../utils' -import { definitionCache, goToDefinitionOnMouseEvent, underlinedDefinitionFacet } from './definition' +import { codeIntelTooltipsExtension } from './code-intel-tooltips' +import { interactiveOccurrencesExtension } from './decorations' +import { definitionExtension, goToDefinitionOnMouseEvent } from './definition' import { documentHighlightsExtension } from './document-highlights' -import { hoverExtension } from './hover' -import { tokenSelectionKeyBindings } from './keybindings' -import { modifierClickFacet } from './modifier-click' -import { selectedOccurrence, syncSelectionWithURL, tokenSelectionTheme } from './selections' +import { keyboardShortcutsExtension } from './keybindings' +import { modifierClickExtension } from './modifier-click' +import { fallbackOccurrences, syncOccurrenceWithURL } from './selections' const LONG_CLICK_DURATION = 500 @@ -32,7 +33,7 @@ class MouseEvents { } // Heuristic to approximate a click event between two mouse events (for -// exampole, mousedown and mouseup). The heuristic returns true based on the +// example, mousedown and mouseup). The heuristic returns true based on the // distance between the coordinates (clientY/clientX) of the two events. function isClickDistance(event1: MouseEvent, event2: MouseEvent): boolean { const distanceX = Math.abs(event1.clientX - event2.clientX) @@ -45,16 +46,16 @@ function isClickDistance(event1: MouseEvent, event2: MouseEvent): boolean { export function tokenSelectionExtension(): Extension { const events = new MouseEvents() + return [ + fallbackOccurrences, + syncOccurrenceWithURL, documentHighlightsExtension(), - modifierClickFacet.of(false), - tokenSelectionTheme, - selectedOccurrence.of(null), - definitionCache, - underlinedDefinitionFacet.of(null), - hoverExtension(), - keymap.of(tokenSelectionKeyBindings), - syncSelectionWithURL, + codeIntelTooltipsExtension(), + interactiveOccurrencesExtension(), + modifierClickExtension(), + definitionExtension(), + keyboardShortcutsExtension(), EditorView.domEventHandlers({ // Approximate `click` with `mouseup` because `click` does not get // triggered in the scenario when the user holds down the meta-key, diff --git a/client/web/src/repo/blob/codemirror/token-selection/keybindings.ts b/client/web/src/repo/blob/codemirror/token-selection/keybindings.ts index 73041b3df082..39cdaac884ad 100644 --- a/client/web/src/repo/blob/codemirror/token-selection/keybindings.ts +++ b/client/web/src/repo/blob/codemirror/token-selection/keybindings.ts @@ -1,64 +1,113 @@ -import { defaultKeymap } from '@codemirror/commands' -import { KeyBinding } from '@codemirror/view' +import { + selectCharLeft, + selectCharRight, + selectGroupLeft, + selectGroupRight, + selectLineDown, + selectLineUp, +} from '@codemirror/commands' +import { Extension } from '@codemirror/state' +import { EditorView, KeyBinding, keymap, layer, RectangleMarker } from '@codemirror/view' import { blobPropsFacet } from '..' import { syntaxHighlight } from '../highlight' -import { occurrenceAtPosition, positionAtCmPosition, closestOccurrenceByCharacter } from '../occurrence-utils' +import { positionAtCmPosition, closestOccurrenceByCharacter } from '../occurrence-utils' import { CodeIntelTooltip } from '../tooltips/CodeIntelTooltip' import { LoadingTooltip } from '../tooltips/LoadingTooltip' +import { positionToOffset } from '../utils' +import { + getCodeIntelTooltipState, + setFocusedOccurrenceTooltip, + selectOccurrence, + getHoverTooltip, +} from './code-intel-tooltips' import { goToDefinitionAtOccurrence } from './definition' -import { closeHover, getHoverTooltip, hoverField, showHover } from './hover' import { isModifierKeyHeld } from './modifier-click' -import { selectOccurrence } from './selections' -const keybindings: readonly KeyBinding[] = [ +const keybindings: KeyBinding[] = [ { key: 'Space', run(view) { - const hover = view.state.field(hoverField) - // Space toggles the codeintel tooltip so we guard against temporary - // tooltips like "loading" or "no definition found". - if (hover !== null && hover instanceof CodeIntelTooltip) { - closeHover(view) + const selected = getCodeIntelTooltipState(view, 'focus') + if (!selected) { return true } - const position = view.state.selection.main.from - const spinner = new LoadingTooltip(view, position) - getHoverTooltip(view, position) - .then( - value => (value ? showHover(view, value) : closeHover(view)), - () => {} - ) - .finally(() => spinner.stop()) + if (selected.tooltip instanceof CodeIntelTooltip) { + view.dispatch({ effects: setFocusedOccurrenceTooltip.of(null) }) + return true + } + + const offset = positionToOffset(view.state.doc, selected.occurrence.range.start) + if (offset === null) { + return true + } + + // show loading tooltip + view.dispatch({ effects: setFocusedOccurrenceTooltip.of(new LoadingTooltip(offset)) }) + + getHoverTooltip(view, offset) + .then(value => view.dispatch({ effects: setFocusedOccurrenceTooltip.of(value) })) + .finally(() => { + // close loading tooltip if any + const current = getCodeIntelTooltipState(view, 'focus') + if (current?.tooltip instanceof LoadingTooltip && current?.occurrence === selected.occurrence) { + view.dispatch({ effects: setFocusedOccurrenceTooltip.of(null) }) + } + }) + + return true + }, + }, + { + key: 'Escape', + run(view) { + const current = getCodeIntelTooltipState(view, 'focus') + if (current?.tooltip instanceof CodeIntelTooltip) { + view.dispatch({ effects: setFocusedOccurrenceTooltip.of(null) }) + } + return true }, }, + { key: 'Enter', run(view) { - const position = positionAtCmPosition(view, view.state.selection.main.from) - const occurrence = occurrenceAtPosition(view.state, position) - if (!occurrence) { + const selected = getCodeIntelTooltipState(view, 'focus') + if (!selected?.occurrence) { return false } - const spinner = new LoadingTooltip(view, view.state.selection.main.from) - goToDefinitionAtOccurrence(view, occurrence) + + const offset = positionToOffset(view.state.doc, selected.occurrence.range.start) + if (offset === null) { + return true + } + + // show loading tooltip + view.dispatch({ effects: setFocusedOccurrenceTooltip.of(new LoadingTooltip(offset)) }) + + goToDefinitionAtOccurrence(view, selected.occurrence) .then( ({ handler, url }) => { if (view.state.field(isModifierKeyHeld) && url) { window.open(url, '_blank') } else { - handler(position) + handler(selected.occurrence.range.start) } }, () => {} ) - .finally(() => spinner.stop()) + .finally(() => { + // hide loading tooltip + view.dispatch({ effects: setFocusedOccurrenceTooltip.of(null) }) + }) + return true }, }, + { key: 'Mod-ArrowRight', run(view) { @@ -73,42 +122,79 @@ const keybindings: readonly KeyBinding[] = [ return true }, }, +] + +/** + * Keybindings to support text selection. + * Modified version of a [standard CodeMirror keymap](https://sourcegraph.com/github.com/codemirror/commands@ca4171b381dc487ec0313cb0ea4dd29151c7a792/-/blob/src/commands.ts?L791-858). + */ +const textSelectionKeybindings: KeyBinding[] = [ { key: 'ArrowLeft', - run(view) { - const position = positionAtCmPosition(view, view.state.selection.main.from) + shift: selectCharLeft, + }, + + { + key: 'ArrowRight', + shift: selectCharRight, + }, + + { + key: 'Mod-ArrowLeft', + mac: 'Alt-ArrowLeft', + shift: selectGroupLeft, + }, + + { + key: 'Mod-ArrowRight', + mac: 'Alt-ArrowRight', + shift: selectGroupRight, + }, + + { + key: 'ArrowUp', + shift: selectLineUp, + }, + + { + key: 'ArrowDown', + shift: selectLineDown, + }, +] + +function keyDownHandler(event: KeyboardEvent, view: EditorView): boolean { + switch (event.key) { + case 'ArrowLeft': { + const selectedOccurrence = getCodeIntelTooltipState(view, 'focus')?.occurrence + const position = selectedOccurrence?.range.start || positionAtCmPosition(view, view.viewport.from) const table = view.state.facet(syntaxHighlight) - const line = position.line - const occurrence = closestOccurrenceByCharacter(line, table, position, occurrence => + const occurrence = closestOccurrenceByCharacter(position.line, table, position, occurrence => occurrence.range.start.isSmaller(position) ) if (occurrence) { selectOccurrence(view, occurrence) } + return true - }, - }, - { - key: 'ArrowRight', - run(view) { - const position = positionAtCmPosition(view, view.state.selection.main.from) + } + case 'ArrowRight': { + const selectedOccurrence = getCodeIntelTooltipState(view, 'focus')?.occurrence + const position = selectedOccurrence?.range.start || positionAtCmPosition(view, view.viewport.from) const table = view.state.facet(syntaxHighlight) - const line = position.line - const occurrence = closestOccurrenceByCharacter(line, table, position, occurrence => + const occurrence = closestOccurrenceByCharacter(position.line, table, position, occurrence => occurrence.range.start.isGreater(position) ) if (occurrence) { selectOccurrence(view, occurrence) } + return true - }, - }, - { - key: 'ArrowDown', - run(view) { - const position = positionAtCmPosition(view, view.state.selection.main.from) + } + case 'ArrowUp': { + const selectedOccurrence = getCodeIntelTooltipState(view, 'focus')?.occurrence + const position = selectedOccurrence?.range.start || positionAtCmPosition(view, view.viewport.from) const table = view.state.facet(syntaxHighlight) - for (let line = position.line + 1; line < table.lineIndex.length; line++) { + for (let line = position.line - 1; line >= 0; line--) { const occurrence = closestOccurrenceByCharacter(line, table, position) if (occurrence) { selectOccurrence(view, occurrence) @@ -116,14 +202,12 @@ const keybindings: readonly KeyBinding[] = [ } } return true - }, - }, - { - key: 'ArrowUp', - run(view) { - const position = positionAtCmPosition(view, view.state.selection.main.from) + } + case 'ArrowDown': { + const selectedOccurrence = getCodeIntelTooltipState(view, 'focus')?.occurrence + const position = selectedOccurrence?.range.start || positionAtCmPosition(view, view.viewport.from) const table = view.state.facet(syntaxHighlight) - for (let line = position.line - 1; line >= 0; line--) { + for (let line = position.line + 1; line < table.lineIndex.length; line++) { const occurrence = closestOccurrenceByCharacter(line, table, position) if (occurrence) { selectOccurrence(view, occurrence) @@ -131,36 +215,69 @@ const keybindings: readonly KeyBinding[] = [ } } return true - }, + } + + default: + return false + } +} + +/** + * Keyboard event handlers defined via {@link keymap} facet do not work with the screen reader enabled while + * keypress handlers defined via {@link EditorView.domEventHandlers} still work. + */ +function occurrenceKeyboardNavigation(): Extension { + return [ + EditorView.domEventHandlers({ + keydown: keyDownHandler, + }), + ] +} + +/** + * For some reason, editor selection updates made by {@link textSelectionKeybindings} handlers are not synced with the + * [browser selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection). This function is a workaround to ensure + * that the browser selection is updated when the editor selection is updated. + * + * @see https://codemirror.net/docs/ref/#view.drawSelection + * @see https://sourcegraph.com/github.com/codemirror/view@84f483ae4097a71d04374cdb24c5edc09d211105/-/blob/src/draw-selection.ts?L92-102 + */ +const selectionLayer = layer({ + above: false, + markers(view) { + return view.state.selection.ranges + .map(range => (range.empty ? [] : RectangleMarker.forRange(view, 'cm-selectionBackground', range))) + .reduce((a, b) => a.concat(b)) }, - { - key: 'Escape', - run(view) { - closeHover(view) - view.contentDOM.blur() - return true - }, + update(update) { + return update.docChanged || update.selectionSet || update.viewportChanged }, -] + class: 'cm-selectionLayer', +}) -const remappedKeyBindings: Record = { - // Open definition in a new tab. - Enter: 'Mod-Enter', - // Vim bindings. - ArrowUp: 'k', - ArrowDown: 'j', - ArrowLeft: 'h', - ArrowRight: 'l', +/** + * Extension that adds support for the text selection with keyboard. + */ +function textSelectionExtension(): Extension { + return [ + keymap.of(textSelectionKeybindings), + selectionLayer, + EditorView.theme({ + '.cm-line': { + '& ::selection': { + backgroundColor: 'transparent !important', + }, + '&::selection': { + backgroundColor: 'transparent !important', + }, + }, + '.cm-selectionLayer .cm-selectionBackground': { + background: 'var(--code-selection-bg)', + }, + }), + ] } -export const tokenSelectionKeyBindings = [ - ...keybindings.flatMap(keybinding => { - const remappedKey = remappedKeyBindings[keybinding.key ?? ''] - if (remappedKey) { - return [keybinding, { ...keybinding, key: remappedKey }] - } - return [keybinding] - }), - // Fallback to the default arrow keys to allow, for example, Shift-ArrowLeft - ...defaultKeymap.filter(({ key }) => key?.includes('Arrow')), -] +export function keyboardShortcutsExtension(): Extension { + return [textSelectionExtension(), keymap.of(keybindings), occurrenceKeyboardNavigation()] +} diff --git a/client/web/src/repo/blob/codemirror/token-selection/modifier-click.ts b/client/web/src/repo/blob/codemirror/token-selection/modifier-click.ts index 60b3e50306b2..9c1bb72605b3 100644 --- a/client/web/src/repo/blob/codemirror/token-selection/modifier-click.ts +++ b/client/web/src/repo/blob/codemirror/token-selection/modifier-click.ts @@ -1,4 +1,4 @@ -import { Facet, StateEffect, StateField } from '@codemirror/state' +import { Extension, StateEffect, StateField } from '@codemirror/state' import { EditorView, PluginValue, ViewPlugin } from '@codemirror/view' import { isMacPlatform } from '@sourcegraph/common' @@ -58,9 +58,16 @@ export function isModifierKey(event: KeyboardEvent | MouseEvent): boolean { return event.ctrlKey } -export const modifierClickFacet = Facet.define({ - combine: sources => sources[0], - enables: [isModifierKeyHeld, cmdPointerCursor], -}) +export function modifierClickExtension(): Extension { + return [ + isModifierKeyHeld, + cmdPointerCursor, + EditorView.theme({ + '.cm-token-selection-clickable:hover': { + cursor: 'pointer', + }, + }), + ] +} export const modifierClickDescription = isMacPlatform() ? 'cmd+click' : 'ctrl+click' diff --git a/client/web/src/repo/blob/codemirror/token-selection/selections.ts b/client/web/src/repo/blob/codemirror/token-selection/selections.ts index 5356fb89d65c..b240093aa4fe 100644 --- a/client/web/src/repo/blob/codemirror/token-selection/selections.ts +++ b/client/web/src/repo/blob/codemirror/token-selection/selections.ts @@ -1,117 +1,24 @@ -import { Extension, Facet, Line, RangeSet, SelectionRange, StateField } from '@codemirror/state' -import { Decoration, EditorView, PluginValue, ViewPlugin } from '@codemirror/view' +import { Extension, SelectionRange, StateField } from '@codemirror/state' +import { EditorView, PluginValue, ViewPlugin } from '@codemirror/view' import * as H from 'history' import { Occurrence, Range } from '@sourcegraph/shared/src/codeintel/scip' import { parseQueryAndHash } from '@sourcegraph/shared/src/util/url' import { blobPropsFacet } from '..' -import { shouldScrollIntoView } from '../linenumbers' import { cmSelectionToRange, occurrenceAtPosition, rangeToCmSelection } from '../occurrence-utils' import { isSelectionInsideDocument } from '../utils' -import { definitionCache, goToDefinitionAtOccurrence } from './definition' -import { showDocumentHighlightsForOccurrence } from './document-highlights' -import { hoverAtOccurrence, hoverCache, setHoveredOccurrenceEffect } from './hover' - -export const tokenSelectionTheme = EditorView.theme({ - '.cm-token-selection-definition-ready': { - textDecoration: 'underline', - }, - '.cm-token-selection-clickable:hover': { - cursor: 'pointer', - }, -}) - -const ariaCurrent = Decoration.mark({ - attributes: { 'aria-current': 'true' }, -}) +import { setFocusedOccurrence } from './code-intel-tooltips' export const fallbackOccurrences = StateField.define>({ create: () => new Map(), update: value => value, }) -const selectedOccurrenceField = StateField.define({ - create: () => undefined, - update(value, transaction) { - const selection = transaction.selection ?? transaction.state.selection - const position = cmSelectionToRange(transaction.state, selection.main) - const occurrence = occurrenceAtPosition(transaction.state, position.start) - if (occurrence) { - return occurrence - } - return undefined - }, -}) -export const selectedOccurrence = Facet.define({ - combine: sources => sources[0], - enables: () => [ - fallbackOccurrences, - selectedOccurrenceField, - EditorView.decorations.compute([selectedOccurrenceField], state => { - const occ = state.field(selectedOccurrenceField) - if (occ) { - const range = rangeToCmSelection(state, occ.range) - if (range.from === range.to) { - return RangeSet.empty - } - return RangeSet.of([ariaCurrent.range(range.from, range.to)]) - } - return RangeSet.empty - }), - ], -}) - -const scrollLineIntoView = (view: EditorView, line: Line): boolean => { - if (shouldScrollIntoView(view, { line: line.number })) { - view.dispatch({ - effects: EditorView.scrollIntoView(line.from, { y: 'nearest' }), - }) - return true - } - return false -} - -export const scrollRangeIntoView = (view: EditorView, range: Range): void => { - const lineAbove = view.state.doc.line(Math.min(view.state.doc.lines, range.start.line + 3)) - if (scrollLineIntoView(view, lineAbove)) { - return - } - const lineBelow = view.state.doc.line(Math.max(1, range.end.line - 1)) - scrollLineIntoView(view, lineBelow) -} - -export const warmupOccurrence = (view: EditorView, occurrence: Occurrence): void => { - if (!view.state.field(hoverCache).has(occurrence)) { - hoverAtOccurrence(view, occurrence).then( - () => {}, - () => {} - ) - } - if (!view.state.field(definitionCache).has(occurrence)) { - goToDefinitionAtOccurrence(view, occurrence).then( - () => {}, - () => {} - ) - } -} - -export const selectOccurrence = (view: EditorView, occurrence: Occurrence): void => { - warmupOccurrence(view, occurrence) - showDocumentHighlightsForOccurrence(view, occurrence) - selectRange(view, occurrence.range) - view.dispatch({ effects: setHoveredOccurrenceEffect.of(occurrence) }) -} - -export const selectRange = (view: EditorView, range: Range): void => { - const selection = rangeToCmSelection(view.state, range) - view.dispatch({ selection }) - scrollRangeIntoView(view, range) -} // View plugin that listens to history location changes and updates editor // selection accordingly. -export const syncSelectionWithURL: Extension = ViewPlugin.fromClass( +export const syncOccurrenceWithURL: Extension = ViewPlugin.fromClass( class implements PluginValue { private onDestroy: H.UnregisterCallback constructor(public view: EditorView) { @@ -121,7 +28,12 @@ export const syncSelectionWithURL: Extension = ViewPlugin.fromClass( public onLocation(location: H.Location): void { const { selection } = selectionFromLocation(this.view, location) if (selection && isSelectionInsideDocument(selection, this.view.state.doc)) { - this.view.dispatch({ selection }) + const occurrence = occurrenceAtPosition( + this.view.state, + cmSelectionToRange(this.view.state, selection).start + ) + + this.view.dispatch({ effects: setFocusedOccurrence.of(occurrence ?? null) }) } } public destroy(): void { diff --git a/client/web/src/repo/blob/codemirror/tooltips/LoadingTooltip.ts b/client/web/src/repo/blob/codemirror/tooltips/LoadingTooltip.ts index 97feb7846ae7..1d95cafbdc8c 100644 --- a/client/web/src/repo/blob/codemirror/tooltips/LoadingTooltip.ts +++ b/client/web/src/repo/blob/codemirror/tooltips/LoadingTooltip.ts @@ -1,28 +1,14 @@ -import { EditorView, getTooltip, Tooltip } from '@codemirror/view' +import { Tooltip, TooltipView } from '@codemirror/view' -import { closeHover, showHover } from '../token-selection/hover' +export class LoadingTooltip implements Tooltip { + public readonly above = true -/** Helper to display a "Loading..." CodeMirror tooltip. */ -export class LoadingTooltip { - private tooltip?: Tooltip - constructor(private view: EditorView, pos: number | null) { - if (pos) { - this.tooltip = { - pos, - above: true, - create() { - const dom = document.createElement('div') - dom.classList.add('tmp-tooltip') - dom.textContent = 'Loading...' - return { dom } - }, - } - showHover(this.view, this.tooltip) - } - } - public stop(): void { - if (this.tooltip && getTooltip(this.view, this.tooltip)) { - closeHover(this.view) - } + constructor(public readonly pos: number) {} + + public create(): TooltipView { + const dom = document.createElement('div') + dom.classList.add('tmp-tooltip') + dom.textContent = 'Loading...' + return { dom } } } diff --git a/client/web/src/repo/blob/codemirror/tooltips/TemporaryTooltip.tsx b/client/web/src/repo/blob/codemirror/tooltips/TemporaryTooltip.tsx index 6f5b6ab718bc..2e58699a433b 100644 --- a/client/web/src/repo/blob/codemirror/tooltips/TemporaryTooltip.tsx +++ b/client/web/src/repo/blob/codemirror/tooltips/TemporaryTooltip.tsx @@ -1,8 +1,8 @@ -import { EditorView, getTooltip, Tooltip, TooltipView } from '@codemirror/view' +import { EditorView, Tooltip, TooltipView } from '@codemirror/view' import * as sourcegraph from '@sourcegraph/extension-api-types' -import { closeHover, showHover } from '../token-selection/hover' +import { getCodeIntelTooltipState, setFocusedOccurrenceTooltip } from '../token-selection/code-intel-tooltips' class TemporaryTooltip implements Tooltip { public readonly above = true @@ -19,7 +19,11 @@ class TemporaryTooltip implements Tooltip { } } -// Displays a simple tooltip that automatically hides after the provided timeout +/** + * Displays a simple tooltip that automatically hides after the provided timeout. + * As temporary tooltips are only shown for selected (focused) occurrences currently, + * we use {@link setFocusedOccurrenceTooltip} to update {@link codeIntelTooltipsState}. + */ export function showTemporaryTooltip( view: EditorView, message: string, @@ -32,10 +36,12 @@ export function showTemporaryTooltip( const line = view.state.doc.line(position.line + 1) const pos = line.from + position.character + 1 const tooltip = new TemporaryTooltip(message, pos, options?.arrow) - showHover(view, tooltip) + view.dispatch({ effects: setFocusedOccurrenceTooltip.of(tooltip) }) setTimeout(() => { - if (getTooltip(view, tooltip)) { - closeHover(view) + // close loading tooltip if any + const current = getCodeIntelTooltipState(view, 'focus') + if (current?.tooltip === tooltip) { + view.dispatch({ effects: setFocusedOccurrenceTooltip.of(null) }) } }, clearTimeout) } From f8368a230bb2724ce962cf19a8c251ac182266ed Mon Sep 17 00:00:00 2001 From: GitStart-SourceGraph <89894075+gitstart-sourcegraph@users.noreply.github.com> Date: Mon, 30 Jan 2023 14:51:24 +0800 Subject: [PATCH 241/678] [SG-22831] - Use `production` build for frontend integration tests (#46495) --- client/shared/src/testing/driver.ts | 35 +++++++++++-- .../dev/webpack/get-html-webpack-plugins.ts | 24 +++++---- client/web/src/integration/context.ts | 50 +++++++------------ client/web/webpack.config.js | 3 ++ .../background-information/sg/reference.md | 3 +- enterprise/dev/ci/internal/ci/operations.go | 1 + sg.config.yaml | 12 ++++- 7 files changed, 81 insertions(+), 47 deletions(-) diff --git a/client/shared/src/testing/driver.ts b/client/shared/src/testing/driver.ts index 6d39292f4e37..dcde7adfb81c 100644 --- a/client/shared/src/testing/driver.ts +++ b/client/shared/src/testing/driver.ts @@ -45,9 +45,38 @@ import { readEnvironmentBoolean, retry } from './utils' export const oncePageEvent = (page: Page, eventName: E): Promise => new Promise(resolve => page.once(eventName, resolve)) -export const percySnapshot = readEnvironmentBoolean({ variable: 'PERCY_ON', defaultValue: false }) - ? realPercySnapshot - : () => Promise.resolve() +export const extractStyles = (page: puppeteer.Page): Promise => + page.evaluate(() => + [...document.styleSheets].reduce( + (styleSheetRules, styleSheet) => + styleSheetRules.concat( + [...styleSheet.cssRules].reduce((rules, rule) => rules.concat(rule.cssText), '') + ), + '' + ) + ) + +interface CommonSnapshotOptions { + widths?: number[] + minHeight?: number + percyCSS?: string + enableJavaScript?: boolean + devicePixelRatio?: number + scope?: string +} + +export const percySnapshot = async ( + page: puppeteer.Page, + name: string, + options: CommonSnapshotOptions = {} +): Promise => { + if (!readEnvironmentBoolean({ variable: 'PERCY_ON', defaultValue: false })) { + return Promise.resolve() + } + + const pageStyles = await extractStyles(page) + return realPercySnapshot(page, name, { ...options, percyCSS: pageStyles.concat(options.percyCSS || '') }) +} export const BROWSER_EXTENSION_DEV_ID = 'bmfbcejdknlknpncfpeloejonjoledha' diff --git a/client/web/dev/webpack/get-html-webpack-plugins.ts b/client/web/dev/webpack/get-html-webpack-plugins.ts index a88e1fbb7e2e..45b7cb500363 100644 --- a/client/web/dev/webpack/get-html-webpack-plugins.ts +++ b/client/web/dev/webpack/get-html-webpack-plugins.ts @@ -7,6 +7,7 @@ import { WebpackPluginInstance } from 'webpack' import { STATIC_ASSETS_PATH } from '@sourcegraph/build-config' +import { SourcegraphContext } from '../../src/jscontext' import { createJsContext, ENVIRONMENT_CONFIG } from '../utils' const { SOURCEGRAPH_HTTPS_PORT, NODE_ENV } = ENVIRONMENT_CONFIG @@ -24,6 +25,8 @@ export interface WebpackManifest { 'opentelemetry.js'?: string /** If script files should be treated as JS modules. Required for esbuild bundle. */ isModule?: boolean + /** The node env value production | development*/ + environment?: 'development' | 'production' } /** @@ -33,14 +36,17 @@ export interface WebpackManifest { * Note: This page should be kept as close as possible to `app.html` to avoid any inconsistencies * between our development server and the actual production server. */ -export const getHTMLPage = ({ - 'app.js': appBundle, - 'app.css': cssBundle, - 'runtime.js': runtimeBundle, - 'react.js': reactBundle, - 'opentelemetry.js': oTelBundle, - isModule, -}: WebpackManifest): string => ` +export const getHTMLPage = ( + { + 'app.js': appBundle, + 'app.css': cssBundle, + 'runtime.js': runtimeBundle, + 'react.js': reactBundle, + 'opentelemetry.js': oTelBundle, + isModule, + }: WebpackManifest, + jsContext?: SourcegraphContext +): string => ` @@ -64,7 +70,7 @@ export const getHTMLPage = ({ // Required mock of the JS context object. window.context = ${JSON.stringify( - createJsContext({ sourcegraphBaseUrl: `http://localhost:${SOURCEGRAPH_HTTPS_PORT}` }) + jsContext ?? createJsContext({ sourcegraphBaseUrl: `http://localhost:${SOURCEGRAPH_HTTPS_PORT}` }) )} diff --git a/client/web/src/integration/context.ts b/client/web/src/integration/context.ts index e32d2ede0f07..b6bbbcf0c46e 100644 --- a/client/web/src/integration/context.ts +++ b/client/web/src/integration/context.ts @@ -1,8 +1,6 @@ import fs from 'fs' import path from 'path' -import html from 'tagged-template-noop' - import { SharedGraphQlOperations } from '@sourcegraph/shared/src/graphql-operations' import { SearchEvent } from '@sourcegraph/shared/src/search/stream' import { TemporarySettings } from '@sourcegraph/shared/src/settings/temporary/TemporarySettings' @@ -13,6 +11,7 @@ import { IntegrationTestOptions, } from '@sourcegraph/shared/src/testing/integration/context' +import { WebpackManifest, getHTMLPage } from '../../dev/webpack/get-html-webpack-plugins' import { WebGraphQlOperations } from '../graphql-operations' import { SourcegraphContext } from '../jscontext' @@ -47,17 +46,9 @@ export interface WebIntegrationTestContext const rootDirectory = path.resolve(__dirname, '..', '..', '..', '..') const manifestFile = path.resolve(rootDirectory, 'ui/assets/webpack.manifest.json') -const getAppBundle = (): string => { +const getManifestBundles = (): Partial => // eslint-disable-next-line no-sync - const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf-8')) as Record - return manifest['app.js'] -} - -const getRuntimeAppBundle = (): string => { - // eslint-disable-next-line no-sync - const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf-8')) as Record - return manifest['runtime.js'] -} + JSON.parse(fs.readFileSync(manifestFile, 'utf-8')) as Partial /** * Creates the integration test context for integration tests testing the web app. @@ -70,6 +61,7 @@ export const createWebIntegrationTestContext = async ({ customContext = {}, }: IntegrationTestOptions): Promise => { const config = getConfig('disableAppAssetsMocking') + const { environment, ...bundles } = getManifestBundles() const sharedTestContext = await createSharedIntegrationTestContext< WebGraphQlOperations & SharedGraphQlOperations, @@ -82,32 +74,26 @@ export const createWebIntegrationTestContext = async ({ const tempSettings = new TemporarySettingsContext() sharedTestContext.overrideGraphQL(tempSettings.getGraphQLOverrides()) - if (!config.disableAppAssetsMocking) { - // On CI, we don't use `react-fast-refresh`, so we don't need the runtime bundle. - // This branching will be redundant after switching to production bundles for integration tests: - // https://github.com/sourcegraph/sourcegraph/issues/22831 - const runtimeChunkScriptTag = isHotReloadEnabled ? `` : '' + const prodChunks = { + 'app.js': bundles['app.js'] || '', + 'app.css': bundles['app.css'], + 'react.js': bundles['react.js'], + 'opentelemetry.js': bundles['opentelemetry.js'], + } + const devChunks = { + 'app.js': bundles['app.js'] || '', + 'runtime.js': isHotReloadEnabled ? bundles['runtime.js'] : undefined, + } + + const appChunks = environment === 'production' ? prodChunks : devChunks + if (!config.disableAppAssetsMocking) { // Serve all requests for index.html (everything that does not match the handlers above) the same index.html sharedTestContext.server .get(new URL('/*path', driver.sourcegraphBaseUrl).href) .filter(request => !request.pathname.startsWith('/-/')) .intercept((request, response) => { - response.type('text/html').send(html` - - - Sourcegraph Test - - -
- - ${runtimeChunkScriptTag} - - - - `) + response.type('text/html').send(getHTMLPage(appChunks, { ...jsContext, ...customContext })) }) } diff --git a/client/web/webpack.config.js b/client/web/webpack.config.js index e2574c6513c6..f0a00d066d7b 100644 --- a/client/web/webpack.config.js +++ b/client/web/webpack.config.js @@ -168,6 +168,9 @@ const config = { new WebpackManifestPlugin({ writeToFileEmit: true, fileName: 'webpack.manifest.json', + seed: { + environment: NODE_ENV, + }, // Only output files that are required to run the application. filter: ({ isInitial, name }) => isInitial || Object.values(initialChunkNames).some(initialChunkName => name?.includes(initialChunkName)), diff --git a/doc/dev/background-information/sg/reference.md b/doc/dev/background-information/sg/reference.md index 200a7256fc6e..1efe59375946 100644 --- a/doc/dev/background-information/sg/reference.md +++ b/doc/dev/background-information/sg/reference.md @@ -129,7 +129,8 @@ Available commands in `sg.config.yaml`: * storybook * symbols * syntax-highlighter -* web-integration-build: Build web application for integration tests +* web-integration-build-prod: Build production web application for integration tests +* web-integration-build: Build development web application for integration tests * web-standalone-http-prod: Standalone web frontend (production) with API proxy to a configurable URL * web-standalone-http: Standalone web frontend (dev) with API proxy to a configurable URL * web: Enterprise version of the web app diff --git a/enterprise/dev/ci/internal/ci/operations.go b/enterprise/dev/ci/internal/ci/operations.go index 7e9e56a4e092..2c925e076cd8 100644 --- a/enterprise/dev/ci/internal/ci/operations.go +++ b/enterprise/dev/ci/internal/ci/operations.go @@ -365,6 +365,7 @@ func clientIntegrationTests(pipeline *bk.Pipeline) { withPnpmCache(), bk.Key(prepStepKey), bk.Env("ENTERPRISE", "1"), + bk.Env("NODE_ENV", "production"), bk.Env("INTEGRATION_TESTS", "true"), bk.Env("COVERAGE_INSTRUMENT", "true"), bk.Cmd("dev/ci/pnpm-build.sh client/web"), diff --git a/sg.config.yaml b/sg.config.yaml index e24334dc0389..385930769f88 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -425,12 +425,20 @@ commands: SOURCEGRAPH_API_URL: https://k8s.sgdev.org web-integration-build: - description: Build web application for integration tests + description: Build development web application for integration tests cmd: pnpm --filter @sourcegraph/web run build env: ENTERPRISE: 1 INTEGRATION_TESTS: true + web-integration-build-prod: + description: Build production web application for integration tests + cmd: pnpm --filter @sourcegraph/web run build + env: + ENTERPRISE: 1 + INTEGRATION_TESTS: true + NODE_ENV: production + docsite: description: Docsite instance serving the docs cmd: .bin/docsite_${DOCSITE_VERSION} -config doc/docsite.json serve -http=localhost:5080 @@ -1218,7 +1226,7 @@ tests: web-integration: preamble: | A web application should be built for these tests to work, most - commonly with: `sg run web-integration-build` + commonly with: `sg run web-integration-build` or `sg run web-integration-build-prod` for production build. See more details: https://docs.sourcegraph.com/dev/how-to/testing#running-integration-tests From c671fcc43c6cf941477071b24f340643c96d69cb Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Mon, 30 Jan 2023 09:15:16 +0200 Subject: [PATCH 242/678] redispool: implement in memory KeyValue via minimal interface (#46927) redispool: implement in memory KeyValue via minimal interface This implementation is targetted at making it very simple to implement a redis like store by only implementing a key value store which stores a byte slice for a key. Right now it is unused, but has extensive test coverage as well. It will be used in the Sourcegraph App in the next commit. Test Plan: go test --- internal/redispool/NOTICE | 177 +++++++++++ internal/redispool/keyvalue_test.go | 82 ++++- internal/redispool/mem.go | 28 ++ internal/redispool/naive.go | 455 ++++++++++++++++++++++++++++ internal/redispool/naive_test.go | 11 + internal/redispool/redis.go | 129 ++++++++ internal/redispool/redis_conn.go | 217 +++++++++++++ 7 files changed, 1093 insertions(+), 6 deletions(-) create mode 100644 internal/redispool/NOTICE create mode 100644 internal/redispool/mem.go create mode 100644 internal/redispool/naive.go create mode 100644 internal/redispool/naive_test.go create mode 100644 internal/redispool/redis.go create mode 100644 internal/redispool/redis_conn.go diff --git a/internal/redispool/NOTICE b/internal/redispool/NOTICE new file mode 100644 index 000000000000..16045d3906fc --- /dev/null +++ b/internal/redispool/NOTICE @@ -0,0 +1,177 @@ +Code in "redis_conn.go" extended from go package +"github.com/gomodule/redigo/redis" + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/internal/redispool/keyvalue_test.go b/internal/redispool/keyvalue_test.go index 9c754c79e103..247adaedfe85 100644 --- a/internal/redispool/keyvalue_test.go +++ b/internal/redispool/keyvalue_test.go @@ -17,6 +17,8 @@ func TestRedisKeyValue(t *testing.T) { } func testKeyValue(t *testing.T, kv redispool.KeyValue) { + errWrongType := errors.New("WRONGTYPE") + // "strings" is the name of the classic group of commands in redis (get, set, ttl, etc). We call it classic since that is less confusing. t.Run("classic", func(t *testing.T) { require := require{TB: t} @@ -47,12 +49,6 @@ func testKeyValue(t *testing.T, kv redispool.KeyValue) { require.Works(kv.Set("funky", []byte{0, 10, 100, 255})) require.Equal(kv.Get("funky"), []byte{0, 10, 100, 255}) - // Ensure we fail hashes when used against non hashes. - require.Equal(kv.HGet("simple", "field"), errors.New("WRONGTYPE")) - if err := kv.HSet("simple", "field", "value"); !strings.Contains(err.Error(), "WRONGTYPE") { - t.Fatalf("expected wrongtype error, got %v", err) - } - // Incr require.Works(kv.Set("incr-set", 5)) require.Works(kv.Incr("incr-set")) @@ -112,6 +108,9 @@ func testKeyValue(t *testing.T, kv redispool.KeyValue) { require.AllEqual(kv.LRange("list", -5, -1), bytes("0", "1", "2", "3", "4")) require.AllEqual(kv.LRange("list", 0, 4), bytes("0", "1", "2", "3", "4")) + // If stop < start we return nothing + require.AllEqual(kv.LRange("list", -1, 0), bytes()) + // Subsets require.AllEqual(kv.LRange("list", 1, 3), bytes("1", "2", "3")) require.AllEqual(kv.LRange("list", 1, -2), bytes("1", "2", "3")) @@ -132,11 +131,35 @@ func testKeyValue(t *testing.T, kv redispool.KeyValue) { require.AllEqual(kv.LRange("list", 0, 4), bytes("1", "2", "3")) require.ListLen(kv, "list", 3) + // Trim all + require.Works(kv.LTrim("list", -1, -2)) + require.AllEqual(kv.LRange("list", 0, 4), bytes()) + require.ListLen(kv, "list", 0) + require.Works(kv.LPush("funky2D", []byte{100, 255})) require.Works(kv.LPush("funky2D", []byte{0, 10})) require.AllEqual(kv.LRange("funky2D", 0, -1), [][]byte{{0, 10}, {100, 255}}) }) + t.Run("empty", func(t *testing.T) { + require := require{TB: t} + + // Strings group + require.Works(kv.Set("empty-number", 0)) + require.Works(kv.Set("empty-string", "")) + require.Works(kv.Set("empty-bytes", []byte{})) + require.Equal(kv.Get("empty-number"), 0) + require.Equal(kv.Get("empty-string"), "") + require.Equal(kv.Get("empty-bytes"), "") + + // List group. Once empty we should be able to do a Get without a + // wrongtype error. + require.Works(kv.LPush("empty-list", "here today gone tomorrow")) + require.Equal(kv.Get("empty-list"), errWrongType) + require.Works(kv.LTrim("empty-list", -1, -2)) + require.Equal(kv.Get("empty-list"), nil) + }) + t.Run("expire", func(t *testing.T) { require := require{TB: t} @@ -145,6 +168,9 @@ func testKeyValue(t *testing.T, kv redispool.KeyValue) { t.Skip() } + // TODO test expire works on different data types after mutation. + // Check that expire works after incr. + // SetEx, Expire and TTL require.Works(kv.SetEx("expires-setex", 60, "1")) require.Works(kv.Set("expires-set", "1")) @@ -168,6 +194,50 @@ func testKeyValue(t *testing.T, kv redispool.KeyValue) { require.TTL(kv, "expires-setex", -2) require.TTL(kv, "expires-set", -2) }) + + t.Run("wrongtype", func(t *testing.T) { + require := require{TB: t} + requireWrongType := func(err error) { + t.Helper() + if err == nil || !strings.Contains(err.Error(), "WRONGTYPE") { + t.Fatalf("expected wrongtype error, got %v", err) + } + } + + require.Works(kv.Set("wrongtype-string", "1")) + require.Works(kv.HSet("wrongtype-hash", "1", "1")) + require.Works(kv.LPush("wrongtype-list", "1")) + + for _, k := range []string{"wrongtype-string", "wrongtype-hash", "wrongtype-list"} { + // Ensure we fail Get when used against non string group + if k != "wrongtype-string" { + require.Equal(kv.Get(k), errWrongType) + require.Equal(kv.GetSet(k, "2"), errWrongType) + require.Equal(kv.Get(k), errWrongType) // ensure GetSet didn't set + requireWrongType(kv.Incr(k)) + } + + // Ensure we fail hashes when used against non hashes. + if k != "wrongtype-hash" { + require.Equal(kv.HGet(k, "field"), errWrongType) + require.Equal(redispool.Value(kv.HGetAll(k)), errWrongType) + requireWrongType(kv.HSet(k, "field", "value")) + } + + // Ensure we fail lists when used against non lists. + if k != "wrongtype-list" { + _, err := kv.LLen(k) + requireWrongType(err) + requireWrongType(kv.LPush(k, "1")) + requireWrongType(kv.LTrim(k, 1, 2)) + require.Equal(redispool.Value(kv.LRange(k, 1, 2)), errWrongType) + } + + // Ensure we can always override values with set + require.Works(kv.Set(k, "2")) + require.Equal(kv.Get(k), "2") + } + }) } // Mostly copy-pasta from rache. Will clean up later as the relationship diff --git a/internal/redispool/mem.go b/internal/redispool/mem.go new file mode 100644 index 000000000000..388c3a9afd01 --- /dev/null +++ b/internal/redispool/mem.go @@ -0,0 +1,28 @@ +package redispool + +import ( + "context" + "sync" +) + +// MemoryKeyValue returns an in memory KeyValue. +func MemoryKeyValue() KeyValue { + var mu sync.Mutex + m := map[string]NaiveValue{} + store := func(_ context.Context, key string, f NaiveUpdater) error { + mu.Lock() + defer mu.Unlock() + before, found := m[key] + after, remove := f(before, found) + if remove { + if found { + delete(m, key) + } + } else if before != after { + m[key] = after + } + return nil + } + + return FromNaiveKeyValueStore(store) +} diff --git a/internal/redispool/naive.go b/internal/redispool/naive.go new file mode 100644 index 000000000000..ddc0fdcc41aa --- /dev/null +++ b/internal/redispool/naive.go @@ -0,0 +1,455 @@ +package redispool + +import ( + "context" + "time" + + "github.com/gomodule/redigo/redis" +) + +// NaiveValue is the value we send to and from a NaiveKeyValueStore. This +// represents the marshalled value the NaiveKeyValueStore operates on. See the +// unexported redisValue type for more details. However, NaiveKeyValueStore +// should treat this value as opaque. +// +// Note: strings are used to ensure we pass copies around and avoid mutating +// values. They should not be treated as utf8 text. +type NaiveValue string + +// NaiveUpdater operates on the value for a key in a NaiveKeyValueStore. +// before is the before value in the store, found is if the key exists in the +// store. after is the new value for it that needs to be stored, or remove is +// true if the key should be removed. +// +// Note: a store should do this update atomically/under concurrency control. +type NaiveUpdater func(before NaiveValue, found bool) (after NaiveValue, remove bool) + +// NaiveKeyValueStore is a function on a store which runs f for key. +// +// This minimal function allows us to implement the full functionality of +// KeyValue via FromNaiveKeyValueStore. This does mean for any read on key we +// have to read the full value, and any mutation requires rewriting the full +// value. This is usually fine, but may be an issue when backed by a large +// Hash or List. As such this function is designed with the functionality of +// Sourcegraph App in mind (single process, low traffic). +type NaiveKeyValueStore func(ctx context.Context, key string, f NaiveUpdater) error + +// FromNaiveKeyValueStore returns a KeyValue based on the store function. +func FromNaiveKeyValueStore(store NaiveKeyValueStore) KeyValue { + return &naiveKeyValue{ + store: store, + ctx: context.Background(), + } +} + +// naiveKeyValue wraps a store to provide the KeyValue interface. Nearly all +// operations go via maybeUpdateGroup method, sink your teeth into that first +// to fully understand how to expand the set of methods provided. +type naiveKeyValue struct { + store NaiveKeyValueStore + ctx context.Context +} + +func (kv *naiveKeyValue) Get(key string) Value { + return kv.maybeUpdateGroup(redisGroupString, key, func(v redisValue, found bool) (redisValue, updaterOp, error) { + return v, readOnly, nil + }) +} + +func (kv *naiveKeyValue) GetSet(key string, value any) Value { + var oldValue Value + v := kv.maybeUpdateGroup(redisGroupString, key, func(before redisValue, found bool) (redisValue, updaterOp, error) { + if found { + oldValue.reply = before.Reply + } else { + oldValue.err = redis.ErrNil + } + + return redisValue{ + Group: redisGroupString, + Reply: value, + }, write, nil + }) + if v.err != nil { + return v + } + return oldValue +} + +func (kv *naiveKeyValue) Set(key string, value any) error { + return kv.maybeUpdate(key, func(_ redisValue, _ bool) (redisValue, updaterOp, error) { + return redisValue{ + Group: redisGroupString, + Reply: value, + }, write, nil + }).err +} + +func (kv *naiveKeyValue) SetEx(key string, ttlSeconds int, value any) error { + return kv.maybeUpdate(key, func(_ redisValue, _ bool) (redisValue, updaterOp, error) { + return redisValue{ + Group: redisGroupString, + Reply: value, + DeadlineUnix: time.Now().UTC().Unix() + int64(ttlSeconds), + }, write, nil + }).err +} + +func (kv *naiveKeyValue) Incr(key string) error { + return kv.maybeUpdateGroup(redisGroupString, key, func(value redisValue, found bool) (redisValue, updaterOp, error) { + if !found { + return redisValue{ + Group: redisGroupString, + Reply: 1, + }, write, nil + } + + num, err := redis.Int(value.Reply, nil) + if err != nil { + return value, readOnly, err + } + + value.Reply = num + 1 + return value, write, nil + }).err +} + +func (kv *naiveKeyValue) Del(key string) error { + return kv.store(kv.ctx, key, func(_ NaiveValue, _ bool) (NaiveValue, bool) { + return "", true + }) +} + +func (kv *naiveKeyValue) TTL(key string) (int, error) { + const ttlUnset = -1 + const ttlDoesNotExist = -2 + var ttl int + err := kv.maybeUpdate(key, func(value redisValue, found bool) (redisValue, updaterOp, error) { + if !found { + ttl = ttlDoesNotExist + } else if value.DeadlineUnix == 0 { + ttl = ttlUnset + } else { + ttl = int(value.DeadlineUnix - time.Now().UTC().Unix()) + // we may have expired since doStore checked + if ttl <= 0 { + ttl = ttlDoesNotExist + } + } + + return value, readOnly, nil + }).err + + if err == redis.ErrNil { + // Already handled above, but just in case lets be explicit + ttl = ttlDoesNotExist + err = nil + } + + return ttl, err +} + +func (kv *naiveKeyValue) Expire(key string, ttlSeconds int) error { + err := kv.maybeUpdate(key, func(value redisValue, found bool) (redisValue, updaterOp, error) { + if !found { + return value, readOnly, nil + } + + value.DeadlineUnix = time.Now().UTC().Unix() + int64(ttlSeconds) + return value, write, nil + }).err + + // expire does not error if the key does not exist + if err == redis.ErrNil { + err = nil + } + + return err +} + +func (kv *naiveKeyValue) HGet(key, field string) Value { + var reply any + err := kv.maybeUpdateValues(redisGroupHash, key, func(li []any) ([]any, updaterOp, error) { + idx, ok, err := hsetValueIndex(li, field) + if err != nil { + return li, readOnly, err + } + if !ok { + return li, readOnly, redis.ErrNil + } + + reply = li[idx] + return li, readOnly, nil + }).err + return Value{reply: reply, err: err} +} + +func (kv *naiveKeyValue) HGetAll(key string) Values { + return Values(kv.maybeUpdateGroup(redisGroupHash, key, func(value redisValue, found bool) (redisValue, updaterOp, error) { + return value, readOnly, nil + })) +} + +func (kv *naiveKeyValue) HSet(key, field string, fieldValue any) error { + return kv.maybeUpdateValues(redisGroupHash, key, func(li []any) ([]any, updaterOp, error) { + idx, ok, err := hsetValueIndex(li, field) + if err != nil { + return li, readOnly, err + } + if ok { + li[idx] = fieldValue + } else { + li = append(li, field, fieldValue) + } + + return li, write, nil + }).err +} + +func hsetValueIndex(li []any, field string) (int, bool, error) { + for i := 1; i < len(li); i += 2 { + if kk, err := redis.String(li[i-1], nil); err != nil { + return -1, false, err + } else if kk == field { + return i, true, nil + } + } + return -1, false, nil +} + +func (kv *naiveKeyValue) LPush(key string, value any) error { + return kv.maybeUpdateValues(redisGroupList, key, func(li []any) ([]any, updaterOp, error) { + return append([]any{value}, li...), write, nil + }).err +} + +func (kv *naiveKeyValue) LTrim(key string, start, stop int) error { + return kv.maybeUpdateValues(redisGroupList, key, func(li []any) ([]any, updaterOp, error) { + beforeLen := len(li) + li = lrange(li, start, stop) + + op := readOnly + if len(li) != beforeLen { + op = write + } + + return li, op, nil + }).err +} + +func (kv *naiveKeyValue) LLen(key string) (int, error) { + var innerLi []any + err := kv.maybeUpdateValues(redisGroupList, key, func(li []any) ([]any, updaterOp, error) { + innerLi = li + return li, readOnly, nil + }).err + return len(innerLi), err +} + +func (kv *naiveKeyValue) LRange(key string, start, stop int) Values { + var innerLi []any + err := kv.maybeUpdateValues(redisGroupList, key, func(li []any) ([]any, updaterOp, error) { + innerLi = li + return li, readOnly, nil + }).err + if err != nil { + return Values{err: err} + } + return Values{reply: lrange(innerLi, start, stop)} +} + +func lrange(li []any, start, stop int) []any { + low, high := rangeOffsetsToHighLow(start, stop, len(li)) + if high <= low { + return []any(nil) + } + return li[low:high] +} + +func rangeOffsetsToHighLow(start, stop, size int) (low, high int) { + if size <= 0 { + return 0, 0 + } + + start = clampRangeOffset(0, size, start) + stop = clampRangeOffset(-1, size, stop) + + // Adjust inclusive ending into exclusive for go + low = start + high = stop + 1 + + return low, high +} + +func clampRangeOffset(low, high, offset int) int { + // negative offset means distance from high + if offset < 0 { + offset = high + offset + } + if offset < low { + return low + } + if offset >= high { + return high - 1 + } + return offset +} + +func (kv *naiveKeyValue) WithContext(ctx context.Context) KeyValue { + return &naiveKeyValue{ + store: kv.store, + ctx: ctx, + } +} + +func (kv *naiveKeyValue) Pool() (pool *redis.Pool, ok bool) { + return nil, false +} + +type updaterOp bool + +var ( + write updaterOp = true + readOnly updaterOp = false +) + +// storeUpdater operates on the redisValue for a key and returns its new value +// or error. See doStore for more information. +type storeUpdater func(before redisValue, found bool) (after redisValue, op updaterOp, err error) + +// maybeUpdate is a helper for NaiveKeyValueStore and NaiveUpdater. It +// provides consistent behaviour for KeyValue as well as reducing the work +// required for each KeyValue method. It does the following: +// +// - Marshal NaiveUpdater values to and from redisValue +// - Handle expiration so updater does not need to. +// - If a value becomes nil we can delete the key. (redis behaviour) +// - Handle updaters that only want to read (readOnly updaterOp, error) +func (kv *naiveKeyValue) maybeUpdate(key string, updater storeUpdater) Value { + var returnValue Value + storeErr := kv.store(kv.ctx, key, func(beforeRaw NaiveValue, found bool) (NaiveValue, bool) { + var before redisValue + defaultDelete := false + if found { + // We found the value so we can unmarshal it. + if err := before.Unmarshal([]byte(beforeRaw)); err != nil { + // Bad data at key, delete it and return an error + returnValue.err = err + return "", true + } + + // The store won't expire for us, we do it here by checking at + // read time if the value has expired. If it has pretend we didn't + // find it and mark that we need to delete the value if we don't + // get a new one. + if before.DeadlineUnix != 0 && time.Now().UTC().Unix() >= before.DeadlineUnix { + found = false + // We need to inform the store to delete the value, unless we + // have a new value to takes its place. + defaultDelete = true + } + } + + // Call out to the provided updater to get back what we need to do to + // the value. + after, op, err := updater(before, found) + if err != nil { + // If updater fails, we tell store to keep the before value (or + // delete if expired). + returnValue.err = err + return beforeRaw, defaultDelete + } + + // We don't need to update the value, so set the appropriate response + // values based on what we found at get time. + if op == readOnly { + if found { + returnValue.reply = before.Reply + } else { + returnValue.err = redis.ErrNil + } + return beforeRaw, defaultDelete + } + + // Redis will automatically delete keys if some value types become + // empty. + if isRedisDeleteValue(after) { + returnValue.reply = after.Reply + return "", true + } + + // Lets convert our redisValue into bytes so we can store the new + // value. + afterRaw, err := after.Marshal() + if err != nil { + returnValue.err = err + return beforeRaw, defaultDelete + } + returnValue.reply = after.Reply + return NaiveValue(afterRaw), false + }) + if storeErr != nil { + return Value{err: storeErr} + } + return returnValue +} + +// maybeUpdateGroup is a wrapper of maybeUpdate which additionally will return +// an error if the before is not of the type group. +func (kv *naiveKeyValue) maybeUpdateGroup(group redisGroup, key string, updater storeUpdater) Value { + return kv.maybeUpdate(key, func(value redisValue, found bool) (redisValue, updaterOp, error) { + if found && value.Group != group { + return value, readOnly, redis.Error("WRONGTYPE Operation against a key holding the wrong kind of value") + } + return updater(value, found) + }) +} + +// valuesUpdater takes in the befores. If afters is different op must +// be write so maybeUpdateValues knows to update. +type valuesUpdater func(befores []any) (afters []any, op updaterOp, err error) + +// maybeUpdateValues is a specialization of maybeUpdate for all values operations +// on key via updater. +func (kv *naiveKeyValue) maybeUpdateValues(group redisGroup, key string, updater valuesUpdater) Values { + v := kv.maybeUpdateGroup(group, key, func(value redisValue, found bool) (redisValue, updaterOp, error) { + var li []any + if found { + var err error + li, err = value.Values() + if err != nil { + return value, readOnly, err + } + } else { + value = redisValue{ + Group: group, + } + } + + li, op, err := updater(li) + value.Reply = li + return value, op, err + }) + + // missing is treated as empty for values + if v.err == redis.ErrNil { + return Values{reply: []any(nil)} + } + + return Values(v) +} + +// isRedisDeleteValue returns true if the redisValue is not allowed to be +// stored. An example of this is when a list becomes empty redis will delete +// the key. +func isRedisDeleteValue(v redisValue) bool { + switch v.Group { + case redisGroupString: + return false + case redisGroupHash, redisGroupList: + vs, _ := v.Reply.([]any) + return len(vs) == 0 + default: + return false + } +} diff --git a/internal/redispool/naive_test.go b/internal/redispool/naive_test.go new file mode 100644 index 000000000000..d035129d6015 --- /dev/null +++ b/internal/redispool/naive_test.go @@ -0,0 +1,11 @@ +package redispool_test + +import ( + "testing" + + "github.com/sourcegraph/sourcegraph/internal/redispool" +) + +func TestInMemoryKeyValue(t *testing.T) { + testKeyValue(t, redispool.MemoryKeyValue()) +} diff --git a/internal/redispool/redis.go b/internal/redispool/redis.go new file mode 100644 index 000000000000..716fe31c1480 --- /dev/null +++ b/internal/redispool/redis.go @@ -0,0 +1,129 @@ +package redispool + +import ( + "bytes" + + "github.com/gomodule/redigo/redis" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// redisGroup is which type of data we have. We use the term group since that +// is what redis uses in its documentation to segregate the different types of +// commands you can run (string, list, hash). +type redisGroup byte + +const ( + // redisGroupString doesn't mean the data is a string. This is the + // original group of command (get, set). + redisGroupString redisGroup = 's' + redisGroupList redisGroup = 'l' + redisGroupHash redisGroup = 'h' +) + +// redisValue represents what we marshal into a NaiveKeyValueStore. +type redisValue struct { + // Group is stored so we can enforce WRONGTYPE errors. + Group redisGroup + // Reply is the actual value the user wants to set. It is called reply to + // match what the redis package expects. + Reply any + // DeadlineUnix is the unix timestamp of when to expire this value, or 0 + // if no expiry. This is a convenient way to store deadlines since redis + // only has 1s resolution on TTLs. + DeadlineUnix int64 +} + +func (v *redisValue) Marshal() ([]byte, error) { + var c conn + + // Header, Group, DeadlineUnix + // + // Note: this writes a small version header which is just the character ! + // and g. This is enough so we can change the data in the future. + // + // We are also gaurenteed to not fail these writes, so we ignore the error + // for convenience. + _ = c.bw.WriteByte('!') + _ = c.bw.WriteByte(byte(v.Group)) + _ = c.writeArg(v.DeadlineUnix) + + // Reply + switch v.Group { + case redisGroupString: + err := c.writeArg(v.Reply) + if err != nil { + return nil, err + } + case redisGroupList, redisGroupHash: + vs, err := v.Values() + if err != nil { + return nil, err + } + _ = c.writeLen('*', len(vs)) + for _, el := range vs { + err := c.writeArg(el) + if err != nil { + return nil, err + } + } + default: + return nil, errors.Errorf("redis naive internal error: unkown redis group %c", byte(v.Group)) + } + + return c.bw.Bytes(), nil +} + +func (v *redisValue) Unmarshal(b []byte) error { + c := conn{bw: *bytes.NewBuffer(b)} + + // Header, Group + var header [2]byte + n, err := c.bw.Read(header[:]) + if err != nil || n != 2 { + return errors.New("redis naive internal error: failed to parse value header") + } + if header[0] != '!' { + return errors.Errorf("redis naive internal error: expected first byte of value header to be '!' got %q", header[0]) + } + v.Group = redisGroup(header[1]) + + // DeadlineUnix + v.DeadlineUnix, err = redis.Int64(c.readReply()) + if err != nil { + return errors.Wrap(err, "redis naive internal error: failed to parse value deadline") + } + + // Reply + v.Reply, err = c.readReply() + if err != nil { + return err + } + + // Validation + switch v.Group { + case redisGroupString: + // noop + case redisGroupList, redisGroupHash: + _, err := v.Values() + if err != nil { + return err + } + default: + return errors.Errorf("redis naive internal error: unkown redis group %c", byte(v.Group)) + } + + return nil +} + +// Values will convert v.Reply into values as well as some validation based on +// the v.Group. +func (v *redisValue) Values() ([]any, error) { + li, ok := v.Reply.([]any) + if !ok { + return nil, errors.Errorf("redis naive internal error: non list returned for redis group %c", byte(v.Group)) + } + if v.Group == redisGroupHash && len(li)%2 != 0 { + return nil, errors.New("redis naive internal error: hash list is not divisible by 2") + } + return li, nil +} diff --git a/internal/redispool/redis_conn.go b/internal/redispool/redis_conn.go new file mode 100644 index 000000000000..79e3a8047516 --- /dev/null +++ b/internal/redispool/redis_conn.go @@ -0,0 +1,217 @@ +package redispool + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strconv" + + "github.com/gomodule/redigo/redis" +) + +// NOTICE: The code below is adapted from "github.com/gomodule/redigo/redis" +// so that we use the same marshalling that redigo expects for replies. See +// file NOTICE for more information. + +type conn struct { + bw bytes.Buffer + + // Scratch space for formatting argument length. + // '*' or '$', length, "\r\n" + lenScratch [32]byte + + // Scratch space for formatting integers and floats. + numScratch [40]byte +} + +func (c *conn) writeArg(arg interface{}) (err error) { + switch arg := arg.(type) { + case string: + return c.writeString(arg) + case []byte: + return c.writeBytes(arg) + case int: + return c.writeInt64(int64(arg)) + case int64: + return c.writeInt64(arg) + case float64: + return c.writeFloat64(arg) + case bool: + if arg { + return c.writeString("1") + } else { + return c.writeString("0") + } + case nil: + return c.writeString("") + default: + // This default clause is intended to handle builtin numeric types. + // The function should return an error for other types, but this is not + // done for compatibility with previous versions of the package. + var buf bytes.Buffer + fmt.Fprint(&buf, arg) + return c.writeBytes(buf.Bytes()) + } +} + +func (c *conn) writeLen(prefix byte, n int) error { + c.lenScratch[len(c.lenScratch)-1] = '\n' + c.lenScratch[len(c.lenScratch)-2] = '\r' + i := len(c.lenScratch) - 3 + for { + c.lenScratch[i] = byte('0' + n%10) + i -= 1 + n = n / 10 + if n == 0 { + break + } + } + c.lenScratch[i] = prefix + _, err := c.bw.Write(c.lenScratch[i:]) + return err +} + +func (c *conn) writeString(s string) error { + c.writeLen('$', len(s)) + c.bw.WriteString(s) + _, err := c.bw.WriteString("\r\n") + return err +} + +func (c *conn) writeBytes(p []byte) error { + c.writeLen('$', len(p)) + c.bw.Write(p) + _, err := c.bw.WriteString("\r\n") + return err +} + +func (c *conn) writeInt64(n int64) error { + return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10)) +} + +func (c *conn) writeFloat64(n float64) error { + return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64)) +} + +func (c *conn) readLine() ([]byte, error) { + p, err := c.bw.ReadBytes('\n') + if err == bufio.ErrBufferFull { + return nil, protocolError("long response line") + } + if err != nil { + return nil, err + } + i := len(p) - 2 + if i < 0 || p[i] != '\r' { + return nil, protocolError("bad response line terminator") + } + return p[:i], nil +} + +func (c *conn) readReply() (interface{}, error) { + line, err := c.readLine() + if err != nil { + return nil, err + } + if len(line) == 0 { + return nil, protocolError("short response line") + } + switch line[0] { + case '+': + return string(line[1:]), nil + case '-': + return redis.Error(string(line[1:])), nil + case ':': + return parseInt(line[1:]) + case '$': + n, err := parseLen(line[1:]) + if n < 0 || err != nil { + return nil, err + } + p := make([]byte, n) + _, err = io.ReadFull(&c.bw, p) + if err != nil { + return nil, err + } + if line, err := c.readLine(); err != nil { + return nil, err + } else if len(line) != 0 { + return nil, protocolError("bad bulk string format") + } + return p, nil + case '*': + n, err := parseLen(line[1:]) + if n < 0 || err != nil { + return nil, err + } + r := make([]interface{}, n) + for i := range r { + r[i], err = c.readReply() + if err != nil { + return nil, err + } + } + return r, nil + } + return nil, protocolError("unexpected response line") +} + +// parseLen parses bulk string and array lengths. +func parseLen(p []byte) (int, error) { + if len(p) == 0 { + return -1, protocolError("malformed length") + } + + if p[0] == '-' && len(p) == 2 && p[1] == '1' { + // handle $-1 and $-1 null replies. + return -1, nil + } + + var n int + for _, b := range p { + n *= 10 + if b < '0' || b > '9' { + return -1, protocolError("illegal bytes in length") + } + n += int(b - '0') + } + + return n, nil +} + +// parseInt parses an integer reply. +func parseInt(p []byte) (interface{}, error) { + if len(p) == 0 { + return 0, protocolError("malformed integer") + } + + var negate bool + if p[0] == '-' { + negate = true + p = p[1:] + if len(p) == 0 { + return 0, protocolError("malformed integer") + } + } + + var n int64 + for _, b := range p { + n *= 10 + if b < '0' || b > '9' { + return 0, protocolError("illegal bytes in length") + } + n += int64(b - '0') + } + + if negate { + n = -n + } + return n, nil +} + +type protocolError string + +func (pe protocolError) Error() string { + return fmt.Sprintf("redigo: %s (possible server error or unsupported concurrent read by application)", string(pe)) +} From 7e7805dd9272f73fc2019bd4204a5c9c203c469f Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Mon, 30 Jan 2023 16:46:22 +0800 Subject: [PATCH 243/678] docs: update integration tests documentation (#47080) --- doc/dev/how-to/testing.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/dev/how-to/testing.md b/doc/dev/how-to/testing.md index 50c16e7e289d..9bd44d5ee679 100644 --- a/doc/dev/how-to/testing.md +++ b/doc/dev/how-to/testing.md @@ -205,6 +205,7 @@ To run integration tests for the web app: 1. Run `INTEGRATION_TESTS=true ENTERPRISE=1 pnpm watch-web` in the repository root in a separate terminal to watch files and build a JavaScript bundle. You can also launch it as the VS Code task "Watch web app". - Alternatively, `sg run web-integration-build` will only build a bundle once. + - Alternatively, `sg run web-integration-build-prod` will only build a bundle once and will also mirror our CI setup where we use the production bundle of the web application for integration tests. 1. Run `sg test web-integration` in the repository root to run the tests. A Sourcegraph instance does not need to be running, because all backend interactions are stubbed. From d709b689965365f5b4ec9e6e48c81b868696c8ab Mon Sep 17 00:00:00 2001 From: Indradhanush Gupta Date: Mon, 30 Jan 2023 16:16:43 +0530 Subject: [PATCH 244/678] internal/database: Scan redacted_contents from critical_and_site_config (#47079) Update tests to test for redacted_contents column and add a new one to test for redaction. Also some house cleaning: - Remove redundant code to scan row - Improve naming in tests - Reuse the config as a variable instead of repeating it --- internal/database/conf.go | 29 ++----- internal/database/conf_test.go | 150 ++++++++++++++++++++++++++------- 2 files changed, 128 insertions(+), 51 deletions(-) diff --git a/internal/database/conf.go b/internal/database/conf.go index 86fde54c86cb..69b899e2a351 100644 --- a/internal/database/conf.go +++ b/internal/database/conf.go @@ -68,9 +68,10 @@ type confStore struct { // SiteConfig contains the contents of a site config along with associated metadata. type SiteConfig struct { - ID int32 // the unique ID of this config - AuthorUserID int32 // the user id of the author that updated this config - Contents string // the raw JSON content (with comments and trailing commas allowed) + ID int32 // the unique ID of this config + AuthorUserID int32 // the user id of the author that updated this config + Contents string // the raw JSON content (with comments and trailing commas allowed) + RedactedContents string // the raw JSON content but with sensitive fields redacted CreatedAt time.Time // the date when this config was created UpdatedAt time.Time // the date when this config was updated @@ -80,6 +81,7 @@ var siteConfigColumns = []*sqlf.Query{ sqlf.Sprintf("critical_and_site_config.id"), sqlf.Sprintf("critical_and_site_config.author_user_id"), sqlf.Sprintf("critical_and_site_config.contents"), + sqlf.Sprintf("critical_and_site_config.redacted_contents"), sqlf.Sprintf("critical_and_site_config.created_at"), sqlf.Sprintf("critical_and_site_config.updated_at"), } @@ -139,29 +141,13 @@ SELECT id, author_user_id, contents, + redacted_contents, created_at, updated_at FROM critical_and_site_config WHERE (%s) ` -var scanSiteConfigs = basestore.NewSliceScanner(scanSiteConfig) - -func scanSiteConfig(s dbutil.Scanner) (*SiteConfig, error) { - var c SiteConfig - err := s.Scan( - &c.ID, - &dbutil.NullInt32{N: &c.AuthorUserID}, - &c.Contents, - &c.CreatedAt, - &c.UpdatedAt, - ) - if err != nil { - return nil, err - } - return &c, nil -} - func (s *confStore) ListSiteConfigs(ctx context.Context, paginationArgs *PaginationArgs) ([]*SiteConfig, error) { where := []*sqlf.Query{sqlf.Sprintf(`type = 'site'`)} @@ -300,8 +286,11 @@ func scanSiteConfigRow(scanner dbutil.Scanner) (*SiteConfig, error) { &s.ID, &dbutil.NullInt32{N: &s.AuthorUserID}, &s.Contents, + &dbutil.NullString{S: &s.RedactedContents}, &s.CreatedAt, &s.UpdatedAt, ) return &s, err } + +var scanSiteConfigs = basestore.NewSliceScanner(scanSiteConfigRow) diff --git a/internal/database/conf_test.go b/internal/database/conf_test.go index 44faca5ede56..97bdf191f996 100644 --- a/internal/database/conf_test.go +++ b/internal/database/conf_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/log/logtest" @@ -55,16 +56,17 @@ func TestSiteCreateIfUpToDate(t *testing.T) { logger := logtest.Scoped(t) type input struct { - lastID int32 - author_user_id int32 - contents string + lastID int32 + authorUserID int32 + contents string } type output struct { - ID int32 - author_user_id int32 - contents string - err error + ID int32 + authorUserID int32 + contents string + redactedContents string + err error } type pair struct { @@ -77,20 +79,34 @@ func TestSiteCreateIfUpToDate(t *testing.T) { sequence []pair } + configRateLimitZero := `{"defaultRateLimit": 0,"auth.providers": []}` + configRateLimitOne := `{"defaultRateLimit": 1,"auth.providers": []}` + + jsonConfigRateLimitZero := `{ + "defaultRateLimit": 0, + "auth.providers": [] +}` + + jsonConfigRateLimitOne := `{ + "defaultRateLimit": 1, + "auth.providers": [] +}` + for _, test := range []test{ { name: "create_with_author_user_id", sequence: []pair{ { input{ - lastID: 0, - author_user_id: 1, - contents: `{"defaultRateLimit": 0,"auth.providers": []}`, + lastID: 0, + authorUserID: 1, + contents: configRateLimitZero, }, output{ - ID: 2, - author_user_id: 1, - contents: `{"defaultRateLimit": 0,"auth.providers": []}`, + ID: 2, + authorUserID: 1, + contents: configRateLimitZero, + redactedContents: jsonConfigRateLimitZero, }, }, }, @@ -101,11 +117,12 @@ func TestSiteCreateIfUpToDate(t *testing.T) { { input{ lastID: 0, - contents: `{"defaultRateLimit": 0,"auth.providers": []}`, + contents: configRateLimitZero, }, output{ - ID: 2, - contents: `{"defaultRateLimit": 0,"auth.providers": []}`, + ID: 2, + contents: configRateLimitZero, + redactedContents: jsonConfigRateLimitZero, }, }, }, @@ -116,21 +133,23 @@ func TestSiteCreateIfUpToDate(t *testing.T) { { input{ lastID: 0, - contents: `{"defaultRateLimit": 0,"auth.providers": []}`, + contents: configRateLimitZero, }, output{ - ID: 2, - contents: `{"defaultRateLimit": 0,"auth.providers": []}`, + ID: 2, + contents: configRateLimitZero, + redactedContents: jsonConfigRateLimitZero, }, }, { input{ lastID: 2, - contents: `{"defaultRateLimit": 1,"auth.providers": []}`, + contents: configRateLimitOne, }, output{ - ID: 3, - contents: `{"defaultRateLimit": 1,"auth.providers": []}`, + ID: 3, + contents: configRateLimitOne, + redactedContents: jsonConfigRateLimitOne, }, }, }, @@ -141,23 +160,25 @@ func TestSiteCreateIfUpToDate(t *testing.T) { { input{ lastID: 0, - contents: `{"defaultRateLimit": 0,"auth.providers": []}`, + contents: configRateLimitZero, }, output{ - ID: 2, - contents: `{"defaultRateLimit": 0,"auth.providers": []}`, + ID: 2, + contents: configRateLimitZero, + redactedContents: jsonConfigRateLimitZero, }, }, { input{ lastID: 0, // This configuration is now behind the first one, so it shouldn't be saved - contents: `{"defaultRateLimit": 1,"auth.providers": []}`, + contents: configRateLimitOne, }, output{ - ID: 2, - contents: `{"defaultRateLimit": 1,"auth.providers": []}`, - err: errors.Append(ErrNewerEdit), + ID: 2, + contents: configRateLimitOne, + redactedContents: jsonConfigRateLimitOne, + err: errors.Append(ErrNewerEdit), }, }, }, @@ -183,6 +204,68 @@ func TestSiteCreateIfUpToDate(t *testing.T) { "defaultRateLimit": 42, "auth.providers": [], }`, + redactedContents: `{ + "disableAutoGitUpdates": true, + // This is a comment. + "defaultRateLimit": 42, + "auth.providers": [], +}`, + }, + }, + }, + }, + + { + name: "redact_sensitive_data", + sequence: []pair{ + { + input{ + lastID: 0, + contents: `{"disableAutoGitUpdates": true, + + // This is a comment. + "defaultRateLimit": 42, + "auth.providers": [ + { + "clientID": "sourcegraph-client-openid", + "clientSecret": "strongsecret", + "displayName": "Keycloak local OpenID Connect #1 (dev)", + "issuer": "http://localhost:3220/auth/realms/master", + "type": "openidconnect" + } + ] + }`, + }, + output{ + ID: 2, + contents: `{"disableAutoGitUpdates": true, + + // This is a comment. + "defaultRateLimit": 42, + "auth.providers": [ + { + "clientID": "sourcegraph-client-openid", + "clientSecret": "strongsecret", + "displayName": "Keycloak local OpenID Connect #1 (dev)", + "issuer": "http://localhost:3220/auth/realms/master", + "type": "openidconnect" + } + ] + }`, + redactedContents: `{ + "disableAutoGitUpdates": true, + // This is a comment. + "defaultRateLimit": 42, + "auth.providers": [ + { + "clientID": "sourcegraph-client-openid", + "clientSecret": "REDACTED-DATA-CHUNK-f434ecc765", + "displayName": "Keycloak local OpenID Connect #1 (dev)", + "issuer": "http://localhost:3220/auth/realms/master", + "type": "openidconnect" + } + ] +}`, }, }, }, @@ -208,9 +291,14 @@ func TestSiteCreateIfUpToDate(t *testing.T) { t.Fatal("got unexpected nil configuration after creation") } - if output.Contents != p.expected.contents { - t.Fatalf("returned configuration contents after creation - expected: %q, got:%q", p.expected.contents, output.Contents) + if diff := cmp.Diff(p.expected.contents, output.Contents); diff != "" { + t.Fatalf("mismatched configuration contents after creation, (-want +got):\n%s", diff) } + + if diff := cmp.Diff(p.expected.redactedContents, output.RedactedContents); diff != "" { + t.Fatalf("mismatched redacted_contents after creation, %v", diff) + } + if output.ID != p.expected.ID { t.Fatalf("returned configuration ID after creation - expected: %v, got:%v", p.expected.ID, output.ID) } From 3b40cc8c83e32217d38d4c49629ee59dc3ec239c Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 30 Jan 2023 11:57:12 +0100 Subject: [PATCH 245/678] Fix blame layout (#47024) --- .../src/repo/blob/BlameDecoration.module.scss | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/client/web/src/repo/blob/BlameDecoration.module.scss b/client/web/src/repo/blob/BlameDecoration.module.scss index 3a75ab077f89..33871569492d 100644 --- a/client/web/src/repo/blob/BlameDecoration.module.scss +++ b/client/web/src/repo/blob/BlameDecoration.module.scss @@ -23,26 +23,28 @@ position: relative; // Using 1px for a hairline border // stylelint-disable-next-line declaration-property-unit-allowed-list - height: calc(1.5rem + 1px); + height: calc(100% + 1px); flex: 0 0 var(--blame-recency-width); } .recency-first-in-hunk { - height: calc(1.5rem) !important; + height: calc(100%) !important; top: 0 !important; } .popover { &-trigger { - overflow: hidden; - text-overflow: ellipsis; - display: inline-block; + display: inline-flex; width: calc(100% - var(--blame-recency-width)); max-width: calc(var(--blame-decoration-width) - var(--blame-recency-width)); - height: 1.5rem; + color: var(--text-muted); &:hover { color: var(--text-muted); + text-decoration: none; + .content { + text-decoration: underline; + } } .avatar { @@ -61,17 +63,25 @@ text-decoration: none; } + .content { + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .date { + min-width: 80px; + display: inline-block; + } + .content, .author, .date { white-space: pre; color: var(--text-muted); font-family: var(--font-family-base); - } - - .date { - min-width: 80px; - display: inline-block; + height: 1.5rem; } } From 38a758682beb1d4c5974c7e80d4626a62d2c3e8b Mon Sep 17 00:00:00 2001 From: Petri-Johan Last Date: Mon, 30 Jan 2023 14:25:52 +0200 Subject: [PATCH 246/678] Fix Account Security page if auth provider removed (#47092) --- CHANGELOG.md | 1 + cmd/frontend/graphqlbackend/external_account.go | 6 +++++- .../external_account_data_resolver_test.go | 14 ++++---------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfdeb625ee58..6fc7fc18847b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ All notable changes to Sourcegraph are documented in this file. - Fixed a bug where saving default Sort & Limit filters in Code Insights did not persist [#46653](https://github.com/sourcegraph/sourcegraph/pull/46653) - Restored the old syntax for `repo:contains` filters that was previously removed in version 4.0.0. For now, both the old and new syntaxes are supported to allow for smooth upgrades. Users are encouraged to switch to the new syntax, since the old one may still be removed in a future version. +- Fixed a bug where removing an auth provider would render a user's Account Security page inaccessible if they still had an external account associated with the removed auth provider. [#47092](https://github.com/sourcegraph/sourcegraph/pull/47092) ### Removed diff --git a/cmd/frontend/graphqlbackend/external_account.go b/cmd/frontend/graphqlbackend/external_account.go index f1e6650ebf94..119fd0936ef9 100644 --- a/cmd/frontend/graphqlbackend/external_account.go +++ b/cmd/frontend/graphqlbackend/external_account.go @@ -102,7 +102,11 @@ func (r *externalAccountResolver) PublicAccountData(ctx context.Context) (*exter } if r.account.Data != nil { - return NewExternalAccountDataResolver(ctx, r.account) + res, err := NewExternalAccountDataResolver(ctx, r.account) + if err != nil { + return nil, nil + } + return res, nil } return nil, nil diff --git a/cmd/frontend/graphqlbackend/external_account_data_resolver_test.go b/cmd/frontend/graphqlbackend/external_account_data_resolver_test.go index 67b084ebaab4..de7ea2cb7b62 100644 --- a/cmd/frontend/graphqlbackend/external_account_data_resolver_test.go +++ b/cmd/frontend/graphqlbackend/external_account_data_resolver_test.go @@ -118,10 +118,10 @@ func TestExternalAccountDataResolver_PublicAccountDataFromJSON(t *testing.T) { ` ctx := actor.WithActor(context.Background(), &actor.Actor{UID: 1}) - t.Run("Errors out if no auth provider match", func(t *testing.T) { + t.Run("Account not returned if no matching auth provider found", func(t *testing.T) { noMatchAccount := account noMatchAccount.ServiceType = "no-match" - externalAccounts.ListFunc.SetDefaultReturn([]*extsvc.Account{&noMatchAccount}, nil) + externalAccounts.ListFunc.SetDefaultReturn([]*extsvc.Account{&noMatchAccount, &account}, nil) defer externalAccounts.ListFunc.SetDefaultReturn([]*extsvc.Account{&account}, nil) RunTests(t, []*Test{ @@ -129,14 +129,8 @@ func TestExternalAccountDataResolver_PublicAccountDataFromJSON(t *testing.T) { Context: ctx, Schema: mustParseGraphQLSchema(t, db), Query: query, - ExpectedResult: `{"user":{"externalAccounts":{"nodes":[{"publicAccountData":null}]}}}`, - ExpectedErrors: []*errors.QueryError{ - { - Message: "cannot find authorization provider for the external account, service type: no-match", - Path: []any{"user", "externalAccounts", "nodes", 0, "publicAccountData"}, - }, - }, - Variables: map[string]any{"username": "alice"}, + ExpectedResult: `{"user":{"externalAccounts":{"nodes":[{"publicAccountData":null},{"publicAccountData":{"displayName":"Alice Smith","login":"alice_2","url":null}}]}}}`, + Variables: map[string]any{"username": "alice"}, }, }) }) From df0cc7d199cd70acf5daf6ddc30fc2442b67de94 Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Mon, 30 Jan 2023 15:12:21 +0200 Subject: [PATCH 247/678] redispool: expand test coverage on expire (#47075) Mostly just ensuring we correctly handle expire on non-"string" types. Additionally I sprinkle in a bunch of t.Parallel since we do a time.Sleep. This also helps us pick up on potential races. Test Plan: go test -race --- internal/redispool/keyvalue_test.go | 95 +++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 4 deletions(-) diff --git a/internal/redispool/keyvalue_test.go b/internal/redispool/keyvalue_test.go index 247adaedfe85..4bc80db9069b 100644 --- a/internal/redispool/keyvalue_test.go +++ b/internal/redispool/keyvalue_test.go @@ -17,10 +17,14 @@ func TestRedisKeyValue(t *testing.T) { } func testKeyValue(t *testing.T, kv redispool.KeyValue) { + t.Parallel() + errWrongType := errors.New("WRONGTYPE") // "strings" is the name of the classic group of commands in redis (get, set, ttl, etc). We call it classic since that is less confusing. t.Run("classic", func(t *testing.T) { + t.Parallel() + require := require{TB: t} // Redis returns nil on unset values @@ -58,6 +62,8 @@ func testKeyValue(t *testing.T, kv redispool.KeyValue) { }) t.Run("hash", func(t *testing.T) { + t.Parallel() + require := require{TB: t} // Pretty much copy-pasta above tests but on a hash @@ -88,6 +94,8 @@ func testKeyValue(t *testing.T, kv redispool.KeyValue) { }) t.Run("list", func(t *testing.T) { + t.Parallel() + require := require{TB: t} // Redis behaviour on unset lists @@ -142,6 +150,8 @@ func testKeyValue(t *testing.T, kv redispool.KeyValue) { }) t.Run("empty", func(t *testing.T) { + t.Parallel() + require := require{TB: t} // Strings group @@ -161,15 +171,26 @@ func testKeyValue(t *testing.T, kv redispool.KeyValue) { }) t.Run("expire", func(t *testing.T) { - require := require{TB: t} - // Skips because of time.Sleep if testing.Short() { t.Skip() } + t.Parallel() + + require := require{TB: t} - // TODO test expire works on different data types after mutation. - // Check that expire works after incr. + // Set removes expire + { + k := "expires-set-reset" + require.Works(kv.SetEx(k, 60, "1")) + require.Equal(kv.Get(k), "1") + require.TTL(kv, k, 60) + + require.Works(kv.Set(k, "2")) + require.Equal(kv.Get(k), "2") + require.TTL(kv, k, -1) + + } // SetEx, Expire and TTL require.Works(kv.SetEx("expires-setex", 60, "1")) @@ -195,7 +216,73 @@ func testKeyValue(t *testing.T, kv redispool.KeyValue) { require.TTL(kv, "expires-set", -2) }) + t.Run("hash-expire", func(t *testing.T) { + // Skips because of time.Sleep + if testing.Short() { + t.Skip() + } + t.Parallel() + + require := require{TB: t} + + // Hash mutations keep expire + require.Works(kv.HSet("expires-unset-hash", "simple", "1")) + require.Works(kv.HSet("expires-set-hash", "simple", "1")) + require.Works(kv.Expire("expires-set-hash", 60)) + require.TTL(kv, "expires-unset-hash", -1) + require.TTL(kv, "expires-set-hash", 60) + require.Equal(kv.HGet("expires-unset-hash", "simple"), "1") + require.Equal(kv.HGet("expires-set-hash", "simple"), "1") + + require.Works(kv.HSet("expires-unset-hash", "simple", "2")) + require.Works(kv.HSet("expires-set-hash", "simple", "2")) + require.TTL(kv, "expires-unset-hash", -1) + require.TTL(kv, "expires-set-hash", 60) + require.Equal(kv.HGet("expires-unset-hash", "simple"), "2") + require.Equal(kv.HGet("expires-set-hash", "simple"), "2") + + // Check expiration happens on hashes + require.Works(kv.Expire("expires-set-hash", 1)) + time.Sleep(1100 * time.Millisecond) + require.Equal(kv.HGet("expires-set-hash", "simple"), nil) + require.TTL(kv, "expires-set-hash", -2) + }) + + t.Run("hash-expire", func(t *testing.T) { + // Skips because of time.Sleep + if testing.Short() { + t.Skip() + } + t.Parallel() + + require := require{TB: t} + + // Hash mutations keep expire + require.Works(kv.LPush("expires-unset-list", "1")) + require.Works(kv.LPush("expires-set-list", "1")) + require.Works(kv.Expire("expires-set-list", 60)) + require.TTL(kv, "expires-unset-list", -1) + require.TTL(kv, "expires-set-list", 60) + require.AllEqual(kv.LRange("expires-unset-list", 0, -1), []string{"1"}) + require.AllEqual(kv.LRange("expires-set-list", 0, -1), []string{"1"}) + + require.Works(kv.LPush("expires-unset-list", "2")) + require.Works(kv.LPush("expires-set-list", "2")) + require.TTL(kv, "expires-unset-list", -1) + require.TTL(kv, "expires-set-list", 60) + require.AllEqual(kv.LRange("expires-unset-list", 0, -1), []string{"2", "1"}) + require.AllEqual(kv.LRange("expires-set-list", 0, -1), []string{"2", "1"}) + + // Check expiration happens on hashes + require.Works(kv.Expire("expires-set-list", 1)) + time.Sleep(1100 * time.Millisecond) + require.Equal(kv.HGet("expires-set-list", "simple"), nil) + require.TTL(kv, "expires-set-list", -2) + }) + t.Run("wrongtype", func(t *testing.T) { + t.Parallel() + require := require{TB: t} requireWrongType := func(err error) { t.Helper() From 64e04105b6f412994c480d2f78d8570ba43e86c0 Mon Sep 17 00:00:00 2001 From: Eric Fritz Date: Mon, 30 Jan 2023 08:13:08 -0600 Subject: [PATCH 248/678] codeintel: Add resolvers for precise index entity (#47048) --- cmd/frontend/graphqlbackend/codeintel.graphql | 199 ++++++ cmd/frontend/graphqlbackend/node.go | 5 + .../frontend/internal/codeintel/resolvers.go | 11 + .../autoindexing/transport/graphql/iface.go | 1 + .../transport/graphql/mocks_test.go | 128 ++++ .../transport/graphql/observability.go | 6 + .../transport/graphql/precise_indexes.go | 610 ++++++++++++++++++ internal/codeintel/resolvers/all.go | 44 ++ 8 files changed, 1004 insertions(+) create mode 100644 enterprise/internal/codeintel/autoindexing/transport/graphql/precise_indexes.go diff --git a/cmd/frontend/graphqlbackend/codeintel.graphql b/cmd/frontend/graphqlbackend/codeintel.graphql index 46ddadaedbe7..b5a1ef62a9b2 100644 --- a/cmd/frontend/graphqlbackend/codeintel.graphql +++ b/cmd/frontend/graphqlbackend/codeintel.graphql @@ -207,6 +207,51 @@ extend type Query { after: String ): CodeIntelligenceConfigurationPolicyConnection! + """ + Query precise code intelligence indexes. + """ + preciseIndexes( + """ + If supplied, only precise indexes for the given repository will be returned. + """ + repo: ID + + """ + If supplied, only precise indexes that match the given terms by their state, + repository name, commit, root, and indexer fields will be returned.. + """ + query: String + + """ + If supplied, only precise indexes in one of the provided states are returned. + """ + states: [PreciseIndexState!] + + """ + If supplied, only precise indexes that are a dependency of the specified index are returned. + """ + dependencyOf: ID + + """ + If supplied, only precise indexes that are a dependent of the specified index are returned. + """ + dependentOf: ID + + """ + If specified, this limits the number of results per request. + """ + first: Int + + """ + If specified, this indicates that the request should be paginated and to fetch results starting + at this cursor. + + A future request can be made for more results by passing in the 'PreciseIndexConnection.pageInfo.endCursor' + that is returned. + """ + after: String + ): PreciseIndexConnection! + """ The repository's LSIF uploads. """ @@ -1070,6 +1115,160 @@ enum LSIFUploadState { DELETED } +""" +A list of precise code intelligence indexes. +""" +type PreciseIndexConnection { + """ + The current page of indexes. + """ + nodes: [PreciseIndex!]! + + """ + The total number of results (over all pages) in this list. + """ + totalCount: Int + + """ + Metadata about the current page of results. + """ + pageInfo: PageInfo! +} + +""" +Possible states for PreciseIndexes. +""" +enum PreciseIndexState { + UPLOADING_INDEX + QUEUED_FOR_PROCESSING + PROCESSING + PROCESSING_ERRORED + COMPLETED + DELETING + DELETED + QUEUED_FOR_INDEXING + INDEXING + INDEXING_ERRORED + INDEXING_COMPLETED +} + +""" +Metadata and status about a precise code intelligence index. +""" +type PreciseIndex implements Node { + """ + The ID. + """ + id: ID! + + """ + The project for which this index provides code intelligence. + """ + projectRoot: CodeIntelGitTree + + """ + The original 40-character commit commit supplied at creation. + """ + inputCommit: String! + + """ + The original root supplied at creation. + """ + inputRoot: String! + + """ + The original indexer name supplied at creation. + """ + inputIndexer: String! + + """ + The tags, if any, associated with this commit. + """ + tags: [String!]! + + """ + The indexer used to produce this index. + """ + indexer: CodeIntelIndexer + + """ + The current state. + """ + state: PreciseIndexState! + + """ + The time the index was queued for indexing. + """ + queuedAt: DateTime + + """ + The time the index job started running. + """ + indexingStartedAt: DateTime + + """ + The time the index job stopped running. + """ + indexingFinishedAt: DateTime + + """ + The time the index data file was uploaded. + """ + uploadedAt: DateTime + + """ + The time the upload data file started being processed. + """ + processingStartedAt: DateTime + + """ + The time the upload data file stopped being processed. + """ + processingFinishedAt: DateTime + + """ + The indexing or processing error message. + """ + failure: String + + """ + The rank of this index job or processing job in its respective queue. + """ + placeInQueue: Int + + """ + The configuration and execution summary (if completed or errored) of this index job. + """ + steps: IndexSteps + + """ + If set, this index has been marked as replaceable by a new auto-indexing job. + """ + shouldReindex: Boolean! + + """ + Whether or not this index provides intelligence for the tip of the default branch. Find reference + queries will return symbols from remote repositories only when this property is true. This property + is updated asynchronously and is eventually consistent with the git data known by the instance. + """ + isLatestForRepo: Boolean! + + """ + The list of retention policies associated with this index. + """ + retentionPolicyOverview( + matchesOnly: Boolean! + query: String + after: String + first: Int + ): CodeIntelligenceRetentionPolicyMatchesConnection! + + """ + Audit logs representing each state change of the upload in order from earliest to latest. + """ + auditLogs: [LSIFUploadAuditLog!] +} + """ Metadata and status about an LSIF upload. """ diff --git a/cmd/frontend/graphqlbackend/node.go b/cmd/frontend/graphqlbackend/node.go index cc0f1c4c5cb6..765599b6c512 100644 --- a/cmd/frontend/graphqlbackend/node.go +++ b/cmd/frontend/graphqlbackend/node.go @@ -233,6 +233,11 @@ func (r *NodeResolver) ToLSIFIndex() (resolverstubs.LSIFIndexResolver, bool) { return n, ok } +func (r *NodeResolver) ToPreciseIndex() (resolverstubs.PreciseIndexResolver, bool) { + n, ok := r.Node.(resolverstubs.PreciseIndexResolver) + return n, ok +} + func (r *NodeResolver) ToCodeIntelligenceConfigurationPolicy() (resolverstubs.CodeIntelligenceConfigurationPolicyResolver, bool) { n, ok := r.Node.(resolverstubs.CodeIntelligenceConfigurationPolicyResolver) return n, ok diff --git a/enterprise/cmd/frontend/internal/codeintel/resolvers.go b/enterprise/cmd/frontend/internal/codeintel/resolvers.go index ad6f683da7c0..132e134b7c6b 100644 --- a/enterprise/cmd/frontend/internal/codeintel/resolvers.go +++ b/enterprise/cmd/frontend/internal/codeintel/resolvers.go @@ -41,6 +41,9 @@ func (r *Resolver) NodeResolvers() map[string]gql.NodeByIDFunc { "CodeIntelligenceConfigurationPolicy": func(ctx context.Context, id graphql.ID) (gql.Node, error) { return r.ConfigurationPolicyByID(ctx, id) }, + "PreciseIndex": func(ctx context.Context, id graphql.ID) (gql.Node, error) { + return r.PreciseIndexByID(ctx, id) + }, } } @@ -52,6 +55,14 @@ func (r *Resolver) LSIFUploads(ctx context.Context, args *resolverstubs.LSIFUplo return r.uploadsRootResolver.LSIFUploads(ctx, args) } +func (r *Resolver) PreciseIndexes(ctx context.Context, args *resolverstubs.PreciseIndexesQueryArgs) (_ resolverstubs.PreciseIndexConnectionResolver, err error) { + return r.autoIndexingRootResolver.PreciseIndexes(ctx, args) +} + +func (r *Resolver) PreciseIndexByID(ctx context.Context, id graphql.ID) (_ resolverstubs.PreciseIndexResolver, err error) { + return r.autoIndexingRootResolver.PreciseIndexByID(ctx, id) +} + func (r *Resolver) LSIFUploadsByRepo(ctx context.Context, args *resolverstubs.LSIFRepositoryUploadsQueryArgs) (_ resolverstubs.LSIFUploadConnectionResolver, err error) { return r.uploadsRootResolver.LSIFUploadsByRepo(ctx, args) } diff --git a/enterprise/internal/codeintel/autoindexing/transport/graphql/iface.go b/enterprise/internal/codeintel/autoindexing/transport/graphql/iface.go index fc9637af13cf..2e01f31cffb9 100644 --- a/enterprise/internal/codeintel/autoindexing/transport/graphql/iface.go +++ b/enterprise/internal/codeintel/autoindexing/transport/graphql/iface.go @@ -53,6 +53,7 @@ type UploadsService interface { GetListTags(ctx context.Context, repo api.RepoName, commitObjs ...string) (_ []*gitdomain.Tag, err error) GetUploadDocumentsForPath(ctx context.Context, bundleID int, pathPattern string) ([]string, int, error) GetUploadsByIDs(ctx context.Context, ids ...int) (_ []types.Upload, err error) + GetUploadByID(ctx context.Context, id int) (_ types.Upload, _ bool, err error) } type PolicyService interface { diff --git a/enterprise/internal/codeintel/autoindexing/transport/graphql/mocks_test.go b/enterprise/internal/codeintel/autoindexing/transport/graphql/mocks_test.go index f2e340f6570a..667023593570 100644 --- a/enterprise/internal/codeintel/autoindexing/transport/graphql/mocks_test.go +++ b/enterprise/internal/codeintel/autoindexing/transport/graphql/mocks_test.go @@ -3232,6 +3232,9 @@ type MockUploadsService struct { // GetRecentUploadsSummaryFunc is an instance of a mock function object // controlling the behavior of the method GetRecentUploadsSummary. GetRecentUploadsSummaryFunc *UploadsServiceGetRecentUploadsSummaryFunc + // GetUploadByIDFunc is an instance of a mock function object + // controlling the behavior of the method GetUploadByID. + GetUploadByIDFunc *UploadsServiceGetUploadByIDFunc // GetUploadDocumentsForPathFunc is an instance of a mock function // object controlling the behavior of the method // GetUploadDocumentsForPath. @@ -3268,6 +3271,11 @@ func NewMockUploadsService() *MockUploadsService { return }, }, + GetUploadByIDFunc: &UploadsServiceGetUploadByIDFunc{ + defaultHook: func(context.Context, int) (r0 types.Upload, r1 bool, r2 error) { + return + }, + }, GetUploadDocumentsForPathFunc: &UploadsServiceGetUploadDocumentsForPathFunc{ defaultHook: func(context.Context, int, string) (r0 []string, r1 int, r2 error) { return @@ -3310,6 +3318,11 @@ func NewStrictMockUploadsService() *MockUploadsService { panic("unexpected invocation of MockUploadsService.GetRecentUploadsSummary") }, }, + GetUploadByIDFunc: &UploadsServiceGetUploadByIDFunc{ + defaultHook: func(context.Context, int) (types.Upload, bool, error) { + panic("unexpected invocation of MockUploadsService.GetUploadByID") + }, + }, GetUploadDocumentsForPathFunc: &UploadsServiceGetUploadDocumentsForPathFunc{ defaultHook: func(context.Context, int, string) ([]string, int, error) { panic("unexpected invocation of MockUploadsService.GetUploadDocumentsForPath") @@ -3345,6 +3358,9 @@ func NewMockUploadsServiceFrom(i UploadsService) *MockUploadsService { GetRecentUploadsSummaryFunc: &UploadsServiceGetRecentUploadsSummaryFunc{ defaultHook: i.GetRecentUploadsSummary, }, + GetUploadByIDFunc: &UploadsServiceGetUploadByIDFunc{ + defaultHook: i.GetUploadByID, + }, GetUploadDocumentsForPathFunc: &UploadsServiceGetUploadDocumentsForPathFunc{ defaultHook: i.GetUploadDocumentsForPath, }, @@ -3812,6 +3828,118 @@ func (c UploadsServiceGetRecentUploadsSummaryFuncCall) Results() []interface{} { return []interface{}{c.Result0, c.Result1} } +// UploadsServiceGetUploadByIDFunc describes the behavior when the +// GetUploadByID method of the parent MockUploadsService instance is +// invoked. +type UploadsServiceGetUploadByIDFunc struct { + defaultHook func(context.Context, int) (types.Upload, bool, error) + hooks []func(context.Context, int) (types.Upload, bool, error) + history []UploadsServiceGetUploadByIDFuncCall + mutex sync.Mutex +} + +// GetUploadByID delegates to the next hook function in the queue and stores +// the parameter and result values of this invocation. +func (m *MockUploadsService) GetUploadByID(v0 context.Context, v1 int) (types.Upload, bool, error) { + r0, r1, r2 := m.GetUploadByIDFunc.nextHook()(v0, v1) + m.GetUploadByIDFunc.appendCall(UploadsServiceGetUploadByIDFuncCall{v0, v1, r0, r1, r2}) + return r0, r1, r2 +} + +// SetDefaultHook sets function that is called when the GetUploadByID method +// of the parent MockUploadsService instance is invoked and the hook queue +// is empty. +func (f *UploadsServiceGetUploadByIDFunc) SetDefaultHook(hook func(context.Context, int) (types.Upload, bool, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// GetUploadByID method of the parent MockUploadsService instance invokes +// the hook at the front of the queue and discards it. After the queue is +// empty, the default hook function is invoked for any future action. +func (f *UploadsServiceGetUploadByIDFunc) PushHook(hook func(context.Context, int) (types.Upload, bool, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *UploadsServiceGetUploadByIDFunc) SetDefaultReturn(r0 types.Upload, r1 bool, r2 error) { + f.SetDefaultHook(func(context.Context, int) (types.Upload, bool, error) { + return r0, r1, r2 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *UploadsServiceGetUploadByIDFunc) PushReturn(r0 types.Upload, r1 bool, r2 error) { + f.PushHook(func(context.Context, int) (types.Upload, bool, error) { + return r0, r1, r2 + }) +} + +func (f *UploadsServiceGetUploadByIDFunc) nextHook() func(context.Context, int) (types.Upload, bool, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *UploadsServiceGetUploadByIDFunc) appendCall(r0 UploadsServiceGetUploadByIDFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of UploadsServiceGetUploadByIDFuncCall objects +// describing the invocations of this function. +func (f *UploadsServiceGetUploadByIDFunc) History() []UploadsServiceGetUploadByIDFuncCall { + f.mutex.Lock() + history := make([]UploadsServiceGetUploadByIDFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// UploadsServiceGetUploadByIDFuncCall is an object that describes an +// invocation of method GetUploadByID on an instance of MockUploadsService. +type UploadsServiceGetUploadByIDFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 int + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 types.Upload + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 bool + // Result2 is the value of the 3rd result returned from this method + // invocation. + Result2 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c UploadsServiceGetUploadByIDFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c UploadsServiceGetUploadByIDFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1, c.Result2} +} + // UploadsServiceGetUploadDocumentsForPathFunc describes the behavior when // the GetUploadDocumentsForPath method of the parent MockUploadsService // instance is invoked. diff --git a/enterprise/internal/codeintel/autoindexing/transport/graphql/observability.go b/enterprise/internal/codeintel/autoindexing/transport/graphql/observability.go index 7892ff54cda6..91a75a4b0bbf 100644 --- a/enterprise/internal/codeintel/autoindexing/transport/graphql/observability.go +++ b/enterprise/internal/codeintel/autoindexing/transport/graphql/observability.go @@ -35,6 +35,9 @@ type operations struct { repositorySummary *observation.Operation getSupportedByCtags *observation.Operation gitBlobCodeIntelInfo *observation.Operation + + preciseIndexes *observation.Operation + preciseIndexByID *observation.Operation } func newOperations(observationCtx *observation.Context) *operations { @@ -81,5 +84,8 @@ func newOperations(observationCtx *observation.Context) *operations { repositorySummary: op("RepositorySummary"), getSupportedByCtags: op("GetSupportedByCtags"), gitBlobCodeIntelInfo: op("GitBlobCodeIntelInfo"), + + preciseIndexes: op("PreciseIndexes"), + preciseIndexByID: op("PreciseIndexByID"), } } diff --git a/enterprise/internal/codeintel/autoindexing/transport/graphql/precise_indexes.go b/enterprise/internal/codeintel/autoindexing/transport/graphql/precise_indexes.go new file mode 100644 index 000000000000..400ab8708c5a --- /dev/null +++ b/enterprise/internal/codeintel/autoindexing/transport/graphql/precise_indexes.go @@ -0,0 +1,610 @@ +package graphql + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/relay" + "github.com/opentracing/opentracing-go/log" + + autoindexingshared "github.com/sourcegraph/sourcegraph/enterprise/internal/codeintel/autoindexing/shared" + sharedresolvers "github.com/sourcegraph/sourcegraph/enterprise/internal/codeintel/shared/resolvers" + "github.com/sourcegraph/sourcegraph/enterprise/internal/codeintel/shared/types" + uploadsshared "github.com/sourcegraph/sourcegraph/enterprise/internal/codeintel/uploads/shared" + resolverstubs "github.com/sourcegraph/sourcegraph/internal/codeintel/resolvers" + "github.com/sourcegraph/sourcegraph/internal/gitserver" + "github.com/sourcegraph/sourcegraph/internal/gqlutil" + "github.com/sourcegraph/sourcegraph/internal/observation" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +const DefaultPageSize = 50 + +func (r *rootResolver) PreciseIndexes(ctx context.Context, args *resolverstubs.PreciseIndexesQueryArgs) (_ resolverstubs.PreciseIndexConnectionResolver, err error) { + ctx, errTracer, endObservation := r.operations.preciseIndexes.WithErrors(ctx, &err, observation.Args{LogFields: []log.Field{ + // log.String("uploadID", string(id)), + }}) + endObservation.OnCancel(ctx, 1, observation.Args{}) + + pageSize := DefaultPageSize + if args.First != nil { + pageSize = int(*args.First) + } + uploadOffset := 0 + indexOffset := 0 + if args.After != nil { + parts := strings.Split(*args.After, ":") + if len(parts) != 2 { + return nil, errors.New("invalid cursor") + } + + if parts[0] != "" { + v, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, errors.New("invalid cursor") + } + + uploadOffset = v + } + if parts[1] != "" { + v, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, errors.New("invalid cursor") + } + + indexOffset = v + } + } + + var uploadStates, indexStates []string + if args.States != nil { + for _, state := range *args.States { + switch state { + case "QUEUED_FOR_INDEXING": + indexStates = append(indexStates, "queued") + case "INDEXING": + indexStates = append(indexStates, "processing") + case "INDEXING_ERRORED": + indexStates = append(indexStates, "errored") + + case "UPLOADING_INDEX": + uploadStates = append(uploadStates, "uploading") + case "QUEUED_FOR_PROCESSING": + uploadStates = append(uploadStates, "queued") + case "PROCESSING": + uploadStates = append(uploadStates, "processing") + case "PROCESSING_ERRORED": + uploadStates = append(uploadStates, "errored") + case "COMPLETED": + uploadStates = append(uploadStates, "completed") + case "DELETING": + uploadStates = append(uploadStates, "deleting") + case "DELETED": + uploadStates = append(uploadStates, "deleted") + + default: + return nil, errors.Newf("filtering by state %q is unsupported", state) + } + } + } + skipUploads := len(uploadStates) == 0 && len(indexStates) != 0 + skipIndexes := len(uploadStates) != 0 && len(indexStates) == 0 + + var dependencyOf int + if args.DependencyOf != nil { + v, v2, err := unmarshalPreciseIndexGQLID(graphql.ID(*args.DependencyOf)) + if err != nil { + return nil, err + } + if v == 0 { + return nil, errors.Newf("requested dependency of precise index record without data (indexid=%d)", v2) + } + + dependencyOf = int(v) + skipIndexes = true + } + var dependentOf int + if args.DependentOf != nil { + v, v2, err := unmarshalPreciseIndexGQLID(graphql.ID(*args.DependentOf)) + if err != nil { + return nil, err + } + if v == 0 { + return nil, errors.Newf("requested dependent of precise index record without data (indexid=%d)", v2) + } + + dependentOf = int(v) + skipIndexes = true + } + + var repositoryID int + if args.Repo != nil { + v, err := UnmarshalRepositoryID(*args.Repo) + if err != nil { + return nil, err + } + + repositoryID = int(v) + } + + term := "" + if args.Query != nil { + term = *args.Query + } + + var uploads []types.Upload + totalUploadCount := 0 + if !skipUploads { + if uploads, totalUploadCount, err = r.uploadSvc.GetUploads(ctx, uploadsshared.GetUploadsOptions{ + RepositoryID: repositoryID, + States: uploadStates, + Term: term, + DependencyOf: dependencyOf, + DependentOf: dependentOf, + Limit: pageSize, + Offset: uploadOffset, + }); err != nil { + return nil, err + } + } + + var indexes []types.Index + totalIndexCount := 0 + if !skipIndexes { + if indexes, totalIndexCount, err = r.autoindexSvc.GetIndexes(ctx, autoindexingshared.GetIndexesOptions{ + RepositoryID: repositoryID, + States: indexStates, + Term: term, + WithoutUpload: true, + Limit: pageSize, + Offset: indexOffset, + }); err != nil { + return nil, err + } + } + + type pair struct { + upload *types.Upload + index *types.Index + } + pairs := make([]pair, 0, pageSize) + addUpload := func(upload types.Upload) { pairs = append(pairs, pair{&upload, nil}) } + addIndex := func(index types.Index) { pairs = append(pairs, pair{nil, &index}) } + + uIdx := 0 + iIdx := 0 + for uIdx < len(uploads) && iIdx < len(indexes) && (uIdx+iIdx) < pageSize { + if uploads[uIdx].UploadedAt.After(indexes[iIdx].QueuedAt) { + addUpload(uploads[uIdx]) + uIdx++ + } else { + addIndex(indexes[iIdx]) + iIdx++ + } + } + for uIdx < len(uploads) && (uIdx+iIdx) < pageSize { + addUpload(uploads[uIdx]) + uIdx++ + } + for iIdx < len(indexes) && (uIdx+iIdx) < pageSize { + addIndex(indexes[iIdx]) + iIdx++ + } + + cursor := "" + if newUploadOffset := uploadOffset + uIdx; newUploadOffset < totalUploadCount { + cursor += strconv.Itoa(newUploadOffset) + } + cursor += ":" + if newIndexOffset := indexOffset + iIdx; newIndexOffset < totalIndexCount { + cursor += strconv.Itoa(newIndexOffset) + } + if cursor == ":" { + cursor = "" + } + + // Create a new prefetcher here as we only want to cache upload and index records in + // the same graphQL request, not across different request. + prefetcher := sharedresolvers.NewPrefetcher(r.autoindexSvc, r.uploadSvc) + db := r.autoindexSvc.GetUnsafeDB() + locationResolver := sharedresolvers.NewCachedLocationResolver(db, gitserver.NewClient()) + + for _, pair := range pairs { + if pair.upload != nil && pair.upload.AssociatedIndexID != nil { + prefetcher.MarkIndex(*pair.upload.AssociatedIndexID) + } + } + + resolvers := make([]resolverstubs.PreciseIndexResolver, 0, len(pairs)) + for _, pair := range pairs { + resolver, err := NewPreciseIndexResolver(ctx, r.autoindexSvc, r.uploadSvc, r.policySvc, prefetcher, locationResolver, errTracer, pair.upload, pair.index) + if err != nil { + return nil, err + } + + resolvers = append(resolvers, resolver) + } + + return NewPreciseIndexConnectionResolver(resolvers, totalUploadCount+totalIndexCount, cursor), nil +} + +type preciseIndexConnectionResolver struct { + nodes []resolverstubs.PreciseIndexResolver + totalCount int + cursor string +} + +func NewPreciseIndexConnectionResolver( + nodes []resolverstubs.PreciseIndexResolver, + totalCount int, + cursor string, +) resolverstubs.PreciseIndexConnectionResolver { + return &preciseIndexConnectionResolver{ + nodes: nodes, + totalCount: totalCount, + cursor: cursor, + } +} + +func (r *rootResolver) PreciseIndexByID(ctx context.Context, id graphql.ID) (_ resolverstubs.PreciseIndexResolver, err error) { + ctx, errTracer, endObservation := r.operations.preciseIndexes.WithErrors(ctx, &err, observation.Args{LogFields: []log.Field{ + log.String("id", string(id)), + }}) + endObservation.OnCancel(ctx, 1, observation.Args{}) + + var v string + if err := relay.UnmarshalSpec(id, &v); err != nil { + return nil, err + } + parts := strings.Split(v, ":") + if len(parts) != 2 { + return nil, errors.New("invalid identifier") + } + rawID, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, errors.New("invalid identifier") + } + + // Create a new prefetcher here as we only want to cache upload and index records in + // the same graphQL request, not across different request. + prefetcher := sharedresolvers.NewPrefetcher(r.autoindexSvc, r.uploadSvc) + db := r.autoindexSvc.GetUnsafeDB() + locationResolver := sharedresolvers.NewCachedLocationResolver(db, gitserver.NewClient()) + + switch parts[0] { + case "U": + upload, ok, err := r.uploadSvc.GetUploadByID(ctx, rawID) + if err != nil || !ok { + return nil, err + } + + return NewPreciseIndexResolver(ctx, r.autoindexSvc, r.uploadSvc, r.policySvc, prefetcher, locationResolver, errTracer, &upload, nil) + + case "I": + index, ok, err := r.autoindexSvc.GetIndexByID(ctx, rawID) + if err != nil || !ok { + return nil, err + } + + return NewPreciseIndexResolver(ctx, r.autoindexSvc, r.uploadSvc, r.policySvc, prefetcher, locationResolver, errTracer, nil, &index) + } + + return nil, errors.New("invalid identifier") +} + +func (r *preciseIndexConnectionResolver) Nodes(ctx context.Context) ([]resolverstubs.PreciseIndexResolver, error) { + return r.nodes, nil +} + +func (r *preciseIndexConnectionResolver) TotalCount(ctx context.Context) (*int32, error) { + count := int32(r.totalCount) + return &count, nil +} + +func (r *preciseIndexConnectionResolver) PageInfo(ctx context.Context) (resolverstubs.PageInfo, error) { + if r.cursor != "" { + return &pageInfo{hasNextPage: true, endCursor: &r.cursor}, nil + } + + return &pageInfo{hasNextPage: false}, nil +} + +type preciseIndexResolver struct { + upload *types.Upload + index *types.Index + uploadResolver resolverstubs.LSIFUploadResolver + indexResolver resolverstubs.LSIFIndexResolver +} + +func NewPreciseIndexResolver( + ctx context.Context, + autoindexingSvc AutoIndexingService, + uploadsSvc UploadsService, + policySvc PolicyService, + prefetcher *sharedresolvers.Prefetcher, + locationResolver *sharedresolvers.CachedLocationResolver, + traceErrs *observation.ErrCollector, + upload *types.Upload, + index *types.Index, +) (resolverstubs.PreciseIndexResolver, error) { + var uploadResolver resolverstubs.LSIFUploadResolver + if upload != nil { + uploadResolver = sharedresolvers.NewUploadResolver(uploadsSvc, autoindexingSvc, policySvc, *upload, prefetcher, locationResolver, traceErrs) + + if upload.AssociatedIndexID != nil { + v, ok, err := prefetcher.GetIndexByID(ctx, *upload.AssociatedIndexID) + if err != nil { + return nil, err + } + if ok { + index = &v + } + } + } + + var indexResolver resolverstubs.LSIFIndexResolver + if index != nil { + indexResolver = sharedresolvers.NewIndexResolver(autoindexingSvc, uploadsSvc, policySvc, *index, prefetcher, locationResolver, traceErrs) + } + + return &preciseIndexResolver{ + upload: upload, + index: index, + uploadResolver: uploadResolver, + indexResolver: indexResolver, + }, nil +} + +func (r *preciseIndexResolver) ID() graphql.ID { + if r.upload != nil { + return relay.MarshalID("PreciseIndex", fmt.Sprintf("U:%d", r.upload.ID)) + } + + return relay.MarshalID("PreciseIndex", fmt.Sprintf("I:%d", r.index.ID)) +} + +func (r *preciseIndexResolver) ProjectRoot(ctx context.Context) (resolverstubs.GitTreeEntryResolver, error) { + if r.uploadResolver != nil { + return r.uploadResolver.ProjectRoot(ctx) + } + + return r.indexResolver.ProjectRoot(ctx) +} + +func (r *preciseIndexResolver) InputCommit() string { + if r.uploadResolver != nil { + return r.uploadResolver.InputCommit() + } + + return r.indexResolver.InputCommit() +} + +func (r *preciseIndexResolver) Tags(ctx context.Context) ([]string, error) { + if r.uploadResolver != nil { + return r.uploadResolver.Tags(ctx) + } + + return r.indexResolver.Tags(ctx) +} + +func (r *preciseIndexResolver) InputRoot() string { + if r.uploadResolver != nil { + return r.uploadResolver.InputRoot() + } + + return r.indexResolver.InputRoot() +} + +func (r *preciseIndexResolver) InputIndexer() string { + if r.uploadResolver != nil { + return r.uploadResolver.InputIndexer() + } + + return r.indexResolver.InputIndexer() +} + +func (r *preciseIndexResolver) Indexer() resolverstubs.CodeIntelIndexerResolver { + if r.uploadResolver != nil { + return r.uploadResolver.Indexer() + } + + return r.indexResolver.Indexer() +} + +func (r *preciseIndexResolver) State() string { + if r.upload != nil { + switch strings.ToUpper(r.upload.State) { + case "UPLOADING": + return "UPLOADING_INDEX" + + case "QUEUED": + return "QUEUED_FOR_PROCESSING" + + case "PROCESSING": + return "PROCESSING" + + case "FAILED": + fallthrough + case "ERRORED": + return "PROCESSING_ERRORED" + + case "COMPLETED": + return "COMPLETED" + + case "DELETING": + return "DELETING" + + case "DELETED": + return "DELETED" + + default: + panic(fmt.Sprintf("unrecognized upload state %q", r.upload.State)) + } + } + + switch strings.ToUpper(r.index.State) { + case "QUEUED": + return "QUEUED_FOR_INDEXING" + + case "PROCESSING": + return "INDEXING" + + case "FAILED": + fallthrough + case "ERRORED": + return "INDEXING_ERRORED" + + case "COMPLETED": + // Should not actually occur in practice (where did upload go?) + return "INDEXING_COMPLETED" + + default: + panic(fmt.Sprintf("unrecognized index state %q", r.index.State)) + } +} + +func (r *preciseIndexResolver) QueuedAt() *gqlutil.DateTime { + if r.indexResolver != nil { + t := r.indexResolver.QueuedAt() + return &t + } + + return nil +} + +func (r *preciseIndexResolver) UploadedAt() *gqlutil.DateTime { + if r.uploadResolver != nil { + t := r.uploadResolver.UploadedAt() + return &t + } + + return nil +} + +func (r *preciseIndexResolver) IndexingStartedAt() *gqlutil.DateTime { + if r.indexResolver != nil { + return r.indexResolver.StartedAt() + } + + return nil +} + +func (r *preciseIndexResolver) ProcessingStartedAt() *gqlutil.DateTime { + if r.uploadResolver != nil { + return r.uploadResolver.StartedAt() + } + + return nil +} + +func (r *preciseIndexResolver) IndexingFinishedAt() *gqlutil.DateTime { + if r.indexResolver != nil { + return r.indexResolver.FinishedAt() + } + + return nil +} + +func (r *preciseIndexResolver) ProcessingFinishedAt() *gqlutil.DateTime { + if r.uploadResolver != nil { + return r.uploadResolver.FinishedAt() + } + + return nil +} + +func (r *preciseIndexResolver) Steps() resolverstubs.IndexStepsResolver { + if r.indexResolver == nil { + return nil + } + + return r.indexResolver.Steps() +} + +func (r *preciseIndexResolver) Failure() *string { + if r.upload != nil && r.upload.FailureMessage != nil { + return r.upload.FailureMessage + } else if r.index != nil { + return r.index.FailureMessage + } + + return nil +} + +func (r *preciseIndexResolver) PlaceInQueue() *int32 { + if r.index != nil && r.index.Rank != nil { + return toInt32(r.index.Rank) + } + + if r.upload != nil && r.upload.Rank != nil { + return toInt32(r.upload.Rank) + } + + return nil +} + +func (r *preciseIndexResolver) ShouldReindex(ctx context.Context) bool { + if r.index == nil { + return false + } + + return r.index.ShouldReindex +} + +func (r *preciseIndexResolver) IsLatestForRepo() bool { + if r.upload == nil { + return false + } + + return r.upload.VisibleAtTip +} + +func (r *preciseIndexResolver) RetentionPolicyOverview(ctx context.Context, args *resolverstubs.LSIFUploadRetentionPolicyMatchesArgs) (resolverstubs.CodeIntelligenceRetentionPolicyMatchesConnectionResolver, error) { + if r.uploadResolver == nil { + return nil, nil + } + + return r.uploadResolver.RetentionPolicyOverview(ctx, args) +} + +func (r *preciseIndexResolver) AuditLogs(ctx context.Context) (*[]resolverstubs.LSIFUploadsAuditLogsResolver, error) { + if r.uploadResolver == nil { + return nil, nil + } + + return r.uploadResolver.AuditLogs(ctx) +} + +func unmarshalPreciseIndexGQLID(id graphql.ID) (uploadID int64, indexID int64, _ error) { + var payload string + if err := relay.UnmarshalSpec(id, &payload); err != nil { + return 0, 0, err + } + + parts := strings.Split(payload, ":") + if len(parts) == 2 { + id, err := strconv.Atoi(parts[1]) + if err != nil { + return 0, 0, err + } + + if parts[0] == "U" { + return int64(id), 0, nil + } else if parts[0] == "I" { + return 0, int64(id), nil + } + } + + return 0, 0, errors.New("unexpected precise index ID") +} + +type pageInfo struct { + endCursor *string + hasNextPage bool +} + +func (r *pageInfo) EndCursor() *string { return r.endCursor } +func (r *pageInfo) HasNextPage() bool { return r.hasNextPage } diff --git a/internal/codeintel/resolvers/all.go b/internal/codeintel/resolvers/all.go index 2744f673ca58..41fca963ee22 100644 --- a/internal/codeintel/resolvers/all.go +++ b/internal/codeintel/resolvers/all.go @@ -43,6 +43,9 @@ type AutoindexingServiceResolver interface { RepositorySummary(ctx context.Context, id graphql.ID) (CodeIntelRepositorySummaryResolver, error) CodeIntelligenceInferenceScript(ctx context.Context) (string, error) UpdateCodeIntelligenceInferenceScript(ctx context.Context, args *UpdateCodeIntelligenceInferenceScriptArgs) (*EmptyResponse, error) + + PreciseIndexByID(ctx context.Context, id graphql.ID) (PreciseIndexResolver, error) + PreciseIndexes(ctx context.Context, args *PreciseIndexesQueryArgs) (PreciseIndexConnectionResolver, error) } type UploadsServiceResolver interface { @@ -53,6 +56,7 @@ type UploadsServiceResolver interface { DeleteLSIFUpload(ctx context.Context, args *struct{ ID graphql.ID }) (*EmptyResponse, error) DeleteLSIFUploads(ctx context.Context, args *DeleteLSIFUploadsArgs) (*EmptyResponse, error) } + type PoliciesServiceResolver interface { CodeIntelligenceConfigurationPolicies(ctx context.Context, args *CodeIntelligenceConfigurationPoliciesArgs) (CodeIntelligenceConfigurationPolicyConnectionResolver, error) ConfigurationPolicyByID(ctx context.Context, id graphql.ID) (CodeIntelligenceConfigurationPolicyResolver, error) @@ -71,6 +75,16 @@ type CodeIntelRepositorySummaryResolver interface { AvailableIndexers() []InferredAvailableIndexersResolver } +type PreciseIndexesQueryArgs struct { + graphqlutil.ConnectionArgs + After *string + Repo *graphql.ID + Query *string + States *[]string + DependencyOf *string + DependentOf *string +} + type LSIFIndexConnectionResolver interface { Nodes(ctx context.Context) ([]LSIFIndexResolver, error) TotalCount(ctx context.Context) (*int32, error) @@ -83,6 +97,36 @@ type LSIFUploadConnectionResolver interface { PageInfo(ctx context.Context) (PageInfo, error) } +type PreciseIndexConnectionResolver interface { + Nodes(ctx context.Context) ([]PreciseIndexResolver, error) + TotalCount(ctx context.Context) (*int32, error) + PageInfo(ctx context.Context) (PageInfo, error) +} + +type PreciseIndexResolver interface { + ID() graphql.ID + ProjectRoot(ctx context.Context) (GitTreeEntryResolver, error) + InputCommit() string + Tags(ctx context.Context) ([]string, error) + InputRoot() string + InputIndexer() string + Indexer() CodeIntelIndexerResolver + State() string + QueuedAt() *gqlutil.DateTime + UploadedAt() *gqlutil.DateTime + IndexingStartedAt() *gqlutil.DateTime + ProcessingStartedAt() *gqlutil.DateTime + IndexingFinishedAt() *gqlutil.DateTime + ProcessingFinishedAt() *gqlutil.DateTime + Steps() IndexStepsResolver + Failure() *string + PlaceInQueue() *int32 + ShouldReindex(ctx context.Context) bool + IsLatestForRepo() bool + RetentionPolicyOverview(ctx context.Context, args *LSIFUploadRetentionPolicyMatchesArgs) (CodeIntelligenceRetentionPolicyMatchesConnectionResolver, error) + AuditLogs(ctx context.Context) (*[]LSIFUploadsAuditLogsResolver, error) +} + type PageInfo interface { EndCursor() *string HasNextPage() bool From b4a20e90ff256e300b056a1507d9631b8a0d6c95 Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Mon, 30 Jan 2023 16:16:41 +0200 Subject: [PATCH 249/678] nix: include bazel 6.0.0 (#47107) Things seem to be progressing fast, lets get bazel installed for the nix users. One downside is this adds a huge amount of stuff to fetch from the cache (I'm looking at you java headless sdk). Test Plan: nix develop followed by bazel version working. --- flake.lock | 6 +++--- shell.nix | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 65b6adf82dbf..83cfef378f6c 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1673704454, - "narHash": "sha256-5Wdj1MgdOgn3+dMFIBtg+IAYZApjF8JzwLWDPieg0C4=", + "lastModified": 1674971084, + "narHash": "sha256-YxBWj2t75Jun+NJUwr2vvtcfLwvGcazSo+pnNxpt+tU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a83ed85c14fcf242653df6f4b0974b7e1c73c6c6", + "rev": "22c4a7a4796a91c297a7e59078a84ec29515f86e", "type": "github" }, "original": { diff --git a/shell.nix b/shell.nix index e70102c792a0..1dfc12de62e6 100644 --- a/shell.nix +++ b/shell.nix @@ -68,6 +68,9 @@ pkgs.mkShell { rustfmt libiconv clippy + + # The future? + bazel_6 ]; # Startup postgres From e73d0fc5e3e6a2f4467accf82277d73c0d6954af Mon Sep 17 00:00:00 2001 From: Taras Yemets Date: Mon, 30 Jan 2023 17:03:20 +0200 Subject: [PATCH 250/678] fix repo/rev pages spacing (#47078) --- client/web/src/components/Breadcrumbs.tsx | 2 +- client/web/src/repo/RepoHeader.module.scss | 4 ++++ client/web/src/repo/RepoHeader.tsx | 4 ++-- client/web/src/repo/RepoRevisionContainer.tsx | 2 +- .../RepoHeaderActions/RepoHeaderActions.module.scss | 1 - 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/client/web/src/components/Breadcrumbs.tsx b/client/web/src/components/Breadcrumbs.tsx index 241a81990fc7..3f67468328a9 100644 --- a/client/web/src/components/Breadcrumbs.tsx +++ b/client/web/src/components/Breadcrumbs.tsx @@ -155,7 +155,7 @@ export const Breadcrumbs: React.FunctionComponent< React.PropsWithChildren<{ breadcrumbs: BreadcrumbAtDepth[]; location: H.Location; className?: string }> > = ({ breadcrumbs, location, className }) => (
GerritTier 2 (Working on Tier 1)
Perforce Tier 2 (Working on Tier 1)
- + )} @@ -96,7 +91,6 @@ export const RepoSettingsPermissionsPage: React.FunctionComponent< interface ScheduleRepositoryPermissionsSyncActionContainerProps { repo: SettingsAreaRepositoryFields - history: H.History } class ScheduleRepositoryPermissionsSyncActionContainer extends React.PureComponent { @@ -110,7 +104,6 @@ class ScheduleRepositoryPermissionsSyncActionContainer extends React.PureCompone buttonLabel="Schedule now" flashText="Added to queue" run={this.scheduleRepositoryPermissions} - history={this.props.history} /> ) } diff --git a/client/web/src/enterprise/repo/settings/routes.tsx b/client/web/src/enterprise/repo/settings/routes.tsx index d5548e4bdc92..3041c9284b6e 100644 --- a/client/web/src/enterprise/repo/settings/routes.tsx +++ b/client/web/src/enterprise/repo/settings/routes.tsx @@ -1,7 +1,6 @@ -import { Redirect, RouteComponentProps } from 'react-router' - import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' +import { RedirectRoute } from '../../../components/RedirectRoute' import { RepoSettingsAreaRoute } from '../../../repo/settings/RepoSettingsArea' import { repoSettingsAreaRoutes } from '../../../repo/settings/routes' @@ -16,23 +15,16 @@ export const enterpriseRepoSettingsAreaRoutes: readonly RepoSettingsAreaRoute[] ...repoSettingsAreaRoutes, { path: '/permissions', - exact: true, render: props => , }, // Legacy routes { path: '/code-intelligence/lsif-uploads/:id', - exact: true, - render: ({ - match: { - params: { id }, - }, - }: RouteComponentProps<{ id: string }>) => , + render: () => `../uploads/${params.id}`} />, }, { - path: '/code-intelligence', - exact: false, - render: props => , + path: '/code-intelligence/*', + render: () => location.pathname.replace('/settings/', '/')} />, }, ] diff --git a/client/web/src/enterprise/searchContexts/SearchContextRepositoriesFormArea.tsx b/client/web/src/enterprise/searchContexts/SearchContextRepositoriesFormArea.tsx index 40f94602a0ee..2c5f511131db 100644 --- a/client/web/src/enterprise/searchContexts/SearchContextRepositoriesFormArea.tsx +++ b/client/web/src/enterprise/searchContexts/SearchContextRepositoriesFormArea.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useState } from 'react' import { mdiCheck } from '@mdi/js' import * as jsonc from 'jsonc-parser' -import { useHistory } from 'react-router' import { Observable } from 'rxjs' import { delay, mergeMap, startWith, tap } from 'rxjs/operators' @@ -129,7 +128,6 @@ export const SearchContextRepositoriesFormArea: React.FunctionComponent< [] ) - const history = useHistory() return (
diff --git a/client/web/src/enterprise/user/settings/auth/UserSettingsPermissionsPage.tsx b/client/web/src/enterprise/user/settings/auth/UserSettingsPermissionsPage.tsx index 78bc2b0cd7c1..d6d392253f92 100644 --- a/client/web/src/enterprise/user/settings/auth/UserSettingsPermissionsPage.tsx +++ b/client/web/src/enterprise/user/settings/auth/UserSettingsPermissionsPage.tsx @@ -103,7 +103,6 @@ class ScheduleUserPermissionsSyncActionContainer extends React.PureComponent ) diff --git a/client/web/src/extensions/components/ActionItemsBar.story.tsx b/client/web/src/extensions/components/ActionItemsBar.story.tsx index 3bea59bc12bb..f57a81f47a4b 100644 --- a/client/web/src/extensions/components/ActionItemsBar.story.tsx +++ b/client/web/src/extensions/components/ActionItemsBar.story.tsx @@ -1,7 +1,6 @@ import React from 'react' import { DecoratorFn, Meta } from '@storybook/react' -import * as H from 'history' import { EMPTY, noop, of } from 'rxjs' import { ContributableMenu, Contributions, Evaluated } from '@sourcegraph/client-api' @@ -16,16 +15,6 @@ import { SourcegraphContext } from '../../jscontext' import { ActionItemsBar, useWebActionItems } from './ActionItemsBar' -import webStyles from '../../SourcegraphWebApp.scss' - -const LOCATION: H.Location = { - search: '', - hash: '', - pathname: '/github.com/sourcegraph/sourcegraph/-/blob/client/browser/src/browser-extension/ThemeWrapper.tsx', - key: 'oq2z4k', - state: undefined, -} - const mockIconURL = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE2LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHdpZHRoPSI5N3B4IiBoZWlnaHQ9Ijk3cHgiIHZpZXdCb3g9IjAgMCA5NyA5NyIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTcgOTciIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8Zz4KCTxwYXRoIGZpbGw9IiNGMDUxMzMiIGQ9Ik05Mi43MSw0NC40MDhMNTIuNTkxLDQuMjkxYy0yLjMxLTIuMzExLTYuMDU3LTIuMzExLTguMzY5LDBsLTguMzMsOC4zMzJMNDYuNDU5LDIzLjE5CgkJYzIuNDU2LTAuODMsNS4yNzItMC4yNzMsNy4yMjksMS42ODVjMS45NjksMS45NywyLjUyMSw0LjgxLDEuNjcsNy4yNzVsMTAuMTg2LDEwLjE4NWMyLjQ2NS0wLjg1LDUuMzA3LTAuMyw3LjI3NSwxLjY3MQoJCWMyLjc1LDIuNzUsMi43NSw3LjIwNiwwLDkuOTU4Yy0yLjc1MiwyLjc1MS03LjIwOCwyLjc1MS05Ljk2MSwwYy0yLjA2OC0yLjA3LTIuNTgtNS4xMS0xLjUzMS03LjY1OGwtOS41LTkuNDk5djI0Ljk5NwoJCWMwLjY3LDAuMzMyLDEuMzAzLDAuNzc0LDEuODYxLDEuMzMyYzIuNzUsMi43NSwyLjc1LDcuMjA2LDAsOS45NTljLTIuNzUsMi43NDktNy4yMDksMi43NDktOS45NTcsMGMtMi43NS0yLjc1NC0yLjc1LTcuMjEsMC05Ljk1OQoJCWMwLjY4LTAuNjc5LDEuNDY3LTEuMTkzLDIuMzA3LTEuNTM3VjM2LjM2OWMtMC44NC0wLjM0NC0xLjYyNS0wLjg1My0yLjMwNy0xLjUzN2MtMi4wODMtMi4wODItMi41ODQtNS4xNC0xLjUxNi03LjY5OAoJCUwzMS43OTgsMTYuNzE1TDQuMjg4LDQ0LjIyMmMtMi4zMTEsMi4zMTMtMi4zMTEsNi4wNiwwLDguMzcxbDQwLjEyMSw0MC4xMThjMi4zMSwyLjMxMSw2LjA1NiwyLjMxMSw4LjM2OSwwTDkyLjcxLDUyLjc3OQoJCUM5NS4wMjEsNTAuNDY4LDk1LjAyMSw0Ni43MTksOTIuNzEsNDQuNDA4eiIvPgo8L2c+Cjwvc3ZnPgo=' @@ -70,16 +59,17 @@ const mockExtensionsController = { } const decorator: DecoratorFn = story => ( - <> - - - {() => ( - -
{story()}
-
- )} -
- + + {() => ( + +
{story()}
+
+ )} +
) const config: Meta = { @@ -102,7 +92,6 @@ export const Default: React.FunctionComponent> return ( { isOpen: boolean | undefined; barReference: React.RefCallback } - location: H.Location source?: 'compare' | 'commit' | 'blob' } @@ -193,7 +192,9 @@ const actionItemClassName = classNames( * Renders extensions (both migrated to the core workflow and legacy) actions items in the sidebar. */ export const ActionItemsBar = React.memo(function ActionItemsBar(props) { - const { extensionsController, location, source } = props + const { extensionsController, source } = props + + const location = useLocation() const { isOpen, barReference } = props.useActionItemsBar() const { repoName, rawRevision, filePath, commitRange, position, range } = parseBrowserRepoURL( location.pathname + location.search + location.hash @@ -225,7 +226,7 @@ export const ActionItemsBar = React.memo(function ActionIte return (
{/* To be clear to users that this isn't an error reported by extensions about e.g. the code they're viewing. */} - Component error: {error.message}}> + Component error: {error.message}}> {canScrollNegative && (
@@ -747,7 +761,7 @@ export const Blob: React.FunctionComponent> = * clicks the same line multiple times. */ export function updateBrowserHistoryIfChanged( - history: H.History, + navigate: NavigateFunction, location: H.Location, newSearchParameters: URLSearchParams, /** If set to true replace the current history entry instead of adding a new one. */ @@ -769,11 +783,8 @@ export function updateBrowserHistoryIfChanged( ...location, search: formatSearchParameters(newSearchParameters), } - if (replace) { - history.replace(entry) - } else { - history.push(entry) - } + + navigate(entry, { replace }) } } diff --git a/client/web/src/repo/blob/BlobPage.tsx b/client/web/src/repo/blob/BlobPage.tsx index d74a88d6d8b9..47fdede75bee 100644 --- a/client/web/src/repo/blob/BlobPage.tsx +++ b/client/web/src/repo/blob/BlobPage.tsx @@ -1,11 +1,10 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react' +import React, { useState, useEffect, useCallback, useMemo, FC } from 'react' import classNames from 'classnames' -import * as H from 'history' import AlertCircleIcon from 'mdi-react/AlertCircleIcon' import FileAlertIcon from 'mdi-react/FileAlertIcon' import MapSearchIcon from 'mdi-react/MapSearchIcon' -import { Redirect } from 'react-router' +import { Navigate, useLocation, useNavigate } from 'react-router-dom-v5-compat' import { Observable, of } from 'rxjs' import { catchError, map, mapTo, startWith, switchMap } from 'rxjs/operators' import { Optional } from 'utility-types' @@ -52,6 +51,7 @@ import { copyNotebook, CopyNotebookProps } from '../../notebooks/notebook' import { OpenInEditorActionItem } from '../../open-in-editor/OpenInEditorActionItem' import { SearchStreamingProps } from '../../search' import { useNotepad, useExperimentalFeatures } from '../../stores' +import { globalHistory } from '../../util/globalHistory' import { basename } from '../../util/path' import { toTreeURL } from '../../util/url' import { serviceKindDisplayNameAndIcon } from '../actions/GoToCodeHostAction' @@ -96,8 +96,6 @@ interface BlobPageProps Pick, Pick, NotebookProps { - location: H.Location - history: H.History authenticatedUser: AuthenticatedUser | null globbing: boolean isMacPlatform: boolean @@ -118,10 +116,13 @@ interface BlobPageInfo extends Optional { aborted: boolean } -export const BlobPage: React.FunctionComponent> = ({ className, ...props }) => { +export const BlobPage: FC = ({ className, ...props }) => { + const location = useLocation() + const navigate = useNavigate() + const { span } = useCurrentSpan() const [wrapCode, setWrapCode] = useState(ToggleLineWrap.getValue()) - let renderMode = getModeFromURL(props.location) + let renderMode = getModeFromURL(location) const { repoID, repoName, revision, commitID, filePath, isLightTheme, useBreadcrumb, mode } = props const enableCodeMirror = useExperimentalFeatures(features => features.enableCodeMirrorFileView ?? false) const experimentalCodeNavigation = useExperimentalFeatures(features => features.codeNavigation) @@ -133,8 +134,8 @@ export const BlobPage: React.FunctionComponent parseQueryAndHash(props.location.search, props.location.hash), - [props.location.search, props.location.hash] + () => parseQueryAndHash(location.search, location.hash), + [location.search, location.hash] ) // Log view event whenever a new Blob, or a Blob with a different render mode, is visited. @@ -401,12 +402,7 @@ export const BlobPage: React.FunctionComponent {context => ( - + )} {renderMode === 'code' && ( @@ -446,7 +442,7 @@ export const BlobPage: React.FunctionComponent + return } return ( <> @@ -549,7 +545,7 @@ export const BlobPage: React.FunctionComponent )} @@ -575,6 +571,9 @@ export const BlobPage: React.FunctionComponent = props => { ariaLabel, role, extensionsController, - location, - history, isBlameVisible, blameHunks, enableLinkDrivenCodeNavigation, @@ -115,6 +114,8 @@ export const Blob: React.FunctionComponent = props => { 'data-testid': dataTestId, } = props + const location = useLocation() + const navigate = useNavigate() const [useFileSearch, setUseFileSearch] = useLocalStorage('blob.overrideBrowserFindOnPage', true) const [container, setContainer] = useState(null) @@ -139,8 +140,8 @@ export const Blob: React.FunctionComponent = props => { // Keep history and location in a ref so that we can use the latest value in // the onSelection callback without having to recreate it and having to // reconfigure the editor extensions - const historyRef = useRef(history) - historyRef.current = history + const navigateRef = useRef(navigate) + navigateRef.current = navigate const locationRef = useRef(location) locationRef.current = location @@ -166,13 +167,13 @@ export const Blob: React.FunctionComponent = props => { const newSearchParameters = addLineRangeQueryParameter(parameters, query) if (customHistoryAction) { customHistoryAction( - historyRef.current.createHref({ + createPath({ ...locationRef.current, search: formatSearchParameters(newSearchParameters), }) ) } else { - updateBrowserHistoryIfChanged(historyRef.current, locationRef.current, newSearchParameters) + updateBrowserHistoryIfChanged(navigateRef.current, locationRef.current, newSearchParameters) } }, [customHistoryAction] @@ -193,7 +194,9 @@ export const Blob: React.FunctionComponent = props => { enableSelectionDrivenCodeNavigation, }), enableSelectionDrivenCodeNavigation ? tokenSelectionExtension() : [], - enableLinkDrivenCodeNavigation ? tokensAsLinks({ history, blobInfo, preloadGoToDefinition }) : [], + enableLinkDrivenCodeNavigation + ? tokensAsLinks({ navigate: navigateRef.current, blobInfo, preloadGoToDefinition }) + : [], syntaxHighlight.of(blobInfo), pin.init(() => (hasPin ? position : null)), extensionsController !== null && !navigateToLineOnAnyClick @@ -251,7 +254,7 @@ export const Blob: React.FunctionComponent = props => { // Sync editor selection/focus with the URL so that triggering // `history.goBack/goForward()` works similar to the "Go back" // command in VS Code. - const { selection } = selectionFromLocation(editor, historyRef.current.location) + const { selection } = selectionFromLocation(editor, locationRef.current) if (selection) { const position = positionAtCmPosition(editor, selection.from) const occurrence = occurrenceAtPosition(editor.state, position) diff --git a/client/web/src/repo/blob/actions/ToggleHistoryPanel.tsx b/client/web/src/repo/blob/actions/ToggleHistoryPanel.tsx index 9b044acafb50..5ef435ff4ea3 100644 --- a/client/web/src/repo/blob/actions/ToggleHistoryPanel.tsx +++ b/client/web/src/repo/blob/actions/ToggleHistoryPanel.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { mdiHistory } from '@mdi/js' -import * as H from 'history' +import { Location, NavigateFunction, To } from 'react-router-dom-v5-compat' import { fromEvent, Subject, Subscription } from 'rxjs' import { filter } from 'rxjs/operators' @@ -25,8 +25,8 @@ import { BlobPanelTabID } from '../panel/BlobPanel' */ export class ToggleHistoryPanel extends React.PureComponent< { - location: H.Location - history: H.History + location: Location + navigate: NavigateFunction } & RepoHeaderContext > { private toggles = new Subject() @@ -35,7 +35,7 @@ export class ToggleHistoryPanel extends React.PureComponent< /** * Reports the current visibility (derived from the location). */ - public static isVisible(location: H.Location): boolean { + public static isVisible(location: Location): boolean { return parseQueryAndHash(location.search, location.hash).viewState === 'history' } @@ -43,7 +43,7 @@ export class ToggleHistoryPanel extends React.PureComponent< * Returns the location object (that can be passed to H.History's push/replace methods) that sets visibility to * the given value. */ - private static locationWithVisibility(location: H.Location, visible: boolean): H.LocationDescriptorObject { + private static locationWithVisibility(location: Location, visible: boolean): To { const parsedQuery = parseQueryAndHash(location.search, location.hash) if (visible) { parsedQuery.viewState = 'history' // defaults to last-viewed tab, or first tab @@ -51,6 +51,7 @@ export class ToggleHistoryPanel extends React.PureComponent< delete parsedQuery.viewState } const lineRangeQueryParameter = toPositionOrRangeQueryParameter({ range: lprToRange(parsedQuery) }) + return { search: formatSearchParameters( addLineRangeQueryParameter(new URLSearchParams(location.search), lineRangeQueryParameter) @@ -64,7 +65,7 @@ export class ToggleHistoryPanel extends React.PureComponent< this.toggles.subscribe(() => { const visible = ToggleHistoryPanel.isVisible(this.props.location) eventLogger.log(visible ? 'HideHistoryPanel' : 'ShowHistoryPanel') - this.props.history.push(ToggleHistoryPanel.locationWithVisibility(this.props.location, !visible)) + this.props.navigate(ToggleHistoryPanel.locationWithVisibility(this.props.location, !visible)) }) ) diff --git a/client/web/src/repo/blob/codemirror/hovercard.tsx b/client/web/src/repo/blob/codemirror/hovercard.tsx index 57a4981ec295..a7c4d18bcc51 100644 --- a/client/web/src/repo/blob/codemirror/hovercard.tsx +++ b/client/web/src/repo/blob/codemirror/hovercard.tsx @@ -75,7 +75,7 @@ import { } from '../../../components/WebHoverOverlay' import { BlobProps, updateBrowserHistoryIfChanged } from '../Blob' -import { Container } from './react-interop' +import { CodeMirrorContainer } from './react-interop' import { preciseWordAtCoords, offsetToUIPosition, @@ -620,7 +620,7 @@ export class HovercardView implements TooltipView { } root.render( - repositionTooltips(this.view)} history={props.history}> + repositionTooltips(this.view)} history={props.history}>
{ @@ -668,7 +668,7 @@ export class HovercardView implements TooltipView { search.set('popover', 'pinned') updateBrowserHistoryIfChanged( - props.history, + props.navigate, props.location, // It may seem strange to set start and end to the same value, but that what's the old blob view is doing as well addLineRangeQueryParameter( @@ -687,7 +687,7 @@ export class HovercardView implements TooltipView { hoverOverlayContainerClassName="position-relative" />
-
+ ) } } diff --git a/client/web/src/repo/blob/codemirror/react-interop.tsx b/client/web/src/repo/blob/codemirror/react-interop.tsx index 14145ed5c721..960d3f9f43d7 100644 --- a/client/web/src/repo/blob/codemirror/react-interop.tsx +++ b/client/web/src/repo/blob/codemirror/react-interop.tsx @@ -6,13 +6,23 @@ import { CompatRouter } from 'react-router-dom-v5-compat' import { WildcardThemeContext } from '@sourcegraph/wildcard' +import { globalHistory } from '../../../util/globalHistory' + +interface CodeMirrorContainerProps { + history: History + onMount?: () => void + onRender?: () => void +} + /** * Creates the necessary context for React components to be rendered inside * CodeMirror. */ -export const Container: React.FunctionComponent< - React.PropsWithChildren<{ history: History; onMount?: () => void; onRender?: () => void }> -> = ({ history, onMount, onRender, children }) => { +export const CodeMirrorContainer: React.FunctionComponent> = ({ + onMount, + onRender, + children, +}) => { useEffect(() => onRender?.()) // This should only be called once when the component is mounted // eslint-disable-next-line react-hooks/exhaustive-deps @@ -20,7 +30,7 @@ export const Container: React.FunctionComponent< return ( - + {children} diff --git a/client/web/src/repo/blob/codemirror/search.tsx b/client/web/src/repo/blob/codemirror/search.tsx index 2e9346efbf74..b4fa02a5c150 100644 --- a/client/web/src/repo/blob/codemirror/search.tsx +++ b/client/web/src/repo/blob/codemirror/search.tsx @@ -30,7 +30,7 @@ import { Button, Icon, Input, Label, Text, Tooltip } from '@sourcegraph/wildcard import { Keybindings } from '../../../components/KeyboardShortcutsHelp/KeyboardShortcutsHelp' import { createElement } from '../../../util/dom' -import { Container } from './react-interop' +import { CodeMirrorContainer } from './react-interop' import { blobPropsFacet } from '.' @@ -152,7 +152,7 @@ class SearchPanel implements Panel { } this.root.render( - { this.input?.focus() @@ -233,7 +233,7 @@ class SearchPanel implements Panel { {searchKeybindingTooltip}
- + ) } diff --git a/client/web/src/repo/blob/codemirror/token-selection/selections.ts b/client/web/src/repo/blob/codemirror/token-selection/selections.ts index b240093aa4fe..2faf30f08125 100644 --- a/client/web/src/repo/blob/codemirror/token-selection/selections.ts +++ b/client/web/src/repo/blob/codemirror/token-selection/selections.ts @@ -22,6 +22,7 @@ export const syncOccurrenceWithURL: Extension = ViewPlugin.fromClass( class implements PluginValue { private onDestroy: H.UnregisterCallback constructor(public view: EditorView) { + // TODO(valery): RR6 const history = view.state.facet(blobPropsFacet).history this.onDestroy = history.listen(location => this.onLocation(location)) } diff --git a/client/web/src/repo/blob/codemirror/tokens-as-links.ts b/client/web/src/repo/blob/codemirror/tokens-as-links.ts index 38eced2face6..0f911f78afa7 100644 --- a/client/web/src/repo/blob/codemirror/tokens-as-links.ts +++ b/client/web/src/repo/blob/codemirror/tokens-as-links.ts @@ -1,6 +1,6 @@ import { Extension, RangeSetBuilder, StateEffect, StateField } from '@codemirror/state' import { Decoration, DecorationSet, EditorView, PluginValue, ViewPlugin, ViewUpdate } from '@codemirror/view' -import { History } from 'history' +import { NavigateFunction } from 'react-router-dom-v5-compat' import { Observable, Subject, Subscription } from 'rxjs' import { concatMap, debounceTime, map } from 'rxjs/operators' import { DeepNonNullable } from 'utility-types' @@ -240,12 +240,12 @@ const tokenLinks = StateField.define({ }) interface TokensAsLinksConfiguration { - history: History + navigate: NavigateFunction blobInfo: BlobInfo preloadGoToDefinition: boolean } -export const tokensAsLinks = ({ history, blobInfo, preloadGoToDefinition }: TokensAsLinksConfiguration): Extension => { +export const tokensAsLinks = ({ navigate, blobInfo, preloadGoToDefinition }: TokensAsLinksConfiguration): Extension => { /** * Prefer precise code intelligence ranges, fall back to making certain Occurrences interactive. */ @@ -276,7 +276,7 @@ export const tokensAsLinks = ({ history, blobInfo, preloadGoToDefinition }: Toke // If it is, push the link to the history stack. if (target.matches('[data-token-link]')) { event.preventDefault() - history.push(target.getAttribute('href')!) + navigate(target.getAttribute('href')!) } }, }), diff --git a/client/web/src/repo/blob/panel/BlobPanel.tsx b/client/web/src/repo/blob/panel/BlobPanel.tsx index 542d8858ac8f..b1f54b8d53b0 100644 --- a/client/web/src/repo/blob/panel/BlobPanel.tsx +++ b/client/web/src/repo/blob/panel/BlobPanel.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useCallback, useMemo } from 'react' -import * as H from 'history' +import { useLocation } from 'react-router-dom-v5-compat' import { EMPTY, from, Observable, ReplaySubject, Subscription } from 'rxjs' import { distinct, map, mapTo, switchMap, tap } from 'rxjs/operators' @@ -38,8 +38,6 @@ interface Props PlatformContextProps, Pick, TelemetryProps { - location: H.Location - history: H.History repoID: Scalars['ID'] repoName: string commitID: string @@ -58,9 +56,6 @@ interface PanelSubject extends AbsoluteRepoFile, ModeSpec, Partial { const parsedHash = parseQueryAndHash(location.search, location.hash) @@ -162,10 +156,8 @@ function useBlobPanelViews({ ? { line: parsedHash.line, character: parsedHash.character || 0 } : undefined, hash: location.hash, - history, - location, } - }, [commitID, filePath, history, location, mode, repoID, repoName, revision]) + }, [commitID, filePath, location, mode, repoID, repoName, revision]) const panelSubjectChanges = useMemo(() => new ReplaySubject(1), []) useEffect(() => { @@ -178,7 +170,7 @@ function useBlobPanelViews({ { id: 'history', provider: panelSubjectChanges.pipe( - map(({ repoID, revision, filePath, history, location }) => ({ + map(({ repoID, revision, filePath }) => ({ title: 'History', content: '', priority: 150, @@ -190,8 +182,6 @@ function useBlobPanelViews({ repoID={repoID} revision={revision} filePath={filePath} - history={history} - location={location} preferAbsoluteTimestamps={preferAbsoluteTimestamps} defaultPageSize={defaultPageSize} /> diff --git a/client/web/src/repo/branches/RepositoryBranchesAllPage.tsx b/client/web/src/repo/branches/RepositoryBranchesAllPage.tsx index d070ca490309..b0d5d49d2287 100644 --- a/client/web/src/repo/branches/RepositoryBranchesAllPage.tsx +++ b/client/web/src/repo/branches/RepositoryBranchesAllPage.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { FC, useCallback, useEffect } from 'react' import { Observable } from 'rxjs' @@ -13,32 +13,35 @@ import { RepositoryBranchesAreaPageProps } from './RepositoryBranchesArea' interface Props extends RepositoryBranchesAreaPageProps {} /** A page that shows all of a repository's branches. */ -export class RepositoryBranchesAllPage extends React.PureComponent { - public componentDidMount(): void { +export const RepositoryBranchesAllPage: FC = props => { + const { repo } = props + + useEffect(() => { eventLogger.logViewEvent('RepositoryBranchesAll') - } - - public render(): JSX.Element | null { - return ( -
- - - inputClassName="w-100" - listClassName="list-group list-group-flush" - noun="branch" - pluralNoun="branches" - queryConnection={this.queryBranches} - nodeComponent={GitReferenceNode} - ariaLabelFunction={(branchDisplayName: string) => - `View this repository using ${branchDisplayName} as the selected revision` - } - defaultFirst={20} - autoFocus={true} - /> -
- ) - } - - private queryBranches = (args: FilteredConnectionQueryArguments): Observable => - queryGitReferences({ ...args, repo: this.props.repo.id, type: GitRefType.GIT_BRANCH }) + }, []) + + const queryBranches = useCallback( + (args: FilteredConnectionQueryArguments): Observable => + queryGitReferences({ ...args, repo: repo.id, type: GitRefType.GIT_BRANCH }), + [repo.id] + ) + + return ( +
+ + + inputClassName="w-100" + listClassName="list-group list-group-flush" + noun="branch" + pluralNoun="branches" + queryConnection={queryBranches} + nodeComponent={GitReferenceNode} + ariaLabelFunction={(branchDisplayName: string) => + `View this repository using ${branchDisplayName} as the selected revision` + } + defaultFirst={20} + autoFocus={true} + /> +
+ ) } diff --git a/client/web/src/repo/branches/RepositoryBranchesArea.tsx b/client/web/src/repo/branches/RepositoryBranchesArea.tsx index 45bfc8894a4d..91328a92b59f 100644 --- a/client/web/src/repo/branches/RepositoryBranchesArea.tsx +++ b/client/web/src/repo/branches/RepositoryBranchesArea.tsx @@ -1,25 +1,16 @@ -import React, { useMemo } from 'react' +import { FC } from 'react' -import MapSearchIcon from 'mdi-react/MapSearchIcon' -import { Route, RouteComponentProps, Switch } from 'react-router' +import { Routes, Route } from 'react-router-dom-v5-compat' import { BreadcrumbSetters } from '../../components/Breadcrumbs' -import { HeroPage } from '../../components/HeroPage' +import { NotFoundPage } from '../../components/HeroPage' import { RepositoryFields } from '../../graphql-operations' import { RepositoryBranchesAllPage } from './RepositoryBranchesAllPage' import { RepositoryBranchesNavbar } from './RepositoryBranchesNavbar' import { RepositoryBranchesOverviewPage } from './RepositoryBranchesOverviewPage' -const NotFoundPage: React.FunctionComponent> = () => ( - -) - -interface Props extends RouteComponentProps<{}>, BreadcrumbSetters { +interface Props extends BreadcrumbSetters { repo: RepositoryFields } @@ -33,42 +24,24 @@ export interface RepositoryBranchesAreaPageProps { repo: RepositoryFields } +const BREADCRUMB = { key: 'branches', element: 'Branches' } + /** * Renders pages related to repository branches. */ -export const RepositoryBranchesArea: React.FunctionComponent> = ({ - useBreadcrumb, - repo, - match, -}) => { - const transferProps: { repo: RepositoryFields } = { - repo, - } +export const RepositoryBranchesArea: FC = props => { + const { useBreadcrumb, repo } = props - useBreadcrumb(useMemo(() => ({ key: 'branches', element: 'Branches' }), [])) + useBreadcrumb(BREADCRUMB) return (
- - ( - - )} - /> - ( - - )} - /> - - + + } /> + } /> + } /> +
) } diff --git a/client/web/src/repo/branches/RepositoryBranchesOverviewPage.tsx b/client/web/src/repo/branches/RepositoryBranchesOverviewPage.tsx index c2e63f42b330..9b9959852dfe 100644 --- a/client/web/src/repo/branches/RepositoryBranchesOverviewPage.tsx +++ b/client/web/src/repo/branches/RepositoryBranchesOverviewPage.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { mdiChevronRight } from '@mdi/js' -import { RouteComponentProps } from 'react-router-dom' import { Observable, Subject, Subscription } from 'rxjs' import { catchError, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators' @@ -78,7 +77,7 @@ export const queryGitBranches = memoizeObservable( args => `${args.repo}:${args.first}` ) -interface Props extends RepositoryBranchesAreaPageProps, RouteComponentProps<{}> {} +interface Props extends RepositoryBranchesAreaPageProps {} interface State { /** The page content, undefined while loading, or an error. */ diff --git a/client/web/src/repo/commit/RepositoryCommitPage.tsx b/client/web/src/repo/commit/RepositoryCommitPage.tsx index 4a3aef42e0a9..69657cd7a3e4 100644 --- a/client/web/src/repo/commit/RepositoryCommitPage.tsx +++ b/client/web/src/repo/commit/RepositoryCommitPage.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useCallback, useEffect } from 'react' import classNames from 'classnames' -import { RouteComponentProps } from 'react-router' +import { useParams } from 'react-router-dom-v5-compat' import { Observable } from 'rxjs' import { gql, useQuery } from '@sourcegraph/http-client' @@ -44,12 +44,7 @@ const COMMIT_QUERY = gql` ${gitCommitFragment} ` -interface RepositoryCommitPageProps - extends RouteComponentProps<{ revspec: string }>, - TelemetryProps, - PlatformContextProps, - ThemeProps, - SettingsCascadeProps { +interface RepositoryCommitPageProps extends TelemetryProps, PlatformContextProps, ThemeProps, SettingsCascadeProps { repo: RepositoryFields onDidUpdateExternalLinks: (externalLinks: ExternalLinkFields[] | undefined) => void } @@ -58,10 +53,16 @@ export type { DiffMode } from '@sourcegraph/shared/src/settings/temporary/diffMo /** Displays a commit. */ export const RepositoryCommitPage: React.FunctionComponent = props => { + const params = useParams<{ revspec: string }>() + + if (!params.revspec) { + throw new Error('Missing `revspec` param!') + } + const { data, error, loading } = useQuery(COMMIT_QUERY, { variables: { repo: props.repo.id, - revspec: props.match.params.revspec, + revspec: params.revspec, }, }) @@ -102,7 +103,7 @@ export const RepositoryCommitPage: React.FunctionComponent - + {loading ? ( ) : error || !commit ? ( diff --git a/client/web/src/repo/commits/GitCommitNode.module.scss b/client/web/src/repo/commits/GitCommitNode.module.scss index 60074ef8d4d7..e9d8ccf86d72 100644 --- a/client/web/src/repo/commits/GitCommitNode.module.scss +++ b/client/web/src/repo/commits/GitCommitNode.module.scss @@ -34,16 +34,6 @@ align-items: center; } - &--compact .sidebar { - flex: none; - padding-right: 1.5rem; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - display: flex; - align-items: center; - } - &--compact .message { flex: 1; min-width: 0; diff --git a/client/web/src/repo/commits/GitCommitNode.tsx b/client/web/src/repo/commits/GitCommitNode.tsx index 5fc83193bb34..811e8fc5427a 100644 --- a/client/web/src/repo/commits/GitCommitNode.tsx +++ b/client/web/src/repo/commits/GitCommitNode.tsx @@ -26,9 +26,6 @@ export interface GitCommitNodeProps { /** Display in a single line (more compactly). */ compact?: boolean - /** Display in sidebar mode. */ - sidebar?: boolean - /** Expand the commit message body. */ expandCommitMessageBody?: boolean @@ -68,7 +65,6 @@ export const GitCommitNode: React.FunctionComponent ) - if (sidebar) { - return ( - -
- {bylineElement} - - - - {oidElement} -
-
- ) - } - return ( , - TelemetryProps { +export interface RepositoryCommitsPageProps extends RevisionSpec, BreadcrumbSetters, TelemetryProps { repo: RepositoryFields - - history: H.History - location: H.Location } // A page that shows a repository's commits at the current revision. -export const RepositoryCommitsPage: React.FunctionComponent> = ({ - useBreadcrumb, - ...props -}) => { - const repo = props.repo - const filePath = props.match.params.filePath +export const RepositoryCommitsPage: FC = props => { + const { useBreadcrumb, repo } = props + + const location = useLocation() + const { filePath = '' } = parseBrowserRepoURL(location.pathname) const { connection, error, loading, hasNextPage, fetchMore } = useShowMorePagination< RepositoryGitCommitsResult, diff --git a/client/web/src/repo/compare/RepositoryCompareArea.tsx b/client/web/src/repo/compare/RepositoryCompareArea.tsx index 4f538ca3174c..6676181fa173 100644 --- a/client/web/src/repo/compare/RepositoryCompareArea.tsx +++ b/client/web/src/repo/compare/RepositoryCompareArea.tsx @@ -1,15 +1,12 @@ -import React, { useMemo } from 'react' +import { FC } from 'react' import classNames from 'classnames' -import * as H from 'history' -import MapSearchIcon from 'mdi-react/MapSearchIcon' -import { Route, RouteComponentProps, Switch } from 'react-router' +import { useLocation, useNavigate, useParams } from 'react-router-dom-v5-compat' import { ThemeProps } from '@sourcegraph/shared/src/theme' import { Alert, LoadingSpinner } from '@sourcegraph/wildcard' import { BreadcrumbSetters } from '../../components/Breadcrumbs' -import { HeroPage } from '../../components/HeroPage' import { RepositoryFields, Scalars } from '../../graphql-operations' import { RepositoryCompareHeader } from './RepositoryCompareHeader' @@ -17,17 +14,8 @@ import { RepositoryCompareOverviewPage } from './RepositoryCompareOverviewPage' import styles from './RepositoryCompareArea.module.scss' -const NotFoundPage: React.FunctionComponent> = () => ( - -) - -interface RepositoryCompareAreaProps extends RouteComponentProps<{ spec: string }>, ThemeProps, BreadcrumbSetters { +interface RepositoryCompareAreaProps extends ThemeProps, BreadcrumbSetters { repo?: RepositoryFields - history: H.History } /** @@ -42,26 +30,25 @@ export interface RepositoryCompareAreaPageProps { /** The head of the comparison. */ head: { repoName: string; repoID: Scalars['ID']; revision?: string | null } - - /** The URL route prefix for the comparison. */ - routePrefix: string } +const BREADCRUMB = { key: 'compare', element: <>Compare } + /** * Renders pages related to a repository comparison. */ -export const RepositoryCompareArea: React.FunctionComponent = ({ - repo, - useBreadcrumb, - match, - location, - isLightTheme, -}) => { - useBreadcrumb(useMemo(() => ({ key: 'compare', element: <>Compare }), [])) +export const RepositoryCompareArea: FC = props => { + const { repo, useBreadcrumb, isLightTheme } = props + + const { '*': splat } = useParams<{ '*': string }>() + const location = useLocation() + const navigate = useNavigate() + + useBreadcrumb(BREADCRUMB) let spec: { base: string | null; head: string | null } | null | undefined - if (match.params.spec) { - spec = parseComparisonSpec(decodeURIComponent(match.params.spec)) + if (splat) { + spec = parseComparisonSpec(splat) } // Parse out the optional filePath search param, which is used to show only a single file in the compare view @@ -76,30 +63,21 @@ export const RepositoryCompareArea: React.FunctionComponent {spec === null ? ( Invalid comparison specifier ) : ( - - ( - - )} - /> - - + )} ) @@ -109,9 +87,11 @@ function parseComparisonSpec(spec: string): { base: string | null; head: string if (!spec.includes('...')) { return null } - const parts = spec.split('...', 2).map(decodeURIComponent) + + const [base, head] = spec.split('...', 2) + return { - base: parts[0] || null, - head: parts[1] || null, + base, + head, } } diff --git a/client/web/src/repo/compare/RepositoryCompareCommitsPage.tsx b/client/web/src/repo/compare/RepositoryCompareCommitsPage.tsx index 8b6a8bda2c09..7500dfd723da 100644 --- a/client/web/src/repo/compare/RepositoryCompareCommitsPage.tsx +++ b/client/web/src/repo/compare/RepositoryCompareCommitsPage.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { RouteComponentProps } from 'react-router' +import { NavigateFunction, Location } from 'react-router-dom-v5-compat' import { Observable, Subject, Subscription } from 'rxjs' import { distinctUntilChanged, map, startWith } from 'rxjs/operators' @@ -60,9 +60,12 @@ function queryRepositoryComparisonCommits(args: { ) } -interface Props extends RepositoryCompareAreaPageProps, RouteComponentProps<{}> { +interface Props extends RepositoryCompareAreaPageProps { /** An optional path of a specific file being compared */ path: string | null + + location: Location + navigate: NavigateFunction } /** A page with a list of commits in the comparison. */ diff --git a/client/web/src/repo/compare/RepositoryCompareHeader.tsx b/client/web/src/repo/compare/RepositoryCompareHeader.tsx index f17204dcd4db..6b36311ec731 100644 --- a/client/web/src/repo/compare/RepositoryCompareHeader.tsx +++ b/client/web/src/repo/compare/RepositoryCompareHeader.tsx @@ -20,7 +20,7 @@ export const RepositoryCompareHeader: React.FunctionComponent<
+ Select a revision or provide a{' '} , ThemeProps { +interface Props extends RepositoryCompareAreaPageProps, ThemeProps { /** The base of the comparison. */ base: { repoName: string; repoID: Scalars['ID']; revision?: string | null } @@ -82,6 +82,10 @@ interface Props extends RepositoryCompareAreaPageProps, RouteComponentProps<{}>, /** An optional path of a specific file to compare */ path: string | null + + /** Required for `RepositoryCompareCommitsPage` */ + location: Location + navigate: NavigateFunction } interface State { diff --git a/client/web/src/repo/index.tsx b/client/web/src/repo/index.tsx deleted file mode 100644 index 074a953cca8b..000000000000 --- a/client/web/src/repo/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Performs a redirect to the host of the given URL with the path, query etc. properties of the current URL. - */ -export function redirectToExternalHost(externalRedirectURL: string): void { - const externalHostURL = new URL(externalRedirectURL) - const redirectURL = new URL(window.location.href) - // Preserve the path of the current URL and redirect to the repo on the external host. - redirectURL.host = externalHostURL.host - redirectURL.protocol = externalHostURL.protocol - window.location.replace(redirectURL.href) -} diff --git a/client/web/src/repo/releases/RepositoryReleasesArea.tsx b/client/web/src/repo/releases/RepositoryReleasesArea.tsx index aabfa3db29e7..af01fac799be 100644 --- a/client/web/src/repo/releases/RepositoryReleasesArea.tsx +++ b/client/web/src/repo/releases/RepositoryReleasesArea.tsx @@ -1,30 +1,13 @@ -import React, { useMemo } from 'react' - -import * as H from 'history' -import MapSearchIcon from 'mdi-react/MapSearchIcon' -import { Route, RouteComponentProps, Switch } from 'react-router' +import { FC } from 'react' import { BreadcrumbSetters } from '../../components/Breadcrumbs' -import { HeroPage } from '../../components/HeroPage' import { RepositoryFields } from '../../graphql-operations' import { RepoContainerContext } from '../RepoContainer' import { RepositoryReleasesTagsPage } from './RepositoryReleasesTagsPage' -const NotFoundPage: React.FunctionComponent> = () => ( - -) - -interface Props - extends RouteComponentProps<{}>, - Pick, - BreadcrumbSetters { +interface Props extends Pick, BreadcrumbSetters { repo: RepositoryFields - history: H.History } /** @@ -37,35 +20,21 @@ export interface RepositoryReleasesAreaPageProps { repo: RepositoryFields } +const BREADCRUMB = { key: 'tags', element: 'Tags' } + /** * Renders pages related to repository branches. */ -export const RepositoryReleasesArea: React.FunctionComponent> = ({ - useBreadcrumb, - repo, - routePrefix, -}) => { - useBreadcrumb(useMemo(() => ({ key: 'tags', element: 'Tags' }), [])) +export const RepositoryReleasesArea: FC = props => { + const { useBreadcrumb, repo } = props - const transferProps: { repo: RepositoryFields } = { - repo, - } + useBreadcrumb(BREADCRUMB) return (
- - ( - - )} - /> - - +
diff --git a/client/web/src/repo/repoContainerRoutes.tsx b/client/web/src/repo/repoContainerRoutes.tsx new file mode 100644 index 000000000000..5eef6575cd3c --- /dev/null +++ b/client/web/src/repo/repoContainerRoutes.tsx @@ -0,0 +1,74 @@ +import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' + +import { ActionItemsBarProps } from '../extensions/components/ActionItemsBar' + +import { RepoRevisionWrapper } from './components/RepoRevision' +import { RepoContainerRoute } from './RepoContainer' + +const RepositoryCommitPage = lazyComponent(() => import('./commit/RepositoryCommitPage'), 'RepositoryCommitPage') +const RepositoryBranchesArea = lazyComponent( + () => import('./branches/RepositoryBranchesArea'), + 'RepositoryBranchesArea' +) + +const RepositoryReleasesArea = lazyComponent( + () => import('./releases/RepositoryReleasesArea'), + 'RepositoryReleasesArea' +) +const RepositoryCompareArea = lazyComponent(() => import('./compare/RepositoryCompareArea'), 'RepositoryCompareArea') +const RepositoryStatsArea = lazyComponent(() => import('./stats/RepositoryStatsArea'), 'RepositoryStatsArea') +const ActionItemsBar = lazyComponent( + () => import('../extensions/components/ActionItemsBar'), + 'ActionItemsBar' +) + +export const compareSpecPath = '/-/compare/*' + +export const repoContainerRoutes: readonly RepoContainerRoute[] = [ + { + path: '/-/commit/:revspec', + render: context => ( + + + {window.context.enableLegacyExtensions && ( + + )} + + ), + }, + { + path: '/-/branches/*', + render: context => , + }, + { + path: '/-/tags', + render: context => , + }, + { + path: compareSpecPath, + render: context => ( + + + {window.context.enableLegacyExtensions && ( + + )} + + ), + }, + { + path: '/-/stats/contributors', + render: context => , + }, +] diff --git a/client/web/src/repo/repoRevisionContainerRoutes.tsx b/client/web/src/repo/repoRevisionContainerRoutes.tsx new file mode 100644 index 000000000000..7ecb37fbe8ee --- /dev/null +++ b/client/web/src/repo/repoRevisionContainerRoutes.tsx @@ -0,0 +1,63 @@ +import { TraceSpanProvider } from '@sourcegraph/observability-client' +import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' +import { LoadingSpinner } from '@sourcegraph/wildcard' + +import { ActionItemsBarProps } from '../extensions/components/ActionItemsBar' + +import { RepoRevisionContainerRoute } from './RepoRevisionContainer' + +const RepositoryCommitsPage = lazyComponent(() => import('./commits/RepositoryCommitsPage'), 'RepositoryCommitsPage') + +const RepositoryFileTreePage = lazyComponent(() => import('./RepositoryFileTreePage'), 'RepositoryFileTreePage') + +const ActionItemsBar = lazyComponent( + () => import('../extensions/components/ActionItemsBar'), + 'ActionItemsBar' +) + +// Work around the issue that react router can not match nested splats when the URL contains spaces +// by expanding the repo matcher to an optional path of up to 10 segments. +// +// We don't rely on the route param names anyway and use `parseBrowserRepoURL` +// instead to parse the repo name. +// +// More info about the issue +// https://github.com/remix-run/react-router/pull/10028 +// +// This splat should be used for all routes inside of `RepoContainer`. +export const repoSplat = + '/:repo_one?/:repo_two?/:repo_three?/:repo_four?/:repo_five?/:repo_six?/:repo_seven?/:repo_eight?/:repo_nine?/:repo_ten?' + +const routeToObjectType = { + [repoSplat + '/-/blob/*']: 'blob', + [repoSplat + '/-/tree/*']: 'tree', + ['*']: undefined, +} as const + +export const commitsPath = repoSplat + '/-/commits/*' + +export const repoRevisionContainerRoutes: readonly RepoRevisionContainerRoute[] = [ + ...Object.entries(routeToObjectType).map(([routePath, objectType]) => ({ + path: routePath, + render: props => ( + + + {window.context.enableLegacyExtensions && ( + + )} + + ), + })), + { + path: commitsPath, + render: ({ revision, repo, ...context }) => + repo ? : , + }, +] diff --git a/client/web/src/repo/routes.tsx b/client/web/src/repo/routes.tsx deleted file mode 100644 index 08fe17579e46..000000000000 --- a/client/web/src/repo/routes.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import React from 'react' - -import { RouteComponentProps } from 'react-router-dom' - -import { TraceSpanProvider } from '@sourcegraph/observability-client' -import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' -import { LoadingSpinner } from '@sourcegraph/wildcard' - -import { ActionItemsBarProps } from '../extensions/components/ActionItemsBar' - -import type { RepositoryCommitsPageProps } from './commits/RepositoryCommitsPage' -import { RepoRevisionWrapper } from './components/RepoRevision' -import { RepoContainerRoute, RepoSettingsContainerRoute } from './RepoContainer' -import { RepoRevisionContainerContext, RepoRevisionContainerRoute } from './RepoRevisionContainer' -import { RepositoryFileTreePageProps } from './RepositoryFileTreePage' -import { RepositoryTagTab } from './tree/TagTab' - -const RepositoryCommitsPage = lazyComponent(() => import('./commits/RepositoryCommitsPage'), 'RepositoryCommitsPage') - -const RepositoryFileTreePage = lazyComponent(() => import('./RepositoryFileTreePage'), 'RepositoryFileTreePage') - -const RepositoryCommitPage = lazyComponent(() => import('./commit/RepositoryCommitPage'), 'RepositoryCommitPage') -const RepositoryBranchesArea = lazyComponent( - () => import('./branches/RepositoryBranchesArea'), - 'RepositoryBranchesArea' -) - -const RepositoryReleasesArea = lazyComponent( - () => import('./releases/RepositoryReleasesArea'), - 'RepositoryReleasesArea' -) -const RepoSettingsArea = lazyComponent(() => import('./settings/RepoSettingsArea'), 'RepoSettingsArea') -const RepositoryCompareArea = lazyComponent(() => import('./compare/RepositoryCompareArea'), 'RepositoryCompareArea') -const RepositoryStatsArea = lazyComponent(() => import('./stats/RepositoryStatsArea'), 'RepositoryStatsArea') -const RepositoryBranchesTab = lazyComponent(() => import('./tree/BranchesTab'), 'RepositoryBranchesTab') -const ActionItemsBar = lazyComponent( - () => import('../extensions/components/ActionItemsBar'), - 'ActionItemsBar' -) - -export const compareSpecPath = '/-/compare/:spec*' - -export const repoContainerRoutes: readonly RepoContainerRoute[] = [ - { - path: '/-/commit/:revspec+', - render: context => ( - - - {window.context.enableLegacyExtensions && ( - - )} - - ), - }, - { - path: '/-/branches', - render: context => , - }, - { - path: '/-/tags', - render: context => , - }, - { - path: compareSpecPath, - render: context => ( - - - {window.context.enableLegacyExtensions && ( - - )} - - ), - }, - { - path: '/-/stats', - render: context => , - }, -] - -export const repoSettingsContainerRoutes: readonly RepoSettingsContainerRoute[] = [ - { - path: '/-/settings', - render: context => , - }, -] - -export const RepoContributors: React.FunctionComponent< - React.PropsWithChildren -> = ({ useBreadcrumb, setBreadcrumb, repo, history, location, match, globbing, repoName }) => ( - -) - -export const RepoCommits: React.FunctionComponent< - Omit & Pick & RouteComponentProps -> = ({ revision, repo, ...context }) => - repo ? : - -const blobPath = '/-/:objectType(blob)/:filePath*' -const treePath = '/-/:objectType(tree)/:filePath*' -export const commitsPath = '/-/commits/:filePath*' - -export const repoRevisionContainerRoutes: readonly RepoRevisionContainerRoute[] = [ - ...[ - '', - blobPath, - treePath, - '/-/docs/tab/:pathID*', - '/-/commits/tab', - '/-/branch/tab', - '/-/tag/tab', - '/-/contributors/tab', - '/-/compare/tab/:spec*', - ].map(routePath => ({ - path: routePath, - exact: routePath === '', - render: (props: RepositoryFileTreePageProps) => ( - - - {window.context.enableLegacyExtensions && ( - - )} - - ), - })), - { - path: commitsPath, - render: RepoCommits, - }, - { - path: '/-/branch', - render: ({ repo }) => , - }, - { - path: '/-/tag', - render: ({ repo }) => , - }, - { - path: compareSpecPath, - render: context => ( - - - {window.context.enableLegacyExtensions && ( - - )} - - ), - }, - { - path: '/-/contributors', - render: RepoContributors, - }, -] diff --git a/client/web/src/repo/settings/RepoSettingsArea.tsx b/client/web/src/repo/settings/RepoSettingsArea.tsx index acfb83971620..411e990435ea 100644 --- a/client/web/src/repo/settings/RepoSettingsArea.tsx +++ b/client/web/src/repo/settings/RepoSettingsArea.tsx @@ -2,9 +2,8 @@ import React, { useMemo } from 'react' import classNames from 'classnames' import AlertCircleIcon from 'mdi-react/AlertCircleIcon' -import MapSearchIcon from 'mdi-react/MapSearchIcon' import MinusCircleIcon from 'mdi-react/MinusCircleIcon' -import { Route, RouteComponentProps, Switch } from 'react-router' +import { Routes, Route } from 'react-router-dom-v5-compat' import { of } from 'rxjs' import { catchError } from 'rxjs/operators' @@ -15,30 +14,22 @@ import { useObservable, ErrorMessage } from '@sourcegraph/wildcard' import { AuthenticatedUser } from '../../auth' import { BreadcrumbSetters } from '../../components/Breadcrumbs' -import { HeroPage } from '../../components/HeroPage' +import { HeroPage, NotFoundPage } from '../../components/HeroPage' import { SettingsAreaRepositoryFields } from '../../graphql-operations' -import { RouteDescriptor } from '../../util/contributions' +import { RouteV6Descriptor } from '../../util/contributions' import { fetchSettingsAreaRepository } from './backend' import { RepoSettingsSidebar, RepoSettingsSideBarGroups } from './RepoSettingsSidebar' import styles from './RepoSettingsArea.module.scss' -const NotFoundPage: React.FunctionComponent> = () => ( - -) - export interface RepoSettingsAreaRouteContext extends ThemeProps, TelemetryProps { repo: SettingsAreaRepositoryFields } -export interface RepoSettingsAreaRoute extends RouteDescriptor {} +export interface RepoSettingsAreaRoute extends RouteV6Descriptor {} -interface Props extends RouteComponentProps<{}>, BreadcrumbSetters, ThemeProps, TelemetryProps { +interface Props extends BreadcrumbSetters, ThemeProps, TelemetryProps { repoSettingsAreaRoutes: readonly RepoSettingsAreaRoute[] repoSettingsSidebarGroups: RepoSettingsSideBarGroups repoName: string @@ -51,7 +42,6 @@ interface Props extends RouteComponentProps<{}>, BreadcrumbSetters, ThemeProps, */ export const RepoSettingsArea: React.FunctionComponent> = ({ useBreadcrumb, - ...props }) => { const repoName = props.repoName @@ -67,12 +57,15 @@ export const RepoSettingsArea: React.FunctionComponent} /> } + if (repoOrError === null) { - return + return } + if (!repoOrError.viewerCanAdminister) { return (
- + {props.repoSettingsAreaRoutes.map( - ({ render, path, exact, condition = () => true }) => - condition(context) && ( - render({ ...context, ...routeComponentProps })} - /> - ) + ({ render, path, condition = () => true }) => + condition(context) && )} - - + } /> +
) diff --git a/client/web/src/repo/settings/RepoSettingsIndexPage.tsx b/client/web/src/repo/settings/RepoSettingsIndexPage.tsx index 1d6fea258ad7..9746b0ac5cdc 100644 --- a/client/web/src/repo/settings/RepoSettingsIndexPage.tsx +++ b/client/web/src/repo/settings/RepoSettingsIndexPage.tsx @@ -1,9 +1,8 @@ -import * as React from 'react' +import React from 'react' import { mdiCheckCircle } from '@mdi/js' import classNames from 'classnames' import prettyBytes from 'pretty-bytes' -import { RouteComponentProps } from 'react-router' import { Observable, Subject, Subscription } from 'rxjs' import { map, switchMap, tap } from 'rxjs/operators' @@ -221,7 +220,7 @@ const TextSearchIndexedReference: React.FunctionComponent< ) } -interface Props extends RouteComponentProps<{}> { +interface Props { repo: SettingsAreaRepositoryFields } diff --git a/client/web/src/repo/settings/RepoSettingsMirrorPage.tsx b/client/web/src/repo/settings/RepoSettingsMirrorPage.tsx index cb2c2f990a18..81515aa1a10f 100644 --- a/client/web/src/repo/settings/RepoSettingsMirrorPage.tsx +++ b/client/web/src/repo/settings/RepoSettingsMirrorPage.tsx @@ -1,9 +1,7 @@ -import React, { useEffect, useState } from 'react' +import React, { FC, useEffect, useState } from 'react' import { mdiChevronDown, mdiChevronUp, mdiLock } from '@mdi/js' import classNames from 'classnames' -import * as H from 'history' -import { RouteComponentProps } from 'react-router' import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp' import { useMutation, useQuery } from '@sourcegraph/http-client' @@ -55,12 +53,9 @@ interface UpdateMirrorRepositoryActionContainerProps { onDidUpdateRepository: () => Promise disabled: boolean disabledReason: string | undefined - history: H.History } -const UpdateMirrorRepositoryActionContainer: React.FunctionComponent< - UpdateMirrorRepositoryActionContainerProps -> = props => { +const UpdateMirrorRepositoryActionContainer: FC = props => { const [updateRepo] = useMutation( UPDATE_MIRROR_REPOSITORY, { variables: { repository: props.repo.id } } @@ -133,7 +128,6 @@ const UpdateMirrorRepositoryActionContainer: React.FunctionComponent< flashText="Added to queue" info={info} run={run} - history={props.history} /> ) } @@ -141,10 +135,9 @@ const UpdateMirrorRepositoryActionContainer: React.FunctionComponent< interface CheckMirrorRepositoryConnectionActionContainerProps { repo: SettingsAreaRepositoryFields onDidUpdateReachability: (reachable: boolean) => void - history: H.History } -const CheckMirrorRepositoryConnectionActionContainer: React.FunctionComponent< +const CheckMirrorRepositoryConnectionActionContainer: FC< CheckMirrorRepositoryConnectionActionContainerProps > = props => { const [checkConnection, { data, loading, error }] = useMutation< @@ -213,10 +206,9 @@ const CheckMirrorRepositoryConnectionActionContainer: React.FunctionComponent< // Add interface for props then create component interface CorruptionLogProps { repo: SettingsAreaRepositoryFields - history: H.History } -const CorruptionLogsContainer: React.FunctionComponent = props => { +const CorruptionLogsContainer: FC = props => { const health = props.repo.mirrorInfo.isCorrupted ? ( <> @@ -225,6 +217,7 @@ const CorruptionLogsContainer: React.FunctionComponent = pro
) : null + const logEvents: JSX.Element[] = props.repo.mirrorInfo.corruptionLogs.map(log => (
  • @@ -278,17 +271,14 @@ const CorruptionLogsContainer: React.FunctionComponent = pro ) } -interface RepoSettingsMirrorPageProps extends RouteComponentProps<{}> { +interface RepoSettingsMirrorPageProps { repo: SettingsAreaRepositoryFields - history: H.History } /** * The repository settings mirror page. */ -export const RepoSettingsMirrorPage: React.FunctionComponent< - React.PropsWithChildren -> = props => { +export const RepoSettingsMirrorPage: FC = props => { eventLogger.logPageView('RepoSettingsMirror') const [reachable, setReachable] = useState() const [recloneRepository] = useMutation( @@ -353,7 +343,6 @@ export const RepoSettingsMirrorPage: React.FunctionComponent< }} disabled={typeof reachable === 'boolean' && !reachable} disabledReason={typeof reachable === 'boolean' && !reachable ? 'Not reachable' : undefined} - history={props.history} /> { await recloneRepository() }} - history={props.history} /> {reachable === false && ( @@ -416,7 +403,7 @@ export const RepoSettingsMirrorPage: React.FunctionComponent< )} - + ) diff --git a/client/web/src/repo/settings/RepoSettingsOptionsPage.tsx b/client/web/src/repo/settings/RepoSettingsOptionsPage.tsx index 120dc89bb3cc..54f3076ad69d 100644 --- a/client/web/src/repo/settings/RepoSettingsOptionsPage.tsx +++ b/client/web/src/repo/settings/RepoSettingsOptionsPage.tsx @@ -1,7 +1,6 @@ import { FC, useCallback, useEffect, useState } from 'react' import { noop } from 'lodash' -import { RouteComponentProps } from 'react-router' import { useMutation, useQuery } from '@sourcegraph/http-client' import { Button, Container, ErrorAlert, H2, LoadingSpinner, PageHeader, renderError, Text } from '@sourcegraph/wildcard' @@ -26,14 +25,14 @@ import { RedirectionAlert } from './components/RedirectionAlert' import styles from './RepoSettingsOptionsPage.module.scss' -interface Props extends RouteComponentProps<{}> { +interface Props { repo: SettingsAreaRepositoryFields } /** * The repository settings options page. */ -export const RepoSettingsOptionsPage: FC = ({ repo, history }) => { +export const RepoSettingsOptionsPage: FC = ({ repo }) => { useEffect(() => { eventLogger.logViewEvent('RepoSettings') }) @@ -91,7 +90,6 @@ export const RepoSettingsOptionsPage: FC = ({ repo, history }) => { updateExclusionLoading={updateExclusion} repo={repo} redirectAfterExclusion={services.length < 2} - history={history} /> ))} {services.length > 1 && ( diff --git a/client/web/src/repo/settings/RepoSettingsSidebar.tsx b/client/web/src/repo/settings/RepoSettingsSidebar.tsx index dc89c6220745..5bd70bc5f6fb 100644 --- a/client/web/src/repo/settings/RepoSettingsSidebar.tsx +++ b/client/web/src/repo/settings/RepoSettingsSidebar.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react' import { mdiMenu } from '@mdi/js' import classNames from 'classnames' -import { RouteComponentProps } from 'react-router-dom' import { Button, Icon } from '@sourcegraph/wildcard' @@ -14,7 +13,7 @@ export interface RepoSettingsSideBarGroup extends Omit { +interface Props { repoSettingsSidebarGroups: RepoSettingsSideBarGroups className?: string repo: SettingsAreaRepositoryFields diff --git a/client/web/src/repo/settings/components/ActionContainer.tsx b/client/web/src/repo/settings/components/ActionContainer.tsx index 62be0b3ace2e..e2ecafcdeead 100644 --- a/client/web/src/repo/settings/components/ActionContainer.tsx +++ b/client/web/src/repo/settings/components/ActionContainer.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import classNames from 'classnames' -import * as H from 'history' import { asError } from '@sourcegraph/common' import { Button, ButtonProps, H4, Tooltip, ErrorAlert } from '@sourcegraph/wildcard' @@ -43,7 +42,6 @@ interface Props { flashText?: string run: () => Promise - history: H.History } interface State { diff --git a/client/web/src/repo/settings/components/ExternalServiceEntry.tsx b/client/web/src/repo/settings/components/ExternalServiceEntry.tsx index 24fa48a241b0..8e3974ee7f91 100644 --- a/client/web/src/repo/settings/components/ExternalServiceEntry.tsx +++ b/client/web/src/repo/settings/components/ExternalServiceEntry.tsx @@ -2,7 +2,6 @@ import { FC } from 'react' import classNames from 'classnames' import { noop } from 'lodash' -import { RouteComponentProps } from 'react-router' import { useMutation } from '@sourcegraph/http-client' import { Alert, Button, ErrorAlert, Link, LoadingSpinner, renderError, Tooltip } from '@sourcegraph/wildcard' @@ -21,7 +20,7 @@ import { RedirectionAlert } from './RedirectionAlert' import styles from './ExternalServiceEntry.module.scss' -interface ExternalServiceEntryProps extends Pick { +interface ExternalServiceEntryProps { repo: SettingsAreaRepositoryFields service: SettingsAreaExternalServiceFields excludingDisabled: boolean diff --git a/client/web/src/repo/settings/components/RedirectionAlert.tsx b/client/web/src/repo/settings/components/RedirectionAlert.tsx index a79fa1968d05..620feedd3441 100644 --- a/client/web/src/repo/settings/components/RedirectionAlert.tsx +++ b/client/web/src/repo/settings/components/RedirectionAlert.tsx @@ -1,6 +1,6 @@ import { FC, useEffect, useState } from 'react' -import { useHistory } from 'react-router' +import { useNavigate } from 'react-router-dom-v5-compat' import { Alert } from '@sourcegraph/wildcard' @@ -15,7 +15,7 @@ interface Props { */ export const RedirectionAlert: FC = ({ to, className, messagePrefix }) => { const [ttl, setTtl] = useState(3) - const history = useHistory() + const navigate = useNavigate() useEffect(() => { const interval = setInterval(() => setTtl(ttl => ttl - 1), 700) @@ -25,9 +25,9 @@ export const RedirectionAlert: FC = ({ to, className, messagePrefix }) => useEffect(() => { if (ttl === 0) { - history.push(to) + navigate(to) } - }, [ttl, history, to]) + }, [ttl, navigate, to]) return ( diff --git a/client/web/src/repo/settings/routes.ts b/client/web/src/repo/settings/routes.ts index e0fd04fe34f1..9652a260e3ff 100644 --- a/client/web/src/repo/settings/routes.ts +++ b/client/web/src/repo/settings/routes.ts @@ -2,20 +2,19 @@ import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' import { RepoSettingsAreaRoute } from './RepoSettingsArea' +export const repoSettingsAreaPath = '/-/settings/*' + export const repoSettingsAreaRoutes: readonly RepoSettingsAreaRoute[] = [ { path: '', - exact: true, render: lazyComponent(() => import('./RepoSettingsOptionsPage'), 'RepoSettingsOptionsPage'), }, { path: '/index', - exact: true, render: lazyComponent(() => import('./RepoSettingsIndexPage'), 'RepoSettingsIndexPage'), }, { path: '/mirror', - exact: true, render: lazyComponent(() => import('./RepoSettingsMirrorPage'), 'RepoSettingsMirrorPage'), }, ] diff --git a/client/web/src/repo/stats/RepositoryStatsArea.tsx b/client/web/src/repo/stats/RepositoryStatsArea.tsx index 22c3840cc232..30fad37770b7 100644 --- a/client/web/src/repo/stats/RepositoryStatsArea.tsx +++ b/client/web/src/repo/stats/RepositoryStatsArea.tsx @@ -1,28 +1,14 @@ -import React, { useMemo } from 'react' - -import MapSearchIcon from 'mdi-react/MapSearchIcon' -import { Route, RouteComponentProps, Switch } from 'react-router' +import { FC } from 'react' import { LoadingSpinner } from '@sourcegraph/wildcard' import { BreadcrumbSetters } from '../../components/Breadcrumbs' -import { HeroPage } from '../../components/HeroPage' import { RepositoryFields } from '../../graphql-operations' import { RepositoryStatsContributorsPage } from './RepositoryStatsContributorsPage' -import { RepositoryStatsNavbar } from './RepositoryStatsNavbar' - -const NotFoundPage: React.FunctionComponent> = () => ( - -) -interface Props extends RouteComponentProps<{}>, BreadcrumbSetters { +interface Props extends BreadcrumbSetters { repo: RepositoryFields | undefined - repoName: string globbing: boolean } @@ -36,35 +22,18 @@ export interface RepositoryStatsAreaPageProps { repo: RepositoryFields } -const showNavbar = false +const BREADCRUMB = { key: 'contributors', element: 'Contributors' } /** * Renders pages related to repository stats. */ -export const RepositoryStatsArea: React.FunctionComponent> = ({ - useBreadcrumb, - ...props -}) => { - useBreadcrumb(useMemo(() => ({ key: 'contributors', element: 'Contributors' }), [])) +export const RepositoryStatsArea: FC = props => { + const { useBreadcrumb, repo, globbing } = props + useBreadcrumb(BREADCRUMB) return (
    - {showNavbar && } - - - props.repo ? ( - - ) : ( - - ) - } - /> - - + {repo ? : }
    ) } diff --git a/client/web/src/repo/stats/RepositoryStatsContributorsPage.tsx b/client/web/src/repo/stats/RepositoryStatsContributorsPage.tsx index 2edb1683c035..cd6b5a1ddae4 100644 --- a/client/web/src/repo/stats/RepositoryStatsContributorsPage.tsx +++ b/client/web/src/repo/stats/RepositoryStatsContributorsPage.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react' import classNames from 'classnames' import { escapeRegExp } from 'lodash' -import { RouteComponentProps } from 'react-router-dom' +import { useLocation, useNavigate } from 'react-router-dom-v5-compat' import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp' import { numberWithCommas, pluralize } from '@sourcegraph/common' @@ -193,7 +193,7 @@ const BATCH_COUNT = 20 const equalOrEmpty = (a: string | undefined, b: string | undefined): boolean => a === b || (!a && !b) -interface Props extends RepositoryStatsAreaPageProps, RouteComponentProps<{}> { +interface Props extends RepositoryStatsAreaPageProps { globbing: boolean } @@ -215,12 +215,9 @@ const getUrlQuery = (spec: Partial): string => { } /** A page that shows a repository's contributors. */ -export const RepositoryStatsContributorsPage: React.FunctionComponent = ({ - location, - history, - repo, - globbing, -}) => { +export const RepositoryStatsContributorsPage: React.FunctionComponent = ({ repo, globbing }) => { + const location = useLocation() + const navigate = useNavigate() const queryParameters = new URLSearchParams(location.search) const spec: QuerySpec = { revisionRange: queryParameters.get('revisionRange') ?? '', @@ -293,7 +290,7 @@ export const RepositoryStatsContributorsPage: React.FunctionComponent = ( // Update the URL to reflect buffer state const onSubmit: React.FormEventHandler = event => { event.preventDefault() - history.push({ + navigate({ search: getUrlQuery({ revisionRange, after, path }), }) } @@ -308,7 +305,7 @@ export const RepositoryStatsContributorsPage: React.FunctionComponent = ( // Push new query param to history, state change will follow via `useEffect` on `location.search` const updateAfter = (after: string | undefined): void => { - history.push({ search: getUrlQuery({ ...spec, after }) }) + navigate({ search: getUrlQuery({ ...spec, after }) }) } // Whether the user has entered new option values that differ from what's in the URL query and has not yet diff --git a/client/web/src/repo/stats/RepositoryStatsNavbar.tsx b/client/web/src/repo/stats/RepositoryStatsNavbar.tsx deleted file mode 100644 index 9ef804bf7651..000000000000 --- a/client/web/src/repo/stats/RepositoryStatsNavbar.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from 'react' - -import classNames from 'classnames' -import { NavLink } from 'react-router-dom' - -export const RepositoryStatsNavbar: React.FunctionComponent< - React.PropsWithChildren<{ repo: string; className: string }> -> = ({ repo, className }) => ( -
      -
    • - - Contributors - -
    • -
    -) diff --git a/client/web/src/repo/tree/BranchesTab.tsx b/client/web/src/repo/tree/BranchesTab.tsx deleted file mode 100644 index f6c0ed139026..000000000000 --- a/client/web/src/repo/tree/BranchesTab.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import React, { useCallback, useState } from 'react' - -import { mdiChevronRight } from '@mdi/js' -import { Observable } from 'rxjs' -import { catchError, map, mapTo, startWith, switchMap } from 'rxjs/operators' - -import { isErrorLike, asError, ErrorLike } from '@sourcegraph/common' -import { Button, Card, CardHeader, Icon, LoadingSpinner, useEventObservable, ErrorAlert } from '@sourcegraph/wildcard' - -import { FilteredConnection, FilteredConnectionQueryArguments } from '../../components/FilteredConnection' -import { PageTitle } from '../../components/PageTitle' -import { GitRefConnectionFields, GitRefFields, GitRefType, TreePageRepositoryFields } from '../../graphql-operations' -import { queryGitBranches } from '../branches/RepositoryBranchesOverviewPage' -import { GitReferenceNode, queryGitReferences } from '../GitReference' - -interface RepositoryBranchesTabProps { - repo?: TreePageRepositoryFields -} - -interface Props { - repo: TreePageRepositoryFields -} - -interface OverviewTabProps { - repo: TreePageRepositoryFields - setShowAll: (spec: boolean) => void -} - -interface Data { - defaultBranch: GitRefFields | null - activeBranches: GitRefFields[] - hasMoreActiveBranches: boolean -} - -/** - * Renders pages related to repository branches. - */ -export const RepositoryBranchesTab: React.FunctionComponent> = ({ - repo, -}) => { - const [showAll, setShowAll] = useState(false) - - return ( -
    -
      -
    • - -
    • -
    • - -
    • -
    - {repo && - (showAll ? ( - - ) : ( - - ))} -
    - ) -} - -export const RepositoryBranchesAllTab: React.FunctionComponent> = ({ repo }) => { - const queryBranches = (args: FilteredConnectionQueryArguments): Observable => - queryGitReferences({ ...args, repo: repo.id, type: GitRefType.GIT_BRANCH }) - - return ( -
    - - - listClassName="list-group list-group-flush" - noun="branch" - pluralNoun="branches" - queryConnection={queryBranches} - nodeComponent={GitReferenceNode} - defaultFirst={20} - autoFocus={true} - /> -
    - ) -} - -export const RepositoryBranchesOverviewTab: React.FunctionComponent> = ({ - repo, - setShowAll, -}) => { - const [branches, setBranches] = useState(undefined) - - useEventObservable( - useCallback( - (clicks: Observable) => - clicks.pipe( - mapTo(true), - startWith(false), - switchMap(() => queryGitBranches({ repo: repo.id, first: 10 })), - map(branch => { - if (branch === null) { - return branch - } - - if (branch) { - setBranches(branch) - } - - return branch - }), - catchError((error): [ErrorLike] => [asError(error)]) - ), - [repo.id] - ) - ) - - return ( -
    - - {branches === undefined ? ( - - ) : isErrorLike(branches) ? ( - - ) : ( -
    - {branches.defaultBranch && ( - - Default branch -
      - -
    -
    - )} - {branches.activeBranches.length > 0 && ( - - Active branches -
    - {branches.activeBranches.map((gitReference, index) => ( - - ))} - {branches.hasMoreActiveBranches && ( - - )} -
    -
    - )} -
    - )} -
    - ) -} diff --git a/client/web/src/repo/tree/HomeTab.module.scss b/client/web/src/repo/tree/HomeTab.module.scss deleted file mode 100644 index 099387267b20..000000000000 --- a/client/web/src/repo/tree/HomeTab.module.scss +++ /dev/null @@ -1,68 +0,0 @@ -.home-page { - // For the other sub-pages in the RepoArea, we use a child selector to add the - // border and radius to the sub-page containers. Since we use the Container - // component here, we already have a pair of borders and had duplicate borders - // without it. This should be removed once we can get rid of that child selector. - border: none !important; - border-radius: 0 !important; - height: 100%; - - :global(.batch-change-badge) { - top: 0; - } -} - -.git-commit-node { - padding-left: 0; - padding-right: 0; - - &-message-subject { - opacity: 0.9; - } - - :global(.btn) { - opacity: 0.85; - } -} - -.container { - overflow-y: auto; - height: 100%; - flex: 1; - background-color: var(--code-bg); -} - -.section { - flex-direction: row; - width: 100%; - max-width: var(--media-xl); - margin-bottom: 1rem; -} - -.item { - flex-direction: row; - margin-bottom: 0.25rem; - width: 100%; - display: flex; - border: 1px solid var(--border-color-2); - border-radius: 3px !important; - padding: 1rem; - padding-top: 1rem; - padding-bottom: 1rem; -} - -.list { - margin: 0; - padding: 0; - list-style: none; -} - -.item-badge { - height: fit-content; -} - -.item-batch-change-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} diff --git a/client/web/src/repo/tree/HomeTab.tsx b/client/web/src/repo/tree/HomeTab.tsx deleted file mode 100644 index c084059290cd..000000000000 --- a/client/web/src/repo/tree/HomeTab.tsx +++ /dev/null @@ -1,515 +0,0 @@ -import React, { useState, useCallback } from 'react' - -import classNames from 'classnames' -import { subYears, formatISO } from 'date-fns' -import * as H from 'history' -import { Observable } from 'rxjs' -import { catchError, map, mapTo, startWith, switchMap } from 'rxjs/operators' - -import { asError, ErrorLike, pluralize, encodeURIPathComponent, memoizeObservable } from '@sourcegraph/common' -import { dataOrThrowErrors, gql, useQuery } from '@sourcegraph/http-client' -import { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings' -import { - Button, - Link, - Badge, - useEventObservable, - Alert, - LoadingSpinner, - H2, - Text, - ButtonLink, - ErrorMessage, -} from '@sourcegraph/wildcard' - -import { queryGraphQL } from '../../backend/graphql' -import { BatchChangesProps } from '../../batches' -import { CodeIntelligenceProps } from '../../codeintel' -import { FilteredConnection } from '../../components/FilteredConnection' -import { - GetRepoBatchChangesSummaryResult, - GetRepoBatchChangesSummaryVariables, - GitCommitFields, - Scalars, - TreeCommits2Result, - TreePageRepositoryFields, -} from '../../graphql-operations' -import { fetchBlob } from '../blob/backend' -import { BlobInfo } from '../blob/Blob' -import { RenderedFile } from '../blob/RenderedFile' -import { GitCommitNodeProps, GitCommitNode } from '../commits/GitCommitNode' -import { gitCommitFragment } from '../commits/RepositoryCommitsPage' - -import styles from './HomeTab.module.scss' - -type TreeCommitsRepositoryCommit = NonNullable< - Extract['commit'] -> - -const fetchTreeCommits = memoizeObservable( - (args: { - repo: Scalars['ID'] - revspec: string - first?: number - filePath?: string - after?: string - }): Observable => - queryGraphQL( - gql` - query TreeCommits2($repo: ID!, $revspec: String!, $first: Int, $filePath: String, $after: String) { - node(id: $repo) { - __typename - ... on Repository { - commit(rev: $revspec) { - ancestors(first: $first, path: $filePath, after: $after) { - nodes { - ...GitCommitFields - } - pageInfo { - hasNextPage - } - } - } - } - } - } - ${gitCommitFragment} - `, - args - ).pipe( - map(dataOrThrowErrors), - map(data => { - if (!data.node) { - throw new Error('Repository not found') - } - if (data.node.__typename !== 'Repository') { - throw new Error('Node is not a Repository') - } - if (!data.node.commit) { - throw new Error('Commit not found') - } - return data.node.commit.ancestors - }) - ), - args => `${args.repo}:${args.revspec}:${String(args.first)}:${String(args.filePath)}:${String(args.after)}` -) - -interface Props extends SettingsCascadeProps, CodeIntelligenceProps, BatchChangesProps { - repo: TreePageRepositoryFields - filePath: string - commitID: string - revision: string - location: H.Location - history?: H.History - globbing?: boolean -} - -export const treePageRepositoryFragment = gql` - fragment TreePageRepositoryFields on Repository { - id - name - description - viewerCanAdminister - url - } -` - -export const HomeTab: React.FunctionComponent> = ({ - repo, - commitID, - revision, - filePath, - codeIntelligenceEnabled, - codeIntelligenceBadgeContent: CodeIntelligenceBadge, - batchChangesEnabled, - ...props -}) => { - const [richHTML, setRichHTML] = useState('loading') - const [aborted, setAborted] = useState(false) - const [nextFetchWithDisabledTimeout, blobInfoOrError] = useEventObservable< - void, - (BlobInfo & { richHTML: string; aborted: boolean }) | null | ErrorLike - >( - useCallback( - (clicks: Observable) => - clicks.pipe( - mapTo(true), - startWith(false), - switchMap(disableTimeout => - fetchBlob({ - repoName: repo.name, - revision, - filePath: `${filePath}/README.md`, - disableTimeout, - }) - ), - map(blob => { - if (blob === null) { - setRichHTML(null) - return blob - } - - // Replace html with lsif generated HTML, if available - if (blob.richHTML) { - setRichHTML(blob.richHTML) - setAborted(blob.highlight.aborted || false) - } else { - setRichHTML(null) - } - - const blobInfo: BlobInfo & { richHTML: string; aborted: boolean } = { - content: blob.content, - html: blob.highlight.html ?? '', - repoName: repo.name, - revision, - commitID, - filePath: `${filePath}/README.md`, - mode: '', - // Properties used in `BlobPage` but not `Blob` - richHTML: blob.richHTML, - aborted: blob.highlight.aborted, - } - return blobInfo - }), - catchError((error): [ErrorLike] => [asError(error)]) - ), - [repo.name, commitID, filePath, revision] - ) - ) - - const onExtendTimeoutClick = useCallback( - (event: React.MouseEvent): void => { - event.preventDefault() - nextFetchWithDisabledTimeout() - }, - [nextFetchWithDisabledTimeout] - ) - - const [showOlderCommits, setShowOlderCommits] = useState(false) - - const onShowOlderCommitsClicked = useCallback( - (event: React.MouseEvent): void => { - event.preventDefault() - setShowOlderCommits(true) - }, - [setShowOlderCommits] - ) - - const queryCommits = useCallback( - (args: { first?: number }): Observable => { - const after: string | undefined = showOlderCommits ? undefined : formatISO(subYears(Date.now(), 1)) - return fetchTreeCommits({ - ...args, - repo: repo.id, - revspec: revision || '', - filePath, - after, - }) - }, - [filePath, repo.id, revision, showOlderCommits] - ) - - const emptyElement = showOlderCommits ? ( -
    No commits in this tree.
    - ) : ( -
    - No commits in this tree in the past year. -
    - -
    -
    - ) - - const TotalCountSummary: React.FunctionComponent> = ({ - totalCount, - }) => ( -
    - {showOlderCommits ? ( - <> - {totalCount} total {pluralize('commit', totalCount)} in this tree. - - ) : ( - <> - - {totalCount} {pluralize('commit', totalCount)} in this tree in the past year. - -
    - -
    - - )} -
    - ) - - interface RecentCommitsProps { - isSidebar: boolean - } - - const RecentCommits: React.FunctionComponent> = ({ isSidebar }) => ( -
    -

    Recent commits

    - - > - className="mt-2 p0 m-0" - listClassName="list-group list-group-flush" - noun="commit in this tree" - pluralNoun="commits in this tree" - queryConnection={queryCommits} - nodeComponent={GitCommitNode} - showMoreClassName="px-0" - nodeComponentProps={{ - className: classNames('list-group-item', styles.gitCommitNode), - messageSubjectClassName: isSidebar ? 'd-none' : styles.gitCommitNodeMessageSubject, - compact: isSidebar, - hideExpandCommitMessageBody: isSidebar, - sidebar: isSidebar, - wrapperElement: 'li', - }} - updateOnChange={`${repo.name}:${revision}:${filePath}:${String(showOlderCommits)}`} - defaultFirst={7} - useURLQuery={false} - hideSearch={true} - emptyElement={emptyElement} - totalCountSummaryComponent={TotalCountSummary} - /> -
    - ) - - const READMEFile: React.FunctionComponent> = () => ( -
    - {richHTML && richHTML !== 'loading' && ( - - )} - {!richHTML && richHTML !== 'loading' && ( -
    - winner -

    No README available :)

    -
    - )} - {blobInfoOrError && richHTML && aborted && ( -
    - - Rendering this file took too long.   - - -
    - )} -
    - ) - - // Only render recent commits and readme for non-root directory - if (filePath) { - return ( -
    - -

    README.md

    - -
    - ) - } - - return ( -
    -
    - {/* RENDER README */} -
    - -
    - {/* SIDE MENU*/} -
    -
    - - {/* CODE-INTEL */} -
    -

    Code intel

    - {CodeIntelligenceBadge && ( - - )} -
    - {/* BATCH CHANGES */} -
    -

    Batch changes

    - {batchChangesEnabled ? ( - - ) : ( -
    -
    - - DISABLED - -
    Not available
    -
    -
    - - Learn more - -
    -
    - )} -
    -
    -
    -
    -
    - ) -} - -interface HomeTabBatchChangeBadgeProps { - repoName: string -} - -export const HomeTabBatchChangeBadge: React.FunctionComponent< - React.PropsWithChildren -> = ({ repoName }) => { - const { loading, error, data } = useQuery( - REPO_BATCH_CHANGES_SUMMARY, - { - variables: { name: repoName }, - } - ) - if (loading) { - return ( -
    - -
    - ) - } - - const allBatchChanges = ( -
    - - View all batch changes - -
    - ) - - if (error || !data?.repository) { - return ( - <> -
    - -
    - {allBatchChanges} - - ) - } - - const badgeClassNames = classNames('text-uppercase col-2 align-self-center', styles.itemBadge) - const batchChanges = data?.repository?.batchChanges - if (!batchChanges || batchChanges.nodes.length === 0) { - return ( - <> -
    -
    No open batch changes for this repository
    - Create a batch change -
    - {allBatchChanges} - - ) - } - - const items: React.ReactElement[] = batchChanges.nodes.map( - ({ id, name, namespace: { namespaceName }, changesetsStats, url }) => { - const summaries: { value: number; name: string }[] = [ - { - name: 'open', - value: changesetsStats.open, - }, - { - name: 'merged', - value: changesetsStats.merged, - }, - { - name: 'closed', - value: changesetsStats.closed, - }, - ] - const summaryTexts = summaries.map(({ value, name }) => `${value} ${name}`) - - return ( -
  • - - OPEN - -
    - - {namespaceName} / {name} - -
    {summaryTexts.join(', ')}
    -
    -
  • - ) - } - ) - return ( - <> -
      {items}
    - {allBatchChanges} - - ) -} - -const REPO_BATCH_CHANGE_FRAGMENT = gql` - fragment RepoBatchChangeSummary on BatchChange { - id - state - name - namespace { - namespaceName - } - url - changesetsStats { - open - merged - closed - } - } -` - -const REPO_BATCH_CHANGES_SUMMARY = gql` - query GetRepoBatchChangesSummary($name: String!) { - repository(name: $name) { - batchChanges(state: OPEN, first: 10) { - nodes { - ...RepoBatchChangeSummary - } - } - } - } - - ${REPO_BATCH_CHANGE_FRAGMENT} -` diff --git a/client/web/src/repo/tree/TabNavigation.tsx b/client/web/src/repo/tree/TabNavigation.tsx deleted file mode 100644 index 58a399a6f0f0..000000000000 --- a/client/web/src/repo/tree/TabNavigation.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react' - -import { mdiSourceCommit, mdiSourceBranch, mdiTag, mdiHistory, mdiAccount, mdiBrain, mdiCog } from '@mdi/js' - -import { encodeURIPathComponent } from '@sourcegraph/common' -import { TreeFields } from '@sourcegraph/shared/src/graphql-operations' -import { Button, ButtonGroup, Icon, Link } from '@sourcegraph/wildcard' - -import { RepoBatchChangesButton } from '../../batches/RepoBatchChangesButton' -import { TreePageRepositoryFields } from '../../graphql-operations' - -interface TabNavigationProps { - setCurrentTab(tabName: string): (tabName: string) => {} - repo: TreePageRepositoryFields - revision: string - tree: TreeFields - codeIntelligenceEnabled: boolean - batchChangesEnabled: boolean -} - -export const TabNavigation: React.FunctionComponent> = ({ - setCurrentTab, - repo, - codeIntelligenceEnabled, - batchChangesEnabled, -}) => ( - - - - - - - {codeIntelligenceEnabled && ( - - )} - {batchChangesEnabled && } - {repo.viewerCanAdminister && ( - - )} - -) diff --git a/client/web/src/repo/tree/TagTab.tsx b/client/web/src/repo/tree/TagTab.tsx deleted file mode 100644 index b898967b8e62..000000000000 --- a/client/web/src/repo/tree/TagTab.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react' - -import { RepositoryFields } from '../../graphql-operations' -import { RepositoryReleasesTagsPage } from '../releases/RepositoryReleasesTagsPage' - -interface Props { - repo: RepositoryFields | undefined -} - -/** - * Renders repository's tags. - */ -export const RepositoryTagTab: React.FunctionComponent> = ({ repo }) => ( -
    -
    -
    - -
    -
    -
    -) diff --git a/client/web/src/repo/tree/TreeEntriesSection.module.scss b/client/web/src/repo/tree/TreeEntriesSection.module.scss deleted file mode 100644 index 7c74792fc7e8..000000000000 --- a/client/web/src/repo/tree/TreeEntriesSection.module.scss +++ /dev/null @@ -1,44 +0,0 @@ -@import 'wildcard/src/global-styles/breakpoints'; - -.tree-entries-section { - // To avoid having empty columns (and thus the items appearing not flush with the left margin), - // the component only applies this class when there are >= 6 items. This number is chosen - // because it is greater than the maximum number of columns that will be shown and ensures that - // at least 1 column has more than 1 item. - // See also MIN_ENTRIES_FOR_COLUMN_LAYOUT. - &--columns { - column-gap: 1.5rem; - column-width: 13rem; - - @media (--sm-breakpoint-up) { - column-count: 1; - } - @media (--md-breakpoint-up) { - column-count: 3; - } - @media (--md-breakpoint-down) { - column-count: 4; - } - } -} - -.tree-entry { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - margin-left: -0.25rem; - margin-right: -0.25rem; - padding: 0.125rem 0.25rem; - - break-inside: avoid-column; - - &:hover { - background-color: var(--color-bg-1); - } - - &--no-columns { - max-width: 18rem; - } -} diff --git a/client/web/src/repo/tree/TreeEntriesSection.test.tsx b/client/web/src/repo/tree/TreeEntriesSection.test.tsx deleted file mode 100644 index 6f82d5664267..000000000000 --- a/client/web/src/repo/tree/TreeEntriesSection.test.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing' - -import { TreeEntriesSection } from './TreeEntriesSection' - -describe('TreeEntriesSection', () => { - it('should render a grid of tree entries at the root', () => { - expect( - renderWithBrandedContext( - - ).asFragment() - ).toMatchSnapshot() - }) - it('should render a grid of tree entries in a subdirectory', () => { - expect( - renderWithBrandedContext( - - ).asFragment() - ).toMatchSnapshot() - }) - it('should render only direct children', () => { - expect( - renderWithBrandedContext( - - ).asFragment() - ).toMatchSnapshot() - }) -}) diff --git a/client/web/src/repo/tree/TreeEntriesSection.tsx b/client/web/src/repo/tree/TreeEntriesSection.tsx deleted file mode 100644 index 6c30a7713c92..000000000000 --- a/client/web/src/repo/tree/TreeEntriesSection.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react' - -import { mdiFileDocumentOutline, mdiFolderOutline } from '@mdi/js' -import classNames from 'classnames' - -import { TreeEntryFields } from '@sourcegraph/shared/src/graphql-operations' -import { Link, Icon } from '@sourcegraph/wildcard' - -import styles from './TreeEntriesSection.module.scss' - -/** - * Use a multi-column layout for tree entries when there are at least this many. See TreeEntriesSection.scss - * for more information. - */ -const MIN_ENTRIES_FOR_COLUMN_LAYOUT = 6 - -const TreeEntry: React.FunctionComponent< - React.PropsWithChildren<{ - isDirectory: boolean - name: string - parentPath: string - url: string - isColumnLayout: boolean - path: string - }> -> = ({ isDirectory, name, url, isColumnLayout, path }) => ( -
  • - -
    - - - {name} - {isDirectory && '/'} - -
    - -
  • -) - -interface TreeEntriesSectionProps { - parentPath: string - entries: Pick[] -} - -export const TreeEntriesSection: React.FunctionComponent> = ({ - parentPath, - entries, -}) => { - const directChildren = entries.filter(entry => entry.path === [parentPath, entry.name].filter(Boolean).join('/')) - if (directChildren.length === 0) { - return null - } - - const isColumnLayout = directChildren.length > MIN_ENTRIES_FOR_COLUMN_LAYOUT - - return ( -
      - {directChildren.map((entry, index) => ( - - ))} -
    - ) -} diff --git a/client/web/src/repo/tree/TreeNavigation.tsx b/client/web/src/repo/tree/TreeNavigation.tsx deleted file mode 100644 index f1bad0a82695..000000000000 --- a/client/web/src/repo/tree/TreeNavigation.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react' - -import { mdiSourceCommit, mdiSourceBranch, mdiTag, mdiHistory, mdiAccount, mdiBrain, mdiCog } from '@mdi/js' - -import { encodeURIPathComponent } from '@sourcegraph/common' -import { TreeFields } from '@sourcegraph/shared/src/graphql-operations' -import { Button, ButtonGroup, Icon, Link } from '@sourcegraph/wildcard' - -import { RepoBatchChangesButton } from '../../batches/RepoBatchChangesButton' - -interface TreeNavigationProps { - repoName: string - viewerCanAdminister: boolean | undefined - revision: string - tree: TreeFields - codeIntelligenceEnabled: boolean - batchChangesEnabled: boolean -} - -export const TreeNavigation: React.FunctionComponent> = ({ - repoName, - viewerCanAdminister, - revision, - tree, - codeIntelligenceEnabled, - batchChangesEnabled, -}) => ( - - - - - - - {codeIntelligenceEnabled && ( - - )} - {batchChangesEnabled && } - {viewerCanAdminister && ( - - )} - -) diff --git a/client/web/src/repo/tree/TreePage.tsx b/client/web/src/repo/tree/TreePage.tsx index b4c8a77a8c9f..8591513bc017 100644 --- a/client/web/src/repo/tree/TreePage.tsx +++ b/client/web/src/repo/tree/TreePage.tsx @@ -2,8 +2,7 @@ import React, { useMemo, useEffect, useState } from 'react' import { mdiBrain, mdiCog, mdiFolder, mdiHistory, mdiSourceBranch, mdiSourceRepository, mdiTag } from '@mdi/js' import classNames from 'classnames' -import * as H from 'history' -import { Redirect } from 'react-router-dom' +import { Navigate, useLocation } from 'react-router-dom-v5-compat' import { catchError } from 'rxjs/operators' import { asError, encodeURIPathComponent, ErrorLike, isErrorLike, logger } from '@sourcegraph/common' @@ -41,7 +40,6 @@ import { ActionItemsBarProps } from '../../extensions/components/ActionItemsBar' import { RepositoryFields } from '../../graphql-operations' import { basename } from '../../util/path' import { FilePathBreadcrumbs } from '../FilePathBreadcrumbs' -import { RepositoryFileTreePageProps } from '../RepositoryFileTreePage' import { TreePageContent } from './TreePageContent' @@ -63,11 +61,8 @@ interface Props filePath: string commitID: string revision: string - location: H.Location - history: H.History globbing: boolean useActionItemsBar: ActionItemsBarProps['useActionItemsBar'] - match: RepositoryFileTreePageProps['match'] isSourcegraphDotCom: boolean className?: string } @@ -83,7 +78,6 @@ export const treePageRepositoryFragment = gql` ` export const TreePage: React.FunctionComponent> = ({ - location, repo, repoName, commitID, @@ -94,11 +88,12 @@ export const TreePage: React.FunctionComponent> = codeIntelligenceEnabled, batchChangesEnabled, useActionItemsBar, - match, isSourcegraphDotCom, className, ...props }) => { + const location = useLocation() + useEffect(() => { if (filePath === '') { props.telemetryService.logViewEvent('Repository') @@ -305,7 +300,7 @@ export const TreePage: React.FunctionComponent> = // If the tree is actually a blob, be helpful and redirect to the blob page. // We don't have error names on GraphQL errors. /not a directory/i.test(treeOrError.message) ? ( - + ) : ( ) diff --git a/client/web/src/repo/tree/TreeTabList.tsx b/client/web/src/repo/tree/TreeTabList.tsx deleted file mode 100644 index 22b7212c9b96..000000000000 --- a/client/web/src/repo/tree/TreeTabList.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React, { useMemo } from 'react' - -import { mdiFileDocument, mdiSourceCommit, mdiSourceBranch, mdiTag, mdiHistory, mdiAccount } from '@mdi/js' -import classNames from 'classnames' -import { useCallbackRef } from 'use-callback-ref' - -import { TreeFields } from '@sourcegraph/shared/src/graphql-operations' -import { Icon, Link } from '@sourcegraph/wildcard' - -interface TreeTabList { - tree: TreeFields - selectedTab: string - setSelectedTab: (tab: string) => void -} - -export const TreeTabList: React.FunctionComponent> = ({ - tree, - selectedTab, - setSelectedTab, -}) => { - type Tabs = { tab: string; title: string; isActive: boolean; logName: string; icon: JSX.Element; url: string }[] - - const tabs: Tabs = useMemo( - () => [ - { - tab: 'home', - title: 'Home', - isActive: selectedTab === 'home', - logName: 'RepoHomeTab', - icon: , - url: `${tree.url}/`, - }, - { - tab: 'commits', - title: 'Commits', - isActive: selectedTab === 'commits', - logName: 'RepoCommitsTab', - icon: , - url: `${tree.url}/-/commits/tab`, - }, - { - tab: 'branch', - title: 'Branches', - isActive: selectedTab === 'branch', - logName: 'RepoBranchesTab', - icon: , - url: `${tree.url}/-/branch/tab`, - }, - { - tab: 'tags', - title: 'Tags', - isActive: selectedTab === 'tags', - logName: 'RepoTagsTab', - icon: , - url: `${tree.url}/-/tag/tab`, - }, - { - tab: 'compare', - title: 'Compare', - isActive: selectedTab === 'compare', - logName: 'RepoCompareTab', - icon: , - url: `${tree.url}/-/compare/tab`, - }, - { - tab: 'contributors', - title: 'Contributors', - isActive: selectedTab === 'contributors', - logName: 'RepoContributorsTab', - icon: , - url: `${tree.url}/-/contributors/tab`, - }, - ], - [selectedTab, tree.url] - ) - - const callbackReference = useCallbackRef(null, ref => ref?.focus()) - - return ( - - ) -} diff --git a/client/web/src/repo/tree/__snapshots__/TreeEntriesSection.test.tsx.snap b/client/web/src/repo/tree/__snapshots__/TreeEntriesSection.test.tsx.snap deleted file mode 100644 index 6732b01a4559..000000000000 --- a/client/web/src/repo/tree/__snapshots__/TreeEntriesSection.test.tsx.snap +++ /dev/null @@ -1,289 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TreeEntriesSection should render a grid of tree entries at the root 1`] = ` - - - -`; - -exports[`TreeEntriesSection should render a grid of tree entries in a subdirectory 1`] = ` - - - -`; - -exports[`TreeEntriesSection should render only direct children 1`] = ` - - - -`; diff --git a/client/web/src/settings/DynamicallyImportedMonacoSettingsEditor.tsx b/client/web/src/settings/DynamicallyImportedMonacoSettingsEditor.tsx index 7888c2a07f25..2488b3a924b7 100644 --- a/client/web/src/settings/DynamicallyImportedMonacoSettingsEditor.tsx +++ b/client/web/src/settings/DynamicallyImportedMonacoSettingsEditor.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import * as H from 'history' import * as _monaco from 'monaco-editor' // type only import { Subscription } from 'rxjs' @@ -10,6 +9,7 @@ import { ThemeProps } from '@sourcegraph/shared/src/theme' import { LoadingSpinner } from '@sourcegraph/wildcard' import { SaveToolbarProps, SaveToolbar, SaveToolbarPropsGenerator } from '../components/SaveToolbar' +import { globalHistory } from '../util/globalHistory' import { EditorAction, EditorActionsGroup } from './EditorActionsGroup' import * as _monacoSettingsEditorModule from './MonacoSettingsEditor' @@ -53,8 +53,6 @@ interface Props } explanation?: JSX.Element - - history: H.History } interface State { @@ -80,9 +78,11 @@ export class DynamicallyImportedMonacoSettingsEditor exte public componentDidMount(): void { if (this.props.blockNavigationIfDirty !== false) { + // TODO(valery): RR6 + // https://github.com/remix-run/react-router/blob/0ce0e4c728129efe214521a22fb902fa652bac70/decisions/0001-use-blocker.md // Prevent navigation when dirty. this.subscriptions.add( - this.props.history.block((location: H.Location, action: H.Action) => { + globalHistory.block((location, action) => { if (action === 'REPLACE') { return undefined } diff --git a/client/web/src/settings/SettingsArea.tsx b/client/web/src/settings/SettingsArea.tsx index 98c96a25d7f4..d290e5063da3 100644 --- a/client/web/src/settings/SettingsArea.tsx +++ b/client/web/src/settings/SettingsArea.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import classNames from 'classnames' import AlertCircleIcon from 'mdi-react/AlertCircleIcon' -import MapSearchIcon from 'mdi-react/MapSearchIcon' import { Route, RouteComponentProps, Switch } from 'react-router' import { combineLatest, Observable, of, Subject, Subscription } from 'rxjs' import { catchError, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators' @@ -20,17 +19,13 @@ import { LoadingSpinner, PageHeader, ErrorMessage } from '@sourcegraph/wildcard' import { AuthenticatedUser } from '../auth' import { queryGraphQL } from '../backend/graphql' -import { HeroPage } from '../components/HeroPage' +import { HeroPage, NotFoundPage } from '../components/HeroPage' import { SettingsCascadeResult } from '../graphql-operations' import { eventLogger } from '../tracking/eventLogger' import { mergeSettingsSchemas } from './configuration' import { SettingsPage } from './SettingsPage' -const NotFoundPage: React.FunctionComponent> = () => ( - -) - /** Props shared by SettingsArea and its sub-pages. */ interface SettingsAreaPageCommonProps extends PlatformContextProps, SettingsCascadeProps, ThemeProps, TelemetryProps { /** The subject whose settings to edit. */ @@ -183,7 +178,7 @@ export class SettingsArea extends React.Component { exact={true} render={routeComponentProps => } /> - + } /> ) diff --git a/client/web/src/site-admin/SiteAdminConfigurationPage.tsx b/client/web/src/site-admin/SiteAdminConfigurationPage.tsx index 03b48b6ff4f8..3365efafa2bc 100644 --- a/client/web/src/site-admin/SiteAdminConfigurationPage.tsx +++ b/client/web/src/site-admin/SiteAdminConfigurationPage.tsx @@ -1,9 +1,7 @@ import * as React from 'react' import classNames from 'classnames' -import * as H from 'history' import * as jsonc from 'jsonc-parser' -import { RouteComponentProps } from 'react-router' import { Subject, Subscription } from 'rxjs' import { delay, mergeMap, retryWhen, tap, timeout } from 'rxjs/operators' @@ -211,9 +209,7 @@ const quickConfigureActions: { }, ] -interface Props extends RouteComponentProps<{}>, ThemeProps, TelemetryProps { - history: H.History -} +interface Props extends ThemeProps, TelemetryProps {} interface State { site?: SiteResult['site'] @@ -412,7 +408,6 @@ export class SiteAdminConfigurationPage extends React.Component { isLightTheme={this.props.isLightTheme} onSave={this.onSave} actions={quickConfigureActions} - history={this.props.history} telemetryService={this.props.telemetryService} explanation={ diff --git a/client/web/src/site-admin/SiteAdminReportBugPage.tsx b/client/web/src/site-admin/SiteAdminReportBugPage.tsx index cc21987ecf7c..bbb531f61462 100644 --- a/client/web/src/site-admin/SiteAdminReportBugPage.tsx +++ b/client/web/src/site-admin/SiteAdminReportBugPage.tsx @@ -152,7 +152,6 @@ export const SiteAdminReportBugPage: React.FunctionComponent diff --git a/client/web/src/tree/Tree.tsx b/client/web/src/tree/Tree.tsx index 534e21a44857..d321212c88db 100644 --- a/client/web/src/tree/Tree.tsx +++ b/client/web/src/tree/Tree.tsx @@ -1,8 +1,8 @@ /* eslint jsx-a11y/no-static-element-interactions: warn, jsx-a11y/tabindex-no-positive: warn, jsx-a11y/no-noninteractive-tabindex: warn */ import * as React from 'react' -import * as H from 'history' import { isEqual } from 'lodash' +import { Location, NavigateFunction } from 'react-router-dom-v5-compat' import { Subject, Subscription } from 'rxjs' import { distinctUntilChanged, startWith } from 'rxjs/operators' import { Key } from 'ts-key-enum' @@ -20,7 +20,8 @@ import { getDomElement, scrollIntoView } from './util' import styles from './Tree.module.scss' interface Props extends AbsoluteRepo, TelemetryProps { - history: H.History + navigate: NavigateFunction + location: Location scrollRootSelector?: string /** The tree entry that is currently active, or '' if none (which means the root). */ @@ -204,7 +205,7 @@ export class Tree extends React.PureComponent { } this.selectNode(this.state.selectedNode) this.setActiveNode(this.state.selectedNode) - this.props.history.push(this.state.selectedNode.url) + this.props.navigate(this.state.selectedNode.url) } }, } @@ -253,7 +254,7 @@ export class Tree extends React.PureComponent { .pipe(startWith(this.props), distinctUntilChanged(isEqual)) .subscribe((props: Props) => { const newParentPath = props.activePathIsDir ? props.activePath : dirname(props.activePath) - const queryParameters = new URLSearchParams(this.props.history.location.search) + const queryParameters = new URLSearchParams(this.props.location.search) const queryParametersHasSubtree = queryParameters.get('subtree') === 'true' // If we're updating due to a file/directory suggestion or code intel action, @@ -282,10 +283,15 @@ export class Tree extends React.PureComponent { // Strip the ?subtree query param. Handle both when going from ancestor -> child and child -> ancestor. queryParameters.delete('subtree') if (queryParametersHasSubtree && !queryParameters.has('tab')) { - this.props.history.replace({ - search: formatSearchParameters(queryParameters), - hash: this.props.history.location.hash, - }) + this.props.navigate( + { + search: formatSearchParameters(queryParameters), + hash: this.props.location.hash, + }, + { + replace: true, + } + ) } }) ) diff --git a/client/web/src/user/area/UserArea.tsx b/client/web/src/user/area/UserArea.tsx index 5e103aa3307c..c9570c77e478 100644 --- a/client/web/src/user/area/UserArea.tsx +++ b/client/web/src/user/area/UserArea.tsx @@ -1,6 +1,5 @@ import React, { useMemo } from 'react' -import MapSearchIcon from 'mdi-react/MapSearchIcon' import { Route, Switch } from 'react-router' import { useParams, useLocation } from 'react-router-dom-v5-compat' @@ -15,7 +14,7 @@ import { AuthenticatedUser } from '../../auth' import { BatchChangesProps } from '../../batches' import { BreadcrumbsProps, BreadcrumbSetters } from '../../components/Breadcrumbs' import { ErrorBoundary } from '../../components/ErrorBoundary' -import { HeroPage } from '../../components/HeroPage' +import { NotFoundPage } from '../../components/HeroPage' import { Page } from '../../components/Page' import { UserAreaUserFields, UserAreaUserProfileResult, UserAreaUserProfileVariables } from '../../graphql-operations' import { NamespaceProps } from '../../namespaces' @@ -166,7 +165,7 @@ export const UserArea: React.FunctionComponent + return } const context: UserAreaRouteContext = { @@ -214,15 +213,9 @@ export const UserArea: React.FunctionComponent ) )} - - - + } /> ) } - -const NotFoundPage: React.FunctionComponent> = () => ( - -) diff --git a/client/web/src/user/settings/UserSettingsArea.tsx b/client/web/src/user/settings/UserSettingsArea.tsx index c0a47ddb7ac5..8c4abd5a174c 100644 --- a/client/web/src/user/settings/UserSettingsArea.tsx +++ b/client/web/src/user/settings/UserSettingsArea.tsx @@ -12,7 +12,7 @@ import { LoadingSpinner } from '@sourcegraph/wildcard' import { AuthenticatedUser } from '../../auth' import { withAuthenticatedUser } from '../../auth/withAuthenticatedUser' import { ErrorBoundary } from '../../components/ErrorBoundary' -import { HeroPage } from '../../components/HeroPage' +import { HeroPage, NotFoundPage } from '../../components/HeroPage' import { UserAreaUserFields, UserSettingsAreaUserFields, @@ -28,10 +28,6 @@ import { UserSettingsSidebar, UserSettingsSidebarItems } from './UserSettingsSid import styles from './UserSettingsArea.module.scss' -const NotFoundPage: React.FunctionComponent> = () => ( - -) - export interface UserSettingsAreaRoute extends RouteDescriptor {} export interface UserSettingsAreaProps @@ -121,7 +117,7 @@ export const AuthenticatedUserSettingsArea: React.FunctionComponent< } if (!user) { - return + return } if (authenticatedUser.id !== user.id && !user.viewerCanAdminister) { @@ -172,7 +168,7 @@ export const AuthenticatedUserSettingsArea: React.FunctionComponent< /> ) )} - + } key="hardcoded-key" /> diff --git a/client/web/src/user/settings/accessTokens/UserSettingsTokensArea.tsx b/client/web/src/user/settings/accessTokens/UserSettingsTokensArea.tsx index 13dc95db8a3d..6c1f4ec0199e 100644 --- a/client/web/src/user/settings/accessTokens/UserSettingsTokensArea.tsx +++ b/client/web/src/user/settings/accessTokens/UserSettingsTokensArea.tsx @@ -1,11 +1,10 @@ import React, { useCallback, useState } from 'react' -import MapSearchIcon from 'mdi-react/MapSearchIcon' import { Route, RouteComponentProps, Switch } from 'react-router' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' -import { HeroPage } from '../../../components/HeroPage' +import { NotFoundPage } from '../../../components/HeroPage' import { CreateAccessTokenResult } from '../../../graphql-operations' import { UserSettingsAreaRouteContext } from '../UserSettingsArea' @@ -13,10 +12,6 @@ import { UserSettingsCreateAccessTokenCallbackPage } from './UserSettingsCreateA import { UserSettingsCreateAccessTokenPage } from './UserSettingsCreateAccessTokenPage' import { UserSettingsTokensPage } from './UserSettingsTokensPage' -const NotFoundPage: React.FunctionComponent> = () => ( - -) - interface Props extends Pick, Pick, 'history' | 'location' | 'match'>, @@ -63,7 +58,7 @@ export const UserSettingsTokensArea: React.FunctionComponent )} /> - + } key="hardcoded-key" /> ) } diff --git a/client/web/src/util/contributions.ts b/client/web/src/util/contributions.ts index 7e7a68a1b2f6..2956dddb5689 100644 --- a/client/web/src/util/contributions.ts +++ b/client/web/src/util/contributions.ts @@ -32,6 +32,17 @@ export interface RouteDescriptor readonly render: (props: C & RouteComponentProps

    ) => React.ReactNode } +/** + * Configuration for a route. + * + * @template C Context information that is passed to `render` and `condition` + */ +export interface RouteV6Descriptor extends Conditional { + /** Path of this route (appended to the current match) */ + readonly path: string + readonly render: (props: C) => React.ReactNode +} + export interface NavGroupDescriptor extends Conditional { readonly header?: { readonly label: string diff --git a/client/web/src/util/globalHistory.ts b/client/web/src/util/globalHistory.ts new file mode 100644 index 000000000000..1874b65b919f --- /dev/null +++ b/client/web/src/util/globalHistory.ts @@ -0,0 +1,7 @@ +import { createBrowserHistory } from 'history' + +/** + * TODO(valery): RR6 + * @deprecated this global history variable should be removed once we complete the migration to react-router 6. + */ +export const globalHistory = createBrowserHistory() diff --git a/client/web/src/util/url.ts b/client/web/src/util/url.ts index 438ca49013ac..6bf77f13307d 100644 --- a/client/web/src/util/url.ts +++ b/client/web/src/util/url.ts @@ -6,7 +6,6 @@ import { ParsedRepoURI, parseQueryAndHash, parseRepoRevision, - RepoDocumentation, RepoFile, } from '@sourcegraph/shared/src/util/url' @@ -14,17 +13,6 @@ export function toTreeURL(target: RepoFile): string { return `/${encodeRepoRevision(target)}/-/tree/${target.filePath}` } -export function toDocumentationURL(target: RepoDocumentation): string { - return `/${encodeRepoRevision(target)}/-/docs${target.pathID}` -} - -export function toDocumentationSingleSymbolURL(target: RepoDocumentation): string { - const hash = target.pathID.indexOf('#') - const path = hash === -1 ? target.pathID : target.pathID.slice(0, hash) - const qualifier = hash === -1 ? '' : target.pathID.slice(hash + '#'.length) - return `/${encodeRepoRevision(target)}/-/docs${path}?${qualifier}` -} - /** * Returns the given URLSearchParams as a string. */ diff --git a/client/web/webpack.config.js b/client/web/webpack.config.js index 74cc97d9c671..2246063ad1cd 100644 --- a/client/web/webpack.config.js +++ b/client/web/webpack.config.js @@ -166,17 +166,16 @@ const config = { : 'styles/[name].bundle.css', }), getMonacoWebpackPlugin(), - !WEBPACK_SERVE_INDEX && - new WebpackManifestPlugin({ - writeToFileEmit: true, - fileName: 'webpack.manifest.json', - seed: { - environment: NODE_ENV, - }, - // Only output files that are required to run the application. - filter: ({ isInitial, name }) => - isInitial || Object.values(initialChunkNames).some(initialChunkName => name?.includes(initialChunkName)), - }), + new WebpackManifestPlugin({ + writeToFileEmit: true, + fileName: 'webpack.manifest.json', + seed: { + environment: NODE_ENV, + }, + // Only output files that are required to run the application. + filter: ({ isInitial, name }) => + isInitial || Object.values(initialChunkNames).some(initialChunkName => name?.includes(initialChunkName)), + }), ...(WEBPACK_SERVE_INDEX ? getHTMLWebpackPlugins() : []), WEBPACK_BUNDLE_ANALYZER && getStatoscopePlugin(WEBPACK_STATS_NAME), isHotReloadEnabled && new ReactRefreshWebpackPlugin({ overlay: false }), diff --git a/client/wildcard/src/utils/linkClickHandler.ts b/client/wildcard/src/utils/linkClickHandler.ts index beeba4ee9420..db074df6fa51 100644 --- a/client/wildcard/src/utils/linkClickHandler.ts +++ b/client/wildcard/src/utils/linkClickHandler.ts @@ -1,15 +1,13 @@ -import * as React from 'react' +import { MouseEventHandler } from 'react' -import * as H from 'history' - -import { anyOf, isInstanceOf, isExternalLink } from '@sourcegraph/common' +import { anyOf, isInstanceOf, isExternalLink, HistoryOrNavigate, compatNavigate } from '@sourcegraph/common' /** * Returns a click handler for link element that will make sure clicks on in-app links are handled on the client * and don't cause a full page reload. */ export const createLinkClickHandler = - (history: H.History): React.MouseEventHandler => + (history: HistoryOrNavigate): MouseEventHandler => event => { // Do nothing if the link was requested to open in a new tab if (event.ctrlKey || event.metaKey) { @@ -34,5 +32,5 @@ export const createLinkClickHandler = // Handle navigation programmatically event.preventDefault() const url = new URL(href) - history.push(url.pathname + url.search + url.hash) + compatNavigate(history, url.pathname + url.search + url.hash) } From b3d8cf79b1b149ed405c76697e43a442321b4766 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Fri, 3 Feb 2023 09:17:51 +0100 Subject: [PATCH 384/678] Make CreateUserAndSave return a user (#47315) --- cmd/frontend/auth/user.go | 10 +-- cmd/frontend/auth/user_test.go | 14 ++--- .../sourcegraphoperator/middleware_test.go | 4 +- .../auth/sourcegraph_operator_cleaner_test.go | 14 ++--- .../internal/authz/integration_test.go | 22 +++---- internal/database/event_logs_test.go | 16 ++--- internal/database/external_accounts.go | 13 ++-- internal/database/external_accounts_test.go | 62 ++++++++----------- internal/database/mocks_temp.go | 26 ++++---- internal/database/users_builtin_auth_test.go | 4 +- internal/database/users_test.go | 6 +- 11 files changed, 91 insertions(+), 100 deletions(-) diff --git a/cmd/frontend/auth/user.go b/cmd/frontend/auth/user.go index f63fd4b76e9b..45b3bbbd8a9a 100644 --- a/cmd/frontend/auth/user.go +++ b/cmd/frontend/auth/user.go @@ -117,7 +117,7 @@ func GetAndSaveUser(ctx context.Context, db database.DB, op GetAndSaveUserOp) (u // information of the actor, especially whether the actor is a Sourcegraph // operator or not. ctx = sgactor.WithActor(ctx, act) - userID, err := externalAccountsStore.CreateUserAndSave(ctx, op.UserProps, op.ExternalAccount, op.ExternalAccountData) + user, err := externalAccountsStore.CreateUserAndSave(ctx, op.UserProps, op.ExternalAccount, op.ExternalAccountData) switch { case database.IsUsernameExists(err): return 0, false, false, fmt.Sprintf("Username %q already exists, but no verified email matched %q", op.UserProps.Username, op.UserProps.Email), err @@ -126,16 +126,16 @@ func GetAndSaveUser(ctx context.Context, db database.DB, op GetAndSaveUserOp) (u case err != nil: return 0, false, false, "Unable to create a new user account due to a unexpected error. Ask a site admin for help.", err } - act.UID = userID + act.UID = user.ID if err = db.Authz().GrantPendingPermissions(ctx, &database.GrantPendingPermissionsArgs{ - UserID: userID, + UserID: user.ID, Perm: authz.Read, Type: authz.PermRepos, }); err != nil { logger.Error( "failed to grant user pending permissions", - sglog.Int32("userID", userID), + sglog.Int32("userID", user.ID), sglog.Error(err), ) // OK to continue, since this is a best-effort to improve the UX with some initial permissions available. @@ -177,7 +177,7 @@ func GetAndSaveUser(ctx context.Context, db database.DB, op GetAndSaveUserOp) (u ) } - return userID, true, true, "", nil + return user.ID, true, true, "", nil }() if err != nil { const eventName = "ExternalAuthSignupFailed" diff --git a/cmd/frontend/auth/user_test.go b/cmd/frontend/auth/user_test.go index cc7b83af470e..aca7ba9995f4 100644 --- a/cmd/frontend/auth/user_test.go +++ b/cmd/frontend/auth/user_test.go @@ -433,9 +433,9 @@ func TestGetAndSaveUser(t *testing.T) { usersStore.GetByVerifiedEmailFunc.SetDefaultReturn(nil, errNotFound) externalAccountsStore := database.NewMockUserExternalAccountsStore() externalAccountsStore.LookupUserAndSaveFunc.SetDefaultReturn(0, errNotFound) - externalAccountsStore.CreateUserAndSaveFunc.SetDefaultHook(func(ctx context.Context, _ database.NewUser, _ extsvc.AccountSpec, _ extsvc.AccountData) (int32, error) { + externalAccountsStore.CreateUserAndSaveFunc.SetDefaultHook(func(ctx context.Context, _ database.NewUser, _ extsvc.AccountSpec, _ extsvc.AccountData) (*types.User, error) { require.True(t, actor.FromContext(ctx).SourcegraphOperator, "the actor should be a Sourcegraph operator") - return 1, nil + return &types.User{ID: 1}, nil }) eventLogsStore := database.NewMockEventLogStore() eventLogsStore.BulkInsertFunc.SetDefaultHook(func(ctx context.Context, _ []*database.Event) error { @@ -655,22 +655,22 @@ func (m *mocks) LookupUserAndSave(_ context.Context, spec extsvc.AccountSpec, da } // CreateUserAndSave mocks database.ExternalAccounts.CreateUserAndSave -func (m *mocks) CreateUserAndSave(_ context.Context, newUser database.NewUser, spec extsvc.AccountSpec, data extsvc.AccountData) (createdUserID int32, err error) { +func (m *mocks) CreateUserAndSave(_ context.Context, newUser database.NewUser, spec extsvc.AccountSpec, data extsvc.AccountData) (createdUser *types.User, err error) { if m.createUserAndSaveErr != nil { - return 0, m.createUserAndSaveErr + return &types.User{}, m.createUserAndSaveErr } // Check if username already exists for _, u := range m.userInfos { if u.user.Username == newUser.Username { - return 0, database.MockCannotCreateUserUsernameExistsErr + return &types.User{}, database.MockCannotCreateUserUsernameExistsErr } } // Check if email already exists for _, u := range m.userInfos { for _, email := range u.emails { if email == newUser.Email { - return 0, database.MockCannotCreateUserEmailExistsErr + return &types.User{}, database.MockCannotCreateUserEmailExistsErr } } } @@ -686,7 +686,7 @@ func (m *mocks) CreateUserAndSave(_ context.Context, newUser database.NewUser, s // Save ext acct m.savedExtAccts[userID] = append(m.savedExtAccts[userID], spec) - return userID, nil + return &types.User{ID: userID}, nil } // AssociateUserAndSave mocks database.ExternalAccounts.AssociateUserAndSave diff --git a/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/middleware_test.go b/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/middleware_test.go index 731f76e66f13..dd52ecbf23a8 100644 --- a/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/middleware_test.go +++ b/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/middleware_test.go @@ -242,9 +242,9 @@ func TestMiddleware(t *testing.T) { CreatedAt: time.Now(), }, nil }) - userExternalAccountsStore.CreateUserAndSaveFunc.SetDefaultHook(func(_ context.Context, user database.NewUser, _ extsvc.AccountSpec, _ extsvc.AccountData) (int32, error) { + userExternalAccountsStore.CreateUserAndSaveFunc.SetDefaultHook(func(_ context.Context, user database.NewUser, _ extsvc.AccountSpec, _ extsvc.AccountData) (*types.User, error) { assert.True(t, strings.HasPrefix(user.Username, usernamePrefix), "%q does not have prefix %q", user.Username, usernamePrefix) - return 1, nil + return &types.User{ID: 1}, nil }) urlStr := fmt.Sprintf("http://example.com/.auth/sourcegraph-operator/callback?code=%s&state=%s", testCode, state.Encode()) diff --git a/enterprise/cmd/frontend/worker/auth/sourcegraph_operator_cleaner_test.go b/enterprise/cmd/frontend/worker/auth/sourcegraph_operator_cleaner_test.go index bc211343c49b..2dbf6c2567ae 100644 --- a/enterprise/cmd/frontend/worker/auth/sourcegraph_operator_cleaner_test.go +++ b/enterprise/cmd/frontend/worker/auth/sourcegraph_operator_cleaner_test.go @@ -65,7 +65,7 @@ func TestSourcegraphOperatorCleanHandler(t *testing.T) { ) require.NoError(t, err) - morganID, err := db.UserExternalAccounts().CreateUserAndSave( + morgan, err := db.UserExternalAccounts().CreateUserAndSave( ctx, database.NewUser{ Username: "morgan", @@ -79,11 +79,11 @@ func TestSourcegraphOperatorCleanHandler(t *testing.T) { extsvc.AccountData{}, ) require.NoError(t, err) - _, err = db.Handle().ExecContext(ctx, `UPDATE users SET created_at = $1 WHERE id = $2`, time.Now().Add(-61*time.Minute), morganID) + _, err = db.Handle().ExecContext(ctx, `UPDATE users SET created_at = $1 WHERE id = $2`, time.Now().Add(-61*time.Minute), morgan.ID) require.NoError(t, err) err = db.UserExternalAccounts().AssociateUserAndSave( ctx, - morganID, + morgan.ID, extsvc.AccountSpec{ ServiceType: extsvc.TypeGitHub, ServiceID: "https://github.com", @@ -109,7 +109,7 @@ func TestSourcegraphOperatorCleanHandler(t *testing.T) { ) require.NoError(t, err) - rileyID, err := db.UserExternalAccounts().CreateUserAndSave( + riley, err := db.UserExternalAccounts().CreateUserAndSave( ctx, database.NewUser{ Username: "riley", @@ -123,7 +123,7 @@ func TestSourcegraphOperatorCleanHandler(t *testing.T) { extsvc.AccountData{}, ) require.NoError(t, err) - _, err = db.Handle().ExecContext(ctx, `UPDATE users SET created_at = $1 WHERE id = $2`, time.Now().Add(-61*time.Minute), rileyID) + _, err = db.Handle().ExecContext(ctx, `UPDATE users SET created_at = $1 WHERE id = $2`, time.Now().Add(-61*time.Minute), riley.ID) require.NoError(t, err) _, err = db.UserExternalAccounts().CreateUserAndSave( @@ -145,7 +145,7 @@ func TestSourcegraphOperatorCleanHandler(t *testing.T) { ServiceAccount: true, }) require.NoError(t, err) - camiID, err := db.UserExternalAccounts().CreateUserAndSave( + cami, err := db.UserExternalAccounts().CreateUserAndSave( ctx, database.NewUser{ Username: "cami", @@ -159,7 +159,7 @@ func TestSourcegraphOperatorCleanHandler(t *testing.T) { accountData, ) require.NoError(t, err) - _, err = db.Handle().ExecContext(ctx, `UPDATE users SET created_at = $1 WHERE id = $2`, time.Now().Add(-61*time.Minute), camiID) + _, err = db.Handle().ExecContext(ctx, `UPDATE users SET created_at = $1 WHERE id = $2`, time.Now().Add(-61*time.Minute), cami.ID) require.NoError(t, err) t.Run("handle with cleanup", func(t *testing.T) { diff --git a/enterprise/cmd/repo-updater/internal/authz/integration_test.go b/enterprise/cmd/repo-updater/internal/authz/integration_test.go index 84e69cd2c877..4b7903377f9a 100644 --- a/enterprise/cmd/repo-updater/internal/authz/integration_test.go +++ b/enterprise/cmd/repo-updater/internal/authz/integration_test.go @@ -131,7 +131,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { t.Fatal(err) } - userID, err := testDB.UserExternalAccounts().CreateUserAndSave(ctx, newUser, spec, extsvc.AccountData{}) + user, err := testDB.UserExternalAccounts().CreateUserAndSave(ctx, newUser, spec, extsvc.AccountData{}) if err != nil { t.Fatal(err) } @@ -151,7 +151,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { }}, providerStates) p := &authz.UserPermissions{ - UserID: userID, + UserID: user.ID, Perm: authz.Read, Type: authz.PermRepos, } @@ -217,7 +217,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { t.Fatal(err) } - userID, err := testDB.UserExternalAccounts().CreateUserAndSave(ctx, newUser, spec, extsvc.AccountData{}) + user, err := testDB.UserExternalAccounts().CreateUserAndSave(ctx, newUser, spec, extsvc.AccountData{}) if err != nil { t.Fatal(err) } @@ -237,7 +237,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { }}, providerStates) p := &authz.UserPermissions{ - UserID: userID, + UserID: user.ID, Perm: authz.Read, Type: authz.PermRepos, } @@ -334,7 +334,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { } authData := json.RawMessage(fmt.Sprintf(`{"access_token": "%s"}`, token)) - userID, err := testDB.UserExternalAccounts().CreateUserAndSave(ctx, newUser, spec, extsvc.AccountData{ + user, err := testDB.UserExternalAccounts().CreateUserAndSave(ctx, newUser, spec, extsvc.AccountData{ AuthData: extsvc.NewUnencryptedData(authData), }) if err != nil { @@ -344,7 +344,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { permsStore := edb.Perms(logger, testDB, timeutil.Now) syncer := NewPermsSyncer(logger, testDB, reposStore, permsStore, timeutil.Now, nil) - providerStates, err := syncer.syncUserPerms(ctx, userID, false, authz.FetchPermsOptions{}) + providerStates, err := syncer.syncUserPerms(ctx, user.ID, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -356,7 +356,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { }}, providerStates) p := &authz.UserPermissions{ - UserID: userID, + UserID: user.ID, Perm: authz.Read, Type: authz.PermRepos, } @@ -423,7 +423,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { } authData := json.RawMessage(fmt.Sprintf(`{"access_token": "%s"}`, token)) - userID, err := testDB.UserExternalAccounts().CreateUserAndSave(ctx, newUser, spec, extsvc.AccountData{ + user, err := testDB.UserExternalAccounts().CreateUserAndSave(ctx, newUser, spec, extsvc.AccountData{ AuthData: extsvc.NewUnencryptedData(authData), }) if err != nil { @@ -433,7 +433,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { permsStore := edb.Perms(logger, testDB, timeutil.Now) syncer := NewPermsSyncer(logger, testDB, reposStore, permsStore, timeutil.Now, nil) - providerStates, err := syncer.syncUserPerms(ctx, userID, false, authz.FetchPermsOptions{}) + providerStates, err := syncer.syncUserPerms(ctx, user.ID, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -445,7 +445,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { }}, providerStates) p := &authz.UserPermissions{ - UserID: userID, + UserID: user.ID, Perm: authz.Read, Type: authz.PermRepos, } @@ -460,7 +460,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { } // sync again and check - providerStates, err = syncer.syncUserPerms(ctx, userID, false, authz.FetchPermsOptions{}) + providerStates, err = syncer.syncUserPerms(ctx, user.ID, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } diff --git a/internal/database/event_logs_test.go b/internal/database/event_logs_test.go index 5293bc44a3cc..27a133d30016 100644 --- a/internal/database/event_logs_test.go +++ b/internal/database/event_logs_test.go @@ -237,8 +237,8 @@ func TestEventLogs_SiteUsageMultiplePeriods(t *testing.T) { events := []*Event{ makeTestEvent(&Event{UserID: uint32(sgAdmin.ID), Timestamp: startDate}), makeTestEvent(&Event{UserID: uint32(sgAdmin.ID), Timestamp: startDate}), - makeTestEvent(&Event{UserID: uint32(soLoganID), Timestamp: startDate, PublicArgument: soPublicArgument}), - makeTestEvent(&Event{UserID: uint32(soLoganID), Timestamp: startDate, PublicArgument: soPublicArgument}), + makeTestEvent(&Event{UserID: uint32(soLoganID.ID), Timestamp: startDate, PublicArgument: soPublicArgument}), + makeTestEvent(&Event{UserID: uint32(soLoganID.ID), Timestamp: startDate, PublicArgument: soPublicArgument}), makeTestEvent(&Event{UserID: uint32(user1.ID), Timestamp: startDate}), makeTestEvent(&Event{UserID: uint32(user1.ID), Timestamp: startDate}), @@ -246,8 +246,8 @@ func TestEventLogs_SiteUsageMultiplePeriods(t *testing.T) { makeTestEvent(&Event{UserID: uint32(user1.ID), Timestamp: secondDay}), makeTestEvent(&Event{UserID: uint32(user2.ID), Timestamp: secondDay}), makeTestEvent(&Event{UserID: uint32(sgAdmin.ID), Timestamp: secondDay}), - makeTestEvent(&Event{UserID: uint32(soLoganID), Timestamp: secondDay, PublicArgument: soPublicArgument}), - makeTestEvent(&Event{UserID: uint32(soLoganID), Timestamp: secondDay, PublicArgument: soPublicArgument}), + makeTestEvent(&Event{UserID: uint32(soLoganID.ID), Timestamp: secondDay, PublicArgument: soPublicArgument}), + makeTestEvent(&Event{UserID: uint32(soLoganID.ID), Timestamp: secondDay, PublicArgument: soPublicArgument}), makeTestEvent(&Event{UserID: uint32(user1.ID), Timestamp: thirdDay}), makeTestEvent(&Event{UserID: uint32(user2.ID), Timestamp: thirdDay}), @@ -458,7 +458,7 @@ func TestEventLogs_SiteUsage_ExcludeSourcegraphAdmins(t *testing.T) { require.NoError(t, err) err = db.UserEmails().Add(ctx, sgAdmin.ID, "admin@sourcegraph.com", nil) require.NoError(t, err) - soLoganID, err := db.UserExternalAccounts().CreateUserAndSave( + soLogan, err := db.UserExternalAccounts().CreateUserAndSave( ctx, NewUser{ Username: "sourcegraph-operator-logan", @@ -487,7 +487,7 @@ func TestEventLogs_SiteUsage_ExcludeSourcegraphAdmins(t *testing.T) { []string{"test", "CODEHOSTINTEGRATION"}, }, now.Add(-time.Hour): { - []uint32{uint32(soLoganID)}, + []uint32{uint32(soLogan.ID)}, []string{"ViewSiteAdminX"}, []string{"test", "CODEHOSTINTEGRATION"}, }, @@ -498,7 +498,7 @@ func TestEventLogs_SiteUsage_ExcludeSourcegraphAdmins(t *testing.T) { []string{"test", "CODEHOSTINTEGRATION"}, }, now.Add(-time.Hour * 24 * 4): { - []uint32{uint32(soLoganID), uint32(user1.ID)}, + []uint32{uint32(soLogan.ID), uint32(user1.ID)}, []string{"ViewRepository", "ViewTree"}, []string{"test", "CODEHOSTINTEGRATION"}, }, @@ -524,7 +524,7 @@ func TestEventLogs_SiteUsage_ExcludeSourcegraphAdmins(t *testing.T) { Timestamp: day.Add(time.Minute * time.Duration(rand.Intn(60)-30)), } - if userID == uint32(soLoganID) { + if userID == uint32(soLogan.ID) { e.PublicArgument = json.RawMessage(fmt.Sprintf(`{"%s": true}`, EventLogsSourcegraphOperatorKey)) } diff --git a/internal/database/external_accounts.go b/internal/database/external_accounts.go index 829e0f90adaf..f15379c569c4 100644 --- a/internal/database/external_accounts.go +++ b/internal/database/external_accounts.go @@ -10,6 +10,7 @@ import ( "github.com/keegancsmith/sqlf" otlog "github.com/opentracing/opentracing-go/log" "github.com/sourcegraph/log" + "github.com/sourcegraph/sourcegraph/internal/types" "github.com/sourcegraph/sourcegraph/internal/database/basestore" "github.com/sourcegraph/sourcegraph/internal/encryption" @@ -51,7 +52,7 @@ type UserExternalAccountsStore interface { // // It creates a new user and associates it with the specified external account. If the user to // create already exists, it returns an error. - CreateUserAndSave(ctx context.Context, newUser NewUser, spec extsvc.AccountSpec, data extsvc.AccountData) (createdUserID int32, err error) + CreateUserAndSave(ctx context.Context, newUser NewUser, spec extsvc.AccountSpec, data extsvc.AccountData) (createdUser *types.User, err error) // Delete will soft delete all accounts matching the options combined using AND. // If options are all zero values then it does nothing. @@ -248,23 +249,23 @@ AND deleted_at IS NULL return nil } -func (s *userExternalAccountsStore) CreateUserAndSave(ctx context.Context, newUser NewUser, spec extsvc.AccountSpec, data extsvc.AccountData) (createdUserID int32, err error) { +func (s *userExternalAccountsStore) CreateUserAndSave(ctx context.Context, newUser NewUser, spec extsvc.AccountSpec, data extsvc.AccountData) (createdUser *types.User, err error) { tx, err := s.Transact(ctx) if err != nil { - return 0, err + return nil, err } defer func() { err = tx.Done(err) }() - createdUser, err := UsersWith(s.logger, tx).CreateInTransaction(ctx, newUser, &spec) + createdUser, err = UsersWith(s.logger, tx).CreateInTransaction(ctx, newUser, &spec) if err != nil { - return 0, err + return nil, err } err = tx.Insert(ctx, createdUser.ID, spec, data) if err == nil { logAccountCreatedEvent(ctx, NewDBWith(s.logger, s), createdUser, spec.ServiceType) } - return createdUser.ID, err + return createdUser, err } func (s *userExternalAccountsStore) Insert(ctx context.Context, userID int32, spec extsvc.AccountSpec, data extsvc.AccountData) (err error) { diff --git a/internal/database/external_accounts_test.go b/internal/database/external_accounts_test.go index 59b7c47c8e76..0d814d61f507 100644 --- a/internal/database/external_accounts_test.go +++ b/internal/database/external_accounts_test.go @@ -33,7 +33,7 @@ func TestExternalAccounts_LookupUserAndSave(t *testing.T) { ClientID: "xc", AccountID: "xd", } - userID, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{Username: "u"}, spec, extsvc.AccountData{}) + user, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{Username: "u"}, spec, extsvc.AccountData{}) if err != nil { t.Fatal(err) } @@ -42,8 +42,8 @@ func TestExternalAccounts_LookupUserAndSave(t *testing.T) { if err != nil { t.Fatal(err) } - if lookedUpUserID != userID { - t.Errorf("got %d, want %d", lookedUpUserID, userID) + if lookedUpUserID != user.ID { + t.Errorf("got %d, want %d", lookedUpUserID, user.ID) } } @@ -121,12 +121,7 @@ func TestExternalAccounts_CreateUserAndSave(t *testing.T) { AuthData: extsvc.NewUnencryptedData(authData), Data: extsvc.NewUnencryptedData(data), } - userID, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{Username: "u"}, spec, accountData) - if err != nil { - t.Fatal(err) - } - - user, err := db.Users().GetByID(ctx, userID) + user, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{Username: "u"}, spec, accountData) if err != nil { t.Fatal(err) } @@ -146,7 +141,7 @@ func TestExternalAccounts_CreateUserAndSave(t *testing.T) { account.ID = 0 want := &extsvc.Account{ - UserID: userID, + UserID: user.ID, AccountSpec: spec, AccountData: accountData, } @@ -171,12 +166,7 @@ func TestExternalAccounts_CreateUserAndSave_NilData(t *testing.T) { AccountID: "xd", } - userID, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{Username: "u"}, spec, extsvc.AccountData{}) - if err != nil { - t.Fatal(err) - } - - user, err := db.Users().GetByID(ctx, userID) + user, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{Username: "u"}, spec, extsvc.AccountData{}) if err != nil { t.Fatal(err) } @@ -196,7 +186,7 @@ func TestExternalAccounts_CreateUserAndSave_NilData(t *testing.T) { account.ID = 0 want := &extsvc.Account{ - UserID: userID, + UserID: user.ID, AccountSpec: spec, } if diff := cmp.Diff(want, account, et.CompareEncryptable); diff != "" { @@ -236,11 +226,11 @@ func TestExternalAccounts_List(t *testing.T) { userIDs := make([]int32, 0, len(specs)) for i, spec := range specs { - id, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{Username: fmt.Sprintf("u%d", i)}, spec, extsvc.AccountData{}) + user, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{Username: fmt.Sprintf("u%d", i)}, spec, extsvc.AccountData{}) if err != nil { t.Fatal(err) } - userIDs = append(userIDs, id) + userIDs = append(userIDs, user.ID) } specByID := make(map[int32]extsvc.AccountSpec) @@ -363,7 +353,7 @@ func TestExternalAccounts_Encryption(t *testing.T) { } // store with encrypted authdata - userID, err := store.CreateUserAndSave(ctx, NewUser{Username: "u"}, spec, accountData) + user, err := store.CreateUserAndSave(ctx, NewUser{Username: "u"}, spec, accountData) if err != nil { t.Fatal(err) } @@ -397,7 +387,7 @@ func TestExternalAccounts_Encryption(t *testing.T) { // List should return decrypted data account := listFirstAccount(store) want := extsvc.Account{ - UserID: userID, + UserID: user.ID, AccountSpec: spec, AccountData: accountData, } @@ -406,7 +396,7 @@ func TestExternalAccounts_Encryption(t *testing.T) { } // LookupUserAndSave should encrypt the accountData correctly - userID, err = store.LookupUserAndSave(ctx, spec, accountData) + userID, err := store.LookupUserAndSave(ctx, spec, accountData) if err != nil { t.Fatal(err) } @@ -446,12 +436,12 @@ func TestExternalAccounts_expiredAt(t *testing.T) { ClientID: "xc", AccountID: "xd", } - userID, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{Username: "u"}, spec, extsvc.AccountData{}) + user, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{Username: "u"}, spec, extsvc.AccountData{}) if err != nil { t.Fatal(err) } - accts, err := db.UserExternalAccounts().List(ctx, ExternalAccountsListOptions{UserID: userID}) + accts, err := db.UserExternalAccounts().List(ctx, ExternalAccountsListOptions{UserID: user.ID}) if err != nil { t.Fatal(err) } else if len(accts) != 1 { @@ -466,7 +456,7 @@ func TestExternalAccounts_expiredAt(t *testing.T) { } accts, err := db.UserExternalAccounts().List(ctx, ExternalAccountsListOptions{ - UserID: userID, + UserID: user.ID, ExcludeExpired: true, }) if err != nil { @@ -485,7 +475,7 @@ func TestExternalAccounts_expiredAt(t *testing.T) { } accts, err := db.UserExternalAccounts().List(ctx, ExternalAccountsListOptions{ - UserID: userID, + UserID: user.ID, OnlyExpired: true, }) if err != nil { @@ -509,7 +499,7 @@ func TestExternalAccounts_expiredAt(t *testing.T) { } accts, err := db.UserExternalAccounts().List(ctx, ExternalAccountsListOptions{ - UserID: userID, + UserID: user.ID, ExcludeExpired: true, }) if err != nil { @@ -527,13 +517,13 @@ func TestExternalAccounts_expiredAt(t *testing.T) { t.Fatal(err) } - err = db.UserExternalAccounts().AssociateUserAndSave(ctx, userID, spec, extsvc.AccountData{}) + err = db.UserExternalAccounts().AssociateUserAndSave(ctx, user.ID, spec, extsvc.AccountData{}) if err != nil { t.Fatal(err) } accts, err := db.UserExternalAccounts().List(ctx, ExternalAccountsListOptions{ - UserID: userID, + UserID: user.ID, ExcludeExpired: true, }) if err != nil { @@ -557,7 +547,7 @@ func TestExternalAccounts_expiredAt(t *testing.T) { } accts, err := db.UserExternalAccounts().List(ctx, ExternalAccountsListOptions{ - UserID: userID, + UserID: user.ID, ExcludeExpired: true, }) if err != nil { @@ -585,13 +575,13 @@ func TestExternalAccounts_DeleteList(t *testing.T) { AccountID: "xd", } - userID, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{Username: "u"}, spec, extsvc.AccountData{}) + user, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{Username: "u"}, spec, extsvc.AccountData{}) spec.ServiceID = "xb2" require.NoError(t, err) - err = db.UserExternalAccounts().Insert(ctx, userID, spec, extsvc.AccountData{}) + err = db.UserExternalAccounts().Insert(ctx, user.ID, spec, extsvc.AccountData{}) require.NoError(t, err) spec.ServiceID = "xb3" - err = db.UserExternalAccounts().Insert(ctx, userID, spec, extsvc.AccountData{}) + err = db.UserExternalAccounts().Insert(ctx, user.ID, spec, extsvc.AccountData{}) require.NoError(t, err) accts, err := db.UserExternalAccounts().List(ctx, ExternalAccountsListOptions{UserID: 1}) @@ -628,13 +618,13 @@ func TestExternalAccounts_TouchExpiredList(t *testing.T) { AccountID: "xd", } - userID, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{Username: "u"}, spec, extsvc.AccountData{}) + user, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{Username: "u"}, spec, extsvc.AccountData{}) spec.ServiceID = "xb2" require.NoError(t, err) - err = db.UserExternalAccounts().Insert(ctx, userID, spec, extsvc.AccountData{}) + err = db.UserExternalAccounts().Insert(ctx, user.ID, spec, extsvc.AccountData{}) require.NoError(t, err) spec.ServiceID = "xb3" - err = db.UserExternalAccounts().Insert(ctx, userID, spec, extsvc.AccountData{}) + err = db.UserExternalAccounts().Insert(ctx, user.ID, spec, extsvc.AccountData{}) require.NoError(t, err) accts, err := db.UserExternalAccounts().List(ctx, ExternalAccountsListOptions{UserID: 1}) diff --git a/internal/database/mocks_temp.go b/internal/database/mocks_temp.go index dc82d730c272..d1a4bb8e4121 100644 --- a/internal/database/mocks_temp.go +++ b/internal/database/mocks_temp.go @@ -50786,7 +50786,7 @@ func NewMockUserExternalAccountsStore() *MockUserExternalAccountsStore { }, }, CreateUserAndSaveFunc: &UserExternalAccountsStoreCreateUserAndSaveFunc{ - defaultHook: func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (r0 int32, r1 error) { + defaultHook: func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (r0 *types.User, r1 error) { return }, }, @@ -50879,7 +50879,7 @@ func NewStrictMockUserExternalAccountsStore() *MockUserExternalAccountsStore { }, }, CreateUserAndSaveFunc: &UserExternalAccountsStoreCreateUserAndSaveFunc{ - defaultHook: func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (int32, error) { + defaultHook: func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (*types.User, error) { panic("unexpected invocation of MockUserExternalAccountsStore.CreateUserAndSave") }, }, @@ -51243,15 +51243,15 @@ func (c UserExternalAccountsStoreCountFuncCall) Results() []interface{} { // when the CreateUserAndSave method of the parent // MockUserExternalAccountsStore instance is invoked. type UserExternalAccountsStoreCreateUserAndSaveFunc struct { - defaultHook func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (int32, error) - hooks []func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (int32, error) + defaultHook func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (*types.User, error) + hooks []func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (*types.User, error) history []UserExternalAccountsStoreCreateUserAndSaveFuncCall mutex sync.Mutex } // CreateUserAndSave delegates to the next hook function in the queue and // stores the parameter and result values of this invocation. -func (m *MockUserExternalAccountsStore) CreateUserAndSave(v0 context.Context, v1 NewUser, v2 extsvc.AccountSpec, v3 extsvc.AccountData) (int32, error) { +func (m *MockUserExternalAccountsStore) CreateUserAndSave(v0 context.Context, v1 NewUser, v2 extsvc.AccountSpec, v3 extsvc.AccountData) (*types.User, error) { r0, r1 := m.CreateUserAndSaveFunc.nextHook()(v0, v1, v2, v3) m.CreateUserAndSaveFunc.appendCall(UserExternalAccountsStoreCreateUserAndSaveFuncCall{v0, v1, v2, v3, r0, r1}) return r0, r1 @@ -51260,7 +51260,7 @@ func (m *MockUserExternalAccountsStore) CreateUserAndSave(v0 context.Context, v1 // SetDefaultHook sets function that is called when the CreateUserAndSave // method of the parent MockUserExternalAccountsStore instance is invoked // and the hook queue is empty. -func (f *UserExternalAccountsStoreCreateUserAndSaveFunc) SetDefaultHook(hook func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (int32, error)) { +func (f *UserExternalAccountsStoreCreateUserAndSaveFunc) SetDefaultHook(hook func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (*types.User, error)) { f.defaultHook = hook } @@ -51269,7 +51269,7 @@ func (f *UserExternalAccountsStoreCreateUserAndSaveFunc) SetDefaultHook(hook fun // instance invokes the hook at the front of the queue and discards it. // After the queue is empty, the default hook function is invoked for any // future action. -func (f *UserExternalAccountsStoreCreateUserAndSaveFunc) PushHook(hook func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (int32, error)) { +func (f *UserExternalAccountsStoreCreateUserAndSaveFunc) PushHook(hook func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (*types.User, error)) { f.mutex.Lock() f.hooks = append(f.hooks, hook) f.mutex.Unlock() @@ -51277,20 +51277,20 @@ func (f *UserExternalAccountsStoreCreateUserAndSaveFunc) PushHook(hook func(cont // SetDefaultReturn calls SetDefaultHook with a function that returns the // given values. -func (f *UserExternalAccountsStoreCreateUserAndSaveFunc) SetDefaultReturn(r0 int32, r1 error) { - f.SetDefaultHook(func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (int32, error) { +func (f *UserExternalAccountsStoreCreateUserAndSaveFunc) SetDefaultReturn(r0 *types.User, r1 error) { + f.SetDefaultHook(func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (*types.User, error) { return r0, r1 }) } // PushReturn calls PushHook with a function that returns the given values. -func (f *UserExternalAccountsStoreCreateUserAndSaveFunc) PushReturn(r0 int32, r1 error) { - f.PushHook(func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (int32, error) { +func (f *UserExternalAccountsStoreCreateUserAndSaveFunc) PushReturn(r0 *types.User, r1 error) { + f.PushHook(func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (*types.User, error) { return r0, r1 }) } -func (f *UserExternalAccountsStoreCreateUserAndSaveFunc) nextHook() func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (int32, error) { +func (f *UserExternalAccountsStoreCreateUserAndSaveFunc) nextHook() func(context.Context, NewUser, extsvc.AccountSpec, extsvc.AccountData) (*types.User, error) { f.mutex.Lock() defer f.mutex.Unlock() @@ -51339,7 +51339,7 @@ type UserExternalAccountsStoreCreateUserAndSaveFuncCall struct { Arg3 extsvc.AccountData // Result0 is the value of the 1st result returned from this method // invocation. - Result0 int32 + Result0 *types.User // Result1 is the value of the 2nd result returned from this method // invocation. Result1 error diff --git a/internal/database/users_builtin_auth_test.go b/internal/database/users_builtin_auth_test.go index 92ec56ddeb5e..a93f9320061a 100644 --- a/internal/database/users_builtin_auth_test.go +++ b/internal/database/users_builtin_auth_test.go @@ -271,7 +271,7 @@ func TestUsers_CreatePassword(t *testing.T) { } // A new user with an external account can't create a password - newID, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{ + newUser, err := db.UserExternalAccounts().CreateUserAndSave(ctx, NewUser{ Email: "usr3@bar.com", Username: "usr3", Password: "", @@ -292,7 +292,7 @@ func TestUsers_CreatePassword(t *testing.T) { t.Fatal(err) } - if err := db.Users().CreatePassword(ctx, newID, "the-new-password"); err == nil { + if err := db.Users().CreatePassword(ctx, newUser.ID, "the-new-password"); err == nil { t.Fatal("Should fail, user has external account") } } diff --git a/internal/database/users_test.go b/internal/database/users_test.go index 61ceccbaf488..dfbae83b041e 100644 --- a/internal/database/users_test.go +++ b/internal/database/users_test.go @@ -456,14 +456,14 @@ func TestUsers_ListForSCIM_Query(t *testing.T) { {NewUser: NewUser{Email: "charlie@example.com", Username: "charlie", EmailIsVerified: true}, SCIMExternalID: "CHARLIE", AdditionalVerifiedEmails: []string{"charlie2@example.com"}}, } for _, newUser := range newUsers { - id, err := db.UserExternalAccounts().CreateUserAndSave(ctx, newUser.NewUser, extsvc.AccountSpec{ServiceType: "scim", AccountID: newUser.SCIMExternalID}, extsvc.AccountData{}) + user, err := db.UserExternalAccounts().CreateUserAndSave(ctx, newUser.NewUser, extsvc.AccountSpec{ServiceType: "scim", AccountID: newUser.SCIMExternalID}, extsvc.AccountData{}) for _, email := range newUser.AdditionalVerifiedEmails { verificationCode := "x" - err := db.UserEmails().Add(ctx, id, email, &verificationCode) + err := db.UserEmails().Add(ctx, user.ID, email, &verificationCode) if err != nil { t.Fatal(err) } - _, err = db.UserEmails().Verify(ctx, id, email, verificationCode) + _, err = db.UserEmails().Verify(ctx, user.ID, email, verificationCode) if err != nil { t.Fatal(err) } From 72bb4b4b2240ddb19b1b5e184921647eed2e6be8 Mon Sep 17 00:00:00 2001 From: Naman Kumar Date: Fri, 3 Feb 2023 14:16:45 +0530 Subject: [PATCH 385/678] [Permissions] records result of added, removed and found permissions (#47354) * [Permissions] records result of added, removed and found permissions --- .../internal/authz/resolvers/resolver.go | 2 +- .../internal/authz/resolvers/resolver_test.go | 6 +- .../internal/authz/integration_test.go | 12 +- .../internal/authz/perms_syncer.go | 51 ++-- .../internal/authz/perms_syncer_test.go | 54 ++-- .../internal/authz/perms_syncer_worker.go | 48 ++-- .../authz/perms_syncer_worker_test.go | 101 +++++--- enterprise/cmd/repo-updater/shared/shared.go | 5 +- .../permissions/bitbucket_projects.go | 2 +- .../batches/testing/mock_repo_perms.go | 2 +- enterprise/internal/database/authz_test.go | 4 +- enterprise/internal/database/mocks_temp.go | 82 ++++--- enterprise/internal/database/perms_store.go | 44 ++-- .../internal/database/perms_store_test.go | 232 +++++++++++++----- internal/database/mocks_temp.go | 128 ++++++++++ internal/database/permission_sync_jobs.go | 33 +++ .../database/permission_sync_jobs_test.go | 41 ++++ internal/database/schema.json | 39 +++ internal/database/schema.md | 3 + .../down.sql | 3 + .../metadata.yaml | 2 + .../up.sql | 3 + migrations/frontend/squashed.sql | 3 + 23 files changed, 666 insertions(+), 234 deletions(-) create mode 100644 migrations/frontend/1675367314_add_results_to_permission_sync_jobs/down.sql create mode 100644 migrations/frontend/1675367314_add_results_to_permission_sync_jobs/metadata.yaml create mode 100644 migrations/frontend/1675367314_add_results_to_permission_sync_jobs/up.sql diff --git a/enterprise/cmd/frontend/internal/authz/resolvers/resolver.go b/enterprise/cmd/frontend/internal/authz/resolvers/resolver.go index bbd8ba989f12..949699c32b7b 100644 --- a/enterprise/cmd/frontend/internal/authz/resolvers/resolver.go +++ b/enterprise/cmd/frontend/internal/authz/resolvers/resolver.go @@ -129,7 +129,7 @@ func (r *Resolver) SetRepositoryPermissionsForUsers(ctx context.Context, args *g AccountIDs: pendingBindIDs, } - if err = txs.SetRepoPermissions(ctx, p); err != nil { + if _, err = txs.SetRepoPermissions(ctx, p); err != nil { return nil, errors.Wrap(err, "set repository permissions") } else if err = txs.SetRepoPendingPermissions(ctx, accounts, p); err != nil { return nil, errors.Wrap(err, "set repository pending permissions") diff --git a/enterprise/cmd/frontend/internal/authz/resolvers/resolver_test.go b/enterprise/cmd/frontend/internal/authz/resolvers/resolver_test.go index e9177a3f7e70..0890de10290d 100644 --- a/enterprise/cmd/frontend/internal/authz/resolvers/resolver_test.go +++ b/enterprise/cmd/frontend/internal/authz/resolvers/resolver_test.go @@ -189,12 +189,12 @@ func TestResolver_SetRepositoryPermissionsForUsers(t *testing.T) { perms := edb.NewStrictMockPermsStore() perms.TransactFunc.SetDefaultReturn(perms, nil) perms.DoneFunc.SetDefaultReturn(nil) - perms.SetRepoPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.RepoPermissions) error { + perms.SetRepoPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.RepoPermissions) (*database.SetPermissionsResult, error) { ids := p.UserIDs if diff := cmp.Diff(test.expUserIDs, ids); diff != "" { - return errors.Errorf("p.UserIDs: %v", diff) + return nil, errors.Errorf("p.UserIDs: %v", diff) } - return nil + return nil, nil }) perms.SetRepoPendingPermissionsFunc.SetDefaultHook(func(_ context.Context, accounts *extsvc.Accounts, _ *authz.RepoPermissions) error { if diff := cmp.Diff(test.expAccounts, accounts); diff != "" { diff --git a/enterprise/cmd/repo-updater/internal/authz/integration_test.go b/enterprise/cmd/repo-updater/internal/authz/integration_test.go index 4b7903377f9a..ac353adf5e36 100644 --- a/enterprise/cmd/repo-updater/internal/authz/integration_test.go +++ b/enterprise/cmd/repo-updater/internal/authz/integration_test.go @@ -139,7 +139,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { permsStore := edb.Perms(logger, testDB, timeutil.Now) syncer := NewPermsSyncer(logger, testDB, reposStore, permsStore, timeutil.Now, nil) - providerStates, err := syncer.syncRepoPerms(ctx, repo.ID, false, authz.FetchPermsOptions{}) + _, providerStates, err := syncer.syncRepoPerms(ctx, repo.ID, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -225,7 +225,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { permsStore := edb.Perms(logger, testDB, timeutil.Now) syncer := NewPermsSyncer(logger, testDB, reposStore, permsStore, timeutil.Now, nil) - providerStates, err := syncer.syncRepoPerms(ctx, repo.ID, false, authz.FetchPermsOptions{}) + _, providerStates, err := syncer.syncRepoPerms(ctx, repo.ID, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -252,7 +252,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { } // sync again and check - providerStates, err = syncer.syncRepoPerms(ctx, repo.ID, false, authz.FetchPermsOptions{}) + _, providerStates, err = syncer.syncRepoPerms(ctx, repo.ID, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -344,7 +344,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { permsStore := edb.Perms(logger, testDB, timeutil.Now) syncer := NewPermsSyncer(logger, testDB, reposStore, permsStore, timeutil.Now, nil) - providerStates, err := syncer.syncUserPerms(ctx, user.ID, false, authz.FetchPermsOptions{}) + _, providerStates, err := syncer.syncUserPerms(ctx, user.ID, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -433,7 +433,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { permsStore := edb.Perms(logger, testDB, timeutil.Now) syncer := NewPermsSyncer(logger, testDB, reposStore, permsStore, timeutil.Now, nil) - providerStates, err := syncer.syncUserPerms(ctx, user.ID, false, authz.FetchPermsOptions{}) + _, providerStates, err := syncer.syncUserPerms(ctx, user.ID, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -460,7 +460,7 @@ func TestIntegration_GitHubPermissions(t *testing.T) { } // sync again and check - providerStates, err = syncer.syncUserPerms(ctx, user.ID, false, authz.FetchPermsOptions{}) + _, providerStates, err = syncer.syncUserPerms(ctx, user.ID, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } diff --git a/enterprise/cmd/repo-updater/internal/authz/perms_syncer.go b/enterprise/cmd/repo-updater/internal/authz/perms_syncer.go index e819f1d675a1..ad7d3106bfc8 100644 --- a/enterprise/cmd/repo-updater/internal/authz/perms_syncer.go +++ b/enterprise/cmd/repo-updater/internal/authz/perms_syncer.go @@ -558,13 +558,13 @@ func (s *PermsSyncer) fetchUserPermsViaExternalAccounts(ctx context.Context, use // syncUserPerms processes permissions syncing request in user-centric way. When `noPerms` is true, // the method will use partial results to update permissions tables even when error occurs. -func (s *PermsSyncer) syncUserPerms(ctx context.Context, userID int32, noPerms bool, fetchOpts authz.FetchPermsOptions) (providerStates []syncjobs.ProviderStatus, err error) { +func (s *PermsSyncer) syncUserPerms(ctx context.Context, userID int32, noPerms bool, fetchOpts authz.FetchPermsOptions) (result *database.SetPermissionsResult, providerStates []syncjobs.ProviderStatus, err error) { ctx, save := s.observe(ctx, "PermsSyncer.syncUserPerms", "") defer save(requestTypeUser, userID, &err) user, err := s.db.Users().GetByID(ctx, userID) if err != nil { - return providerStates, errors.Wrap(err, "get user") + return result, providerStates, errors.Wrap(err, "get user") } logger := s.logger.Scoped("syncUserPerms", "processes permissions sync request in user-centric way").With( @@ -585,7 +585,7 @@ func (s *PermsSyncer) syncUserPerms(ctx context.Context, userID int32, noPerms b providerStates = results.providerStates if err != nil { tryTouchUserPerms() - return providerStates, errors.Wrapf(err, "fetch permissions via external accounts for user %q (id: %d)", user.Username, user.ID) + return result, providerStates, errors.Wrapf(err, "fetch permissions via external accounts for user %q (id: %d)", user.Username, user.ID) } // fetch current permissions from database @@ -613,7 +613,7 @@ func (s *PermsSyncer) syncUserPerms(ctx context.Context, userID int32, noPerms b srp := edb.NewEnterpriseDB(s.db).SubRepoPerms() for spec, perm := range results.subRepoPerms { if err := srp.UpsertWithSpec(ctx, user.ID, spec, *perm); err != nil { - return providerStates, errors.Wrapf(err, "upserting sub repo perms %v for user %q (id: %d)", spec, user.Username, user.ID) + return result, providerStates, errors.Wrapf(err, "upserting sub repo perms %v for user %q (id: %d)", spec, user.Username, user.ID) } } @@ -627,9 +627,9 @@ func (s *PermsSyncer) syncUserPerms(ctx context.Context, userID int32, noPerms b s.permsUpdateLock.Lock() defer s.permsUpdateLock.Unlock() - err = s.permsStore.SetUserPermissions(ctx, p) + result, err = s.permsStore.SetUserPermissions(ctx, p) if err != nil { - return providerStates, errors.Wrapf(err, "set user permissions for user %q (id: %d)", user.Username, user.ID) + return result, providerStates, errors.Wrapf(err, "set user permissions for user %q (id: %d)", user.Username, user.ID) } logger.Debug("synced", @@ -646,22 +646,22 @@ func (s *PermsSyncer) syncUserPerms(ctx context.Context, userID int32, noPerms b metricsPermsFirstSyncDelay.WithLabelValues("user").Set(p.SyncedAt.Sub(user.CreatedAt).Seconds()) } - return providerStates, nil + return result, providerStates, nil } // syncRepoPerms processes permissions syncing request in repository-centric way. // When `noPerms` is true, the method will use partial results to update permissions // tables even when error occurs. -func (s *PermsSyncer) syncRepoPerms(ctx context.Context, repoID api.RepoID, noPerms bool, fetchOpts authz.FetchPermsOptions) (providerStates []syncjobs.ProviderStatus, err error) { +func (s *PermsSyncer) syncRepoPerms(ctx context.Context, repoID api.RepoID, noPerms bool, fetchOpts authz.FetchPermsOptions) (result *database.SetPermissionsResult, providerStates []syncjobs.ProviderStatus, err error) { ctx, save := s.observe(ctx, "PermsSyncer.syncRepoPerms", "") defer save(requestTypeRepo, int32(repoID), &err) repo, err := s.reposStore.RepoStore().Get(ctx, repoID) if err != nil { if errcode.IsNotFound(err) { - return providerStates, nil + return result, providerStates, nil } - return providerStates, errors.Wrap(err, "get repository") + return result, providerStates, errors.Wrap(err, "get repository") } var provider authz.Provider @@ -694,14 +694,14 @@ func (s *PermsSyncer) syncRepoPerms(ctx context.Context, repoID api.RepoID, noPe // We have no authz provider configured for the repository. // However, we need to upsert the dummy record in order to // prevent scheduler keep scheduling this repository. - return providerStates, errors.Wrap(s.permsStore.TouchRepoPermissions(ctx, int32(repoID)), "touch repository permissions") + return result, providerStates, errors.Wrap(s.permsStore.TouchRepoPermissions(ctx, int32(repoID)), "touch repository permissions") } pendingAccountIDsSet := make(map[string]struct{}) accountIDsToUserIDs := make(map[string]int32) // Account ID -> User ID if err := s.waitForRateLimit(ctx, provider.URN(), 1, "repo"); err != nil { - return providerStates, errors.Wrap(err, "wait for rate limiter") + return result, providerStates, errors.Wrap(err, "wait for rate limiter") } extAccountIDs, err := provider.FetchRepoPerms(ctx, &extsvc.Repository{ @@ -720,7 +720,7 @@ func (s *PermsSyncer) syncRepoPerms(ctx context.Context, repoID api.RepoID, noPe log.Error(err), log.String("suggestion", "GitHub access token user may only have read access to the repository, but needs write for permissions"), ) - return providerStates, errors.Wrap(s.permsStore.TouchRepoPermissions(ctx, int32(repoID)), "touch repository permissions") + return result, providerStates, errors.Wrap(s.permsStore.TouchRepoPermissions(ctx, int32(repoID)), "touch repository permissions") } // Skip repo if unimplemented @@ -733,13 +733,13 @@ func (s *PermsSyncer) syncRepoPerms(ctx context.Context, repoID api.RepoID, noPe logger.Warn("error touching permissions for unimplemented authz provider", log.Error(err)) } - return providerStates, nil + return result, providerStates, nil } if err != nil { // Process partial results if this is an initial fetch. if !noPerms { - return providerStates, errors.Wrapf(err, "fetch repository permissions for repository %q (id: %d)", repo.Name, repo.ID) + return result, providerStates, errors.Wrapf(err, "fetch repository permissions for repository %q (id: %d)", repo.Name, repo.ID) } logger.Warn("proceedWithPartialResults", log.Error(err)) } @@ -758,7 +758,7 @@ func (s *PermsSyncer) syncRepoPerms(ctx context.Context, repoID api.RepoID, noPe }) if err != nil { - return providerStates, errors.Wrapf(err, "get user IDs by external accounts for repository %q (id: %d)", repo.Name, repo.ID) + return result, providerStates, errors.Wrapf(err, "get user IDs by external accounts for repository %q (id: %d)", repo.Name, repo.ID) } // Set up the set of all account IDs that need to be bound to permissions @@ -802,12 +802,13 @@ func (s *PermsSyncer) syncRepoPerms(ctx context.Context, repoID api.RepoID, noPe txs, err := s.permsStore.Transact(ctx) if err != nil { - return providerStates, errors.Wrapf(err, "start transaction for repository %q (id: %d)", repo.Name, repo.ID) + return result, providerStates, errors.Wrapf(err, "start transaction for repository %q (id: %d)", repo.Name, repo.ID) } defer func() { err = txs.Done(err) }() - if err = txs.SetRepoPermissions(ctx, p); err != nil { - return providerStates, errors.Wrapf(err, "set repository permissions for repository %q (id: %d)", repo.Name, repo.ID) + result, err = txs.SetRepoPermissions(ctx, p) + if err != nil { + return result, providerStates, errors.Wrapf(err, "set repository permissions for repository %q (id: %d)", repo.Name, repo.ID) } regularCount := len(p.UserIDs) @@ -817,7 +818,7 @@ func (s *PermsSyncer) syncRepoPerms(ctx context.Context, repoID api.RepoID, noPe AccountIDs: pendingAccountIDs, } if err = txs.SetRepoPendingPermissions(ctx, accounts, p); err != nil { - return providerStates, errors.Wrapf(err, "set repository pending permissions for repository %q (id: %d)", repo.Name, repo.ID) + return result, providerStates, errors.Wrapf(err, "set repository pending permissions for repository %q (id: %d)", repo.Name, repo.ID) } pendingCount := len(p.UserIDs) @@ -842,7 +843,7 @@ func (s *PermsSyncer) syncRepoPerms(ctx context.Context, repoID api.RepoID, noPe delayMetricField, ) - return providerStates, nil + return result, providerStates, nil } // waitForRateLimit blocks until rate limit permits n events to happen. It returns @@ -878,16 +879,16 @@ func (s *PermsSyncer) syncPerms(ctx context.Context, syncGroups map[requestType] defer s.queue.remove(request.Type, request.ID, true) - var runSync func() (providerStatesSet, error) + var runSync func() (*database.SetPermissionsResult, providerStatesSet, error) switch request.Type { case requestTypeUser: - runSync = func() (providerStatesSet, error) { + runSync = func() (*database.SetPermissionsResult, providerStatesSet, error) { // Ensure the job field is recorded when monitoring external API calls ctx = metrics.ContextWithTask(ctx, "SyncUserPerms") return s.syncUserPerms(ctx, request.ID, request.NoPerms, request.Options) } case requestTypeRepo: - runSync = func() (providerStatesSet, error) { + runSync = func() (*database.SetPermissionsResult, providerStatesSet, error) { // Ensure the job field is recorded when monitoring external API calls ctx = metrics.ContextWithTask(ctx, "SyncRepoPerms") return s.syncRepoPerms(ctx, api.RepoID(request.ID), request.NoPerms, request.Options) @@ -903,7 +904,7 @@ func (s *PermsSyncer) syncPerms(ctx context.Context, syncGroups map[requestType] metricsConcurrentSyncs.WithLabelValues(request.Type.String()).Inc() defer metricsConcurrentSyncs.WithLabelValues(request.Type.String()).Dec() - providerStates, err := runSync() + _, providerStates, err := runSync() if err != nil { logger.Error("failed to sync permissions", providerStates.SummaryField(), diff --git a/enterprise/cmd/repo-updater/internal/authz/perms_syncer_test.go b/enterprise/cmd/repo-updater/internal/authz/perms_syncer_test.go index bc3c59bc3e1b..bb3af7478a9f 100644 --- a/enterprise/cmd/repo-updater/internal/authz/perms_syncer_test.go +++ b/enterprise/cmd/repo-updater/internal/authz/perms_syncer_test.go @@ -162,10 +162,10 @@ func TestPermsSyncer_syncUserPerms(t *testing.T) { reposStore.RepoStoreFunc.SetDefaultReturn(mockRepos) perms := edb.NewMockPermsStore() - perms.SetUserPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.UserPermissions) error { + perms.SetUserPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.UserPermissions) (*database.SetPermissionsResult, error) { wantIDs := []int32{1, 2, 3, 4} assert.Equal(t, wantIDs, p.GenerateSortedIDsSlice()) - return nil + return nil, nil }) s := NewPermsSyncer(logtest.Scoped(t), db, reposStore, perms, timeutil.Now, nil) @@ -176,7 +176,7 @@ func TestPermsSyncer_syncUserPerms(t *testing.T) { }, nil } - providers, err := s.syncUserPerms(context.Background(), 1, true, authz.FetchPermsOptions{}) + _, providers, err := s.syncUserPerms(context.Background(), 1, true, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -237,10 +237,10 @@ func TestPermsSyncer_syncUserPerms_touchUserPermissions(t *testing.T) { reposStore.RepoStoreFunc.SetDefaultReturn(mockRepos) perms := edb.NewMockPermsStore() - perms.SetUserPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.UserPermissions) error { + perms.SetUserPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.UserPermissions) (*database.SetPermissionsResult, error) { wantIDs := []int32{1, 2, 3, 4, 5} assert.Equal(t, wantIDs, p.GenerateSortedIDsSlice()) - return nil + return nil, nil }) perms.TouchUserPermissionsFunc.SetDefaultHook(func(ctx context.Context, i int32) error { return nil @@ -249,7 +249,7 @@ func TestPermsSyncer_syncUserPerms_touchUserPermissions(t *testing.T) { s := NewPermsSyncer(logtest.Scoped(t), db, reposStore, perms, timeutil.Now, nil) t.Run("fetchUserPermsViaExternalAccounts", func(t *testing.T) { - _, err := s.syncUserPerms(context.Background(), 1, true, authz.FetchPermsOptions{}) + _, _, err := s.syncUserPerms(context.Background(), 1, true, authz.FetchPermsOptions{}) if err == nil { t.Fatal("expected an error") } @@ -318,10 +318,10 @@ func TestPermsSyncer_syncUserPermsTemporaryProviderError(t *testing.T) { reposStore.RepoStoreFunc.SetDefaultReturn(mockRepos) perms := edb.NewMockPermsStore() - perms.SetUserPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.UserPermissions) error { + perms.SetUserPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.UserPermissions) (*database.SetPermissionsResult, error) { wantIDs := []int32{} assert.Equal(t, wantIDs, p.GenerateSortedIDsSlice()) - return nil + return nil, nil }) s := NewPermsSyncer(logtest.Scoped(t), db, reposStore, perms, timeutil.Now, nil) @@ -331,7 +331,7 @@ func TestPermsSyncer_syncUserPermsTemporaryProviderError(t *testing.T) { return nil, context.DeadlineExceeded } - providers, err := s.syncUserPerms(context.Background(), 1, true, authz.FetchPermsOptions{}) + _, providers, err := s.syncUserPerms(context.Background(), 1, true, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -386,10 +386,10 @@ func TestPermsSyncer_syncUserPerms_noPerms(t *testing.T) { reposStore.RepoStoreFunc.SetDefaultReturn(mockRepos) perms := edb.NewMockPermsStore() - perms.SetUserPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.UserPermissions) error { + perms.SetUserPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.UserPermissions) (*database.SetPermissionsResult, error) { assert.Equal(t, int32(1), p.UserID) assert.Equal(t, []int32{1}, p.GenerateSortedIDsSlice()) - return nil + return nil, nil }) s := NewPermsSyncer(logtest.Scoped(t), db, reposStore, perms, timeutil.Now, nil) @@ -417,7 +417,7 @@ func TestPermsSyncer_syncUserPerms_noPerms(t *testing.T) { }, test.fetchErr } - _, err := s.syncUserPerms(context.Background(), 1, test.noPerms, authz.FetchPermsOptions{}) + _, _, err := s.syncUserPerms(context.Background(), 1, test.noPerms, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -476,7 +476,7 @@ func TestPermsSyncer_syncUserPerms_tokenExpire(t *testing.T) { return nil, &github.APIError{Code: http.StatusUnauthorized} } - _, err := s.syncUserPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) + _, _, err := s.syncUserPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -489,7 +489,7 @@ func TestPermsSyncer_syncUserPerms_tokenExpire(t *testing.T) { return nil, gitlab.NewHTTPError(http.StatusForbidden, nil) } - _, err := s.syncUserPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) + _, _, err := s.syncUserPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -506,7 +506,7 @@ func TestPermsSyncer_syncUserPerms_tokenExpire(t *testing.T) { } } - _, err := s.syncUserPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) + _, _, err := s.syncUserPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -575,7 +575,7 @@ func TestPermsSyncer_syncUserPerms_prefixSpecs(t *testing.T) { }, nil } - _, err := s.syncUserPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) + _, _, err := s.syncUserPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -653,7 +653,7 @@ func TestPermsSyncer_syncUserPerms_subRepoPermissions(t *testing.T) { }, nil } - _, err := s.syncUserPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) + _, _, err := s.syncUserPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -691,7 +691,7 @@ func TestPermsSyncer_syncRepoPerms(t *testing.T) { perms := edb.NewMockPermsStore() s := newPermsSyncer(reposStore, perms) - _, err := s.syncRepoPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) + _, _, err := s.syncRepoPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -744,15 +744,15 @@ func TestPermsSyncer_syncRepoPerms(t *testing.T) { perms := edb.NewMockPermsStore() perms.TransactFunc.SetDefaultReturn(perms, nil) perms.GetUserIDsByExternalAccountsFunc.SetDefaultReturn(map[string]int32{"user": 1}, nil) - perms.SetRepoPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.RepoPermissions) error { + perms.SetRepoPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.RepoPermissions) (*database.SetPermissionsResult, error) { assert.Equal(t, int32(1), p.RepoID) assert.Equal(t, []int32{1}, p.GenerateSortedIDsSlice()) - return nil + return nil, nil }) s := newPermsSyncer(reposStore, perms) - _, err := s.syncRepoPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) + _, _, err := s.syncRepoPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -775,15 +775,15 @@ func TestPermsSyncer_syncRepoPerms(t *testing.T) { perms := edb.NewMockPermsStore() perms.TransactFunc.SetDefaultReturn(perms, nil) perms.GetUserIDsByExternalAccountsFunc.SetDefaultReturn(map[string]int32{"user": 1}, nil) - perms.SetRepoPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.RepoPermissions) error { + perms.SetRepoPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.RepoPermissions) (*database.SetPermissionsResult, error) { assert.Equal(t, int32(1), p.RepoID) assert.Equal(t, []int32{1}, p.GenerateSortedIDsSlice()) - return nil + return nil, nil }) s := newPermsSyncer(reposStore, perms) - _, err := s.syncRepoPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) + _, _, err := s.syncRepoPerms(context.Background(), 1, false, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } @@ -820,10 +820,10 @@ func TestPermsSyncer_syncRepoPerms(t *testing.T) { perms := edb.NewMockPermsStore() perms.TransactFunc.SetDefaultReturn(perms, nil) perms.GetUserIDsByExternalAccountsFunc.SetDefaultReturn(map[string]int32{"user": 1}, nil) - perms.SetRepoPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.RepoPermissions) error { + perms.SetRepoPermissionsFunc.SetDefaultHook(func(_ context.Context, p *authz.RepoPermissions) (*database.SetPermissionsResult, error) { assert.Equal(t, int32(1), p.RepoID) assert.Equal(t, []int32{1}, p.GenerateSortedIDsSlice()) - return nil + return nil, nil }) perms.SetRepoPendingPermissionsFunc.SetDefaultHook(func(_ context.Context, accounts *extsvc.Accounts, _ *authz.RepoPermissions) error { wantAccounts := &extsvc.Accounts{ @@ -858,7 +858,7 @@ func TestPermsSyncer_syncRepoPerms(t *testing.T) { return []extsvc.AccountID{"user", "pending_user"}, test.fetchErr } - _, err := s.syncRepoPerms(context.Background(), 1, test.noPerms, authz.FetchPermsOptions{}) + _, _, err := s.syncRepoPerms(context.Background(), 1, test.noPerms, authz.FetchPermsOptions{}) if err != nil { t.Fatal(err) } diff --git a/enterprise/cmd/repo-updater/internal/authz/perms_syncer_worker.go b/enterprise/cmd/repo-updater/internal/authz/perms_syncer_worker.go index 9e6b65bc7544..0718cfc19be7 100644 --- a/enterprise/cmd/repo-updater/internal/authz/perms_syncer_worker.go +++ b/enterprise/cmd/repo-updater/internal/authz/perms_syncer_worker.go @@ -2,6 +2,7 @@ package authz import ( "context" + "fmt" "time" "github.com/keegancsmith/sqlf" @@ -26,7 +27,7 @@ const ( SyncTypeUser ) -func MakePermsSyncerWorker(observationCtx *observation.Context, syncer permsSyncer, syncType syncType) *permsSyncerWorker { +func MakePermsSyncerWorker(observationCtx *observation.Context, syncer permsSyncer, syncType syncType, jobsStore database.PermissionSyncJobStore) *permsSyncerWorker { logger := observationCtx.Logger.Scoped("RepoPermsSyncerWorkerRepo", "Repository permission sync worker") recordsStore := syncjobs.NewRecordsStore(logger.Scoped("records", "Records provider states in redis"), conf.DefaultClient()) if syncType == SyncTypeUser { @@ -37,12 +38,13 @@ func MakePermsSyncerWorker(observationCtx *observation.Context, syncer permsSync syncer: syncer, syncType: syncType, recordsStore: recordsStore, + jobsStore: jobsStore, } } type permsSyncer interface { - syncRepoPerms(context.Context, api.RepoID, bool, authz.FetchPermsOptions) ([]syncjobs.ProviderStatus, error) - syncUserPerms(context.Context, int32, bool, authz.FetchPermsOptions) ([]syncjobs.ProviderStatus, error) + syncRepoPerms(context.Context, api.RepoID, bool, authz.FetchPermsOptions) (*database.SetPermissionsResult, []syncjobs.ProviderStatus, error) + syncUserPerms(context.Context, int32, bool, authz.FetchPermsOptions) (*database.SetPermissionsResult, []syncjobs.ProviderStatus, error) } type permsSyncerWorker struct { @@ -50,6 +52,7 @@ type permsSyncerWorker struct { syncer permsSyncer syncType syncType recordsStore *syncjobs.RecordsStore + jobsStore database.PermissionSyncJobStore } // PreDequeue in our case does a nice trick of adding a predicate (WHERE clause) @@ -78,42 +81,39 @@ func (h *permsSyncerWorker) Handle(ctx context.Context, _ log.Logger, record *da log.Int("priority", int(record.Priority)), ) - // TODO(naman): when removing old perms syncer, `requestMeta` must be replaced - // by a new type to include new priority enum. `requestMeta.Priority` itself - // is not used anywhere in `syncer.syncPerms()`, therefore it is okay for now - // to pass old priority enum values. - // `requestQueue` can also be removed as it is only used by the old perms syncer. - return h.handlePermsSync(ctx, reqType, reqID, record.NoPerms, record.InvalidateCaches) + return h.handlePermsSync(ctx, reqType, reqID, record.ID, record.NoPerms, record.InvalidateCaches) } // handlePermsSync is effectively a sync version of `perms_syncer.syncPerms` // which calls `perms_syncer.syncUserPerms` or `perms_syncer.syncRepoPerms` // depending on a request type and logs/adds metrics of sync statistics // afterwards. -func (h *permsSyncerWorker) handlePermsSync(ctx context.Context, reqType requestType, reqID int32, noPerms, invalidateCaches bool) error { +func (h *permsSyncerWorker) handlePermsSync(ctx context.Context, reqType requestType, reqID int32, recordID int, noPerms, invalidateCaches bool) error { + var err error + var result *database.SetPermissionsResult + var providerStates providerStatesSet + switch reqType { case requestTypeUser: - providerStatuses, err := h.syncer.syncUserPerms(ctx, reqID, noPerms, authz.FetchPermsOptions{InvalidateCaches: invalidateCaches}) - return h.handleSyncResults(reqType, reqID, providerStatuses, err) + result, providerStates, err = h.syncer.syncUserPerms(ctx, reqID, noPerms, authz.FetchPermsOptions{InvalidateCaches: invalidateCaches}) case requestTypeRepo: - providerStatuses, err := h.syncer.syncRepoPerms(ctx, api.RepoID(reqID), noPerms, authz.FetchPermsOptions{InvalidateCaches: invalidateCaches}) - return h.handleSyncResults(reqType, reqID, providerStatuses, err) + result, providerStates, err = h.syncer.syncRepoPerms(ctx, api.RepoID(reqID), noPerms, authz.FetchPermsOptions{InvalidateCaches: invalidateCaches}) default: return errors.Newf("unexpected request type: %q", reqType) } -} -func (h *permsSyncerWorker) handleSyncResults(reqType requestType, reqID int32, providerStates providerStatesSet, err error) error { if err != nil { h.logger.Error("failed to sync permissions", providerStates.SummaryField(), log.Error(err)) - - if reqType == requestTypeUser { - metricsFailedPermsSyncs.WithLabelValues("user").Inc() - } else { - metricsFailedPermsSyncs.WithLabelValues("repo").Inc() - } } else { h.logger.Debug("succeeded in syncing permissions", providerStates.SummaryField()) + + // NOTE(naman): here we are saving permissions added, removed and found results to the job record + if result != nil { + err = h.jobsStore.SaveSyncResult(ctx, recordID, result) + if err != nil { + h.logger.Error(fmt.Sprintf("failed to save permissions sync job(%d) results", recordID), log.Error(err)) + } + } } h.recordsStore.Record(reqType.String(), reqID, providerStates, err) @@ -141,8 +141,8 @@ func MakeStore(observationCtx *observation.Context, dbHandle basestore.Transacta }) } -func MakeWorker(ctx context.Context, observationCtx *observation.Context, workerStore dbworkerstore.Store[*database.PermissionSyncJob], permsSyncer *PermsSyncer, syncType syncType) *workerutil.Worker[*database.PermissionSyncJob] { - handler := MakePermsSyncerWorker(observationCtx, permsSyncer, syncType) +func MakeWorker(ctx context.Context, observationCtx *observation.Context, workerStore dbworkerstore.Store[*database.PermissionSyncJob], permsSyncer *PermsSyncer, syncType syncType, jobsStore database.PermissionSyncJobStore) *workerutil.Worker[*database.PermissionSyncJob] { + handler := MakePermsSyncerWorker(observationCtx, permsSyncer, syncType, jobsStore) // Number of handlers depends on a type of perms sync jobs this worker processes. numHandlers := 1 name := "repo_permission_sync_job_worker" diff --git a/enterprise/cmd/repo-updater/internal/authz/perms_syncer_worker_test.go b/enterprise/cmd/repo-updater/internal/authz/perms_syncer_worker_test.go index 248c9bbb4f3a..2a627ebb4884 100644 --- a/enterprise/cmd/repo-updater/internal/authz/perms_syncer_worker_test.go +++ b/enterprise/cmd/repo-updater/internal/authz/perms_syncer_worker_test.go @@ -26,9 +26,12 @@ const errorMsg = "Sorry, wrong number." func TestPermsSyncerWorker_Handle(t *testing.T) { ctx := context.Background() dummySyncer := &dummyPermsSyncer{} + logger := logtest.Scoped(t) + db := database.NewDB(logger, dbtest.NewDB(logger, t)) + syncJobsStore := db.PermissionSyncJobs() t.Run("user sync request", func(t *testing.T) { - worker := MakePermsSyncerWorker(&observation.TestContext, dummySyncer, SyncTypeUser) + worker := MakePermsSyncerWorker(&observation.TestContext, dummySyncer, SyncTypeUser, syncJobsStore) _ = worker.Handle(ctx, logtest.Scoped(t), &database.PermissionSyncJob{ ID: 99, UserID: 1234, @@ -50,7 +53,7 @@ func TestPermsSyncerWorker_Handle(t *testing.T) { }) t.Run("repo sync request", func(t *testing.T) { - worker := MakePermsSyncerWorker(&observation.TestContext, dummySyncer, SyncTypeRepo) + worker := MakePermsSyncerWorker(&observation.TestContext, dummySyncer, SyncTypeRepo, syncJobsStore) _ = worker.Handle(ctx, logtest.Scoped(t), &database.PermissionSyncJob{ ID: 777, RepositoryID: 4567, @@ -87,28 +90,31 @@ func TestPermsSyncerWorker_RepoSyncJobs(t *testing.T) { user2, err := userStore.Create(ctx, database.NewUser{Username: "user2"}) require.NoError(t, err) repoStore := db.Repos() - err = repoStore.Create(ctx, &types.Repo{Name: "github.com/soucegraph/sourcegraph"}, &types.Repo{Name: "github.com/soucegraph/about"}) + err = repoStore.Create(ctx, &types.Repo{Name: "github.com/soucegraph/sourcegraph"}, &types.Repo{Name: "github.com/soucegraph/about"}, &types.Repo{Name: "github.com/soucegraph/hello"}) require.NoError(t, err) // Creating a worker. observationCtx := &observation.TestContext dummySyncer := &dummySyncerWithErrors{ - repoIDErrors: map[api.RepoID]struct{}{2: {}}, + repoIDErrors: map[api.RepoID]struct{}{3: {}}, } + syncJobsStore := db.PermissionSyncJobs() workerStore := MakeStore(observationCtx, db.Handle(), SyncTypeRepo) - worker := MakeTestWorker(ctx, observationCtx, workerStore, dummySyncer, SyncTypeRepo) + worker := MakeTestWorker(ctx, observationCtx, workerStore, dummySyncer, SyncTypeRepo, syncJobsStore) go worker.Start() t.Cleanup(worker.Stop) // Adding repo perms sync jobs. - syncJobsStore := db.PermissionSyncJobs() err = syncJobsStore.CreateRepoSyncJob(ctx, api.RepoID(1), database.PermissionSyncJobOpts{Reason: database.ReasonManualRepoSync, Priority: database.MediumPriorityPermissionSync, TriggeredByUserID: user1.ID}) require.NoError(t, err) err = syncJobsStore.CreateRepoSyncJob(ctx, api.RepoID(2), database.PermissionSyncJobOpts{Reason: database.ReasonManualRepoSync, Priority: database.MediumPriorityPermissionSync, TriggeredByUserID: user1.ID}) require.NoError(t, err) + err = syncJobsStore.CreateRepoSyncJob(ctx, api.RepoID(3), database.PermissionSyncJobOpts{Reason: database.ReasonManualRepoSync, Priority: database.MediumPriorityPermissionSync, TriggeredByUserID: user1.ID}) + require.NoError(t, err) + // Adding user perms sync job, which should not be processed by current worker! err = syncJobsStore.CreateUserSyncJob(ctx, user2.ID, database.PermissionSyncJobOpts{Reason: database.ReasonRepoNoPermissions, Priority: database.HighPriorityPermissionSync, TriggeredByUserID: user1.ID}) @@ -124,9 +130,9 @@ loop: t.Fatal(err) } for _, job := range jobs { - // We don't check job with ID=3 because it is a user sync job which is not + // We don't check job with ID=4 because it is a user sync job which is not // processed by current worker. - if job.ID != 3 && (job.State == "queued" || job.State == "processing") { + if job.ID != 4 && (job.State == "queued" || job.State == "processing") { // wait and retry time.Sleep(500 * time.Millisecond) continue loop @@ -138,7 +144,7 @@ loop: for _, job := range jobs { // We only check job with ID=3 because it is a user sync job which should not // processed by current worker. - if job.ID == 3 && remainingRounds > 0 { + if job.ID == 4 && remainingRounds > 0 { // wait and retry time.Sleep(500 * time.Millisecond) remainingRounds = remainingRounds - 1 @@ -165,17 +171,32 @@ loop: require.Equal(t, jobID, job.RepositoryID) } - // Check that failed job has the failure message. + // Check that repo sync job was completed and results were saved. if jobID == 2 { + require.Equal(t, "completed", job.State) + require.Nil(t, job.FailureMessage) + require.Equal(t, 1, job.PermissionsAdded) + require.Equal(t, 2, job.PermissionsRemoved) + require.Equal(t, 5, job.PermissionsFound) + } + + // Check that failed job has the failure message. + if jobID == 3 { require.NotNil(t, job.FailureMessage) require.Equal(t, errorMsg, *job.FailureMessage) require.Equal(t, 1, job.NumFailures) + require.Equal(t, 0, job.PermissionsAdded) + require.Equal(t, 0, job.PermissionsRemoved) + require.Equal(t, 0, job.PermissionsFound) } // Check that user sync job wasn't picked up by repo sync worker. - if jobID == 3 { + if jobID == 4 { require.Equal(t, "queued", job.State) require.Nil(t, job.FailureMessage) + require.Equal(t, 0, job.PermissionsAdded) + require.Equal(t, 0, job.PermissionsRemoved) + require.Equal(t, 0, job.PermissionsFound) } } } @@ -195,6 +216,8 @@ func TestPermsSyncerWorker_UserSyncJobs(t *testing.T) { require.NoError(t, err) user2, err := userStore.Create(ctx, database.NewUser{Username: "user2"}) require.NoError(t, err) + user3, err := userStore.Create(ctx, database.NewUser{Username: "user3"}) + require.NoError(t, err) repoStore := db.Repos() err = repoStore.Create(ctx, &types.Repo{Name: "github.com/soucegraph/sourcegraph"}, &types.Repo{Name: "github.com/soucegraph/about"}) require.NoError(t, err) @@ -202,16 +225,16 @@ func TestPermsSyncerWorker_UserSyncJobs(t *testing.T) { // Creating a worker. observationCtx := &observation.TestContext dummySyncer := &dummySyncerWithErrors{ - userIDErrors: map[int32]struct{}{2: {}}, + userIDErrors: map[int32]struct{}{3: {}}, } + syncJobsStore := db.PermissionSyncJobs() workerStore := MakeStore(observationCtx, db.Handle(), SyncTypeUser) - worker := MakeTestWorker(ctx, observationCtx, workerStore, dummySyncer, SyncTypeUser) + worker := MakeTestWorker(ctx, observationCtx, workerStore, dummySyncer, SyncTypeUser, syncJobsStore) go worker.Start() t.Cleanup(worker.Stop) // Adding user perms sync jobs. - syncJobsStore := db.PermissionSyncJobs() err = syncJobsStore.CreateUserSyncJob(ctx, user1.ID, database.PermissionSyncJobOpts{Reason: database.ReasonUserOutdatedPermissions, Priority: database.LowPriorityPermissionSync}) require.NoError(t, err) @@ -220,6 +243,10 @@ func TestPermsSyncerWorker_UserSyncJobs(t *testing.T) { database.PermissionSyncJobOpts{Reason: database.ReasonRepoNoPermissions, NoPerms: true, Priority: database.HighPriorityPermissionSync, TriggeredByUserID: user1.ID}) require.NoError(t, err) + err = syncJobsStore.CreateUserSyncJob(ctx, user3.ID, + database.PermissionSyncJobOpts{Reason: database.ReasonRepoNoPermissions, NoPerms: true, Priority: database.HighPriorityPermissionSync, TriggeredByUserID: user1.ID}) + require.NoError(t, err) + // Adding repo perms sync job, which should not be processed by current worker! err = syncJobsStore.CreateRepoSyncJob(ctx, api.RepoID(1), database.PermissionSyncJobOpts{Reason: database.ReasonManualRepoSync, Priority: database.MediumPriorityPermissionSync, TriggeredByUserID: user1.ID}) require.NoError(t, err) @@ -236,7 +263,7 @@ loop: for _, job := range jobs { // We don't check job with ID=3 because it is a repo sync job which is not // processed by current worker. - if job.ID != 3 && (job.State == "queued" || job.State == "processing") { + if job.ID != 4 && (job.State == "queued" || job.State == "processing") { // wait and retry time.Sleep(500 * time.Millisecond) continue loop @@ -248,7 +275,7 @@ loop: for _, job := range jobs { // We only check job with ID=3 because it is a repo sync job which should not // processed by current worker. - if job.ID == 3 && remainingRounds > 0 { + if job.ID == 4 && remainingRounds > 0 { // wait and retry time.Sleep(500 * time.Millisecond) remainingRounds = remainingRounds - 1 @@ -275,18 +302,32 @@ loop: require.Equal(t, jobID, job.UserID) } - // Check that failed job has the failure message. if jobID == 2 { + require.Equal(t, "completed", job.State) + require.Nil(t, job.FailureMessage) + require.Equal(t, 1, job.PermissionsAdded) + require.Equal(t, 2, job.PermissionsRemoved) + require.Equal(t, 5, job.PermissionsFound) + } + + // Check that failed job has the failure message. + if jobID == 3 { require.NotNil(t, job.FailureMessage) require.Equal(t, errorMsg, *job.FailureMessage) require.Equal(t, 1, job.NumFailures) require.True(t, job.NoPerms) + require.Equal(t, 0, job.PermissionsAdded) + require.Equal(t, 0, job.PermissionsRemoved) + require.Equal(t, 0, job.PermissionsFound) } // Check that repo sync job wasn't picked up by user sync worker. - if jobID == 3 { + if jobID == 4 { require.Equal(t, "queued", job.State) require.Nil(t, job.FailureMessage) + require.Equal(t, 0, job.PermissionsAdded) + require.Equal(t, 0, job.PermissionsRemoved) + require.Equal(t, 0, job.PermissionsFound) } } } @@ -363,8 +404,8 @@ func TestPermsSyncerWorker_Store_Dequeue_Order(t *testing.T) { } } -func MakeTestWorker(ctx context.Context, observationCtx *observation.Context, workerStore dbworkerstore.Store[*database.PermissionSyncJob], permsSyncer permsSyncer, typ syncType) *workerutil.Worker[*database.PermissionSyncJob] { - handler := MakePermsSyncerWorker(observationCtx, permsSyncer, typ) +func MakeTestWorker(ctx context.Context, observationCtx *observation.Context, workerStore dbworkerstore.Store[*database.PermissionSyncJob], permsSyncer permsSyncer, typ syncType, jobsStore database.PermissionSyncJobStore) *workerutil.Worker[*database.PermissionSyncJob] { + handler := MakePermsSyncerWorker(observationCtx, permsSyncer, typ, jobsStore) return dbworker.NewWorker[*database.PermissionSyncJob](ctx, workerStore, handler, workerutil.WorkerOptions{ Name: "permission_sync_job_worker", Interval: time.Second, @@ -387,21 +428,21 @@ type dummyPermsSyncer struct { request combinedRequest } -func (d *dummyPermsSyncer) syncRepoPerms(_ context.Context, repoID api.RepoID, noPerms bool, options authz.FetchPermsOptions) ([]syncjobs.ProviderStatus, error) { +func (d *dummyPermsSyncer) syncRepoPerms(_ context.Context, repoID api.RepoID, noPerms bool, options authz.FetchPermsOptions) (*database.SetPermissionsResult, []syncjobs.ProviderStatus, error) { d.request = combinedRequest{ RepoID: repoID, NoPerms: noPerms, Options: options, } - return []syncjobs.ProviderStatus{}, nil + return &database.SetPermissionsResult{Added: 1, Removed: 2, Found: 5}, []syncjobs.ProviderStatus{}, nil } -func (d *dummyPermsSyncer) syncUserPerms(_ context.Context, userID int32, noPerms bool, options authz.FetchPermsOptions) ([]syncjobs.ProviderStatus, error) { +func (d *dummyPermsSyncer) syncUserPerms(_ context.Context, userID int32, noPerms bool, options authz.FetchPermsOptions) (*database.SetPermissionsResult, []syncjobs.ProviderStatus, error) { d.request = combinedRequest{ UserID: userID, NoPerms: noPerms, Options: options, } - return []syncjobs.ProviderStatus{}, nil + return &database.SetPermissionsResult{Added: 1, Removed: 2, Found: 5}, []syncjobs.ProviderStatus{}, nil } type dummySyncerWithErrors struct { @@ -410,25 +451,25 @@ type dummySyncerWithErrors struct { repoIDErrors map[api.RepoID]struct{} } -func (d *dummySyncerWithErrors) syncRepoPerms(_ context.Context, repoID api.RepoID, noPerms bool, options authz.FetchPermsOptions) ([]syncjobs.ProviderStatus, error) { +func (d *dummySyncerWithErrors) syncRepoPerms(_ context.Context, repoID api.RepoID, noPerms bool, options authz.FetchPermsOptions) (*database.SetPermissionsResult, []syncjobs.ProviderStatus, error) { if _, ok := d.repoIDErrors[repoID]; ok { - return nil, errors.New(errorMsg) + return nil, nil, errors.New(errorMsg) } d.request = combinedRequest{ RepoID: repoID, NoPerms: noPerms, Options: options, } - return []syncjobs.ProviderStatus{}, nil + return &database.SetPermissionsResult{Added: 1, Removed: 2, Found: 5}, []syncjobs.ProviderStatus{}, nil } -func (d *dummySyncerWithErrors) syncUserPerms(_ context.Context, userID int32, noPerms bool, options authz.FetchPermsOptions) ([]syncjobs.ProviderStatus, error) { +func (d *dummySyncerWithErrors) syncUserPerms(_ context.Context, userID int32, noPerms bool, options authz.FetchPermsOptions) (*database.SetPermissionsResult, []syncjobs.ProviderStatus, error) { if _, ok := d.userIDErrors[userID]; ok { - return nil, errors.New(errorMsg) + return nil, nil, errors.New(errorMsg) } d.request = combinedRequest{ UserID: userID, NoPerms: noPerms, Options: options, } - return []syncjobs.ProviderStatus{}, nil + return &database.SetPermissionsResult{Added: 1, Removed: 2, Found: 5}, []syncjobs.ProviderStatus{}, nil } diff --git a/enterprise/cmd/repo-updater/shared/shared.go b/enterprise/cmd/repo-updater/shared/shared.go index 72129d7b8ea9..3de13be60f9a 100644 --- a/enterprise/cmd/repo-updater/shared/shared.go +++ b/enterprise/cmd/repo-updater/shared/shared.go @@ -79,8 +79,9 @@ func EnterpriseInit( repoWorkerStore := authz.MakeStore(observationCtx, db.Handle(), authz.SyncTypeRepo) userWorkerStore := authz.MakeStore(observationCtx, db.Handle(), authz.SyncTypeUser) - repoSyncWorker := authz.MakeWorker(ctx, observationCtx, repoWorkerStore, permsSyncer, authz.SyncTypeRepo) - userSyncWorker := authz.MakeWorker(ctx, observationCtx, userWorkerStore, permsSyncer, authz.SyncTypeUser) + permissionSyncJobStore := ossDB.PermissionSyncJobsWith(observationCtx.Logger, db) + repoSyncWorker := authz.MakeWorker(ctx, observationCtx, repoWorkerStore, permsSyncer, authz.SyncTypeRepo, permissionSyncJobStore) + userSyncWorker := authz.MakeWorker(ctx, observationCtx, userWorkerStore, permsSyncer, authz.SyncTypeUser, permissionSyncJobStore) // Type of store (repo/user) for resetter doesn't matter, because it has its // separate name for logging and metrics. resetter := authz.MakeResetter(observationCtx, repoWorkerStore) diff --git a/enterprise/cmd/worker/internal/permissions/bitbucket_projects.go b/enterprise/cmd/worker/internal/permissions/bitbucket_projects.go index f431bfb1266e..c12794ffe1b6 100644 --- a/enterprise/cmd/worker/internal/permissions/bitbucket_projects.go +++ b/enterprise/cmd/worker/internal/permissions/bitbucket_projects.go @@ -325,7 +325,7 @@ func (h *bitbucketProjectPermissionsHandler) setRepoPermissions(ctx context.Cont } // set repo permissions (and user permissions) - err = txs.SetRepoPermissions(ctx, &p) + _, err = txs.SetRepoPermissions(ctx, &p) if err != nil { return errors.Wrapf(err, "failed to set repo permissions for repo %d", repoID) } diff --git a/enterprise/internal/batches/testing/mock_repo_perms.go b/enterprise/internal/batches/testing/mock_repo_perms.go index 4f4a2e3fe52c..b397905c8098 100644 --- a/enterprise/internal/batches/testing/mock_repo_perms.go +++ b/enterprise/internal/batches/testing/mock_repo_perms.go @@ -25,7 +25,7 @@ func MockRepoPermissions(t *testing.T, db database.DB, userID int32, repoIDs ... userID: {}, } for _, id := range repoIDs { - err := permsStore.SetRepoPermissions(context.Background(), + _, err := permsStore.SetRepoPermissions(context.Background(), &authz.RepoPermissions{ RepoID: int32(id), Perm: authz.Read, diff --git a/enterprise/internal/database/authz_test.go b/enterprise/internal/database/authz_test.go index 34492919fb4f..849401db4098 100644 --- a/enterprise/internal/database/authz_test.go +++ b/enterprise/internal/database/authz_test.go @@ -298,7 +298,7 @@ func TestAuthzStore_AuthorizedRepos(t *testing.T) { defer cleanupPermsTables(t, s.store.(*permsStore)) for _, update := range test.updates { - err := s.store.SetRepoPermissions(ctx, &authz.RepoPermissions{ + _, err := s.store.SetRepoPermissions(ctx, &authz.RepoPermissions{ RepoID: update.repoID, Perm: authz.Read, UserIDs: toMapset(update.userIDs...), @@ -336,7 +336,7 @@ func TestAuthzStore_RevokeUserPermissions(t *testing.T) { } // Set both effective and pending permissions for a user - if err := s.store.SetRepoPermissions(ctx, &authz.RepoPermissions{ + if _, err := s.store.SetRepoPermissions(ctx, &authz.RepoPermissions{ RepoID: int32(repo.ID), Perm: authz.Read, UserIDs: toMapset(user.ID), diff --git a/enterprise/internal/database/mocks_temp.go b/enterprise/internal/database/mocks_temp.go index 330f95061a45..025acc7d34e5 100644 --- a/enterprise/internal/database/mocks_temp.go +++ b/enterprise/internal/database/mocks_temp.go @@ -12865,7 +12865,7 @@ func NewMockPermsStore() *MockPermsStore { }, }, SetRepoPermissionsFunc: &PermsStoreSetRepoPermissionsFunc{ - defaultHook: func(context.Context, *authz.RepoPermissions) (r0 error) { + defaultHook: func(context.Context, *authz.RepoPermissions) (r0 *database.SetPermissionsResult, r1 error) { return }, }, @@ -12875,7 +12875,7 @@ func NewMockPermsStore() *MockPermsStore { }, }, SetUserPermissionsFunc: &PermsStoreSetUserPermissionsFunc{ - defaultHook: func(context.Context, *authz.UserPermissions) (r0 error) { + defaultHook: func(context.Context, *authz.UserPermissions) (r0 *database.SetPermissionsResult, r1 error) { return }, }, @@ -12997,7 +12997,7 @@ func NewStrictMockPermsStore() *MockPermsStore { }, }, SetRepoPermissionsFunc: &PermsStoreSetRepoPermissionsFunc{ - defaultHook: func(context.Context, *authz.RepoPermissions) error { + defaultHook: func(context.Context, *authz.RepoPermissions) (*database.SetPermissionsResult, error) { panic("unexpected invocation of MockPermsStore.SetRepoPermissions") }, }, @@ -13007,7 +13007,7 @@ func NewStrictMockPermsStore() *MockPermsStore { }, }, SetUserPermissionsFunc: &PermsStoreSetUserPermissionsFunc{ - defaultHook: func(context.Context, *authz.UserPermissions) error { + defaultHook: func(context.Context, *authz.UserPermissions) (*database.SetPermissionsResult, error) { panic("unexpected invocation of MockPermsStore.SetUserPermissions") }, }, @@ -14871,24 +14871,24 @@ func (c PermsStoreSetRepoPendingPermissionsFuncCall) Results() []interface{} { // SetRepoPermissions method of the parent MockPermsStore instance is // invoked. type PermsStoreSetRepoPermissionsFunc struct { - defaultHook func(context.Context, *authz.RepoPermissions) error - hooks []func(context.Context, *authz.RepoPermissions) error + defaultHook func(context.Context, *authz.RepoPermissions) (*database.SetPermissionsResult, error) + hooks []func(context.Context, *authz.RepoPermissions) (*database.SetPermissionsResult, error) history []PermsStoreSetRepoPermissionsFuncCall mutex sync.Mutex } // SetRepoPermissions delegates to the next hook function in the queue and // stores the parameter and result values of this invocation. -func (m *MockPermsStore) SetRepoPermissions(v0 context.Context, v1 *authz.RepoPermissions) error { - r0 := m.SetRepoPermissionsFunc.nextHook()(v0, v1) - m.SetRepoPermissionsFunc.appendCall(PermsStoreSetRepoPermissionsFuncCall{v0, v1, r0}) - return r0 +func (m *MockPermsStore) SetRepoPermissions(v0 context.Context, v1 *authz.RepoPermissions) (*database.SetPermissionsResult, error) { + r0, r1 := m.SetRepoPermissionsFunc.nextHook()(v0, v1) + m.SetRepoPermissionsFunc.appendCall(PermsStoreSetRepoPermissionsFuncCall{v0, v1, r0, r1}) + return r0, r1 } // SetDefaultHook sets function that is called when the SetRepoPermissions // method of the parent MockPermsStore instance is invoked and the hook // queue is empty. -func (f *PermsStoreSetRepoPermissionsFunc) SetDefaultHook(hook func(context.Context, *authz.RepoPermissions) error) { +func (f *PermsStoreSetRepoPermissionsFunc) SetDefaultHook(hook func(context.Context, *authz.RepoPermissions) (*database.SetPermissionsResult, error)) { f.defaultHook = hook } @@ -14896,7 +14896,7 @@ func (f *PermsStoreSetRepoPermissionsFunc) SetDefaultHook(hook func(context.Cont // SetRepoPermissions method of the parent MockPermsStore instance invokes // the hook at the front of the queue and discards it. After the queue is // empty, the default hook function is invoked for any future action. -func (f *PermsStoreSetRepoPermissionsFunc) PushHook(hook func(context.Context, *authz.RepoPermissions) error) { +func (f *PermsStoreSetRepoPermissionsFunc) PushHook(hook func(context.Context, *authz.RepoPermissions) (*database.SetPermissionsResult, error)) { f.mutex.Lock() f.hooks = append(f.hooks, hook) f.mutex.Unlock() @@ -14904,20 +14904,20 @@ func (f *PermsStoreSetRepoPermissionsFunc) PushHook(hook func(context.Context, * // SetDefaultReturn calls SetDefaultHook with a function that returns the // given values. -func (f *PermsStoreSetRepoPermissionsFunc) SetDefaultReturn(r0 error) { - f.SetDefaultHook(func(context.Context, *authz.RepoPermissions) error { - return r0 +func (f *PermsStoreSetRepoPermissionsFunc) SetDefaultReturn(r0 *database.SetPermissionsResult, r1 error) { + f.SetDefaultHook(func(context.Context, *authz.RepoPermissions) (*database.SetPermissionsResult, error) { + return r0, r1 }) } // PushReturn calls PushHook with a function that returns the given values. -func (f *PermsStoreSetRepoPermissionsFunc) PushReturn(r0 error) { - f.PushHook(func(context.Context, *authz.RepoPermissions) error { - return r0 +func (f *PermsStoreSetRepoPermissionsFunc) PushReturn(r0 *database.SetPermissionsResult, r1 error) { + f.PushHook(func(context.Context, *authz.RepoPermissions) (*database.SetPermissionsResult, error) { + return r0, r1 }) } -func (f *PermsStoreSetRepoPermissionsFunc) nextHook() func(context.Context, *authz.RepoPermissions) error { +func (f *PermsStoreSetRepoPermissionsFunc) nextHook() func(context.Context, *authz.RepoPermissions) (*database.SetPermissionsResult, error) { f.mutex.Lock() defer f.mutex.Unlock() @@ -14958,7 +14958,10 @@ type PermsStoreSetRepoPermissionsFuncCall struct { Arg1 *authz.RepoPermissions // Result0 is the value of the 1st result returned from this method // invocation. - Result0 error + Result0 *database.SetPermissionsResult + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error } // Args returns an interface slice containing the arguments of this @@ -14970,7 +14973,7 @@ func (c PermsStoreSetRepoPermissionsFuncCall) Args() []interface{} { // Results returns an interface slice containing the results of this // invocation. func (c PermsStoreSetRepoPermissionsFuncCall) Results() []interface{} { - return []interface{}{c.Result0} + return []interface{}{c.Result0, c.Result1} } // PermsStoreSetRepoPermissionsUnrestrictedFunc describes the behavior when @@ -15089,24 +15092,24 @@ func (c PermsStoreSetRepoPermissionsUnrestrictedFuncCall) Results() []interface{ // SetUserPermissions method of the parent MockPermsStore instance is // invoked. type PermsStoreSetUserPermissionsFunc struct { - defaultHook func(context.Context, *authz.UserPermissions) error - hooks []func(context.Context, *authz.UserPermissions) error + defaultHook func(context.Context, *authz.UserPermissions) (*database.SetPermissionsResult, error) + hooks []func(context.Context, *authz.UserPermissions) (*database.SetPermissionsResult, error) history []PermsStoreSetUserPermissionsFuncCall mutex sync.Mutex } // SetUserPermissions delegates to the next hook function in the queue and // stores the parameter and result values of this invocation. -func (m *MockPermsStore) SetUserPermissions(v0 context.Context, v1 *authz.UserPermissions) error { - r0 := m.SetUserPermissionsFunc.nextHook()(v0, v1) - m.SetUserPermissionsFunc.appendCall(PermsStoreSetUserPermissionsFuncCall{v0, v1, r0}) - return r0 +func (m *MockPermsStore) SetUserPermissions(v0 context.Context, v1 *authz.UserPermissions) (*database.SetPermissionsResult, error) { + r0, r1 := m.SetUserPermissionsFunc.nextHook()(v0, v1) + m.SetUserPermissionsFunc.appendCall(PermsStoreSetUserPermissionsFuncCall{v0, v1, r0, r1}) + return r0, r1 } // SetDefaultHook sets function that is called when the SetUserPermissions // method of the parent MockPermsStore instance is invoked and the hook // queue is empty. -func (f *PermsStoreSetUserPermissionsFunc) SetDefaultHook(hook func(context.Context, *authz.UserPermissions) error) { +func (f *PermsStoreSetUserPermissionsFunc) SetDefaultHook(hook func(context.Context, *authz.UserPermissions) (*database.SetPermissionsResult, error)) { f.defaultHook = hook } @@ -15114,7 +15117,7 @@ func (f *PermsStoreSetUserPermissionsFunc) SetDefaultHook(hook func(context.Cont // SetUserPermissions method of the parent MockPermsStore instance invokes // the hook at the front of the queue and discards it. After the queue is // empty, the default hook function is invoked for any future action. -func (f *PermsStoreSetUserPermissionsFunc) PushHook(hook func(context.Context, *authz.UserPermissions) error) { +func (f *PermsStoreSetUserPermissionsFunc) PushHook(hook func(context.Context, *authz.UserPermissions) (*database.SetPermissionsResult, error)) { f.mutex.Lock() f.hooks = append(f.hooks, hook) f.mutex.Unlock() @@ -15122,20 +15125,20 @@ func (f *PermsStoreSetUserPermissionsFunc) PushHook(hook func(context.Context, * // SetDefaultReturn calls SetDefaultHook with a function that returns the // given values. -func (f *PermsStoreSetUserPermissionsFunc) SetDefaultReturn(r0 error) { - f.SetDefaultHook(func(context.Context, *authz.UserPermissions) error { - return r0 +func (f *PermsStoreSetUserPermissionsFunc) SetDefaultReturn(r0 *database.SetPermissionsResult, r1 error) { + f.SetDefaultHook(func(context.Context, *authz.UserPermissions) (*database.SetPermissionsResult, error) { + return r0, r1 }) } // PushReturn calls PushHook with a function that returns the given values. -func (f *PermsStoreSetUserPermissionsFunc) PushReturn(r0 error) { - f.PushHook(func(context.Context, *authz.UserPermissions) error { - return r0 +func (f *PermsStoreSetUserPermissionsFunc) PushReturn(r0 *database.SetPermissionsResult, r1 error) { + f.PushHook(func(context.Context, *authz.UserPermissions) (*database.SetPermissionsResult, error) { + return r0, r1 }) } -func (f *PermsStoreSetUserPermissionsFunc) nextHook() func(context.Context, *authz.UserPermissions) error { +func (f *PermsStoreSetUserPermissionsFunc) nextHook() func(context.Context, *authz.UserPermissions) (*database.SetPermissionsResult, error) { f.mutex.Lock() defer f.mutex.Unlock() @@ -15176,7 +15179,10 @@ type PermsStoreSetUserPermissionsFuncCall struct { Arg1 *authz.UserPermissions // Result0 is the value of the 1st result returned from this method // invocation. - Result0 error + Result0 *database.SetPermissionsResult + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error } // Args returns an interface slice containing the arguments of this @@ -15188,7 +15194,7 @@ func (c PermsStoreSetUserPermissionsFuncCall) Args() []interface{} { // Results returns an interface slice containing the results of this // invocation. func (c PermsStoreSetUserPermissionsFuncCall) Results() []interface{} { - return []interface{}{c.Result0} + return []interface{}{c.Result0, c.Result1} } // PermsStoreTouchRepoPermissionsFunc describes the behavior when the diff --git a/enterprise/internal/database/perms_store.go b/enterprise/internal/database/perms_store.go index be351ad4b66d..081e3385499d 100644 --- a/enterprise/internal/database/perms_store.go +++ b/enterprise/internal/database/perms_store.go @@ -70,7 +70,7 @@ type PermsStore interface { // ---------+------------+---------------+------------+------------- // 1 | read | {1} | NOW() | // 2 | read | {1} | NOW() | - SetUserPermissions(ctx context.Context, p *authz.UserPermissions) error + SetUserPermissions(ctx context.Context, p *authz.UserPermissions) (*database.SetPermissionsResult, error) // SetRepoPermissions performs a full update for p, new user IDs found in p will // be upserted and user IDs no longer in p will be removed. This method updates // both `user_permissions` and `repo_permissions` tables. @@ -95,7 +95,7 @@ type PermsStore interface { // repo_id | permission | user_ids_ints | updated_at | synced_at // ---------+------------+---------------+------------+----------- // 1 | read | {1, 2} | NOW() | NOW() - SetRepoPermissions(ctx context.Context, p *authz.RepoPermissions) error + SetRepoPermissions(ctx context.Context, p *authz.RepoPermissions) (*database.SetPermissionsResult, error) // SetRepoPermissionsUnrestricted sets the unrestricted on the // repo_permissions table for all the provided repos. Either all or non // are updated. If the repository ID is not in repo_permissions yet, a row @@ -338,14 +338,14 @@ func (s *permsStore) LoadRepoPermissions(ctx context.Context, p *authz.RepoPermi return nil } -func (s *permsStore) SetUserPermissions(ctx context.Context, p *authz.UserPermissions) (err error) { +func (s *permsStore) SetUserPermissions(ctx context.Context, p *authz.UserPermissions) (_ *database.SetPermissionsResult, err error) { ctx, save := s.observe(ctx, "SetUserPermissions", "") defer func() { save(&err, p.TracingFields()...) }() // Open a transaction for update consistency. txs, err := s.transact(ctx) if err != nil { - return err + return nil, err } defer func() { err = txs.Done(err) }() @@ -354,7 +354,7 @@ func (s *permsStore) SetUserPermissions(ctx context.Context, p *authz.UserPermis ids, _, _, err := txs.loadUserPermissions(ctx, p, "FOR UPDATE") if err != nil { if err != authz.ErrPermsNotFound { - return errors.Wrap(err, "load user permissions") + return nil, errors.Wrap(err, "load user permissions") } } else { oldIDs = sliceToSet(ids) @@ -385,9 +385,9 @@ func (s *permsStore) SetUserPermissions(ctx context.Context, p *authz.UserPermis page, addQueue, removeQueue, hasNextPage = newUpsertRepoPermissionsPage(addQueue, removeQueue) if q, err := upsertRepoPermissionsBatchQuery(page, allAdded, []int32{p.UserID}, p.Perm, updatedAt); err != nil { - return err + return nil, err } else if err = txs.execute(ctx, q); err != nil { - return errors.Wrap(err, "execute upsert repo permissions batch query") + return nil, errors.Wrap(err, "execute upsert repo permissions batch query") } } } @@ -398,12 +398,16 @@ func (s *permsStore) SetUserPermissions(ctx context.Context, p *authz.UserPermis p.UpdatedAt = updatedAt p.SyncedAt = updatedAt if q, err := upsertUserPermissionsQuery(p); err != nil { - return err + return nil, err } else if err = txs.execute(ctx, q); err != nil { - return errors.Wrap(err, "execute upsert user permissions query") + return nil, errors.Wrap(err, "execute upsert user permissions query") } - return nil + return &database.SetPermissionsResult{ + Added: len(added), + Removed: len(removed), + Found: len(p.IDs), + }, nil } // upsertUserPermissionsQuery upserts single row of user permissions, it does the @@ -445,7 +449,7 @@ DO UPDATE SET ), nil } -func (s *permsStore) SetRepoPermissions(ctx context.Context, p *authz.RepoPermissions) (err error) { +func (s *permsStore) SetRepoPermissions(ctx context.Context, p *authz.RepoPermissions) (_ *database.SetPermissionsResult, err error) { ctx, save := s.observe(ctx, "SetRepoPermissions", "") defer func() { save(&err, p.TracingFields()...) }() @@ -455,7 +459,7 @@ func (s *permsStore) SetRepoPermissions(ctx context.Context, p *authz.RepoPermis } else { txs, err = s.transact(ctx) if err != nil { - return err + return nil, err } defer func() { err = txs.Done(err) }() } @@ -465,7 +469,7 @@ func (s *permsStore) SetRepoPermissions(ctx context.Context, p *authz.RepoPermis ids, _, _, _, err := txs.loadRepoPermissions(ctx, p, "FOR UPDATE") if err != nil { if err != authz.ErrPermsNotFound { - return errors.Wrap(err, "load repo permissions") + return nil, errors.Wrap(err, "load repo permissions") } } else { oldIDs = sliceToSet(ids) @@ -484,9 +488,9 @@ func (s *permsStore) SetRepoPermissions(ctx context.Context, p *authz.RepoPermis updatedAt := txs.clock() if len(added) != 0 || len(removed) != 0 { if q, err := upsertUserPermissionsBatchQuery(added, removed, []int32{p.RepoID}, p.Perm, authz.PermRepos, updatedAt); err != nil { - return err + return nil, err } else if err = txs.execute(ctx, q); err != nil { - return errors.Wrap(err, "execute upsert user permissions batch query") + return nil, errors.Wrap(err, "execute upsert user permissions batch query") } } @@ -496,12 +500,16 @@ func (s *permsStore) SetRepoPermissions(ctx context.Context, p *authz.RepoPermis p.UpdatedAt = updatedAt p.SyncedAt = updatedAt if q, err := upsertRepoPermissionsQuery(p); err != nil { - return err + return nil, err } else if err = txs.execute(ctx, q); err != nil { - return errors.Wrap(err, "execute upsert repo permissions query") + return nil, errors.Wrap(err, "execute upsert repo permissions query") } - return nil + return &database.SetPermissionsResult{ + Added: len(added), + Removed: len(removed), + Found: len(p.UserIDs), + }, nil } // upsertUserPermissionsBatchQuery composes a SQL query that does both addition (for `addedUserIDs`) and deletion ( diff --git a/enterprise/internal/database/perms_store_test.go b/enterprise/internal/database/perms_store_test.go index ba567a856701..fd6c37f63fcf 100644 --- a/enterprise/internal/database/perms_store_test.go +++ b/enterprise/internal/database/perms_store_test.go @@ -78,7 +78,7 @@ func testPermsStore_LoadUserPermissions(db database.DB) func(*testing.T) { Perm: authz.Read, UserIDs: toMapset(2), } - if err := s.SetRepoPermissions(context.Background(), rp); err != nil { + if _, err := s.SetRepoPermissions(context.Background(), rp); err != nil { t.Fatal(err) } @@ -105,7 +105,7 @@ func testPermsStore_LoadUserPermissions(db database.DB) func(*testing.T) { Perm: authz.Read, UserIDs: toMapset(2), } - if err := s.SetRepoPermissions(context.Background(), rp); err != nil { + if _, err := s.SetRepoPermissions(context.Background(), rp); err != nil { t.Fatal(err) } @@ -136,7 +136,7 @@ func testPermsStore_LoadUserPermissions(db database.DB) func(*testing.T) { Perm: authz.Read, UserIDs: toMapset(1, 2), } - if err := s.SetRepoPermissions(context.Background(), rp); err != nil { + if _, err := s.SetRepoPermissions(context.Background(), rp); err != nil { t.Fatal(err) } @@ -145,7 +145,7 @@ func testPermsStore_LoadUserPermissions(db database.DB) func(*testing.T) { Perm: authz.Read, UserIDs: toMapset(2, 3), } - if err := s.SetRepoPermissions(context.Background(), rp); err != nil { + if _, err := s.SetRepoPermissions(context.Background(), rp); err != nil { t.Fatal(err) } @@ -212,7 +212,7 @@ func testPermsStore_LoadRepoPermissions(db database.DB) func(*testing.T) { Type: authz.PermRepos, IDs: toMapset(1), } - if err := s.SetUserPermissions(context.Background(), up); err != nil { + if _, err := s.SetUserPermissions(context.Background(), up); err != nil { t.Fatal(err) } @@ -239,7 +239,7 @@ func testPermsStore_LoadRepoPermissions(db database.DB) func(*testing.T) { Type: authz.PermRepos, IDs: toMapset(1), } - if err := s.SetUserPermissions(context.Background(), up); err != nil { + if _, err := s.SetUserPermissions(context.Background(), up); err != nil { t.Fatal(err) } @@ -279,7 +279,7 @@ func testPermsStore_FetchReposByUserAndExternalService(db database.DB) func(*tes Perm: authz.Read, UserIDs: toMapset(2), } - if err := s.SetRepoPermissions(context.Background(), rp); err != nil { + if _, err := s.SetRepoPermissions(context.Background(), rp); err != nil { t.Fatal(err) } @@ -305,7 +305,7 @@ func testPermsStore_FetchReposByUserAndExternalService(db database.DB) func(*tes Perm: authz.Read, UserIDs: toMapset(2), } - if err := s.SetRepoPermissions(context.Background(), rp); err != nil { + if _, err := s.SetRepoPermissions(context.Background(), rp); err != nil { t.Fatal(err) } @@ -371,6 +371,7 @@ func testPermsStore_SetUserPermissions(db database.DB) func(*testing.T) { updates []*authz.UserPermissions expectUserPerms map[int32][]uint32 // user_id -> object_ids expectRepoPerms map[int32][]uint32 // repo_id -> user_ids + expectedResult []*database.SetPermissionsResult upsertRepoPermissionsPageSize int }{ @@ -385,6 +386,11 @@ func testPermsStore_SetUserPermissions(db database.DB) func(*testing.T) { expectUserPerms: map[int32][]uint32{ 1: {}, }, + expectedResult: []*database.SetPermissionsResult{{ + Added: 0, + Removed: 0, + Found: 0, + }}, }, { name: "add", @@ -414,6 +420,23 @@ func testPermsStore_SetUserPermissions(db database.DB) func(*testing.T) { 3: {3}, 4: {3}, }, + expectedResult: []*database.SetPermissionsResult{ + { + Added: 1, + Removed: 0, + Found: 1, + }, + { + Added: 2, + Removed: 0, + Found: 2, + }, + { + Added: 2, + Removed: 0, + Found: 2, + }, + }, }, { name: "add and update", @@ -445,6 +468,28 @@ func testPermsStore_SetUserPermissions(db database.DB) func(*testing.T) { 2: {1}, 3: {1, 2}, }, + expectedResult: []*database.SetPermissionsResult{ + { + Added: 1, + Removed: 0, + Found: 1, + }, + { + Added: 2, + Removed: 1, + Found: 2, + }, + { + Added: 2, + Removed: 0, + Found: 2, + }, + { + Added: 1, + Removed: 1, + Found: 2, + }, + }, }, { name: "add and clear", @@ -467,6 +512,18 @@ func testPermsStore_SetUserPermissions(db database.DB) func(*testing.T) { 2: {}, 3: {}, }, + expectedResult: []*database.SetPermissionsResult{ + { + Added: 3, + Removed: 0, + Found: 3, + }, + { + Added: 0, + Removed: 3, + Found: 0, + }, + }, }, { name: "add and page", @@ -486,6 +543,13 @@ func testPermsStore_SetUserPermissions(db database.DB) func(*testing.T) { 2: {1}, 3: {1}, }, + expectedResult: []*database.SetPermissionsResult{ + { + Added: 3, + Removed: 0, + Found: 3, + }, + }, }, { name: postgresParameterLimitTest, @@ -533,7 +597,7 @@ func testPermsStore_SetUserPermissions(db database.DB) func(*testing.T) { Type: authz.PermRepos, IDs: toMapset(1), } - if err := s.SetUserPermissions(context.Background(), up); err != nil { + if _, err := s.SetUserPermissions(context.Background(), up); err != nil { t.Fatal(err) } @@ -570,23 +634,22 @@ func testPermsStore_SetUserPermissions(db database.DB) func(*testing.T) { } }) - for _, p := range test.updates { - const numOps = 30 - g, ctx := errgroup.WithContext(context.Background()) - for i := 0; i < numOps; i++ { - g.Go(func() error { - tmp := &authz.UserPermissions{ - UserID: p.UserID, - Perm: p.Perm, - UpdatedAt: p.UpdatedAt, - } - if p.IDs != nil { - tmp.IDs = p.IDs - } - return s.SetUserPermissions(ctx, tmp) - }) + for index, p := range test.updates { + tmp := &authz.UserPermissions{ + UserID: p.UserID, + Perm: p.Perm, + UpdatedAt: p.UpdatedAt, } - if err := g.Wait(); err != nil { + if p.IDs != nil { + tmp.IDs = p.IDs + } + result, err := s.SetUserPermissions(context.Background(), tmp) + + if diff := cmp.Diff(test.expectedResult[index], result); diff != "" { + t.Fatal(diff) + } + + if err != nil { t.Fatal(err) } } @@ -632,7 +695,7 @@ func testPermsStore_SetRepoPermissionsUnrestricted(db database.DB) func(*testing Perm: authz.Read, UserIDs: toMapset(2), } - if err := s.SetRepoPermissions(context.Background(), rp); err != nil { + if _, err := s.SetRepoPermissions(context.Background(), rp); err != nil { t.Fatal(err) } } @@ -685,6 +748,7 @@ func testPermsStore_SetRepoPermissions(db database.DB) func(*testing.T) { updates []*authz.RepoPermissions expectUserPerms map[int32][]uint32 // user_id -> object_ids expectRepoPerms map[int32][]uint32 // repo_id -> user_ids + expectedResult []*database.SetPermissionsResult }{ { name: "empty", @@ -697,6 +761,13 @@ func testPermsStore_SetRepoPermissions(db database.DB) func(*testing.T) { expectRepoPerms: map[int32][]uint32{ 1: {}, }, + expectedResult: []*database.SetPermissionsResult{ + { + Added: 0, + Removed: 0, + Found: 0, + }, + }, }, { name: "add", @@ -726,6 +797,23 @@ func testPermsStore_SetRepoPermissions(db database.DB) func(*testing.T) { 2: {1, 2}, 3: {3, 4}, }, + expectedResult: []*database.SetPermissionsResult{ + { + Added: 1, + Removed: 0, + Found: 1, + }, + { + Added: 2, + Removed: 0, + Found: 2, + }, + { + Added: 2, + Removed: 0, + Found: 2, + }, + }, }, { name: "add and update", @@ -758,6 +846,28 @@ func testPermsStore_SetRepoPermissions(db database.DB) func(*testing.T) { 1: {2, 3}, 2: {3, 4}, }, + expectedResult: []*database.SetPermissionsResult{ + { + Added: 1, + Removed: 0, + Found: 1, + }, + { + Added: 2, + Removed: 1, + Found: 2, + }, + { + Added: 2, + Removed: 0, + Found: 2, + }, + { + Added: 2, + Removed: 2, + Found: 2, + }, + }, }, { name: "add and clear", @@ -780,6 +890,18 @@ func testPermsStore_SetRepoPermissions(db database.DB) func(*testing.T) { expectRepoPerms: map[int32][]uint32{ 1: {}, }, + expectedResult: []*database.SetPermissionsResult{ + { + Added: 3, + Removed: 0, + Found: 3, + }, + { + Added: 0, + Removed: 3, + Found: 0, + }, + }, }, } @@ -796,7 +918,7 @@ func testPermsStore_SetRepoPermissions(db database.DB) func(*testing.T) { Perm: authz.Read, UserIDs: toMapset(2), } - if err := s.SetRepoPermissions(context.Background(), rp); err != nil { + if _, err := s.SetRepoPermissions(context.Background(), rp); err != nil { t.Fatal(err) } @@ -824,7 +946,7 @@ func testPermsStore_SetRepoPermissions(db database.DB) func(*testing.T) { UserIDs: toMapset(2), Unrestricted: true, } - if err := s.SetRepoPermissions(context.Background(), rp); err != nil { + if _, err := s.SetRepoPermissions(context.Background(), rp); err != nil { t.Fatal(err) } @@ -847,25 +969,23 @@ func testPermsStore_SetRepoPermissions(db database.DB) func(*testing.T) { cleanupPermsTables(t, s) }) - for _, p := range test.updates { - const numOps = 30 - g, ctx := errgroup.WithContext(context.Background()) - for i := 0; i < numOps; i++ { - g.Go(func() error { - tmp := &authz.RepoPermissions{ - RepoID: p.RepoID, - Perm: p.Perm, - UpdatedAt: p.UpdatedAt, - } - if p.UserIDs != nil { - tmp.UserIDs = p.UserIDs - } - return s.SetRepoPermissions(ctx, tmp) - }) + for index, p := range test.updates { + tmp := &authz.RepoPermissions{ + RepoID: p.RepoID, + Perm: p.Perm, + UpdatedAt: p.UpdatedAt, } - if err := g.Wait(); err != nil { + if p.UserIDs != nil { + tmp.UserIDs = p.UserIDs + } + result, err := s.SetRepoPermissions(context.Background(), tmp) + if err != nil { t.Fatal(err) } + + if diff := cmp.Diff(test.expectedResult[index], result); diff != "" { + t.Fatal(diff) + } } err := checkRegularPermsTable(s, `SELECT user_id, object_ids_ints FROM user_permissions`, test.expectUserPerms) @@ -904,7 +1024,7 @@ func testPermsStore_TouchRepoPermissions(db database.DB) func(*testing.T) { Perm: authz.Read, UserIDs: toMapset(2), } - if err := s.SetRepoPermissions(context.Background(), rp); err != nil { + if _, err := s.SetRepoPermissions(context.Background(), rp); err != nil { t.Fatal(err) } @@ -956,7 +1076,7 @@ func testPermsStore_TouchUserPermissions(db database.DB) func(*testing.T) { Type: authz.PermRepos, IDs: toMapset(2), } - if err := s.SetUserPermissions(ctx, up); err != nil { + if _, err := s.SetUserPermissions(ctx, up); err != nil { t.Fatal(err) } @@ -2140,7 +2260,7 @@ func testPermsStore_GrantPendingPermissions(db database.DB) func(*testing.T) { for _, update := range test.updates { for _, p := range update.regulars { - if err := s.SetRepoPermissions(ctx, p); err != nil { + if _, err := s.SetRepoPermissions(ctx, p); err != nil { t.Fatal(err) } } @@ -2254,14 +2374,14 @@ func testPermsStore_DeleteAllUserPermissions(db database.DB) func(*testing.T) { ctx := context.Background() // Set permissions for user 1 and 2 - if err := s.SetRepoPermissions(ctx, &authz.RepoPermissions{ + if _, err := s.SetRepoPermissions(ctx, &authz.RepoPermissions{ RepoID: 1, Perm: authz.Read, UserIDs: toMapset(1, 2), }); err != nil { t.Fatal(err) } - if err := s.SetRepoPermissions(ctx, &authz.RepoPermissions{ + if _, err := s.SetRepoPermissions(ctx, &authz.RepoPermissions{ RepoID: 2, Perm: authz.Read, UserIDs: toMapset(1, 2), @@ -2367,7 +2487,7 @@ func testPermsStore_DatabaseDeadlocks(db database.DB) func(*testing.T) { ctx := context.Background() setUserPermissions := func(ctx context.Context, t *testing.T) { - if err := s.SetUserPermissions(ctx, &authz.UserPermissions{ + if _, err := s.SetUserPermissions(ctx, &authz.UserPermissions{ UserID: 1, Perm: authz.Read, IDs: toMapset(1), @@ -2376,7 +2496,7 @@ func testPermsStore_DatabaseDeadlocks(db database.DB) func(*testing.T) { } } setRepoPermissions := func(ctx context.Context, t *testing.T) { - if err := s.SetRepoPermissions(ctx, &authz.RepoPermissions{ + if _, err := s.SetRepoPermissions(ctx, &authz.RepoPermissions{ RepoID: 1, Perm: authz.Read, UserIDs: toMapset(1), @@ -2565,7 +2685,7 @@ func testPermsStore_UserIDsWithNoPerms(db database.DB) func(*testing.T) { } // Give "alice" some permissions - err = s.SetRepoPermissions(ctx, &authz.RepoPermissions{ + _, err = s.SetRepoPermissions(ctx, &authz.RepoPermissions{ RepoID: 1, Perm: authz.Read, UserIDs: toMapset(1), @@ -2635,7 +2755,7 @@ func testPermsStore_RepoIDsWithNoPerms(db database.DB) func(*testing.T) { } // Give "private_repo" regular permissions and "private_repo_2" pending permissions - err = s.SetRepoPermissions(ctx, &authz.RepoPermissions{ + _, err = s.SetRepoPermissions(ctx, &authz.RepoPermissions{ RepoID: 1, Perm: authz.Read, UserIDs: toMapset(1), @@ -2701,7 +2821,7 @@ func testPermsStore_UserIDsWithOldestPerms(db database.DB) func(*testing.T) { } // Set up some permissions - err := s.SetRepoPermissions(ctx, &authz.RepoPermissions{ + _, err := s.SetRepoPermissions(ctx, &authz.RepoPermissions{ RepoID: 1, Perm: authz.Read, UserIDs: toMapset(1, 2), @@ -2826,7 +2946,7 @@ func testPermsStore_ReposIDsWithOldestPerms(db database.DB) func(*testing.T) { }, } for _, perm := range perms { - err := s.SetRepoPermissions(ctx, perm) + _, err := s.SetRepoPermissions(ctx, perm) if err != nil { t.Fatal(err) } @@ -3009,7 +3129,7 @@ func testPermsStore_Metrics(db database.DB) func(*testing.T) { // Set up permissions for the various repos. for i := 0; i < 4; i++ { - err := s.SetRepoPermissions(ctx, &authz.RepoPermissions{ + _, err := s.SetRepoPermissions(ctx, &authz.RepoPermissions{ RepoID: int32(i), Perm: authz.Read, UserIDs: toMapset(1, 2, 3, 4), diff --git a/internal/database/mocks_temp.go b/internal/database/mocks_temp.go index d1a4bb8e4121..dc552be538db 100644 --- a/internal/database/mocks_temp.go +++ b/internal/database/mocks_temp.go @@ -35240,6 +35240,9 @@ type MockPermissionSyncJobStore struct { // ListFunc is an instance of a mock function object controlling the // behavior of the method List. ListFunc *PermissionSyncJobStoreListFunc + // SaveSyncResultFunc is an instance of a mock function object + // controlling the behavior of the method SaveSyncResult. + SaveSyncResultFunc *PermissionSyncJobStoreSaveSyncResultFunc // TransactFunc is an instance of a mock function object controlling the // behavior of the method Transact. TransactFunc *PermissionSyncJobStoreTransactFunc @@ -35283,6 +35286,11 @@ func NewMockPermissionSyncJobStore() *MockPermissionSyncJobStore { return }, }, + SaveSyncResultFunc: &PermissionSyncJobStoreSaveSyncResultFunc{ + defaultHook: func(context.Context, int, *SetPermissionsResult) (r0 error) { + return + }, + }, TransactFunc: &PermissionSyncJobStoreTransactFunc{ defaultHook: func(context.Context) (r0 PermissionSyncJobStore, r1 error) { return @@ -35331,6 +35339,11 @@ func NewStrictMockPermissionSyncJobStore() *MockPermissionSyncJobStore { panic("unexpected invocation of MockPermissionSyncJobStore.List") }, }, + SaveSyncResultFunc: &PermissionSyncJobStoreSaveSyncResultFunc{ + defaultHook: func(context.Context, int, *SetPermissionsResult) error { + panic("unexpected invocation of MockPermissionSyncJobStore.SaveSyncResult") + }, + }, TransactFunc: &PermissionSyncJobStoreTransactFunc{ defaultHook: func(context.Context) (PermissionSyncJobStore, error) { panic("unexpected invocation of MockPermissionSyncJobStore.Transact") @@ -35367,6 +35380,9 @@ func NewMockPermissionSyncJobStoreFrom(i PermissionSyncJobStore) *MockPermission ListFunc: &PermissionSyncJobStoreListFunc{ defaultHook: i.List, }, + SaveSyncResultFunc: &PermissionSyncJobStoreSaveSyncResultFunc{ + defaultHook: i.SaveSyncResult, + }, TransactFunc: &PermissionSyncJobStoreTransactFunc{ defaultHook: i.Transact, }, @@ -36021,6 +36037,118 @@ func (c PermissionSyncJobStoreListFuncCall) Results() []interface{} { return []interface{}{c.Result0, c.Result1} } +// PermissionSyncJobStoreSaveSyncResultFunc describes the behavior when the +// SaveSyncResult method of the parent MockPermissionSyncJobStore instance +// is invoked. +type PermissionSyncJobStoreSaveSyncResultFunc struct { + defaultHook func(context.Context, int, *SetPermissionsResult) error + hooks []func(context.Context, int, *SetPermissionsResult) error + history []PermissionSyncJobStoreSaveSyncResultFuncCall + mutex sync.Mutex +} + +// SaveSyncResult delegates to the next hook function in the queue and +// stores the parameter and result values of this invocation. +func (m *MockPermissionSyncJobStore) SaveSyncResult(v0 context.Context, v1 int, v2 *SetPermissionsResult) error { + r0 := m.SaveSyncResultFunc.nextHook()(v0, v1, v2) + m.SaveSyncResultFunc.appendCall(PermissionSyncJobStoreSaveSyncResultFuncCall{v0, v1, v2, r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the SaveSyncResult +// method of the parent MockPermissionSyncJobStore instance is invoked and +// the hook queue is empty. +func (f *PermissionSyncJobStoreSaveSyncResultFunc) SetDefaultHook(hook func(context.Context, int, *SetPermissionsResult) error) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// SaveSyncResult method of the parent MockPermissionSyncJobStore instance +// invokes the hook at the front of the queue and discards it. After the +// queue is empty, the default hook function is invoked for any future +// action. +func (f *PermissionSyncJobStoreSaveSyncResultFunc) PushHook(hook func(context.Context, int, *SetPermissionsResult) error) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *PermissionSyncJobStoreSaveSyncResultFunc) SetDefaultReturn(r0 error) { + f.SetDefaultHook(func(context.Context, int, *SetPermissionsResult) error { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *PermissionSyncJobStoreSaveSyncResultFunc) PushReturn(r0 error) { + f.PushHook(func(context.Context, int, *SetPermissionsResult) error { + return r0 + }) +} + +func (f *PermissionSyncJobStoreSaveSyncResultFunc) nextHook() func(context.Context, int, *SetPermissionsResult) error { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *PermissionSyncJobStoreSaveSyncResultFunc) appendCall(r0 PermissionSyncJobStoreSaveSyncResultFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of +// PermissionSyncJobStoreSaveSyncResultFuncCall objects describing the +// invocations of this function. +func (f *PermissionSyncJobStoreSaveSyncResultFunc) History() []PermissionSyncJobStoreSaveSyncResultFuncCall { + f.mutex.Lock() + history := make([]PermissionSyncJobStoreSaveSyncResultFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// PermissionSyncJobStoreSaveSyncResultFuncCall is an object that describes +// an invocation of method SaveSyncResult on an instance of +// MockPermissionSyncJobStore. +type PermissionSyncJobStoreSaveSyncResultFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 int + // Arg2 is the value of the 3rd argument passed to this method + // invocation. + Arg2 *SetPermissionsResult + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c PermissionSyncJobStoreSaveSyncResultFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1, c.Arg2} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c PermissionSyncJobStoreSaveSyncResultFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + // PermissionSyncJobStoreTransactFunc describes the behavior when the // Transact method of the parent MockPermissionSyncJobStore instance is // invoked. diff --git a/internal/database/permission_sync_jobs.go b/internal/database/permission_sync_jobs.go index 122580ed2366..746d9dbef0f8 100644 --- a/internal/database/permission_sync_jobs.go +++ b/internal/database/permission_sync_jobs.go @@ -87,6 +87,7 @@ type PermissionSyncJobStore interface { List(ctx context.Context, opts ListPermissionSyncJobOpts) ([]*PermissionSyncJob, error) CancelQueuedJob(ctx context.Context, reason string, id int) error + SaveSyncResult(ctx context.Context, id int, result *SetPermissionsResult) error } type permissionSyncJobStore struct { @@ -281,6 +282,26 @@ WHERE id = %s AND state = 'queued' AND cancel IS FALSE return nil } +type SetPermissionsResult struct { + Added int + Removed int + Found int +} + +func (s *permissionSyncJobStore) SaveSyncResult(ctx context.Context, id int, result *SetPermissionsResult) error { + q := sqlf.Sprintf(` + UPDATE permission_sync_jobs + SET + permissions_added = %d, + permissions_removed = %d, + permissions_found = %d + WHERE id = %d + `, result.Added, result.Removed, result.Found, id) + + _, err := s.ExecResult(ctx, q) + return err +} + type ListPermissionSyncJobOpts struct { ID int UserID int @@ -401,6 +422,10 @@ type PermissionSyncJob struct { Priority PermissionSyncJobPriority NoPerms bool InvalidateCaches bool + + PermissionsAdded int + PermissionsRemoved int + PermissionsFound int } func (j *PermissionSyncJob) RecordID() int { return j.ID } @@ -429,6 +454,10 @@ var PermissionSyncJobColumns = []*sqlf.Query{ sqlf.Sprintf("permission_sync_jobs.priority"), sqlf.Sprintf("permission_sync_jobs.no_perms"), sqlf.Sprintf("permission_sync_jobs.invalidate_caches"), + + sqlf.Sprintf("permission_sync_jobs.permissions_added"), + sqlf.Sprintf("permission_sync_jobs.permissions_removed"), + sqlf.Sprintf("permission_sync_jobs.permissions_found"), } func ScanPermissionSyncJob(s dbutil.Scanner) (*PermissionSyncJob, error) { @@ -466,6 +495,10 @@ func scanPermissionSyncJob(job *PermissionSyncJob, s dbutil.Scanner) error { &job.Priority, &job.NoPerms, &job.InvalidateCaches, + + &job.PermissionsAdded, + &job.PermissionsRemoved, + &job.PermissionsFound, ); err != nil { return err } diff --git a/internal/database/permission_sync_jobs_test.go b/internal/database/permission_sync_jobs_test.go index 6e73482d172f..a64114c38f3a 100644 --- a/internal/database/permission_sync_jobs_test.go +++ b/internal/database/permission_sync_jobs_test.go @@ -353,6 +353,47 @@ func TestPermissionSyncJobs_CancelQueuedJob(t *testing.T) { require.True(t, errcode.IsNotFound(err)) } +func TestPermissionSyncJobs_SaveSyncResult(t *testing.T) { + if testing.Short() { + t.Skip() + } + + logger := logtest.Scoped(t) + db := NewDB(logger, dbtest.NewDB(logger, t)) + ctx := context.Background() + + store := PermissionSyncJobsWith(logger, db) + reposStore := ReposWith(logger, db) + + // Create repo. + repo1 := types.Repo{Name: "test-repo-1", ID: 101} + err := reposStore.Create(ctx, &repo1) + require.NoError(t, err) + + // Creating result. + result := SetPermissionsResult{ + Added: 1, + Removed: 2, + Found: 5, + } + + // Adding a job. + err = store.CreateRepoSyncJob(ctx, repo1.ID, PermissionSyncJobOpts{Reason: ReasonManualUserSync}) + require.NoError(t, err) + + // Saving result should be successful. + err = store.SaveSyncResult(ctx, 1, &result) + require.NoError(t, err) + + // Checking that all the results are set. + jobs, err := store.List(ctx, ListPermissionSyncJobOpts{RepoID: int(repo1.ID)}) + require.NoError(t, err) + require.Len(t, jobs, 1) + require.Equal(t, 1, jobs[0].PermissionsAdded) + require.Equal(t, 2, jobs[0].PermissionsRemoved) + require.Equal(t, 5, jobs[0].PermissionsFound) +} + func TestPermissionSyncJobs_CascadeOnRepoDelete(t *testing.T) { if testing.Short() { t.Skip() diff --git a/internal/database/schema.json b/internal/database/schema.json index f38032f58013..a4834e702a56 100755 --- a/internal/database/schema.json +++ b/internal/database/schema.json @@ -17324,6 +17324,45 @@ "GenerationExpression": "", "Comment": "" }, + { + "Name": "permissions_added", + "Index": 22, + "TypeName": "integer", + "IsNullable": false, + "Default": "0", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, + { + "Name": "permissions_found", + "Index": 24, + "TypeName": "integer", + "IsNullable": false, + "Default": "0", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, + { + "Name": "permissions_removed", + "Index": 23, + "TypeName": "integer", + "IsNullable": false, + "Default": "0", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, { "Name": "priority", "Index": 18, diff --git a/internal/database/schema.md b/internal/database/schema.md index 162cfb1e9a7c..99f4365ae036 100755 --- a/internal/database/schema.md +++ b/internal/database/schema.md @@ -2680,6 +2680,9 @@ Foreign-key constraints: invalidate_caches | boolean | | not null | false cancellation_reason | text | | | no_perms | boolean | | not null | false + permissions_added | integer | | not null | 0 + permissions_removed | integer | | not null | 0 + permissions_found | integer | | not null | 0 Indexes: "permission_sync_jobs_pkey" PRIMARY KEY, btree (id) "permission_sync_jobs_unique" UNIQUE, btree (priority, user_id, repository_id, cancel, process_after) WHERE state = 'queued'::text diff --git a/migrations/frontend/1675367314_add_results_to_permission_sync_jobs/down.sql b/migrations/frontend/1675367314_add_results_to_permission_sync_jobs/down.sql new file mode 100644 index 000000000000..a5154461786c --- /dev/null +++ b/migrations/frontend/1675367314_add_results_to_permission_sync_jobs/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE permission_sync_jobs DROP COLUMN IF EXISTS permissions_added; +ALTER TABLE permission_sync_jobs DROP COLUMN IF EXISTS permissions_removed; +ALTER TABLE permission_sync_jobs DROP COLUMN IF EXISTS permissions_found; diff --git a/migrations/frontend/1675367314_add_results_to_permission_sync_jobs/metadata.yaml b/migrations/frontend/1675367314_add_results_to_permission_sync_jobs/metadata.yaml new file mode 100644 index 000000000000..c1970457db62 --- /dev/null +++ b/migrations/frontend/1675367314_add_results_to_permission_sync_jobs/metadata.yaml @@ -0,0 +1,2 @@ +name: add_results_to_permission_sync_jobs +parents: [1675277968] diff --git a/migrations/frontend/1675367314_add_results_to_permission_sync_jobs/up.sql b/migrations/frontend/1675367314_add_results_to_permission_sync_jobs/up.sql new file mode 100644 index 000000000000..767642339be2 --- /dev/null +++ b/migrations/frontend/1675367314_add_results_to_permission_sync_jobs/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE permission_sync_jobs ADD COLUMN IF NOT EXISTS permissions_added integer NOT NULL DEFAULT 0; +ALTER TABLE permission_sync_jobs ADD COLUMN IF NOT EXISTS permissions_removed integer NOT NULL DEFAULT 0; +ALTER TABLE permission_sync_jobs ADD COLUMN IF NOT EXISTS permissions_found integer NOT NULL DEFAULT 0; diff --git a/migrations/frontend/squashed.sql b/migrations/frontend/squashed.sql index dfa99435c383..7d1e4d1850dd 100755 --- a/migrations/frontend/squashed.sql +++ b/migrations/frontend/squashed.sql @@ -3333,6 +3333,9 @@ CREATE TABLE permission_sync_jobs ( invalidate_caches boolean DEFAULT false NOT NULL, cancellation_reason text, no_perms boolean DEFAULT false NOT NULL, + permissions_added integer DEFAULT 0 NOT NULL, + permissions_removed integer DEFAULT 0 NOT NULL, + permissions_found integer DEFAULT 0 NOT NULL, CONSTRAINT permission_sync_jobs_for_repo_or_user CHECK (((user_id IS NULL) <> (repository_id IS NULL))) ); From e026e7516752fab923165a6b422df68f77c7fd85 Mon Sep 17 00:00:00 2001 From: Stefan Hengl Date: Fri, 3 Feb 2023 10:19:20 +0100 Subject: [PATCH 386/678] search: send stats for retry of structural search (#47319) If the retry fails, we don't send any stats which can lead to a "success" status even if the search timed out. This triggered our alarms over the last couple of days because our sentinel query for structural seems to suddenly take the retry code path. --- internal/search/structural/structural.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/search/structural/structural.go b/internal/search/structural/structural.go index 5ba7ed735705..ebed9476d255 100644 --- a/internal/search/structural/structural.go +++ b/internal/search/structural/structural.go @@ -120,9 +120,16 @@ func runStructuralSearch(ctx context.Context, clients job.RuntimeClients, args * event := agg.SearchEvent if len(event.Results) == 0 && err == nil { // retry structural search with a higher limit. - agg := streaming.NewAggregatingStream() - err := retryStructuralSearch(ctx, clients, args, repos, agg) + aggRetry := streaming.NewAggregatingStream() + err := retryStructuralSearch(ctx, clients, args, repos, aggRetry) if err != nil { + // It is possible that the retry couldn't search any repos before the context + // expired, in which case we send the stats from the first try. + stats := aggRetry.Stats + if stats.Zero() { + stats = agg.Stats + } + stream.Send(streaming.SearchEvent{Stats: stats}) return err } From abc1ed2961d5a88ae4a3045dc5b8ab8c98f3a6b1 Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Fri, 3 Feb 2023 11:38:27 +0200 Subject: [PATCH 387/678] graphqlbackend: deleted accidently commited file foo (#47313) Test Plan: CI --- cmd/frontend/graphqlbackend/foo | 35 --------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 cmd/frontend/graphqlbackend/foo diff --git a/cmd/frontend/graphqlbackend/foo b/cmd/frontend/graphqlbackend/foo deleted file mode 100644 index 592af5114d16..000000000000 --- a/cmd/frontend/graphqlbackend/foo +++ /dev/null @@ -1,35 +0,0 @@ -✓ cmd/frontend/graphqlbackend/externallink (cached) -✖ cmd/frontend/graphqlbackend (9.736s) -∅ cmd/frontend/graphqlbackend/graphqlutil - -=== Failed -=== FAIL: cmd/frontend/graphqlbackend TestUserEventLogResolver_URL/valid_URL (0.00s) - event_log_test.go:52: - Error Trace: /Users/tech/work/sourcegraph/cmd/frontend/graphqlbackend/event_log_test.go:52 - Error: Not equal: - expected: 10 - actual : 20 - Test: TestUserEventLogResolver_URL/valid_URL - --- FAIL: TestUserEventLogResolver_URL/valid_URL (0.00s) - -=== FAIL: cmd/frontend/graphqlbackend TestUserEventLogResolver_URL/invalid_URL (0.00s) - event_log_test.go:52: - Error Trace: /Users/tech/work/sourcegraph/cmd/frontend/graphqlbackend/event_log_test.go:52 - Error: Not equal: - expected: 10 - actual : 20 - Test: TestUserEventLogResolver_URL/invalid_URL - --- FAIL: TestUserEventLogResolver_URL/invalid_URL (0.00s) - -=== FAIL: cmd/frontend/graphqlbackend TestUserEventLogResolver_URL/not_a_URL (0.00s) - event_log_test.go:52: - Error Trace: /Users/tech/work/sourcegraph/cmd/frontend/graphqlbackend/event_log_test.go:52 - Error: Not equal: - expected: 10 - actual : 20 - Test: TestUserEventLogResolver_URL/not_a_URL - --- FAIL: TestUserEventLogResolver_URL/not_a_URL (0.00s) - -=== FAIL: cmd/frontend/graphqlbackend TestUserEventLogResolver_URL (0.00s) - -DONE 631 tests, 4 failures in 13.352s From cac660b8a4e4d58d5eec77895b1fddcca912c078 Mon Sep 17 00:00:00 2001 From: leo Date: Fri, 3 Feb 2023 09:40:44 +0000 Subject: [PATCH 388/678] insights: exporting is forbidden without valid code insights license (#47334) --- enterprise/internal/insights/httpapi/export.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/enterprise/internal/insights/httpapi/export.go b/enterprise/internal/insights/httpapi/export.go index ecb51e916f46..5dbb92e49208 100644 --- a/enterprise/internal/insights/httpapi/export.go +++ b/enterprise/internal/insights/httpapi/export.go @@ -15,6 +15,7 @@ import ( edb "github.com/sourcegraph/sourcegraph/enterprise/internal/database" "github.com/sourcegraph/sourcegraph/enterprise/internal/insights/store" + "github.com/sourcegraph/sourcegraph/enterprise/internal/licensing" "github.com/sourcegraph/sourcegraph/internal/actor" "github.com/sourcegraph/sourcegraph/internal/database" "github.com/sourcegraph/sourcegraph/lib/errors" @@ -55,6 +56,8 @@ func (h *ExportHandler) ExportFunc() http.HandlerFunc { http.Error(w, err.Error(), http.StatusNotFound) } else if errors.Is(err, authenticationError) { http.Error(w, err.Error(), http.StatusUnauthorized) + } else if errors.Is(err, invalidLicenseError) { + http.Error(w, err.Error(), http.StatusForbidden) } else { http.Error(w, fmt.Sprintf("failed to export data: %v", err), http.StatusInternalServerError) } @@ -77,6 +80,7 @@ type codeInsightsDataArchive struct { var notFoundError = errors.New("insight not found") var authenticationError = errors.New("authentication error") +var invalidLicenseError = errors.New("invalid license for code insights") func (h *ExportHandler) exportCodeInsightData(ctx context.Context, id string) (*codeInsightsDataArchive, error) { currentActor := actor.FromContext(ctx) @@ -88,6 +92,11 @@ func (h *ExportHandler) exportCodeInsightData(ctx context.Context, id string) (* return nil, authenticationError } + licenseError := licensing.Check(licensing.FeatureCodeInsights) + if licenseError != nil { + return nil, invalidLicenseError + } + var insightViewId string if err := relay.UnmarshalSpec(graphql.ID(id), &insightViewId); err != nil { return nil, errors.Wrap(err, "could not unmarshal insight view ID") From 0f7f0aff94e24f1e68059050cbee7c78cf3612d5 Mon Sep 17 00:00:00 2001 From: Taras Yemets Date: Fri, 3 Feb 2023 12:02:53 +0200 Subject: [PATCH 389/678] blob view code folding (#47266) --- client/web/src/repo/blob/CodeMirrorBlob.tsx | 2 + .../src/repo/blob/codemirror/code-folding.tsx | 177 ++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 client/web/src/repo/blob/codemirror/code-folding.tsx diff --git a/client/web/src/repo/blob/CodeMirrorBlob.tsx b/client/web/src/repo/blob/CodeMirrorBlob.tsx index 701bab11f4eb..c6b967b5b5b6 100644 --- a/client/web/src/repo/blob/CodeMirrorBlob.tsx +++ b/client/web/src/repo/blob/CodeMirrorBlob.tsx @@ -25,6 +25,7 @@ import { useExperimentalFeatures } from '../../stores' import { BlobInfo, BlobProps, updateBrowserHistoryIfChanged } from './Blob' import { blobPropsFacet } from './codemirror' import { createBlameDecorationsExtension } from './codemirror/blame-decorations' +import { codeFoldingExtension } from './codemirror/code-folding' import { syntaxHighlight } from './codemirror/highlight' import { pin, updatePin } from './codemirror/hovercard' import { selectableLineNumbers, SelectedLineRange, selectLines } from './codemirror/linenumbers' @@ -219,6 +220,7 @@ export const Blob: React.FunctionComponent = props => { overrideBrowserFindInPageShortcut: useFileSearch, onOverrideBrowserFindInPageToggle: setUseFileSearch, }), + codeFoldingExtension(), ], // A couple of values are not dependencies (blameDecorations, blobProps, // hasPin, position and settings) because those are updated in effects diff --git a/client/web/src/repo/blob/codemirror/code-folding.tsx b/client/web/src/repo/blob/codemirror/code-folding.tsx new file mode 100644 index 000000000000..34bd6fbdc696 --- /dev/null +++ b/client/web/src/repo/blob/codemirror/code-folding.tsx @@ -0,0 +1,177 @@ +import { foldEffect, foldGutter, foldKeymap, foldService } from '@codemirror/language' +import { EditorState, Extension, StateField } from '@codemirror/state' +import { EditorView, keymap, ViewPlugin, ViewUpdate } from '@codemirror/view' +import { mdiMenuDown, mdiMenuRight } from '@mdi/js' +import { createRoot } from 'react-dom/client' + +import { Icon } from '@sourcegraph/wildcard/src' + +import { rangeToCmSelection } from './occurrence-utils' +import { getCodeIntelTooltipState } from './token-selection/code-intel-tooltips' + +enum CharCode { + /** + * The `\t` character. + */ + Tab = 9, + + Space = 32, +} + +/** + * Returns the indent level or -1 if the line consists only of whitespace. + */ +function computeIndentLevel(line: string, tabSize: number): number { + let indent = 0 + let index = 0 + const len = line.length + + while (index < len) { + const charCode = line.charCodeAt(index) + + if (charCode === CharCode.Space) { + indent++ + } else if (charCode === CharCode.Tab) { + indent = indent - (indent % tabSize) + tabSize + } else { + break + } + index++ + } + + if (index === len) { + return -1 // line only consists of whitespace + } + + return indent +} + +/** + * Computes foldable ranges based on lines indentation. + * + * Implements similar to [VSCode indent-based folding](https://sourcegraph.com/github.com/microsoft/vscode@e3d73a5a2fd03412d83887a073c9c71bad38e964/-/blob/src/vs/editor/contrib/folding/browser/indentRangeProvider.ts?L126-200) logic. + */ +function computeFoldableRanges(state: EditorState): Map { + const ranges = new Map() + const previousRanges = [{ indent: -1, endAbove: state.doc.lines + 1 }] + + for (let lineNumber = state.doc.lines; lineNumber > 0; lineNumber--) { + const line = state.doc.line(lineNumber) + const indent = computeIndentLevel(line.text, state.tabSize) + if (indent === -1) { + continue + } + + let previous = previousRanges[previousRanges.length - 1] + if (previous.indent > indent) { + // remove ranges with larger indent + do { + previousRanges.pop() + previous = previousRanges[previousRanges.length - 1] + } while (previous.indent > indent) + + // new folding range + const endLineNumber = previous.endAbove - 1 + if (endLineNumber - lineNumber >= 1) { + // should be at least 2 lines + ranges.set(lineNumber, endLineNumber) + } + } + if (previous.indent === indent) { + previous.endAbove = lineNumber + } else { + // previous.indent < indent + // new range with a bigger indent + previousRanges.push({ indent, endAbove: lineNumber }) + } + } + + return ranges +} + +/** + * Stores foldable lines ranges as start line number to end line number map. + * + * Value is computed when field is initialized and never updated. + */ +const foldingRanges = StateField.define>({ + create: computeFoldableRanges, + update(value) { + return value + }, +}) + +function getFoldRange(state: EditorState, lineStart: number): { from: number; to: number } | null { + const ranges = state.field(foldingRanges) + const startLine = state.doc.lineAt(lineStart) + const endLineNumber = ranges.get(startLine.number) + + if (endLineNumber === undefined) { + return null + } + + const endLine = state.doc.line(endLineNumber) + return { from: startLine.to, to: endLine.to } +} + +/** + * Enables indent-based code folding. + */ +export function codeFoldingExtension(): Extension { + return [ + foldingRanges, + + foldService.of(getFoldRange), + + foldGutter({ + markerDOM(open: boolean): HTMLElement { + const container = document.createElement('div') + const root = createRoot(container) + root.render( + + ) + return container + }, + }), + + keymap.of(foldKeymap), + + ViewPlugin.define(view => ({ + update(update: ViewUpdate) { + for (const transaction of update.transactions) { + for (const effect of transaction.effects) { + if (effect.is(foldEffect)) { + const focusedOccurrence = getCodeIntelTooltipState(view, 'focus')?.occurrence + if (focusedOccurrence) { + const range = rangeToCmSelection(view.state, focusedOccurrence.range) + if (range.from >= effect.value.from && range.to <= effect.value.to) { + // Occurrence is inside the folded range. + // It will be removed from DOM triggering editor's blur. + // Focus it back for the keyboard navigation to work. + view.contentDOM.focus() + } + } + } + } + } + }, + })), + + EditorView.theme({ + '.cm-foldGutter': { + height: '1rem', + width: '1rem', + }, + '.cm-foldGutter .fold-icon': { + width: '100%', + height: '100%', + color: 'var(--text-muted)', + cursor: 'pointer', + }, + '.cm-foldPlaceholder': { + background: 'var(--color-bg-3)', + borderColor: 'var(--border-color)', + }, + }), + ] +} From fd1b26bd7bb12e8b267de6dfca4b654037a1efde Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 3 Feb 2023 11:06:44 +0100 Subject: [PATCH 390/678] Fix missing dependencies array that caused contributions to be registered more than once (#47321) --- client/web/src/contributions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/web/src/contributions.ts b/client/web/src/contributions.ts index 8ecebe5585b0..826de7a9f7ac 100644 --- a/client/web/src/contributions.ts +++ b/client/web/src/contributions.ts @@ -44,12 +44,12 @@ export function GlobalContributions(props: Props): null { historyOrNavigate: navigate, getLocation: () => locationRef.current, extensionsController, - locationAssign: globalThis.location.assign.bind(location), + locationAssign: globalThis.location.assign.bind(globalThis.location), }) ) } return () => subscriptions.unsubscribe() - }) + }, [extensionsController, navigate, platformContext]) // Throw error to the if (error) { From 359f39e613c663a59592eacdfcc7784643f3a5ea Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Fri, 3 Feb 2023 18:08:00 +0800 Subject: [PATCH 391/678] web: add warnings for react-router migration (#47365) --- .eslintrc.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index da90a3fee6c2..326e502e890d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -207,6 +207,31 @@ See https://handbook.sourcegraph.com/community/faq#is-all-of-sourcegraph-open-so message: "Spreading props can be unsafe. Prefer destructuring the props object, or continue only if you're sure.", }, + { + selector: 'ImportDeclaration[source.value="react-router"]', + message: + 'Use `react-router-dom-v5-compat` instead. We are in the process of migrating from react-router v5 to v6. More info https://github.com/sourcegraph/sourcegraph/issues/33834', + }, + { + selector: 'CallExpression[callee.name="useHistory"]', + message: + 'Use `useNavigate` from `react-router-dom-v5-compat` if possible. We are in the process of migrating from react-router v5 to v6. More info https://github.com/sourcegraph/sourcegraph/issues/33834', + }, + { + selector: 'ImportSpecifier[imported.name="RouteDescriptor"]', + message: + 'Use `RouteV6Descriptor` instead. We are in the process of migrating from react-router v5 to v6. More info https://github.com/sourcegraph/sourcegraph/issues/33834', + }, + { + selector: 'ImportSpecifier[imported.name="RouteComponentProps"]', + message: + 'Use `react-router-dom-v5-compat` hooks instead. We are in the process of migrating from react-router v5 to v6. More info https://github.com/sourcegraph/sourcegraph/issues/33834', + }, + { + selector: 'TSInterfaceHeritage[expression.name="RouteComponentProps"]', + message: + 'Use `react-router-dom-v5-compat` hooks instead. We are in the process of migrating from react-router v5 to v6. More info https://github.com/sourcegraph/sourcegraph/issues/33834', + }, ], // https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#eslint 'react/jsx-uses-react': 'off', From b72c8bb0709524da0cf5633bacbe660365519038 Mon Sep 17 00:00:00 2001 From: Felix Kling Date: Fri, 3 Feb 2023 11:56:30 +0100 Subject: [PATCH 392/678] experimental search input: Add back toggle buttons (#47268) This PR adds back the toggle buttons to the search input. This is an intermediate solution until we've explored whether we can provide the same functionality in a different way. Most notable difference is that the toggles are only shown when the input is focused. Unfortunately this doesn't work well with the smart search toggle button popover. Opening the popover will hide the buttons. A couple of ways to address this: - Don't show the smart search toggle button on the homepage. - Always show toggle buttons (not only on focus) - Find a way to render the popover inside the the search input container --- .../CodeMirrorQueryInputWrapper.module.scss | 16 +++++ .../CodeMirrorQueryInputWrapper.tsx | 64 ++++++++++++++----- .../experimental/LazyCodeMirrorQueryInput.tsx | 6 +- .../web/src/search/home/SearchPageInput.tsx | 15 ++++- 4 files changed, 80 insertions(+), 21 deletions(-) diff --git a/client/branded/src/search-ui/input/experimental/CodeMirrorQueryInputWrapper.module.scss b/client/branded/src/search-ui/input/experimental/CodeMirrorQueryInputWrapper.module.scss index f02edf878be4..aa0d08631a6d 100644 --- a/client/branded/src/search-ui/input/experimental/CodeMirrorQueryInputWrapper.module.scss +++ b/client/branded/src/search-ui/input/experimental/CodeMirrorQueryInputWrapper.module.scss @@ -35,6 +35,14 @@ min-height: 32px; align-items: center; + .show-when-focused { + display: none; + } + + .hide-when-focused { + display: block; + } + &:focus-within { outline: 2px solid rgba(163, 208, 255, 1); outline-offset: 0; @@ -76,3 +84,11 @@ position: relative; display: none; } + +.separator { + // stylelint-disable-next-line declaration-property-unit-allowed-list + width: 1px; + height: 1.125rem; + margin-right: 0.5rem; + background-color: var(--border-color-2); +} diff --git a/client/branded/src/search-ui/input/experimental/CodeMirrorQueryInputWrapper.tsx b/client/branded/src/search-ui/input/experimental/CodeMirrorQueryInputWrapper.tsx index 2429bb866670..d0c023c20a3b 100644 --- a/client/branded/src/search-ui/input/experimental/CodeMirrorQueryInputWrapper.tsx +++ b/client/branded/src/search-ui/input/experimental/CodeMirrorQueryInputWrapper.tsx @@ -27,8 +27,6 @@ import styles from './CodeMirrorQueryInputWrapper.module.scss' interface ExtensionConfig { popoverID: string - patternType: SearchPatternType - interpretComments: boolean isLightTheme: boolean placeholder: string onChange: (querySate: QueryState) => void @@ -78,8 +76,6 @@ const extensionsCompartment = new Compartment() // creating the editor and to update it when the props change. function configureExtensions({ popoverID, - patternType, - interpretComments, isLightTheme, placeholder, onChange, @@ -91,7 +87,6 @@ function configureExtensions({ const extensions = [ singleLine, EditorView.darkTheme.of(isLightTheme === false), - parseInputAsQuery({ patternType, interpretComments }), EditorView.updateListener.of(update => { if (update.docChanged) { onChange({ @@ -137,11 +132,28 @@ function configureExtensions({ return extensions } +// Holds extensions that somehow depend on the query or query parameters. They +// are stored in a separate compartment to avoid re-creating other extensions. +// (if we didn't do this the suggestions list would flicker because it gets +// recreated) +const querySettingsCompartment = new Compartment() + +function configureQueryExtensions({ + patternType, + interpretComments, +}: { + patternType: SearchPatternType + interpretComments: boolean +}): Extension { + return parseInputAsQuery({ patternType, interpretComments }) +} + function createEditor( parent: HTMLDivElement, popoverID: string, queryState: QueryState, - extensions: Extension + extensions: Extension, + queryExtensions: Extension ): EditorView { return new EditorView({ state: EditorState.create({ @@ -176,6 +188,7 @@ function createEditor( color: 'var(--search-query-text-color)', }, }), + querySettingsCompartment.of(queryExtensions), extensionsCompartment.of(extensions), ], }), @@ -183,12 +196,18 @@ function createEditor( }) } -function updateEditor(editor: EditorView | null, extensions: Extension): void { +function updateExtensions(editor: EditorView | null, extensions: Extension): void { if (editor) { editor.dispatch({ effects: extensionsCompartment.reconfigure(extensions) }) } } +function updateQueryExtensions(editor: EditorView | null, extensions: Extension): void { + if (editor) { + editor.dispatch({ effects: querySettingsCompartment.reconfigure(extensions) }) + } +} + function updateValueIfNecessary(editor: EditorView | null, queryState: QueryState): void { if (editor && queryState.changeSource !== QueryChangeSource.userInput) { editor.dispatch({ @@ -209,7 +228,9 @@ export interface CodeMirrorQueryInputWrapperProps { suggestionSource: Source } -export const CodeMirrorQueryInputWrapper: React.FunctionComponent = ({ +export const CodeMirrorQueryInputWrapper: React.FunctionComponent< + React.PropsWithChildren +> = ({ queryState, onChange, onSubmit, @@ -218,6 +239,7 @@ export const CodeMirrorQueryInputWrapper: React.FunctionComponent { const navigate = useNavigate() const [container, setContainer] = useState(null) @@ -236,8 +258,6 @@ export const CodeMirrorQueryInputWrapper: React.FunctionComponent configureExtensions({ popoverID, - patternType, - interpretComments, isLightTheme, placeholder, onChange, @@ -248,8 +268,6 @@ export const CodeMirrorQueryInputWrapper: React.FunctionComponent configureQueryExtensions({ patternType, interpretComments }), + [patternType, interpretComments] + ) + const editor = useMemo( - () => (container ? createEditor(container, popoverID, queryState, extensions) : null), + () => (container ? createEditor(container, popoverID, queryState, extensions, queryExtensions) : null), // Should only run once when the component is created, not when // extensions for state update (this is handled in separate hooks) // eslint-disable-next-line react-hooks/exhaustive-deps @@ -276,7 +300,8 @@ export const CodeMirrorQueryInputWrapper: React.FunctionComponent updateValueIfNecessary(editorRef.current, queryState), [queryState]) // Update editor configuration whenever extensions change - useEffect(() => updateEditor(editorRef.current, extensions), [extensions]) + useEffect(() => updateExtensions(editorRef.current, extensions), [extensions]) + useEffect(() => updateQueryExtensions(editorRef.current, queryExtensions), [queryExtensions]) const focus = useCallback(() => { editorRef.current?.contentDOM.focus() @@ -301,18 +326,25 @@ export const CodeMirrorQueryInputWrapper: React.FunctionComponent + {children && } + {children}

    diff --git a/client/branded/src/search-ui/input/experimental/LazyCodeMirrorQueryInput.tsx b/client/branded/src/search-ui/input/experimental/LazyCodeMirrorQueryInput.tsx index ec256f5290ff..c374e5ddcb63 100644 --- a/client/branded/src/search-ui/input/experimental/LazyCodeMirrorQueryInput.tsx +++ b/client/branded/src/search-ui/input/experimental/LazyCodeMirrorQueryInput.tsx @@ -6,6 +6,6 @@ import type { CodeMirrorQueryInputWrapperProps } from './CodeMirrorQueryInputWra const CodeMirrorQueryInput = lazyComponent(() => import('./CodeMirrorQueryInputWrapper'), 'CodeMirrorQueryInputWrapper') -export const LazyCodeMirrorQueryInput: React.FunctionComponent = props => ( - -) +export const LazyCodeMirrorQueryInput: React.FunctionComponent< + React.PropsWithChildren +> = props => diff --git a/client/web/src/search/home/SearchPageInput.tsx b/client/web/src/search/home/SearchPageInput.tsx index b696de9dfeca..c2ca00fc5783 100644 --- a/client/web/src/search/home/SearchPageInput.tsx +++ b/client/web/src/search/home/SearchPageInput.tsx @@ -4,7 +4,7 @@ import { useLocation, useNavigate } from 'react-router-dom-v5-compat' import { NavbarQueryState } from 'src/stores/navbarSearchQueryState' import shallow from 'zustand/shallow' -import { SearchBox } from '@sourcegraph/branded' +import { SearchBox, Toggles } from '@sourcegraph/branded' // The experimental search input should be shown on the search home page // eslint-disable-next-line no-restricted-imports import { LazyCodeMirrorQueryInput } from '@sourcegraph/branded/src/search-ui/experimental' @@ -154,7 +154,18 @@ export const SearchPageInput: React.FunctionComponent + > + + ) : ( Date: Fri, 3 Feb 2023 15:20:19 +0400 Subject: [PATCH 393/678] permissions-center: add basic permission sync jobs API. (#47367) Test plan: Tests added. --- .../graphqlbackend/permission_sync_jobs.go | 48 +++++ .../authz/resolvers/permission_sync_jobs.go | 190 ++++++++++++++++++ .../resolvers/permission_sync_jobs_test.go | 50 +++++ internal/database/mocks_temp.go | 121 +++++++++++ internal/database/permission_sync_jobs.go | 15 ++ .../database/permission_sync_jobs_test.go | 30 +++ 6 files changed, 454 insertions(+) create mode 100644 cmd/frontend/graphqlbackend/permission_sync_jobs.go create mode 100644 enterprise/cmd/frontend/internal/authz/resolvers/permission_sync_jobs.go create mode 100644 enterprise/cmd/frontend/internal/authz/resolvers/permission_sync_jobs_test.go diff --git a/cmd/frontend/graphqlbackend/permission_sync_jobs.go b/cmd/frontend/graphqlbackend/permission_sync_jobs.go new file mode 100644 index 000000000000..663b2d474e24 --- /dev/null +++ b/cmd/frontend/graphqlbackend/permission_sync_jobs.go @@ -0,0 +1,48 @@ +package graphqlbackend + +import ( + "context" + "time" + + "github.com/graph-gophers/graphql-go" + "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/executor" +) + +// PermissionSyncJobsResolver is a main interface for all GraphQL operations with +// permission sync jobs. +type PermissionSyncJobsResolver interface { + PermissionSyncJobs(ctx context.Context, args ListPermissionSyncJobsArgs) (*graphqlutil.ConnectionResolver[PermissionSyncJobResolver], error) +} + +type PermissionSyncJobResolver interface { + ID() graphql.ID + State() string + FailureMessage() *string + Reason() database.PermissionSyncJobReason + CancellationReason() string + TriggeredByUserID() int32 + QueuedAt() time.Time + StartedAt() time.Time + FinishedAt() time.Time + ProcessAfter() time.Time + NumResets() int + NumFailures() int + LastHeartbeatAt() time.Time + ExecutionLogs() []executor.ExecutionLogEntry + WorkerHostname() string + Cancel() bool + RepositoryID() graphql.ID + UserID() graphql.ID + Priority() database.PermissionSyncJobPriority + NoPerms() bool + InvalidateCaches() bool + PermissionsAdded() int + PermissionsRemoved() int + PermissionsFound() int +} + +type ListPermissionSyncJobsArgs struct { + graphqlutil.ConnectionResolverArgs +} diff --git a/enterprise/cmd/frontend/internal/authz/resolvers/permission_sync_jobs.go b/enterprise/cmd/frontend/internal/authz/resolvers/permission_sync_jobs.go new file mode 100644 index 000000000000..6305cb63ca3b --- /dev/null +++ b/enterprise/cmd/frontend/internal/authz/resolvers/permission_sync_jobs.go @@ -0,0 +1,190 @@ +package resolvers + +import ( + "context" + "strconv" + "sync" + "time" + + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/relay" + "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" + "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil" + "github.com/sourcegraph/sourcegraph/internal/api" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/executor" +) + +var _ graphqlbackend.PermissionSyncJobsResolver = &permissionSyncJobsResolver{} + +type permissionSyncJobsResolver struct { + db database.DB + once sync.Once + jobs []*database.PermissionSyncJob + err error +} + +func NewPermissionSyncJobsResolver(db database.DB) graphqlbackend.PermissionSyncJobsResolver { + return &permissionSyncJobsResolver{db: db} +} + +func (r *permissionSyncJobsResolver) PermissionSyncJobs(_ context.Context, args graphqlbackend.ListPermissionSyncJobsArgs) (*graphqlutil.ConnectionResolver[graphqlbackend.PermissionSyncJobResolver], error) { + store := &permissionSyncJobConnectionStore{ + db: r.db, + args: args, + } + return graphqlutil.NewConnectionResolver[graphqlbackend.PermissionSyncJobResolver](store, &args.ConnectionResolverArgs, nil) +} + +type permissionSyncJobConnectionStore struct { + db database.DB + args graphqlbackend.ListPermissionSyncJobsArgs +} + +func (s *permissionSyncJobConnectionStore) ComputeTotal(ctx context.Context) (*int32, error) { + count, err := s.db.PermissionSyncJobs().Count(ctx) + if err != nil { + return nil, err + } + total := int32(count) + return &total, nil +} + +func (s *permissionSyncJobConnectionStore) ComputeNodes(ctx context.Context, args *database.PaginationArgs) ([]graphqlbackend.PermissionSyncJobResolver, error) { + jobs, err := s.db.PermissionSyncJobs().List(ctx, database.ListPermissionSyncJobOpts{PaginationArgs: args}) + if err != nil { + return nil, err + } + + resolvers := make([]graphqlbackend.PermissionSyncJobResolver, 0, len(jobs)) + for _, job := range jobs { + resolvers = append(resolvers, &permissionSyncJobResolver{ + db: s.db, + job: job, + }) + } + return resolvers, nil +} + +func (s *permissionSyncJobConnectionStore) MarshalCursor(node graphqlbackend.PermissionSyncJobResolver, _ database.OrderBy) (*string, error) { + id, err := unmarshalPermissionSyncJobID(node.ID()) + if err != nil { + return nil, err + } + cursor := strconv.Itoa(id) + return &cursor, nil +} + +func (s *permissionSyncJobConnectionStore) UnmarshalCursor(cursor string, _ database.OrderBy) (*string, error) { + return &cursor, nil +} + +type permissionSyncJobResolver struct { + db database.DB + job *database.PermissionSyncJob +} + +func (p *permissionSyncJobResolver) ID() graphql.ID { + return marshalPermissionSyncJobID(p.job.ID) +} + +func (p *permissionSyncJobResolver) State() string { + return p.job.State +} + +func (p *permissionSyncJobResolver) FailureMessage() *string { + return p.job.FailureMessage +} + +func (p *permissionSyncJobResolver) Reason() database.PermissionSyncJobReason { + return p.job.Reason +} + +func (p *permissionSyncJobResolver) CancellationReason() string { + return p.job.CancellationReason +} + +func (p *permissionSyncJobResolver) TriggeredByUserID() int32 { + return p.job.TriggeredByUserID +} + +func (p *permissionSyncJobResolver) QueuedAt() time.Time { + return p.job.QueuedAt +} + +func (p *permissionSyncJobResolver) StartedAt() time.Time { + return p.job.StartedAt +} + +func (p *permissionSyncJobResolver) FinishedAt() time.Time { + return p.job.FinishedAt +} + +func (p *permissionSyncJobResolver) ProcessAfter() time.Time { + return p.job.ProcessAfter +} + +func (p *permissionSyncJobResolver) NumResets() int { + return p.job.NumResets +} + +func (p *permissionSyncJobResolver) NumFailures() int { + return p.job.NumFailures +} + +func (p *permissionSyncJobResolver) LastHeartbeatAt() time.Time { + return p.job.LastHeartbeatAt +} + +func (p *permissionSyncJobResolver) ExecutionLogs() []executor.ExecutionLogEntry { + return p.job.ExecutionLogs +} + +func (p *permissionSyncJobResolver) WorkerHostname() string { + return p.job.WorkerHostname +} + +func (p *permissionSyncJobResolver) Cancel() bool { + return p.job.Cancel +} + +func (p *permissionSyncJobResolver) RepositoryID() graphql.ID { + return graphqlbackend.MarshalRepositoryID(api.RepoID(p.job.RepositoryID)) +} + +func (p *permissionSyncJobResolver) UserID() graphql.ID { + return graphqlbackend.MarshalUserID(int32(p.job.UserID)) +} + +func (p *permissionSyncJobResolver) Priority() database.PermissionSyncJobPriority { + return p.job.Priority +} + +func (p *permissionSyncJobResolver) NoPerms() bool { + return p.job.NoPerms +} + +func (p *permissionSyncJobResolver) InvalidateCaches() bool { + return p.job.InvalidateCaches +} + +func (p *permissionSyncJobResolver) PermissionsAdded() int { + return p.job.PermissionsAdded +} + +func (p *permissionSyncJobResolver) PermissionsRemoved() int { + return p.job.PermissionsRemoved +} + +func (p *permissionSyncJobResolver) PermissionsFound() int { + return p.job.PermissionsFound +} + +func marshalPermissionSyncJobID(id int) graphql.ID { + return relay.MarshalID("PermissionSyncJob", id) +} + +func unmarshalPermissionSyncJobID(id graphql.ID) (jobID int, err error) { + err = relay.UnmarshalSpec(id, &jobID) + return +} diff --git a/enterprise/cmd/frontend/internal/authz/resolvers/permission_sync_jobs_test.go b/enterprise/cmd/frontend/internal/authz/resolvers/permission_sync_jobs_test.go new file mode 100644 index 000000000000..0974705cc9d5 --- /dev/null +++ b/enterprise/cmd/frontend/internal/authz/resolvers/permission_sync_jobs_test.go @@ -0,0 +1,50 @@ +package resolvers + +import ( + "context" + "testing" + + "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" + "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/stretchr/testify/require" +) + +func TestPermissionSyncJobsResolver(t *testing.T) { + ctx := context.Background() + first := int32(1337) + args := graphqlutil.ConnectionResolverArgs{First: &first} + + t.Run("No jobs found", func(t *testing.T) { + db := database.NewMockDB() + + jobsStore := database.NewMockPermissionSyncJobStore() + jobsStore.ListFunc.SetDefaultReturn([]*database.PermissionSyncJob{}, nil) + + db.PermissionSyncJobsFunc.SetDefaultReturn(jobsStore) + + resolver := NewPermissionSyncJobsResolver(db) + jobsResolver, err := resolver.PermissionSyncJobs(ctx, graphqlbackend.ListPermissionSyncJobsArgs{ConnectionResolverArgs: args}) + require.NoError(t, err) + jobs, err := jobsResolver.Nodes(ctx) + require.NoError(t, err) + require.Empty(t, jobs) + }) + + t.Run("One job found", func(t *testing.T) { + db := database.NewMockDB() + + jobsStore := database.NewMockPermissionSyncJobStore() + jobsStore.ListFunc.SetDefaultReturn([]*database.PermissionSyncJob{{ID: 1}}, nil) + + db.PermissionSyncJobsFunc.SetDefaultReturn(jobsStore) + + resolver := NewPermissionSyncJobsResolver(db) + jobsResolver, err := resolver.PermissionSyncJobs(ctx, graphqlbackend.ListPermissionSyncJobsArgs{ConnectionResolverArgs: args}) + require.NoError(t, err) + jobs, err := jobsResolver.Nodes(ctx) + require.NoError(t, err) + require.Len(t, jobs, 1) + require.Equal(t, marshalPermissionSyncJobID(1), jobs[0].ID()) + }) +} diff --git a/internal/database/mocks_temp.go b/internal/database/mocks_temp.go index dc552be538db..5ef00143822d 100644 --- a/internal/database/mocks_temp.go +++ b/internal/database/mocks_temp.go @@ -35225,6 +35225,9 @@ type MockPermissionSyncJobStore struct { // CancelQueuedJobFunc is an instance of a mock function object // controlling the behavior of the method CancelQueuedJob. CancelQueuedJobFunc *PermissionSyncJobStoreCancelQueuedJobFunc + // CountFunc is an instance of a mock function object controlling the + // behavior of the method Count. + CountFunc *PermissionSyncJobStoreCountFunc // CreateRepoSyncJobFunc is an instance of a mock function object // controlling the behavior of the method CreateRepoSyncJob. CreateRepoSyncJobFunc *PermissionSyncJobStoreCreateRepoSyncJobFunc @@ -35261,6 +35264,11 @@ func NewMockPermissionSyncJobStore() *MockPermissionSyncJobStore { return }, }, + CountFunc: &PermissionSyncJobStoreCountFunc{ + defaultHook: func(context.Context) (r0 int, r1 error) { + return + }, + }, CreateRepoSyncJobFunc: &PermissionSyncJobStoreCreateRepoSyncJobFunc{ defaultHook: func(context.Context, api.RepoID, PermissionSyncJobOpts) (r0 error) { return @@ -35314,6 +35322,11 @@ func NewStrictMockPermissionSyncJobStore() *MockPermissionSyncJobStore { panic("unexpected invocation of MockPermissionSyncJobStore.CancelQueuedJob") }, }, + CountFunc: &PermissionSyncJobStoreCountFunc{ + defaultHook: func(context.Context) (int, error) { + panic("unexpected invocation of MockPermissionSyncJobStore.Count") + }, + }, CreateRepoSyncJobFunc: &PermissionSyncJobStoreCreateRepoSyncJobFunc{ defaultHook: func(context.Context, api.RepoID, PermissionSyncJobOpts) error { panic("unexpected invocation of MockPermissionSyncJobStore.CreateRepoSyncJob") @@ -35365,6 +35378,9 @@ func NewMockPermissionSyncJobStoreFrom(i PermissionSyncJobStore) *MockPermission CancelQueuedJobFunc: &PermissionSyncJobStoreCancelQueuedJobFunc{ defaultHook: i.CancelQueuedJob, }, + CountFunc: &PermissionSyncJobStoreCountFunc{ + defaultHook: i.Count, + }, CreateRepoSyncJobFunc: &PermissionSyncJobStoreCreateRepoSyncJobFunc{ defaultHook: i.CreateRepoSyncJob, }, @@ -35504,6 +35520,111 @@ func (c PermissionSyncJobStoreCancelQueuedJobFuncCall) Results() []interface{} { return []interface{}{c.Result0} } +// PermissionSyncJobStoreCountFunc describes the behavior when the Count +// method of the parent MockPermissionSyncJobStore instance is invoked. +type PermissionSyncJobStoreCountFunc struct { + defaultHook func(context.Context) (int, error) + hooks []func(context.Context) (int, error) + history []PermissionSyncJobStoreCountFuncCall + mutex sync.Mutex +} + +// Count delegates to the next hook function in the queue and stores the +// parameter and result values of this invocation. +func (m *MockPermissionSyncJobStore) Count(v0 context.Context) (int, error) { + r0, r1 := m.CountFunc.nextHook()(v0) + m.CountFunc.appendCall(PermissionSyncJobStoreCountFuncCall{v0, r0, r1}) + return r0, r1 +} + +// SetDefaultHook sets function that is called when the Count method of the +// parent MockPermissionSyncJobStore instance is invoked and the hook queue +// is empty. +func (f *PermissionSyncJobStoreCountFunc) SetDefaultHook(hook func(context.Context) (int, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// Count method of the parent MockPermissionSyncJobStore instance invokes +// the hook at the front of the queue and discards it. After the queue is +// empty, the default hook function is invoked for any future action. +func (f *PermissionSyncJobStoreCountFunc) PushHook(hook func(context.Context) (int, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *PermissionSyncJobStoreCountFunc) SetDefaultReturn(r0 int, r1 error) { + f.SetDefaultHook(func(context.Context) (int, error) { + return r0, r1 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *PermissionSyncJobStoreCountFunc) PushReturn(r0 int, r1 error) { + f.PushHook(func(context.Context) (int, error) { + return r0, r1 + }) +} + +func (f *PermissionSyncJobStoreCountFunc) nextHook() func(context.Context) (int, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *PermissionSyncJobStoreCountFunc) appendCall(r0 PermissionSyncJobStoreCountFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of PermissionSyncJobStoreCountFuncCall objects +// describing the invocations of this function. +func (f *PermissionSyncJobStoreCountFunc) History() []PermissionSyncJobStoreCountFuncCall { + f.mutex.Lock() + history := make([]PermissionSyncJobStoreCountFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// PermissionSyncJobStoreCountFuncCall is an object that describes an +// invocation of method Count on an instance of MockPermissionSyncJobStore. +type PermissionSyncJobStoreCountFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 int + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c PermissionSyncJobStoreCountFuncCall) Args() []interface{} { + return []interface{}{c.Arg0} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c PermissionSyncJobStoreCountFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1} +} + // PermissionSyncJobStoreCreateRepoSyncJobFunc describes the behavior when // the CreateRepoSyncJob method of the parent MockPermissionSyncJobStore // instance is invoked. diff --git a/internal/database/permission_sync_jobs.go b/internal/database/permission_sync_jobs.go index 746d9dbef0f8..50f006a8703a 100644 --- a/internal/database/permission_sync_jobs.go +++ b/internal/database/permission_sync_jobs.go @@ -86,6 +86,7 @@ type PermissionSyncJobStore interface { CreateRepoSyncJob(ctx context.Context, repo api.RepoID, opts PermissionSyncJobOpts) error List(ctx context.Context, opts ListPermissionSyncJobOpts) ([]*PermissionSyncJob, error) + Count(ctx context.Context) (int, error) CancelQueuedJob(ctx context.Context, reason string, id int) error SaveSyncResult(ctx context.Context, id int, result *SetPermissionsResult) error } @@ -398,6 +399,20 @@ func (s *permissionSyncJobStore) List(ctx context.Context, opts ListPermissionSy return syncJobs, nil } +const countPermissionSyncJobsQuery = ` +SELECT COUNT(*) +FROM permission_sync_jobs +` + +func (s *permissionSyncJobStore) Count(ctx context.Context) (int, error) { + q := sqlf.Sprintf(countPermissionSyncJobsQuery) + var count int + if err := s.QueryRow(ctx, q).Scan(&count); err != nil { + return 0, err + } + return count, nil +} + type PermissionSyncJob struct { ID int State string diff --git a/internal/database/permission_sync_jobs_test.go b/internal/database/permission_sync_jobs_test.go index a64114c38f3a..38223d3cbaa4 100644 --- a/internal/database/permission_sync_jobs_test.go +++ b/internal/database/permission_sync_jobs_test.go @@ -527,6 +527,36 @@ func TestPermissionSyncJobs_Pagination(t *testing.T) { } } +func TestPermissionSyncJobs_Count(t *testing.T) { + if testing.Short() { + t.Skip() + } + + ctx := context.Background() + logger := logtest.Scoped(t) + db := NewDB(logger, dbtest.NewDB(logger, t)) + user, err := db.Users().Create(ctx, NewUser{Username: "horse"}) + require.NoError(t, err) + + store := PermissionSyncJobsWith(logger, db) + + // Create 10 sync jobs. + createSyncJobs(t, ctx, user.ID, store) + + _, err = store.List(ctx, ListPermissionSyncJobOpts{}) + require.NoError(t, err) + + count, err := store.Count(ctx) + require.NoError(t, err) + require.Equal(t, 10, count) + + // Create 10 more sync jobs. + createSyncJobs(t, ctx, user.ID, store) + count, err = store.Count(ctx) + require.NoError(t, err) + require.Equal(t, 20, count) +} + func createSyncJobs(t *testing.T, ctx context.Context, userID int32, store PermissionSyncJobStore) { t.Helper() clock := timeutil.NewFakeClock(time.Now(), 0) From 18fddaee6a6727be78072c0cf76e44e357a77ec3 Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Fri, 3 Feb 2023 14:32:08 +0200 Subject: [PATCH 394/678] singleprogram: use localhost for gitserver (#47366) * singleprogram: use localhost for gitserver Using the hostname isn't gaurenteed to work, it depends on how the machine is configured. For example on my linux desktop this fails. We only start the services on localhost and we use ipv4 localhost address for all other services. Additionally introduce GITSERVER_EXTERNAL_ADDR. Treating the hostname as an address is incorrect. We instead use a more common pattern of having an environment variable which specifies the reachable address for the gitserver service. Test Plan: sg start app and sg start work --- cmd/gitserver/shared/shared.go | 18 +++++++++++++++++- internal/singleprogram/singleprogram.go | 13 +++++-------- sg.config.yaml | 8 ++++---- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/cmd/gitserver/shared/shared.go b/cmd/gitserver/shared/shared.go index a705556ba7be..515e46699018 100644 --- a/cmd/gitserver/shared/shared.go +++ b/cmd/gitserver/shared/shared.go @@ -136,7 +136,7 @@ func Main(ctx context.Context, observationCtx *observation.Context, ready servic GetVCSSyncer: func(ctx context.Context, repo api.RepoName) (server.VCSSyncer, error) { return getVCSSyncer(ctx, externalServiceStore, repoStore, dependenciesSvc, repo, config.ReposDir) }, - Hostname: hostname.Get(), + Hostname: externalAddress(), DB: db, CloneQueue: server.NewCloneQueue(list.New()), GlobalBatchLogSemaphore: semaphore.NewWeighted(int64(batchLogGlobalConcurrencyLimit)), @@ -571,3 +571,19 @@ func syncRateLimiters(ctx context.Context, logger log.Logger, store database.Ext } } } + +// externalAddress calculates the name of this gitserver as it would appear in +// SRC_GIT_SERVERS. +// +// Note: we can't just rely on the listen address since more than likely +// gitserver is behind a k8s service. +func externalAddress() string { + // First we check for it being explicitly set. This should only be + // happening in environments were we run gitserver on localhost. + if addr := os.Getenv("GITSERVER_EXTERNAL_ADDR"); addr != "" { + return addr + } + // Otherwise we assume we can reach gitserver via its hostname / its + // hostname is a prefix of the reachable address (see hostnameMatch). + return hostname.Get() +} diff --git a/internal/singleprogram/singleprogram.go b/internal/singleprogram/singleprogram.go index 9d023189dfa6..a16342f24daa 100644 --- a/internal/singleprogram/singleprogram.go +++ b/internal/singleprogram/singleprogram.go @@ -22,14 +22,11 @@ func Init(logger log.Logger) { // INDEXED_SEARCH_SERVERS is empty (but defined) so that indexed search is disabled. setDefaultEnv(logger, "INDEXED_SEARCH_SERVERS", "") - // Need to set this to avoid trying to look up gitservers via k8s service discovery. - // TODO(sqs) TODO(single-binary): Make this not require the hostname. - hostname, err := os.Hostname() - if err != nil { - fmt.Fprintln(os.Stderr, "unable to determine hostname:", err) - os.Exit(1) - } - setDefaultEnv(logger, "SRC_GIT_SERVERS", hostname+":3178") + // GITSERVER_EXTERNAL_ADDR is used by gitserver to identify itself in the + // list in SRC_GIT_SERVERS. + setDefaultEnv(logger, "GITSERVER_ADDR", "127.0.0.1:3178") + setDefaultEnv(logger, "GITSERVER_EXTERNAL_ADDR", "127.0.0.1:3178") + setDefaultEnv(logger, "SRC_GIT_SERVERS", "127.0.0.1:3178") setDefaultEnv(logger, "SYMBOLS_URL", "http://127.0.0.1:3184") setDefaultEnv(logger, "SEARCHER_URL", "http://127.0.0.1:3181") diff --git a/sg.config.yaml b/sg.config.yaml index ab5658d15aa8..3e1d9be04810 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -221,7 +221,7 @@ commands: <<: *oss_gitserver_template env: <<: *gitserverenv - HOSTNAME: 127.0.0.1:3501 + GITSERVER_EXTERNAL_ADDR: 127.0.0.1:3501 GITSERVER_ADDR: 127.0.0.1:3501 SRC_REPOS_DIR: $HOME/.sourcegraph/repos_1 SRC_PROF_HTTP: 127.0.0.1:3551 @@ -230,7 +230,7 @@ commands: <<: *oss_gitserver_template env: <<: *oss_gitserverenv - HOSTNAME: 127.0.0.1:3502 + GITSERVER_EXTERNAL_ADDR: 127.0.0.1:3502 GITSERVER_ADDR: 127.0.0.1:3502 SRC_REPOS_DIR: $HOME/.sourcegraph/repos_2 SRC_PROF_HTTP: 127.0.0.1:3552 @@ -244,7 +244,7 @@ commands: <<: *gitserver_template env: <<: *gitserverenv - HOSTNAME: 127.0.0.1:3501 + GITSERVER_EXTERNAL_ADDR: 127.0.0.1:3501 GITSERVER_ADDR: 127.0.0.1:3501 SRC_REPOS_DIR: $HOME/.sourcegraph/repos_1 SRC_PROF_HTTP: 127.0.0.1:3551 @@ -253,7 +253,7 @@ commands: <<: *gitserver_template env: <<: *gitserverenv - HOSTNAME: 127.0.0.1:3502 + GITSERVER_EXTERNAL_ADDR: 127.0.0.1:3502 GITSERVER_ADDR: 127.0.0.1:3502 SRC_REPOS_DIR: $HOME/.sourcegraph/repos_2 SRC_PROF_HTTP: 127.0.0.1:3552 From a879b16d03eb77558e59a94f1ea9dc88409ec86f Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Fri, 3 Feb 2023 14:50:07 +0200 Subject: [PATCH 395/678] sg: do not set repo-updater to gitserver's hostname (#47371) Looking at git blame I think this was just some copy-pasta mistakes. Test Plan: sg start and the logs don't scream at me. --- sg.config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/sg.config.yaml b/sg.config.yaml index 3e1d9be04810..d2ceff77a9c3 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -323,7 +323,6 @@ commands: go build -gcflags="$GCFLAGS" -o .bin/repo-updater github.com/sourcegraph/sourcegraph/enterprise/cmd/repo-updater checkBinary: .bin/repo-updater env: - HOSTNAME: $SRC_GIT_SERVER_1 ENTERPRISE: 1 watch: - lib From 35497d5527a3eeef9fcd2658dffcd8b17b99a493 Mon Sep 17 00:00:00 2001 From: Keegan Carruthers-Smith Date: Fri, 3 Feb 2023 14:59:05 +0200 Subject: [PATCH 396/678] redispool: use postgres for redispool.Store in App (#47188) We introduce a new table "redis_key_value" which is written to by KeyValue. The DB backed KeyValue store needs to wait for the database to be ready. However, Store is a package global. As such we use a pattern were we register the DB into redispool once the DB is ready. Before that all the KeyValue operations will return an error. Test Plan: go test and manual testing of Sourcegraph App. --- cmd/frontend/internal/cli/serve_cmd.go | 17 +++ enterprise/internal/database/mocks_temp.go | 115 ++++++++++++++++++ internal/database/database.go | 5 + internal/database/mocks_temp.go | 114 +++++++++++++++++ internal/database/redis_key_value.go | 81 ++++++++++++ internal/database/redis_key_value_test.go | 64 ++++++++++ internal/database/schema.json | 59 +++++++++ internal/database/schema.md | 12 ++ internal/redispool/db.go | 105 ++++++++++++++++ internal/redispool/keyvalue.go | 22 +++- internal/redispool/mem.go | 4 - internal/redispool/naive_test.go | 57 +++++++++ internal/redispool/redispool.go | 2 +- internal/redispool/redispool_test.go | 14 ++- .../1675257827_redis_key_value/down.sql | 3 + .../1675257827_redis_key_value/metadata.yaml | 2 + .../1675257827_redis_key_value/up.sql | 8 ++ migrations/frontend/squashed.sql | 9 ++ sg.config.yaml | 4 +- 19 files changed, 687 insertions(+), 10 deletions(-) create mode 100644 internal/database/redis_key_value.go create mode 100644 internal/database/redis_key_value_test.go create mode 100644 internal/redispool/db.go create mode 100644 migrations/frontend/1675257827_redis_key_value/down.sql create mode 100644 migrations/frontend/1675257827_redis_key_value/metadata.yaml create mode 100644 migrations/frontend/1675257827_redis_key_value/up.sql diff --git a/cmd/frontend/internal/cli/serve_cmd.go b/cmd/frontend/internal/cli/serve_cmd.go index 9ffdacc4c8a2..fd216c1c5210 100644 --- a/cmd/frontend/internal/cli/serve_cmd.go +++ b/cmd/frontend/internal/cli/serve_cmd.go @@ -115,6 +115,11 @@ func Main(ctx context.Context, observationCtx *observation.Context, ready servic } } + // After our DB, redis is our next most important datastore + if err := redispoolRegisterDB(db); err != nil { + return errors.Wrap(err, "failed to register postgres backed redis") + } + // override site config first if err := overrideSiteConfig(ctx, logger, db); err != nil { return errors.Wrap(err, "failed to apply site config overrides") @@ -380,3 +385,15 @@ func makeRateLimitWatcher() (*graphqlbackend.BasicLimitWatcher, error) { return graphqlbackend.NewBasicLimitWatcher(sglog.Scoped("BasicLimitWatcher", "basic rate-limiter"), store), nil } + +// redispoolRegisterDB registers our postgres backed redis. These package +// avoid depending on each other, hence the wrapping to get Go to play nice +// with the interface definitions. +func redispoolRegisterDB(db database.DB) error { + kvNoTX := db.RedisKeyValue() + return redispool.DBRegisterStore(func(ctx context.Context, f func(redispool.DBStore) error) error { + return kvNoTX.WithTransact(ctx, func(tx database.RedisKeyValueStore) error { + return f(tx) + }) + }) +} diff --git a/enterprise/internal/database/mocks_temp.go b/enterprise/internal/database/mocks_temp.go index 025acc7d34e5..ce2aa253507a 100644 --- a/enterprise/internal/database/mocks_temp.go +++ b/enterprise/internal/database/mocks_temp.go @@ -6901,6 +6901,9 @@ type MockEnterpriseDB struct { // QueryRowContextFunc is an instance of a mock function object // controlling the behavior of the method QueryRowContext. QueryRowContextFunc *EnterpriseDBQueryRowContextFunc + // RedisKeyValueFunc is an instance of a mock function object + // controlling the behavior of the method RedisKeyValue. + RedisKeyValueFunc *EnterpriseDBRedisKeyValueFunc // RepoKVPsFunc is an instance of a mock function object controlling the // behavior of the method RepoKVPs. RepoKVPsFunc *EnterpriseDBRepoKVPsFunc @@ -7115,6 +7118,11 @@ func NewMockEnterpriseDB() *MockEnterpriseDB { return }, }, + RedisKeyValueFunc: &EnterpriseDBRedisKeyValueFunc{ + defaultHook: func() (r0 database.RedisKeyValueStore) { + return + }, + }, RepoKVPsFunc: &EnterpriseDBRepoKVPsFunc{ defaultHook: func() (r0 database.RepoKVPStore) { return @@ -7372,6 +7380,11 @@ func NewStrictMockEnterpriseDB() *MockEnterpriseDB { panic("unexpected invocation of MockEnterpriseDB.QueryRowContext") }, }, + RedisKeyValueFunc: &EnterpriseDBRedisKeyValueFunc{ + defaultHook: func() database.RedisKeyValueStore { + panic("unexpected invocation of MockEnterpriseDB.RedisKeyValue") + }, + }, RepoKVPsFunc: &EnterpriseDBRepoKVPsFunc{ defaultHook: func() database.RepoKVPStore { panic("unexpected invocation of MockEnterpriseDB.RepoKVPs") @@ -7572,6 +7585,9 @@ func NewMockEnterpriseDBFrom(i EnterpriseDB) *MockEnterpriseDB { QueryRowContextFunc: &EnterpriseDBQueryRowContextFunc{ defaultHook: i.QueryRowContext, }, + RedisKeyValueFunc: &EnterpriseDBRedisKeyValueFunc{ + defaultHook: i.RedisKeyValue, + }, RepoKVPsFunc: &EnterpriseDBRepoKVPsFunc{ defaultHook: i.RepoKVPs, }, @@ -10593,6 +10609,105 @@ func (c EnterpriseDBQueryRowContextFuncCall) Results() []interface{} { return []interface{}{c.Result0} } +// EnterpriseDBRedisKeyValueFunc describes the behavior when the +// RedisKeyValue method of the parent MockEnterpriseDB instance is invoked. +type EnterpriseDBRedisKeyValueFunc struct { + defaultHook func() database.RedisKeyValueStore + hooks []func() database.RedisKeyValueStore + history []EnterpriseDBRedisKeyValueFuncCall + mutex sync.Mutex +} + +// RedisKeyValue delegates to the next hook function in the queue and stores +// the parameter and result values of this invocation. +func (m *MockEnterpriseDB) RedisKeyValue() database.RedisKeyValueStore { + r0 := m.RedisKeyValueFunc.nextHook()() + m.RedisKeyValueFunc.appendCall(EnterpriseDBRedisKeyValueFuncCall{r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the RedisKeyValue method +// of the parent MockEnterpriseDB instance is invoked and the hook queue is +// empty. +func (f *EnterpriseDBRedisKeyValueFunc) SetDefaultHook(hook func() database.RedisKeyValueStore) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// RedisKeyValue method of the parent MockEnterpriseDB instance invokes the +// hook at the front of the queue and discards it. After the queue is empty, +// the default hook function is invoked for any future action. +func (f *EnterpriseDBRedisKeyValueFunc) PushHook(hook func() database.RedisKeyValueStore) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *EnterpriseDBRedisKeyValueFunc) SetDefaultReturn(r0 database.RedisKeyValueStore) { + f.SetDefaultHook(func() database.RedisKeyValueStore { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *EnterpriseDBRedisKeyValueFunc) PushReturn(r0 database.RedisKeyValueStore) { + f.PushHook(func() database.RedisKeyValueStore { + return r0 + }) +} + +func (f *EnterpriseDBRedisKeyValueFunc) nextHook() func() database.RedisKeyValueStore { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *EnterpriseDBRedisKeyValueFunc) appendCall(r0 EnterpriseDBRedisKeyValueFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of EnterpriseDBRedisKeyValueFuncCall objects +// describing the invocations of this function. +func (f *EnterpriseDBRedisKeyValueFunc) History() []EnterpriseDBRedisKeyValueFuncCall { + f.mutex.Lock() + history := make([]EnterpriseDBRedisKeyValueFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// EnterpriseDBRedisKeyValueFuncCall is an object that describes an +// invocation of method RedisKeyValue on an instance of MockEnterpriseDB. +type EnterpriseDBRedisKeyValueFuncCall struct { + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 database.RedisKeyValueStore +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c EnterpriseDBRedisKeyValueFuncCall) Args() []interface{} { + return []interface{}{} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c EnterpriseDBRedisKeyValueFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + // EnterpriseDBRepoKVPsFunc describes the behavior when the RepoKVPs method // of the parent MockEnterpriseDB instance is invoked. type EnterpriseDBRepoKVPsFunc struct { diff --git a/internal/database/database.go b/internal/database/database.go index d20b1e5228cf..1e5a72b74439 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -41,6 +41,7 @@ type DB interface { Permissions() PermissionStore PermissionSyncJobs() PermissionSyncJobStore Phabricator() PhabricatorStore + RedisKeyValue() RedisKeyValueStore Repos() RepoStore RepoKVPs() RepoKVPStore RolePermissions() RolePermissionStore @@ -200,6 +201,10 @@ func (d *db) Phabricator() PhabricatorStore { return PhabricatorWith(d.Store) } +func (d *db) RedisKeyValue() RedisKeyValueStore { + return &redisKeyValueStore{d.Store} +} + func (d *db) Repos() RepoStore { return ReposWith(d.logger, d.Store) } diff --git a/internal/database/mocks_temp.go b/internal/database/mocks_temp.go index 5ef00143822d..5b46c4e955d6 100644 --- a/internal/database/mocks_temp.go +++ b/internal/database/mocks_temp.go @@ -3828,6 +3828,9 @@ type MockDB struct { // QueryRowContextFunc is an instance of a mock function object // controlling the behavior of the method QueryRowContext. QueryRowContextFunc *DBQueryRowContextFunc + // RedisKeyValueFunc is an instance of a mock function object + // controlling the behavior of the method RedisKeyValue. + RedisKeyValueFunc *DBRedisKeyValueFunc // RepoKVPsFunc is an instance of a mock function object controlling the // behavior of the method RepoKVPs. RepoKVPsFunc *DBRepoKVPsFunc @@ -4029,6 +4032,11 @@ func NewMockDB() *MockDB { return }, }, + RedisKeyValueFunc: &DBRedisKeyValueFunc{ + defaultHook: func() (r0 RedisKeyValueStore) { + return + }, + }, RepoKVPsFunc: &DBRepoKVPsFunc{ defaultHook: func() (r0 RepoKVPStore) { return @@ -4271,6 +4279,11 @@ func NewStrictMockDB() *MockDB { panic("unexpected invocation of MockDB.QueryRowContext") }, }, + RedisKeyValueFunc: &DBRedisKeyValueFunc{ + defaultHook: func() RedisKeyValueStore { + panic("unexpected invocation of MockDB.RedisKeyValue") + }, + }, RepoKVPsFunc: &DBRepoKVPsFunc{ defaultHook: func() RepoKVPStore { panic("unexpected invocation of MockDB.RepoKVPs") @@ -4459,6 +4472,9 @@ func NewMockDBFrom(i DB) *MockDB { QueryRowContextFunc: &DBQueryRowContextFunc{ defaultHook: i.QueryRowContext, }, + RedisKeyValueFunc: &DBRedisKeyValueFunc{ + defaultHook: i.RedisKeyValue, + }, RepoKVPsFunc: &DBRepoKVPsFunc{ defaultHook: i.RepoKVPs, }, @@ -7248,6 +7264,104 @@ func (c DBQueryRowContextFuncCall) Results() []interface{} { return []interface{}{c.Result0} } +// DBRedisKeyValueFunc describes the behavior when the RedisKeyValue method +// of the parent MockDB instance is invoked. +type DBRedisKeyValueFunc struct { + defaultHook func() RedisKeyValueStore + hooks []func() RedisKeyValueStore + history []DBRedisKeyValueFuncCall + mutex sync.Mutex +} + +// RedisKeyValue delegates to the next hook function in the queue and stores +// the parameter and result values of this invocation. +func (m *MockDB) RedisKeyValue() RedisKeyValueStore { + r0 := m.RedisKeyValueFunc.nextHook()() + m.RedisKeyValueFunc.appendCall(DBRedisKeyValueFuncCall{r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the RedisKeyValue method +// of the parent MockDB instance is invoked and the hook queue is empty. +func (f *DBRedisKeyValueFunc) SetDefaultHook(hook func() RedisKeyValueStore) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// RedisKeyValue method of the parent MockDB instance invokes the hook at +// the front of the queue and discards it. After the queue is empty, the +// default hook function is invoked for any future action. +func (f *DBRedisKeyValueFunc) PushHook(hook func() RedisKeyValueStore) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *DBRedisKeyValueFunc) SetDefaultReturn(r0 RedisKeyValueStore) { + f.SetDefaultHook(func() RedisKeyValueStore { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *DBRedisKeyValueFunc) PushReturn(r0 RedisKeyValueStore) { + f.PushHook(func() RedisKeyValueStore { + return r0 + }) +} + +func (f *DBRedisKeyValueFunc) nextHook() func() RedisKeyValueStore { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *DBRedisKeyValueFunc) appendCall(r0 DBRedisKeyValueFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of DBRedisKeyValueFuncCall objects describing +// the invocations of this function. +func (f *DBRedisKeyValueFunc) History() []DBRedisKeyValueFuncCall { + f.mutex.Lock() + history := make([]DBRedisKeyValueFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// DBRedisKeyValueFuncCall is an object that describes an invocation of +// method RedisKeyValue on an instance of MockDB. +type DBRedisKeyValueFuncCall struct { + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 RedisKeyValueStore +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c DBRedisKeyValueFuncCall) Args() []interface{} { + return []interface{}{} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c DBRedisKeyValueFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + // DBRepoKVPsFunc describes the behavior when the RepoKVPs method of the // parent MockDB instance is invoked. type DBRepoKVPsFunc struct { diff --git a/internal/database/redis_key_value.go b/internal/database/redis_key_value.go new file mode 100644 index 000000000000..29ffb442637f --- /dev/null +++ b/internal/database/redis_key_value.go @@ -0,0 +1,81 @@ +package database + +import ( + "context" + "database/sql" + + "github.com/keegancsmith/sqlf" + "github.com/sourcegraph/sourcegraph/internal/database/basestore" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// RedisKeyValueStore is a store that exists to satisfy the interface +// redispool.DBStore. This is the interface that is needed to replace redis +// with postgres. +// +// We do not directly implement the interface since that introduces +// complications around dependency graphs. +type RedisKeyValueStore interface { + basestore.ShareableStore + WithTransact(context.Context, func(RedisKeyValueStore) error) error + Get(ctx context.Context, namespace, key string) (value []byte, ok bool, err error) + Set(ctx context.Context, namespace, key string, value []byte) (err error) + Delete(ctx context.Context, namespace, key string) (err error) +} + +type redisKeyValueStore struct { + *basestore.Store +} + +var _ RedisKeyValueStore = (*redisKeyValueStore)(nil) + +func (f *redisKeyValueStore) WithTransact(ctx context.Context, fn func(RedisKeyValueStore) error) error { + return f.Store.WithTransact(ctx, func(tx *basestore.Store) error { + return fn(&redisKeyValueStore{Store: tx}) + }) +} + +func (s *redisKeyValueStore) Get(ctx context.Context, namespace, key string) ([]byte, bool, error) { + // redispool will often follow up a Get with a Set (eg for implementing + // redis INCR). As such we need to lock the row with FOR UPDATE. + q := sqlf.Sprintf(` + SELECT value FROM redis_key_value + WHERE namespace = %s AND key = %s + FOR UPDATE + `, namespace, key) + row := s.QueryRow(ctx, q) + + var value []byte + err := row.Scan(&value) + if errors.Is(err, sql.ErrNoRows) { + return nil, false, nil + } else if err != nil { + return nil, false, err + } else { + return value, true, nil + } +} + +func (s *redisKeyValueStore) Set(ctx context.Context, namespace, key string, value []byte) error { + // value schema does not allow null, nor do we need to preserve nil. So + // convert to empty string for robustness. This invariant is documented in + // redispool.DBStore and enforced by tests. + if value == nil { + value = []byte{} + } + + q := sqlf.Sprintf(` + INSERT INTO redis_key_value (namespace, key, value) + VALUES (%s, %s, %s) + ON CONFLICT (namespace, key) DO UPDATE SET value = EXCLUDED.value + `, namespace, key, value) + return s.Exec(ctx, q) +} + +func (s *redisKeyValueStore) Delete(ctx context.Context, namespace, key string) error { + q := sqlf.Sprintf(` + DELETE FROM redis_key_value + WHERE namespace = %s AND key = %s + `, namespace, key) + return s.Exec(ctx, q) +} diff --git a/internal/database/redis_key_value_test.go b/internal/database/redis_key_value_test.go new file mode 100644 index 000000000000..dcbcd1cb4435 --- /dev/null +++ b/internal/database/redis_key_value_test.go @@ -0,0 +1,64 @@ +package database + +import ( + "context" + "testing" + + "github.com/sourcegraph/log/logtest" + "github.com/sourcegraph/sourcegraph/internal/database/dbtest" + "github.com/stretchr/testify/require" +) + +func TestRedisKeyValue(t *testing.T) { + require := require.New(t) + logger := logtest.Scoped(t) + db := NewDB(logger, dbtest.NewDB(logger, t)) + ctx := context.Background() + kv := db.RedisKeyValue() + + // Two basic helpers to reduce the noise of get testing + requireMissing := func(namespace, key string) { + t.Helper() + _, ok, err := kv.Get(ctx, namespace, key) + require.NoError(err) + require.False(ok) + } + requireValue := func(namespace, key, value string) { + want := []byte(value) + t.Helper() + v, ok, err := kv.Get(ctx, namespace, key) + require.NoError(err) + require.True(ok) + require.Equal(want, v) + } + + // Basic testing. We heavily rely on the integration test in redispool to + // properly exercise the store. + + // get on missing, set, then get works + requireMissing("namespace", "key") + require.NoError(kv.Set(ctx, "namespace", "key", []byte("value"))) + requireValue("namespace", "key", "value") + + // set on existing key updates it + require.NoError(kv.Set(ctx, "namespace", "key", []byte("horsegraph"))) + requireValue("namespace", "key", "horsegraph") + + // delete makes the following get missing + require.NoError(kv.Delete(ctx, "namespace", "key")) + requireMissing("namespace", "key") + + // deleting a key that doesn't exist doesn't fail + require.NoError(kv.Delete(ctx, "namespace", "missing")) + + // test binary data + binary := string([]byte{0, 1, 0}) // use string to ensure we don't mutate in Set. + require.NoError(kv.Set(ctx, "namespace", "binary", []byte(binary))) + requireValue("namespace", "binary", binary) + + // nil should be treated like an empty slice + require.NoError(kv.Set(ctx, "namespace", "nil", nil)) + require.NoError(kv.Set(ctx, "namespace", "empty", []byte{})) + requireValue("namespace", "nil", "") + requireValue("namespace", "empty", "") +} diff --git a/internal/database/schema.json b/internal/database/schema.json index a4834e702a56..fdb6fa596b1f 100755 --- a/internal/database/schema.json +++ b/internal/database/schema.json @@ -18118,6 +18118,65 @@ "Constraints": null, "Triggers": [] }, + { + "Name": "redis_key_value", + "Comment": "", + "Columns": [ + { + "Name": "key", + "Index": 2, + "TypeName": "text", + "IsNullable": false, + "Default": "", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, + { + "Name": "namespace", + "Index": 1, + "TypeName": "text", + "IsNullable": false, + "Default": "", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, + { + "Name": "value", + "Index": 3, + "TypeName": "bytea", + "IsNullable": false, + "Default": "", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + } + ], + "Indexes": [ + { + "Name": "redis_key_value_pkey", + "IsPrimaryKey": true, + "IsUnique": true, + "IsExclusion": false, + "IsDeferrable": false, + "IndexDefinition": "CREATE UNIQUE INDEX redis_key_value_pkey ON redis_key_value USING btree (namespace, key) INCLUDE (value)", + "ConstraintType": "p", + "ConstraintDefinition": "PRIMARY KEY (namespace, key) INCLUDE (value)" + } + ], + "Constraints": null, + "Triggers": [] + }, { "Name": "registry_extension_releases", "Comment": "", diff --git a/internal/database/schema.md b/internal/database/schema.md index 99f4365ae036..7eab5161e090 100755 --- a/internal/database/schema.md +++ b/internal/database/schema.md @@ -2793,6 +2793,18 @@ Referenced by: ``` +# Table "public.redis_key_value" +``` + Column | Type | Collation | Nullable | Default +-----------+-------+-----------+----------+--------- + namespace | text | | not null | + key | text | | not null | + value | bytea | | not null | +Indexes: + "redis_key_value_pkey" PRIMARY KEY, btree (namespace, key) INCLUDE (value) + +``` + # Table "public.registry_extension_releases" ``` Column | Type | Collation | Nullable | Default diff --git a/internal/redispool/db.go b/internal/redispool/db.go new file mode 100644 index 000000000000..90acfaabddc1 --- /dev/null +++ b/internal/redispool/db.go @@ -0,0 +1,105 @@ +package redispool + +import ( + "context" + "sync" + "sync/atomic" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// DBStore is the methods needed by DBKeyValue to implement the core of +// KeyValue. See database.RedisKeyValueStore for the implementation of this +// interface. +// +// We do not directly import that interface since that introduces +// complications around dependency graphs. +// +// Note: DBKeyValue uses a coarse global mutex for all transactions on-top of +// whatever transaction DBStoreTransact provides. The intention of these +// interfaces is to be used in a single process application (like Sourcegraph +// App). We would need to change the design of NaiveKeyValueStore to allow for +// retries to smoothly avoid global mutexes. +type DBStore interface { + // Get returns the value for (namespace, key). ok is false if the + // (namespace, key) has not been set. + // + // Note: We recommend using "SELECT ... FOR UPDATE" since this call is + // often followed by Set in the same transaction. + Get(ctx context.Context, namespace, key string) (value []byte, ok bool, err error) + // Set will upsert value for (namespace, key). If value is nil it should + // be persisted as an empty byte slice. + Set(ctx context.Context, namespace, key string, value []byte) (err error) + // Delete will remove (namespace, key). If (namespace, key) is not in the + // store, the delete is a noop. + Delete(ctx context.Context, namespace, key string) (err error) +} + +// DBStoreTransact is a function which is like the WithTransact which will run +// f inside of a transaction. f is a function which will read/update a +// DBStore. +type DBStoreTransact func(ctx context.Context, f func(DBStore) error) error + +var dbStoreTransact atomic.Value + +// DBRegisterStore registers our database with the redispool package. Until +// this is called all KeyValue operations against a DB backed KeyValue will +// fail with an error. As such this function should be called early on (as +// soon as we have a useable DB connection). +// +// An error will be returned if this function is called more than once. +func DBRegisterStore(transact DBStoreTransact) error { + ok := dbStoreTransact.CompareAndSwap(nil, transact) + if !ok { + return errors.New("redispool.DBRegisterStore has already been called") + } + return nil +} + +// dbMu protects _all_ possible interactions with the database in DBKeyValue. +// This is to avoid concurrent get/sets on the same key resulting in one of +// the sets failing due to serializability. +var dbMu sync.Mutex + +// DBKeyValue returns a KeyValue with namespace. Namespaces allow us to have +// distinct KeyValue stores, but still use the same underlying DBStore +// storage. +// +// Note: This is designed for use in a single process application like +// Sourcegraph App. All transactions are additionally protected by a global +// mutex to avoid the need to handle database serializability errors. +func DBKeyValue(namespace string) KeyValue { + store := func(ctx context.Context, key string, f NaiveUpdater) error { + dbMu.Lock() + defer dbMu.Unlock() + + transact := dbStoreTransact.Load() + if transact == nil { + return errors.New("redispool.DBRegisterStore has not been called") + } + + return transact.(DBStoreTransact)(ctx, func(store DBStore) error { + beforeStr, found, err := store.Get(ctx, namespace, key) + if err != nil { + return errors.Wrapf(err, "redispool.DBKeyValue failed to get %q in namespace %q", key, namespace) + } + + before := NaiveValue(beforeStr) + after, remove := f(before, found) + if remove { + if found { + if err := store.Delete(ctx, namespace, key); err != nil { + return errors.Wrapf(err, "redispool.DBKeyValue failed to delete %q in namespace %q", key, namespace) + } + } + } else if before != after { + if err := store.Set(ctx, namespace, key, []byte(after)); err != nil { + return errors.Wrapf(err, "redispool.DBKeyValue failed to set %q in namespace %q", key, namespace) + } + } + return nil + }) + } + + return FromNaiveKeyValueStore(store) +} diff --git a/internal/redispool/keyvalue.go b/internal/redispool/keyvalue.go index 4a241953763a..481d2474978b 100644 --- a/internal/redispool/keyvalue.go +++ b/internal/redispool/keyvalue.go @@ -2,6 +2,7 @@ package redispool import ( "context" + "strings" "time" "github.com/gomodule/redigo/redis" @@ -105,11 +106,25 @@ type redisKeyValue struct { prefix string } +// MemoryKeyValue is the special URI which is recognized by NewKeyValue to +// create an in memory key value. +const MemoryKeyValueURI = "redis+memory:memory" + +const dbKeyValueURIScheme = "redis+postgres" + +// DBKeyValueURI returns a URI to connect to the DB backed redis with the +// specified namespace. +func DBKeyValueURI(namespace string) string { + return dbKeyValueURIScheme + ":" + namespace +} + // NewKeyValue returns a KeyValue for addr. addr is treated as follows: // // 1. if addr == MemoryKeyValueURI we use a KeyValue that lives // in memory of the current process. -// 2. otherwise treat as a redis address. +// 2. if addr was created by DBKeyValueURI we use a KeyValue that is backed +// by postgres. +// 3. otherwise treat as a redis address. // // poolOpts is a required argument which sets defaults in the case we connect // to redis. If used we only override TestOnBorrow and Dial. @@ -117,6 +132,11 @@ func NewKeyValue(addr string, poolOpts *redis.Pool) KeyValue { if addr == MemoryKeyValueURI { return MemoryKeyValue() } + + if schema, namespace, ok := strings.Cut(addr, ":"); ok && schema == dbKeyValueURIScheme { + return DBKeyValue(namespace) + } + poolOpts.TestOnBorrow = func(c redis.Conn, t time.Time) error { _, err := c.Do("PING") return err diff --git a/internal/redispool/mem.go b/internal/redispool/mem.go index d9d353317343..388c3a9afd01 100644 --- a/internal/redispool/mem.go +++ b/internal/redispool/mem.go @@ -5,10 +5,6 @@ import ( "sync" ) -// MemoryKeyValue is the special URI which is recognized by NewKeyValue to -// create an in memory key value. -const MemoryKeyValueURI = "keyvalue:memory" - // MemoryKeyValue returns an in memory KeyValue. func MemoryKeyValue() KeyValue { var mu sync.Mutex diff --git a/internal/redispool/naive_test.go b/internal/redispool/naive_test.go index d035129d6015..320fd833a5cc 100644 --- a/internal/redispool/naive_test.go +++ b/internal/redispool/naive_test.go @@ -1,11 +1,68 @@ package redispool_test import ( + "context" "testing" + "github.com/gomodule/redigo/redis" + "github.com/sourcegraph/log/logtest" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/database/dbtest" "github.com/sourcegraph/sourcegraph/internal/redispool" + "github.com/sourcegraph/sourcegraph/lib/errors" ) func TestInMemoryKeyValue(t *testing.T) { testKeyValue(t, redispool.MemoryKeyValue()) } + +func TestDBKeyValue(t *testing.T) { + if testing.Short() { + t.Skip("skipping DB test since -short is specified") + } + t.Parallel() + + require := require{TB: t} + db := redispool.DBKeyValue("test") + + require.Equal(db.Get("db_test"), errors.New("redispool.DBRegisterStore has not been called")) + + // Now register and check if db starts to work + if err := redispool.DBRegisterStore(dbStoreTransact(t)); err != nil { + t.Fatal(err) + } + + t.Run("integration", func(t *testing.T) { + testKeyValue(t, db) + }) + + // Ensure we can't register twice + if err := redispool.DBRegisterStore(dbStoreTransact(t)); err == nil { + t.Fatal("expected second call to DBRegisterStore to fail") + } + if err := redispool.DBRegisterStore(nil); err == nil { + t.Fatal("expected third call to DBRegisterStore to fail") + } + // Ensure we are still working + require.Equal(db.Get("db_test"), redis.ErrNil) + + // Check that namespacing works. Intentionally use same namespace as db + // for db1. + db1 := redispool.DBKeyValue("test") + db2 := redispool.DBKeyValue("test2") + require.Works(db1.Set("db_test", "1")) + require.Works(db2.Set("db_test", "2")) + require.Equal(db1.Get("db_test"), "1") + require.Equal(db2.Get("db_test"), "2") +} + +func dbStoreTransact(t *testing.T) redispool.DBStoreTransact { + logger := logtest.Scoped(t) + kvNoTX := database.NewDB(logger, dbtest.NewDB(logger, t)).RedisKeyValue() + + return func(ctx context.Context, f func(redispool.DBStore) error) error { + return kvNoTX.WithTransact(ctx, func(tx database.RedisKeyValueStore) error { + return f(tx) + }) + } +} diff --git a/internal/redispool/redispool.go b/internal/redispool/redispool.go index 915f86214b5b..3cc3e17c49c7 100644 --- a/internal/redispool/redispool.go +++ b/internal/redispool/redispool.go @@ -58,7 +58,7 @@ var addresses = func() struct { for _, addr := range []string{ env.Get("REDIS_STORE_ENDPOINT", "", "redis used for persistent stores (eg HTTP sessions). Default redis-store:6379"), fallback, - maybe(deploy.IsDeployTypeSingleProgram(deployType), MemoryKeyValueURI), + maybe(deploy.IsDeployTypeSingleProgram(deployType), DBKeyValueURI("store")), "redis-store:6379", } { if addr != "" { diff --git a/internal/redispool/redispool_test.go b/internal/redispool/redispool_test.go index ba75ed28a79a..5400c1f1ce4c 100644 --- a/internal/redispool/redispool_test.go +++ b/internal/redispool/redispool_test.go @@ -1,6 +1,12 @@ package redispool -import "testing" +import ( + "flag" + "os" + "testing" + + "github.com/sourcegraph/log/logtest" +) func TestSchemeMatcher(t *testing.T) { tests := []struct { @@ -20,3 +26,9 @@ func TestSchemeMatcher(t *testing.T) { } } } + +func TestMain(m *testing.M) { + flag.Parse() + logtest.Init(m) + os.Exit(m.Run()) +} diff --git a/migrations/frontend/1675257827_redis_key_value/down.sql b/migrations/frontend/1675257827_redis_key_value/down.sql new file mode 100644 index 000000000000..8abfec1f6e36 --- /dev/null +++ b/migrations/frontend/1675257827_redis_key_value/down.sql @@ -0,0 +1,3 @@ +-- Undo the changes made in the up migration + +DROP TABLE IF EXISTS redis_key_value; diff --git a/migrations/frontend/1675257827_redis_key_value/metadata.yaml b/migrations/frontend/1675257827_redis_key_value/metadata.yaml new file mode 100644 index 000000000000..2c1a6ff21150 --- /dev/null +++ b/migrations/frontend/1675257827_redis_key_value/metadata.yaml @@ -0,0 +1,2 @@ +name: redis_key_value +parents: [1675155867] diff --git a/migrations/frontend/1675257827_redis_key_value/up.sql b/migrations/frontend/1675257827_redis_key_value/up.sql new file mode 100644 index 000000000000..8d77bd8f3efa --- /dev/null +++ b/migrations/frontend/1675257827_redis_key_value/up.sql @@ -0,0 +1,8 @@ +-- Perform migration here. + +CREATE TABLE IF NOT EXISTS redis_key_value ( + namespace TEXT NOT NULL, + key TEXT NOT NULL, + value BYTEA NOT NULL, + PRIMARY KEY (namespace, key) INCLUDE (value) +); diff --git a/migrations/frontend/squashed.sql b/migrations/frontend/squashed.sql index 7d1e4d1850dd..c75c24478252 100755 --- a/migrations/frontend/squashed.sql +++ b/migrations/frontend/squashed.sql @@ -3497,6 +3497,12 @@ CREATE VIEW reconciler_changesets AS LEFT JOIN orgs namespace_org ON ((batch_changes.namespace_org_id = namespace_org.id))) WHERE ((c.batch_change_ids ? (batch_changes.id)::text) AND (namespace_user.deleted_at IS NULL) AND (namespace_org.deleted_at IS NULL))))); +CREATE TABLE redis_key_value ( + namespace text NOT NULL, + key text NOT NULL, + value bytea NOT NULL +); + CREATE TABLE registry_extension_releases ( id bigint NOT NULL, registry_extension_id integer NOT NULL, @@ -4551,6 +4557,9 @@ ALTER TABLE ONLY product_licenses ALTER TABLE ONLY product_subscriptions ADD CONSTRAINT product_subscriptions_pkey PRIMARY KEY (id); +ALTER TABLE ONLY redis_key_value + ADD CONSTRAINT redis_key_value_pkey PRIMARY KEY (namespace, key) INCLUDE (value); + ALTER TABLE ONLY registry_extension_releases ADD CONSTRAINT registry_extension_releases_pkey PRIMARY KEY (id); diff --git a/sg.config.yaml b/sg.config.yaml index d2ceff77a9c3..5bac1a84a5a3 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -827,7 +827,7 @@ commands: sourcegraph: description: Single program (Go static binary) distribution cmd: | - unset SRC_GIT_SERVERS INDEXED_SEARCH_SERVERS + unset SRC_GIT_SERVERS INDEXED_SEARCH_SERVERS REDIS_ENDPOINT # TODO: This should be fixed export SOURCEGRAPH_LICENSE_GENERATION_KEY=$(cat ../dev-private/enterprise/dev/test-license-generation-key.pem) @@ -1131,8 +1131,6 @@ commandsets: requiresDevPrivate: true checks: - docker - # TODO: App's dependency on Redis will be removed. - - redis - postgres - git commands: From 6a5f955280deb73fe4efe53fe4a64ed1a493a279 Mon Sep 17 00:00:00 2001 From: Petri-Johan Last Date: Fri, 3 Feb 2023 16:24:31 +0200 Subject: [PATCH 397/678] Remove rate limiter waiting in perms syncer (#47374) --- .../repo-updater/internal/authz/metrics.go | 5 --- .../internal/authz/perms_syncer.go | 27 ------------ .../internal/authz/perms_syncer_test.go | 42 ------------------- .../dependencies/internal/store/scan.go | 3 -- 4 files changed, 77 deletions(-) diff --git a/enterprise/cmd/repo-updater/internal/authz/metrics.go b/enterprise/cmd/repo-updater/internal/authz/metrics.go index b119ca754299..98a332b4895f 100644 --- a/enterprise/cmd/repo-updater/internal/authz/metrics.go +++ b/enterprise/cmd/repo-updater/internal/authz/metrics.go @@ -36,11 +36,6 @@ var ( Name: "src_repoupdater_perms_syncer_queue_size", Help: "The size of the sync request queue", }) - metricsRateLimiterWaitDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ - Name: "src_repoupdater_perms_syncer_sync_wait_duration_seconds", - Help: "Time spent waiting on rate-limiter to sync permissions", - Buckets: []float64{0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120}, - }, []string{"type", "success"}) metricsConcurrentSyncs = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "src_repoupdater_perms_syncer_concurrent_syncs", Help: "The number of concurrent permissions syncs", diff --git a/enterprise/cmd/repo-updater/internal/authz/perms_syncer.go b/enterprise/cmd/repo-updater/internal/authz/perms_syncer.go index ad7d3106bfc8..4b5fc13afb7a 100644 --- a/enterprise/cmd/repo-updater/internal/authz/perms_syncer.go +++ b/enterprise/cmd/repo-updater/internal/authz/perms_syncer.go @@ -384,10 +384,6 @@ func (s *PermsSyncer) fetchUserPermsViaExternalAccounts(ctx context.Context, use continue } - if err := s.waitForRateLimit(ctx, provider.URN(), 1, "user"); err != nil { - return results, errors.Wrap(err, "wait for rate limiter") - } - acctLogger.Debug("update GitHub App installation access", log.Int32("accountID", acct.ID)) // FetchUserPerms makes API requests using a client that will deal with the token @@ -700,10 +696,6 @@ func (s *PermsSyncer) syncRepoPerms(ctx context.Context, repoID api.RepoID, noPe pendingAccountIDsSet := make(map[string]struct{}) accountIDsToUserIDs := make(map[string]int32) // Account ID -> User ID - if err := s.waitForRateLimit(ctx, provider.URN(), 1, "repo"); err != nil { - return result, providerStates, errors.Wrap(err, "wait for rate limiter") - } - extAccountIDs, err := provider.FetchRepoPerms(ctx, &extsvc.Repository{ URI: repo.URI, ExternalRepoSpec: repo.ExternalRepo, @@ -846,25 +838,6 @@ func (s *PermsSyncer) syncRepoPerms(ctx context.Context, repoID api.RepoID, noPe return result, providerStates, nil } -// waitForRateLimit blocks until rate limit permits n events to happen. It returns -// an error if n exceeds the limiter's burst size, the context is canceled, or the -// expected wait time exceeds the context's deadline. The burst limit is ignored if -// the rate limit is Inf. -func (s *PermsSyncer) waitForRateLimit(ctx context.Context, urn string, n int, syncType string) error { - if s.rateLimiterRegistry == nil { - return nil - } - - rl := s.rateLimiterRegistry.Get(urn) - began := time.Now() - if err := rl.WaitN(ctx, n); err != nil { - metricsRateLimiterWaitDuration.WithLabelValues(syncType, strconv.FormatBool(false)).Observe(time.Since(began).Seconds()) - return err - } - metricsRateLimiterWaitDuration.WithLabelValues(syncType, strconv.FormatBool(true)).Observe(time.Since(began).Seconds()) - return nil -} - // syncPerms processes the permissions syncing request and removes the request // from the queue once the process is done (regardless of success or failure). // The given sync groups are used to control the max concurrency, this method diff --git a/enterprise/cmd/repo-updater/internal/authz/perms_syncer_test.go b/enterprise/cmd/repo-updater/internal/authz/perms_syncer_test.go index bb3af7478a9f..377f115251f9 100644 --- a/enterprise/cmd/repo-updater/internal/authz/perms_syncer_test.go +++ b/enterprise/cmd/repo-updater/internal/authz/perms_syncer_test.go @@ -23,7 +23,6 @@ import ( "github.com/sourcegraph/sourcegraph/internal/extsvc" "github.com/sourcegraph/sourcegraph/internal/extsvc/github" "github.com/sourcegraph/sourcegraph/internal/extsvc/gitlab" - "github.com/sourcegraph/sourcegraph/internal/ratelimit" "github.com/sourcegraph/sourcegraph/internal/repos" "github.com/sourcegraph/sourcegraph/internal/timeutil" "github.com/sourcegraph/sourcegraph/internal/types" @@ -866,47 +865,6 @@ func TestPermsSyncer_syncRepoPerms(t *testing.T) { } } -func TestPermsSyncer_waitForRateLimit(t *testing.T) { - ctx := context.Background() - t.Run("no rate limit registry", func(t *testing.T) { - s := NewPermsSyncer(logtest.Scoped(t), nil, nil, nil, nil, nil) - - ctx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - err := s.waitForRateLimit(ctx, "https://github.com/", 100000, "user") - if err != nil { - t.Fatal(err) - } - }) - - t.Run("enough quota available", func(t *testing.T) { - rateLimiterRegistry := ratelimit.NewRegistry() - s := NewPermsSyncer(logtest.Scoped(t), nil, nil, nil, nil, rateLimiterRegistry) - - ctx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - err := s.waitForRateLimit(ctx, "https://github.com/", 1, "user") - if err != nil { - t.Fatal(err) - } - }) - - t.Run("not enough quota available", func(t *testing.T) { - rateLimiterRegistry := ratelimit.NewRegistry() - l := rateLimiterRegistry.Get("extsvc:github:1") - l.SetLimit(1) - l.SetBurst(1) - s := NewPermsSyncer(logtest.Scoped(t), nil, nil, nil, nil, rateLimiterRegistry) - - ctx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - err := s.waitForRateLimit(ctx, "extsvc:github:1", 10, "user") - if err == nil { - t.Fatalf("err: want %v but got nil", context.Canceled) - } - }) -} - func TestPermsSyncer_syncPerms(t *testing.T) { ctx := context.Background() diff --git a/internal/codeintel/dependencies/internal/store/scan.go b/internal/codeintel/dependencies/internal/store/scan.go index bd0c1c938788..d8a0d6693dcf 100644 --- a/internal/codeintel/dependencies/internal/store/scan.go +++ b/internal/codeintel/dependencies/internal/store/scan.go @@ -2,7 +2,6 @@ package store import ( "github.com/sourcegraph/sourcegraph/internal/codeintel/dependencies/shared" - "github.com/sourcegraph/sourcegraph/internal/database/basestore" "github.com/sourcegraph/sourcegraph/internal/database/dbutil" ) @@ -13,5 +12,3 @@ func scanDependencyRepo(s dbutil.Scanner) (shared.PackageRepoReference, error) { ref.Versions = []shared.PackageRepoRefVersion{version} return ref, err } - -var scanDependencyRepos = basestore.NewSliceScanner(scanDependencyRepo) From 66e15cfa4759d4de5a007fdb736bb62aa672cad2 Mon Sep 17 00:00:00 2001 From: Cezary Bartoszuk Date: Fri, 3 Feb 2023 08:29:55 -0600 Subject: [PATCH 398/678] GraphQL: Teams connection query (#47355) * Draft teams query implementation * Pagination test working * All tests implemented * Update relevant team connection tests to use RunTest * Add clarifying comments --- cmd/frontend/graphqlbackend/teams.go | 121 +++++++++++- cmd/frontend/graphqlbackend/teams_test.go | 227 +++++++++++++++++++++- 2 files changed, 336 insertions(+), 12 deletions(-) diff --git a/cmd/frontend/graphqlbackend/teams.go b/cmd/frontend/graphqlbackend/teams.go index cde736033fca..29d35964d286 100644 --- a/cmd/frontend/graphqlbackend/teams.go +++ b/cmd/frontend/graphqlbackend/teams.go @@ -2,6 +2,7 @@ package graphqlbackend import ( "context" + "sync" "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/relay" @@ -21,19 +22,104 @@ type ListTeamsArgs struct { Search *string } -type teamConnectionResolver struct{} +type teamConnectionResolver struct { + db database.DB + parentID int32 + search string + cursor int32 + limit int + once sync.Once + teams []*types.Team + pageInfo *graphqlutil.PageInfo + err error +} -func (r *teamConnectionResolver) TotalCount(args *struct{ CountDeeplyNestedTeams bool }) int32 { - return 0 +// applyArgs unmarshals query conditions and limites set in `ListTeamsArgs` +// into `teamConnectionResolver` fields for convenient use in database query. +func (r *teamConnectionResolver) applyArgs(args *ListTeamsArgs) error { + if args.After != nil { + cursor, err := graphqlutil.DecodeIntCursor(args.After) + if err != nil { + return err + } + r.cursor = int32(cursor) + if int(r.cursor) != cursor { + return errors.Newf("cursor int32 overflow: %d", cursor) + } + } + if args.Search != nil { + r.search = *args.Search + } + if args.First != nil { + r.limit = int(*args.First) + } + return nil } -func (r *teamConnectionResolver) PageInfo() *graphqlutil.PageInfo { - return graphqlutil.HasNextPage(false) + +// compute resolves teams queried for this resolver. +// The result of running it is setting `teams`, `next` and `err` +// fields on the resolver. This ensures that resolving multiple +// graphQL attributes that require listing (like `pageInfo` and `nodes`) +// results in just one query. +func (r *teamConnectionResolver) compute(ctx context.Context) { + r.once.Do(func() { + opts := database.ListTeamsOpts{ + Cursor: r.cursor, + WithParentID: r.parentID, + Search: r.search, + } + if r.limit != 0 { + opts.LimitOffset = &database.LimitOffset{Limit: r.limit} + } + teams, next, err := r.db.Teams().ListTeams(ctx, opts) + if err != nil { + r.err = err + return + } + r.teams = teams + if next > 0 { + r.pageInfo = graphqlutil.EncodeIntCursor(&next) + } else { + r.pageInfo = graphqlutil.HasNextPage(false) + } + }) +} + +func (r *teamConnectionResolver) TotalCount(ctx context.Context, args *struct{ CountDeeplyNestedTeams bool }) (int32, error) { + if args != nil && args.CountDeeplyNestedTeams { + return 0, errors.New("Not supported: counting deeply nested teams.") + } + // Not taking into account limit or cursor for count. + opts := database.ListTeamsOpts{ + WithParentID: r.parentID, + Search: r.search, + } + return r.db.Teams().CountTeams(ctx, opts) +} + +func (r *teamConnectionResolver) PageInfo(ctx context.Context) (*graphqlutil.PageInfo, error) { + r.compute(ctx) + return r.pageInfo, r.err +} + +func (r *teamConnectionResolver) Nodes(ctx context.Context) ([]*teamResolver, error) { + r.compute(ctx) + if r.err != nil { + return nil, r.err + } + var rs []*teamResolver + for _, t := range r.teams { + rs = append(rs, &teamResolver{ + db: r.db, + team: t, + }) + } + return rs, nil } -func (r *teamConnectionResolver) Nodes() []*teamResolver { return nil } type teamResolver struct { - team *types.Team db database.DB + team *types.Team } func (r *teamResolver) ID() graphql.ID { @@ -66,8 +152,15 @@ func (r *teamResolver) ViewerCanAdminister(ctx context.Context) bool { func (r *teamResolver) Members(args *ListTeamsArgs) *teamMemberConnection { return &teamMemberConnection{} } -func (r *teamResolver) ChildTeams(args *ListTeamsArgs) *teamConnectionResolver { - return &teamConnectionResolver{} +func (r *teamResolver) ChildTeams(ctx context.Context, args *ListTeamsArgs) (*teamConnectionResolver, error) { + c := &teamConnectionResolver{ + db: r.db, + parentID: r.team.ID, + } + if err := c.applyArgs(args); err != nil { + return nil, err + } + return c, nil } type teamMemberConnection struct{} @@ -249,7 +342,15 @@ func (r *schemaResolver) RemoveTeamMembers(args *TeamMembersArgs) *teamResolver } func (r *schemaResolver) Teams(ctx context.Context, args *ListTeamsArgs) (*teamConnectionResolver, error) { - return &teamConnectionResolver{}, nil + // 🚨 SECURITY: For now we only allow site admins to use teams. + if err := auth.CheckCurrentUserIsSiteAdmin(ctx, r.db); err != nil { + return nil, errors.New("only site admins can view teams") + } + c := &teamConnectionResolver{db: r.db} + if err := c.applyArgs(args); err != nil { + return nil, err + } + return c, nil } type TeamArgs struct { diff --git a/cmd/frontend/graphqlbackend/teams_test.go b/cmd/frontend/graphqlbackend/teams_test.go index 0586f9ae9891..96c0969de2b8 100644 --- a/cmd/frontend/graphqlbackend/teams_test.go +++ b/cmd/frontend/graphqlbackend/teams_test.go @@ -2,7 +2,9 @@ package graphqlbackend import ( "context" + "encoding/json" "fmt" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -79,6 +81,49 @@ func (teams *fakeTeamsDb) DeleteTeam(_ context.Context, id int32) error { return database.TeamNotFoundError{} } +func (teams *fakeTeamsDb) ListTeams(_ context.Context, opts database.ListTeamsOpts) (selected []*types.Team, next int32, err error) { + for _, t := range teams.list { + if matches(t, opts) { + selected = append(selected, t) + } + } + if opts.LimitOffset != nil { + selected = selected[opts.LimitOffset.Offset:] + if limit := opts.LimitOffset.Limit; limit != 0 && len(selected) > limit { + next = selected[opts.LimitOffset.Limit].ID + selected = selected[:opts.LimitOffset.Limit] + } + } + return selected, next, nil +} + +func (teams *fakeTeamsDb) CountTeams(ctx context.Context, opts database.ListTeamsOpts) (int32, error) { + selected, _, err := teams.ListTeams(ctx, opts) + return int32(len(selected)), err +} + +func matches(team *types.Team, opts database.ListTeamsOpts) bool { + if opts.Cursor != 0 && team.ID < opts.Cursor { + return false + } + if opts.WithParentID != 0 && team.ParentTeamID != opts.WithParentID { + return false + } + if opts.RootOnly && team.ParentTeamID != 0 { + return false + } + if opts.Search != "" { + search := strings.ToLower(opts.Search) + name := strings.ToLower(team.Name) + displayName := strings.ToLower(team.DisplayName) + if !strings.Contains(name, search) && !strings.Contains(displayName, search) { + return false + } + } + // opts.ForUserMember is not supported yet as there is no membership fake. + return true +} + func setupDB() (*database.MockDB, *fakeTeamsDb) { ts := &fakeTeamsDb{} db := database.NewMockDB() @@ -89,7 +134,7 @@ func setupDB() (*database.MockDB, *fakeTeamsDb) { return db, ts } -func TestQuery(t *testing.T) { +func TestTeamNode(t *testing.T) { db, ts := setupDB() ctx, _, _ := fakeUser(t, context.Background(), db, true) if err := ts.CreateTeam(ctx, &types.Team{Name: "team"}); err != nil { @@ -122,7 +167,7 @@ func TestQuery(t *testing.T) { }) } -func TestQuerySiteAdminCanAdminister(t *testing.T) { +func TestTeamNodeSiteAdminCanAdminister(t *testing.T) { for _, isAdmin := range []bool{true, false} { t.Run(fmt.Sprintf("viewer is admin = %v", isAdmin), func(t *testing.T) { db, ts := setupDB() @@ -798,3 +843,181 @@ func TestTeamByNameUnauthorized(t *testing.T) { }, }) } + +func TestTeamsPaginated(t *testing.T) { + db, ts := setupDB() + ctx, _, _ := fakeUser(t, context.Background(), db, true) + for i := 1; i <= 25; i++ { + name := fmt.Sprintf("team-%d", i) + if err := ts.CreateTeam(ctx, &types.Team{Name: name}); err != nil { + t.Fatalf("failed to create a team: %s", err) + } + } + var ( + hasNextPage bool = true + cursor string + ) + query := `query Teams($cursor: String!) { + teams(after: $cursor, first: 10) { + pageInfo { + endCursor + hasNextPage + } + nodes { + name + } + } + }` + operationName := "" + var gotNames []string + for hasNextPage { + variables := map[string]any{ + "cursor": cursor, + } + r := mustParseGraphQLSchema(t, db).Exec(ctx, query, operationName, variables) + var wantErrors []*gqlerrors.QueryError + checkErrors(t, wantErrors, r.Errors) + var result struct { + Teams *struct { + PageInfo *struct { + EndCursor string + HasNextPage bool + } + Nodes []struct { + Name string + } + } + } + if err := json.Unmarshal(r.Data, &result); err != nil { + t.Fatalf("cannot interpret graphQL query result: %s", err) + } + hasNextPage = result.Teams.PageInfo.HasNextPage + cursor = result.Teams.PageInfo.EndCursor + for _, node := range result.Teams.Nodes { + gotNames = append(gotNames, node.Name) + } + } + var wantNames []string + for _, team := range ts.list { + wantNames = append(wantNames, team.Name) + } + if diff := cmp.Diff(wantNames, gotNames); diff != "" { + t.Errorf("unexpected team names (-want,+got):\n%s", diff) + } +} + +// Skip testing DisplayName search as this is the same except the fake behavior. +func TestTeamsNameSearch(t *testing.T) { + db, ts := setupDB() + ctx, _, _ := fakeUser(t, context.Background(), db, true) + for _, name := range []string{"hit-1", "Hit-2", "HIT-3", "miss-4", "mIss-5", "MISS-6"} { + if err := ts.CreateTeam(ctx, &types.Team{Name: name}); err != nil { + t.Fatalf("failed to create a team: %s", err) + } + } + RunTest(t, &Test{ + Schema: mustParseGraphQLSchema(t, db), + Context: ctx, + Query: `{ + teams(search: "hit") { + nodes { + name + } + } + }`, + ExpectedResult: `{ + "teams": { + "nodes": [ + {"name": "hit-1"}, + {"name": "Hit-2"}, + {"name": "HIT-3"} + ] + } + }`, + }) +} + +func TestTeamsCount(t *testing.T) { + db, ts := setupDB() + ctx, _, _ := fakeUser(t, context.Background(), db, true) + for i := 1; i <= 25; i++ { + name := fmt.Sprintf("team-%d", i) + if err := ts.CreateTeam(ctx, &types.Team{Name: name}); err != nil { + t.Fatalf("failed to create a team: %s", err) + } + } + RunTest(t, &Test{ + Schema: mustParseGraphQLSchema(t, db), + Context: ctx, + Query: `{ + teams(first: 5) { + totalCount + nodes { + name + } + } + }`, + ExpectedResult: `{ + "teams": { + "totalCount": 25, + "nodes": [ + {"name": "team-1"}, + {"name": "team-2"}, + {"name": "team-3"}, + {"name": "team-4"}, + {"name": "team-5"} + ] + } + }`, + }) +} + +func TestChildTeams(t *testing.T) { + db, ts := setupDB() + ctx, _, _ := fakeUser(t, context.Background(), db, true) + if err := ts.CreateTeam(ctx, &types.Team{Name: "parent"}); err != nil { + t.Fatalf("failed to create parent team: %s", err) + } + parent, err := ts.GetTeamByName(ctx, "parent") + if err != nil { + t.Fatalf("cannot fetch parent team: %s", err) + } + for i := 1; i <= 5; i++ { + name := fmt.Sprintf("child-%d", i) + if err := ts.CreateTeam(ctx, &types.Team{Name: name, ParentTeamID: parent.ID}); err != nil { + t.Fatalf("cannot create child team: %s", err) + } + } + for i := 6; i <= 10; i++ { + name := fmt.Sprintf("not-child-%d", i) + if err := ts.CreateTeam(ctx, &types.Team{Name: name}); err != nil { + t.Fatalf("cannot create a team: %s", err) + } + } + RunTest(t, &Test{ + Schema: mustParseGraphQLSchema(t, db), + Context: ctx, + Query: `{ + team(name: "parent") { + childTeams { + nodes { + name + } + } + } + }`, + ExpectedResult: `{ + "team": { + "childTeams": { + "nodes": [ + {"name": "child-1"}, + {"name": "child-2"}, + {"name": "child-3"}, + {"name": "child-4"}, + {"name": "child-5"} + ] + } + } + }`, + }) +} From 23f617f1f2250259b5af793f33dc31c5b080d5c4 Mon Sep 17 00:00:00 2001 From: Petri-Johan Last Date: Fri, 3 Feb 2023 16:32:21 +0200 Subject: [PATCH 399/678] Add beta tags to Gerrit docs (#47161) --- doc/admin/auth/index.md | 3 ++- doc/admin/external_service/gerrit.md | 1 + doc/admin/external_service/index.md | 4 ++-- doc/admin/external_service/other.md | 2 +- doc/admin/repo/permissions.md | 3 ++- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/doc/admin/auth/index.md b/doc/admin/auth/index.md index 04e264cd2020..d241201cf39b 100644 --- a/doc/admin/auth/index.md +++ b/doc/admin/auth/index.md @@ -7,7 +7,7 @@ Sourcegraph supports the following ways for users to sign in: - [GitHub](#github) - [GitLab](#gitlab) - [Bitbucket Cloud](#bitbucket-cloud) -- [Gerrit](#gerrit) +- [Gerrit](#gerrit) Beta - [SAML](saml/index.md) - [OpenID Connect](#openid-connect) - [Google Workspace (Google accounts)](#google-workspace-google-accounts) @@ -352,6 +352,7 @@ Then add the following lines to your [site configuration](config/site_config.md) Replace the `clientKey` and `clientSecret` values with the values from your Bitbucket Cloud OAuth consumer. ## Gerrit +Beta To enable users to add Gerrit credentials and verify their access to repositories on Sourcegraph, add the following lines to your [site configuration](config/site_config.md): diff --git a/doc/admin/external_service/gerrit.md b/doc/admin/external_service/gerrit.md index 428cf650c02c..d93c0132d772 100644 --- a/doc/admin/external_service/gerrit.md +++ b/doc/admin/external_service/gerrit.md @@ -1,4 +1,5 @@ # Gerrit +Beta Site admins can sync Git repositories hosted on their private Gerrit instance with Sourcegraph so that users can search and navigate the repositories. diff --git a/doc/admin/external_service/index.md b/doc/admin/external_service/index.md index 72f5993a7979..608424cc1dfc 100644 --- a/doc/admin/external_service/index.md +++ b/doc/admin/external_service/index.md @@ -96,7 +96,7 @@ Tier 1 code hosts are our highest level of support for code hosts. When leveragi ✓ - Gerrit + Gerrit Beta Tier 2 (Working on Tier 1) ✓ @@ -153,7 +153,7 @@ We recognize there are other code hosts including CVS, Azure Dev Ops, SVN, and m - [GitLab](gitlab.md) - [Bitbucket Cloud](bitbucket_cloud.md) - [Bitbucket Server / Bitbucket Data Center](bitbucket_server.md) -- [Gerrit](gerrit.md) +- [Gerrit](gerrit.md) Beta - [Other Git code hosts (using a Git URL)](other.md) - [Non-Git code hosts](non-git.md) - [Perforce](../repo/perforce.md) diff --git a/doc/admin/external_service/other.md b/doc/admin/external_service/other.md index c4e6a54deee9..38776781d898 100644 --- a/doc/admin/external_service/other.md +++ b/doc/admin/external_service/other.md @@ -21,7 +21,7 @@ docker exec $CONTAINER ssh -p $PORT $USER@$HOSTNAME - $CONTAINER is the name or ID of your sourcegraph/server container - $PORT is the port on which your code host's git server is listening for connections -- $USER is your user on your code host (Gerrit defaults to `admin`) +- $USER is your user on your code host - $HOSTNAME is the hostname of your code host from within the sourcegraph/server container (e.g. `githost.example.com`) Here's an example: diff --git a/doc/admin/repo/permissions.md b/doc/admin/repo/permissions.md index 33c4f02061be..cc77c8361bf0 100644 --- a/doc/admin/repo/permissions.md +++ b/doc/admin/repo/permissions.md @@ -5,7 +5,7 @@ Sourcegraph can be configured to enforce repository permissions from code hosts. - [GitHub / GitHub Enterprise](#github) - [GitLab](#gitlab) - [Bitbucket Server / Bitbucket Data Center](#bitbucket-server-bitbucket-data-center) -- [Gerrit](#gerrit) +- [Gerrit](#gerrit) Beta - [Unified SSO](https://unknwon.io/posts/200915_setup-sourcegraph-gitlab-keycloak/) - [Explicit permissions API](#explicit-permissions-api) @@ -264,6 +264,7 @@ By installing the [Bitbucket Server plugin](../../../integration/bitbucket_serve
    ## Gerrit +Beta Prerequisite: [Add Gerrit as an authentication provider](../auth/index.md#gerrit). From 79f8d5f451224f07b077e7ad6c5c292e5c322d13 Mon Sep 17 00:00:00 2001 From: Erik Seliger Date: Fri, 3 Feb 2023 16:25:09 +0100 Subject: [PATCH 400/678] Move insights resolvers into cmd/frontend (#47345) These resolvers don't seem to depend on living where they used to, so moving them into here to get rid of imports from cmd/frontend in internal. They were super easy to move, so this shouldn't affect anything except that go is happer that we don't do imports across cmds. This should make bazel a little happier, too. --- .../internal/insights/httpapi/BUILD.bazel | 0 .../internal/insights/httpapi/export.go | 0 .../cmd/frontend/internal/insights/init.go | 44 +++++++++++++++++++ .../internal/insights/resolvers/BUILD.bazel | 0 .../insights/resolvers/admin_resolver.go | 0 .../resolvers/aggregates_resolvers.go | 0 .../resolvers/aggregates_resolvers_test.go | 0 .../insights/resolvers/dashboard_id.go | 0 .../insights/resolvers/dashboard_resolvers.go | 0 .../resolvers/dashboard_resolvers_test.go | 0 .../insights/resolvers/disabled_resolver.go | 0 .../resolvers/insight_series_resolver.go | 0 .../resolvers/insight_series_resolver_test.go | 0 .../resolvers/insight_view_resolvers.go | 0 .../resolvers/insight_view_resolvers_test.go | 0 .../resolvers/live_preview_resolvers.go | 0 .../internal/insights/resolvers/resolver.go | 0 .../insights/resolvers/resolver_test.go | 0 .../resolvers/scoped_insight_resolvers.go | 0 .../internal/insights/resolvers/validator.go | 0 enterprise/cmd/frontend/shared/shared.go | 4 +- enterprise/internal/insights/insights.go | 36 --------------- 22 files changed, 46 insertions(+), 38 deletions(-) rename enterprise/{ => cmd/frontend}/internal/insights/httpapi/BUILD.bazel (100%) rename enterprise/{ => cmd/frontend}/internal/insights/httpapi/export.go (100%) create mode 100644 enterprise/cmd/frontend/internal/insights/init.go rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/BUILD.bazel (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/admin_resolver.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/aggregates_resolvers.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/aggregates_resolvers_test.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/dashboard_id.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/dashboard_resolvers.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/dashboard_resolvers_test.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/disabled_resolver.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/insight_series_resolver.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/insight_series_resolver_test.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/insight_view_resolvers.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/insight_view_resolvers_test.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/live_preview_resolvers.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/resolver.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/resolver_test.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/scoped_insight_resolvers.go (100%) rename enterprise/{ => cmd/frontend}/internal/insights/resolvers/validator.go (100%) diff --git a/enterprise/internal/insights/httpapi/BUILD.bazel b/enterprise/cmd/frontend/internal/insights/httpapi/BUILD.bazel similarity index 100% rename from enterprise/internal/insights/httpapi/BUILD.bazel rename to enterprise/cmd/frontend/internal/insights/httpapi/BUILD.bazel diff --git a/enterprise/internal/insights/httpapi/export.go b/enterprise/cmd/frontend/internal/insights/httpapi/export.go similarity index 100% rename from enterprise/internal/insights/httpapi/export.go rename to enterprise/cmd/frontend/internal/insights/httpapi/export.go diff --git a/enterprise/cmd/frontend/internal/insights/init.go b/enterprise/cmd/frontend/internal/insights/init.go new file mode 100644 index 000000000000..ec3676d691dc --- /dev/null +++ b/enterprise/cmd/frontend/internal/insights/init.go @@ -0,0 +1,44 @@ +package insights + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/cmd/frontend/enterprise" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/insights/httpapi" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/insights/resolvers" + "github.com/sourcegraph/sourcegraph/enterprise/internal/codeintel" + internalinsights "github.com/sourcegraph/sourcegraph/enterprise/internal/insights" + "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" + "github.com/sourcegraph/sourcegraph/internal/conf/deploy" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/observation" +) + +// Init initializes the given enterpriseServices to include the required resolvers for insights. +func Init( + ctx context.Context, + observationCtx *observation.Context, + db database.DB, + _ codeintel.Services, + _ conftypes.UnifiedWatchable, + enterpriseServices *enterprise.Services, +) error { + enterpriseServices.InsightsAggregationResolver = resolvers.NewAggregationResolver(observationCtx, db) + + if !internalinsights.IsEnabled() { + if deploy.IsDeployTypeSingleDockerContainer(deploy.Type()) { + enterpriseServices.InsightsResolver = resolvers.NewDisabledResolver("code insights are not available on single-container deployments") + } else { + enterpriseServices.InsightsResolver = resolvers.NewDisabledResolver("code insights has been disabled") + } + return nil + } + rawInsightsDB, err := internalinsights.InitializeCodeInsightsDB(observationCtx, "frontend") + if err != nil { + return err + } + enterpriseServices.InsightsResolver = resolvers.New(rawInsightsDB, db) + enterpriseServices.CodeInsightsDataExportHandler = httpapi.NewExportHandler(db, rawInsightsDB).ExportFunc() + + return nil +} diff --git a/enterprise/internal/insights/resolvers/BUILD.bazel b/enterprise/cmd/frontend/internal/insights/resolvers/BUILD.bazel similarity index 100% rename from enterprise/internal/insights/resolvers/BUILD.bazel rename to enterprise/cmd/frontend/internal/insights/resolvers/BUILD.bazel diff --git a/enterprise/internal/insights/resolvers/admin_resolver.go b/enterprise/cmd/frontend/internal/insights/resolvers/admin_resolver.go similarity index 100% rename from enterprise/internal/insights/resolvers/admin_resolver.go rename to enterprise/cmd/frontend/internal/insights/resolvers/admin_resolver.go diff --git a/enterprise/internal/insights/resolvers/aggregates_resolvers.go b/enterprise/cmd/frontend/internal/insights/resolvers/aggregates_resolvers.go similarity index 100% rename from enterprise/internal/insights/resolvers/aggregates_resolvers.go rename to enterprise/cmd/frontend/internal/insights/resolvers/aggregates_resolvers.go diff --git a/enterprise/internal/insights/resolvers/aggregates_resolvers_test.go b/enterprise/cmd/frontend/internal/insights/resolvers/aggregates_resolvers_test.go similarity index 100% rename from enterprise/internal/insights/resolvers/aggregates_resolvers_test.go rename to enterprise/cmd/frontend/internal/insights/resolvers/aggregates_resolvers_test.go diff --git a/enterprise/internal/insights/resolvers/dashboard_id.go b/enterprise/cmd/frontend/internal/insights/resolvers/dashboard_id.go similarity index 100% rename from enterprise/internal/insights/resolvers/dashboard_id.go rename to enterprise/cmd/frontend/internal/insights/resolvers/dashboard_id.go diff --git a/enterprise/internal/insights/resolvers/dashboard_resolvers.go b/enterprise/cmd/frontend/internal/insights/resolvers/dashboard_resolvers.go similarity index 100% rename from enterprise/internal/insights/resolvers/dashboard_resolvers.go rename to enterprise/cmd/frontend/internal/insights/resolvers/dashboard_resolvers.go diff --git a/enterprise/internal/insights/resolvers/dashboard_resolvers_test.go b/enterprise/cmd/frontend/internal/insights/resolvers/dashboard_resolvers_test.go similarity index 100% rename from enterprise/internal/insights/resolvers/dashboard_resolvers_test.go rename to enterprise/cmd/frontend/internal/insights/resolvers/dashboard_resolvers_test.go diff --git a/enterprise/internal/insights/resolvers/disabled_resolver.go b/enterprise/cmd/frontend/internal/insights/resolvers/disabled_resolver.go similarity index 100% rename from enterprise/internal/insights/resolvers/disabled_resolver.go rename to enterprise/cmd/frontend/internal/insights/resolvers/disabled_resolver.go diff --git a/enterprise/internal/insights/resolvers/insight_series_resolver.go b/enterprise/cmd/frontend/internal/insights/resolvers/insight_series_resolver.go similarity index 100% rename from enterprise/internal/insights/resolvers/insight_series_resolver.go rename to enterprise/cmd/frontend/internal/insights/resolvers/insight_series_resolver.go diff --git a/enterprise/internal/insights/resolvers/insight_series_resolver_test.go b/enterprise/cmd/frontend/internal/insights/resolvers/insight_series_resolver_test.go similarity index 100% rename from enterprise/internal/insights/resolvers/insight_series_resolver_test.go rename to enterprise/cmd/frontend/internal/insights/resolvers/insight_series_resolver_test.go diff --git a/enterprise/internal/insights/resolvers/insight_view_resolvers.go b/enterprise/cmd/frontend/internal/insights/resolvers/insight_view_resolvers.go similarity index 100% rename from enterprise/internal/insights/resolvers/insight_view_resolvers.go rename to enterprise/cmd/frontend/internal/insights/resolvers/insight_view_resolvers.go diff --git a/enterprise/internal/insights/resolvers/insight_view_resolvers_test.go b/enterprise/cmd/frontend/internal/insights/resolvers/insight_view_resolvers_test.go similarity index 100% rename from enterprise/internal/insights/resolvers/insight_view_resolvers_test.go rename to enterprise/cmd/frontend/internal/insights/resolvers/insight_view_resolvers_test.go diff --git a/enterprise/internal/insights/resolvers/live_preview_resolvers.go b/enterprise/cmd/frontend/internal/insights/resolvers/live_preview_resolvers.go similarity index 100% rename from enterprise/internal/insights/resolvers/live_preview_resolvers.go rename to enterprise/cmd/frontend/internal/insights/resolvers/live_preview_resolvers.go diff --git a/enterprise/internal/insights/resolvers/resolver.go b/enterprise/cmd/frontend/internal/insights/resolvers/resolver.go similarity index 100% rename from enterprise/internal/insights/resolvers/resolver.go rename to enterprise/cmd/frontend/internal/insights/resolvers/resolver.go diff --git a/enterprise/internal/insights/resolvers/resolver_test.go b/enterprise/cmd/frontend/internal/insights/resolvers/resolver_test.go similarity index 100% rename from enterprise/internal/insights/resolvers/resolver_test.go rename to enterprise/cmd/frontend/internal/insights/resolvers/resolver_test.go diff --git a/enterprise/internal/insights/resolvers/scoped_insight_resolvers.go b/enterprise/cmd/frontend/internal/insights/resolvers/scoped_insight_resolvers.go similarity index 100% rename from enterprise/internal/insights/resolvers/scoped_insight_resolvers.go rename to enterprise/cmd/frontend/internal/insights/resolvers/scoped_insight_resolvers.go diff --git a/enterprise/internal/insights/resolvers/validator.go b/enterprise/cmd/frontend/internal/insights/resolvers/validator.go similarity index 100% rename from enterprise/internal/insights/resolvers/validator.go rename to enterprise/cmd/frontend/internal/insights/resolvers/validator.go diff --git a/enterprise/cmd/frontend/shared/shared.go b/enterprise/cmd/frontend/shared/shared.go index 0c0cf8b93acb..67b5f1ef4a01 100644 --- a/enterprise/cmd/frontend/shared/shared.go +++ b/enterprise/cmd/frontend/shared/shared.go @@ -11,7 +11,6 @@ import ( "strconv" "github.com/sourcegraph/log" - "github.com/sourcegraph/sourcegraph/enterprise/internal/scim" "github.com/sourcegraph/sourcegraph/cmd/frontend/enterprise" "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/app" @@ -23,6 +22,7 @@ import ( "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/compute" "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/dotcom" executor "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/executorqueue" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/insights" licensing "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/licensing/init" "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/notebooks" _ "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/registry" @@ -30,7 +30,7 @@ import ( "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/searchcontexts" "github.com/sourcegraph/sourcegraph/enterprise/internal/codeintel" codeintelshared "github.com/sourcegraph/sourcegraph/enterprise/internal/codeintel/shared" - "github.com/sourcegraph/sourcegraph/enterprise/internal/insights" + "github.com/sourcegraph/sourcegraph/enterprise/internal/scim" "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" "github.com/sourcegraph/sourcegraph/internal/database" diff --git a/enterprise/internal/insights/insights.go b/enterprise/internal/insights/insights.go index 5111390e7975..f545d21c3662 100644 --- a/enterprise/internal/insights/insights.go +++ b/enterprise/internal/insights/insights.go @@ -1,17 +1,10 @@ package insights import ( - "context" - "github.com/sourcegraph/sourcegraph/cmd/frontend/enterprise" - "github.com/sourcegraph/sourcegraph/enterprise/internal/codeintel" edb "github.com/sourcegraph/sourcegraph/enterprise/internal/database" - "github.com/sourcegraph/sourcegraph/enterprise/internal/insights/httpapi" - "github.com/sourcegraph/sourcegraph/enterprise/internal/insights/resolvers" "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" - "github.com/sourcegraph/sourcegraph/internal/conf/deploy" - "github.com/sourcegraph/sourcegraph/internal/database" connections "github.com/sourcegraph/sourcegraph/internal/database/connections/live" "github.com/sourcegraph/sourcegraph/internal/observation" "github.com/sourcegraph/sourcegraph/lib/errors" @@ -21,35 +14,6 @@ func IsEnabled() bool { return enterprise.IsCodeInsightsEnabled() } -// Init initializes the given enterpriseServices to include the required resolvers for insights. -func Init( - ctx context.Context, - observationCtx *observation.Context, - db database.DB, - _ codeintel.Services, - _ conftypes.UnifiedWatchable, - enterpriseServices *enterprise.Services, -) error { - enterpriseServices.InsightsAggregationResolver = resolvers.NewAggregationResolver(observationCtx, db) - - if !IsEnabled() { - if deploy.IsDeployTypeSingleDockerContainer(deploy.Type()) { - enterpriseServices.InsightsResolver = resolvers.NewDisabledResolver("code insights are not available on single-container deployments") - } else { - enterpriseServices.InsightsResolver = resolvers.NewDisabledResolver("code insights has been disabled") - } - return nil - } - rawInsightsDB, err := InitializeCodeInsightsDB(observationCtx, "frontend") - if err != nil { - return err - } - enterpriseServices.InsightsResolver = resolvers.New(rawInsightsDB, db) - enterpriseServices.CodeInsightsDataExportHandler = httpapi.NewExportHandler(db, rawInsightsDB).ExportFunc() - - return nil -} - // InitializeCodeInsightsDB connects to and initializes the Code Insights Postgres DB, running // database migrations before returning. It is safe to call from multiple services/containers (in // which case, one's migration will win and the other caller will receive an error and should exit From 0d9c4d52cfaee41bf3d6cfba7e5b9527e170b816 Mon Sep 17 00:00:00 2001 From: Felix Kling Date: Fri, 3 Feb 2023 17:04:29 +0100 Subject: [PATCH 401/678] experimental search input: Add support for history mode (#47203) --- .../search-ui/input/codemirror/placeholder.ts | 76 +++-- .../CodeMirrorQueryInputWrapper.tsx | 32 ++- .../experimental/Suggestions.module.scss | 7 +- .../input/experimental/Suggestions.tsx | 148 ++++++---- .../SyntaxHighlightedSearchQuery.tsx | 46 +++ .../input/experimental/codemirror/history.ts | 146 ++++++++++ .../codemirror/syntax-highlighting.ts | 12 +- .../src/search-ui/input/experimental/index.ts | 3 +- .../src/search-ui/input/experimental/modes.ts | 199 +++++++++++++ .../input/experimental/optionRenderer.tsx | 7 +- .../experimental/suggestionsExtension.ts | 272 +++++++++++------- client/shared/src/search/query/utils.ts | 8 + .../web/src/search/home/SearchPageInput.tsx | 30 +- client/web/src/search/input/suggestions.ts | 72 ++--- 14 files changed, 799 insertions(+), 259 deletions(-) create mode 100644 client/branded/src/search-ui/input/experimental/SyntaxHighlightedSearchQuery.tsx create mode 100644 client/branded/src/search-ui/input/experimental/codemirror/history.ts create mode 100644 client/branded/src/search-ui/input/experimental/modes.ts create mode 100644 client/shared/src/search/query/utils.ts diff --git a/client/branded/src/search-ui/input/codemirror/placeholder.ts b/client/branded/src/search-ui/input/codemirror/placeholder.ts index 0195a7515fd1..d348231c93c9 100644 --- a/client/branded/src/search-ui/input/codemirror/placeholder.ts +++ b/client/branded/src/search-ui/input/codemirror/placeholder.ts @@ -2,7 +2,7 @@ * This is an adaption of the built-in CodeMirror placeholder to make it * configurable when the placeholder should be shown or not. */ -import { EditorState, Extension } from '@codemirror/state' +import { EditorState, Extension, Facet } from '@codemirror/state' import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate, WidgetType } from '@codemirror/view' class Placeholder extends WidgetType { @@ -28,33 +28,59 @@ function showWhenEmpty(state: EditorState): boolean { return state.doc.length === 0 } +interface PlaceholderConfig { + content: string + show?: (state: EditorState) => boolean +} + +export const placeholderConfig = Facet.define>({ + combine(configs) { + // Keep highest priority config + return configs.length > 0 ? { show: showWhenEmpty, ...configs[0] } : { content: '', show: showWhenEmpty } + }, + enables: facet => + ViewPlugin.fromClass( + class { + private placeholderDecoration: Decoration + public decorations: DecorationSet + + constructor(view: EditorView) { + const config = view.state.facet(facet) + this.placeholderDecoration = this.createWidget(config.content) + this.decorations = this.createDecorationSet(view.state, config) + } + + public update(update: ViewUpdate): void { + let updateDecorations = update.docChanged || update.selectionSet + + const config = update.view.state.facet(facet) + if (config !== update.startState.facet(facet)) { + this.placeholderDecoration = this.createWidget(config.content) + updateDecorations = true + } + if (updateDecorations) { + this.decorations = this.createDecorationSet(update.view.state, config) + } + } + + private createWidget(content: string): Decoration { + return Decoration.widget({ widget: new Placeholder(content), side: 1 }) + } + + private createDecorationSet(state: EditorState, config: Required): DecorationSet { + return config.show(state) + ? Decoration.set([this.placeholderDecoration.range(state.doc.length)]) + : Decoration.none + } + }, + { decorations: plugin => plugin.decorations } + ), +}) + /** * Extension that shows a placeholder when the provided condition is met. By * default it will show the placeholder when the document is empty. */ export function placeholder(content: string, show: (state: EditorState) => boolean = showWhenEmpty): Extension { - return ViewPlugin.fromClass( - class { - private placeholderDecoration: Decoration - public decorations: DecorationSet - - constructor(view: EditorView) { - this.placeholderDecoration = Decoration.widget({ widget: new Placeholder(content), side: 1 }) - this.decorations = this.createDecorationSet(view.state) - } - - public update(update: ViewUpdate): void { - if (update.docChanged || update.selectionSet) { - this.decorations = this.createDecorationSet(update.view.state) - } - } - - private createDecorationSet(state: EditorState): DecorationSet { - return show(state) - ? Decoration.set([this.placeholderDecoration.range(state.doc.length)]) - : Decoration.none - } - }, - { decorations: plugin => plugin.decorations } - ) + return placeholderConfig.of({ content, show }) } diff --git a/client/branded/src/search-ui/input/experimental/CodeMirrorQueryInputWrapper.tsx b/client/branded/src/search-ui/input/experimental/CodeMirrorQueryInputWrapper.tsx index d0c023c20a3b..30c6d497661b 100644 --- a/client/branded/src/search-ui/input/experimental/CodeMirrorQueryInputWrapper.tsx +++ b/client/branded/src/search-ui/input/experimental/CodeMirrorQueryInputWrapper.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { defaultKeymap, historyKeymap, history as codemirrorHistory } from '@codemirror/commands' import { Compartment, EditorState, Extension, Prec } from '@codemirror/state' -import { EditorView, keymap } from '@codemirror/view' +import { EditorView, keymap, drawSelection } from '@codemirror/view' import { mdiClose } from '@mdi/js' import classNames from 'classnames' import inRange from 'lodash/inRange' @@ -14,6 +14,7 @@ import { HistoryOrNavigate } from '@sourcegraph/common' import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations' import { Shortcut } from '@sourcegraph/shared/src/react-shortcuts' import { QueryChangeSource, QueryState } from '@sourcegraph/shared/src/search' +import { getTokenLength } from '@sourcegraph/shared/src/search/query/utils' import { Icon } from '@sourcegraph/wildcard' import { singleLine, placeholder as placeholderExtension } from '../codemirror' @@ -21,6 +22,7 @@ import { parseInputAsQuery, tokens } from '../codemirror/parsedQuery' import { querySyntaxHighlighting } from '../codemirror/syntax-highlighting' import { filterHighlight } from './codemirror/syntax-highlighting' +import { modeScope } from './modes' import { editorConfigFacet, Source, suggestions } from './suggestionsExtension' import styles from './CodeMirrorQueryInputWrapper.module.scss' @@ -55,8 +57,8 @@ function showWhenEmptyWithoutContext(state: EditorState): boolean { } // If there are two tokens, only show the placeholder if the second one is a - // whitespace. - if (queryTokens.length === 2 && queryTokens[1].type !== 'whitespace') { + // whitespace of length 1 + if (queryTokens.length === 2 && (queryTokens[1].type !== 'whitespace' || getTokenLength(queryTokens[1]) !== 1)) { return false } @@ -126,7 +128,14 @@ function configureExtensions({ } if (suggestionSource && suggestionsContainer) { - extensions.push(suggestions(popoverID, suggestionsContainer, suggestionSource, historyOrNavigate)) + extensions.push( + suggestions({ + id: popoverID, + parent: suggestionsContainer, + source: suggestionSource, + historyOrNavigate, + }) + ) } return extensions @@ -160,6 +169,7 @@ function createEditor( doc: queryState.query, selection: { anchor: queryState.query.length }, extensions: [ + drawSelection(), EditorView.lineWrapping, EditorView.contentAttributes.of({ role: 'combobox', @@ -170,7 +180,7 @@ function createEditor( keymap.of(historyKeymap), keymap.of(defaultKeymap), codemirrorHistory(), - Prec.low([querySyntaxHighlighting, filterHighlight]), + Prec.low([querySyntaxHighlighting, modeScope(filterHighlight, [null])]), EditorView.theme({ '&': { flex: 1, @@ -187,6 +197,9 @@ function createEditor( fontSize: 'var(--code-font-size)', color: 'var(--search-query-text-color)', }, + '.cm-line': { + paddingLeft: '0.25rem', + }, }), querySettingsCompartment.of(queryExtensions), extensionsCompartment.of(extensions), @@ -217,6 +230,8 @@ function updateValueIfNecessary(editor: EditorView | null, queryState: QueryStat } } +const empty: any[] = [] + export interface CodeMirrorQueryInputWrapperProps { queryState: QueryState onChange: (queryState: QueryState) => void @@ -226,6 +241,7 @@ export interface CodeMirrorQueryInputWrapperProps { patternType: SearchPatternType placeholder: string suggestionSource: Source + extensions?: Extension } export const CodeMirrorQueryInputWrapper: React.FunctionComponent< @@ -239,6 +255,7 @@ export const CodeMirrorQueryInputWrapper: React.FunctionComponent< patternType, placeholder, suggestionSource, + extensions: externalExtensions = empty, children, }) => { const navigate = useNavigate() @@ -255,7 +272,7 @@ export const CodeMirrorQueryInputWrapper: React.FunctionComponent< // Update extensions whenever any of these props change const extensions = useMemo( - () => + () => [ configureExtensions({ popoverID, isLightTheme, @@ -266,6 +283,8 @@ export const CodeMirrorQueryInputWrapper: React.FunctionComponent< suggestionSource, historyOrNavigate: navigate, }), + externalExtensions, + ], [ popoverID, isLightTheme, @@ -276,6 +295,7 @@ export const CodeMirrorQueryInputWrapper: React.FunctionComponent< suggestionsContainer, suggestionSource, navigate, + externalExtensions, ] ) diff --git a/client/branded/src/search-ui/input/experimental/Suggestions.module.scss b/client/branded/src/search-ui/input/experimental/Suggestions.module.scss index f6e144dfc201..f5a794d3b401 100644 --- a/client/branded/src/search-ui/input/experimental/Suggestions.module.scss +++ b/client/branded/src/search-ui/input/experimental/Suggestions.module.scss @@ -60,12 +60,15 @@ cursor: pointer; } + .label { + margin-right: 0.5rem; + } + .match { font-weight: bold; } .description { - margin-left: 0.5rem; color: var(--input-placeholder-color); } @@ -75,6 +78,7 @@ color: var(--text-muted); font-family: var(--font-family-base); display: flex; + white-space: nowrap; > [role='gridcell'] { padding: 0 0.5rem; @@ -91,7 +95,6 @@ } .filter-option { - display: flex; font-family: var(--code-font-family); font-size: 0.75rem; diff --git a/client/branded/src/search-ui/input/experimental/Suggestions.tsx b/client/branded/src/search-ui/input/experimental/Suggestions.tsx index bdd872b8e80d..1326450f78b4 100644 --- a/client/branded/src/search-ui/input/experimental/Suggestions.tsx +++ b/client/branded/src/search-ui/input/experimental/Suggestions.tsx @@ -3,13 +3,16 @@ import React, { MouseEvent, useMemo, useState, useCallback, useLayoutEffect } fr import { mdiInformationOutline } from '@mdi/js' import classnames from 'classnames' +import { isSafari } from '@sourcegraph/common' import { shortcutDisplayName } from '@sourcegraph/shared/src/keyboardShortcuts' import { Icon, useWindowSize } from '@sourcegraph/wildcard' -import { Action, Group, Option } from './suggestionsExtension' +import { Action, CustomRenderer, Group, Option } from './suggestionsExtension' import styles from './Suggestions.module.scss' +type Renderable = React.ReactElement | string | null + function getActionName(action: Action): string { switch (action.type) { case 'completion': @@ -66,7 +69,9 @@ export const Suggestions: React.FunctionComponent = ({ useLayoutEffect(() => { if (container) { - container.querySelector('[aria-selected="true"]')?.scrollIntoView(false) + // Options are not supported in Safari according to + // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView#browser_compatibility + container.querySelector('[aria-selected="true"]')?.scrollIntoView(isSafari() ? false : { block: 'nearest' }) } }, [container, focusedItem]) @@ -97,24 +102,26 @@ export const Suggestions: React.FunctionComponent = ({ aria-selected={focusedItem === option} > {option.icon && ( -
    +
    )} -
    - {option.render ? ( - option.render(option) - ) : option.matches ? ( - - ) : ( - option.label +
    +
    + {option.render ? ( + renderStringOrRenderer(option.render, option) + ) : option.matches ? ( + + ) : ( + option.label + )} +
    + {option.description && ( +
    + {option.description} +
    )}
    - {option.description && ( -
    - {option.description} -
    - )}
    {getActionName(option.action)}
    {option.alternativeAction && ( @@ -135,7 +142,7 @@ export const Suggestions: React.FunctionComponent = ({ const Footer: React.FunctionComponent<{ option: Option }> = ({ option }) => (
    - {option.info?.(option)} + {option.info && renderStringOrRenderer(option.info, option)} {!option.info && ( <> {' '} @@ -150,42 +157,83 @@ const Footer: React.FunctionComponent<{ option: Option }> = ({ option }) => ( ) const ActionInfo: React.FunctionComponent<{ action: Action; shortcut: string }> = ({ action, shortcut }) => { - const displayName = shortcutDisplayName(shortcut) - switch (action.type) { - case 'completion': - return ( - <> - Press {displayName} to add to your query. - - ) - case 'goto': - return ( - <> - Press {displayName} to go to the suggestion. - - ) - case 'command': - return ( - <> - Press {displayName} to execute the command. - - ) + let info: Renderable = action.info ? renderStringOrRenderer(action.info, action) : null + if (!info) { + switch (action.type) { + case 'completion': + info = ( + <> + add to your query + + ) + break + case 'goto': + info = ( + <> + go to the suggestion + + ) + break + case 'command': + info = ( + <> + execute the command + + ) + break + } } + + return ( + <> + Press {shortcutDisplayName(shortcut)} to {info}. + + ) +} + +function renderStringOrRenderer(renderer: CustomRenderer, obj: T): Renderable { + if (typeof renderer === 'string') { + return renderer + } + return renderer(obj) } -export const HighlightedLabel: React.FunctionComponent<{ label: string; matches: Set }> = ({ +export const HighlightedLabel: React.FunctionComponent<{ label: string; matches: Set; offset?: number }> = ({ label, matches, -}) => ( - <> - {[...label].map((char, index) => - matches.has(index) ? ( - - {char} - - ) : ( - char - ) - )} - -) + offset = 0, +}) => { + const spans: [number, number, boolean][] = [] + let currentStart = 0 + let currentEnd = 0 + let currentMatch = false + + // Includes length as upper bound to include the last character when + // creating the last span. + for (let index = 0; index <= label.length; index++) { + currentEnd = index + + const match = matches.has(index + offset) + if (currentMatch !== match || index === label.length) { + // close previous span + spans.push([currentStart, currentEnd, currentMatch]) + currentStart = index + currentMatch = match + } + } + + return ( + + {spans.map(([start, end, match]) => { + const value = label.slice(start, end) + return match ? ( + + {value} + + ) : ( + value + ) + })} + + ) +} diff --git a/client/branded/src/search-ui/input/experimental/SyntaxHighlightedSearchQuery.tsx b/client/branded/src/search-ui/input/experimental/SyntaxHighlightedSearchQuery.tsx new file mode 100644 index 000000000000..236333d57382 --- /dev/null +++ b/client/branded/src/search-ui/input/experimental/SyntaxHighlightedSearchQuery.tsx @@ -0,0 +1,46 @@ +import React, { Fragment, useMemo } from 'react' + +import classNames from 'classnames' + +import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations' +import { decorate, toDecoration } from '@sourcegraph/shared/src/search/query/decoratedToken' +import { scanSearchQuery } from '@sourcegraph/shared/src/search/query/scanner' + +import { HighlightedLabel } from './Suggestions' + +interface SyntaxHighlightedSearchQueryProps extends React.HTMLAttributes { + query: string + matches?: Set + searchPatternType?: SearchPatternType +} + +// A read-only syntax highlighted search query +export const SyntaxHighlightedSearchQuery: React.FunctionComponent< + React.PropsWithChildren +> = ({ query, searchPatternType, matches, ...otherProps }) => { + const tokens = useMemo(() => { + const tokens = searchPatternType ? scanSearchQuery(query, false, searchPatternType) : scanSearchQuery(query) + return tokens.type === 'success' + ? tokens.term.flatMap(token => + decorate(token).map(token => { + const { value, key, className } = toDecoration(query, token) + return ( + + {matches ? ( + + ) : ( + value + )} + + ) + }) + ) + : [{query}] + }, [query, matches, searchPatternType]) + + return ( + + {tokens} + + ) +} diff --git a/client/branded/src/search-ui/input/experimental/codemirror/history.ts b/client/branded/src/search-ui/input/experimental/codemirror/history.ts new file mode 100644 index 000000000000..13a29e608348 --- /dev/null +++ b/client/branded/src/search-ui/input/experimental/codemirror/history.ts @@ -0,0 +1,146 @@ +import { Extension, Prec } from '@codemirror/state' +import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view' +import { mdiClockOutline } from '@mdi/js' +import { formatDistanceToNow, parseISO } from 'date-fns' +import { Fzf, FzfOptions } from 'fzf' + +import { pluralize } from '@sourcegraph/common' +import { RecentSearch } from '@sourcegraph/shared/src/settings/temporary/recentSearches' +import { createSVGIcon } from '@sourcegraph/shared/src/util/dom' + +import { clearMode, getSelectedMode, ModeDefinition, modesFacet, setMode } from '../modes' +import { queryRenderer } from '../optionRenderer' +import { Source, suggestionSources, Option } from '../suggestionsExtension' + +const theme = EditorView.theme({ + '.sg-history-button': { + marginRight: '0.25rem', + paddingRight: '0.25rem', + borderRight: '1px solid var(--border-color-2)', + color: 'var(--icon-color)', + }, + '.sg-history-button button': { + width: '1rem', + border: '0', + backgroundColor: 'transparent', + padding: 0, + color: 'inherit', + }, + '.sg-history-button svg': { + display: 'inline-block', + width: 'var(--icon-inline-size)', + height: 'var(--icon-inline-size)', + // Setting this simplifies event handling for the history button widget + pointerEvents: 'none', + }, + '.sg-mode-History .sg-history-button': { + color: 'var(--logo-purple)', + marginRight: '0', + border: '0', + }, +}) + +function toggleHistoryMode(event: MouseEvent | KeyboardEvent, view: EditorView): void { + event.preventDefault() + const selectedMode = getSelectedMode(view.state) + if (selectedMode?.name === 'History') { + clearMode(view) + } else { + setMode(view, 'History') + } + view.focus() +} + +/** + * This ViewPlugin renders the history button at the beginning of the search + * input. + */ +const historyButton = ViewPlugin.define( + () => ({ + decorations: Decoration.set( + Decoration.widget({ + side: -1, + widget: new (class extends WidgetType { + public toDOM(view: EditorView): HTMLElement { + const container = document.createElement('span') + container.className = 'sg-history-button' + const button = document.createElement('button') + button.type = 'button' + const icon = createSVGIcon(mdiClockOutline) + + container.append(button) + button.append(icon) + button.addEventListener('click', event => toggleHistoryMode(event, view)) + return container + } + })(), + }).range(0) + ), + }), + { + decorations: plugin => plugin.decorations, + } +) + +const fzfOptions: FzfOptions = { + selector: search => search.query, + // match: extendedMatch, + // fuzzy: false, +} + +const formatTimeOptions = { + addSuffix: true, +} + +function createHistorySuggestionSource( + source: () => RecentSearch[], + submitQuery: (query: string) => void +): Source['query'] { + const applySuggestion = (option: Option): void => { + submitQuery(option.label) + } + + return state => { + const query = state.sliceDoc() + const fzf = new Fzf(source(), fzfOptions) + const results = fzf.find(query) + return { + result: [ + { + title: 'History', + options: results.map(({ item, positions }) => ({ + label: item.query, + icon: mdiClockOutline, + matches: positions, + action: { + type: 'command', + name: `${item.resultCount}${item.limitHit ? '+' : ''} ${pluralize( + 'result', + item.resultCount + )} • ${formatDistanceToNow(parseISO(item.timestamp), formatTimeOptions)}`, + apply: applySuggestion, + info: 'run the query', + }, + render: queryRenderer, + })), + }, + ], + } + } +} + +export function searchHistoryExtension(config: { + mode: ModeDefinition + source: () => RecentSearch[] + submitQuery: (query: string) => void +}): Extension { + return [ + modesFacet.of([config.mode]), + theme, + Prec.highest(historyButton), + suggestionSources.of({ + query: createHistorySuggestionSource(config.source, config.submitQuery), + mode: config.mode.name, + }), + ] +} diff --git a/client/branded/src/search-ui/input/experimental/codemirror/syntax-highlighting.ts b/client/branded/src/search-ui/input/experimental/codemirror/syntax-highlighting.ts index de3aea252957..5b97b806c2a8 100644 --- a/client/branded/src/search-ui/input/experimental/codemirror/syntax-highlighting.ts +++ b/client/branded/src/search-ui/input/experimental/codemirror/syntax-highlighting.ts @@ -16,9 +16,9 @@ import { createSVGIcon } from '@sourcegraph/shared/src/util/dom' import { decoratedTokens, queryTokens } from '../../codemirror/parsedQuery' -const validFilter = Decoration.mark({ class: 'sg-filter', inclusive: false }) -const invalidFilter = Decoration.mark({ class: 'sg-filter sg-invalid-filter', inclusive: false }) -const contextFilter = Decoration.mark({ class: 'sg-context-filter', inclusive: true }) +const validFilter = Decoration.mark({ class: 'sg-filter' }) +const invalidFilter = Decoration.mark({ class: 'sg-filter sg-invalid-filter' }) +const contextFilter = Decoration.mark({ class: 'sg-context-filter', inclusiveEnd: true }) const replaceContext = Decoration.replace({}) class ClearTokenWidget extends WidgetType { constructor(private token: Token) { @@ -121,15 +121,17 @@ export const filterHighlight = [ token.range.start, token.range.end + 1 ) // or cursor is within field - builder.add(token.range.start, token.range.end, contextFilter) if (token.value?.value && (!withinRange || !view.hasFocus)) { // hide context: field name and show remove button builder.add(token.range.start, token.field.range.end + 1, replaceContext) + builder.add(token.range.start, token.range.end, contextFilter) builder.add( token.range.end, token.range.end, - Decoration.widget({ widget: new ClearTokenWidget(token) }) + Decoration.widget({ widget: new ClearTokenWidget(token), side: -1 }) ) + } else { + builder.add(token.range.start, token.range.end, contextFilter) } } } diff --git a/client/branded/src/search-ui/input/experimental/index.ts b/client/branded/src/search-ui/input/experimental/index.ts index a75ab8a7c748..469cdc83e8c6 100644 --- a/client/branded/src/search-ui/input/experimental/index.ts +++ b/client/branded/src/search-ui/input/experimental/index.ts @@ -8,6 +8,7 @@ export type { Source, SuggestionResult, } from './suggestionsExtension' -export { getEditorConfig } from './suggestionsExtension' +export { getEditorConfig, combineResults } from './suggestionsExtension' export * from './optionRenderer' export * from './utils' +export * from './codemirror/history' diff --git a/client/branded/src/search-ui/input/experimental/modes.ts b/client/branded/src/search-ui/input/experimental/modes.ts new file mode 100644 index 000000000000..95feeb35fa13 --- /dev/null +++ b/client/branded/src/search-ui/input/experimental/modes.ts @@ -0,0 +1,199 @@ +import { + Compartment, + EditorState, + Extension, + Facet, + Prec, + StateEffect, + StateField, + Transaction, +} from '@codemirror/state' +import { Decoration, EditorView, KeyBinding, keymap, WidgetType } from '@codemirror/view' + +import { placeholderConfig } from '../codemirror/placeholder' + +export interface ModeDefinition { + name: string + keybinding?: Omit + placeholder?: string +} + +class SelectedModeState { + constructor( + public readonly selectedMode: ModeDefinition | null = null, + public readonly previousInput: string | null = null + ) {} + + public update(transaction: Transaction): SelectedModeState { + // Aliasing makes it easier to update the state + // eslint-disable-next-line @typescript-eslint/no-this-alias,unicorn/no-this-assignment + let state: SelectedModeState = this + const modes = transaction.state.facet(modesFacet) + + for (const effect of transaction.effects) { + if (effect.is(setModeEffect)) { + if (!effect.value) { + state = new SelectedModeState() + } else if (state.selectedMode?.name !== effect.value) { + const mode = modes.find(mode => mode.name === effect.value) + state = mode + ? new SelectedModeState(mode, transaction.startState.sliceDoc()) + : new SelectedModeState() + } + } + } + + if (state.selectedMode && !modes.includes(state.selectedMode)) { + // Availabel modes might have been changed, in which case we need to + // update the state. + const mode = modes.find(mode => mode.name === state.selectedMode?.name) + if (mode) { + state = new SelectedModeState(mode, state.previousInput) + } + } + + return state + } +} + +export const setModeEffect = StateEffect.define() +const selectedModeField = StateField.define({ + create() { + return new SelectedModeState() + }, + update(selectedMode, transaction) { + return selectedMode.update(transaction) + }, + provide(field) { + return [ + Prec.highest( + placeholderConfig.computeN([field], state => { + const selectedMode = state.field(field).selectedMode + if (!selectedMode?.placeholder) { + return [] + } + return [{ content: selectedMode.placeholder }] + }) + ), + EditorView.contentAttributes.compute([field], state => { + const selectedMode = state.field(field).selectedMode + return { + class: selectedMode ? `sg-mode-${selectedMode.name}` : '', + } + }), + EditorView.theme({ + '.sg-mode-marker': { + color: 'var(--logo-purple)', + paddingRight: '0.125rem', + }, + }), + EditorView.decorations.compute([field], state => { + const selectedMode = state.field(field).selectedMode + if (!selectedMode) { + return Decoration.none + } + return Decoration.set( + Decoration.widget({ + widget: new (class extends WidgetType { + public toDOM(): HTMLElement { + const marker = document.createElement('span') + marker.className = 'sg-mode-marker' + marker.textContent = selectedMode.name + ':' + return marker + } + })(), + side: -1, + }).range(0) + ) + }), + ] + }, +}) + +export const modesFacet = Facet.define({ + combine(modes) { + return modes.flat() + }, + enables(facet) { + return [ + selectedModeField, + Prec.highest([ + keymap.compute([facet], state => { + const modes = state.facet(facet) + return [ + { + key: 'Escape', + run: clearMode, + }, + ...modes + .filter(mode => mode.keybinding) + .map( + (mode): KeyBinding => ({ + ...mode.keybinding, + run: view => setMode(view, mode.name), + }) + ), + ] + }), + ]), + ] + }, +}) + +export function modeChanged({ startState, state }: Transaction): boolean { + return getSelectedMode(startState) !== getSelectedMode(state) +} + +export function getSelectedMode(state: EditorState): ModeDefinition | null { + return state.field(selectedModeField, false)?.selectedMode ?? null +} + +export function setMode(view: EditorView, name: string): boolean { + view.dispatch({ + effects: setModeEffect.of(name), + // Clear input + changes: { from: 0, to: view.state.doc.length, insert: '' }, + // It seems that setting the selection explicitly + // ensures that the cursor is rendered correctly after the widget decoration. + selection: { anchor: 0 }, + }) + return true +} + +export function clearMode(view: EditorView, restoreInput = true): boolean { + const state = view.state.field(selectedModeField, false) + if (state?.selectedMode) { + const changes = restoreInput + ? { from: 0, to: view.state.doc.length, insert: state.previousInput ?? '' } + : undefined + view.dispatch({ + effects: setModeEffect.of(null), + changes, + selection: changes ? { anchor: changes.insert.length } : undefined, + }) + return true + } + return false +} + +const isSetModeEffect = (effect: StateEffect): effect is StateEffect => effect.is(setModeEffect) + +/** + * The provided extensions are only enabled when the specified modes are active + * or, when `null` is passed, when no mode is active. + */ +export function modeScope(extension: Extension, modes: (string | null)[]): Extension { + const compartment = new Compartment() + return [ + compartment.of(extension), + EditorState.transactionExtender.of(transaction => { + const effect = transaction.effects.find(isSetModeEffect) + if (!effect) { + return null + } + return { + effects: compartment.reconfigure(modes.includes(effect.value) ? extension : []), + } + }), + ] +} diff --git a/client/branded/src/search-ui/input/experimental/optionRenderer.tsx b/client/branded/src/search-ui/input/experimental/optionRenderer.tsx index 37abc90dabbf..c774d2fb8861 100644 --- a/client/branded/src/search-ui/input/experimental/optionRenderer.tsx +++ b/client/branded/src/search-ui/input/experimental/optionRenderer.tsx @@ -1,9 +1,8 @@ import classnames from 'classnames' -import { SyntaxHighlightedSearchQuery } from '../../components' - import { HighlightedLabel } from './Suggestions' import { Option } from './suggestionsExtension' +import { SyntaxHighlightedSearchQuery } from './SyntaxHighlightedSearchQuery' import styles from './Suggestions.module.scss' @@ -23,7 +22,7 @@ const FilterValueOption: React.FunctionComponent<{ option: Option }> = ({ option return ( - {option.matches ? : option.label} + {field} : {option.matches ? : option.label} @@ -32,7 +31,7 @@ const FilterValueOption: React.FunctionComponent<{ option: Option }> = ({ option } const QueryOption: React.FunctionComponent<{ option: Option }> = ({ option }) => ( - + ) // Custom renderer for filter suggestions diff --git a/client/branded/src/search-ui/input/experimental/suggestionsExtension.ts b/client/branded/src/search-ui/input/experimental/suggestionsExtension.ts index 2a7aaabefe90..d41f3b4c3096 100644 --- a/client/branded/src/search-ui/input/experimental/suggestionsExtension.ts +++ b/client/branded/src/search-ui/input/experimental/suggestionsExtension.ts @@ -10,11 +10,12 @@ import { StateField, Transaction, } from '@codemirror/state' -import { Command as CodeMirrorCommand, EditorView, keymap, ViewPlugin, ViewUpdate } from '@codemirror/view' +import { Command as CodeMirrorCommand, EditorView, KeyBinding, keymap, ViewPlugin, ViewUpdate } from '@codemirror/view' import { createRoot, Root } from 'react-dom/client' import { compatNavigate, HistoryOrNavigate } from '@sourcegraph/common' +import { getSelectedMode, modeChanged, modesFacet, setModeEffect } from './modes' import { Suggestions } from './Suggestions' // Temporary solution to make some editor settings available to other extensions @@ -33,7 +34,10 @@ export function getEditorConfig(state: EditorState): EditorConfig { /** * A source for completion/suggestion results */ -export type Source = (state: EditorState, position: number) => SuggestionResult +export interface Source { + query: (state: EditorState, position: number, mode?: string) => SuggestionResult + mode?: string +} export interface SuggestionResult { /** @@ -50,7 +54,7 @@ export interface SuggestionResult { valid?: (state: EditorState, position: number) => boolean } -export type CustomRenderer = (option: Option) => React.ReactElement +export type CustomRenderer = ((value: T) => React.ReactElement) | string export interface Option { /** @@ -78,11 +82,11 @@ export interface Option { * If present the provided component will be used to render the label of the * option. */ - render?: CustomRenderer + render?: CustomRenderer
    -
    diff --git a/client/web/src/enterprise/executors/ExecutorsSiteAdminArea.tsx b/client/web/src/enterprise/executors/ExecutorsSiteAdminArea.tsx index 50acbd9e09fd..ad4a93d35b97 100644 --- a/client/web/src/enterprise/executors/ExecutorsSiteAdminArea.tsx +++ b/client/web/src/enterprise/executors/ExecutorsSiteAdminArea.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { Route, RouteComponentProps, Switch } from 'react-router' +import { Route, Switch } from 'react-router' import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' @@ -19,19 +19,16 @@ const GlobalExecutorSecretsListPage = lazyComponent< 'GlobalExecutorSecretsListPage' >(() => import('./secrets/ExecutorSecretsListPage'), 'GlobalExecutorSecretsListPage') -export interface ExecutorsSiteAdminAreaProps extends RouteComponentProps {} +const URL = '/site-admin/executors' /** The page area for all executors settings in site-admin. */ -export const ExecutorsSiteAdminArea: React.FunctionComponent> = ({ - match, - ...outerProps -}) => ( +export const ExecutorsSiteAdminArea: React.FC<{}> = () => ( <> - } path={match.url} exact={true} /> + } path={URL} exact={true} /> } + path={`${URL}/secrets`} + render={props => } exact={true} /> } key="hardcoded-key" /> diff --git a/client/web/src/enterprise/executors/instances/ExecutorsListPage.tsx b/client/web/src/enterprise/executors/instances/ExecutorsListPage.tsx index 01c0971012c0..baec501764c6 100644 --- a/client/web/src/enterprise/executors/instances/ExecutorsListPage.tsx +++ b/client/web/src/enterprise/executors/instances/ExecutorsListPage.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent, useCallback, useEffect } from 'react' +import React, { useCallback, useEffect } from 'react' import { useApolloClient } from '@apollo/client' import { mdiMapSearch } from '@mdi/js' @@ -43,9 +43,7 @@ export interface ExecutorsListPageProps { queryExecutors?: typeof defaultQueryExecutors } -export const ExecutorsListPage: FunctionComponent> = ({ - queryExecutors = defaultQueryExecutors, -}) => { +export const ExecutorsListPage: React.FC = ({ queryExecutors = defaultQueryExecutors }) => { useEffect(() => eventLogger.logViewEvent('ExecutorsList')) const apolloClient = useApolloClient() diff --git a/client/web/src/enterprise/site-admin/SiteAdminLsifUploadPage.tsx b/client/web/src/enterprise/site-admin/SiteAdminLsifUploadPage.tsx index 3c2df66a7dac..f891020150f8 100644 --- a/client/web/src/enterprise/site-admin/SiteAdminLsifUploadPage.tsx +++ b/client/web/src/enterprise/site-admin/SiteAdminLsifUploadPage.tsx @@ -1,6 +1,6 @@ -import { FunctionComponent, useEffect, useMemo } from 'react' +import { useEffect, useMemo } from 'react' -import { RouteComponentProps, Redirect } from 'react-router' +import { Navigate, useParams } from 'react-router-dom-v5-compat' import { catchError } from 'rxjs/operators' import { asError, ErrorLike, isErrorLike } from '@sourcegraph/common' @@ -11,16 +11,11 @@ import { eventLogger } from '../../tracking/eventLogger' import { fetchLsifUpload } from './backend' -interface Props extends RouteComponentProps<{ id: string }> {} - /** * A page displaying metadata about an LSIF upload. */ -export const SiteAdminLsifUploadPage: FunctionComponent> = ({ - match: { - params: { id }, - }, -}) => { +export const SiteAdminLsifUploadPage: React.FC<{}> = () => { + const { id = '' } = useParams<{ id: string }>() useEffect(() => eventLogger.logViewEvent('SiteAdminLsifUpload')) const uploadOrError = useObservable( @@ -37,7 +32,10 @@ export const SiteAdminLsifUploadPage: FunctionComponent ) : ( - + )}
    ) diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage.tsx index 42f7079712f6..d8d23ee25f9d 100644 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage.tsx +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage.tsx @@ -1,8 +1,7 @@ import React, { useCallback, useEffect } from 'react' import { mdiPlus } from '@mdi/js' -import * as H from 'history' -import { Redirect, RouteComponentProps } from 'react-router' +import { Navigate } from 'react-router-dom-v5-compat' import { merge, of, Observable } from 'rxjs' import { catchError, concatMapTo, map, tap } from 'rxjs/operators' @@ -28,11 +27,6 @@ interface UserCreateSubscriptionNodeProps { * The user to display in this list item. */ node: ProductSubscriptionAccountFields - - /** - * Browser history, used to redirect the user to the new subscription after one is successfully created. - */ - history: H.History } const createProductSubscription = ( @@ -85,7 +79,9 @@ const UserCreateSubscriptionNode: React.FunctionComponent} + createdSubscription.urlForSiteAdmin && ( + + )}
  • @@ -119,7 +115,7 @@ const UserCreateSubscriptionNode: React.FunctionComponent { +interface Props { authenticatedUser: AuthenticatedUser } @@ -138,7 +134,7 @@ export const SiteAdminCreateProductSubscriptionPage: React.FunctionComponent<

    Create product subscription

    - > + {...props} className="list-group list-group-flush mt-3" noun="user" diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.test.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.test.tsx index 27d1d4882b24..590c8d2c7f2b 100644 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.test.tsx +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.test.tsx @@ -1,5 +1,4 @@ import { act } from '@testing-library/react' -import * as H from 'history' import { of } from 'rxjs' import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing' @@ -12,16 +11,10 @@ jest.mock('mdi-react/ArrowLeftIcon', () => 'ArrowLeftIcon') jest.mock('mdi-react/AddIcon', () => 'AddIcon') -const history = H.createMemoryHistory() -const location = H.createLocation('/') - describe('SiteAdminProductSubscriptionPage', () => { test('renders', () => { const component = renderWithBrandedContext( of({ __typename: 'ProductSubscription', @@ -81,7 +74,7 @@ describe('SiteAdminProductSubscriptionPage', () => { }) } />, - { history } + { route: '/p' } ) act(() => undefined) expect(component.asFragment()).toMatchSnapshot() diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx index f0d0deabd9f1..b359d1f2bbdd 100644 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx @@ -1,8 +1,7 @@ import React, { useState, useMemo, useEffect, useCallback } from 'react' import { mdiArrowLeft, mdiPlus } from '@mdi/js' -import * as H from 'history' -import { RouteComponentProps } from 'react-router' +import { useNavigate, useParams } from 'react-router-dom-v5-compat' import { Observable, Subject, NEVER } from 'rxjs' import { catchError, map, mapTo, startWith, switchMap, tap, filter } from 'rxjs/operators' @@ -46,13 +45,12 @@ import { SiteAdminProductLicenseNodeProps, } from './SiteAdminProductLicenseNode' -interface Props extends RouteComponentProps<{ subscriptionUUID: string }> { +interface Props { /** For mocking in tests only. */ _queryProductSubscription?: typeof queryProductSubscription /** For mocking in tests only. */ _queryProductLicenses?: typeof queryProductLicenses - history: H.History } const LOADING = 'loading' as const @@ -61,14 +59,11 @@ const LOADING = 'loading' as const * Displays a product subscription in the site admin area. */ export const SiteAdminProductSubscriptionPage: React.FunctionComponent> = ({ - history, - location, - match: { - params: { subscriptionUUID }, - }, _queryProductSubscription = queryProductSubscription, _queryProductLicenses = queryProductLicenses, }) => { + const navigate = useNavigate() + const { subscriptionUUID = '' } = useParams<{ subscriptionUUID: string }>() useEffect(() => eventLogger.logViewEvent('SiteAdminProductSubscription'), []) const [showGenerate, setShowGenerate] = useState(false) @@ -104,14 +99,14 @@ export const SiteAdminProductSubscriptionPage: React.FunctionComponent archiveProductSubscription({ id: productSubscription.id }).pipe( mapTo(undefined), - tap(() => history.push('/site-admin/dotcom/product/subscriptions')), + tap(() => navigate('/site-admin/dotcom/product/subscriptions')), catchError(error => [asError(error)]), startWith(LOADING) ) ) ) }, - [history, productSubscription] + [navigate, productSubscription] ) ) diff --git a/client/web/src/enterprise/site-admin/productSubscription/SiteAdminProductSubscriptionPage.tsx b/client/web/src/enterprise/site-admin/productSubscription/SiteAdminProductSubscriptionPage.tsx index 57d1b2ccab09..77f09a06f9e8 100644 --- a/client/web/src/enterprise/site-admin/productSubscription/SiteAdminProductSubscriptionPage.tsx +++ b/client/web/src/enterprise/site-admin/productSubscription/SiteAdminProductSubscriptionPage.tsx @@ -1,7 +1,5 @@ import React, { useEffect } from 'react' -import { RouteComponentProps } from 'react-router' - import { PageTitle } from '../../../components/PageTitle' import { eventLogger } from '../../../tracking/eventLogger' @@ -10,15 +8,13 @@ import { ProductSubscriptionStatus } from './ProductSubscriptionStatus' /** * Displays the product subscription information from the license key in site configuration. */ -export const SiteAdminProductSubscriptionPage: React.FunctionComponent< - React.PropsWithChildren -> = props => { +export const SiteAdminProductSubscriptionPage: React.FunctionComponent = () => { useEffect(() => eventLogger.logViewEvent('SiteAdminProductSubscription'), []) return (
    - +
    ) } diff --git a/client/web/src/enterprise/site-admin/routes.tsx b/client/web/src/enterprise/site-admin/routes.tsx index bc3788d87aff..b059c030452c 100644 --- a/client/web/src/enterprise/site-admin/routes.tsx +++ b/client/web/src/enterprise/site-admin/routes.tsx @@ -1,101 +1,129 @@ -import { Redirect } from 'react-router' +import { Navigate, useLocation, useParams } from 'react-router-dom-v5-compat' import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' import { siteAdminAreaRoutes } from '../../site-admin/routes' import { SiteAdminAreaRoute } from '../../site-admin/SiteAdminArea' import { SHOW_BUSINESS_FEATURES } from '../dotcom/productSubscriptions/features' -import type { ExecutorsSiteAdminAreaProps } from '../executors/ExecutorsSiteAdminArea' + +const SiteAdminProductSubscriptionPage = lazyComponent( + () => import('./productSubscription/SiteAdminProductSubscriptionPage'), + 'SiteAdminProductSubscriptionPage' +) +const SiteAdminProductCustomersPage = lazyComponent( + () => import('./dotcom/customers/SiteAdminCustomersPage'), + 'SiteAdminProductCustomersPage' +) +const SiteAdminCreateProductSubscriptionPage = lazyComponent( + () => import('./dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage'), + 'SiteAdminCreateProductSubscriptionPage' +) +const DotComSiteAdminProductSubscriptionPage = lazyComponent( + () => import('./dotcom/productSubscriptions/SiteAdminProductSubscriptionPage'), + 'SiteAdminProductSubscriptionPage' +) +const SiteAdminProductSubscriptionsPage = lazyComponent( + () => import('./dotcom/productSubscriptions/SiteAdminProductSubscriptionsPage'), + 'SiteAdminProductSubscriptionsPage' +) +const SiteAdminProductLicensesPage = lazyComponent( + () => import('./dotcom/productSubscriptions/SiteAdminProductLicensesPage'), + 'SiteAdminProductLicensesPage' +) +const SiteAdminAuthenticationProvidersPage = lazyComponent( + () => import('./SiteAdminAuthenticationProvidersPage'), + 'SiteAdminAuthenticationProvidersPage' +) +const SiteAdminExternalAccountsPage = lazyComponent( + () => import('./SiteAdminExternalAccountsPage'), + 'SiteAdminExternalAccountsPage' +) +const BatchChangesSiteConfigSettingsArea = lazyComponent( + () => import('../batches/settings/BatchChangesSiteConfigSettingsArea'), + 'BatchChangesSiteConfigSettingsArea' +) +const BatchSpecsPage = lazyComponent(() => import('../batches/BatchSpecsPage'), 'BatchSpecsPage') +const WebhookLogPage = lazyComponent(() => import('../../site-admin/webhooks/WebhookLogPage'), 'WebhookLogPage') +const CodeIntelPreciseIndexesPage = lazyComponent( + () => import('../codeintel/indexes/pages/CodeIntelPreciseIndexesPage'), + 'CodeIntelPreciseIndexesPage' +) +const CodeIntelPreciseIndexPage = lazyComponent( + () => import('../codeintel/indexes/pages/CodeIntelPreciseIndexPage'), + 'CodeIntelPreciseIndexPage' +) +const CodeIntelConfigurationPage = lazyComponent( + () => import('../codeintel/configuration/pages/CodeIntelConfigurationPage'), + 'CodeIntelConfigurationPage' +) +const CodeIntelConfigurationPolicyPage = lazyComponent( + () => import('../codeintel/configuration/pages/CodeIntelConfigurationPolicyPage'), + 'CodeIntelConfigurationPolicyPage' +) +const CodeIntelInferenceConfigurationPage = lazyComponent( + () => import('../codeintel/configuration/pages/CodeIntelInferenceConfigurationPage'), + 'CodeIntelInferenceConfigurationPage' +) +const SiteAdminLsifUploadPage = lazyComponent(() => import('./SiteAdminLsifUploadPage'), 'SiteAdminLsifUploadPage') +const ExecutorsSiteAdminArea = lazyComponent( + () => import('../executors/ExecutorsSiteAdminArea'), + 'ExecutorsSiteAdminArea' +) export const enterpriseSiteAdminAreaRoutes: readonly SiteAdminAreaRoute[] = ( [ ...siteAdminAreaRoutes, { path: '/license', - render: lazyComponent( - () => import('./productSubscription/SiteAdminProductSubscriptionPage'), - 'SiteAdminProductSubscriptionPage' - ), - exact: true, + render: () => , }, { path: '/dotcom/customers', - render: lazyComponent( - () => import('./dotcom/customers/SiteAdminCustomersPage'), - 'SiteAdminProductCustomersPage' - ), + render: () => , condition: () => SHOW_BUSINESS_FEATURES, - exact: true, }, { path: '/dotcom/product/subscriptions/new', - render: lazyComponent( - () => import('./dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage'), - 'SiteAdminCreateProductSubscriptionPage' - ), + render: props => , condition: () => SHOW_BUSINESS_FEATURES, - exact: true, }, { path: '/dotcom/product/subscriptions/:subscriptionUUID', - render: lazyComponent( - () => import('./dotcom/productSubscriptions/SiteAdminProductSubscriptionPage'), - 'SiteAdminProductSubscriptionPage' - ), + render: () => , condition: () => SHOW_BUSINESS_FEATURES, - exact: true, }, { path: '/dotcom/product/subscriptions', - render: lazyComponent( - () => import('./dotcom/productSubscriptions/SiteAdminProductSubscriptionsPage'), - 'SiteAdminProductSubscriptionsPage' - ), + render: () => , condition: () => SHOW_BUSINESS_FEATURES, - exact: true, }, { path: '/dotcom/product/licenses', - render: lazyComponent( - () => import('./dotcom/productSubscriptions/SiteAdminProductLicensesPage'), - 'SiteAdminProductLicensesPage' - ), + render: () => , condition: () => SHOW_BUSINESS_FEATURES, - exact: true, }, { path: '/auth/providers', - render: lazyComponent( - () => import('./SiteAdminAuthenticationProvidersPage'), - 'SiteAdminAuthenticationProvidersPage' - ), - exact: true, + render: () => , }, { path: '/auth/external-accounts', - render: lazyComponent(() => import('./SiteAdminExternalAccountsPage'), 'SiteAdminExternalAccountsPage'), - exact: true, + render: () => , }, { path: '/batch-changes', - exact: true, - render: lazyComponent( - () => import('../batches/settings/BatchChangesSiteConfigSettingsArea'), - 'BatchChangesSiteConfigSettingsArea' - ), + render: () => , condition: ({ batchChangesEnabled }) => batchChangesEnabled, }, { path: '/batch-changes/specs', - exact: true, - render: lazyComponent(() => import('../batches/BatchSpecsPage'), 'BatchSpecsPage'), + render: props => , condition: ({ batchChangesEnabled, batchChangesExecutionEnabled }) => batchChangesEnabled && batchChangesExecutionEnabled, }, { path: '/batch-changes/webhook-logs', - exact: true, - render: lazyComponent(() => import('../../site-admin/webhooks/WebhookLogPage'), 'WebhookLogPage'), + render: () => , condition: ({ batchChangesEnabled, batchChangesWebhookLogsEnabled }) => batchChangesEnabled && batchChangesWebhookLogsEnabled, }, @@ -103,80 +131,71 @@ export const enterpriseSiteAdminAreaRoutes: readonly SiteAdminAreaRoute[] = ( // Code intelligence redirect { path: '/code-intelligence', - exact: false, - render: props => , + render: () => , + }, + { + path: '/code-intelligence/*', + render: () => , }, // Precise index routes { path: '/code-graph/indexes', - render: lazyComponent( - () => import('../codeintel/indexes/pages/CodeIntelPreciseIndexesPage'), - 'CodeIntelPreciseIndexesPage' - ), - exact: true, + render: props => , }, { path: '/code-graph/indexes/:id', - render: lazyComponent( - () => import('../codeintel/indexes/pages/CodeIntelPreciseIndexPage'), - 'CodeIntelPreciseIndexPage' - ), - exact: true, + render: props => , }, // Code graph configuration { path: '/code-graph/configuration', - render: lazyComponent( - () => import('../codeintel/configuration/pages/CodeIntelConfigurationPage'), - 'CodeIntelConfigurationPage' - ), - exact: true, + render: props => , }, { path: '/code-graph/configuration/:id', - render: lazyComponent( - () => import('../codeintel/configuration/pages/CodeIntelConfigurationPolicyPage'), - 'CodeIntelConfigurationPolicyPage' - ), - exact: true, + render: props => , }, { path: '/code-graph/inference-configuration', - render: lazyComponent( - () => import('../codeintel/configuration/pages/CodeIntelInferenceConfigurationPage'), - 'CodeIntelInferenceConfigurationPage' - ), - exact: true, + render: props => , }, // Legacy routes { path: '/code-graph/uploads/:id', - render: props => ( - - ), - exact: true, + render: () => , }, { path: '/lsif-uploads/:id', - render: lazyComponent(() => import('./SiteAdminLsifUploadPage'), 'SiteAdminLsifUploadPage'), - exact: true, + render: () => , }, // Executor routes { path: '/executors', - render: lazyComponent( - () => import('../executors/ExecutorsSiteAdminArea'), - 'ExecutorsSiteAdminArea' - ), + render: () => , + condition: () => Boolean(window.context?.executorsEnabled), + }, + { + path: '/executors/*', + render: () => , condition: () => Boolean(window.context?.executorsEnabled), }, ] as readonly (SiteAdminAreaRoute | undefined)[] ).filter(Boolean) as readonly SiteAdminAreaRoute[] + +function NavigateToCodeGraph(): JSX.Element { + const location = useLocation() + return +} + +function NavigateToLegacyUploadPage(): JSX.Element { + const { id = '' } = useParams<{ id: string }>() + return ( + + ) +} diff --git a/client/web/src/namespaces/routes.ts b/client/web/src/namespaces/routes.ts deleted file mode 100644 index 281fa91b88ef..000000000000 --- a/client/web/src/namespaces/routes.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' - -import { NamespaceAreaRoute } from './NamespaceArea' - -export const namespaceAreaRoutes: readonly NamespaceAreaRoute[] = [ - { - path: '/searches', - exact: true, - render: lazyComponent(() => import('../savedSearches/SavedSearchListPage'), 'SavedSearchListPage'), - }, - { - path: '/searches/add', - render: lazyComponent(() => import('../savedSearches/SavedSearchCreateForm'), 'SavedSearchCreateForm'), - }, - { - path: '/searches/:id', - render: lazyComponent(() => import('../savedSearches/SavedSearchUpdateForm'), 'SavedSearchUpdateForm'), - }, -] diff --git a/client/web/src/namespaces/routes.tsx b/client/web/src/namespaces/routes.tsx new file mode 100644 index 000000000000..8b30aac0b547 --- /dev/null +++ b/client/web/src/namespaces/routes.tsx @@ -0,0 +1,35 @@ +import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' + +import { NamespaceAreaRoute } from './NamespaceArea' + +const SavedSearchListPage = lazyComponent(() => import('../savedSearches/SavedSearchListPage'), 'SavedSearchListPage') + +const SavedSearchCreateForm = lazyComponent( + () => import('../savedSearches/SavedSearchCreateForm'), + 'SavedSearchCreateForm' +) +const SavedSearchUpdateForm = lazyComponent( + () => import('../savedSearches/SavedSearchUpdateForm'), + 'SavedSearchUpdateForm' +) + +export const namespaceAreaRoutes: readonly NamespaceAreaRoute[] = [ + { + path: '/searches', + render: props => , + // TODO: Remove once RR6 migration is finished. For now these work on both RR5 and RR6. + exact: true, + }, + { + path: '/searches/add', + render: props => , + // TODO: Remove once RR6 migration is finished. For now these work on both RR5 and RR6. + exact: true, + }, + { + path: '/searches/:id', + render: props => , + // TODO: Remove once RR6 migration is finished. For now these work on both RR5 and RR6. + exact: true, + }, +] diff --git a/client/web/src/org/settings/routes.tsx b/client/web/src/org/settings/routes.tsx index 6d3a1edea207..c7f9d3604074 100644 --- a/client/web/src/org/settings/routes.tsx +++ b/client/web/src/org/settings/routes.tsx @@ -16,6 +16,7 @@ export const orgSettingsAreaRoutes: readonly OrgSettingsAreaRoute[] = [
    diff --git a/client/web/src/settings/SettingsArea.tsx b/client/web/src/settings/SettingsArea.tsx index d290e5063da3..ec5bcb25af9e 100644 --- a/client/web/src/settings/SettingsArea.tsx +++ b/client/web/src/settings/SettingsArea.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import classNames from 'classnames' import AlertCircleIcon from 'mdi-react/AlertCircleIcon' -import { Route, RouteComponentProps, Switch } from 'react-router' +import { Route, Switch } from 'react-router' import { combineLatest, Observable, of, Subject, Subscription } from 'rxjs' import { catchError, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators' @@ -50,9 +50,10 @@ export interface SettingsAreaPageProps extends SettingsAreaPageCommonProps { onUpdate: () => void } -interface Props extends SettingsAreaPageCommonProps, RouteComponentProps<{}> { +interface Props extends SettingsAreaPageCommonProps { className?: string extraHeader?: JSX.Element + url: string } const LOADING = 'loading' as const @@ -173,7 +174,7 @@ export class SettingsArea extends React.Component { {this.props.extraHeader} } diff --git a/client/web/src/site-admin/SiteAdminArea.tsx b/client/web/src/site-admin/SiteAdminArea.tsx index f53952867d89..148ef486a90d 100644 --- a/client/web/src/site-admin/SiteAdminArea.tsx +++ b/client/web/src/site-admin/SiteAdminArea.tsx @@ -2,8 +2,7 @@ import React, { useRef } from 'react' import classNames from 'classnames' import MapSearchIcon from 'mdi-react/MapSearchIcon' -import { Route, Switch } from 'react-router' -import { useLocation } from 'react-router-dom-v5-compat' +import { useLocation, Routes, Route } from 'react-router-dom-v5-compat' import { SiteSettingFields } from '@sourcegraph/shared/src/graphql-operations' import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context' @@ -17,7 +16,7 @@ import { BatchChangesProps } from '../batches' import { ErrorBoundary } from '../components/ErrorBoundary' import { HeroPage } from '../components/HeroPage' import { Page } from '../components/Page' -import { RouteDescriptor } from '../util/contributions' +import { RouteV6Descriptor } from '../util/contributions' import { SiteAdminSidebar, SiteAdminSideBarGroups } from './SiteAdminSidebar' @@ -49,7 +48,7 @@ export interface SiteAdminAreaRouteContext overviewComponents: readonly React.ComponentType>[] } -export interface SiteAdminAreaRoute extends RouteDescriptor {} +export interface SiteAdminAreaRoute extends RouteV6Descriptor {} interface SiteAdminAreaProps extends PlatformContextProps, SettingsCascadeProps, BatchChangesProps, TelemetryProps { routes: readonly SiteAdminAreaRoute[] @@ -102,23 +101,20 @@ const AuthenticatedSiteAdminArea: React.FunctionComponent }> - + {props.routes.map( - ({ render, path, exact, condition = () => true }) => + ({ render, path, condition = () => true }) => condition(context) && ( - render({ ...context, ...routeComponentProps }) - } + path={path} + element={render(context)} /> ) )} - - + } /> +
    diff --git a/client/web/src/site-admin/SiteAdminBackgroundJobsPage.tsx b/client/web/src/site-admin/SiteAdminBackgroundJobsPage.tsx index 82ed19e01b80..231e1993a0e0 100644 --- a/client/web/src/site-admin/SiteAdminBackgroundJobsPage.tsx +++ b/client/web/src/site-admin/SiteAdminBackgroundJobsPage.tsx @@ -12,7 +12,6 @@ import { mdiShape, } from '@mdi/js' import format from 'date-fns/format' -import { RouteComponentProps } from 'react-router' import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp' import { pluralize } from '@sourcegraph/common' @@ -41,7 +40,7 @@ import { BACKGROUND_JOBS, BACKGROUND_JOBS_PAGE_POLL_INTERVAL_MS } from './backen import styles from './SiteAdminBackgroundJobsPage.module.scss' -export interface SiteAdminBackgroundJobsPageProps extends RouteComponentProps, TelemetryProps {} +export interface SiteAdminBackgroundJobsPageProps extends TelemetryProps {} export type BackgroundJob = BackgroundJobsResult['backgroundJobs']['nodes'][0] export type BackgroundRoutine = BackgroundJob['routines'][0] diff --git a/client/web/src/site-admin/SiteAdminCreateUserPage.tsx b/client/web/src/site-admin/SiteAdminCreateUserPage.tsx index 09fbeccafb0d..e42616171fa0 100644 --- a/client/web/src/site-admin/SiteAdminCreateUserPage.tsx +++ b/client/web/src/site-admin/SiteAdminCreateUserPage.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import classNames from 'classnames' -import { RouteComponentProps } from 'react-router' import { Subject, Subscription } from 'rxjs' import { catchError, mergeMap, tap } from 'rxjs/operators' @@ -35,7 +34,7 @@ interface State { /** * A page with a form to create a user account. */ -export class SiteAdminCreateUserPage extends React.Component, State> { +export class SiteAdminCreateUserPage extends React.Component<{}, State> { public state: State = { loading: false, username: '', diff --git a/client/web/src/site-admin/SiteAdminExternalServicesArea.tsx b/client/web/src/site-admin/SiteAdminExternalServicesArea.tsx index ae6e7913652e..829e30b91fe1 100644 --- a/client/web/src/site-admin/SiteAdminExternalServicesArea.tsx +++ b/client/web/src/site-admin/SiteAdminExternalServicesArea.tsx @@ -34,19 +34,14 @@ const AddExternalServicesPage = lazyComponent( 'AddExternalServicesPage' ) -interface Props - extends RouteComponentProps<{}>, - ThemeProps, - TelemetryProps, - PlatformContextProps, - SettingsCascadeProps { +interface Props extends ThemeProps, TelemetryProps, PlatformContextProps, SettingsCascadeProps { authenticatedUser: AuthenticatedUser } export const SiteAdminExternalServicesArea: React.FunctionComponent> = ({ - match, ...outerProps }) => { + const url = '/external-services' const { data, error, loading } = useQuery( SITE_EXTERNAL_SERVICE_CONFIG, {} @@ -67,7 +62,7 @@ export const SiteAdminExternalServicesArea: React.FunctionComponent ( - } exact={true} /> + } exact={true} /> ( ) => ( ) => ( , TelemetryProps { +export interface SiteAdminFeatureFlagConfigurationProps extends TelemetryProps { fetchFeatureFlags?: typeof defaultFetchFeatureFlags productVersion?: string } export const SiteAdminFeatureFlagConfigurationPage: FunctionComponent< React.PropsWithChildren -> = ({ match: { params }, fetchFeatureFlags = defaultFetchFeatureFlags, productVersion = window.context.version }) => { - const history = useHistory() +> = ({ fetchFeatureFlags = defaultFetchFeatureFlags, productVersion = window.context.version }) => { + const { name = '' } = useParams<{ name: string }>() + const navigate = useNavigate() const productGitVersion = parseProductReference(productVersion) - const isCreateFeatureFlag = params.name === 'new' + const isCreateFeatureFlag = name === 'new' // Load the initial feature flag, unless we are creating a new feature flag. const featureFlagOrError = useObservable( @@ -58,16 +59,16 @@ export const SiteAdminFeatureFlagConfigurationPage: FunctionComponent< isCreateFeatureFlag ? of(undefined) : fetchFeatureFlags().pipe( - map(flags => flags.find(flag => flag.name === params.name)), + map(flags => flags.find(flag => flag.name === name)), map(flag => { if (flag === undefined) { - throw new Error(`Could not find feature flag with name '${params.name}'.`) + throw new Error(`Could not find feature flag with name '${name}'.`) } return flag }), catchError((error): [ErrorLike] => [asError(error)]) ), - [isCreateFeatureFlag, params.name, fetchFeatureFlags] + [isCreateFeatureFlag, name, fetchFeatureFlags] ) ) @@ -127,7 +128,7 @@ export const SiteAdminFeatureFlagConfigurationPage: FunctionComponent< ...flagValue, }, }).then(() => { - history.push(`./${flagName || 'new'}`) + navigate(`/site-admin/feature-flags/configuration/${flagName || 'new'}`) }) } > @@ -167,7 +168,7 @@ export const SiteAdminFeatureFlagConfigurationPage: FunctionComponent< ...flagValue, }, }).then(() => { - history.push(`./${flagName}`) + navigate(`/site-admin/feature-flags/configuration/${flagName}`) }) } > @@ -190,7 +191,7 @@ export const SiteAdminFeatureFlagConfigurationPage: FunctionComponent< name: flagName, }, }).then(() => { - history.push('../') + navigate('/site-admin/feature-flags') }) } > @@ -236,7 +237,12 @@ export const SiteAdminFeatureFlagConfigurationPage: FunctionComponent<
    {actions} -
    diff --git a/client/web/src/site-admin/SiteAdminPingsPage.tsx b/client/web/src/site-admin/SiteAdminPingsPage.tsx index 5b134bcce7bc..16f503ec325a 100644 --- a/client/web/src/site-admin/SiteAdminPingsPage.tsx +++ b/client/web/src/site-admin/SiteAdminPingsPage.tsx @@ -6,7 +6,6 @@ import { search, searchKeymap } from '@codemirror/search' import { EditorState } from '@codemirror/state' import { EditorView, keymap } from '@codemirror/view' import { isEmpty } from 'lodash' -import { RouteComponentProps } from 'react-router-dom' import { fromFetch } from 'rxjs/fetch' import { checkOk } from '@sourcegraph/http-client' @@ -22,7 +21,7 @@ import { LoadingSpinner, H2, H3, Text, useObservable } from '@sourcegraph/wildca import { PageTitle } from '../components/PageTitle' import { eventLogger } from '../tracking/eventLogger' -interface Props extends RouteComponentProps, ThemeProps {} +interface Props extends ThemeProps {} /** * A page displaying information about telemetry pings for the site. diff --git a/client/web/src/site-admin/SiteAdminReportBugPage.tsx b/client/web/src/site-admin/SiteAdminReportBugPage.tsx index bbb531f61462..6dc0b109b3e6 100644 --- a/client/web/src/site-admin/SiteAdminReportBugPage.tsx +++ b/client/web/src/site-admin/SiteAdminReportBugPage.tsx @@ -1,7 +1,6 @@ import React, { useMemo } from 'react' import { mapValues, values } from 'lodash' -import { RouteComponentProps } from 'react-router' import { ExternalServiceKind } from '@sourcegraph/shared/src/graphql-operations' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -110,12 +109,11 @@ const allConfigSchema = { .reduce((allDefinitions, definitions) => ({ ...allDefinitions, ...definitions }), {}), } -interface Props extends RouteComponentProps, ThemeProps, TelemetryProps {} +interface Props extends ThemeProps, TelemetryProps {} export const SiteAdminReportBugPage: React.FunctionComponent> = ({ isLightTheme, telemetryService, - history, }) => { const allConfig = useObservable(useMemo(fetchAllConfigAndSettings, [])) return ( diff --git a/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx b/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx index ddfb796840f6..92aca3e9c893 100644 --- a/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx +++ b/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react' import { mdiCloudDownload, mdiCog, mdiBrain } from '@mdi/js' import { isEqual } from 'lodash' -import { RouteComponentProps } from 'react-router' +import { useLocation, useNavigate } from 'react-router-dom-v5-compat' import { logger } from '@sourcegraph/common' import { useQuery } from '@sourcegraph/http-client' @@ -109,7 +109,7 @@ const RepositoryNode: React.FunctionComponent ) -interface Props extends RouteComponentProps<{}>, TelemetryProps {} +interface Props extends TelemetryProps {} const STATUS_FILTERS: { [label: string]: FilteredConnectionFilterValue } = { All: { @@ -218,10 +218,11 @@ const FILTERS: FilteredConnectionFilter[] = [ * A page displaying the repositories on this site. */ export const SiteAdminRepositoriesPage: React.FunctionComponent> = ({ - history, - location, telemetryService, }) => { + const location = useLocation() + const navigate = useNavigate() + useEffect(() => { telemetryService.logPageView('SiteAdminRepos') }, [telemetryService]) @@ -415,14 +416,19 @@ export const SiteAdminRepositoriesPage: React.FunctionComponent(() => { const args = buildFilterArgs(filterValues) diff --git a/client/web/src/site-admin/SiteAdminSettingsPage.tsx b/client/web/src/site-admin/SiteAdminSettingsPage.tsx index 6304bcfc31dc..cad0c64ac4ff 100644 --- a/client/web/src/site-admin/SiteAdminSettingsPage.tsx +++ b/client/web/src/site-admin/SiteAdminSettingsPage.tsx @@ -1,7 +1,5 @@ import * as React from 'react' -import { RouteComponentProps } from 'react-router' - import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context' import { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -13,12 +11,7 @@ import { PageTitle } from '../components/PageTitle' import { SiteResult } from '../graphql-operations' import { SettingsArea } from '../settings/SettingsArea' -interface Props - extends RouteComponentProps<{}>, - PlatformContextProps, - SettingsCascadeProps, - ThemeProps, - TelemetryProps { +interface Props extends PlatformContextProps, SettingsCascadeProps, ThemeProps, TelemetryProps { authenticatedUser: AuthenticatedUser site: Pick } @@ -28,6 +21,7 @@ export const SiteAdminSettingsPage: React.FunctionComponent { {() => ( - + )} @@ -85,12 +79,7 @@ export const WebhookCreatePageWithError: Story = () => { {() => ( - + )} diff --git a/client/web/src/site-admin/SiteAdminWebhookCreatePage.tsx b/client/web/src/site-admin/SiteAdminWebhookCreatePage.tsx index 5720f2a648a4..0a9a2f1ad054 100644 --- a/client/web/src/site-admin/SiteAdminWebhookCreatePage.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookCreatePage.tsx @@ -1,7 +1,6 @@ import { FC, useEffect } from 'react' import { mdiCog } from '@mdi/js' -import { RouteComponentProps } from 'react-router' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' import { Container, PageHeader } from '@sourcegraph/wildcard' @@ -10,9 +9,9 @@ import { PageTitle } from '../components/PageTitle' import { WebhookCreateUpdatePage } from './WebhookCreateUpdatePage' -export interface SiteAdminWebhookCreatePageProps extends TelemetryProps, RouteComponentProps<{}> {} +export interface SiteAdminWebhookCreatePageProps extends TelemetryProps {} -export const SiteAdminWebhookCreatePage: FC = ({ telemetryService, history }) => { +export const SiteAdminWebhookCreatePage: FC = ({ telemetryService }) => { useEffect(() => { telemetryService.logPageView('SiteAdminWebhookCreatePage') }, [telemetryService]) @@ -25,7 +24,7 @@ export const SiteAdminWebhookCreatePage: FC = ( className="mb-3" headingElement="h2" /> - + ) } diff --git a/client/web/src/site-admin/SiteAdminWebhookPage.story.tsx b/client/web/src/site-admin/SiteAdminWebhookPage.story.tsx index 1783e02fff8d..9f1044044056 100644 --- a/client/web/src/site-admin/SiteAdminWebhookPage.story.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookPage.story.tsx @@ -1,6 +1,6 @@ import { DecoratorFn, Meta, Story } from '@storybook/react' import { addMinutes, formatRFC3339 } from 'date-fns' -import * as H from 'history' +import { Route, Routes } from 'react-router-dom-v5-compat' import { MATCH_ANY_PARAMETERS, WildcardMockLink } from 'wildcard-mock-link' import { getDocumentNode } from '@sourcegraph/http-client' @@ -53,7 +53,7 @@ export const SiteAdminWebhookPageStory: Story = args => { after: null, onlyErrors: false, onlyUnmatched: false, - webhookID: '1', + webhookID: '', }, }, result: { @@ -108,15 +108,15 @@ export const SiteAdminWebhookPageStory: Story = args => { ]) return ( - + {() => ( - + + } + /> + )} @@ -124,13 +124,6 @@ export const SiteAdminWebhookPageStory: Story = args => { } SiteAdminWebhookPageStory.storyName = 'Incoming webhook' -SiteAdminWebhookPageStory.args = { - match: { - params: { - id: '1', - }, - }, -} export const SiteAdminWebhookPageWithoutLogsStory: Story = args => { const buildWebhookLogsMock = new WildcardMockLink([ @@ -138,7 +131,7 @@ export const SiteAdminWebhookPageWithoutLogsStory: Story = args => { request: { query: getDocumentNode(WEBHOOK_BY_ID), variables: { - id: '1', + id: '', }, }, result: { @@ -168,7 +161,7 @@ export const SiteAdminWebhookPageWithoutLogsStory: Story = args => { request: { query: getDocumentNode(WEBHOOK_BY_ID_LOG_PAGE_HEADER), variables: { - webhookID: '1', + webhookID: '', }, }, result: { @@ -186,12 +179,7 @@ export const SiteAdminWebhookPageWithoutLogsStory: Story = args => { {() => ( - + )} @@ -199,13 +187,6 @@ export const SiteAdminWebhookPageWithoutLogsStory: Story = args => { } SiteAdminWebhookPageWithoutLogsStory.storyName = 'Incoming webhook without logs' -SiteAdminWebhookPageWithoutLogsStory.args = { - match: { - params: { - id: '1', - }, - }, -} function buildWebhookLogs(): WebhookLogFields[] { const logs: WebhookLogFields[] = [] diff --git a/client/web/src/site-admin/SiteAdminWebhookPage.tsx b/client/web/src/site-admin/SiteAdminWebhookPage.tsx index 6893b0628130..8ceaf868e332 100644 --- a/client/web/src/site-admin/SiteAdminWebhookPage.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookPage.tsx @@ -2,7 +2,7 @@ import { FC, useEffect, useState } from 'react' import { mdiCog, mdiDelete } from '@mdi/js' import { noop } from 'lodash' -import { RouteComponentProps } from 'react-router' +import { useNavigate, useParams } from 'react-router-dom-v5-compat' import { useMutation } from '@sourcegraph/http-client' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -40,16 +40,13 @@ import { WebhookLogNode } from './webhooks/WebhookLogNode' import styles from './SiteAdminWebhookPage.module.scss' -export interface WebhookPageProps extends TelemetryProps, RouteComponentProps<{ id: string }> {} +export interface WebhookPageProps extends TelemetryProps {} export const SiteAdminWebhookPage: FC = props => { - const { - match: { - params: { id }, - }, - telemetryService, - history, - } = props + const { telemetryService } = props + + const { id = '' } = useParams<{ id: string }>() + const navigate = useNavigate() const [onlyErrors, setOnlyErrors] = useState(false) const { loading, hasNextPage, fetchMore, connection, error } = useWebhookLogsConnection(id, 20, onlyErrors) @@ -62,7 +59,7 @@ export const SiteAdminWebhookPage: FC = props => { const [deleteWebhook, { error: deleteError, loading: isDeleting }] = useMutation< DeleteWebhookResult, DeleteWebhookVariables - >(DELETE_WEBHOOK, { variables: { hookID: id }, onCompleted: () => history.push('/site-admin/webhooks') }) + >(DELETE_WEBHOOK, { variables: { hookID: id }, onCompleted: () => navigate('/site-admin/webhooks') }) return ( diff --git a/client/web/src/site-admin/SiteAdminWebhookUpdatePage.story.tsx b/client/web/src/site-admin/SiteAdminWebhookUpdatePage.story.tsx index 08836b9d7c1a..1bb166063c7b 100644 --- a/client/web/src/site-admin/SiteAdminWebhookUpdatePage.story.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookUpdatePage.story.tsx @@ -1,5 +1,5 @@ import { DecoratorFn, Meta, Story } from '@storybook/react' -import * as H from 'history' +import { Route, Routes } from 'react-router-dom-v5-compat' import { WildcardMockLink } from 'wildcard-mock-link' import { getDocumentNode } from '@sourcegraph/http-client' @@ -23,8 +23,8 @@ const config: Meta = { export default config -export const WebhookUpdatePage: Story = args => ( - +export const WebhookUpdatePage: Story = () => ( + {() => ( ( ]) } > - + + } + /> + )} ) WebhookUpdatePage.storyName = 'Update webhook' -WebhookUpdatePage.args = { - match: { - params: { - id: '1', - }, - }, -} diff --git a/client/web/src/site-admin/SiteAdminWebhookUpdatePage.tsx b/client/web/src/site-admin/SiteAdminWebhookUpdatePage.tsx index 47105216f2c9..07cd821b439a 100644 --- a/client/web/src/site-admin/SiteAdminWebhookUpdatePage.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookUpdatePage.tsx @@ -1,7 +1,7 @@ import { FC, useEffect } from 'react' import { mdiCog } from '@mdi/js' -import { RouteComponentProps } from 'react-router' +import { useParams } from 'react-router-dom-v5-compat' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' import { Container, PageHeader } from '@sourcegraph/wildcard' @@ -13,19 +13,15 @@ import { PageTitle } from '../components/PageTitle' import { useWebhookQuery } from './backend' import { WebhookCreateUpdatePage } from './WebhookCreateUpdatePage' -export interface SiteAdminWebhookUpdatePageProps extends TelemetryProps, RouteComponentProps<{ id: string }> {} +export interface SiteAdminWebhookUpdatePageProps extends TelemetryProps {} -export const SiteAdminWebhookUpdatePage: FC = ({ - match: { - params: { id }, - }, - telemetryService, - history, -}) => { +export const SiteAdminWebhookUpdatePage: FC = ({ telemetryService }) => { useEffect(() => { telemetryService.logPageView('SiteAdminWebhookUpdatePage') }, [telemetryService]) + const { id = '' } = useParams<{ id: string }>() + const { loading, data } = useWebhookQuery(id) const webhook = data?.node && data.node.__typename === 'Webhook' ? data.node : undefined @@ -52,7 +48,7 @@ export const SiteAdminWebhookUpdatePage: FC = ( className="mb-3" headingElement="h2" /> - + )} diff --git a/client/web/src/site-admin/SiteAdminWebhooksPage.story.tsx b/client/web/src/site-admin/SiteAdminWebhooksPage.story.tsx index 6456289e6c7a..c2ccd669164f 100644 --- a/client/web/src/site-admin/SiteAdminWebhooksPage.story.tsx +++ b/client/web/src/site-admin/SiteAdminWebhooksPage.story.tsx @@ -1,5 +1,4 @@ import { DecoratorFn, Meta, Story } from '@storybook/react' -import * as H from 'history' import { MATCH_ANY_PARAMETERS, WildcardMockLink } from 'wildcard-mock-link' import { getDocumentNode } from '@sourcegraph/http-client' @@ -66,12 +65,7 @@ export const NoWebhooksFound: Story = () => ( ]) } > - + )} @@ -175,12 +169,7 @@ export const FiveWebhooksFound: Story = () => ( ]) } > - + )} diff --git a/client/web/src/site-admin/SiteAdminWebhooksPage.tsx b/client/web/src/site-admin/SiteAdminWebhooksPage.tsx index 15f1d05816dc..4b15c4c3db8b 100644 --- a/client/web/src/site-admin/SiteAdminWebhooksPage.tsx +++ b/client/web/src/site-admin/SiteAdminWebhooksPage.tsx @@ -1,7 +1,6 @@ import React, { useEffect } from 'react' import { mdiCog, mdiMapSearch, mdiPlus } from '@mdi/js' -import { RouteComponentProps } from 'react-router' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' import { ButtonLink, Container, H5, Icon, PageHeader } from '@sourcegraph/wildcard' @@ -23,7 +22,7 @@ import { PerformanceGauge } from './webhooks/PerformanceGauge' import styles from './SiteAdminWebhooksPage.module.scss' -interface Props extends RouteComponentProps<{}>, TelemetryProps {} +interface Props extends TelemetryProps {} export const SiteAdminWebhooksPage: React.FunctionComponent> = ({ telemetryService, diff --git a/client/web/src/site-admin/UserManagement/index.tsx b/client/web/src/site-admin/UserManagement/index.tsx index 22272f5ab638..0d37e52abe7f 100644 --- a/client/web/src/site-admin/UserManagement/index.tsx +++ b/client/web/src/site-admin/UserManagement/index.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useMemo } from 'react' import { mdiAccount, mdiPlus, mdiDownload } from '@mdi/js' -import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { H1, Card, Text, Icon, Button, Link, Alert, LoadingSpinner, AnchorLink } from '@sourcegraph/wildcard' @@ -15,7 +14,7 @@ import { USERS_MANAGEMENT_SUMMARY } from './queries' import styles from './index.module.scss' -export const UsersManagement: React.FunctionComponent> = () => { +export const UsersManagement: React.FunctionComponent = () => { useEffect(() => { eventLogger.logPageView('UsersManagement') }, []) diff --git a/client/web/src/site-admin/WebhookCreateUpdatePage.tsx b/client/web/src/site-admin/WebhookCreateUpdatePage.tsx index fc39d583a25f..fc922ef762a6 100644 --- a/client/web/src/site-admin/WebhookCreateUpdatePage.tsx +++ b/client/web/src/site-admin/WebhookCreateUpdatePage.tsx @@ -3,7 +3,7 @@ import React, { FC, useCallback, useMemo, useState } from 'react' import classNames from 'classnames' import { parse as parseJSONC } from 'jsonc-parser' import { noop } from 'lodash' -import { RouteComponentProps } from 'react-router' +import { useNavigate } from 'react-router-dom-v5-compat' import { useMutation, useQuery } from '@sourcegraph/http-client' import { Alert, Button, ButtonLink, H2, Input, Select, ErrorAlert, Form } from '@sourcegraph/wildcard' @@ -27,7 +27,7 @@ import { CREATE_WEBHOOK_QUERY, UPDATE_WEBHOOK_QUERY } from './backend' import styles from './WebhookCreateUpdatePage.module.scss' -interface WebhookCreateUpdatePageProps extends Pick { +interface WebhookCreateUpdatePageProps { // existingWebhook is present when this page is used as an update page. existingWebhook?: WebhookFields } @@ -39,7 +39,8 @@ export interface Webhook { secret: string | null } -export const WebhookCreateUpdatePage: FC = ({ history, existingWebhook }) => { +export const WebhookCreateUpdatePage: FC = ({ existingWebhook }) => { + const navigate = useNavigate() const update = existingWebhook !== undefined const initialWebhook = update ? { @@ -133,14 +134,14 @@ export const WebhookCreateUpdatePage: FC = ({ hist const [createWebhook, { error: createWebhookError, loading: creationLoading }] = useMutation< CreateWebhookResult, CreateWebhookVariables - >(CREATE_WEBHOOK_QUERY, { onCompleted: data => history.push(`/site-admin/webhooks/${data.createWebhook.id}`) }) + >(CREATE_WEBHOOK_QUERY, { onCompleted: data => navigate(`/site-admin/webhooks/${data.createWebhook.id}`) }) const [updateWebhook, { error: updateWebhookError, loading: updateLoading }] = useMutation< UpdateWebhookResult, UpdateWebhookVariables >(UPDATE_WEBHOOK_QUERY, { variables: buildUpdateWebhookVariables(webhook, existingWebhook?.id), - onCompleted: data => history.push(`/site-admin/webhooks/${data.updateWebhook.id}`), + onCompleted: data => navigate(`/site-admin/webhooks/${data.updateWebhook.id}`), }) return ( diff --git a/client/web/src/site-admin/analytics/AnalyticsBatchChangesPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsBatchChangesPage/index.tsx index 054b74993fab..721568f79df2 100644 --- a/client/web/src/site-admin/analytics/AnalyticsBatchChangesPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsBatchChangesPage/index.tsx @@ -1,7 +1,6 @@ import React, { useMemo, useEffect } from 'react' import { startCase } from 'lodash' -import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { Card, LoadingSpinner, H2, Text, LineChart, Series } from '@sourcegraph/wildcard' @@ -20,7 +19,7 @@ import { BATCHCHANGES_STATISTICS } from './queries' export const DEFAULT_MINS_SAVED_PER_CHANGESET = 15 -export const AnalyticsBatchChangesPage: React.FunctionComponent> = () => { +export const AnalyticsBatchChangesPage: React.FunctionComponent = () => { const { dateRange, grouping } = useChartFilters({ name: 'BatchChanges' }) const { data, error, loading } = useQuery( BATCHCHANGES_STATISTICS, diff --git a/client/web/src/site-admin/analytics/AnalyticsCodeInsightsPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsCodeInsightsPage/index.tsx index 6054a4eb60e7..5ca0e2e46ca7 100644 --- a/client/web/src/site-admin/analytics/AnalyticsCodeInsightsPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsCodeInsightsPage/index.tsx @@ -1,7 +1,6 @@ import React, { useMemo, useEffect } from 'react' import { startCase } from 'lodash' -import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { Card, LoadingSpinner, Text, LineChart, Series, H2 } from '@sourcegraph/wildcard' @@ -37,7 +36,7 @@ export const calculateMinutesSaved = (data: typeof MinutesSaved): number => data.LanguageSeries * MinutesSaved.LanguageSeries + data.ComputeSeries * MinutesSaved.ComputeSeries -export const AnalyticsCodeInsightsPage: React.FunctionComponent = () => { +export const AnalyticsCodeInsightsPage: React.FunctionComponent = () => { const { dateRange, aggregation, grouping } = useChartFilters({ name: 'Insights', aggregation: 'count' }) const { data, error, loading } = useQuery( INSIGHTS_STATISTICS, diff --git a/client/web/src/site-admin/analytics/AnalyticsCodeIntelPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsCodeIntelPage/index.tsx index 735ebdf659df..0b307d64051d 100644 --- a/client/web/src/site-admin/analytics/AnalyticsCodeIntelPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsCodeIntelPage/index.tsx @@ -2,7 +2,6 @@ import React, { useMemo, useEffect, useState } from 'react' import classNames from 'classnames' import { groupBy, sortBy, startCase, sumBy } from 'lodash' -import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { @@ -35,7 +34,7 @@ import { CODEINTEL_STATISTICS } from './queries' import styles from './index.module.scss' -export const AnalyticsCodeIntelPage: React.FunctionComponent> = () => { +export const AnalyticsCodeIntelPage: React.FC = () => { const { dateRange, aggregation, grouping } = useChartFilters({ name: 'CodeIntel' }) const { data, error, loading } = useQuery( CODEINTEL_STATISTICS, diff --git a/client/web/src/site-admin/analytics/AnalyticsExtensionsPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsExtensionsPage/index.tsx index b07a46cfb929..1e4936346438 100644 --- a/client/web/src/site-admin/analytics/AnalyticsExtensionsPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsExtensionsPage/index.tsx @@ -2,7 +2,6 @@ import React, { useMemo, useEffect } from 'react' import classNames from 'classnames' import { startCase } from 'lodash' -import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { Card, H2, Text, LoadingSpinner, AnchorLink, H4, LineChart, Series } from '@sourcegraph/wildcard' @@ -22,7 +21,7 @@ import { EXTENSIONS_STATISTICS } from './queries' import styles from './index.module.scss' -export const AnalyticsExtensionsPage: React.FunctionComponent> = () => { +export const AnalyticsExtensionsPage: React.FunctionComponent = () => { const { dateRange, aggregation, grouping } = useChartFilters({ name: 'Extensions' }) const { data, error, loading } = useQuery( EXTENSIONS_STATISTICS, diff --git a/client/web/src/site-admin/analytics/AnalyticsNotebooksPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsNotebooksPage/index.tsx index d31a902665ee..824fbd287310 100644 --- a/client/web/src/site-admin/analytics/AnalyticsNotebooksPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsNotebooksPage/index.tsx @@ -2,7 +2,6 @@ import React, { useMemo, useEffect } from 'react' import classNames from 'classnames' import { startCase } from 'lodash' -import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { Card, LoadingSpinner, H2, Text, H4, AnchorLink, LineChart, Series } from '@sourcegraph/wildcard' @@ -22,7 +21,7 @@ import { NOTEBOOKS_STATISTICS } from './queries' import styles from './index.module.scss' -export const AnalyticsNotebooksPage: React.FunctionComponent> = () => { +export const AnalyticsNotebooksPage: React.FunctionComponent = () => { const { dateRange, aggregation, grouping } = useChartFilters({ name: 'Notebooks' }) const { data, error, loading } = useQuery( NOTEBOOKS_STATISTICS, diff --git a/client/web/src/site-admin/analytics/AnalyticsOverviewPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsOverviewPage/index.tsx index 3ac8c7e1f715..b47f0ca5717d 100644 --- a/client/web/src/site-admin/analytics/AnalyticsOverviewPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsOverviewPage/index.tsx @@ -3,7 +3,6 @@ import React, { useEffect } from 'react' import { mdiAccount, mdiSourceRepository, mdiCommentOutline } from '@mdi/js' import classNames from 'classnames' import format from 'date-fns/format' -import * as H from 'history' import { useQuery } from '@sourcegraph/http-client' import { Card, H2, Text, LoadingSpinner, AnchorLink } from '@sourcegraph/wildcard' @@ -23,11 +22,9 @@ import { Sidebar } from './Sidebar' import styles from './index.module.scss' -interface IProps { - history: H.History -} +interface Props {} -export const AnalyticsOverviewPage: React.FunctionComponent = ({ history }) => { +export const AnalyticsOverviewPage: React.FunctionComponent = () => { const { dateRange } = useChartFilters({ name: 'Overview' }) const { data, error, loading } = useQuery( OVERVIEW_STATISTICS, diff --git a/client/web/src/site-admin/analytics/AnalyticsSearchPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsSearchPage/index.tsx index 951565492815..80155bcc19b0 100644 --- a/client/web/src/site-admin/analytics/AnalyticsSearchPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsSearchPage/index.tsx @@ -2,7 +2,6 @@ import React, { useMemo, useEffect } from 'react' import classNames from 'classnames' import { startCase } from 'lodash' -import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { Card, H2, Text, LoadingSpinner, AnchorLink, H4, LineChart, Series } from '@sourcegraph/wildcard' @@ -22,7 +21,7 @@ import { SEARCH_STATISTICS } from './queries' import styles from './index.module.scss' -export const AnalyticsSearchPage: React.FunctionComponent> = () => { +export const AnalyticsSearchPage: React.FC = () => { const { dateRange, aggregation, grouping } = useChartFilters({ name: 'Search' }) const { data, error, loading } = useQuery(SEARCH_STATISTICS, { variables: { diff --git a/client/web/src/site-admin/analytics/AnalyticsUsersPage/AnalyticsUsersPage.story.tsx b/client/web/src/site-admin/analytics/AnalyticsUsersPage/AnalyticsUsersPage.story.tsx index af34354fcb64..df88dc0414c5 100644 --- a/client/web/src/site-admin/analytics/AnalyticsUsersPage/AnalyticsUsersPage.story.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsUsersPage/AnalyticsUsersPage.story.tsx @@ -692,6 +692,6 @@ const USER_ANALYTICS_QUERY_MOCK: MockedResponse = { export const AnalyticsUsersPageExample: Story = () => ( - + ) diff --git a/client/web/src/site-admin/analytics/AnalyticsUsersPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsUsersPage/index.tsx index 453eb4d40301..90572c6eff90 100644 --- a/client/web/src/site-admin/analytics/AnalyticsUsersPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsUsersPage/index.tsx @@ -2,7 +2,6 @@ import { useState, useMemo, useEffect, FC } from 'react' import classNames from 'classnames' import { startCase } from 'lodash' -import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { Card, LoadingSpinner, useMatchMedia, Text, LineChart, BarChart, Series } from '@sourcegraph/wildcard' @@ -21,7 +20,7 @@ import { USERS_STATISTICS } from './queries' import styles from './AnalyticsUsersPage.module.scss' -export const AnalyticsUsersPage: FC = () => { +export const AnalyticsUsersPage: FC = () => { const { dateRange, aggregation, grouping } = useChartFilters({ name: 'Users', aggregation: 'uniqueUsers' }) const { data, error, loading } = useQuery(USERS_STATISTICS, { variables: { diff --git a/client/web/src/site-admin/outbound-webhooks/CreatePage.story.tsx b/client/web/src/site-admin/outbound-webhooks/CreatePage.story.tsx index 0db65a2c1533..8d268447a476 100644 --- a/client/web/src/site-admin/outbound-webhooks/CreatePage.story.tsx +++ b/client/web/src/site-admin/outbound-webhooks/CreatePage.story.tsx @@ -1,5 +1,4 @@ import { DecoratorFn, Meta, Story } from '@storybook/react' -import * as H from 'history' import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService' import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo' @@ -22,12 +21,7 @@ export const Page: Story = () => ( {() => ( - + )} diff --git a/client/web/src/site-admin/outbound-webhooks/CreatePage.tsx b/client/web/src/site-admin/outbound-webhooks/CreatePage.tsx index d79b167c6475..47c896feb505 100644 --- a/client/web/src/site-admin/outbound-webhooks/CreatePage.tsx +++ b/client/web/src/site-admin/outbound-webhooks/CreatePage.tsx @@ -2,7 +2,7 @@ import { FC, useEffect, useState } from 'react' import { mdiCog } from '@mdi/js' import { noop } from 'lodash' -import { RouteComponentProps } from 'react-router' +import { useNavigate } from 'react-router-dom-v5-compat' import { useMutation } from '@sourcegraph/http-client' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -16,9 +16,10 @@ import { CREATE_OUTBOUND_WEBHOOK } from './backend' import { EventTypes } from './create-edit/EventTypes' import { SubmitButton } from './create-edit/SubmitButton' -export interface CreatePageProps extends TelemetryProps, RouteComponentProps<{}> {} +export interface CreatePageProps extends TelemetryProps {} -export const CreatePage: FC = ({ telemetryService, history }) => { +export const CreatePage: FC = ({ telemetryService }) => { + const navigate = useNavigate() useEffect(() => { telemetryService.logPageView('OutboundWebhooksCreatePage') }, [telemetryService]) @@ -40,7 +41,7 @@ export const CreatePage: FC = ({ telemetryService, history }) = url, }, }, - onCompleted: () => history.push('/site-admin/outbound-webhooks'), + onCompleted: () => navigate('/site-admin/outbound-webhooks'), }) return ( diff --git a/client/web/src/site-admin/outbound-webhooks/EditPage.story.tsx b/client/web/src/site-admin/outbound-webhooks/EditPage.story.tsx index ea72282d40fd..84a1f6f9bdaf 100644 --- a/client/web/src/site-admin/outbound-webhooks/EditPage.story.tsx +++ b/client/web/src/site-admin/outbound-webhooks/EditPage.story.tsx @@ -1,5 +1,4 @@ import { DecoratorFn, Meta, Story } from '@storybook/react' -import * as H from 'history' import { WildcardMockLink } from 'wildcard-mock-link' import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -23,14 +22,9 @@ export const Page: Story = () => ( {() => ( - + )} diff --git a/client/web/src/site-admin/outbound-webhooks/EditPage.tsx b/client/web/src/site-admin/outbound-webhooks/EditPage.tsx index d69a8e77a230..2b25d09fb90c 100644 --- a/client/web/src/site-admin/outbound-webhooks/EditPage.tsx +++ b/client/web/src/site-admin/outbound-webhooks/EditPage.tsx @@ -2,7 +2,7 @@ import { FC, useCallback, useEffect, useState } from 'react' import { mdiCog } from '@mdi/js' import { noop } from 'lodash' -import { RouteComponentProps } from 'react-router' +import { useNavigate, useParams } from 'react-router-dom-v5-compat' import { useMutation, useQuery } from '@sourcegraph/http-client' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -23,10 +23,11 @@ import { SubmitButton } from './create-edit/SubmitButton' import { DeleteButton } from './delete/DeleteButton' import { Logs } from './logs/Logs' -export interface EditPageProps extends TelemetryProps, RouteComponentProps<{ id: string }> {} +export interface EditPageProps extends TelemetryProps {} -export const EditPage: FC = ({ history, match, telemetryService }) => { - const { id } = match.params +export const EditPage: FC = ({ telemetryService }) => { + const navigate = useNavigate() + const { id = '' } = useParams<{ id: string }>() useEffect(() => { telemetryService.logPageView('OutboundWebhooksEditPage') @@ -39,8 +40,8 @@ export const EditPage: FC = ({ history, match, telemetryService } const webhookURL = data?.node?.__typename === 'OutboundWebhook' ? data.node.url : undefined const onDeleted = useCallback(() => { - history.push('/site-admin/outbound-webhooks') - }, [history]) + navigate('/site-admin/outbound-webhooks') + }, [navigate]) if (error) { return ( diff --git a/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.story.tsx b/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.story.tsx index b996a65b04e8..f54d78913cb4 100644 --- a/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.story.tsx +++ b/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.story.tsx @@ -1,5 +1,4 @@ import { DecoratorFn, Meta, Story } from '@storybook/react' -import * as H from 'history' import { WildcardMockLink } from 'wildcard-mock-link' import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -23,12 +22,7 @@ export const Empty: Story = () => ( {() => ( - + )} @@ -40,12 +34,7 @@ export const NotEmpty: Story = () => ( {() => ( - + )} diff --git a/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.tsx b/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.tsx index 506131d0e7af..584987c5f1ba 100644 --- a/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.tsx +++ b/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.tsx @@ -1,7 +1,6 @@ import { FC, useEffect } from 'react' import { mdiAlertCircle, mdiCog, mdiMapSearch, mdiPencil, mdiPlus } from '@mdi/js' -import { RouteComponentProps } from 'react-router' import { pluralize } from '@sourcegraph/common' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -24,7 +23,7 @@ import { DeleteButton } from './delete/DeleteButton' import styles from './OutboundWebhooksPage.module.scss' -export interface OutboundWebhooksPageProps extends TelemetryProps, RouteComponentProps<{}> {} +export interface OutboundWebhooksPageProps extends TelemetryProps {} export const OutboundWebhooksPage: FC = ({ telemetryService }) => { useEffect(() => { diff --git a/client/web/src/site-admin/routes.tsx b/client/web/src/site-admin/routes.tsx index 2c8e901b9ae5..623c40d184f7 100644 --- a/client/web/src/site-admin/routes.tsx +++ b/client/web/src/site-admin/routes.tsx @@ -2,172 +2,226 @@ import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' import { SiteAdminAreaRoute } from './SiteAdminArea' +const AnalyticsOverviewPage = lazyComponent(() => import('./analytics/AnalyticsOverviewPage'), 'AnalyticsOverviewPage') +const AnalyticsSearchPage = lazyComponent(() => import('./analytics/AnalyticsSearchPage'), 'AnalyticsSearchPage') +const AnalyticsCodeIntelPage = lazyComponent( + () => import('./analytics/AnalyticsCodeIntelPage'), + 'AnalyticsCodeIntelPage' +) +const AnalyticsExtensionsPage = lazyComponent( + () => import('./analytics/AnalyticsExtensionsPage'), + 'AnalyticsExtensionsPage' +) +const AnalyticsUsersPage = lazyComponent(() => import('./analytics/AnalyticsUsersPage'), 'AnalyticsUsersPage') +const AnalyticsCodeInsightsPage = lazyComponent( + () => import('./analytics/AnalyticsCodeInsightsPage'), + 'AnalyticsCodeInsightsPage' +) +const AnalyticsBatchChangesPage = lazyComponent( + () => import('./analytics/AnalyticsBatchChangesPage'), + 'AnalyticsBatchChangesPage' +) +const AnalyticsNotebooksPage = lazyComponent( + () => import('./analytics/AnalyticsNotebooksPage'), + 'AnalyticsNotebooksPage' +) +const SiteAdminConfigurationPage = lazyComponent( + () => import('./SiteAdminConfigurationPage'), + 'SiteAdminConfigurationPage' +) +const SiteAdminSettingsPage = lazyComponent(() => import('./SiteAdminSettingsPage'), 'SiteAdminSettingsPage') +const SiteAdminExternalServicesArea = lazyComponent( + () => import('./SiteAdminExternalServicesArea'), + 'SiteAdminExternalServicesArea' +) +const SiteAdminRepositoriesPage = lazyComponent( + () => import('./SiteAdminRepositoriesPage'), + 'SiteAdminRepositoriesPage' +) +const SiteAdminOrgsPage = lazyComponent(() => import('./SiteAdminOrgsPage'), 'SiteAdminOrgsPage') +const UsersManagement = lazyComponent(() => import('./UserManagement'), 'UsersManagement') +const SiteAdminCreateUserPage = lazyComponent(() => import('./SiteAdminCreateUserPage'), 'SiteAdminCreateUserPage') +const SiteAdminTokensPage = lazyComponent(() => import('./SiteAdminTokensPage'), 'SiteAdminTokensPage') +const SiteAdminUpdatesPage = lazyComponent(() => import('./SiteAdminUpdatesPage'), 'SiteAdminUpdatesPage') +const SiteAdminPingsPage = lazyComponent(() => import('./SiteAdminPingsPage'), 'SiteAdminPingsPage') +const SiteAdminReportBugPage = lazyComponent(() => import('./SiteAdminReportBugPage'), 'SiteAdminReportBugPage') +const SiteAdminSurveyResponsesPage = lazyComponent( + () => import('./SiteAdminSurveyResponsesPage'), + 'SiteAdminSurveyResponsesPage' +) +const SiteAdminMigrationsPage = lazyComponent(() => import('./SiteAdminMigrationsPage'), 'SiteAdminMigrationsPage') +const SiteAdminOutboundRequestsPage = lazyComponent( + () => import('./SiteAdminOutboundRequestsPage'), + 'SiteAdminOutboundRequestsPage' +) +const SiteAdminBackgroundJobsPage = lazyComponent( + () => import('./SiteAdminBackgroundJobsPage'), + 'SiteAdminBackgroundJobsPage' +) +const SiteAdminFeatureFlagsPage = lazyComponent( + () => import('./SiteAdminFeatureFlagsPage'), + 'SiteAdminFeatureFlagsPage' +) +const SiteAdminFeatureFlagConfigurationPage = lazyComponent( + () => import('./SiteAdminFeatureFlagConfigurationPage'), + 'SiteAdminFeatureFlagConfigurationPage' +) +const OutboundWebhooksPage = lazyComponent( + () => import('./outbound-webhooks/OutboundWebhooksPage'), + 'OutboundWebhooksPage' +) +const CreatePage = lazyComponent(() => import('./outbound-webhooks/CreatePage'), 'CreatePage') +const EditPage = lazyComponent(() => import('./outbound-webhooks/EditPage'), 'EditPage') +const SiteAdminWebhooksPage = lazyComponent(() => import('./SiteAdminWebhooksPage'), 'SiteAdminWebhooksPage') +const SiteAdminWebhookCreatePage = lazyComponent( + () => import('./SiteAdminWebhookCreatePage'), + 'SiteAdminWebhookCreatePage' +) +const SiteAdminWebhookPage = lazyComponent(() => import('./SiteAdminWebhookPage'), 'SiteAdminWebhookPage') +const SiteAdminSlowRequestsPage = lazyComponent( + () => import('./SiteAdminSlowRequestsPage'), + 'SiteAdminSlowRequestsPage' +) +const SiteAdminWebhookUpdatePage = lazyComponent( + () => import('./SiteAdminWebhookUpdatePage'), + 'SiteAdminWebhookUpdatePage' +) + export const siteAdminAreaRoutes: readonly SiteAdminAreaRoute[] = [ { path: '/', - render: lazyComponent(() => import('./analytics/AnalyticsOverviewPage'), 'AnalyticsOverviewPage'), - exact: true, + render: () => , }, { path: '/analytics/search', - render: lazyComponent(() => import('./analytics/AnalyticsSearchPage'), 'AnalyticsSearchPage'), - exact: true, + render: () => , }, { path: '/analytics/code-intel', - render: lazyComponent(() => import('./analytics/AnalyticsCodeIntelPage'), 'AnalyticsCodeIntelPage'), - exact: true, + render: () => , }, { path: '/analytics/extensions', - render: lazyComponent(() => import('./analytics/AnalyticsExtensionsPage'), 'AnalyticsExtensionsPage'), - exact: true, + render: () => , }, { path: '/analytics/users', - render: lazyComponent(() => import('./analytics/AnalyticsUsersPage'), 'AnalyticsUsersPage'), - exact: true, + render: () => , }, { path: '/analytics/code-insights', - render: lazyComponent(() => import('./analytics/AnalyticsCodeInsightsPage'), 'AnalyticsCodeInsightsPage'), - exact: true, + render: () => , }, { path: '/analytics/batch-changes', - render: lazyComponent(() => import('./analytics/AnalyticsBatchChangesPage'), 'AnalyticsBatchChangesPage'), - exact: true, + render: () => , }, { path: '/analytics/notebooks', - render: lazyComponent(() => import('./analytics/AnalyticsNotebooksPage'), 'AnalyticsNotebooksPage'), - exact: true, + render: () => , }, { path: '/configuration', - exact: true, - render: lazyComponent(() => import('./SiteAdminConfigurationPage'), 'SiteAdminConfigurationPage'), + render: props => , }, { path: '/global-settings', - exact: true, - render: lazyComponent(() => import('./SiteAdminSettingsPage'), 'SiteAdminSettingsPage'), + render: props => , }, { path: '/external-services', - render: lazyComponent(() => import('./SiteAdminExternalServicesArea'), 'SiteAdminExternalServicesArea'), + render: props => , + }, + { + path: '/external-services/*', + render: props => , }, { path: '/repositories', - render: lazyComponent(() => import('./SiteAdminRepositoriesPage'), 'SiteAdminRepositoriesPage'), - exact: true, + render: props => , }, { path: '/organizations', - render: lazyComponent(() => import('./SiteAdminOrgsPage'), 'SiteAdminOrgsPage'), - exact: true, + render: props => , }, { path: '/users', - exact: true, - render: lazyComponent(() => import('./UserManagement'), 'UsersManagement'), + render: () => , }, { path: '/users/new', - render: lazyComponent(() => import('./SiteAdminCreateUserPage'), 'SiteAdminCreateUserPage'), - exact: true, + render: () => , }, { path: '/tokens', - exact: true, - render: lazyComponent(() => import('./SiteAdminTokensPage'), 'SiteAdminTokensPage'), + render: props => , }, { path: '/updates', - render: lazyComponent(() => import('./SiteAdminUpdatesPage'), 'SiteAdminUpdatesPage'), - exact: true, + render: props => , }, { path: '/pings', - render: lazyComponent(() => import('./SiteAdminPingsPage'), 'SiteAdminPingsPage'), - exact: true, + render: props => , }, { path: '/report-bug', - exact: true, - render: lazyComponent(() => import('./SiteAdminReportBugPage'), 'SiteAdminReportBugPage'), + render: props => , }, { path: '/surveys', - exact: true, - render: lazyComponent(() => import('./SiteAdminSurveyResponsesPage'), 'SiteAdminSurveyResponsesPage'), + render: props => , }, { path: '/migrations', - exact: true, - render: lazyComponent(() => import('./SiteAdminMigrationsPage'), 'SiteAdminMigrationsPage'), + render: props => , }, { path: '/outbound-requests', - exact: true, - render: lazyComponent(() => import('./SiteAdminOutboundRequestsPage'), 'SiteAdminOutboundRequestsPage'), + render: props => , }, { path: '/background-jobs', - exact: true, - render: lazyComponent(() => import('./SiteAdminBackgroundJobsPage'), 'SiteAdminBackgroundJobsPage'), + render: props => , }, { path: '/feature-flags', - exact: true, - render: lazyComponent(() => import('./SiteAdminFeatureFlagsPage'), 'SiteAdminFeatureFlagsPage'), + render: props => , }, { path: '/feature-flags/configuration/:name', - exact: true, - render: lazyComponent( - () => import('./SiteAdminFeatureFlagConfigurationPage'), - 'SiteAdminFeatureFlagConfigurationPage' - ), + render: props => , }, { path: '/outbound-webhooks', - exact: true, - render: lazyComponent(() => import('./outbound-webhooks/OutboundWebhooksPage'), 'OutboundWebhooksPage'), + render: props => , }, { path: '/outbound-webhooks/create', - exact: true, - render: lazyComponent(() => import('./outbound-webhooks/CreatePage'), 'CreatePage'), + render: props => , }, { path: '/outbound-webhooks/:id', - exact: true, - render: lazyComponent(() => import('./outbound-webhooks/EditPage'), 'EditPage'), + render: props => , }, { path: '/webhooks', - exact: true, - render: lazyComponent(() => import('./SiteAdminWebhooksPage'), 'SiteAdminWebhooksPage'), + render: props => , }, { path: '/webhooks/create', - exact: true, - render: lazyComponent(() => import('./SiteAdminWebhookCreatePage'), 'SiteAdminWebhookCreatePage'), + render: props => , }, { path: '/webhooks/:id', - exact: true, - render: lazyComponent(() => import('./SiteAdminWebhookPage'), 'SiteAdminWebhookPage'), + render: props => , }, { path: '/slow-requests', - exact: true, - render: lazyComponent(() => import('./SiteAdminSlowRequestsPage'), 'SiteAdminSlowRequestsPage'), + render: props => , }, { path: '/webhooks/:id/edit', - exact: true, - render: lazyComponent(() => import('./SiteAdminWebhookUpdatePage'), 'SiteAdminWebhookUpdatePage'), + render: props => , }, ] diff --git a/client/web/src/user/settings/routes.tsx b/client/web/src/user/settings/routes.tsx index 15d4c62f4d87..db15bae2c966 100644 --- a/client/web/src/user/settings/routes.tsx +++ b/client/web/src/user/settings/routes.tsx @@ -29,6 +29,7 @@ export const userSettingsAreaRoutes: readonly UserSettingsAreaRoute[] = [ return ( } /** - * Configuration for a route. + * Configuration for a react-router 6 route. * * @template C Context information that is passed to `render` and `condition` */ export interface RouteV6Descriptor extends Conditional { - /** Path of this route (appended to the current match) */ readonly path: string readonly render: (props: C) => React.ReactNode } From 5da1ad78d30b7506e34db59e96c38851ae86c14c Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 3 Feb 2023 20:26:50 +0100 Subject: [PATCH 406/678] Treeview: Tweak ".." UI (#47379) --- .../web/src/repo/RepoRevisionSidebarFileTree.tsx | 16 ++++++++++++---- .../src/components/Tree/Tree.module.scss | 2 +- client/wildcard/src/components/Tree/Tree.tsx | 9 +-------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/client/web/src/repo/RepoRevisionSidebarFileTree.tsx b/client/web/src/repo/RepoRevisionSidebarFileTree.tsx index 4f17a5ea16ee..692d22217404 100644 --- a/client/web/src/repo/RepoRevisionSidebarFileTree.tsx +++ b/client/web/src/repo/RepoRevisionSidebarFileTree.tsx @@ -1,6 +1,12 @@ import { useCallback, useEffect, useRef, useState } from 'react' -import { mdiFileDocumentOutline, mdiSourceRepository, mdiFolderOutline, mdiFolderOpenOutline } from '@mdi/js' +import { + mdiFileDocumentOutline, + mdiSourceRepository, + mdiFolderOutline, + mdiFolderOpenOutline, + mdiFolderArrowUp, +} from '@mdi/js' import classNames from 'classnames' import { useNavigate } from 'react-router-dom-v5-compat' @@ -279,11 +285,11 @@ function renderNode({ }} > - .. + {name} ) } @@ -537,9 +543,11 @@ function insertRootNode(tree: TreeData, rootTreeUrl: string, alwaysLoadAncestors if (!alwaysLoadAncestors && tree.rootPath !== '') { const id = tree.nodes.length + const parentPathName = getParentPath(tree.rootPath) + const parentDirName = parentPathName === '' ? 'Repository root' : parentPathName.split('/').pop()! const path = tree.rootPath + '/..' const node: TreeNode = { - name: '..', + name: parentDirName, id, isBranch: false, parent: 0, diff --git a/client/wildcard/src/components/Tree/Tree.module.scss b/client/wildcard/src/components/Tree/Tree.module.scss index 7bc99818727b..22f3ad031051 100644 --- a/client/wildcard/src/components/Tree/Tree.module.scss +++ b/client/wildcard/src/components/Tree/Tree.module.scss @@ -50,7 +50,7 @@ .collapse-icon { min-height: 1.75rem; - min-width: 1.5rem; + min-width: 1rem; display: flex; align-items: center; justify-content: center; diff --git a/client/wildcard/src/components/Tree/Tree.tsx b/client/wildcard/src/components/Tree/Tree.tsx index f42308302060..66e185be73c7 100644 --- a/client/wildcard/src/components/Tree/Tree.tsx +++ b/client/wildcard/src/components/Tree/Tree.tsx @@ -133,15 +133,8 @@ export function Tree(props: Props): JSX.Element { } function getMarginLeft(level: number, isBranch: boolean): string { - // The level starts with 1 so the least margin by this logic is 0.75 * 1. - // - // Since folders render a chevron icon that is 1.25rem wide and we want to - // render it to the left of the item, we need to add 0.5rem so we don't have - // a negative margin - level += 0.5 - if (isBranch) { - return `${0.75 * level - 1.25}rem` + return `${0.75 * level - 0.75}rem` } return `${0.75 * level}rem` } From f9e20d2b01d2165f62ea6069efd0ae6026ef483f Mon Sep 17 00:00:00 2001 From: Cezary Bartoszuk Date: Fri, 3 Feb 2023 13:32:30 -0600 Subject: [PATCH 407/678] Teams/GraphQL: Add URL resolver (#47382) --- cmd/frontend/graphqlbackend/teams.go | 24 +++++++++++++++++--- cmd/frontend/graphqlbackend/teams_test.go | 27 +++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/cmd/frontend/graphqlbackend/teams.go b/cmd/frontend/graphqlbackend/teams.go index 29d35964d286..493438234517 100644 --- a/cmd/frontend/graphqlbackend/teams.go +++ b/cmd/frontend/graphqlbackend/teams.go @@ -2,6 +2,8 @@ package graphqlbackend import ( "context" + "fmt" + "net/url" "sync" "github.com/graph-gophers/graphql-go" @@ -125,15 +127,28 @@ type teamResolver struct { func (r *teamResolver) ID() graphql.ID { return relay.MarshalID("Team", r.team.ID) } -func (r *teamResolver) Name() string { return r.team.Name } -func (r *teamResolver) URL() string { return "" } + +func (r *teamResolver) Name() string { + return r.team.Name +} + +func (r *teamResolver) URL() string { + absolutePath := fmt.Sprintf("/teams/%s", r.team.Name) + u := &url.URL{Path: absolutePath} + return u.String() +} + func (r *teamResolver) DisplayName() *string { if r.team.DisplayName == "" { return nil } return &r.team.DisplayName } -func (r *teamResolver) Readonly() bool { return r.team.ReadOnly } + +func (r *teamResolver) Readonly() bool { + return r.team.ReadOnly +} + func (r *teamResolver) ParentTeam(ctx context.Context) (*teamResolver, error) { if r.team.ParentTeamID == 0 { return nil, nil @@ -144,14 +159,17 @@ func (r *teamResolver) ParentTeam(ctx context.Context) (*teamResolver, error) { } return &teamResolver{team: parentTeam, db: r.db}, nil } + func (r *teamResolver) ViewerCanAdminister(ctx context.Context) bool { // 🚨 SECURITY: For now administration is only allowed for site admins. err := auth.CheckCurrentUserIsSiteAdmin(ctx, r.db) return err == nil } + func (r *teamResolver) Members(args *ListTeamsArgs) *teamMemberConnection { return &teamMemberConnection{} } + func (r *teamResolver) ChildTeams(ctx context.Context, args *ListTeamsArgs) (*teamConnectionResolver, error) { c := &teamConnectionResolver{ db: r.db, diff --git a/cmd/frontend/graphqlbackend/teams_test.go b/cmd/frontend/graphqlbackend/teams_test.go index 96c0969de2b8..ca42e0524499 100644 --- a/cmd/frontend/graphqlbackend/teams_test.go +++ b/cmd/frontend/graphqlbackend/teams_test.go @@ -167,6 +167,33 @@ func TestTeamNode(t *testing.T) { }) } +func TestTeamNodeURL(t *testing.T) { + db, ts := setupDB() + ctx, _, _ := fakeUser(t, context.Background(), db, true) + team := &types.Team{ + Name: "team-刺身", // team-sashimi + } + if err := ts.CreateTeam(ctx, team); err != nil { + t.Fatalf("failed to create fake team: %s", err) + } + RunTest(t, &Test{ + Schema: mustParseGraphQLSchema(t, db), + Context: ctx, + Query: `{ + team(name: "team-刺身") { + ... on Team { + url + } + } + }`, + ExpectedResult: `{ + "team": { + "url": "/teams/team-%E5%88%BA%E8%BA%AB" + } + }`, + }) +} + func TestTeamNodeSiteAdminCanAdminister(t *testing.T) { for _, isAdmin := range []bool{true, false} { t.Run(fmt.Sprintf("viewer is admin = %v", isAdmin), func(t *testing.T) { From a96a20d6f901983c87cfde6d9dead87dd0f4846b Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 3 Feb 2023 20:49:08 +0100 Subject: [PATCH 408/678] Fix references panel issues (#47377) --- client/web/src/codeintel/ReferencesPanel.tsx | 4 +--- client/web/src/repo/blob/Blob.tsx | 7 ++++--- client/web/src/repo/blob/CodeMirrorBlob.tsx | 7 ++++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/web/src/codeintel/ReferencesPanel.tsx b/client/web/src/codeintel/ReferencesPanel.tsx index 2c4a2a1f9311..cf0e77ef3220 100644 --- a/client/web/src/codeintel/ReferencesPanel.tsx +++ b/client/web/src/codeintel/ReferencesPanel.tsx @@ -143,10 +143,8 @@ function createStateFromLocation(location: H.Location): null | State { } export const ReferencesPanel: React.FunctionComponent> = props => { - // We store the state in a React state so that we do not update it when the - // URL changes. const location = useLocation() - const [state] = useState(createStateFromLocation(location)) + const state = useMemo(() => createStateFromLocation(location), [location]) if (state === null) { return null diff --git a/client/web/src/repo/blob/Blob.tsx b/client/web/src/repo/blob/Blob.tsx index d40d4c722bef..b375aa0c9771 100644 --- a/client/web/src/repo/blob/Blob.tsx +++ b/client/web/src/repo/blob/Blob.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames' import { Remote } from 'comlink' import * as H from 'history' import { isEqual } from 'lodash' -import { Location, createPath, NavigateFunction, useLocation, useNavigate } from 'react-router-dom-v5-compat' +import { Location, createPath, NavigateFunction } from 'react-router-dom-v5-compat' import { BehaviorSubject, combineLatest, @@ -220,10 +220,11 @@ export const Blob: React.FunctionComponent> = ariaLabel, history, 'data-testid': dataTestId, + + location, + navigate, } = props - const location = useLocation() - const navigate = useNavigate() const settingsChanges = useMemo(() => new BehaviorSubject(null), []) useEffect(() => { if ( diff --git a/client/web/src/repo/blob/CodeMirrorBlob.tsx b/client/web/src/repo/blob/CodeMirrorBlob.tsx index c6b967b5b5b6..29dc24b8d333 100644 --- a/client/web/src/repo/blob/CodeMirrorBlob.tsx +++ b/client/web/src/repo/blob/CodeMirrorBlob.tsx @@ -8,7 +8,7 @@ import { openSearchPanel } from '@codemirror/search' import { Compartment, EditorState, Extension } from '@codemirror/state' import { EditorView } from '@codemirror/view' import { isEqual } from 'lodash' -import { createPath, useLocation, useNavigate } from 'react-router-dom-v5-compat' +import { createPath } from 'react-router-dom-v5-compat' import { addLineRangeQueryParameter, @@ -113,10 +113,11 @@ export const Blob: React.FunctionComponent = props => { overrideBrowserSearchKeybinding, 'data-testid': dataTestId, + + location, + navigate, } = props - const location = useLocation() - const navigate = useNavigate() const [useFileSearch, setUseFileSearch] = useLocalStorage('blob.overrideBrowserFindOnPage', true) const [container, setContainer] = useState(null) From 9aec7f11588775caaab61bdd211c5d19890f3cf6 Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Fri, 3 Feb 2023 11:57:57 -0800 Subject: [PATCH 409/678] Search: fix zoekt result limits (#47343) For indexed search, frontend translates a user's query into zoekt options. This PR makes these fixes to the zoekt result limits, which could help search performance and memory usage a bit: * Removes `resultCountFactor`, and instead uses simple default limits for ShardMaxDocCount and TotalMaxDocCount. * Updates old logic that increases ShardMaxDocCount and TotalMaxDocCount too much. The check used the wrong default value, so it always fired and multiplied the intended value by 50. * Removes MaxDocDisplayCount adjustment. Before we always added 2000, but this is not needed because we no longer use these extra results. --- internal/search/zoekt/indexed_search_test.go | 82 +++++--------------- internal/search/zoekt/zoekt.go | 72 ++++------------- 2 files changed, 34 insertions(+), 120 deletions(-) diff --git a/internal/search/zoekt/indexed_search_test.go b/internal/search/zoekt/indexed_search_test.go index 501f49912c46..bc2517a785d9 100644 --- a/internal/search/zoekt/indexed_search_test.go +++ b/internal/search/zoekt/indexed_search_test.go @@ -437,67 +437,6 @@ func TestZoektIndexedRepos(t *testing.T) { } } -func TestZoektResultCountFactor(t *testing.T) { - cases := []struct { - name string - numRepos int - globalSearch bool - pattern *search.TextPatternInfo - want int - }{ - { - name: "One repo implies max scaling factor", - numRepos: 1, - pattern: &search.TextPatternInfo{}, - want: 100, - }, - { - name: "Eleven repos implies a scaling factor between min and max", - numRepos: 11, - pattern: &search.TextPatternInfo{}, - want: 8, - }, - { - name: "More than 500 repos implies a min scaling factor", - numRepos: 501, - pattern: &search.TextPatternInfo{}, - want: 1, - }, - { - name: "Setting a count greater than defautl max results (30) adapts scaling factor", - numRepos: 501, - pattern: &search.TextPatternInfo{FileMatchLimit: 100}, - want: 10, - }, - { - name: "for global searches, k should be 1", - numRepos: 0, - globalSearch: true, - pattern: &search.TextPatternInfo{}, - want: 1, - }, - { - name: "for global searches, k should be 1, adjusted by the FileMatchLimit", - numRepos: 0, - globalSearch: true, - pattern: &search.TextPatternInfo{FileMatchLimit: 100}, - want: 10, - }, - } - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - got := (&Options{ - NumRepos: tt.numRepos, - FileMatchLimit: tt.pattern.FileMatchLimit, - GlobalSearch: tt.globalSearch, - }).resultCountFactor() - if tt.want != got { - t.Fatalf("Want scaling factor %d but got %d", tt.want, got) - } - }) - } -} - func TestZoektSearchOptions(t *testing.T) { cases := []struct { name string @@ -514,10 +453,10 @@ func TestZoektSearchOptions(t *testing.T) { NumRepos: 3, }, want: &zoekt.SearchOptions{ - ShardMaxMatchCount: 500000, - TotalMaxMatchCount: 500000, + ShardMaxMatchCount: 10000, + TotalMaxMatchCount: 100000, MaxWallTime: 20000000000, - MaxDocDisplayCount: 2500, + MaxDocDisplayCount: 500, ChunkMatches: true, }, }, @@ -559,6 +498,21 @@ func TestZoektSearchOptions(t *testing.T) { ChunkMatches: true, }, }, + { + name: "test large file match limit", + context: context.Background(), + options: &Options{ + FileMatchLimit: 100_000, + NumRepos: 3, + }, + want: &zoekt.SearchOptions{ + ShardMaxMatchCount: 100_000, + TotalMaxMatchCount: 100_000, + MaxWallTime: 20000000000, + MaxDocDisplayCount: 100_000, + ChunkMatches: true, + }, + }, { name: "test document ranks weight", context: context.Background(), diff --git a/internal/search/zoekt/zoekt.go b/internal/search/zoekt/zoekt.go index efec9aa8da4b..e19d68482f95 100644 --- a/internal/search/zoekt/zoekt.go +++ b/internal/search/zoekt/zoekt.go @@ -13,7 +13,6 @@ import ( "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/search" "github.com/sourcegraph/sourcegraph/internal/search/filter" - "github.com/sourcegraph/sourcegraph/internal/search/limits" "github.com/sourcegraph/sourcegraph/internal/trace/ot" "github.com/sourcegraph/sourcegraph/internal/trace/policy" "github.com/sourcegraph/sourcegraph/internal/types" @@ -111,25 +110,6 @@ func (o *Options) ToSearch(ctx context.Context) *zoekt.SearchOptions { } if o.Features.Ranking { - limit := int(o.FileMatchLimit) - - // Tell each zoekt replica to not send back more than limit results. - searchOpts.MaxDocDisplayCount = limit - - // These are reasonable default amounts of work to do per shard and - // replica respectively. - searchOpts.ShardMaxMatchCount = 10_000 - searchOpts.TotalMaxMatchCount = 100_000 - - // If we are searching for large limits, raise the amount of work we - // are willing to do per shard and zoekt replica respectively. - if limit > searchOpts.ShardMaxMatchCount { - searchOpts.ShardMaxMatchCount = limit - } - if limit > searchOpts.TotalMaxMatchCount { - searchOpts.TotalMaxMatchCount = limit - } - // This enables our stream based ranking were we wait upto 500ms to // collect results before ranking. searchOpts.FlushWallTime = 500 * time.Millisecond @@ -137,47 +117,27 @@ func (o *Options) ToSearch(ctx context.Context) *zoekt.SearchOptions { // This enables the use of document ranks in scoring, if they are available. searchOpts.UseDocumentRanks = true searchOpts.DocumentRanksWeight = conf.SearchDocumentRanksWeight() - } else { - k := o.resultCountFactor() - searchOpts.ShardMaxMatchCount = 100 * k - searchOpts.TotalMaxMatchCount = 100 * k - // Ask for 2000 more results so we have results to populate - // RepoStatusLimitHit. - searchOpts.MaxDocDisplayCount = int(o.FileMatchLimit) + 2000 } - return searchOpts -} + // These are reasonable default amounts of work to do per shard and + // replica respectively. + searchOpts.ShardMaxMatchCount = 10_000 + searchOpts.TotalMaxMatchCount = 100_000 -func (o *Options) resultCountFactor() (k int) { - if o.GlobalSearch { - // for globalSearch, numRepos = 0, but effectively we are searching over all - // indexed repos, hence k should be 1 - k = 1 - } else { - // If we're only searching a small number of repositories, return more - // comprehensive results. This is arbitrary. - switch { - case o.NumRepos <= 5: - k = 100 - case o.NumRepos <= 10: - k = 10 - case o.NumRepos <= 25: - k = 8 - case o.NumRepos <= 50: - k = 5 - case o.NumRepos <= 100: - k = 3 - case o.NumRepos <= 500: - k = 2 - default: - k = 1 - } + // Tell each zoekt replica to not send back more than limit results. + limit := int(o.FileMatchLimit) + searchOpts.MaxDocDisplayCount = limit + + // If we are searching for large limits, raise the amount of work we + // are willing to do per shard and zoekt replica respectively. + if limit > searchOpts.ShardMaxMatchCount { + searchOpts.ShardMaxMatchCount = limit } - if o.FileMatchLimit > limits.DefaultMaxSearchResults { - k = int(float64(k) * 3 * float64(o.FileMatchLimit) / float64(limits.DefaultMaxSearchResults)) + if limit > searchOpts.TotalMaxMatchCount { + searchOpts.TotalMaxMatchCount = limit } - return k + + return searchOpts } // repoRevFunc is a function which maps repository names returned from Zoekt From c33ed29008b8044811dd71162ee608ae7f939dc4 Mon Sep 17 00:00:00 2001 From: Erik Seliger Date: Fri, 3 Feb 2023 21:21:23 +0100 Subject: [PATCH 410/678] Reduce amount of import from graphqlbackend from outside of cmd/frontend (#47346) --- dev/gqltest/code_insights_test.go | 4 ++-- .../batches/workers/batch_spec_workspace_creator_test.go | 4 ++-- enterprise/internal/batches/service/service_test.go | 6 +++--- enterprise/internal/batches/types/changeset_spec.go | 8 +++++--- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/dev/gqltest/code_insights_test.go b/dev/gqltest/code_insights_test.go index a35b77c3c7ec..8ef10adede5c 100644 --- a/dev/gqltest/code_insights_test.go +++ b/dev/gqltest/code_insights_test.go @@ -6,10 +6,10 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/graph-gophers/graphql-go/relay" "github.com/stretchr/testify/assert" "k8s.io/utils/strings/slices" - "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" "github.com/sourcegraph/sourcegraph/internal/gqltestutil" ) @@ -46,7 +46,7 @@ func TestCreateDashboard(t *testing.T) { title := "Dashboard Title 1" _, err := client.CreateDashboard(gqltestutil.DashboardInputArgs{ Title: title, - UserGrant: string(graphqlbackend.MarshalUserID(9999)), + UserGrant: string(relay.MarshalID("User", 9999)), }) if !strings.Contains(err.Error(), "user does not have permission") { t.Fatal("Should have thrown an error") diff --git a/enterprise/cmd/worker/internal/batches/workers/batch_spec_workspace_creator_test.go b/enterprise/cmd/worker/internal/batches/workers/batch_spec_workspace_creator_test.go index 5cea3eaf47e3..5e76a1d9c1e4 100644 --- a/enterprise/cmd/worker/internal/batches/workers/batch_spec_workspace_creator_test.go +++ b/enterprise/cmd/worker/internal/batches/workers/batch_spec_workspace_creator_test.go @@ -9,10 +9,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/graph-gophers/graphql-go/relay" "github.com/sourcegraph/log/logtest" - "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" "github.com/sourcegraph/sourcegraph/enterprise/internal/batches/service" "github.com/sourcegraph/sourcegraph/enterprise/internal/batches/store" bt "github.com/sourcegraph/sourcegraph/enterprise/internal/batches/testing" @@ -255,7 +255,7 @@ func TestBatchSpecWorkspaceCreatorProcess_Caching(t *testing.T) { Description: batchSpec.Spec.Description, }, batcheslib.Repository{ - ID: string(graphqlbackend.MarshalRepositoryID(workspace.Repo.ID)), + ID: string(relay.MarshalID("Repository", workspace.Repo.ID)), Name: string(workspace.Repo.Name), BaseRef: workspace.Branch, BaseRev: string(workspace.Commit), diff --git a/enterprise/internal/batches/service/service_test.go b/enterprise/internal/batches/service/service_test.go index c94956ba1305..2a410ba61a07 100644 --- a/enterprise/internal/batches/service/service_test.go +++ b/enterprise/internal/batches/service/service_test.go @@ -10,12 +10,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/graph-gophers/graphql-go/relay" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/sourcegraph/log/logtest" - "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" stesting "github.com/sourcegraph/sourcegraph/enterprise/internal/batches/sources/testing" "github.com/sourcegraph/sourcegraph/enterprise/internal/batches/store" bt "github.com/sourcegraph/sourcegraph/enterprise/internal/batches/testing" @@ -582,7 +582,7 @@ func TestService(t *testing.T) { t.Run("CreateChangesetSpec", func(t *testing.T) { repo := rs[0] - rawSpec := bt.NewRawChangesetSpecGitBranch(graphqlbackend.MarshalRepositoryID(repo.ID), "d34db33f") + rawSpec := bt.NewRawChangesetSpecGitBranch(relay.MarshalID("Repository", repo.ID), "d34db33f") t.Run("success", func(t *testing.T) { spec, err := svc.CreateChangesetSpec(ctx, rawSpec, admin.ID) @@ -666,7 +666,7 @@ index e5af166..d44c3fc 100644 }) t.Run("CreateChangesetSpecs", func(t *testing.T) { - rawSpec := bt.NewRawChangesetSpecGitBranch(graphqlbackend.MarshalRepositoryID(rs[0].ID), "d34db33f") + rawSpec := bt.NewRawChangesetSpecGitBranch(relay.MarshalID("Repository", rs[0].ID), "d34db33f") t.Run("success", func(t *testing.T) { specs, err := svc.CreateChangesetSpecs(ctx, []string{rawSpec}, admin.ID) diff --git a/enterprise/internal/batches/types/changeset_spec.go b/enterprise/internal/batches/types/changeset_spec.go index 6eb1020ac91f..81d119d0cea9 100644 --- a/enterprise/internal/batches/types/changeset_spec.go +++ b/enterprise/internal/batches/types/changeset_spec.go @@ -6,9 +6,9 @@ import ( "time" "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/relay" godiff "github.com/sourcegraph/go-diff/diff" - "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" "github.com/sourcegraph/sourcegraph/internal/api" "github.com/sourcegraph/sourcegraph/internal/conf" batcheslib "github.com/sourcegraph/sourcegraph/lib/batches" @@ -24,7 +24,8 @@ func NewChangesetSpecFromRaw(rawSpec string) (*ChangesetSpec, error) { } func NewChangesetSpecFromSpec(spec *batcheslib.ChangesetSpec) (*ChangesetSpec, error) { - baseRepoID, err := graphqlbackend.UnmarshalRepositoryID(graphql.ID(spec.BaseRepository)) + var baseRepoID api.RepoID + err := relay.UnmarshalSpec(graphql.ID(spec.BaseRepository), &baseRepoID) if err != nil { return nil, err } @@ -40,7 +41,8 @@ func NewChangesetSpecFromSpec(spec *batcheslib.ChangesetSpec) (*ChangesetSpec, e if spec.IsImportingExisting() { c.Type = ChangesetSpecTypeExisting } else { - headRepoID, err := graphqlbackend.UnmarshalRepositoryID(graphql.ID(spec.HeadRepository)) + var headRepoID api.RepoID + err := relay.UnmarshalSpec(graphql.ID(spec.HeadRepository), &headRepoID) if err != nil { return nil, err } From 9531928768b842300e3fd63e131e9a69b05b11da Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Fri, 3 Feb 2023 12:40:52 -0800 Subject: [PATCH 411/678] graphqlbackend: add sourcegraphoperator handling to addExternalAccount mutation (#47235) Allows `addExternalAccount` to configure SOAP accounts with magic args, most notably the ability to create service accounts (see #47059), given the following: - the user is already a site admin - the user is not yet a SOAP user - the user provides arguments matching Cloud-specific configuration exactly The associated external account is not validated. Risk evaluation: - being a SOAP user grants no additional privileges to being a site admin, except that your account might be auto-deleted unless you mark yourself as a service account - we prevent an existing SOAP user from escalating themselves to a permanent service account via `associateExternalAccount` by only allowing non-SOAP users to have a SOAP account association added - the caller must dig for Cloud-specific configuration that is not visible in site config via `site { authProviders }` query, and know the shape of our magic string var by reading the source code, making it unlikely this will be used accidentally See https://github.com/sourcegraph/customer/issues/1929 --------- Co-authored-by: Joe Chen Co-authored-by: Petri-Johan Last Co-authored-by: Milan Freml --- .../graphqlbackend/external_accounts.go | 15 +- .../graphqlbackend/external_accounts_test.go | 27 +- .../auth/sourcegraphoperator/associate.go | 93 +++++++ .../sourcegraphoperator/associate_test.go | 237 ++++++++++++++++++ .../auth/sourcegraphoperator/config.go | 5 + .../auth/sourcegraphoperator/provider.go | 8 +- .../auth/sourcegraph_operator_cleaner_test.go | 1 + .../auth/sourcegraphoperator/associate.go | 30 +++ 8 files changed, 407 insertions(+), 9 deletions(-) create mode 100644 enterprise/cmd/frontend/internal/auth/sourcegraphoperator/associate.go create mode 100644 enterprise/cmd/frontend/internal/auth/sourcegraphoperator/associate_test.go create mode 100644 internal/auth/sourcegraphoperator/associate.go diff --git a/cmd/frontend/graphqlbackend/external_accounts.go b/cmd/frontend/graphqlbackend/external_accounts.go index 61b5ebf36fc1..449722f080df 100644 --- a/cmd/frontend/graphqlbackend/external_accounts.go +++ b/cmd/frontend/graphqlbackend/external_accounts.go @@ -9,6 +9,7 @@ import ( "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil" "github.com/sourcegraph/sourcegraph/internal/actor" "github.com/sourcegraph/sourcegraph/internal/auth" + "github.com/sourcegraph/sourcegraph/internal/auth/sourcegraphoperator" "github.com/sourcegraph/sourcegraph/internal/authz/permssync" "github.com/sourcegraph/sourcegraph/internal/database" "github.com/sourcegraph/sourcegraph/internal/extsvc" @@ -158,13 +159,21 @@ func (r *schemaResolver) AddExternalAccount(ctx context.Context, args *struct { return nil, auth.ErrNotAuthenticated } - if args.ServiceType == "gerrit" { + switch args.ServiceType { + case extsvc.TypeGerrit: err := gext.AddGerritExternalAccount(ctx, r.db, a.UID, args.ServiceID, args.AccountDetails) if err != nil { return nil, err } - } else { - return nil, errors.New("unsupported service type") + + case auth.SourcegraphOperatorProviderType: + err := sourcegraphoperator.AddSourcegraphOperatorExternalAccount(ctx, r.db, a.UID, args.ServiceID, args.AccountDetails) + if err != nil { + return nil, errors.Wrap(err, "failed to add Sourcegraph Operator external account") + } + + default: + return nil, errors.Newf("unsupported service type %q", args.ServiceType) } permssync.SchedulePermsSync(ctx, r.logger, r.db, protocol.PermsSyncRequest{ diff --git a/cmd/frontend/graphqlbackend/external_accounts_test.go b/cmd/frontend/graphqlbackend/external_accounts_test.go index 9fab7546108c..ca6e4f396c88 100644 --- a/cmd/frontend/graphqlbackend/external_accounts_test.go +++ b/cmd/frontend/graphqlbackend/external_accounts_test.go @@ -9,12 +9,14 @@ import ( "github.com/graph-gophers/graphql-go" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/sourcegraph/log" "github.com/sourcegraph/log/logtest" "github.com/sourcegraph/sourcegraph/internal/actor" + "github.com/sourcegraph/sourcegraph/internal/auth" "github.com/sourcegraph/sourcegraph/internal/authz/permssync" "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/database" @@ -76,11 +78,12 @@ func TestExternalAccounts_AddExternalAccount(t *testing.T) { gerritURL := "https://gerrit.mycorp.com/" testCases := map[string]struct { - user *types.User - serviceType string - serviceID string - accountDetails string - wantErr bool + user *types.User + serviceType string + serviceID string + accountDetails string + wantErr bool + wantErrContains string }{ "unauthed returns err": { user: nil, @@ -104,6 +107,17 @@ func TestExternalAccounts_AddExternalAccount(t *testing.T) { wantErr: false, accountDetails: `{"username": "alice", "password": "test"}`, }, + // OSS packages cannot import enterprise packages, but when we build the entire + // application this will be implemented. + // + // See enterprise/cmd/frontend/internal/auth/sourcegraphoperator for more details + // and additional test coverage on the functionality. + "Sourcegraph operator unimplemented in OSS": { + user: &types.User{ID: 1, SiteAdmin: true}, + serviceType: auth.SourcegraphOperatorProviderType, + wantErr: true, + wantErrContains: "unimplemented in Sourcegraph OSS", + }, } for name, tc := range testCases { @@ -179,6 +193,9 @@ func TestExternalAccounts_AddExternalAccount(t *testing.T) { _, err = sr.AddExternalAccount(ctx, &args) if tc.wantErr { require.Error(t, err) + if tc.wantErrContains != "" { + assert.Contains(t, err.Error(), tc.wantErrContains) + } } else { require.NoError(t, err) } diff --git a/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/associate.go b/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/associate.go new file mode 100644 index 000000000000..948859369a8a --- /dev/null +++ b/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/associate.go @@ -0,0 +1,93 @@ +package sourcegraphoperator + +import ( + "context" + "encoding/json" + + "github.com/sourcegraph/sourcegraph/cmd/frontend/auth/providers" + "github.com/sourcegraph/sourcegraph/internal/auth" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/extsvc" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +type accountDetailsBody struct { + ClientID string `json:"clientID"` + AccountID string `json:"accountID"` + + ExternalAccountData +} + +// addSourcegraphOperatorExternalAccount links the given user with a Sourcegraph Operator +// provider, if and only if it already exists. The provider can only be added through +// Enterprise Sourcegraph Cloud config, so this essentially no-ops outside of Cloud. +// +// It implements internal/auth/sourcegraphoperator.AddSourcegraphOperatorExternalAccount +// +// 🚨 SECURITY: Some important things to note: +// - Being a SOAP user does not grant any extra privilege over being a site admin. +// - The operation will fail if the user is already a SOAP user, which prevents escalating +// time-bound accounts to permanent service accounts. +// - Both the client ID and the service ID must match the SOAP configuration exactly. +func addSourcegraphOperatorExternalAccount(ctx context.Context, db database.DB, userID int32, serviceID string, accountDetails string) error { + // 🚨 SECURITY: Caller must be a site admin. + if err := auth.CheckCurrentUserIsSiteAdmin(ctx, db); err != nil { + return err + } + + p := providers.GetProviderByConfigID(providers.ConfigID{ + Type: auth.SourcegraphOperatorProviderType, + ID: serviceID, + }) + if p == nil { + return errors.New("provider does not exist") + } + + if accountDetails == "" { + return errors.New("account details are required") + } + var details accountDetailsBody + if err := json.Unmarshal([]byte(accountDetails), &details); err != nil { + return errors.Wrap(err, "invalid account details") + } + + // Additionally check client ID matches - service ID was already checked in the + // initial GetProviderByConfigID call + if details.ClientID != p.CachedInfo().ClientID { + return errors.Newf("unknown client ID %q", details.ClientID) + } + + // Run account count verification and association in a single transaction, to ensure + // we have no funny business with accounts being created in the time between the two. + return db.WithTransact(ctx, func(db database.DB) error { + // Make sure this user has no other SOAP accounts. + numSOAPAccounts, err := db.UserExternalAccounts().Count(ctx, database.ExternalAccountsListOptions{ + UserID: userID, + // For provider matching, we explicitly do not provider the service ID - there + // should only be one SOAP registered. + ServiceType: auth.SourcegraphOperatorProviderType, + }) + if err != nil { + return errors.Wrap(err, "failed to check for an existing Sourcegraph Operator accounts") + } + if numSOAPAccounts > 0 { + return errors.New("user already has an associated Sourcegraph Operator account") + } + + // Create an association + accountData, err := MarshalAccountData(details.ExternalAccountData) + if err != nil { + return errors.Wrap(err, "failed to marshal account data") + } + if err := db.UserExternalAccounts().AssociateUserAndSave(ctx, userID, extsvc.AccountSpec{ + ServiceType: auth.SourcegraphOperatorProviderType, + ServiceID: serviceID, + ClientID: details.ClientID, + + AccountID: details.AccountID, + }, accountData); err != nil { + return errors.Wrap(err, "failed to associate user with Sourcegraph Operator provider") + } + return nil + }) +} diff --git a/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/associate_test.go b/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/associate_test.go new file mode 100644 index 000000000000..8bfccded44a5 --- /dev/null +++ b/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/associate_test.go @@ -0,0 +1,237 @@ +package sourcegraphoperator + +import ( + "context" + "encoding/json" + "testing" + + "github.com/hexops/autogold/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/sourcegraph/log/logtest" + + "github.com/sourcegraph/sourcegraph/cmd/frontend/auth/providers" + "github.com/sourcegraph/sourcegraph/enterprise/internal/cloud" + "github.com/sourcegraph/sourcegraph/internal/actor" + "github.com/sourcegraph/sourcegraph/internal/auth" + osssourcegraphoperator "github.com/sourcegraph/sourcegraph/internal/auth/sourcegraphoperator" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/database/dbtest" + "github.com/sourcegraph/sourcegraph/internal/extsvc" + "github.com/sourcegraph/sourcegraph/internal/types" +) + +func TestAddSourcegraphOperatorExternalAccountBinding(t *testing.T) { + // Enable SOAP + cloud.MockSiteConfig(t, &cloud.SchemaSiteConfig{ + AuthProviders: &cloud.SchemaAuthProviders{ + SourcegraphOperator: &cloud.SchemaAuthProviderSourcegraphOperator{ + ClientID: "foobar", + }, + }, + }) + defer cloud.MockSiteConfig(t, nil) + // Initialize package + Init() + t.Cleanup(func() { providers.Update(auth.SourcegraphOperatorProviderType, nil) }) + // Assert handler is registered - we check this by making sure we get a site admin + // error instead of an "unimplemented" error. + users := database.NewMockUserStore() + users.GetByCurrentAuthUserFunc.SetDefaultReturn(&types.User{SiteAdmin: false}, nil) + db := database.NewMockDB() + db.UsersFunc.SetDefaultReturn(users) + err := osssourcegraphoperator.AddSourcegraphOperatorExternalAccount(context.Background(), db, 1, "foo", "") + assert.ErrorIs(t, err, auth.ErrMustBeSiteAdmin) +} + +func TestAddSourcegraphOperatorExternalAccount(t *testing.T) { + ctx := context.Background() + soap := NewProvider(cloud.SchemaAuthProviderSourcegraphOperator{ + ClientID: "soap_client", + }) + serviceID := soap.ConfigID().ID + + mockDB := func(siteAdmin bool) database.DB { + users := database.NewMockUserStore() + users.GetByCurrentAuthUserFunc.SetDefaultReturn(&types.User{ + SiteAdmin: siteAdmin, + }, nil) + db := database.NewMockDB() + db.UsersFunc.SetDefaultReturn(users) + return db + } + + for _, tc := range []struct { + name string + // db, user, and other setup + setup func(t *testing.T) (userID int32, db database.DB) + // accountDetails parameter + accountDetails *accountDetailsBody + // validate result of AddSourcegraphOperatorExternalAccount + expectErr autogold.Value + // assert state of the DB (optional) + assert func(t *testing.T, uid int32, db database.DB) + }{ + { + name: "user is not a side admin", + setup: func(t *testing.T) (int32, database.DB) { + providers.MockProviders = []providers.Provider{soap} + t.Cleanup(func() { providers.MockProviders = nil }) + + return 42, mockDB(false) + }, + accountDetails: &accountDetailsBody{ + ClientID: "foobar", + AccountID: "bob", + ExternalAccountData: ExternalAccountData{ + ServiceAccount: true, + }, + }, + expectErr: autogold.Expect(`must be site admin`), + }, + { + name: "provider does not exist", + setup: func(t *testing.T) (int32, database.DB) { + providers.MockProviders = nil + return 42, mockDB(true) + }, + expectErr: autogold.Expect("provider does not exist"), + }, + { + name: "incorrect details for SOAP provider", + setup: func(t *testing.T) (int32, database.DB) { + providers.MockProviders = []providers.Provider{soap} + t.Cleanup(func() { providers.MockProviders = nil }) + + return 42, mockDB(true) + }, + accountDetails: &accountDetailsBody{ + ClientID: "foobar", + AccountID: "bob", + ExternalAccountData: ExternalAccountData{ + ServiceAccount: true, + }, + }, + expectErr: autogold.Expect(`unknown client ID "foobar"`), + }, + { + name: "new user associate", + setup: func(t *testing.T) (int32, database.DB) { + if testing.Short() { + t.Skip() + } + + providers.MockProviders = []providers.Provider{soap} + t.Cleanup(func() { providers.MockProviders = nil }) + + logger := logtest.NoOp(t) + db := database.NewDB(logger, dbtest.NewDB(logger, t)) + u, err := db.Users().Create( + ctx, + database.NewUser{ + Username: "logan", + }, + ) + require.NoError(t, err) + err = db.Users().SetIsSiteAdmin(ctx, u.ID, true) + require.NoError(t, err) + return u.ID, db + }, + accountDetails: &accountDetailsBody{ + ClientID: "soap_client", + AccountID: "bob", + ExternalAccountData: ExternalAccountData{ + ServiceAccount: true, + }, + }, + expectErr: autogold.Expect(nil), + assert: func(t *testing.T, uid int32, db database.DB) { + accts, err := db.UserExternalAccounts().List(ctx, database.ExternalAccountsListOptions{ + UserID: uid, + }) + require.NoError(t, err) + require.Len(t, accts, 1) + assert.Equal(t, auth.SourcegraphOperatorProviderType, accts[0].ServiceType) + assert.Equal(t, "bob", accts[0].AccountID) + assert.Equal(t, "soap_client", accts[0].ClientID) + assert.Equal(t, serviceID, accts[0].ServiceID) + + data, err := GetAccountData(ctx, accts[0].AccountData) + require.NoError(t, err) + assert.True(t, data.ServiceAccount) + }, + }, + { + name: "double associate is not allowed (prevents escalation)", + setup: func(t *testing.T) (int32, database.DB) { + if testing.Short() { + t.Skip() + } + + providers.MockProviders = []providers.Provider{soap} + t.Cleanup(func() { providers.MockProviders = nil }) + + logger := logtest.NoOp(t) + db := database.NewDB(logger, dbtest.NewDB(logger, t)) + u, err := db.Users().Create( + ctx, + database.NewUser{ + Username: "bib", + }, + ) + require.NoError(t, err) + err = db.Users().SetIsSiteAdmin(ctx, u.ID, true) + require.NoError(t, err) + err = db.UserExternalAccounts().AssociateUserAndSave(ctx, u.ID, extsvc.AccountSpec{ + ServiceType: auth.SourcegraphOperatorProviderType, + ServiceID: serviceID, + ClientID: "soap_client", + AccountID: "bib", + }, extsvc.AccountData{}) // not a service account initially + require.NoError(t, err) + return u.ID, db + }, + accountDetails: &accountDetailsBody{ + ClientID: "soap_client", + AccountID: "bob", // trying to change account ID + ExternalAccountData: ExternalAccountData{ + ServiceAccount: true, // trying to promote themselves to service account + }, + }, + expectErr: autogold.Expect("user already has an associated Sourcegraph Operator account"), + assert: func(t *testing.T, uid int32, db database.DB) { + accts, err := db.UserExternalAccounts().List(ctx, database.ExternalAccountsListOptions{ + UserID: uid, + }) + require.NoError(t, err) + require.Len(t, accts, 1) + assert.Equal(t, auth.SourcegraphOperatorProviderType, accts[0].ServiceType) + assert.Equal(t, "bib", accts[0].AccountID) // the original account + assert.Equal(t, "soap_client", accts[0].ClientID) + assert.Equal(t, serviceID, accts[0].ServiceID) + + data, err := GetAccountData(ctx, accts[0].AccountData) + require.NoError(t, err) + assert.False(t, data.ServiceAccount) // still not a service account + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + uid, db := tc.setup(t) + details, err := json.Marshal(tc.accountDetails) + require.NoError(t, err) + + ctx := actor.WithActor(context.Background(), actor.FromMockUser(uid)) + err = addSourcegraphOperatorExternalAccount(ctx, db, uid, serviceID, string(details)) + if err != nil { + tc.expectErr.Equal(t, err.Error()) + } else { + tc.expectErr.Equal(t, nil) + } + if tc.assert != nil { + tc.assert(t, uid, db) + } + }) + } +} diff --git a/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/config.go b/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/config.go index 6bb4db25e5d3..bf342c2acaf6 100644 --- a/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/config.go +++ b/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/config.go @@ -9,6 +9,7 @@ import ( "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/auth/openidconnect" "github.com/sourcegraph/sourcegraph/enterprise/internal/cloud" "github.com/sourcegraph/sourcegraph/internal/auth" + osssourcegraphoperator "github.com/sourcegraph/sourcegraph/internal/auth/sourcegraphoperator" "github.com/sourcegraph/sourcegraph/internal/conf" "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" ) @@ -29,6 +30,7 @@ func GetOIDCProvider(id string) *openidconnect.Provider { return nil } +// Init registers Sourcegraph Operator handlers and providers. func Init() { cloudSiteConfig := cloud.SiteConfig() if !cloudSiteConfig.SourcegraphOperatorAuthProviderEnabled() { @@ -45,6 +47,9 @@ func Init() { } }() providers.Update(auth.SourcegraphOperatorProviderType, []providers.Provider{p}) + + // Register enterprise handler implementation in OSS + osssourcegraphoperator.RegisterAddSourcegraphOperatorExternalAccountHandler(addSourcegraphOperatorExternalAccount) } func validateConfig(c conftypes.SiteConfigQuerier) (problems conf.Problems) { diff --git a/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/provider.go b/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/provider.go index 7c56c0e2cea3..b59e2953aed2 100644 --- a/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/provider.go +++ b/enterprise/cmd/frontend/internal/auth/sourcegraphoperator/provider.go @@ -13,7 +13,13 @@ import ( ) // provider is an implementation of providers.Provider for the Sourcegraph -// Operator authentication. +// Operator authentication, also referred to as "SOAP". There can only ever be +// one provider of this type, and it can only be provisioned through Cloud site +// configuration (see github.com/sourcegraph/sourcegraph/enterprise/internal/cloud) +// +// SOAP is used to provision accounts for Sourcegraph teammates in Sourcegraph +// Cloud - for more details, refer to +// https://handbook.sourcegraph.com/departments/cloud/technical-docs/oidc_site_admin/#faq type provider struct { config cloud.SchemaAuthProviderSourcegraphOperator *openidconnect.Provider diff --git a/enterprise/cmd/frontend/worker/auth/sourcegraph_operator_cleaner_test.go b/enterprise/cmd/frontend/worker/auth/sourcegraph_operator_cleaner_test.go index 2dbf6c2567ae..dc9ddf30f43a 100644 --- a/enterprise/cmd/frontend/worker/auth/sourcegraph_operator_cleaner_test.go +++ b/enterprise/cmd/frontend/worker/auth/sourcegraph_operator_cleaner_test.go @@ -54,6 +54,7 @@ func TestSourcegraphOperatorCleanHandler(t *testing.T) { // 4. riley, who is an expired SOAP user (will be cleaned up) // 5. cris, who has a non-SOAP external account // 6. cami, who is an expired SOAP user on the permanent accounts list + // (is a service account) // All the above except riley will be deleted. wantNotDeleted := []string{"logan", "morgan", "jordan", "cris", "cami"} diff --git a/internal/auth/sourcegraphoperator/associate.go b/internal/auth/sourcegraphoperator/associate.go new file mode 100644 index 000000000000..37d6a8438a54 --- /dev/null +++ b/internal/auth/sourcegraphoperator/associate.go @@ -0,0 +1,30 @@ +package sourcegraphoperator + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +type addSourcegraphOperatorExternalAccountFunc func(ctx context.Context, db database.DB, userID int32, serviceID string, accountDetails string) error + +var addSourcegraphOperatorExternalAccountHandler addSourcegraphOperatorExternalAccountFunc + +// RegisterAddSourcegraphOperatorExternalAccountHandler is used by +// enterprise/cmd/frontend/internal/auth/sourcegraphoperator to register an +// enterprise handler for AddSourcegraphOperatorExternalAccount. +func RegisterAddSourcegraphOperatorExternalAccountHandler(handler addSourcegraphOperatorExternalAccountFunc) { + addSourcegraphOperatorExternalAccountHandler = handler +} + +// AddSourcegraphOperatorExternalAccount is implemented in +// enterprise/cmd/frontend/internal/auth/sourcegraphoperator.AddSourcegraphOperatorExternalAccount +// +// Outside of Sourcegraph Enterprise, this will no-op and return an error. +func AddSourcegraphOperatorExternalAccount(ctx context.Context, db database.DB, userID int32, serviceID string, accountDetails string) error { + if addSourcegraphOperatorExternalAccountHandler == nil { + return errors.New("AddSourcegraphOperatorExternalAccount unimplemented in Sourcegraph OSS") + } + return addSourcegraphOperatorExternalAccountHandler(ctx, db, userID, serviceID, accountDetails) +} From 93bb511f847e36a98f8fc6d4c71b103a1db47fa5 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 3 Feb 2023 22:00:37 +0100 Subject: [PATCH 412/678] Revert "Migrate admin area to RR6 (#47276) (#47389) --- .../pages/CodeIntelConfigurationPage.tsx | 10 +- .../pages/CodeIntelPreciseIndexesPage.tsx | 2 + .../executors/ExecutorsSiteAdminArea.tsx | 15 +- .../executors/instances/ExecutorsListPage.tsx | 6 +- .../site-admin/SiteAdminLsifUploadPage.tsx | 18 +- ...SiteAdminCreateProductSubscriptionPage.tsx | 16 +- .../SiteAdminProductSubscriptionPage.test.tsx | 9 +- .../SiteAdminProductSubscriptionPage.tsx | 17 +- .../SiteAdminProductSubscriptionPage.tsx | 8 +- .../web/src/enterprise/site-admin/routes.tsx | 197 ++++++++---------- client/web/src/namespaces/routes.ts | 19 ++ client/web/src/namespaces/routes.tsx | 35 ---- client/web/src/org/settings/routes.tsx | 1 - client/web/src/settings/SettingsArea.tsx | 7 +- client/web/src/site-admin/SiteAdminArea.tsx | 22 +- .../SiteAdminBackgroundJobsPage.tsx | 3 +- .../site-admin/SiteAdminCreateUserPage.tsx | 3 +- .../SiteAdminExternalServicesArea.tsx | 19 +- .../SiteAdminFeatureFlagConfigurationPage.tsx | 30 ++- .../web/src/site-admin/SiteAdminPingsPage.tsx | 3 +- .../src/site-admin/SiteAdminReportBugPage.tsx | 4 +- .../site-admin/SiteAdminRepositoriesPage.tsx | 28 +-- .../src/site-admin/SiteAdminSettingsPage.tsx | 10 +- .../SiteAdminWebhookCreatePage.story.tsx | 15 +- .../site-admin/SiteAdminWebhookCreatePage.tsx | 7 +- .../site-admin/SiteAdminWebhookPage.story.tsx | 43 ++-- .../src/site-admin/SiteAdminWebhookPage.tsx | 17 +- .../SiteAdminWebhookUpdatePage.story.tsx | 25 ++- .../site-admin/SiteAdminWebhookUpdatePage.tsx | 16 +- .../SiteAdminWebhooksPage.story.tsx | 15 +- .../src/site-admin/SiteAdminWebhooksPage.tsx | 3 +- .../src/site-admin/UserManagement/index.tsx | 3 +- .../site-admin/WebhookCreateUpdatePage.tsx | 11 +- .../AnalyticsBatchChangesPage/index.tsx | 3 +- .../AnalyticsCodeInsightsPage/index.tsx | 3 +- .../AnalyticsCodeIntelPage/index.tsx | 3 +- .../AnalyticsExtensionsPage/index.tsx | 3 +- .../AnalyticsNotebooksPage/index.tsx | 3 +- .../analytics/AnalyticsOverviewPage/index.tsx | 7 +- .../analytics/AnalyticsSearchPage/index.tsx | 3 +- .../AnalyticsUsersPage.story.tsx | 2 +- .../analytics/AnalyticsUsersPage/index.tsx | 3 +- .../outbound-webhooks/CreatePage.story.tsx | 8 +- .../outbound-webhooks/CreatePage.tsx | 9 +- .../outbound-webhooks/EditPage.story.tsx | 10 +- .../site-admin/outbound-webhooks/EditPage.tsx | 13 +- .../OutboundWebhooksPage.story.tsx | 15 +- .../OutboundWebhooksPage.tsx | 3 +- client/web/src/site-admin/routes.tsx | 190 ++++++----------- client/web/src/user/settings/routes.tsx | 1 - client/web/src/util/contributions.ts | 3 +- 51 files changed, 475 insertions(+), 444 deletions(-) create mode 100644 client/web/src/namespaces/routes.ts delete mode 100644 client/web/src/namespaces/routes.tsx diff --git a/client/web/src/enterprise/codeintel/configuration/pages/CodeIntelConfigurationPage.tsx b/client/web/src/enterprise/codeintel/configuration/pages/CodeIntelConfigurationPage.tsx index 10a9841ca762..05925bfc1e9c 100644 --- a/client/web/src/enterprise/codeintel/configuration/pages/CodeIntelConfigurationPage.tsx +++ b/client/web/src/enterprise/codeintel/configuration/pages/CodeIntelConfigurationPage.tsx @@ -190,19 +190,15 @@ const CreatePolicyButtons: FunctionComponent = ({ repo return ( <> - - - diff --git a/client/web/src/enterprise/codeintel/indexes/pages/CodeIntelPreciseIndexesPage.tsx b/client/web/src/enterprise/codeintel/indexes/pages/CodeIntelPreciseIndexesPage.tsx index d647a80b7a19..7d603e341437 100644 --- a/client/web/src/enterprise/codeintel/indexes/pages/CodeIntelPreciseIndexesPage.tsx +++ b/client/web/src/enterprise/codeintel/indexes/pages/CodeIntelPreciseIndexesPage.tsx @@ -387,6 +387,7 @@ interface IndexNodeProps { const IndexNode: FunctionComponent = ({ node, repo, selection, onCheckboxToggle }) => { const navigate = useNavigate() + return ( <>
    = ({ node, repo, selection, o
    +
    diff --git a/client/web/src/enterprise/executors/ExecutorsSiteAdminArea.tsx b/client/web/src/enterprise/executors/ExecutorsSiteAdminArea.tsx index ad4a93d35b97..50acbd9e09fd 100644 --- a/client/web/src/enterprise/executors/ExecutorsSiteAdminArea.tsx +++ b/client/web/src/enterprise/executors/ExecutorsSiteAdminArea.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { Route, Switch } from 'react-router' +import { Route, RouteComponentProps, Switch } from 'react-router' import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' @@ -19,16 +19,19 @@ const GlobalExecutorSecretsListPage = lazyComponent< 'GlobalExecutorSecretsListPage' >(() => import('./secrets/ExecutorSecretsListPage'), 'GlobalExecutorSecretsListPage') -const URL = '/site-admin/executors' +export interface ExecutorsSiteAdminAreaProps extends RouteComponentProps {} /** The page area for all executors settings in site-admin. */ -export const ExecutorsSiteAdminArea: React.FC<{}> = () => ( +export const ExecutorsSiteAdminArea: React.FunctionComponent> = ({ + match, + ...outerProps +}) => ( <> - } path={URL} exact={true} /> + } path={match.url} exact={true} /> } + path={`${match.url}/secrets`} + render={props => } exact={true} /> } key="hardcoded-key" /> diff --git a/client/web/src/enterprise/executors/instances/ExecutorsListPage.tsx b/client/web/src/enterprise/executors/instances/ExecutorsListPage.tsx index baec501764c6..01c0971012c0 100644 --- a/client/web/src/enterprise/executors/instances/ExecutorsListPage.tsx +++ b/client/web/src/enterprise/executors/instances/ExecutorsListPage.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect } from 'react' +import React, { FunctionComponent, useCallback, useEffect } from 'react' import { useApolloClient } from '@apollo/client' import { mdiMapSearch } from '@mdi/js' @@ -43,7 +43,9 @@ export interface ExecutorsListPageProps { queryExecutors?: typeof defaultQueryExecutors } -export const ExecutorsListPage: React.FC = ({ queryExecutors = defaultQueryExecutors }) => { +export const ExecutorsListPage: FunctionComponent> = ({ + queryExecutors = defaultQueryExecutors, +}) => { useEffect(() => eventLogger.logViewEvent('ExecutorsList')) const apolloClient = useApolloClient() diff --git a/client/web/src/enterprise/site-admin/SiteAdminLsifUploadPage.tsx b/client/web/src/enterprise/site-admin/SiteAdminLsifUploadPage.tsx index f891020150f8..3c2df66a7dac 100644 --- a/client/web/src/enterprise/site-admin/SiteAdminLsifUploadPage.tsx +++ b/client/web/src/enterprise/site-admin/SiteAdminLsifUploadPage.tsx @@ -1,6 +1,6 @@ -import { useEffect, useMemo } from 'react' +import { FunctionComponent, useEffect, useMemo } from 'react' -import { Navigate, useParams } from 'react-router-dom-v5-compat' +import { RouteComponentProps, Redirect } from 'react-router' import { catchError } from 'rxjs/operators' import { asError, ErrorLike, isErrorLike } from '@sourcegraph/common' @@ -11,11 +11,16 @@ import { eventLogger } from '../../tracking/eventLogger' import { fetchLsifUpload } from './backend' +interface Props extends RouteComponentProps<{ id: string }> {} + /** * A page displaying metadata about an LSIF upload. */ -export const SiteAdminLsifUploadPage: React.FC<{}> = () => { - const { id = '' } = useParams<{ id: string }>() +export const SiteAdminLsifUploadPage: FunctionComponent> = ({ + match: { + params: { id }, + }, +}) => { useEffect(() => eventLogger.logViewEvent('SiteAdminLsifUpload')) const uploadOrError = useObservable( @@ -32,10 +37,7 @@ export const SiteAdminLsifUploadPage: React.FC<{}> = () => { ) : !uploadOrError.projectRoot ? ( ) : ( - + )}
    ) diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage.tsx index d8d23ee25f9d..42f7079712f6 100644 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage.tsx +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage.tsx @@ -1,7 +1,8 @@ import React, { useCallback, useEffect } from 'react' import { mdiPlus } from '@mdi/js' -import { Navigate } from 'react-router-dom-v5-compat' +import * as H from 'history' +import { Redirect, RouteComponentProps } from 'react-router' import { merge, of, Observable } from 'rxjs' import { catchError, concatMapTo, map, tap } from 'rxjs/operators' @@ -27,6 +28,11 @@ interface UserCreateSubscriptionNodeProps { * The user to display in this list item. */ node: ProductSubscriptionAccountFields + + /** + * Browser history, used to redirect the user to the new subscription after one is successfully created. + */ + history: H.History } const createProductSubscription = ( @@ -79,9 +85,7 @@ const UserCreateSubscriptionNode: React.FunctionComponent - )} + createdSubscription.urlForSiteAdmin && }
  • @@ -115,7 +119,7 @@ const UserCreateSubscriptionNode: React.FunctionComponent { authenticatedUser: AuthenticatedUser } @@ -134,7 +138,7 @@ export const SiteAdminCreateProductSubscriptionPage: React.FunctionComponent<

    Create product subscription

    - + > {...props} className="list-group list-group-flush mt-3" noun="user" diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.test.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.test.tsx index 590c8d2c7f2b..27d1d4882b24 100644 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.test.tsx +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.test.tsx @@ -1,4 +1,5 @@ import { act } from '@testing-library/react' +import * as H from 'history' import { of } from 'rxjs' import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing' @@ -11,10 +12,16 @@ jest.mock('mdi-react/ArrowLeftIcon', () => 'ArrowLeftIcon') jest.mock('mdi-react/AddIcon', () => 'AddIcon') +const history = H.createMemoryHistory() +const location = H.createLocation('/') + describe('SiteAdminProductSubscriptionPage', () => { test('renders', () => { const component = renderWithBrandedContext( of({ __typename: 'ProductSubscription', @@ -74,7 +81,7 @@ describe('SiteAdminProductSubscriptionPage', () => { }) } />, - { route: '/p' } + { history } ) act(() => undefined) expect(component.asFragment()).toMatchSnapshot() diff --git a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx index b359d1f2bbdd..f0d0deabd9f1 100644 --- a/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx +++ b/client/web/src/enterprise/site-admin/dotcom/productSubscriptions/SiteAdminProductSubscriptionPage.tsx @@ -1,7 +1,8 @@ import React, { useState, useMemo, useEffect, useCallback } from 'react' import { mdiArrowLeft, mdiPlus } from '@mdi/js' -import { useNavigate, useParams } from 'react-router-dom-v5-compat' +import * as H from 'history' +import { RouteComponentProps } from 'react-router' import { Observable, Subject, NEVER } from 'rxjs' import { catchError, map, mapTo, startWith, switchMap, tap, filter } from 'rxjs/operators' @@ -45,12 +46,13 @@ import { SiteAdminProductLicenseNodeProps, } from './SiteAdminProductLicenseNode' -interface Props { +interface Props extends RouteComponentProps<{ subscriptionUUID: string }> { /** For mocking in tests only. */ _queryProductSubscription?: typeof queryProductSubscription /** For mocking in tests only. */ _queryProductLicenses?: typeof queryProductLicenses + history: H.History } const LOADING = 'loading' as const @@ -59,11 +61,14 @@ const LOADING = 'loading' as const * Displays a product subscription in the site admin area. */ export const SiteAdminProductSubscriptionPage: React.FunctionComponent> = ({ + history, + location, + match: { + params: { subscriptionUUID }, + }, _queryProductSubscription = queryProductSubscription, _queryProductLicenses = queryProductLicenses, }) => { - const navigate = useNavigate() - const { subscriptionUUID = '' } = useParams<{ subscriptionUUID: string }>() useEffect(() => eventLogger.logViewEvent('SiteAdminProductSubscription'), []) const [showGenerate, setShowGenerate] = useState(false) @@ -99,14 +104,14 @@ export const SiteAdminProductSubscriptionPage: React.FunctionComponent archiveProductSubscription({ id: productSubscription.id }).pipe( mapTo(undefined), - tap(() => navigate('/site-admin/dotcom/product/subscriptions')), + tap(() => history.push('/site-admin/dotcom/product/subscriptions')), catchError(error => [asError(error)]), startWith(LOADING) ) ) ) }, - [navigate, productSubscription] + [history, productSubscription] ) ) diff --git a/client/web/src/enterprise/site-admin/productSubscription/SiteAdminProductSubscriptionPage.tsx b/client/web/src/enterprise/site-admin/productSubscription/SiteAdminProductSubscriptionPage.tsx index 77f09a06f9e8..57d1b2ccab09 100644 --- a/client/web/src/enterprise/site-admin/productSubscription/SiteAdminProductSubscriptionPage.tsx +++ b/client/web/src/enterprise/site-admin/productSubscription/SiteAdminProductSubscriptionPage.tsx @@ -1,5 +1,7 @@ import React, { useEffect } from 'react' +import { RouteComponentProps } from 'react-router' + import { PageTitle } from '../../../components/PageTitle' import { eventLogger } from '../../../tracking/eventLogger' @@ -8,13 +10,15 @@ import { ProductSubscriptionStatus } from './ProductSubscriptionStatus' /** * Displays the product subscription information from the license key in site configuration. */ -export const SiteAdminProductSubscriptionPage: React.FunctionComponent = () => { +export const SiteAdminProductSubscriptionPage: React.FunctionComponent< + React.PropsWithChildren +> = props => { useEffect(() => eventLogger.logViewEvent('SiteAdminProductSubscription'), []) return (
    - +
    ) } diff --git a/client/web/src/enterprise/site-admin/routes.tsx b/client/web/src/enterprise/site-admin/routes.tsx index b059c030452c..bc3788d87aff 100644 --- a/client/web/src/enterprise/site-admin/routes.tsx +++ b/client/web/src/enterprise/site-admin/routes.tsx @@ -1,129 +1,101 @@ -import { Navigate, useLocation, useParams } from 'react-router-dom-v5-compat' +import { Redirect } from 'react-router' import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' import { siteAdminAreaRoutes } from '../../site-admin/routes' import { SiteAdminAreaRoute } from '../../site-admin/SiteAdminArea' import { SHOW_BUSINESS_FEATURES } from '../dotcom/productSubscriptions/features' - -const SiteAdminProductSubscriptionPage = lazyComponent( - () => import('./productSubscription/SiteAdminProductSubscriptionPage'), - 'SiteAdminProductSubscriptionPage' -) -const SiteAdminProductCustomersPage = lazyComponent( - () => import('./dotcom/customers/SiteAdminCustomersPage'), - 'SiteAdminProductCustomersPage' -) -const SiteAdminCreateProductSubscriptionPage = lazyComponent( - () => import('./dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage'), - 'SiteAdminCreateProductSubscriptionPage' -) -const DotComSiteAdminProductSubscriptionPage = lazyComponent( - () => import('./dotcom/productSubscriptions/SiteAdminProductSubscriptionPage'), - 'SiteAdminProductSubscriptionPage' -) -const SiteAdminProductSubscriptionsPage = lazyComponent( - () => import('./dotcom/productSubscriptions/SiteAdminProductSubscriptionsPage'), - 'SiteAdminProductSubscriptionsPage' -) -const SiteAdminProductLicensesPage = lazyComponent( - () => import('./dotcom/productSubscriptions/SiteAdminProductLicensesPage'), - 'SiteAdminProductLicensesPage' -) -const SiteAdminAuthenticationProvidersPage = lazyComponent( - () => import('./SiteAdminAuthenticationProvidersPage'), - 'SiteAdminAuthenticationProvidersPage' -) -const SiteAdminExternalAccountsPage = lazyComponent( - () => import('./SiteAdminExternalAccountsPage'), - 'SiteAdminExternalAccountsPage' -) -const BatchChangesSiteConfigSettingsArea = lazyComponent( - () => import('../batches/settings/BatchChangesSiteConfigSettingsArea'), - 'BatchChangesSiteConfigSettingsArea' -) -const BatchSpecsPage = lazyComponent(() => import('../batches/BatchSpecsPage'), 'BatchSpecsPage') -const WebhookLogPage = lazyComponent(() => import('../../site-admin/webhooks/WebhookLogPage'), 'WebhookLogPage') -const CodeIntelPreciseIndexesPage = lazyComponent( - () => import('../codeintel/indexes/pages/CodeIntelPreciseIndexesPage'), - 'CodeIntelPreciseIndexesPage' -) -const CodeIntelPreciseIndexPage = lazyComponent( - () => import('../codeintel/indexes/pages/CodeIntelPreciseIndexPage'), - 'CodeIntelPreciseIndexPage' -) -const CodeIntelConfigurationPage = lazyComponent( - () => import('../codeintel/configuration/pages/CodeIntelConfigurationPage'), - 'CodeIntelConfigurationPage' -) -const CodeIntelConfigurationPolicyPage = lazyComponent( - () => import('../codeintel/configuration/pages/CodeIntelConfigurationPolicyPage'), - 'CodeIntelConfigurationPolicyPage' -) -const CodeIntelInferenceConfigurationPage = lazyComponent( - () => import('../codeintel/configuration/pages/CodeIntelInferenceConfigurationPage'), - 'CodeIntelInferenceConfigurationPage' -) -const SiteAdminLsifUploadPage = lazyComponent(() => import('./SiteAdminLsifUploadPage'), 'SiteAdminLsifUploadPage') -const ExecutorsSiteAdminArea = lazyComponent( - () => import('../executors/ExecutorsSiteAdminArea'), - 'ExecutorsSiteAdminArea' -) +import type { ExecutorsSiteAdminAreaProps } from '../executors/ExecutorsSiteAdminArea' export const enterpriseSiteAdminAreaRoutes: readonly SiteAdminAreaRoute[] = ( [ ...siteAdminAreaRoutes, { path: '/license', - render: () => , + render: lazyComponent( + () => import('./productSubscription/SiteAdminProductSubscriptionPage'), + 'SiteAdminProductSubscriptionPage' + ), + exact: true, }, { path: '/dotcom/customers', - render: () => , + render: lazyComponent( + () => import('./dotcom/customers/SiteAdminCustomersPage'), + 'SiteAdminProductCustomersPage' + ), condition: () => SHOW_BUSINESS_FEATURES, + exact: true, }, { path: '/dotcom/product/subscriptions/new', - render: props => , + render: lazyComponent( + () => import('./dotcom/productSubscriptions/SiteAdminCreateProductSubscriptionPage'), + 'SiteAdminCreateProductSubscriptionPage' + ), condition: () => SHOW_BUSINESS_FEATURES, + exact: true, }, { path: '/dotcom/product/subscriptions/:subscriptionUUID', - render: () => , + render: lazyComponent( + () => import('./dotcom/productSubscriptions/SiteAdminProductSubscriptionPage'), + 'SiteAdminProductSubscriptionPage' + ), condition: () => SHOW_BUSINESS_FEATURES, + exact: true, }, { path: '/dotcom/product/subscriptions', - render: () => , + render: lazyComponent( + () => import('./dotcom/productSubscriptions/SiteAdminProductSubscriptionsPage'), + 'SiteAdminProductSubscriptionsPage' + ), condition: () => SHOW_BUSINESS_FEATURES, + exact: true, }, { path: '/dotcom/product/licenses', - render: () => , + render: lazyComponent( + () => import('./dotcom/productSubscriptions/SiteAdminProductLicensesPage'), + 'SiteAdminProductLicensesPage' + ), condition: () => SHOW_BUSINESS_FEATURES, + exact: true, }, { path: '/auth/providers', - render: () => , + render: lazyComponent( + () => import('./SiteAdminAuthenticationProvidersPage'), + 'SiteAdminAuthenticationProvidersPage' + ), + exact: true, }, { path: '/auth/external-accounts', - render: () => , + render: lazyComponent(() => import('./SiteAdminExternalAccountsPage'), 'SiteAdminExternalAccountsPage'), + exact: true, }, { path: '/batch-changes', - render: () => , + exact: true, + render: lazyComponent( + () => import('../batches/settings/BatchChangesSiteConfigSettingsArea'), + 'BatchChangesSiteConfigSettingsArea' + ), condition: ({ batchChangesEnabled }) => batchChangesEnabled, }, { path: '/batch-changes/specs', - render: props => , + exact: true, + render: lazyComponent(() => import('../batches/BatchSpecsPage'), 'BatchSpecsPage'), condition: ({ batchChangesEnabled, batchChangesExecutionEnabled }) => batchChangesEnabled && batchChangesExecutionEnabled, }, { path: '/batch-changes/webhook-logs', - render: () => , + exact: true, + render: lazyComponent(() => import('../../site-admin/webhooks/WebhookLogPage'), 'WebhookLogPage'), condition: ({ batchChangesEnabled, batchChangesWebhookLogsEnabled }) => batchChangesEnabled && batchChangesWebhookLogsEnabled, }, @@ -131,71 +103,80 @@ export const enterpriseSiteAdminAreaRoutes: readonly SiteAdminAreaRoute[] = ( // Code intelligence redirect { path: '/code-intelligence', - render: () => , - }, - { - path: '/code-intelligence/*', - render: () => , + exact: false, + render: props => , }, // Precise index routes { path: '/code-graph/indexes', - render: props => , + render: lazyComponent( + () => import('../codeintel/indexes/pages/CodeIntelPreciseIndexesPage'), + 'CodeIntelPreciseIndexesPage' + ), + exact: true, }, { path: '/code-graph/indexes/:id', - render: props => , + render: lazyComponent( + () => import('../codeintel/indexes/pages/CodeIntelPreciseIndexPage'), + 'CodeIntelPreciseIndexPage' + ), + exact: true, }, // Code graph configuration { path: '/code-graph/configuration', - render: props => , + render: lazyComponent( + () => import('../codeintel/configuration/pages/CodeIntelConfigurationPage'), + 'CodeIntelConfigurationPage' + ), + exact: true, }, { path: '/code-graph/configuration/:id', - render: props => , + render: lazyComponent( + () => import('../codeintel/configuration/pages/CodeIntelConfigurationPolicyPage'), + 'CodeIntelConfigurationPolicyPage' + ), + exact: true, }, { path: '/code-graph/inference-configuration', - render: props => , + render: lazyComponent( + () => import('../codeintel/configuration/pages/CodeIntelInferenceConfigurationPage'), + 'CodeIntelInferenceConfigurationPage' + ), + exact: true, }, // Legacy routes { path: '/code-graph/uploads/:id', - render: () => , + render: props => ( + + ), + exact: true, }, { path: '/lsif-uploads/:id', - render: () => , + render: lazyComponent(() => import('./SiteAdminLsifUploadPage'), 'SiteAdminLsifUploadPage'), + exact: true, }, // Executor routes { path: '/executors', - render: () => , - condition: () => Boolean(window.context?.executorsEnabled), - }, - { - path: '/executors/*', - render: () => , + render: lazyComponent( + () => import('../executors/ExecutorsSiteAdminArea'), + 'ExecutorsSiteAdminArea' + ), condition: () => Boolean(window.context?.executorsEnabled), }, ] as readonly (SiteAdminAreaRoute | undefined)[] ).filter(Boolean) as readonly SiteAdminAreaRoute[] - -function NavigateToCodeGraph(): JSX.Element { - const location = useLocation() - return -} - -function NavigateToLegacyUploadPage(): JSX.Element { - const { id = '' } = useParams<{ id: string }>() - return ( - - ) -} diff --git a/client/web/src/namespaces/routes.ts b/client/web/src/namespaces/routes.ts new file mode 100644 index 000000000000..281fa91b88ef --- /dev/null +++ b/client/web/src/namespaces/routes.ts @@ -0,0 +1,19 @@ +import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' + +import { NamespaceAreaRoute } from './NamespaceArea' + +export const namespaceAreaRoutes: readonly NamespaceAreaRoute[] = [ + { + path: '/searches', + exact: true, + render: lazyComponent(() => import('../savedSearches/SavedSearchListPage'), 'SavedSearchListPage'), + }, + { + path: '/searches/add', + render: lazyComponent(() => import('../savedSearches/SavedSearchCreateForm'), 'SavedSearchCreateForm'), + }, + { + path: '/searches/:id', + render: lazyComponent(() => import('../savedSearches/SavedSearchUpdateForm'), 'SavedSearchUpdateForm'), + }, +] diff --git a/client/web/src/namespaces/routes.tsx b/client/web/src/namespaces/routes.tsx deleted file mode 100644 index 8b30aac0b547..000000000000 --- a/client/web/src/namespaces/routes.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' - -import { NamespaceAreaRoute } from './NamespaceArea' - -const SavedSearchListPage = lazyComponent(() => import('../savedSearches/SavedSearchListPage'), 'SavedSearchListPage') - -const SavedSearchCreateForm = lazyComponent( - () => import('../savedSearches/SavedSearchCreateForm'), - 'SavedSearchCreateForm' -) -const SavedSearchUpdateForm = lazyComponent( - () => import('../savedSearches/SavedSearchUpdateForm'), - 'SavedSearchUpdateForm' -) - -export const namespaceAreaRoutes: readonly NamespaceAreaRoute[] = [ - { - path: '/searches', - render: props => , - // TODO: Remove once RR6 migration is finished. For now these work on both RR5 and RR6. - exact: true, - }, - { - path: '/searches/add', - render: props => , - // TODO: Remove once RR6 migration is finished. For now these work on both RR5 and RR6. - exact: true, - }, - { - path: '/searches/:id', - render: props => , - // TODO: Remove once RR6 migration is finished. For now these work on both RR5 and RR6. - exact: true, - }, -] diff --git a/client/web/src/org/settings/routes.tsx b/client/web/src/org/settings/routes.tsx index c7f9d3604074..6d3a1edea207 100644 --- a/client/web/src/org/settings/routes.tsx +++ b/client/web/src/org/settings/routes.tsx @@ -16,7 +16,6 @@ export const orgSettingsAreaRoutes: readonly OrgSettingsAreaRoute[] = [
    diff --git a/client/web/src/settings/SettingsArea.tsx b/client/web/src/settings/SettingsArea.tsx index ec5bcb25af9e..d290e5063da3 100644 --- a/client/web/src/settings/SettingsArea.tsx +++ b/client/web/src/settings/SettingsArea.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import classNames from 'classnames' import AlertCircleIcon from 'mdi-react/AlertCircleIcon' -import { Route, Switch } from 'react-router' +import { Route, RouteComponentProps, Switch } from 'react-router' import { combineLatest, Observable, of, Subject, Subscription } from 'rxjs' import { catchError, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators' @@ -50,10 +50,9 @@ export interface SettingsAreaPageProps extends SettingsAreaPageCommonProps { onUpdate: () => void } -interface Props extends SettingsAreaPageCommonProps { +interface Props extends SettingsAreaPageCommonProps, RouteComponentProps<{}> { className?: string extraHeader?: JSX.Element - url: string } const LOADING = 'loading' as const @@ -174,7 +173,7 @@ export class SettingsArea extends React.Component { {this.props.extraHeader} } diff --git a/client/web/src/site-admin/SiteAdminArea.tsx b/client/web/src/site-admin/SiteAdminArea.tsx index 148ef486a90d..f53952867d89 100644 --- a/client/web/src/site-admin/SiteAdminArea.tsx +++ b/client/web/src/site-admin/SiteAdminArea.tsx @@ -2,7 +2,8 @@ import React, { useRef } from 'react' import classNames from 'classnames' import MapSearchIcon from 'mdi-react/MapSearchIcon' -import { useLocation, Routes, Route } from 'react-router-dom-v5-compat' +import { Route, Switch } from 'react-router' +import { useLocation } from 'react-router-dom-v5-compat' import { SiteSettingFields } from '@sourcegraph/shared/src/graphql-operations' import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context' @@ -16,7 +17,7 @@ import { BatchChangesProps } from '../batches' import { ErrorBoundary } from '../components/ErrorBoundary' import { HeroPage } from '../components/HeroPage' import { Page } from '../components/Page' -import { RouteV6Descriptor } from '../util/contributions' +import { RouteDescriptor } from '../util/contributions' import { SiteAdminSidebar, SiteAdminSideBarGroups } from './SiteAdminSidebar' @@ -48,7 +49,7 @@ export interface SiteAdminAreaRouteContext overviewComponents: readonly React.ComponentType>[] } -export interface SiteAdminAreaRoute extends RouteV6Descriptor {} +export interface SiteAdminAreaRoute extends RouteDescriptor {} interface SiteAdminAreaProps extends PlatformContextProps, SettingsCascadeProps, BatchChangesProps, TelemetryProps { routes: readonly SiteAdminAreaRoute[] @@ -101,20 +102,23 @@ const AuthenticatedSiteAdminArea: React.FunctionComponent }> - + {props.routes.map( - ({ render, path, condition = () => true }) => + ({ render, path, exact, condition = () => true }) => condition(context) && ( + render({ ...context, ...routeComponentProps }) + } /> ) )} - } /> - + +
    diff --git a/client/web/src/site-admin/SiteAdminBackgroundJobsPage.tsx b/client/web/src/site-admin/SiteAdminBackgroundJobsPage.tsx index 231e1993a0e0..82ed19e01b80 100644 --- a/client/web/src/site-admin/SiteAdminBackgroundJobsPage.tsx +++ b/client/web/src/site-admin/SiteAdminBackgroundJobsPage.tsx @@ -12,6 +12,7 @@ import { mdiShape, } from '@mdi/js' import format from 'date-fns/format' +import { RouteComponentProps } from 'react-router' import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp' import { pluralize } from '@sourcegraph/common' @@ -40,7 +41,7 @@ import { BACKGROUND_JOBS, BACKGROUND_JOBS_PAGE_POLL_INTERVAL_MS } from './backen import styles from './SiteAdminBackgroundJobsPage.module.scss' -export interface SiteAdminBackgroundJobsPageProps extends TelemetryProps {} +export interface SiteAdminBackgroundJobsPageProps extends RouteComponentProps, TelemetryProps {} export type BackgroundJob = BackgroundJobsResult['backgroundJobs']['nodes'][0] export type BackgroundRoutine = BackgroundJob['routines'][0] diff --git a/client/web/src/site-admin/SiteAdminCreateUserPage.tsx b/client/web/src/site-admin/SiteAdminCreateUserPage.tsx index e42616171fa0..09fbeccafb0d 100644 --- a/client/web/src/site-admin/SiteAdminCreateUserPage.tsx +++ b/client/web/src/site-admin/SiteAdminCreateUserPage.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import classNames from 'classnames' +import { RouteComponentProps } from 'react-router' import { Subject, Subscription } from 'rxjs' import { catchError, mergeMap, tap } from 'rxjs/operators' @@ -34,7 +35,7 @@ interface State { /** * A page with a form to create a user account. */ -export class SiteAdminCreateUserPage extends React.Component<{}, State> { +export class SiteAdminCreateUserPage extends React.Component, State> { public state: State = { loading: false, username: '', diff --git a/client/web/src/site-admin/SiteAdminExternalServicesArea.tsx b/client/web/src/site-admin/SiteAdminExternalServicesArea.tsx index 829e30b91fe1..ae6e7913652e 100644 --- a/client/web/src/site-admin/SiteAdminExternalServicesArea.tsx +++ b/client/web/src/site-admin/SiteAdminExternalServicesArea.tsx @@ -34,14 +34,19 @@ const AddExternalServicesPage = lazyComponent( 'AddExternalServicesPage' ) -interface Props extends ThemeProps, TelemetryProps, PlatformContextProps, SettingsCascadeProps { +interface Props + extends RouteComponentProps<{}>, + ThemeProps, + TelemetryProps, + PlatformContextProps, + SettingsCascadeProps { authenticatedUser: AuthenticatedUser } export const SiteAdminExternalServicesArea: React.FunctionComponent> = ({ + match, ...outerProps }) => { - const url = '/external-services' const { data, error, loading } = useQuery( SITE_EXTERNAL_SERVICE_CONFIG, {} @@ -62,7 +67,7 @@ export const SiteAdminExternalServicesArea: React.FunctionComponent ( - } exact={true} /> + } exact={true} /> ( ) => ( ) => ( , TelemetryProps { fetchFeatureFlags?: typeof defaultFetchFeatureFlags productVersion?: string } export const SiteAdminFeatureFlagConfigurationPage: FunctionComponent< React.PropsWithChildren -> = ({ fetchFeatureFlags = defaultFetchFeatureFlags, productVersion = window.context.version }) => { - const { name = '' } = useParams<{ name: string }>() - const navigate = useNavigate() +> = ({ match: { params }, fetchFeatureFlags = defaultFetchFeatureFlags, productVersion = window.context.version }) => { + const history = useHistory() const productGitVersion = parseProductReference(productVersion) - const isCreateFeatureFlag = name === 'new' + const isCreateFeatureFlag = params.name === 'new' // Load the initial feature flag, unless we are creating a new feature flag. const featureFlagOrError = useObservable( @@ -59,16 +58,16 @@ export const SiteAdminFeatureFlagConfigurationPage: FunctionComponent< isCreateFeatureFlag ? of(undefined) : fetchFeatureFlags().pipe( - map(flags => flags.find(flag => flag.name === name)), + map(flags => flags.find(flag => flag.name === params.name)), map(flag => { if (flag === undefined) { - throw new Error(`Could not find feature flag with name '${name}'.`) + throw new Error(`Could not find feature flag with name '${params.name}'.`) } return flag }), catchError((error): [ErrorLike] => [asError(error)]) ), - [isCreateFeatureFlag, name, fetchFeatureFlags] + [isCreateFeatureFlag, params.name, fetchFeatureFlags] ) ) @@ -128,7 +127,7 @@ export const SiteAdminFeatureFlagConfigurationPage: FunctionComponent< ...flagValue, }, }).then(() => { - navigate(`/site-admin/feature-flags/configuration/${flagName || 'new'}`) + history.push(`./${flagName || 'new'}`) }) } > @@ -168,7 +167,7 @@ export const SiteAdminFeatureFlagConfigurationPage: FunctionComponent< ...flagValue, }, }).then(() => { - navigate(`/site-admin/feature-flags/configuration/${flagName}`) + history.push(`./${flagName}`) }) } > @@ -191,7 +190,7 @@ export const SiteAdminFeatureFlagConfigurationPage: FunctionComponent< name: flagName, }, }).then(() => { - navigate('/site-admin/feature-flags') + history.push('../') }) } > @@ -237,12 +236,7 @@ export const SiteAdminFeatureFlagConfigurationPage: FunctionComponent<
    {actions} -
    diff --git a/client/web/src/site-admin/SiteAdminPingsPage.tsx b/client/web/src/site-admin/SiteAdminPingsPage.tsx index 16f503ec325a..5b134bcce7bc 100644 --- a/client/web/src/site-admin/SiteAdminPingsPage.tsx +++ b/client/web/src/site-admin/SiteAdminPingsPage.tsx @@ -6,6 +6,7 @@ import { search, searchKeymap } from '@codemirror/search' import { EditorState } from '@codemirror/state' import { EditorView, keymap } from '@codemirror/view' import { isEmpty } from 'lodash' +import { RouteComponentProps } from 'react-router-dom' import { fromFetch } from 'rxjs/fetch' import { checkOk } from '@sourcegraph/http-client' @@ -21,7 +22,7 @@ import { LoadingSpinner, H2, H3, Text, useObservable } from '@sourcegraph/wildca import { PageTitle } from '../components/PageTitle' import { eventLogger } from '../tracking/eventLogger' -interface Props extends ThemeProps {} +interface Props extends RouteComponentProps, ThemeProps {} /** * A page displaying information about telemetry pings for the site. diff --git a/client/web/src/site-admin/SiteAdminReportBugPage.tsx b/client/web/src/site-admin/SiteAdminReportBugPage.tsx index 6dc0b109b3e6..bbb531f61462 100644 --- a/client/web/src/site-admin/SiteAdminReportBugPage.tsx +++ b/client/web/src/site-admin/SiteAdminReportBugPage.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react' import { mapValues, values } from 'lodash' +import { RouteComponentProps } from 'react-router' import { ExternalServiceKind } from '@sourcegraph/shared/src/graphql-operations' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -109,11 +110,12 @@ const allConfigSchema = { .reduce((allDefinitions, definitions) => ({ ...allDefinitions, ...definitions }), {}), } -interface Props extends ThemeProps, TelemetryProps {} +interface Props extends RouteComponentProps, ThemeProps, TelemetryProps {} export const SiteAdminReportBugPage: React.FunctionComponent> = ({ isLightTheme, telemetryService, + history, }) => { const allConfig = useObservable(useMemo(fetchAllConfigAndSettings, [])) return ( diff --git a/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx b/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx index 92aca3e9c893..ddfb796840f6 100644 --- a/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx +++ b/client/web/src/site-admin/SiteAdminRepositoriesPage.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react' import { mdiCloudDownload, mdiCog, mdiBrain } from '@mdi/js' import { isEqual } from 'lodash' -import { useLocation, useNavigate } from 'react-router-dom-v5-compat' +import { RouteComponentProps } from 'react-router' import { logger } from '@sourcegraph/common' import { useQuery } from '@sourcegraph/http-client' @@ -109,7 +109,7 @@ const RepositoryNode: React.FunctionComponent ) -interface Props extends TelemetryProps {} +interface Props extends RouteComponentProps<{}>, TelemetryProps {} const STATUS_FILTERS: { [label: string]: FilteredConnectionFilterValue } = { All: { @@ -218,11 +218,10 @@ const FILTERS: FilteredConnectionFilter[] = [ * A page displaying the repositories on this site. */ export const SiteAdminRepositoriesPage: React.FunctionComponent> = ({ + history, + location, telemetryService, }) => { - const location = useLocation() - const navigate = useNavigate() - useEffect(() => { telemetryService.logPageView('SiteAdminRepos') }, [telemetryService]) @@ -416,19 +415,14 @@ export const SiteAdminRepositoriesPage: React.FunctionComponent(() => { const args = buildFilterArgs(filterValues) diff --git a/client/web/src/site-admin/SiteAdminSettingsPage.tsx b/client/web/src/site-admin/SiteAdminSettingsPage.tsx index cad0c64ac4ff..6304bcfc31dc 100644 --- a/client/web/src/site-admin/SiteAdminSettingsPage.tsx +++ b/client/web/src/site-admin/SiteAdminSettingsPage.tsx @@ -1,5 +1,7 @@ import * as React from 'react' +import { RouteComponentProps } from 'react-router' + import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context' import { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -11,7 +13,12 @@ import { PageTitle } from '../components/PageTitle' import { SiteResult } from '../graphql-operations' import { SettingsArea } from '../settings/SettingsArea' -interface Props extends PlatformContextProps, SettingsCascadeProps, ThemeProps, TelemetryProps { +interface Props + extends RouteComponentProps<{}>, + PlatformContextProps, + SettingsCascadeProps, + ThemeProps, + TelemetryProps { authenticatedUser: AuthenticatedUser site: Pick } @@ -21,7 +28,6 @@ export const SiteAdminSettingsPage: React.FunctionComponent { {() => ( - + )} @@ -79,7 +85,12 @@ export const WebhookCreatePageWithError: Story = () => { {() => ( - + )} diff --git a/client/web/src/site-admin/SiteAdminWebhookCreatePage.tsx b/client/web/src/site-admin/SiteAdminWebhookCreatePage.tsx index 0a9a2f1ad054..5720f2a648a4 100644 --- a/client/web/src/site-admin/SiteAdminWebhookCreatePage.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookCreatePage.tsx @@ -1,6 +1,7 @@ import { FC, useEffect } from 'react' import { mdiCog } from '@mdi/js' +import { RouteComponentProps } from 'react-router' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' import { Container, PageHeader } from '@sourcegraph/wildcard' @@ -9,9 +10,9 @@ import { PageTitle } from '../components/PageTitle' import { WebhookCreateUpdatePage } from './WebhookCreateUpdatePage' -export interface SiteAdminWebhookCreatePageProps extends TelemetryProps {} +export interface SiteAdminWebhookCreatePageProps extends TelemetryProps, RouteComponentProps<{}> {} -export const SiteAdminWebhookCreatePage: FC = ({ telemetryService }) => { +export const SiteAdminWebhookCreatePage: FC = ({ telemetryService, history }) => { useEffect(() => { telemetryService.logPageView('SiteAdminWebhookCreatePage') }, [telemetryService]) @@ -24,7 +25,7 @@ export const SiteAdminWebhookCreatePage: FC = ( className="mb-3" headingElement="h2" /> - + ) } diff --git a/client/web/src/site-admin/SiteAdminWebhookPage.story.tsx b/client/web/src/site-admin/SiteAdminWebhookPage.story.tsx index 9f1044044056..1783e02fff8d 100644 --- a/client/web/src/site-admin/SiteAdminWebhookPage.story.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookPage.story.tsx @@ -1,6 +1,6 @@ import { DecoratorFn, Meta, Story } from '@storybook/react' import { addMinutes, formatRFC3339 } from 'date-fns' -import { Route, Routes } from 'react-router-dom-v5-compat' +import * as H from 'history' import { MATCH_ANY_PARAMETERS, WildcardMockLink } from 'wildcard-mock-link' import { getDocumentNode } from '@sourcegraph/http-client' @@ -53,7 +53,7 @@ export const SiteAdminWebhookPageStory: Story = args => { after: null, onlyErrors: false, onlyUnmatched: false, - webhookID: '', + webhookID: '1', }, }, result: { @@ -108,15 +108,15 @@ export const SiteAdminWebhookPageStory: Story = args => { ]) return ( - + {() => ( - - } - /> - + )} @@ -124,6 +124,13 @@ export const SiteAdminWebhookPageStory: Story = args => { } SiteAdminWebhookPageStory.storyName = 'Incoming webhook' +SiteAdminWebhookPageStory.args = { + match: { + params: { + id: '1', + }, + }, +} export const SiteAdminWebhookPageWithoutLogsStory: Story = args => { const buildWebhookLogsMock = new WildcardMockLink([ @@ -131,7 +138,7 @@ export const SiteAdminWebhookPageWithoutLogsStory: Story = args => { request: { query: getDocumentNode(WEBHOOK_BY_ID), variables: { - id: '', + id: '1', }, }, result: { @@ -161,7 +168,7 @@ export const SiteAdminWebhookPageWithoutLogsStory: Story = args => { request: { query: getDocumentNode(WEBHOOK_BY_ID_LOG_PAGE_HEADER), variables: { - webhookID: '', + webhookID: '1', }, }, result: { @@ -179,7 +186,12 @@ export const SiteAdminWebhookPageWithoutLogsStory: Story = args => { {() => ( - + )} @@ -187,6 +199,13 @@ export const SiteAdminWebhookPageWithoutLogsStory: Story = args => { } SiteAdminWebhookPageWithoutLogsStory.storyName = 'Incoming webhook without logs' +SiteAdminWebhookPageWithoutLogsStory.args = { + match: { + params: { + id: '1', + }, + }, +} function buildWebhookLogs(): WebhookLogFields[] { const logs: WebhookLogFields[] = [] diff --git a/client/web/src/site-admin/SiteAdminWebhookPage.tsx b/client/web/src/site-admin/SiteAdminWebhookPage.tsx index 8ceaf868e332..6893b0628130 100644 --- a/client/web/src/site-admin/SiteAdminWebhookPage.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookPage.tsx @@ -2,7 +2,7 @@ import { FC, useEffect, useState } from 'react' import { mdiCog, mdiDelete } from '@mdi/js' import { noop } from 'lodash' -import { useNavigate, useParams } from 'react-router-dom-v5-compat' +import { RouteComponentProps } from 'react-router' import { useMutation } from '@sourcegraph/http-client' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -40,13 +40,16 @@ import { WebhookLogNode } from './webhooks/WebhookLogNode' import styles from './SiteAdminWebhookPage.module.scss' -export interface WebhookPageProps extends TelemetryProps {} +export interface WebhookPageProps extends TelemetryProps, RouteComponentProps<{ id: string }> {} export const SiteAdminWebhookPage: FC = props => { - const { telemetryService } = props - - const { id = '' } = useParams<{ id: string }>() - const navigate = useNavigate() + const { + match: { + params: { id }, + }, + telemetryService, + history, + } = props const [onlyErrors, setOnlyErrors] = useState(false) const { loading, hasNextPage, fetchMore, connection, error } = useWebhookLogsConnection(id, 20, onlyErrors) @@ -59,7 +62,7 @@ export const SiteAdminWebhookPage: FC = props => { const [deleteWebhook, { error: deleteError, loading: isDeleting }] = useMutation< DeleteWebhookResult, DeleteWebhookVariables - >(DELETE_WEBHOOK, { variables: { hookID: id }, onCompleted: () => navigate('/site-admin/webhooks') }) + >(DELETE_WEBHOOK, { variables: { hookID: id }, onCompleted: () => history.push('/site-admin/webhooks') }) return ( diff --git a/client/web/src/site-admin/SiteAdminWebhookUpdatePage.story.tsx b/client/web/src/site-admin/SiteAdminWebhookUpdatePage.story.tsx index 1bb166063c7b..08836b9d7c1a 100644 --- a/client/web/src/site-admin/SiteAdminWebhookUpdatePage.story.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookUpdatePage.story.tsx @@ -1,5 +1,5 @@ import { DecoratorFn, Meta, Story } from '@storybook/react' -import { Route, Routes } from 'react-router-dom-v5-compat' +import * as H from 'history' import { WildcardMockLink } from 'wildcard-mock-link' import { getDocumentNode } from '@sourcegraph/http-client' @@ -23,8 +23,8 @@ const config: Meta = { export default config -export const WebhookUpdatePage: Story = () => ( - +export const WebhookUpdatePage: Story = args => ( + {() => ( ( ]) } > - - } - /> - + )} ) WebhookUpdatePage.storyName = 'Update webhook' +WebhookUpdatePage.args = { + match: { + params: { + id: '1', + }, + }, +} diff --git a/client/web/src/site-admin/SiteAdminWebhookUpdatePage.tsx b/client/web/src/site-admin/SiteAdminWebhookUpdatePage.tsx index 07cd821b439a..47105216f2c9 100644 --- a/client/web/src/site-admin/SiteAdminWebhookUpdatePage.tsx +++ b/client/web/src/site-admin/SiteAdminWebhookUpdatePage.tsx @@ -1,7 +1,7 @@ import { FC, useEffect } from 'react' import { mdiCog } from '@mdi/js' -import { useParams } from 'react-router-dom-v5-compat' +import { RouteComponentProps } from 'react-router' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' import { Container, PageHeader } from '@sourcegraph/wildcard' @@ -13,15 +13,19 @@ import { PageTitle } from '../components/PageTitle' import { useWebhookQuery } from './backend' import { WebhookCreateUpdatePage } from './WebhookCreateUpdatePage' -export interface SiteAdminWebhookUpdatePageProps extends TelemetryProps {} +export interface SiteAdminWebhookUpdatePageProps extends TelemetryProps, RouteComponentProps<{ id: string }> {} -export const SiteAdminWebhookUpdatePage: FC = ({ telemetryService }) => { +export const SiteAdminWebhookUpdatePage: FC = ({ + match: { + params: { id }, + }, + telemetryService, + history, +}) => { useEffect(() => { telemetryService.logPageView('SiteAdminWebhookUpdatePage') }, [telemetryService]) - const { id = '' } = useParams<{ id: string }>() - const { loading, data } = useWebhookQuery(id) const webhook = data?.node && data.node.__typename === 'Webhook' ? data.node : undefined @@ -48,7 +52,7 @@ export const SiteAdminWebhookUpdatePage: FC = ( className="mb-3" headingElement="h2" /> - + )} diff --git a/client/web/src/site-admin/SiteAdminWebhooksPage.story.tsx b/client/web/src/site-admin/SiteAdminWebhooksPage.story.tsx index c2ccd669164f..6456289e6c7a 100644 --- a/client/web/src/site-admin/SiteAdminWebhooksPage.story.tsx +++ b/client/web/src/site-admin/SiteAdminWebhooksPage.story.tsx @@ -1,4 +1,5 @@ import { DecoratorFn, Meta, Story } from '@storybook/react' +import * as H from 'history' import { MATCH_ANY_PARAMETERS, WildcardMockLink } from 'wildcard-mock-link' import { getDocumentNode } from '@sourcegraph/http-client' @@ -65,7 +66,12 @@ export const NoWebhooksFound: Story = () => ( ]) } > - + )} @@ -169,7 +175,12 @@ export const FiveWebhooksFound: Story = () => ( ]) } > - + )} diff --git a/client/web/src/site-admin/SiteAdminWebhooksPage.tsx b/client/web/src/site-admin/SiteAdminWebhooksPage.tsx index 4b15c4c3db8b..15f1d05816dc 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 { mdiCog, mdiMapSearch, mdiPlus } from '@mdi/js' +import { RouteComponentProps } from 'react-router' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' import { ButtonLink, Container, H5, Icon, PageHeader } from '@sourcegraph/wildcard' @@ -22,7 +23,7 @@ import { PerformanceGauge } from './webhooks/PerformanceGauge' import styles from './SiteAdminWebhooksPage.module.scss' -interface Props extends TelemetryProps {} +interface Props extends RouteComponentProps<{}>, TelemetryProps {} export const SiteAdminWebhooksPage: React.FunctionComponent> = ({ telemetryService, diff --git a/client/web/src/site-admin/UserManagement/index.tsx b/client/web/src/site-admin/UserManagement/index.tsx index 0d37e52abe7f..22272f5ab638 100644 --- a/client/web/src/site-admin/UserManagement/index.tsx +++ b/client/web/src/site-admin/UserManagement/index.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useMemo } from 'react' import { mdiAccount, mdiPlus, mdiDownload } from '@mdi/js' +import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { H1, Card, Text, Icon, Button, Link, Alert, LoadingSpinner, AnchorLink } from '@sourcegraph/wildcard' @@ -14,7 +15,7 @@ import { USERS_MANAGEMENT_SUMMARY } from './queries' import styles from './index.module.scss' -export const UsersManagement: React.FunctionComponent = () => { +export const UsersManagement: React.FunctionComponent> = () => { useEffect(() => { eventLogger.logPageView('UsersManagement') }, []) diff --git a/client/web/src/site-admin/WebhookCreateUpdatePage.tsx b/client/web/src/site-admin/WebhookCreateUpdatePage.tsx index fc922ef762a6..fc39d583a25f 100644 --- a/client/web/src/site-admin/WebhookCreateUpdatePage.tsx +++ b/client/web/src/site-admin/WebhookCreateUpdatePage.tsx @@ -3,7 +3,7 @@ import React, { 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-v5-compat' +import { RouteComponentProps } from 'react-router' import { useMutation, useQuery } from '@sourcegraph/http-client' import { Alert, Button, ButtonLink, H2, Input, Select, ErrorAlert, Form } from '@sourcegraph/wildcard' @@ -27,7 +27,7 @@ import { CREATE_WEBHOOK_QUERY, UPDATE_WEBHOOK_QUERY } from './backend' import styles from './WebhookCreateUpdatePage.module.scss' -interface WebhookCreateUpdatePageProps { +interface WebhookCreateUpdatePageProps extends Pick { // existingWebhook is present when this page is used as an update page. existingWebhook?: WebhookFields } @@ -39,8 +39,7 @@ export interface Webhook { secret: string | null } -export const WebhookCreateUpdatePage: FC = ({ existingWebhook }) => { - const navigate = useNavigate() +export const WebhookCreateUpdatePage: FC = ({ history, existingWebhook }) => { const update = existingWebhook !== undefined const initialWebhook = update ? { @@ -134,14 +133,14 @@ export const WebhookCreateUpdatePage: FC = ({ exis const [createWebhook, { error: createWebhookError, loading: creationLoading }] = useMutation< CreateWebhookResult, CreateWebhookVariables - >(CREATE_WEBHOOK_QUERY, { onCompleted: data => navigate(`/site-admin/webhooks/${data.createWebhook.id}`) }) + >(CREATE_WEBHOOK_QUERY, { onCompleted: data => history.push(`/site-admin/webhooks/${data.createWebhook.id}`) }) const [updateWebhook, { error: updateWebhookError, loading: updateLoading }] = useMutation< UpdateWebhookResult, UpdateWebhookVariables >(UPDATE_WEBHOOK_QUERY, { variables: buildUpdateWebhookVariables(webhook, existingWebhook?.id), - onCompleted: data => navigate(`/site-admin/webhooks/${data.updateWebhook.id}`), + onCompleted: data => history.push(`/site-admin/webhooks/${data.updateWebhook.id}`), }) return ( diff --git a/client/web/src/site-admin/analytics/AnalyticsBatchChangesPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsBatchChangesPage/index.tsx index 721568f79df2..054b74993fab 100644 --- a/client/web/src/site-admin/analytics/AnalyticsBatchChangesPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsBatchChangesPage/index.tsx @@ -1,6 +1,7 @@ import React, { useMemo, useEffect } from 'react' import { startCase } from 'lodash' +import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { Card, LoadingSpinner, H2, Text, LineChart, Series } from '@sourcegraph/wildcard' @@ -19,7 +20,7 @@ import { BATCHCHANGES_STATISTICS } from './queries' export const DEFAULT_MINS_SAVED_PER_CHANGESET = 15 -export const AnalyticsBatchChangesPage: React.FunctionComponent = () => { +export const AnalyticsBatchChangesPage: React.FunctionComponent> = () => { const { dateRange, grouping } = useChartFilters({ name: 'BatchChanges' }) const { data, error, loading } = useQuery( BATCHCHANGES_STATISTICS, diff --git a/client/web/src/site-admin/analytics/AnalyticsCodeInsightsPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsCodeInsightsPage/index.tsx index 5ca0e2e46ca7..6054a4eb60e7 100644 --- a/client/web/src/site-admin/analytics/AnalyticsCodeInsightsPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsCodeInsightsPage/index.tsx @@ -1,6 +1,7 @@ import React, { useMemo, useEffect } from 'react' import { startCase } from 'lodash' +import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { Card, LoadingSpinner, Text, LineChart, Series, H2 } from '@sourcegraph/wildcard' @@ -36,7 +37,7 @@ export const calculateMinutesSaved = (data: typeof MinutesSaved): number => data.LanguageSeries * MinutesSaved.LanguageSeries + data.ComputeSeries * MinutesSaved.ComputeSeries -export const AnalyticsCodeInsightsPage: React.FunctionComponent = () => { +export const AnalyticsCodeInsightsPage: React.FunctionComponent = () => { const { dateRange, aggregation, grouping } = useChartFilters({ name: 'Insights', aggregation: 'count' }) const { data, error, loading } = useQuery( INSIGHTS_STATISTICS, diff --git a/client/web/src/site-admin/analytics/AnalyticsCodeIntelPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsCodeIntelPage/index.tsx index 0b307d64051d..735ebdf659df 100644 --- a/client/web/src/site-admin/analytics/AnalyticsCodeIntelPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsCodeIntelPage/index.tsx @@ -2,6 +2,7 @@ import React, { useMemo, useEffect, useState } from 'react' import classNames from 'classnames' import { groupBy, sortBy, startCase, sumBy } from 'lodash' +import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { @@ -34,7 +35,7 @@ import { CODEINTEL_STATISTICS } from './queries' import styles from './index.module.scss' -export const AnalyticsCodeIntelPage: React.FC = () => { +export const AnalyticsCodeIntelPage: React.FunctionComponent> = () => { const { dateRange, aggregation, grouping } = useChartFilters({ name: 'CodeIntel' }) const { data, error, loading } = useQuery( CODEINTEL_STATISTICS, diff --git a/client/web/src/site-admin/analytics/AnalyticsExtensionsPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsExtensionsPage/index.tsx index 1e4936346438..b07a46cfb929 100644 --- a/client/web/src/site-admin/analytics/AnalyticsExtensionsPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsExtensionsPage/index.tsx @@ -2,6 +2,7 @@ import React, { useMemo, useEffect } from 'react' import classNames from 'classnames' import { startCase } from 'lodash' +import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { Card, H2, Text, LoadingSpinner, AnchorLink, H4, LineChart, Series } from '@sourcegraph/wildcard' @@ -21,7 +22,7 @@ import { EXTENSIONS_STATISTICS } from './queries' import styles from './index.module.scss' -export const AnalyticsExtensionsPage: React.FunctionComponent = () => { +export const AnalyticsExtensionsPage: React.FunctionComponent> = () => { const { dateRange, aggregation, grouping } = useChartFilters({ name: 'Extensions' }) const { data, error, loading } = useQuery( EXTENSIONS_STATISTICS, diff --git a/client/web/src/site-admin/analytics/AnalyticsNotebooksPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsNotebooksPage/index.tsx index 824fbd287310..d31a902665ee 100644 --- a/client/web/src/site-admin/analytics/AnalyticsNotebooksPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsNotebooksPage/index.tsx @@ -2,6 +2,7 @@ import React, { useMemo, useEffect } from 'react' import classNames from 'classnames' import { startCase } from 'lodash' +import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { Card, LoadingSpinner, H2, Text, H4, AnchorLink, LineChart, Series } from '@sourcegraph/wildcard' @@ -21,7 +22,7 @@ import { NOTEBOOKS_STATISTICS } from './queries' import styles from './index.module.scss' -export const AnalyticsNotebooksPage: React.FunctionComponent = () => { +export const AnalyticsNotebooksPage: React.FunctionComponent> = () => { const { dateRange, aggregation, grouping } = useChartFilters({ name: 'Notebooks' }) const { data, error, loading } = useQuery( NOTEBOOKS_STATISTICS, diff --git a/client/web/src/site-admin/analytics/AnalyticsOverviewPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsOverviewPage/index.tsx index b47f0ca5717d..3ac8c7e1f715 100644 --- a/client/web/src/site-admin/analytics/AnalyticsOverviewPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsOverviewPage/index.tsx @@ -3,6 +3,7 @@ import React, { useEffect } from 'react' import { mdiAccount, mdiSourceRepository, mdiCommentOutline } from '@mdi/js' import classNames from 'classnames' import format from 'date-fns/format' +import * as H from 'history' import { useQuery } from '@sourcegraph/http-client' import { Card, H2, Text, LoadingSpinner, AnchorLink } from '@sourcegraph/wildcard' @@ -22,9 +23,11 @@ import { Sidebar } from './Sidebar' import styles from './index.module.scss' -interface Props {} +interface IProps { + history: H.History +} -export const AnalyticsOverviewPage: React.FunctionComponent = () => { +export const AnalyticsOverviewPage: React.FunctionComponent = ({ history }) => { const { dateRange } = useChartFilters({ name: 'Overview' }) const { data, error, loading } = useQuery( OVERVIEW_STATISTICS, diff --git a/client/web/src/site-admin/analytics/AnalyticsSearchPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsSearchPage/index.tsx index 80155bcc19b0..951565492815 100644 --- a/client/web/src/site-admin/analytics/AnalyticsSearchPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsSearchPage/index.tsx @@ -2,6 +2,7 @@ import React, { useMemo, useEffect } from 'react' import classNames from 'classnames' import { startCase } from 'lodash' +import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { Card, H2, Text, LoadingSpinner, AnchorLink, H4, LineChart, Series } from '@sourcegraph/wildcard' @@ -21,7 +22,7 @@ import { SEARCH_STATISTICS } from './queries' import styles from './index.module.scss' -export const AnalyticsSearchPage: React.FC = () => { +export const AnalyticsSearchPage: React.FunctionComponent> = () => { const { dateRange, aggregation, grouping } = useChartFilters({ name: 'Search' }) const { data, error, loading } = useQuery(SEARCH_STATISTICS, { variables: { diff --git a/client/web/src/site-admin/analytics/AnalyticsUsersPage/AnalyticsUsersPage.story.tsx b/client/web/src/site-admin/analytics/AnalyticsUsersPage/AnalyticsUsersPage.story.tsx index df88dc0414c5..af34354fcb64 100644 --- a/client/web/src/site-admin/analytics/AnalyticsUsersPage/AnalyticsUsersPage.story.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsUsersPage/AnalyticsUsersPage.story.tsx @@ -692,6 +692,6 @@ const USER_ANALYTICS_QUERY_MOCK: MockedResponse = { export const AnalyticsUsersPageExample: Story = () => ( - + ) diff --git a/client/web/src/site-admin/analytics/AnalyticsUsersPage/index.tsx b/client/web/src/site-admin/analytics/AnalyticsUsersPage/index.tsx index 90572c6eff90..453eb4d40301 100644 --- a/client/web/src/site-admin/analytics/AnalyticsUsersPage/index.tsx +++ b/client/web/src/site-admin/analytics/AnalyticsUsersPage/index.tsx @@ -2,6 +2,7 @@ import { useState, useMemo, useEffect, FC } from 'react' import classNames from 'classnames' import { startCase } from 'lodash' +import { RouteComponentProps } from 'react-router' import { useQuery } from '@sourcegraph/http-client' import { Card, LoadingSpinner, useMatchMedia, Text, LineChart, BarChart, Series } from '@sourcegraph/wildcard' @@ -20,7 +21,7 @@ import { USERS_STATISTICS } from './queries' import styles from './AnalyticsUsersPage.module.scss' -export const AnalyticsUsersPage: FC = () => { +export const AnalyticsUsersPage: FC = () => { const { dateRange, aggregation, grouping } = useChartFilters({ name: 'Users', aggregation: 'uniqueUsers' }) const { data, error, loading } = useQuery(USERS_STATISTICS, { variables: { diff --git a/client/web/src/site-admin/outbound-webhooks/CreatePage.story.tsx b/client/web/src/site-admin/outbound-webhooks/CreatePage.story.tsx index 8d268447a476..0db65a2c1533 100644 --- a/client/web/src/site-admin/outbound-webhooks/CreatePage.story.tsx +++ b/client/web/src/site-admin/outbound-webhooks/CreatePage.story.tsx @@ -1,4 +1,5 @@ import { DecoratorFn, Meta, Story } from '@storybook/react' +import * as H from 'history' import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService' import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo' @@ -21,7 +22,12 @@ export const Page: Story = () => ( {() => ( - + )} diff --git a/client/web/src/site-admin/outbound-webhooks/CreatePage.tsx b/client/web/src/site-admin/outbound-webhooks/CreatePage.tsx index 47c896feb505..d79b167c6475 100644 --- a/client/web/src/site-admin/outbound-webhooks/CreatePage.tsx +++ b/client/web/src/site-admin/outbound-webhooks/CreatePage.tsx @@ -2,7 +2,7 @@ import { FC, useEffect, useState } from 'react' import { mdiCog } from '@mdi/js' import { noop } from 'lodash' -import { useNavigate } from 'react-router-dom-v5-compat' +import { RouteComponentProps } from 'react-router' import { useMutation } from '@sourcegraph/http-client' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -16,10 +16,9 @@ import { CREATE_OUTBOUND_WEBHOOK } from './backend' import { EventTypes } from './create-edit/EventTypes' import { SubmitButton } from './create-edit/SubmitButton' -export interface CreatePageProps extends TelemetryProps {} +export interface CreatePageProps extends TelemetryProps, RouteComponentProps<{}> {} -export const CreatePage: FC = ({ telemetryService }) => { - const navigate = useNavigate() +export const CreatePage: FC = ({ telemetryService, history }) => { useEffect(() => { telemetryService.logPageView('OutboundWebhooksCreatePage') }, [telemetryService]) @@ -41,7 +40,7 @@ export const CreatePage: FC = ({ telemetryService }) => { url, }, }, - onCompleted: () => navigate('/site-admin/outbound-webhooks'), + onCompleted: () => history.push('/site-admin/outbound-webhooks'), }) return ( diff --git a/client/web/src/site-admin/outbound-webhooks/EditPage.story.tsx b/client/web/src/site-admin/outbound-webhooks/EditPage.story.tsx index 84a1f6f9bdaf..ea72282d40fd 100644 --- a/client/web/src/site-admin/outbound-webhooks/EditPage.story.tsx +++ b/client/web/src/site-admin/outbound-webhooks/EditPage.story.tsx @@ -1,4 +1,5 @@ import { DecoratorFn, Meta, Story } from '@storybook/react' +import * as H from 'history' import { WildcardMockLink } from 'wildcard-mock-link' import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -22,9 +23,14 @@ export const Page: Story = () => ( {() => ( - + )} diff --git a/client/web/src/site-admin/outbound-webhooks/EditPage.tsx b/client/web/src/site-admin/outbound-webhooks/EditPage.tsx index 2b25d09fb90c..d69a8e77a230 100644 --- a/client/web/src/site-admin/outbound-webhooks/EditPage.tsx +++ b/client/web/src/site-admin/outbound-webhooks/EditPage.tsx @@ -2,7 +2,7 @@ import { FC, useCallback, useEffect, useState } from 'react' import { mdiCog } from '@mdi/js' import { noop } from 'lodash' -import { useNavigate, useParams } from 'react-router-dom-v5-compat' +import { RouteComponentProps } from 'react-router' import { useMutation, useQuery } from '@sourcegraph/http-client' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -23,11 +23,10 @@ import { SubmitButton } from './create-edit/SubmitButton' import { DeleteButton } from './delete/DeleteButton' import { Logs } from './logs/Logs' -export interface EditPageProps extends TelemetryProps {} +export interface EditPageProps extends TelemetryProps, RouteComponentProps<{ id: string }> {} -export const EditPage: FC = ({ telemetryService }) => { - const navigate = useNavigate() - const { id = '' } = useParams<{ id: string }>() +export const EditPage: FC = ({ history, match, telemetryService }) => { + const { id } = match.params useEffect(() => { telemetryService.logPageView('OutboundWebhooksEditPage') @@ -40,8 +39,8 @@ export const EditPage: FC = ({ telemetryService }) => { const webhookURL = data?.node?.__typename === 'OutboundWebhook' ? data.node.url : undefined const onDeleted = useCallback(() => { - navigate('/site-admin/outbound-webhooks') - }, [navigate]) + history.push('/site-admin/outbound-webhooks') + }, [history]) if (error) { return ( diff --git a/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.story.tsx b/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.story.tsx index f54d78913cb4..b996a65b04e8 100644 --- a/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.story.tsx +++ b/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.story.tsx @@ -1,4 +1,5 @@ import { DecoratorFn, Meta, Story } from '@storybook/react' +import * as H from 'history' import { WildcardMockLink } from 'wildcard-mock-link' import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -22,7 +23,12 @@ export const Empty: Story = () => ( {() => ( - + )} @@ -34,7 +40,12 @@ export const NotEmpty: Story = () => ( {() => ( - + )} diff --git a/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.tsx b/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.tsx index 584987c5f1ba..506131d0e7af 100644 --- a/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.tsx +++ b/client/web/src/site-admin/outbound-webhooks/OutboundWebhooksPage.tsx @@ -1,6 +1,7 @@ import { FC, useEffect } from 'react' import { mdiAlertCircle, mdiCog, mdiMapSearch, mdiPencil, mdiPlus } from '@mdi/js' +import { RouteComponentProps } from 'react-router' import { pluralize } from '@sourcegraph/common' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -23,7 +24,7 @@ import { DeleteButton } from './delete/DeleteButton' import styles from './OutboundWebhooksPage.module.scss' -export interface OutboundWebhooksPageProps extends TelemetryProps {} +export interface OutboundWebhooksPageProps extends TelemetryProps, RouteComponentProps<{}> {} export const OutboundWebhooksPage: FC = ({ telemetryService }) => { useEffect(() => { diff --git a/client/web/src/site-admin/routes.tsx b/client/web/src/site-admin/routes.tsx index 623c40d184f7..2c8e901b9ae5 100644 --- a/client/web/src/site-admin/routes.tsx +++ b/client/web/src/site-admin/routes.tsx @@ -2,226 +2,172 @@ import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' import { SiteAdminAreaRoute } from './SiteAdminArea' -const AnalyticsOverviewPage = lazyComponent(() => import('./analytics/AnalyticsOverviewPage'), 'AnalyticsOverviewPage') -const AnalyticsSearchPage = lazyComponent(() => import('./analytics/AnalyticsSearchPage'), 'AnalyticsSearchPage') -const AnalyticsCodeIntelPage = lazyComponent( - () => import('./analytics/AnalyticsCodeIntelPage'), - 'AnalyticsCodeIntelPage' -) -const AnalyticsExtensionsPage = lazyComponent( - () => import('./analytics/AnalyticsExtensionsPage'), - 'AnalyticsExtensionsPage' -) -const AnalyticsUsersPage = lazyComponent(() => import('./analytics/AnalyticsUsersPage'), 'AnalyticsUsersPage') -const AnalyticsCodeInsightsPage = lazyComponent( - () => import('./analytics/AnalyticsCodeInsightsPage'), - 'AnalyticsCodeInsightsPage' -) -const AnalyticsBatchChangesPage = lazyComponent( - () => import('./analytics/AnalyticsBatchChangesPage'), - 'AnalyticsBatchChangesPage' -) -const AnalyticsNotebooksPage = lazyComponent( - () => import('./analytics/AnalyticsNotebooksPage'), - 'AnalyticsNotebooksPage' -) -const SiteAdminConfigurationPage = lazyComponent( - () => import('./SiteAdminConfigurationPage'), - 'SiteAdminConfigurationPage' -) -const SiteAdminSettingsPage = lazyComponent(() => import('./SiteAdminSettingsPage'), 'SiteAdminSettingsPage') -const SiteAdminExternalServicesArea = lazyComponent( - () => import('./SiteAdminExternalServicesArea'), - 'SiteAdminExternalServicesArea' -) -const SiteAdminRepositoriesPage = lazyComponent( - () => import('./SiteAdminRepositoriesPage'), - 'SiteAdminRepositoriesPage' -) -const SiteAdminOrgsPage = lazyComponent(() => import('./SiteAdminOrgsPage'), 'SiteAdminOrgsPage') -const UsersManagement = lazyComponent(() => import('./UserManagement'), 'UsersManagement') -const SiteAdminCreateUserPage = lazyComponent(() => import('./SiteAdminCreateUserPage'), 'SiteAdminCreateUserPage') -const SiteAdminTokensPage = lazyComponent(() => import('./SiteAdminTokensPage'), 'SiteAdminTokensPage') -const SiteAdminUpdatesPage = lazyComponent(() => import('./SiteAdminUpdatesPage'), 'SiteAdminUpdatesPage') -const SiteAdminPingsPage = lazyComponent(() => import('./SiteAdminPingsPage'), 'SiteAdminPingsPage') -const SiteAdminReportBugPage = lazyComponent(() => import('./SiteAdminReportBugPage'), 'SiteAdminReportBugPage') -const SiteAdminSurveyResponsesPage = lazyComponent( - () => import('./SiteAdminSurveyResponsesPage'), - 'SiteAdminSurveyResponsesPage' -) -const SiteAdminMigrationsPage = lazyComponent(() => import('./SiteAdminMigrationsPage'), 'SiteAdminMigrationsPage') -const SiteAdminOutboundRequestsPage = lazyComponent( - () => import('./SiteAdminOutboundRequestsPage'), - 'SiteAdminOutboundRequestsPage' -) -const SiteAdminBackgroundJobsPage = lazyComponent( - () => import('./SiteAdminBackgroundJobsPage'), - 'SiteAdminBackgroundJobsPage' -) -const SiteAdminFeatureFlagsPage = lazyComponent( - () => import('./SiteAdminFeatureFlagsPage'), - 'SiteAdminFeatureFlagsPage' -) -const SiteAdminFeatureFlagConfigurationPage = lazyComponent( - () => import('./SiteAdminFeatureFlagConfigurationPage'), - 'SiteAdminFeatureFlagConfigurationPage' -) -const OutboundWebhooksPage = lazyComponent( - () => import('./outbound-webhooks/OutboundWebhooksPage'), - 'OutboundWebhooksPage' -) -const CreatePage = lazyComponent(() => import('./outbound-webhooks/CreatePage'), 'CreatePage') -const EditPage = lazyComponent(() => import('./outbound-webhooks/EditPage'), 'EditPage') -const SiteAdminWebhooksPage = lazyComponent(() => import('./SiteAdminWebhooksPage'), 'SiteAdminWebhooksPage') -const SiteAdminWebhookCreatePage = lazyComponent( - () => import('./SiteAdminWebhookCreatePage'), - 'SiteAdminWebhookCreatePage' -) -const SiteAdminWebhookPage = lazyComponent(() => import('./SiteAdminWebhookPage'), 'SiteAdminWebhookPage') -const SiteAdminSlowRequestsPage = lazyComponent( - () => import('./SiteAdminSlowRequestsPage'), - 'SiteAdminSlowRequestsPage' -) -const SiteAdminWebhookUpdatePage = lazyComponent( - () => import('./SiteAdminWebhookUpdatePage'), - 'SiteAdminWebhookUpdatePage' -) - export const siteAdminAreaRoutes: readonly SiteAdminAreaRoute[] = [ { path: '/', - render: () => , + render: lazyComponent(() => import('./analytics/AnalyticsOverviewPage'), 'AnalyticsOverviewPage'), + exact: true, }, { path: '/analytics/search', - render: () => , + render: lazyComponent(() => import('./analytics/AnalyticsSearchPage'), 'AnalyticsSearchPage'), + exact: true, }, { path: '/analytics/code-intel', - render: () => , + render: lazyComponent(() => import('./analytics/AnalyticsCodeIntelPage'), 'AnalyticsCodeIntelPage'), + exact: true, }, { path: '/analytics/extensions', - render: () => , + render: lazyComponent(() => import('./analytics/AnalyticsExtensionsPage'), 'AnalyticsExtensionsPage'), + exact: true, }, { path: '/analytics/users', - render: () => , + render: lazyComponent(() => import('./analytics/AnalyticsUsersPage'), 'AnalyticsUsersPage'), + exact: true, }, { path: '/analytics/code-insights', - render: () => , + render: lazyComponent(() => import('./analytics/AnalyticsCodeInsightsPage'), 'AnalyticsCodeInsightsPage'), + exact: true, }, { path: '/analytics/batch-changes', - render: () => , + render: lazyComponent(() => import('./analytics/AnalyticsBatchChangesPage'), 'AnalyticsBatchChangesPage'), + exact: true, }, { path: '/analytics/notebooks', - render: () => , + render: lazyComponent(() => import('./analytics/AnalyticsNotebooksPage'), 'AnalyticsNotebooksPage'), + exact: true, }, { path: '/configuration', - render: props => , + exact: true, + render: lazyComponent(() => import('./SiteAdminConfigurationPage'), 'SiteAdminConfigurationPage'), }, { path: '/global-settings', - render: props => , + exact: true, + render: lazyComponent(() => import('./SiteAdminSettingsPage'), 'SiteAdminSettingsPage'), }, { path: '/external-services', - render: props => , - }, - { - path: '/external-services/*', - render: props => , + render: lazyComponent(() => import('./SiteAdminExternalServicesArea'), 'SiteAdminExternalServicesArea'), }, { path: '/repositories', - render: props => , + render: lazyComponent(() => import('./SiteAdminRepositoriesPage'), 'SiteAdminRepositoriesPage'), + exact: true, }, { path: '/organizations', - render: props => , + render: lazyComponent(() => import('./SiteAdminOrgsPage'), 'SiteAdminOrgsPage'), + exact: true, }, { path: '/users', - render: () => , + exact: true, + render: lazyComponent(() => import('./UserManagement'), 'UsersManagement'), }, { path: '/users/new', - render: () => , + render: lazyComponent(() => import('./SiteAdminCreateUserPage'), 'SiteAdminCreateUserPage'), + exact: true, }, { path: '/tokens', - render: props => , + exact: true, + render: lazyComponent(() => import('./SiteAdminTokensPage'), 'SiteAdminTokensPage'), }, { path: '/updates', - render: props => , + render: lazyComponent(() => import('./SiteAdminUpdatesPage'), 'SiteAdminUpdatesPage'), + exact: true, }, { path: '/pings', - render: props => , + render: lazyComponent(() => import('./SiteAdminPingsPage'), 'SiteAdminPingsPage'), + exact: true, }, { path: '/report-bug', - render: props => , + exact: true, + render: lazyComponent(() => import('./SiteAdminReportBugPage'), 'SiteAdminReportBugPage'), }, { path: '/surveys', - render: props => , + exact: true, + render: lazyComponent(() => import('./SiteAdminSurveyResponsesPage'), 'SiteAdminSurveyResponsesPage'), }, { path: '/migrations', - render: props => , + exact: true, + render: lazyComponent(() => import('./SiteAdminMigrationsPage'), 'SiteAdminMigrationsPage'), }, { path: '/outbound-requests', - render: props => , + exact: true, + render: lazyComponent(() => import('./SiteAdminOutboundRequestsPage'), 'SiteAdminOutboundRequestsPage'), }, { path: '/background-jobs', - render: props => , + exact: true, + render: lazyComponent(() => import('./SiteAdminBackgroundJobsPage'), 'SiteAdminBackgroundJobsPage'), }, { path: '/feature-flags', - render: props => , + exact: true, + render: lazyComponent(() => import('./SiteAdminFeatureFlagsPage'), 'SiteAdminFeatureFlagsPage'), }, { path: '/feature-flags/configuration/:name', - render: props => , + exact: true, + render: lazyComponent( + () => import('./SiteAdminFeatureFlagConfigurationPage'), + 'SiteAdminFeatureFlagConfigurationPage' + ), }, { path: '/outbound-webhooks', - render: props => , + exact: true, + render: lazyComponent(() => import('./outbound-webhooks/OutboundWebhooksPage'), 'OutboundWebhooksPage'), }, { path: '/outbound-webhooks/create', - render: props => , + exact: true, + render: lazyComponent(() => import('./outbound-webhooks/CreatePage'), 'CreatePage'), }, { path: '/outbound-webhooks/:id', - render: props => , + exact: true, + render: lazyComponent(() => import('./outbound-webhooks/EditPage'), 'EditPage'), }, { path: '/webhooks', - render: props => , + exact: true, + render: lazyComponent(() => import('./SiteAdminWebhooksPage'), 'SiteAdminWebhooksPage'), }, { path: '/webhooks/create', - render: props => , + exact: true, + render: lazyComponent(() => import('./SiteAdminWebhookCreatePage'), 'SiteAdminWebhookCreatePage'), }, { path: '/webhooks/:id', - render: props => , + exact: true, + render: lazyComponent(() => import('./SiteAdminWebhookPage'), 'SiteAdminWebhookPage'), }, { path: '/slow-requests', - render: props => , + exact: true, + render: lazyComponent(() => import('./SiteAdminSlowRequestsPage'), 'SiteAdminSlowRequestsPage'), }, { path: '/webhooks/:id/edit', - render: props => , + exact: true, + render: lazyComponent(() => import('./SiteAdminWebhookUpdatePage'), 'SiteAdminWebhookUpdatePage'), }, ] diff --git a/client/web/src/user/settings/routes.tsx b/client/web/src/user/settings/routes.tsx index db15bae2c966..15d4c62f4d87 100644 --- a/client/web/src/user/settings/routes.tsx +++ b/client/web/src/user/settings/routes.tsx @@ -29,7 +29,6 @@ export const userSettingsAreaRoutes: readonly UserSettingsAreaRoute[] = [ return ( } /** - * Configuration for a react-router 6 route. + * Configuration for a route. * * @template C Context information that is passed to `render` and `condition` */ export interface RouteV6Descriptor extends Conditional { + /** Path of this route (appended to the current match) */ readonly path: string readonly render: (props: C) => React.ReactNode } From c3766a3a8d1460fde9010485819fa9bd2570f24b Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 3 Feb 2023 23:56:17 +0100 Subject: [PATCH 413/678] Treeview: Fix history bug and 404 handling (#47372) --- .../src/repo/RepoRevisionSidebarFileTree.tsx | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/client/web/src/repo/RepoRevisionSidebarFileTree.tsx b/client/web/src/repo/RepoRevisionSidebarFileTree.tsx index 692d22217404..316640dcb63a 100644 --- a/client/web/src/repo/RepoRevisionSidebarFileTree.tsx +++ b/client/web/src/repo/RepoRevisionSidebarFileTree.tsx @@ -8,7 +8,7 @@ import { mdiFolderArrowUp, } from '@mdi/js' import classNames from 'classnames' -import { useNavigate } from 'react-router-dom-v5-compat' +import { useNavigate, useLocation } from 'react-router-dom-v5-compat' import { gql, useQuery } from '@sourcegraph/http-client' import { TelemetryService } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -112,6 +112,7 @@ export const RepoRevisionSidebarFileTree: React.FunctionComponent = props const [selectedIds, setSelectedIds] = useState(defaultSelectedIds) const navigate = useNavigate() + const location = useLocation() const [defaultVariables] = useState({ repoName: props.repoName, @@ -127,11 +128,8 @@ export const RepoRevisionSidebarFileTree: React.FunctionComponent = props ancestors: alwaysLoadAncestors, }, onCompleted(data) { - const rootTreeUrl = data?.repository?.commit?.tree?.url - const entries = data?.repository?.commit?.tree?.entries - if (!entries || !rootTreeUrl) { - throw new Error('No entries or root data') - } + const rootTreeUrl = data?.repository?.commit?.tree?.url ?? location.pathname + const entries = data?.repository?.commit?.tree?.entries ?? [] if (treeData === null) { setTreeData( appendTreeData( @@ -174,6 +172,11 @@ export const RepoRevisionSidebarFileTree: React.FunctionComponent = props return } + // Bail out if we controlled the selection update. + if (selectedIds.length > 0 && selectedIds[0] === element.id) { + return + } + // On the initial rendering, an onSelect event is fired for the // default node. We don't want to navigate to that node though. if (defaultSelectFiredRef.current === false && element.id === defaultNodeId) { @@ -201,7 +204,15 @@ export const RepoRevisionSidebarFileTree: React.FunctionComponent = props } setSelectedIds([element.id]) }, - [defaultNodeId, telemetryService, navigate, props.initialFilePathIsDirectory, initialFilePath, onExpandParent] + [ + selectedIds, + defaultNodeId, + telemetryService, + navigate, + props.initialFilePathIsDirectory, + initialFilePath, + onExpandParent, + ] ) // We need a mutable reference to the tree data since we don't want the From ab099a1ba97bc5beed1135a9c787e8283272000d Mon Sep 17 00:00:00 2001 From: Gabe Torres <69164745+gabtorre@users.noreply.github.com> Date: Fri, 3 Feb 2023 15:54:05 -0800 Subject: [PATCH 414/678] Add filter to remove empty queries (#47072) * Add filter to remove empty queries * add query to test * prettier * update test * prettier --- .../drill-down-filters/validators.ts | 5 ++--- .../src/integration/insights/drill-down-filters.test.ts | 8 +++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/drill-down-filters/validators.ts b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/drill-down-filters/validators.ts index ea3b440c5722..cc971ae014d8 100644 --- a/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/drill-down-filters/validators.ts +++ b/client/web/src/enterprise/insights/components/insights-view-grid/components/backend-insight/components/drill-down-filters-panel/drill-down-filters/validators.ts @@ -26,6 +26,7 @@ const GET_CONTEXT_BY_NAME = gql` searchContexts(query: $query) { nodes { spec + query } } } @@ -49,9 +50,7 @@ export const createSearchContextValidator = return error.message } - const { - searchContexts: { nodes }, - } = data + const nodes = data.searchContexts.nodes.filter(node => node.query !== '') if (!nodes.some(context => context.spec === sanitizedValue)) { return `We couldn't find the context ${sanitizedValue}. Please ensure the context exists.` diff --git a/client/web/src/integration/insights/drill-down-filters.test.ts b/client/web/src/integration/insights/drill-down-filters.test.ts index 74f7e97146b0..c614d914c9da 100644 --- a/client/web/src/integration/insights/drill-down-filters.test.ts +++ b/client/web/src/integration/insights/drill-down-filters.test.ts @@ -71,7 +71,13 @@ describe('Backend insight drill down filters', () => { GetSearchContextByName: () => ({ searchContexts: { __typename: 'SearchContextConnection', - nodes: [{ __typename: 'SearchContext', spec: '@sourcegraph/sourcegraph' }], + nodes: [ + { + __typename: 'SearchContext', + spec: '@sourcegraph/sourcegraph', + query: 'repo:github.com/sourcegraph/sourcegraph', + }, + ], }, }), From 5294b5466db4162efddfdeb30f347c7799d43320 Mon Sep 17 00:00:00 2001 From: Bolaji Olajide <25608335+BolajiOlajide@users.noreply.github.com> Date: Sat, 4 Feb 2023 05:39:18 +0100 Subject: [PATCH 415/678] rbac: setup graphql schema and query for fetching roles and permissions (#46190) --- cmd/frontend/enterprise/enterprise.go | 1 + cmd/frontend/graphqlbackend/graphqlbackend.go | 31 ++- cmd/frontend/graphqlbackend/node.go | 10 + cmd/frontend/graphqlbackend/rbac.go | 49 ++++ cmd/frontend/graphqlbackend/rbac.graphql | 191 ++++++++++++++ cmd/frontend/graphqlbackend/schema.go | 5 + cmd/frontend/graphqlbackend/testing.go | 2 +- cmd/frontend/graphqlbackend/user.go | 13 + .../internal/bg/update_permissions.go | 2 +- cmd/frontend/internal/cli/serve_cmd.go | 1 + enterprise/cmd/frontend/internal/rbac/init.go | 29 +++ .../internal/rbac/resolvers/apitest/exec.go | 86 ++++++ .../internal/rbac/resolvers/apitest/types.go | 52 ++++ .../internal/rbac/resolvers/error_test.go | 13 + .../internal/rbac/resolvers/errors.go | 7 + .../internal/rbac/resolvers/main_test.go | 58 +++++ .../internal/rbac/resolvers/permission.go | 41 +++ .../resolvers/permission_connection_store.go | 65 +++++ .../rbac/resolvers/permission_test.go | 94 +++++++ .../internal/rbac/resolvers/permissions.go | 87 +++++++ .../rbac/resolvers/permissions_test.go | 245 ++++++++++++++++++ .../internal/rbac/resolvers/resolver.go | 33 +++ .../frontend/internal/rbac/resolvers/role.go | 67 +++++ .../rbac/resolvers/role_connection_store.go | 64 +++++ .../internal/rbac/resolvers/role_test.go | 137 ++++++++++ .../frontend/internal/rbac/resolvers/roles.go | 75 ++++++ .../internal/rbac/resolvers/roles_test.go | 230 ++++++++++++++++ enterprise/cmd/frontend/shared/shared.go | 2 + internal/database/permissions.go | 161 ++++++++++-- internal/database/permissions_test.go | 152 ++++++++++- internal/database/roles.go | 73 ++++-- internal/database/roles_test.go | 94 +++++-- 32 files changed, 2094 insertions(+), 76 deletions(-) create mode 100644 cmd/frontend/graphqlbackend/rbac.go create mode 100644 cmd/frontend/graphqlbackend/rbac.graphql create mode 100644 enterprise/cmd/frontend/internal/rbac/init.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/apitest/exec.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/apitest/types.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/error_test.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/errors.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/main_test.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/permission.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/permission_connection_store.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/permission_test.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/permissions.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/permissions_test.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/resolver.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/role.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/role_connection_store.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/role_test.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/roles.go create mode 100644 enterprise/cmd/frontend/internal/rbac/resolvers/roles_test.go diff --git a/cmd/frontend/enterprise/enterprise.go b/cmd/frontend/enterprise/enterprise.go index 35f0739be57b..7c3e941199ae 100644 --- a/cmd/frontend/enterprise/enterprise.go +++ b/cmd/frontend/enterprise/enterprise.go @@ -59,6 +59,7 @@ type Services struct { ComputeResolver graphqlbackend.ComputeResolver InsightsAggregationResolver graphqlbackend.InsightsAggregationResolver WebhooksResolver graphqlbackend.WebhooksResolver + RBACResolver graphqlbackend.RBACResolver } // NewCodeIntelUploadHandler creates a new handler for the LSIF upload endpoint. The diff --git a/cmd/frontend/graphqlbackend/graphqlbackend.go b/cmd/frontend/graphqlbackend/graphqlbackend.go index 76adfe29cc3a..1aec0f652e46 100644 --- a/cmd/frontend/graphqlbackend/graphqlbackend.go +++ b/cmd/frontend/graphqlbackend/graphqlbackend.go @@ -380,31 +380,35 @@ func prometheusGraphQLRequestName(requestName string) string { } func NewSchemaWithoutResolvers(db database.DB) (*graphql.Schema, error) { - return NewSchema(db, gitserver.NewClient(), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) + return NewSchema(db, gitserver.NewClient(), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) } func NewSchemaWithNotebooksResolver(db database.DB, notebooks NotebooksResolver) (*graphql.Schema, error) { - return NewSchema(db, gitserver.NewClient(), nil, nil, nil, nil, nil, nil, nil, nil, notebooks, nil, nil, nil) + return NewSchema(db, gitserver.NewClient(), nil, nil, nil, nil, nil, nil, nil, nil, notebooks, nil, nil, nil, nil) } func NewSchemaWithAuthzResolver(db database.DB, authz AuthzResolver) (*graphql.Schema, error) { - return NewSchema(db, gitserver.NewClient(), nil, nil, nil, authz, nil, nil, nil, nil, nil, nil, nil, nil) + return NewSchema(db, gitserver.NewClient(), nil, nil, nil, authz, nil, nil, nil, nil, nil, nil, nil, nil, nil) } func NewSchemaWithBatchChangesResolver(db database.DB, batchChanges BatchChangesResolver) (*graphql.Schema, error) { - return NewSchema(db, gitserver.NewClient(), batchChanges, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) + return NewSchema(db, gitserver.NewClient(), batchChanges, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) } func NewSchemaWithCodeMonitorsResolver(db database.DB, codeMonitors CodeMonitorsResolver) (*graphql.Schema, error) { - return NewSchema(db, gitserver.NewClient(), nil, nil, nil, nil, codeMonitors, nil, nil, nil, nil, nil, nil, nil) + return NewSchema(db, gitserver.NewClient(), nil, nil, nil, nil, codeMonitors, nil, nil, nil, nil, nil, nil, nil, nil) } func NewSchemaWithLicenseResolver(db database.DB, license LicenseResolver) (*graphql.Schema, error) { - return NewSchema(db, gitserver.NewClient(), nil, nil, nil, nil, nil, license, nil, nil, nil, nil, nil, nil) + return NewSchema(db, gitserver.NewClient(), nil, nil, nil, nil, nil, license, nil, nil, nil, nil, nil, nil, nil) } func NewSchemaWithWebhooksResolver(db database.DB, webhooksResolver WebhooksResolver) (*graphql.Schema, error) { - return NewSchema(db, gitserver.NewClient(), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, webhooksResolver) + return NewSchema(db, gitserver.NewClient(), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, webhooksResolver, nil) +} + +func NewSchemaWithRBACResolver(db database.DB, rbacResolver RBACResolver) (*graphql.Schema, error) { + return NewSchema(db, gitserver.NewClient(), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, rbacResolver) } func NewSchema( @@ -422,6 +426,7 @@ func NewSchema( compute ComputeResolver, insightsAggregation InsightsAggregationResolver, webhooksResolver WebhooksResolver, + rbacResolver RBACResolver, ) (*graphql.Schema, error) { resolver := newSchemaResolver(db, gitserverClient) schemas := []string{mainSchema, outboundWebhooksSchema} @@ -529,6 +534,16 @@ func NewSchema( } } + if rbacResolver != nil { + EnterpriseResolvers.rbacResolver = rbacResolver + resolver.RBACResolver = rbacResolver + schemas = append(schemas, rbacSchema) + // Register NodeByID handlers. + for kind, res := range rbacResolver.NodeResolvers() { + resolver.nodeByIDFns[kind] = res + } + } + logger := log.Scoped("GraphQL", "general GraphQL logging") return graphql.ParseSchema( strings.Join(schemas, "\n"), @@ -567,6 +582,7 @@ type schemaResolver struct { NotebooksResolver InsightsAggregationResolver WebhooksResolver + RBACResolver } // newSchemaResolver will return a new, safely instantiated schemaResolver with some @@ -669,6 +685,7 @@ var EnterpriseResolvers = struct { notebooksResolver NotebooksResolver InsightsAggregationResolver InsightsAggregationResolver webhooksResolver WebhooksResolver + rbacResolver RBACResolver }{} // DEPRECATED diff --git a/cmd/frontend/graphqlbackend/node.go b/cmd/frontend/graphqlbackend/node.go index 765599b6c512..6fd226942f42 100644 --- a/cmd/frontend/graphqlbackend/node.go +++ b/cmd/frontend/graphqlbackend/node.go @@ -338,3 +338,13 @@ func (r *NodeResolver) ToTeam() (*teamResolver, bool) { n, ok := r.Node.(*teamResolver) return n, ok } + +func (r *NodeResolver) ToRole() (RoleResolver, bool) { + n, ok := r.Node.(RoleResolver) + return n, ok +} + +func (r *NodeResolver) ToPermission() (PermissionResolver, bool) { + n, ok := r.Node.(PermissionResolver) + return n, ok +} diff --git a/cmd/frontend/graphqlbackend/rbac.go b/cmd/frontend/graphqlbackend/rbac.go new file mode 100644 index 000000000000..6cc837d94b1c --- /dev/null +++ b/cmd/frontend/graphqlbackend/rbac.go @@ -0,0 +1,49 @@ +package graphqlbackend + +import ( + "context" + + "github.com/graph-gophers/graphql-go" + + "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil" + "github.com/sourcegraph/sourcegraph/internal/gqlutil" +) + +type RoleResolver interface { + ID() graphql.ID + Name() string + System() bool + CreatedAt() gqlutil.DateTime + Permissions(context.Context, *ListPermissionArgs) (*graphqlutil.ConnectionResolver[PermissionResolver], error) +} + +type PermissionResolver interface { + ID() graphql.ID + Namespace() string + Action() string + CreatedAt() gqlutil.DateTime +} + +type RBACResolver interface { + // MUTATIONS + + // QUERIES + Roles(ctx context.Context, args *ListRoleArgs) (*graphqlutil.ConnectionResolver[RoleResolver], error) + Permissions(ctx context.Context, args *ListPermissionArgs) (*graphqlutil.ConnectionResolver[PermissionResolver], error) + + NodeResolvers() map[string]NodeByIDFunc +} + +type ListRoleArgs struct { + graphqlutil.ConnectionResolverArgs + + System bool + User *graphql.ID +} + +type ListPermissionArgs struct { + graphqlutil.ConnectionResolverArgs + + Role *graphql.ID + User *graphql.ID +} diff --git a/cmd/frontend/graphqlbackend/rbac.graphql b/cmd/frontend/graphqlbackend/rbac.graphql new file mode 100644 index 000000000000..2848607034a8 --- /dev/null +++ b/cmd/frontend/graphqlbackend/rbac.graphql @@ -0,0 +1,191 @@ +""" +A role +""" +type Role implements Node { + """ + The globally unique identifier for this role. + """ + id: ID! + """ + The human readable name for this role. + """ + name: String! + """ + Indicates whether a role is a default system role, which cannot be modified or deleted, or a custom role added by a site admin. + """ + system: Boolean! + """ + The list of permissions that will be granted to any user with this role. + """ + permissions( + """ + The limit argument for forward pagination. + """ + first: Int + """ + The limit argument for backward pagination. + """ + last: Int + """ + The cursor argument for forward pagination. + """ + after: String + """ + The cursor argument for backward pagination. + """ + before: String + ): PermissionConnection! + """ + The date and time when the role was created. + """ + createdAt: DateTime! +} + +""" +A list of roles. +""" +type RoleConnection { + """ + A list of roles. + """ + nodes: [Role!]! + """ + The total count of roles in the connection. + """ + totalCount: Int! + """ + Pagination information. + """ + pageInfo: ConnectionPageInfo! +} + +""" +A list of permissions. +""" +type PermissionConnection { + """ + A list of permissions. + """ + nodes: [Permission!]! + """ + The total count of permissions in the connection. + """ + totalCount: Int! + """ + Pagination information. + """ + pageInfo: ConnectionPageInfo! +} + +""" +A permission +""" +type Permission implements Node { + """ + The globally unique identifier for this permission. + """ + id: ID! + """ + The namespace in which this permission belongs to. + """ + namespace: String! + """ + The unique action which is granted to a bearer of this permission. + """ + action: String! + """ + The date and time when the permission was created. + """ + createdAt: DateTime! +} + +extend type Query { + """ + Roles returns all the roles in the database that matches the arguments + """ + roles( + """ + The limit argument for forward pagination. + """ + first: Int + """ + The limit argument for backward pagination. + """ + last: Int + """ + The cursor argument for forward pagination. + """ + after: String + """ + The cursor argument for backward pagination. + """ + before: String + ): RoleConnection! + + """ + All permissions + """ + permissions( + """ + The limit argument for forward pagination. + """ + first: Int + """ + The limit argument for backward pagination. + """ + last: Int + """ + The cursor argument for forward pagination. + """ + after: String + """ + The cursor argument for backward pagination. + """ + before: String + ): PermissionConnection! +} + +extend type User { + """ + The list of all roles assigned to this user. + """ + roles( + """ + The limit argument for forward pagination. + """ + first: Int + """ + The limit argument for backward pagination. + """ + last: Int + """ + The cursor argument for forward pagination. + """ + after: String + """ + The cursor argument for backward pagination. + """ + before: String + ): RoleConnection! + """ + The list of permissions granted to this user based on their roles. + """ + permissions( + """ + The limit argument for forward pagination. + """ + first: Int + """ + The limit argument for backward pagination. + """ + last: Int + """ + The cursor argument for forward pagination. + """ + after: String + """ + The cursor argument for backward pagination. + """ + before: String + ): PermissionConnection! +} diff --git a/cmd/frontend/graphqlbackend/schema.go b/cmd/frontend/graphqlbackend/schema.go index e69285973085..8eff092670ba 100644 --- a/cmd/frontend/graphqlbackend/schema.go +++ b/cmd/frontend/graphqlbackend/schema.go @@ -68,3 +68,8 @@ var insightsAggregationsSchema string // //go:embed outbound_webhooks.graphql var outboundWebhooksSchema string + +// rbacSchema is the RBAC raw graphql schema. +// +//go:embed rbac.graphql +var rbacSchema string diff --git a/cmd/frontend/graphqlbackend/testing.go b/cmd/frontend/graphqlbackend/testing.go index f6006691c00b..9ab8e15c3e8b 100644 --- a/cmd/frontend/graphqlbackend/testing.go +++ b/cmd/frontend/graphqlbackend/testing.go @@ -25,7 +25,7 @@ func mustParseGraphQLSchema(t *testing.T, db database.DB) *graphql.Schema { func mustParseGraphQLSchemaWithClient(t *testing.T, db database.DB, gitserverClient gitserver.Client) *graphql.Schema { t.Helper() - parsedSchema, parseSchemaErr := NewSchema(db, gitserverClient, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) + parsedSchema, parseSchemaErr := NewSchema(db, gitserverClient, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) if parseSchemaErr != nil { t.Fatal(parseSchemaErr) } diff --git a/cmd/frontend/graphqlbackend/user.go b/cmd/frontend/graphqlbackend/user.go index a1bf20ce405d..e242fe918ad8 100644 --- a/cmd/frontend/graphqlbackend/user.go +++ b/cmd/frontend/graphqlbackend/user.go @@ -13,6 +13,7 @@ import ( "github.com/sourcegraph/sourcegraph/cmd/frontend/auth/providers" "github.com/sourcegraph/sourcegraph/cmd/frontend/backend" "github.com/sourcegraph/sourcegraph/cmd/frontend/envvar" + "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil" "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/suspiciousnames" "github.com/sourcegraph/sourcegraph/internal/api" "github.com/sourcegraph/sourcegraph/internal/auth" @@ -478,6 +479,18 @@ func (r *UserResolver) BatchChangesCodeHosts(ctx context.Context, args *ListBatc return EnterpriseResolvers.batchChangesResolver.BatchChangesCodeHosts(ctx, args) } +func (r *UserResolver) Roles(ctx context.Context, args *ListRoleArgs) (*graphqlutil.ConnectionResolver[RoleResolver], error) { + id := r.ID() + args.User = &id + return EnterpriseResolvers.rbacResolver.Roles(ctx, args) +} + +func (r *UserResolver) Permissions(ctx context.Context, args *ListPermissionArgs) (*graphqlutil.ConnectionResolver[PermissionResolver], error) { + id := r.ID() + args.User = &id + return EnterpriseResolvers.rbacResolver.Permissions(ctx, args) +} + func viewerCanChangeUsername(ctx context.Context, db database.DB, userID int32) bool { if err := auth.CheckSiteAdminOrSameUser(ctx, db, userID); err != nil { return false diff --git a/cmd/frontend/internal/bg/update_permissions.go b/cmd/frontend/internal/bg/update_permissions.go index ac0962d1b223..b9561ac7c026 100644 --- a/cmd/frontend/internal/bg/update_permissions.go +++ b/cmd/frontend/internal/bg/update_permissions.go @@ -20,7 +20,7 @@ func UpdatePermissions(ctx context.Context, logger log.Logger, db database.DB) { err := db.WithTransact(ctx, func(tx database.DB) error { pstore := tx.Permissions() - dbPerms, err := pstore.List(ctx) + dbPerms, err := pstore.FetchAll(ctx) if err != nil { return errors.Wrap(err, "fetching permissions from database") } diff --git a/cmd/frontend/internal/cli/serve_cmd.go b/cmd/frontend/internal/cli/serve_cmd.go index fd216c1c5210..d28780fba317 100644 --- a/cmd/frontend/internal/cli/serve_cmd.go +++ b/cmd/frontend/internal/cli/serve_cmd.go @@ -226,6 +226,7 @@ func Main(ctx context.Context, observationCtx *observation.Context, ready servic enterpriseServices.ComputeResolver, enterpriseServices.InsightsAggregationResolver, enterpriseServices.WebhooksResolver, + enterpriseServices.RBACResolver, ) if err != nil { return err diff --git a/enterprise/cmd/frontend/internal/rbac/init.go b/enterprise/cmd/frontend/internal/rbac/init.go new file mode 100644 index 000000000000..f3723aea0640 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/init.go @@ -0,0 +1,29 @@ +package rbac + +import ( + "context" + + "github.com/sourcegraph/log" + + "github.com/sourcegraph/sourcegraph/cmd/frontend/enterprise" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/rbac/resolvers" + "github.com/sourcegraph/sourcegraph/enterprise/internal/codeintel" + "github.com/sourcegraph/sourcegraph/internal/conf/conftypes" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/observation" +) + +// Init initializes the given enterpriseServices to include the required resolvers for RBAC. +func Init( + ctx context.Context, + _ *observation.Context, + db database.DB, + _ codeintel.Services, + _ conftypes.UnifiedWatchable, + enterpriseServices *enterprise.Services, +) error { + logger := log.Scoped("rbac", "") + enterpriseServices.RBACResolver = resolvers.New(logger, db) + + return nil +} diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/apitest/exec.go b/enterprise/cmd/frontend/internal/rbac/resolvers/apitest/exec.go new file mode 100644 index 000000000000..69529bd1ad59 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/apitest/exec.go @@ -0,0 +1,86 @@ +package apitest + +import ( + "context" + "encoding/json" + "os" + "strings" + "testing" + + "github.com/graph-gophers/graphql-go" + gqlerrors "github.com/graph-gophers/graphql-go/errors" + + "github.com/sourcegraph/sourcegraph/internal/jsonc" +) + +// MustExec uses Exec to execute the given query and calls t.Fatalf if Exec failed. +func MustExec( + ctx context.Context, + t testing.TB, + s *graphql.Schema, + in map[string]any, + out any, + query string, +) { + t.Helper() + if errs := Exec(ctx, t, s, in, out, query); len(errs) > 0 { + t.Fatalf("unexpected graphql query errors: %v", errs) + } +} + +// Exec executes the given query with the given input in the given +// graphql.Schema. The response will be rendered into out. +func Exec( + ctx context.Context, + t testing.TB, + s *graphql.Schema, + in map[string]any, + out any, + query string, +) []*gqlerrors.QueryError { + t.Helper() + + query = strings.ReplaceAll(query, "\t", " ") + + b, err := json.Marshal(in) + if err != nil { + t.Fatalf("failed to marshal input: %s", err) + } + + var anonInput map[string]any + err = json.Unmarshal(b, &anonInput) + if err != nil { + t.Fatalf("failed to unmarshal input back: %s", err) + } + + r := s.Exec(ctx, query, "", anonInput) + if len(r.Errors) != 0 { + return r.Errors + } + + _, disableLog := os.LookupEnv("NO_GRAPHQL_LOG") + + if testing.Verbose() && !disableLog { + t.Logf("\n---- GraphQL Query ----\n%s\n\nVars: %s\n---- GraphQL Result ----\n%s\n -----------", query, toJSON(t, in), r.Data) + } + + if err := json.Unmarshal(r.Data, out); err != nil { + t.Fatalf("failed to unmarshal graphql data: %v", err) + } + + return nil +} + +func toJSON(t testing.TB, v any) string { + data, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + + formatted, err := jsonc.Format(string(data), nil) + if err != nil { + t.Fatal(err) + } + + return formatted +} diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/apitest/types.go b/enterprise/cmd/frontend/internal/rbac/resolvers/apitest/types.go new file mode 100644 index 000000000000..d3965495ce05 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/apitest/types.go @@ -0,0 +1,52 @@ +package apitest + +import "github.com/sourcegraph/sourcegraph/internal/gqlutil" + +type Permission struct { + Typename string `json:"__typename"` + ID string + Namespace string + Action string + CreatedAt gqlutil.DateTime +} + +type PageInfo struct { + HasNextPage bool + HasPreviousPage bool + + EndCursor *string + StartCursor *string +} + +type PermissionConnection struct { + Nodes []Permission + TotalCount int + PageInfo PageInfo +} + +type Role struct { + Typename string `json:"__typename"` + ID string + Name string + System bool + CreatedAt gqlutil.DateTime + DeletedAt *gqlutil.DateTime + Permissions PermissionConnection +} + +type RoleConnection struct { + Nodes []Role + TotalCount int + PageInfo PageInfo +} + +type User struct { + ID string + DatabaseID int32 + SiteAdmin bool + + // All permissions associated with the roles that have been assigned to the user. + Permissions PermissionConnection + // All roles assigned to this user. + Roles RoleConnection +} diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/error_test.go b/enterprise/cmd/frontend/internal/rbac/resolvers/error_test.go new file mode 100644 index 000000000000..3801bd464092 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/error_test.go @@ -0,0 +1,13 @@ +package resolvers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestErrIDIsZero(t *testing.T) { + e := ErrIDIsZero{} + + assert.Equal(t, e.Error(), "invalid node id") +} diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/errors.go b/enterprise/cmd/frontend/internal/rbac/resolvers/errors.go new file mode 100644 index 000000000000..ee6236aa2c86 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/errors.go @@ -0,0 +1,7 @@ +package resolvers + +type ErrIDIsZero struct{} + +func (e ErrIDIsZero) Error() string { + return "invalid node id" +} diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/main_test.go b/enterprise/cmd/frontend/internal/rbac/resolvers/main_test.go new file mode 100644 index 000000000000..f480237e6539 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/main_test.go @@ -0,0 +1,58 @@ +package resolvers + +import ( + "context" + "fmt" + "sync" + "testing" + + "github.com/graph-gophers/graphql-go" + "github.com/keegancsmith/sqlf" + + gql "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/types" +) + +func newSchema(db database.DB, r gql.RBACResolver) (*graphql.Schema, error) { + return gql.NewSchemaWithRBACResolver(db, r) +} + +var createTestUser = func() func(*testing.T, database.DB, bool) *types.User { + var mu sync.Mutex + count := 0 + + // This function replicates the minimum amount of work required by + // database.Users.Create to create a new user, but it doesn't require passing in + // a full database.NewUser every time. + return func(t *testing.T, db database.DB, siteAdmin bool) *types.User { + t.Helper() + + mu.Lock() + num := count + count++ + mu.Unlock() + + user := &types.User{ + Username: fmt.Sprintf("testuser-%d", num), + DisplayName: "testuser", + } + + q := sqlf.Sprintf("INSERT INTO users (username, site_admin) VALUES (%s, %t) RETURNING id, site_admin", user.Username, siteAdmin) + err := db.QueryRowContext(context.Background(), q.Query(sqlf.PostgresBindVar), q.Args()...).Scan(&user.ID, &user.SiteAdmin) + if err != nil { + t.Fatal(err) + } + + if user.SiteAdmin != siteAdmin { + t.Fatalf("user.SiteAdmin=%t, but expected is %t", user.SiteAdmin, siteAdmin) + } + + _, err = db.ExecContext(context.Background(), "INSERT INTO names(name, user_id) VALUES($1, $2)", user.Username, user.ID) + if err != nil { + t.Fatalf("failed to create name: %s", err) + } + + return user + } +}() diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/permission.go b/enterprise/cmd/frontend/internal/rbac/resolvers/permission.go new file mode 100644 index 000000000000..9d3302d9eea3 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/permission.go @@ -0,0 +1,41 @@ +package resolvers + +import ( + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/relay" + + gql "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" + "github.com/sourcegraph/sourcegraph/internal/gqlutil" + "github.com/sourcegraph/sourcegraph/internal/types" +) + +type permissionResolver struct { + permission *types.Permission +} + +var _ gql.PermissionResolver = &permissionResolver{} + +const permissionIDKind = "Permission" + +func marshalPermissionID(id int32) graphql.ID { return relay.MarshalID(permissionIDKind, id) } + +func unmarshalPermissionID(id graphql.ID) (permissionID int32, err error) { + err = relay.UnmarshalSpec(id, &permissionID) + return +} + +func (r *permissionResolver) ID() graphql.ID { + return marshalPermissionID(r.permission.ID) +} + +func (r *permissionResolver) Namespace() string { + return r.permission.Namespace +} + +func (r *permissionResolver) Action() string { + return r.permission.Action +} + +func (r *permissionResolver) CreatedAt() gqlutil.DateTime { + return gqlutil.DateTime{Time: r.permission.CreatedAt} +} diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/permission_connection_store.go b/enterprise/cmd/frontend/internal/rbac/resolvers/permission_connection_store.go new file mode 100644 index 000000000000..0e60abaffc80 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/permission_connection_store.go @@ -0,0 +1,65 @@ +package resolvers + +import ( + "context" + "strconv" + + "github.com/graph-gophers/graphql-go" + + gql "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" + "github.com/sourcegraph/sourcegraph/internal/database" +) + +type permisionConnectionStore struct { + db database.DB + roleID int32 + userID int32 +} + +func (pcs *permisionConnectionStore) MarshalCursor(node gql.PermissionResolver, _ database.OrderBy) (*string, error) { + cursor := string(node.ID()) + + return &cursor, nil +} + +func (pcs *permisionConnectionStore) UnmarshalCursor(cursor string, _ database.OrderBy) (*string, error) { + nodeID, err := unmarshalPermissionID(graphql.ID(cursor)) + if err != nil { + return nil, err + } + + id := strconv.Itoa(int(nodeID)) + + return &id, nil +} + +func (pcs *permisionConnectionStore) ComputeTotal(ctx context.Context) (*int32, error) { + count, err := pcs.db.Permissions().Count(ctx, database.PermissionListOpts{ + RoleID: pcs.roleID, + UserID: pcs.userID, + }) + if err != nil { + return nil, err + } + + total := int32(count) + return &total, nil +} + +func (pcs *permisionConnectionStore) ComputeNodes(ctx context.Context, args *database.PaginationArgs) ([]gql.PermissionResolver, error) { + permissions, err := pcs.db.Permissions().List(ctx, database.PermissionListOpts{ + PaginationArgs: args, + RoleID: pcs.roleID, + UserID: pcs.userID, + }) + if err != nil { + return nil, err + } + + var permissionResolvers []gql.PermissionResolver + for _, permission := range permissions { + permissionResolvers = append(permissionResolvers, &permissionResolver{permission: permission}) + } + + return permissionResolvers, nil +} diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/permission_test.go b/enterprise/cmd/frontend/internal/rbac/resolvers/permission_test.go new file mode 100644 index 000000000000..5e0cc2d04a9b --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/permission_test.go @@ -0,0 +1,94 @@ +package resolvers + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/sourcegraph/log/logtest" + "github.com/stretchr/testify/assert" + + "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/rbac/resolvers/apitest" + "github.com/sourcegraph/sourcegraph/internal/actor" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/database/dbtest" + "github.com/sourcegraph/sourcegraph/internal/gqlutil" +) + +func TestPermissionResolver(t *testing.T) { + if testing.Short() { + t.Skip() + } + + logger := logtest.Scoped(t) + + ctx := context.Background() + + db := database.NewDB(logger, dbtest.NewDB(logger, t)) + + user := createTestUser(t, db, false) + admin := createTestUser(t, db, true) + + userCtx := actor.WithActor(ctx, actor.FromUser(user.ID)) + adminCtx := actor.WithActor(ctx, actor.FromUser(admin.ID)) + + perm, err := db.Permissions().Create(ctx, database.CreatePermissionOpts{ + Namespace: "BATCHCHANGES", + Action: "READ", + }) + if err != nil { + t.Fatal(err) + } + + s, err := newSchema(db, &Resolver{ + db: db, + logger: logger, + }) + if err != nil { + t.Fatal(err) + } + + mpid := string(marshalPermissionID(perm.ID)) + + t.Run("as non site-administrator", func(t *testing.T) { + input := map[string]any{"permission": mpid} + var response struct{ Node apitest.Permission } + errs := apitest.Exec(userCtx, t, s, input, &response, queryPermissionNode) + + assert.Len(t, errs, 1) + assert.Equal(t, errs[0].Message, "must be site admin") + }) + + t.Run(" as site-administrator", func(t *testing.T) { + want := apitest.Permission{ + Typename: "Permission", + ID: mpid, + Namespace: perm.Namespace, + Action: perm.Action, + CreatedAt: gqlutil.DateTime{Time: perm.CreatedAt.Truncate(time.Second)}, + } + + input := map[string]any{"permission": mpid} + var response struct{ Node apitest.Permission } + apitest.MustExec(adminCtx, t, s, input, &response, queryPermissionNode) + if diff := cmp.Diff(want, response.Node); diff != "" { + t.Fatalf("unexpected response (-want +got):\n%s", diff) + } + }) +} + +const queryPermissionNode = ` +query ($permission: ID!) { + node(id: $permission) { + __typename + + ... on Permission { + id + namespace + action + createdAt + } + } +} +` diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/permissions.go b/enterprise/cmd/frontend/internal/rbac/resolvers/permissions.go new file mode 100644 index 000000000000..2eda544cb377 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/permissions.go @@ -0,0 +1,87 @@ +package resolvers + +import ( + "context" + + "github.com/graph-gophers/graphql-go" + + "github.com/sourcegraph/sourcegraph/lib/errors" + + gql "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" + "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil" + "github.com/sourcegraph/sourcegraph/internal/auth" + "github.com/sourcegraph/sourcegraph/internal/database" +) + +func (r *Resolver) permissionByID(ctx context.Context, id graphql.ID) (gql.PermissionResolver, error) { + // 🚨 SECURITY: Only site admins can query role permissions or all permissions. + if err := auth.CheckCurrentUserIsSiteAdmin(ctx, r.db); err != nil { + return nil, err + } + + permissionID, err := unmarshalPermissionID(id) + if err != nil { + return nil, err + } + + if permissionID == 0 { + return nil, ErrIDIsZero{} + } + + permission, err := r.db.Permissions().GetByID(ctx, database.GetPermissionOpts{ + ID: permissionID, + }) + if err != nil { + return nil, err + } + return &permissionResolver{permission: permission}, nil +} + +func (r *Resolver) Permissions(ctx context.Context, args *gql.ListPermissionArgs) (*graphqlutil.ConnectionResolver[gql.PermissionResolver], error) { + connectionStore := permisionConnectionStore{ + db: r.db, + } + + if args.User != nil { + userID, err := gql.UnmarshalUserID(*args.User) + if err != nil { + return nil, err + } + + if userID == 0 { + return nil, errors.New("invalid user id provided") + } + + // 🚨 SECURITY: Only viewable for self or by site admins. + if err := auth.CheckSiteAdminOrSameUser(ctx, r.db, userID); err != nil { + return nil, err + } + + connectionStore.userID = userID + } else if err := auth.CheckCurrentUserIsSiteAdmin(ctx, r.db); err != nil { // 🚨 SECURITY: Only site admins can query role permissions or all permissions. + return nil, err + } + + if args.Role != nil { + roleID, err := unmarshalRoleID(*args.Role) + if err != nil { + return nil, err + } + + if roleID == 0 { + return nil, errors.New("invalid role id provided") + } + + connectionStore.roleID = roleID + } + + return graphqlutil.NewConnectionResolver[gql.PermissionResolver]( + &connectionStore, + &args.ConnectionResolverArgs, + &graphqlutil.ConnectionResolverOptions{ + OrderBy: database.OrderBy{ + {Field: "permissions.id"}, + }, + }, + ) +} diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/permissions_test.go b/enterprise/cmd/frontend/internal/rbac/resolvers/permissions_test.go new file mode 100644 index 000000000000..10603741c3f5 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/permissions_test.go @@ -0,0 +1,245 @@ +package resolvers + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/sourcegraph/log/logtest" + "github.com/stretchr/testify/assert" + + gql "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/rbac/resolvers/apitest" + "github.com/sourcegraph/sourcegraph/internal/actor" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/database/dbtest" +) + +func TestPermissionsResolver(t *testing.T) { + logger := logtest.Scoped(t) + if testing.Short() { + t.Skip() + } + + ctx := context.Background() + + db := database.NewDB(logger, dbtest.NewDB(logger, t)) + + admin := createTestUser(t, db, true) + user := createTestUser(t, db, false) + + adminCtx := actor.WithActor(ctx, actor.FromUser(admin.ID)) + userCtx := actor.WithActor(ctx, actor.FromUser(user.ID)) + + s, err := newSchema(db, &Resolver{logger: logger, db: db}) + if err != nil { + t.Fatal(err) + } + + ps, err := db.Permissions().BulkCreate(ctx, []database.CreatePermissionOpts{ + { + Namespace: "TEST-NAMESPACE", + Action: "READ", + }, + { + Namespace: "TEST-NAMESPACE", + Action: "WRITE", + }, + { + Namespace: "TEST-NAMESPACE", + Action: "EXECUTE", + }, + }) + assert.NoError(t, err) + + t.Run("as non site-administrator", func(t *testing.T) { + input := map[string]any{"first": 1} + var response struct{ Permissions apitest.PermissionConnection } + errs := apitest.Exec(actor.WithActor(userCtx, actor.FromUser(user.ID)), t, s, input, &response, queryPermissionConnection) + + assert.Len(t, errs, 1) + assert.Equal(t, errs[0].Message, "must be site admin") + }) + + t.Run("as site-administrator", func(t *testing.T) { + want := []apitest.Permission{ + { + ID: string(marshalPermissionID(ps[2].ID)), + }, + { + ID: string(marshalPermissionID(ps[1].ID)), + }, + { + ID: string(marshalPermissionID(ps[0].ID)), + }, + } + + tests := []struct { + firstParam int + wantHasPreviousPage bool + wantHasNextPage bool + wantTotalCount int + wantNodes []apitest.Permission + }{ + {firstParam: 1, wantHasNextPage: true, wantHasPreviousPage: false, wantTotalCount: 3, wantNodes: want[:1]}, + {firstParam: 2, wantHasNextPage: true, wantHasPreviousPage: false, wantTotalCount: 3, wantNodes: want[:2]}, + {firstParam: 3, wantHasNextPage: false, wantHasPreviousPage: false, wantTotalCount: 3, wantNodes: want}, + {firstParam: 4, wantHasNextPage: false, wantHasPreviousPage: false, wantTotalCount: 3, wantNodes: want}, + } + + for _, tc := range tests { + t.Run(fmt.Sprintf("first=%d", tc.firstParam), func(t *testing.T) { + input := map[string]any{"first": int64(tc.firstParam)} + var response struct{ Permissions apitest.PermissionConnection } + apitest.MustExec(actor.WithActor(adminCtx, actor.FromUser(admin.ID)), t, s, input, &response, queryPermissionConnection) + + wantConnection := apitest.PermissionConnection{ + TotalCount: tc.wantTotalCount, + PageInfo: apitest.PageInfo{ + HasNextPage: tc.wantHasNextPage, + EndCursor: response.Permissions.PageInfo.EndCursor, + HasPreviousPage: tc.wantHasPreviousPage, + }, + Nodes: tc.wantNodes, + } + + if diff := cmp.Diff(wantConnection, response.Permissions); diff != "" { + t.Fatalf("wrong permissions response (-want +got):\n%s", diff) + } + }) + } + }) +} + +const queryPermissionConnection = ` +query($first: Int!) { + permissions(first: $first) { + totalCount + pageInfo { + hasNextPage + endCursor + } + nodes { + id + } + } +} +` + +// Check if its a different user, site admin and same user +func TestUserPermissionsListing(t *testing.T) { + logger := logtest.Scoped(t) + if testing.Short() { + t.Skip() + } + + ctx := context.Background() + db := database.NewDB(logger, dbtest.NewDB(logger, t)) + + userID := createTestUser(t, db, false).ID + actorCtx := actor.WithActor(ctx, actor.FromUser(userID)) + + adminUserID := createTestUser(t, db, true).ID + adminActorCtx := actor.WithActor(ctx, actor.FromUser(adminUserID)) + + r := &Resolver{logger: logger, db: db} + s, err := newSchema(db, r) + assert.NoError(t, err) + + // create a new role + role, err := db.Roles().Create(ctx, "TEST-ROLE", false) + assert.NoError(t, err) + + _, err = db.UserRoles().Create(ctx, database.CreateUserRoleOpts{ + RoleID: role.ID, + UserID: userID, + }) + assert.NoError(t, err) + + p, err := db.Permissions().Create(ctx, database.CreatePermissionOpts{ + Namespace: "TEST-NAMESPACE", + Action: "READ", + }) + assert.NoError(t, err) + + _, err = db.RolePermissions().Create(ctx, database.CreateRolePermissionOpts{ + RoleID: role.ID, + PermissionID: p.ID, + }) + assert.NoError(t, err) + + t.Run("listing a user's permissions (same user)", func(t *testing.T) { + userAPIID := string(gql.MarshalUserID(userID)) + input := map[string]any{"node": userAPIID} + + want := apitest.User{ + ID: userAPIID, + Permissions: apitest.PermissionConnection{ + TotalCount: 1, + Nodes: []apitest.Permission{ + { + ID: string(marshalPermissionID(p.ID)), + }, + }, + }, + } + + var response struct{ Node apitest.User } + apitest.MustExec(actorCtx, t, s, input, &response, listUserPermissions) + + if diff := cmp.Diff(want, response.Node); diff != "" { + t.Fatalf("wrong permission response (-want +got):\n%s", diff) + } + }) + + t.Run("listing a user's permissions (site admin)", func(t *testing.T) { + userAPIID := string(gql.MarshalUserID(userID)) + input := map[string]any{"node": userAPIID} + + want := apitest.User{ + ID: userAPIID, + Permissions: apitest.PermissionConnection{ + TotalCount: 1, + Nodes: []apitest.Permission{ + { + ID: string(marshalPermissionID(p.ID)), + }, + }, + }, + } + + var response struct{ Node apitest.User } + apitest.MustExec(adminActorCtx, t, s, input, &response, listUserPermissions) + + if diff := cmp.Diff(want, response.Node); diff != "" { + t.Fatalf("wrong permissions response (-want +got):\n%s", diff) + } + }) + + t.Run("non site-admin listing another user's permission", func(t *testing.T) { + userAPIID := string(gql.MarshalUserID(adminUserID)) + input := map[string]any{"node": userAPIID} + + var response struct{} + errs := apitest.Exec(actorCtx, t, s, input, &response, listUserPermissions) + assert.Len(t, errs, 1) + assert.Equal(t, errs[0].Message, "must be authenticated as the authorized user or site admin") + }) +} + +const listUserPermissions = ` +query ($node: ID!) { + node(id: $node) { + ... on User { + id + permissions(first: 10) { + totalCount + nodes { + id + } + } + } + } +} +` diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/resolver.go b/enterprise/cmd/frontend/internal/rbac/resolvers/resolver.go new file mode 100644 index 000000000000..a9956d0a55c1 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/resolver.go @@ -0,0 +1,33 @@ +package resolvers + +import ( + "context" + + "github.com/graph-gophers/graphql-go" + "github.com/sourcegraph/log" + + "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" + gql "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" + "github.com/sourcegraph/sourcegraph/internal/database" +) + +// Resolver is the GraphQL resolver of all things related to batch changes. +type Resolver struct { + logger log.Logger + db database.DB +} + +func New(logger log.Logger, db database.DB) gql.RBACResolver { + return &Resolver{logger: logger, db: db} +} + +func (r *Resolver) NodeResolvers() map[string]graphqlbackend.NodeByIDFunc { + return map[string]graphqlbackend.NodeByIDFunc{ + roleIDKind: func(ctx context.Context, id graphql.ID) (gql.Node, error) { + return r.roleByID(ctx, id) + }, + permissionIDKind: func(ctx context.Context, id graphql.ID) (gql.Node, error) { + return r.permissionByID(ctx, id) + }, + } +} diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/role.go b/enterprise/cmd/frontend/internal/rbac/resolvers/role.go new file mode 100644 index 000000000000..043de024b169 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/role.go @@ -0,0 +1,67 @@ +package resolvers + +import ( + "context" + + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/relay" + + gql "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" + "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil" + "github.com/sourcegraph/sourcegraph/internal/auth" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/gqlutil" + "github.com/sourcegraph/sourcegraph/internal/types" +) + +type roleResolver struct { + db database.DB + role *types.Role +} + +var _ gql.RoleResolver = &roleResolver{} + +const roleIDKind = "Role" + +func marshalRoleID(id int32) graphql.ID { return relay.MarshalID(roleIDKind, id) } + +func unmarshalRoleID(id graphql.ID) (roleID int32, err error) { + err = relay.UnmarshalSpec(id, &roleID) + return +} + +func (r *roleResolver) ID() graphql.ID { + return marshalRoleID(r.role.ID) +} + +func (r *roleResolver) Name() string { + return r.role.Name +} + +func (r *roleResolver) System() bool { + return r.role.System +} + +func (r *roleResolver) Permissions(ctx context.Context, args *gql.ListPermissionArgs) (*graphqlutil.ConnectionResolver[gql.PermissionResolver], error) { + // 🚨 SECURITY: Only viewable by site admins. + if err := auth.CheckCurrentUserIsSiteAdmin(ctx, r.db); err != nil { + return nil, err + } + + rid := marshalRoleID(r.role.ID) + args.Role = &rid + args.User = nil + connectionStore := &permisionConnectionStore{ + db: r.db, + roleID: r.role.ID, + } + return graphqlutil.NewConnectionResolver[gql.PermissionResolver]( + connectionStore, + &args.ConnectionResolverArgs, + nil, + ) +} + +func (r *roleResolver) CreatedAt() gqlutil.DateTime { + return gqlutil.DateTime{Time: r.role.CreatedAt} +} diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/role_connection_store.go b/enterprise/cmd/frontend/internal/rbac/resolvers/role_connection_store.go new file mode 100644 index 000000000000..f1eb88b6fd2b --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/role_connection_store.go @@ -0,0 +1,64 @@ +package resolvers + +import ( + "context" + "strconv" + + "github.com/graph-gophers/graphql-go" + + gql "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" + "github.com/sourcegraph/sourcegraph/internal/database" +) + +type roleConnectionStore struct { + db database.DB + system bool + userID int32 +} + +func (rcs *roleConnectionStore) MarshalCursor(node gql.RoleResolver, _ database.OrderBy) (*string, error) { + cursor := string(node.ID()) + + return &cursor, nil +} + +func (rcs *roleConnectionStore) UnmarshalCursor(cursor string, _ database.OrderBy) (*string, error) { + nodeID, err := unmarshalRoleID(graphql.ID(cursor)) + if err != nil { + return nil, err + } + + id := strconv.Itoa(int(nodeID)) + + return &id, nil +} + +func (rcs *roleConnectionStore) ComputeTotal(ctx context.Context) (*int32, error) { + count, err := rcs.db.Roles().Count(ctx, database.RolesListOptions{ + UserID: rcs.userID, + }) + if err != nil { + return nil, err + } + + total := int32(count) + return &total, nil +} + +func (rcs *roleConnectionStore) ComputeNodes(ctx context.Context, args *database.PaginationArgs) ([]gql.RoleResolver, error) { + roles, err := rcs.db.Roles().List(ctx, database.RolesListOptions{ + PaginationArgs: args, + System: rcs.system, + UserID: rcs.userID, + }) + if err != nil { + return nil, err + } + + var roleResolvers []gql.RoleResolver + for _, role := range roles { + roleResolvers = append(roleResolvers, &roleResolver{role: role, db: rcs.db}) + } + + return roleResolvers, nil +} diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/role_test.go b/enterprise/cmd/frontend/internal/rbac/resolvers/role_test.go new file mode 100644 index 000000000000..c80c12f84660 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/role_test.go @@ -0,0 +1,137 @@ +package resolvers + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/sourcegraph/log/logtest" + "github.com/stretchr/testify/assert" + + "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/rbac/resolvers/apitest" + "github.com/sourcegraph/sourcegraph/internal/actor" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/database/dbtest" + "github.com/sourcegraph/sourcegraph/internal/gqlutil" +) + +func TestRoleResolver(t *testing.T) { + if testing.Short() { + t.Skip() + } + + logger := logtest.Scoped(t) + + ctx := context.Background() + db := database.NewDB(logger, dbtest.NewDB(logger, t)) + + userID := createTestUser(t, db, false).ID + userCtx := actor.WithActor(ctx, actor.FromUser(userID)) + + adminUserID := createTestUser(t, db, true).ID + adminCtx := actor.WithActor(ctx, actor.FromUser(adminUserID)) + + perm, err := db.Permissions().Create(ctx, database.CreatePermissionOpts{ + Namespace: "BATCHCHANGES", + Action: "READ", + }) + if err != nil { + t.Fatal(err) + } + + role, err := db.Roles().Create(ctx, "BATCHCHANGES_ADMIN", false) + if err != nil { + t.Fatal(err) + } + + _, err = db.RolePermissions().Create(ctx, database.CreateRolePermissionOpts{ + RoleID: role.ID, + PermissionID: perm.ID, + }) + if err != nil { + t.Fatal(err) + } + + s, err := newSchema(db, &Resolver{ + db: db, + logger: logger, + }) + if err != nil { + t.Fatal(err) + } + + mrid := string(marshalRoleID(role.ID)) + mpid := string(marshalPermissionID(perm.ID)) + + t.Run("as site-administrator", func(t *testing.T) { + want := apitest.Role{ + Typename: "Role", + ID: mrid, + Name: role.Name, + System: role.System, + CreatedAt: gqlutil.DateTime{Time: role.CreatedAt.Truncate(time.Second)}, + DeletedAt: nil, + Permissions: apitest.PermissionConnection{ + TotalCount: 1, + PageInfo: apitest.PageInfo{ + HasNextPage: false, + HasPreviousPage: false, + }, + Nodes: []apitest.Permission{ + { + ID: mpid, + Namespace: perm.Namespace, + Action: perm.Action, + CreatedAt: gqlutil.DateTime{Time: perm.CreatedAt.Truncate(time.Second)}, + }, + }, + }, + } + + input := map[string]any{"role": mrid} + var response struct{ Node apitest.Role } + apitest.MustExec(adminCtx, t, s, input, &response, queryRoleNode) + if diff := cmp.Diff(want, response.Node); diff != "" { + t.Fatalf("unexpected response (-want +got):\n%s", diff) + } + }) + + t.Run("non site-administrator", func(t *testing.T) { + input := map[string]any{"role": mrid} + var response struct{ Node apitest.Role } + errs := apitest.Exec(userCtx, t, s, input, &response, queryRoleNode) + + assert.Len(t, errs, 1) + assert.Equal(t, errs[0].Message, "must be site admin") + }) + +} + +const queryRoleNode = ` +query ($role: ID!) { + node(id: $role) { + __typename + + ... on Role { + id + name + system + createdAt + permissions(first: 50) { + nodes { + id + namespace + action + createdAt + } + totalCount + pageInfo { + hasPreviousPage + hasNextPage + } + } + } + } +} +` diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/roles.go b/enterprise/cmd/frontend/internal/rbac/resolvers/roles.go new file mode 100644 index 000000000000..50cf10c63758 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/roles.go @@ -0,0 +1,75 @@ +package resolvers + +import ( + "context" + + "github.com/graph-gophers/graphql-go" + + "github.com/sourcegraph/sourcegraph/lib/errors" + + gql "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" + "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil" + "github.com/sourcegraph/sourcegraph/internal/auth" + "github.com/sourcegraph/sourcegraph/internal/database" +) + +func (r *Resolver) Roles(ctx context.Context, args *gql.ListRoleArgs) (*graphqlutil.ConnectionResolver[gql.RoleResolver], error) { + connectionStore := roleConnectionStore{ + db: r.db, + system: args.System, + } + + if args.User != nil { + userID, err := gql.UnmarshalUserID(*args.User) + if err != nil { + return nil, err + } + + if userID == 0 { + return nil, errors.New("invalid user id provided") + } + + // 🚨 SECURITY: Only viewable for self or by site admins. + if err := auth.CheckSiteAdminOrSameUser(ctx, r.db, userID); err != nil { + return nil, err + } + + connectionStore.userID = userID + } else if err := auth.CheckCurrentUserIsSiteAdmin(ctx, r.db); err != nil { // 🚨 SECURITY: Only site admins can query all roles. + return nil, err + } + + return graphqlutil.NewConnectionResolver[gql.RoleResolver]( + &connectionStore, + &args.ConnectionResolverArgs, + &graphqlutil.ConnectionResolverOptions{ + OrderBy: database.OrderBy{ + {Field: "roles.id"}, + }, + }, + ) +} + +func (r *Resolver) roleByID(ctx context.Context, id graphql.ID) (gql.RoleResolver, error) { + // 🚨 SECURITY: Only site admins can query role permissions or all permissions. + if err := auth.CheckCurrentUserIsSiteAdmin(ctx, r.db); err != nil { + return nil, err + } + + roleID, err := unmarshalRoleID(id) + if err != nil { + return nil, err + } + + if roleID == 0 { + return nil, ErrIDIsZero{} + } + + role, err := r.db.Roles().Get(ctx, database.GetRoleOpts{ + ID: roleID, + }) + if err != nil { + return nil, err + } + return &roleResolver{role: role, db: r.db}, nil +} diff --git a/enterprise/cmd/frontend/internal/rbac/resolvers/roles_test.go b/enterprise/cmd/frontend/internal/rbac/resolvers/roles_test.go new file mode 100644 index 000000000000..c1b91ea4d941 --- /dev/null +++ b/enterprise/cmd/frontend/internal/rbac/resolvers/roles_test.go @@ -0,0 +1,230 @@ +package resolvers + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/sourcegraph/log/logtest" + "github.com/stretchr/testify/assert" + + gql "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/rbac/resolvers/apitest" + "github.com/sourcegraph/sourcegraph/internal/actor" + "github.com/sourcegraph/sourcegraph/internal/database" + "github.com/sourcegraph/sourcegraph/internal/database/dbtest" + "github.com/sourcegraph/sourcegraph/internal/types" +) + +func TestRoleConnectionResolver(t *testing.T) { + logger := logtest.Scoped(t) + if testing.Short() { + t.Skip() + } + + ctx := context.Background() + db := database.NewDB(logger, dbtest.NewDB(logger, t)) + + userID := createTestUser(t, db, false).ID + userCtx := actor.WithActor(ctx, actor.FromUser(userID)) + + adminID := createTestUser(t, db, true).ID + adminCtx := actor.WithActor(ctx, actor.FromUser(adminID)) + + s, err := newSchema(db, &Resolver{logger: logger, db: db}) + if err != nil { + t.Fatal(err) + } + + // All sourcegraph instances are seeded with two system roles at migration, + // so we take those into account when querying roles. + siteAdminRole, err := db.Roles().Get(ctx, database.GetRoleOpts{ + Name: string(types.SiteAdministratorSystemRole), + }) + assert.NoError(t, err) + + userRole, err := db.Roles().Get(ctx, database.GetRoleOpts{ + Name: string(types.UserSystemRole), + }) + assert.NoError(t, err) + + r, err := db.Roles().Create(ctx, "TEST-ROLE", false) + assert.NoError(t, err) + + t.Run("as non site-administrator", func(t *testing.T) { + input := map[string]any{"first": 1} + var response struct{ Permissions apitest.PermissionConnection } + errs := apitest.Exec(userCtx, t, s, input, &response, queryPermissionConnection) + + assert.Len(t, errs, 1) + assert.Equal(t, errs[0].Message, "must be site admin") + }) + + t.Run("as site-administrator", func(t *testing.T) { + want := []apitest.Role{ + { + ID: string(marshalRoleID(r.ID)), + }, + { + ID: string(marshalRoleID(siteAdminRole.ID)), + }, + { + ID: string(marshalRoleID(userRole.ID)), + }, + } + + tests := []struct { + firstParam int + wantHasNextPage bool + wantHasPreviousPage bool + wantTotalCount int + wantNodes []apitest.Role + }{ + {firstParam: 1, wantHasNextPage: true, wantHasPreviousPage: false, wantTotalCount: 3, wantNodes: want[:1]}, + {firstParam: 2, wantHasNextPage: true, wantHasPreviousPage: false, wantTotalCount: 3, wantNodes: want[:2]}, + {firstParam: 3, wantHasNextPage: false, wantHasPreviousPage: false, wantTotalCount: 3, wantNodes: want}, + {firstParam: 4, wantHasNextPage: false, wantHasPreviousPage: false, wantTotalCount: 3, wantNodes: want}, + } + + for _, tc := range tests { + t.Run(fmt.Sprintf("first=%d", tc.firstParam), func(t *testing.T) { + input := map[string]any{"first": int64(tc.firstParam)} + var response struct{ Roles apitest.RoleConnection } + apitest.MustExec(adminCtx, t, s, input, &response, queryRoleConnection) + + wantConnection := apitest.RoleConnection{ + TotalCount: tc.wantTotalCount, + PageInfo: apitest.PageInfo{ + HasNextPage: tc.wantHasNextPage, + HasPreviousPage: tc.wantHasPreviousPage, + }, + Nodes: tc.wantNodes, + } + + if diff := cmp.Diff(wantConnection, response.Roles); diff != "" { + t.Fatalf("wrong roles response (-want +got):\n%s", diff) + } + }) + } + }) +} + +const queryRoleConnection = ` +query($first: Int!) { + roles(first: $first) { + totalCount + pageInfo { + hasNextPage + hasPreviousPage + } + nodes { + id + } + } +} +` + +func TestUserRoleListing(t *testing.T) { + logger := logtest.Scoped(t) + if testing.Short() { + t.Skip() + } + + ctx := context.Background() + db := database.NewDB(logger, dbtest.NewDB(logger, t)) + + userID := createTestUser(t, db, false).ID + actorCtx := actor.WithActor(ctx, actor.FromUser(userID)) + + adminUserID := createTestUser(t, db, true).ID + adminActorCtx := actor.WithActor(ctx, actor.FromUser(adminUserID)) + + r := &Resolver{logger: logger, db: db} + s, err := newSchema(db, r) + assert.NoError(t, err) + + // create a new role + role, err := db.Roles().Create(ctx, "TEST-ROLE", false) + assert.NoError(t, err) + + _, err = db.UserRoles().Create(ctx, database.CreateUserRoleOpts{ + RoleID: role.ID, + UserID: userID, + }) + assert.NoError(t, err) + + t.Run("listing a user's roles (same user)", func(t *testing.T) { + userAPIID := string(gql.MarshalUserID(userID)) + input := map[string]any{"node": userAPIID} + + want := apitest.User{ + ID: userAPIID, + Roles: apitest.RoleConnection{ + TotalCount: 1, + Nodes: []apitest.Role{ + { + ID: string(marshalRoleID(role.ID)), + }, + }, + }, + } + + var response struct{ Node apitest.User } + apitest.MustExec(actorCtx, t, s, input, &response, listUserRoles) + + if diff := cmp.Diff(want, response.Node); diff != "" { + t.Fatalf("wrong role response (-want +got):\n%s", diff) + } + }) + + t.Run("listing a user's roles (site admin)", func(t *testing.T) { + userAPIID := string(gql.MarshalUserID(userID)) + input := map[string]any{"node": userAPIID} + + want := apitest.User{ + ID: userAPIID, + Roles: apitest.RoleConnection{ + TotalCount: 1, + Nodes: []apitest.Role{ + { + ID: string(marshalRoleID(role.ID)), + }, + }, + }, + } + + var response struct{ Node apitest.User } + apitest.MustExec(adminActorCtx, t, s, input, &response, listUserRoles) + + if diff := cmp.Diff(want, response.Node); diff != "" { + t.Fatalf("wrong roles response (-want +got):\n%s", diff) + } + }) + + t.Run("non site-admin listing another user's roles", func(t *testing.T) { + userAPIID := string(gql.MarshalUserID(adminUserID)) + input := map[string]any{"node": userAPIID} + + var response struct{} + errs := apitest.Exec(actorCtx, t, s, input, &response, listUserRoles) + assert.Len(t, errs, 1) + assert.Equal(t, errs[0].Message, "must be authenticated as the authorized user or site admin") + }) +} + +const listUserRoles = ` +query ($node: ID!) { + node(id: $node) { + ... on User { + id + roles(first: 50) { + totalCount + nodes { + id + } + } + } + } +} +` diff --git a/enterprise/cmd/frontend/shared/shared.go b/enterprise/cmd/frontend/shared/shared.go index 67b5f1ef4a01..0240f28e727a 100644 --- a/enterprise/cmd/frontend/shared/shared.go +++ b/enterprise/cmd/frontend/shared/shared.go @@ -25,6 +25,7 @@ import ( "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/insights" licensing "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/licensing/init" "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/notebooks" + "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/rbac" _ "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/registry" "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/repos/webhooks" "github.com/sourcegraph/sourcegraph/enterprise/cmd/frontend/internal/searchcontexts" @@ -54,6 +55,7 @@ var initFunctions = map[string]EnterpriseInitializer{ "scim": scim.Init, "searchcontexts": searchcontexts.Init, "repos.webhooks": webhooks.Init, + "rbac": rbac.Init, } func EnterpriseSetupHook(db database.DB, conf conftypes.UnifiedWatchable) enterprise.Services { diff --git a/internal/database/permissions.go b/internal/database/permissions.go index 8d25b3f12261..81b3c9e55503 100644 --- a/internal/database/permissions.go +++ b/internal/database/permissions.go @@ -31,18 +31,22 @@ type PermissionStore interface { // WithTransact creates a transaction-enabled store for the permissionStore WithTransact(context.Context, func(PermissionStore) error) error - // Create inserts the given permission into the database. - Create(ctx context.Context, opts CreatePermissionOpts) (*types.Permission, error) // BulkCreate inserts multiple permissions into the database BulkCreate(ctx context.Context, opts []CreatePermissionOpts) ([]*types.Permission, error) - // Delete deletes a permission with the provided ID - Delete(ctx context.Context, opts DeletePermissionOpts) error // BulkDelete deletes a permission with the provided ID BulkDelete(ctx context.Context, opts []DeletePermissionOpts) error + // Count returns the number of permissions in the database matching the options provided. + Count(ctx context.Context, opts PermissionListOpts) (int, error) + // Create inserts the given permission into the database. + Create(ctx context.Context, opts CreatePermissionOpts) (*types.Permission, error) + // Delete deletes a permission with the provided ID + Delete(ctx context.Context, opts DeletePermissionOpts) error + // FetchAll returns all permissions in the database. This list is not paginated and is meant for internal use only. + FetchAll(ctx context.Context) ([]*types.Permission, error) // GetByID returns the permission matching the given ID, or PermissionNotFoundErr if no such record exists. GetByID(ctx context.Context, opts GetPermissionOpts) (*types.Permission, error) - // List returns all the permissions in the database. - List(ctx context.Context) ([]*types.Permission, error) + // List returns all the permissions in the database that matches the options. + List(ctx context.Context, opts PermissionListOpts) ([]*types.Permission, error) } type CreatePermissionOpts struct { @@ -59,6 +63,13 @@ type ( DeletePermissionOpts PermissionOpts ) +type PermissionListOpts struct { + PaginationArgs *PaginationArgs + + RoleID int32 + UserID int32 +} + type PermissionNotFoundErr struct { ID int32 } @@ -104,7 +115,7 @@ func (p *permissionStore) Create(ctx context.Context, opts CreatePermissionOpts) permission, err := scanPermission(p.QueryRow(ctx, q)) if err != nil { - return nil, errors.Wrap(err, "scanning role") + return nil, errors.Wrap(err, "scanning permission") } return permission, nil @@ -176,7 +187,7 @@ func (p *permissionStore) Delete(ctx context.Context, opts DeletePermissionOpts) } if rowsAffected == 0 { - return errors.Wrap(&RoleNotFoundErr{opts.ID}, "failed to delete permission") + return errors.Wrap(&PermissionNotFoundErr{opts.ID}, "failed to delete permission") } return nil } @@ -241,30 +252,136 @@ func (p *permissionStore) GetByID(ctx context.Context, opts GetPermissionOpts) ( return permission, nil } -// The ORDER BY clause should not be changed because it ensures permissions retrieved -// from the database are already sorted therefore making the rbac schema migration easy. -// We compare permissions in the database to those generated from the schema and both -// need to be sorted. -const permissionListQueryFmtStr = ` -SELECT * FROM permissions -ORDER BY permissions.namespace, permissions.action ASC -` +func (p *permissionStore) FetchAll(ctx context.Context) ([]*types.Permission, error) { + query := sqlf.Sprintf( + "SELECT %s FROM permissions", + sqlf.Join(permissionColumns, ", "), + ) -func (p *permissionStore) List(ctx context.Context) ([]*types.Permission, error) { - var permissions []*types.Permission - rows, err := p.Query(ctx, sqlf.Sprintf(permissionListQueryFmtStr)) + rows, err := p.Query(ctx, query) if err != nil { return nil, errors.Wrap(err, "error running query") } defer rows.Close() + + var permissions []*types.Permission for rows.Next() { - perm, err := scanPermission(rows) + permission, err := scanPermission(rows) if err != nil { - return nil, err + return nil, errors.Wrap(err, "scanning permission") } - permissions = append(permissions, perm) + + permissions = append(permissions, permission) } return permissions, rows.Err() } + +const permissionListQueryFmtStr = ` +SELECT %s FROM permissions +%s +WHERE %s +` + +func (p *permissionStore) List(ctx context.Context, opts PermissionListOpts) ([]*types.Permission, error) { + var permissions []*types.Permission + + scanFunc := func(rows *sql.Rows) error { + permission, err := scanPermission(rows) + if err != nil { + return errors.Wrap(err, "scanning permission") + } + permissions = append(permissions, permission) + return nil + } + + err := p.list(ctx, opts, scanFunc) + return permissions, err +} + +func (p *permissionStore) list(ctx context.Context, opts PermissionListOpts, scanFunc func(rows *sql.Rows) error) error { + conds, joins := p.computeConditionsAndJoins(opts) + + queryArgs := opts.PaginationArgs.SQL() + if queryArgs.Where != nil { + conds = append(conds, queryArgs.Where) + } + + if len(conds) == 0 { + conds = append(conds, sqlf.Sprintf("TRUE")) + } + + query := sqlf.Sprintf( + permissionListQueryFmtStr, + sqlf.Join(permissionColumns, ", "), + joins, + sqlf.Join(conds, "AND "), + ) + + if opts.UserID != 0 { + // We group by `permissions.id` because it's possible for a user to have multiple occurrences of a particular + // permission. We only want the distinct permissions assigned to a user. + query = sqlf.Sprintf("%s\n%s", query, sqlf.Sprintf("GROUP BY permissions.id")) + } + + query = queryArgs.AppendOrderToQuery(query) + query = queryArgs.AppendLimitToQuery(query) + + rows, err := p.Query(ctx, query) + if err != nil { + return errors.Wrap(err, "error running query") + } + + defer rows.Close() + for rows.Next() { + if err := scanFunc(rows); err != nil { + return err + } + } + + return rows.Err() +} + +func (p *permissionStore) computeConditionsAndJoins(opts PermissionListOpts) ([]*sqlf.Query, *sqlf.Query) { + conds := []*sqlf.Query{} + joins := sqlf.Sprintf("") + + if opts.RoleID != 0 { + conds = append(conds, sqlf.Sprintf("role_permissions.role_id = %s", opts.RoleID)) + joins = sqlf.Sprintf("INNER JOIN role_permissions ON role_permissions.permission_id = permissions.id") + } + + if opts.UserID != 0 { + conds = append(conds, sqlf.Sprintf("user_roles.user_id = %s", opts.UserID)) + joins = sqlf.Sprintf(` +INNER JOIN role_permissions ON role_permissions.permission_id = permissions.id +INNER JOIN user_roles ON user_roles.role_id = role_permissions.role_id +`) + } + + return conds, joins +} + +const permissionCountQueryFmtstr = ` +SELECT COUNT(DISTINCT id) FROM permissions +%s +WHERE %s +` + +func (p *permissionStore) Count(ctx context.Context, opts PermissionListOpts) (c int, err error) { + conds, joins := p.computeConditionsAndJoins(opts) + + if len(conds) == 0 { + conds = append(conds, sqlf.Sprintf("TRUE")) + } + + query := sqlf.Sprintf( + permissionCountQueryFmtstr, + joins, + sqlf.Join(conds, " AND "), + ) + + count, _, err := basestore.ScanFirstInt(p.Query(ctx, query)) + return count, err +} diff --git a/internal/database/permissions_test.go b/internal/database/permissions_test.go index e20e7077094f..ca0dfd0cbb24 100644 --- a/internal/database/permissions_test.go +++ b/internal/database/permissions_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/sourcegraph/sourcegraph/internal/database/dbtest" + "github.com/sourcegraph/sourcegraph/internal/types" ) func TestPermissionGetByID(t *testing.T) { @@ -71,18 +72,56 @@ func TestPermissionList(t *testing.T) { db := NewDB(logger, dbtest.NewDB(logger, t)) store := db.Permissions() - totalPerms := 10 - for i := 1; i <= totalPerms; i++ { - _, err := store.Create(ctx, CreatePermissionOpts{ - Namespace: fmt.Sprintf("PERMISSION-%d", i), - Action: "READ", + role, user, totalPerms := seedPermissionDataForList(ctx, t, store, db) + firstParam := 100 + + t.Run("all permissions", func(t *testing.T) { + ps, err := store.List(ctx, PermissionListOpts{ + PaginationArgs: &PaginationArgs{ + First: &firstParam, + }, }) + assert.NoError(t, err) - } + assert.Len(t, ps, totalPerms) + assert.LessOrEqual(t, len(ps), firstParam) + }) - ps, err := store.List(ctx) - assert.NoError(t, err) - assert.Len(t, ps, totalPerms) + t.Run("with pagination", func(t *testing.T) { + firstParam := 2 + ps, err := store.List(ctx, PermissionListOpts{ + PaginationArgs: &PaginationArgs{ + First: &firstParam, + }, + }) + + assert.NoError(t, err) + assert.Len(t, ps, firstParam) + }) + + t.Run("role association", func(t *testing.T) { + ps, err := store.List(ctx, PermissionListOpts{ + PaginationArgs: &PaginationArgs{ + First: &firstParam, + }, + RoleID: role.ID, + }) + + assert.NoError(t, err) + assert.Len(t, ps, 2) + }) + + t.Run("user association", func(t *testing.T) { + ps, err := store.List(ctx, PermissionListOpts{ + PaginationArgs: &PaginationArgs{ + First: &firstParam, + }, + UserID: user.ID, + }) + + assert.NoError(t, err) + assert.Len(t, ps, 2) + }) } func TestPermissionDelete(t *testing.T) { @@ -206,3 +245,98 @@ func TestPermissionBulkDelete(t *testing.T) { assert.Equal(t, err, &PermissionNotFoundErr{ps[0].ID}) }) } + +func TestPermissionCount(t *testing.T) { + ctx := context.Background() + logger := logtest.Scoped(t) + db := NewDB(logger, dbtest.NewDB(logger, t)) + store := db.Permissions() + + role, user, totalPerms := seedPermissionDataForList(ctx, t, store, db) + + t.Run("all permissions", func(t *testing.T) { + count, err := store.Count(ctx, PermissionListOpts{}) + + assert.NoError(t, err) + assert.Equal(t, count, totalPerms) + }) + + t.Run("role permissions", func(t *testing.T) { + count, err := store.Count(ctx, PermissionListOpts{ + RoleID: role.ID, + }) + + assert.NoError(t, err) + assert.Equal(t, count, 2) + }) + + t.Run("user permissions", func(t *testing.T) { + count, err := store.Count(ctx, PermissionListOpts{ + UserID: user.ID, + }) + + assert.NoError(t, err) + assert.Equal(t, count, 2) + }) +} + +func TestPermissionFetchAll(t *testing.T) { + ctx := context.Background() + logger := logtest.Scoped(t) + db := NewDB(logger, dbtest.NewDB(logger, t)) + store := db.Permissions() + + _, _, totalPerms := seedPermissionDataForList(ctx, t, store, db) + + perms, err := store.FetchAll(ctx) + + assert.NoError(t, err) + assert.Len(t, perms, totalPerms) +} + +func seedPermissionDataForList(ctx context.Context, t *testing.T, store PermissionStore, db DB) (*types.Role, *types.User, int) { + t.Helper() + + perms, totalPerms := createTestPermissions(ctx, t, store) + user := createTestUserForUserRole(ctx, "test@test.com", "test-user-1", t, db) + role, err := createTestRole(ctx, "TEST-ROLE", false, t, db.Roles()) + assert.NoError(t, err) + + _, err = db.RolePermissions().Create(ctx, CreateRolePermissionOpts{ + RoleID: role.ID, + PermissionID: perms[0].ID, + }) + assert.NoError(t, err) + + _, err = db.RolePermissions().Create(ctx, CreateRolePermissionOpts{ + RoleID: role.ID, + PermissionID: perms[1].ID, + }) + assert.NoError(t, err) + + _, err = db.UserRoles().Create(ctx, CreateUserRoleOpts{ + RoleID: role.ID, + UserID: user.ID, + }) + assert.NoError(t, err) + + return role, user, totalPerms +} + +func createTestPermissions(ctx context.Context, t *testing.T, store PermissionStore) ([]*types.Permission, int) { + t.Helper() + + var permissions []*types.Permission + + totalPerms := 10 + for i := 1; i <= totalPerms; i++ { + permission, err := store.Create(ctx, CreatePermissionOpts{ + Namespace: fmt.Sprintf("PERMISSION-%d", i), + Action: "READ", + }) + assert.NoError(t, err) + permissions = append(permissions, permission) + } + + return permissions, totalPerms +} diff --git a/internal/database/roles.go b/internal/database/roles.go index 9dd61cdb22d2..df91f3e81731 100644 --- a/internal/database/roles.go +++ b/internal/database/roles.go @@ -59,8 +59,10 @@ type ( ) type RolesListOptions struct { - *LimitOffset + PaginationArgs *PaginationArgs + System bool + UserID int32 } type RoleNotFoundErr struct { @@ -136,7 +138,7 @@ func scanRole(sc dbutil.Scanner) (*types.Role, error) { } func (r *roleStore) List(ctx context.Context, opts RolesListOptions) ([]*types.Role, error) { - roles := make([]*types.Role, 0, 20) + var roles []*types.Role scanFunc := func(rows *sql.Rows) error { role, err := scanRole(rows) @@ -147,32 +149,35 @@ func (r *roleStore) List(ctx context.Context, opts RolesListOptions) ([]*types.R return nil } - err := r.list(ctx, opts, sqlf.Join(roleColumns, ", "), sqlf.Sprintf("ORDER BY roles.created_at ASC"), scanFunc) + err := r.list(ctx, opts, sqlf.Join(roleColumns, ", "), scanFunc) return roles, err } const roleListQueryFmtstr = ` -SELECT - %s -FROM roles -WHERE %s +SELECT %s FROM roles %s +WHERE %s ` -func (r *roleStore) list(ctx context.Context, opts RolesListOptions, selects, orderByQuery *sqlf.Query, scanRole func(rows *sql.Rows) error) error { - var conds = []*sqlf.Query{sqlf.Sprintf("deleted_at IS NULL")} +func (r *roleStore) list(ctx context.Context, opts RolesListOptions, selects *sqlf.Query, scanRole func(rows *sql.Rows) error) error { + conds, joins := r.computeConditionsAndJoins(opts) - if opts.System { - conds = append(conds, sqlf.Sprintf("system IS TRUE")) + queryArgs := opts.PaginationArgs.SQL() + if queryArgs.Where != nil { + conds = append(conds, queryArgs.Where) } - q := sqlf.Sprintf(roleListQueryFmtstr, selects, sqlf.Join(conds, " AND "), orderByQuery) + query := sqlf.Sprintf( + roleListQueryFmtstr, + selects, + joins, + sqlf.Join(conds, " AND "), + ) - if opts.LimitOffset != nil { - q = sqlf.Sprintf("%s\n%s", q, opts.LimitOffset.SQL()) - } + query = queryArgs.AppendOrderToQuery(query) + query = queryArgs.AppendLimitToQuery(query) - rows, err := r.Query(ctx, q) + rows, err := r.Query(ctx, query) if err != nil { return errors.Wrap(err, "error running query") } @@ -186,6 +191,22 @@ func (r *roleStore) list(ctx context.Context, opts RolesListOptions, selects, or return rows.Err() } +func (r *roleStore) computeConditionsAndJoins(opts RolesListOptions) ([]*sqlf.Query, *sqlf.Query) { + var conds = []*sqlf.Query{sqlf.Sprintf("deleted_at IS NULL")} + var joins = sqlf.Sprintf("") + + if opts.System { + conds = append(conds, sqlf.Sprintf("system IS TRUE")) + } + + if opts.UserID != 0 { + conds = append(conds, sqlf.Sprintf("user_roles.user_id = %s", opts.UserID)) + joins = sqlf.Sprintf("INNER JOIN user_roles ON user_roles.role_id = roles.id") + } + + return conds, joins +} + const roleCreateQueryFmtStr = ` INSERT INTO roles (%s) @@ -215,12 +236,22 @@ func (r *roleStore) Create(ctx context.Context, name string, isSystemRole bool) return role, nil } +const roleCountQueryFmtstr = ` +SELECT COUNT(1) FROM roles +%s +WHERE %s +` + func (r *roleStore) Count(ctx context.Context, opts RolesListOptions) (c int, err error) { - opts.LimitOffset = nil - err = r.list(ctx, opts, sqlf.Sprintf("COUNT(1)"), sqlf.Sprintf(""), func(rows *sql.Rows) error { - return rows.Scan(&c) - }) - return c, err + conds, joins := r.computeConditionsAndJoins(opts) + + query := sqlf.Sprintf( + roleCountQueryFmtstr, + joins, + sqlf.Join(conds, " AND "), + ) + count, _, err := basestore.ScanFirstInt(r.Query(ctx, query)) + return count, err } const roleUpdateQueryFmtstr = ` diff --git a/internal/database/roles_test.go b/internal/database/roles_test.go index 48b51a107cf1..e03b5c6b1de2 100644 --- a/internal/database/roles_test.go +++ b/internal/database/roles_test.go @@ -13,12 +13,12 @@ import ( ) // The database is already seeded with two roles: -// - DEFAULT +// - USER // - SITE_ADMINISTRATOR // // These roles come by default on any sourcegraph instance and will always exist in the database, // so we need to account for these roles when accessing the database. -var numberOfDefaultRoles = 2 +var numberOfSystemRoles = 2 func TestRoleGet(t *testing.T) { ctx := context.Background() @@ -61,28 +61,62 @@ func TestRoleList(t *testing.T) { db := NewDB(logger, dbtest.NewDB(logger, t)) store := db.Roles() - total := createTestRoles(ctx, t, store) + roles, total := createTestRoles(ctx, t, store) + user := createTestUserForUserRole(ctx, "test@test.com", "test-user-1", t, db) + + _, err := db.UserRoles().Create(ctx, CreateUserRoleOpts{ + RoleID: roles[0].ID, + UserID: user.ID, + }) + assert.NoError(t, err) + + firstParam := 100 + + t.Run("all roles", func(t *testing.T) { + allRoles, err := store.List(ctx, RolesListOptions{ + PaginationArgs: &PaginationArgs{ + First: &firstParam, + }, + }) - t.Run("basic no opts", func(t *testing.T) { - allRoles, err := store.List(ctx, RolesListOptions{}) assert.NoError(t, err) - assert.Len(t, allRoles, total+numberOfDefaultRoles) + assert.LessOrEqual(t, len(allRoles), firstParam) + assert.Len(t, allRoles, total+numberOfSystemRoles) }) t.Run("system roles", func(t *testing.T) { allSystemRoles, err := store.List(ctx, RolesListOptions{ + PaginationArgs: &PaginationArgs{ + First: &firstParam, + }, System: true, }) assert.NoError(t, err) - assert.Len(t, allSystemRoles, numberOfDefaultRoles) + assert.Len(t, allSystemRoles, numberOfSystemRoles) }) t.Run("with pagination", func(t *testing.T) { + firstParam := 2 roles, err := store.List(ctx, RolesListOptions{ - LimitOffset: &LimitOffset{Limit: 2, Offset: 1}, + PaginationArgs: &PaginationArgs{ + First: &firstParam, + }, + }) + + assert.NoError(t, err) + assert.Len(t, roles, firstParam) + }) + + t.Run("user roles", func(t *testing.T) { + userRoles, err := store.List(ctx, RolesListOptions{ + PaginationArgs: &PaginationArgs{ + First: &firstParam, + }, + UserID: user.ID, }) assert.NoError(t, err) - assert.Len(t, roles, 2) + assert.Len(t, userRoles, 1) + assert.Equal(t, userRoles[0].ID, roles[0].ID) }) } @@ -103,11 +137,39 @@ func TestRoleCount(t *testing.T) { db := NewDB(logger, dbtest.NewDB(logger, t)) store := db.Roles() - total := createTestRoles(ctx, t, store) + user := createTestUserForUserRole(ctx, "test@test.com", "test-user-1", t, db) + roles, total := createTestRoles(ctx, t, store) - count, err := store.Count(ctx, RolesListOptions{}) + _, err := db.UserRoles().Create(ctx, CreateUserRoleOpts{ + RoleID: roles[0].ID, + UserID: user.ID, + }) assert.NoError(t, err) - assert.Equal(t, count, total+numberOfDefaultRoles) + + t.Run("all roles", func(t *testing.T) { + count, err := store.Count(ctx, RolesListOptions{}) + + assert.NoError(t, err) + assert.Equal(t, count, total+numberOfSystemRoles) + }) + + t.Run("system roles", func(t *testing.T) { + count, err := store.Count(ctx, RolesListOptions{ + System: true, + }) + + assert.NoError(t, err) + assert.Equal(t, count, numberOfSystemRoles) + }) + + t.Run("user roles", func(t *testing.T) { + count, err := store.Count(ctx, RolesListOptions{ + UserID: user.ID, + }) + + assert.NoError(t, err) + assert.Equal(t, count, 1) + }) } func TestRoleUpdate(t *testing.T) { @@ -174,15 +236,17 @@ func TestRoleDelete(t *testing.T) { }) } -func createTestRoles(ctx context.Context, t *testing.T, store RoleStore) int { +func createTestRoles(ctx context.Context, t *testing.T, store RoleStore) ([]*types.Role, int) { t.Helper() + var roles []*types.Role totalRoles := 10 name := "TESTROLE" for i := 1; i <= totalRoles; i++ { - _, err := createTestRole(ctx, fmt.Sprintf("%s-%d", name, i), false, t, store) + role, err := createTestRole(ctx, fmt.Sprintf("%s-%d", name, i), false, t, store) assert.NoError(t, err) + roles = append(roles, role) } - return totalRoles + return roles, totalRoles } func createTestRole(ctx context.Context, name string, isSystemRole bool, t *testing.T, store RoleStore) (*types.Role, error) { From 2939fb1300d431075994ebb7d50ab2352b8983a9 Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Sat, 4 Feb 2023 07:20:02 +0100 Subject: [PATCH 416/678] make Cloud CTAs less obtrusive (#47400) --- .../searchContexts/SearchContextsListPage.tsx | 52 +++--- client/web/src/nav/GlobalNavbar.module.scss | 11 -- client/web/src/nav/GlobalNavbar.tsx | 5 +- .../__snapshots__/GlobalNavbar.test.tsx.snap | 2 +- .../notebooks/listPage/NotebooksListPage.tsx | 21 +-- .../web/src/search/home/CloudHomepageCta.tsx | 47 ------ client/web/src/search/home/SearchPage.tsx | 20 +-- .../search/results/SearchResultsInfoBar.tsx | 20 +-- .../SearchResultsInfoBar.test.tsx.snap | 155 ------------------ 9 files changed, 43 insertions(+), 290 deletions(-) delete mode 100644 client/web/src/search/home/CloudHomepageCta.tsx diff --git a/client/web/src/enterprise/searchContexts/SearchContextsListPage.tsx b/client/web/src/enterprise/searchContexts/SearchContextsListPage.tsx index a918a949abc2..e8f489060311 100644 --- a/client/web/src/enterprise/searchContexts/SearchContextsListPage.tsx +++ b/client/web/src/enterprise/searchContexts/SearchContextsListPage.tsx @@ -8,6 +8,7 @@ import { buildCloudTrialURL } from '@sourcegraph/shared/src/util/url' import { PageHeader, Link, Button, Icon, Alert } from '@sourcegraph/wildcard' import { AuthenticatedUser } from '../../auth' +import { CloudCtaBanner } from '../../components/CloudCtaBanner' import { Page } from '../../components/Page' import { eventLogger } from '../../tracking/eventLogger' @@ -41,32 +42,37 @@ export const SearchContextsListPage: React.FunctionComponent Create search context - {isSourcegraphDotCom && ( - - )}
    } description={ - - Search code you care about with search contexts.{' '} - - Learn more - - + <> + + Search code you care about with search contexts.{' '} + + Learn more + + + {isSourcegraphDotCom && ( + + To search across your team's private repos,{' '} + + eventLogger.log('ClickedOnCloudCTA', { cloudCtaType: 'ContextsSettings' }) + } + > + try Sourcegraph Cloud + + . + + )} + } className="mb-3" > diff --git a/client/web/src/nav/GlobalNavbar.module.scss b/client/web/src/nav/GlobalNavbar.module.scss index fcbdf8147a5f..b5a89d7f0cf8 100644 --- a/client/web/src/nav/GlobalNavbar.module.scss +++ b/client/web/src/nav/GlobalNavbar.module.scss @@ -8,17 +8,6 @@ border-radius: var(--border-radius); } -.sign-up { - border: 1px solid transparent; - background-color: var(--brand-secondary); - color: var(--white); - - &:hover { - background-color: var(--brand-secondary-3); - color: var(--white); - } -} - .feedback-trigger { border: 1px solid var(--border-color); white-space: normal; diff --git a/client/web/src/nav/GlobalNavbar.tsx b/client/web/src/nav/GlobalNavbar.tsx index a8e8dab05906..c754c645c02b 100644 --- a/client/web/src/nav/GlobalNavbar.tsx +++ b/client/web/src/nav/GlobalNavbar.tsx @@ -287,7 +287,8 @@ export const GlobalNavbar: React.FunctionComponent eventLogger.log('ClickedOnCloudCTA', { cloudCtaType: 'NavBarLoggedIn' })} @@ -329,8 +330,8 @@ export const GlobalNavbar: React.FunctionComponent eventLogger.log('ClickedOnTopNavTrialButton')} > diff --git a/client/web/src/nav/__snapshots__/GlobalNavbar.test.tsx.snap b/client/web/src/nav/__snapshots__/GlobalNavbar.test.tsx.snap index 1be53c298d47..c6b21baa6b91 100644 --- a/client/web/src/nav/__snapshots__/GlobalNavbar.test.tsx.snap +++ b/client/web/src/nav/__snapshots__/GlobalNavbar.test.tsx.snap @@ -283,7 +283,7 @@ exports[`GlobalNavbar default 1`] = ` Sign in