content/static: display NotAtLatest badge
Display a variant of the "latest" badge when the unit is not
in the latest version of the module.
For golang/go#337631
Change-Id: I82aa30711f1f1e162e44fd1db9195c6e05718de1
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/280612
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
diff --git a/content/static/css/stylesheet.css b/content/static/css/stylesheet.css
index 6021a27..550421f 100644
--- a/content/static/css/stylesheet.css
+++ b/content/static/css/stylesheet.css
@@ -496,6 +496,27 @@
.DetailsHeader-badge--latest a {
display: none;
}
+.DetailsHeader-badge--latest span.DetailsHeader-span--notAtLatest {
+ display: none;
+}
+
+.DetailsHeader-badge--notAtLatest {
+ background: var(--blue);
+}
+.DetailsHeader-badge--notAtLatest a {
+ display: none;
+}
+.DetailsHeader-badge--notAtLatest span.DetailsHeader-span--latest {
+ display: none;
+}
+.DetailsHeader-badge--notAtLatest .UnitMetaDetails-icon {
+ z-index: 1;
+}
+.DetailsHeader-badge--notAtLatest .UnitMetaDetails-toggletipBubble {
+ color: black;
+ text-transform: none;
+}
+
.DetailsHeader-badge--goToLatest {
background: var(--pink);
}
diff --git a/content/static/css/unit_meta.css b/content/static/css/unit_meta.css
index 0c0f889e..81420af 100644
--- a/content/static/css/unit_meta.css
+++ b/content/static/css/unit_meta.css
@@ -63,7 +63,7 @@
cursor: pointer;
width: 1.625rem;
}
-.UnitMetaDetails [role='status'] {
+.UnitMetaDetails-toggletip [role='status'] {
height: 0;
position: absolute;
width: 0;
diff --git a/content/static/html/helpers/_unit_header.tmpl b/content/static/html/helpers/_unit_header.tmpl
index 6170fc8..0a22951 100644
--- a/content/static/html/helpers/_unit_header.tmpl
+++ b/content/static/html/helpers/_unit_header.tmpl
@@ -57,7 +57,13 @@
data-mpath="{{.Unit.ModulePath}}"
data-ppath="{{.Unit.Path}}"
data-pagetype="{{.PageType}}">
- <span>Latest</span>
+ <span class="DetailsHeader-span--latest">Latest</span>
+ {{if (.Experiments.IsActive "not-at-latest")}}
+ <span class="DetailsHeader-span--notAtLatest">
+ Latest
+ {{template "unit_meta_details_toggletip" "This package is not in the latest version of its module."}}
+ </span>
+ {{end}}
<a href="{{.LatestURL}}">Go to latest</a>
</div>
</span>
diff --git a/internal/experiment.go b/internal/experiment.go
index 73abee8..476c5f0 100644
--- a/internal/experiment.go
+++ b/internal/experiment.go
@@ -8,6 +8,7 @@
const (
ExperimentGetUnitMetaQuery = "get-unit-meta-query"
ExperimentGoldmark = "goldmark"
+ ExperimentNotAtLatest = "not-at-latest"
ExperimentReadmeOutline = "readme-outline"
ExperimentUnitSidebarDetails = "unit-sidebar-details"
)
@@ -17,6 +18,7 @@
var Experiments = map[string]string{
ExperimentGetUnitMetaQuery: "Enable the new get unit meta query, which reads from the paths table.",
ExperimentGoldmark: "Enable the usage of rendering markdown using goldmark instead of blackfriday.",
+ ExperimentNotAtLatest: "Enable the display of a 'not at latest' badge.",
ExperimentReadmeOutline: "Enable the readme outline in the side nav.",
ExperimentUnitSidebarDetails: "Enable the details section in the right sidebar.",
}
diff --git a/internal/frontend/server_test.go b/internal/frontend/server_test.go
index 93f8a27..292e07d 100644
--- a/internal/frontend/server_test.go
+++ b/internal/frontend/server_test.go
@@ -271,6 +271,26 @@
},
},
},
+ // A module with a package that is not in the module's latest version. Since
+ // our testModule struct can only describe modules where all packages are at
+ // all versions, we need two of them.
+ {
+ path: "golang.org/x/tools",
+ redistributable: true,
+ versions: []string{"v1.1.0"},
+ packages: []testPackage{
+ {name: "blog", suffix: "blog"},
+ {name: "vet", suffix: "cmd/vet"},
+ },
+ },
+ {
+ path: "golang.org/x/tools",
+ redistributable: true,
+ versions: []string{"v1.2.0"},
+ packages: []testPackage{
+ {name: "blog", suffix: "blog"},
+ },
+ },
}
func insertTestModules(ctx context.Context, t *testing.T, mods []testModule) {
@@ -327,6 +347,22 @@
}
)
+var notAtLatestPkg = &pagecheck.Page{
+ ModulePath: "golang.org/x/tools",
+ Suffix: "cmd/vet",
+ Title: "vet",
+ ModuleURL: "github.com/golang/tools",
+ Version: "v1.1.0",
+ FormattedVersion: "v1.1.0",
+ LicenseType: "MIT",
+ LicenseFilePath: "LICENSE",
+ MissingInMinor: true,
+ IsLatestMajor: true,
+ UnitURLFormat: "/golang.org/x/tools/cmd/vet%s",
+ LatestLink: "/golang.org/x/tools/cmd/vet",
+ LatestMajorVersionLink: "/golang.org/x/tools",
+}
+
// serverTestCases are the test cases valid for any experiment. For experiments
// that modify any part of the behaviour covered by the test cases in
// serverTestCase(), a new test generator should be created and added to
@@ -996,6 +1032,14 @@
checkLink(4, "title2", "about:invalid#zGoSafez"),
),
},
+ {
+ name: "not at latest",
+ // A package which is at its own latest minor version but not at the
+ // latest minor version of its module.
+ urlPath: "/golang.org/x/tools/cmd/vet",
+ wantStatusCode: http.StatusOK,
+ want: in("", pagecheck.UnitHeader(notAtLatestPkg, false, true)),
+ },
}
// TestServer checks the contents of served pages by looking for
@@ -1025,7 +1069,7 @@
testCasesFunc: func() []serverTestCase {
return append(serverTestCases(), linksTestCases...)
},
- experiments: []string{internal.ExperimentReadmeOutline, internal.ExperimentGoldmark},
+ experiments: []string{internal.ExperimentReadmeOutline, internal.ExperimentGoldmark, internal.ExperimentNotAtLatest},
},
} {
t.Run(test.name, func(t *testing.T) {
@@ -1218,8 +1262,8 @@
t.Fatal(err)
}
mw := middleware.Chain(
- middleware.LatestVersions(s.GetLatestInfo),
- middleware.Experiment(exp))
+ middleware.Experiment(exp),
+ middleware.LatestVersions(s.GetLatestInfo))
return s, mw(mux), func() {
teardown()
postgres.ResetTestDB(testDB, t)
diff --git a/internal/middleware/latestversion.go b/internal/middleware/latestversion.go
index 5960584..6409168 100644
--- a/internal/middleware/latestversion.go
+++ b/internal/middleware/latestversion.go
@@ -13,6 +13,7 @@
"golang.org/x/mod/module"
"golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/experiment"
"golang.org/x/pkgsite/internal/log"
)
@@ -51,6 +52,8 @@
switch {
case latest.MinorVersion == "":
latestMinorClass += "--unknown"
+ case latest.MinorVersion == version && !latest.UnitExistsAtMinor && experiment.IsActive(r.Context(), internal.ExperimentNotAtLatest):
+ latestMinorClass += "--notAtLatest"
case latest.MinorVersion == version:
latestMinorClass += "--latest"
default:
diff --git a/internal/middleware/latestversion_test.go b/internal/middleware/latestversion_test.go
index 977eb89..6b7a148 100644
--- a/internal/middleware/latestversion_test.go
+++ b/internal/middleware/latestversion_test.go
@@ -13,34 +13,35 @@
"testing"
"golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/experiment"
)
func TestLatestMinorVersion(t *testing.T) {
for _, test := range []struct {
name string
- latest latestFunc
+ latest internal.LatestInfo
in string
want string
}{
{
name: "package version is not latest",
- latest: constLatestFunc("v1.2.3", "", ""),
+ latest: internal.LatestInfo{MinorVersion: "v1.2.3"},
in: `
<div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
- data-version="v1.0.0" data-mpath="p1/p2" data-ppath="p1/p2/p3" data-pagetype="pkg">
+ data-version="v1.0.0" data-mpath="p1/p2" data-ppath="p1/p2/p3" data-pagetype="pkg">
<span>Latest</span>
<a href="p1/p2@$$GODISCOVERY_LATESTMINORVERSION$$/p3">Go to latest</a>
</div>`,
want: `
<div class="DetailsHeader-badge DetailsHeader-badge--goToLatest"
- data-version="v1.0.0" data-mpath="p1/p2" data-ppath="p1/p2/p3" data-pagetype="pkg">
+ data-version="v1.0.0" data-mpath="p1/p2" data-ppath="p1/p2/p3" data-pagetype="pkg">
<span>Latest</span>
<a href="p1/p2@v1.2.3/p3">Go to latest</a>
</div>`,
},
{
name: "package version is latest",
- latest: constLatestFunc("v1.2.3", "", ""),
+ latest: internal.LatestInfo{MinorVersion: "v1.2.3", UnitExistsAtMinor: true},
in: `
<div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
data-version="v1.2.3" data-mpath="p1/p2" data-ppath="p1/p2/p3" data-pagetype="pkg">
@@ -56,7 +57,7 @@
},
{
name: "package version with build is latest",
- latest: constLatestFunc("v1.2.3+build", "", ""),
+ latest: internal.LatestInfo{MinorVersion: "v1.2.3+build", UnitExistsAtMinor: true},
in: `
<div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
data-version="v1.2.3+build" data-mpath="p1/p2" data-ppath="p1/p2/p3" data-pagetype="pkg">
@@ -72,7 +73,7 @@
},
{
name: "module version is not latest",
- latest: constLatestFunc("v1.2.3", "", ""),
+ latest: internal.LatestInfo{MinorVersion: "v1.2.3"},
in: `
<div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
data-version="v1.0.0" data-mpath="p1/p2" data-ppath="" data-pagetype="pkg">
@@ -88,56 +89,77 @@
},
{
name: "module version is latest",
- latest: constLatestFunc("v1.2.3", "", ""),
+ latest: internal.LatestInfo{MinorVersion: "v1.2.3", UnitExistsAtMinor: true},
in: `
- <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
+ <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
data-version="v1.2.3" data-mpath="p1/p2" data-ppath="" data-pagetype="pkg">
- <span>Latest</span>
- <a href="mod/p1/p2@$$GODISCOVERY_LATESTMINORVERSION$$">Go to latest</a>
- </div>`,
+ <span>Latest</span>
+ <a href="mod/p1/p2@$$GODISCOVERY_LATESTMINORVERSION$$">Go to latest</a>
+ </div>`,
want: `
- <div class="DetailsHeader-badge DetailsHeader-badge--latest"
+ <div class="DetailsHeader-badge DetailsHeader-badge--latest"
data-version="v1.2.3" data-mpath="p1/p2" data-ppath="" data-pagetype="pkg">
- <span>Latest</span>
- <a href="mod/p1/p2@v1.2.3">Go to latest</a>
- </div>`,
+ <span>Latest</span>
+ <a href="mod/p1/p2@v1.2.3">Go to latest</a>
+ </div>`,
+ },
+ {
+ name: "package not in module latest",
+ latest: internal.LatestInfo{MinorVersion: "v1.2.3", UnitExistsAtMinor: false},
+ in: `
+ <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
+ data-version="v1.2.3" data-mpath="p1/p2" data-ppath="" data-pagetype="pkg">
+ <span>Latest</span>
+ <a href="mod/p1/p2@$$GODISCOVERY_LATESTMINORVERSION$$">Go to latest</a>
+ </div>`,
+ want: `
+ <div class="DetailsHeader-badge DetailsHeader-badge--notAtLatest"
+ data-version="v1.2.3" data-mpath="p1/p2" data-ppath="" data-pagetype="pkg">
+ <span>Latest</span>
+ <a href="mod/p1/p2@v1.2.3">Go to latest</a>
+ </div>`,
},
{
name: "latest func returns empty string",
- latest: constLatestFunc("", "", ""),
+ latest: internal.LatestInfo{},
in: `
- <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
+ <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$"
data-version="v1.2.3" data-mpath="p1/p2" data-ppath="" data-pagetype="pkg">
- <span>Latest</span>
- <a href="mod/p1/p2@$$GODISCOVERY_LATESTMINORVERSION$$">Go to latest</a>
- </div>`,
+ <span>Latest</span>
+ <a href="mod/p1/p2@$$GODISCOVERY_LATESTMINORVERSION$$">Go to latest</a>
+ </div>`,
want: `
- <div class="DetailsHeader-badge DetailsHeader-badge--unknown"
+ <div class="DetailsHeader-badge DetailsHeader-badge--unknown"
data-version="v1.2.3" data-mpath="p1/p2" data-ppath="" data-pagetype="pkg">
- <span>Latest</span>
- <a href="mod/p1/p2@">Go to latest</a>
- </div>`,
+ <span>Latest</span>
+ <a href="mod/p1/p2@">Go to latest</a>
+ </div>`,
},
{
name: "no regexp match",
- latest: constLatestFunc("v1.2.3", "", ""),
+ latest: internal.LatestInfo{MinorVersion: "v1.2.3"},
in: `
- <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$">
- <span>Latest</span>
- <a href="mod/p1/p2@$$GODISCOVERY_LATESTMINORVERSION$$">Go to latest</a>
- </div>`,
+ <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$">
+ <span>Latest</span>
+ <a href="mod/p1/p2@$$GODISCOVERY_LATESTMINORVERSION$$">Go to latest</a>
+ </div>`,
want: `
- <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$">
- <span>Latest</span>
- <a href="mod/p1/p2@$$GODISCOVERY_LATESTMINORVERSION$$">Go to latest</a>
- </div>`,
+ <div class="DetailsHeader-badge $$GODISCOVERY_LATESTMINORCLASS$$">
+ <span>Latest</span>
+ <a href="mod/p1/p2@$$GODISCOVERY_LATESTMINORVERSION$$">Go to latest</a>
+ </div>`,
},
} {
t.Run(test.name, func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, test.in)
})
- ts := httptest.NewServer(LatestVersions(test.latest)(handler))
+ lfunc := func(context.Context, string, string) internal.LatestInfo { return test.latest }
+ lv := LatestVersions(lfunc)(handler)
+ addExp := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ lv.ServeHTTP(w, r.WithContext(experiment.NewContext(r.Context(), internal.ExperimentNotAtLatest)))
+ })
+ ts := httptest.NewServer(addExp)
defer ts.Close()
resp, err := ts.Client().Get(ts.URL)
if err != nil {
@@ -155,28 +177,17 @@
}
}
-func constLatestFunc(minorVersion, majorModPath, majorPackagePath string) latestFunc {
- return func(context.Context, string, string) internal.LatestInfo {
- return internal.LatestInfo{
- MinorVersion: minorVersion,
- MinorModulePath: "",
- MajorModulePath: majorModPath,
- MajorUnitPath: majorPackagePath,
- }
- }
-}
-
func TestLatestMajorVersion(t *testing.T) {
for _, test := range []struct {
name string
- latest latestFunc
+ latest internal.LatestInfo
modulePaths []string
in string
want string
}{
{
name: "module path is not at latest",
- latest: constLatestFunc("", "foo.com/bar/v3", "foo.com/bar/v3"),
+ latest: internal.LatestInfo{MajorModulePath: "foo.com/bar/v3", MajorUnitPath: "foo.com/bar/v3"},
modulePaths: []string{
"foo.com/bar",
"foo.com/bar/v2",
@@ -199,7 +210,7 @@
},
{
name: "module path is at latest",
- latest: constLatestFunc("", "foo.com/bar/v3", "foo.com/bar/v3"),
+ latest: internal.LatestInfo{MajorModulePath: "foo.com/bar/v3", MajorUnitPath: "foo.com/bar/v3"},
modulePaths: []string{
"foo.com/bar",
"foo.com/bar/v2",
@@ -222,7 +233,7 @@
},
{
name: "full path is not at the latest",
- latest: constLatestFunc("", "foo.com/bar/v3", "foo.com/bar/v3/far"),
+ latest: internal.LatestInfo{MajorModulePath: "foo.com/bar/v3", MajorUnitPath: "foo.com/bar/v3/far"},
modulePaths: []string{
"foo.com/bar",
"foo.com/bar/v2",
@@ -248,7 +259,8 @@
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, test.in)
})
- ts := httptest.NewServer(LatestVersions(test.latest)(handler))
+ lfunc := func(context.Context, string, string) internal.LatestInfo { return test.latest }
+ ts := httptest.NewServer(LatestVersions(lfunc)(handler))
defer ts.Close()
resp, err := ts.Client().Get(ts.URL)
if err != nil {
diff --git a/internal/testing/pagecheck/pagecheck.go b/internal/testing/pagecheck/pagecheck.go
index 2225e05..c705e18 100644
--- a/internal/testing/pagecheck/pagecheck.go
+++ b/internal/testing/pagecheck/pagecheck.go
@@ -43,6 +43,9 @@
// IsLatestMinor is the latest minor version of this module.
IsLatestMinor bool
+ // MissingInMinor says that the unit is missing in the latest minor version of this module.
+ MissingInMinor bool
+
// IsLatestMajor is the latest major version of this series.
IsLatestMajor bool
@@ -198,9 +201,12 @@
// versionBadge checks the latest-version badge on a header.
func versionBadge(p *Page) htmlcheck.Checker {
class := "DetailsHeader-badge"
- if p.IsLatestMinor {
+ switch {
+ case p.MissingInMinor:
+ class += "--notAtLatest"
+ case p.IsLatestMinor:
class += "--latest"
- } else {
+ default:
class += "--goToLatest"
}
return in(`[data-test-id="UnitHeader-minorVersionBanner"]`,