Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit 0517ceb

Browse files
tjkandalaunknwon
andauthored
extensibility: add featured extensions to registry (#21665)
Co-authored-by: ᴜɴᴋɴᴡᴏɴ <[email protected]>
1 parent db2f3e8 commit 0517ceb

23 files changed

+455
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ All notable changes to Sourcegraph are documented in this file.
2121
- Code Insights creation UI now has auto-save logic and clear all fields functionality [#21744](https://github.com/sourcegraph/sourcegraph/pull/21744)
2222
- A new bulk operation to retry many changesets at once has been added to Batch Changes. [#21173](https://github.com/sourcegraph/sourcegraph/pull/21173)
2323
- A `security_event_logs` database table has been added in support of upcoming security-related efforts. [#21949](https://github.com/sourcegraph/sourcegraph/pull/21949)
24+
- Added featured Sourcegraph extensions query to the GraphQL API, as well as a section in the extension registry to display featured extensions. [#21665](https://github.com/sourcegraph/sourcegraph/pull/21665)
2425

2526
### Changed
2627

client/web/src/extensions/ExtensionCard.scss

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
@import '../../../branded/src/components/Toggle.scss';
22

33
.extension-card {
4+
--icon-width: 3rem;
5+
46
&__background-section {
57
height: 4.25rem;
68
position: relative;
@@ -19,16 +21,26 @@
1921
opacity: 0.15;
2022
}
2123
}
24+
25+
&--featured {
26+
height: 8rem;
27+
}
2228
}
2329

2430
&__icon {
25-
width: 3rem;
31+
width: var(--icon-width);
2632
height: 3rem;
2733
object-fit: contain;
2834

2935
position: absolute;
3036
left: 0;
3137
margin-left: 0.75rem;
38+
39+
&--featured {
40+
// horizontally center icon over ::before pseudo-element background
41+
left: calc(50% - (var(--icon-width) / 2));
42+
margin-left: 0;
43+
}
3244
}
3345

3446
&__badge {

client/web/src/extensions/ExtensionCard.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ interface Props extends SettingsCascadeProps, PlatformContextProps<'updateSettin
4545
settingsURL: string | null | undefined
4646
/** The currently authenticated user. */
4747
authenticatedUser: AuthenticatedUser | null
48+
49+
/** Whether this is a featured extension. */
50+
featured?: boolean
4851
}
4952

5053
/** ms after which to remove visual feedback */
@@ -63,6 +66,7 @@ export const ExtensionCard = memo<Props>(function ExtensionCard({
6366
viewerSubject,
6467
siteSubject,
6568
authenticatedUser,
69+
featured,
6670
}) {
6771
const manifest: ExtensionManifest | undefined =
6872
extension.manifest && !isErrorLike(extension.manifest) ? extension.manifest : undefined
@@ -174,6 +178,8 @@ export const ExtensionCard = memo<Props>(function ExtensionCard({
174178
return headerColorFromExtensionID(extension.id)
175179
}, [manifest?.headerColor, extension.id])
176180

181+
const iconClassName = classNames('extension-card__icon', featured && 'extension-card__icon--featured')
182+
177183
return (
178184
<div
179185
className={classNames('extension-card card position-relative flex-1', {
@@ -185,15 +191,16 @@ export const ExtensionCard = memo<Props>(function ExtensionCard({
185191
<div
186192
className={classNames(
187193
'extension-card__background-section d-flex align-items-center',
188-
headerColorStyles[headerColorClassName]
194+
headerColorStyles[headerColorClassName],
195+
featured && 'extension-card__background-section--featured'
189196
)}
190197
>
191198
{icon ? (
192-
<img className="extension-card__icon" src={icon} alt="" />
199+
<img className={iconClassName} src={icon} alt="" />
193200
) : isSourcegraphExtension ? (
194-
<DefaultSourcegraphExtensionIcon className="extension-card__icon" />
201+
<DefaultSourcegraphExtensionIcon className={iconClassName} />
195202
) : (
196-
<DefaultExtensionIcon className="extension-card__icon" />
203+
<DefaultExtensionIcon className={iconClassName} />
197204
)}
198205
{extension.registryExtension?.isWorkInProgress && (
199206
<ExtensionStatusBadge
@@ -215,7 +222,12 @@ export const ExtensionCard = memo<Props>(function ExtensionCard({
215222
)}
216223
</span>
217224
</div>
218-
<div className="mt-3 extension-card__description">
225+
<div
226+
className={classNames(
227+
'mt-3 extension-card__description',
228+
featured && 'extension-card__description--featured'
229+
)}
230+
>
219231
{extension.manifest ? (
220232
isErrorLike(extension.manifest) ? (
221233
<span className="text-danger small" title={extension.manifest.message}>

client/web/src/extensions/ExtensionRegistry.tsx

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ import { eventLogger } from '../tracking/eventLogger'
2525

2626
import { ExtensionBanner } from './ExtensionBanner'
2727
import { ExtensionRegistrySidenav } from './ExtensionRegistrySidenav'
28-
import { configureExtensionRegistry, ConfiguredExtensionRegistry } from './extensions'
28+
import {
29+
configureExtensionRegistry,
30+
ConfiguredExtensionRegistry,
31+
MinimalConfiguredRegistryExtension,
32+
configureFeaturedExtensions,
33+
} from './extensions'
2934
import { ExtensionsAreaRouteContext } from './ExtensionsArea'
3035
import { ExtensionsList } from './ExtensionsList'
3136

@@ -43,21 +48,33 @@ const URL_QUERY_PARAM = 'query'
4348
const URL_CATEGORY_PARAM = 'category'
4449
const SHOW_EXPERIMENTAL_EXTENSIONS_KEY = 'show-experimental-extensions'
4550

46-
export type ExtensionListData = typeof LOADING | (ConfiguredExtensionRegistry & { error: string | null }) | ErrorLike
51+
export type ExtensionListData =
52+
| typeof LOADING
53+
| (ConfiguredExtensionRegistry & {
54+
featuredExtensions?: MinimalConfiguredRegistryExtension[]
55+
error: string | null
56+
})
57+
| ErrorLike
4758

4859
export type ExtensionsEnablement = 'all' | 'enabled' | 'disabled'
4960

5061
export type ExtensionCategoryOrAll = ExtensionCategory | 'All'
5162

5263
const extensionRegistryQuery = gql`
53-
query RegistryExtensions($query: String, $prioritizeExtensionIDs: [String!]!) {
64+
query RegistryExtensions($query: String, $prioritizeExtensionIDs: [String!]!, $getFeatured: Boolean!) {
5465
extensionRegistry {
5566
extensions(query: $query, prioritizeExtensionIDs: $prioritizeExtensionIDs) {
5667
nodes {
5768
...RegistryExtensionFieldsForList
5869
}
5970
error
6071
}
72+
featuredExtensions @include(if: $getFeatured) {
73+
nodes {
74+
...RegistryExtensionFieldsForList
75+
}
76+
error
77+
}
6178
}
6279
}
6380
fragment RegistryExtensionFieldsForList on RegistryExtension {
@@ -95,10 +112,7 @@ const extensionRegistryQuery = gql`
95112
}
96113
`
97114

98-
export type ConfiguredExtensionCache = Map<
99-
string,
100-
Pick<ConfiguredRegistryExtension<RegistryExtensionFieldsForList>, 'manifest' | 'id'>
101-
>
115+
export type ConfiguredExtensionCache = Map<string, MinimalConfiguredRegistryExtension>
102116

103117
/** A page that displays overview information about the available extensions. */
104118
export const ExtensionRegistry: React.FunctionComponent<Props> = props => {
@@ -179,12 +193,19 @@ export const ExtensionRegistry: React.FunctionComponent<Props> = props => {
179193
query = `${query} category:"${category}"`
180194
}
181195

196+
// Only fetch + show featured extensions when there's no query or category selected.
197+
const shouldGetFeaturedExtensions = category === 'All' && query.trim() === ''
198+
182199
const resultOrError = platformContext.requestGraphQL<
183200
RegistryExtensionsResult,
184201
RegistryExtensionsVariables
185202
>({
186203
request: extensionRegistryQuery,
187-
variables: { query, prioritizeExtensionIDs: viewerConfiguredExtensions },
204+
variables: {
205+
query,
206+
prioritizeExtensionIDs: viewerConfiguredExtensions,
207+
getFeatured: shouldGetFeaturedExtensions,
208+
},
188209
mightContainPrivateInfo: true,
189210
})
190211

@@ -206,8 +227,16 @@ export const ExtensionRegistry: React.FunctionComponent<Props> = props => {
206227

207228
const { error, nodes } = data.extensionRegistry.extensions
208229

230+
const featuredExtensions = data.extensionRegistry.featuredExtensions?.nodes
231+
? configureFeaturedExtensions(
232+
data.extensionRegistry.featuredExtensions.nodes,
233+
configuredExtensionCache
234+
)
235+
: undefined
236+
209237
return {
210238
error,
239+
featuredExtensions,
211240
...configureExtensionRegistry(nodes, configuredExtensionCache),
212241
}
213242
}),

client/web/src/extensions/ExtensionsList.scss

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,36 @@
22
&__cards {
33
display: grid;
44
grid-template-columns: repeat(auto-fill, minmax(17.5rem, 1fr));
5-
grid-auto-rows: minmax(4rem, auto);
5+
grid-auto-rows: minmax(16.25rem, auto);
66
gap: $spacer * 0.75;
7+
8+
&--featured {
9+
grid-auto-rows: minmax(21.25rem, auto);
10+
}
711
}
812

913
&__category {
1014
margin-top: 2rem;
1115
font-size: 1.5rem !important;
1216
}
17+
18+
&__featured-section {
19+
position: relative;
20+
// 2rem gap before next section, 1rem extra for visual effect of ::before pseudo-element.
21+
margin-bottom: 3rem;
22+
// Same as above but for 2rem gap between featured section and search bar.
23+
margin-top: 3rem;
24+
25+
// Use pseudo-element to make background "bleed out" of the container.
26+
&::before {
27+
content: '';
28+
position: absolute;
29+
z-index: -1;
30+
inset: -1rem;
31+
background-color: var(--color-bg-1);
32+
border: 1px solid var(--border-color-2);
33+
border-radius: var(--border-radius);
34+
padding: 0.5rem;
35+
}
36+
}
1337
}

client/web/src/extensions/ExtensionsList.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,39 @@ export const ExtensionsList: React.FunctionComponent<Props> = ({
8787
return <ErrorAlert error={data} />
8888
}
8989

90-
const { error, extensions, extensionIDsByCategory } = data
90+
const { error, extensions, extensionIDsByCategory, featuredExtensions } = data
91+
92+
const featuredExtensionsSection = featuredExtensions && featuredExtensions.length > 0 && (
93+
<div key="Featured" className="extensions-list__featured-section">
94+
<h3
95+
className="extensions-list__category mb-3 font-weight-normal"
96+
data-test-extension-category-header="Featured"
97+
>
98+
Featured
99+
</h3>
100+
<div className="extensions-list__cards extensions-list__cards--featured mt-1">
101+
{featuredExtensions.map(featuredExtension => (
102+
<ExtensionCard
103+
key={featuredExtension.id}
104+
subject={subject}
105+
viewerSubject={viewerSubject?.subject}
106+
siteSubject={siteSubject?.subject}
107+
node={featuredExtension}
108+
settingsCascade={settingsCascade}
109+
platformContext={platformContext}
110+
enabled={isExtensionEnabled(settingsCascade.final, featuredExtension.id)}
111+
enabledForAllUsers={
112+
siteSubject ? isExtensionEnabled(siteSubject.settings, featuredExtension.id) : false
113+
}
114+
isLightTheme={props.isLightTheme}
115+
settingsURL={authenticatedUser?.settingsURL}
116+
authenticatedUser={authenticatedUser}
117+
featured={true}
118+
/>
119+
))}
120+
</div>
121+
</div>
122+
)
91123

92124
if (Object.keys(extensions).length === 0) {
93125
return (
@@ -213,6 +245,7 @@ export const ExtensionsList: React.FunctionComponent<Props> = ({
213245
return (
214246
<>
215247
{error && <ErrorAlert className="mb-2" error={error} />}
248+
{featuredExtensionsSection}
216249
{categorySections.length > 0 ? (
217250
categorySections
218251
) : (

client/web/src/extensions/extensions.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@ import { RegistryExtensionFieldsForList } from '../graphql-operations'
1414
import { validCategories } from './extension/extension'
1515
import { ConfiguredExtensionCache, ExtensionsEnablement } from './ExtensionRegistry'
1616

17+
export type MinimalConfiguredRegistryExtension = Pick<
18+
ConfiguredRegistryExtension<RegistryExtensionFieldsForList>,
19+
'manifest' | 'id'
20+
>
21+
1722
export interface ConfiguredRegistryExtensions {
18-
[id: string]: Pick<ConfiguredRegistryExtension<RegistryExtensionFieldsForList>, 'manifest' | 'id'>
23+
[id: string]: MinimalConfiguredRegistryExtension
1924
}
2025

2126
export interface ConfiguredExtensionRegistry {
@@ -85,6 +90,30 @@ export function configureExtensionRegistry(
8590
return { extensions, extensionIDsByCategory }
8691
}
8792

93+
/**
94+
* Configures featured extensions to be displayed on the extension registry.
95+
*
96+
* Share configured extension cache with `configureExtensionRegistry`
97+
* since featured extensions are likely to be displayed twice on the page.
98+
*/
99+
export function configureFeaturedExtensions(
100+
featuredExtensions: RegistryExtensionFieldsForList[],
101+
configuredExtensionCache: ConfiguredExtensionCache
102+
): MinimalConfiguredRegistryExtension[] {
103+
const extensions: MinimalConfiguredRegistryExtension[] = []
104+
105+
for (const featuredExtension of featuredExtensions) {
106+
let configuredRegistryExtension = configuredExtensionCache.get(featuredExtension.id)
107+
if (!configuredRegistryExtension) {
108+
configuredRegistryExtension = toConfiguredRegistryExtension(featuredExtension)
109+
configuredExtensionCache.set(featuredExtension.id, configuredRegistryExtension)
110+
}
111+
extensions.push(configuredRegistryExtension)
112+
}
113+
114+
return extensions
115+
}
116+
88117
/**
89118
* Removes extensions that do not satify the enablement filter.
90119
*

client/web/src/integration/extension-registry.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ describe('Extension Registry', () => {
196196
error: null,
197197
nodes: registryExtensionNodes,
198198
},
199+
featuredExtensions: null,
199200
},
200201
}),
201202
Extensions: () => ({
@@ -274,7 +275,11 @@ describe('Extension Registry', () => {
274275
})
275276
}, 'RegistryExtensions')
276277

277-
assert.deepStrictEqual(request, { query: 'sqs', prioritizeExtensionIDs: ['sqs/word-count'] })
278+
assert.deepStrictEqual(request, {
279+
getFeatured: false,
280+
query: 'sqs',
281+
prioritizeExtensionIDs: ['sqs/word-count'],
282+
})
278283
})
279284
})
280285

client/web/src/integration/search.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,9 @@ describe('Search', () => {
222222
test('Is set from the URL query parameter when loading a search-related page', async () => {
223223
testContext.overrideGraphQL({
224224
...commonSearchGraphQLResults,
225-
RegistryExtensions: () => ({ extensionRegistry: { extensions: { error: null, nodes: [] } } }),
225+
RegistryExtensions: () => ({
226+
extensionRegistry: { extensions: { error: null, nodes: [] }, featuredExtensions: null },
227+
}),
226228
})
227229
testContext.overrideSearchStreamEvents(mockDefaultStreamEvents)
228230

cmd/frontend/graphqlbackend/extension_registry.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type ExtensionRegistryResolver interface {
4646
PublishExtension(context.Context, *ExtensionRegistryPublishExtensionArgs) (ExtensionRegistryMutationResult, error)
4747
DeleteExtension(context.Context, *ExtensionRegistryDeleteExtensionArgs) (*EmptyResponse, error)
4848
LocalExtensionIDPrefix() *string
49+
FeaturedExtensions(context.Context) (FeaturedExtensionsConnection, error)
4950

5051
ImplementsLocalExtensionRegistry() bool // not exposed via GraphQL
5152
// FilterRemoteExtensions enforces `allowRemoteExtensions` by returning a
@@ -154,3 +155,9 @@ type RegistryPublisherConnection interface {
154155
TotalCount(context.Context) (int32, error)
155156
PageInfo(context.Context) (*graphqlutil.PageInfo, error)
156157
}
158+
159+
// FeaturedExtensions is the interface for the GraphQL type FeaturedExtensionsConnection.
160+
type FeaturedExtensionsConnection interface {
161+
Nodes(context.Context) ([]RegistryExtension, error)
162+
Error(context.Context) *string
163+
}

0 commit comments

Comments
 (0)