footer.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import styled from '@emotion/styled';
  2. import Feature from 'sentry/components/acl/feature';
  3. import Button from 'sentry/components/button';
  4. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  5. import {IconAdd} from 'sentry/icons';
  6. import {t} from 'sentry/locale';
  7. import {growIn} from 'sentry/styles/animations';
  8. import space from 'sentry/styles/space';
  9. import {Organization} from 'sentry/types';
  10. type ShowAllButtonProps = {
  11. canShowAllProjects: boolean;
  12. onButtonClick: () => void;
  13. };
  14. type FeatureRenderProps = {
  15. hasFeature: boolean;
  16. renderShowAllButton?: (p: ShowAllButtonProps) => React.ReactNode;
  17. };
  18. type Props = {
  19. onApply: () => void;
  20. onShowAllProjects: () => void;
  21. onShowMyProjects: () => void;
  22. organization: Organization;
  23. disableMultipleProjectSelection?: boolean;
  24. hasChanges?: boolean;
  25. message?: React.ReactNode;
  26. selected?: Set<number>;
  27. };
  28. const ProjectSelectorFooter = ({
  29. selected,
  30. disableMultipleProjectSelection,
  31. hasChanges,
  32. onApply,
  33. onShowAllProjects,
  34. onShowMyProjects,
  35. organization,
  36. message,
  37. }: Props) => {
  38. // Nothing to show.
  39. if (disableMultipleProjectSelection && !hasChanges && !message) {
  40. return null;
  41. }
  42. // see if we should show "All Projects" or "My Projects" if disableMultipleProjectSelection isn't true
  43. const hasGlobalRole = organization.role === 'owner' || organization.role === 'manager';
  44. const hasOpenMembership = organization.features.includes('open-membership');
  45. const allSelected = selected && selected.has(ALL_ACCESS_PROJECTS);
  46. const canShowAllProjects = (hasGlobalRole || hasOpenMembership) && !allSelected;
  47. const onProjectClick = canShowAllProjects ? onShowAllProjects : onShowMyProjects;
  48. const buttonText = canShowAllProjects
  49. ? t('Select All Projects')
  50. : t('Select My Projects');
  51. const hasProjectWrite = organization.access.includes('project:write');
  52. const newProjectUrl = `/organizations/${organization.slug}/projects/new/`;
  53. return (
  54. <FooterContainer hasMessage={!!message}>
  55. {message && <FooterMessage>{message}</FooterMessage>}
  56. <FooterActions>
  57. <Button
  58. aria-label={t('Add Project')}
  59. disabled={!hasProjectWrite}
  60. to={newProjectUrl}
  61. size="xs"
  62. icon={<IconAdd size="xs" isCircled />}
  63. title={
  64. !hasProjectWrite ? t("You don't have permission to add a project") : undefined
  65. }
  66. >
  67. {t('Project')}
  68. </Button>
  69. {!disableMultipleProjectSelection && (
  70. <Feature
  71. features={['organizations:global-views']}
  72. organization={organization}
  73. hookName="feature-disabled:project-selector-all-projects"
  74. renderDisabled={false}
  75. >
  76. {({renderShowAllButton, hasFeature}: FeatureRenderProps) => {
  77. // if our hook is adding renderShowAllButton, render that
  78. if (renderShowAllButton) {
  79. return renderShowAllButton({
  80. onButtonClick: onProjectClick,
  81. canShowAllProjects,
  82. });
  83. }
  84. // if no hook, render null if feature is disabled
  85. if (!hasFeature) {
  86. return null;
  87. }
  88. // otherwise render the buton
  89. return (
  90. <Button priority="default" size="xs" onClick={onProjectClick}>
  91. {buttonText}
  92. </Button>
  93. );
  94. }}
  95. </Feature>
  96. )}
  97. {hasChanges && (
  98. <SubmitButton onClick={onApply} size="xs" priority="primary">
  99. {t('Apply Filter')}
  100. </SubmitButton>
  101. )}
  102. </FooterActions>
  103. </FooterContainer>
  104. );
  105. };
  106. export default ProjectSelectorFooter;
  107. const FooterContainer = styled('div')<{hasMessage: boolean}>`
  108. display: flex;
  109. justify-content: ${p => (p.hasMessage ? 'space-between' : 'flex-end')};
  110. `;
  111. const FooterActions = styled('div')`
  112. display: grid;
  113. grid-auto-flow: column dense;
  114. justify-items: end;
  115. padding: ${space(1)} 0;
  116. gap: ${space(1)};
  117. & > * {
  118. margin-left: ${space(0.5)};
  119. }
  120. &:empty {
  121. display: none;
  122. }
  123. `;
  124. const SubmitButton = styled(Button)`
  125. animation: 0.1s ${growIn} ease-in;
  126. `;
  127. const FooterMessage = styled('div')`
  128. font-size: ${p => p.theme.fontSizeSmall};
  129. padding: ${space(1)} ${space(0.5)};
  130. `;