Skip to content
Snippets Groups Projects
Commit de7141af authored by Alexander Lesnenko's avatar Alexander Lesnenko
Browse files

slightly split existing search components (#17581)

* slightly split existing search components

* address review comments

* review fixes
parent 4826248e
No related branches found
No related tags found
No related merge requests found
/* eslint-disable react/prop-types */
import React from "react";
import ReactDOM from "react-dom";
import { Flex } from "grid-styled";
import styled from "styled-components";
import { space } from "styled-system";
import { t } from "ttag";
import { color, lighten } from "metabase/lib/colors";
import Card from "metabase/components/Card";
import Icon from "metabase/components/Icon";
import OnClickOutsideWrapper from "metabase/components/OnClickOutsideWrapper";
import SearchResult from "metabase/search/components/SearchResult";
import { DefaultSearchColor } from "metabase/nav/constants";
import MetabaseSettings from "metabase/lib/settings";
const ActiveSearchColor = lighten(color("nav"), 0.1);
import Search from "metabase/entities/search";
const SearchWrapper = styled(Flex)`
position: relative;
background-color: ${props =>
props.active ? ActiveSearchColor : DefaultSearchColor};
border-radius: 6px;
flex: 1 1 auto;
max-width: 50em;
align-items: center;
color: white;
transition: background 300ms ease-in;
&:hover {
background-color: ${ActiveSearchColor};
}
`;
const SearchInput = styled.input`
${space};
background-color: transparent;
width: 100%;
border: none;
color: white;
font-size: 1em;
font-weight: 700;
&:focus {
outline: none;
}
&::placeholder {
color: ${color("text-white")};
}
`;
import { SearchInput, SearchWrapper } from "./SearchBar.styled";
import { SearchResults } from "./SearchResults";
const ALLOWED_SEARCH_FOCUS_ELEMENTS = new Set(["BODY", "A"]);
const SEARCH_LIMIT = 50;
export default class SearchBar extends React.Component {
state = {
......@@ -84,7 +44,7 @@ export default class SearchBar extends React.Component {
this.setState({ searchText: "" });
}
}
handleKeyUp = (e: KeyboardEvent) => {
handleKeyUp = e => {
const FORWARD_SLASH_KEY = 191;
if (
e.keyCode === FORWARD_SLASH_KEY &&
......@@ -95,25 +55,6 @@ export default class SearchBar extends React.Component {
}
};
renderResults(results) {
if (results.length === 0) {
return (
<li className="flex flex-column align-center justify-center p4 text-medium text-centered">
<div className="my3">
<Icon name="search" mb={1} size={24} />
<h3 className="text-light">{t`Didn't find anything`}</h3>
</div>
</li>
);
} else {
return results.map(l => (
<li key={`${l.model}:${l.id}`}>
<SearchResult result={l} compact={true} />
</li>
));
}
}
render() {
const { active, searchText } = this.state;
return (
......@@ -152,20 +93,7 @@ export default class SearchBar extends React.Component {
style={{ maxHeight: 400 }}
py={1}
>
<Search.ListLoader
query={{ q: searchText.trim(), limit: SEARCH_LIMIT }}
wrapped
reload
debounced
>
{({ list }) => {
return (
<ol data-testid="search-results-list">
{this.renderResults(list)}
</ol>
);
}}
</Search.ListLoader>
<SearchResults searchText={searchText.trim()} />
</Card>
) : null}
</div>
......
import styled from "styled-components";
import { Flex } from "grid-styled";
import { space } from "styled-system";
import { DefaultSearchColor } from "metabase/nav/constants";
import { color, lighten } from "metabase/lib/colors";
const ActiveSearchColor = lighten(color("nav"), 0.1);
export const SearchWrapper = styled(Flex)`
position: relative;
background-color: ${props =>
props.active ? ActiveSearchColor : DefaultSearchColor};
border-radius: 6px;
flex: 1 1 auto;
max-width: 50em;
align-items: center;
color: white;
transition: background 300ms ease-in;
&:hover {
background-color: ${ActiveSearchColor};
}
`;
export const SearchInput = styled.input`
${space};
background-color: transparent;
width: 100%;
border: none;
color: white;
font-size: 1em;
font-weight: 700;
&:focus {
outline: none;
}
&::placeholder {
color: ${color("text-white")};
}
`;
import React from "react";
import PropTypes from "prop-types";
import { Box } from "grid-styled";
import { t } from "ttag";
import Search from "metabase/entities/search";
import SearchResult from "metabase/search/components/SearchResult";
import EmptyState from "metabase/components/EmptyState";
const SEARCH_LIMIT = 50;
const propTypes = {
searchText: PropTypes.string,
};
export const SearchResults = ({ searchText }) => {
return (
<Search.ListLoader
query={{ q: searchText, limit: SEARCH_LIMIT }}
wrapped
reload
debounced
>
{({ list }) => {
const hasResults = list.length > 0;
return (
<ul data-testid="search-results-list">
{hasResults ? (
list.map(item => (
<li key={`${item.model}:${item.id}`}>
<SearchResult result={item} compact={true} />
</li>
))
) : (
<Box mt={4} mb={3}>
<EmptyState message={t`Didn't find anything`} icon="search" />
</Box>
)}
</ul>
);
}}
</Search.ListLoader>
);
};
SearchResults.propTypes = propTypes;
import React from "react";
import PropTypes from "prop-types";
import * as Urls from "metabase/lib/urls";
import {
CollectionBadgeRoot,
CollectionLink,
AuthorityLevelIcon,
} from "./CollectionBadge.styled";
const propTypes = {
collection: PropTypes.shape({
name: PropTypes.string,
}),
};
export function CollectionBadge({ collection }) {
return (
<CollectionBadgeRoot>
<CollectionLink to={Urls.collection(collection)}>
<AuthorityLevelIcon collection={collection} />
{collection.name}
</CollectionLink>
</CollectionBadgeRoot>
);
}
CollectionBadge.propTypes = propTypes;
import styled from "styled-components";
import { color } from "metabase/lib/colors";
import Link from "metabase/components/Link";
import { PLUGIN_COLLECTION_COMPONENTS } from "metabase/plugins";
const { CollectionAuthorityLevelIcon } = PLUGIN_COLLECTION_COMPONENTS;
export const CollectionBadgeRoot = styled.div`
display: inline-block;
`;
export const CollectionLink = styled(Link)`
display: flex;
align-items: center;
text-decoration: dashed;
&:hover {
color: ${color("brand")};
}
`;
export const AuthorityLevelIcon = styled(CollectionAuthorityLevelIcon).attrs({
size: 13,
})`
padding-right: 2px;
`;
import React from "react";
import { t, jt } from "ttag";
import * as Urls from "metabase/lib/urls";
import { capitalize } from "metabase/lib/formatting";
import Icon from "metabase/components/Icon";
import Link from "metabase/components/Link";
import Schema from "metabase/entities/schemas";
import Database from "metabase/entities/databases";
import Table from "metabase/entities/tables";
import { PLUGIN_COLLECTIONS } from "metabase/plugins";
import { CollectionBadge } from "./CollectionBadge";
function formatCollection(collection) {
return collection.id && <CollectionBadge collection={collection} />;
}
function getCollectionInfoText(collection) {
if (PLUGIN_COLLECTIONS.isRegularCollection(collection)) {
return t`Collection`;
}
const level = PLUGIN_COLLECTIONS.AUTHORITY_LEVEL[collection.authority_level];
return `${level.name} ${t`Collection`}`;
}
export function InfoText({ result }) {
const collection = result.getCollection();
switch (result.model) {
case "card":
return jt`Saved question in ${formatCollection(collection)}`;
case "collection":
return getCollectionInfoText(result.collection);
case "database":
return t`Database`;
case "table":
return (
<span>
{jt`Table in ${(
<span>
<Database.Link id={result.database_id} />{" "}
{result.table_schema && (
<Schema.ListLoader
query={{ dbId: result.database_id }}
loadingAndErrorWrapper={false}
>
{({ list }) =>
list && list.length > 1 ? (
<span>
<Icon name="chevronright" mx="4px" size={10} />
{/* we have to do some {} manipulation here to make this look like the table object that browseSchema was written for originally */}
<Link
to={Urls.browseSchema({
db: { id: result.database_id },
schema_name: result.table_schema,
})}
>
{result.table_schema}
</Link>
</span>
) : null
}
</Schema.ListLoader>
)}
</span>
)}`}
</span>
);
case "segment":
case "metric":
return (
<span>
{result.model === "segment" ? t`Segment of ` : t`Metric for `}
<Link to={Urls.tableRowsQuery(result.database_id, result.table_id)}>
<Table.Loader id={result.table_id} loadingAndErrorWrapper={false}>
{({ table }) =>
table ? <span>{table.display_name}</span> : null
}
</Table.Loader>
</Link>
</span>
);
default:
return jt`${capitalize(result.model)} in ${formatCollection(collection)}`;
}
}
/* eslint-disable react/prop-types */
import React from "react";
import { Box, Flex } from "grid-styled";
import styled from "styled-components";
import { t, jt } from "ttag";
import * as Urls from "metabase/lib/urls";
import { color, lighten } from "metabase/lib/colors";
import { capitalize } from "metabase/lib/formatting";
import { color } from "metabase/lib/colors";
import Icon from "metabase/components/Icon";
import Link from "metabase/components/Link";
import Text from "metabase/components/type/Text";
import {
PLUGIN_COLLECTIONS,
PLUGIN_COLLECTION_COMPONENTS,
PLUGIN_MODERATION,
} from "metabase/plugins";
import Schema from "metabase/entities/schemas";
import Database from "metabase/entities/databases";
import Table from "metabase/entities/tables";
const { CollectionAuthorityLevelIcon } = PLUGIN_COLLECTION_COMPONENTS;
import { PLUGIN_COLLECTIONS, PLUGIN_MODERATION } from "metabase/plugins";
function getColorForIconWrapper(props) {
if (props.item.collection_position) {
return color("saturated-yellow");
}
switch (props.type) {
case "collection":
return lighten("brand", 0.35);
default:
return color("brand");
}
}
const IconWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
color: ${getColorForIconWrapper};
margin-right: 10px;
flex-shrink: 0;
`;
const ResultLink = styled(Link)`
display: block;
background-color: transparent;
min-height: ${props => (props.compact ? "36px" : "54px")};
padding-top: 8px;
padding-bottom: 8px;
padding-left: 14px;
padding-right: ${props => (props.compact ? "20px" : "32px")};
&:hover {
background-color: ${lighten("brand", 0.63)};
h3 {
color: ${color("brand")};
}
}
${Link} {
text-underline-position: under;
text-decoration: underline ${color("text-light")};
text-decoration-style: dashed;
&:hover {
color: ${color("brand")};
text-decoration-color: ${color("brand")};
}
}
${Text} {
margin-top: 0;
margin-bottom: 0;
font-size: 13px;
line-height: 19px;
}
h3 {
font-size: ${props => (props.compact ? "14px" : "16px")};
line-height: 1.2em;
word-wrap: break-word;
margin-bottom: 0;
}
.Icon-info {
color: ${color("text-light")};
}
`;
import {
IconWrapper,
ResultLink,
Title,
TitleWrapper,
Description,
ContextText,
} from "./SearchResult.styled";
import { InfoText } from "./InfoText";
const TitleWrapper = styled.div`
display: flex;
grid-gap: 0.25rem;
align-items: center;
`;
const DEFAULT_ICON_SIZE = 20;
function TableIcon() {
......@@ -134,154 +55,35 @@ function ItemIcon({ item, type }) {
);
}
const CollectionBadgeRoot = styled.div`
display: inline-block;
`;
const CollectionLink = styled(Link)`
display: flex;
align-items: center;
text-decoration: dashed;
&:hover {
color: ${color("brand")};
}
`;
const AuthorityLevelIcon = styled(CollectionAuthorityLevelIcon).attrs({
size: 13,
})`
padding-right: 2px;
`;
function CollectionBadge({ collection }) {
return (
<CollectionBadgeRoot>
<CollectionLink to={Urls.collection(collection)}>
<AuthorityLevelIcon collection={collection} />
{collection.name}
</CollectionLink>
</CollectionBadgeRoot>
);
}
const Title = styled("h3")`
margin-bottom: 4px;
`;
function Score({ scores }) {
return (
<pre className="hide search-score">{JSON.stringify(scores, null, 2)}</pre>
);
}
const Context = styled("p")`
line-height: 1.4em;
color: ${color("text-medium")};
margin-top: 0;
`;
function formatContext(context, compact) {
return (
!compact &&
context && (
<Box ml="42px" mt="12px" style={{ maxWidth: 620 }}>
<Context>{contextText(context)}</Context>
</Box>
)
);
}
function formatCollection(collection) {
return collection.id && <CollectionBadge collection={collection} />;
}
const Description = styled(Text)`
padding-left: 8px;
margin-top: 6px !important;
border-left: 2px solid ${lighten("brand", 0.45)};
`;
function contextText(context) {
return context.map(function({ is_match, text }, i) {
if (is_match) {
return (
<strong key={i} style={{ color: color("brand") }}>
{" "}
{text}
</strong>
);
} else {
return <span key={i}> {text}</span>;
}
});
}
function getCollectionInfoText(collection) {
if (PLUGIN_COLLECTIONS.isRegularCollection(collection)) {
return t`Collection`;
function Context({ context }) {
if (!context) {
return null;
}
const level = PLUGIN_COLLECTIONS.AUTHORITY_LEVEL[collection.authority_level];
return `${level.name} ${t`Collection`}`;
}
function InfoText({ result }) {
const collection = result.getCollection();
switch (result.model) {
case "card":
return jt`Saved question in ${formatCollection(collection)}`;
case "collection":
return getCollectionInfoText(result.collection);
case "database":
return t`Database`;
case "table":
return (
<span>
{jt`Table in ${(
<span>
<Database.Link id={result.database_id} />{" "}
{result.table_schema && (
<Schema.ListLoader
query={{ dbId: result.database_id }}
loadingAndErrorWrapper={false}
>
{({ list }) =>
list && list.length > 1 ? (
<span>
<Icon name="chevronright" mx="4px" size={10} />
{/* we have to do some {} manipulation here to make this look like the table object that browseSchema was written for originally */}
<Link
to={Urls.browseSchema({
db: { id: result.database_id },
schema_name: result.table_schema,
})}
>
{result.table_schema}
</Link>
</span>
) : null
}
</Schema.ListLoader>
)}
</span>
)}`}
</span>
);
case "segment":
case "metric":
return (
<span>
{result.model === "segment" ? t`Segment of ` : t`Metric for `}
<Link to={Urls.tableRowsQuery(result.database_id, result.table_id)}>
<Table.Loader id={result.table_id} loadingAndErrorWrapper={false}>
{({ table }) =>
table ? <span>{table.display_name}</span> : null
}
</Table.Loader>
</Link>
</span>
);
default:
return jt`${capitalize(result.model)} in ${formatCollection(collection)}`;
}
return (
<Box ml="42px" mt="12px" style={{ maxWidth: 620 }}>
<ContextText>
{context.map(({ is_match, text }, i) => {
if (!is_match) {
return <span key={i}> {text}</span>;
}
return (
<strong key={i} style={{ color: color("brand") }}>
{" "}
{text}
</strong>
);
})}
</ContextText>
</Box>
);
}
export default function SearchResult({ result, compact }) {
......@@ -310,7 +112,7 @@ export default function SearchResult({ result, compact }) {
<Score scores={result.scores} />
</Box>
</Flex>
{formatContext(result.context, compact)}
{!compact && <Context context={result.context} />}
</ResultLink>
);
}
import styled from "styled-components";
import { color, lighten } from "metabase/lib/colors";
import Link from "metabase/components/Link";
import Text from "metabase/components/type/Text";
import { space } from "metabase/styled-components/theme";
function getColorForIconWrapper(props) {
if (props.item.collection_position) {
return color("saturated-yellow");
}
switch (props.type) {
case "collection":
return lighten("brand", 0.35);
default:
return color("brand");
}
}
export const IconWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
color: ${getColorForIconWrapper};
margin-right: 10px;
flex-shrink: 0;
`;
export const ResultLink = styled(Link)`
display: block;
background-color: transparent;
min-height: ${props => (props.compact ? "36px" : "54px")};
padding-top: ${space(1)};
padding-bottom: ${space(1)};
padding-left: 14px;
padding-right: ${props => (props.compact ? "20px" : space(3))};
&:hover {
background-color: ${lighten("brand", 0.63)};
h3 {
color: ${color("brand")};
}
}
${Link} {
text-underline-position: under;
text-decoration: underline ${color("text-light")};
text-decoration-style: dashed;
&:hover {
color: ${color("brand")};
text-decoration-color: ${color("brand")};
}
}
${Text} {
margin-top: 0;
margin-bottom: 0;
font-size: 13px;
line-height: 19px;
}
h3 {
font-size: ${props => (props.compact ? "14px" : "16px")};
line-height: 1.2em;
word-wrap: break-word;
margin-bottom: 0;
}
.Icon-info {
color: ${color("text-light")};
}
`;
export const TitleWrapper = styled.div`
display: flex;
grid-gap: 0.25rem;
align-items: center;
`;
export const ContextText = styled("p")`
line-height: 1.4em;
color: ${color("text-medium")};
margin-top: 0;
`;
export const Title = styled("h3")`
margin-bottom: 4px;
`;
export const Description = styled(Text)`
padding-left: ${space(1)};
margin-top: ${space(1)} !important;
border-left: 2px solid ${lighten("brand", 0.45)};
`;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment