extensibility: add featured extensions to registry (#21665)

Co-authored-by: ᴜɴᴋɴᴡᴏɴ <joe@sourcegraph.com>
This commit is contained in:
TJ Kandala 2021-06-10 13:55:20 -04:00 committed by GitHub
parent db2f3e8a3a
commit 0517ceb801
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 455 additions and 29 deletions

View File

@ -21,6 +21,7 @@ All notable changes to Sourcegraph are documented in this file.
- Code Insights creation UI now has auto-save logic and clear all fields functionality [#21744](https://github.com/sourcegraph/sourcegraph/pull/21744)
- A new bulk operation to retry many changesets at once has been added to Batch Changes. [#21173](https://github.com/sourcegraph/sourcegraph/pull/21173)
- A `security_event_logs` database table has been added in support of upcoming security-related efforts. [#21949](https://github.com/sourcegraph/sourcegraph/pull/21949)
- 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)
### Changed

View File

@ -1,6 +1,8 @@
@import '../../../branded/src/components/Toggle.scss';
.extension-card {
--icon-width: 3rem;
&__background-section {
height: 4.25rem;
position: relative;
@ -19,16 +21,26 @@
opacity: 0.15;
}
}
&--featured {
height: 8rem;
}
}
&__icon {
width: 3rem;
width: var(--icon-width);
height: 3rem;
object-fit: contain;
position: absolute;
left: 0;
margin-left: 0.75rem;
&--featured {
// horizontally center icon over ::before pseudo-element background
left: calc(50% - (var(--icon-width) / 2));
margin-left: 0;
}
}
&__badge {

View File

@ -45,6 +45,9 @@ interface Props extends SettingsCascadeProps, PlatformContextProps<'updateSettin
settingsURL: string | null | undefined
/** The currently authenticated user. */
authenticatedUser: AuthenticatedUser | null
/** Whether this is a featured extension. */
featured?: boolean
}
/** ms after which to remove visual feedback */
@ -63,6 +66,7 @@ export const ExtensionCard = memo<Props>(function ExtensionCard({
viewerSubject,
siteSubject,
authenticatedUser,
featured,
}) {
const manifest: ExtensionManifest | undefined =
extension.manifest && !isErrorLike(extension.manifest) ? extension.manifest : undefined
@ -174,6 +178,8 @@ export const ExtensionCard = memo<Props>(function ExtensionCard({
return headerColorFromExtensionID(extension.id)
}, [manifest?.headerColor, extension.id])
const iconClassName = classNames('extension-card__icon', featured && 'extension-card__icon--featured')
return (
<div
className={classNames('extension-card card position-relative flex-1', {
@ -185,15 +191,16 @@ export const ExtensionCard = memo<Props>(function ExtensionCard({
<div
className={classNames(
'extension-card__background-section d-flex align-items-center',
headerColorStyles[headerColorClassName]
headerColorStyles[headerColorClassName],
featured && 'extension-card__background-section--featured'
)}
>
{icon ? (
<img className="extension-card__icon" src={icon} alt="" />
<img className={iconClassName} src={icon} alt="" />
) : isSourcegraphExtension ? (
<DefaultSourcegraphExtensionIcon className="extension-card__icon" />
<DefaultSourcegraphExtensionIcon className={iconClassName} />
) : (
<DefaultExtensionIcon className="extension-card__icon" />
<DefaultExtensionIcon className={iconClassName} />
)}
{extension.registryExtension?.isWorkInProgress && (
<ExtensionStatusBadge
@ -215,7 +222,12 @@ export const ExtensionCard = memo<Props>(function ExtensionCard({
)}
</span>
</div>
<div className="mt-3 extension-card__description">
<div
className={classNames(
'mt-3 extension-card__description',
featured && 'extension-card__description--featured'
)}
>
{extension.manifest ? (
isErrorLike(extension.manifest) ? (
<span className="text-danger small" title={extension.manifest.message}>

View File

@ -25,7 +25,12 @@ import { eventLogger } from '../tracking/eventLogger'
import { ExtensionBanner } from './ExtensionBanner'
import { ExtensionRegistrySidenav } from './ExtensionRegistrySidenav'
import { configureExtensionRegistry, ConfiguredExtensionRegistry } from './extensions'
import {
configureExtensionRegistry,
ConfiguredExtensionRegistry,
MinimalConfiguredRegistryExtension,
configureFeaturedExtensions,
} from './extensions'
import { ExtensionsAreaRouteContext } from './ExtensionsArea'
import { ExtensionsList } from './ExtensionsList'
@ -43,14 +48,20 @@ const URL_QUERY_PARAM = 'query'
const URL_CATEGORY_PARAM = 'category'
const SHOW_EXPERIMENTAL_EXTENSIONS_KEY = 'show-experimental-extensions'
export type ExtensionListData = typeof LOADING | (ConfiguredExtensionRegistry & { error: string | null }) | ErrorLike
export type ExtensionListData =
| typeof LOADING
| (ConfiguredExtensionRegistry & {
featuredExtensions?: MinimalConfiguredRegistryExtension[]
error: string | null
})
| ErrorLike
export type ExtensionsEnablement = 'all' | 'enabled' | 'disabled'
export type ExtensionCategoryOrAll = ExtensionCategory | 'All'
const extensionRegistryQuery = gql`
query RegistryExtensions($query: String, $prioritizeExtensionIDs: [String!]!) {
query RegistryExtensions($query: String, $prioritizeExtensionIDs: [String!]!, $getFeatured: Boolean!) {
extensionRegistry {
extensions(query: $query, prioritizeExtensionIDs: $prioritizeExtensionIDs) {
nodes {
@ -58,6 +69,12 @@ const extensionRegistryQuery = gql`
}
error
}
featuredExtensions @include(if: $getFeatured) {
nodes {
...RegistryExtensionFieldsForList
}
error
}
}
}
fragment RegistryExtensionFieldsForList on RegistryExtension {
@ -95,10 +112,7 @@ const extensionRegistryQuery = gql`
}
`
export type ConfiguredExtensionCache = Map<
string,
Pick<ConfiguredRegistryExtension<RegistryExtensionFieldsForList>, 'manifest' | 'id'>
>
export type ConfiguredExtensionCache = Map<string, MinimalConfiguredRegistryExtension>
/** A page that displays overview information about the available extensions. */
export const ExtensionRegistry: React.FunctionComponent<Props> = props => {
@ -179,12 +193,19 @@ export const ExtensionRegistry: React.FunctionComponent<Props> = props => {
query = `${query} category:"${category}"`
}
// Only fetch + show featured extensions when there's no query or category selected.
const shouldGetFeaturedExtensions = category === 'All' && query.trim() === ''
const resultOrError = platformContext.requestGraphQL<
RegistryExtensionsResult,
RegistryExtensionsVariables
>({
request: extensionRegistryQuery,
variables: { query, prioritizeExtensionIDs: viewerConfiguredExtensions },
variables: {
query,
prioritizeExtensionIDs: viewerConfiguredExtensions,
getFeatured: shouldGetFeaturedExtensions,
},
mightContainPrivateInfo: true,
})
@ -206,8 +227,16 @@ export const ExtensionRegistry: React.FunctionComponent<Props> = props => {
const { error, nodes } = data.extensionRegistry.extensions
const featuredExtensions = data.extensionRegistry.featuredExtensions?.nodes
? configureFeaturedExtensions(
data.extensionRegistry.featuredExtensions.nodes,
configuredExtensionCache
)
: undefined
return {
error,
featuredExtensions,
...configureExtensionRegistry(nodes, configuredExtensionCache),
}
}),

View File

@ -2,12 +2,36 @@
&__cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(17.5rem, 1fr));
grid-auto-rows: minmax(4rem, auto);
grid-auto-rows: minmax(16.25rem, auto);
gap: $spacer * 0.75;
&--featured {
grid-auto-rows: minmax(21.25rem, auto);
}
}
&__category {
margin-top: 2rem;
font-size: 1.5rem !important;
}
&__featured-section {
position: relative;
// 2rem gap before next section, 1rem extra for visual effect of ::before pseudo-element.
margin-bottom: 3rem;
// Same as above but for 2rem gap between featured section and search bar.
margin-top: 3rem;
// Use pseudo-element to make background "bleed out" of the container.
&::before {
content: '';
position: absolute;
z-index: -1;
inset: -1rem;
background-color: var(--color-bg-1);
border: 1px solid var(--border-color-2);
border-radius: var(--border-radius);
padding: 0.5rem;
}
}
}

View File

@ -87,7 +87,39 @@ export const ExtensionsList: React.FunctionComponent<Props> = ({
return <ErrorAlert error={data} />
}
const { error, extensions, extensionIDsByCategory } = data
const { error, extensions, extensionIDsByCategory, featuredExtensions } = data
const featuredExtensionsSection = featuredExtensions && featuredExtensions.length > 0 && (
<div key="Featured" className="extensions-list__featured-section">
<h3
className="extensions-list__category mb-3 font-weight-normal"
data-test-extension-category-header="Featured"
>
Featured
</h3>
<div className="extensions-list__cards extensions-list__cards--featured mt-1">
{featuredExtensions.map(featuredExtension => (
<ExtensionCard
key={featuredExtension.id}
subject={subject}
viewerSubject={viewerSubject?.subject}
siteSubject={siteSubject?.subject}
node={featuredExtension}
settingsCascade={settingsCascade}
platformContext={platformContext}
enabled={isExtensionEnabled(settingsCascade.final, featuredExtension.id)}
enabledForAllUsers={
siteSubject ? isExtensionEnabled(siteSubject.settings, featuredExtension.id) : false
}
isLightTheme={props.isLightTheme}
settingsURL={authenticatedUser?.settingsURL}
authenticatedUser={authenticatedUser}
featured={true}
/>
))}
</div>
</div>
)
if (Object.keys(extensions).length === 0) {
return (
@ -213,6 +245,7 @@ export const ExtensionsList: React.FunctionComponent<Props> = ({
return (
<>
{error && <ErrorAlert className="mb-2" error={error} />}
{featuredExtensionsSection}
{categorySections.length > 0 ? (
categorySections
) : (

View File

@ -14,8 +14,13 @@ import { RegistryExtensionFieldsForList } from '../graphql-operations'
import { validCategories } from './extension/extension'
import { ConfiguredExtensionCache, ExtensionsEnablement } from './ExtensionRegistry'
export type MinimalConfiguredRegistryExtension = Pick<
ConfiguredRegistryExtension<RegistryExtensionFieldsForList>,
'manifest' | 'id'
>
export interface ConfiguredRegistryExtensions {
[id: string]: Pick<ConfiguredRegistryExtension<RegistryExtensionFieldsForList>, 'manifest' | 'id'>
[id: string]: MinimalConfiguredRegistryExtension
}
export interface ConfiguredExtensionRegistry {
@ -85,6 +90,30 @@ export function configureExtensionRegistry(
return { extensions, extensionIDsByCategory }
}
/**
* Configures featured extensions to be displayed on the extension registry.
*
* Share configured extension cache with `configureExtensionRegistry`
* since featured extensions are likely to be displayed twice on the page.
*/
export function configureFeaturedExtensions(
featuredExtensions: RegistryExtensionFieldsForList[],
configuredExtensionCache: ConfiguredExtensionCache
): MinimalConfiguredRegistryExtension[] {
const extensions: MinimalConfiguredRegistryExtension[] = []
for (const featuredExtension of featuredExtensions) {
let configuredRegistryExtension = configuredExtensionCache.get(featuredExtension.id)
if (!configuredRegistryExtension) {
configuredRegistryExtension = toConfiguredRegistryExtension(featuredExtension)
configuredExtensionCache.set(featuredExtension.id, configuredRegistryExtension)
}
extensions.push(configuredRegistryExtension)
}
return extensions
}
/**
* Removes extensions that do not satify the enablement filter.
*

View File

@ -196,6 +196,7 @@ describe('Extension Registry', () => {
error: null,
nodes: registryExtensionNodes,
},
featuredExtensions: null,
},
}),
Extensions: () => ({
@ -274,7 +275,11 @@ describe('Extension Registry', () => {
})
}, 'RegistryExtensions')
assert.deepStrictEqual(request, { query: 'sqs', prioritizeExtensionIDs: ['sqs/word-count'] })
assert.deepStrictEqual(request, {
getFeatured: false,
query: 'sqs',
prioritizeExtensionIDs: ['sqs/word-count'],
})
})
})

View File

@ -222,7 +222,9 @@ describe('Search', () => {
test('Is set from the URL query parameter when loading a search-related page', async () => {
testContext.overrideGraphQL({
...commonSearchGraphQLResults,
RegistryExtensions: () => ({ extensionRegistry: { extensions: { error: null, nodes: [] } } }),
RegistryExtensions: () => ({
extensionRegistry: { extensions: { error: null, nodes: [] }, featuredExtensions: null },
}),
})
testContext.overrideSearchStreamEvents(mockDefaultStreamEvents)

View File

@ -46,6 +46,7 @@ type ExtensionRegistryResolver interface {
PublishExtension(context.Context, *ExtensionRegistryPublishExtensionArgs) (ExtensionRegistryMutationResult, error)
DeleteExtension(context.Context, *ExtensionRegistryDeleteExtensionArgs) (*EmptyResponse, error)
LocalExtensionIDPrefix() *string
FeaturedExtensions(context.Context) (FeaturedExtensionsConnection, error)
ImplementsLocalExtensionRegistry() bool // not exposed via GraphQL
// FilterRemoteExtensions enforces `allowRemoteExtensions` by returning a
@ -154,3 +155,9 @@ type RegistryPublisherConnection interface {
TotalCount(context.Context) (int32, error)
PageInfo(context.Context) (*graphqlutil.PageInfo, error)
}
// FeaturedExtensions is the interface for the GraphQL type FeaturedExtensionsConnection.
type FeaturedExtensionsConnection interface {
Nodes(context.Context) ([]RegistryExtension, error)
Error(context.Context) *string
}

View File

@ -5819,6 +5819,10 @@ type ExtensionRegistry {
Examples: "sourcegraph.example.com/", "sourcegraph.example.com:1234/"
"""
localExtensionIDPrefix: String
"""
A list of featured extensions in the registry.
"""
featuredExtensions: FeaturedExtensionsConnection
}
"""
@ -6072,6 +6076,23 @@ type RegistryExtensionConnection {
error: String
}
"""
A list of featured extensions in the registry.
"""
type FeaturedExtensionsConnection {
"""
A list of featured registry extensions.
"""
nodes: [RegistryExtension!]!
"""
Errors that occurred while communicating with remote registries to obtain the list of extensions.
In order to be able to return local extensions even when the remote registry is unreachable, errors are
recorded here instead of in the top-level GraphQL errors list.
"""
error: String
}
"""
Aggregate local code intelligence for all ranges that fall between a window of lines in a document.
"""

View File

@ -204,6 +204,41 @@ func listRemoteRegistryExtensions(ctx context.Context, query string) ([]*registr
return xs, nil
}
// GetLocalFeaturedExtensions looks up and returns the featured registry extensions in the local registry
// If this is not sourcegraph.com, it is not implemented.
var GetLocalFeaturedExtensions func(ctx context.Context, db dbutil.DB) ([]graphqlbackend.RegistryExtension, error)
// GetFeaturedExtensions returns the set of featured extensions.
//
// If this is sourcegraph.com, these are local extensions. Otherwise, these are remote extensions
// retrieved from sourcegraph.com.
func GetFeaturedExtensions(ctx context.Context, db dbutil.DB) ([]graphqlbackend.RegistryExtension, error) {
if envvar.SourcegraphDotComMode() && GetLocalFeaturedExtensions != nil {
return GetLocalFeaturedExtensions(ctx, db)
}
// Get remote featured extensions if the remote registry is sourcegraph.com.
registryURL, err := getRemoteRegistryURL()
if registryURL == nil || registryURL.String() != "https://sourcegraph.com/.api/registry" || err != nil {
return nil, err
}
remote, err := registry.GetFeaturedExtensions(ctx, registryURL)
if err != nil {
return nil, err
}
remote = FilterRemoteExtensions(remote)
for _, x := range remote {
x.RegistryURL = registryURL.String()
}
registryExtensions := make([]graphqlbackend.RegistryExtension, len(remote))
for i, x := range remote {
registryExtensions[i] = &registryExtensionRemoteResolver{v: x}
}
return registryExtensions, nil
}
// IsWorkInProgressExtension reports whether the extension manifest indicates that this extension is
// marked as a work-in-progress extension (by having a "wip": true property, or (for backcompat) a
// title that begins with "WIP:" or "[WIP]").

View File

@ -0,0 +1,45 @@
package api
import (
"context"
"sync"
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
"github.com/sourcegraph/sourcegraph/internal/database/dbutil"
)
type featuredExtensionsResolver struct {
// cache results because they are used by multiple fields
once sync.Once
featuredExtensions []graphqlbackend.RegistryExtension
err error
db dbutil.DB
}
func (r *extensionRegistryResolver) FeaturedExtensions(ctx context.Context) (graphqlbackend.FeaturedExtensionsConnection, error) {
return &featuredExtensionsResolver{db: r.db}, nil
}
func (r *featuredExtensionsResolver) compute(ctx context.Context) ([]graphqlbackend.RegistryExtension, error) {
r.once.Do(func() {
r.featuredExtensions, r.err = GetFeaturedExtensions(ctx, r.db)
})
return r.featuredExtensions, r.err
}
func (r *featuredExtensionsResolver) Nodes(ctx context.Context) ([]graphqlbackend.RegistryExtension, error) {
// See (*featuredExtensionsResolver).Error for why we ignore the error.
xs, _ := r.compute(ctx)
return xs, nil
}
func (r *featuredExtensionsResolver) Error(ctx context.Context) *string {
// See the GraphQL API schema documentation for this field for an explanation of why we return
// errors in this way.
_, err := r.compute(ctx)
if err != nil {
return strptr(err.Error())
}
return nil
}

View File

@ -72,6 +72,14 @@ func getBy(ctx context.Context, registry *url.URL, op, field, value string) (*Ex
return x, nil
}
func GetFeaturedExtensions(ctx context.Context, registry *url.URL) ([]*Extension, error) {
var x []*Extension
if err := httpGet(ctx, "registry.GetFeaturedExtensions", toURL(registry, "extensions/featured", nil), &x); err != nil {
return nil, err
}
return x, nil
}
type notFoundError struct{ field, value string }
func (notFoundError) NotFound() bool { return true }

View File

@ -21,6 +21,18 @@ func init() {
}
return &extensionDBResolver{db: db, v: x}, nil
}
registry.GetLocalFeaturedExtensions = func(ctx context.Context, db dbutil.DB) ([]graphqlbackend.RegistryExtension, error) {
dbExtensions, err := dbExtensions{}.GetFeaturedExtensions(ctx)
if err != nil {
return nil, err
}
registryExtensions := make([]graphqlbackend.RegistryExtension, len(dbExtensions))
for i, x := range dbExtensions {
registryExtensions[i] = &extensionDBResolver{db: db, v: x}
}
return registryExtensions, nil
}
}
// prefixLocalExtensionID adds the local registry's extension ID prefix (from

View File

@ -11,6 +11,7 @@ import (
"github.com/keegancsmith/sqlf"
"github.com/pkg/errors"
"github.com/sourcegraph/sourcegraph/cmd/frontend/envvar"
registry "github.com/sourcegraph/sourcegraph/cmd/frontend/registry/api"
"github.com/sourcegraph/sourcegraph/internal/database"
"github.com/sourcegraph/sourcegraph/internal/database/dbconn"
@ -180,6 +181,52 @@ func (s dbExtensions) GetByExtensionID(ctx context.Context, extensionID string)
return results[0], nil
}
// Temporary: we manually set these. Featured extensions live on sourcegraph.com, all other instances ask
// dotcom for these extensions and filter based on site configuration.
var featuredExtensionIDs = []string{"sourcegraph/codecov", "sourcegraph/sentry", "sourcegraph/vscode-extras"}
// GetFeaturedExtensions retrieves the set of currently featured extensions.
// This should only be called on dotcom; all other instances should retrieve these
// extensions from dotcom through the HTTP API.
//
// 🚨 SECURITY: The caller must ensure that the actor is permitted to view these registry extensions.
func (s dbExtensions) GetFeaturedExtensions(ctx context.Context) ([]*dbExtension, error) {
if envvar.SourcegraphDotComMode() {
return s.getFeaturedExtensions(ctx, featuredExtensionIDs)
}
return nil, errors.New("GetFeaturedExtensions should only be called on Sourcegraph.com")
}
func (s dbExtensions) getFeaturedExtensions(ctx context.Context, featuredExtensionIDs []string) ([]*dbExtension, error) {
if mocks.extensions.GetFeaturedExtensions != nil {
return mocks.extensions.GetFeaturedExtensions()
}
conds := make([]*sqlf.Query, 0, len(featuredExtensionIDs))
for i := 0; i < len(featuredExtensionIDs); i++ {
extensionID := featuredExtensionIDs[i]
parts := strings.SplitN(extensionID, "/", 2)
if len(parts) < 2 {
continue
}
publisherName := parts[0]
extensionName := parts[1]
conds = append(conds, sqlf.Sprintf("(x.name=%s AND (users.username=%s OR orgs.name=%s))", extensionName, publisherName, publisherName))
}
conds = []*sqlf.Query{
sqlf.Join(conds, " OR "),
}
results, err := s.list(ctx, conds, nil, nil)
if err != nil {
return nil, err
}
return results, nil
}
// dbExtensionsListOptions contains options for listing registry extensions.
type dbExtensionsListOptions struct {
Publisher dbPublisher
@ -366,10 +413,11 @@ func (dbExtensions) Delete(ctx context.Context, id int32) error {
// mockExtensions mocks the registry extensions store.
type mockExtensions struct {
Create func(publisherUserID, publisherOrgID int32, name string) (int32, error)
GetByID func(id int32) (*dbExtension, error)
GetByUUID func(uuid string) (*dbExtension, error)
GetByExtensionID func(extensionID string) (*dbExtension, error)
Update func(id int32, name *string) error
Delete func(id int32) error
Create func(publisherUserID, publisherOrgID int32, name string) (int32, error)
GetByID func(id int32) (*dbExtension, error)
GetByUUID func(uuid string) (*dbExtension, error)
GetByExtensionID func(extensionID string) (*dbExtension, error)
GetFeaturedExtensions func() ([]*dbExtension, error)
Update func(id int32, name *string) error
Delete func(id int32) error
}

View File

@ -7,6 +7,7 @@ import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/jackc/pgconn"
"github.com/sourcegraph/sourcegraph/internal/database"
@ -423,6 +424,68 @@ func TestRegistryExtensions_ListCount(t *testing.T) {
})
}
func TestFeaturedExtensions(t *testing.T) {
if testing.Short() {
t.Skip()
}
db := dbtesting.GetDB(t)
ctx := context.Background()
user, err := database.Users(db).Create(ctx, database.NewUser{Username: "u"})
if err != nil {
t.Fatal(err)
}
createAndGet := func(t *testing.T, name, manifest string) *dbExtension {
t.Helper()
xID, err := dbExtensions{}.Create(ctx, user.ID, 0, name)
if err != nil {
t.Fatal(err)
}
if manifest != "" {
_, err = dbReleases{}.Create(ctx, &dbRelease{
RegistryExtensionID: xID,
CreatorUserID: user.ID,
ReleaseTag: "release",
Manifest: manifest,
Bundle: strptr(""),
SourceMap: strptr(""),
})
if err != nil {
t.Fatal(err)
}
}
x, err := dbExtensions{}.GetByID(ctx, xID)
if err != nil {
t.Fatal(err)
}
return x
}
mockFeaturedExtensionIDs := []string{"u/one", "u/two", "u/three"}
one := createAndGet(t, "one", `{"name": "one", "publisher": "u"}`)
two := createAndGet(t, "two", `{"name": "two", "publisher": "u"}`)
three := createAndGet(t, "three", `{"name": "three", "publisher": "u"}`)
// Non-featured extension shouldn't be returned.
createAndGet(t, "four", `{"name": "four", "publisher": "u"}`)
want := []*dbExtension{
one,
two,
three,
}
featuredExtensions, err := dbExtensions{}.getFeaturedExtensions(ctx, mockFeaturedExtensionIDs)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(want, featuredExtensions); diff != "" {
t.Fatalf("Mismatch (-want +got):\n%s", diff)
}
}
func asJSON(t *testing.T, v interface{}) string {
b, err := json.MarshalIndent(v, "", " ")
if err != nil {

View File

@ -69,6 +69,22 @@ var (
}
return toRegistryAPIExtension(ctx, x)
}
registryGetFeaturedExtensions = func(ctx context.Context) ([]*registry.Extension, error) {
dbExtensions, err := dbExtensions{}.GetFeaturedExtensions(ctx)
if err != nil {
return nil, err
}
registryExtensions := []*registry.Extension{}
for _, x := range dbExtensions {
registryExtension, err := toRegistryAPIExtension(ctx, x)
if err != nil {
continue
}
registryExtensions = append(registryExtensions, registryExtension)
}
return registryExtensions, nil
}
)
func toRegistryAPIExtension(ctx context.Context, v *dbExtension) (*registry.Extension, error) {
@ -184,6 +200,14 @@ func handleRegistry(w http.ResponseWriter, r *http.Request) (err error) {
}
result = xs
case urlPath == extensionsPath+"/featured":
operation = "featured"
x, err := registryGetFeaturedExtensions(r.Context())
if err != nil {
return err
}
result = x
case strings.HasPrefix(urlPath, extensionsPath+"/"):
var (
spec = strings.TrimPrefix(urlPath, extensionsPath+"/")
@ -270,4 +294,7 @@ func init() {
}
return frontendregistry.FindRegistryExtension(xs, "extensionID", extensionID), nil
}
registryGetFeaturedExtensions = func(ctx context.Context) ([]*registry.Extension, error) {
return []*registry.Extension{}, nil
}
}

2
go.mod
View File

@ -188,7 +188,7 @@ require (
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
golang.org/x/tools v0.1.2
golang.org/x/tools v0.1.3
google.golang.org/api v0.46.0
google.golang.org/genproto v0.0.0-20210517163617-5e0236093d7a
google.golang.org/protobuf v1.26.0

4
go.sum
View File

@ -1847,8 +1847,8 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3 h1:L69ShwSZEyCsLKoAxDKeMvLDZkumEe8gXUZAjab0tX8=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -350,6 +350,7 @@
"monaco-editor": "^0.24.0",
"nice-ticks": "^1.0.1",
"open-color": "^1.8.0",
"postcss-inset": "^1.0.0",
"pretty-bytes": "^5.3.0",
"prop-types": "^15.7.2",
"react": "^16.14.0",

View File

@ -1,3 +1,8 @@
module.exports = {
plugins: [require('autoprefixer'), require('postcss-focus-visible'), require('postcss-custom-media')],
plugins: [
require('autoprefixer'),
require('postcss-focus-visible'),
require('postcss-custom-media'),
require('postcss-inset'),
],
}

View File

@ -17705,6 +17705,13 @@ postcss-html@^0.36.0:
dependencies:
htmlparser2 "^3.10.0"
postcss-inset@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/postcss-inset/-/postcss-inset-1.0.0.tgz#1bc0937996a5f042f7054643dbf8f60e49065fa7"
integrity sha512-GAGG8dCDL8zq3BLo2spfcY7YxVQzIZscHDklcJVy/VI2V5lO8V5N9b/p96411w8nf40v7lx3VOiAzBNJlmrI4Q==
dependencies:
postcss "^6.0.1"
postcss-less@^3.1.4:
version "3.1.4"
resolved "https://registry.npmjs.org/postcss-less/-/postcss-less-3.1.4.tgz#369f58642b5928ef898ffbc1a6e93c958304c5ad"