footer.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  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. function 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 =
  44. organization.orgRole === 'owner' || organization.orgRole === 'manager';
  45. const hasOpenMembership = organization.features.includes('open-membership');
  46. const allSelected = selected && selected.has(ALL_ACCESS_PROJECTS);
  47. const canShowAllProjects = (hasGlobalRole || hasOpenMembership) && !allSelected;
  48. const onProjectClick = canShowAllProjects ? onShowAllProjects : onShowMyProjects;
  49. const buttonText = canShowAllProjects
  50. ? t('Select All Projects')
  51. : t('Select My Projects');
  52. const hasProjectWrite = organization.access.includes('project:write');
  53. const newProjectUrl = `/organizations/${organization.slug}/projects/new/`;
  54. return (
  55. <FooterContainer hasMessage={!!message}>
  56. {message && <FooterMessage>{message}</FooterMessage>}
  57. <FooterActions>
  58. <Button
  59. aria-label={t('Add Project')}
  60. disabled={!hasProjectWrite}
  61. to={newProjectUrl}
  62. size="xs"
  63. icon={<IconAdd size="xs" isCircled />}
  64. title={
  65. !hasProjectWrite ? t("You don't have permission to add a project") : undefined
  66. }
  67. >
  68. {t('Project')}
  69. </Button>
  70. {!disableMultipleProjectSelection && (
  71. <Feature
  72. features={['organizations:global-views']}
  73. organization={organization}
  74. hookName="feature-disabled:project-selector-all-projects"
  75. renderDisabled={false}
  76. >
  77. {({renderShowAllButton, hasFeature}: FeatureRenderProps) => {
  78. // if our hook is adding renderShowAllButton, render that
  79. if (renderShowAllButton) {
  80. return renderShowAllButton({
  81. onButtonClick: onProjectClick,
  82. canShowAllProjects,
  83. });
  84. }
  85. // if no hook, render null if feature is disabled
  86. if (!hasFeature) {
  87. return null;
  88. }
  89. // otherwise render the buton
  90. return (
  91. <Button priority="default" size="xs" onClick={onProjectClick}>
  92. {buttonText}
  93. </Button>
  94. );
  95. }}
  96. </Feature>
  97. )}
  98. {hasChanges && (
  99. <SubmitButton onClick={onApply} size="xs" priority="primary">
  100. {t('Apply Filter')}
  101. </SubmitButton>
  102. )}
  103. </FooterActions>
  104. </FooterContainer>
  105. );
  106. }
  107. export default ProjectSelectorFooter;
  108. const FooterContainer = styled('div')<{hasMessage: boolean}>`
  109. display: flex;
  110. justify-content: ${p => (p.hasMessage ? 'space-between' : 'flex-end')};
  111. `;
  112. const FooterActions = styled('div')`
  113. display: grid;
  114. grid-auto-flow: column dense;
  115. justify-items: end;
  116. padding: ${space(1)} 0;
  117. gap: ${space(1)};
  118. & > * {
  119. margin-left: ${space(0.5)};
  120. }
  121. &:empty {
  122. display: none;
  123. }
  124. `;
  125. const SubmitButton = styled(Button)`
  126. animation: 0.1s ${growIn} ease-in;
  127. `;
  128. const FooterMessage = styled('div')`
  129. font-size: ${p => p.theme.fontSizeSmall};
  130. padding: ${space(1)} ${space(0.5)};
  131. `;