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

Commit 612f313

Browse files
authored
show custom notices on the homepage (and consolidate motd) (#2959)
- Adds a new setting property `notices` that contains messages to be shown on the homepage and in the global alerts at the top of the page (like `motd`) - Cleans up CSS to make the display of dismissible and non-dismissible alerts consistent (w.r.t. padding and sizing) closes #653
1 parent d2b0714 commit 612f313

13 files changed

+220
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ All notable changes to Sourcegraph are documented in this file.
1010
### Added
1111

1212
- Enterprise admins can now customize the appearance of the homepage and search icon.
13+
- A new settings property `notices` allows showing custom informational messages on the homepage and at the top of each page.
1314

1415
### Changed
1516

schema/schema.go

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

schema/settings.schema.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,35 @@
6565
"$ref": "#/definitions/SlackNotificationsConfig"
6666
},
6767
"motd": {
68-
"description": "An array (often with just one element) of messages to display at the top of all pages, including for unauthenticated users. Users may dismiss a message (and any message with the same string value will remain dismissed for the user).\n\nMarkdown formatting is supported.\n\nUsually this setting is used in global and organization settings. If set in user settings, the message will only be displayed to that user. (This is useful for testing the correctness of the message's Markdown formatting.)\n\nMOTD stands for \"message of the day\" (which is the conventional Unix name for this type of message).",
68+
"description": "DEPRECATED: Use `notices` instead.\n\nAn array (often with just one element) of messages to display at the top of all pages, including for unauthenticated users. Users may dismiss a message (and any message with the same string value will remain dismissed for the user).\n\nMarkdown formatting is supported.\n\nUsually this setting is used in global and organization settings. If set in user settings, the message will only be displayed to that user. (This is useful for testing the correctness of the message's Markdown formatting.)\n\nMOTD stands for \"message of the day\" (which is the conventional Unix name for this type of message).",
6969
"type": "array",
7070
"items": { "type": "string" }
7171
},
72+
"notices": {
73+
"description": "Custom informational messages to display to users at specific locations in the Sourcegraph user interface.\n\nUsually this setting is used in global and organization settings. If set in user settings, the message will only be displayed to that single user.",
74+
"type": "array",
75+
"items": {
76+
"title": "Notice",
77+
"type": "object",
78+
"required": ["message", "location"],
79+
"properties": {
80+
"message": {
81+
"description": "The message to display. Markdown formatting is supported.",
82+
"type": "string"
83+
},
84+
"location": {
85+
"description": "The location where this notice is shown: \"top\" for the top of every page, \"home\" for the homepage.",
86+
"type": "string",
87+
"enum": ["top", "home"]
88+
},
89+
"dismissible": {
90+
"description": "Whether this notice can be dismissed (closed) by the user.",
91+
"type": "boolean",
92+
"default": false
93+
}
94+
}
95+
}
96+
},
7297
"extensions": {
7398
"description": "The Sourcegraph extensions to use. Enable an extension by adding a property `\"my/extension\": true` (where `my/extension` is the extension ID). Override a previously enabled extension and disable it by setting its value to `false`.",
7499
"type": "object",

schema/settings_stringdata.go

Lines changed: 26 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/src/SourcegraphWebApp.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ hr {
234234
@import './extensions/extension/ExtensionArea';
235235
@import './global/GlobalAlerts';
236236
@import './global/GlobalDebug';
237+
@import './global/Notices';
237238
@import './search/input/ScopePage';
238239
@import './search/input/SearchPage';
239240
@import './search/results/SearchResults';

web/src/components/DismissibleAlert.scss

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
.dismissible-alert {
22
display: flex;
33
align-items: flex-start;
4-
padding: 0.75rem;
5-
margin-bottom: 0;
4+
padding-right: 0.5rem;
65

76
&__content {
87
flex: 1 1 auto;

web/src/global/GlobalAlerts.scss

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,12 @@
44

55
&__alert {
66
width: 100%;
7-
padding-top: 0.75rem;
8-
padding-bottom: 0.75rem;
7+
padding: 0.5rem !important;
98
margin-bottom: 0 !important;
109

1110
border-radius: 0;
1211
border-width: 0 0 1px 0;
1312

14-
// stylelint-disable-next-line declaration-property-unit-whitelist
15-
font-size: 16px;
16-
1713
// The trailing after-paragraph/list margin looks unbalanced in MOTDs.
1814
p:last-child,
1915
ul:last-child,

web/src/global/GlobalAlerts.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { NeedsRepositoryConfigurationAlert } from '../site/NeedsRepositoryConfig
1212
import { NoRepositoriesEnabledAlert } from '../site/NoRepositoriesEnabledAlert'
1313
import { UpdateAvailableAlert } from '../site/UpdateAvailableAlert'
1414
import { GlobalAlert } from './GlobalAlert'
15+
import { Notices } from './Notices'
1516

1617
interface Props extends SettingsCascadeProps {
1718
isSiteAdmin: boolean
@@ -82,6 +83,11 @@ export class GlobalAlerts extends React.PureComponent<Props, State> {
8283
<Markdown dangerousInnerHTML={renderMarkdown(m)} />
8384
</DismissibleAlert>
8485
))}
86+
<Notices
87+
alertClassName="global-alerts__alert"
88+
location="top"
89+
settingsCascade={this.props.settingsCascade}
90+
/>
8591
</div>
8692
)
8793
}

web/src/global/Notices.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.notices {
2+
p:last-child {
3+
margin-bottom: 0; // override Bootstrap to fix uneven spacing at end of alert
4+
}
5+
}

web/src/global/Notices.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from 'react'
2+
import renderer from 'react-test-renderer'
3+
import { Notices } from './Notices'
4+
5+
describe('Notices', () => {
6+
test('shows notices for location', () =>
7+
expect(
8+
renderer
9+
.create(
10+
<Notices
11+
location="home"
12+
settingsCascade={{
13+
subjects: [],
14+
final: {
15+
notices: [
16+
{ message: 'a', location: 'home' },
17+
{ message: 'a', location: 'home', dismissible: true },
18+
{ message: 'b', location: 'top' },
19+
],
20+
},
21+
}}
22+
/>
23+
)
24+
.toJSON()
25+
).toMatchSnapshot())
26+
27+
test('no notices', () =>
28+
expect(
29+
renderer
30+
.create(<Notices location="home" settingsCascade={{ subjects: [], final: { notices: null } }} />)
31+
.toJSON()
32+
).toMatchSnapshot())
33+
})

web/src/global/Notices.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as React from 'react'
2+
import { Markdown } from '../../../shared/src/components/Markdown'
3+
import { isSettingsValid, SettingsCascadeProps } from '../../../shared/src/settings/settings'
4+
import { renderMarkdown } from '../../../shared/src/util/markdown'
5+
import { DismissibleAlert } from '../components/DismissibleAlert'
6+
import { Notice, Settings } from '../schema/settings.schema'
7+
8+
const Notice: React.FunctionComponent<{ notice: Notice; className?: string }> = ({ notice, className = '' }) => {
9+
const content = <Markdown dangerousInnerHTML={renderMarkdown(notice.message)} />
10+
return !!notice.dismissible ? (
11+
<DismissibleAlert className={`alert-info ${className}`} partialStorageKey={`notice.${notice.message}`}>
12+
{content}
13+
</DismissibleAlert>
14+
) : (
15+
<div className={`alert alert-info ${className}`}>{content}</div>
16+
)
17+
}
18+
19+
interface Props extends SettingsCascadeProps {
20+
className?: string
21+
22+
/** Apply this class name to each notice (alongside .alert). */
23+
alertClassName?: string
24+
25+
/** Display notices for this location. */
26+
location: Notice['location']
27+
}
28+
29+
/**
30+
* Displays notices from settings for a specific location.
31+
*/
32+
export const Notices: React.FunctionComponent<Props> = ({
33+
className = '',
34+
alertClassName,
35+
settingsCascade,
36+
location,
37+
}) => {
38+
if (
39+
!isSettingsValid<Settings>(settingsCascade) ||
40+
!settingsCascade.final.notices ||
41+
!Array.isArray(settingsCascade.final.notices)
42+
) {
43+
return null
44+
}
45+
const notices = settingsCascade.final.notices.filter(n => n.location === location)
46+
if (notices.length === 0) {
47+
return null
48+
}
49+
return (
50+
<div className={`notices ${className}`}>
51+
{notices.map((notice, i) => (
52+
<Notice key={i} className={alertClassName} notice={notice} />
53+
))}
54+
</div>
55+
)
56+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Notices no notices 1`] = `null`;
4+
5+
exports[`Notices shows notices for location 1`] = `
6+
<div
7+
className="notices "
8+
>
9+
<div
10+
className="alert alert-info "
11+
>
12+
<div
13+
className="markdown undefined"
14+
dangerouslySetInnerHTML={
15+
Object {
16+
"__html": "<p>a</p>
17+
",
18+
}
19+
}
20+
/>
21+
</div>
22+
<div
23+
className="alert dismissible-alert alert-info "
24+
>
25+
<div
26+
className="dismissible-alert__content"
27+
>
28+
<div
29+
className="markdown undefined"
30+
dangerouslySetInnerHTML={
31+
Object {
32+
"__html": "<p>a</p>
33+
",
34+
}
35+
}
36+
/>
37+
</div>
38+
<button
39+
className="btn btn-icon"
40+
onClick={[Function]}
41+
>
42+
<svg
43+
className="mdi-icon icon-inline"
44+
fill="currentColor"
45+
height={24}
46+
viewBox="0 0 24 24"
47+
width={24}
48+
>
49+
<path
50+
d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
51+
/>
52+
</svg>
53+
</button>
54+
</div>
55+
</div>
56+
`;

web/src/search/input/SearchPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as GQL from '../../../../shared/src/graphql/schema'
66
import { isSettingsValid, SettingsCascadeProps } from '../../../../shared/src/settings/settings'
77
import { Form } from '../../components/Form'
88
import { PageTitle } from '../../components/PageTitle'
9+
import { Notices } from '../../global/Notices'
910
import { Settings } from '../../schema/settings.schema'
1011
import { ThemePreferenceProps, ThemeProps } from '../../theme'
1112
import { eventLogger } from '../../tracking/eventLogger'
@@ -123,6 +124,7 @@ export class SearchPage extends React.Component<Props, State> {
123124
</div>
124125
</>
125126
)}
127+
<Notices className="my-3" location="home" settingsCascade={this.props.settingsCascade} />
126128
</Form>
127129
</div>
128130
)

0 commit comments

Comments
 (0)